Merge branch 'main' of https://github.com/immich-app/immich into feat/offline-files-job
2
.github/workflows/build-mobile.yml
vendored
@ -45,7 +45,7 @@ jobs:
|
|||||||
uses: subosito/flutter-action@v2
|
uses: subosito/flutter-action@v2
|
||||||
with:
|
with:
|
||||||
channel: "stable"
|
channel: "stable"
|
||||||
flutter-version: "3.16.9"
|
flutter-version: "3.19.3"
|
||||||
cache: true
|
cache: true
|
||||||
|
|
||||||
- name: Create the Keystore
|
- name: Create the Keystore
|
||||||
|
2
.github/workflows/static_analysis.yml
vendored
@ -23,7 +23,7 @@ jobs:
|
|||||||
uses: subosito/flutter-action@v2
|
uses: subosito/flutter-action@v2
|
||||||
with:
|
with:
|
||||||
channel: "stable"
|
channel: "stable"
|
||||||
flutter-version: "3.16.9"
|
flutter-version: "3.19.3"
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: dart pub get
|
run: dart pub get
|
||||||
|
2
.github/workflows/test.yml
vendored
@ -354,7 +354,7 @@ jobs:
|
|||||||
id: verify-changed-sql-files
|
id: verify-changed-sql-files
|
||||||
with:
|
with:
|
||||||
files: |
|
files: |
|
||||||
server/src/infra/sql
|
server/src/queries
|
||||||
|
|
||||||
- name: Verify SQL files have not changed
|
- name: Verify SQL files have not changed
|
||||||
if: steps.verify-changed-sql-files.outputs.files_changed == 'true'
|
if: steps.verify-changed-sql-files.outputs.files_changed == 'true'
|
||||||
|
4
.vscode/settings.json
vendored
@ -27,4 +27,8 @@
|
|||||||
"cSpell.words": [
|
"cSpell.words": [
|
||||||
"immich"
|
"immich"
|
||||||
],
|
],
|
||||||
|
"explorer.fileNesting.enabled": true,
|
||||||
|
"explorer.fileNesting.patterns": {
|
||||||
|
"*.ts": "${capture}.spec.ts,${capture}.mock.ts"
|
||||||
|
}
|
||||||
}
|
}
|
22
README.md
@ -18,17 +18,17 @@
|
|||||||
</a>
|
</a>
|
||||||
<br/>
|
<br/>
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="README_ca_ES.md">Català</a>
|
<a href="readme_i18n/README_ca_ES.md">Català</a>
|
||||||
<a href="README_es_ES.md">Español</a>
|
<a href="readme_i18n/README_es_ES.md">Español</a>
|
||||||
<a href="README_fr_FR.md">Français</a>
|
<a href="readme_i18n/README_fr_FR.md">Français</a>
|
||||||
<a href="README_it_IT.md">Italiano</a>
|
<a href="readme_i18n/README_it_IT.md">Italiano</a>
|
||||||
<a href="README_ja_JP.md">日本語</a>
|
<a href="readme_i18n/README_ja_JP.md">日本語</a>
|
||||||
<a href="README_ko_KR.md">한국어</a>
|
<a href="readme_i18n/README_ko_KR.md">한국어</a>
|
||||||
<a href="README_de_DE.md">Deutsch</a>
|
<a href="readme_i18n/README_de_DE.md">Deutsch</a>
|
||||||
<a href="README_nl_NL.md">Nederlands</a>
|
<a href="readme_i18n/README_nl_NL.md">Nederlands</a>
|
||||||
<a href="README_tr_TR.md">Türkçe</a>
|
<a href="readme_i18n/README_tr_TR.md">Türkçe</a>
|
||||||
<a href="README_zh_CN.md">中文</a>
|
<a href="readme_i18n/README_zh_CN.md">中文</a>
|
||||||
<a href="README_ru_RU.md">Русский</a>
|
<a href="readme_i18n/README_ru_RU.md">Русский</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
## Disclaimer
|
## Disclaimer
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
|
Action,
|
||||||
AssetBulkUploadCheckResult,
|
AssetBulkUploadCheckResult,
|
||||||
|
AssetFileUploadResponseDto,
|
||||||
addAssetsToAlbum,
|
addAssetsToAlbum,
|
||||||
checkBulkUpload,
|
checkBulkUpload,
|
||||||
createAlbum,
|
createAlbum,
|
||||||
@ -8,162 +10,213 @@ import {
|
|||||||
getSupportedMediaTypes,
|
getSupportedMediaTypes,
|
||||||
} from '@immich/sdk';
|
} from '@immich/sdk';
|
||||||
import byteSize from 'byte-size';
|
import byteSize from 'byte-size';
|
||||||
import cliProgress from 'cli-progress';
|
import { Presets, SingleBar } from 'cli-progress';
|
||||||
import { chunk, zip } from 'lodash-es';
|
import { chunk } from 'lodash-es';
|
||||||
import { createHash } from 'node:crypto';
|
import { Stats, createReadStream } from 'node:fs';
|
||||||
import fs, { createReadStream } from 'node:fs';
|
import { stat, unlink } from 'node:fs/promises';
|
||||||
import { access, constants, stat, unlink } from 'node:fs/promises';
|
|
||||||
import os from 'node:os';
|
import os from 'node:os';
|
||||||
import { basename } from 'node:path';
|
import path, { basename } from 'node:path';
|
||||||
import { CrawlService } from 'src/services/crawl.service';
|
import { BaseOptions, authenticate, crawl, sha1 } from 'src/utils';
|
||||||
import { BaseOptions, authenticate } from 'src/utils';
|
|
||||||
|
|
||||||
const zipDefined = zip as <T, U>(a: T[], b: U[]) => [T, U][];
|
const s = (count: number) => (count === 1 ? '' : 's');
|
||||||
|
|
||||||
enum CheckResponseStatus {
|
// TODO figure out why `id` is missing
|
||||||
ACCEPT = 'accept',
|
type AssetBulkUploadCheckResults = Array<AssetBulkUploadCheckResult & { id: string }>;
|
||||||
REJECT = 'reject',
|
type Asset = { id: string; filepath: string };
|
||||||
DUPLICATE = 'duplicate',
|
|
||||||
|
interface UploadOptionsDto {
|
||||||
|
recursive?: boolean;
|
||||||
|
exclusionPatterns?: string[];
|
||||||
|
dryRun?: boolean;
|
||||||
|
skipHash?: boolean;
|
||||||
|
delete?: boolean;
|
||||||
|
album?: boolean;
|
||||||
|
albumName?: string;
|
||||||
|
includeHidden?: boolean;
|
||||||
|
concurrency: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
class Asset {
|
class UploadFile extends File {
|
||||||
readonly path: string;
|
constructor(
|
||||||
|
private filepath: string,
|
||||||
id?: string;
|
private _size: number,
|
||||||
deviceAssetId?: string;
|
) {
|
||||||
fileCreatedAt?: Date;
|
super([], basename(filepath));
|
||||||
fileModifiedAt?: Date;
|
|
||||||
sidecarPath?: string;
|
|
||||||
fileSize?: number;
|
|
||||||
albumName?: string;
|
|
||||||
|
|
||||||
constructor(path: string) {
|
|
||||||
this.path = path;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async prepare() {
|
get size() {
|
||||||
const stats = await stat(this.path);
|
return this._size;
|
||||||
this.deviceAssetId = `${basename(this.path)}-${stats.size}`.replaceAll(/\s+/g, '');
|
|
||||||
this.fileCreatedAt = stats.mtime;
|
|
||||||
this.fileModifiedAt = stats.mtime;
|
|
||||||
this.fileSize = stats.size;
|
|
||||||
this.albumName = this.extractAlbumName();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getUploadFormData(): Promise<FormData> {
|
stream() {
|
||||||
if (!this.deviceAssetId) {
|
return createReadStream(this.filepath) as any;
|
||||||
throw new Error('Device asset id not set');
|
|
||||||
}
|
}
|
||||||
if (!this.fileCreatedAt) {
|
}
|
||||||
throw new Error('File created at not set');
|
|
||||||
}
|
export const upload = async (paths: string[], baseOptions: BaseOptions, options: UploadOptionsDto) => {
|
||||||
if (!this.fileModifiedAt) {
|
await authenticate(baseOptions);
|
||||||
throw new Error('File modified at not set');
|
|
||||||
|
const files = await scan(paths, options);
|
||||||
|
if (files.length === 0) {
|
||||||
|
console.log('No files found, exiting');
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: doesn't xmp replace the file extension? Will need investigation
|
const { newFiles, duplicates } = await checkForDuplicates(files, options);
|
||||||
const sideCarPath = `${this.path}.xmp`;
|
|
||||||
let sidecarData: Blob | undefined = undefined;
|
const newAssets = await uploadFiles(newFiles, options);
|
||||||
|
await updateAlbums([...newAssets, ...duplicates], options);
|
||||||
|
await deleteFiles(newFiles, options);
|
||||||
|
};
|
||||||
|
|
||||||
|
const scan = async (pathsToCrawl: string[], options: UploadOptionsDto) => {
|
||||||
|
const { image, video } = await getSupportedMediaTypes();
|
||||||
|
|
||||||
|
console.log('Crawling for assets...');
|
||||||
|
const files = await crawl({
|
||||||
|
pathsToCrawl,
|
||||||
|
recursive: options.recursive,
|
||||||
|
exclusionPatterns: options.exclusionPatterns,
|
||||||
|
includeHidden: options.includeHidden,
|
||||||
|
extensions: [...image, ...video],
|
||||||
|
});
|
||||||
|
|
||||||
|
return files;
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkForDuplicates = async (files: string[], { concurrency }: UploadOptionsDto) => {
|
||||||
|
const progressBar = new SingleBar(
|
||||||
|
{ format: 'Checking files | {bar} | {percentage}% | ETA: {eta}s | {value}/{total} assets' },
|
||||||
|
Presets.shades_classic,
|
||||||
|
);
|
||||||
|
|
||||||
|
progressBar.start(files.length, 0);
|
||||||
|
|
||||||
|
const newFiles: string[] = [];
|
||||||
|
const duplicates: Asset[] = [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await access(sideCarPath, constants.R_OK);
|
// TODO refactor into a queue
|
||||||
sidecarData = new File([await fs.openAsBlob(sideCarPath)], basename(sideCarPath));
|
for (const items of chunk(files, concurrency)) {
|
||||||
} catch {}
|
const dto = await Promise.all(items.map(async (filepath) => ({ id: filepath, checksum: await sha1(filepath) })));
|
||||||
|
const { results } = await checkBulkUpload({ assetBulkUploadCheckDto: { assets: dto } });
|
||||||
|
|
||||||
const data: any = {
|
for (const { id: filepath, assetId, action } of results as AssetBulkUploadCheckResults) {
|
||||||
assetData: new File([await fs.openAsBlob(this.path)], basename(this.path)),
|
if (action === Action.Accept) {
|
||||||
deviceAssetId: this.deviceAssetId,
|
newFiles.push(filepath);
|
||||||
deviceId: 'CLI',
|
} else {
|
||||||
fileCreatedAt: this.fileCreatedAt.toISOString(),
|
// rejects are always duplicates
|
||||||
fileModifiedAt: this.fileModifiedAt.toISOString(),
|
duplicates.push({ id: assetId as string, filepath });
|
||||||
isFavorite: String(false),
|
|
||||||
};
|
|
||||||
const formData = new FormData();
|
|
||||||
|
|
||||||
for (const property in data) {
|
|
||||||
formData.append(property, data[property]);
|
|
||||||
}
|
}
|
||||||
|
progressBar.increment();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
progressBar.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Found ${newFiles.length} new files and ${duplicates.length} duplicate${s(duplicates.length)}`);
|
||||||
|
|
||||||
|
return { newFiles, duplicates };
|
||||||
|
};
|
||||||
|
|
||||||
|
const uploadFiles = async (files: string[], { dryRun, concurrency }: UploadOptionsDto): Promise<Asset[]> => {
|
||||||
|
if (files.length === 0) {
|
||||||
|
console.log('All assets were already uploaded, nothing to do.');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute total size first
|
||||||
|
let totalSize = 0;
|
||||||
|
const statsMap = new Map<string, Stats>();
|
||||||
|
for (const filepath of files) {
|
||||||
|
const stats = await stat(filepath);
|
||||||
|
statsMap.set(filepath, stats);
|
||||||
|
totalSize += stats.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dryRun) {
|
||||||
|
console.log(`Would have uploaded ${files.length} asset${s(files.length)} (${byteSize(totalSize)})`);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const uploadProgress = new SingleBar(
|
||||||
|
{ format: 'Uploading assets | {bar} | {percentage}% | ETA: {eta_formatted} | {value_formatted}/{total_formatted}' },
|
||||||
|
Presets.shades_classic,
|
||||||
|
);
|
||||||
|
uploadProgress.start(totalSize, 0);
|
||||||
|
uploadProgress.update({ value_formatted: 0, total_formatted: byteSize(totalSize) });
|
||||||
|
|
||||||
|
let totalSizeUploaded = 0;
|
||||||
|
const newAssets: Asset[] = [];
|
||||||
|
try {
|
||||||
|
for (const items of chunk(files, concurrency)) {
|
||||||
|
await Promise.all(
|
||||||
|
items.map(async (filepath) => {
|
||||||
|
const stats = statsMap.get(filepath) as Stats;
|
||||||
|
const response = await uploadFile(filepath, stats);
|
||||||
|
totalSizeUploaded += stats.size ?? 0;
|
||||||
|
uploadProgress.update(totalSizeUploaded, { value_formatted: byteSize(totalSizeUploaded) });
|
||||||
|
newAssets.push({ id: response.id, filepath });
|
||||||
|
return response;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
uploadProgress.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Successfully uploaded ${newAssets.length} asset${s(newAssets.length)} (${byteSize(totalSizeUploaded)})`);
|
||||||
|
return newAssets;
|
||||||
|
};
|
||||||
|
|
||||||
|
const uploadFile = async (input: string, stats: Stats): Promise<AssetFileUploadResponseDto> => {
|
||||||
|
const { baseUrl, headers } = defaults;
|
||||||
|
|
||||||
|
const assetPath = path.parse(input);
|
||||||
|
const noExtension = path.join(assetPath.dir, assetPath.name);
|
||||||
|
|
||||||
|
const sidecarsFiles = await Promise.all(
|
||||||
|
// XMP sidecars can come in two filename formats. For a photo named photo.ext, the filenames are photo.ext.xmp and photo.xmp
|
||||||
|
[`${noExtension}.xmp`, `${input}.xmp`].map(async (sidecarPath) => {
|
||||||
|
try {
|
||||||
|
const stats = await stat(sidecarPath);
|
||||||
|
return new UploadFile(sidecarPath, stats.size);
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const sidecarData = sidecarsFiles.find((file): file is UploadFile => file !== false);
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('deviceAssetId', `${basename(input)}-${stats.size}`.replaceAll(/\s+/g, ''));
|
||||||
|
formData.append('deviceId', 'CLI');
|
||||||
|
formData.append('fileCreatedAt', stats.mtime.toISOString());
|
||||||
|
formData.append('fileModifiedAt', stats.mtime.toISOString());
|
||||||
|
formData.append('fileSize', String(stats.size));
|
||||||
|
formData.append('isFavorite', 'false');
|
||||||
|
formData.append('assetData', new UploadFile(input, stats.size));
|
||||||
|
|
||||||
if (sidecarData) {
|
if (sidecarData) {
|
||||||
formData.append('sidecarData', sidecarData);
|
formData.append('sidecarData', sidecarData);
|
||||||
}
|
}
|
||||||
|
|
||||||
return formData;
|
const response = await fetch(`${baseUrl}/asset/upload`, {
|
||||||
}
|
method: 'post',
|
||||||
|
redirect: 'error',
|
||||||
async delete(): Promise<void> {
|
headers: headers as Record<string, string>,
|
||||||
return unlink(this.path);
|
body: formData,
|
||||||
}
|
|
||||||
|
|
||||||
public async hash(): Promise<string> {
|
|
||||||
const sha1 = (filePath: string) => {
|
|
||||||
const hash = createHash('sha1');
|
|
||||||
return new Promise<string>((resolve, reject) => {
|
|
||||||
const rs = createReadStream(filePath);
|
|
||||||
rs.on('error', reject);
|
|
||||||
rs.on('data', (chunk) => hash.update(chunk));
|
|
||||||
rs.on('end', () => resolve(hash.digest('hex')));
|
|
||||||
});
|
});
|
||||||
};
|
if (response.status !== 200 && response.status !== 201) {
|
||||||
|
throw new Error(await response.text());
|
||||||
return await sha1(this.path);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private extractAlbumName(): string | undefined {
|
return response.json();
|
||||||
return os.platform() === 'win32' ? this.path.split('\\').at(-2) : this.path.split('/').at(-2);
|
};
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class UploadOptionsDto {
|
|
||||||
recursive? = false;
|
|
||||||
exclusionPatterns?: string[] = [];
|
|
||||||
dryRun? = false;
|
|
||||||
skipHash? = false;
|
|
||||||
delete? = false;
|
|
||||||
album? = false;
|
|
||||||
albumName? = '';
|
|
||||||
includeHidden? = false;
|
|
||||||
concurrency? = 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const upload = (paths: string[], baseOptions: BaseOptions, uploadOptions: UploadOptionsDto) =>
|
|
||||||
new UploadCommand().run(paths, baseOptions, uploadOptions);
|
|
||||||
|
|
||||||
// TODO refactor this
|
|
||||||
class UploadCommand {
|
|
||||||
public async run(paths: string[], baseOptions: BaseOptions, options: UploadOptionsDto): Promise<void> {
|
|
||||||
await authenticate(baseOptions);
|
|
||||||
|
|
||||||
console.log('Crawling for assets...');
|
|
||||||
const files = await this.getFiles(paths, options);
|
|
||||||
|
|
||||||
if (files.length === 0) {
|
|
||||||
console.log('No assets found, exiting');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const assetsToCheck = files.map((path) => new Asset(path));
|
|
||||||
|
|
||||||
const { newAssets, duplicateAssets } = await this.checkAssets(assetsToCheck, options.concurrency ?? 4);
|
|
||||||
|
|
||||||
const totalSizeUploaded = await this.upload(newAssets, options);
|
|
||||||
const messageStart = options.dryRun ? 'Would have' : 'Successfully';
|
|
||||||
if (newAssets.length === 0) {
|
|
||||||
console.log('All assets were already uploaded, nothing to do.');
|
|
||||||
} else {
|
|
||||||
console.log(
|
|
||||||
`${messageStart} uploaded ${newAssets.length} asset${newAssets.length === 1 ? '' : 's'} (${byteSize(totalSizeUploaded)})`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.album || options.albumName) {
|
|
||||||
const { createdAlbumCount, updatedAssetCount } = await this.updateAlbums(
|
|
||||||
[...newAssets, ...duplicateAssets],
|
|
||||||
options,
|
|
||||||
);
|
|
||||||
console.log(`${messageStart} created ${createdAlbumCount} new album${createdAlbumCount === 1 ? '' : 's'}`);
|
|
||||||
console.log(`${messageStart} updated ${updatedAssetCount} asset${updatedAssetCount === 1 ? '' : 's'}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
const deleteFiles = async (files: string[], options: UploadOptionsDto): Promise<void> => {
|
||||||
if (!options.delete) {
|
if (!options.delete) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -175,169 +228,75 @@ class UploadCommand {
|
|||||||
|
|
||||||
console.log('Deleting assets that have been uploaded...');
|
console.log('Deleting assets that have been uploaded...');
|
||||||
|
|
||||||
await this.deleteAssets(newAssets, options);
|
const deletionProgress = new SingleBar(
|
||||||
}
|
{ format: 'Deleting local assets | {bar} | {percentage}% | ETA: {eta}s | {value}/{total} assets' },
|
||||||
|
Presets.shades_classic,
|
||||||
public async checkAssets(
|
|
||||||
assetsToCheck: Asset[],
|
|
||||||
concurrency: number,
|
|
||||||
): Promise<{ newAssets: Asset[]; duplicateAssets: Asset[]; rejectedAssets: Asset[] }> {
|
|
||||||
for (const assets of chunk(assetsToCheck, concurrency)) {
|
|
||||||
await Promise.all(assets.map((asset: Asset) => asset.prepare()));
|
|
||||||
}
|
|
||||||
|
|
||||||
const checkProgress = new cliProgress.SingleBar(
|
|
||||||
{ format: 'Checking assets | {bar} | {percentage}% | ETA: {eta}s | {value}/{total} assets' },
|
|
||||||
cliProgress.Presets.shades_classic,
|
|
||||||
);
|
);
|
||||||
checkProgress.start(assetsToCheck.length, 0);
|
deletionProgress.start(files.length, 0);
|
||||||
|
|
||||||
const newAssets = [];
|
|
||||||
const duplicateAssets = [];
|
|
||||||
const rejectedAssets = [];
|
|
||||||
try {
|
|
||||||
for (const assets of chunk(assetsToCheck, concurrency)) {
|
|
||||||
const checkedAssets = await this.getStatus(assets);
|
|
||||||
for (const checked of checkedAssets) {
|
|
||||||
if (checked.status === CheckResponseStatus.ACCEPT) {
|
|
||||||
newAssets.push(checked.asset);
|
|
||||||
} else if (checked.status === CheckResponseStatus.DUPLICATE) {
|
|
||||||
duplicateAssets.push(checked.asset);
|
|
||||||
} else {
|
|
||||||
rejectedAssets.push(checked.asset);
|
|
||||||
}
|
|
||||||
checkProgress.increment();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
checkProgress.stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
return { newAssets, duplicateAssets, rejectedAssets };
|
|
||||||
}
|
|
||||||
|
|
||||||
public async upload(assetsToUpload: Asset[], options: UploadOptionsDto): Promise<number> {
|
|
||||||
let totalSize = 0;
|
|
||||||
|
|
||||||
// Compute total size first
|
|
||||||
for (const asset of assetsToUpload) {
|
|
||||||
totalSize += asset.fileSize ?? 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.dryRun) {
|
|
||||||
return totalSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
const uploadProgress = new cliProgress.SingleBar(
|
|
||||||
{
|
|
||||||
format: 'Uploading assets | {bar} | {percentage}% | ETA: {eta_formatted} | {value_formatted}/{total_formatted}',
|
|
||||||
},
|
|
||||||
cliProgress.Presets.shades_classic,
|
|
||||||
);
|
|
||||||
uploadProgress.start(totalSize, 0);
|
|
||||||
uploadProgress.update({ value_formatted: 0, total_formatted: byteSize(totalSize) });
|
|
||||||
|
|
||||||
let totalSizeUploaded = 0;
|
|
||||||
try {
|
|
||||||
for (const assets of chunk(assetsToUpload, options.concurrency)) {
|
|
||||||
const ids = await this.uploadAssets(assets);
|
|
||||||
for (const [asset, id] of zipDefined(assets, ids)) {
|
|
||||||
asset.id = id;
|
|
||||||
if (asset.fileSize) {
|
|
||||||
totalSizeUploaded += asset.fileSize ?? 0;
|
|
||||||
} else {
|
|
||||||
console.log(`Could not determine file size for ${asset.path}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
uploadProgress.update(totalSizeUploaded, { value_formatted: byteSize(totalSizeUploaded) });
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
uploadProgress.stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
return totalSizeUploaded;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getFiles(paths: string[], options: UploadOptionsDto): Promise<string[]> {
|
|
||||||
const inputFiles: string[] = [];
|
|
||||||
for (const pathArgument of paths) {
|
|
||||||
const fileStat = await fs.promises.lstat(pathArgument);
|
|
||||||
if (fileStat.isFile()) {
|
|
||||||
inputFiles.push(pathArgument);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const files: string[] = await this.crawl(paths, options);
|
|
||||||
files.push(...inputFiles);
|
|
||||||
return files;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getAlbums(): Promise<Map<string, string>> {
|
|
||||||
const existingAlbums = await getAllAlbums({});
|
|
||||||
|
|
||||||
const albumMapping = new Map<string, string>();
|
|
||||||
for (const album of existingAlbums) {
|
|
||||||
albumMapping.set(album.albumName, album.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
return albumMapping;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async updateAlbums(
|
|
||||||
assets: Asset[],
|
|
||||||
options: UploadOptionsDto,
|
|
||||||
): Promise<{ createdAlbumCount: number; updatedAssetCount: number }> {
|
|
||||||
if (options.albumName) {
|
|
||||||
for (const asset of assets) {
|
|
||||||
asset.albumName = options.albumName;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const existingAlbums = await this.getAlbums();
|
|
||||||
const assetsToUpdate = assets.filter(
|
|
||||||
(asset): asset is Asset & { albumName: string; id: string } => !!(asset.albumName && asset.id),
|
|
||||||
);
|
|
||||||
|
|
||||||
const newAlbumsSet: Set<string> = new Set();
|
|
||||||
for (const asset of assetsToUpdate) {
|
|
||||||
if (!existingAlbums.has(asset.albumName)) {
|
|
||||||
newAlbumsSet.add(asset.albumName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const newAlbums = [...newAlbumsSet];
|
|
||||||
|
|
||||||
if (options.dryRun) {
|
|
||||||
return { createdAlbumCount: newAlbums.length, updatedAssetCount: assetsToUpdate.length };
|
|
||||||
}
|
|
||||||
|
|
||||||
const albumCreationProgress = new cliProgress.SingleBar(
|
|
||||||
{
|
|
||||||
format: 'Creating albums | {bar} | {percentage}% | ETA: {eta}s | {value}/{total} albums',
|
|
||||||
},
|
|
||||||
cliProgress.Presets.shades_classic,
|
|
||||||
);
|
|
||||||
albumCreationProgress.start(newAlbums.length, 0);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
for (const albumNames of chunk(newAlbums, options.concurrency)) {
|
for (const assetBatch of chunk(files, options.concurrency)) {
|
||||||
const newAlbumIds = await Promise.all(
|
await Promise.all(assetBatch.map((input: string) => unlink(input)));
|
||||||
albumNames.map((albumName: string) => createAlbum({ createAlbumDto: { albumName } }).then((r) => r.id)),
|
deletionProgress.update(assetBatch.length);
|
||||||
);
|
|
||||||
|
|
||||||
for (const [albumName, albumId] of zipDefined(albumNames, newAlbumIds)) {
|
|
||||||
existingAlbums.set(albumName, albumId);
|
|
||||||
}
|
|
||||||
|
|
||||||
albumCreationProgress.increment(albumNames.length);
|
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
albumCreationProgress.stop();
|
deletionProgress.stop();
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateAlbums = async (assets: Asset[], options: UploadOptionsDto) => {
|
||||||
|
if (!options.album && !options.albumName) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { dryRun, concurrency } = options;
|
||||||
|
|
||||||
|
const albums = await getAllAlbums({});
|
||||||
|
const existingAlbums = new Map(albums.map((album) => [album.albumName, album.id]));
|
||||||
|
const newAlbums: Set<string> = new Set();
|
||||||
|
for (const { filepath } of assets) {
|
||||||
|
const albumName = getAlbumName(filepath, options);
|
||||||
|
if (albumName && !existingAlbums.has(albumName)) {
|
||||||
|
newAlbums.add(albumName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dryRun) {
|
||||||
|
// TODO print asset counts for new albums
|
||||||
|
console.log(`Would have created ${newAlbums.size} new album${s(newAlbums.size)}`);
|
||||||
|
console.log(`Would have updated ${assets.length} asset${s(assets.length)}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const progressBar = new SingleBar(
|
||||||
|
{ format: 'Creating albums | {bar} | {percentage}% | ETA: {eta}s | {value}/{total} albums' },
|
||||||
|
Presets.shades_classic,
|
||||||
|
);
|
||||||
|
progressBar.start(newAlbums.size, 0);
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (const albumNames of chunk([...newAlbums], concurrency)) {
|
||||||
|
const items = await Promise.all(
|
||||||
|
albumNames.map((albumName: string) => createAlbum({ createAlbumDto: { albumName } })),
|
||||||
|
);
|
||||||
|
for (const { id, albumName } of items) {
|
||||||
|
existingAlbums.set(albumName, id);
|
||||||
|
}
|
||||||
|
progressBar.increment(albumNames.length);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
progressBar.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Successfully created ${newAlbums.size} new album${s(newAlbums.size)}`);
|
||||||
|
console.log(`Successfully updated ${assets.length} asset${s(assets.length)}`);
|
||||||
|
|
||||||
const albumToAssets = new Map<string, string[]>();
|
const albumToAssets = new Map<string, string[]>();
|
||||||
for (const asset of assetsToUpdate) {
|
for (const asset of assets) {
|
||||||
const albumId = existingAlbums.get(asset.albumName);
|
const albumName = getAlbumName(asset.filepath, options);
|
||||||
|
if (!albumName) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const albumId = existingAlbums.get(albumName);
|
||||||
if (albumId) {
|
if (albumId) {
|
||||||
if (!albumToAssets.has(albumId)) {
|
if (!albumToAssets.has(albumId)) {
|
||||||
albumToAssets.set(albumId, []);
|
albumToAssets.set(albumId, []);
|
||||||
@ -346,17 +305,15 @@ class UploadCommand {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const albumUpdateProgress = new cliProgress.SingleBar(
|
const albumUpdateProgress = new SingleBar(
|
||||||
{
|
{ format: 'Adding assets to albums | {bar} | {percentage}% | ETA: {eta}s | {value}/{total} assets' },
|
||||||
format: 'Adding assets to albums | {bar} | {percentage}% | ETA: {eta}s | {value}/{total} assets',
|
Presets.shades_classic,
|
||||||
},
|
|
||||||
cliProgress.Presets.shades_classic,
|
|
||||||
);
|
);
|
||||||
albumUpdateProgress.start(assetsToUpdate.length, 0);
|
albumUpdateProgress.start(assets.length, 0);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
for (const [albumId, assets] of albumToAssets.entries()) {
|
for (const [albumId, assets] of albumToAssets.entries()) {
|
||||||
for (const assetBatch of chunk(assets, Math.min(1000 * (options.concurrency ?? 4), 65_000))) {
|
for (const assetBatch of chunk(assets, Math.min(1000 * concurrency, 65_000))) {
|
||||||
await addAssetsToAlbum({ id: albumId, bulkIdsDto: { ids: assetBatch } });
|
await addAssetsToAlbum({ id: albumId, bulkIdsDto: { ids: assetBatch } });
|
||||||
albumUpdateProgress.increment(assetBatch.length);
|
albumUpdateProgress.increment(assetBatch.length);
|
||||||
}
|
}
|
||||||
@ -364,89 +321,9 @@ class UploadCommand {
|
|||||||
} finally {
|
} finally {
|
||||||
albumUpdateProgress.stop();
|
albumUpdateProgress.stop();
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return { createdAlbumCount: newAlbums.length, updatedAssetCount: assetsToUpdate.length };
|
const getAlbumName = (filepath: string, options: UploadOptionsDto) => {
|
||||||
}
|
const folderName = os.platform() === 'win32' ? filepath.split('\\').at(-2) : filepath.split('/').at(-2);
|
||||||
|
return options.albumName ?? folderName;
|
||||||
public async deleteAssets(assets: Asset[], options: UploadOptionsDto): Promise<void> {
|
};
|
||||||
const deletionProgress = new cliProgress.SingleBar(
|
|
||||||
{
|
|
||||||
format: 'Deleting local assets | {bar} | {percentage}% | ETA: {eta}s | {value}/{total} assets',
|
|
||||||
},
|
|
||||||
cliProgress.Presets.shades_classic,
|
|
||||||
);
|
|
||||||
deletionProgress.start(assets.length, 0);
|
|
||||||
|
|
||||||
try {
|
|
||||||
for (const assetBatch of chunk(assets, options.concurrency)) {
|
|
||||||
await Promise.all(assetBatch.map((asset: Asset) => asset.delete()));
|
|
||||||
deletionProgress.update(assetBatch.length);
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
deletionProgress.stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async getStatus(assets: Asset[]): Promise<{ asset: Asset; status: CheckResponseStatus }[]> {
|
|
||||||
const checkResponse = await this.checkHashes(assets);
|
|
||||||
|
|
||||||
const responses = [];
|
|
||||||
for (const [check, asset] of zipDefined(checkResponse, assets)) {
|
|
||||||
if (check.assetId) {
|
|
||||||
asset.id = check.assetId;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (check.action === 'accept') {
|
|
||||||
responses.push({ asset, status: CheckResponseStatus.ACCEPT });
|
|
||||||
} else if (check.reason === 'duplicate') {
|
|
||||||
responses.push({ asset, status: CheckResponseStatus.DUPLICATE });
|
|
||||||
} else {
|
|
||||||
responses.push({ asset, status: CheckResponseStatus.REJECT });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return responses;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async checkHashes(assetsToCheck: Asset[]): Promise<AssetBulkUploadCheckResult[]> {
|
|
||||||
const checksums = await Promise.all(assetsToCheck.map((asset) => asset.hash()));
|
|
||||||
const assetBulkUploadCheckDto = {
|
|
||||||
assets: zipDefined(assetsToCheck, checksums).map(([asset, checksum]) => ({ id: asset.path, checksum })),
|
|
||||||
};
|
|
||||||
const checkResponse = await checkBulkUpload({ assetBulkUploadCheckDto });
|
|
||||||
return checkResponse.results;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async uploadAssets(assets: Asset[]): Promise<string[]> {
|
|
||||||
const fileRequests = await Promise.all(assets.map((asset) => asset.getUploadFormData()));
|
|
||||||
const results = await Promise.all(fileRequests.map((request) => this.uploadAsset(request)));
|
|
||||||
return results.map((response) => response.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async crawl(paths: string[], options: UploadOptionsDto): Promise<string[]> {
|
|
||||||
const formatResponse = await getSupportedMediaTypes();
|
|
||||||
const crawlService = new CrawlService(formatResponse.image, formatResponse.video);
|
|
||||||
|
|
||||||
return crawlService.crawl({
|
|
||||||
pathsToCrawl: paths,
|
|
||||||
recursive: options.recursive,
|
|
||||||
exclusionPatterns: options.exclusionPatterns,
|
|
||||||
includeHidden: options.includeHidden,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private async uploadAsset(data: FormData): Promise<{ id: string }> {
|
|
||||||
const { baseUrl, headers } = defaults;
|
|
||||||
|
|
||||||
const response = await fetch(`${baseUrl}/asset/upload`, {
|
|
||||||
method: 'post',
|
|
||||||
redirect: 'error',
|
|
||||||
headers: headers as Record<string, string>,
|
|
||||||
body: data,
|
|
||||||
});
|
|
||||||
if (response.status !== 200 && response.status !== 201) {
|
|
||||||
throw new Error(await response.text());
|
|
||||||
}
|
|
||||||
return response.json();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -3,12 +3,12 @@ import { existsSync } from 'node:fs';
|
|||||||
import { mkdir, unlink } from 'node:fs/promises';
|
import { mkdir, unlink } from 'node:fs/promises';
|
||||||
import { BaseOptions, connect, getAuthFilePath, logError, withError, writeAuthFile } from 'src/utils';
|
import { BaseOptions, connect, getAuthFilePath, logError, withError, writeAuthFile } from 'src/utils';
|
||||||
|
|
||||||
export const login = async (instanceUrl: string, apiKey: string, options: BaseOptions) => {
|
export const login = async (url: string, key: string, options: BaseOptions) => {
|
||||||
console.log(`Logging in to ${instanceUrl}`);
|
console.log(`Logging in to ${url}`);
|
||||||
|
|
||||||
const { configDirectory: configDir } = options;
|
const { configDirectory: configDir } = options;
|
||||||
|
|
||||||
await connect(instanceUrl, apiKey);
|
await connect(url, key);
|
||||||
|
|
||||||
const [error, userInfo] = await withError(getMyUserInfo());
|
const [error, userInfo] = await withError(getMyUserInfo());
|
||||||
if (error) {
|
if (error) {
|
||||||
@ -27,7 +27,7 @@ export const login = async (instanceUrl: string, apiKey: string, options: BaseOp
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await writeAuthFile(configDir, { instanceUrl, apiKey });
|
await writeAuthFile(configDir, { url, key });
|
||||||
|
|
||||||
console.log(`Wrote auth info to ${getAuthFilePath(configDir)}`);
|
console.log(`Wrote auth info to ${getAuthFilePath(configDir)}`);
|
||||||
};
|
};
|
||||||
|
@ -19,7 +19,7 @@ const program = new Command()
|
|||||||
.default(defaultConfigDirectory),
|
.default(defaultConfigDirectory),
|
||||||
)
|
)
|
||||||
.addOption(new Option('-u, --url [url]', 'Immich server URL').env('IMMICH_INSTANCE_URL'))
|
.addOption(new Option('-u, --url [url]', 'Immich server URL').env('IMMICH_INSTANCE_URL'))
|
||||||
.addOption(new Option('-k, --key [apiKey]', 'Immich API key').env('IMMICH_API_KEY'));
|
.addOption(new Option('-k, --key [key]', 'Immich API key').env('IMMICH_API_KEY'));
|
||||||
|
|
||||||
program
|
program
|
||||||
.command('login')
|
.command('login')
|
||||||
|
@ -1,70 +0,0 @@
|
|||||||
import { glob } from 'glob';
|
|
||||||
import * as fs from 'node:fs';
|
|
||||||
|
|
||||||
export class CrawlOptions {
|
|
||||||
pathsToCrawl!: string[];
|
|
||||||
recursive? = false;
|
|
||||||
includeHidden? = false;
|
|
||||||
exclusionPatterns?: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export class CrawlService {
|
|
||||||
private readonly extensions!: string[];
|
|
||||||
|
|
||||||
constructor(image: string[], video: string[]) {
|
|
||||||
this.extensions = [...image, ...video].map((extension) => extension.replace('.', ''));
|
|
||||||
}
|
|
||||||
|
|
||||||
async crawl(options: CrawlOptions): Promise<string[]> {
|
|
||||||
const { recursive, pathsToCrawl, exclusionPatterns, includeHidden } = options;
|
|
||||||
|
|
||||||
if (!pathsToCrawl) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const patterns: string[] = [];
|
|
||||||
const crawledFiles: string[] = [];
|
|
||||||
|
|
||||||
for await (const currentPath of pathsToCrawl) {
|
|
||||||
try {
|
|
||||||
const stats = await fs.promises.stat(currentPath);
|
|
||||||
if (stats.isFile() || stats.isSymbolicLink()) {
|
|
||||||
crawledFiles.push(currentPath);
|
|
||||||
} else {
|
|
||||||
patterns.push(currentPath);
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
if (error.code === 'ENOENT') {
|
|
||||||
patterns.push(currentPath);
|
|
||||||
} else {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let searchPattern: string;
|
|
||||||
if (patterns.length === 1) {
|
|
||||||
searchPattern = patterns[0];
|
|
||||||
} else if (patterns.length === 0) {
|
|
||||||
return crawledFiles;
|
|
||||||
} else {
|
|
||||||
searchPattern = '{' + patterns.join(',') + '}';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (recursive) {
|
|
||||||
searchPattern = searchPattern + '/**/';
|
|
||||||
}
|
|
||||||
|
|
||||||
searchPattern = `${searchPattern}/*.{${this.extensions.join(',')}}`;
|
|
||||||
|
|
||||||
const globbedFiles = await glob(searchPattern, {
|
|
||||||
absolute: true,
|
|
||||||
nocase: true,
|
|
||||||
nodir: true,
|
|
||||||
dot: includeHidden,
|
|
||||||
ignore: exclusionPatterns,
|
|
||||||
});
|
|
||||||
|
|
||||||
return [...crawledFiles, ...globbedFiles].sort();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,14 +1,31 @@
|
|||||||
import mockfs from 'mock-fs';
|
import mockfs from 'mock-fs';
|
||||||
import { CrawlOptions, CrawlService } from './crawl.service';
|
import { CrawlOptions, crawl } from 'src/utils';
|
||||||
|
|
||||||
interface Test {
|
interface Test {
|
||||||
test: string;
|
test: string;
|
||||||
options: CrawlOptions;
|
options: Omit<CrawlOptions, 'extensions'>;
|
||||||
files: Record<string, boolean>;
|
files: Record<string, boolean>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const cwd = process.cwd();
|
const cwd = process.cwd();
|
||||||
|
|
||||||
|
const extensions = [
|
||||||
|
'.jpg',
|
||||||
|
'.jpeg',
|
||||||
|
'.png',
|
||||||
|
'.heif',
|
||||||
|
'.heic',
|
||||||
|
'.tif',
|
||||||
|
'.nef',
|
||||||
|
'.webp',
|
||||||
|
'.tiff',
|
||||||
|
'.dng',
|
||||||
|
'.gif',
|
||||||
|
'.mov',
|
||||||
|
'.mp4',
|
||||||
|
'.webm',
|
||||||
|
];
|
||||||
|
|
||||||
const tests: Test[] = [
|
const tests: Test[] = [
|
||||||
{
|
{
|
||||||
test: 'should return empty when crawling an empty path list',
|
test: 'should return empty when crawling an empty path list',
|
||||||
@ -251,12 +268,7 @@ const tests: Test[] = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
describe(CrawlService.name, () => {
|
describe('crawl', () => {
|
||||||
const sut = new CrawlService(
|
|
||||||
['.jpg', '.jpeg', '.png', '.heif', '.heic', '.tif', '.nef', '.webp', '.tiff', '.dng', '.gif'],
|
|
||||||
['.mov', '.mp4', '.webm'],
|
|
||||||
);
|
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
mockfs.restore();
|
mockfs.restore();
|
||||||
});
|
});
|
||||||
@ -266,7 +278,7 @@ describe(CrawlService.name, () => {
|
|||||||
it(test, async () => {
|
it(test, async () => {
|
||||||
mockfs(Object.fromEntries(Object.keys(files).map((file) => [file, ''])));
|
mockfs(Object.fromEntries(Object.keys(files).map((file) => [file, ''])));
|
||||||
|
|
||||||
const actual = await sut.crawl(options);
|
const actual = await crawl({ ...options, extensions });
|
||||||
const expected = Object.entries(files)
|
const expected = Object.entries(files)
|
||||||
.filter((entry) => entry[1])
|
.filter((entry) => entry[1])
|
||||||
.map(([file]) => file);
|
.map(([file]) => file);
|
117
cli/src/utils.ts
@ -1,48 +1,49 @@
|
|||||||
import { defaults, getMyUserInfo, isHttpError } from '@immich/sdk';
|
import { defaults, getMyUserInfo, isHttpError } from '@immich/sdk';
|
||||||
import { readFile, writeFile } from 'node:fs/promises';
|
import { glob } from 'glob';
|
||||||
|
import { createHash } from 'node:crypto';
|
||||||
|
import { createReadStream } from 'node:fs';
|
||||||
|
import { readFile, stat, writeFile } from 'node:fs/promises';
|
||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
import yaml from 'yaml';
|
import yaml from 'yaml';
|
||||||
|
|
||||||
export interface BaseOptions {
|
export interface BaseOptions {
|
||||||
configDirectory: string;
|
configDirectory: string;
|
||||||
apiKey?: string;
|
key?: string;
|
||||||
instanceUrl?: string;
|
url?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AuthDto {
|
export type AuthDto = { url: string; key: string };
|
||||||
instanceUrl: string;
|
type OldAuthDto = { instanceUrl: string; apiKey: string };
|
||||||
apiKey: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const authenticate = async (options: BaseOptions): Promise<void> => {
|
export const authenticate = async (options: BaseOptions): Promise<void> => {
|
||||||
const { configDirectory: configDir, instanceUrl, apiKey } = options;
|
const { configDirectory: configDir, url, key } = options;
|
||||||
|
|
||||||
// provided in command
|
// provided in command
|
||||||
if (instanceUrl && apiKey) {
|
if (url && key) {
|
||||||
await connect(instanceUrl, apiKey);
|
await connect(url, key);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// fallback to file
|
// fallback to auth file
|
||||||
const config = await readAuthFile(configDir);
|
const config = await readAuthFile(configDir);
|
||||||
await connect(config.instanceUrl, config.apiKey);
|
await connect(config.url, config.key);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const connect = async (instanceUrl: string, apiKey: string): Promise<void> => {
|
export const connect = async (url: string, key: string): Promise<void> => {
|
||||||
const wellKnownUrl = new URL('.well-known/immich', instanceUrl);
|
const wellKnownUrl = new URL('.well-known/immich', url);
|
||||||
try {
|
try {
|
||||||
const wellKnown = await fetch(wellKnownUrl).then((response) => response.json());
|
const wellKnown = await fetch(wellKnownUrl).then((response) => response.json());
|
||||||
const endpoint = new URL(wellKnown.api.endpoint, instanceUrl).toString();
|
const endpoint = new URL(wellKnown.api.endpoint, url).toString();
|
||||||
if (endpoint !== instanceUrl) {
|
if (endpoint !== url) {
|
||||||
console.debug(`Discovered API at ${endpoint}`);
|
console.debug(`Discovered API at ${endpoint}`);
|
||||||
}
|
}
|
||||||
instanceUrl = endpoint;
|
url = endpoint;
|
||||||
} catch {
|
} catch {
|
||||||
// noop
|
// noop
|
||||||
}
|
}
|
||||||
|
|
||||||
defaults.baseUrl = instanceUrl;
|
defaults.baseUrl = url;
|
||||||
defaults.headers = { 'x-api-key': apiKey };
|
defaults.headers = { 'x-api-key': key };
|
||||||
|
|
||||||
const [error] = await withError(getMyUserInfo());
|
const [error] = await withError(getMyUserInfo());
|
||||||
if (isHttpError(error)) {
|
if (isHttpError(error)) {
|
||||||
@ -66,7 +67,12 @@ export const readAuthFile = async (dir: string) => {
|
|||||||
try {
|
try {
|
||||||
const data = await readFile(getAuthFilePath(dir));
|
const data = await readFile(getAuthFilePath(dir));
|
||||||
// TODO add class-transform/validation
|
// TODO add class-transform/validation
|
||||||
return yaml.parse(data.toString()) as AuthDto;
|
const auth = yaml.parse(data.toString()) as AuthDto | OldAuthDto;
|
||||||
|
const { instanceUrl, apiKey } = auth as OldAuthDto;
|
||||||
|
if (instanceUrl && apiKey) {
|
||||||
|
return { url: instanceUrl, key: apiKey };
|
||||||
|
}
|
||||||
|
return auth as AuthDto;
|
||||||
} catch (error: Error | any) {
|
} catch (error: Error | any) {
|
||||||
if (error.code === 'ENOENT' || error.code === 'ENOTDIR') {
|
if (error.code === 'ENOENT' || error.code === 'ENOTDIR') {
|
||||||
console.log('No auth file exists. Please login first.');
|
console.log('No auth file exists. Please login first.');
|
||||||
@ -87,3 +93,74 @@ export const withError = async <T>(promise: Promise<T>): Promise<[Error, undefin
|
|||||||
return [error, undefined];
|
return [error, undefined];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface CrawlOptions {
|
||||||
|
pathsToCrawl: string[];
|
||||||
|
recursive?: boolean;
|
||||||
|
includeHidden?: boolean;
|
||||||
|
exclusionPatterns?: string[];
|
||||||
|
extensions: string[];
|
||||||
|
}
|
||||||
|
export const crawl = async (options: CrawlOptions): Promise<string[]> => {
|
||||||
|
const { extensions: extensionsWithPeriod, recursive, pathsToCrawl, exclusionPatterns, includeHidden } = options;
|
||||||
|
const extensions = extensionsWithPeriod.map((extension) => extension.replace('.', ''));
|
||||||
|
|
||||||
|
if (pathsToCrawl.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const patterns: string[] = [];
|
||||||
|
const crawledFiles: string[] = [];
|
||||||
|
|
||||||
|
for await (const currentPath of pathsToCrawl) {
|
||||||
|
try {
|
||||||
|
const stats = await stat(currentPath);
|
||||||
|
if (stats.isFile() || stats.isSymbolicLink()) {
|
||||||
|
crawledFiles.push(currentPath);
|
||||||
|
} else {
|
||||||
|
patterns.push(currentPath);
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.code === 'ENOENT') {
|
||||||
|
patterns.push(currentPath);
|
||||||
|
} else {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let searchPattern: string;
|
||||||
|
if (patterns.length === 1) {
|
||||||
|
searchPattern = patterns[0];
|
||||||
|
} else if (patterns.length === 0) {
|
||||||
|
return crawledFiles;
|
||||||
|
} else {
|
||||||
|
searchPattern = '{' + patterns.join(',') + '}';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (recursive) {
|
||||||
|
searchPattern = searchPattern + '/**/';
|
||||||
|
}
|
||||||
|
|
||||||
|
searchPattern = `${searchPattern}/*.{${extensions.join(',')}}`;
|
||||||
|
|
||||||
|
const globbedFiles = await glob(searchPattern, {
|
||||||
|
absolute: true,
|
||||||
|
nocase: true,
|
||||||
|
nodir: true,
|
||||||
|
dot: includeHidden,
|
||||||
|
ignore: exclusionPatterns,
|
||||||
|
});
|
||||||
|
|
||||||
|
return [...crawledFiles, ...globbedFiles].sort();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const sha1 = (filepath: string) => {
|
||||||
|
const hash = createHash('sha1');
|
||||||
|
return new Promise<string>((resolve, reject) => {
|
||||||
|
const rs = createReadStream(filepath);
|
||||||
|
rs.on('error', reject);
|
||||||
|
rs.on('data', (chunk) => hash.update(chunk));
|
||||||
|
rs.on('end', () => resolve(hash.digest('hex')));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
Before Width: | Height: | Size: 1.7 MiB After Width: | Height: | Size: 1.8 MiB |
@ -99,7 +99,7 @@ services:
|
|||||||
|
|
||||||
redis:
|
redis:
|
||||||
container_name: immich_redis
|
container_name: immich_redis
|
||||||
image: redis:6.2-alpine@sha256:fd3535746075ba01b73c3602c0704bc944dd064c0a4ac46341a4a351bec69db8
|
image: redis:6.2-alpine@sha256:3fcb624d83a9c478357f16dc173c58ded325ccc5fd2a4375f3916c04cc579f70
|
||||||
|
|
||||||
database:
|
database:
|
||||||
container_name: immich_postgres
|
container_name: immich_postgres
|
||||||
|
@ -56,7 +56,7 @@ services:
|
|||||||
|
|
||||||
redis:
|
redis:
|
||||||
container_name: immich_redis
|
container_name: immich_redis
|
||||||
image: redis:6.2-alpine@sha256:fd3535746075ba01b73c3602c0704bc944dd064c0a4ac46341a4a351bec69db8
|
image: redis:6.2-alpine@sha256:3fcb624d83a9c478357f16dc173c58ded325ccc5fd2a4375f3916c04cc579f70
|
||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
database:
|
database:
|
||||||
@ -90,7 +90,7 @@ services:
|
|||||||
command: ['./run.sh', '-disable-reporting']
|
command: ['./run.sh', '-disable-reporting']
|
||||||
ports:
|
ports:
|
||||||
- 3000:3000
|
- 3000:3000
|
||||||
image: grafana/grafana:10.4.0-ubuntu@sha256:c1f582b7cc4c1b9805d187b5600ce7879550a12ef6d29571da133c3d3fc67a9c
|
image: grafana/grafana:10.4.1-ubuntu@sha256:65e0e7d0f0b001cb0478bce5093bff917677dc308dd27a0aa4b3ac38e4fd877c
|
||||||
volumes:
|
volumes:
|
||||||
- grafana-data:/var/lib/grafana
|
- grafana-data:/var/lib/grafana
|
||||||
|
|
||||||
|
@ -67,14 +67,20 @@ Once you have a new OAuth client application configured, Immich can be configure
|
|||||||
| Client Secret | string | (required) | Required. Client Secret (previous step) |
|
| Client Secret | string | (required) | Required. Client Secret (previous step) |
|
||||||
| Scope | string | openid email profile | Full list of scopes to send with the request (space delimited) |
|
| Scope | string | openid email profile | Full list of scopes to send with the request (space delimited) |
|
||||||
| Signing Algorithm | string | RS256 | The algorithm used to sign the id token (examples: RS256, HS256) |
|
| Signing Algorithm | string | RS256 | The algorithm used to sign the id token (examples: RS256, HS256) |
|
||||||
| Storage Label Claim | string | preferred_username | Claim mapping for the user's storage label |
|
| Storage Label Claim | string | preferred_username | Claim mapping for the user's storage label**¹** |
|
||||||
| Storage Quota Claim | string | immich_quota | Claim mapping for the user's storage |
|
| Storage Quota Claim | string | immich_quota | Claim mapping for the user's storage**¹** |
|
||||||
| Default Storage Quota (GiB) | number | 0 | Default quota for user without storage quota claim (Enter 0 for unlimited quota) |
|
| Default Storage Quota (GiB) | number | 0 | Default quota for user without storage quota claim (Enter 0 for unlimited quota) |
|
||||||
| Button Text | string | Login with OAuth | Text for the OAuth button on the web |
|
| Button Text | string | Login with OAuth | Text for the OAuth button on the web |
|
||||||
| Auto Register | boolean | true | When true, will automatically register a user the first time they sign in |
|
| Auto Register | boolean | true | When true, will automatically register a user the first time they sign in |
|
||||||
| [Auto Launch](#auto-launch) | boolean | false | When true, will skip the login page and automatically start the OAuth login process |
|
| [Auto Launch](#auto-launch) | boolean | false | When true, will skip the login page and automatically start the OAuth login process |
|
||||||
| [Mobile Redirect URI Override](#mobile-redirect-uri) | URL | (empty) | Http(s) alternative mobile redirect URI |
|
| [Mobile Redirect URI Override](#mobile-redirect-uri) | URL | (empty) | Http(s) alternative mobile redirect URI |
|
||||||
|
|
||||||
|
:::note Claim Options [1]
|
||||||
|
|
||||||
|
Claim is only used on user creation and not synchronized after that.
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
:::info
|
:::info
|
||||||
The Issuer URL should look something like the following, and return a valid json document.
|
The Issuer URL should look something like the following, and return a valid json document.
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@ After making any changes in the `server/src/entities`, a database migration need
|
|||||||
1. Run the command
|
1. Run the command
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run typeorm:migrations:generate ./src/infra/<migration-name>
|
npm run typeorm:migrations:generate <migration-name>
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Check if the migration file makes sense.
|
2. Check if the migration file makes sense.
|
||||||
|
@ -61,7 +61,7 @@ Options:
|
|||||||
Commands:
|
Commands:
|
||||||
upload [options] [paths...] Upload assets
|
upload [options] [paths...] Upload assets
|
||||||
server-info Display server information
|
server-info Display server information
|
||||||
login-key [instanceUrl] [apiKey] Login using an API key
|
login [url] [key] Login using an API key
|
||||||
logout Remove stored credentials
|
logout Remove stored credentials
|
||||||
help [command] display help for command
|
help [command] display help for command
|
||||||
```
|
```
|
||||||
@ -97,13 +97,13 @@ Note that the above options can read from environment variables as well.
|
|||||||
You begin by authenticating to your Immich server.
|
You begin by authenticating to your Immich server.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
immich login-key [instanceUrl] [apiKey]
|
immich login [url] [key]
|
||||||
```
|
```
|
||||||
|
|
||||||
For instance,
|
For instance,
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
immich login-key http://192.168.1.216:2283/api HFEJ38DNSDUEG
|
immich login http://192.168.1.216:2283/api HFEJ38DNSDUEG
|
||||||
```
|
```
|
||||||
|
|
||||||
This will store your credentials in a `auth.yml` file in the configuration directory which defaults to `~/.config/`. The directory can be set with the `-d` option or the environment variable `IMMICH_CONFIG_DIR`. Please keep the file secure, either by performing the logout command after you are done, or deleting it manually.
|
This will store your credentials in a `auth.yml` file in the configuration directory which defaults to `~/.config/`. The directory can be set with the `-d` option or the environment variable `IMMICH_CONFIG_DIR`. Please keep the file secure, either by performing the logout command after you are done, or deleting it manually.
|
||||||
|
@ -16,7 +16,12 @@ version: '3.8'
|
|||||||
services:
|
services:
|
||||||
immich-machine-learning:
|
immich-machine-learning:
|
||||||
container_name: immich_machine_learning
|
container_name: immich_machine_learning
|
||||||
|
# For hardware acceleration, add one of -[armnn, cuda, openvino] to the image tag.
|
||||||
|
# Example tag: ${IMMICH_VERSION:-release}-cuda
|
||||||
image: ghcr.io/immich-app/immich-machine-learning:${IMMICH_VERSION:-release}
|
image: ghcr.io/immich-app/immich-machine-learning:${IMMICH_VERSION:-release}
|
||||||
|
# extends:
|
||||||
|
# file: hwaccel.ml.yml
|
||||||
|
# service: # set to one of [armnn, cuda, openvino, openvino-wsl] for accelerated inference - use the `-wsl` version for WSL2 where applicable
|
||||||
volumes:
|
volumes:
|
||||||
- model-cache:/cache
|
- model-cache:/cache
|
||||||
restart: always
|
restart: always
|
||||||
|
@ -36,7 +36,7 @@ services:
|
|||||||
<<: *server-common
|
<<: *server-common
|
||||||
|
|
||||||
redis:
|
redis:
|
||||||
image: redis:6.2-alpine@sha256:fd3535746075ba01b73c3602c0704bc944dd064c0a4ac46341a4a351bec69db8
|
image: redis:6.2-alpine@sha256:3fcb624d83a9c478357f16dc173c58ded325ccc5fd2a4375f3916c04cc579f70
|
||||||
|
|
||||||
database:
|
database:
|
||||||
image: tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:90724186f0a3517cf6914295b5ab410db9ce23190a2d9d0b9dd6463e3fa298f0
|
image: tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:90724186f0a3517cf6914295b5ab410db9ce23190a2d9d0b9dd6463e3fa298f0
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
import { LoginResponseDto } from '@immich/sdk';
|
import { LoginResponseDto, getConfig } from '@immich/sdk';
|
||||||
import { createUserDto } from 'src/fixtures';
|
import { createUserDto } from 'src/fixtures';
|
||||||
import { errorDto } from 'src/responses';
|
import { errorDto } from 'src/responses';
|
||||||
import { app, utils } from 'src/utils';
|
import { app, asBearerAuth, utils } from 'src/utils';
|
||||||
import request from 'supertest';
|
import request from 'supertest';
|
||||||
import { beforeAll, describe, expect, it } from 'vitest';
|
import { beforeAll, describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
const getSystemConfig = (accessToken: string) => getConfig({ headers: asBearerAuth(accessToken) });
|
||||||
|
|
||||||
describe('/system-config', () => {
|
describe('/system-config', () => {
|
||||||
let admin: LoginResponseDto;
|
let admin: LoginResponseDto;
|
||||||
let nonAdmin: LoginResponseDto;
|
let nonAdmin: LoginResponseDto;
|
||||||
@ -60,4 +62,25 @@ describe('/system-config', () => {
|
|||||||
expect(body).toEqual(expect.objectContaining({ id: 'immich-map-dark' }));
|
expect(body).toEqual(expect.objectContaining({ id: 'immich-map-dark' }));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('PUT /system-config', () => {
|
||||||
|
it('should require authentication', async () => {
|
||||||
|
const { status, body } = await request(app).put('/system-config');
|
||||||
|
expect(status).toBe(401);
|
||||||
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject an invalid config entry', async () => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.put('/system-config')
|
||||||
|
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||||
|
.send({
|
||||||
|
...(await getSystemConfig(admin.accessToken)),
|
||||||
|
storageTemplate: { enabled: true, hashVerificationEnabled: true, template: '{{foo}}' },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(status).toBe(400);
|
||||||
|
expect(body).toEqual(errorDto.badRequest(expect.stringContaining('Invalid storage template')));
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -2,25 +2,25 @@ import { stat } from 'node:fs/promises';
|
|||||||
import { app, immichCli, utils } from 'src/utils';
|
import { app, immichCli, utils } from 'src/utils';
|
||||||
import { beforeEach, describe, expect, it } from 'vitest';
|
import { beforeEach, describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
describe(`immich login-key`, () => {
|
describe(`immich login`, () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await utils.resetDatabase();
|
await utils.resetDatabase();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should require a url', async () => {
|
it('should require a url', async () => {
|
||||||
const { stderr, exitCode } = await immichCli(['login-key']);
|
const { stderr, exitCode } = await immichCli(['login']);
|
||||||
expect(stderr).toBe("error: missing required argument 'url'");
|
expect(stderr).toBe("error: missing required argument 'url'");
|
||||||
expect(exitCode).toBe(1);
|
expect(exitCode).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should require a key', async () => {
|
it('should require a key', async () => {
|
||||||
const { stderr, exitCode } = await immichCli(['login-key', app]);
|
const { stderr, exitCode } = await immichCli(['login', app]);
|
||||||
expect(stderr).toBe("error: missing required argument 'key'");
|
expect(stderr).toBe("error: missing required argument 'key'");
|
||||||
expect(exitCode).toBe(1);
|
expect(exitCode).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should require a valid key', async () => {
|
it('should require a valid key', async () => {
|
||||||
const { stderr, exitCode } = await immichCli(['login-key', app, 'immich-is-so-cool']);
|
const { stderr, exitCode } = await immichCli(['login', app, 'immich-is-so-cool']);
|
||||||
expect(stderr).toContain('Failed to connect to server');
|
expect(stderr).toContain('Failed to connect to server');
|
||||||
expect(stderr).toContain('Invalid API key');
|
expect(stderr).toContain('Invalid API key');
|
||||||
expect(stderr).toContain('401');
|
expect(stderr).toContain('401');
|
||||||
@ -30,7 +30,7 @@ describe(`immich login-key`, () => {
|
|||||||
it('should login and save auth.yml with 600', async () => {
|
it('should login and save auth.yml with 600', async () => {
|
||||||
const admin = await utils.adminSetup();
|
const admin = await utils.adminSetup();
|
||||||
const key = await utils.createApiKey(admin.accessToken);
|
const key = await utils.createApiKey(admin.accessToken);
|
||||||
const { stdout, stderr, exitCode } = await immichCli(['login-key', app, `${key.secret}`]);
|
const { stdout, stderr, exitCode } = await immichCli(['login', app, `${key.secret}`]);
|
||||||
expect(stdout.split('\n')).toEqual([
|
expect(stdout.split('\n')).toEqual([
|
||||||
'Logging in to http://127.0.0.1:2283/api',
|
'Logging in to http://127.0.0.1:2283/api',
|
||||||
'Logged in as admin@immich.cloud',
|
'Logged in as admin@immich.cloud',
|
||||||
@ -47,7 +47,7 @@ describe(`immich login-key`, () => {
|
|||||||
it('should login without /api in the url', async () => {
|
it('should login without /api in the url', async () => {
|
||||||
const admin = await utils.adminSetup();
|
const admin = await utils.adminSetup();
|
||||||
const key = await utils.createApiKey(admin.accessToken);
|
const key = await utils.createApiKey(admin.accessToken);
|
||||||
const { stdout, stderr, exitCode } = await immichCli(['login-key', app.replaceAll('/api', ''), `${key.secret}`]);
|
const { stdout, stderr, exitCode } = await immichCli(['login', app.replaceAll('/api', ''), `${key.secret}`]);
|
||||||
expect(stdout.split('\n')).toEqual([
|
expect(stdout.split('\n')).toEqual([
|
||||||
'Logging in to http://127.0.0.1:2283',
|
'Logging in to http://127.0.0.1:2283',
|
||||||
'Discovered API at http://127.0.0.1:2283/api',
|
'Discovered API at http://127.0.0.1:2283/api',
|
||||||
|
@ -4,7 +4,8 @@ import { beforeAll, describe, expect, it } from 'vitest';
|
|||||||
describe(`immich server-info`, () => {
|
describe(`immich server-info`, () => {
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await utils.resetDatabase();
|
await utils.resetDatabase();
|
||||||
await utils.cliLogin();
|
const admin = await utils.adminSetup();
|
||||||
|
await utils.cliLogin(admin.accessToken);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return the server info', async () => {
|
it('should return the server info', async () => {
|
||||||
|
@ -1,20 +1,69 @@
|
|||||||
import { getAllAlbums, getAllAssets } from '@immich/sdk';
|
import { LoginResponseDto, getAllAlbums, getAllAssets } from '@immich/sdk';
|
||||||
import { mkdir, readdir, rm, symlink } from 'node:fs/promises';
|
import { mkdir, readdir, rm, symlink } from 'node:fs/promises';
|
||||||
import { asKeyAuth, immichCli, testAssetDir, utils } from 'src/utils';
|
import { asKeyAuth, immichCli, testAssetDir, utils } from 'src/utils';
|
||||||
import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
|
import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
describe(`immich upload`, () => {
|
describe(`immich upload`, () => {
|
||||||
|
let admin: LoginResponseDto;
|
||||||
let key: string;
|
let key: string;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await utils.resetDatabase();
|
await utils.resetDatabase();
|
||||||
key = await utils.cliLogin();
|
|
||||||
|
admin = await utils.adminSetup();
|
||||||
|
key = await utils.cliLogin(admin.accessToken);
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await utils.resetDatabase(['assets', 'albums']);
|
await utils.resetDatabase(['assets', 'albums']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe(`immich upload /path/to/file.jpg`, () => {
|
||||||
|
it('should upload a single file', async () => {
|
||||||
|
const { stderr, stdout, exitCode } = await immichCli(['upload', `${testAssetDir}/albums/nature/silver_fir.jpg`]);
|
||||||
|
expect(stderr).toBe('');
|
||||||
|
expect(stdout.split('\n')).toEqual(
|
||||||
|
expect.arrayContaining([expect.stringContaining('Successfully uploaded 1 asset')]),
|
||||||
|
);
|
||||||
|
expect(exitCode).toBe(0);
|
||||||
|
|
||||||
|
const assets = await getAllAssets({}, { headers: asKeyAuth(key) });
|
||||||
|
expect(assets.length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should skip a duplicate file', async () => {
|
||||||
|
const first = await immichCli(['upload', `${testAssetDir}/albums/nature/silver_fir.jpg`]);
|
||||||
|
expect(first.stderr).toBe('');
|
||||||
|
expect(first.stdout.split('\n')).toEqual(
|
||||||
|
expect.arrayContaining([expect.stringContaining('Successfully uploaded 1 asset')]),
|
||||||
|
);
|
||||||
|
expect(first.exitCode).toBe(0);
|
||||||
|
|
||||||
|
const assets = await getAllAssets({}, { headers: asKeyAuth(key) });
|
||||||
|
expect(assets.length).toBe(1);
|
||||||
|
|
||||||
|
const second = await immichCli(['upload', `${testAssetDir}/albums/nature/silver_fir.jpg`]);
|
||||||
|
expect(second.stderr).toBe('');
|
||||||
|
expect(second.stdout.split('\n')).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.stringContaining('Found 0 new files and 1 duplicate'),
|
||||||
|
expect.stringContaining('All assets were already uploaded, nothing to do'),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
expect(first.exitCode).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should skip files that do not exist', async () => {
|
||||||
|
const { stderr, stdout, exitCode } = await immichCli(['upload', `/path/to/file`]);
|
||||||
|
expect(stderr).toBe('');
|
||||||
|
expect(stdout.split('\n')).toEqual(expect.arrayContaining([expect.stringContaining('No files found, exiting')]));
|
||||||
|
expect(exitCode).toBe(0);
|
||||||
|
|
||||||
|
const assets = await getAllAssets({}, { headers: asKeyAuth(key) });
|
||||||
|
expect(assets.length).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('immich upload --recursive', () => {
|
describe('immich upload --recursive', () => {
|
||||||
it('should upload a folder recursively', async () => {
|
it('should upload a folder recursively', async () => {
|
||||||
const { stderr, stdout, exitCode } = await immichCli(['upload', `${testAssetDir}/albums/nature/`, '--recursive']);
|
const { stderr, stdout, exitCode } = await immichCli(['upload', `${testAssetDir}/albums/nature/`, '--recursive']);
|
||||||
|
@ -404,10 +404,9 @@ export const utils = {
|
|||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
|
|
||||||
cliLogin: async () => {
|
cliLogin: async (accessToken: string) => {
|
||||||
const admin = await utils.adminSetup();
|
const key = await utils.createApiKey(accessToken);
|
||||||
const key = await utils.createApiKey(admin.accessToken);
|
await immichCli(['login', app, `${key.secret}`]);
|
||||||
await immichCli(['login-key', app, `${key.secret}`]);
|
|
||||||
return key.secret;
|
return key.secret;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
FROM mambaorg/micromamba:bookworm-slim@sha256:96586e238e2fed914b839e50cf91943b5655262348d141466b34ced2e0b5b155 as builder
|
FROM mambaorg/micromamba:bookworm-slim@sha256:881dbb68d115182b2c12e7e77dc54ea5005fd4e0123ca009d822adb5b0631785 as builder
|
||||||
|
|
||||||
ENV NODE_ENV=production \
|
ENV NODE_ENV=production \
|
||||||
TRANSFORMERS_CACHE=/cache \
|
TRANSFORMERS_CACHE=/cache \
|
||||||
|
42
machine-learning/poetry.lock
generated
@ -877,13 +877,13 @@ tqdm = ["tqdm"]
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ftfy"
|
name = "ftfy"
|
||||||
version = "6.1.3"
|
version = "6.2.0"
|
||||||
description = "Fixes mojibake and other problems with Unicode, after the fact"
|
description = "Fixes mojibake and other problems with Unicode, after the fact"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8,<4"
|
python-versions = ">=3.8,<4"
|
||||||
files = [
|
files = [
|
||||||
{file = "ftfy-6.1.3-py3-none-any.whl", hash = "sha256:e49c306c06a97f4986faa7a8740cfe3c13f3106e85bcec73eb629817e671557c"},
|
{file = "ftfy-6.2.0-py3-none-any.whl", hash = "sha256:f94a2c34b76e07475720e3096f5ca80911d152406fbde66fdb45c4d0c9150026"},
|
||||||
{file = "ftfy-6.1.3.tar.gz", hash = "sha256:693274aead811cff24c1e8784165aa755cd2f6e442a5ec535c7d697f6422a422"},
|
{file = "ftfy-6.2.0.tar.gz", hash = "sha256:5e42143c7025ef97944ca2619d6b61b0619fc6654f98771d39e862c1424c75c0"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
@ -2844,28 +2844,28 @@ files = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ruff"
|
name = "ruff"
|
||||||
version = "0.3.2"
|
version = "0.3.3"
|
||||||
description = "An extremely fast Python linter and code formatter, written in Rust."
|
description = "An extremely fast Python linter and code formatter, written in Rust."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.7"
|
||||||
files = [
|
files = [
|
||||||
{file = "ruff-0.3.2-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:77f2612752e25f730da7421ca5e3147b213dca4f9a0f7e0b534e9562c5441f01"},
|
{file = "ruff-0.3.3-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:973a0e388b7bc2e9148c7f9be8b8c6ae7471b9be37e1cc732f8f44a6f6d7720d"},
|
||||||
{file = "ruff-0.3.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9966b964b2dd1107797be9ca7195002b874424d1d5472097701ae8f43eadef5d"},
|
{file = "ruff-0.3.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:cfa60d23269d6e2031129b053fdb4e5a7b0637fc6c9c0586737b962b2f834493"},
|
||||||
{file = "ruff-0.3.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b83d17ff166aa0659d1e1deaf9f2f14cbe387293a906de09bc4860717eb2e2da"},
|
{file = "ruff-0.3.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1eca7ff7a47043cf6ce5c7f45f603b09121a7cc047447744b029d1b719278eb5"},
|
||||||
{file = "ruff-0.3.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb875c6cc87b3703aeda85f01c9aebdce3d217aeaca3c2e52e38077383f7268a"},
|
{file = "ruff-0.3.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7d3f6762217c1da954de24b4a1a70515630d29f71e268ec5000afe81377642d"},
|
||||||
{file = "ruff-0.3.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:be75e468a6a86426430373d81c041b7605137a28f7014a72d2fc749e47f572aa"},
|
{file = "ruff-0.3.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b24c19e8598916d9c6f5a5437671f55ee93c212a2c4c569605dc3842b6820386"},
|
||||||
{file = "ruff-0.3.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:967978ac2d4506255e2f52afe70dda023fc602b283e97685c8447d036863a302"},
|
{file = "ruff-0.3.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:5a6cbf216b69c7090f0fe4669501a27326c34e119068c1494f35aaf4cc683778"},
|
||||||
{file = "ruff-0.3.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1231eacd4510f73222940727ac927bc5d07667a86b0cbe822024dd00343e77e9"},
|
{file = "ruff-0.3.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:352e95ead6964974b234e16ba8a66dad102ec7bf8ac064a23f95371d8b198aab"},
|
||||||
{file = "ruff-0.3.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2c6d613b19e9a8021be2ee1d0e27710208d1603b56f47203d0abbde906929a9b"},
|
{file = "ruff-0.3.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d6ab88c81c4040a817aa432484e838aaddf8bfd7ca70e4e615482757acb64f8"},
|
||||||
{file = "ruff-0.3.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8439338a6303585d27b66b4626cbde89bb3e50fa3cae86ce52c1db7449330a7"},
|
{file = "ruff-0.3.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79bca3a03a759cc773fca69e0bdeac8abd1c13c31b798d5bb3c9da4a03144a9f"},
|
||||||
{file = "ruff-0.3.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:de8b480d8379620cbb5ea466a9e53bb467d2fb07c7eca54a4aa8576483c35d36"},
|
{file = "ruff-0.3.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:2700a804d5336bcffe063fd789ca2c7b02b552d2e323a336700abb8ae9e6a3f8"},
|
||||||
{file = "ruff-0.3.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b74c3de9103bd35df2bb05d8b2899bf2dbe4efda6474ea9681280648ec4d237d"},
|
{file = "ruff-0.3.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:fd66469f1a18fdb9d32e22b79f486223052ddf057dc56dea0caaf1a47bdfaf4e"},
|
||||||
{file = "ruff-0.3.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:f380be9fc15a99765c9cf316b40b9da1f6ad2ab9639e551703e581a5e6da6745"},
|
{file = "ruff-0.3.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:45817af234605525cdf6317005923bf532514e1ea3d9270acf61ca2440691376"},
|
||||||
{file = "ruff-0.3.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:0ac06a3759c3ab9ef86bbeca665d31ad3aa9a4b1c17684aadb7e61c10baa0df4"},
|
{file = "ruff-0.3.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:0da458989ce0159555ef224d5b7c24d3d2e4bf4c300b85467b08c3261c6bc6a8"},
|
||||||
{file = "ruff-0.3.2-py3-none-win32.whl", hash = "sha256:9bd640a8f7dd07a0b6901fcebccedadeb1a705a50350fb86b4003b805c81385a"},
|
{file = "ruff-0.3.3-py3-none-win32.whl", hash = "sha256:f2831ec6a580a97f1ea82ea1eda0401c3cdf512cf2045fa3c85e8ef109e87de0"},
|
||||||
{file = "ruff-0.3.2-py3-none-win_amd64.whl", hash = "sha256:0c1bdd9920cab5707c26c8b3bf33a064a4ca7842d91a99ec0634fec68f9f4037"},
|
{file = "ruff-0.3.3-py3-none-win_amd64.whl", hash = "sha256:be90bcae57c24d9f9d023b12d627e958eb55f595428bafcb7fec0791ad25ddfc"},
|
||||||
{file = "ruff-0.3.2-py3-none-win_arm64.whl", hash = "sha256:5f65103b1d76e0d600cabd577b04179ff592064eaa451a70a81085930e907d0b"},
|
{file = "ruff-0.3.3-py3-none-win_arm64.whl", hash = "sha256:0171aab5fecdc54383993389710a3d1227f2da124d76a2784a7098e818f92d61"},
|
||||||
{file = "ruff-0.3.2.tar.gz", hash = "sha256:fa78ec9418eb1ca3db392811df3376b46471ae93792a81af2d1cbb0e5dcb5142"},
|
{file = "ruff-0.3.3.tar.gz", hash = "sha256:38671be06f57a2f8aba957d9f701ea889aa5736be806f18c0cd03d6ff0cbca8d"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"flutterSdkVersion": "3.16.9",
|
"flutterSdkVersion": "3.19.3",
|
||||||
"flavors": {}
|
"flavors": {}
|
||||||
}
|
}
|
Before Width: | Height: | Size: 4.8 KiB |
Before Width: | Height: | Size: 3.3 KiB |
Before Width: | Height: | Size: 6.2 KiB |
Before Width: | Height: | Size: 7.3 KiB |
Before Width: | Height: | Size: 19 KiB |
@ -1,70 +1,27 @@
|
|||||||
<vector android:height="200dp"
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
android:viewportHeight="1300"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:viewportWidth="1300"
|
android:width="108dp"
|
||||||
android:width="200dp"
|
android:height="108dp"
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
android:viewportWidth="108"
|
||||||
|
android:viewportHeight="108">
|
||||||
<path
|
<path
|
||||||
android:fillColor="#4081ef"
|
android:fillColor="#FA2921"
|
||||||
android:fillType="nonZero"
|
android:fillType="nonZero"
|
||||||
android:pathData="M578,922.6c-2.2,-0.2 -5.5,-1 -9.7,-2.2c-52.4,-15.7 -99,-46.5 -133.8,-88.5c-8.8,-10.7 -17.2,-22.4 -19.4,-27.5c-8.1,-18.1 -6.3,-38.7 4.8,-55.4c5,-7.5 13.2,-15 20.5,-18.7c1.2,-0.6 54.1,-20 55.8,-20.4c0.5,-0.1 0.5,0.2 -0.3,2.1c-0.7,1.7 -1,3.1 -1.1,5.5l-0.1,3.2l2.8,5.8c8.7,17.9 19.2,32.7 33.2,46.4c6.3,6.2 7.8,7.6 13.8,12.3c22.7,18.1 52,30.7 79.9,34.3c2.5,0.3 5,0.8 5.7,1c2.8,0.9 7.7,-0.8 11,-3.7l1.8,-1.6l-0.2,4.8c-0.1,2.7 -0.6,15.4 -1,28.3c-0.6,20.3 -0.8,24 -1.5,27.5c-3.9,20.7 -18.6,37.5 -38.4,44.1c-4.6,1.5 -8,2.2 -13.1,2.7c-4.6,0.5 -5.9,0.4 -10.7,0Z" />
|
android:pathData="m52.25,43.55c3.4,3.08 6.15,6.39 7.91,9.5 3.03,-5.55 5.06,-12.14 5.08,-16.34 0,-0.03 0,-0.06 0,-0.08 0,-6.21 -6.06,-8.63 -11.28,-8.63 -5.22,0 -11.28,2.42 -11.28,8.63 0,0.08 0,0.2 0,0.34 2.91,1.32 6.36,3.69 9.56,6.59z" />
|
||||||
<path
|
<path
|
||||||
android:fillColor="#31a452"
|
android:fillColor="#ED79B5"
|
||||||
android:fillType="nonZero"
|
android:fillType="nonZero"
|
||||||
android:pathData="M707.3,922.4c-4,-0.4 -9.4,-1.6 -13.2,-2.9c-3.4,-1.2 -10,-4.4 -12.5,-6.1c-10.9,-7.4 -19,-17.9 -23.1,-30c-2.2,-6.7 -2.3,-7.5 -3.3,-36.9c-0.5,-14.9 -0.9,-27.9 -0.9,-28.9l-0,-1.9l2.3,1.8c2.6,2 6.6,3.4 8.5,3.1c0.6,-0.1 3,-0.5 5.3,-0.8c37.7,-5.3 71.2,-22.2 97.4,-49.1c12.2,-12.5 21.4,-25.5 29.9,-42.4l3.5,-7l0,-3.6c0,-3.1 -0.1,-3.8 -1,-5.7c-0.5,-1.2 -0.9,-2.1 -0.9,-2.2c0.2,-0.2 55.3,20.1 56.9,20.9c2.6,1.3 6.6,4.1 9.9,7c9.2,7.7 16.1,19.4 18.8,31.8c0.7,3.1 0.8,4.8 0.8,11.3c0,8.6 -0.5,11.7 -2.9,18.7c-1.7,5 -2.9,7.2 -7.1,13.1c-7.6,11 -15.3,20.5 -25.2,31.2c-32.8,35.4 -76.5,62.5 -123.4,76.3c-8,2.5 -12.4,3 -19.8,2.3Z" />
|
android:pathData="m33.68,60.5c2.13,-2.42 5.39,-5.05 9.08,-7.27 3.92,-2.36 7.84,-4.01 11.28,-4.76 -4.22,-4.67 -9.72,-8.67 -13.62,-10 -0.03,-0.01 -0.05,-0.02 -0.08,-0.03 -5.78,-1.92 -9.9,3.23 -11.51,8.31 -1.61,5.08 -1.24,11.72 4.54,13.64 0.08,0.03 0.18,0.06 0.31,0.1z" />
|
||||||
<path
|
<path
|
||||||
android:fillColor="#de7fb3"
|
android:fillColor="#FFB400"
|
||||||
android:fillType="nonZero"
|
android:fillType="nonZero"
|
||||||
android:pathData="M623.1,811c-25.9,-4.2 -50.7,-14.9 -71.7,-31c-5.2,-4 -8.7,-7.1 -14.1,-12.4c-12.7,-12.5 -21.9,-24.9 -30.5,-41.4c-2.3,-4.4 -2.4,-4.7 -2.4,-7.1c0,-8.8 8.5,-15.2 16.9,-12.7c5.6,1.7 9.6,6.8 9.7,12.2c0,2.6 -0.8,4.6 -2.6,6.2c-1.2,1.1 -3.2,1.9 -4.6,1.9c-1.2,0 -3.3,-0.8 -4.3,-1.6c-2.1,-1.8 -2,-1 0.4,3.2c19.3,33.8 52.3,59.1 90,69.1c5.7,1.5 11.5,2.7 11.8,2.4c0.1,-0.1 -0.4,-0.8 -1.3,-1.6c-5.1,-4.5 -2.3,-11.7 5,-12.8c5.4,-0.8 11.4,2.7 13.9,8c0.8,1.7 1,2.5 1,5.3c0,2.8 -0.1,3.5 -1,5.3c-2,4.3 -6.8,7.9 -10.3,7.8c-0.9,-0.1 -3.6,-0.5 -5.9,-0.8Z" />
|
android:pathData="M79.17,46.67C77.56,41.59 73.44,36.44 67.66,38.36c-0.08,0.03 -0.19,0.06 -0.31,0.1 -0.33,3.24 -1.46,7.33 -3.17,11.34 -1.81,4.27 -4.04,7.96 -6.39,10.64 6.1,1.24 12.85,1.17 16.76,-0.1 0.03,-0.01 0.05,-0.02 0.08,-0.03 5.78,-1.92 6.15,-8.56 4.54,-13.64z" />
|
||||||
<path
|
<path
|
||||||
android:fillColor="#4081ef"
|
android:fillColor="#1E83F7"
|
||||||
android:fillType="nonZero"
|
android:fillType="nonZero"
|
||||||
android:pathData="M665.1,811.2c-3.4,-1.3 -6.4,-4.3 -7.8,-8.1c-1.1,-2.9 -0.9,-7.3 0.5,-10.2c2.6,-5.3 8.7,-8.5 14.4,-7.5c2.9,0.5 4.7,1.9 6,4.3c0.8,1.6 1,2.2 0.8,3.6c-0.3,2.2 -0.9,3.3 -2.7,4.8c-0.8,0.7 -1.4,1.4 -1.3,1.5c0.5,0.5 13.4,-2.7 21.3,-5.4c33.6,-11.3 62.5,-35.1 80.4,-66.1c2.5,-4.4 2.6,-5 0.5,-3.2c-2.8,2.4 -7,1.9 -9.6,-1c-4,-4.6 -0.7,-13.8 6.1,-16.9c2,-0.9 2.7,-1 5.5,-1c2.9,0 3.5,0.1 5.6,1.1c4.4,2.1 7.4,6.4 7.8,11c0.2,2.2 0.1,2.3 -2.2,6.9c-23,45.9 -67,78.1 -117.2,85.9c-5.5,0.9 -6.3,1 -8.1,0.3Z" />
|
android:pathData="m48.82,65.46c-0.98,-4.54 -1.3,-8.87 -0.94,-12.45 -5.64,2.67 -11.07,6.78 -13.5,10.16 -0.02,0.02 -0.03,0.05 -0.05,0.07 -3.57,5.03 -0.06,10.63 4.16,13.77 4.22,3.14 10.51,4.83 14.09,-0.2 0.05,-0.07 0.11,-0.16 0.19,-0.27 -1.59,-2.82 -3.03,-6.81 -3.95,-11.08z" />
|
||||||
<path
|
<path
|
||||||
android:fillColor="#31a452"
|
android:fillColor="#18C249"
|
||||||
android:fillType="nonZero"
|
android:fillType="nonZero"
|
||||||
android:pathData="M578.6,771.5c-4.7,-0.9 -8.7,-2.7 -12.9,-5.9c-10.8,-8.1 -13.5,-22.3 -6.6,-33.7c0.7,-1.2 1.1,-2.2 1,-2.4c-0.2,-0.2 -1.2,-0.6 -2.3,-1.1c-7.6,-3 -13,-10.6 -13.5,-19.1c-0.5,-7.4 3.1,-15 9,-19.4c1,-0.7 2.2,-1.5 2.6,-1.8c0.8,-0.4 68.9,-22.7 69.4,-22.7c0.2,0 0.7,0.7 1.2,1.5c0.5,0.8 1.6,2.3 2.4,3.3c1.2,1.4 1.5,1.9 1.2,2.3c-0.2,0.3 -6.9,9.5 -14.8,20.5c-15.9,21.9 -15.5,21.3 -13.4,23.4c1.3,1.3 2.9,1.4 4.4,0.3c0.6,-0.4 7.5,-9.7 15.5,-20.7c11.2,-15.4 14.6,-19.9 15,-19.7c0.9,0.4 5.5,1.9 6.6,2.1l1,0.2l-0,35.3c-0,39.7 -0,38.8 -2.5,44c-2.6,5.3 -7.2,9.3 -12.7,11.2c-3.7,1.3 -6.8,1.6 -10.2,1c-5.5,-0.9 -9.8,-3.2 -13.7,-7.4l-2.2,-2.4l-0.6,0.9c-3,4.3 -8.6,8.1 -14,9.5c-2.8,0.9 -7.8,1.2 -9.9,0.8Z" />
|
android:pathData="m73.58,62.92c-3.11,0.68 -7.26,0.84 -11.52,0.42 -4.53,-0.45 -8.64,-1.47 -11.86,-2.93 0.73,6.31 2.88,12.87 5.28,16.28 0.02,0.02 0.03,0.05 0.05,0.07 3.57,5.03 9.86,3.34 14.09,0.2 4.22,-3.14 7.74,-8.74 4.16,-13.77 -0.05,-0.07 -0.11,-0.16 -0.19,-0.27z" />
|
||||||
<path
|
|
||||||
android:fillColor="#ffb800"
|
|
||||||
android:fillType="nonZero"
|
|
||||||
android:pathData="M710.4,771.5c-5.5,-0.9 -9.9,-3.2 -14.3,-7.6l-3.2,-3.2l-0.7,1c-2.3,3.3 -6.8,6.5 -11.1,7.9c-3.7,1.2 -9.2,1.4 -12.6,0.3c-7.1,-2.1 -12.7,-7.4 -15.2,-14.3l-0.9,-2.6l0,-74.2l1.8,-0.4c1,-0.2 2.7,-0.8 3.9,-1.2c1.1,-0.5 2.1,-0.8 2.2,-0.7c0.1,0.1 6.5,9 14.4,19.9c7.8,10.9 14.7,20.1 15.2,20.5c2.2,1.9 5.4,0.4 5.4,-2.6c0,-1.4 -1,-2.9 -13.8,-20.5c-7.6,-10.5 -14.2,-19.6 -14.7,-20.4l-0.9,-1.3l1.4,-1.7c0.8,-0.9 1.9,-2.5 2.5,-3.4l1,-1.6l34.4,11.2c18.9,6.2 35.1,11.6 35.9,12.1c6.8,4 11.1,11.3 11.1,19.1c0,4.1 -0.5,6.4 -2.4,10.2c-2,4.1 -5.5,7.6 -9.6,9.7c-1.6,0.8 -3.2,1.5 -3.4,1.5c-1,0 -0.9,0.7 0.3,2.6c2.8,4.3 4,8.5 3.9,13.7c0,8.1 -3.7,15.2 -10.6,20.3c-6.5,4.8 -13.4,6.7 -20,5.7Z" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#de7fb3"
|
|
||||||
android:fillType="nonZero"
|
|
||||||
android:pathData="M421.4,714.9c-0.5,-0.1 -2.3,-0.4 -3.9,-0.7c-15.6,-2.6 -30.4,-12.6 -38.8,-26.2c-3.5,-5.7 -6.4,-13.2 -7.8,-19.9c-1.2,-6.1 -0.8,-28.1 0.8,-43.1c4.5,-43 19,-84.3 42.2,-120.7c6.5,-10.2 14.9,-21.5 18.2,-24.6c17.8,-16.6 43.1,-20.5 64.8,-10c4.3,2.1 8.8,5.1 12.7,8.6c2.8,2.4 5.8,6.1 20.9,25.5c9.7,12.5 17.8,22.8 17.9,23c0.2,0.2 -0.9,0.4 -3.2,0.4c-2.5,0 -4.1,0.2 -5.7,0.7c-2.1,0.7 -2.6,1.1 -7.9,6.3c-8.2,8.1 -14.4,15.3 -20.3,23.9c-15.5,22.2 -25.4,47.7 -28.8,74.8c-2.2,16.9 -1.6,37.5 1.6,52.3c0.3,1.4 0.5,2.8 0.4,3c-0.1,0.2 0.2,1.3 0.8,2.4c1.1,2.4 4.3,5.7 6.5,6.8l1.5,0.8l-1.2,0.4c-0.7,0.2 -13.1,3.8 -27.6,8c-16.4,4.7 -27.7,7.8 -29.8,8.1c-3.1,0.4 -11.1,0.6 -13.3,0.2Z" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#ffb800"
|
|
||||||
android:fillType="nonZero"
|
|
||||||
android:pathData="M862.2,714.7c-2.1,-0.3 -33.8,-9.1 -56.5,-15.8l-2.5,-0.7l1.6,-0.8c3.4,-1.7 7.2,-6.6 7.3,-9.6c0,-0.7 0.4,-3.3 0.8,-5.8c3.9,-22.7 3.1,-46.1 -2.5,-68.4c-6.4,-25.5 -18.6,-49.2 -35.8,-69.1c-4.6,-5.3 -14.8,-15.4 -16.4,-16.1c-2.4,-1.1 -5.1,-1.6 -8,-1.4l-2.7,0.2l1.2,-1.5c0.7,-0.8 8.5,-10.8 17.5,-22.3c8.9,-11.5 17.2,-21.8 18.5,-23.1c2.6,-2.7 7,-6.2 10.3,-8.2c19.3,-11.6 43,-11.1 61.6,1.2c5.4,3.6 8.2,6.2 12.3,11.7c26.4,34.5 44,73.7 52.3,116.2c3.4,17.6 4.9,33.3 5,52.4c0,13 -0.2,14.8 -2.5,21.8c-8.4,26.2 -34.5,42.8 -61.5,39.3Z" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#e64132"
|
|
||||||
android:fillType="nonZero"
|
|
||||||
android:pathData="M501.4,691.5c-2,-0.5 -4.6,-1.9 -6,-3.3c-2.5,-2.4 -3.1,-3.5 -3.7,-7.3c-4.4,-27.3 -2.2,-54 6.7,-79.3c5.3,-15.1 13.5,-30.5 23,-43.1c5.8,-7.8 16.6,-19.5 19,-20.7c4.7,-2.4 11.3,-1.2 15.2,2.7c5.4,5.4 5.2,13.9 -0.3,19.1c-4.3,4 -9.4,4.4 -12.6,0.9c-1.7,-1.9 -2.2,-3.9 -1.7,-6.4c0.2,-1.1 0.3,-2 0.2,-2.2c-0.3,-0.3 -3.6,3.3 -8.3,9.1c-17.6,21.8 -28.5,48 -31.9,76.5c-1.1,9.3 -1,26.4 0.1,34.6c0.3,1.8 0.8,1.9 1.4,0.1c0.9,-2.6 4,-4.7 6.8,-4.7c3,-0 5.9,2.2 7.5,5.7c0.6,1.3 0.8,2.3 0.8,5.2c-0,3.3 -0.1,3.8 -1.1,5.7c-1.4,2.7 -4.6,5.7 -7.1,6.6c-2.5,0.9 -6.1,1.2 -8,0.8Z" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#31a452"
|
|
||||||
android:fillType="nonZero"
|
|
||||||
android:pathData="M790.1,691.5c-3.7,-0.6 -7.7,-3.6 -9.4,-7.1c-3.8,-7.5 0.1,-16.9 6.9,-16.9c3.1,0 5.8,2 6.9,5.2c0.4,1.2 0.5,1.3 0.7,0.7c1.3,-3.7 1.7,-26.4 0.6,-35.7c-3.6,-29.6 -14.5,-55.3 -33,-77.9c-5.5,-6.7 -8.4,-9.4 -7.1,-6.6c0.7,1.4 0.5,4.3 -0.3,5.9c-0.9,1.7 -3.2,3.5 -5,3.8c-3.2,0.6 -7.9,-1.6 -10.2,-4.8c-6.5,-8.8 -0.5,-21.2 10.4,-21.4c4.6,-0.1 5.2,0.3 11.2,6.4c12.1,12.3 21.1,24.9 28.8,40.3c13.2,26.3 18.6,54.9 16.1,84.5c-0.5,5.6 -2,15.7 -2.6,17.1c-1.3,2.8 -4.8,5.5 -8.4,6.5c-2.3,0.4 -3.1,0.4 -5.6,0Z" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#4081ef"
|
|
||||||
android:fillType="nonZero"
|
|
||||||
android:pathData="M545.7,680.2c-6,-1.3 -12.2,-6.2 -14.9,-11.7c-3.4,-7 -3.1,-15.1 0.9,-21.6c0.7,-1.2 1.2,-2.3 1.1,-2.4c-0.1,-0.1 -1.1,-0.6 -2.1,-1c-3.9,-1.5 -8.1,-4.8 -10.7,-8.3c-4.6,-6.2 -6.1,-14.6 -3.9,-22.1c2.9,-10.3 9.4,-16.8 19.1,-19.3c2.8,-0.7 9,-0.8 11.7,-0c1.1,0.3 2.2,0.5 2.4,0.5c0.2,-0 0.3,-0.7 0.3,-1.5c0,-2.9 0.8,-5.8 2.4,-9.2c5.2,-10.8 18.1,-15.5 29,-10.5c2.7,1.2 6.2,3.8 7.8,5.8c0.7,0.8 10.3,14 21.5,29.4l20.3,27.9l-1.5,1.8c-0.8,1 -1.9,2.6 -2.5,3.5c-0.6,1 -1.2,1.7 -1.5,1.6c-4.5,-1.7 -46.7,-15 -47.7,-15c-1.9,0 -3.1,1.3 -3.1,3.2c0,1 0.2,1.7 0.8,2.3c0.6,0.6 7.8,3.1 24.5,8.5l23.7,7.7l-0.2,8.6l-32.6,10.5c-18,5.9 -33.9,10.9 -35.2,11.2c-3.1,0.7 -6.6,0.7 -9.6,0.1Z" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#e64132"
|
|
||||||
android:fillType="nonZero"
|
|
||||||
android:pathData="M740,679.8c-1.8,-0.5 -17.5,-5.6 -35,-11.3l-31.8,-10.4l1,-4.3l0,-4.3l22.6,-7.7c15,-4.9 24,-8 24.6,-8.5c0.7,-0.6 0.9,-1.1 0.9,-2.2c-0,-2 -1.2,-3.3 -3.1,-3.3c-0.9,-0 -10.5,2.9 -24.7,7.5c-12.8,4.1 -23.4,7.5 -23.6,7.5c-0.1,-0 -0.7,-0.8 -1.3,-1.9c-0.6,-1 -1.6,-2.5 -2.2,-3.2c-0.7,-0.7 -1.2,-1.5 -1.2,-1.6c0,-0.2 9.6,-13.5 21.4,-29.6c18.9,-26 21.6,-29.6 23.6,-31.1c5.7,-4.4 13.1,-5.8 19.7,-3.9c9,2.7 16.1,11.6 16.1,20.3c0,2.3 -0.1,2.3 3.1,1.5c4.7,-1.1 11.5,-0.5 16,1.5c4.6,2 9,6 11.5,10.2c2.1,3.6 3.9,9.4 4.2,13.2c0.3,5.2 -1.1,10.7 -4,15.3c-2.6,4.1 -7.8,8.3 -12.1,9.8c-0.9,0.3 -1.7,0.8 -1.7,1c0,0.2 0.4,1 0.9,1.7c2.4,3.6 3.6,7.7 3.5,12.7c0,5.8 -2.1,10.7 -6.4,15.1c-4,4.1 -8.9,6.3 -14.9,6.5c-3.3,0.4 -4.3,0.3 -7.1,-0.5Z" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#f2f5fb"
|
|
||||||
android:fillType="nonZero"
|
|
||||||
android:pathData="M643.7,671.9c-6.1,-1.6 -11.4,-6.8 -13.2,-12.9c-0.7,-2.4 -0.7,-7.5 0,-9.9c1.7,-5.8 6.6,-10.8 12.3,-12.5c2.7,-0.8 7.2,-0.9 10,-0.2c6.2,1.6 11.6,7.1 13.2,13.3c1.6,6 -0.3,12.6 -5,17.3c-4.6,4.6 -11.3,6.5 -17.3,4.9Z" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#de7fb3"
|
|
||||||
android:fillType="nonZero"
|
|
||||||
android:pathData="M615.8,602.8c-13.3,-18.3 -21.2,-29.6 -22,-31.1c-1.4,-3 -1.9,-5.5 -1.9,-9.4c0,-14.1 13.1,-24.4 27.1,-21.4c1.4,0.3 2.6,0.5 2.7,0.5c0.1,0 0.3,-1.3 0.4,-2.8c0.8,-10.7 8.4,-19.6 18.9,-22.4c3.9,-1 10.6,-1 14.5,-0c8.9,2.3 15.9,9.3 18.2,18.2c0.4,1.5 0.7,3.7 0.7,4.9c-0,1.2 0.1,2.1 0.3,2.1c0.2,-0 1.5,-0.3 3,-0.6c7.4,-1.6 15.2,0.7 20.5,6c4.3,4.3 6.6,9.6 6.6,15.6c-0,4 -0.6,6.5 -2.4,10c-0.6,1.2 -10.4,15 -21.7,30.7c-17.8,24.5 -20.8,28.5 -21.4,28.3c-0.4,-0.1 -1.9,-0.6 -3.4,-1.1c-1.5,-0.5 -2.9,-0.9 -3.3,-0.9c-0.7,-0 -0.7,-0.8 -0.3,-25.5l-0,-25.5l-1.4,-0.9c-1,-1.1 -2.5,-1.5 -3.8,-0.9c-2,0.8 -2,-0.5 -1.8,27.2l-0,25.8l-1.2,-0c-0.5,-0.2 -2.4,0.3 -4,0.9c-1.6,0.6 -3.1,1.1 -3.2,1.1c-0.2,-0.1 -9.6,-13 -21.1,-28.8Z" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#ffb800"
|
|
||||||
android:fillType="nonZero"
|
|
||||||
android:pathData="M578.4,537.8c-4.1,-0.9 -7.7,-3.6 -9.6,-7.4c-1.4,-2.8 -1.7,-7.3 -0.5,-10.3c1.7,-4.5 3.9,-6.1 15.6,-11.2c15.8,-7 31.4,-11.1 49.2,-12.9c7.3,-0.8 23.2,-0.8 30.6,0c17.4,1.8 33.3,6 49.1,13c7.3,3.2 12.5,6.1 13.6,7.5c4.3,5.6 3.8,12.7 -1.1,17.6c-5.1,5.1 -12.9,5.4 -18.1,0.7c-2,-1.8 -3,-3.5 -3.4,-5.6c-0.7,-4 2.9,-8.1 7.3,-8.2c1.4,0 1.5,-0.1 1.1,-0.5c-0.3,-0.3 -2.2,-1.2 -4.3,-2.1c-33.2,-14.5 -70.5,-16.4 -105,-5.4c-7.5,2.4 -19,7.2 -18.6,7.7c0.1,0.2 0.8,0.3 1.6,0.3c5.6,0 9.1,6.2 6.1,10.8c-2.9,4.5 -8.6,7.1 -13.6,6Z" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#e64132"
|
|
||||||
android:fillType="nonZero"
|
|
||||||
android:pathData="M542.2,496.4c-8.9,-13.1 -16.8,-25.1 -17.5,-26.6c-1.6,-3.3 -3.6,-9.2 -4.4,-13c-2.6,-12.5 -0.9,-25.8 5,-37.5c4.2,-8.3 11.2,-16.3 18.6,-21.3c5,-3.4 6.1,-3.9 12.8,-6.3c23.1,-8.2 47.2,-13.1 73.4,-15c7.5,-0.6 28.5,-0.6 36.3,-0c25.5,1.8 50.6,6.9 73,14.8c6.4,2.2 8.2,3.1 13.1,6.5c9.8,6.6 18.1,17.5 22,29.2c2.2,6.5 2.7,10 2.7,17.9c0,7.9 -0.5,11.3 -2.7,17.9c-2.3,6.8 -3.7,9.1 -20.3,33.6l-16.1,23.8l-0.4,-2.2c-0.2,-1.2 -0.9,-3 -1.4,-4c-1,-1.8 -4.4,-5.6 -4.7,-5.2c-0.1,0.1 -1.2,-0.4 -2.4,-1.1c-9.1,-5.2 -21.9,-10.5 -33.2,-13.9c-37,-11 -77.2,-8.8 -113,6.1c-4.9,2.1 -17.7,8.4 -19.2,9.5c-2.2,1.6 -5.1,6.8 -5.1,9c0,0.4 -0.1,1 -0.3,1.2c0.1,0.2 -6.2,-8.8 -16.2,-23.4Z" />
|
|
||||||
</vector>
|
</vector>
|
||||||
|
@ -1,70 +1,27 @@
|
|||||||
<vector android:height="200dp"
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
android:viewportHeight="1300"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:viewportWidth="1300"
|
android:width="108dp"
|
||||||
android:width="200dp"
|
android:height="108dp"
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
android:viewportWidth="108"
|
||||||
|
android:viewportHeight="108">
|
||||||
<path
|
<path
|
||||||
android:fillColor="#fff"
|
android:fillColor="#fff"
|
||||||
android:fillType="nonZero"
|
android:fillType="nonZero"
|
||||||
android:pathData="M578,922.6c-2.2,-0.2 -5.5,-1 -9.7,-2.2c-52.4,-15.7 -99,-46.5 -133.8,-88.5c-8.8,-10.7 -17.2,-22.4 -19.4,-27.5c-8.1,-18.1 -6.3,-38.7 4.8,-55.4c5,-7.5 13.2,-15 20.5,-18.7c1.2,-0.6 54.1,-20 55.8,-20.4c0.5,-0.1 0.5,0.2 -0.3,2.1c-0.7,1.7 -1,3.1 -1.1,5.5l-0.1,3.2l2.8,5.8c8.7,17.9 19.2,32.7 33.2,46.4c6.3,6.2 7.8,7.6 13.8,12.3c22.7,18.1 52,30.7 79.9,34.3c2.5,0.3 5,0.8 5.7,1c2.8,0.9 7.7,-0.8 11,-3.7l1.8,-1.6l-0.2,4.8c-0.1,2.7 -0.6,15.4 -1,28.3c-0.6,20.3 -0.8,24 -1.5,27.5c-3.9,20.7 -18.6,37.5 -38.4,44.1c-4.6,1.5 -8,2.2 -13.1,2.7c-4.6,0.5 -5.9,0.4 -10.7,0Z" />
|
android:pathData="m52.25,43.55c3.4,3.08 6.15,6.39 7.91,9.5 3.03,-5.55 5.06,-12.14 5.08,-16.34 0,-0.03 0,-0.06 0,-0.08 0,-6.21 -6.06,-8.63 -11.28,-8.63 -5.22,0 -11.28,2.42 -11.28,8.63 0,0.08 0,0.2 0,0.34 2.91,1.32 6.36,3.69 9.56,6.59z" />
|
||||||
<path
|
<path
|
||||||
android:fillColor="#fff"
|
android:fillColor="#fff"
|
||||||
android:fillType="nonZero"
|
android:fillType="nonZero"
|
||||||
android:pathData="M707.3,922.4c-4,-0.4 -9.4,-1.6 -13.2,-2.9c-3.4,-1.2 -10,-4.4 -12.5,-6.1c-10.9,-7.4 -19,-17.9 -23.1,-30c-2.2,-6.7 -2.3,-7.5 -3.3,-36.9c-0.5,-14.9 -0.9,-27.9 -0.9,-28.9l-0,-1.9l2.3,1.8c2.6,2 6.6,3.4 8.5,3.1c0.6,-0.1 3,-0.5 5.3,-0.8c37.7,-5.3 71.2,-22.2 97.4,-49.1c12.2,-12.5 21.4,-25.5 29.9,-42.4l3.5,-7l0,-3.6c0,-3.1 -0.1,-3.8 -1,-5.7c-0.5,-1.2 -0.9,-2.1 -0.9,-2.2c0.2,-0.2 55.3,20.1 56.9,20.9c2.6,1.3 6.6,4.1 9.9,7c9.2,7.7 16.1,19.4 18.8,31.8c0.7,3.1 0.8,4.8 0.8,11.3c0,8.6 -0.5,11.7 -2.9,18.7c-1.7,5 -2.9,7.2 -7.1,13.1c-7.6,11 -15.3,20.5 -25.2,31.2c-32.8,35.4 -76.5,62.5 -123.4,76.3c-8,2.5 -12.4,3 -19.8,2.3Z" />
|
android:pathData="m33.68,60.5c2.13,-2.42 5.39,-5.05 9.08,-7.27 3.92,-2.36 7.84,-4.01 11.28,-4.76 -4.22,-4.67 -9.72,-8.67 -13.62,-10 -0.03,-0.01 -0.05,-0.02 -0.08,-0.03 -5.78,-1.92 -9.9,3.23 -11.51,8.31 -1.61,5.08 -1.24,11.72 4.54,13.64 0.08,0.03 0.18,0.06 0.31,0.1z" />
|
||||||
<path
|
<path
|
||||||
android:fillColor="#fff"
|
android:fillColor="#fff"
|
||||||
android:fillType="nonZero"
|
android:fillType="nonZero"
|
||||||
android:pathData="M623.1,811c-25.9,-4.2 -50.7,-14.9 -71.7,-31c-5.2,-4 -8.7,-7.1 -14.1,-12.4c-12.7,-12.5 -21.9,-24.9 -30.5,-41.4c-2.3,-4.4 -2.4,-4.7 -2.4,-7.1c0,-8.8 8.5,-15.2 16.9,-12.7c5.6,1.7 9.6,6.8 9.7,12.2c0,2.6 -0.8,4.6 -2.6,6.2c-1.2,1.1 -3.2,1.9 -4.6,1.9c-1.2,0 -3.3,-0.8 -4.3,-1.6c-2.1,-1.8 -2,-1 0.4,3.2c19.3,33.8 52.3,59.1 90,69.1c5.7,1.5 11.5,2.7 11.8,2.4c0.1,-0.1 -0.4,-0.8 -1.3,-1.6c-5.1,-4.5 -2.3,-11.7 5,-12.8c5.4,-0.8 11.4,2.7 13.9,8c0.8,1.7 1,2.5 1,5.3c0,2.8 -0.1,3.5 -1,5.3c-2,4.3 -6.8,7.9 -10.3,7.8c-0.9,-0.1 -3.6,-0.5 -5.9,-0.8Z" />
|
android:pathData="M79.17,46.67C77.56,41.59 73.44,36.44 67.66,38.36c-0.08,0.03 -0.19,0.06 -0.31,0.1 -0.33,3.24 -1.46,7.33 -3.17,11.34 -1.81,4.27 -4.04,7.96 -6.39,10.64 6.1,1.24 12.85,1.17 16.76,-0.1 0.03,-0.01 0.05,-0.02 0.08,-0.03 5.78,-1.92 6.15,-8.56 4.54,-13.64z" />
|
||||||
<path
|
<path
|
||||||
android:fillColor="#fff"
|
android:fillColor="#fff"
|
||||||
android:fillType="nonZero"
|
android:fillType="nonZero"
|
||||||
android:pathData="M665.1,811.2c-3.4,-1.3 -6.4,-4.3 -7.8,-8.1c-1.1,-2.9 -0.9,-7.3 0.5,-10.2c2.6,-5.3 8.7,-8.5 14.4,-7.5c2.9,0.5 4.7,1.9 6,4.3c0.8,1.6 1,2.2 0.8,3.6c-0.3,2.2 -0.9,3.3 -2.7,4.8c-0.8,0.7 -1.4,1.4 -1.3,1.5c0.5,0.5 13.4,-2.7 21.3,-5.4c33.6,-11.3 62.5,-35.1 80.4,-66.1c2.5,-4.4 2.6,-5 0.5,-3.2c-2.8,2.4 -7,1.9 -9.6,-1c-4,-4.6 -0.7,-13.8 6.1,-16.9c2,-0.9 2.7,-1 5.5,-1c2.9,0 3.5,0.1 5.6,1.1c4.4,2.1 7.4,6.4 7.8,11c0.2,2.2 0.1,2.3 -2.2,6.9c-23,45.9 -67,78.1 -117.2,85.9c-5.5,0.9 -6.3,1 -8.1,0.3Z" />
|
android:pathData="m48.82,65.46c-0.98,-4.54 -1.3,-8.87 -0.94,-12.45 -5.64,2.67 -11.07,6.78 -13.5,10.16 -0.02,0.02 -0.03,0.05 -0.05,0.07 -3.57,5.03 -0.06,10.63 4.16,13.77 4.22,3.14 10.51,4.83 14.09,-0.2 0.05,-0.07 0.11,-0.16 0.19,-0.27 -1.59,-2.82 -3.03,-6.81 -3.95,-11.08z" />
|
||||||
<path
|
<path
|
||||||
android:fillColor="#fff"
|
android:fillColor="#fff"
|
||||||
android:fillType="nonZero"
|
android:fillType="nonZero"
|
||||||
android:pathData="M578.6,771.5c-4.7,-0.9 -8.7,-2.7 -12.9,-5.9c-10.8,-8.1 -13.5,-22.3 -6.6,-33.7c0.7,-1.2 1.1,-2.2 1,-2.4c-0.2,-0.2 -1.2,-0.6 -2.3,-1.1c-7.6,-3 -13,-10.6 -13.5,-19.1c-0.5,-7.4 3.1,-15 9,-19.4c1,-0.7 2.2,-1.5 2.6,-1.8c0.8,-0.4 68.9,-22.7 69.4,-22.7c0.2,0 0.7,0.7 1.2,1.5c0.5,0.8 1.6,2.3 2.4,3.3c1.2,1.4 1.5,1.9 1.2,2.3c-0.2,0.3 -6.9,9.5 -14.8,20.5c-15.9,21.9 -15.5,21.3 -13.4,23.4c1.3,1.3 2.9,1.4 4.4,0.3c0.6,-0.4 7.5,-9.7 15.5,-20.7c11.2,-15.4 14.6,-19.9 15,-19.7c0.9,0.4 5.5,1.9 6.6,2.1l1,0.2l-0,35.3c-0,39.7 -0,38.8 -2.5,44c-2.6,5.3 -7.2,9.3 -12.7,11.2c-3.7,1.3 -6.8,1.6 -10.2,1c-5.5,-0.9 -9.8,-3.2 -13.7,-7.4l-2.2,-2.4l-0.6,0.9c-3,4.3 -8.6,8.1 -14,9.5c-2.8,0.9 -7.8,1.2 -9.9,0.8Z" />
|
android:pathData="m73.58,62.92c-3.11,0.68 -7.26,0.84 -11.52,0.42 -4.53,-0.45 -8.64,-1.47 -11.86,-2.93 0.73,6.31 2.88,12.87 5.28,16.28 0.02,0.02 0.03,0.05 0.05,0.07 3.57,5.03 9.86,3.34 14.09,0.2 4.22,-3.14 7.74,-8.74 4.16,-13.77 -0.05,-0.07 -0.11,-0.16 -0.19,-0.27z" />
|
||||||
<path
|
|
||||||
android:fillColor="#fff"
|
|
||||||
android:fillType="nonZero"
|
|
||||||
android:pathData="M710.4,771.5c-5.5,-0.9 -9.9,-3.2 -14.3,-7.6l-3.2,-3.2l-0.7,1c-2.3,3.3 -6.8,6.5 -11.1,7.9c-3.7,1.2 -9.2,1.4 -12.6,0.3c-7.1,-2.1 -12.7,-7.4 -15.2,-14.3l-0.9,-2.6l0,-74.2l1.8,-0.4c1,-0.2 2.7,-0.8 3.9,-1.2c1.1,-0.5 2.1,-0.8 2.2,-0.7c0.1,0.1 6.5,9 14.4,19.9c7.8,10.9 14.7,20.1 15.2,20.5c2.2,1.9 5.4,0.4 5.4,-2.6c0,-1.4 -1,-2.9 -13.8,-20.5c-7.6,-10.5 -14.2,-19.6 -14.7,-20.4l-0.9,-1.3l1.4,-1.7c0.8,-0.9 1.9,-2.5 2.5,-3.4l1,-1.6l34.4,11.2c18.9,6.2 35.1,11.6 35.9,12.1c6.8,4 11.1,11.3 11.1,19.1c0,4.1 -0.5,6.4 -2.4,10.2c-2,4.1 -5.5,7.6 -9.6,9.7c-1.6,0.8 -3.2,1.5 -3.4,1.5c-1,0 -0.9,0.7 0.3,2.6c2.8,4.3 4,8.5 3.9,13.7c0,8.1 -3.7,15.2 -10.6,20.3c-6.5,4.8 -13.4,6.7 -20,5.7Z" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#fff"
|
|
||||||
android:fillType="nonZero"
|
|
||||||
android:pathData="M421.4,714.9c-0.5,-0.1 -2.3,-0.4 -3.9,-0.7c-15.6,-2.6 -30.4,-12.6 -38.8,-26.2c-3.5,-5.7 -6.4,-13.2 -7.8,-19.9c-1.2,-6.1 -0.8,-28.1 0.8,-43.1c4.5,-43 19,-84.3 42.2,-120.7c6.5,-10.2 14.9,-21.5 18.2,-24.6c17.8,-16.6 43.1,-20.5 64.8,-10c4.3,2.1 8.8,5.1 12.7,8.6c2.8,2.4 5.8,6.1 20.9,25.5c9.7,12.5 17.8,22.8 17.9,23c0.2,0.2 -0.9,0.4 -3.2,0.4c-2.5,0 -4.1,0.2 -5.7,0.7c-2.1,0.7 -2.6,1.1 -7.9,6.3c-8.2,8.1 -14.4,15.3 -20.3,23.9c-15.5,22.2 -25.4,47.7 -28.8,74.8c-2.2,16.9 -1.6,37.5 1.6,52.3c0.3,1.4 0.5,2.8 0.4,3c-0.1,0.2 0.2,1.3 0.8,2.4c1.1,2.4 4.3,5.7 6.5,6.8l1.5,0.8l-1.2,0.4c-0.7,0.2 -13.1,3.8 -27.6,8c-16.4,4.7 -27.7,7.8 -29.8,8.1c-3.1,0.4 -11.1,0.6 -13.3,0.2Z" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#fff"
|
|
||||||
android:fillType="nonZero"
|
|
||||||
android:pathData="M862.2,714.7c-2.1,-0.3 -33.8,-9.1 -56.5,-15.8l-2.5,-0.7l1.6,-0.8c3.4,-1.7 7.2,-6.6 7.3,-9.6c0,-0.7 0.4,-3.3 0.8,-5.8c3.9,-22.7 3.1,-46.1 -2.5,-68.4c-6.4,-25.5 -18.6,-49.2 -35.8,-69.1c-4.6,-5.3 -14.8,-15.4 -16.4,-16.1c-2.4,-1.1 -5.1,-1.6 -8,-1.4l-2.7,0.2l1.2,-1.5c0.7,-0.8 8.5,-10.8 17.5,-22.3c8.9,-11.5 17.2,-21.8 18.5,-23.1c2.6,-2.7 7,-6.2 10.3,-8.2c19.3,-11.6 43,-11.1 61.6,1.2c5.4,3.6 8.2,6.2 12.3,11.7c26.4,34.5 44,73.7 52.3,116.2c3.4,17.6 4.9,33.3 5,52.4c0,13 -0.2,14.8 -2.5,21.8c-8.4,26.2 -34.5,42.8 -61.5,39.3Z" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#fff"
|
|
||||||
android:fillType="nonZero"
|
|
||||||
android:pathData="M501.4,691.5c-2,-0.5 -4.6,-1.9 -6,-3.3c-2.5,-2.4 -3.1,-3.5 -3.7,-7.3c-4.4,-27.3 -2.2,-54 6.7,-79.3c5.3,-15.1 13.5,-30.5 23,-43.1c5.8,-7.8 16.6,-19.5 19,-20.7c4.7,-2.4 11.3,-1.2 15.2,2.7c5.4,5.4 5.2,13.9 -0.3,19.1c-4.3,4 -9.4,4.4 -12.6,0.9c-1.7,-1.9 -2.2,-3.9 -1.7,-6.4c0.2,-1.1 0.3,-2 0.2,-2.2c-0.3,-0.3 -3.6,3.3 -8.3,9.1c-17.6,21.8 -28.5,48 -31.9,76.5c-1.1,9.3 -1,26.4 0.1,34.6c0.3,1.8 0.8,1.9 1.4,0.1c0.9,-2.6 4,-4.7 6.8,-4.7c3,-0 5.9,2.2 7.5,5.7c0.6,1.3 0.8,2.3 0.8,5.2c-0,3.3 -0.1,3.8 -1.1,5.7c-1.4,2.7 -4.6,5.7 -7.1,6.6c-2.5,0.9 -6.1,1.2 -8,0.8Z" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#fff"
|
|
||||||
android:fillType="nonZero"
|
|
||||||
android:pathData="M790.1,691.5c-3.7,-0.6 -7.7,-3.6 -9.4,-7.1c-3.8,-7.5 0.1,-16.9 6.9,-16.9c3.1,0 5.8,2 6.9,5.2c0.4,1.2 0.5,1.3 0.7,0.7c1.3,-3.7 1.7,-26.4 0.6,-35.7c-3.6,-29.6 -14.5,-55.3 -33,-77.9c-5.5,-6.7 -8.4,-9.4 -7.1,-6.6c0.7,1.4 0.5,4.3 -0.3,5.9c-0.9,1.7 -3.2,3.5 -5,3.8c-3.2,0.6 -7.9,-1.6 -10.2,-4.8c-6.5,-8.8 -0.5,-21.2 10.4,-21.4c4.6,-0.1 5.2,0.3 11.2,6.4c12.1,12.3 21.1,24.9 28.8,40.3c13.2,26.3 18.6,54.9 16.1,84.5c-0.5,5.6 -2,15.7 -2.6,17.1c-1.3,2.8 -4.8,5.5 -8.4,6.5c-2.3,0.4 -3.1,0.4 -5.6,0Z" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#fff"
|
|
||||||
android:fillType="nonZero"
|
|
||||||
android:pathData="M545.7,680.2c-6,-1.3 -12.2,-6.2 -14.9,-11.7c-3.4,-7 -3.1,-15.1 0.9,-21.6c0.7,-1.2 1.2,-2.3 1.1,-2.4c-0.1,-0.1 -1.1,-0.6 -2.1,-1c-3.9,-1.5 -8.1,-4.8 -10.7,-8.3c-4.6,-6.2 -6.1,-14.6 -3.9,-22.1c2.9,-10.3 9.4,-16.8 19.1,-19.3c2.8,-0.7 9,-0.8 11.7,-0c1.1,0.3 2.2,0.5 2.4,0.5c0.2,-0 0.3,-0.7 0.3,-1.5c0,-2.9 0.8,-5.8 2.4,-9.2c5.2,-10.8 18.1,-15.5 29,-10.5c2.7,1.2 6.2,3.8 7.8,5.8c0.7,0.8 10.3,14 21.5,29.4l20.3,27.9l-1.5,1.8c-0.8,1 -1.9,2.6 -2.5,3.5c-0.6,1 -1.2,1.7 -1.5,1.6c-4.5,-1.7 -46.7,-15 -47.7,-15c-1.9,0 -3.1,1.3 -3.1,3.2c0,1 0.2,1.7 0.8,2.3c0.6,0.6 7.8,3.1 24.5,8.5l23.7,7.7l-0.2,8.6l-32.6,10.5c-18,5.9 -33.9,10.9 -35.2,11.2c-3.1,0.7 -6.6,0.7 -9.6,0.1Z" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#fff"
|
|
||||||
android:fillType="nonZero"
|
|
||||||
android:pathData="M740,679.8c-1.8,-0.5 -17.5,-5.6 -35,-11.3l-31.8,-10.4l1,-4.3l0,-4.3l22.6,-7.7c15,-4.9 24,-8 24.6,-8.5c0.7,-0.6 0.9,-1.1 0.9,-2.2c-0,-2 -1.2,-3.3 -3.1,-3.3c-0.9,-0 -10.5,2.9 -24.7,7.5c-12.8,4.1 -23.4,7.5 -23.6,7.5c-0.1,-0 -0.7,-0.8 -1.3,-1.9c-0.6,-1 -1.6,-2.5 -2.2,-3.2c-0.7,-0.7 -1.2,-1.5 -1.2,-1.6c0,-0.2 9.6,-13.5 21.4,-29.6c18.9,-26 21.6,-29.6 23.6,-31.1c5.7,-4.4 13.1,-5.8 19.7,-3.9c9,2.7 16.1,11.6 16.1,20.3c0,2.3 -0.1,2.3 3.1,1.5c4.7,-1.1 11.5,-0.5 16,1.5c4.6,2 9,6 11.5,10.2c2.1,3.6 3.9,9.4 4.2,13.2c0.3,5.2 -1.1,10.7 -4,15.3c-2.6,4.1 -7.8,8.3 -12.1,9.8c-0.9,0.3 -1.7,0.8 -1.7,1c0,0.2 0.4,1 0.9,1.7c2.4,3.6 3.6,7.7 3.5,12.7c0,5.8 -2.1,10.7 -6.4,15.1c-4,4.1 -8.9,6.3 -14.9,6.5c-3.3,0.4 -4.3,0.3 -7.1,-0.5Z" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#fff"
|
|
||||||
android:fillType="nonZero"
|
|
||||||
android:pathData="M643.7,671.9c-6.1,-1.6 -11.4,-6.8 -13.2,-12.9c-0.7,-2.4 -0.7,-7.5 0,-9.9c1.7,-5.8 6.6,-10.8 12.3,-12.5c2.7,-0.8 7.2,-0.9 10,-0.2c6.2,1.6 11.6,7.1 13.2,13.3c1.6,6 -0.3,12.6 -5,17.3c-4.6,4.6 -11.3,6.5 -17.3,4.9Z" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#fff"
|
|
||||||
android:fillType="nonZero"
|
|
||||||
android:pathData="M615.8,602.8c-13.3,-18.3 -21.2,-29.6 -22,-31.1c-1.4,-3 -1.9,-5.5 -1.9,-9.4c0,-14.1 13.1,-24.4 27.1,-21.4c1.4,0.3 2.6,0.5 2.7,0.5c0.1,0 0.3,-1.3 0.4,-2.8c0.8,-10.7 8.4,-19.6 18.9,-22.4c3.9,-1 10.6,-1 14.5,-0c8.9,2.3 15.9,9.3 18.2,18.2c0.4,1.5 0.7,3.7 0.7,4.9c-0,1.2 0.1,2.1 0.3,2.1c0.2,-0 1.5,-0.3 3,-0.6c7.4,-1.6 15.2,0.7 20.5,6c4.3,4.3 6.6,9.6 6.6,15.6c-0,4 -0.6,6.5 -2.4,10c-0.6,1.2 -10.4,15 -21.7,30.7c-17.8,24.5 -20.8,28.5 -21.4,28.3c-0.4,-0.1 -1.9,-0.6 -3.4,-1.1c-1.5,-0.5 -2.9,-0.9 -3.3,-0.9c-0.7,-0 -0.7,-0.8 -0.3,-25.5l-0,-25.5l-1.4,-0.9c-1,-1.1 -2.5,-1.5 -3.8,-0.9c-2,0.8 -2,-0.5 -1.8,27.2l-0,25.8l-1.2,-0c-0.5,-0.2 -2.4,0.3 -4,0.9c-1.6,0.6 -3.1,1.1 -3.2,1.1c-0.2,-0.1 -9.6,-13 -21.1,-28.8Z" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#fff"
|
|
||||||
android:fillType="nonZero"
|
|
||||||
android:pathData="M578.4,537.8c-4.1,-0.9 -7.7,-3.6 -9.6,-7.4c-1.4,-2.8 -1.7,-7.3 -0.5,-10.3c1.7,-4.5 3.9,-6.1 15.6,-11.2c15.8,-7 31.4,-11.1 49.2,-12.9c7.3,-0.8 23.2,-0.8 30.6,0c17.4,1.8 33.3,6 49.1,13c7.3,3.2 12.5,6.1 13.6,7.5c4.3,5.6 3.8,12.7 -1.1,17.6c-5.1,5.1 -12.9,5.4 -18.1,0.7c-2,-1.8 -3,-3.5 -3.4,-5.6c-0.7,-4 2.9,-8.1 7.3,-8.2c1.4,0 1.5,-0.1 1.1,-0.5c-0.3,-0.3 -2.2,-1.2 -4.3,-2.1c-33.2,-14.5 -70.5,-16.4 -105,-5.4c-7.5,2.4 -19,7.2 -18.6,7.7c0.1,0.2 0.8,0.3 1.6,0.3c5.6,0 9.1,6.2 6.1,10.8c-2.9,4.5 -8.6,7.1 -13.6,6Z" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#fff"
|
|
||||||
android:fillType="nonZero"
|
|
||||||
android:pathData="M542.2,496.4c-8.9,-13.1 -16.8,-25.1 -17.5,-26.6c-1.6,-3.3 -3.6,-9.2 -4.4,-13c-2.6,-12.5 -0.9,-25.8 5,-37.5c4.2,-8.3 11.2,-16.3 18.6,-21.3c5,-3.4 6.1,-3.9 12.8,-6.3c23.1,-8.2 47.2,-13.1 73.4,-15c7.5,-0.6 28.5,-0.6 36.3,-0c25.5,1.8 50.6,6.9 73,14.8c6.4,2.2 8.2,3.1 13.1,6.5c9.8,6.6 18.1,17.5 22,29.2c2.2,6.5 2.7,10 2.7,17.9c0,7.9 -0.5,11.3 -2.7,17.9c-2.3,6.8 -3.7,9.1 -20.3,33.6l-16.1,23.8l-0.4,-2.2c-0.2,-1.2 -0.9,-3 -1.4,-4c-1,-1.8 -4.4,-5.6 -4.7,-5.2c-0.1,0.1 -1.2,-0.4 -2.4,-1.1c-9.1,-5.2 -21.9,-10.5 -33.2,-13.9c-37,-11 -77.2,-8.8 -113,6.1c-4.9,2.1 -17.7,8.4 -19.2,9.5c-2.2,1.6 -5.1,6.8 -5.1,9c0,0.4 -0.1,1 -0.3,1.2c0.1,0.2 -6.2,-8.8 -16.2,-23.4Z" />
|
|
||||||
</vector>
|
</vector>
|
||||||
|
@ -2,4 +2,5 @@
|
|||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<background android:drawable="@color/ic_launcher_background"/>
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||||
|
<monochrome android:drawable="@drawable/ic_launcher_monochrome"/>
|
||||||
</adaptive-icon>
|
</adaptive-icon>
|
||||||
|
@ -17,9 +17,6 @@ PODS:
|
|||||||
- fluttertoast (0.0.2):
|
- fluttertoast (0.0.2):
|
||||||
- Flutter
|
- Flutter
|
||||||
- Toast
|
- Toast
|
||||||
- FMDB (2.7.5):
|
|
||||||
- FMDB/standard (= 2.7.5)
|
|
||||||
- FMDB/standard (2.7.5)
|
|
||||||
- geolocator_apple (1.2.0):
|
- geolocator_apple (1.2.0):
|
||||||
- Flutter
|
- Flutter
|
||||||
- image_picker_ios (0.0.1):
|
- image_picker_ios (0.0.1):
|
||||||
@ -39,7 +36,7 @@ PODS:
|
|||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- path_provider_ios (0.0.1):
|
- path_provider_ios (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- permission_handler_apple (9.1.1):
|
- permission_handler_apple (9.3.0):
|
||||||
- Flutter
|
- Flutter
|
||||||
- photo_manager (2.0.0):
|
- photo_manager (2.0.0):
|
||||||
- Flutter
|
- Flutter
|
||||||
@ -53,7 +50,7 @@ PODS:
|
|||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- sqflite (0.0.3):
|
- sqflite (0.0.3):
|
||||||
- Flutter
|
- Flutter
|
||||||
- FMDB (>= 2.7.5)
|
- FlutterMacOS
|
||||||
- Toast (4.0.0)
|
- Toast (4.0.0)
|
||||||
- url_launcher_ios (0.0.1):
|
- url_launcher_ios (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
@ -84,14 +81,13 @@ DEPENDENCIES:
|
|||||||
- photo_manager (from `.symlinks/plugins/photo_manager/ios`)
|
- photo_manager (from `.symlinks/plugins/photo_manager/ios`)
|
||||||
- share_plus (from `.symlinks/plugins/share_plus/ios`)
|
- share_plus (from `.symlinks/plugins/share_plus/ios`)
|
||||||
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
|
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||||
- sqflite (from `.symlinks/plugins/sqflite/ios`)
|
- sqflite (from `.symlinks/plugins/sqflite/darwin`)
|
||||||
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
||||||
- video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`)
|
- video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`)
|
||||||
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
|
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
|
||||||
|
|
||||||
SPEC REPOS:
|
SPEC REPOS:
|
||||||
trunk:
|
trunk:
|
||||||
- FMDB
|
|
||||||
- MapLibre
|
- MapLibre
|
||||||
- ReachabilitySwift
|
- ReachabilitySwift
|
||||||
- SAMKeychain
|
- SAMKeychain
|
||||||
@ -139,7 +135,7 @@ EXTERNAL SOURCES:
|
|||||||
shared_preferences_foundation:
|
shared_preferences_foundation:
|
||||||
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
|
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
|
||||||
sqflite:
|
sqflite:
|
||||||
:path: ".symlinks/plugins/sqflite/ios"
|
:path: ".symlinks/plugins/sqflite/darwin"
|
||||||
url_launcher_ios:
|
url_launcher_ios:
|
||||||
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
||||||
video_player_avfoundation:
|
video_player_avfoundation:
|
||||||
@ -155,24 +151,23 @@ SPEC CHECKSUMS:
|
|||||||
flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef
|
flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef
|
||||||
flutter_udid: a2482c67a61b9c806ef59dd82ed8d007f1b7ac04
|
flutter_udid: a2482c67a61b9c806ef59dd82ed8d007f1b7ac04
|
||||||
flutter_web_auth: c25208760459cec375a3c39f6a8759165ca0fa4d
|
flutter_web_auth: c25208760459cec375a3c39f6a8759165ca0fa4d
|
||||||
fluttertoast: fafc4fa4d01a6a9e4f772ecd190ffa525e9e2d9c
|
fluttertoast: 31b00dabfa7fb7bacd9e7dbee580d7a2ff4bf265
|
||||||
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
|
|
||||||
geolocator_apple: 9157311f654584b9bb72686c55fc02a97b73f461
|
geolocator_apple: 9157311f654584b9bb72686c55fc02a97b73f461
|
||||||
image_picker_ios: 4a8aadfbb6dc30ad5141a2ce3832af9214a705b5
|
image_picker_ios: 99dfe1854b4fa34d0364e74a78448a0151025425
|
||||||
integration_test: 13825b8a9334a850581300559b8839134b124670
|
integration_test: 13825b8a9334a850581300559b8839134b124670
|
||||||
isar_flutter_libs: b69f437aeab9c521821c3f376198c4371fa21073
|
isar_flutter_libs: b69f437aeab9c521821c3f376198c4371fa21073
|
||||||
MapLibre: 620fc933c1d6029b33738c905c1490d024e5d4ef
|
MapLibre: 620fc933c1d6029b33738c905c1490d024e5d4ef
|
||||||
maplibre_gl: a2efec727dd340e4c65e26d2b03b584f14881fd9
|
maplibre_gl: a2efec727dd340e4c65e26d2b03b584f14881fd9
|
||||||
package_info_plus: 115f4ad11e0698c8c1c5d8a689390df880f47e85
|
package_info_plus: 115f4ad11e0698c8c1c5d8a689390df880f47e85
|
||||||
path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943
|
path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c
|
||||||
path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02
|
path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02
|
||||||
permission_handler_apple: e76247795d700c14ea09e3a2d8855d41ee80a2e6
|
permission_handler_apple: 036b856153a2b1f61f21030ff725f3e6fece2b78
|
||||||
photo_manager: 4f6810b7dfc4feb03b461ac1a70dacf91fba7604
|
photo_manager: 4f6810b7dfc4feb03b461ac1a70dacf91fba7604
|
||||||
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
|
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
|
||||||
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
|
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
|
||||||
share_plus: c3fef564749587fc939ef86ffb283ceac0baf9f5
|
share_plus: c3fef564749587fc939ef86ffb283ceac0baf9f5
|
||||||
shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126
|
shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695
|
||||||
sqflite: 31f7eba61e3074736dff8807a9b41581e4f7f15a
|
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
|
||||||
Toast: 91b396c56ee72a5790816f40d3a94dd357abc196
|
Toast: 91b396c56ee72a5790816f40d3a94dd357abc196
|
||||||
url_launcher_ios: bbd758c6e7f9fd7b5b1d4cde34d2b95fcce5e812
|
url_launcher_ios: bbd758c6e7f9fd7b5b1d4cde34d2b95fcce5e812
|
||||||
video_player_avfoundation: 02011213dab73ae3687df27ce441fbbcc82b5579
|
video_player_avfoundation: 02011213dab73ae3687df27ce441fbbcc82b5579
|
||||||
@ -180,4 +175,4 @@ SPEC CHECKSUMS:
|
|||||||
|
|
||||||
PODFILE CHECKSUM: 64c9b5291666c0ca3caabdfe9865c141ac40321d
|
PODFILE CHECKSUM: 64c9b5291666c0ca3caabdfe9865c141ac40321d
|
||||||
|
|
||||||
COCOAPODS: 1.12.1
|
COCOAPODS: 1.15.2
|
||||||
|
@ -172,7 +172,7 @@
|
|||||||
isa = PBXProject;
|
isa = PBXProject;
|
||||||
attributes = {
|
attributes = {
|
||||||
BuildIndependentTargetsInParallel = YES;
|
BuildIndependentTargetsInParallel = YES;
|
||||||
LastUpgradeCheck = 1430;
|
LastUpgradeCheck = 1510;
|
||||||
ORGANIZATIONNAME = "";
|
ORGANIZATIONNAME = "";
|
||||||
TargetAttributes = {
|
TargetAttributes = {
|
||||||
97C146ED1CF9000F007C117D = {
|
97C146ED1CF9000F007C117D = {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<Scheme
|
<Scheme
|
||||||
LastUpgradeVersion = "1430"
|
LastUpgradeVersion = "1510"
|
||||||
version = "1.3">
|
version = "1.3">
|
||||||
<BuildAction
|
<BuildAction
|
||||||
parallelizeBuildables = "YES"
|
parallelizeBuildables = "YES"
|
||||||
|
@ -16,8 +16,6 @@ class ImageLoader {
|
|||||||
required ImageCacheManager cache,
|
required ImageCacheManager cache,
|
||||||
required ImageDecoderCallback decode,
|
required ImageDecoderCallback decode,
|
||||||
StreamController<ImageChunkEvent>? chunkEvents,
|
StreamController<ImageChunkEvent>? chunkEvents,
|
||||||
int? height,
|
|
||||||
int? width,
|
|
||||||
}) async {
|
}) async {
|
||||||
final headers = {
|
final headers = {
|
||||||
'x-immich-user-token': Store.get(StoreKey.accessToken),
|
'x-immich-user-token': Store.get(StoreKey.accessToken),
|
||||||
@ -25,10 +23,8 @@ class ImageLoader {
|
|||||||
|
|
||||||
final stream = cache.getImageFile(
|
final stream = cache.getImageFile(
|
||||||
uri,
|
uri,
|
||||||
withProgress: true,
|
withProgress: chunkEvents != null,
|
||||||
headers: headers,
|
headers: headers,
|
||||||
maxHeight: height,
|
|
||||||
maxWidth: width,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
await for (final result in stream) {
|
await for (final result in stream) {
|
||||||
@ -40,13 +36,9 @@ class ImageLoader {
|
|||||||
expectedTotalBytes: result.totalSize,
|
expectedTotalBytes: result.totalSize,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
} else if (result is FileInfo) {
|
||||||
|
|
||||||
if (result is FileInfo) {
|
|
||||||
// We have the file
|
// We have the file
|
||||||
final file = result.file;
|
final buffer = await ui.ImmutableBuffer.fromFilePath(result.file.path);
|
||||||
final bytes = await file.readAsBytes();
|
|
||||||
final buffer = await ui.ImmutableBuffer.fromUint8List(bytes);
|
|
||||||
final decoded = await decode(buffer);
|
final decoded = await decode(buffer);
|
||||||
return decoded;
|
return decoded;
|
||||||
}
|
}
|
||||||
|
@ -64,7 +64,7 @@ class ExifMap extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
padding: const EdgeInsets.symmetric(vertical: 16.0),
|
||||||
child: LayoutBuilder(
|
child: LayoutBuilder(
|
||||||
builder: (context, constraints) {
|
builder: (context, constraints) {
|
||||||
return MapThumbnail(
|
return MapThumbnail(
|
||||||
|
@ -26,7 +26,7 @@ class ExifPeople extends ConsumerWidget {
|
|||||||
.watch(assetPeopleNotifierProvider(asset))
|
.watch(assetPeopleNotifierProvider(asset))
|
||||||
.value
|
.value
|
||||||
?.where((p) => !p.isHidden);
|
?.where((p) => !p.isHidden);
|
||||||
final double imageSize = math.min(context.width / 3, 120);
|
final double imageSize = math.min(context.width / 3, 150);
|
||||||
|
|
||||||
showPersonNameEditModel(
|
showPersonNameEditModel(
|
||||||
String personId,
|
String personId,
|
||||||
|
@ -80,7 +80,6 @@ class _AssetDragRegionState extends State<AssetDragRegion> {
|
|||||||
recognizer.onLongPressMoveUpdate = (details) => _onLongPressMove(details);
|
recognizer.onLongPressMoveUpdate = (details) => _onLongPressMove(details);
|
||||||
recognizer.onLongPressStart = (details) => _onLongPressStart(details);
|
recognizer.onLongPressStart = (details) => _onLongPressStart(details);
|
||||||
recognizer.onLongPressUp = _onLongPressEnd;
|
recognizer.onLongPressUp = _onLongPressEnd;
|
||||||
recognizer.onLongPressCancel = _onLongPressEnd;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
AssetIndex? _getValueKeyAtPositon(Offset position) {
|
AssetIndex? _getValueKeyAtPositon(Offset position) {
|
||||||
|
@ -1,13 +1,18 @@
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/shared/models/asset.dart';
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
import 'package:immich_mobile/shared/providers/db.provider.dart';
|
import 'package:immich_mobile/shared/providers/db.provider.dart';
|
||||||
|
import 'package:immich_mobile/shared/providers/user.provider.dart';
|
||||||
import 'package:isar/isar.dart';
|
import 'package:isar/isar.dart';
|
||||||
|
|
||||||
final recentlyAddedProvider = FutureProvider<List<Asset>>((ref) async {
|
final recentlyAddedProvider = FutureProvider<List<Asset>>((ref) async {
|
||||||
|
final user = ref.read(currentUserProvider);
|
||||||
|
if (user == null) return [];
|
||||||
|
|
||||||
return ref
|
return ref
|
||||||
.watch(dbProvider)
|
.watch(dbProvider)
|
||||||
.assets
|
.assets
|
||||||
.where()
|
.where()
|
||||||
|
.ownerIdEqualToAnyChecksum(user.isarId)
|
||||||
.sortByFileCreatedAtDesc()
|
.sortByFileCreatedAtDesc()
|
||||||
.findAll();
|
.findAll();
|
||||||
});
|
});
|
||||||
|
@ -24,7 +24,7 @@ class CuratedPeopleRow extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
const imageSize = 70.0;
|
const imageSize = 60.0;
|
||||||
|
|
||||||
// Guard empty [content]
|
// Guard empty [content]
|
||||||
if (content.isEmpty) {
|
if (content.isEmpty) {
|
||||||
|
@ -3,7 +3,6 @@ description: Immich - selfhosted backup media file on mobile phone
|
|||||||
|
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
version: 1.99.0+129
|
version: 1.99.0+129
|
||||||
isar_version: &isar_version 3.1.0+1
|
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=3.0.0 <4.0.0'
|
sdk: '>=3.0.0 <4.0.0'
|
||||||
@ -37,10 +36,10 @@ dependencies:
|
|||||||
flutter_svg: ^2.0.9
|
flutter_svg: ^2.0.9
|
||||||
package_info_plus: ^5.0.1
|
package_info_plus: ^5.0.1
|
||||||
url_launcher: ^6.2.4
|
url_launcher: ^6.2.4
|
||||||
http: 0.13.5
|
http: ^0.13.6
|
||||||
cancellation_token_http: ^1.1.0
|
cancellation_token_http: ^2.0.0
|
||||||
easy_localization: ^3.0.3
|
easy_localization: ^3.0.3
|
||||||
share_plus: ^7.2.1
|
share_plus: ^7.2.2
|
||||||
flutter_displaymode: ^0.6.0
|
flutter_displaymode: ^0.6.0
|
||||||
scrollable_positioned_list: ^0.3.8
|
scrollable_positioned_list: ^0.3.8
|
||||||
path: ^1.8.3
|
path: ^1.8.3
|
||||||
@ -49,8 +48,8 @@ dependencies:
|
|||||||
http_parser: ^4.0.2
|
http_parser: ^4.0.2
|
||||||
flutter_web_auth: ^0.5.0
|
flutter_web_auth: ^0.5.0
|
||||||
easy_image_viewer: ^1.4.0
|
easy_image_viewer: ^1.4.0
|
||||||
isar: *isar_version
|
isar: ^3.1.0+1
|
||||||
isar_flutter_libs: *isar_version # contains Isar Core
|
isar_flutter_libs: ^3.1.0+1
|
||||||
permission_handler: ^11.2.0
|
permission_handler: ^11.2.0
|
||||||
device_info_plus: ^9.1.1
|
device_info_plus: ^9.1.1
|
||||||
connectivity_plus: ^5.0.2
|
connectivity_plus: ^5.0.2
|
||||||
@ -91,10 +90,10 @@ dev_dependencies:
|
|||||||
auto_route_generator: ^7.3.2
|
auto_route_generator: ^7.3.2
|
||||||
flutter_launcher_icons: ^0.13.1
|
flutter_launcher_icons: ^0.13.1
|
||||||
flutter_native_splash: ^2.3.9
|
flutter_native_splash: ^2.3.9
|
||||||
isar_generator: *isar_version
|
isar_generator: ^3.1.0+1
|
||||||
integration_test:
|
integration_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
custom_lint: ^0.5.8
|
custom_lint: ^0.6.0
|
||||||
riverpod_lint: ^2.3.7
|
riverpod_lint: ^2.3.7
|
||||||
riverpod_generator: ^2.3.9
|
riverpod_generator: ^2.3.9
|
||||||
mocktail: ^1.0.3
|
mocktail: ^1.0.3
|
||||||
|
@ -18,7 +18,7 @@
|
|||||||
</a>
|
</a>
|
||||||
<br/>
|
<br/>
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="README.md">English</a>
|
<a href="../README.md">English</a>
|
||||||
<a href="README_ca_ES.md">Español</a>
|
<a href="README_ca_ES.md">Español</a>
|
||||||
<a href="README_fr_FR.md">Français</a>
|
<a href="README_fr_FR.md">Français</a>
|
||||||
<a href="README_it_IT.md">Italiano</a>
|
<a href="README_it_IT.md">Italiano</a>
|
@ -18,7 +18,7 @@
|
|||||||
</a>
|
</a>
|
||||||
<br/>
|
<br/>
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="README.md">English</a>
|
<a href="../README.md">English</a>
|
||||||
<a href="README_ca_ES.md">Català</a>
|
<a href="README_ca_ES.md">Català</a>
|
||||||
<a href="README_es_ES.md">Español</a>
|
<a href="README_es_ES.md">Español</a>
|
||||||
<a href="README_fr_FR.md">Français</a>
|
<a href="README_fr_FR.md">Français</a>
|
@ -18,7 +18,7 @@
|
|||||||
</a>
|
</a>
|
||||||
<br/>
|
<br/>
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="README.md">English</a>
|
<a href="../README.md">English</a>
|
||||||
<a href="README_ca_ES.md">Català</a>
|
<a href="README_ca_ES.md">Català</a>
|
||||||
<a href="README_fr_FR.md">Français</a>
|
<a href="README_fr_FR.md">Français</a>
|
||||||
<a href="README_it_IT.md">Italiano</a>
|
<a href="README_it_IT.md">Italiano</a>
|
@ -18,7 +18,7 @@
|
|||||||
</a>
|
</a>
|
||||||
<br/>
|
<br/>
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="README.md">English</a>
|
<a href="../README.md">English</a>
|
||||||
<a href="README_ca_ES.md">Català</a>
|
<a href="README_ca_ES.md">Català</a>
|
||||||
<a href="README_es_ES.md">Español</a>
|
<a href="README_es_ES.md">Español</a>
|
||||||
<a href="README_it_IT.md">Italiano</a>
|
<a href="README_it_IT.md">Italiano</a>
|
@ -18,7 +18,7 @@
|
|||||||
</a>
|
</a>
|
||||||
<br/>
|
<br/>
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="README.md">English</a>
|
<a href="../README.md">English</a>
|
||||||
<a href="README_ca_ES.md">Català</a>
|
<a href="README_ca_ES.md">Català</a>
|
||||||
<a href="README_es_ES.md">Español</a>
|
<a href="README_es_ES.md">Español</a>
|
||||||
<a href="README_fr_FR.md">Français</a>
|
<a href="README_fr_FR.md">Français</a>
|
@ -18,7 +18,7 @@
|
|||||||
</a>
|
</a>
|
||||||
<br/>
|
<br/>
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="README.md">English</a>
|
<a href="../README.md">English</a>
|
||||||
<a href="README_ca_ES.md">Català</a>
|
<a href="README_ca_ES.md">Català</a>
|
||||||
<a href="README_es_ES.md">Español</a>
|
<a href="README_es_ES.md">Español</a>
|
||||||
<a href="README_fr_FR.md">Français</a>
|
<a href="README_fr_FR.md">Français</a>
|
@ -18,7 +18,7 @@
|
|||||||
</a>
|
</a>
|
||||||
<br/>
|
<br/>
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="README.md">English</a>
|
<a href="../README.md">English</a>
|
||||||
<a href="README_ca_ES.md">Català</a>
|
<a href="README_ca_ES.md">Català</a>
|
||||||
<a href="README_es_ES.md">Español</a>
|
<a href="README_es_ES.md">Español</a>
|
||||||
<a href="README_fr_FR.md">Français</a>
|
<a href="README_fr_FR.md">Français</a>
|
@ -18,7 +18,7 @@
|
|||||||
</a>
|
</a>
|
||||||
<br/>
|
<br/>
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="README.md">English</a>
|
<a href="../README.md">English</a>
|
||||||
<a href="README_ca_ES.md">Català</a>
|
<a href="README_ca_ES.md">Català</a>
|
||||||
<a href="README_es_ES.md">Español</a>
|
<a href="README_es_ES.md">Español</a>
|
||||||
<a href="README_fr_FR.md">Français</a>
|
<a href="README_fr_FR.md">Français</a>
|
@ -18,7 +18,7 @@
|
|||||||
</a>
|
</a>
|
||||||
<br/>
|
<br/>
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="README.md">English</a>
|
<a href="../README.md">English</a>
|
||||||
<a href="README_ca_ES.md">Català</a>
|
<a href="README_ca_ES.md">Català</a>
|
||||||
<a href="README_es_ES.md">Español</a>
|
<a href="README_es_ES.md">Español</a>
|
||||||
<a href="README_fr_FR.md">Français</a>
|
<a href="README_fr_FR.md">Français</a>
|
@ -22,7 +22,7 @@
|
|||||||
<br/>
|
<br/>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="README.md">English</a>
|
<a href="../README.md">English</a>
|
||||||
<a href="README_ca_ES.md">Català</a>
|
<a href="README_ca_ES.md">Català</a>
|
||||||
<a href="README_es_ES.md">Español</a>
|
<a href="README_es_ES.md">Español</a>
|
||||||
<a href="README_fr_FR.md">Français</a>
|
<a href="README_fr_FR.md">Français</a>
|
@ -1,4 +1,4 @@
|
|||||||
import { AssetResponseDto } from 'src/domain/asset/response-dto/asset-response.dto';
|
import { AssetResponseDto } from 'src/dtos/asset-response.dto';
|
||||||
import request from 'supertest';
|
import request from 'supertest';
|
||||||
|
|
||||||
export const assetApi = {
|
export const assetApi = {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { LoginResponseDto } from 'src/domain/auth/auth.dto';
|
import { LoginResponseDto } from 'src/dtos/auth.dto';
|
||||||
import { UserResponseDto } from 'src/domain/user/response-dto/user-response.dto';
|
import { UserResponseDto } from 'src/dtos/user.dto';
|
||||||
import request from 'supertest';
|
import request from 'supertest';
|
||||||
import { adminSignupStub, loginResponseStub, loginStub } from 'test/fixtures/auth.stub';
|
import { adminSignupStub, loginResponseStub, loginStub } from 'test/fixtures/auth.stub';
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { CreateLibraryDto, LibraryResponseDto, ScanLibraryDto } from 'src/domain/library/library.dto';
|
import { CreateLibraryDto, LibraryResponseDto, ScanLibraryDto } from 'src/dtos/library.dto';
|
||||||
import request from 'supertest';
|
import request from 'supertest';
|
||||||
|
|
||||||
export const libraryApi = {
|
export const libraryApi = {
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import { api } from 'e2e/client';
|
import { api } from 'e2e/client';
|
||||||
import fs from 'node:fs/promises';
|
import fs from 'node:fs/promises';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { LoginResponseDto } from 'src/domain/auth/auth.dto';
|
import { LoginResponseDto } from 'src/dtos/auth.dto';
|
||||||
import { LibraryResponseDto } from 'src/domain/library/library.dto';
|
import { LibraryResponseDto } from 'src/dtos/library.dto';
|
||||||
import { LibraryService } from 'src/domain/library/library.service';
|
|
||||||
import { AssetType } from 'src/entities/asset.entity';
|
import { AssetType } from 'src/entities/asset.entity';
|
||||||
import { LibraryType } from 'src/entities/library.entity';
|
import { LibraryType } from 'src/entities/library.entity';
|
||||||
import { StorageEventType } from 'src/interfaces/storage.repository';
|
import { StorageEventType } from 'src/interfaces/storage.interface';
|
||||||
|
import { LibraryService } from 'src/services/library.service';
|
||||||
import {
|
import {
|
||||||
IMMICH_TEST_ASSET_PATH,
|
IMMICH_TEST_ASSET_PATH,
|
||||||
IMMICH_TEST_ASSET_TEMP_PATH,
|
IMMICH_TEST_ASSET_TEMP_PATH,
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { api } from 'e2e/client';
|
import { api } from 'e2e/client';
|
||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
import { LibraryController } from 'src/controllers/library.controller';
|
import { LibraryController } from 'src/controllers/library.controller';
|
||||||
import { LoginResponseDto } from 'src/domain/auth/auth.dto';
|
import { LoginResponseDto } from 'src/dtos/auth.dto';
|
||||||
import { LibraryType } from 'src/entities/library.entity';
|
import { LibraryType } from 'src/entities/library.entity';
|
||||||
import request from 'supertest';
|
import request from 'supertest';
|
||||||
import { errorStub } from 'test/fixtures/error.stub';
|
import { errorStub } from 'test/fixtures/error.stub';
|
||||||
|
20
server/package-lock.json
generated
@ -81,7 +81,6 @@
|
|||||||
"@types/mock-fs": "^4.13.1",
|
"@types/mock-fs": "^4.13.1",
|
||||||
"@types/multer": "^1.4.7",
|
"@types/multer": "^1.4.7",
|
||||||
"@types/node": "^20.5.7",
|
"@types/node": "^20.5.7",
|
||||||
"@types/sharp": "^0.32.0",
|
|
||||||
"@types/supertest": "^6.0.0",
|
"@types/supertest": "^6.0.0",
|
||||||
"@types/ua-parser-js": "^0.7.36",
|
"@types/ua-parser-js": "^0.7.36",
|
||||||
"@typescript-eslint/eslint-plugin": "^7.0.0",
|
"@typescript-eslint/eslint-plugin": "^7.0.0",
|
||||||
@ -4840,16 +4839,6 @@
|
|||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/sharp": {
|
|
||||||
"version": "0.32.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/sharp/-/sharp-0.32.0.tgz",
|
|
||||||
"integrity": "sha512-OOi3kL+FZDnPhVzsfD37J88FNeZh6gQsGcLc95NbeURRGvmSjeXiDcyWzF2o3yh/gQAUn2uhh/e+CPCa5nwAxw==",
|
|
||||||
"deprecated": "This is a stub types definition. sharp provides its own type definitions, so you do not need this installed.",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"sharp": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@types/shimmer": {
|
"node_modules/@types/shimmer": {
|
||||||
"version": "1.0.5",
|
"version": "1.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/shimmer/-/shimmer-1.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/shimmer/-/shimmer-1.0.5.tgz",
|
||||||
@ -17900,15 +17889,6 @@
|
|||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@types/sharp": {
|
|
||||||
"version": "0.32.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/sharp/-/sharp-0.32.0.tgz",
|
|
||||||
"integrity": "sha512-OOi3kL+FZDnPhVzsfD37J88FNeZh6gQsGcLc95NbeURRGvmSjeXiDcyWzF2o3yh/gQAUn2uhh/e+CPCa5nwAxw==",
|
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
|
||||||
"sharp": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"@types/shimmer": {
|
"@types/shimmer": {
|
||||||
"version": "1.0.5",
|
"version": "1.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/shimmer/-/shimmer-1.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/shimmer/-/shimmer-1.0.5.tgz",
|
||||||
|
@ -25,12 +25,12 @@
|
|||||||
"e2e:jobs": "jest --config e2e/jobs/jest-e2e.json --runInBand",
|
"e2e:jobs": "jest --config e2e/jobs/jest-e2e.json --runInBand",
|
||||||
"typeorm": "typeorm",
|
"typeorm": "typeorm",
|
||||||
"typeorm:migrations:create": "typeorm migration:create",
|
"typeorm:migrations:create": "typeorm migration:create",
|
||||||
"typeorm:migrations:generate": "typeorm migration:generate -d ./dist/infra/database.config.js",
|
"typeorm:migrations:generate": "typeorm migration:generate -d ./dist/database.config.js",
|
||||||
"typeorm:migrations:run": "typeorm migration:run -d ./dist/infra/database.config.js",
|
"typeorm:migrations:run": "typeorm migration:run -d ./dist/database.config.js",
|
||||||
"typeorm:migrations:revert": "typeorm migration:revert -d ./dist/infra/database.config.js",
|
"typeorm:migrations:revert": "typeorm migration:revert -d ./dist/database.config.js",
|
||||||
"typeorm:schema:drop": "typeorm query -d ./dist/infra/database.config.js 'DROP schema public cascade; CREATE schema public;'",
|
"typeorm:schema:drop": "typeorm query -d ./dist/database.config.js 'DROP schema public cascade; CREATE schema public;'",
|
||||||
"typeorm:schema:reset": "npm run typeorm:schema:drop && npm run typeorm:migrations:run",
|
"typeorm:schema:reset": "npm run typeorm:schema:drop && npm run typeorm:migrations:run",
|
||||||
"sql:generate": "node ./dist/infra/sql-generator/"
|
"sql:generate": "node ./dist/utils/sql.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.22.11",
|
"@babel/runtime": "^7.22.11",
|
||||||
@ -105,7 +105,6 @@
|
|||||||
"@types/mock-fs": "^4.13.1",
|
"@types/mock-fs": "^4.13.1",
|
||||||
"@types/multer": "^1.4.7",
|
"@types/multer": "^1.4.7",
|
||||||
"@types/node": "^20.5.7",
|
"@types/node": "^20.5.7",
|
||||||
"@types/sharp": "^0.32.0",
|
|
||||||
"@types/supertest": "^6.0.0",
|
"@types/supertest": "^6.0.0",
|
||||||
"@types/ua-parser-js": "^0.7.36",
|
"@types/ua-parser-js": "^0.7.36",
|
||||||
"@typescript-eslint/eslint-plugin": "^7.0.0",
|
"@typescript-eslint/eslint-plugin": "^7.0.0",
|
||||||
@ -145,19 +144,20 @@
|
|||||||
"^.+\\.ts$": "ts-jest"
|
"^.+\\.ts$": "ts-jest"
|
||||||
},
|
},
|
||||||
"collectCoverageFrom": [
|
"collectCoverageFrom": [
|
||||||
"<rootDir>/src/**/*.(t|j)s",
|
"<rootDir>/src/cores/*.(t|j)s",
|
||||||
"!<rootDir>/src/infra/**/*",
|
"<rootDir>/src/dtos/*.(t|j)s",
|
||||||
"!<rootDir>/src/migrations/**/*",
|
"<rootDir>/src/interfaces/*.(t|j)s",
|
||||||
"!<rootDir>/src/subscribers/**/*",
|
"<rootDir>/src/services/*.(t|j)s",
|
||||||
"!<rootDir>/src/immich/controllers/**/*"
|
"<rootDir>/src/utils/*.(t|j)s",
|
||||||
|
"<rootDir>/src/*.t|j)s"
|
||||||
],
|
],
|
||||||
"coverageDirectory": "./coverage",
|
"coverageDirectory": "./coverage",
|
||||||
"coverageThreshold": {
|
"coverageThreshold": {
|
||||||
"./src/domain/": {
|
"./src/": {
|
||||||
"branches": 75,
|
"branches": 70,
|
||||||
"functions": 80,
|
"functions": 75,
|
||||||
"lines": 85,
|
"lines": 80,
|
||||||
"statements": 85
|
"statements": 80
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"testEnvironment": "node",
|
"testEnvironment": "node",
|
||||||
|
284
server/src/app.module.ts
Normal file
@ -0,0 +1,284 @@
|
|||||||
|
import { BullModule } from '@nestjs/bullmq';
|
||||||
|
import { Module, OnModuleInit, Provider, ValidationPipe } from '@nestjs/common';
|
||||||
|
import { ConfigModule } from '@nestjs/config';
|
||||||
|
import { APP_GUARD, APP_INTERCEPTOR, APP_PIPE } from '@nestjs/core';
|
||||||
|
import { EventEmitterModule } from '@nestjs/event-emitter';
|
||||||
|
import { ScheduleModule, SchedulerRegistry } from '@nestjs/schedule';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { OpenTelemetryModule } from 'nestjs-otel';
|
||||||
|
import { ListUsersCommand } from 'src/commands/list-users.command';
|
||||||
|
import { DisableOAuthLogin, EnableOAuthLogin } from 'src/commands/oauth-login';
|
||||||
|
import { DisablePasswordLoginCommand, EnablePasswordLoginCommand } from 'src/commands/password-login';
|
||||||
|
import { PromptPasswordQuestions, ResetAdminPasswordCommand } from 'src/commands/reset-admin-password.command';
|
||||||
|
import { bullConfig, bullQueues, immichAppConfig } from 'src/config';
|
||||||
|
import { ActivityController } from 'src/controllers/activity.controller';
|
||||||
|
import { AlbumController } from 'src/controllers/album.controller';
|
||||||
|
import { APIKeyController } from 'src/controllers/api-key.controller';
|
||||||
|
import { AppController } from 'src/controllers/app.controller';
|
||||||
|
import { AssetControllerV1 } from 'src/controllers/asset-v1.controller';
|
||||||
|
import { AssetController, AssetsController } from 'src/controllers/asset.controller';
|
||||||
|
import { AuditController } from 'src/controllers/audit.controller';
|
||||||
|
import { AuthController } from 'src/controllers/auth.controller';
|
||||||
|
import { DownloadController } from 'src/controllers/download.controller';
|
||||||
|
import { FaceController } from 'src/controllers/face.controller';
|
||||||
|
import { JobController } from 'src/controllers/job.controller';
|
||||||
|
import { LibraryController } from 'src/controllers/library.controller';
|
||||||
|
import { OAuthController } from 'src/controllers/oauth.controller';
|
||||||
|
import { PartnerController } from 'src/controllers/partner.controller';
|
||||||
|
import { PersonController } from 'src/controllers/person.controller';
|
||||||
|
import { SearchController } from 'src/controllers/search.controller';
|
||||||
|
import { ServerInfoController } from 'src/controllers/server-info.controller';
|
||||||
|
import { SharedLinkController } from 'src/controllers/shared-link.controller';
|
||||||
|
import { SystemConfigController } from 'src/controllers/system-config.controller';
|
||||||
|
import { TagController } from 'src/controllers/tag.controller';
|
||||||
|
import { TrashController } from 'src/controllers/trash.controller';
|
||||||
|
import { UserController } from 'src/controllers/user.controller';
|
||||||
|
import { databaseConfig } from 'src/database.config';
|
||||||
|
import { databaseEntities } from 'src/entities';
|
||||||
|
import { IAccessRepository } from 'src/interfaces/access.interface';
|
||||||
|
import { IActivityRepository } from 'src/interfaces/activity.interface';
|
||||||
|
import { IAlbumRepository } from 'src/interfaces/album.interface';
|
||||||
|
import { IKeyRepository } from 'src/interfaces/api-key.interface';
|
||||||
|
import { IAssetStackRepository } from 'src/interfaces/asset-stack.interface';
|
||||||
|
import { IAssetRepositoryV1 } from 'src/interfaces/asset-v1.interface';
|
||||||
|
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
||||||
|
import { IAuditRepository } from 'src/interfaces/audit.interface';
|
||||||
|
import { ICommunicationRepository } from 'src/interfaces/communication.interface';
|
||||||
|
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||||
|
import { IDatabaseRepository } from 'src/interfaces/database.interface';
|
||||||
|
import { IJobRepository } from 'src/interfaces/job.interface';
|
||||||
|
import { ILibraryRepository } from 'src/interfaces/library.interface';
|
||||||
|
import { IMachineLearningRepository } from 'src/interfaces/machine-learning.interface';
|
||||||
|
import { IMediaRepository } from 'src/interfaces/media.interface';
|
||||||
|
import { IMetadataRepository } from 'src/interfaces/metadata.interface';
|
||||||
|
import { IMoveRepository } from 'src/interfaces/move.interface';
|
||||||
|
import { IPartnerRepository } from 'src/interfaces/partner.interface';
|
||||||
|
import { IPersonRepository } from 'src/interfaces/person.interface';
|
||||||
|
import { ISearchRepository } from 'src/interfaces/search.interface';
|
||||||
|
import { IServerInfoRepository } from 'src/interfaces/server-info.interface';
|
||||||
|
import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface';
|
||||||
|
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
||||||
|
import { ISystemConfigRepository } from 'src/interfaces/system-config.interface';
|
||||||
|
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||||
|
import { ITagRepository } from 'src/interfaces/tag.interface';
|
||||||
|
import { IUserTokenRepository } from 'src/interfaces/user-token.interface';
|
||||||
|
import { IUserRepository } from 'src/interfaces/user.interface';
|
||||||
|
import { AuthGuard } from 'src/middleware/auth.guard';
|
||||||
|
import { ErrorInterceptor } from 'src/middleware/error.interceptor';
|
||||||
|
import { FileUploadInterceptor } from 'src/middleware/file-upload.interceptor';
|
||||||
|
import { AccessRepository } from 'src/repositories/access.repository';
|
||||||
|
import { ActivityRepository } from 'src/repositories/activity.repository';
|
||||||
|
import { AlbumRepository } from 'src/repositories/album.repository';
|
||||||
|
import { ApiKeyRepository } from 'src/repositories/api-key.repository';
|
||||||
|
import { AssetStackRepository } from 'src/repositories/asset-stack.repository';
|
||||||
|
import { AssetRepositoryV1 } from 'src/repositories/asset-v1.repository';
|
||||||
|
import { AssetRepository } from 'src/repositories/asset.repository';
|
||||||
|
import { AuditRepository } from 'src/repositories/audit.repository';
|
||||||
|
import { CommunicationRepository } from 'src/repositories/communication.repository';
|
||||||
|
import { CryptoRepository } from 'src/repositories/crypto.repository';
|
||||||
|
import { DatabaseRepository } from 'src/repositories/database.repository';
|
||||||
|
import { FilesystemProvider } from 'src/repositories/filesystem.provider';
|
||||||
|
import { JobRepository } from 'src/repositories/job.repository';
|
||||||
|
import { LibraryRepository } from 'src/repositories/library.repository';
|
||||||
|
import { MachineLearningRepository } from 'src/repositories/machine-learning.repository';
|
||||||
|
import { MediaRepository } from 'src/repositories/media.repository';
|
||||||
|
import { MetadataRepository } from 'src/repositories/metadata.repository';
|
||||||
|
import { MoveRepository } from 'src/repositories/move.repository';
|
||||||
|
import { PartnerRepository } from 'src/repositories/partner.repository';
|
||||||
|
import { PersonRepository } from 'src/repositories/person.repository';
|
||||||
|
import { SearchRepository } from 'src/repositories/search.repository';
|
||||||
|
import { ServerInfoRepository } from 'src/repositories/server-info.repository';
|
||||||
|
import { SharedLinkRepository } from 'src/repositories/shared-link.repository';
|
||||||
|
import { SystemConfigRepository } from 'src/repositories/system-config.repository';
|
||||||
|
import { SystemMetadataRepository } from 'src/repositories/system-metadata.repository';
|
||||||
|
import { TagRepository } from 'src/repositories/tag.repository';
|
||||||
|
import { UserTokenRepository } from 'src/repositories/user-token.repository';
|
||||||
|
import { UserRepository } from 'src/repositories/user.repository';
|
||||||
|
import { ActivityService } from 'src/services/activity.service';
|
||||||
|
import { AlbumService } from 'src/services/album.service';
|
||||||
|
import { APIKeyService } from 'src/services/api-key.service';
|
||||||
|
import { ApiService } from 'src/services/api.service';
|
||||||
|
import { AssetServiceV1 } from 'src/services/asset-v1.service';
|
||||||
|
import { AssetService } from 'src/services/asset.service';
|
||||||
|
import { AuditService } from 'src/services/audit.service';
|
||||||
|
import { AuthService } from 'src/services/auth.service';
|
||||||
|
import { DatabaseService } from 'src/services/database.service';
|
||||||
|
import { DownloadService } from 'src/services/download.service';
|
||||||
|
import { JobService } from 'src/services/job.service';
|
||||||
|
import { LibraryService } from 'src/services/library.service';
|
||||||
|
import { MediaService } from 'src/services/media.service';
|
||||||
|
import { MetadataService } from 'src/services/metadata.service';
|
||||||
|
import { MicroservicesService } from 'src/services/microservices.service';
|
||||||
|
import { PartnerService } from 'src/services/partner.service';
|
||||||
|
import { PersonService } from 'src/services/person.service';
|
||||||
|
import { SearchService } from 'src/services/search.service';
|
||||||
|
import { ServerInfoService } from 'src/services/server-info.service';
|
||||||
|
import { SharedLinkService } from 'src/services/shared-link.service';
|
||||||
|
import { SmartInfoService } from 'src/services/smart-info.service';
|
||||||
|
import { StorageTemplateService } from 'src/services/storage-template.service';
|
||||||
|
import { StorageService } from 'src/services/storage.service';
|
||||||
|
import { SystemConfigService } from 'src/services/system-config.service';
|
||||||
|
import { TagService } from 'src/services/tag.service';
|
||||||
|
import { TrashService } from 'src/services/trash.service';
|
||||||
|
import { UserService } from 'src/services/user.service';
|
||||||
|
import { otelConfig } from 'src/utils/instrumentation';
|
||||||
|
import { ImmichLogger } from 'src/utils/logger';
|
||||||
|
|
||||||
|
const commands = [
|
||||||
|
ResetAdminPasswordCommand,
|
||||||
|
PromptPasswordQuestions,
|
||||||
|
EnablePasswordLoginCommand,
|
||||||
|
DisablePasswordLoginCommand,
|
||||||
|
EnableOAuthLogin,
|
||||||
|
DisableOAuthLogin,
|
||||||
|
ListUsersCommand,
|
||||||
|
];
|
||||||
|
|
||||||
|
const controllers = [
|
||||||
|
ActivityController,
|
||||||
|
AssetsController,
|
||||||
|
AssetControllerV1,
|
||||||
|
AssetController,
|
||||||
|
AppController,
|
||||||
|
AlbumController,
|
||||||
|
APIKeyController,
|
||||||
|
AuditController,
|
||||||
|
AuthController,
|
||||||
|
DownloadController,
|
||||||
|
FaceController,
|
||||||
|
JobController,
|
||||||
|
LibraryController,
|
||||||
|
OAuthController,
|
||||||
|
PartnerController,
|
||||||
|
SearchController,
|
||||||
|
ServerInfoController,
|
||||||
|
SharedLinkController,
|
||||||
|
SystemConfigController,
|
||||||
|
TagController,
|
||||||
|
TrashController,
|
||||||
|
UserController,
|
||||||
|
PersonController,
|
||||||
|
];
|
||||||
|
|
||||||
|
const services: Provider[] = [
|
||||||
|
ApiService,
|
||||||
|
MicroservicesService,
|
||||||
|
|
||||||
|
APIKeyService,
|
||||||
|
ActivityService,
|
||||||
|
AlbumService,
|
||||||
|
AssetService,
|
||||||
|
AssetServiceV1,
|
||||||
|
AuditService,
|
||||||
|
AuthService,
|
||||||
|
DatabaseService,
|
||||||
|
DownloadService,
|
||||||
|
ImmichLogger,
|
||||||
|
JobService,
|
||||||
|
LibraryService,
|
||||||
|
MediaService,
|
||||||
|
MetadataService,
|
||||||
|
PartnerService,
|
||||||
|
PersonService,
|
||||||
|
SearchService,
|
||||||
|
ServerInfoService,
|
||||||
|
SharedLinkService,
|
||||||
|
SmartInfoService,
|
||||||
|
StorageService,
|
||||||
|
StorageTemplateService,
|
||||||
|
SystemConfigService,
|
||||||
|
TagService,
|
||||||
|
TrashService,
|
||||||
|
UserService,
|
||||||
|
];
|
||||||
|
|
||||||
|
const repositories: Provider[] = [
|
||||||
|
{ provide: IActivityRepository, useClass: ActivityRepository },
|
||||||
|
{ provide: IAccessRepository, useClass: AccessRepository },
|
||||||
|
{ provide: IAlbumRepository, useClass: AlbumRepository },
|
||||||
|
{ provide: IAssetRepository, useClass: AssetRepository },
|
||||||
|
{ provide: IAssetRepositoryV1, useClass: AssetRepositoryV1 },
|
||||||
|
{ provide: IAssetStackRepository, useClass: AssetStackRepository },
|
||||||
|
{ provide: IAuditRepository, useClass: AuditRepository },
|
||||||
|
{ provide: ICommunicationRepository, useClass: CommunicationRepository },
|
||||||
|
{ provide: ICryptoRepository, useClass: CryptoRepository },
|
||||||
|
{ provide: IDatabaseRepository, useClass: DatabaseRepository },
|
||||||
|
{ provide: IJobRepository, useClass: JobRepository },
|
||||||
|
{ provide: ILibraryRepository, useClass: LibraryRepository },
|
||||||
|
{ provide: IKeyRepository, useClass: ApiKeyRepository },
|
||||||
|
{ provide: IMachineLearningRepository, useClass: MachineLearningRepository },
|
||||||
|
{ provide: IMetadataRepository, useClass: MetadataRepository },
|
||||||
|
{ provide: IMoveRepository, useClass: MoveRepository },
|
||||||
|
{ provide: IPartnerRepository, useClass: PartnerRepository },
|
||||||
|
{ provide: IPersonRepository, useClass: PersonRepository },
|
||||||
|
{ provide: IServerInfoRepository, useClass: ServerInfoRepository },
|
||||||
|
{ provide: ISharedLinkRepository, useClass: SharedLinkRepository },
|
||||||
|
{ provide: ISearchRepository, useClass: SearchRepository },
|
||||||
|
{ provide: IStorageRepository, useClass: FilesystemProvider },
|
||||||
|
{ provide: ISystemConfigRepository, useClass: SystemConfigRepository },
|
||||||
|
{ provide: ISystemMetadataRepository, useClass: SystemMetadataRepository },
|
||||||
|
{ provide: ITagRepository, useClass: TagRepository },
|
||||||
|
{ provide: IMediaRepository, useClass: MediaRepository },
|
||||||
|
{ provide: IUserRepository, useClass: UserRepository },
|
||||||
|
{ provide: IUserTokenRepository, useClass: UserTokenRepository },
|
||||||
|
];
|
||||||
|
|
||||||
|
const middleware = [
|
||||||
|
FileUploadInterceptor,
|
||||||
|
{ provide: APP_PIPE, useValue: new ValidationPipe({ transform: true, whitelist: true }) },
|
||||||
|
{ provide: APP_INTERCEPTOR, useClass: ErrorInterceptor },
|
||||||
|
{ provide: APP_GUARD, useClass: AuthGuard },
|
||||||
|
];
|
||||||
|
|
||||||
|
const imports = [
|
||||||
|
BullModule.forRoot(bullConfig),
|
||||||
|
BullModule.registerQueue(...bullQueues),
|
||||||
|
ConfigModule.forRoot(immichAppConfig),
|
||||||
|
EventEmitterModule.forRoot(),
|
||||||
|
OpenTelemetryModule.forRoot(otelConfig),
|
||||||
|
TypeOrmModule.forRoot(databaseConfig),
|
||||||
|
TypeOrmModule.forFeature(databaseEntities),
|
||||||
|
];
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [...imports, ScheduleModule.forRoot()],
|
||||||
|
controllers: [...controllers],
|
||||||
|
providers: [...services, ...repositories, ...middleware],
|
||||||
|
})
|
||||||
|
export class ApiModule implements OnModuleInit {
|
||||||
|
constructor(private service: ApiService) {}
|
||||||
|
|
||||||
|
async onModuleInit() {
|
||||||
|
await this.service.init();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [...imports],
|
||||||
|
providers: [...services, ...repositories, SchedulerRegistry],
|
||||||
|
})
|
||||||
|
export class MicroservicesModule implements OnModuleInit {
|
||||||
|
constructor(private service: MicroservicesService) {}
|
||||||
|
|
||||||
|
async onModuleInit() {
|
||||||
|
await this.service.init();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [...imports],
|
||||||
|
providers: [...services, ...repositories, ...commands, SchedulerRegistry],
|
||||||
|
})
|
||||||
|
export class ImmichAdminModule {}
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
ConfigModule.forRoot(immichAppConfig),
|
||||||
|
EventEmitterModule.forRoot(),
|
||||||
|
TypeOrmModule.forRoot(databaseConfig),
|
||||||
|
TypeOrmModule.forFeature(databaseEntities),
|
||||||
|
],
|
||||||
|
controllers: [...controllers],
|
||||||
|
providers: [...services, ...repositories, ...middleware, SchedulerRegistry],
|
||||||
|
})
|
||||||
|
export class AppTestModule {}
|
@ -1,57 +0,0 @@
|
|||||||
import { NestFactory } from '@nestjs/core';
|
|
||||||
import { NestExpressApplication } from '@nestjs/platform-express';
|
|
||||||
import { json } from 'body-parser';
|
|
||||||
import cookieParser from 'cookie-parser';
|
|
||||||
import { existsSync } from 'node:fs';
|
|
||||||
import sirv from 'sirv';
|
|
||||||
import { ApiModule } from 'src/apps/api.module';
|
|
||||||
import { ApiService } from 'src/apps/api.service';
|
|
||||||
import { excludePaths } from 'src/config';
|
|
||||||
import { WEB_ROOT, envName, isDev, serverVersion } from 'src/domain/domain.constant';
|
|
||||||
import { useSwagger } from 'src/immich/app.utils';
|
|
||||||
import { otelSDK } from 'src/infra/instrumentation';
|
|
||||||
import { ImmichLogger } from 'src/infra/logger';
|
|
||||||
import { WebSocketAdapter } from 'src/infra/websocket.adapter';
|
|
||||||
|
|
||||||
const logger = new ImmichLogger('ImmichServer');
|
|
||||||
const port = Number(process.env.SERVER_PORT) || 3001;
|
|
||||||
|
|
||||||
export async function bootstrapApi() {
|
|
||||||
otelSDK.start();
|
|
||||||
const app = await NestFactory.create<NestExpressApplication>(ApiModule, { bufferLogs: true });
|
|
||||||
|
|
||||||
app.useLogger(app.get(ImmichLogger));
|
|
||||||
app.set('trust proxy', ['loopback', 'linklocal', 'uniquelocal']);
|
|
||||||
app.set('etag', 'strong');
|
|
||||||
app.use(cookieParser());
|
|
||||||
app.use(json({ limit: '10mb' }));
|
|
||||||
if (isDev) {
|
|
||||||
app.enableCors();
|
|
||||||
}
|
|
||||||
app.useWebSocketAdapter(new WebSocketAdapter(app));
|
|
||||||
useSwagger(app, isDev);
|
|
||||||
|
|
||||||
app.setGlobalPrefix('api', { exclude: excludePaths });
|
|
||||||
if (existsSync(WEB_ROOT)) {
|
|
||||||
// copied from https://github.com/sveltejs/kit/blob/679b5989fe62e3964b9a73b712d7b41831aa1f07/packages/adapter-node/src/handler.js#L46
|
|
||||||
// provides serving of precompressed assets and caching of immutable assets
|
|
||||||
app.use(
|
|
||||||
sirv(WEB_ROOT, {
|
|
||||||
etag: true,
|
|
||||||
gzip: true,
|
|
||||||
brotli: true,
|
|
||||||
setHeaders: (res, pathname) => {
|
|
||||||
if (pathname.startsWith(`/_app/immutable`) && res.statusCode === 200) {
|
|
||||||
res.setHeader('cache-control', 'public,max-age=31536000,immutable');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
app.use(app.get(ApiService).ssr(excludePaths));
|
|
||||||
|
|
||||||
const server = await app.listen(port);
|
|
||||||
server.requestTimeout = 30 * 60 * 1000;
|
|
||||||
|
|
||||||
logger.log(`Immich Server is listening on ${await app.getUrl()} [v${serverVersion}] [${envName}] `);
|
|
||||||
}
|
|
@ -1,87 +0,0 @@
|
|||||||
import { Module, OnModuleInit, ValidationPipe } from '@nestjs/common';
|
|
||||||
import { APP_GUARD, APP_INTERCEPTOR, APP_PIPE } from '@nestjs/core';
|
|
||||||
import { ScheduleModule } from '@nestjs/schedule';
|
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
|
||||||
import { ApiService } from 'src/apps/api.service';
|
|
||||||
import { ActivityController } from 'src/controllers/activity.controller';
|
|
||||||
import { AlbumController } from 'src/controllers/album.controller';
|
|
||||||
import { APIKeyController } from 'src/controllers/api-key.controller';
|
|
||||||
import { AppController } from 'src/controllers/app.controller';
|
|
||||||
import { AssetController, AssetsController } from 'src/controllers/asset.controller';
|
|
||||||
import { AuditController } from 'src/controllers/audit.controller';
|
|
||||||
import { AuthController } from 'src/controllers/auth.controller';
|
|
||||||
import { DownloadController } from 'src/controllers/download.controller';
|
|
||||||
import { FaceController } from 'src/controllers/face.controller';
|
|
||||||
import { JobController } from 'src/controllers/job.controller';
|
|
||||||
import { LibraryController } from 'src/controllers/library.controller';
|
|
||||||
import { OAuthController } from 'src/controllers/oauth.controller';
|
|
||||||
import { PartnerController } from 'src/controllers/partner.controller';
|
|
||||||
import { PersonController } from 'src/controllers/person.controller';
|
|
||||||
import { SearchController } from 'src/controllers/search.controller';
|
|
||||||
import { ServerInfoController } from 'src/controllers/server-info.controller';
|
|
||||||
import { SharedLinkController } from 'src/controllers/shared-link.controller';
|
|
||||||
import { SystemConfigController } from 'src/controllers/system-config.controller';
|
|
||||||
import { TagController } from 'src/controllers/tag.controller';
|
|
||||||
import { TrashController } from 'src/controllers/trash.controller';
|
|
||||||
import { UserController } from 'src/controllers/user.controller';
|
|
||||||
import { DomainModule } from 'src/domain/domain.module';
|
|
||||||
import { AssetEntity } from 'src/entities/asset.entity';
|
|
||||||
import { ExifEntity } from 'src/entities/exif.entity';
|
|
||||||
import { AssetRepositoryV1, IAssetRepositoryV1 } from 'src/immich/api-v1/asset/asset-repository';
|
|
||||||
import { AssetController as AssetControllerV1 } from 'src/immich/api-v1/asset/asset.controller';
|
|
||||||
import { AssetService as AssetServiceV1 } from 'src/immich/api-v1/asset/asset.service';
|
|
||||||
import { InfraModule } from 'src/infra/infra.module';
|
|
||||||
import { AuthGuard } from 'src/middleware/auth.guard';
|
|
||||||
import { ErrorInterceptor } from 'src/middleware/error.interceptor';
|
|
||||||
import { FileUploadInterceptor } from 'src/middleware/file-upload.interceptor';
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
imports: [
|
|
||||||
//
|
|
||||||
InfraModule,
|
|
||||||
DomainModule,
|
|
||||||
ScheduleModule.forRoot(),
|
|
||||||
TypeOrmModule.forFeature([AssetEntity, ExifEntity]),
|
|
||||||
],
|
|
||||||
controllers: [
|
|
||||||
ActivityController,
|
|
||||||
AssetsController,
|
|
||||||
AssetControllerV1,
|
|
||||||
AssetController,
|
|
||||||
AppController,
|
|
||||||
AlbumController,
|
|
||||||
APIKeyController,
|
|
||||||
AuditController,
|
|
||||||
AuthController,
|
|
||||||
DownloadController,
|
|
||||||
FaceController,
|
|
||||||
JobController,
|
|
||||||
LibraryController,
|
|
||||||
OAuthController,
|
|
||||||
PartnerController,
|
|
||||||
SearchController,
|
|
||||||
ServerInfoController,
|
|
||||||
SharedLinkController,
|
|
||||||
SystemConfigController,
|
|
||||||
TagController,
|
|
||||||
TrashController,
|
|
||||||
UserController,
|
|
||||||
PersonController,
|
|
||||||
],
|
|
||||||
providers: [
|
|
||||||
{ provide: APP_PIPE, useValue: new ValidationPipe({ transform: true, whitelist: true }) },
|
|
||||||
{ provide: APP_INTERCEPTOR, useClass: ErrorInterceptor },
|
|
||||||
{ provide: APP_GUARD, useClass: AuthGuard },
|
|
||||||
{ provide: IAssetRepositoryV1, useClass: AssetRepositoryV1 },
|
|
||||||
ApiService,
|
|
||||||
AssetServiceV1,
|
|
||||||
FileUploadInterceptor,
|
|
||||||
],
|
|
||||||
})
|
|
||||||
export class ApiModule implements OnModuleInit {
|
|
||||||
constructor(private appService: ApiService) {}
|
|
||||||
|
|
||||||
async onModuleInit() {
|
|
||||||
await this.appService.init();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,8 +0,0 @@
|
|||||||
import { CommandFactory } from 'nest-commander';
|
|
||||||
import { ImmichAdminModule } from 'src/apps/immich-admin.module';
|
|
||||||
import { LogLevel } from 'src/entities/system-config.entity';
|
|
||||||
|
|
||||||
export async function bootstrapImmichAdmin() {
|
|
||||||
process.env.LOG_LEVEL = LogLevel.WARN;
|
|
||||||
await CommandFactory.run(ImmichAdminModule);
|
|
||||||
}
|
|
@ -1,21 +0,0 @@
|
|||||||
import { Module } from '@nestjs/common';
|
|
||||||
import { ListUsersCommand } from 'src/commands/list-users.command';
|
|
||||||
import { DisableOAuthLogin, EnableOAuthLogin } from 'src/commands/oauth-login';
|
|
||||||
import { DisablePasswordLoginCommand, EnablePasswordLoginCommand } from 'src/commands/password-login';
|
|
||||||
import { PromptPasswordQuestions, ResetAdminPasswordCommand } from 'src/commands/reset-admin-password.command';
|
|
||||||
import { DomainModule } from 'src/domain/domain.module';
|
|
||||||
import { InfraModule } from 'src/infra/infra.module';
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
imports: [InfraModule, DomainModule],
|
|
||||||
providers: [
|
|
||||||
ResetAdminPasswordCommand,
|
|
||||||
PromptPasswordQuestions,
|
|
||||||
EnablePasswordLoginCommand,
|
|
||||||
DisablePasswordLoginCommand,
|
|
||||||
EnableOAuthLogin,
|
|
||||||
DisableOAuthLogin,
|
|
||||||
ListUsersCommand,
|
|
||||||
],
|
|
||||||
})
|
|
||||||
export class ImmichAdminModule {}
|
|
@ -1,20 +0,0 @@
|
|||||||
import { NestFactory } from '@nestjs/core';
|
|
||||||
import { MicroservicesModule } from 'src/apps/microservices.module';
|
|
||||||
import { envName, serverVersion } from 'src/domain/domain.constant';
|
|
||||||
import { otelSDK } from 'src/infra/instrumentation';
|
|
||||||
import { ImmichLogger } from 'src/infra/logger';
|
|
||||||
import { WebSocketAdapter } from 'src/infra/websocket.adapter';
|
|
||||||
|
|
||||||
const logger = new ImmichLogger('ImmichMicroservice');
|
|
||||||
const port = Number(process.env.MICROSERVICES_PORT) || 3002;
|
|
||||||
|
|
||||||
export async function bootstrapMicroservices() {
|
|
||||||
otelSDK.start();
|
|
||||||
const app = await NestFactory.create(MicroservicesModule, { bufferLogs: true });
|
|
||||||
app.useLogger(app.get(ImmichLogger));
|
|
||||||
app.useWebSocketAdapter(new WebSocketAdapter(app));
|
|
||||||
|
|
||||||
await app.listen(port);
|
|
||||||
|
|
||||||
logger.log(`Immich Microservices is listening on ${await app.getUrl()} [v${serverVersion}] [${envName}] `);
|
|
||||||
}
|
|
@ -1,16 +0,0 @@
|
|||||||
import { Module, OnModuleInit } from '@nestjs/common';
|
|
||||||
import { MicroservicesService } from 'src/apps/microservices.service';
|
|
||||||
import { DomainModule } from 'src/domain/domain.module';
|
|
||||||
import { InfraModule } from 'src/infra/infra.module';
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
imports: [InfraModule, DomainModule],
|
|
||||||
providers: [MicroservicesService],
|
|
||||||
})
|
|
||||||
export class MicroservicesModule implements OnModuleInit {
|
|
||||||
constructor(private appService: MicroservicesService) {}
|
|
||||||
|
|
||||||
async onModuleInit() {
|
|
||||||
await this.appService.init();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,6 +1,6 @@
|
|||||||
import { Command, CommandRunner } from 'nest-commander';
|
import { Command, CommandRunner } from 'nest-commander';
|
||||||
import { UserService } from 'src/domain/user/user.service';
|
|
||||||
import { UserEntity } from 'src/entities/user.entity';
|
import { UserEntity } from 'src/entities/user.entity';
|
||||||
|
import { UserService } from 'src/services/user.service';
|
||||||
|
|
||||||
@Command({
|
@Command({
|
||||||
name: 'list-users',
|
name: 'list-users',
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Command, CommandRunner } from 'nest-commander';
|
import { Command, CommandRunner } from 'nest-commander';
|
||||||
import { SystemConfigService } from 'src/domain/system-config/system-config.service';
|
import { SystemConfigService } from 'src/services/system-config.service';
|
||||||
|
|
||||||
@Command({
|
@Command({
|
||||||
name: 'enable-oauth-login',
|
name: 'enable-oauth-login',
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Command, CommandRunner } from 'nest-commander';
|
import { Command, CommandRunner } from 'nest-commander';
|
||||||
import { SystemConfigService } from 'src/domain/system-config/system-config.service';
|
import { SystemConfigService } from 'src/services/system-config.service';
|
||||||
|
|
||||||
@Command({
|
@Command({
|
||||||
name: 'enable-password-login',
|
name: 'enable-password-login',
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { Command, CommandRunner, InquirerService, Question, QuestionSet } from 'nest-commander';
|
import { Command, CommandRunner, InquirerService, Question, QuestionSet } from 'nest-commander';
|
||||||
import { UserResponseDto } from 'src/domain/user/response-dto/user-response.dto';
|
import { UserResponseDto } from 'src/dtos/user.dto';
|
||||||
import { UserService } from 'src/domain/user/user.service';
|
import { UserService } from 'src/services/user.service';
|
||||||
|
|
||||||
@Command({
|
@Command({
|
||||||
name: 'reset-admin-password',
|
name: 'reset-admin-password',
|
||||||
|
@ -3,8 +3,8 @@ import { ConfigModuleOptions } from '@nestjs/config';
|
|||||||
import { QueueOptions } from 'bullmq';
|
import { QueueOptions } from 'bullmq';
|
||||||
import { RedisOptions } from 'ioredis';
|
import { RedisOptions } from 'ioredis';
|
||||||
import Joi from 'joi';
|
import Joi from 'joi';
|
||||||
import { QueueName } from 'src/domain/job/job.constants';
|
|
||||||
import { LogLevel } from 'src/entities/system-config.entity';
|
import { LogLevel } from 'src/entities/system-config.entity';
|
||||||
|
import { QueueName } from 'src/interfaces/job.interface';
|
||||||
|
|
||||||
const WHEN_DB_URL_SET = Joi.when('DB_URL', {
|
const WHEN_DB_URL_SET = Joi.when('DB_URL', {
|
||||||
is: Joi.exist(),
|
is: Joi.exist(),
|
||||||
@ -69,5 +69,3 @@ export const bullConfig: QueueOptions = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const bullQueues: RegisterQueueOptions[] = Object.values(QueueName).map((name) => ({ name }));
|
export const bullQueues: RegisterQueueOptions[] = Object.values(QueueName).map((name) => ({ name }));
|
||||||
|
|
||||||
export const excludePaths = ['/.well-known/immich', '/custom.css', '/favicon.ico'];
|
|
||||||
|
107
server/src/constants.ts
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
import { Duration } from 'luxon';
|
||||||
|
import { readFileSync } from 'node:fs';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
import { Version } from 'src/utils/version';
|
||||||
|
|
||||||
|
const { version } = JSON.parse(readFileSync('./package.json', 'utf8'));
|
||||||
|
export const serverVersion = Version.fromString(version);
|
||||||
|
|
||||||
|
export const AUDIT_LOG_MAX_DURATION = Duration.fromObject({ days: 100 });
|
||||||
|
export const ONE_HOUR = Duration.fromObject({ hours: 1 });
|
||||||
|
|
||||||
|
export const envName = (process.env.NODE_ENV || 'development').toUpperCase();
|
||||||
|
export const isDev = process.env.NODE_ENV === 'development';
|
||||||
|
export const APP_MEDIA_LOCATION = process.env.IMMICH_MEDIA_LOCATION || './upload';
|
||||||
|
export const WEB_ROOT = process.env.IMMICH_WEB_ROOT || '/usr/src/app/www';
|
||||||
|
|
||||||
|
const GEODATA_ROOT_PATH = process.env.IMMICH_REVERSE_GEOCODING_ROOT || '/usr/src/resources';
|
||||||
|
|
||||||
|
export const citiesFile = 'cities500.txt';
|
||||||
|
export const geodataDatePath = join(GEODATA_ROOT_PATH, 'geodata-date.txt');
|
||||||
|
export const geodataAdmin1Path = join(GEODATA_ROOT_PATH, 'admin1CodesASCII.txt');
|
||||||
|
export const geodataAdmin2Path = join(GEODATA_ROOT_PATH, 'admin2Codes.txt');
|
||||||
|
export const geodataCities500Path = join(GEODATA_ROOT_PATH, citiesFile);
|
||||||
|
|
||||||
|
export const MOBILE_REDIRECT = 'app.immich:/';
|
||||||
|
export const LOGIN_URL = '/auth/login?autoLaunch=0';
|
||||||
|
export const IMMICH_ACCESS_COOKIE = 'immich_access_token';
|
||||||
|
export const IMMICH_IS_AUTHENTICATED = 'immich_is_authenticated';
|
||||||
|
export const IMMICH_AUTH_TYPE_COOKIE = 'immich_auth_type';
|
||||||
|
export const IMMICH_API_KEY_NAME = 'api_key';
|
||||||
|
export const IMMICH_API_KEY_HEADER = 'x-api-key';
|
||||||
|
export const IMMICH_SHARED_LINK_ACCESS_COOKIE = 'immich_shared_link_token';
|
||||||
|
export enum AuthType {
|
||||||
|
PASSWORD = 'password',
|
||||||
|
OAUTH = 'oauth',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const excludePaths = ['/.well-known/immich', '/custom.css', '/favicon.ico'];
|
||||||
|
|
||||||
|
export const FACE_THUMBNAIL_SIZE = 250;
|
||||||
|
|
||||||
|
export const supportedYearTokens = ['y', 'yy'];
|
||||||
|
export const supportedMonthTokens = ['M', 'MM', 'MMM', 'MMMM'];
|
||||||
|
export const supportedWeekTokens = ['W', 'WW'];
|
||||||
|
export const supportedDayTokens = ['d', 'dd'];
|
||||||
|
export const supportedHourTokens = ['h', 'hh', 'H', 'HH'];
|
||||||
|
export const supportedMinuteTokens = ['m', 'mm'];
|
||||||
|
export const supportedSecondTokens = ['s', 'ss', 'SSS'];
|
||||||
|
export const supportedPresetTokens = [
|
||||||
|
'{{y}}/{{y}}-{{MM}}-{{dd}}/{{filename}}',
|
||||||
|
'{{y}}/{{MM}}-{{dd}}/{{filename}}',
|
||||||
|
'{{y}}/{{MMMM}}-{{dd}}/{{filename}}',
|
||||||
|
'{{y}}/{{MM}}/{{filename}}',
|
||||||
|
'{{y}}/{{MMM}}/{{filename}}',
|
||||||
|
'{{y}}/{{MMMM}}/{{filename}}',
|
||||||
|
'{{y}}/{{MM}}/{{dd}}/{{filename}}',
|
||||||
|
'{{y}}/{{MMMM}}/{{dd}}/{{filename}}',
|
||||||
|
'{{y}}/{{y}}-{{MM}}/{{y}}-{{MM}}-{{dd}}/{{filename}}',
|
||||||
|
'{{y}}-{{MM}}-{{dd}}/{{filename}}',
|
||||||
|
'{{y}}-{{MMM}}-{{dd}}/{{filename}}',
|
||||||
|
'{{y}}-{{MMMM}}-{{dd}}/{{filename}}',
|
||||||
|
'{{y}}/{{y}}-{{MM}}/{{filename}}',
|
||||||
|
'{{y}}/{{y}}-{{WW}}/{{filename}}',
|
||||||
|
'{{y}}/{{y}}-{{MM}}-{{dd}}/{{assetId}}',
|
||||||
|
'{{y}}/{{y}}-{{MM}}/{{assetId}}',
|
||||||
|
'{{y}}/{{y}}-{{WW}}/{{assetId}}',
|
||||||
|
'{{album}}/{{filename}}',
|
||||||
|
];
|
||||||
|
|
||||||
|
type ModelInfo = { dimSize: number };
|
||||||
|
export const CLIP_MODEL_INFO: Record<string, ModelInfo> = {
|
||||||
|
RN50__openai: { dimSize: 1024 },
|
||||||
|
RN50__yfcc15m: { dimSize: 1024 },
|
||||||
|
RN50__cc12m: { dimSize: 1024 },
|
||||||
|
RN101__openai: { dimSize: 512 },
|
||||||
|
RN101__yfcc15m: { dimSize: 512 },
|
||||||
|
RN50x4__openai: { dimSize: 640 },
|
||||||
|
RN50x16__openai: { dimSize: 768 },
|
||||||
|
RN50x64__openai: { dimSize: 1024 },
|
||||||
|
'ViT-B-32__openai': { dimSize: 512 },
|
||||||
|
'ViT-B-32__laion2b_e16': { dimSize: 512 },
|
||||||
|
'ViT-B-32__laion400m_e31': { dimSize: 512 },
|
||||||
|
'ViT-B-32__laion400m_e32': { dimSize: 512 },
|
||||||
|
'ViT-B-32__laion2b-s34b-b79k': { dimSize: 512 },
|
||||||
|
'ViT-B-16__openai': { dimSize: 512 },
|
||||||
|
'ViT-B-16__laion400m_e31': { dimSize: 512 },
|
||||||
|
'ViT-B-16__laion400m_e32': { dimSize: 512 },
|
||||||
|
'ViT-B-16-plus-240__laion400m_e31': { dimSize: 640 },
|
||||||
|
'ViT-B-16-plus-240__laion400m_e32': { dimSize: 640 },
|
||||||
|
'ViT-L-14__openai': { dimSize: 768 },
|
||||||
|
'ViT-L-14__laion400m_e31': { dimSize: 768 },
|
||||||
|
'ViT-L-14__laion400m_e32': { dimSize: 768 },
|
||||||
|
'ViT-L-14__laion2b-s32b-b82k': { dimSize: 768 },
|
||||||
|
'ViT-L-14-336__openai': { dimSize: 768 },
|
||||||
|
'ViT-L-14-quickgelu__dfn2b': { dimSize: 768 },
|
||||||
|
'ViT-H-14__laion2b-s32b-b79k': { dimSize: 1024 },
|
||||||
|
'ViT-H-14-quickgelu__dfn5b': { dimSize: 1024 },
|
||||||
|
'ViT-H-14-378-quickgelu__dfn5b': { dimSize: 1024 },
|
||||||
|
'ViT-g-14__laion2b-s12b-b42k': { dimSize: 1024 },
|
||||||
|
'LABSE-Vit-L-14': { dimSize: 768 },
|
||||||
|
'XLM-Roberta-Large-Vit-B-32': { dimSize: 512 },
|
||||||
|
'XLM-Roberta-Large-Vit-B-16Plus': { dimSize: 640 },
|
||||||
|
'XLM-Roberta-Large-Vit-L-14': { dimSize: 768 },
|
||||||
|
'XLM-Roberta-Large-ViT-H-14__frozen_laion5b_s13b_b90k': { dimSize: 1024 },
|
||||||
|
'nllb-clip-base-siglip__v1': { dimSize: 768 },
|
||||||
|
'nllb-clip-large-siglip__v1': { dimSize: 1152 },
|
||||||
|
};
|
@ -7,10 +7,10 @@ import {
|
|||||||
ActivityResponseDto,
|
ActivityResponseDto,
|
||||||
ActivitySearchDto,
|
ActivitySearchDto,
|
||||||
ActivityStatisticsResponseDto,
|
ActivityStatisticsResponseDto,
|
||||||
} from 'src/domain/activity/activity.dto';
|
} from 'src/dtos/activity.dto';
|
||||||
import { ActivityService } from 'src/domain/activity/activity.service';
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
import { AuthDto } from 'src/domain/auth/auth.dto';
|
|
||||||
import { Auth, Authenticated } from 'src/middleware/auth.guard';
|
import { Auth, Authenticated } from 'src/middleware/auth.guard';
|
||||||
|
import { ActivityService } from 'src/services/activity.service';
|
||||||
import { UUIDParamDto } from 'src/validation';
|
import { UUIDParamDto } from 'src/validation';
|
||||||
|
|
||||||
@ApiTags('Activity')
|
@ApiTags('Activity')
|
||||||
|
@ -1,15 +1,18 @@
|
|||||||
import { Body, Controller, Delete, Get, Param, Patch, Post, Put, Query } from '@nestjs/common';
|
import { Body, Controller, Delete, Get, Param, Patch, Post, Put, Query } from '@nestjs/common';
|
||||||
import { ApiTags } from '@nestjs/swagger';
|
import { ApiTags } from '@nestjs/swagger';
|
||||||
import { AlbumCountResponseDto, AlbumResponseDto } from 'src/domain/album/album-response.dto';
|
import {
|
||||||
import { AlbumService } from 'src/domain/album/album.service';
|
AddUsersDto,
|
||||||
import { AddUsersDto } from 'src/domain/album/dto/album-add-users.dto';
|
AlbumCountResponseDto,
|
||||||
import { CreateAlbumDto } from 'src/domain/album/dto/album-create.dto';
|
AlbumInfoDto,
|
||||||
import { UpdateAlbumDto } from 'src/domain/album/dto/album-update.dto';
|
AlbumResponseDto,
|
||||||
import { AlbumInfoDto } from 'src/domain/album/dto/album.dto';
|
CreateAlbumDto,
|
||||||
import { GetAlbumsDto } from 'src/domain/album/dto/get-albums.dto';
|
GetAlbumsDto,
|
||||||
import { BulkIdResponseDto, BulkIdsDto } from 'src/domain/asset/response-dto/asset-ids-response.dto';
|
UpdateAlbumDto,
|
||||||
import { AuthDto } from 'src/domain/auth/auth.dto';
|
} from 'src/dtos/album.dto';
|
||||||
|
import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
|
||||||
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
import { Auth, Authenticated, SharedLinkRoute } from 'src/middleware/auth.guard';
|
import { Auth, Authenticated, SharedLinkRoute } from 'src/middleware/auth.guard';
|
||||||
|
import { AlbumService } from 'src/services/album.service';
|
||||||
import { ParseMeUUIDPipe, UUIDParamDto } from 'src/validation';
|
import { ParseMeUUIDPipe, UUIDParamDto } from 'src/validation';
|
||||||
|
|
||||||
@ApiTags('Album')
|
@ApiTags('Album')
|
||||||
|
@ -1,14 +1,9 @@
|
|||||||
import { Body, Controller, Delete, Get, Param, Post, Put } from '@nestjs/common';
|
import { Body, Controller, Delete, Get, Param, Post, Put } from '@nestjs/common';
|
||||||
import { ApiTags } from '@nestjs/swagger';
|
import { ApiTags } from '@nestjs/swagger';
|
||||||
import {
|
import { APIKeyCreateDto, APIKeyCreateResponseDto, APIKeyResponseDto, APIKeyUpdateDto } from 'src/dtos/api-key.dto';
|
||||||
APIKeyCreateDto,
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
APIKeyCreateResponseDto,
|
|
||||||
APIKeyResponseDto,
|
|
||||||
APIKeyUpdateDto,
|
|
||||||
} from 'src/domain/api-key/api-key.dto';
|
|
||||||
import { APIKeyService } from 'src/domain/api-key/api-key.service';
|
|
||||||
import { AuthDto } from 'src/domain/auth/auth.dto';
|
|
||||||
import { Auth, Authenticated } from 'src/middleware/auth.guard';
|
import { Auth, Authenticated } from 'src/middleware/auth.guard';
|
||||||
|
import { APIKeyService } from 'src/services/api-key.service';
|
||||||
import { UUIDParamDto } from 'src/validation';
|
import { UUIDParamDto } from 'src/validation';
|
||||||
|
|
||||||
@ApiTags('API Key')
|
@ApiTags('API Key')
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { Controller, Get, Header } from '@nestjs/common';
|
import { Controller, Get, Header } from '@nestjs/common';
|
||||||
import { ApiExcludeEndpoint } from '@nestjs/swagger';
|
import { ApiExcludeEndpoint } from '@nestjs/swagger';
|
||||||
import { SystemConfigService } from 'src/domain/system-config/system-config.service';
|
|
||||||
import { PublicRoute } from 'src/middleware/auth.guard';
|
import { PublicRoute } from 'src/middleware/auth.guard';
|
||||||
|
import { SystemConfigService } from 'src/services/system-config.service';
|
||||||
|
|
||||||
@Controller()
|
@Controller()
|
||||||
export class AppController {
|
export class AppController {
|
||||||
|
@ -15,23 +15,27 @@ import {
|
|||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { ApiBody, ApiConsumes, ApiHeader, ApiTags } from '@nestjs/swagger';
|
import { ApiBody, ApiConsumes, ApiHeader, ApiTags } from '@nestjs/swagger';
|
||||||
import { NextFunction, Response } from 'express';
|
import { NextFunction, Response } from 'express';
|
||||||
import { AssetResponseDto } from 'src/domain/asset/response-dto/asset-response.dto';
|
import { AssetResponseDto } from 'src/dtos/asset-response.dto';
|
||||||
import { AuthDto } from 'src/domain/auth/auth.dto';
|
import {
|
||||||
import { AssetService as AssetServiceV1 } from 'src/immich/api-v1/asset/asset.service';
|
AssetBulkUploadCheckResponseDto,
|
||||||
import { AssetBulkUploadCheckDto } from 'src/immich/api-v1/asset/dto/asset-check.dto';
|
AssetFileUploadResponseDto,
|
||||||
import { AssetSearchDto } from 'src/immich/api-v1/asset/dto/asset-search.dto';
|
CheckExistingAssetsResponseDto,
|
||||||
import { CheckExistingAssetsDto } from 'src/immich/api-v1/asset/dto/check-existing-assets.dto';
|
CuratedLocationsResponseDto,
|
||||||
import { CreateAssetDto } from 'src/immich/api-v1/asset/dto/create-asset.dto';
|
CuratedObjectsResponseDto,
|
||||||
import { GetAssetThumbnailDto } from 'src/immich/api-v1/asset/dto/get-asset-thumbnail.dto';
|
} from 'src/dtos/asset-v1-response.dto';
|
||||||
import { ServeFileDto } from 'src/immich/api-v1/asset/dto/serve-file.dto';
|
import {
|
||||||
import { AssetBulkUploadCheckResponseDto } from 'src/immich/api-v1/asset/response-dto/asset-check-response.dto';
|
AssetBulkUploadCheckDto,
|
||||||
import { AssetFileUploadResponseDto } from 'src/immich/api-v1/asset/response-dto/asset-file-upload-response.dto';
|
AssetSearchDto,
|
||||||
import { CheckExistingAssetsResponseDto } from 'src/immich/api-v1/asset/response-dto/check-existing-assets-response.dto';
|
CheckExistingAssetsDto,
|
||||||
import { CuratedLocationsResponseDto } from 'src/immich/api-v1/asset/response-dto/curated-locations-response.dto';
|
CreateAssetDto,
|
||||||
import { CuratedObjectsResponseDto } from 'src/immich/api-v1/asset/response-dto/curated-objects-response.dto';
|
GetAssetThumbnailDto,
|
||||||
import { sendFile } from 'src/immich/app.utils';
|
ServeFileDto,
|
||||||
|
} from 'src/dtos/asset-v1.dto';
|
||||||
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
import { Auth, Authenticated, FileResponse, SharedLinkRoute } from 'src/middleware/auth.guard';
|
import { Auth, Authenticated, FileResponse, SharedLinkRoute } from 'src/middleware/auth.guard';
|
||||||
import { FileUploadInterceptor, ImmichFile, Route, mapToUploadFile } from 'src/middleware/file-upload.interceptor';
|
import { FileUploadInterceptor, ImmichFile, Route, mapToUploadFile } from 'src/middleware/file-upload.interceptor';
|
||||||
|
import { AssetServiceV1 } from 'src/services/asset-v1.service';
|
||||||
|
import { sendFile } from 'src/utils/file';
|
||||||
import { FileNotEmptyValidator, UUIDParamDto } from 'src/validation';
|
import { FileNotEmptyValidator, UUIDParamDto } from 'src/validation';
|
||||||
|
|
||||||
interface UploadFiles {
|
interface UploadFiles {
|
||||||
@ -43,8 +47,8 @@ interface UploadFiles {
|
|||||||
@ApiTags('Asset')
|
@ApiTags('Asset')
|
||||||
@Controller(Route.ASSET)
|
@Controller(Route.ASSET)
|
||||||
@Authenticated()
|
@Authenticated()
|
||||||
export class AssetController {
|
export class AssetControllerV1 {
|
||||||
constructor(private serviceV1: AssetServiceV1) {}
|
constructor(private service: AssetServiceV1) {}
|
||||||
|
|
||||||
@SharedLinkRoute()
|
@SharedLinkRoute()
|
||||||
@Post('upload')
|
@Post('upload')
|
||||||
@ -73,7 +77,7 @@ export class AssetController {
|
|||||||
sidecarFile = mapToUploadFile(_sidecarFile);
|
sidecarFile = mapToUploadFile(_sidecarFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
const responseDto = await this.serviceV1.uploadFile(auth, dto, file, livePhotoFile, sidecarFile);
|
const responseDto = await this.service.uploadFile(auth, dto, file, livePhotoFile, sidecarFile);
|
||||||
if (responseDto.duplicate) {
|
if (responseDto.duplicate) {
|
||||||
res.status(HttpStatus.OK);
|
res.status(HttpStatus.OK);
|
||||||
}
|
}
|
||||||
@ -91,7 +95,7 @@ export class AssetController {
|
|||||||
@Param() { id }: UUIDParamDto,
|
@Param() { id }: UUIDParamDto,
|
||||||
@Query() dto: ServeFileDto,
|
@Query() dto: ServeFileDto,
|
||||||
) {
|
) {
|
||||||
await sendFile(res, next, () => this.serviceV1.serveFile(auth, id, dto));
|
await sendFile(res, next, () => this.service.serveFile(auth, id, dto));
|
||||||
}
|
}
|
||||||
|
|
||||||
@SharedLinkRoute()
|
@SharedLinkRoute()
|
||||||
@ -104,22 +108,22 @@ export class AssetController {
|
|||||||
@Param() { id }: UUIDParamDto,
|
@Param() { id }: UUIDParamDto,
|
||||||
@Query() dto: GetAssetThumbnailDto,
|
@Query() dto: GetAssetThumbnailDto,
|
||||||
) {
|
) {
|
||||||
await sendFile(res, next, () => this.serviceV1.serveThumbnail(auth, id, dto));
|
await sendFile(res, next, () => this.service.serveThumbnail(auth, id, dto));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('/curated-objects')
|
@Get('/curated-objects')
|
||||||
getCuratedObjects(@Auth() auth: AuthDto): Promise<CuratedObjectsResponseDto[]> {
|
getCuratedObjects(@Auth() auth: AuthDto): Promise<CuratedObjectsResponseDto[]> {
|
||||||
return this.serviceV1.getCuratedObject(auth);
|
return this.service.getCuratedObject(auth);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('/curated-locations')
|
@Get('/curated-locations')
|
||||||
getCuratedLocations(@Auth() auth: AuthDto): Promise<CuratedLocationsResponseDto[]> {
|
getCuratedLocations(@Auth() auth: AuthDto): Promise<CuratedLocationsResponseDto[]> {
|
||||||
return this.serviceV1.getCuratedLocation(auth);
|
return this.service.getCuratedLocation(auth);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('/search-terms')
|
@Get('/search-terms')
|
||||||
getAssetSearchTerms(@Auth() auth: AuthDto): Promise<string[]> {
|
getAssetSearchTerms(@Auth() auth: AuthDto): Promise<string[]> {
|
||||||
return this.serviceV1.getAssetSearchTerm(auth);
|
return this.service.getAssetSearchTerm(auth);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -133,7 +137,7 @@ export class AssetController {
|
|||||||
schema: { type: 'string' },
|
schema: { type: 'string' },
|
||||||
})
|
})
|
||||||
getAllAssets(@Auth() auth: AuthDto, @Query() dto: AssetSearchDto): Promise<AssetResponseDto[]> {
|
getAllAssets(@Auth() auth: AuthDto, @Query() dto: AssetSearchDto): Promise<AssetResponseDto[]> {
|
||||||
return this.serviceV1.getAllAssets(auth, dto);
|
return this.service.getAllAssets(auth, dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -145,7 +149,7 @@ export class AssetController {
|
|||||||
@Auth() auth: AuthDto,
|
@Auth() auth: AuthDto,
|
||||||
@Body() dto: CheckExistingAssetsDto,
|
@Body() dto: CheckExistingAssetsDto,
|
||||||
): Promise<CheckExistingAssetsResponseDto> {
|
): Promise<CheckExistingAssetsResponseDto> {
|
||||||
return this.serviceV1.checkExistingAssets(auth, dto);
|
return this.service.checkExistingAssets(auth, dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -157,6 +161,6 @@ export class AssetController {
|
|||||||
@Auth() auth: AuthDto,
|
@Auth() auth: AuthDto,
|
||||||
@Body() dto: AssetBulkUploadCheckDto,
|
@Body() dto: AssetBulkUploadCheckDto,
|
||||||
): Promise<AssetBulkUploadCheckResponseDto> {
|
): Promise<AssetBulkUploadCheckResponseDto> {
|
||||||
return this.serviceV1.bulkUploadCheck(auth, dto);
|
return this.service.bulkUploadCheck(auth, dto);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,27 +1,24 @@
|
|||||||
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put, Query } from '@nestjs/common';
|
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put, Query } from '@nestjs/common';
|
||||||
import { ApiOperation, ApiTags } from '@nestjs/swagger';
|
import { ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||||
import { AssetService } from 'src/domain/asset/asset.service';
|
import { AssetResponseDto, MemoryLaneResponseDto } from 'src/dtos/asset-response.dto';
|
||||||
import { AssetJobsDto } from 'src/domain/asset/dto/asset-ids.dto';
|
|
||||||
import { UpdateStackParentDto } from 'src/domain/asset/dto/asset-stack.dto';
|
|
||||||
import { AssetStatsDto, AssetStatsResponseDto } from 'src/domain/asset/dto/asset-statistics.dto';
|
|
||||||
import {
|
import {
|
||||||
AssetBulkDeleteDto,
|
AssetBulkDeleteDto,
|
||||||
AssetBulkUpdateDto,
|
AssetBulkUpdateDto,
|
||||||
|
AssetJobsDto,
|
||||||
|
AssetStatsDto,
|
||||||
|
AssetStatsResponseDto,
|
||||||
DeviceIdDto,
|
DeviceIdDto,
|
||||||
RandomAssetsDto,
|
RandomAssetsDto,
|
||||||
UpdateAssetDto,
|
UpdateAssetDto,
|
||||||
} from 'src/domain/asset/dto/asset.dto';
|
} from 'src/dtos/asset.dto';
|
||||||
import { MapMarkerDto } from 'src/domain/asset/dto/map-marker.dto';
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
import { MemoryLaneDto } from 'src/domain/asset/dto/memory-lane.dto';
|
import { MapMarkerDto, MapMarkerResponseDto, MemoryLaneDto, MetadataSearchDto } from 'src/dtos/search.dto';
|
||||||
import { TimeBucketAssetDto, TimeBucketDto } from 'src/domain/asset/dto/time-bucket.dto';
|
import { UpdateStackParentDto } from 'src/dtos/stack.dto';
|
||||||
import { AssetResponseDto, MemoryLaneResponseDto } from 'src/domain/asset/response-dto/asset-response.dto';
|
import { TimeBucketAssetDto, TimeBucketDto, TimeBucketResponseDto } from 'src/dtos/time-bucket.dto';
|
||||||
import { MapMarkerResponseDto } from 'src/domain/asset/response-dto/map-marker-response.dto';
|
|
||||||
import { TimeBucketResponseDto } from 'src/domain/asset/response-dto/time-bucket-response.dto';
|
|
||||||
import { AuthDto } from 'src/domain/auth/auth.dto';
|
|
||||||
import { MetadataSearchDto } from 'src/domain/search/dto/search.dto';
|
|
||||||
import { SearchService } from 'src/domain/search/search.service';
|
|
||||||
import { Auth, Authenticated, SharedLinkRoute } from 'src/middleware/auth.guard';
|
import { Auth, Authenticated, SharedLinkRoute } from 'src/middleware/auth.guard';
|
||||||
import { Route } from 'src/middleware/file-upload.interceptor';
|
import { Route } from 'src/middleware/file-upload.interceptor';
|
||||||
|
import { AssetService } from 'src/services/asset.service';
|
||||||
|
import { SearchService } from 'src/services/search.service';
|
||||||
import { UUIDParamDto } from 'src/validation';
|
import { UUIDParamDto } from 'src/validation';
|
||||||
|
|
||||||
@ApiTags('Asset')
|
@ApiTags('Asset')
|
||||||
|
@ -7,10 +7,10 @@ import {
|
|||||||
FileChecksumResponseDto,
|
FileChecksumResponseDto,
|
||||||
FileReportDto,
|
FileReportDto,
|
||||||
FileReportFixDto,
|
FileReportFixDto,
|
||||||
} from 'src/domain/audit/audit.dto';
|
} from 'src/dtos/audit.dto';
|
||||||
import { AuditService } from 'src/domain/audit/audit.service';
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
import { AuthDto } from 'src/domain/auth/auth.dto';
|
|
||||||
import { AdminRoute, Auth, Authenticated } from 'src/middleware/auth.guard';
|
import { AdminRoute, Auth, Authenticated } from 'src/middleware/auth.guard';
|
||||||
|
import { AuditService } from 'src/services/audit.service';
|
||||||
|
|
||||||
@ApiTags('Audit')
|
@ApiTags('Audit')
|
||||||
@Controller('audit')
|
@Controller('audit')
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Req, Res } from '@nestjs/common';
|
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Req, Res } from '@nestjs/common';
|
||||||
import { ApiTags } from '@nestjs/swagger';
|
import { ApiTags } from '@nestjs/swagger';
|
||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import { IMMICH_ACCESS_COOKIE, IMMICH_AUTH_TYPE_COOKIE, IMMICH_IS_AUTHENTICATED } from 'src/domain/auth/auth.constant';
|
import { IMMICH_ACCESS_COOKIE, IMMICH_AUTH_TYPE_COOKIE, IMMICH_IS_AUTHENTICATED } from 'src/constants';
|
||||||
import {
|
import {
|
||||||
AuthDeviceResponseDto,
|
AuthDeviceResponseDto,
|
||||||
AuthDto,
|
AuthDto,
|
||||||
@ -11,10 +11,10 @@ import {
|
|||||||
LogoutResponseDto,
|
LogoutResponseDto,
|
||||||
SignUpDto,
|
SignUpDto,
|
||||||
ValidateAccessTokenResponseDto,
|
ValidateAccessTokenResponseDto,
|
||||||
} from 'src/domain/auth/auth.dto';
|
} from 'src/dtos/auth.dto';
|
||||||
import { AuthService, LoginDetails } from 'src/domain/auth/auth.service';
|
import { UserResponseDto, mapUser } from 'src/dtos/user.dto';
|
||||||
import { UserResponseDto, mapUser } from 'src/domain/user/response-dto/user-response.dto';
|
|
||||||
import { Auth, Authenticated, GetLoginDetails, PublicRoute } from 'src/middleware/auth.guard';
|
import { Auth, Authenticated, GetLoginDetails, PublicRoute } from 'src/middleware/auth.guard';
|
||||||
|
import { AuthService, LoginDetails } from 'src/services/auth.service';
|
||||||
import { UUIDParamDto } from 'src/validation';
|
import { UUIDParamDto } from 'src/validation';
|
||||||
|
|
||||||
@ApiTags('Authentication')
|
@ApiTags('Authentication')
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import { Body, Controller, HttpCode, HttpStatus, Next, Param, Post, Res, StreamableFile } from '@nestjs/common';
|
import { Body, Controller, HttpCode, HttpStatus, Next, Param, Post, Res, StreamableFile } from '@nestjs/common';
|
||||||
import { ApiTags } from '@nestjs/swagger';
|
import { ApiTags } from '@nestjs/swagger';
|
||||||
import { NextFunction, Response } from 'express';
|
import { NextFunction, Response } from 'express';
|
||||||
import { AssetIdsDto } from 'src/domain/asset/dto/asset-ids.dto';
|
import { AssetIdsDto } from 'src/dtos/asset.dto';
|
||||||
import { AuthDto } from 'src/domain/auth/auth.dto';
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
import { DownloadInfoDto, DownloadResponseDto } from 'src/domain/download/download.dto';
|
import { DownloadInfoDto, DownloadResponseDto } from 'src/dtos/download.dto';
|
||||||
import { DownloadService } from 'src/domain/download/download.service';
|
|
||||||
import { asStreamableFile, sendFile } from 'src/immich/app.utils';
|
|
||||||
import { Auth, Authenticated, FileResponse, SharedLinkRoute } from 'src/middleware/auth.guard';
|
import { Auth, Authenticated, FileResponse, SharedLinkRoute } from 'src/middleware/auth.guard';
|
||||||
|
import { DownloadService } from 'src/services/download.service';
|
||||||
|
import { asStreamableFile, sendFile } from 'src/utils/file';
|
||||||
import { UUIDParamDto } from 'src/validation';
|
import { UUIDParamDto } from 'src/validation';
|
||||||
|
|
||||||
@ApiTags('Download')
|
@ApiTags('Download')
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { Body, Controller, Get, Param, Put, Query } from '@nestjs/common';
|
import { Body, Controller, Get, Param, Put, Query } from '@nestjs/common';
|
||||||
import { ApiTags } from '@nestjs/swagger';
|
import { ApiTags } from '@nestjs/swagger';
|
||||||
import { AuthDto } from 'src/domain/auth/auth.dto';
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
import { AssetFaceResponseDto, FaceDto, PersonResponseDto } from 'src/domain/person/person.dto';
|
import { AssetFaceResponseDto, FaceDto, PersonResponseDto } from 'src/dtos/person.dto';
|
||||||
import { PersonService } from 'src/domain/person/person.service';
|
|
||||||
import { Auth, Authenticated } from 'src/middleware/auth.guard';
|
import { Auth, Authenticated } from 'src/middleware/auth.guard';
|
||||||
|
import { PersonService } from 'src/services/person.service';
|
||||||
import { UUIDParamDto } from 'src/validation';
|
import { UUIDParamDto } from 'src/validation';
|
||||||
|
|
||||||
@ApiTags('Face')
|
@ApiTags('Face')
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { Body, Controller, Get, Param, Put } from '@nestjs/common';
|
import { Body, Controller, Get, Param, Put } from '@nestjs/common';
|
||||||
import { ApiTags } from '@nestjs/swagger';
|
import { ApiTags } from '@nestjs/swagger';
|
||||||
import { AllJobStatusResponseDto, JobCommandDto, JobIdParamDto, JobStatusDto } from 'src/domain/job/job.dto';
|
import { AllJobStatusResponseDto, JobCommandDto, JobIdParamDto, JobStatusDto } from 'src/dtos/job.dto';
|
||||||
import { JobService } from 'src/domain/job/job.service';
|
|
||||||
import { Authenticated } from 'src/middleware/auth.guard';
|
import { Authenticated } from 'src/middleware/auth.guard';
|
||||||
|
import { JobService } from 'src/services/job.service';
|
||||||
|
|
||||||
@ApiTags('Job')
|
@ApiTags('Job')
|
||||||
@Controller('jobs')
|
@Controller('jobs')
|
||||||
|
@ -9,9 +9,9 @@ import {
|
|||||||
UpdateLibraryDto,
|
UpdateLibraryDto,
|
||||||
ValidateLibraryDto,
|
ValidateLibraryDto,
|
||||||
ValidateLibraryResponseDto,
|
ValidateLibraryResponseDto,
|
||||||
} from 'src/domain/library/library.dto';
|
} from 'src/dtos/library.dto';
|
||||||
import { LibraryService } from 'src/domain/library/library.service';
|
|
||||||
import { AdminRoute, Authenticated } from 'src/middleware/auth.guard';
|
import { AdminRoute, Authenticated } from 'src/middleware/auth.guard';
|
||||||
|
import { LibraryService } from 'src/services/library.service';
|
||||||
import { UUIDParamDto } from 'src/validation';
|
import { UUIDParamDto } from 'src/validation';
|
||||||
|
|
||||||
@ApiTags('Library')
|
@ApiTags('Library')
|
||||||
|
@ -7,10 +7,10 @@ import {
|
|||||||
OAuthAuthorizeResponseDto,
|
OAuthAuthorizeResponseDto,
|
||||||
OAuthCallbackDto,
|
OAuthCallbackDto,
|
||||||
OAuthConfigDto,
|
OAuthConfigDto,
|
||||||
} from 'src/domain/auth/auth.dto';
|
} from 'src/dtos/auth.dto';
|
||||||
import { AuthService, LoginDetails } from 'src/domain/auth/auth.service';
|
import { UserResponseDto } from 'src/dtos/user.dto';
|
||||||
import { UserResponseDto } from 'src/domain/user/response-dto/user-response.dto';
|
|
||||||
import { Auth, Authenticated, GetLoginDetails, PublicRoute } from 'src/middleware/auth.guard';
|
import { Auth, Authenticated, GetLoginDetails, PublicRoute } from 'src/middleware/auth.guard';
|
||||||
|
import { AuthService, LoginDetails } from 'src/services/auth.service';
|
||||||
|
|
||||||
@ApiTags('OAuth')
|
@ApiTags('OAuth')
|
||||||
@Controller('oauth')
|
@Controller('oauth')
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import { Body, Controller, Delete, Get, Param, Post, Put, Query } from '@nestjs/common';
|
import { Body, Controller, Delete, Get, Param, Post, Put, Query } from '@nestjs/common';
|
||||||
import { ApiQuery, ApiTags } from '@nestjs/swagger';
|
import { ApiQuery, ApiTags } from '@nestjs/swagger';
|
||||||
import { AuthDto } from 'src/domain/auth/auth.dto';
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
import { PartnerResponseDto, UpdatePartnerDto } from 'src/domain/partner/partner.dto';
|
import { PartnerResponseDto, UpdatePartnerDto } from 'src/dtos/partner.dto';
|
||||||
import { PartnerService } from 'src/domain/partner/partner.service';
|
import { PartnerDirection } from 'src/interfaces/partner.interface';
|
||||||
import { PartnerDirection } from 'src/interfaces/partner.repository';
|
|
||||||
import { Auth, Authenticated } from 'src/middleware/auth.guard';
|
import { Auth, Authenticated } from 'src/middleware/auth.guard';
|
||||||
|
import { PartnerService } from 'src/services/partner.service';
|
||||||
import { UUIDParamDto } from 'src/validation';
|
import { UUIDParamDto } from 'src/validation';
|
||||||
|
|
||||||
@ApiTags('Partner')
|
@ApiTags('Partner')
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { Body, Controller, Get, Next, Param, Post, Put, Query, Res } from '@nestjs/common';
|
import { Body, Controller, Get, Next, Param, Post, Put, Query, Res } from '@nestjs/common';
|
||||||
import { ApiTags } from '@nestjs/swagger';
|
import { ApiTags } from '@nestjs/swagger';
|
||||||
import { NextFunction, Response } from 'express';
|
import { NextFunction, Response } from 'express';
|
||||||
import { BulkIdResponseDto } from 'src/domain/asset/response-dto/asset-ids-response.dto';
|
import { BulkIdResponseDto } from 'src/dtos/asset-ids.response.dto';
|
||||||
import { AssetResponseDto } from 'src/domain/asset/response-dto/asset-response.dto';
|
import { AssetResponseDto } from 'src/dtos/asset-response.dto';
|
||||||
import { AuthDto } from 'src/domain/auth/auth.dto';
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
import {
|
import {
|
||||||
AssetFaceUpdateDto,
|
AssetFaceUpdateDto,
|
||||||
MergePersonDto,
|
MergePersonDto,
|
||||||
@ -14,10 +14,10 @@ import {
|
|||||||
PersonSearchDto,
|
PersonSearchDto,
|
||||||
PersonStatisticsResponseDto,
|
PersonStatisticsResponseDto,
|
||||||
PersonUpdateDto,
|
PersonUpdateDto,
|
||||||
} from 'src/domain/person/person.dto';
|
} from 'src/dtos/person.dto';
|
||||||
import { PersonService } from 'src/domain/person/person.service';
|
|
||||||
import { sendFile } from 'src/immich/app.utils';
|
|
||||||
import { Auth, Authenticated, FileResponse } from 'src/middleware/auth.guard';
|
import { Auth, Authenticated, FileResponse } from 'src/middleware/auth.guard';
|
||||||
|
import { PersonService } from 'src/services/person.service';
|
||||||
|
import { sendFile } from 'src/utils/file';
|
||||||
import { UUIDParamDto } from 'src/validation';
|
import { UUIDParamDto } from 'src/validation';
|
||||||
|
|
||||||
@ApiTags('Person')
|
@ApiTags('Person')
|
||||||
|
@ -1,21 +1,21 @@
|
|||||||
import { Body, Controller, Get, HttpCode, HttpStatus, Post, Query } from '@nestjs/common';
|
import { Body, Controller, Get, HttpCode, HttpStatus, Post, Query } from '@nestjs/common';
|
||||||
import { ApiOperation, ApiTags } from '@nestjs/swagger';
|
import { ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||||
import { AssetResponseDto } from 'src/domain/asset/response-dto/asset-response.dto';
|
import { AssetResponseDto } from 'src/dtos/asset-response.dto';
|
||||||
import { AuthDto } from 'src/domain/auth/auth.dto';
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
import { PersonResponseDto } from 'src/domain/person/person.dto';
|
import { PersonResponseDto } from 'src/dtos/person.dto';
|
||||||
import { SearchSuggestionRequestDto } from 'src/domain/search/dto/search-suggestion.dto';
|
|
||||||
import {
|
import {
|
||||||
MetadataSearchDto,
|
MetadataSearchDto,
|
||||||
PlacesResponseDto,
|
PlacesResponseDto,
|
||||||
SearchDto,
|
SearchDto,
|
||||||
|
SearchExploreResponseDto,
|
||||||
SearchPeopleDto,
|
SearchPeopleDto,
|
||||||
SearchPlacesDto,
|
SearchPlacesDto,
|
||||||
|
SearchResponseDto,
|
||||||
|
SearchSuggestionRequestDto,
|
||||||
SmartSearchDto,
|
SmartSearchDto,
|
||||||
} from 'src/domain/search/dto/search.dto';
|
} from 'src/dtos/search.dto';
|
||||||
import { SearchExploreResponseDto } from 'src/domain/search/response-dto/search-explore.response.dto';
|
|
||||||
import { SearchResponseDto } from 'src/domain/search/response-dto/search-response.dto';
|
|
||||||
import { SearchService } from 'src/domain/search/search.service';
|
|
||||||
import { Auth, Authenticated } from 'src/middleware/auth.guard';
|
import { Auth, Authenticated } from 'src/middleware/auth.guard';
|
||||||
|
import { SearchService } from 'src/services/search.service';
|
||||||
|
|
||||||
@ApiTags('Search')
|
@ApiTags('Search')
|
||||||
@Controller('search')
|
@Controller('search')
|
||||||
|
@ -9,9 +9,9 @@ import {
|
|||||||
ServerStatsResponseDto,
|
ServerStatsResponseDto,
|
||||||
ServerThemeDto,
|
ServerThemeDto,
|
||||||
ServerVersionResponseDto,
|
ServerVersionResponseDto,
|
||||||
} from 'src/domain/server-info/server-info.dto';
|
} from 'src/dtos/server-info.dto';
|
||||||
import { ServerInfoService } from 'src/domain/server-info/server-info.service';
|
|
||||||
import { AdminRoute, Authenticated, PublicRoute } from 'src/middleware/auth.guard';
|
import { AdminRoute, Authenticated, PublicRoute } from 'src/middleware/auth.guard';
|
||||||
|
import { ServerInfoService } from 'src/services/server-info.service';
|
||||||
|
|
||||||
@ApiTags('Server Info')
|
@ApiTags('Server Info')
|
||||||
@Controller('server-info')
|
@Controller('server-info')
|
||||||
|
@ -1,14 +1,18 @@
|
|||||||
import { Body, Controller, Delete, Get, Param, Patch, Post, Put, Query, Req, Res } from '@nestjs/common';
|
import { Body, Controller, Delete, Get, Param, Patch, Post, Put, Query, Req, Res } from '@nestjs/common';
|
||||||
import { ApiTags } from '@nestjs/swagger';
|
import { ApiTags } from '@nestjs/swagger';
|
||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import { AssetIdsDto } from 'src/domain/asset/dto/asset-ids.dto';
|
import { IMMICH_SHARED_LINK_ACCESS_COOKIE } from 'src/constants';
|
||||||
import { AssetIdsResponseDto } from 'src/domain/asset/response-dto/asset-ids-response.dto';
|
import { AssetIdsResponseDto } from 'src/dtos/asset-ids.response.dto';
|
||||||
import { IMMICH_SHARED_LINK_ACCESS_COOKIE } from 'src/domain/auth/auth.constant';
|
import { AssetIdsDto } from 'src/dtos/asset.dto';
|
||||||
import { AuthDto } from 'src/domain/auth/auth.dto';
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
import { SharedLinkResponseDto } from 'src/domain/shared-link/shared-link-response.dto';
|
import {
|
||||||
import { SharedLinkCreateDto, SharedLinkEditDto, SharedLinkPasswordDto } from 'src/domain/shared-link/shared-link.dto';
|
SharedLinkCreateDto,
|
||||||
import { SharedLinkService } from 'src/domain/shared-link/shared-link.service';
|
SharedLinkEditDto,
|
||||||
|
SharedLinkPasswordDto,
|
||||||
|
SharedLinkResponseDto,
|
||||||
|
} from 'src/dtos/shared-link.dto';
|
||||||
import { Auth, Authenticated, SharedLinkRoute } from 'src/middleware/auth.guard';
|
import { Auth, Authenticated, SharedLinkRoute } from 'src/middleware/auth.guard';
|
||||||
|
import { SharedLinkService } from 'src/services/shared-link.service';
|
||||||
import { UUIDParamDto } from 'src/validation';
|
import { UUIDParamDto } from 'src/validation';
|
||||||
|
|
||||||
@ApiTags('Shared Link')
|
@ApiTags('Shared Link')
|
||||||
|
@ -1,10 +1,8 @@
|
|||||||
import { Body, Controller, Get, Put, Query } from '@nestjs/common';
|
import { Body, Controller, Get, Put, Query } from '@nestjs/common';
|
||||||
import { ApiTags } from '@nestjs/swagger';
|
import { ApiTags } from '@nestjs/swagger';
|
||||||
import { SystemConfigDto } from 'src/domain/system-config/dto/system-config.dto';
|
import { MapThemeDto, SystemConfigDto, SystemConfigTemplateStorageOptionDto } from 'src/dtos/system-config.dto';
|
||||||
import { SystemConfigTemplateStorageOptionDto } from 'src/domain/system-config/response-dto/system-config-template-storage-option.dto';
|
|
||||||
import { MapThemeDto } from 'src/domain/system-config/system-config-map-theme.dto';
|
|
||||||
import { SystemConfigService } from 'src/domain/system-config/system-config.service';
|
|
||||||
import { AdminRoute, Authenticated } from 'src/middleware/auth.guard';
|
import { AdminRoute, Authenticated } from 'src/middleware/auth.guard';
|
||||||
|
import { SystemConfigService } from 'src/services/system-config.service';
|
||||||
|
|
||||||
@ApiTags('System Config')
|
@ApiTags('System Config')
|
||||||
@Controller('system-config')
|
@Controller('system-config')
|
||||||
|
@ -1,13 +1,12 @@
|
|||||||
import { Body, Controller, Delete, Get, Param, Patch, Post, Put } from '@nestjs/common';
|
import { Body, Controller, Delete, Get, Param, Patch, Post, Put } from '@nestjs/common';
|
||||||
import { ApiTags } from '@nestjs/swagger';
|
import { ApiTags } from '@nestjs/swagger';
|
||||||
import { AssetIdsDto } from 'src/domain/asset/dto/asset-ids.dto';
|
import { AssetIdsResponseDto } from 'src/dtos/asset-ids.response.dto';
|
||||||
import { AssetIdsResponseDto } from 'src/domain/asset/response-dto/asset-ids-response.dto';
|
import { AssetResponseDto } from 'src/dtos/asset-response.dto';
|
||||||
import { AssetResponseDto } from 'src/domain/asset/response-dto/asset-response.dto';
|
import { AssetIdsDto } from 'src/dtos/asset.dto';
|
||||||
import { AuthDto } from 'src/domain/auth/auth.dto';
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
import { TagResponseDto } from 'src/domain/tag/tag-response.dto';
|
import { CreateTagDto, TagResponseDto, UpdateTagDto } from 'src/dtos/tag.dto';
|
||||||
import { CreateTagDto, UpdateTagDto } from 'src/domain/tag/tag.dto';
|
|
||||||
import { TagService } from 'src/domain/tag/tag.service';
|
|
||||||
import { Auth, Authenticated } from 'src/middleware/auth.guard';
|
import { Auth, Authenticated } from 'src/middleware/auth.guard';
|
||||||
|
import { TagService } from 'src/services/tag.service';
|
||||||
import { UUIDParamDto } from 'src/validation';
|
import { UUIDParamDto } from 'src/validation';
|
||||||
|
|
||||||
@ApiTags('Tag')
|
@ApiTags('Tag')
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { Body, Controller, HttpCode, HttpStatus, Post } from '@nestjs/common';
|
import { Body, Controller, HttpCode, HttpStatus, Post } from '@nestjs/common';
|
||||||
import { ApiTags } from '@nestjs/swagger';
|
import { ApiTags } from '@nestjs/swagger';
|
||||||
import { BulkIdsDto } from 'src/domain/asset/response-dto/asset-ids-response.dto';
|
import { BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
|
||||||
import { AuthDto } from 'src/domain/auth/auth.dto';
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
import { TrashService } from 'src/domain/trash/trash.service';
|
|
||||||
import { Auth, Authenticated } from 'src/middleware/auth.guard';
|
import { Auth, Authenticated } from 'src/middleware/auth.guard';
|
||||||
|
import { TrashService } from 'src/services/trash.service';
|
||||||
|
|
||||||
@ApiTags('Trash')
|
@ApiTags('Trash')
|
||||||
@Controller('trash')
|
@Controller('trash')
|
||||||
|
@ -16,17 +16,13 @@ import {
|
|||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { ApiBody, ApiConsumes, ApiTags } from '@nestjs/swagger';
|
import { ApiBody, ApiConsumes, ApiTags } from '@nestjs/swagger';
|
||||||
import { NextFunction, Response } from 'express';
|
import { NextFunction, Response } from 'express';
|
||||||
import { AuthDto } from 'src/domain/auth/auth.dto';
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
import { CreateProfileImageDto } from 'src/domain/user/dto/create-profile-image.dto';
|
import { CreateProfileImageDto, CreateProfileImageResponseDto } from 'src/dtos/user-profile.dto';
|
||||||
import { CreateUserDto } from 'src/domain/user/dto/create-user.dto';
|
import { CreateUserDto, DeleteUserDto, UpdateUserDto, UserResponseDto } from 'src/dtos/user.dto';
|
||||||
import { DeleteUserDto } from 'src/domain/user/dto/delete-user.dto';
|
|
||||||
import { UpdateUserDto } from 'src/domain/user/dto/update-user.dto';
|
|
||||||
import { CreateProfileImageResponseDto } from 'src/domain/user/response-dto/create-profile-image-response.dto';
|
|
||||||
import { UserResponseDto } from 'src/domain/user/response-dto/user-response.dto';
|
|
||||||
import { UserService } from 'src/domain/user/user.service';
|
|
||||||
import { sendFile } from 'src/immich/app.utils';
|
|
||||||
import { AdminRoute, Auth, Authenticated, FileResponse } from 'src/middleware/auth.guard';
|
import { AdminRoute, Auth, Authenticated, FileResponse } from 'src/middleware/auth.guard';
|
||||||
import { FileUploadInterceptor, Route } from 'src/middleware/file-upload.interceptor';
|
import { FileUploadInterceptor, Route } from 'src/middleware/file-upload.interceptor';
|
||||||
|
import { UserService } from 'src/services/user.service';
|
||||||
|
import { sendFile } from 'src/utils/file';
|
||||||
import { UUIDParamDto } from 'src/validation';
|
import { UUIDParamDto } from 'src/validation';
|
||||||
|
|
||||||
@ApiTags('User')
|
@ApiTags('User')
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { BadRequestException, UnauthorizedException } from '@nestjs/common';
|
import { BadRequestException, UnauthorizedException } from '@nestjs/common';
|
||||||
import { AuthDto } from 'src/domain/auth/auth.dto';
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
import { SharedLinkEntity } from 'src/entities/shared-link.entity';
|
import { SharedLinkEntity } from 'src/entities/shared-link.entity';
|
||||||
import { IAccessRepository } from 'src/interfaces/access.repository';
|
import { IAccessRepository } from 'src/interfaces/access.interface';
|
||||||
import { setDifference, setIsEqual, setUnion } from 'src/utils';
|
import { setDifference, setIsEqual, setUnion } from 'src/utils/set';
|
||||||
|
|
||||||
export enum Permission {
|
export enum Permission {
|
||||||
ACTIVITY_CREATE = 'activity.create',
|
ACTIVITY_CREATE = 'activity.create',
|
||||||
|