diff --git a/.github/workflows/build-mobile.yml b/.github/workflows/build-mobile.yml index 24e0859d5..3da7cbe10 100644 --- a/.github/workflows/build-mobile.yml +++ b/.github/workflows/build-mobile.yml @@ -45,7 +45,7 @@ jobs: uses: subosito/flutter-action@v2 with: channel: "stable" - flutter-version: "3.16.9" + flutter-version: "3.19.3" cache: true - name: Create the Keystore diff --git a/.github/workflows/static_analysis.yml b/.github/workflows/static_analysis.yml index c2b633c4b..1fb9cb55f 100644 --- a/.github/workflows/static_analysis.yml +++ b/.github/workflows/static_analysis.yml @@ -23,7 +23,7 @@ jobs: uses: subosito/flutter-action@v2 with: channel: "stable" - flutter-version: "3.16.9" + flutter-version: "3.19.3" - name: Install dependencies run: dart pub get diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 13f5a73a1..6a5df111d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -354,7 +354,7 @@ jobs: id: verify-changed-sql-files with: files: | - server/src/infra/sql + server/src/queries - name: Verify SQL files have not changed if: steps.verify-changed-sql-files.outputs.files_changed == 'true' diff --git a/.vscode/settings.json b/.vscode/settings.json index eae6e1a16..3267c6713 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -27,4 +27,8 @@ "cSpell.words": [ "immich" ], + "explorer.fileNesting.enabled": true, + "explorer.fileNesting.patterns": { + "*.ts": "${capture}.spec.ts,${capture}.mock.ts" + } } \ No newline at end of file diff --git a/README.md b/README.md index 255013501..9f9b46982 100644 --- a/README.md +++ b/README.md @@ -18,17 +18,17 @@

- Català - Español - Français - Italiano - 日本語 - 한국어 - Deutsch - Nederlands - Türkçe - 中文 - Русский + Català + Español + Français + Italiano + 日本語 + 한국어 + Deutsch + Nederlands + Türkçe + 中文 + Русский

## Disclaimer diff --git a/cli/src/commands/asset.ts b/cli/src/commands/asset.ts index b6c159c9b..aa45ce547 100644 --- a/cli/src/commands/asset.ts +++ b/cli/src/commands/asset.ts @@ -1,5 +1,7 @@ import { + Action, AssetBulkUploadCheckResult, + AssetFileUploadResponseDto, addAssetsToAlbum, checkBulkUpload, createAlbum, @@ -8,445 +10,320 @@ import { getSupportedMediaTypes, } from '@immich/sdk'; import byteSize from 'byte-size'; -import cliProgress from 'cli-progress'; -import { chunk, zip } from 'lodash-es'; -import { createHash } from 'node:crypto'; -import fs, { createReadStream } from 'node:fs'; -import { access, constants, stat, unlink } from 'node:fs/promises'; +import { Presets, SingleBar } from 'cli-progress'; +import { chunk } from 'lodash-es'; +import { Stats, createReadStream } from 'node:fs'; +import { stat, unlink } from 'node:fs/promises'; import os from 'node:os'; -import { basename } from 'node:path'; -import { CrawlService } from 'src/services/crawl.service'; -import { BaseOptions, authenticate } from 'src/utils'; +import path, { basename } from 'node:path'; +import { BaseOptions, authenticate, crawl, sha1 } from 'src/utils'; -const zipDefined = zip as (a: T[], b: U[]) => [T, U][]; +const s = (count: number) => (count === 1 ? '' : 's'); -enum CheckResponseStatus { - ACCEPT = 'accept', - REJECT = 'reject', - DUPLICATE = 'duplicate', -} +// TODO figure out why `id` is missing +type AssetBulkUploadCheckResults = Array; +type Asset = { id: string; filepath: string }; -class Asset { - readonly path: string; - - id?: string; - deviceAssetId?: string; - fileCreatedAt?: Date; - fileModifiedAt?: Date; - sidecarPath?: string; - fileSize?: number; +interface UploadOptionsDto { + recursive?: boolean; + exclusionPatterns?: string[]; + dryRun?: boolean; + skipHash?: boolean; + delete?: boolean; + album?: boolean; albumName?: string; + includeHidden?: boolean; + concurrency: number; +} - constructor(path: string) { - this.path = path; +class UploadFile extends File { + constructor( + private filepath: string, + private _size: number, + ) { + super([], basename(filepath)); } - async prepare() { - const stats = await stat(this.path); - 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(); + get size() { + return this._size; } - async getUploadFormData(): Promise { - if (!this.deviceAssetId) { - throw new Error('Device asset id not set'); - } - if (!this.fileCreatedAt) { - throw new Error('File created at not set'); - } - if (!this.fileModifiedAt) { - throw new Error('File modified at not set'); - } - - // TODO: doesn't xmp replace the file extension? Will need investigation - const sideCarPath = `${this.path}.xmp`; - let sidecarData: Blob | undefined = undefined; - try { - await access(sideCarPath, constants.R_OK); - sidecarData = new File([await fs.openAsBlob(sideCarPath)], basename(sideCarPath)); - } catch {} - - const data: any = { - assetData: new File([await fs.openAsBlob(this.path)], basename(this.path)), - deviceAssetId: this.deviceAssetId, - deviceId: 'CLI', - fileCreatedAt: this.fileCreatedAt.toISOString(), - fileModifiedAt: this.fileModifiedAt.toISOString(), - isFavorite: String(false), - }; - const formData = new FormData(); - - for (const property in data) { - formData.append(property, data[property]); - } - - if (sidecarData) { - formData.append('sidecarData', sidecarData); - } - - return formData; - } - - async delete(): Promise { - return unlink(this.path); - } - - public async hash(): Promise { - const sha1 = (filePath: string) => { - const hash = createHash('sha1'); - return new Promise((resolve, reject) => { - const rs = createReadStream(filePath); - rs.on('error', reject); - rs.on('data', (chunk) => hash.update(chunk)); - rs.on('end', () => resolve(hash.digest('hex'))); - }); - }; - - return await sha1(this.path); - } - - private extractAlbumName(): string | undefined { - return os.platform() === 'win32' ? this.path.split('\\').at(-2) : this.path.split('/').at(-2); + stream() { + return createReadStream(this.filepath) as any; } } -class UploadOptionsDto { - recursive? = false; - exclusionPatterns?: string[] = []; - dryRun? = false; - skipHash? = false; - delete? = false; - album? = false; - albumName? = ''; - includeHidden? = false; - concurrency? = 4; -} +export const upload = async (paths: string[], baseOptions: BaseOptions, options: UploadOptionsDto) => { + await authenticate(baseOptions); -export const upload = (paths: string[], baseOptions: BaseOptions, uploadOptions: UploadOptionsDto) => - new UploadCommand().run(paths, baseOptions, uploadOptions); + const files = await scan(paths, options); + if (files.length === 0) { + console.log('No files found, exiting'); + return; + } -// TODO refactor this -class UploadCommand { - public async run(paths: string[], baseOptions: BaseOptions, options: UploadOptionsDto): Promise { - await authenticate(baseOptions); + const { newFiles, duplicates } = await checkForDuplicates(files, options); - console.log('Crawling for assets...'); - const files = await this.getFiles(paths, options); + const newAssets = await uploadFiles(newFiles, options); + await updateAlbums([...newAssets, ...duplicates], options); + await deleteFiles(newFiles, options); +}; - if (files.length === 0) { - console.log('No assets found, exiting'); - return; +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 { + // TODO refactor into a queue + for (const items of chunk(files, concurrency)) { + const dto = await Promise.all(items.map(async (filepath) => ({ id: filepath, checksum: await sha1(filepath) }))); + const { results } = await checkBulkUpload({ assetBulkUploadCheckDto: { assets: dto } }); + + for (const { id: filepath, assetId, action } of results as AssetBulkUploadCheckResults) { + if (action === Action.Accept) { + newFiles.push(filepath); + } else { + // rejects are always duplicates + duplicates.push({ id: assetId as string, filepath }); + } + progressBar.increment(); + } } + } finally { + progressBar.stop(); + } - const assetsToCheck = files.map((path) => new Asset(path)); + console.log(`Found ${newFiles.length} new files and ${duplicates.length} duplicate${s(duplicates.length)}`); - const { newAssets, duplicateAssets } = await this.checkAssets(assetsToCheck, options.concurrency ?? 4); + return { newFiles, duplicates }; +}; - 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)})`, +const uploadFiles = async (files: string[], { dryRun, concurrency }: UploadOptionsDto): Promise => { + 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(); + 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(); + } - if (options.album || options.albumName) { - const { createdAlbumCount, updatedAssetCount } = await this.updateAlbums( - [...newAssets, ...duplicateAssets], - options, + console.log(`Successfully uploaded ${newAssets.length} asset${s(newAssets.length)} (${byteSize(totalSizeUploaded)})`); + return newAssets; +}; + +const uploadFile = async (input: string, stats: Stats): Promise => { + 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) { + formData.append('sidecarData', sidecarData); + } + + const response = await fetch(`${baseUrl}/asset/upload`, { + method: 'post', + redirect: 'error', + headers: headers as Record, + body: formData, + }); + if (response.status !== 200 && response.status !== 201) { + throw new Error(await response.text()); + } + + return response.json(); +}; + +const deleteFiles = async (files: string[], options: UploadOptionsDto): Promise => { + if (!options.delete) { + return; + } + + if (options.dryRun) { + console.log(`Would now have deleted assets, but skipped due to dry run`); + return; + } + + console.log('Deleting assets that have been uploaded...'); + + const deletionProgress = new SingleBar( + { format: 'Deleting local assets | {bar} | {percentage}% | ETA: {eta}s | {value}/{total} assets' }, + Presets.shades_classic, + ); + deletionProgress.start(files.length, 0); + + try { + for (const assetBatch of chunk(files, options.concurrency)) { + await Promise.all(assetBatch.map((input: string) => unlink(input))); + deletionProgress.update(assetBatch.length); + } + } finally { + 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 = 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 } })), ); - console.log(`${messageStart} created ${createdAlbumCount} new album${createdAlbumCount === 1 ? '' : 's'}`); - console.log(`${messageStart} updated ${updatedAssetCount} asset${updatedAssetCount === 1 ? '' : 's'}`); - } - - if (!options.delete) { - return; - } - - if (options.dryRun) { - console.log(`Would now have deleted assets, but skipped due to dry run`); - return; - } - - console.log('Deleting assets that have been uploaded...'); - - await this.deleteAssets(newAssets, options); - } - - 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); - - 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(); - } + for (const { id, albumName } of items) { + existingAlbums.set(albumName, id); } - } finally { - checkProgress.stop(); + progressBar.increment(albumNames.length); } - - return { newAssets, duplicateAssets, rejectedAssets }; + } finally { + progressBar.stop(); } - public async upload(assetsToUpload: Asset[], options: UploadOptionsDto): Promise { - let totalSize = 0; + console.log(`Successfully created ${newAlbums.size} new album${s(newAlbums.size)}`); + console.log(`Successfully updated ${assets.length} asset${s(assets.length)}`); - // Compute total size first - for (const asset of assetsToUpload) { - totalSize += asset.fileSize ?? 0; + const albumToAssets = new Map(); + for (const asset of assets) { + const albumName = getAlbumName(asset.filepath, options); + if (!albumName) { + continue; } - - 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) }); + const albumId = existingAlbums.get(albumName); + if (albumId) { + if (!albumToAssets.has(albumId)) { + albumToAssets.set(albumId, []); } - } finally { - uploadProgress.stop(); + albumToAssets.get(albumId)?.push(asset.id); } - - return totalSizeUploaded; } - public async getFiles(paths: string[], options: UploadOptionsDto): Promise { - const inputFiles: string[] = []; - for (const pathArgument of paths) { - const fileStat = await fs.promises.lstat(pathArgument); - if (fileStat.isFile()) { - inputFiles.push(pathArgument); + const albumUpdateProgress = new SingleBar( + { format: 'Adding assets to albums | {bar} | {percentage}% | ETA: {eta}s | {value}/{total} assets' }, + Presets.shades_classic, + ); + albumUpdateProgress.start(assets.length, 0); + + try { + for (const [albumId, assets] of albumToAssets.entries()) { + for (const assetBatch of chunk(assets, Math.min(1000 * concurrency, 65_000))) { + await addAssetsToAlbum({ id: albumId, bulkIdsDto: { ids: assetBatch } }); + albumUpdateProgress.increment(assetBatch.length); } } - - const files: string[] = await this.crawl(paths, options); - files.push(...inputFiles); - return files; + } finally { + albumUpdateProgress.stop(); } +}; - public async getAlbums(): Promise> { - const existingAlbums = await getAllAlbums({}); - - const albumMapping = new Map(); - 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 = 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 { - for (const albumNames of chunk(newAlbums, options.concurrency)) { - const newAlbumIds = await Promise.all( - albumNames.map((albumName: string) => createAlbum({ createAlbumDto: { albumName } }).then((r) => r.id)), - ); - - for (const [albumName, albumId] of zipDefined(albumNames, newAlbumIds)) { - existingAlbums.set(albumName, albumId); - } - - albumCreationProgress.increment(albumNames.length); - } - } finally { - albumCreationProgress.stop(); - } - - const albumToAssets = new Map(); - for (const asset of assetsToUpdate) { - const albumId = existingAlbums.get(asset.albumName); - if (albumId) { - if (!albumToAssets.has(albumId)) { - albumToAssets.set(albumId, []); - } - albumToAssets.get(albumId)?.push(asset.id); - } - } - - const albumUpdateProgress = new cliProgress.SingleBar( - { - format: 'Adding assets to albums | {bar} | {percentage}% | ETA: {eta}s | {value}/{total} assets', - }, - cliProgress.Presets.shades_classic, - ); - albumUpdateProgress.start(assetsToUpdate.length, 0); - - try { - for (const [albumId, assets] of albumToAssets.entries()) { - for (const assetBatch of chunk(assets, Math.min(1000 * (options.concurrency ?? 4), 65_000))) { - await addAssetsToAlbum({ id: albumId, bulkIdsDto: { ids: assetBatch } }); - albumUpdateProgress.increment(assetBatch.length); - } - } - } finally { - albumUpdateProgress.stop(); - } - - return { createdAlbumCount: newAlbums.length, updatedAssetCount: assetsToUpdate.length }; - } - - public async deleteAssets(assets: Asset[], options: UploadOptionsDto): Promise { - 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 { - 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 { - 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 { - 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, - body: data, - }); - if (response.status !== 200 && response.status !== 201) { - throw new Error(await response.text()); - } - return response.json(); - } -} +const getAlbumName = (filepath: string, options: UploadOptionsDto) => { + const folderName = os.platform() === 'win32' ? filepath.split('\\').at(-2) : filepath.split('/').at(-2); + return options.albumName ?? folderName; +}; diff --git a/cli/src/commands/auth.ts b/cli/src/commands/auth.ts index 05f3d7953..6675201a7 100644 --- a/cli/src/commands/auth.ts +++ b/cli/src/commands/auth.ts @@ -3,12 +3,12 @@ import { existsSync } from 'node:fs'; import { mkdir, unlink } from 'node:fs/promises'; import { BaseOptions, connect, getAuthFilePath, logError, withError, writeAuthFile } from 'src/utils'; -export const login = async (instanceUrl: string, apiKey: string, options: BaseOptions) => { - console.log(`Logging in to ${instanceUrl}`); +export const login = async (url: string, key: string, options: BaseOptions) => { + console.log(`Logging in to ${url}`); const { configDirectory: configDir } = options; - await connect(instanceUrl, apiKey); + await connect(url, key); const [error, userInfo] = await withError(getMyUserInfo()); 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)}`); }; diff --git a/cli/src/index.ts b/cli/src/index.ts index bf7e13f44..c3da631b9 100644 --- a/cli/src/index.ts +++ b/cli/src/index.ts @@ -19,7 +19,7 @@ const program = new Command() .default(defaultConfigDirectory), ) .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 .command('login') diff --git a/cli/src/services/crawl.service.ts b/cli/src/services/crawl.service.ts deleted file mode 100644 index 3ad0fcf3b..000000000 --- a/cli/src/services/crawl.service.ts +++ /dev/null @@ -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 { - 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(); - } -} diff --git a/cli/src/services/crawl.service.spec.ts b/cli/src/utils.spec.ts similarity index 94% rename from cli/src/services/crawl.service.spec.ts rename to cli/src/utils.spec.ts index 93879f21e..9c2570279 100644 --- a/cli/src/services/crawl.service.spec.ts +++ b/cli/src/utils.spec.ts @@ -1,14 +1,31 @@ import mockfs from 'mock-fs'; -import { CrawlOptions, CrawlService } from './crawl.service'; +import { CrawlOptions, crawl } from 'src/utils'; interface Test { test: string; - options: CrawlOptions; + options: Omit; files: Record; } const cwd = process.cwd(); +const extensions = [ + '.jpg', + '.jpeg', + '.png', + '.heif', + '.heic', + '.tif', + '.nef', + '.webp', + '.tiff', + '.dng', + '.gif', + '.mov', + '.mp4', + '.webm', +]; + const tests: Test[] = [ { test: 'should return empty when crawling an empty path list', @@ -251,12 +268,7 @@ const tests: Test[] = [ }, ]; -describe(CrawlService.name, () => { - const sut = new CrawlService( - ['.jpg', '.jpeg', '.png', '.heif', '.heic', '.tif', '.nef', '.webp', '.tiff', '.dng', '.gif'], - ['.mov', '.mp4', '.webm'], - ); - +describe('crawl', () => { afterEach(() => { mockfs.restore(); }); @@ -266,7 +278,7 @@ describe(CrawlService.name, () => { it(test, async () => { 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) .filter((entry) => entry[1]) .map(([file]) => file); diff --git a/cli/src/utils.ts b/cli/src/utils.ts index f99a0e66a..c17ad6903 100644 --- a/cli/src/utils.ts +++ b/cli/src/utils.ts @@ -1,48 +1,49 @@ 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 yaml from 'yaml'; export interface BaseOptions { configDirectory: string; - apiKey?: string; - instanceUrl?: string; + key?: string; + url?: string; } -export interface AuthDto { - instanceUrl: string; - apiKey: string; -} +export type AuthDto = { url: string; key: string }; +type OldAuthDto = { instanceUrl: string; apiKey: string }; export const authenticate = async (options: BaseOptions): Promise => { - const { configDirectory: configDir, instanceUrl, apiKey } = options; + const { configDirectory: configDir, url, key } = options; // provided in command - if (instanceUrl && apiKey) { - await connect(instanceUrl, apiKey); + if (url && key) { + await connect(url, key); return; } - // fallback to file + // fallback to auth file 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 => { - const wellKnownUrl = new URL('.well-known/immich', instanceUrl); +export const connect = async (url: string, key: string): Promise => { + const wellKnownUrl = new URL('.well-known/immich', url); try { const wellKnown = await fetch(wellKnownUrl).then((response) => response.json()); - const endpoint = new URL(wellKnown.api.endpoint, instanceUrl).toString(); - if (endpoint !== instanceUrl) { + const endpoint = new URL(wellKnown.api.endpoint, url).toString(); + if (endpoint !== url) { console.debug(`Discovered API at ${endpoint}`); } - instanceUrl = endpoint; + url = endpoint; } catch { // noop } - defaults.baseUrl = instanceUrl; - defaults.headers = { 'x-api-key': apiKey }; + defaults.baseUrl = url; + defaults.headers = { 'x-api-key': key }; const [error] = await withError(getMyUserInfo()); if (isHttpError(error)) { @@ -66,7 +67,12 @@ export const readAuthFile = async (dir: string) => { try { const data = await readFile(getAuthFilePath(dir)); // 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) { if (error.code === 'ENOENT' || error.code === 'ENOTDIR') { console.log('No auth file exists. Please login first.'); @@ -87,3 +93,74 @@ export const withError = async (promise: Promise): Promise<[Error, undefin return [error, undefined]; } }; + +export interface CrawlOptions { + pathsToCrawl: string[]; + recursive?: boolean; + includeHidden?: boolean; + exclusionPatterns?: string[]; + extensions: string[]; +} +export const crawl = async (options: CrawlOptions): Promise => { + 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((resolve, reject) => { + const rs = createReadStream(filepath); + rs.on('error', reject); + rs.on('data', (chunk) => hash.update(chunk)); + rs.on('end', () => resolve(hash.digest('hex'))); + }); +}; diff --git a/design/immich-screenshots.png b/design/immich-screenshots.png index 1db7cab76..6123279f2 100644 Binary files a/design/immich-screenshots.png and b/design/immich-screenshots.png differ diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml index ac56a2e06..4725c561f 100644 --- a/docker/docker-compose.dev.yml +++ b/docker/docker-compose.dev.yml @@ -99,7 +99,7 @@ services: redis: container_name: immich_redis - image: redis:6.2-alpine@sha256:fd3535746075ba01b73c3602c0704bc944dd064c0a4ac46341a4a351bec69db8 + image: redis:6.2-alpine@sha256:3fcb624d83a9c478357f16dc173c58ded325ccc5fd2a4375f3916c04cc579f70 database: container_name: immich_postgres diff --git a/docker/docker-compose.prod.yml b/docker/docker-compose.prod.yml index b797e9e8e..a442b4543 100644 --- a/docker/docker-compose.prod.yml +++ b/docker/docker-compose.prod.yml @@ -56,7 +56,7 @@ services: redis: container_name: immich_redis - image: redis:6.2-alpine@sha256:fd3535746075ba01b73c3602c0704bc944dd064c0a4ac46341a4a351bec69db8 + image: redis:6.2-alpine@sha256:3fcb624d83a9c478357f16dc173c58ded325ccc5fd2a4375f3916c04cc579f70 restart: always database: @@ -90,7 +90,7 @@ services: command: ['./run.sh', '-disable-reporting'] ports: - 3000:3000 - image: grafana/grafana:10.4.0-ubuntu@sha256:c1f582b7cc4c1b9805d187b5600ce7879550a12ef6d29571da133c3d3fc67a9c + image: grafana/grafana:10.4.1-ubuntu@sha256:65e0e7d0f0b001cb0478bce5093bff917677dc308dd27a0aa4b3ac38e4fd877c volumes: - grafana-data:/var/lib/grafana diff --git a/docs/docs/administration/oauth.md b/docs/docs/administration/oauth.md index f97f8da7d..b273f2771 100644 --- a/docs/docs/administration/oauth.md +++ b/docs/docs/administration/oauth.md @@ -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) | | 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) | -| 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 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**¹** | | 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 | | 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 | | [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 The Issuer URL should look something like the following, and return a valid json document. diff --git a/docs/docs/developer/database-migrations.md b/docs/docs/developer/database-migrations.md index eccf78b44..2cddf5f38 100644 --- a/docs/docs/developer/database-migrations.md +++ b/docs/docs/developer/database-migrations.md @@ -5,7 +5,7 @@ After making any changes in the `server/src/entities`, a database migration need 1. Run the command ```bash -npm run typeorm:migrations:generate ./src/infra/ +npm run typeorm:migrations:generate ``` 2. Check if the migration file makes sense. diff --git a/docs/docs/features/command-line-interface.md b/docs/docs/features/command-line-interface.md index 360b3c372..d4c3a1d8e 100644 --- a/docs/docs/features/command-line-interface.md +++ b/docs/docs/features/command-line-interface.md @@ -61,7 +61,7 @@ Options: Commands: upload [options] [paths...] Upload assets 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 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. ```bash -immich login-key [instanceUrl] [apiKey] +immich login [url] [key] ``` For instance, ```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. diff --git a/docs/docs/guides/remote-machine-learning.md b/docs/docs/guides/remote-machine-learning.md index b7865686c..087f9aab7 100644 --- a/docs/docs/guides/remote-machine-learning.md +++ b/docs/docs/guides/remote-machine-learning.md @@ -16,7 +16,12 @@ version: '3.8' services: 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} + # 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: - model-cache:/cache restart: always diff --git a/e2e/docker-compose.yml b/e2e/docker-compose.yml index 2c22fa72a..e10f22156 100644 --- a/e2e/docker-compose.yml +++ b/e2e/docker-compose.yml @@ -36,7 +36,7 @@ services: <<: *server-common redis: - image: redis:6.2-alpine@sha256:fd3535746075ba01b73c3602c0704bc944dd064c0a4ac46341a4a351bec69db8 + image: redis:6.2-alpine@sha256:3fcb624d83a9c478357f16dc173c58ded325ccc5fd2a4375f3916c04cc579f70 database: image: tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:90724186f0a3517cf6914295b5ab410db9ce23190a2d9d0b9dd6463e3fa298f0 diff --git a/e2e/src/api/specs/system-config.e2e-spec.ts b/e2e/src/api/specs/system-config.e2e-spec.ts index c223df487..8cf389134 100644 --- a/e2e/src/api/specs/system-config.e2e-spec.ts +++ b/e2e/src/api/specs/system-config.e2e-spec.ts @@ -1,10 +1,12 @@ -import { LoginResponseDto } from '@immich/sdk'; +import { LoginResponseDto, getConfig } from '@immich/sdk'; import { createUserDto } from 'src/fixtures'; import { errorDto } from 'src/responses'; -import { app, utils } from 'src/utils'; +import { app, asBearerAuth, utils } from 'src/utils'; import request from 'supertest'; import { beforeAll, describe, expect, it } from 'vitest'; +const getSystemConfig = (accessToken: string) => getConfig({ headers: asBearerAuth(accessToken) }); + describe('/system-config', () => { let admin: LoginResponseDto; let nonAdmin: LoginResponseDto; @@ -60,4 +62,25 @@ describe('/system-config', () => { 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'))); + }); + }); }); diff --git a/e2e/src/cli/specs/login.e2e-spec.ts b/e2e/src/cli/specs/login.e2e-spec.ts index 42877221f..0fb48188a 100644 --- a/e2e/src/cli/specs/login.e2e-spec.ts +++ b/e2e/src/cli/specs/login.e2e-spec.ts @@ -2,25 +2,25 @@ import { stat } from 'node:fs/promises'; import { app, immichCli, utils } from 'src/utils'; import { beforeEach, describe, expect, it } from 'vitest'; -describe(`immich login-key`, () => { +describe(`immich login`, () => { beforeEach(async () => { await utils.resetDatabase(); }); 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(exitCode).toBe(1); }); 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(exitCode).toBe(1); }); 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('Invalid API key'); expect(stderr).toContain('401'); @@ -30,7 +30,7 @@ describe(`immich login-key`, () => { it('should login and save auth.yml with 600', async () => { const admin = await utils.adminSetup(); 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([ 'Logging in to http://127.0.0.1:2283/api', 'Logged in as admin@immich.cloud', @@ -47,7 +47,7 @@ describe(`immich login-key`, () => { it('should login without /api in the url', async () => { const admin = await utils.adminSetup(); 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([ 'Logging in to http://127.0.0.1:2283', 'Discovered API at http://127.0.0.1:2283/api', diff --git a/e2e/src/cli/specs/server-info.e2e-spec.ts b/e2e/src/cli/specs/server-info.e2e-spec.ts index 6efe002b8..f207f1fa2 100644 --- a/e2e/src/cli/specs/server-info.e2e-spec.ts +++ b/e2e/src/cli/specs/server-info.e2e-spec.ts @@ -4,7 +4,8 @@ import { beforeAll, describe, expect, it } from 'vitest'; describe(`immich server-info`, () => { beforeAll(async () => { await utils.resetDatabase(); - await utils.cliLogin(); + const admin = await utils.adminSetup(); + await utils.cliLogin(admin.accessToken); }); it('should return the server info', async () => { diff --git a/e2e/src/cli/specs/upload.e2e-spec.ts b/e2e/src/cli/specs/upload.e2e-spec.ts index bc4382f98..a74a57c71 100644 --- a/e2e/src/cli/specs/upload.e2e-spec.ts +++ b/e2e/src/cli/specs/upload.e2e-spec.ts @@ -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 { asKeyAuth, immichCli, testAssetDir, utils } from 'src/utils'; import { beforeAll, beforeEach, describe, expect, it } from 'vitest'; describe(`immich upload`, () => { + let admin: LoginResponseDto; let key: string; beforeAll(async () => { await utils.resetDatabase(); - key = await utils.cliLogin(); + + admin = await utils.adminSetup(); + key = await utils.cliLogin(admin.accessToken); }); beforeEach(async () => { 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', () => { it('should upload a folder recursively', async () => { const { stderr, stdout, exitCode } = await immichCli(['upload', `${testAssetDir}/albums/nature/`, '--recursive']); diff --git a/e2e/src/utils.ts b/e2e/src/utils.ts index 8ca7fba60..6b538129a 100644 --- a/e2e/src/utils.ts +++ b/e2e/src/utils.ts @@ -404,10 +404,9 @@ export const utils = { }, ]), - cliLogin: async () => { - const admin = await utils.adminSetup(); - const key = await utils.createApiKey(admin.accessToken); - await immichCli(['login-key', app, `${key.secret}`]); + cliLogin: async (accessToken: string) => { + const key = await utils.createApiKey(accessToken); + await immichCli(['login', app, `${key.secret}`]); return key.secret; }, }; diff --git a/machine-learning/export/Dockerfile b/machine-learning/export/Dockerfile index 1016b330c..b9aa8f1ed 100644 --- a/machine-learning/export/Dockerfile +++ b/machine-learning/export/Dockerfile @@ -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 \ TRANSFORMERS_CACHE=/cache \ diff --git a/machine-learning/poetry.lock b/machine-learning/poetry.lock index 7a0c66bdc..8d53fc84e 100644 --- a/machine-learning/poetry.lock +++ b/machine-learning/poetry.lock @@ -877,13 +877,13 @@ tqdm = ["tqdm"] [[package]] name = "ftfy" -version = "6.1.3" +version = "6.2.0" description = "Fixes mojibake and other problems with Unicode, after the fact" optional = false python-versions = ">=3.8,<4" files = [ - {file = "ftfy-6.1.3-py3-none-any.whl", hash = "sha256:e49c306c06a97f4986faa7a8740cfe3c13f3106e85bcec73eb629817e671557c"}, - {file = "ftfy-6.1.3.tar.gz", hash = "sha256:693274aead811cff24c1e8784165aa755cd2f6e442a5ec535c7d697f6422a422"}, + {file = "ftfy-6.2.0-py3-none-any.whl", hash = "sha256:f94a2c34b76e07475720e3096f5ca80911d152406fbde66fdb45c4d0c9150026"}, + {file = "ftfy-6.2.0.tar.gz", hash = "sha256:5e42143c7025ef97944ca2619d6b61b0619fc6654f98771d39e862c1424c75c0"}, ] [package.dependencies] @@ -2844,28 +2844,28 @@ files = [ [[package]] name = "ruff" -version = "0.3.2" +version = "0.3.3" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" 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.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9966b964b2dd1107797be9ca7195002b874424d1d5472097701ae8f43eadef5d"}, - {file = "ruff-0.3.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b83d17ff166aa0659d1e1deaf9f2f14cbe387293a906de09bc4860717eb2e2da"}, - {file = "ruff-0.3.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb875c6cc87b3703aeda85f01c9aebdce3d217aeaca3c2e52e38077383f7268a"}, - {file = "ruff-0.3.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:be75e468a6a86426430373d81c041b7605137a28f7014a72d2fc749e47f572aa"}, - {file = "ruff-0.3.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:967978ac2d4506255e2f52afe70dda023fc602b283e97685c8447d036863a302"}, - {file = "ruff-0.3.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1231eacd4510f73222940727ac927bc5d07667a86b0cbe822024dd00343e77e9"}, - {file = "ruff-0.3.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2c6d613b19e9a8021be2ee1d0e27710208d1603b56f47203d0abbde906929a9b"}, - {file = "ruff-0.3.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8439338a6303585d27b66b4626cbde89bb3e50fa3cae86ce52c1db7449330a7"}, - {file = "ruff-0.3.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:de8b480d8379620cbb5ea466a9e53bb467d2fb07c7eca54a4aa8576483c35d36"}, - {file = "ruff-0.3.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b74c3de9103bd35df2bb05d8b2899bf2dbe4efda6474ea9681280648ec4d237d"}, - {file = "ruff-0.3.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:f380be9fc15a99765c9cf316b40b9da1f6ad2ab9639e551703e581a5e6da6745"}, - {file = "ruff-0.3.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:0ac06a3759c3ab9ef86bbeca665d31ad3aa9a4b1c17684aadb7e61c10baa0df4"}, - {file = "ruff-0.3.2-py3-none-win32.whl", hash = "sha256:9bd640a8f7dd07a0b6901fcebccedadeb1a705a50350fb86b4003b805c81385a"}, - {file = "ruff-0.3.2-py3-none-win_amd64.whl", hash = "sha256:0c1bdd9920cab5707c26c8b3bf33a064a4ca7842d91a99ec0634fec68f9f4037"}, - {file = "ruff-0.3.2-py3-none-win_arm64.whl", hash = "sha256:5f65103b1d76e0d600cabd577b04179ff592064eaa451a70a81085930e907d0b"}, - {file = "ruff-0.3.2.tar.gz", hash = "sha256:fa78ec9418eb1ca3db392811df3376b46471ae93792a81af2d1cbb0e5dcb5142"}, + {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.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:cfa60d23269d6e2031129b053fdb4e5a7b0637fc6c9c0586737b962b2f834493"}, + {file = "ruff-0.3.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1eca7ff7a47043cf6ce5c7f45f603b09121a7cc047447744b029d1b719278eb5"}, + {file = "ruff-0.3.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7d3f6762217c1da954de24b4a1a70515630d29f71e268ec5000afe81377642d"}, + {file = "ruff-0.3.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b24c19e8598916d9c6f5a5437671f55ee93c212a2c4c569605dc3842b6820386"}, + {file = "ruff-0.3.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:5a6cbf216b69c7090f0fe4669501a27326c34e119068c1494f35aaf4cc683778"}, + {file = "ruff-0.3.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:352e95ead6964974b234e16ba8a66dad102ec7bf8ac064a23f95371d8b198aab"}, + {file = "ruff-0.3.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d6ab88c81c4040a817aa432484e838aaddf8bfd7ca70e4e615482757acb64f8"}, + {file = "ruff-0.3.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79bca3a03a759cc773fca69e0bdeac8abd1c13c31b798d5bb3c9da4a03144a9f"}, + {file = "ruff-0.3.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:2700a804d5336bcffe063fd789ca2c7b02b552d2e323a336700abb8ae9e6a3f8"}, + {file = "ruff-0.3.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:fd66469f1a18fdb9d32e22b79f486223052ddf057dc56dea0caaf1a47bdfaf4e"}, + {file = "ruff-0.3.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:45817af234605525cdf6317005923bf532514e1ea3d9270acf61ca2440691376"}, + {file = "ruff-0.3.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:0da458989ce0159555ef224d5b7c24d3d2e4bf4c300b85467b08c3261c6bc6a8"}, + {file = "ruff-0.3.3-py3-none-win32.whl", hash = "sha256:f2831ec6a580a97f1ea82ea1eda0401c3cdf512cf2045fa3c85e8ef109e87de0"}, + {file = "ruff-0.3.3-py3-none-win_amd64.whl", hash = "sha256:be90bcae57c24d9f9d023b12d627e958eb55f595428bafcb7fec0791ad25ddfc"}, + {file = "ruff-0.3.3-py3-none-win_arm64.whl", hash = "sha256:0171aab5fecdc54383993389710a3d1227f2da124d76a2784a7098e818f92d61"}, + {file = "ruff-0.3.3.tar.gz", hash = "sha256:38671be06f57a2f8aba957d9f701ea889aa5736be806f18c0cd03d6ff0cbca8d"}, ] [[package]] diff --git a/mobile/.fvm/fvm_config.json b/mobile/.fvm/fvm_config.json index 3e7195748..36eb0ad1c 100644 --- a/mobile/.fvm/fvm_config.json +++ b/mobile/.fvm/fvm_config.json @@ -1,4 +1,4 @@ { - "flutterSdkVersion": "3.16.9", + "flutterSdkVersion": "3.19.3", "flavors": {} } \ No newline at end of file diff --git a/mobile/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png b/mobile/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png deleted file mode 100644 index 5b91b2258..000000000 Binary files a/mobile/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png and /dev/null differ diff --git a/mobile/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png b/mobile/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png deleted file mode 100644 index 18470610c..000000000 Binary files a/mobile/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png and /dev/null differ diff --git a/mobile/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png b/mobile/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png deleted file mode 100644 index f0d6d66ac..000000000 Binary files a/mobile/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png and /dev/null differ diff --git a/mobile/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png b/mobile/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png deleted file mode 100644 index 6eb60e8b7..000000000 Binary files a/mobile/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png and /dev/null differ diff --git a/mobile/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png b/mobile/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png deleted file mode 100644 index c8842b651..000000000 Binary files a/mobile/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png and /dev/null differ diff --git a/mobile/android/app/src/main/res/drawable/ic_launcher_foreground.xml b/mobile/android/app/src/main/res/drawable/ic_launcher_foreground.xml index 6452c29ba..d14fce37e 100644 --- a/mobile/android/app/src/main/res/drawable/ic_launcher_foreground.xml +++ b/mobile/android/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -1,70 +1,27 @@ - - - - - - - - - - - - - - - - - + + + + + + + diff --git a/mobile/android/app/src/main/res/drawable/ic_launcher_monochrome.xml b/mobile/android/app/src/main/res/drawable/ic_launcher_monochrome.xml index d98b89c74..8b19a433f 100644 --- a/mobile/android/app/src/main/res/drawable/ic_launcher_monochrome.xml +++ b/mobile/android/app/src/main/res/drawable/ic_launcher_monochrome.xml @@ -1,70 +1,27 @@ - - - - - - - - - - - - - - - - - + + + + + + + diff --git a/mobile/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/mobile/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml index 5f349f7f4..f606c4d83 100644 --- a/mobile/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ b/mobile/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -2,4 +2,5 @@ + diff --git a/mobile/ios/Podfile.lock b/mobile/ios/Podfile.lock index a9ac5b338..8ee2cdcc0 100644 --- a/mobile/ios/Podfile.lock +++ b/mobile/ios/Podfile.lock @@ -17,9 +17,6 @@ PODS: - fluttertoast (0.0.2): - Flutter - Toast - - FMDB (2.7.5): - - FMDB/standard (= 2.7.5) - - FMDB/standard (2.7.5) - geolocator_apple (1.2.0): - Flutter - image_picker_ios (0.0.1): @@ -39,7 +36,7 @@ PODS: - FlutterMacOS - path_provider_ios (0.0.1): - Flutter - - permission_handler_apple (9.1.1): + - permission_handler_apple (9.3.0): - Flutter - photo_manager (2.0.0): - Flutter @@ -53,7 +50,7 @@ PODS: - FlutterMacOS - sqflite (0.0.3): - Flutter - - FMDB (>= 2.7.5) + - FlutterMacOS - Toast (4.0.0) - url_launcher_ios (0.0.1): - Flutter @@ -84,14 +81,13 @@ DEPENDENCIES: - photo_manager (from `.symlinks/plugins/photo_manager/ios`) - share_plus (from `.symlinks/plugins/share_plus/ios`) - 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`) - video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`) - wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`) SPEC REPOS: trunk: - - FMDB - MapLibre - ReachabilitySwift - SAMKeychain @@ -139,7 +135,7 @@ EXTERNAL SOURCES: shared_preferences_foundation: :path: ".symlinks/plugins/shared_preferences_foundation/darwin" sqflite: - :path: ".symlinks/plugins/sqflite/ios" + :path: ".symlinks/plugins/sqflite/darwin" url_launcher_ios: :path: ".symlinks/plugins/url_launcher_ios/ios" video_player_avfoundation: @@ -155,24 +151,23 @@ SPEC CHECKSUMS: flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef flutter_udid: a2482c67a61b9c806ef59dd82ed8d007f1b7ac04 flutter_web_auth: c25208760459cec375a3c39f6a8759165ca0fa4d - fluttertoast: fafc4fa4d01a6a9e4f772ecd190ffa525e9e2d9c - FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a + fluttertoast: 31b00dabfa7fb7bacd9e7dbee580d7a2ff4bf265 geolocator_apple: 9157311f654584b9bb72686c55fc02a97b73f461 - image_picker_ios: 4a8aadfbb6dc30ad5141a2ce3832af9214a705b5 + image_picker_ios: 99dfe1854b4fa34d0364e74a78448a0151025425 integration_test: 13825b8a9334a850581300559b8839134b124670 isar_flutter_libs: b69f437aeab9c521821c3f376198c4371fa21073 MapLibre: 620fc933c1d6029b33738c905c1490d024e5d4ef maplibre_gl: a2efec727dd340e4c65e26d2b03b584f14881fd9 package_info_plus: 115f4ad11e0698c8c1c5d8a689390df880f47e85 - path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943 + path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02 - permission_handler_apple: e76247795d700c14ea09e3a2d8855d41ee80a2e6 + permission_handler_apple: 036b856153a2b1f61f21030ff725f3e6fece2b78 photo_manager: 4f6810b7dfc4feb03b461ac1a70dacf91fba7604 ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825 SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c share_plus: c3fef564749587fc939ef86ffb283ceac0baf9f5 - shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126 - sqflite: 31f7eba61e3074736dff8807a9b41581e4f7f15a + shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695 + sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec Toast: 91b396c56ee72a5790816f40d3a94dd357abc196 url_launcher_ios: bbd758c6e7f9fd7b5b1d4cde34d2b95fcce5e812 video_player_avfoundation: 02011213dab73ae3687df27ce441fbbcc82b5579 @@ -180,4 +175,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 64c9b5291666c0ca3caabdfe9865c141ac40321d -COCOAPODS: 1.12.1 +COCOAPODS: 1.15.2 diff --git a/mobile/ios/Runner.xcodeproj/project.pbxproj b/mobile/ios/Runner.xcodeproj/project.pbxproj index d6056f3f1..2ca8dd5ff 100644 --- a/mobile/ios/Runner.xcodeproj/project.pbxproj +++ b/mobile/ios/Runner.xcodeproj/project.pbxproj @@ -172,7 +172,7 @@ isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = YES; - LastUpgradeCheck = 1430; + LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { 97C146ED1CF9000F007C117D = { diff --git a/mobile/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/mobile/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 43b89d762..1860f98f3 100644 --- a/mobile/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/mobile/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ ? chunkEvents, - int? height, - int? width, }) async { final headers = { 'x-immich-user-token': Store.get(StoreKey.accessToken), @@ -25,10 +23,8 @@ class ImageLoader { final stream = cache.getImageFile( uri, - withProgress: true, + withProgress: chunkEvents != null, headers: headers, - maxHeight: height, - maxWidth: width, ); await for (final result in stream) { @@ -40,13 +36,9 @@ class ImageLoader { expectedTotalBytes: result.totalSize, ), ); - } - - if (result is FileInfo) { + } else if (result is FileInfo) { // We have the file - final file = result.file; - final bytes = await file.readAsBytes(); - final buffer = await ui.ImmutableBuffer.fromUint8List(bytes); + final buffer = await ui.ImmutableBuffer.fromFilePath(result.file.path); final decoded = await decode(buffer); return decoded; } diff --git a/mobile/lib/modules/asset_viewer/ui/exif_sheet/exif_map.dart b/mobile/lib/modules/asset_viewer/ui/exif_sheet/exif_map.dart index 69644e678..6c0050aee 100644 --- a/mobile/lib/modules/asset_viewer/ui/exif_sheet/exif_map.dart +++ b/mobile/lib/modules/asset_viewer/ui/exif_sheet/exif_map.dart @@ -64,7 +64,7 @@ class ExifMap extends StatelessWidget { } return Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), + padding: const EdgeInsets.symmetric(vertical: 16.0), child: LayoutBuilder( builder: (context, constraints) { return MapThumbnail( diff --git a/mobile/lib/modules/asset_viewer/ui/exif_sheet/exif_people.dart b/mobile/lib/modules/asset_viewer/ui/exif_sheet/exif_people.dart index 620f4f3cd..b0dd65371 100644 --- a/mobile/lib/modules/asset_viewer/ui/exif_sheet/exif_people.dart +++ b/mobile/lib/modules/asset_viewer/ui/exif_sheet/exif_people.dart @@ -26,7 +26,7 @@ class ExifPeople extends ConsumerWidget { .watch(assetPeopleNotifierProvider(asset)) .value ?.where((p) => !p.isHidden); - final double imageSize = math.min(context.width / 3, 120); + final double imageSize = math.min(context.width / 3, 150); showPersonNameEditModel( String personId, diff --git a/mobile/lib/modules/home/ui/asset_grid/asset_drag_region.dart b/mobile/lib/modules/home/ui/asset_grid/asset_drag_region.dart index cf1de0383..d66220f1a 100644 --- a/mobile/lib/modules/home/ui/asset_grid/asset_drag_region.dart +++ b/mobile/lib/modules/home/ui/asset_grid/asset_drag_region.dart @@ -80,7 +80,6 @@ class _AssetDragRegionState extends State { recognizer.onLongPressMoveUpdate = (details) => _onLongPressMove(details); recognizer.onLongPressStart = (details) => _onLongPressStart(details); recognizer.onLongPressUp = _onLongPressEnd; - recognizer.onLongPressCancel = _onLongPressEnd; } AssetIndex? _getValueKeyAtPositon(Offset position) { diff --git a/mobile/lib/modules/search/providers/recently_added.provider.dart b/mobile/lib/modules/search/providers/recently_added.provider.dart index 5b7380979..4e6d2c156 100644 --- a/mobile/lib/modules/search/providers/recently_added.provider.dart +++ b/mobile/lib/modules/search/providers/recently_added.provider.dart @@ -1,13 +1,18 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/shared/models/asset.dart'; import 'package:immich_mobile/shared/providers/db.provider.dart'; +import 'package:immich_mobile/shared/providers/user.provider.dart'; import 'package:isar/isar.dart'; final recentlyAddedProvider = FutureProvider>((ref) async { + final user = ref.read(currentUserProvider); + if (user == null) return []; + return ref .watch(dbProvider) .assets .where() + .ownerIdEqualToAnyChecksum(user.isarId) .sortByFileCreatedAtDesc() .findAll(); }); diff --git a/mobile/lib/modules/search/ui/curated_people_row.dart b/mobile/lib/modules/search/ui/curated_people_row.dart index 8af610ad1..a712c6929 100644 --- a/mobile/lib/modules/search/ui/curated_people_row.dart +++ b/mobile/lib/modules/search/ui/curated_people_row.dart @@ -24,7 +24,7 @@ class CuratedPeopleRow extends StatelessWidget { @override Widget build(BuildContext context) { - const imageSize = 70.0; + const imageSize = 60.0; // Guard empty [content] if (content.isEmpty) { diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index 2f35cf591..bf01dfcc8 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -5,18 +5,18 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: "0f7b1783ddb1e4600580b8c00d0ddae5b06ae7f0382bd4fcce5db4df97b618e1" + sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7" url: "https://pub.dev" source: hosted - version: "66.0.0" + version: "67.0.0" analyzer: dependency: "direct overridden" description: name: analyzer - sha256: "5e8bdcda061d91da6b034d64d8e4026f355bcb8c3e7a0ac2da1523205a91a737" + sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d" url: "https://pub.dev" source: hosted - version: "6.4.0" + version: "6.4.1" analyzer_plugin: dependency: "direct overridden" description: @@ -101,18 +101,18 @@ packages: dependency: transitive description: name: build_daemon - sha256: "5f02d73eb2ba16483e693f80bee4f088563a820e47d1027d4cdfe62b5bb43e65" + sha256: "0343061a33da9c5810b2d6cee51945127d8f4c060b7fbdd9d54917f0a3feaaa1" url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "4.0.1" build_resolvers: dependency: transitive description: name: build_resolvers - sha256: "6c4dd11d05d056e76320b828a1db0fc01ccd376922526f8e9d6c796a5adbac20" + sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a" url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.4.2" build_runner: dependency: "direct dev" description: @@ -125,10 +125,10 @@ packages: dependency: transitive description: name: build_runner_core - sha256: "6d6ee4276b1c5f34f21fdf39425202712d2be82019983d52f351c94aafbc2c41" + sha256: "4ae8ffe5ac758da294ecf1802f2aff01558d8b1b00616aa7538ea9a8a5d50799" url: "https://pub.dev" source: hosted - version: "7.2.10" + version: "7.3.0" built_collection: dependency: transitive description: @@ -141,10 +141,10 @@ packages: dependency: transitive description: name: built_value - sha256: "598a2a682e2a7a90f08ba39c0aaa9374c5112340f0a2e275f61b59389543d166" + sha256: a3ec2e0f967bc47f69f95009bb93db936288d61d5343b9436e378b28a2f830c6 url: "https://pub.dev" source: hosted - version: "8.6.1" + version: "8.9.0" cached_network_image: dependency: "direct main" description: @@ -181,10 +181,10 @@ packages: dependency: "direct main" description: name: cancellation_token_http - sha256: bb91655e2e47d6274b681261ee6a687b7aa9023f49cfc28f42d095b2f86febc3 + sha256: "37ad2a20dba02aeb1f0a4d845e7a57eebacdb709e1186e0491e7cd81c559c4ff" url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "2.0.0" characters: dependency: transitive description: @@ -205,10 +205,10 @@ packages: dependency: "direct main" description: name: chewie - sha256: "3427e469d7cc99536ac4fbaa069b3352c21760263e65ffb4f0e1c054af43a73e" + sha256: "8bc4ac4cf3f316e50a25958c0f5eb9bb12cf7e8308bb1d74a43b230da2cfc144" url: "https://pub.dev" source: hosted - version: "1.7.4" + version: "1.7.5" ci: dependency: transitive description: @@ -221,10 +221,10 @@ packages: dependency: transitive description: name: cli_util - sha256: b8db3080e59b2503ca9e7922c3df2072cf13992354d5e944074ffa836fba43b7 + sha256: c05b7406fdabc7a49a3929d4af76bcaccbbffcbcdcf185b082e1ae07da323d19 url: "https://pub.dev" source: hosted - version: "0.4.0" + version: "0.4.1" clock: dependency: transitive description: @@ -237,10 +237,10 @@ packages: dependency: transitive description: name: code_builder - sha256: "4ad01d6e56db961d29661561effde45e519939fdaeb46c351275b182eac70189" + sha256: f692079e25e7869c14132d39f223f8eec9830eb76131925143b2129c4bb01b37 url: "https://pub.dev" source: hosted - version: "4.5.0" + version: "4.10.0" collection: dependency: "direct main" description: @@ -277,10 +277,10 @@ packages: dependency: transitive description: name: cross_file - sha256: "0b0036e8cccbfbe0555fd83c1d31a6f30b77a96b598b35a5d36dd41f718695e9" + sha256: fedaadfa3a6996f75211d835aaeb8fede285dae94262485698afd832371b9a5e url: "https://pub.dev" source: hosted - version: "0.3.3+4" + version: "0.3.3+8" crypto: dependency: transitive description: @@ -301,42 +301,42 @@ packages: dependency: transitive description: name: cupertino_icons - sha256: e35129dc44c9118cee2a5603506d823bab99c68393879edb440e0090d07586be + sha256: d57953e10f9f8327ce64a508a355f0b1ec902193f66288e8cb5070e7c47eeb2d url: "https://pub.dev" source: hosted - version: "1.0.5" + version: "1.0.6" custom_lint: dependency: "direct dev" description: name: custom_lint - sha256: "22bd87a362f433ba6aae127a7bac2838645270737f3721b180916d7c5946cb5d" + sha256: f89ff83efdba7c8996e86bb3bad0b759d58f9b19ae4d0e277a386ddd8b481217 url: "https://pub.dev" source: hosted - version: "0.5.11" + version: "0.6.0" custom_lint_builder: dependency: transitive description: name: custom_lint_builder - sha256: "7d0b094266b5c357769fffb920826b1a08373290f3c5c44b86253aae6873d5fc" + sha256: "3a14687fc71a5e2124a29722106f7b7e67dd5a6d58e33f2859650b46acff1d54" url: "https://pub.dev" source: hosted - version: "0.5.11" + version: "0.6.1" custom_lint_core: dependency: transitive description: name: custom_lint_core - sha256: "77dd37e9afe5ed86fc75de22ed3dfae5afb75303111358766550af23e7de53cd" + sha256: "1e9128e095ad5e0973469bdaac1ead8bfc86c485954c23cf617299de5e6fa029" url: "https://pub.dev" source: hosted - version: "0.5.11" + version: "0.6.1" dart_style: dependency: transitive description: name: dart_style - sha256: "1efa911ca7086affd35f463ca2fc1799584fb6aa89883cf0af8e3664d6a02d55" + sha256: "40ae61a5d43feea6d24bd22c0537a6629db858963b99b4bc1c3db80676f32368" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.3.4" dartx: dependency: transitive description: @@ -349,18 +349,18 @@ packages: dependency: transitive description: name: dbus - sha256: "6f07cba3f7b3448d42d015bfd3d53fe12e5b36da2423f23838efc1d5fb31a263" + sha256: "365c771ac3b0e58845f39ec6deebc76e3276aa9922b0cc60840712094d9047ac" url: "https://pub.dev" source: hosted - version: "0.7.8" + version: "0.7.10" device_info_plus: dependency: "direct main" description: name: device_info_plus - sha256: "0042cb3b2a76413ea5f8a2b40cec2a33e01d0c937e91f0f7c211fde4f7739ba6" + sha256: "77f757b789ff68e4eaf9c56d1752309bd9f7ad557cb105b938a7f8eb89e59110" url: "https://pub.dev" source: hosted - version: "9.1.1" + version: "9.1.2" device_info_plus_platform_interface: dependency: transitive description: @@ -373,18 +373,18 @@ packages: dependency: "direct main" description: name: easy_image_viewer - sha256: "6d765e9040a6e625796b387140b95f23318f25a448bf2647af30d17a77cea022" + sha256: "750bb85e0a34504557d378a616110540caeec2324490fc040709589219e75834" url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.4.1" easy_localization: dependency: "direct main" description: name: easy_localization - sha256: de63e3b422adfc97f256cbb3f8cf12739b6a4993d390f3cadb3f51837afaefe5 + sha256: "9c86754b22aaa3e74e471635b25b33729f958dd6fb83df0ad6612948a7b231af" url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.4" easy_logger: dependency: transitive description: @@ -413,42 +413,42 @@ packages: dependency: transitive description: name: file - sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" + sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" url: "https://pub.dev" source: hosted - version: "6.1.4" + version: "7.0.0" file_selector_linux: dependency: transitive description: name: file_selector_linux - sha256: "770eb1ab057b5ae4326d1c24cc57710758b9a46026349d021d6311bd27580046" + sha256: "045d372bf19b02aeb69cacf8b4009555fb5f6f0b7ad8016e5f46dd1387ddd492" url: "https://pub.dev" source: hosted - version: "0.9.2" + version: "0.9.2+1" file_selector_macos: dependency: transitive description: name: file_selector_macos - sha256: "4ada532862917bf16e3adb3891fe3a5917a58bae03293e497082203a80909412" + sha256: b15c3da8bd4908b9918111fa486903f5808e388b8d1c559949f584725a6594d6 url: "https://pub.dev" source: hosted - version: "0.9.3+1" + version: "0.9.3+3" file_selector_platform_interface: dependency: transitive description: name: file_selector_platform_interface - sha256: "412705a646a0ae90f33f37acfae6a0f7cbc02222d6cd34e479421c3e74d3853c" + sha256: a3994c26f10378a039faa11de174d7b78eb8f79e4dd0af2a451410c1a5c3f66b url: "https://pub.dev" source: hosted - version: "2.6.0" + version: "2.6.2" file_selector_windows: dependency: transitive description: name: file_selector_windows - sha256: "1372760c6b389842b77156203308940558a2817360154084368608413835fc26" + sha256: d3547240c20cabf205c7c7f01a50ecdbc413755814d6677f3cb366f04abcead0 url: "https://pub.dev" source: hosted - version: "0.9.3" + version: "0.9.3+1" fixnum: dependency: transitive description: @@ -487,10 +487,10 @@ packages: dependency: "direct main" description: name: flutter_hooks - sha256: "09f64db63fee3b2ab8b9038a1346be7d8986977fae3fec601275bf32455ccfc0" + sha256: cde36b12f7188c85286fba9b38cc5a902e7279f36dd676967106c041dc9dde70 url: "https://pub.dev" source: hosted - version: "0.20.4" + version: "0.20.5" flutter_launcher_icons: dependency: "direct dev" description: @@ -548,18 +548,18 @@ packages: dependency: transitive description: name: flutter_plugin_android_lifecycle - sha256: "950e77c2bbe1692bc0874fc7fb491b96a4dc340457f4ea1641443d0a6c1ea360" + sha256: b068ffc46f82a55844acfa4fdbb61fad72fa2aef0905548419d97f0f95c456da url: "https://pub.dev" source: hosted - version: "2.0.15" + version: "2.0.17" flutter_riverpod: dependency: transitive description: name: flutter_riverpod - sha256: da9591d1f8d5881628ccd5c25c40e74fc3eef50ba45e40c3905a06e1712412d5 + sha256: "4bce556b7ecbfea26109638d5237684538d4abc509d253e6c5c4c5733b360098" url: "https://pub.dev" source: hosted - version: "2.4.9" + version: "2.4.10" flutter_svg: dependency: "direct main" description: @@ -635,26 +635,26 @@ packages: dependency: transitive description: name: geolocator_android - sha256: "93906636752ea4d4e778afa981fdfe7409f545b3147046300df194330044d349" + sha256: "136f1c97e1903366393bda514c5d9e98843418baea52899aa45edae9af8a5cd6" url: "https://pub.dev" source: hosted - version: "4.3.1" + version: "4.5.2" geolocator_apple: dependency: transitive description: name: geolocator_apple - sha256: "79babf44b692ec5e789d322dc736ef71586056e8e6828f747c9e005456b248bf" + sha256: "2f2d4ee16c4df269e93c0e382be075cc01d5db6703c3196e4af20a634fe49ef4" url: "https://pub.dev" source: hosted - version: "2.3.5" + version: "2.3.6" geolocator_platform_interface: dependency: transitive description: name: geolocator_platform_interface - sha256: b8cc1d3be0ca039a3f2174b0b026feab8af3610e220b8532e42cff8ec6658535 + sha256: "009a21c4bc2761e58dccf07c24f219adaebe0ff707abdfd40b0a763d4003fab9" url: "https://pub.dev" source: hosted - version: "4.1.0" + version: "4.2.2" geolocator_web: dependency: transitive description: @@ -691,18 +691,18 @@ packages: dependency: "direct main" description: name: hooks_riverpod - sha256: c12a456e03ef9be65b0be66963596650ad7a3220e96c7e7b0a048562ea32d6ae + sha256: "758b07eba336e3cbacbd81dba481f2228a14102083fdde07045e8514e8054c49" url: "https://pub.dev" source: hosted - version: "2.4.9" + version: "2.4.10" hotreloader: dependency: transitive description: name: hotreloader - sha256: "94ee21a60ea2836500799f3af035dc3212b1562027f1e0031c14e087f0231449" + sha256: ed56fdc1f3a8ac924e717257621d09e9ec20e308ab6352a73a50a1d7a4d9158e url: "https://pub.dev" source: hosted - version: "4.1.0" + version: "4.2.0" html: dependency: transitive description: @@ -715,10 +715,10 @@ packages: dependency: "direct main" description: name: http - sha256: "6aa2946395183537c8b880962d935877325d6a09a2867c3970c05c0fed6ac482" + sha256: "5895291c13fa8a3bd82e76d5627f69e0d85ca6a30dcac95c4ea19a5d555879c2" url: "https://pub.dev" source: hosted - version: "0.13.5" + version: "0.13.6" http_multi_server: dependency: transitive description: @@ -739,10 +739,10 @@ packages: dependency: transitive description: name: image - sha256: "004a2e90ce080f8627b5a04aecb4cdfac87d2c3f3b520aa291260be5a32c033d" + sha256: "4c68bfd5ae83e700b5204c1e74451e7bf3cf750e6843c6e158289cf56bda018e" url: "https://pub.dev" source: hosted - version: "4.1.4" + version: "4.1.7" image_picker: dependency: "direct main" description: @@ -755,58 +755,58 @@ packages: dependency: transitive description: name: image_picker_android - sha256: "8179b54039b50eee561676232304f487602e2950ffb3e8995ed9034d6505ca34" + sha256: "39f2bfe497e495450c81abcd44b62f56c2a36a37a175da7d137b4454977b51b1" url: "https://pub.dev" source: hosted - version: "0.8.7+4" + version: "0.8.9+3" image_picker_for_web: dependency: transitive description: name: image_picker_for_web - sha256: "869fe8a64771b7afbc99fc433a5f7be2fea4d1cb3d7c11a48b6b579eb9c797f0" + sha256: e2423c53a68b579a7c37a1eda967b8ae536c3d98518e5db95ca1fe5719a730a3 url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "3.0.2" image_picker_ios: dependency: transitive description: name: image_picker_ios - sha256: b3e2f21feb28b24dd73a35d7ad6e83f568337c70afab5eabac876e23803f264b + sha256: fadafce49e8569257a0cad56d24438a6fa1f0cbd7ee0af9b631f7492818a4ca3 url: "https://pub.dev" source: hosted - version: "0.8.8" + version: "0.8.9+1" image_picker_linux: dependency: transitive description: name: image_picker_linux - sha256: "02cbc21fe1706b97942b575966e5fbbeaac535e76deef70d3a242e4afb857831" + sha256: "4ed1d9bb36f7cd60aa6e6cd479779cc56a4cb4e4de8f49d487b1aaad831300fa" url: "https://pub.dev" source: hosted - version: "0.2.1" + version: "0.2.1+1" image_picker_macos: dependency: transitive description: name: image_picker_macos - sha256: cee2aa86c56780c13af2c77b5f2f72973464db204569e1ba2dd744459a065af4 + sha256: "3f5ad1e8112a9a6111c46d0b57a7be2286a9a07fc6e1976fdf5be2bd31d4ff62" url: "https://pub.dev" source: hosted - version: "0.2.1" + version: "0.2.1+1" image_picker_platform_interface: dependency: transitive description: name: image_picker_platform_interface - sha256: c1134543ae2187e85299996d21c526b2f403854994026d575ae4cf30d7bb2a32 + sha256: fa4e815e6fcada50e35718727d83ba1c92f1edf95c0b4436554cec301b56233b url: "https://pub.dev" source: hosted - version: "2.9.0" + version: "2.9.3" image_picker_windows: dependency: transitive description: name: image_picker_windows - sha256: c3066601ea42113922232c7b7b3330a2d86f029f685bba99d82c30e799914952 + sha256: "6ad07afc4eb1bc25f3a01084d28520496c4a3bb0cb13685435838167c9dcedeb" url: "https://pub.dev" source: hosted - version: "0.2.1" + version: "0.2.1+1" integration_test: dependency: "direct dev" description: flutter @@ -868,6 +868,30 @@ packages: url: "https://pub.dev" source: hosted version: "4.8.1" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" + url: "https://pub.dev" + source: hosted + version: "10.0.0" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 + url: "https://pub.dev" + source: hosted + version: "2.0.1" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 + url: "https://pub.dev" + source: hosted + version: "2.0.1" lints: dependency: transitive description: @@ -898,7 +922,7 @@ packages: description: path: maplibre_gl_platform_interface ref: main - resolved-ref: acb428a005efd9832a0a8e7ef0945f899dfb3ca5 + resolved-ref: "3cf0abb051849ca3f14e6aa19d2261ad18f22ce1" url: "https://github.com/maplibre/flutter-maplibre-gl.git" source: git version: "0.18.0" @@ -907,7 +931,7 @@ packages: description: path: maplibre_gl_web ref: main - resolved-ref: acb428a005efd9832a0a8e7ef0945f899dfb3ca5 + resolved-ref: "3cf0abb051849ca3f14e6aa19d2261ad18f22ce1" url: "https://github.com/maplibre/flutter-maplibre-gl.git" source: git version: "0.18.0" @@ -915,34 +939,34 @@ packages: dependency: transitive description: name: matcher - sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb url: "https://pub.dev" source: hosted - version: "0.12.16" + version: "0.12.16+1" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" + sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" url: "https://pub.dev" source: hosted - version: "0.5.0" + version: "0.8.0" meta: dependency: "direct overridden" description: name: meta - sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 + sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.12.0" mime: dependency: transitive description: name: mime - sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e + sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.0.5" mocktail: dependency: "direct dev" description: @@ -1010,10 +1034,10 @@ packages: dependency: "direct main" description: name: path - sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" url: "https://pub.dev" source: hosted - version: "1.8.3" + version: "1.9.0" path_parsing: dependency: transitive description: @@ -1034,18 +1058,18 @@ packages: dependency: transitive description: name: path_provider_android - sha256: "5d44fc3314d969b84816b569070d7ace0f1dea04bd94a83f74c4829615d22ad8" + sha256: "477184d672607c0a3bf68fbbf601805f92ef79c82b64b4d6eb318cbca4c48668" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.2.2" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: "1b744d3d774e5a879bb76d6cd1ecee2ba2c6960c03b1020cd35212f6aa267ac5" + sha256: "5a7999be66e000916500be4f15a3633ebceb8302719b47b9cc49ce924125350f" url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.3.2" path_provider_ios: dependency: "direct main" description: @@ -1058,50 +1082,50 @@ packages: dependency: transitive description: name: path_provider_linux - sha256: ba2b77f0c52a33db09fc8caf85b12df691bf28d983e84cf87ff6d693cfa007b3 + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.2.1" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface - sha256: bced5679c7df11190e1ddc35f3222c858f328fff85c3942e46e7f5589bf9eb84 + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.2" path_provider_windows: dependency: transitive description: name: path_provider_windows - sha256: ee0e0d164516b90ae1f970bdf29f726f1aa730d7cfc449ecc74c495378b705da + sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.2.1" permission_handler: dependency: "direct main" description: name: permission_handler - sha256: "45ff3fbcb99040fde55c528d5e3e6ca29171298a85436274d49c6201002087d6" + sha256: "74e962b7fad7ff75959161bb2c0ad8fe7f2568ee82621c9c2660b751146bfe44" url: "https://pub.dev" source: hosted - version: "11.2.0" + version: "11.3.0" permission_handler_android: dependency: transitive description: name: permission_handler_android - sha256: "758284a0976772f9c744d6384fc5dc4834aa61e3f7aa40492927f244767374eb" + sha256: "1acac6bae58144b442f11e66621c062aead9c99841093c38f5bcdcc24c1c3474" url: "https://pub.dev" source: hosted - version: "12.0.3" + version: "12.0.5" permission_handler_apple: dependency: transitive description: name: permission_handler_apple - sha256: c6bf440f80acd2a873d3d91a699e4cc770f86e7e6b576dda98759e8b92b39830 + sha256: bdafc6db74253abb63907f4e357302e6bb786ab41465e8635f362ee71fd8707b url: "https://pub.dev" source: hosted - version: "9.3.0" + version: "9.4.0" permission_handler_html: dependency: transitive description: @@ -1114,10 +1138,10 @@ packages: dependency: transitive description: name: permission_handler_platform_interface - sha256: "5c43148f2bfb6d14c5a8162c0a712afe891f2d847f35fcff29c406b37da43c3c" + sha256: "23dfba8447c076ab5be3dee9ceb66aad345c4a648f0cac292c77b1eb0e800b78" url: "https://pub.dev" source: hosted - version: "4.1.0" + version: "4.2.0" permission_handler_windows: dependency: transitive description: @@ -1154,26 +1178,26 @@ packages: dependency: transitive description: name: platform - sha256: ae68c7bfcd7383af3629daafb32fb4e8681c7154428da4febcff06200585f102 + sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" url: "https://pub.dev" source: hosted - version: "3.1.2" + version: "3.1.4" plugin_platform_interface: dependency: transitive description: name: plugin_platform_interface - sha256: "43798d895c929056255600343db8f049921cbec94d31ec87f1dc5c16c01935dd" + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" url: "https://pub.dev" source: hosted - version: "2.1.5" + version: "2.1.8" pointycastle: dependency: transitive description: name: pointycastle - sha256: "7c1e5f0d23c9016c5bbd8b1473d0d3fb3fc851b876046039509e18e0c7485f2c" + sha256: "43ac87de6e10afabc85c445745a7b799e04de84cebaa4fd7bf55a5e1e9604d29" url: "https://pub.dev" source: hosted - version: "3.7.3" + version: "3.7.4" pool: dependency: transitive description: @@ -1186,18 +1210,18 @@ packages: dependency: transitive description: name: process - sha256: "53fd8db9cec1d37b0574e12f07520d582019cb6c44abf5479a01505099a34a09" + sha256: "21e54fd2faf1b5bdd5102afd25012184a6793927648ea81eea80552ac9405b32" url: "https://pub.dev" source: hosted - version: "4.2.4" + version: "5.0.2" provider: dependency: transitive description: name: provider - sha256: cdbe7530b12ecd9eb455bdaa2fcb8d4dad22e80b8afb4798b41479d5ce26847f + sha256: "9a96a0a19b594dbc5bf0f1f27d2bc67d5f95957359b461cd9feb44ed6ae75096" url: "https://pub.dev" source: hosted - version: "6.0.5" + version: "6.1.1" pub_semver: dependency: transitive description: @@ -1218,42 +1242,42 @@ packages: dependency: transitive description: name: riverpod - sha256: "942999ee48b899f8a46a860f1e13cee36f2f77609eb54c5b7a669bb20d550b11" + sha256: "548e2192eb7aeb826eb89387f814edb76594f3363e2c0bb99dd733d795ba3589" url: "https://pub.dev" source: hosted - version: "2.4.9" + version: "2.5.0" riverpod_analyzer_utils: dependency: transitive description: name: riverpod_analyzer_utils - sha256: d4dabc35358413bf4611fcb6abb46308a67c4ef4cd5e69fd3367b11925c59f57 + sha256: "8b71f03fc47ae27d13769496a1746332df4cec43918aeba9aff1e232783a780f" url: "https://pub.dev" source: hosted - version: "0.5.0" + version: "0.5.1" riverpod_annotation: dependency: "direct main" description: name: riverpod_annotation - sha256: b70e95fbd5ca7ce42f5148092022971bb2e9843b6ab71e97d479e8ab52e98979 + sha256: "77e5d51afa4fa3e67903fb8746f33d368728d7051a0b6c292bcee60aeba46d95" url: "https://pub.dev" source: hosted - version: "2.3.3" + version: "2.3.4" riverpod_generator: dependency: "direct dev" description: name: riverpod_generator - sha256: ff8f064f1d7ef3cc6af481bba8e9a3fcdb4d34df34fac1b39bbc003167065be0 + sha256: "359068f04879347ae4edbe66c81cc95f83fa1743806d1a0c86e55dd3c33ebb32" url: "https://pub.dev" source: hosted - version: "2.3.9" + version: "2.3.11" riverpod_lint: dependency: "direct dev" description: name: riverpod_lint - sha256: "944929ef82c9bfeaa455ccab97920abcf847a0ffed5c9f6babc520a95db25176" + sha256: e9bbd02e9e89e18eecb183bbca556d7b523a0669024da9b8167c08903f442937 url: "https://pub.dev" source: hosted - version: "2.3.7" + version: "2.3.9" rxdart: dependency: transitive description: @@ -1274,10 +1298,10 @@ packages: dependency: "direct main" description: name: share_plus - sha256: f74fc3f1cbd99f39760182e176802f693fa0ec9625c045561cfad54681ea93dd + sha256: "3ef39599b00059db0990ca2e30fca0a29d8b37aae924d60063f8e0184cf20900" url: "https://pub.dev" source: hosted - version: "7.2.1" + version: "7.2.2" share_plus_platform_interface: dependency: transitive description: @@ -1290,26 +1314,26 @@ packages: dependency: transitive description: name: shared_preferences - sha256: "0344316c947ffeb3a529eac929e1978fcd37c26be4e8468628bac399365a3ca1" + sha256: "81429e4481e1ccfb51ede496e916348668fd0921627779233bd24cc3ff6abd02" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.2.2" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: fe8401ec5b6dcd739a0fe9588802069e608c3fdbfd3c3c93e546cf2f90438076 + sha256: "8568a389334b6e83415b6aae55378e158fbc2314e074983362d20c562780fb06" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.2.1" shared_preferences_foundation: dependency: transitive description: name: shared_preferences_foundation - sha256: d29753996d8eb8f7619a1f13df6ce65e34bc107bef6330739ed76f18b22310ef + sha256: "7708d83064f38060c7b39db12aefe449cb8cdc031d6062280087bc4cdb988f5c" url: "https://pub.dev" source: hosted - version: "2.3.3" + version: "2.3.5" shared_preferences_linux: dependency: transitive description: @@ -1322,18 +1346,18 @@ packages: dependency: transitive description: name: shared_preferences_platform_interface - sha256: "23b052f17a25b90ff2b61aad4cc962154da76fb62848a9ce088efe30d7c50ab1" + sha256: "22e2ecac9419b4246d7c22bfbbda589e3acf5c0351137d87dd2939d984d37c3b" url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.3.2" shared_preferences_web: dependency: transitive description: name: shared_preferences_web - sha256: "7347b194fb0bbeb4058e6a4e87ee70350b6b2b90f8ac5f8bd5b3a01548f6d33a" + sha256: "7b15ffb9387ea3e237bb7a66b8a23d2147663d391cafc5c8f37b2e7b4bde5d21" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.2.2" shared_preferences_windows: dependency: transitive description: @@ -1383,10 +1407,10 @@ packages: dependency: transitive description: name: source_gen - sha256: fc0da689e5302edb6177fdd964efcb7f58912f43c28c2047a808f5bfff643d16 + sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832" url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.5.0" source_span: dependency: transitive description: @@ -1395,22 +1419,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.0" + sprintf: + dependency: transitive + description: + name: sprintf + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + url: "https://pub.dev" + source: hosted + version: "7.0.0" sqflite: dependency: transitive description: name: sqflite - sha256: "591f1602816e9c31377d5f008c2d9ef7b8aca8941c3f89cc5fd9d84da0c38a9a" + sha256: a9016f495c927cb90557c909ff26a6d92d9bd54fc42ba92e19d4e79d61e798c6 url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.3.2" sqflite_common: dependency: transitive description: name: sqflite_common - sha256: "1b92f368f44b0dee2425bb861cfa17b6f6cf3961f762ff6f941d20b33355660a" + sha256: "28d8c66baee4968519fb8bd6cdbedad982d6e53359091f0b74544a9f32ec72d5" url: "https://pub.dev" source: hosted - version: "2.5.0" + version: "2.5.3" stack_trace: dependency: transitive description: @@ -1463,10 +1495,10 @@ packages: dependency: transitive description: name: synchronized - sha256: "5fcbd27688af6082f5abd611af56ee575342c30e87541d0245f7ff99faa02c60" + sha256: "539ef412b170d65ecdafd780f924e5be3f60032a1128df156adad6c5b373d558" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.1.0+1" term_glyph: dependency: transitive description: @@ -1495,10 +1527,10 @@ packages: dependency: transitive description: name: time - sha256: "83427e11d9072e038364a5e4da559e85869b227cf699a541be0da74f14140124" + sha256: ad8e018a6c9db36cb917a031853a1aae49467a93e0d464683e029537d848c221 url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.4" timezone: dependency: "direct main" description: @@ -1543,10 +1575,10 @@ packages: dependency: transitive description: name: url_launcher_android - sha256: "507dc655b1d9cb5ebc756032eb785f114e415f91557b73bf60b7e201dfedeb2f" + sha256: d4ed0711849dd8e33eb2dd69c25db0d0d3fdc37e0a62e629fe32f57a22db2745 url: "https://pub.dev" source: hosted - version: "6.2.2" + version: "6.3.0" url_launcher_ios: dependency: transitive description: @@ -1575,10 +1607,10 @@ packages: dependency: transitive description: name: url_launcher_platform_interface - sha256: "4aca1e060978e19b2998ee28503f40b5ba6226819c2b5e3e4d1821e8ccd92198" + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.3.2" url_launcher_web: dependency: transitive description: @@ -1599,10 +1631,10 @@ packages: dependency: transitive description: name: uuid - sha256: "648e103079f7c64a36dc7d39369cabb358d377078a051d6ae2ad3aa539519313" + sha256: cd210a09f7c18cbe5a02511718e0334de6559871052c90a90c0cca46a4aa81c8 url: "https://pub.dev" source: hosted - version: "3.0.7" + version: "4.3.3" vector_graphics: dependency: transitive description: @@ -1647,10 +1679,10 @@ packages: dependency: transitive description: name: video_player_android - sha256: f338a5a396c845f4632959511cad3542cdf3167e1b2a1a948ef07f7123c03608 + sha256: "4dd9b8b86d70d65eecf3dcabfcdfbb9c9115d244d022654aba49a00336d540c2" url: "https://pub.dev" source: hosted - version: "2.4.9" + version: "2.4.12" video_player_avfoundation: dependency: transitive description: @@ -1663,26 +1695,26 @@ packages: dependency: transitive description: name: video_player_platform_interface - sha256: "1ca9acd7a0fb15fb1a990cb554e6f004465c6f37c99d2285766f08a4b2802988" + sha256: "236454725fafcacf98f0f39af0d7c7ab2ce84762e3b63f2cbb3ef9a7e0550bc6" url: "https://pub.dev" source: hosted - version: "6.2.0" + version: "6.2.2" video_player_web: dependency: transitive description: name: video_player_web - sha256: "44ce41424d104dfb7cf6982cc6b84af2b007a24d126406025bf40de5d481c74c" + sha256: "34beb3a07d4331a24f7e7b2f75b8e2b103289038e07e65529699a671b6a6e2cb" url: "https://pub.dev" source: hosted - version: "2.0.16" + version: "2.1.3" vm_service: dependency: transitive description: name: vm_service - sha256: c538be99af830f478718b51630ec1b6bee5e74e52c8a802d328d9e71d35d2583 + sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 url: "https://pub.dev" source: hosted - version: "11.10.0" + version: "13.0.0" wakelock_plus: dependency: "direct main" description: @@ -1727,34 +1759,34 @@ packages: dependency: transitive description: name: webdriver - sha256: "3c923e918918feeb90c4c9fdf1fe39220fa4c0e8e2c0fffaded174498ef86c49" + sha256: "003d7da9519e1e5f329422b36c4dcdf18d7d2978d1ba099ea4e45ba490ed845e" url: "https://pub.dev" source: hosted - version: "3.0.2" + version: "3.0.3" win32: dependency: transitive description: name: win32 - sha256: "5a751eddf9db89b3e5f9d50c20ab8612296e4e8db69009788d6c8b060a84191c" + sha256: "464f5674532865248444b4c3daca12bd9bf2d7c47f759ce2617986e7229494a8" url: "https://pub.dev" source: hosted - version: "4.1.4" + version: "5.2.0" win32_registry: dependency: transitive description: name: win32_registry - sha256: "1c52f994bdccb77103a6231ad4ea331a244dbcef5d1f37d8462f713143b0bfae" + sha256: "41fd8a189940d8696b1b810efb9abcf60827b6cbfab90b0c43e8439e3a39d85a" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.2" xdg_directories: dependency: transitive description: name: xdg_directories - sha256: f0c26453a2d47aa4c2570c6a033246a3fc62da2fe23c7ffdd0a7495086dc0247 + sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "1.0.4" xml: dependency: transitive description: diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 785d338da..852a2bb08 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -3,7 +3,6 @@ description: Immich - selfhosted backup media file on mobile phone publish_to: 'none' version: 1.99.0+129 -isar_version: &isar_version 3.1.0+1 environment: sdk: '>=3.0.0 <4.0.0' @@ -37,10 +36,10 @@ dependencies: flutter_svg: ^2.0.9 package_info_plus: ^5.0.1 url_launcher: ^6.2.4 - http: 0.13.5 - cancellation_token_http: ^1.1.0 + http: ^0.13.6 + cancellation_token_http: ^2.0.0 easy_localization: ^3.0.3 - share_plus: ^7.2.1 + share_plus: ^7.2.2 flutter_displaymode: ^0.6.0 scrollable_positioned_list: ^0.3.8 path: ^1.8.3 @@ -49,8 +48,8 @@ dependencies: http_parser: ^4.0.2 flutter_web_auth: ^0.5.0 easy_image_viewer: ^1.4.0 - isar: *isar_version - isar_flutter_libs: *isar_version # contains Isar Core + isar: ^3.1.0+1 + isar_flutter_libs: ^3.1.0+1 permission_handler: ^11.2.0 device_info_plus: ^9.1.1 connectivity_plus: ^5.0.2 @@ -91,10 +90,10 @@ dev_dependencies: auto_route_generator: ^7.3.2 flutter_launcher_icons: ^0.13.1 flutter_native_splash: ^2.3.9 - isar_generator: *isar_version + isar_generator: ^3.1.0+1 integration_test: sdk: flutter - custom_lint: ^0.5.8 + custom_lint: ^0.6.0 riverpod_lint: ^2.3.7 riverpod_generator: ^2.3.9 mocktail: ^1.0.3 diff --git a/README_ca_ES.md b/readme_i18n/README_ca_ES.md similarity index 99% rename from README_ca_ES.md rename to readme_i18n/README_ca_ES.md index cf7bcb4f5..d46ae5c26 100644 --- a/README_ca_ES.md +++ b/readme_i18n/README_ca_ES.md @@ -18,7 +18,7 @@

- English + English Español Français Italiano diff --git a/README_de_DE.md b/readme_i18n/README_de_DE.md similarity index 99% rename from README_de_DE.md rename to readme_i18n/README_de_DE.md index 322a58b96..76cdaf5a2 100644 --- a/README_de_DE.md +++ b/readme_i18n/README_de_DE.md @@ -18,7 +18,7 @@

- English + English Català Español Français diff --git a/README_es_ES.md b/readme_i18n/README_es_ES.md similarity index 99% rename from README_es_ES.md rename to readme_i18n/README_es_ES.md index 047c47260..1f0dd0666 100644 --- a/README_es_ES.md +++ b/readme_i18n/README_es_ES.md @@ -18,7 +18,7 @@

- English + English Català Français Italiano diff --git a/README_fr_FR.md b/readme_i18n/README_fr_FR.md similarity index 99% rename from README_fr_FR.md rename to readme_i18n/README_fr_FR.md index 3b4c6d428..783189c69 100644 --- a/README_fr_FR.md +++ b/readme_i18n/README_fr_FR.md @@ -18,7 +18,7 @@

- English + English Català Español Italiano diff --git a/README_it_IT.md b/readme_i18n/README_it_IT.md similarity index 99% rename from README_it_IT.md rename to readme_i18n/README_it_IT.md index 7e8863a93..9dea33c99 100644 --- a/README_it_IT.md +++ b/readme_i18n/README_it_IT.md @@ -18,7 +18,7 @@

- English + English Català Español Français diff --git a/README_ja_JP.md b/readme_i18n/README_ja_JP.md similarity index 99% rename from README_ja_JP.md rename to readme_i18n/README_ja_JP.md index 90143025a..e002bfb21 100644 --- a/README_ja_JP.md +++ b/readme_i18n/README_ja_JP.md @@ -18,7 +18,7 @@

- English + English Català Español Français diff --git a/README_ko_KR.md b/readme_i18n/README_ko_KR.md similarity index 99% rename from README_ko_KR.md rename to readme_i18n/README_ko_KR.md index 3a1599597..05ffc96d9 100644 --- a/README_ko_KR.md +++ b/readme_i18n/README_ko_KR.md @@ -18,7 +18,7 @@

- English + English Català Español Français diff --git a/README_nl_NL.md b/readme_i18n/README_nl_NL.md similarity index 99% rename from README_nl_NL.md rename to readme_i18n/README_nl_NL.md index 1eeb41e5a..98de2581c 100644 --- a/README_nl_NL.md +++ b/readme_i18n/README_nl_NL.md @@ -18,7 +18,7 @@

- English + English Català Español Français diff --git a/README_ru_RU.md b/readme_i18n/README_ru_RU.md similarity index 100% rename from README_ru_RU.md rename to readme_i18n/README_ru_RU.md diff --git a/README_tr_TR.md b/readme_i18n/README_tr_TR.md similarity index 99% rename from README_tr_TR.md rename to readme_i18n/README_tr_TR.md index 6e5c43ee6..b07e023f4 100644 --- a/README_tr_TR.md +++ b/readme_i18n/README_tr_TR.md @@ -18,7 +18,7 @@

- English + English Català Español Français diff --git a/README_zh_CN.md b/readme_i18n/README_zh_CN.md similarity index 99% rename from README_zh_CN.md rename to readme_i18n/README_zh_CN.md index 874b18f09..dc573f735 100644 --- a/README_zh_CN.md +++ b/readme_i18n/README_zh_CN.md @@ -22,7 +22,7 @@

- English + English Català Español Français diff --git a/server/e2e/client/asset-api.ts b/server/e2e/client/asset-api.ts index 8f30e1f4a..f32d58611 100644 --- a/server/e2e/client/asset-api.ts +++ b/server/e2e/client/asset-api.ts @@ -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'; export const assetApi = { diff --git a/server/e2e/client/auth-api.ts b/server/e2e/client/auth-api.ts index 46b21fb98..a8cfe4660 100644 --- a/server/e2e/client/auth-api.ts +++ b/server/e2e/client/auth-api.ts @@ -1,5 +1,5 @@ -import { LoginResponseDto } from 'src/domain/auth/auth.dto'; -import { UserResponseDto } from 'src/domain/user/response-dto/user-response.dto'; +import { LoginResponseDto } from 'src/dtos/auth.dto'; +import { UserResponseDto } from 'src/dtos/user.dto'; import request from 'supertest'; import { adminSignupStub, loginResponseStub, loginStub } from 'test/fixtures/auth.stub'; diff --git a/server/e2e/client/library-api.ts b/server/e2e/client/library-api.ts index 90b1b7451..70c8c4c36 100644 --- a/server/e2e/client/library-api.ts +++ b/server/e2e/client/library-api.ts @@ -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'; export const libraryApi = { diff --git a/server/e2e/jobs/specs/library-watcher.e2e-spec.ts b/server/e2e/jobs/specs/library-watcher.e2e-spec.ts index 3d43227b7..20a9d3202 100644 --- a/server/e2e/jobs/specs/library-watcher.e2e-spec.ts +++ b/server/e2e/jobs/specs/library-watcher.e2e-spec.ts @@ -1,12 +1,12 @@ import { api } from 'e2e/client'; import fs from 'node:fs/promises'; import path from 'node:path'; -import { LoginResponseDto } from 'src/domain/auth/auth.dto'; -import { LibraryResponseDto } from 'src/domain/library/library.dto'; -import { LibraryService } from 'src/domain/library/library.service'; +import { LoginResponseDto } from 'src/dtos/auth.dto'; +import { LibraryResponseDto } from 'src/dtos/library.dto'; import { AssetType } from 'src/entities/asset.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 { IMMICH_TEST_ASSET_PATH, IMMICH_TEST_ASSET_TEMP_PATH, diff --git a/server/e2e/jobs/specs/library.e2e-spec.ts b/server/e2e/jobs/specs/library.e2e-spec.ts index 0e2ce08f9..d6885cbf3 100644 --- a/server/e2e/jobs/specs/library.e2e-spec.ts +++ b/server/e2e/jobs/specs/library.e2e-spec.ts @@ -1,7 +1,7 @@ import { api } from 'e2e/client'; import fs from 'node:fs'; 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 request from 'supertest'; import { errorStub } from 'test/fixtures/error.stub'; diff --git a/server/package-lock.json b/server/package-lock.json index 079b8db53..5d8bfa17c 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -81,7 +81,6 @@ "@types/mock-fs": "^4.13.1", "@types/multer": "^1.4.7", "@types/node": "^20.5.7", - "@types/sharp": "^0.32.0", "@types/supertest": "^6.0.0", "@types/ua-parser-js": "^0.7.36", "@typescript-eslint/eslint-plugin": "^7.0.0", @@ -4840,16 +4839,6 @@ "@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": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/shimmer/-/shimmer-1.0.5.tgz", @@ -17900,15 +17889,6 @@ "@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": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/shimmer/-/shimmer-1.0.5.tgz", diff --git a/server/package.json b/server/package.json index d6f7036e3..3283e42ae 100644 --- a/server/package.json +++ b/server/package.json @@ -25,12 +25,12 @@ "e2e:jobs": "jest --config e2e/jobs/jest-e2e.json --runInBand", "typeorm": "typeorm", "typeorm:migrations:create": "typeorm migration:create", - "typeorm:migrations:generate": "typeorm migration:generate -d ./dist/infra/database.config.js", - "typeorm:migrations:run": "typeorm migration:run -d ./dist/infra/database.config.js", - "typeorm:migrations:revert": "typeorm migration:revert -d ./dist/infra/database.config.js", - "typeorm:schema:drop": "typeorm query -d ./dist/infra/database.config.js 'DROP schema public cascade; CREATE schema public;'", + "typeorm:migrations:generate": "typeorm migration:generate -d ./dist/database.config.js", + "typeorm:migrations:run": "typeorm migration:run -d ./dist/database.config.js", + "typeorm:migrations:revert": "typeorm migration:revert -d ./dist/database.config.js", + "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", - "sql:generate": "node ./dist/infra/sql-generator/" + "sql:generate": "node ./dist/utils/sql.js" }, "dependencies": { "@babel/runtime": "^7.22.11", @@ -105,7 +105,6 @@ "@types/mock-fs": "^4.13.1", "@types/multer": "^1.4.7", "@types/node": "^20.5.7", - "@types/sharp": "^0.32.0", "@types/supertest": "^6.0.0", "@types/ua-parser-js": "^0.7.36", "@typescript-eslint/eslint-plugin": "^7.0.0", @@ -145,19 +144,20 @@ "^.+\\.ts$": "ts-jest" }, "collectCoverageFrom": [ - "/src/**/*.(t|j)s", - "!/src/infra/**/*", - "!/src/migrations/**/*", - "!/src/subscribers/**/*", - "!/src/immich/controllers/**/*" + "/src/cores/*.(t|j)s", + "/src/dtos/*.(t|j)s", + "/src/interfaces/*.(t|j)s", + "/src/services/*.(t|j)s", + "/src/utils/*.(t|j)s", + "/src/*.t|j)s" ], "coverageDirectory": "./coverage", "coverageThreshold": { - "./src/domain/": { - "branches": 75, - "functions": 80, - "lines": 85, - "statements": 85 + "./src/": { + "branches": 70, + "functions": 75, + "lines": 80, + "statements": 80 } }, "testEnvironment": "node", diff --git a/server/src/app.module.ts b/server/src/app.module.ts new file mode 100644 index 000000000..c44b18727 --- /dev/null +++ b/server/src/app.module.ts @@ -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 {} diff --git a/server/src/apps/api.main.ts b/server/src/apps/api.main.ts deleted file mode 100644 index 9ffdd1d48..000000000 --- a/server/src/apps/api.main.ts +++ /dev/null @@ -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(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}] `); -} diff --git a/server/src/apps/api.module.ts b/server/src/apps/api.module.ts deleted file mode 100644 index 04c95f199..000000000 --- a/server/src/apps/api.module.ts +++ /dev/null @@ -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(); - } -} diff --git a/server/src/apps/immich-admin.main.ts b/server/src/apps/immich-admin.main.ts deleted file mode 100755 index 5f528a21b..000000000 --- a/server/src/apps/immich-admin.main.ts +++ /dev/null @@ -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); -} diff --git a/server/src/apps/immich-admin.module.ts b/server/src/apps/immich-admin.module.ts deleted file mode 100644 index d0e5cab4c..000000000 --- a/server/src/apps/immich-admin.module.ts +++ /dev/null @@ -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 {} diff --git a/server/src/apps/microservices.main.ts b/server/src/apps/microservices.main.ts deleted file mode 100644 index 552fb714c..000000000 --- a/server/src/apps/microservices.main.ts +++ /dev/null @@ -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}] `); -} diff --git a/server/src/apps/microservices.module.ts b/server/src/apps/microservices.module.ts deleted file mode 100644 index 3ddfb28f3..000000000 --- a/server/src/apps/microservices.module.ts +++ /dev/null @@ -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(); - } -} diff --git a/server/src/commands/list-users.command.ts b/server/src/commands/list-users.command.ts index 222833fad..32bcc35d9 100644 --- a/server/src/commands/list-users.command.ts +++ b/server/src/commands/list-users.command.ts @@ -1,6 +1,6 @@ import { Command, CommandRunner } from 'nest-commander'; -import { UserService } from 'src/domain/user/user.service'; import { UserEntity } from 'src/entities/user.entity'; +import { UserService } from 'src/services/user.service'; @Command({ name: 'list-users', diff --git a/server/src/commands/oauth-login.ts b/server/src/commands/oauth-login.ts index 12562fae1..c9bb4d5ef 100644 --- a/server/src/commands/oauth-login.ts +++ b/server/src/commands/oauth-login.ts @@ -1,5 +1,5 @@ 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({ name: 'enable-oauth-login', diff --git a/server/src/commands/password-login.ts b/server/src/commands/password-login.ts index 953a1293a..3d992f858 100644 --- a/server/src/commands/password-login.ts +++ b/server/src/commands/password-login.ts @@ -1,5 +1,5 @@ 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({ name: 'enable-password-login', diff --git a/server/src/commands/reset-admin-password.command.ts b/server/src/commands/reset-admin-password.command.ts index ce0a897a8..a186603a3 100644 --- a/server/src/commands/reset-admin-password.command.ts +++ b/server/src/commands/reset-admin-password.command.ts @@ -1,6 +1,6 @@ import { Command, CommandRunner, InquirerService, Question, QuestionSet } from 'nest-commander'; -import { UserResponseDto } from 'src/domain/user/response-dto/user-response.dto'; -import { UserService } from 'src/domain/user/user.service'; +import { UserResponseDto } from 'src/dtos/user.dto'; +import { UserService } from 'src/services/user.service'; @Command({ name: 'reset-admin-password', diff --git a/server/src/config.ts b/server/src/config.ts index b97f0bb7e..c7d2302c1 100644 --- a/server/src/config.ts +++ b/server/src/config.ts @@ -3,8 +3,8 @@ import { ConfigModuleOptions } from '@nestjs/config'; import { QueueOptions } from 'bullmq'; import { RedisOptions } from 'ioredis'; import Joi from 'joi'; -import { QueueName } from 'src/domain/job/job.constants'; import { LogLevel } from 'src/entities/system-config.entity'; +import { QueueName } from 'src/interfaces/job.interface'; const WHEN_DB_URL_SET = Joi.when('DB_URL', { is: Joi.exist(), @@ -69,5 +69,3 @@ export const bullConfig: QueueOptions = { }; export const bullQueues: RegisterQueueOptions[] = Object.values(QueueName).map((name) => ({ name })); - -export const excludePaths = ['/.well-known/immich', '/custom.css', '/favicon.ico']; diff --git a/server/src/constants.ts b/server/src/constants.ts new file mode 100644 index 000000000..f6cef9059 --- /dev/null +++ b/server/src/constants.ts @@ -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 = { + 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 }, +}; diff --git a/server/src/controllers/activity.controller.ts b/server/src/controllers/activity.controller.ts index c405b0c6f..a65b284ca 100644 --- a/server/src/controllers/activity.controller.ts +++ b/server/src/controllers/activity.controller.ts @@ -7,10 +7,10 @@ import { ActivityResponseDto, ActivitySearchDto, ActivityStatisticsResponseDto, -} from 'src/domain/activity/activity.dto'; -import { ActivityService } from 'src/domain/activity/activity.service'; -import { AuthDto } from 'src/domain/auth/auth.dto'; +} from 'src/dtos/activity.dto'; +import { AuthDto } from 'src/dtos/auth.dto'; import { Auth, Authenticated } from 'src/middleware/auth.guard'; +import { ActivityService } from 'src/services/activity.service'; import { UUIDParamDto } from 'src/validation'; @ApiTags('Activity') diff --git a/server/src/controllers/album.controller.ts b/server/src/controllers/album.controller.ts index 18fd549b6..c4b11fbb4 100644 --- a/server/src/controllers/album.controller.ts +++ b/server/src/controllers/album.controller.ts @@ -1,15 +1,18 @@ import { Body, Controller, Delete, Get, Param, Patch, Post, Put, Query } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; -import { AlbumCountResponseDto, AlbumResponseDto } from 'src/domain/album/album-response.dto'; -import { AlbumService } from 'src/domain/album/album.service'; -import { AddUsersDto } from 'src/domain/album/dto/album-add-users.dto'; -import { CreateAlbumDto } from 'src/domain/album/dto/album-create.dto'; -import { UpdateAlbumDto } from 'src/domain/album/dto/album-update.dto'; -import { AlbumInfoDto } from 'src/domain/album/dto/album.dto'; -import { GetAlbumsDto } from 'src/domain/album/dto/get-albums.dto'; -import { BulkIdResponseDto, BulkIdsDto } from 'src/domain/asset/response-dto/asset-ids-response.dto'; -import { AuthDto } from 'src/domain/auth/auth.dto'; +import { + AddUsersDto, + AlbumCountResponseDto, + AlbumInfoDto, + AlbumResponseDto, + CreateAlbumDto, + GetAlbumsDto, + UpdateAlbumDto, +} 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 { AlbumService } from 'src/services/album.service'; import { ParseMeUUIDPipe, UUIDParamDto } from 'src/validation'; @ApiTags('Album') diff --git a/server/src/controllers/api-key.controller.ts b/server/src/controllers/api-key.controller.ts index 2d4f8db15..564b90387 100644 --- a/server/src/controllers/api-key.controller.ts +++ b/server/src/controllers/api-key.controller.ts @@ -1,14 +1,9 @@ import { Body, Controller, Delete, Get, Param, Post, Put } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; -import { - APIKeyCreateDto, - 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 { APIKeyCreateDto, APIKeyCreateResponseDto, APIKeyResponseDto, APIKeyUpdateDto } from 'src/dtos/api-key.dto'; +import { AuthDto } from 'src/dtos/auth.dto'; import { Auth, Authenticated } from 'src/middleware/auth.guard'; +import { APIKeyService } from 'src/services/api-key.service'; import { UUIDParamDto } from 'src/validation'; @ApiTags('API Key') diff --git a/server/src/controllers/app.controller.ts b/server/src/controllers/app.controller.ts index d4c7ea5b4..472d0da3f 100644 --- a/server/src/controllers/app.controller.ts +++ b/server/src/controllers/app.controller.ts @@ -1,7 +1,7 @@ import { Controller, Get, Header } from '@nestjs/common'; import { ApiExcludeEndpoint } from '@nestjs/swagger'; -import { SystemConfigService } from 'src/domain/system-config/system-config.service'; import { PublicRoute } from 'src/middleware/auth.guard'; +import { SystemConfigService } from 'src/services/system-config.service'; @Controller() export class AppController { diff --git a/server/src/immich/api-v1/asset/asset.controller.ts b/server/src/controllers/asset-v1.controller.ts similarity index 63% rename from server/src/immich/api-v1/asset/asset.controller.ts rename to server/src/controllers/asset-v1.controller.ts index d29f61fdd..2ba9aa7a0 100644 --- a/server/src/immich/api-v1/asset/asset.controller.ts +++ b/server/src/controllers/asset-v1.controller.ts @@ -15,23 +15,27 @@ import { } from '@nestjs/common'; import { ApiBody, ApiConsumes, ApiHeader, ApiTags } from '@nestjs/swagger'; import { NextFunction, Response } from 'express'; -import { AssetResponseDto } from 'src/domain/asset/response-dto/asset-response.dto'; -import { AuthDto } from 'src/domain/auth/auth.dto'; -import { AssetService as AssetServiceV1 } from 'src/immich/api-v1/asset/asset.service'; -import { AssetBulkUploadCheckDto } from 'src/immich/api-v1/asset/dto/asset-check.dto'; -import { AssetSearchDto } from 'src/immich/api-v1/asset/dto/asset-search.dto'; -import { CheckExistingAssetsDto } from 'src/immich/api-v1/asset/dto/check-existing-assets.dto'; -import { CreateAssetDto } from 'src/immich/api-v1/asset/dto/create-asset.dto'; -import { GetAssetThumbnailDto } from 'src/immich/api-v1/asset/dto/get-asset-thumbnail.dto'; -import { ServeFileDto } from 'src/immich/api-v1/asset/dto/serve-file.dto'; -import { AssetBulkUploadCheckResponseDto } from 'src/immich/api-v1/asset/response-dto/asset-check-response.dto'; -import { AssetFileUploadResponseDto } from 'src/immich/api-v1/asset/response-dto/asset-file-upload-response.dto'; -import { CheckExistingAssetsResponseDto } from 'src/immich/api-v1/asset/response-dto/check-existing-assets-response.dto'; -import { CuratedLocationsResponseDto } from 'src/immich/api-v1/asset/response-dto/curated-locations-response.dto'; -import { CuratedObjectsResponseDto } from 'src/immich/api-v1/asset/response-dto/curated-objects-response.dto'; -import { sendFile } from 'src/immich/app.utils'; +import { AssetResponseDto } from 'src/dtos/asset-response.dto'; +import { + AssetBulkUploadCheckResponseDto, + AssetFileUploadResponseDto, + CheckExistingAssetsResponseDto, + CuratedLocationsResponseDto, + CuratedObjectsResponseDto, +} from 'src/dtos/asset-v1-response.dto'; +import { + AssetBulkUploadCheckDto, + AssetSearchDto, + CheckExistingAssetsDto, + CreateAssetDto, + GetAssetThumbnailDto, + 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 { 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'; interface UploadFiles { @@ -43,8 +47,8 @@ interface UploadFiles { @ApiTags('Asset') @Controller(Route.ASSET) @Authenticated() -export class AssetController { - constructor(private serviceV1: AssetServiceV1) {} +export class AssetControllerV1 { + constructor(private service: AssetServiceV1) {} @SharedLinkRoute() @Post('upload') @@ -73,7 +77,7 @@ export class AssetController { 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) { res.status(HttpStatus.OK); } @@ -91,7 +95,7 @@ export class AssetController { @Param() { id }: UUIDParamDto, @Query() dto: ServeFileDto, ) { - await sendFile(res, next, () => this.serviceV1.serveFile(auth, id, dto)); + await sendFile(res, next, () => this.service.serveFile(auth, id, dto)); } @SharedLinkRoute() @@ -104,22 +108,22 @@ export class AssetController { @Param() { id }: UUIDParamDto, @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') getCuratedObjects(@Auth() auth: AuthDto): Promise { - return this.serviceV1.getCuratedObject(auth); + return this.service.getCuratedObject(auth); } @Get('/curated-locations') getCuratedLocations(@Auth() auth: AuthDto): Promise { - return this.serviceV1.getCuratedLocation(auth); + return this.service.getCuratedLocation(auth); } @Get('/search-terms') getAssetSearchTerms(@Auth() auth: AuthDto): Promise { - return this.serviceV1.getAssetSearchTerm(auth); + return this.service.getAssetSearchTerm(auth); } /** @@ -133,7 +137,7 @@ export class AssetController { schema: { type: 'string' }, }) getAllAssets(@Auth() auth: AuthDto, @Query() dto: AssetSearchDto): Promise { - return this.serviceV1.getAllAssets(auth, dto); + return this.service.getAllAssets(auth, dto); } /** @@ -145,7 +149,7 @@ export class AssetController { @Auth() auth: AuthDto, @Body() dto: CheckExistingAssetsDto, ): Promise { - return this.serviceV1.checkExistingAssets(auth, dto); + return this.service.checkExistingAssets(auth, dto); } /** @@ -157,6 +161,6 @@ export class AssetController { @Auth() auth: AuthDto, @Body() dto: AssetBulkUploadCheckDto, ): Promise { - return this.serviceV1.bulkUploadCheck(auth, dto); + return this.service.bulkUploadCheck(auth, dto); } } diff --git a/server/src/controllers/asset.controller.ts b/server/src/controllers/asset.controller.ts index a6db59815..37e169113 100644 --- a/server/src/controllers/asset.controller.ts +++ b/server/src/controllers/asset.controller.ts @@ -1,27 +1,24 @@ import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put, Query } from '@nestjs/common'; import { ApiOperation, ApiTags } from '@nestjs/swagger'; -import { AssetService } from 'src/domain/asset/asset.service'; -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 { AssetResponseDto, MemoryLaneResponseDto } from 'src/dtos/asset-response.dto'; import { AssetBulkDeleteDto, AssetBulkUpdateDto, + AssetJobsDto, + AssetStatsDto, + AssetStatsResponseDto, DeviceIdDto, RandomAssetsDto, UpdateAssetDto, -} from 'src/domain/asset/dto/asset.dto'; -import { MapMarkerDto } from 'src/domain/asset/dto/map-marker.dto'; -import { MemoryLaneDto } from 'src/domain/asset/dto/memory-lane.dto'; -import { TimeBucketAssetDto, TimeBucketDto } from 'src/domain/asset/dto/time-bucket.dto'; -import { AssetResponseDto, MemoryLaneResponseDto } from 'src/domain/asset/response-dto/asset-response.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'; +} from 'src/dtos/asset.dto'; +import { AuthDto } from 'src/dtos/auth.dto'; +import { MapMarkerDto, MapMarkerResponseDto, MemoryLaneDto, MetadataSearchDto } from 'src/dtos/search.dto'; +import { UpdateStackParentDto } from 'src/dtos/stack.dto'; +import { TimeBucketAssetDto, TimeBucketDto, TimeBucketResponseDto } from 'src/dtos/time-bucket.dto'; import { Auth, Authenticated, SharedLinkRoute } from 'src/middleware/auth.guard'; 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'; @ApiTags('Asset') diff --git a/server/src/controllers/audit.controller.ts b/server/src/controllers/audit.controller.ts index bb92f990e..1487e78d4 100644 --- a/server/src/controllers/audit.controller.ts +++ b/server/src/controllers/audit.controller.ts @@ -7,10 +7,10 @@ import { FileChecksumResponseDto, FileReportDto, FileReportFixDto, -} from 'src/domain/audit/audit.dto'; -import { AuditService } from 'src/domain/audit/audit.service'; -import { AuthDto } from 'src/domain/auth/auth.dto'; +} from 'src/dtos/audit.dto'; +import { AuthDto } from 'src/dtos/auth.dto'; import { AdminRoute, Auth, Authenticated } from 'src/middleware/auth.guard'; +import { AuditService } from 'src/services/audit.service'; @ApiTags('Audit') @Controller('audit') diff --git a/server/src/controllers/auth.controller.ts b/server/src/controllers/auth.controller.ts index 941002fe6..9b4e7a3bc 100644 --- a/server/src/controllers/auth.controller.ts +++ b/server/src/controllers/auth.controller.ts @@ -1,7 +1,7 @@ import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Req, Res } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; 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 { AuthDeviceResponseDto, AuthDto, @@ -11,10 +11,10 @@ import { LogoutResponseDto, SignUpDto, ValidateAccessTokenResponseDto, -} from 'src/domain/auth/auth.dto'; -import { AuthService, LoginDetails } from 'src/domain/auth/auth.service'; -import { UserResponseDto, mapUser } from 'src/domain/user/response-dto/user-response.dto'; +} from 'src/dtos/auth.dto'; +import { UserResponseDto, mapUser } from 'src/dtos/user.dto'; import { Auth, Authenticated, GetLoginDetails, PublicRoute } from 'src/middleware/auth.guard'; +import { AuthService, LoginDetails } from 'src/services/auth.service'; import { UUIDParamDto } from 'src/validation'; @ApiTags('Authentication') diff --git a/server/src/controllers/download.controller.ts b/server/src/controllers/download.controller.ts index 0fb3520cf..4e4bf09d1 100644 --- a/server/src/controllers/download.controller.ts +++ b/server/src/controllers/download.controller.ts @@ -1,12 +1,12 @@ import { Body, Controller, HttpCode, HttpStatus, Next, Param, Post, Res, StreamableFile } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { NextFunction, Response } from 'express'; -import { AssetIdsDto } from 'src/domain/asset/dto/asset-ids.dto'; -import { AuthDto } from 'src/domain/auth/auth.dto'; -import { DownloadInfoDto, DownloadResponseDto } from 'src/domain/download/download.dto'; -import { DownloadService } from 'src/domain/download/download.service'; -import { asStreamableFile, sendFile } from 'src/immich/app.utils'; +import { AssetIdsDto } from 'src/dtos/asset.dto'; +import { AuthDto } from 'src/dtos/auth.dto'; +import { DownloadInfoDto, DownloadResponseDto } from 'src/dtos/download.dto'; 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'; @ApiTags('Download') diff --git a/server/src/controllers/face.controller.ts b/server/src/controllers/face.controller.ts index c7caba016..a3f33fb86 100644 --- a/server/src/controllers/face.controller.ts +++ b/server/src/controllers/face.controller.ts @@ -1,9 +1,9 @@ import { Body, Controller, Get, Param, Put, Query } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; -import { AuthDto } from 'src/domain/auth/auth.dto'; -import { AssetFaceResponseDto, FaceDto, PersonResponseDto } from 'src/domain/person/person.dto'; -import { PersonService } from 'src/domain/person/person.service'; +import { AuthDto } from 'src/dtos/auth.dto'; +import { AssetFaceResponseDto, FaceDto, PersonResponseDto } from 'src/dtos/person.dto'; import { Auth, Authenticated } from 'src/middleware/auth.guard'; +import { PersonService } from 'src/services/person.service'; import { UUIDParamDto } from 'src/validation'; @ApiTags('Face') diff --git a/server/src/controllers/job.controller.ts b/server/src/controllers/job.controller.ts index f0e9ed2be..d6bd45b1e 100644 --- a/server/src/controllers/job.controller.ts +++ b/server/src/controllers/job.controller.ts @@ -1,8 +1,8 @@ import { Body, Controller, Get, Param, Put } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; -import { AllJobStatusResponseDto, JobCommandDto, JobIdParamDto, JobStatusDto } from 'src/domain/job/job.dto'; -import { JobService } from 'src/domain/job/job.service'; +import { AllJobStatusResponseDto, JobCommandDto, JobIdParamDto, JobStatusDto } from 'src/dtos/job.dto'; import { Authenticated } from 'src/middleware/auth.guard'; +import { JobService } from 'src/services/job.service'; @ApiTags('Job') @Controller('jobs') diff --git a/server/src/controllers/library.controller.ts b/server/src/controllers/library.controller.ts index 677773f2d..70d357187 100644 --- a/server/src/controllers/library.controller.ts +++ b/server/src/controllers/library.controller.ts @@ -9,9 +9,9 @@ import { UpdateLibraryDto, ValidateLibraryDto, ValidateLibraryResponseDto, -} from 'src/domain/library/library.dto'; -import { LibraryService } from 'src/domain/library/library.service'; +} from 'src/dtos/library.dto'; import { AdminRoute, Authenticated } from 'src/middleware/auth.guard'; +import { LibraryService } from 'src/services/library.service'; import { UUIDParamDto } from 'src/validation'; @ApiTags('Library') diff --git a/server/src/controllers/oauth.controller.ts b/server/src/controllers/oauth.controller.ts index 48b3eafd5..debbd4e67 100644 --- a/server/src/controllers/oauth.controller.ts +++ b/server/src/controllers/oauth.controller.ts @@ -7,10 +7,10 @@ import { OAuthAuthorizeResponseDto, OAuthCallbackDto, OAuthConfigDto, -} from 'src/domain/auth/auth.dto'; -import { AuthService, LoginDetails } from 'src/domain/auth/auth.service'; -import { UserResponseDto } from 'src/domain/user/response-dto/user-response.dto'; +} from 'src/dtos/auth.dto'; +import { UserResponseDto } from 'src/dtos/user.dto'; import { Auth, Authenticated, GetLoginDetails, PublicRoute } from 'src/middleware/auth.guard'; +import { AuthService, LoginDetails } from 'src/services/auth.service'; @ApiTags('OAuth') @Controller('oauth') diff --git a/server/src/controllers/partner.controller.ts b/server/src/controllers/partner.controller.ts index 8ca55e73c..f654a7263 100644 --- a/server/src/controllers/partner.controller.ts +++ b/server/src/controllers/partner.controller.ts @@ -1,10 +1,10 @@ import { Body, Controller, Delete, Get, Param, Post, Put, Query } from '@nestjs/common'; import { ApiQuery, ApiTags } from '@nestjs/swagger'; -import { AuthDto } from 'src/domain/auth/auth.dto'; -import { PartnerResponseDto, UpdatePartnerDto } from 'src/domain/partner/partner.dto'; -import { PartnerService } from 'src/domain/partner/partner.service'; -import { PartnerDirection } from 'src/interfaces/partner.repository'; +import { AuthDto } from 'src/dtos/auth.dto'; +import { PartnerResponseDto, UpdatePartnerDto } from 'src/dtos/partner.dto'; +import { PartnerDirection } from 'src/interfaces/partner.interface'; import { Auth, Authenticated } from 'src/middleware/auth.guard'; +import { PartnerService } from 'src/services/partner.service'; import { UUIDParamDto } from 'src/validation'; @ApiTags('Partner') diff --git a/server/src/controllers/person.controller.ts b/server/src/controllers/person.controller.ts index bc955fd2c..c9128a1f7 100644 --- a/server/src/controllers/person.controller.ts +++ b/server/src/controllers/person.controller.ts @@ -1,9 +1,9 @@ import { Body, Controller, Get, Next, Param, Post, Put, Query, Res } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { NextFunction, Response } from 'express'; -import { BulkIdResponseDto } from 'src/domain/asset/response-dto/asset-ids-response.dto'; -import { AssetResponseDto } from 'src/domain/asset/response-dto/asset-response.dto'; -import { AuthDto } from 'src/domain/auth/auth.dto'; +import { BulkIdResponseDto } from 'src/dtos/asset-ids.response.dto'; +import { AssetResponseDto } from 'src/dtos/asset-response.dto'; +import { AuthDto } from 'src/dtos/auth.dto'; import { AssetFaceUpdateDto, MergePersonDto, @@ -14,10 +14,10 @@ import { PersonSearchDto, PersonStatisticsResponseDto, PersonUpdateDto, -} from 'src/domain/person/person.dto'; -import { PersonService } from 'src/domain/person/person.service'; -import { sendFile } from 'src/immich/app.utils'; +} from 'src/dtos/person.dto'; 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'; @ApiTags('Person') diff --git a/server/src/controllers/search.controller.ts b/server/src/controllers/search.controller.ts index 76fce1609..eaf45be29 100644 --- a/server/src/controllers/search.controller.ts +++ b/server/src/controllers/search.controller.ts @@ -1,21 +1,21 @@ import { Body, Controller, Get, HttpCode, HttpStatus, Post, Query } from '@nestjs/common'; import { ApiOperation, ApiTags } from '@nestjs/swagger'; -import { AssetResponseDto } from 'src/domain/asset/response-dto/asset-response.dto'; -import { AuthDto } from 'src/domain/auth/auth.dto'; -import { PersonResponseDto } from 'src/domain/person/person.dto'; -import { SearchSuggestionRequestDto } from 'src/domain/search/dto/search-suggestion.dto'; +import { AssetResponseDto } from 'src/dtos/asset-response.dto'; +import { AuthDto } from 'src/dtos/auth.dto'; +import { PersonResponseDto } from 'src/dtos/person.dto'; import { MetadataSearchDto, PlacesResponseDto, SearchDto, + SearchExploreResponseDto, SearchPeopleDto, SearchPlacesDto, + SearchResponseDto, + SearchSuggestionRequestDto, SmartSearchDto, -} from 'src/domain/search/dto/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'; +} from 'src/dtos/search.dto'; import { Auth, Authenticated } from 'src/middleware/auth.guard'; +import { SearchService } from 'src/services/search.service'; @ApiTags('Search') @Controller('search') diff --git a/server/src/controllers/server-info.controller.ts b/server/src/controllers/server-info.controller.ts index aae617493..e32b0d191 100644 --- a/server/src/controllers/server-info.controller.ts +++ b/server/src/controllers/server-info.controller.ts @@ -9,9 +9,9 @@ import { ServerStatsResponseDto, ServerThemeDto, ServerVersionResponseDto, -} from 'src/domain/server-info/server-info.dto'; -import { ServerInfoService } from 'src/domain/server-info/server-info.service'; +} from 'src/dtos/server-info.dto'; import { AdminRoute, Authenticated, PublicRoute } from 'src/middleware/auth.guard'; +import { ServerInfoService } from 'src/services/server-info.service'; @ApiTags('Server Info') @Controller('server-info') diff --git a/server/src/controllers/shared-link.controller.ts b/server/src/controllers/shared-link.controller.ts index 1e03a5c42..990f4e322 100644 --- a/server/src/controllers/shared-link.controller.ts +++ b/server/src/controllers/shared-link.controller.ts @@ -1,14 +1,18 @@ import { Body, Controller, Delete, Get, Param, Patch, Post, Put, Query, Req, Res } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { Request, Response } from 'express'; -import { AssetIdsDto } from 'src/domain/asset/dto/asset-ids.dto'; -import { AssetIdsResponseDto } from 'src/domain/asset/response-dto/asset-ids-response.dto'; -import { IMMICH_SHARED_LINK_ACCESS_COOKIE } from 'src/domain/auth/auth.constant'; -import { AuthDto } from 'src/domain/auth/auth.dto'; -import { SharedLinkResponseDto } from 'src/domain/shared-link/shared-link-response.dto'; -import { SharedLinkCreateDto, SharedLinkEditDto, SharedLinkPasswordDto } from 'src/domain/shared-link/shared-link.dto'; -import { SharedLinkService } from 'src/domain/shared-link/shared-link.service'; +import { IMMICH_SHARED_LINK_ACCESS_COOKIE } from 'src/constants'; +import { AssetIdsResponseDto } from 'src/dtos/asset-ids.response.dto'; +import { AssetIdsDto } from 'src/dtos/asset.dto'; +import { AuthDto } from 'src/dtos/auth.dto'; +import { + SharedLinkCreateDto, + SharedLinkEditDto, + SharedLinkPasswordDto, + SharedLinkResponseDto, +} from 'src/dtos/shared-link.dto'; import { Auth, Authenticated, SharedLinkRoute } from 'src/middleware/auth.guard'; +import { SharedLinkService } from 'src/services/shared-link.service'; import { UUIDParamDto } from 'src/validation'; @ApiTags('Shared Link') diff --git a/server/src/controllers/system-config.controller.ts b/server/src/controllers/system-config.controller.ts index d10bccee0..1fb9dfbea 100644 --- a/server/src/controllers/system-config.controller.ts +++ b/server/src/controllers/system-config.controller.ts @@ -1,10 +1,8 @@ import { Body, Controller, Get, Put, Query } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; -import { SystemConfigDto } from 'src/domain/system-config/dto/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 { MapThemeDto, SystemConfigDto, SystemConfigTemplateStorageOptionDto } from 'src/dtos/system-config.dto'; import { AdminRoute, Authenticated } from 'src/middleware/auth.guard'; +import { SystemConfigService } from 'src/services/system-config.service'; @ApiTags('System Config') @Controller('system-config') diff --git a/server/src/controllers/tag.controller.ts b/server/src/controllers/tag.controller.ts index e914e577e..1caed8d52 100644 --- a/server/src/controllers/tag.controller.ts +++ b/server/src/controllers/tag.controller.ts @@ -1,13 +1,12 @@ import { Body, Controller, Delete, Get, Param, Patch, Post, Put } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; -import { AssetIdsDto } from 'src/domain/asset/dto/asset-ids.dto'; -import { AssetIdsResponseDto } from 'src/domain/asset/response-dto/asset-ids-response.dto'; -import { AssetResponseDto } from 'src/domain/asset/response-dto/asset-response.dto'; -import { AuthDto } from 'src/domain/auth/auth.dto'; -import { TagResponseDto } from 'src/domain/tag/tag-response.dto'; -import { CreateTagDto, UpdateTagDto } from 'src/domain/tag/tag.dto'; -import { TagService } from 'src/domain/tag/tag.service'; +import { AssetIdsResponseDto } from 'src/dtos/asset-ids.response.dto'; +import { AssetResponseDto } from 'src/dtos/asset-response.dto'; +import { AssetIdsDto } from 'src/dtos/asset.dto'; +import { AuthDto } from 'src/dtos/auth.dto'; +import { CreateTagDto, TagResponseDto, UpdateTagDto } from 'src/dtos/tag.dto'; import { Auth, Authenticated } from 'src/middleware/auth.guard'; +import { TagService } from 'src/services/tag.service'; import { UUIDParamDto } from 'src/validation'; @ApiTags('Tag') diff --git a/server/src/controllers/trash.controller.ts b/server/src/controllers/trash.controller.ts index 2c0b0c946..25df3543c 100644 --- a/server/src/controllers/trash.controller.ts +++ b/server/src/controllers/trash.controller.ts @@ -1,9 +1,9 @@ import { Body, Controller, HttpCode, HttpStatus, Post } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; -import { BulkIdsDto } from 'src/domain/asset/response-dto/asset-ids-response.dto'; -import { AuthDto } from 'src/domain/auth/auth.dto'; -import { TrashService } from 'src/domain/trash/trash.service'; +import { BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; +import { AuthDto } from 'src/dtos/auth.dto'; import { Auth, Authenticated } from 'src/middleware/auth.guard'; +import { TrashService } from 'src/services/trash.service'; @ApiTags('Trash') @Controller('trash') diff --git a/server/src/controllers/user.controller.ts b/server/src/controllers/user.controller.ts index e573584ad..c108e8852 100644 --- a/server/src/controllers/user.controller.ts +++ b/server/src/controllers/user.controller.ts @@ -16,17 +16,13 @@ import { } from '@nestjs/common'; import { ApiBody, ApiConsumes, ApiTags } from '@nestjs/swagger'; import { NextFunction, Response } from 'express'; -import { AuthDto } from 'src/domain/auth/auth.dto'; -import { CreateProfileImageDto } from 'src/domain/user/dto/create-profile-image.dto'; -import { CreateUserDto } from 'src/domain/user/dto/create-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 { AuthDto } from 'src/dtos/auth.dto'; +import { CreateProfileImageDto, CreateProfileImageResponseDto } from 'src/dtos/user-profile.dto'; +import { CreateUserDto, DeleteUserDto, UpdateUserDto, UserResponseDto } from 'src/dtos/user.dto'; import { AdminRoute, Auth, Authenticated, FileResponse } from 'src/middleware/auth.guard'; 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'; @ApiTags('User') diff --git a/server/src/cores/access.core.ts b/server/src/cores/access.core.ts index f9e22bb6b..f836e9762 100644 --- a/server/src/cores/access.core.ts +++ b/server/src/cores/access.core.ts @@ -1,8 +1,8 @@ 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 { IAccessRepository } from 'src/interfaces/access.repository'; -import { setDifference, setIsEqual, setUnion } from 'src/utils'; +import { IAccessRepository } from 'src/interfaces/access.interface'; +import { setDifference, setIsEqual, setUnion } from 'src/utils/set'; export enum Permission { ACTIVITY_CREATE = 'activity.create', diff --git a/server/src/cores/storage.core.ts b/server/src/cores/storage.core.ts index 8bf65b248..b9dad8642 100644 --- a/server/src/cores/storage.core.ts +++ b/server/src/cores/storage.core.ts @@ -1,16 +1,16 @@ import { dirname, join, resolve } from 'node:path'; +import { APP_MEDIA_LOCATION } from 'src/constants'; import { SystemConfigCore } from 'src/cores/system-config.core'; -import { APP_MEDIA_LOCATION } from 'src/domain/domain.constant'; import { AssetEntity } from 'src/entities/asset.entity'; import { AssetPathType, PathType, PersonPathType } from 'src/entities/move.entity'; import { PersonEntity } from 'src/entities/person.entity'; -import { ImmichLogger } from 'src/infra/logger'; -import { IAssetRepository } from 'src/interfaces/asset.repository'; -import { ICryptoRepository } from 'src/interfaces/crypto.repository'; -import { IMoveRepository } from 'src/interfaces/move.repository'; -import { IPersonRepository } from 'src/interfaces/person.repository'; -import { IStorageRepository } from 'src/interfaces/storage.repository'; -import { ISystemConfigRepository } from 'src/interfaces/system-config.repository'; +import { IAssetRepository } from 'src/interfaces/asset.interface'; +import { ICryptoRepository } from 'src/interfaces/crypto.interface'; +import { IMoveRepository } from 'src/interfaces/move.interface'; +import { IPersonRepository } from 'src/interfaces/person.interface'; +import { IStorageRepository } from 'src/interfaces/storage.interface'; +import { ISystemConfigRepository } from 'src/interfaces/system-config.interface'; +import { ImmichLogger } from 'src/utils/logger'; export enum StorageFolder { ENCODED_VIDEO = 'encoded-video', diff --git a/server/src/cores/system-config.core.ts b/server/src/cores/system-config.core.ts index 295a0e5ed..01bfacc9b 100644 --- a/server/src/cores/system-config.core.ts +++ b/server/src/cores/system-config.core.ts @@ -5,8 +5,7 @@ import { validate } from 'class-validator'; import { load as loadYaml } from 'js-yaml'; import * as _ from 'lodash'; import { Subject } from 'rxjs'; -import { QueueName } from 'src/domain/job/job.constants'; -import { SystemConfigDto } from 'src/domain/system-config/dto/system-config.dto'; +import { SystemConfigDto } from 'src/dtos/system-config.dto'; import { AudioCodec, CQMode, @@ -21,8 +20,9 @@ import { TranscodePolicy, VideoCodec, } from 'src/entities/system-config.entity'; -import { ImmichLogger } from 'src/infra/logger'; -import { ISystemConfigRepository } from 'src/interfaces/system-config.repository'; +import { QueueName } from 'src/interfaces/job.interface'; +import { ISystemConfigRepository } from 'src/interfaces/system-config.interface'; +import { ImmichLogger } from 'src/utils/logger'; export type SystemConfigValidator = (config: SystemConfig, newConfig: SystemConfig) => void | Promise; diff --git a/server/src/cores/user.core.ts b/server/src/cores/user.core.ts index d91f2bae5..4d7da25de 100644 --- a/server/src/cores/user.core.ts +++ b/server/src/cores/user.core.ts @@ -1,11 +1,11 @@ import { BadRequestException, ForbiddenException } from '@nestjs/common'; import sanitize from 'sanitize-filename'; -import { UserResponseDto } from 'src/domain/user/response-dto/user-response.dto'; +import { UserResponseDto } from 'src/dtos/user.dto'; import { LibraryType } from 'src/entities/library.entity'; import { UserEntity } from 'src/entities/user.entity'; -import { ICryptoRepository } from 'src/interfaces/crypto.repository'; -import { ILibraryRepository } from 'src/interfaces/library.repository'; -import { IUserRepository } from 'src/interfaces/user.repository'; +import { ICryptoRepository } from 'src/interfaces/crypto.interface'; +import { ILibraryRepository } from 'src/interfaces/library.interface'; +import { IUserRepository } from 'src/interfaces/user.interface'; const SALT_ROUNDS = 10; diff --git a/server/src/infra/database.config.ts b/server/src/database.config.ts similarity index 80% rename from server/src/infra/database.config.ts rename to server/src/database.config.ts index 572288c59..867b7f4cb 100644 --- a/server/src/infra/database.config.ts +++ b/server/src/database.config.ts @@ -1,4 +1,4 @@ -import { DatabaseExtension } from 'src/interfaces/database.repository'; +import { DatabaseExtension } from 'src/interfaces/database.interface'; import { DataSource } from 'typeorm'; import { PostgresConnectionOptions } from 'typeorm/driver/postgres/PostgresConnectionOptions.js'; @@ -16,9 +16,9 @@ const urlOrParts = url /* eslint unicorn/prefer-module: "off" -- We can fix this when migrating to ESM*/ export const databaseConfig: PostgresConnectionOptions = { type: 'postgres', - entities: [__dirname + '/../entities/*.entity.{js,ts}'], - migrations: [__dirname + '/../migrations/*.{js,ts}'], - subscribers: [__dirname + '/../subscribers/*.{js,ts}'], + entities: [__dirname + '/entities/*.entity.{js,ts}'], + migrations: [__dirname + '/migrations/*.{js,ts}'], + subscribers: [__dirname + '/subscribers/*.{js,ts}'], migrationsRun: false, synchronize: false, connectTimeoutMS: 10_000, // 10 seconds diff --git a/server/src/decorators.ts b/server/src/decorators.ts index 06dc0bfdc..b913fe00f 100644 --- a/server/src/decorators.ts +++ b/server/src/decorators.ts @@ -1,6 +1,8 @@ import { SetMetadata } from '@nestjs/common'; +import { OnEvent, OnEventType } from '@nestjs/event-emitter'; +import { OnEventOptions } from '@nestjs/event-emitter/dist/interfaces'; import _ from 'lodash'; -import { setUnion } from 'src/utils'; +import { setUnion } from 'src/utils/set'; // PostgreSQL uses a 16-bit integer to indicate the number of bound parameters. This means that the // maximum number of parameters is 65535. Any query that tries to bind more than that (e.g. searching @@ -122,3 +124,6 @@ export interface GenerateSqlQueries { /** Decorator to enable versioning/tracking of generated Sql */ export const GenerateSql = (...options: GenerateSqlQueries[]) => SetMetadata(GENERATE_SQL_KEY, options); + +export const OnEventInternal = (event: OnEventType, options?: OnEventOptions) => + OnEvent(event, { suppressErrors: false, ...options }); diff --git a/server/src/domain/album/dto/album-add-users.dto.ts b/server/src/domain/album/dto/album-add-users.dto.ts deleted file mode 100644 index 1a6be4823..000000000 --- a/server/src/domain/album/dto/album-add-users.dto.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { ArrayNotEmpty } from 'class-validator'; -import { ValidateUUID } from 'src/validation'; - -export class AddUsersDto { - @ValidateUUID({ each: true }) - @ArrayNotEmpty() - sharedUserIds!: string[]; -} diff --git a/server/src/domain/album/dto/album-create.dto.ts b/server/src/domain/album/dto/album-create.dto.ts deleted file mode 100644 index 1b4a75332..000000000 --- a/server/src/domain/album/dto/album-create.dto.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsString } from 'class-validator'; -import { Optional, ValidateUUID } from 'src/validation'; - -export class CreateAlbumDto { - @IsString() - @ApiProperty() - albumName!: string; - - @IsString() - @Optional() - description?: string; - - @ValidateUUID({ optional: true, each: true }) - sharedWithUserIds?: string[]; - - @ValidateUUID({ optional: true, each: true }) - assetIds?: string[]; -} diff --git a/server/src/domain/album/dto/album-update.dto.ts b/server/src/domain/album/dto/album-update.dto.ts deleted file mode 100644 index cf2d5f8c2..000000000 --- a/server/src/domain/album/dto/album-update.dto.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsEnum, IsString } from 'class-validator'; -import { AssetOrder } from 'src/entities/album.entity'; -import { Optional, ValidateBoolean, ValidateUUID } from 'src/validation'; - -export class UpdateAlbumDto { - @Optional() - @IsString() - albumName?: string; - - @Optional() - @IsString() - description?: string; - - @ValidateUUID({ optional: true }) - albumThumbnailAssetId?: string; - - @ValidateBoolean({ optional: true }) - isActivityEnabled?: boolean; - - @IsEnum(AssetOrder) - @Optional() - @ApiProperty({ enum: AssetOrder, enumName: 'AssetOrder' }) - order?: AssetOrder; -} diff --git a/server/src/domain/album/dto/album.dto.ts b/server/src/domain/album/dto/album.dto.ts deleted file mode 100644 index fe0eb0d5c..000000000 --- a/server/src/domain/album/dto/album.dto.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { ValidateBoolean } from 'src/validation'; - -export class AlbumInfoDto { - @ValidateBoolean({ optional: true }) - withoutAssets?: boolean; -} diff --git a/server/src/domain/album/dto/get-albums.dto.ts b/server/src/domain/album/dto/get-albums.dto.ts deleted file mode 100644 index 15e4f1cf2..000000000 --- a/server/src/domain/album/dto/get-albums.dto.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { ValidateBoolean, ValidateUUID } from 'src/validation'; - -export class GetAlbumsDto { - @ValidateBoolean({ optional: true }) - /** - * true: only shared albums - * false: only non-shared own albums - * undefined: shared and owned albums - */ - shared?: boolean; - - /** - * Only returns albums that contain the asset - * Ignores the shared parameter - * undefined: get all albums - */ - @ValidateUUID({ optional: true }) - assetId?: string; -} diff --git a/server/src/domain/asset/dto/asset-ids.dto.ts b/server/src/domain/asset/dto/asset-ids.dto.ts deleted file mode 100644 index ea875e85e..000000000 --- a/server/src/domain/asset/dto/asset-ids.dto.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsEnum } from 'class-validator'; -import { ValidateUUID } from 'src/validation'; - -export class AssetIdsDto { - @ValidateUUID({ each: true }) - assetIds!: string[]; -} - -export enum AssetJobName { - REGENERATE_THUMBNAIL = 'regenerate-thumbnail', - REFRESH_METADATA = 'refresh-metadata', - TRANSCODE_VIDEO = 'transcode-video', -} - -export class AssetJobsDto extends AssetIdsDto { - @ApiProperty({ enumName: 'AssetJobName', enum: AssetJobName }) - @IsEnum(AssetJobName) - name!: AssetJobName; -} diff --git a/server/src/domain/asset/dto/asset-statistics.dto.ts b/server/src/domain/asset/dto/asset-statistics.dto.ts deleted file mode 100644 index 93e007029..000000000 --- a/server/src/domain/asset/dto/asset-statistics.dto.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { AssetType } from 'src/entities/asset.entity'; -import { AssetStats } from 'src/interfaces/asset.repository'; -import { ValidateBoolean } from 'src/validation'; - -export class AssetStatsDto { - @ValidateBoolean({ optional: true }) - isArchived?: boolean; - - @ValidateBoolean({ optional: true }) - isFavorite?: boolean; - - @ValidateBoolean({ optional: true }) - isTrashed?: boolean; -} - -export class AssetStatsResponseDto { - @ApiProperty({ type: 'integer' }) - images!: number; - - @ApiProperty({ type: 'integer' }) - videos!: number; - - @ApiProperty({ type: 'integer' }) - total!: number; -} - -export const mapStats = (stats: AssetStats): AssetStatsResponseDto => { - return { - images: stats[AssetType.IMAGE], - videos: stats[AssetType.VIDEO], - total: Object.values(stats).reduce((total, value) => total + value, 0), - }; -}; diff --git a/server/src/domain/asset/dto/asset.dto.ts b/server/src/domain/asset/dto/asset.dto.ts deleted file mode 100644 index a93a59ae3..000000000 --- a/server/src/domain/asset/dto/asset.dto.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { Type } from 'class-transformer'; -import { - IsDateString, - IsInt, - IsLatitude, - IsLongitude, - IsNotEmpty, - IsPositive, - IsString, - ValidateIf, -} from 'class-validator'; -import { BulkIdsDto } from 'src/domain/asset/response-dto/asset-ids-response.dto'; -import { Optional, ValidateBoolean, ValidateUUID } from 'src/validation'; - -export class DeviceIdDto { - @IsNotEmpty() - @IsString() - deviceId!: string; -} - -const hasGPS = (o: { latitude: undefined; longitude: undefined }) => - o.latitude !== undefined || o.longitude !== undefined; -const ValidateGPS = () => ValidateIf(hasGPS); - -export class UpdateAssetBase { - @ValidateBoolean({ optional: true }) - isFavorite?: boolean; - - @ValidateBoolean({ optional: true }) - isArchived?: boolean; - - @Optional() - @IsDateString() - dateTimeOriginal?: string; - - @ValidateGPS() - @IsLatitude() - @IsNotEmpty() - latitude?: number; - - @ValidateGPS() - @IsLongitude() - @IsNotEmpty() - longitude?: number; -} - -export class AssetBulkUpdateDto extends UpdateAssetBase { - @ValidateUUID({ each: true }) - ids!: string[]; - - @ValidateUUID({ optional: true }) - stackParentId?: string; - - @ValidateBoolean({ optional: true }) - removeParent?: boolean; -} - -export class UpdateAssetDto extends UpdateAssetBase { - @Optional() - @IsString() - description?: string; -} - -export class RandomAssetsDto { - @Optional() - @IsInt() - @IsPositive() - @Type(() => Number) - count?: number; -} - -export class AssetBulkDeleteDto extends BulkIdsDto { - @ValidateBoolean({ optional: true }) - force?: boolean; -} diff --git a/server/src/domain/asset/dto/map-marker.dto.ts b/server/src/domain/asset/dto/map-marker.dto.ts deleted file mode 100644 index 158750e51..000000000 --- a/server/src/domain/asset/dto/map-marker.dto.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { ValidateBoolean, ValidateDate } from 'src/validation'; - -export class MapMarkerDto { - @ValidateBoolean({ optional: true }) - isArchived?: boolean; - - @ValidateBoolean({ optional: true }) - isFavorite?: boolean; - - @ValidateDate({ optional: true }) - fileCreatedAfter?: Date; - - @ValidateDate({ optional: true }) - fileCreatedBefore?: Date; - - @ValidateBoolean({ optional: true }) - withPartners?: boolean; -} diff --git a/server/src/domain/asset/dto/memory-lane.dto.ts b/server/src/domain/asset/dto/memory-lane.dto.ts deleted file mode 100644 index 43f74aff1..000000000 --- a/server/src/domain/asset/dto/memory-lane.dto.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; -import { IsInt, Max, Min } from 'class-validator'; - -export class MemoryLaneDto { - @IsInt() - @Type(() => Number) - @Max(31) - @Min(1) - @ApiProperty({ type: 'integer' }) - day!: number; - - @IsInt() - @Type(() => Number) - @Max(12) - @Min(1) - @ApiProperty({ type: 'integer' }) - month!: number; -} diff --git a/server/src/domain/asset/response-dto/map-marker-response.dto.ts b/server/src/domain/asset/response-dto/map-marker-response.dto.ts deleted file mode 100644 index f5148883f..000000000 --- a/server/src/domain/asset/response-dto/map-marker-response.dto.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; - -export class MapMarkerResponseDto { - @ApiProperty() - id!: string; - - @ApiProperty({ format: 'double' }) - lat!: number; - - @ApiProperty({ format: 'double' }) - lon!: number; - - @ApiProperty() - city!: string | null; - - @ApiProperty() - state!: string | null; - - @ApiProperty() - country!: string | null; -} diff --git a/server/src/domain/asset/response-dto/smart-info-response.dto.ts b/server/src/domain/asset/response-dto/smart-info-response.dto.ts deleted file mode 100644 index c55c7ffb3..000000000 --- a/server/src/domain/asset/response-dto/smart-info-response.dto.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { SmartInfoEntity } from 'src/entities/smart-info.entity'; - -export class SmartInfoResponseDto { - tags?: string[] | null; - objects?: string[] | null; -} - -export function mapSmartInfo(entity: SmartInfoEntity): SmartInfoResponseDto { - return { - tags: entity.tags, - objects: entity.objects, - }; -} diff --git a/server/src/domain/asset/response-dto/time-bucket-response.dto.ts b/server/src/domain/asset/response-dto/time-bucket-response.dto.ts deleted file mode 100644 index e143dde46..000000000 --- a/server/src/domain/asset/response-dto/time-bucket-response.dto.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; - -export class TimeBucketResponseDto { - @ApiProperty({ type: 'string' }) - timeBucket!: string; - - @ApiProperty({ type: 'integer' }) - count!: number; -} diff --git a/server/src/domain/auth/auth.constant.ts b/server/src/domain/auth/auth.constant.ts deleted file mode 100644 index f29fc9274..000000000 --- a/server/src/domain/auth/auth.constant.ts +++ /dev/null @@ -1,12 +0,0 @@ -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', -} diff --git a/server/src/domain/domain.module.ts b/server/src/domain/domain.module.ts deleted file mode 100644 index 04d7c51f4..000000000 --- a/server/src/domain/domain.module.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { Global, Module, Provider } from '@nestjs/common'; -import { ActivityService } from 'src/domain/activity/activity.service'; -import { AlbumService } from 'src/domain/album/album.service'; -import { APIKeyService } from 'src/domain/api-key/api-key.service'; -import { AssetService } from 'src/domain/asset/asset.service'; -import { AuditService } from 'src/domain/audit/audit.service'; -import { AuthService } from 'src/domain/auth/auth.service'; -import { DatabaseService } from 'src/domain/database/database.service'; -import { DownloadService } from 'src/domain/download/download.service'; -import { JobService } from 'src/domain/job/job.service'; -import { LibraryService } from 'src/domain/library/library.service'; -import { MediaService } from 'src/domain/media/media.service'; -import { MetadataService } from 'src/domain/metadata/metadata.service'; -import { PartnerService } from 'src/domain/partner/partner.service'; -import { PersonService } from 'src/domain/person/person.service'; -import { SearchService } from 'src/domain/search/search.service'; -import { ServerInfoService } from 'src/domain/server-info/server-info.service'; -import { SharedLinkService } from 'src/domain/shared-link/shared-link.service'; -import { SmartInfoService } from 'src/domain/smart-info/smart-info.service'; -import { StorageTemplateService } from 'src/domain/storage-template/storage-template.service'; -import { StorageService } from 'src/domain/storage/storage.service'; -import { SystemConfigService } from 'src/domain/system-config/system-config.service'; -import { TagService } from 'src/domain/tag/tag.service'; -import { TrashService } from 'src/domain/trash/trash.service'; -import { UserService } from 'src/domain/user/user.service'; -import { ImmichLogger } from 'src/infra/logger'; - -const providers: Provider[] = [ - APIKeyService, - ActivityService, - AlbumService, - AssetService, - AuditService, - AuthService, - DatabaseService, - DownloadService, - ImmichLogger, - JobService, - LibraryService, - MediaService, - MetadataService, - PartnerService, - PersonService, - SearchService, - ServerInfoService, - SharedLinkService, - SmartInfoService, - StorageService, - StorageTemplateService, - SystemConfigService, - TagService, - TrashService, - UserService, -]; - -@Global() -@Module({ - imports: [], - providers: [...providers], - exports: [...providers], -}) -export class DomainModule {} diff --git a/server/src/domain/job/job.constants.ts b/server/src/domain/job/job.constants.ts deleted file mode 100644 index b2dac1551..000000000 --- a/server/src/domain/job/job.constants.ts +++ /dev/null @@ -1,157 +0,0 @@ -export enum QueueName { - THUMBNAIL_GENERATION = 'thumbnailGeneration', - METADATA_EXTRACTION = 'metadataExtraction', - VIDEO_CONVERSION = 'videoConversion', - FACE_DETECTION = 'faceDetection', - FACIAL_RECOGNITION = 'facialRecognition', - SMART_SEARCH = 'smartSearch', - BACKGROUND_TASK = 'backgroundTask', - STORAGE_TEMPLATE_MIGRATION = 'storageTemplateMigration', - MIGRATION = 'migration', - SEARCH = 'search', - SIDECAR = 'sidecar', - LIBRARY = 'library', -} - -export type ConcurrentQueueName = Exclude< - QueueName, - QueueName.STORAGE_TEMPLATE_MIGRATION | QueueName.FACIAL_RECOGNITION ->; - -export enum JobCommand { - START = 'start', - PAUSE = 'pause', - RESUME = 'resume', - EMPTY = 'empty', - CLEAR_FAILED = 'clear-failed', -} - -export enum JobName { - // conversion - QUEUE_VIDEO_CONVERSION = 'queue-video-conversion', - VIDEO_CONVERSION = 'video-conversion', - - // thumbnails - QUEUE_GENERATE_THUMBNAILS = 'queue-generate-thumbnails', - GENERATE_JPEG_THUMBNAIL = 'generate-jpeg-thumbnail', - GENERATE_WEBP_THUMBNAIL = 'generate-webp-thumbnail', - GENERATE_THUMBHASH_THUMBNAIL = 'generate-thumbhash-thumbnail', - GENERATE_PERSON_THUMBNAIL = 'generate-person-thumbnail', - - // metadata - QUEUE_METADATA_EXTRACTION = 'queue-metadata-extraction', - METADATA_EXTRACTION = 'metadata-extraction', - LINK_LIVE_PHOTOS = 'link-live-photos', - - // user - USER_DELETION = 'user-deletion', - USER_DELETE_CHECK = 'user-delete-check', - USER_SYNC_USAGE = 'user-sync-usage', - - // asset - ASSET_DELETION = 'asset-deletion', - ASSET_DELETION_CHECK = 'asset-deletion-check', - - // storage template - STORAGE_TEMPLATE_MIGRATION = 'storage-template-migration', - STORAGE_TEMPLATE_MIGRATION_SINGLE = 'storage-template-migration-single', - - // migration - QUEUE_MIGRATION = 'queue-migration', - MIGRATE_ASSET = 'migrate-asset', - MIGRATE_PERSON = 'migrate-person', - - // facial recognition - PERSON_CLEANUP = 'person-cleanup', - QUEUE_FACE_DETECTION = 'queue-face-detection', - FACE_DETECTION = 'face-detection', - QUEUE_FACIAL_RECOGNITION = 'queue-facial-recognition', - FACIAL_RECOGNITION = 'facial-recognition', - - // library managment - LIBRARY_SCAN = 'library-refresh', - LIBRARY_SCAN_ASSET = 'library-refresh-asset', - LIBRARY_DELETE = 'library-delete', - LIBRARY_QUEUE_SCAN_ALL = 'library-queue-all-refresh', - LIBRARY_QUEUE_CLEANUP = 'library-queue-cleanup', - LIBRARY_CHECK_OFFLINE = 'library-check-if-online', - LIBRARY_REMOVE_OFFLINE = 'library-remove-offline', - - // cleanup - DELETE_FILES = 'delete-files', - CLEAN_OLD_AUDIT_LOGS = 'clean-old-audit-logs', - - // smart search - QUEUE_SMART_SEARCH = 'queue-smart-search', - SMART_SEARCH = 'smart-search', - - // XMP sidecars - QUEUE_SIDECAR = 'queue-sidecar', - SIDECAR_DISCOVERY = 'sidecar-discovery', - SIDECAR_SYNC = 'sidecar-sync', - SIDECAR_WRITE = 'sidecar-write', -} - -export const JOBS_ASSET_PAGINATION_SIZE = 1000; - -export const JOBS_TO_QUEUE: Record = { - // misc - [JobName.ASSET_DELETION]: QueueName.BACKGROUND_TASK, - [JobName.ASSET_DELETION_CHECK]: QueueName.BACKGROUND_TASK, - [JobName.USER_DELETE_CHECK]: QueueName.BACKGROUND_TASK, - [JobName.USER_DELETION]: QueueName.BACKGROUND_TASK, - [JobName.DELETE_FILES]: QueueName.BACKGROUND_TASK, - [JobName.CLEAN_OLD_AUDIT_LOGS]: QueueName.BACKGROUND_TASK, - [JobName.PERSON_CLEANUP]: QueueName.BACKGROUND_TASK, - [JobName.USER_SYNC_USAGE]: QueueName.BACKGROUND_TASK, - - // conversion - [JobName.QUEUE_VIDEO_CONVERSION]: QueueName.VIDEO_CONVERSION, - [JobName.VIDEO_CONVERSION]: QueueName.VIDEO_CONVERSION, - - // thumbnails - [JobName.QUEUE_GENERATE_THUMBNAILS]: QueueName.THUMBNAIL_GENERATION, - [JobName.GENERATE_JPEG_THUMBNAIL]: QueueName.THUMBNAIL_GENERATION, - [JobName.GENERATE_WEBP_THUMBNAIL]: QueueName.THUMBNAIL_GENERATION, - [JobName.GENERATE_THUMBHASH_THUMBNAIL]: QueueName.THUMBNAIL_GENERATION, - [JobName.GENERATE_PERSON_THUMBNAIL]: QueueName.THUMBNAIL_GENERATION, - - // metadata - [JobName.QUEUE_METADATA_EXTRACTION]: QueueName.METADATA_EXTRACTION, - [JobName.METADATA_EXTRACTION]: QueueName.METADATA_EXTRACTION, - [JobName.LINK_LIVE_PHOTOS]: QueueName.METADATA_EXTRACTION, - - // storage template - [JobName.STORAGE_TEMPLATE_MIGRATION]: QueueName.STORAGE_TEMPLATE_MIGRATION, - [JobName.STORAGE_TEMPLATE_MIGRATION_SINGLE]: QueueName.STORAGE_TEMPLATE_MIGRATION, - - // migration - [JobName.QUEUE_MIGRATION]: QueueName.MIGRATION, - [JobName.MIGRATE_ASSET]: QueueName.MIGRATION, - [JobName.MIGRATE_PERSON]: QueueName.MIGRATION, - - // facial recognition - [JobName.QUEUE_FACE_DETECTION]: QueueName.FACE_DETECTION, - [JobName.FACE_DETECTION]: QueueName.FACE_DETECTION, - [JobName.QUEUE_FACIAL_RECOGNITION]: QueueName.FACIAL_RECOGNITION, - [JobName.FACIAL_RECOGNITION]: QueueName.FACIAL_RECOGNITION, - - // smart search - [JobName.QUEUE_SMART_SEARCH]: QueueName.SMART_SEARCH, - [JobName.SMART_SEARCH]: QueueName.SMART_SEARCH, - - // XMP sidecars - [JobName.QUEUE_SIDECAR]: QueueName.SIDECAR, - [JobName.SIDECAR_DISCOVERY]: QueueName.SIDECAR, - [JobName.SIDECAR_SYNC]: QueueName.SIDECAR, - [JobName.SIDECAR_WRITE]: QueueName.SIDECAR, - - // Library management - [JobName.LIBRARY_SCAN_ASSET]: QueueName.LIBRARY, - [JobName.LIBRARY_SCAN]: QueueName.LIBRARY, - [JobName.LIBRARY_DELETE]: QueueName.LIBRARY, - [JobName.LIBRARY_REMOVE_OFFLINE]: QueueName.LIBRARY, - [JobName.LIBRARY_CHECK_OFFLINE]: QueueName.LIBRARY, - [JobName.LIBRARY_QUEUE_SCAN_ALL]: QueueName.LIBRARY, - [JobName.LIBRARY_QUEUE_CLEANUP]: QueueName.LIBRARY, -}; diff --git a/server/src/domain/job/job.interface.ts b/server/src/domain/job/job.interface.ts deleted file mode 100644 index 1ea7c8c8f..000000000 --- a/server/src/domain/job/job.interface.ts +++ /dev/null @@ -1,45 +0,0 @@ -export interface IBaseJob { - force?: boolean; -} - -export interface IEntityJob extends IBaseJob { - id: string; - source?: 'upload' | 'sidecar-write'; -} - -export interface IAssetDeletionJob extends IEntityJob { - fromExternal?: boolean; -} - -export interface ILibraryFileJob extends IEntityJob { - ownerId: string; - assetPath: string; -} - -export interface ILibraryRefreshJob extends IEntityJob { - refreshModifiedFiles: boolean; - refreshAllFiles: boolean; -} - -export interface ILibraryOfflineJob extends IEntityJob { - importPaths: string[]; -} - -export interface IBulkEntityJob extends IBaseJob { - ids: string[]; -} - -export interface IDeleteFilesJob extends IBaseJob { - files: Array; -} - -export interface ISidecarWriteJob extends IEntityJob { - description?: string; - dateTimeOriginal?: string; - latitude?: number; - longitude?: number; -} - -export interface IDeferrableJob extends IEntityJob { - deferred?: boolean; -} diff --git a/server/src/domain/media/media.constant.ts b/server/src/domain/media/media.constant.ts deleted file mode 100644 index 3a8ee414b..000000000 --- a/server/src/domain/media/media.constant.ts +++ /dev/null @@ -1 +0,0 @@ -export const FACE_THUMBNAIL_SIZE = 250; diff --git a/server/src/domain/search/dto/search-suggestion.dto.ts b/server/src/domain/search/dto/search-suggestion.dto.ts deleted file mode 100644 index f702293d0..000000000 --- a/server/src/domain/search/dto/search-suggestion.dto.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsEnum, IsNotEmpty, IsString } from 'class-validator'; -import { Optional } from 'src/validation'; - -export enum SearchSuggestionType { - COUNTRY = 'country', - STATE = 'state', - CITY = 'city', - CAMERA_MAKE = 'camera-make', - CAMERA_MODEL = 'camera-model', -} - -export class SearchSuggestionRequestDto { - @IsEnum(SearchSuggestionType) - @IsNotEmpty() - @ApiProperty({ enumName: 'SearchSuggestionType', enum: SearchSuggestionType }) - type!: SearchSuggestionType; - - @IsString() - @Optional() - country?: string; - - @IsString() - @Optional() - state?: string; - - @IsString() - @Optional() - make?: string; - - @IsString() - @Optional() - model?: string; -} diff --git a/server/src/domain/search/response-dto/search-explore.response.dto.ts b/server/src/domain/search/response-dto/search-explore.response.dto.ts deleted file mode 100644 index 33689b979..000000000 --- a/server/src/domain/search/response-dto/search-explore.response.dto.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { AssetResponseDto } from 'src/domain/asset/response-dto/asset-response.dto'; - -class SearchExploreItem { - value!: string; - data!: AssetResponseDto; -} - -export class SearchExploreResponseDto { - fieldName!: string; - items!: SearchExploreItem[]; -} diff --git a/server/src/domain/search/response-dto/search-response.dto.ts b/server/src/domain/search/response-dto/search-response.dto.ts deleted file mode 100644 index 53563a9ac..000000000 --- a/server/src/domain/search/response-dto/search-response.dto.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { AlbumResponseDto } from 'src/domain/album/album-response.dto'; -import { AssetResponseDto } from 'src/domain/asset/response-dto/asset-response.dto'; - -class SearchFacetCountResponseDto { - @ApiProperty({ type: 'integer' }) - count!: number; - value!: string; -} - -class SearchFacetResponseDto { - fieldName!: string; - counts!: SearchFacetCountResponseDto[]; -} - -class SearchAlbumResponseDto { - @ApiProperty({ type: 'integer' }) - total!: number; - @ApiProperty({ type: 'integer' }) - count!: number; - items!: AlbumResponseDto[]; - facets!: SearchFacetResponseDto[]; -} - -class SearchAssetResponseDto { - @ApiProperty({ type: 'integer' }) - total!: number; - @ApiProperty({ type: 'integer' }) - count!: number; - items!: AssetResponseDto[]; - facets!: SearchFacetResponseDto[]; - nextPage!: string | null; -} - -export class SearchResponseDto { - albums!: SearchAlbumResponseDto; - assets!: SearchAssetResponseDto; -} diff --git a/server/src/domain/shared-link/shared-link.dto.ts b/server/src/domain/shared-link/shared-link.dto.ts deleted file mode 100644 index 73928aac2..000000000 --- a/server/src/domain/shared-link/shared-link.dto.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsEnum, IsString } from 'class-validator'; -import { SharedLinkType } from 'src/entities/shared-link.entity'; -import { Optional, ValidateBoolean, ValidateDate, ValidateUUID } from 'src/validation'; - -export class SharedLinkCreateDto { - @IsEnum(SharedLinkType) - @ApiProperty({ enum: SharedLinkType, enumName: 'SharedLinkType' }) - type!: SharedLinkType; - - @ValidateUUID({ each: true, optional: true }) - assetIds?: string[]; - - @ValidateUUID({ optional: true }) - albumId?: string; - - @IsString() - @Optional() - description?: string; - - @IsString() - @Optional() - password?: string; - - @ValidateDate({ optional: true, nullable: true }) - expiresAt?: Date | null = null; - - @ValidateBoolean({ optional: true }) - allowUpload?: boolean; - - @ValidateBoolean({ optional: true }) - allowDownload?: boolean = true; - - @ValidateBoolean({ optional: true }) - showMetadata?: boolean = true; -} - -export class SharedLinkEditDto { - @Optional() - description?: string; - - @Optional() - password?: string; - - @Optional({ nullable: true }) - expiresAt?: Date | null; - - @Optional() - allowUpload?: boolean; - - @ValidateBoolean({ optional: true }) - allowDownload?: boolean; - - @ValidateBoolean({ optional: true }) - showMetadata?: boolean; - - /** - * Few clients cannot send null to set the expiryTime to never. - * Setting this flag and not sending expiryAt is considered as null instead. - * Clients that can send null values can ignore this. - */ - @ValidateBoolean({ optional: true }) - changeExpiryTime?: boolean; -} - -export class SharedLinkPasswordDto { - @IsString() - @Optional() - @ApiProperty({ example: 'password' }) - password?: string; - - @IsString() - @Optional() - token?: string; -} diff --git a/server/src/domain/smart-info/smart-info.constant.ts b/server/src/domain/smart-info/smart-info.constant.ts deleted file mode 100644 index 66c31b985..000000000 --- a/server/src/domain/smart-info/smart-info.constant.ts +++ /dev/null @@ -1,129 +0,0 @@ -export type ModelInfo = { - dimSize: number; -}; - -export const CLIP_MODEL_INFO: Record = { - 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, - }, -}; - -export function cleanModelName(modelName: string): string { - const token = modelName.split('/').at(-1); - if (!token) { - throw new Error(`Invalid model name: ${modelName}`); - } - - return token.replaceAll(':', '_'); -} - -export function getCLIPModelInfo(modelName: string): ModelInfo { - const modelInfo = CLIP_MODEL_INFO[cleanModelName(modelName)]; - if (!modelInfo) { - throw new Error(`Unknown CLIP model: ${modelName}`); - } - - return modelInfo; -} diff --git a/server/src/domain/system-config/dto/system-config-ffmpeg.dto.ts b/server/src/domain/system-config/dto/system-config-ffmpeg.dto.ts deleted file mode 100644 index 017f69c2d..000000000 --- a/server/src/domain/system-config/dto/system-config-ffmpeg.dto.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; -import { IsEnum, IsInt, IsString, Max, Min } from 'class-validator'; -import { - AudioCodec, - CQMode, - ToneMapping, - TranscodeHWAccel, - TranscodePolicy, - VideoCodec, -} from 'src/entities/system-config.entity'; -import { ValidateBoolean } from 'src/validation'; - -export class SystemConfigFFmpegDto { - @IsInt() - @Min(0) - @Max(51) - @Type(() => Number) - @ApiProperty({ type: 'integer' }) - crf!: number; - - @IsInt() - @Min(0) - @Type(() => Number) - @ApiProperty({ type: 'integer' }) - threads!: number; - - @IsString() - preset!: string; - - @IsEnum(VideoCodec) - @ApiProperty({ enumName: 'VideoCodec', enum: VideoCodec }) - targetVideoCodec!: VideoCodec; - - @IsEnum(VideoCodec, { each: true }) - @ApiProperty({ enumName: 'VideoCodec', enum: VideoCodec, isArray: true }) - acceptedVideoCodecs!: VideoCodec[]; - - @IsEnum(AudioCodec) - @ApiProperty({ enumName: 'AudioCodec', enum: AudioCodec }) - targetAudioCodec!: AudioCodec; - - @IsEnum(AudioCodec, { each: true }) - @ApiProperty({ enumName: 'AudioCodec', enum: AudioCodec, isArray: true }) - acceptedAudioCodecs!: AudioCodec[]; - - @IsString() - targetResolution!: string; - - @IsString() - maxBitrate!: string; - - @IsInt() - @Min(-1) - @Max(16) - @Type(() => Number) - @ApiProperty({ type: 'integer' }) - bframes!: number; - - @IsInt() - @Min(0) - @Max(6) - @Type(() => Number) - @ApiProperty({ type: 'integer' }) - refs!: number; - - @IsInt() - @Min(0) - @Type(() => Number) - @ApiProperty({ type: 'integer' }) - gopSize!: number; - - @IsInt() - @Min(0) - @Type(() => Number) - @ApiProperty({ type: 'integer' }) - npl!: number; - - @ValidateBoolean() - temporalAQ!: boolean; - - @IsEnum(CQMode) - @ApiProperty({ enumName: 'CQMode', enum: CQMode }) - cqMode!: CQMode; - - @ValidateBoolean() - twoPass!: boolean; - - @IsString() - preferredHwDevice!: string; - - @IsEnum(TranscodePolicy) - @ApiProperty({ enumName: 'TranscodePolicy', enum: TranscodePolicy }) - transcode!: TranscodePolicy; - - @IsEnum(TranscodeHWAccel) - @ApiProperty({ enumName: 'TranscodeHWAccel', enum: TranscodeHWAccel }) - accel!: TranscodeHWAccel; - - @IsEnum(ToneMapping) - @ApiProperty({ enumName: 'ToneMapping', enum: ToneMapping }) - tonemap!: ToneMapping; -} diff --git a/server/src/domain/system-config/dto/system-config-job.dto.ts b/server/src/domain/system-config/dto/system-config-job.dto.ts deleted file mode 100644 index 2769da327..000000000 --- a/server/src/domain/system-config/dto/system-config-job.dto.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; -import { IsInt, IsObject, IsPositive, ValidateNested } from 'class-validator'; -import { ConcurrentQueueName, QueueName } from 'src/domain/job/job.constants'; - -export class JobSettingsDto { - @IsInt() - @IsPositive() - @ApiProperty({ type: 'integer' }) - concurrency!: number; -} - -export class SystemConfigJobDto implements Record { - @ApiProperty({ type: JobSettingsDto }) - @ValidateNested() - @IsObject() - @Type(() => JobSettingsDto) - [QueueName.THUMBNAIL_GENERATION]!: JobSettingsDto; - - @ApiProperty({ type: JobSettingsDto }) - @ValidateNested() - @IsObject() - @Type(() => JobSettingsDto) - [QueueName.METADATA_EXTRACTION]!: JobSettingsDto; - - @ApiProperty({ type: JobSettingsDto }) - @ValidateNested() - @IsObject() - @Type(() => JobSettingsDto) - [QueueName.VIDEO_CONVERSION]!: JobSettingsDto; - - @ApiProperty({ type: JobSettingsDto }) - @ValidateNested() - @IsObject() - @Type(() => JobSettingsDto) - [QueueName.SMART_SEARCH]!: JobSettingsDto; - - @ApiProperty({ type: JobSettingsDto }) - @ValidateNested() - @IsObject() - @Type(() => JobSettingsDto) - [QueueName.MIGRATION]!: JobSettingsDto; - - @ApiProperty({ type: JobSettingsDto }) - @ValidateNested() - @IsObject() - @Type(() => JobSettingsDto) - [QueueName.BACKGROUND_TASK]!: JobSettingsDto; - - @ApiProperty({ type: JobSettingsDto }) - @ValidateNested() - @IsObject() - @Type(() => JobSettingsDto) - [QueueName.SEARCH]!: JobSettingsDto; - - @ApiProperty({ type: JobSettingsDto }) - @ValidateNested() - @IsObject() - @Type(() => JobSettingsDto) - [QueueName.FACE_DETECTION]!: JobSettingsDto; - - @ApiProperty({ type: JobSettingsDto }) - @ValidateNested() - @IsObject() - @Type(() => JobSettingsDto) - [QueueName.SIDECAR]!: JobSettingsDto; - - @ApiProperty({ type: JobSettingsDto }) - @ValidateNested() - @IsObject() - @Type(() => JobSettingsDto) - [QueueName.LIBRARY]!: JobSettingsDto; -} diff --git a/server/src/domain/system-config/dto/system-config-library.dto.ts b/server/src/domain/system-config/dto/system-config-library.dto.ts deleted file mode 100644 index 8c7501ae4..000000000 --- a/server/src/domain/system-config/dto/system-config-library.dto.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { Type } from 'class-transformer'; -import { - IsNotEmpty, - IsObject, - IsString, - Validate, - ValidateIf, - ValidateNested, - ValidatorConstraint, - ValidatorConstraintInterface, -} from 'class-validator'; -import { ValidateBoolean, validateCronExpression } from 'src/validation'; - -const isEnabled = (config: SystemConfigLibraryScanDto) => config.enabled; - -@ValidatorConstraint({ name: 'cronValidator' }) -class CronValidator implements ValidatorConstraintInterface { - validate(expression: string): boolean { - return validateCronExpression(expression); - } -} - -export class SystemConfigLibraryScanDto { - @ValidateBoolean() - enabled!: boolean; - - @ValidateIf(isEnabled) - @IsNotEmpty() - @Validate(CronValidator, { message: 'Invalid cron expression' }) - @IsString() - cronExpression!: string; -} - -export class SystemConfigLibraryWatchDto { - @ValidateBoolean() - enabled!: boolean; -} - -export class SystemConfigLibraryDto { - @Type(() => SystemConfigLibraryScanDto) - @ValidateNested() - @IsObject() - scan!: SystemConfigLibraryScanDto; - - @Type(() => SystemConfigLibraryWatchDto) - @ValidateNested() - @IsObject() - watch!: SystemConfigLibraryWatchDto; -} diff --git a/server/src/domain/system-config/dto/system-config-logging.dto.ts b/server/src/domain/system-config/dto/system-config-logging.dto.ts deleted file mode 100644 index 53ef5d2f0..000000000 --- a/server/src/domain/system-config/dto/system-config-logging.dto.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsEnum } from 'class-validator'; -import { LogLevel } from 'src/entities/system-config.entity'; -import { ValidateBoolean } from 'src/validation'; - -export class SystemConfigLoggingDto { - @ValidateBoolean() - enabled!: boolean; - - @ApiProperty({ enum: LogLevel, enumName: 'LogLevel' }) - @IsEnum(LogLevel) - level!: LogLevel; -} diff --git a/server/src/domain/system-config/dto/system-config-machine-learning.dto.ts b/server/src/domain/system-config/dto/system-config-machine-learning.dto.ts deleted file mode 100644 index 058585992..000000000 --- a/server/src/domain/system-config/dto/system-config-machine-learning.dto.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Type } from 'class-transformer'; -import { IsObject, IsUrl, ValidateIf, ValidateNested } from 'class-validator'; -import { CLIPConfig, RecognitionConfig } from 'src/domain/smart-info/dto/model-config.dto'; -import { ValidateBoolean } from 'src/validation'; - -export class SystemConfigMachineLearningDto { - @ValidateBoolean() - enabled!: boolean; - - @IsUrl({ require_tld: false, allow_underscores: true }) - @ValidateIf((dto) => dto.enabled) - url!: string; - - @Type(() => CLIPConfig) - @ValidateNested() - @IsObject() - clip!: CLIPConfig; - - @Type(() => RecognitionConfig) - @ValidateNested() - @IsObject() - facialRecognition!: RecognitionConfig; -} diff --git a/server/src/domain/system-config/dto/system-config-map.dto.ts b/server/src/domain/system-config/dto/system-config-map.dto.ts deleted file mode 100644 index 9ec0abfa4..000000000 --- a/server/src/domain/system-config/dto/system-config-map.dto.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { IsString } from 'class-validator'; -import { ValidateBoolean } from 'src/validation'; - -export class SystemConfigMapDto { - @ValidateBoolean() - enabled!: boolean; - - @IsString() - lightStyle!: string; - - @IsString() - darkStyle!: string; -} diff --git a/server/src/domain/system-config/dto/system-config-new-version-check.dto.ts b/server/src/domain/system-config/dto/system-config-new-version-check.dto.ts deleted file mode 100644 index 7d5c5134f..000000000 --- a/server/src/domain/system-config/dto/system-config-new-version-check.dto.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { ValidateBoolean } from 'src/validation'; - -export class SystemConfigNewVersionCheckDto { - @ValidateBoolean() - enabled!: boolean; -} diff --git a/server/src/domain/system-config/dto/system-config-oauth.dto.ts b/server/src/domain/system-config/dto/system-config-oauth.dto.ts deleted file mode 100644 index 9c7fc5f40..000000000 --- a/server/src/domain/system-config/dto/system-config-oauth.dto.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { IsNotEmpty, IsNumber, IsString, IsUrl, Min, ValidateIf } from 'class-validator'; -import { ValidateBoolean } from 'src/validation'; - -const isEnabled = (config: SystemConfigOAuthDto) => config.enabled; -const isOverrideEnabled = (config: SystemConfigOAuthDto) => config.mobileOverrideEnabled; - -export class SystemConfigOAuthDto { - @ValidateBoolean() - autoLaunch!: boolean; - - @ValidateBoolean() - autoRegister!: boolean; - - @IsString() - buttonText!: string; - - @ValidateIf(isEnabled) - @IsNotEmpty() - @IsString() - clientId!: string; - - @ValidateIf(isEnabled) - @IsNotEmpty() - @IsString() - clientSecret!: string; - - @IsNumber() - @Min(0) - defaultStorageQuota!: number; - - @ValidateBoolean() - enabled!: boolean; - - @ValidateIf(isEnabled) - @IsNotEmpty() - @IsString() - issuerUrl!: string; - - @ValidateBoolean() - mobileOverrideEnabled!: boolean; - - @ValidateIf(isOverrideEnabled) - @IsUrl() - mobileRedirectUri!: string; - - @IsString() - scope!: string; - - @IsString() - @IsNotEmpty() - signingAlgorithm!: string; - - @IsString() - storageLabelClaim!: string; - - @IsString() - storageQuotaClaim!: string; -} diff --git a/server/src/domain/system-config/dto/system-config-password-login.dto.ts b/server/src/domain/system-config/dto/system-config-password-login.dto.ts deleted file mode 100644 index 8d49a7002..000000000 --- a/server/src/domain/system-config/dto/system-config-password-login.dto.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { ValidateBoolean } from 'src/validation'; - -export class SystemConfigPasswordLoginDto { - @ValidateBoolean() - enabled!: boolean; -} diff --git a/server/src/domain/system-config/dto/system-config-reverse-geocoding.dto.ts b/server/src/domain/system-config/dto/system-config-reverse-geocoding.dto.ts deleted file mode 100644 index 8ff286601..000000000 --- a/server/src/domain/system-config/dto/system-config-reverse-geocoding.dto.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { ValidateBoolean } from 'src/validation'; - -export class SystemConfigReverseGeocodingDto { - @ValidateBoolean() - enabled!: boolean; -} diff --git a/server/src/domain/system-config/dto/system-config-server.dto.ts b/server/src/domain/system-config/dto/system-config-server.dto.ts deleted file mode 100644 index 83a2b0df9..000000000 --- a/server/src/domain/system-config/dto/system-config-server.dto.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { IsString } from 'class-validator'; - -export class SystemConfigServerDto { - @IsString() - externalDomain!: string; - - @IsString() - loginPageMessage!: string; -} diff --git a/server/src/domain/system-config/dto/system-config-storage-template.dto.ts b/server/src/domain/system-config/dto/system-config-storage-template.dto.ts deleted file mode 100644 index 77204b46e..000000000 --- a/server/src/domain/system-config/dto/system-config-storage-template.dto.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { IsNotEmpty, IsString } from 'class-validator'; -import { ValidateBoolean } from 'src/validation'; - -export class SystemConfigStorageTemplateDto { - @ValidateBoolean() - enabled!: boolean; - - @ValidateBoolean() - hashVerificationEnabled!: boolean; - - @IsNotEmpty() - @IsString() - template!: string; -} diff --git a/server/src/domain/system-config/dto/system-config-theme.dto.ts b/server/src/domain/system-config/dto/system-config-theme.dto.ts deleted file mode 100644 index f47b51e0e..000000000 --- a/server/src/domain/system-config/dto/system-config-theme.dto.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { IsString } from 'class-validator'; - -export class SystemConfigThemeDto { - @IsString() - customCss!: string; -} diff --git a/server/src/domain/system-config/dto/system-config-thumbnail.dto.ts b/server/src/domain/system-config/dto/system-config-thumbnail.dto.ts deleted file mode 100644 index d3240efb1..000000000 --- a/server/src/domain/system-config/dto/system-config-thumbnail.dto.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; -import { IsEnum, IsInt, Max, Min } from 'class-validator'; -import { Colorspace } from 'src/entities/system-config.entity'; - -export class SystemConfigThumbnailDto { - @IsInt() - @Min(1) - @Type(() => Number) - @ApiProperty({ type: 'integer' }) - webpSize!: number; - - @IsInt() - @Min(1) - @Type(() => Number) - @ApiProperty({ type: 'integer' }) - jpegSize!: number; - - @IsInt() - @Min(1) - @Max(100) - @Type(() => Number) - @ApiProperty({ type: 'integer' }) - quality!: number; - - @IsEnum(Colorspace) - @ApiProperty({ enumName: 'Colorspace', enum: Colorspace }) - colorspace!: Colorspace; -} diff --git a/server/src/domain/system-config/dto/system-config-trash.dto.ts b/server/src/domain/system-config/dto/system-config-trash.dto.ts deleted file mode 100644 index a9e5483eb..000000000 --- a/server/src/domain/system-config/dto/system-config-trash.dto.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; -import { IsInt, Min } from 'class-validator'; -import { ValidateBoolean } from 'src/validation'; - -export class SystemConfigTrashDto { - @ValidateBoolean() - enabled!: boolean; - - @IsInt() - @Min(0) - @Type(() => Number) - @ApiProperty({ type: 'integer' }) - days!: number; -} diff --git a/server/src/domain/system-config/dto/system-config-user.dto.ts b/server/src/domain/system-config/dto/system-config-user.dto.ts deleted file mode 100644 index 22d6ef5fc..000000000 --- a/server/src/domain/system-config/dto/system-config-user.dto.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; -import { IsInt, Min } from 'class-validator'; - -export class SystemConfigUserDto { - @IsInt() - @Min(1) - @Type(() => Number) - @ApiProperty({ type: 'integer' }) - deleteDelay!: number; -} diff --git a/server/src/domain/system-config/dto/system-config.dto.ts b/server/src/domain/system-config/dto/system-config.dto.ts deleted file mode 100644 index 9aef92303..000000000 --- a/server/src/domain/system-config/dto/system-config.dto.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { Type } from 'class-transformer'; -import { IsObject, ValidateNested } from 'class-validator'; -import { SystemConfigFFmpegDto } from 'src/domain/system-config/dto/system-config-ffmpeg.dto'; -import { SystemConfigJobDto } from 'src/domain/system-config/dto/system-config-job.dto'; -import { SystemConfigLibraryDto } from 'src/domain/system-config/dto/system-config-library.dto'; -import { SystemConfigLoggingDto } from 'src/domain/system-config/dto/system-config-logging.dto'; -import { SystemConfigMachineLearningDto } from 'src/domain/system-config/dto/system-config-machine-learning.dto'; -import { SystemConfigMapDto } from 'src/domain/system-config/dto/system-config-map.dto'; -import { SystemConfigNewVersionCheckDto } from 'src/domain/system-config/dto/system-config-new-version-check.dto'; -import { SystemConfigOAuthDto } from 'src/domain/system-config/dto/system-config-oauth.dto'; -import { SystemConfigPasswordLoginDto } from 'src/domain/system-config/dto/system-config-password-login.dto'; -import { SystemConfigReverseGeocodingDto } from 'src/domain/system-config/dto/system-config-reverse-geocoding.dto'; -import { SystemConfigServerDto } from 'src/domain/system-config/dto/system-config-server.dto'; -import { SystemConfigStorageTemplateDto } from 'src/domain/system-config/dto/system-config-storage-template.dto'; -import { SystemConfigThemeDto } from 'src/domain/system-config/dto/system-config-theme.dto'; -import { SystemConfigThumbnailDto } from 'src/domain/system-config/dto/system-config-thumbnail.dto'; -import { SystemConfigTrashDto } from 'src/domain/system-config/dto/system-config-trash.dto'; -import { SystemConfigUserDto } from 'src/domain/system-config/dto/system-config-user.dto'; -import { SystemConfig } from 'src/entities/system-config.entity'; - -export class SystemConfigDto implements SystemConfig { - @Type(() => SystemConfigFFmpegDto) - @ValidateNested() - @IsObject() - ffmpeg!: SystemConfigFFmpegDto; - - @Type(() => SystemConfigLoggingDto) - @ValidateNested() - @IsObject() - logging!: SystemConfigLoggingDto; - - @Type(() => SystemConfigMachineLearningDto) - @ValidateNested() - @IsObject() - machineLearning!: SystemConfigMachineLearningDto; - - @Type(() => SystemConfigMapDto) - @ValidateNested() - @IsObject() - map!: SystemConfigMapDto; - - @Type(() => SystemConfigNewVersionCheckDto) - @ValidateNested() - @IsObject() - newVersionCheck!: SystemConfigNewVersionCheckDto; - - @Type(() => SystemConfigOAuthDto) - @ValidateNested() - @IsObject() - oauth!: SystemConfigOAuthDto; - - @Type(() => SystemConfigPasswordLoginDto) - @ValidateNested() - @IsObject() - passwordLogin!: SystemConfigPasswordLoginDto; - - @Type(() => SystemConfigReverseGeocodingDto) - @ValidateNested() - @IsObject() - reverseGeocoding!: SystemConfigReverseGeocodingDto; - - @Type(() => SystemConfigStorageTemplateDto) - @ValidateNested() - @IsObject() - storageTemplate!: SystemConfigStorageTemplateDto; - - @Type(() => SystemConfigJobDto) - @ValidateNested() - @IsObject() - job!: SystemConfigJobDto; - - @Type(() => SystemConfigThumbnailDto) - @ValidateNested() - @IsObject() - thumbnail!: SystemConfigThumbnailDto; - - @Type(() => SystemConfigTrashDto) - @ValidateNested() - @IsObject() - trash!: SystemConfigTrashDto; - - @Type(() => SystemConfigThemeDto) - @ValidateNested() - @IsObject() - theme!: SystemConfigThemeDto; - - @Type(() => SystemConfigLibraryDto) - @ValidateNested() - @IsObject() - library!: SystemConfigLibraryDto; - - @Type(() => SystemConfigServerDto) - @ValidateNested() - @IsObject() - server!: SystemConfigServerDto; - - @Type(() => SystemConfigUserDto) - @ValidateNested() - @IsObject() - user!: SystemConfigUserDto; -} - -export function mapConfig(config: SystemConfig): SystemConfigDto { - return config; -} diff --git a/server/src/domain/system-config/response-dto/system-config-template-storage-option.dto.ts b/server/src/domain/system-config/response-dto/system-config-template-storage-option.dto.ts deleted file mode 100644 index f0c8b9b64..000000000 --- a/server/src/domain/system-config/response-dto/system-config-template-storage-option.dto.ts +++ /dev/null @@ -1,10 +0,0 @@ -export class SystemConfigTemplateStorageOptionDto { - yearOptions!: string[]; - monthOptions!: string[]; - weekOptions!: string[]; - dayOptions!: string[]; - hourOptions!: string[]; - minuteOptions!: string[]; - secondOptions!: string[]; - presetOptions!: string[]; -} diff --git a/server/src/domain/system-config/system-config-map-theme.dto.ts b/server/src/domain/system-config/system-config-map-theme.dto.ts deleted file mode 100644 index 9286d8d23..000000000 --- a/server/src/domain/system-config/system-config-map-theme.dto.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsEnum } from 'class-validator'; - -export enum MapTheme { - LIGHT = 'light', - DARK = 'dark', -} - -export class MapThemeDto { - @IsEnum(MapTheme) - @ApiProperty({ enum: MapTheme, enumName: 'MapTheme' }) - theme!: MapTheme; -} diff --git a/server/src/domain/system-config/system-config.constants.ts b/server/src/domain/system-config/system-config.constants.ts deleted file mode 100644 index 0290472aa..000000000 --- a/server/src/domain/system-config/system-config.constants.ts +++ /dev/null @@ -1,27 +0,0 @@ -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}}', -]; diff --git a/server/src/domain/tag/tag.dto.ts b/server/src/domain/tag/tag.dto.ts deleted file mode 100644 index abf9549d8..000000000 --- a/server/src/domain/tag/tag.dto.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsEnum, IsNotEmpty, IsString } from 'class-validator'; -import { TagType } from 'src/entities/tag.entity'; -import { Optional } from 'src/validation'; - -export class CreateTagDto { - @IsString() - @IsNotEmpty() - name!: string; - - @IsEnum(TagType) - @IsNotEmpty() - @ApiProperty({ enumName: 'TagTypeEnum', enum: TagType }) - type!: TagType; -} - -export class UpdateTagDto { - @IsString() - @Optional() - name?: string; -} diff --git a/server/src/domain/user/dto/create-profile-image.dto.ts b/server/src/domain/user/dto/create-profile-image.dto.ts deleted file mode 100644 index 37a7d1340..000000000 --- a/server/src/domain/user/dto/create-profile-image.dto.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { UploadFieldName } from 'src/domain/asset/asset.service'; - -export class CreateProfileImageDto { - @ApiProperty({ type: 'string', format: 'binary' }) - [UploadFieldName.PROFILE_DATA]!: Express.Multer.File; -} diff --git a/server/src/domain/user/dto/create-user.dto.ts b/server/src/domain/user/dto/create-user.dto.ts deleted file mode 100644 index 7861c58c2..000000000 --- a/server/src/domain/user/dto/create-user.dto.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { Transform } from 'class-transformer'; -import { IsEmail, IsNotEmpty, IsNumber, IsPositive, IsString } from 'class-validator'; -import { Optional, ValidateBoolean, toEmail, toSanitized } from 'src/validation'; - -export class CreateUserDto { - @IsEmail({ require_tld: false }) - @Transform(toEmail) - email!: string; - - @IsNotEmpty() - @IsString() - password!: string; - - @IsNotEmpty() - @IsString() - name!: string; - - @Optional({ nullable: true }) - @IsString() - @Transform(toSanitized) - storageLabel?: string | null; - - @ValidateBoolean({ optional: true }) - memoriesEnabled?: boolean; - - @Optional({ nullable: true }) - @IsNumber() - @IsPositive() - @ApiProperty({ type: 'integer', format: 'int64' }) - quotaSizeInBytes?: number | null; - - @ValidateBoolean({ optional: true }) - shouldChangePassword?: boolean; -} - -export class CreateAdminDto { - @IsNotEmpty() - isAdmin!: true; - - @IsEmail({ require_tld: false }) - @Transform(({ value }) => value?.toLowerCase()) - email!: string; - - @IsNotEmpty() - password!: string; - - @IsNotEmpty() - name!: string; -} - -export class CreateUserOAuthDto { - @IsEmail({ require_tld: false }) - @Transform(({ value }) => value?.toLowerCase()) - email!: string; - - @IsNotEmpty() - oauthId!: string; - - name?: string; -} diff --git a/server/src/domain/user/dto/delete-user.dto.ts b/server/src/domain/user/dto/delete-user.dto.ts deleted file mode 100644 index aa41e18aa..000000000 --- a/server/src/domain/user/dto/delete-user.dto.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { ValidateBoolean } from 'src/validation'; - -export class DeleteUserDto { - @ValidateBoolean({ optional: true }) - force?: boolean; -} diff --git a/server/src/domain/user/dto/update-user.dto.spec.ts b/server/src/domain/user/dto/update-user.dto.spec.ts deleted file mode 100644 index 0ad407be3..000000000 --- a/server/src/domain/user/dto/update-user.dto.spec.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { plainToInstance } from 'class-transformer'; -import { validate } from 'class-validator'; -import { UpdateUserDto } from 'src/domain/user/dto/update-user.dto'; - -describe('update user DTO', () => { - it('should allow emails without a tld', async () => { - const someEmail = 'test@test'; - - const dto = plainToInstance(UpdateUserDto, { - email: someEmail, - id: '3fe388e4-2078-44d7-b36c-39d9dee3a657', - }); - const errors = await validate(dto); - expect(errors).toHaveLength(0); - expect(dto.email).toEqual(someEmail); - }); -}); diff --git a/server/src/domain/user/dto/update-user.dto.ts b/server/src/domain/user/dto/update-user.dto.ts deleted file mode 100644 index 059971e6c..000000000 --- a/server/src/domain/user/dto/update-user.dto.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { Transform } from 'class-transformer'; -import { IsEmail, IsEnum, IsNotEmpty, IsNumber, IsPositive, IsString, IsUUID } from 'class-validator'; -import { UserAvatarColor } from 'src/entities/user.entity'; -import { Optional, ValidateBoolean, toEmail, toSanitized } from 'src/validation'; - -export class UpdateUserDto { - @Optional() - @IsEmail({ require_tld: false }) - @Transform(toEmail) - email?: string; - - @Optional() - @IsNotEmpty() - @IsString() - password?: string; - - @Optional() - @IsString() - @IsNotEmpty() - name?: string; - - @Optional() - @IsString() - @Transform(toSanitized) - storageLabel?: string; - - @IsNotEmpty() - @IsUUID('4') - @ApiProperty({ format: 'uuid' }) - id!: string; - - @ValidateBoolean({ optional: true }) - isAdmin?: boolean; - - @ValidateBoolean({ optional: true }) - shouldChangePassword?: boolean; - - @ValidateBoolean({ optional: true }) - memoriesEnabled?: boolean; - - @Optional() - @IsEnum(UserAvatarColor) - @ApiProperty({ enumName: 'UserAvatarColor', enum: UserAvatarColor }) - avatarColor?: UserAvatarColor; - - @Optional({ nullable: true }) - @IsNumber() - @IsPositive() - @ApiProperty({ type: 'integer', format: 'int64' }) - quotaSizeInBytes?: number | null; -} diff --git a/server/src/domain/user/response-dto/create-profile-image-response.dto.ts b/server/src/domain/user/response-dto/create-profile-image-response.dto.ts deleted file mode 100644 index 2c7fd17be..000000000 --- a/server/src/domain/user/response-dto/create-profile-image-response.dto.ts +++ /dev/null @@ -1,11 +0,0 @@ -export class CreateProfileImageResponseDto { - userId!: string; - profileImagePath!: string; -} - -export function mapCreateProfileImageResponse(userId: string, profileImagePath: string): CreateProfileImageResponseDto { - return { - userId: userId, - profileImagePath: profileImagePath, - }; -} diff --git a/server/src/domain/user/response-dto/user-response.dto.ts b/server/src/domain/user/response-dto/user-response.dto.ts deleted file mode 100644 index 12ffca48c..000000000 --- a/server/src/domain/user/response-dto/user-response.dto.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsEnum } from 'class-validator'; -import { UserAvatarColor, UserEntity, UserStatus } from 'src/entities/user.entity'; - -export const getRandomAvatarColor = (user: UserEntity): UserAvatarColor => { - const values = Object.values(UserAvatarColor); - const randomIndex = Math.floor( - [...user.email].map((letter) => letter.codePointAt(0) ?? 0).reduce((a, b) => a + b, 0) % values.length, - ); - return values[randomIndex] as UserAvatarColor; -}; - -export class UserDto { - id!: string; - name!: string; - email!: string; - profileImagePath!: string; - @IsEnum(UserAvatarColor) - @ApiProperty({ enumName: 'UserAvatarColor', enum: UserAvatarColor }) - avatarColor!: UserAvatarColor; -} - -export class UserResponseDto extends UserDto { - storageLabel!: string | null; - shouldChangePassword!: boolean; - isAdmin!: boolean; - createdAt!: Date; - deletedAt!: Date | null; - updatedAt!: Date; - oauthId!: string; - memoriesEnabled?: boolean; - @ApiProperty({ type: 'integer', format: 'int64' }) - quotaSizeInBytes!: number | null; - @ApiProperty({ type: 'integer', format: 'int64' }) - quotaUsageInBytes!: number | null; - @ApiProperty({ enumName: 'UserStatus', enum: UserStatus }) - status!: string; -} - -export const mapSimpleUser = (entity: UserEntity): UserDto => { - return { - id: entity.id, - email: entity.email, - name: entity.name, - profileImagePath: entity.profileImagePath, - avatarColor: entity.avatarColor ?? getRandomAvatarColor(entity), - }; -}; - -export function mapUser(entity: UserEntity): UserResponseDto { - return { - ...mapSimpleUser(entity), - storageLabel: entity.storageLabel, - shouldChangePassword: entity.shouldChangePassword, - isAdmin: entity.isAdmin, - createdAt: entity.createdAt, - deletedAt: entity.deletedAt, - updatedAt: entity.updatedAt, - oauthId: entity.oauthId, - memoriesEnabled: entity.memoriesEnabled, - quotaSizeInBytes: entity.quotaSizeInBytes, - quotaUsageInBytes: entity.quotaUsageInBytes, - status: entity.status, - }; -} diff --git a/server/src/domain/activity/activity.dto.ts b/server/src/dtos/activity.dto.ts similarity index 95% rename from server/src/domain/activity/activity.dto.ts rename to server/src/dtos/activity.dto.ts index a9d865778..bd0d40095 100644 --- a/server/src/domain/activity/activity.dto.ts +++ b/server/src/dtos/activity.dto.ts @@ -1,6 +1,6 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsEnum, IsNotEmpty, IsString, ValidateIf } from 'class-validator'; -import { UserDto, mapSimpleUser } from 'src/domain/user/response-dto/user-response.dto'; +import { UserDto, mapSimpleUser } from 'src/dtos/user.dto'; import { ActivityEntity } from 'src/entities/activity.entity'; import { Optional, ValidateUUID } from 'src/validation'; diff --git a/server/src/domain/album/album-response.dto.spec.ts b/server/src/dtos/album-response.dto.spec.ts similarity index 89% rename from server/src/domain/album/album-response.dto.spec.ts rename to server/src/dtos/album-response.dto.spec.ts index 568b416b4..2a6d59abf 100644 --- a/server/src/domain/album/album-response.dto.spec.ts +++ b/server/src/dtos/album-response.dto.spec.ts @@ -1,4 +1,4 @@ -import { mapAlbum } from 'src/domain/album/album-response.dto'; +import { mapAlbum } from 'src/dtos/album.dto'; import { albumStub } from 'test/fixtures/album.stub'; describe('mapAlbum', () => { diff --git a/server/src/domain/album/album-response.dto.ts b/server/src/dtos/album.dto.ts similarity index 59% rename from server/src/domain/album/album-response.dto.ts rename to server/src/dtos/album.dto.ts index b016a1200..3f7af0f53 100644 --- a/server/src/domain/album/album-response.dto.ts +++ b/server/src/dtos/album.dto.ts @@ -1,9 +1,87 @@ import { ApiProperty } from '@nestjs/swagger'; -import { AssetResponseDto, mapAsset } from 'src/domain/asset/response-dto/asset-response.dto'; -import { AuthDto } from 'src/domain/auth/auth.dto'; -import { UserResponseDto, mapUser } from 'src/domain/user/response-dto/user-response.dto'; +import { ArrayNotEmpty, IsEnum, IsString } from 'class-validator'; +import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto'; +import { AuthDto } from 'src/dtos/auth.dto'; +import { UserResponseDto, mapUser } from 'src/dtos/user.dto'; import { AlbumEntity, AssetOrder } from 'src/entities/album.entity'; -import { Optional } from 'src/validation'; +import { Optional, ValidateBoolean, ValidateUUID } from 'src/validation'; + +export class AlbumInfoDto { + @ValidateBoolean({ optional: true }) + withoutAssets?: boolean; +} + +export class AddUsersDto { + @ValidateUUID({ each: true }) + @ArrayNotEmpty() + sharedUserIds!: string[]; +} + +export class CreateAlbumDto { + @IsString() + @ApiProperty() + albumName!: string; + + @IsString() + @Optional() + description?: string; + + @ValidateUUID({ optional: true, each: true }) + sharedWithUserIds?: string[]; + + @ValidateUUID({ optional: true, each: true }) + assetIds?: string[]; +} + +export class UpdateAlbumDto { + @Optional() + @IsString() + albumName?: string; + + @Optional() + @IsString() + description?: string; + + @ValidateUUID({ optional: true }) + albumThumbnailAssetId?: string; + + @ValidateBoolean({ optional: true }) + isActivityEnabled?: boolean; + + @IsEnum(AssetOrder) + @Optional() + @ApiProperty({ enum: AssetOrder, enumName: 'AssetOrder' }) + order?: AssetOrder; +} + +export class GetAlbumsDto { + @ValidateBoolean({ optional: true }) + /** + * true: only shared albums + * false: only non-shared own albums + * undefined: shared and owned albums + */ + shared?: boolean; + + /** + * Only returns albums that contain the asset + * Ignores the shared parameter + * undefined: get all albums + */ + @ValidateUUID({ optional: true }) + assetId?: string; +} + +export class AlbumCountResponseDto { + @ApiProperty({ type: 'integer' }) + owned!: number; + + @ApiProperty({ type: 'integer' }) + shared!: number; + + @ApiProperty({ type: 'integer' }) + notShared!: number; +} export class AlbumResponseDto { id!: string; @@ -73,14 +151,3 @@ export const mapAlbum = (entity: AlbumEntity, withAssets: boolean, auth?: AuthDt export const mapAlbumWithAssets = (entity: AlbumEntity) => mapAlbum(entity, true); export const mapAlbumWithoutAssets = (entity: AlbumEntity) => mapAlbum(entity, false); - -export class AlbumCountResponseDto { - @ApiProperty({ type: 'integer' }) - owned!: number; - - @ApiProperty({ type: 'integer' }) - shared!: number; - - @ApiProperty({ type: 'integer' }) - notShared!: number; -} diff --git a/server/src/domain/api-key/api-key.dto.ts b/server/src/dtos/api-key.dto.ts similarity index 100% rename from server/src/domain/api-key/api-key.dto.ts rename to server/src/dtos/api-key.dto.ts diff --git a/server/src/domain/asset/response-dto/asset-ids-response.dto.ts b/server/src/dtos/asset-ids.response.dto.ts similarity index 100% rename from server/src/domain/asset/response-dto/asset-ids-response.dto.ts rename to server/src/dtos/asset-ids.response.dto.ts diff --git a/server/src/domain/asset/response-dto/asset-response.dto.ts b/server/src/dtos/asset-response.dto.ts similarity index 88% rename from server/src/domain/asset/response-dto/asset-response.dto.ts rename to server/src/dtos/asset-response.dto.ts index dff12d481..04e36645e 100644 --- a/server/src/domain/asset/response-dto/asset-response.dto.ts +++ b/server/src/dtos/asset-response.dto.ts @@ -1,12 +1,12 @@ import { ApiProperty } from '@nestjs/swagger'; -import { ExifResponseDto, mapExif } from 'src/domain/asset/response-dto/exif-response.dto'; -import { SmartInfoResponseDto, mapSmartInfo } from 'src/domain/asset/response-dto/smart-info-response.dto'; -import { AuthDto } from 'src/domain/auth/auth.dto'; -import { PersonWithFacesResponseDto, mapFacesWithoutPerson, mapPerson } from 'src/domain/person/person.dto'; -import { TagResponseDto, mapTag } from 'src/domain/tag/tag-response.dto'; -import { UserResponseDto, mapUser } from 'src/domain/user/response-dto/user-response.dto'; +import { AuthDto } from 'src/dtos/auth.dto'; +import { ExifResponseDto, mapExif } from 'src/dtos/exif.dto'; +import { PersonWithFacesResponseDto, mapFacesWithoutPerson, mapPerson } from 'src/dtos/person.dto'; +import { TagResponseDto, mapTag } from 'src/dtos/tag.dto'; +import { UserResponseDto, mapUser } from 'src/dtos/user.dto'; import { AssetFaceEntity } from 'src/entities/asset-face.entity'; import { AssetEntity, AssetType } from 'src/entities/asset.entity'; +import { SmartInfoEntity } from 'src/entities/smart-info.entity'; export class SanitizedAssetResponseDto { id!: string; @@ -134,3 +134,15 @@ export class MemoryLaneResponseDto { title!: string; assets!: AssetResponseDto[]; } + +export class SmartInfoResponseDto { + tags?: string[] | null; + objects?: string[] | null; +} + +export function mapSmartInfo(entity: SmartInfoEntity): SmartInfoResponseDto { + return { + tags: entity.tags, + objects: entity.objects, + }; +} diff --git a/server/src/dtos/asset-v1-response.dto.ts b/server/src/dtos/asset-v1-response.dto.ts new file mode 100644 index 000000000..4b1e97b47 --- /dev/null +++ b/server/src/dtos/asset-v1-response.dto.ts @@ -0,0 +1,45 @@ +export class AssetBulkUploadCheckResult { + id!: string; + action!: AssetUploadAction; + reason?: AssetRejectReason; + assetId?: string; +} + +export class AssetBulkUploadCheckResponseDto { + results!: AssetBulkUploadCheckResult[]; +} + +export enum AssetUploadAction { + ACCEPT = 'accept', + REJECT = 'reject', +} + +export enum AssetRejectReason { + DUPLICATE = 'duplicate', + UNSUPPORTED_FORMAT = 'unsupported-format', +} + +export class AssetFileUploadResponseDto { + id!: string; + duplicate!: boolean; +} + +export class CheckExistingAssetsResponseDto { + existingIds!: string[]; +} + +export class CuratedLocationsResponseDto { + id!: string; + city!: string; + resizePath!: string; + deviceAssetId!: string; + deviceId!: string; +} + +export class CuratedObjectsResponseDto { + id!: string; + object!: string; + resizePath!: string; + deviceAssetId!: string; + deviceId!: string; +} diff --git a/server/src/dtos/asset-v1.dto.ts b/server/src/dtos/asset-v1.dto.ts new file mode 100644 index 000000000..50ff3d18b --- /dev/null +++ b/server/src/dtos/asset-v1.dto.ts @@ -0,0 +1,154 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { ArrayNotEmpty, IsArray, IsEnum, IsInt, IsNotEmpty, IsString, IsUUID, ValidateNested } from 'class-validator'; +import { UploadFieldName } from 'src/dtos/asset.dto'; +import { Optional, ValidateBoolean, ValidateDate, ValidateUUID } from 'src/validation'; + +export class AssetBulkUploadCheckItem { + @IsString() + @IsNotEmpty() + id!: string; + + /** base64 or hex encoded sha1 hash */ + @IsString() + @IsNotEmpty() + checksum!: string; +} + +export class AssetBulkUploadCheckDto { + @IsArray() + @ValidateNested({ each: true }) + @Type(() => AssetBulkUploadCheckItem) + assets!: AssetBulkUploadCheckItem[]; +} + +export class AssetSearchDto { + @ValidateBoolean({ optional: true }) + isFavorite?: boolean; + + @ValidateBoolean({ optional: true }) + isArchived?: boolean; + + @Optional() + @IsInt() + @Type(() => Number) + @ApiProperty({ type: 'integer' }) + skip?: number; + + @Optional() + @IsInt() + @Type(() => Number) + @ApiProperty({ type: 'integer' }) + take?: number; + + @Optional() + @IsUUID('4') + @ApiProperty({ format: 'uuid' }) + userId?: string; + + @ValidateDate({ optional: true }) + updatedAfter?: Date; + + @ValidateDate({ optional: true }) + updatedBefore?: Date; +} + +export class CheckExistingAssetsDto { + @ArrayNotEmpty() + @IsString({ each: true }) + @IsNotEmpty({ each: true }) + deviceAssetIds!: string[]; + + @IsNotEmpty() + deviceId!: string; +} + +export class CreateAssetDto { + @ValidateUUID({ optional: true }) + libraryId?: string; + + @IsNotEmpty() + @IsString() + deviceAssetId!: string; + + @IsNotEmpty() + @IsString() + deviceId!: string; + + @ValidateDate() + fileCreatedAt!: Date; + + @ValidateDate() + fileModifiedAt!: Date; + + @Optional() + @IsString() + duration?: string; + + @ValidateBoolean({ optional: true }) + isFavorite?: boolean; + + @ValidateBoolean({ optional: true }) + isArchived?: boolean; + + @ValidateBoolean({ optional: true }) + isVisible?: boolean; + + @ValidateBoolean({ optional: true }) + isOffline?: boolean; + + @ValidateBoolean({ optional: true }) + isReadOnly?: boolean; + + // The properties below are added to correctly generate the API docs + // and client SDKs. Validation should be handled in the controller. + @ApiProperty({ type: 'string', format: 'binary' }) + [UploadFieldName.ASSET_DATA]!: any; + + @ApiProperty({ type: 'string', format: 'binary', required: false }) + [UploadFieldName.LIVE_PHOTO_DATA]?: any; + + @ApiProperty({ type: 'string', format: 'binary', required: false }) + [UploadFieldName.SIDECAR_DATA]?: any; +} + +export enum GetAssetThumbnailFormatEnum { + JPEG = 'JPEG', + WEBP = 'WEBP', +} + +export class GetAssetThumbnailDto { + @Optional() + @IsEnum(GetAssetThumbnailFormatEnum) + @ApiProperty({ + type: String, + enum: GetAssetThumbnailFormatEnum, + default: GetAssetThumbnailFormatEnum.WEBP, + required: false, + enumName: 'ThumbnailFormat', + }) + format: GetAssetThumbnailFormatEnum = GetAssetThumbnailFormatEnum.WEBP; +} + +export class SearchPropertiesDto { + tags?: string[]; + objects?: string[]; + assetType?: string; + orientation?: string; + lensModel?: string; + make?: string; + model?: string; + city?: string; + state?: string; + country?: string; +} + +export class ServeFileDto { + @ValidateBoolean({ optional: true }) + @ApiProperty({ title: 'Is serve thumbnail (resize) file' }) + isThumb?: boolean; + + @ValidateBoolean({ optional: true }) + @ApiProperty({ title: 'Is request made from web' }) + isWeb?: boolean; +} diff --git a/server/src/dtos/asset.dto.ts b/server/src/dtos/asset.dto.ts new file mode 100644 index 000000000..72f1b24c1 --- /dev/null +++ b/server/src/dtos/asset.dto.ts @@ -0,0 +1,132 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { + IsDateString, + IsEnum, + IsInt, + IsLatitude, + IsLongitude, + IsNotEmpty, + IsPositive, + IsString, + ValidateIf, +} from 'class-validator'; +import { BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; +import { AssetType } from 'src/entities/asset.entity'; +import { AssetStats } from 'src/interfaces/asset.interface'; +import { Optional, ValidateBoolean, ValidateUUID } from 'src/validation'; + +export class DeviceIdDto { + @IsNotEmpty() + @IsString() + deviceId!: string; +} + +const hasGPS = (o: { latitude: undefined; longitude: undefined }) => + o.latitude !== undefined || o.longitude !== undefined; +const ValidateGPS = () => ValidateIf(hasGPS); + +export class UpdateAssetBase { + @ValidateBoolean({ optional: true }) + isFavorite?: boolean; + + @ValidateBoolean({ optional: true }) + isArchived?: boolean; + + @Optional() + @IsDateString() + dateTimeOriginal?: string; + + @ValidateGPS() + @IsLatitude() + @IsNotEmpty() + latitude?: number; + + @ValidateGPS() + @IsLongitude() + @IsNotEmpty() + longitude?: number; +} + +export class AssetBulkUpdateDto extends UpdateAssetBase { + @ValidateUUID({ each: true }) + ids!: string[]; + + @ValidateUUID({ optional: true }) + stackParentId?: string; + + @ValidateBoolean({ optional: true }) + removeParent?: boolean; +} + +export class UpdateAssetDto extends UpdateAssetBase { + @Optional() + @IsString() + description?: string; +} + +export class RandomAssetsDto { + @Optional() + @IsInt() + @IsPositive() + @Type(() => Number) + count?: number; +} + +export class AssetBulkDeleteDto extends BulkIdsDto { + @ValidateBoolean({ optional: true }) + force?: boolean; +} + +export class AssetIdsDto { + @ValidateUUID({ each: true }) + assetIds!: string[]; +} + +export enum AssetJobName { + REGENERATE_THUMBNAIL = 'regenerate-thumbnail', + REFRESH_METADATA = 'refresh-metadata', + TRANSCODE_VIDEO = 'transcode-video', +} + +export class AssetJobsDto extends AssetIdsDto { + @ApiProperty({ enumName: 'AssetJobName', enum: AssetJobName }) + @IsEnum(AssetJobName) + name!: AssetJobName; +} + +export class AssetStatsDto { + @ValidateBoolean({ optional: true }) + isArchived?: boolean; + + @ValidateBoolean({ optional: true }) + isFavorite?: boolean; + + @ValidateBoolean({ optional: true }) + isTrashed?: boolean; +} + +export class AssetStatsResponseDto { + @ApiProperty({ type: 'integer' }) + images!: number; + + @ApiProperty({ type: 'integer' }) + videos!: number; + + @ApiProperty({ type: 'integer' }) + total!: number; +} + +export const mapStats = (stats: AssetStats): AssetStatsResponseDto => { + return { + images: stats[AssetType.IMAGE], + videos: stats[AssetType.VIDEO], + total: Object.values(stats).reduce((total, value) => total + value, 0), + }; +}; +export enum UploadFieldName { + ASSET_DATA = 'assetData', + LIVE_PHOTO_DATA = 'livePhotoData', + SIDECAR_DATA = 'sidecarData', + PROFILE_DATA = 'file', +} diff --git a/server/src/domain/audit/audit.dto.ts b/server/src/dtos/audit.dto.ts similarity index 100% rename from server/src/domain/audit/audit.dto.ts rename to server/src/dtos/audit.dto.ts diff --git a/server/src/domain/auth/auth.dto.ts b/server/src/dtos/auth.dto.ts similarity index 100% rename from server/src/domain/auth/auth.dto.ts rename to server/src/dtos/auth.dto.ts diff --git a/server/src/domain/download/download.dto.ts b/server/src/dtos/download.dto.ts similarity index 100% rename from server/src/domain/download/download.dto.ts rename to server/src/dtos/download.dto.ts diff --git a/server/src/domain/asset/response-dto/exif-response.dto.ts b/server/src/dtos/exif.dto.ts similarity index 100% rename from server/src/domain/asset/response-dto/exif-response.dto.ts rename to server/src/dtos/exif.dto.ts diff --git a/server/src/domain/job/job.dto.ts b/server/src/dtos/job.dto.ts similarity index 96% rename from server/src/domain/job/job.dto.ts rename to server/src/dtos/job.dto.ts index fd463a9b0..1173ad8d6 100644 --- a/server/src/domain/job/job.dto.ts +++ b/server/src/dtos/job.dto.ts @@ -1,6 +1,6 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsEnum, IsNotEmpty } from 'class-validator'; -import { JobCommand, QueueName } from 'src/domain/job/job.constants'; +import { JobCommand, QueueName } from 'src/interfaces/job.interface'; import { ValidateBoolean } from 'src/validation'; export class JobIdParamDto { diff --git a/server/src/domain/library/library.dto.ts b/server/src/dtos/library.dto.ts similarity index 100% rename from server/src/domain/library/library.dto.ts rename to server/src/dtos/library.dto.ts diff --git a/server/src/domain/smart-info/dto/model-config.dto.ts b/server/src/dtos/model-config.dto.ts similarity index 98% rename from server/src/domain/smart-info/dto/model-config.dto.ts rename to server/src/dtos/model-config.dto.ts index b63ec7414..d1e8bf339 100644 --- a/server/src/domain/smart-info/dto/model-config.dto.ts +++ b/server/src/dtos/model-config.dto.ts @@ -1,7 +1,7 @@ import { ApiProperty } from '@nestjs/swagger'; import { Type } from 'class-transformer'; import { IsEnum, IsNotEmpty, IsNumber, IsString, Max, Min } from 'class-validator'; -import { CLIPMode, ModelType } from 'src/interfaces/machine-learning.repository'; +import { CLIPMode, ModelType } from 'src/interfaces/machine-learning.interface'; import { Optional, ValidateBoolean } from 'src/validation'; export class ModelConfig { diff --git a/server/src/domain/partner/partner.dto.ts b/server/src/dtos/partner.dto.ts similarity index 71% rename from server/src/domain/partner/partner.dto.ts rename to server/src/dtos/partner.dto.ts index c197d2079..187f8f341 100644 --- a/server/src/domain/partner/partner.dto.ts +++ b/server/src/dtos/partner.dto.ts @@ -1,5 +1,5 @@ import { IsNotEmpty } from 'class-validator'; -import { UserResponseDto } from 'src/domain/user/response-dto/user-response.dto'; +import { UserResponseDto } from 'src/dtos/user.dto'; export class UpdatePartnerDto { @IsNotEmpty() diff --git a/server/src/domain/person/person.dto.ts b/server/src/dtos/person.dto.ts similarity index 98% rename from server/src/domain/person/person.dto.ts rename to server/src/dtos/person.dto.ts index 4153ba813..b28f18603 100644 --- a/server/src/domain/person/person.dto.ts +++ b/server/src/dtos/person.dto.ts @@ -1,7 +1,7 @@ import { ApiProperty } from '@nestjs/swagger'; import { Type } from 'class-transformer'; import { IsArray, IsNotEmpty, IsString, MaxDate, ValidateNested } from 'class-validator'; -import { AuthDto } from 'src/domain/auth/auth.dto'; +import { AuthDto } from 'src/dtos/auth.dto'; import { AssetFaceEntity } from 'src/entities/asset-face.entity'; import { PersonEntity } from 'src/entities/person.entity'; import { Optional, ValidateBoolean, ValidateDate, ValidateUUID } from 'src/validation'; diff --git a/server/src/domain/search/dto/search.dto.ts b/server/src/dtos/search.dto.ts similarity index 66% rename from server/src/domain/search/dto/search.dto.ts rename to server/src/dtos/search.dto.ts index 4f77517b3..799baddee 100644 --- a/server/src/domain/search/dto/search.dto.ts +++ b/server/src/dtos/search.dto.ts @@ -1,6 +1,8 @@ import { ApiProperty } from '@nestjs/swagger'; import { Type } from 'class-transformer'; import { IsEnum, IsInt, IsNotEmpty, IsString, Max, Min } from 'class-validator'; +import { AlbumResponseDto } from 'src/dtos/album.dto'; +import { AssetResponseDto } from 'src/dtos/asset-response.dto'; import { AssetOrder } from 'src/entities/album.entity'; import { AssetType } from 'src/entities/asset.entity'; import { GeodataPlacesEntity } from 'src/entities/geodata-places.entity'; @@ -264,3 +266,130 @@ export function mapPlaces(place: GeodataPlacesEntity): PlacesResponseDto { admin2name: place.admin2Name, }; } +export enum SearchSuggestionType { + COUNTRY = 'country', + STATE = 'state', + CITY = 'city', + CAMERA_MAKE = 'camera-make', + CAMERA_MODEL = 'camera-model', +} + +export class SearchSuggestionRequestDto { + @IsEnum(SearchSuggestionType) + @IsNotEmpty() + @ApiProperty({ enumName: 'SearchSuggestionType', enum: SearchSuggestionType }) + type!: SearchSuggestionType; + + @IsString() + @Optional() + country?: string; + + @IsString() + @Optional() + state?: string; + + @IsString() + @Optional() + make?: string; + + @IsString() + @Optional() + model?: string; +} + +class SearchFacetCountResponseDto { + @ApiProperty({ type: 'integer' }) + count!: number; + value!: string; +} + +class SearchFacetResponseDto { + fieldName!: string; + counts!: SearchFacetCountResponseDto[]; +} + +class SearchAlbumResponseDto { + @ApiProperty({ type: 'integer' }) + total!: number; + @ApiProperty({ type: 'integer' }) + count!: number; + items!: AlbumResponseDto[]; + facets!: SearchFacetResponseDto[]; +} + +class SearchAssetResponseDto { + @ApiProperty({ type: 'integer' }) + total!: number; + @ApiProperty({ type: 'integer' }) + count!: number; + items!: AssetResponseDto[]; + facets!: SearchFacetResponseDto[]; + nextPage!: string | null; +} + +export class SearchResponseDto { + albums!: SearchAlbumResponseDto; + assets!: SearchAssetResponseDto; +} + +class SearchExploreItem { + value!: string; + data!: AssetResponseDto; +} + +export class SearchExploreResponseDto { + fieldName!: string; + items!: SearchExploreItem[]; +} + +export class MapMarkerDto { + @ValidateBoolean({ optional: true }) + isArchived?: boolean; + + @ValidateBoolean({ optional: true }) + isFavorite?: boolean; + + @ValidateDate({ optional: true }) + fileCreatedAfter?: Date; + + @ValidateDate({ optional: true }) + fileCreatedBefore?: Date; + + @ValidateBoolean({ optional: true }) + withPartners?: boolean; +} + +export class MemoryLaneDto { + @IsInt() + @Type(() => Number) + @Max(31) + @Min(1) + @ApiProperty({ type: 'integer' }) + day!: number; + + @IsInt() + @Type(() => Number) + @Max(12) + @Min(1) + @ApiProperty({ type: 'integer' }) + month!: number; +} +export class MapMarkerResponseDto { + @ApiProperty() + id!: string; + + @ApiProperty({ format: 'double' }) + lat!: number; + + @ApiProperty({ format: 'double' }) + lon!: number; + + @ApiProperty() + city!: string | null; + + @ApiProperty() + state!: string | null; + + @ApiProperty() + country!: string | null; +} diff --git a/server/src/domain/server-info/server-info.dto.ts b/server/src/dtos/server-info.dto.ts similarity index 94% rename from server/src/domain/server-info/server-info.dto.ts rename to server/src/dtos/server-info.dto.ts index 0cbfbe773..497b7ab5e 100644 --- a/server/src/domain/server-info/server-info.dto.ts +++ b/server/src/dtos/server-info.dto.ts @@ -1,8 +1,8 @@ import { ApiProperty, ApiResponseProperty } from '@nestjs/swagger'; import type { DateTime } from 'luxon'; import { FeatureFlags } from 'src/cores/system-config.core'; -import { IVersion, VersionType } from 'src/domain/domain.constant'; -import { SystemConfigThemeDto } from 'src/domain/system-config/dto/system-config-theme.dto'; +import { SystemConfigThemeDto } from 'src/dtos/system-config.dto'; +import { IVersion, VersionType } from 'src/utils/version'; export class ServerPingResponse { @ApiResponseProperty({ type: String, example: 'pong' }) diff --git a/server/src/domain/shared-link/shared-link-response.dto.ts b/server/src/dtos/shared-link.dto.ts similarity index 57% rename from server/src/domain/shared-link/shared-link-response.dto.ts rename to server/src/dtos/shared-link.dto.ts index 44024506f..9a90901d2 100644 --- a/server/src/domain/shared-link/shared-link-response.dto.ts +++ b/server/src/dtos/shared-link.dto.ts @@ -1,9 +1,81 @@ import { ApiProperty } from '@nestjs/swagger'; +import { IsEnum, IsString } from 'class-validator'; import _ from 'lodash'; -import { AlbumResponseDto, mapAlbumWithoutAssets } from 'src/domain/album/album-response.dto'; -import { AssetResponseDto, mapAsset } from 'src/domain/asset/response-dto/asset-response.dto'; +import { AlbumResponseDto, mapAlbumWithoutAssets } from 'src/dtos/album.dto'; +import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto'; import { SharedLinkEntity, SharedLinkType } from 'src/entities/shared-link.entity'; +import { Optional, ValidateBoolean, ValidateDate, ValidateUUID } from 'src/validation'; +export class SharedLinkCreateDto { + @IsEnum(SharedLinkType) + @ApiProperty({ enum: SharedLinkType, enumName: 'SharedLinkType' }) + type!: SharedLinkType; + + @ValidateUUID({ each: true, optional: true }) + assetIds?: string[]; + + @ValidateUUID({ optional: true }) + albumId?: string; + + @IsString() + @Optional() + description?: string; + + @IsString() + @Optional() + password?: string; + + @ValidateDate({ optional: true, nullable: true }) + expiresAt?: Date | null = null; + + @ValidateBoolean({ optional: true }) + allowUpload?: boolean; + + @ValidateBoolean({ optional: true }) + allowDownload?: boolean = true; + + @ValidateBoolean({ optional: true }) + showMetadata?: boolean = true; +} + +export class SharedLinkEditDto { + @Optional() + description?: string; + + @Optional() + password?: string; + + @Optional({ nullable: true }) + expiresAt?: Date | null; + + @Optional() + allowUpload?: boolean; + + @ValidateBoolean({ optional: true }) + allowDownload?: boolean; + + @ValidateBoolean({ optional: true }) + showMetadata?: boolean; + + /** + * Few clients cannot send null to set the expiryTime to never. + * Setting this flag and not sending expiryAt is considered as null instead. + * Clients that can send null values can ignore this. + */ + @ValidateBoolean({ optional: true }) + changeExpiryTime?: boolean; +} + +export class SharedLinkPasswordDto { + @IsString() + @Optional() + @ApiProperty({ example: 'password' }) + password?: string; + + @IsString() + @Optional() + token?: string; +} export class SharedLinkResponseDto { id!: string; description!: string | null; diff --git a/server/src/domain/asset/dto/asset-stack.dto.ts b/server/src/dtos/stack.dto.ts similarity index 100% rename from server/src/domain/asset/dto/asset-stack.dto.ts rename to server/src/dtos/stack.dto.ts diff --git a/server/src/dtos/system-config.dto.ts b/server/src/dtos/system-config.dto.ts new file mode 100644 index 000000000..740f1672e --- /dev/null +++ b/server/src/dtos/system-config.dto.ts @@ -0,0 +1,516 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { + IsEnum, + IsInt, + IsNotEmpty, + IsNumber, + IsObject, + IsPositive, + IsString, + IsUrl, + Max, + Min, + Validate, + ValidateIf, + ValidateNested, + ValidatorConstraint, + ValidatorConstraintInterface, +} from 'class-validator'; +import { CLIPConfig, RecognitionConfig } from 'src/dtos/model-config.dto'; +import { + AudioCodec, + CQMode, + Colorspace, + LogLevel, + SystemConfig, + ToneMapping, + TranscodeHWAccel, + TranscodePolicy, + VideoCodec, +} from 'src/entities/system-config.entity'; +import { ConcurrentQueueName, QueueName } from 'src/interfaces/job.interface'; +import { ValidateBoolean, validateCronExpression } from 'src/validation'; + +@ValidatorConstraint({ name: 'cronValidator' }) +class CronValidator implements ValidatorConstraintInterface { + validate(expression: string): boolean { + return validateCronExpression(expression); + } +} + +const isLibraryScanEnabled = (config: SystemConfigLibraryScanDto) => config.enabled; +const isOAuthEnabled = (config: SystemConfigOAuthDto) => config.enabled; +const isOAuthOverrideEnabled = (config: SystemConfigOAuthDto) => config.mobileOverrideEnabled; + +export class SystemConfigFFmpegDto { + @IsInt() + @Min(0) + @Max(51) + @Type(() => Number) + @ApiProperty({ type: 'integer' }) + crf!: number; + + @IsInt() + @Min(0) + @Type(() => Number) + @ApiProperty({ type: 'integer' }) + threads!: number; + + @IsString() + preset!: string; + + @IsEnum(VideoCodec) + @ApiProperty({ enumName: 'VideoCodec', enum: VideoCodec }) + targetVideoCodec!: VideoCodec; + + @IsEnum(VideoCodec, { each: true }) + @ApiProperty({ enumName: 'VideoCodec', enum: VideoCodec, isArray: true }) + acceptedVideoCodecs!: VideoCodec[]; + + @IsEnum(AudioCodec) + @ApiProperty({ enumName: 'AudioCodec', enum: AudioCodec }) + targetAudioCodec!: AudioCodec; + + @IsEnum(AudioCodec, { each: true }) + @ApiProperty({ enumName: 'AudioCodec', enum: AudioCodec, isArray: true }) + acceptedAudioCodecs!: AudioCodec[]; + + @IsString() + targetResolution!: string; + + @IsString() + maxBitrate!: string; + + @IsInt() + @Min(-1) + @Max(16) + @Type(() => Number) + @ApiProperty({ type: 'integer' }) + bframes!: number; + + @IsInt() + @Min(0) + @Max(6) + @Type(() => Number) + @ApiProperty({ type: 'integer' }) + refs!: number; + + @IsInt() + @Min(0) + @Type(() => Number) + @ApiProperty({ type: 'integer' }) + gopSize!: number; + + @IsInt() + @Min(0) + @Type(() => Number) + @ApiProperty({ type: 'integer' }) + npl!: number; + + @ValidateBoolean() + temporalAQ!: boolean; + + @IsEnum(CQMode) + @ApiProperty({ enumName: 'CQMode', enum: CQMode }) + cqMode!: CQMode; + + @ValidateBoolean() + twoPass!: boolean; + + @IsString() + preferredHwDevice!: string; + + @IsEnum(TranscodePolicy) + @ApiProperty({ enumName: 'TranscodePolicy', enum: TranscodePolicy }) + transcode!: TranscodePolicy; + + @IsEnum(TranscodeHWAccel) + @ApiProperty({ enumName: 'TranscodeHWAccel', enum: TranscodeHWAccel }) + accel!: TranscodeHWAccel; + + @IsEnum(ToneMapping) + @ApiProperty({ enumName: 'ToneMapping', enum: ToneMapping }) + tonemap!: ToneMapping; +} + +class JobSettingsDto { + @IsInt() + @IsPositive() + @ApiProperty({ type: 'integer' }) + concurrency!: number; +} + +class SystemConfigJobDto implements Record { + @ApiProperty({ type: JobSettingsDto }) + @ValidateNested() + @IsObject() + @Type(() => JobSettingsDto) + [QueueName.THUMBNAIL_GENERATION]!: JobSettingsDto; + + @ApiProperty({ type: JobSettingsDto }) + @ValidateNested() + @IsObject() + @Type(() => JobSettingsDto) + [QueueName.METADATA_EXTRACTION]!: JobSettingsDto; + + @ApiProperty({ type: JobSettingsDto }) + @ValidateNested() + @IsObject() + @Type(() => JobSettingsDto) + [QueueName.VIDEO_CONVERSION]!: JobSettingsDto; + + @ApiProperty({ type: JobSettingsDto }) + @ValidateNested() + @IsObject() + @Type(() => JobSettingsDto) + [QueueName.SMART_SEARCH]!: JobSettingsDto; + + @ApiProperty({ type: JobSettingsDto }) + @ValidateNested() + @IsObject() + @Type(() => JobSettingsDto) + [QueueName.MIGRATION]!: JobSettingsDto; + + @ApiProperty({ type: JobSettingsDto }) + @ValidateNested() + @IsObject() + @Type(() => JobSettingsDto) + [QueueName.BACKGROUND_TASK]!: JobSettingsDto; + + @ApiProperty({ type: JobSettingsDto }) + @ValidateNested() + @IsObject() + @Type(() => JobSettingsDto) + [QueueName.SEARCH]!: JobSettingsDto; + + @ApiProperty({ type: JobSettingsDto }) + @ValidateNested() + @IsObject() + @Type(() => JobSettingsDto) + [QueueName.FACE_DETECTION]!: JobSettingsDto; + + @ApiProperty({ type: JobSettingsDto }) + @ValidateNested() + @IsObject() + @Type(() => JobSettingsDto) + [QueueName.SIDECAR]!: JobSettingsDto; + + @ApiProperty({ type: JobSettingsDto }) + @ValidateNested() + @IsObject() + @Type(() => JobSettingsDto) + [QueueName.LIBRARY]!: JobSettingsDto; +} + +class SystemConfigLibraryScanDto { + @ValidateBoolean() + enabled!: boolean; + + @ValidateIf(isLibraryScanEnabled) + @IsNotEmpty() + @Validate(CronValidator, { message: 'Invalid cron expression' }) + @IsString() + cronExpression!: string; +} + +class SystemConfigLibraryWatchDto { + @ValidateBoolean() + enabled!: boolean; +} + +class SystemConfigLibraryDto { + @Type(() => SystemConfigLibraryScanDto) + @ValidateNested() + @IsObject() + scan!: SystemConfigLibraryScanDto; + + @Type(() => SystemConfigLibraryWatchDto) + @ValidateNested() + @IsObject() + watch!: SystemConfigLibraryWatchDto; +} + +class SystemConfigLoggingDto { + @ValidateBoolean() + enabled!: boolean; + + @ApiProperty({ enum: LogLevel, enumName: 'LogLevel' }) + @IsEnum(LogLevel) + level!: LogLevel; +} + +class SystemConfigMachineLearningDto { + @ValidateBoolean() + enabled!: boolean; + + @IsUrl({ require_tld: false, allow_underscores: true }) + @ValidateIf((dto) => dto.enabled) + url!: string; + + @Type(() => CLIPConfig) + @ValidateNested() + @IsObject() + clip!: CLIPConfig; + + @Type(() => RecognitionConfig) + @ValidateNested() + @IsObject() + facialRecognition!: RecognitionConfig; +} + +enum MapTheme { + LIGHT = 'light', + DARK = 'dark', +} + +export class MapThemeDto { + @IsEnum(MapTheme) + @ApiProperty({ enum: MapTheme, enumName: 'MapTheme' }) + theme!: MapTheme; +} + +class SystemConfigMapDto { + @ValidateBoolean() + enabled!: boolean; + + @IsString() + lightStyle!: string; + + @IsString() + darkStyle!: string; +} + +class SystemConfigNewVersionCheckDto { + @ValidateBoolean() + enabled!: boolean; +} + +class SystemConfigOAuthDto { + @ValidateBoolean() + autoLaunch!: boolean; + + @ValidateBoolean() + autoRegister!: boolean; + + @IsString() + buttonText!: string; + + @ValidateIf(isOAuthEnabled) + @IsNotEmpty() + @IsString() + clientId!: string; + + @ValidateIf(isOAuthEnabled) + @IsNotEmpty() + @IsString() + clientSecret!: string; + + @IsNumber() + @Min(0) + defaultStorageQuota!: number; + + @ValidateBoolean() + enabled!: boolean; + + @ValidateIf(isOAuthEnabled) + @IsNotEmpty() + @IsString() + issuerUrl!: string; + + @ValidateBoolean() + mobileOverrideEnabled!: boolean; + + @ValidateIf(isOAuthOverrideEnabled) + @IsUrl() + mobileRedirectUri!: string; + + @IsString() + scope!: string; + + @IsString() + @IsNotEmpty() + signingAlgorithm!: string; + + @IsString() + storageLabelClaim!: string; + + @IsString() + storageQuotaClaim!: string; +} + +class SystemConfigPasswordLoginDto { + @ValidateBoolean() + enabled!: boolean; +} + +class SystemConfigReverseGeocodingDto { + @ValidateBoolean() + enabled!: boolean; +} + +class SystemConfigServerDto { + @IsString() + externalDomain!: string; + + @IsString() + loginPageMessage!: string; +} + +class SystemConfigStorageTemplateDto { + @ValidateBoolean() + enabled!: boolean; + + @ValidateBoolean() + hashVerificationEnabled!: boolean; + + @IsNotEmpty() + @IsString() + template!: string; +} + +export class SystemConfigTemplateStorageOptionDto { + yearOptions!: string[]; + monthOptions!: string[]; + weekOptions!: string[]; + dayOptions!: string[]; + hourOptions!: string[]; + minuteOptions!: string[]; + secondOptions!: string[]; + presetOptions!: string[]; +} + +export class SystemConfigThemeDto { + @IsString() + customCss!: string; +} + +class SystemConfigThumbnailDto { + @IsInt() + @Min(1) + @Type(() => Number) + @ApiProperty({ type: 'integer' }) + webpSize!: number; + + @IsInt() + @Min(1) + @Type(() => Number) + @ApiProperty({ type: 'integer' }) + jpegSize!: number; + + @IsInt() + @Min(1) + @Max(100) + @Type(() => Number) + @ApiProperty({ type: 'integer' }) + quality!: number; + + @IsEnum(Colorspace) + @ApiProperty({ enumName: 'Colorspace', enum: Colorspace }) + colorspace!: Colorspace; +} + +class SystemConfigTrashDto { + @ValidateBoolean() + enabled!: boolean; + + @IsInt() + @Min(0) + @Type(() => Number) + @ApiProperty({ type: 'integer' }) + days!: number; +} + +class SystemConfigUserDto { + @IsInt() + @Min(1) + @Type(() => Number) + @ApiProperty({ type: 'integer' }) + deleteDelay!: number; +} + +export class SystemConfigDto implements SystemConfig { + @Type(() => SystemConfigFFmpegDto) + @ValidateNested() + @IsObject() + ffmpeg!: SystemConfigFFmpegDto; + + @Type(() => SystemConfigLoggingDto) + @ValidateNested() + @IsObject() + logging!: SystemConfigLoggingDto; + + @Type(() => SystemConfigMachineLearningDto) + @ValidateNested() + @IsObject() + machineLearning!: SystemConfigMachineLearningDto; + + @Type(() => SystemConfigMapDto) + @ValidateNested() + @IsObject() + map!: SystemConfigMapDto; + + @Type(() => SystemConfigNewVersionCheckDto) + @ValidateNested() + @IsObject() + newVersionCheck!: SystemConfigNewVersionCheckDto; + + @Type(() => SystemConfigOAuthDto) + @ValidateNested() + @IsObject() + oauth!: SystemConfigOAuthDto; + + @Type(() => SystemConfigPasswordLoginDto) + @ValidateNested() + @IsObject() + passwordLogin!: SystemConfigPasswordLoginDto; + + @Type(() => SystemConfigReverseGeocodingDto) + @ValidateNested() + @IsObject() + reverseGeocoding!: SystemConfigReverseGeocodingDto; + + @Type(() => SystemConfigStorageTemplateDto) + @ValidateNested() + @IsObject() + storageTemplate!: SystemConfigStorageTemplateDto; + + @Type(() => SystemConfigJobDto) + @ValidateNested() + @IsObject() + job!: SystemConfigJobDto; + + @Type(() => SystemConfigThumbnailDto) + @ValidateNested() + @IsObject() + thumbnail!: SystemConfigThumbnailDto; + + @Type(() => SystemConfigTrashDto) + @ValidateNested() + @IsObject() + trash!: SystemConfigTrashDto; + + @Type(() => SystemConfigThemeDto) + @ValidateNested() + @IsObject() + theme!: SystemConfigThemeDto; + + @Type(() => SystemConfigLibraryDto) + @ValidateNested() + @IsObject() + library!: SystemConfigLibraryDto; + + @Type(() => SystemConfigServerDto) + @ValidateNested() + @IsObject() + server!: SystemConfigServerDto; + + @Type(() => SystemConfigUserDto) + @ValidateNested() + @IsObject() + user!: SystemConfigUserDto; +} + +export function mapConfig(config: SystemConfig): SystemConfigDto { + return config; +} diff --git a/server/src/domain/tag/tag-response.dto.ts b/server/src/dtos/tag.dto.ts similarity index 54% rename from server/src/domain/tag/tag-response.dto.ts rename to server/src/dtos/tag.dto.ts index 535efcf43..1094d70df 100644 --- a/server/src/domain/tag/tag-response.dto.ts +++ b/server/src/dtos/tag.dto.ts @@ -1,5 +1,24 @@ import { ApiProperty } from '@nestjs/swagger'; +import { IsEnum, IsNotEmpty, IsString } from 'class-validator'; import { TagEntity, TagType } from 'src/entities/tag.entity'; +import { Optional } from 'src/validation'; + +export class CreateTagDto { + @IsString() + @IsNotEmpty() + name!: string; + + @IsEnum(TagType) + @IsNotEmpty() + @ApiProperty({ enumName: 'TagTypeEnum', enum: TagType }) + type!: TagType; +} + +export class UpdateTagDto { + @IsString() + @Optional() + name?: string; +} export class TagResponseDto { id!: string; diff --git a/server/src/domain/asset/dto/time-bucket.dto.ts b/server/src/dtos/time-bucket.dto.ts similarity index 83% rename from server/src/domain/asset/dto/time-bucket.dto.ts rename to server/src/dtos/time-bucket.dto.ts index a5ff023d1..a55126013 100644 --- a/server/src/domain/asset/dto/time-bucket.dto.ts +++ b/server/src/dtos/time-bucket.dto.ts @@ -1,7 +1,7 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsEnum, IsNotEmpty, IsString } from 'class-validator'; import { AssetOrder } from 'src/entities/album.entity'; -import { TimeBucketSize } from 'src/interfaces/asset.repository'; +import { TimeBucketSize } from 'src/interfaces/asset.interface'; import { Optional, ValidateBoolean, ValidateUUID } from 'src/validation'; export class TimeBucketDto { @@ -44,3 +44,11 @@ export class TimeBucketAssetDto extends TimeBucketDto { @IsString() timeBucket!: string; } + +export class TimeBucketResponseDto { + @ApiProperty({ type: 'string' }) + timeBucket!: string; + + @ApiProperty({ type: 'integer' }) + count!: number; +} diff --git a/server/src/dtos/user-profile.dto.ts b/server/src/dtos/user-profile.dto.ts new file mode 100644 index 000000000..2f3d8cf22 --- /dev/null +++ b/server/src/dtos/user-profile.dto.ts @@ -0,0 +1,28 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { UploadFieldName } from 'src/dtos/asset.dto'; +import { UserAvatarColor, UserEntity } from 'src/entities/user.entity'; + +export class CreateProfileImageDto { + @ApiProperty({ type: 'string', format: 'binary' }) + [UploadFieldName.PROFILE_DATA]!: Express.Multer.File; +} + +export class CreateProfileImageResponseDto { + userId!: string; + profileImagePath!: string; +} + +export function mapCreateProfileImageResponse(userId: string, profileImagePath: string): CreateProfileImageResponseDto { + return { + userId: userId, + profileImagePath: profileImagePath, + }; +} + +export const getRandomAvatarColor = (user: UserEntity): UserAvatarColor => { + const values = Object.values(UserAvatarColor); + const randomIndex = Math.floor( + [...user.email].map((letter) => letter.codePointAt(0) ?? 0).reduce((a, b) => a + b, 0) % values.length, + ); + return values[randomIndex] as UserAvatarColor; +}; diff --git a/server/src/domain/user/dto/create-user.dto.spec.ts b/server/src/dtos/user.dto.spec.ts similarity index 79% rename from server/src/domain/user/dto/create-user.dto.spec.ts rename to server/src/dtos/user.dto.spec.ts index 28abc44ad..d07399f0e 100644 --- a/server/src/domain/user/dto/create-user.dto.spec.ts +++ b/server/src/dtos/user.dto.spec.ts @@ -1,6 +1,20 @@ import { plainToInstance } from 'class-transformer'; import { validate } from 'class-validator'; -import { CreateAdminDto, CreateUserDto, CreateUserOAuthDto } from 'src/domain/user/dto/create-user.dto'; +import { CreateAdminDto, CreateUserDto, CreateUserOAuthDto, UpdateUserDto } from 'src/dtos/user.dto'; + +describe('update user DTO', () => { + it('should allow emails without a tld', async () => { + const someEmail = 'test@test'; + + const dto = plainToInstance(UpdateUserDto, { + email: someEmail, + id: '3fe388e4-2078-44d7-b36c-39d9dee3a657', + }); + const errors = await validate(dto); + expect(errors).toHaveLength(0); + expect(dto.email).toEqual(someEmail); + }); +}); describe('create user DTO', () => { it('validates the email', async () => { diff --git a/server/src/dtos/user.dto.ts b/server/src/dtos/user.dto.ts new file mode 100644 index 000000000..309006822 --- /dev/null +++ b/server/src/dtos/user.dto.ts @@ -0,0 +1,169 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Transform } from 'class-transformer'; +import { IsEmail, IsEnum, IsNotEmpty, IsNumber, IsPositive, IsString, IsUUID } from 'class-validator'; +import { getRandomAvatarColor } from 'src/dtos/user-profile.dto'; +import { UserAvatarColor, UserEntity, UserStatus } from 'src/entities/user.entity'; +import { Optional, ValidateBoolean, toEmail, toSanitized } from 'src/validation'; + +export class CreateUserDto { + @IsEmail({ require_tld: false }) + @Transform(toEmail) + email!: string; + + @IsNotEmpty() + @IsString() + password!: string; + + @IsNotEmpty() + @IsString() + name!: string; + + @Optional({ nullable: true }) + @IsString() + @Transform(toSanitized) + storageLabel?: string | null; + + @ValidateBoolean({ optional: true }) + memoriesEnabled?: boolean; + + @Optional({ nullable: true }) + @IsNumber() + @IsPositive() + @ApiProperty({ type: 'integer', format: 'int64' }) + quotaSizeInBytes?: number | null; + + @ValidateBoolean({ optional: true }) + shouldChangePassword?: boolean; +} + +export class CreateAdminDto { + @IsNotEmpty() + isAdmin!: true; + + @IsEmail({ require_tld: false }) + @Transform(({ value }) => value?.toLowerCase()) + email!: string; + + @IsNotEmpty() + password!: string; + + @IsNotEmpty() + name!: string; +} + +export class CreateUserOAuthDto { + @IsEmail({ require_tld: false }) + @Transform(({ value }) => value?.toLowerCase()) + email!: string; + + @IsNotEmpty() + oauthId!: string; + + name?: string; +} + +export class DeleteUserDto { + @ValidateBoolean({ optional: true }) + force?: boolean; +} + +export class UpdateUserDto { + @Optional() + @IsEmail({ require_tld: false }) + @Transform(toEmail) + email?: string; + + @Optional() + @IsNotEmpty() + @IsString() + password?: string; + + @Optional() + @IsString() + @IsNotEmpty() + name?: string; + + @Optional() + @IsString() + @Transform(toSanitized) + storageLabel?: string; + + @IsNotEmpty() + @IsUUID('4') + @ApiProperty({ format: 'uuid' }) + id!: string; + + @ValidateBoolean({ optional: true }) + isAdmin?: boolean; + + @ValidateBoolean({ optional: true }) + shouldChangePassword?: boolean; + + @ValidateBoolean({ optional: true }) + memoriesEnabled?: boolean; + + @Optional() + @IsEnum(UserAvatarColor) + @ApiProperty({ enumName: 'UserAvatarColor', enum: UserAvatarColor }) + avatarColor?: UserAvatarColor; + + @Optional({ nullable: true }) + @IsNumber() + @IsPositive() + @ApiProperty({ type: 'integer', format: 'int64' }) + quotaSizeInBytes?: number | null; +} + +export class UserDto { + id!: string; + name!: string; + email!: string; + profileImagePath!: string; + @IsEnum(UserAvatarColor) + @ApiProperty({ enumName: 'UserAvatarColor', enum: UserAvatarColor }) + avatarColor!: UserAvatarColor; +} + +export class UserResponseDto extends UserDto { + storageLabel!: string | null; + shouldChangePassword!: boolean; + isAdmin!: boolean; + createdAt!: Date; + deletedAt!: Date | null; + updatedAt!: Date; + oauthId!: string; + memoriesEnabled?: boolean; + @ApiProperty({ type: 'integer', format: 'int64' }) + quotaSizeInBytes!: number | null; + @ApiProperty({ type: 'integer', format: 'int64' }) + quotaUsageInBytes!: number | null; + @ApiProperty({ enumName: 'UserStatus', enum: UserStatus }) + status!: string; +} + +export const mapSimpleUser = (entity: UserEntity): UserDto => { + return { + id: entity.id, + email: entity.email, + name: entity.name, + profileImagePath: entity.profileImagePath, + avatarColor: entity.avatarColor ?? getRandomAvatarColor(entity), + }; +}; + +export function mapUser(entity: UserEntity): UserResponseDto { + return { + ...mapSimpleUser(entity), + storageLabel: entity.storageLabel, + shouldChangePassword: entity.shouldChangePassword, + isAdmin: entity.isAdmin, + createdAt: entity.createdAt, + deletedAt: entity.deletedAt, + updatedAt: entity.updatedAt, + oauthId: entity.oauthId, + memoriesEnabled: entity.memoriesEnabled, + quotaSizeInBytes: entity.quotaSizeInBytes, + quotaUsageInBytes: entity.quotaUsageInBytes, + status: entity.status, + }; +} diff --git a/server/src/entities/system-config.entity.ts b/server/src/entities/system-config.entity.ts index cf7474417..98b882a36 100644 --- a/server/src/entities/system-config.entity.ts +++ b/server/src/entities/system-config.entity.ts @@ -1,4 +1,4 @@ -import { ConcurrentQueueName } from 'src/domain/job/job.constants'; +import { ConcurrentQueueName } from 'src/interfaces/job.interface'; import { Column, Entity, PrimaryColumn } from 'typeorm'; @Entity('system_config') diff --git a/server/src/immich/api-v1/asset/dto/asset-check.dto.ts b/server/src/immich/api-v1/asset/dto/asset-check.dto.ts deleted file mode 100644 index d3474171f..000000000 --- a/server/src/immich/api-v1/asset/dto/asset-check.dto.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Type } from 'class-transformer'; -import { IsArray, IsNotEmpty, IsString, ValidateNested } from 'class-validator'; - -export class AssetBulkUploadCheckItem { - @IsString() - @IsNotEmpty() - id!: string; - - /** base64 or hex encoded sha1 hash */ - @IsString() - @IsNotEmpty() - checksum!: string; -} - -export class AssetBulkUploadCheckDto { - @IsArray() - @ValidateNested({ each: true }) - @Type(() => AssetBulkUploadCheckItem) - assets!: AssetBulkUploadCheckItem[]; -} diff --git a/server/src/immich/api-v1/asset/dto/asset-search.dto.ts b/server/src/immich/api-v1/asset/dto/asset-search.dto.ts deleted file mode 100644 index 97d0aa1fa..000000000 --- a/server/src/immich/api-v1/asset/dto/asset-search.dto.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; -import { IsInt, IsUUID } from 'class-validator'; -import { Optional, ValidateBoolean, ValidateDate } from 'src/validation'; - -export class AssetSearchDto { - @ValidateBoolean({ optional: true }) - isFavorite?: boolean; - - @ValidateBoolean({ optional: true }) - isArchived?: boolean; - - @Optional() - @IsInt() - @Type(() => Number) - @ApiProperty({ type: 'integer' }) - skip?: number; - - @Optional() - @IsInt() - @Type(() => Number) - @ApiProperty({ type: 'integer' }) - take?: number; - - @Optional() - @IsUUID('4') - @ApiProperty({ format: 'uuid' }) - userId?: string; - - @ValidateDate({ optional: true }) - updatedAfter?: Date; - - @ValidateDate({ optional: true }) - updatedBefore?: Date; -} diff --git a/server/src/immich/api-v1/asset/dto/check-existing-assets.dto.spec.ts b/server/src/immich/api-v1/asset/dto/check-existing-assets.dto.spec.ts deleted file mode 100644 index a634ba42e..000000000 --- a/server/src/immich/api-v1/asset/dto/check-existing-assets.dto.spec.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { plainToInstance } from 'class-transformer'; -import { validateSync } from 'class-validator'; -import { CheckExistingAssetsDto } from 'src/immich/api-v1/asset/dto/check-existing-assets.dto'; - -describe('CheckExistingAssetsDto', () => { - it('should fail with an empty list', () => { - const dto = plainToInstance(CheckExistingAssetsDto, { deviceAssetIds: [], deviceId: 'test-device' }); - const errors = validateSync(dto); - expect(errors).toHaveLength(1); - expect(errors[0].property).toEqual('deviceAssetIds'); - }); - - it('should fail with an empty string', () => { - const dto = plainToInstance(CheckExistingAssetsDto, { deviceAssetIds: [''], deviceId: 'test-device' }); - const errors = validateSync(dto); - expect(errors).toHaveLength(1); - expect(errors[0].property).toEqual('deviceAssetIds'); - }); - - it('should work with valid asset ids', () => { - const dto = plainToInstance(CheckExistingAssetsDto, { - deviceAssetIds: ['asset-1', 'asset-2'], - deviceId: 'test-device', - }); - const errors = validateSync(dto); - expect(errors).toHaveLength(0); - }); -}); diff --git a/server/src/immich/api-v1/asset/dto/check-existing-assets.dto.ts b/server/src/immich/api-v1/asset/dto/check-existing-assets.dto.ts deleted file mode 100644 index 65740ab89..000000000 --- a/server/src/immich/api-v1/asset/dto/check-existing-assets.dto.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { ArrayNotEmpty, IsNotEmpty, IsString } from 'class-validator'; - -export class CheckExistingAssetsDto { - @ArrayNotEmpty() - @IsString({ each: true }) - @IsNotEmpty({ each: true }) - deviceAssetIds!: string[]; - - @IsNotEmpty() - deviceId!: string; -} diff --git a/server/src/immich/api-v1/asset/dto/create-asset.dto.ts b/server/src/immich/api-v1/asset/dto/create-asset.dto.ts deleted file mode 100644 index d16a9c05c..000000000 --- a/server/src/immich/api-v1/asset/dto/create-asset.dto.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsNotEmpty, IsString } from 'class-validator'; -import { UploadFieldName } from 'src/domain/asset/asset.service'; -import { Optional, ValidateBoolean, ValidateDate, ValidateUUID } from 'src/validation'; - -export class CreateAssetDto { - @ValidateUUID({ optional: true }) - libraryId?: string; - - @IsNotEmpty() - @IsString() - deviceAssetId!: string; - - @IsNotEmpty() - @IsString() - deviceId!: string; - - @ValidateDate() - fileCreatedAt!: Date; - - @ValidateDate() - fileModifiedAt!: Date; - - @Optional() - @IsString() - duration?: string; - - @ValidateBoolean({ optional: true }) - isFavorite?: boolean; - - @ValidateBoolean({ optional: true }) - isArchived?: boolean; - - @ValidateBoolean({ optional: true }) - isVisible?: boolean; - - @ValidateBoolean({ optional: true }) - isOffline?: boolean; - - @ValidateBoolean({ optional: true }) - isReadOnly?: boolean; - - // The properties below are added to correctly generate the API docs - // and client SDKs. Validation should be handled in the controller. - @ApiProperty({ type: 'string', format: 'binary' }) - [UploadFieldName.ASSET_DATA]!: any; - - @ApiProperty({ type: 'string', format: 'binary', required: false }) - [UploadFieldName.LIVE_PHOTO_DATA]?: any; - - @ApiProperty({ type: 'string', format: 'binary', required: false }) - [UploadFieldName.SIDECAR_DATA]?: any; -} diff --git a/server/src/immich/api-v1/asset/dto/get-asset-thumbnail.dto.ts b/server/src/immich/api-v1/asset/dto/get-asset-thumbnail.dto.ts deleted file mode 100644 index 6c709eb02..000000000 --- a/server/src/immich/api-v1/asset/dto/get-asset-thumbnail.dto.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsEnum } from 'class-validator'; -import { Optional } from 'src/validation'; - -export enum GetAssetThumbnailFormatEnum { - JPEG = 'JPEG', - WEBP = 'WEBP', -} - -export class GetAssetThumbnailDto { - @Optional() - @IsEnum(GetAssetThumbnailFormatEnum) - @ApiProperty({ - type: String, - enum: GetAssetThumbnailFormatEnum, - default: GetAssetThumbnailFormatEnum.WEBP, - required: false, - enumName: 'ThumbnailFormat', - }) - format: GetAssetThumbnailFormatEnum = GetAssetThumbnailFormatEnum.WEBP; -} diff --git a/server/src/immich/api-v1/asset/dto/search-properties.dto.ts b/server/src/immich/api-v1/asset/dto/search-properties.dto.ts deleted file mode 100644 index 669b29b2e..000000000 --- a/server/src/immich/api-v1/asset/dto/search-properties.dto.ts +++ /dev/null @@ -1,12 +0,0 @@ -export class SearchPropertiesDto { - tags?: string[]; - objects?: string[]; - assetType?: string; - orientation?: string; - lensModel?: string; - make?: string; - model?: string; - city?: string; - state?: string; - country?: string; -} diff --git a/server/src/immich/api-v1/asset/dto/serve-file.dto.ts b/server/src/immich/api-v1/asset/dto/serve-file.dto.ts deleted file mode 100644 index 8b3147fc2..000000000 --- a/server/src/immich/api-v1/asset/dto/serve-file.dto.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { ValidateBoolean } from 'src/validation'; - -export class ServeFileDto { - @ValidateBoolean({ optional: true }) - @ApiProperty({ title: 'Is serve thumbnail (resize) file' }) - isThumb?: boolean; - - @ValidateBoolean({ optional: true }) - @ApiProperty({ title: 'Is request made from web' }) - isWeb?: boolean; -} diff --git a/server/src/immich/api-v1/asset/response-dto/asset-check-response.dto.ts b/server/src/immich/api-v1/asset/response-dto/asset-check-response.dto.ts deleted file mode 100644 index 1a51dc53f..000000000 --- a/server/src/immich/api-v1/asset/response-dto/asset-check-response.dto.ts +++ /dev/null @@ -1,20 +0,0 @@ -export class AssetBulkUploadCheckResult { - id!: string; - action!: AssetUploadAction; - reason?: AssetRejectReason; - assetId?: string; -} - -export class AssetBulkUploadCheckResponseDto { - results!: AssetBulkUploadCheckResult[]; -} - -export enum AssetUploadAction { - ACCEPT = 'accept', - REJECT = 'reject', -} - -export enum AssetRejectReason { - DUPLICATE = 'duplicate', - UNSUPPORTED_FORMAT = 'unsupported-format', -} diff --git a/server/src/immich/api-v1/asset/response-dto/asset-file-upload-response.dto.ts b/server/src/immich/api-v1/asset/response-dto/asset-file-upload-response.dto.ts deleted file mode 100644 index f628b708d..000000000 --- a/server/src/immich/api-v1/asset/response-dto/asset-file-upload-response.dto.ts +++ /dev/null @@ -1,4 +0,0 @@ -export class AssetFileUploadResponseDto { - id!: string; - duplicate!: boolean; -} diff --git a/server/src/immich/api-v1/asset/response-dto/check-existing-assets-response.dto.ts b/server/src/immich/api-v1/asset/response-dto/check-existing-assets-response.dto.ts deleted file mode 100644 index c39a79606..000000000 --- a/server/src/immich/api-v1/asset/response-dto/check-existing-assets-response.dto.ts +++ /dev/null @@ -1,3 +0,0 @@ -export class CheckExistingAssetsResponseDto { - existingIds!: string[]; -} diff --git a/server/src/immich/api-v1/asset/response-dto/curated-locations-response.dto.ts b/server/src/immich/api-v1/asset/response-dto/curated-locations-response.dto.ts deleted file mode 100644 index 63b1b0969..000000000 --- a/server/src/immich/api-v1/asset/response-dto/curated-locations-response.dto.ts +++ /dev/null @@ -1,7 +0,0 @@ -export class CuratedLocationsResponseDto { - id!: string; - city!: string; - resizePath!: string; - deviceAssetId!: string; - deviceId!: string; -} diff --git a/server/src/immich/api-v1/asset/response-dto/curated-objects-response.dto.ts b/server/src/immich/api-v1/asset/response-dto/curated-objects-response.dto.ts deleted file mode 100644 index 0d23b3eb7..000000000 --- a/server/src/immich/api-v1/asset/response-dto/curated-objects-response.dto.ts +++ /dev/null @@ -1,7 +0,0 @@ -export class CuratedObjectsResponseDto { - id!: string; - object!: string; - resizePath!: string; - deviceAssetId!: string; - deviceId!: string; -} diff --git a/server/src/infra/infra.module.ts b/server/src/infra/infra.module.ts deleted file mode 100644 index b0387ba96..000000000 --- a/server/src/infra/infra.module.ts +++ /dev/null @@ -1,127 +0,0 @@ -import { BullModule } from '@nestjs/bullmq'; -import { Global, Module, Provider } from '@nestjs/common'; -import { ConfigModule } from '@nestjs/config'; -import { EventEmitterModule } from '@nestjs/event-emitter'; -import { ScheduleModule, SchedulerRegistry } from '@nestjs/schedule'; -import { TypeOrmModule } from '@nestjs/typeorm'; -import { OpenTelemetryModule } from 'nestjs-otel'; -import { bullConfig, bullQueues, immichAppConfig } from 'src/config'; -import { databaseEntities } from 'src/entities'; -import { databaseConfig } from 'src/infra/database.config'; -import { otelConfig } from 'src/infra/instrumentation'; -import { IAccessRepository } from 'src/interfaces/access.repository'; -import { IActivityRepository } from 'src/interfaces/activity.repository'; -import { IAlbumRepository } from 'src/interfaces/album.repository'; -import { IKeyRepository } from 'src/interfaces/api-key.repository'; -import { IAssetStackRepository } from 'src/interfaces/asset-stack.repository'; -import { IAssetRepository } from 'src/interfaces/asset.repository'; -import { IAuditRepository } from 'src/interfaces/audit.repository'; -import { ICommunicationRepository } from 'src/interfaces/communication.repository'; -import { ICryptoRepository } from 'src/interfaces/crypto.repository'; -import { IDatabaseRepository } from 'src/interfaces/database.repository'; -import { IJobRepository } from 'src/interfaces/job.repository'; -import { ILibraryRepository } from 'src/interfaces/library.repository'; -import { IMachineLearningRepository } from 'src/interfaces/machine-learning.repository'; -import { IMediaRepository } from 'src/interfaces/media.repository'; -import { IMetadataRepository } from 'src/interfaces/metadata.repository'; -import { IMoveRepository } from 'src/interfaces/move.repository'; -import { IPartnerRepository } from 'src/interfaces/partner.repository'; -import { IPersonRepository } from 'src/interfaces/person.repository'; -import { ISearchRepository } from 'src/interfaces/search.repository'; -import { IServerInfoRepository } from 'src/interfaces/server-info.repository'; -import { ISharedLinkRepository } from 'src/interfaces/shared-link.repository'; -import { IStorageRepository } from 'src/interfaces/storage.repository'; -import { ISystemConfigRepository } from 'src/interfaces/system-config.repository'; -import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.repository'; -import { ITagRepository } from 'src/interfaces/tag.repository'; -import { IUserTokenRepository } from 'src/interfaces/user-token.repository'; -import { IUserRepository } from 'src/interfaces/user.repository'; -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 { 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'; - -const providers: Provider[] = [ - { provide: IActivityRepository, useClass: ActivityRepository }, - { provide: IAccessRepository, useClass: AccessRepository }, - { provide: IAlbumRepository, useClass: AlbumRepository }, - { provide: IAssetRepository, useClass: AssetRepository }, - { 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 }, - SchedulerRegistry, -]; - -@Global() -@Module({ - imports: [ - ConfigModule.forRoot(immichAppConfig), - EventEmitterModule.forRoot(), - TypeOrmModule.forRoot(databaseConfig), - TypeOrmModule.forFeature(databaseEntities), - ScheduleModule, - BullModule.forRoot(bullConfig), - BullModule.registerQueue(...bullQueues), - OpenTelemetryModule.forRoot(otelConfig), - ], - providers: [...providers], - exports: [...providers, BullModule], -}) -export class InfraModule {} - -@Global() -@Module({ - imports: [ - ConfigModule.forRoot(immichAppConfig), - EventEmitterModule.forRoot(), - TypeOrmModule.forRoot(databaseConfig), - TypeOrmModule.forFeature(databaseEntities), - ScheduleModule, - ], - providers: [...providers], - exports: [...providers], -}) -export class InfraTestModule {} diff --git a/server/src/infra/sql-generator/sql.logger.ts b/server/src/infra/sql-generator/sql.logger.ts deleted file mode 100644 index 6f3c298c0..000000000 --- a/server/src/infra/sql-generator/sql.logger.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { format } from 'sql-formatter'; -import { Logger } from 'typeorm'; - -export class SqlLogger implements Logger { - queries: string[] = []; - errors: Array<{ error: string | Error; query: string }> = []; - - clear() { - this.queries = []; - this.errors = []; - } - - logQuery(query: string) { - this.queries.push(format(query, { language: 'postgresql' })); - } - - logQueryError(error: string | Error, query: string) { - this.errors.push({ error, query }); - } - - logQuerySlow() {} - logSchemaBuild() {} - logMigration() {} - log() {} -} diff --git a/server/src/interfaces/access.repository.ts b/server/src/interfaces/access.interface.ts similarity index 100% rename from server/src/interfaces/access.repository.ts rename to server/src/interfaces/access.interface.ts diff --git a/server/src/interfaces/activity.repository.ts b/server/src/interfaces/activity.interface.ts similarity index 100% rename from server/src/interfaces/activity.repository.ts rename to server/src/interfaces/activity.interface.ts diff --git a/server/src/interfaces/album.repository.ts b/server/src/interfaces/album.interface.ts similarity index 100% rename from server/src/interfaces/album.repository.ts rename to server/src/interfaces/album.interface.ts diff --git a/server/src/interfaces/api-key.repository.ts b/server/src/interfaces/api-key.interface.ts similarity index 100% rename from server/src/interfaces/api-key.repository.ts rename to server/src/interfaces/api-key.interface.ts diff --git a/server/src/interfaces/asset-stack.repository.ts b/server/src/interfaces/asset-stack.interface.ts similarity index 100% rename from server/src/interfaces/asset-stack.repository.ts rename to server/src/interfaces/asset-stack.interface.ts diff --git a/server/src/interfaces/asset-v1.interface.ts b/server/src/interfaces/asset-v1.interface.ts new file mode 100644 index 000000000..8348bfaee --- /dev/null +++ b/server/src/interfaces/asset-v1.interface.ts @@ -0,0 +1,25 @@ +import { CuratedLocationsResponseDto, CuratedObjectsResponseDto } from 'src/dtos/asset-v1-response.dto'; +import { AssetSearchDto, CheckExistingAssetsDto, SearchPropertiesDto } from 'src/dtos/asset-v1.dto'; +import { AssetEntity } from 'src/entities/asset.entity'; + +export interface AssetCheck { + id: string; + checksum: Buffer; +} + +export interface AssetOwnerCheck extends AssetCheck { + ownerId: string; +} + +export interface IAssetRepositoryV1 { + get(id: string): Promise; + getLocationsByUserId(userId: string): Promise; + getDetectedObjectsByUserId(userId: string): Promise; + getAllByUserId(userId: string, dto: AssetSearchDto): Promise; + getSearchPropertiesByUserId(userId: string): Promise; + getAssetsByChecksums(userId: string, checksums: Buffer[]): Promise; + getExistingAssets(userId: string, checkDuplicateAssetDto: CheckExistingAssetsDto): Promise; + getByOriginalPath(originalPath: string): Promise; +} + +export const IAssetRepositoryV1 = 'IAssetRepositoryV1'; diff --git a/server/src/interfaces/asset.repository.ts b/server/src/interfaces/asset.interface.ts similarity index 97% rename from server/src/interfaces/asset.repository.ts rename to server/src/interfaces/asset.interface.ts index a04969a4f..0d86bdc05 100644 --- a/server/src/interfaces/asset.repository.ts +++ b/server/src/interfaces/asset.interface.ts @@ -2,9 +2,9 @@ import { AssetOrder } from 'src/entities/album.entity'; import { AssetJobStatusEntity } from 'src/entities/asset-job-status.entity'; import { AssetEntity, AssetType } from 'src/entities/asset.entity'; import { ExifEntity } from 'src/entities/exif.entity'; -import { ReverseGeocodeResult } from 'src/interfaces/metadata.repository'; -import { AssetSearchOptions, SearchExploreItem } from 'src/interfaces/search.repository'; -import { Paginated, PaginationOptions } from 'src/utils'; +import { ReverseGeocodeResult } from 'src/interfaces/metadata.interface'; +import { AssetSearchOptions, SearchExploreItem } from 'src/interfaces/search.interface'; +import { Paginated, PaginationOptions } from 'src/utils/pagination'; import { FindOptionsRelations, FindOptionsSelect } from 'typeorm'; export type AssetStats = Record; diff --git a/server/src/interfaces/audit.repository.ts b/server/src/interfaces/audit.interface.ts similarity index 100% rename from server/src/interfaces/audit.repository.ts rename to server/src/interfaces/audit.interface.ts diff --git a/server/src/interfaces/communication.repository.ts b/server/src/interfaces/communication.interface.ts similarity index 94% rename from server/src/interfaces/communication.repository.ts rename to server/src/interfaces/communication.interface.ts index 870c0d937..4627e5265 100644 --- a/server/src/interfaces/communication.repository.ts +++ b/server/src/interfaces/communication.interface.ts @@ -1,5 +1,5 @@ -import { AssetResponseDto } from 'src/domain/asset/response-dto/asset-response.dto'; -import { ReleaseNotification, ServerVersionResponseDto } from 'src/domain/server-info/server-info.dto'; +import { AssetResponseDto } from 'src/dtos/asset-response.dto'; +import { ReleaseNotification, ServerVersionResponseDto } from 'src/dtos/server-info.dto'; import { SystemConfig } from 'src/entities/system-config.entity'; export const ICommunicationRepository = 'ICommunicationRepository'; diff --git a/server/src/interfaces/crypto.repository.ts b/server/src/interfaces/crypto.interface.ts similarity index 100% rename from server/src/interfaces/crypto.repository.ts rename to server/src/interfaces/crypto.interface.ts diff --git a/server/src/interfaces/database.repository.ts b/server/src/interfaces/database.interface.ts similarity index 96% rename from server/src/interfaces/database.repository.ts rename to server/src/interfaces/database.interface.ts index e87775c3e..42342eccc 100644 --- a/server/src/interfaces/database.repository.ts +++ b/server/src/interfaces/database.interface.ts @@ -1,4 +1,4 @@ -import { Version } from 'src/domain/domain.constant'; +import { Version } from 'src/utils/version'; export enum DatabaseExtension { CUBE = 'cube', diff --git a/server/src/interfaces/job.repository.ts b/server/src/interfaces/job.interface.ts similarity index 52% rename from server/src/interfaces/job.repository.ts rename to server/src/interfaces/job.interface.ts index 84b39bbad..3257bd721 100644 --- a/server/src/interfaces/job.repository.ts +++ b/server/src/interfaces/job.interface.ts @@ -1,14 +1,140 @@ -import { JobName, QueueName } from 'src/domain/job/job.constants'; -import { - IAssetDeletionJob, - IBaseJob, - IDeferrableJob, - IDeleteFilesJob, - IEntityJob, - ILibraryFileJob, - ILibraryRefreshJob, - ISidecarWriteJob, -} from 'src/domain/job/job.interface'; +export enum QueueName { + THUMBNAIL_GENERATION = 'thumbnailGeneration', + METADATA_EXTRACTION = 'metadataExtraction', + VIDEO_CONVERSION = 'videoConversion', + FACE_DETECTION = 'faceDetection', + FACIAL_RECOGNITION = 'facialRecognition', + SMART_SEARCH = 'smartSearch', + BACKGROUND_TASK = 'backgroundTask', + STORAGE_TEMPLATE_MIGRATION = 'storageTemplateMigration', + MIGRATION = 'migration', + SEARCH = 'search', + SIDECAR = 'sidecar', + LIBRARY = 'library', +} + +export type ConcurrentQueueName = Exclude< + QueueName, + QueueName.STORAGE_TEMPLATE_MIGRATION | QueueName.FACIAL_RECOGNITION +>; + +export enum JobCommand { + START = 'start', + PAUSE = 'pause', + RESUME = 'resume', + EMPTY = 'empty', + CLEAR_FAILED = 'clear-failed', +} + +export enum JobName { + // conversion + QUEUE_VIDEO_CONVERSION = 'queue-video-conversion', + VIDEO_CONVERSION = 'video-conversion', + + // thumbnails + QUEUE_GENERATE_THUMBNAILS = 'queue-generate-thumbnails', + GENERATE_JPEG_THUMBNAIL = 'generate-jpeg-thumbnail', + GENERATE_WEBP_THUMBNAIL = 'generate-webp-thumbnail', + GENERATE_THUMBHASH_THUMBNAIL = 'generate-thumbhash-thumbnail', + GENERATE_PERSON_THUMBNAIL = 'generate-person-thumbnail', + + // metadata + QUEUE_METADATA_EXTRACTION = 'queue-metadata-extraction', + METADATA_EXTRACTION = 'metadata-extraction', + LINK_LIVE_PHOTOS = 'link-live-photos', + + // user + USER_DELETION = 'user-deletion', + USER_DELETE_CHECK = 'user-delete-check', + USER_SYNC_USAGE = 'user-sync-usage', + + // asset + ASSET_DELETION = 'asset-deletion', + ASSET_DELETION_CHECK = 'asset-deletion-check', + + // storage template + STORAGE_TEMPLATE_MIGRATION = 'storage-template-migration', + STORAGE_TEMPLATE_MIGRATION_SINGLE = 'storage-template-migration-single', + + // migration + QUEUE_MIGRATION = 'queue-migration', + MIGRATE_ASSET = 'migrate-asset', + MIGRATE_PERSON = 'migrate-person', + + // facial recognition + PERSON_CLEANUP = 'person-cleanup', + QUEUE_FACE_DETECTION = 'queue-face-detection', + FACE_DETECTION = 'face-detection', + QUEUE_FACIAL_RECOGNITION = 'queue-facial-recognition', + FACIAL_RECOGNITION = 'facial-recognition', + + // library management + LIBRARY_SCAN = 'library-refresh', + LIBRARY_SCAN_ASSET = 'library-refresh-asset', + LIBRARY_REMOVE_OFFLINE = 'library-remove-offline', + LIBRARY_CHECK_OFFLINE = 'library-check-offline', + LIBRARY_DELETE = 'library-delete', + LIBRARY_QUEUE_SCAN_ALL = 'library-queue-all-refresh', + LIBRARY_QUEUE_CLEANUP = 'library-queue-cleanup', + + // cleanup + DELETE_FILES = 'delete-files', + CLEAN_OLD_AUDIT_LOGS = 'clean-old-audit-logs', + + // smart search + QUEUE_SMART_SEARCH = 'queue-smart-search', + SMART_SEARCH = 'smart-search', + + // XMP sidecars + QUEUE_SIDECAR = 'queue-sidecar', + SIDECAR_DISCOVERY = 'sidecar-discovery', + SIDECAR_SYNC = 'sidecar-sync', + SIDECAR_WRITE = 'sidecar-write', +} + +export const JOBS_ASSET_PAGINATION_SIZE = 1000; + +export interface IBaseJob { + force?: boolean; +} + +export interface IEntityJob extends IBaseJob { + id: string; + source?: 'upload' | 'sidecar-write'; +} + +export interface IAssetDeletionJob extends IEntityJob { + fromExternal?: boolean; +} + +export interface ILibraryFileJob extends IEntityJob { + ownerId: string; + assetPath: string; +} + +export interface ILibraryRefreshJob extends IEntityJob { + refreshModifiedFiles: boolean; + refreshAllFiles: boolean; +} + +export interface IBulkEntityJob extends IBaseJob { + ids: string[]; +} + +export interface IDeleteFilesJob extends IBaseJob { + files: Array; +} + +export interface ISidecarWriteJob extends IEntityJob { + description?: string; + dateTimeOriginal?: string; + latitude?: number; + longitude?: number; +} + +export interface IDeferrableJob extends IEntityJob { + deferred?: boolean; +} export interface JobCounts { active: number; diff --git a/server/src/interfaces/library.repository.ts b/server/src/interfaces/library.interface.ts similarity index 92% rename from server/src/interfaces/library.repository.ts rename to server/src/interfaces/library.interface.ts index 5638d6024..dbc7fab81 100644 --- a/server/src/interfaces/library.repository.ts +++ b/server/src/interfaces/library.interface.ts @@ -1,4 +1,4 @@ -import { LibraryStatsResponseDto } from 'src/domain/library/library.dto'; +import { LibraryStatsResponseDto } from 'src/dtos/library.dto'; import { LibraryEntity, LibraryType } from 'src/entities/library.entity'; export const ILibraryRepository = 'ILibraryRepository'; diff --git a/server/src/interfaces/machine-learning.repository.ts b/server/src/interfaces/machine-learning.interface.ts similarity index 90% rename from server/src/interfaces/machine-learning.repository.ts rename to server/src/interfaces/machine-learning.interface.ts index d11e2e8f7..0aeed7635 100644 --- a/server/src/interfaces/machine-learning.repository.ts +++ b/server/src/interfaces/machine-learning.interface.ts @@ -1,4 +1,4 @@ -import { CLIPConfig, RecognitionConfig } from 'src/domain/smart-info/dto/model-config.dto'; +import { CLIPConfig, RecognitionConfig } from 'src/dtos/model-config.dto'; export const IMachineLearningRepository = 'IMachineLearningRepository'; diff --git a/server/src/interfaces/media.repository.ts b/server/src/interfaces/media.interface.ts similarity index 100% rename from server/src/interfaces/media.repository.ts rename to server/src/interfaces/media.interface.ts diff --git a/server/src/interfaces/metadata.repository.ts b/server/src/interfaces/metadata.interface.ts similarity index 100% rename from server/src/interfaces/metadata.repository.ts rename to server/src/interfaces/metadata.interface.ts diff --git a/server/src/interfaces/move.repository.ts b/server/src/interfaces/move.interface.ts similarity index 100% rename from server/src/interfaces/move.repository.ts rename to server/src/interfaces/move.interface.ts diff --git a/server/src/interfaces/partner.repository.ts b/server/src/interfaces/partner.interface.ts similarity index 100% rename from server/src/interfaces/partner.repository.ts rename to server/src/interfaces/partner.interface.ts diff --git a/server/src/interfaces/person.repository.ts b/server/src/interfaces/person.interface.ts similarity index 97% rename from server/src/interfaces/person.repository.ts rename to server/src/interfaces/person.interface.ts index cba11fbb9..382bbda22 100644 --- a/server/src/interfaces/person.repository.ts +++ b/server/src/interfaces/person.interface.ts @@ -1,7 +1,7 @@ import { AssetFaceEntity } from 'src/entities/asset-face.entity'; import { AssetEntity } from 'src/entities/asset.entity'; import { PersonEntity } from 'src/entities/person.entity'; -import { Paginated, PaginationOptions } from 'src/utils'; +import { Paginated, PaginationOptions } from 'src/utils/pagination'; import { FindManyOptions, FindOptionsRelations, FindOptionsSelect } from 'typeorm'; export const IPersonRepository = 'IPersonRepository'; diff --git a/server/src/interfaces/search.repository.ts b/server/src/interfaces/search.interface.ts similarity index 98% rename from server/src/interfaces/search.repository.ts rename to server/src/interfaces/search.interface.ts index 29eaa32b7..a4be05549 100644 --- a/server/src/interfaces/search.repository.ts +++ b/server/src/interfaces/search.interface.ts @@ -2,7 +2,7 @@ import { AssetFaceEntity } from 'src/entities/asset-face.entity'; import { AssetEntity, AssetType } from 'src/entities/asset.entity'; import { GeodataPlacesEntity } from 'src/entities/geodata-places.entity'; import { SmartInfoEntity } from 'src/entities/smart-info.entity'; -import { Paginated } from 'src/utils'; +import { Paginated } from 'src/utils/pagination'; export const ISearchRepository = 'ISearchRepository'; diff --git a/server/src/interfaces/server-info.repository.ts b/server/src/interfaces/server-info.interface.ts similarity index 100% rename from server/src/interfaces/server-info.repository.ts rename to server/src/interfaces/server-info.interface.ts diff --git a/server/src/interfaces/shared-link.repository.ts b/server/src/interfaces/shared-link.interface.ts similarity index 100% rename from server/src/interfaces/shared-link.repository.ts rename to server/src/interfaces/shared-link.interface.ts diff --git a/server/src/interfaces/storage.repository.ts b/server/src/interfaces/storage.interface.ts similarity index 96% rename from server/src/interfaces/storage.repository.ts rename to server/src/interfaces/storage.interface.ts index 505e535b0..e78bb0195 100644 --- a/server/src/interfaces/storage.repository.ts +++ b/server/src/interfaces/storage.interface.ts @@ -2,7 +2,7 @@ import { WatchOptions } from 'chokidar'; import { Stats } from 'node:fs'; import { FileReadOptions } from 'node:fs/promises'; import { Readable } from 'node:stream'; -import { CrawlOptionsDto } from 'src/domain/library/library.dto'; +import { CrawlOptionsDto } from 'src/dtos/library.dto'; export interface ImmichReadStream { stream: Readable; diff --git a/server/src/interfaces/system-config.repository.ts b/server/src/interfaces/system-config.interface.ts similarity index 100% rename from server/src/interfaces/system-config.repository.ts rename to server/src/interfaces/system-config.interface.ts diff --git a/server/src/interfaces/system-metadata.repository.ts b/server/src/interfaces/system-metadata.interface.ts similarity index 100% rename from server/src/interfaces/system-metadata.repository.ts rename to server/src/interfaces/system-metadata.interface.ts diff --git a/server/src/interfaces/tag.repository.ts b/server/src/interfaces/tag.interface.ts similarity index 100% rename from server/src/interfaces/tag.repository.ts rename to server/src/interfaces/tag.interface.ts diff --git a/server/src/interfaces/user-token.repository.ts b/server/src/interfaces/user-token.interface.ts similarity index 100% rename from server/src/interfaces/user-token.repository.ts rename to server/src/interfaces/user-token.interface.ts diff --git a/server/src/interfaces/user.repository.ts b/server/src/interfaces/user.interface.ts similarity index 100% rename from server/src/interfaces/user.repository.ts rename to server/src/interfaces/user.interface.ts diff --git a/server/src/main.ts b/server/src/main.ts index ee60c793c..3a9303868 100644 --- a/server/src/main.ts +++ b/server/src/main.ts @@ -1,6 +1,75 @@ -import { bootstrapApi } from 'src/apps/api.main'; -import { bootstrapImmichAdmin } from 'src/apps/immich-admin.main'; -import { bootstrapMicroservices } from 'src/apps/microservices.main'; +import { NestFactory } from '@nestjs/core'; +import { NestExpressApplication } from '@nestjs/platform-express'; +import { json } from 'body-parser'; +import cookieParser from 'cookie-parser'; +import { CommandFactory } from 'nest-commander'; +import { existsSync } from 'node:fs'; +import sirv from 'sirv'; +import { ApiModule, ImmichAdminModule, MicroservicesModule } from 'src/app.module'; +import { WEB_ROOT, envName, excludePaths, isDev, serverVersion } from 'src/constants'; +import { LogLevel } from 'src/entities/system-config.entity'; +import { WebSocketAdapter } from 'src/middleware/websocket.adapter'; +import { ApiService } from 'src/services/api.service'; +import { otelSDK } from 'src/utils/instrumentation'; +import { ImmichLogger } from 'src/utils/logger'; +import { useSwagger } from 'src/utils/misc'; + +async function bootstrapMicroservices() { + const logger = new ImmichLogger('ImmichMicroservice'); + const port = Number(process.env.MICROSERVICES_PORT) || 3002; + + 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}] `); +} + +async function bootstrapApi() { + const logger = new ImmichLogger('ImmichServer'); + const port = Number(process.env.SERVER_PORT) || 3001; + + otelSDK.start(); + const app = await NestFactory.create(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}] `); +} const immichApp = process.argv[2] || process.env.IMMICH_APP; @@ -8,6 +77,11 @@ if (process.argv[2] === immichApp) { process.argv.splice(2, 1); } +async function bootstrapImmichAdmin() { + process.env.LOG_LEVEL = LogLevel.WARN; + await CommandFactory.run(ImmichAdminModule); +} + function bootstrap() { switch (immichApp) { case 'immich': { @@ -23,8 +97,9 @@ function bootstrap() { return bootstrapImmichAdmin(); } default: { - throw new Error(`Invalid app name: ${immichApp}. Expected one of immich|microservices|cli`); + throw new Error(`Invalid app name: ${immichApp}. Expected one of immich|microservices|immich-admin`); } } } + void bootstrap(); diff --git a/server/src/middleware/auth.guard.ts b/server/src/middleware/auth.guard.ts index 070bf15e8..eaa47d013 100644 --- a/server/src/middleware/auth.guard.ts +++ b/server/src/middleware/auth.guard.ts @@ -9,10 +9,10 @@ import { import { Reflector } from '@nestjs/core'; import { ApiBearerAuth, ApiCookieAuth, ApiOkResponse, ApiQuery, ApiSecurity } from '@nestjs/swagger'; import { Request } from 'express'; -import { IMMICH_API_KEY_NAME } from 'src/domain/auth/auth.constant'; -import { AuthDto } from 'src/domain/auth/auth.dto'; -import { AuthService, LoginDetails } from 'src/domain/auth/auth.service'; -import { ImmichLogger } from 'src/infra/logger'; +import { IMMICH_API_KEY_NAME } from 'src/constants'; +import { AuthDto } from 'src/dtos/auth.dto'; +import { AuthService, LoginDetails } from 'src/services/auth.service'; +import { ImmichLogger } from 'src/utils/logger'; import { UAParser } from 'ua-parser-js'; export enum Metadata { diff --git a/server/src/middleware/error.interceptor.ts b/server/src/middleware/error.interceptor.ts index 70c129922..9e2273b97 100644 --- a/server/src/middleware/error.interceptor.ts +++ b/server/src/middleware/error.interceptor.ts @@ -7,9 +7,8 @@ import { NestInterceptor, } from '@nestjs/common'; import { Observable, catchError, throwError } from 'rxjs'; -import { routeToErrorMessage } from 'src/immich/app.utils'; -import { ImmichLogger } from 'src/infra/logger'; -import { isConnectionAborted } from 'src/utils'; +import { ImmichLogger } from 'src/utils/logger'; +import { isConnectionAborted, routeToErrorMessage } from 'src/utils/misc'; @Injectable() export class ErrorInterceptor implements NestInterceptor { diff --git a/server/src/middleware/file-upload.interceptor.ts b/server/src/middleware/file-upload.interceptor.ts index a7598f99d..53acbefa8 100644 --- a/server/src/middleware/file-upload.interceptor.ts +++ b/server/src/middleware/file-upload.interceptor.ts @@ -6,9 +6,10 @@ import { NextFunction, RequestHandler } from 'express'; import multer, { StorageEngine, diskStorage } from 'multer'; import { createHash, randomUUID } from 'node:crypto'; import { Observable } from 'rxjs'; -import { AssetService, UploadFieldName, UploadFile } from 'src/domain/asset/asset.service'; -import { ImmichLogger } from 'src/infra/logger'; +import { UploadFieldName } from 'src/dtos/asset.dto'; import { AuthRequest } from 'src/middleware/auth.guard'; +import { AssetService, UploadFile } from 'src/services/asset.service'; +import { ImmichLogger } from 'src/utils/logger'; export enum Route { ASSET = 'asset', diff --git a/server/src/infra/websocket.adapter.ts b/server/src/middleware/websocket.adapter.ts similarity index 100% rename from server/src/infra/websocket.adapter.ts rename to server/src/middleware/websocket.adapter.ts diff --git a/server/src/migrations/1700713871511-UsePgVectors.ts b/server/src/migrations/1700713871511-UsePgVectors.ts index 46d5320ce..75c85e3e0 100644 --- a/server/src/migrations/1700713871511-UsePgVectors.ts +++ b/server/src/migrations/1700713871511-UsePgVectors.ts @@ -1,5 +1,5 @@ -import { getCLIPModelInfo } from 'src/domain/smart-info/smart-info.constant'; -import { vectorExt } from 'src/infra/database.config'; +import { vectorExt } from 'src/database.config'; +import { getCLIPModelInfo } from 'src/utils/misc'; import { MigrationInterface, QueryRunner } from 'typeorm'; export class UsePgVectors1700713871511 implements MigrationInterface { diff --git a/server/src/migrations/1700713994428-AddCLIPEmbeddingIndex.ts b/server/src/migrations/1700713994428-AddCLIPEmbeddingIndex.ts index b045ba62b..908ebdb8f 100644 --- a/server/src/migrations/1700713994428-AddCLIPEmbeddingIndex.ts +++ b/server/src/migrations/1700713994428-AddCLIPEmbeddingIndex.ts @@ -1,5 +1,5 @@ -import { vectorExt } from 'src/infra/database.config'; -import { DatabaseExtension } from 'src/interfaces/database.repository'; +import { vectorExt } from 'src/database.config'; +import { DatabaseExtension } from 'src/interfaces/database.interface'; import { MigrationInterface, QueryRunner } from 'typeorm'; export class AddCLIPEmbeddingIndex1700713994428 implements MigrationInterface { diff --git a/server/src/migrations/1700714033632-AddFaceEmbeddingIndex.ts b/server/src/migrations/1700714033632-AddFaceEmbeddingIndex.ts index e77ce3b0b..75bebfa8e 100644 --- a/server/src/migrations/1700714033632-AddFaceEmbeddingIndex.ts +++ b/server/src/migrations/1700714033632-AddFaceEmbeddingIndex.ts @@ -1,5 +1,5 @@ -import { vectorExt } from 'src/infra/database.config'; -import { DatabaseExtension } from 'src/interfaces/database.repository'; +import { vectorExt } from 'src/database.config'; +import { DatabaseExtension } from 'src/interfaces/database.interface'; import { MigrationInterface, QueryRunner } from 'typeorm'; export class AddFaceEmbeddingIndex1700714033632 implements MigrationInterface { diff --git a/server/src/infra/sql/access.repository.sql b/server/src/queries/access.repository.sql similarity index 100% rename from server/src/infra/sql/access.repository.sql rename to server/src/queries/access.repository.sql diff --git a/server/src/infra/sql/album.repository.sql b/server/src/queries/album.repository.sql similarity index 100% rename from server/src/infra/sql/album.repository.sql rename to server/src/queries/album.repository.sql diff --git a/server/src/infra/sql/api.key.repository.sql b/server/src/queries/api.key.repository.sql similarity index 100% rename from server/src/infra/sql/api.key.repository.sql rename to server/src/queries/api.key.repository.sql diff --git a/server/src/infra/sql/asset.repository.sql b/server/src/queries/asset.repository.sql similarity index 100% rename from server/src/infra/sql/asset.repository.sql rename to server/src/queries/asset.repository.sql diff --git a/server/src/infra/sql/audit.repository.sql b/server/src/queries/audit.repository.sql similarity index 100% rename from server/src/infra/sql/audit.repository.sql rename to server/src/queries/audit.repository.sql diff --git a/server/src/infra/sql/library.repository.sql b/server/src/queries/library.repository.sql similarity index 100% rename from server/src/infra/sql/library.repository.sql rename to server/src/queries/library.repository.sql diff --git a/server/src/infra/sql/move.repository.sql b/server/src/queries/move.repository.sql similarity index 100% rename from server/src/infra/sql/move.repository.sql rename to server/src/queries/move.repository.sql diff --git a/server/src/infra/sql/partner.repository.sql b/server/src/queries/partner.repository.sql similarity index 100% rename from server/src/infra/sql/partner.repository.sql rename to server/src/queries/partner.repository.sql diff --git a/server/src/infra/sql/person.repository.sql b/server/src/queries/person.repository.sql similarity index 100% rename from server/src/infra/sql/person.repository.sql rename to server/src/queries/person.repository.sql diff --git a/server/src/infra/sql/search.repository.sql b/server/src/queries/search.repository.sql similarity index 100% rename from server/src/infra/sql/search.repository.sql rename to server/src/queries/search.repository.sql diff --git a/server/src/infra/sql/shared.link.repository.sql b/server/src/queries/shared.link.repository.sql similarity index 100% rename from server/src/infra/sql/shared.link.repository.sql rename to server/src/queries/shared.link.repository.sql diff --git a/server/src/infra/sql/system.config.repository.sql b/server/src/queries/system.config.repository.sql similarity index 100% rename from server/src/infra/sql/system.config.repository.sql rename to server/src/queries/system.config.repository.sql diff --git a/server/src/infra/sql/system.metadata.repository.sql b/server/src/queries/system.metadata.repository.sql similarity index 100% rename from server/src/infra/sql/system.metadata.repository.sql rename to server/src/queries/system.metadata.repository.sql diff --git a/server/src/infra/sql/tag.repository.sql b/server/src/queries/tag.repository.sql similarity index 100% rename from server/src/infra/sql/tag.repository.sql rename to server/src/queries/tag.repository.sql diff --git a/server/src/infra/sql/user.repository.sql b/server/src/queries/user.repository.sql similarity index 100% rename from server/src/infra/sql/user.repository.sql rename to server/src/queries/user.repository.sql diff --git a/server/src/infra/sql/user.token.repository.sql b/server/src/queries/user.token.repository.sql similarity index 100% rename from server/src/infra/sql/user.token.repository.sql rename to server/src/queries/user.token.repository.sql diff --git a/server/src/repositories/access.repository.ts b/server/src/repositories/access.repository.ts index 418cf542f..37b5be0e8 100644 --- a/server/src/repositories/access.repository.ts +++ b/server/src/repositories/access.repository.ts @@ -9,8 +9,8 @@ import { PartnerEntity } from 'src/entities/partner.entity'; import { PersonEntity } from 'src/entities/person.entity'; import { SharedLinkEntity } from 'src/entities/shared-link.entity'; import { UserTokenEntity } from 'src/entities/user-token.entity'; -import { Instrumentation } from 'src/infra/instrumentation'; -import { IAccessRepository } from 'src/interfaces/access.repository'; +import { IAccessRepository } from 'src/interfaces/access.interface'; +import { Instrumentation } from 'src/utils/instrumentation'; import { Brackets, In, Repository } from 'typeorm'; type IActivityAccess = IAccessRepository['activity']; diff --git a/server/src/repositories/activity.repository.ts b/server/src/repositories/activity.repository.ts index bec4a4f17..475ad3b85 100644 --- a/server/src/repositories/activity.repository.ts +++ b/server/src/repositories/activity.repository.ts @@ -2,8 +2,8 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { DummyValue, GenerateSql } from 'src/decorators'; import { ActivityEntity } from 'src/entities/activity.entity'; -import { Instrumentation } from 'src/infra/instrumentation'; -import { IActivityRepository } from 'src/interfaces/activity.repository'; +import { IActivityRepository } from 'src/interfaces/activity.interface'; +import { Instrumentation } from 'src/utils/instrumentation'; import { IsNull, Repository } from 'typeorm'; export interface ActivitySearch { diff --git a/server/src/repositories/album.repository.ts b/server/src/repositories/album.repository.ts index 4646ef3d9..f98be2161 100644 --- a/server/src/repositories/album.repository.ts +++ b/server/src/repositories/album.repository.ts @@ -1,19 +1,19 @@ import { Injectable } from '@nestjs/common'; import { InjectDataSource, InjectRepository } from '@nestjs/typeorm'; import _ from 'lodash'; +import { dataSource } from 'src/database.config'; import { Chunked, ChunkedArray, DATABASE_PARAMETER_CHUNK_SIZE, DummyValue, GenerateSql } from 'src/decorators'; import { AlbumEntity } from 'src/entities/album.entity'; import { AssetEntity } from 'src/entities/asset.entity'; -import { dataSource } from 'src/infra/database.config'; -import { Instrumentation } from 'src/infra/instrumentation'; import { AlbumAsset, AlbumAssetCount, AlbumAssets, AlbumInfoOptions, IAlbumRepository, -} from 'src/interfaces/album.repository'; -import { setUnion } from 'src/utils'; +} from 'src/interfaces/album.interface'; +import { Instrumentation } from 'src/utils/instrumentation'; +import { setUnion } from 'src/utils/set'; import { DataSource, FindOptionsOrder, FindOptionsRelations, In, IsNull, Not, Repository } from 'typeorm'; @Instrumentation() diff --git a/server/src/repositories/api-key.repository.ts b/server/src/repositories/api-key.repository.ts index 14a7db9c9..d03d04806 100644 --- a/server/src/repositories/api-key.repository.ts +++ b/server/src/repositories/api-key.repository.ts @@ -2,8 +2,8 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { DummyValue, GenerateSql } from 'src/decorators'; import { APIKeyEntity } from 'src/entities/api-key.entity'; -import { Instrumentation } from 'src/infra/instrumentation'; -import { IKeyRepository } from 'src/interfaces/api-key.repository'; +import { IKeyRepository } from 'src/interfaces/api-key.interface'; +import { Instrumentation } from 'src/utils/instrumentation'; import { Repository } from 'typeorm'; @Instrumentation() diff --git a/server/src/repositories/asset-stack.repository.ts b/server/src/repositories/asset-stack.repository.ts index 9859c5fad..660dfbe47 100644 --- a/server/src/repositories/asset-stack.repository.ts +++ b/server/src/repositories/asset-stack.repository.ts @@ -1,8 +1,8 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { AssetStackEntity } from 'src/entities/asset-stack.entity'; -import { Instrumentation } from 'src/infra/instrumentation'; -import { IAssetStackRepository } from 'src/interfaces/asset-stack.repository'; +import { IAssetStackRepository } from 'src/interfaces/asset-stack.interface'; +import { Instrumentation } from 'src/utils/instrumentation'; import { Repository } from 'typeorm'; @Instrumentation() diff --git a/server/src/immich/api-v1/asset/asset-repository.ts b/server/src/repositories/asset-v1.repository.ts similarity index 72% rename from server/src/immich/api-v1/asset/asset-repository.ts rename to server/src/repositories/asset-v1.repository.ts index e7b870239..6f53d820c 100644 --- a/server/src/immich/api-v1/asset/asset-repository.ts +++ b/server/src/repositories/asset-v1.repository.ts @@ -1,43 +1,16 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; +import { CuratedLocationsResponseDto, CuratedObjectsResponseDto } from 'src/dtos/asset-v1-response.dto'; +import { AssetSearchDto, CheckExistingAssetsDto, SearchPropertiesDto } from 'src/dtos/asset-v1.dto'; import { AssetEntity } from 'src/entities/asset.entity'; -import { ExifEntity } from 'src/entities/exif.entity'; -import { AssetSearchDto } from 'src/immich/api-v1/asset/dto/asset-search.dto'; -import { CheckExistingAssetsDto } from 'src/immich/api-v1/asset/dto/check-existing-assets.dto'; -import { SearchPropertiesDto } from 'src/immich/api-v1/asset/dto/search-properties.dto'; -import { CuratedLocationsResponseDto } from 'src/immich/api-v1/asset/response-dto/curated-locations-response.dto'; -import { CuratedObjectsResponseDto } from 'src/immich/api-v1/asset/response-dto/curated-objects-response.dto'; -import { OptionalBetween } from 'src/infra/infra.utils'; +import { AssetCheck, AssetOwnerCheck, IAssetRepositoryV1 } from 'src/interfaces/asset-v1.interface'; +import { OptionalBetween } from 'src/utils/database'; import { In } from 'typeorm/find-options/operator/In.js'; import { Repository } from 'typeorm/repository/Repository.js'; -export interface AssetCheck { - id: string; - checksum: Buffer; -} - -export interface AssetOwnerCheck extends AssetCheck { - ownerId: string; -} - -export interface IAssetRepositoryV1 { - get(id: string): Promise; - getLocationsByUserId(userId: string): Promise; - getDetectedObjectsByUserId(userId: string): Promise; - getAllByUserId(userId: string, dto: AssetSearchDto): Promise; - getSearchPropertiesByUserId(userId: string): Promise; - getAssetsByChecksums(userId: string, checksums: Buffer[]): Promise; - getExistingAssets(userId: string, checkDuplicateAssetDto: CheckExistingAssetsDto): Promise; - getByOriginalPath(originalPath: string): Promise; -} - -export const IAssetRepositoryV1 = 'IAssetRepositoryV1'; @Injectable() export class AssetRepositoryV1 implements IAssetRepositoryV1 { - constructor( - @InjectRepository(AssetEntity) private assetRepository: Repository, - @InjectRepository(ExifEntity) private exifRepository: Repository, - ) {} + constructor(@InjectRepository(AssetEntity) private assetRepository: Repository) {} /** * Retrieves all assets by user ID. diff --git a/server/src/repositories/asset.repository.ts b/server/src/repositories/asset.repository.ts index 2ffdf8dd2..68b786cbf 100644 --- a/server/src/repositories/asset.repository.ts +++ b/server/src/repositories/asset.repository.ts @@ -8,8 +8,6 @@ import { AssetJobStatusEntity } from 'src/entities/asset-job-status.entity'; import { AssetEntity, AssetType } from 'src/entities/asset.entity'; import { ExifEntity } from 'src/entities/exif.entity'; import { SmartInfoEntity } from 'src/entities/smart-info.entity'; -import { OptionalBetween, paginate, paginatedBuilder, searchAssetBuilder } from 'src/infra/infra.utils'; -import { Instrumentation } from 'src/infra/instrumentation'; import { AssetBuilderOptions, AssetCreate, @@ -30,9 +28,11 @@ import { TimeBucketSize, WithProperty, WithoutProperty, -} from 'src/interfaces/asset.repository'; -import { AssetSearchOptions, SearchExploreItem } from 'src/interfaces/search.repository'; -import { Paginated, PaginationMode, PaginationOptions } from 'src/utils'; +} from 'src/interfaces/asset.interface'; +import { AssetSearchOptions, SearchExploreItem } from 'src/interfaces/search.interface'; +import { OptionalBetween, searchAssetBuilder } from 'src/utils/database'; +import { Instrumentation } from 'src/utils/instrumentation'; +import { Paginated, PaginationMode, PaginationOptions, paginate, paginatedBuilder } from 'src/utils/pagination'; import { Brackets, FindOptionsRelations, diff --git a/server/src/repositories/audit.repository.ts b/server/src/repositories/audit.repository.ts index 1d892dab2..50f5631f3 100644 --- a/server/src/repositories/audit.repository.ts +++ b/server/src/repositories/audit.repository.ts @@ -1,7 +1,7 @@ import { InjectRepository } from '@nestjs/typeorm'; import { AuditEntity } from 'src/entities/audit.entity'; -import { Instrumentation } from 'src/infra/instrumentation'; -import { AuditSearch, IAuditRepository } from 'src/interfaces/audit.repository'; +import { AuditSearch, IAuditRepository } from 'src/interfaces/audit.interface'; +import { Instrumentation } from 'src/utils/instrumentation'; import { LessThan, MoreThan, Repository } from 'typeorm'; @Instrumentation() diff --git a/server/src/repositories/communication.repository.ts b/server/src/repositories/communication.repository.ts index e92fe1387..046190e1a 100644 --- a/server/src/repositories/communication.repository.ts +++ b/server/src/repositories/communication.repository.ts @@ -7,9 +7,6 @@ import { WebSocketServer, } from '@nestjs/websockets'; import { Server, Socket } from 'socket.io'; -import { AuthService } from 'src/domain/auth/auth.service'; -import { Instrumentation } from 'src/infra/instrumentation'; -import { ImmichLogger } from 'src/infra/logger'; import { ClientEvent, ICommunicationRepository, @@ -17,7 +14,10 @@ import { OnConnectCallback, OnServerEventCallback, ServerEvent, -} from 'src/interfaces/communication.repository'; +} from 'src/interfaces/communication.interface'; +import { AuthService } from 'src/services/auth.service'; +import { Instrumentation } from 'src/utils/instrumentation'; +import { ImmichLogger } from 'src/utils/logger'; @Instrumentation() @WebSocketGateway({ diff --git a/server/src/repositories/crypto.repository.ts b/server/src/repositories/crypto.repository.ts index 121943af3..84b74052c 100644 --- a/server/src/repositories/crypto.repository.ts +++ b/server/src/repositories/crypto.repository.ts @@ -2,8 +2,8 @@ import { Injectable } from '@nestjs/common'; import { compareSync, hash } from 'bcrypt'; import { createHash, randomBytes, randomUUID } from 'node:crypto'; import { createReadStream } from 'node:fs'; -import { Instrumentation } from 'src/infra/instrumentation'; -import { ICryptoRepository } from 'src/interfaces/crypto.repository'; +import { ICryptoRepository } from 'src/interfaces/crypto.interface'; +import { Instrumentation } from 'src/utils/instrumentation'; @Instrumentation() @Injectable() diff --git a/server/src/repositories/database.repository.ts b/server/src/repositories/database.repository.ts index 60586edc5..4ff24eeaa 100644 --- a/server/src/repositories/database.repository.ts +++ b/server/src/repositories/database.repository.ts @@ -1,10 +1,7 @@ import { Injectable } from '@nestjs/common'; import { InjectDataSource } from '@nestjs/typeorm'; import AsyncLock from 'async-lock'; -import { Version, VersionType } from 'src/domain/domain.constant'; -import { vectorExt } from 'src/infra/database.config'; -import { Instrumentation } from 'src/infra/instrumentation'; -import { ImmichLogger } from 'src/infra/logger'; +import { vectorExt } from 'src/database.config'; import { DatabaseExtension, DatabaseLock, @@ -13,7 +10,10 @@ import { VectorIndex, VectorUpdateResult, extName, -} from 'src/interfaces/database.repository'; +} from 'src/interfaces/database.interface'; +import { Instrumentation } from 'src/utils/instrumentation'; +import { ImmichLogger } from 'src/utils/logger'; +import { Version, VersionType } from 'src/utils/version'; import { isValidInteger } from 'src/validation'; import { DataSource, EntityManager, QueryRunner } from 'typeorm'; diff --git a/server/src/repositories/filesystem.provider.spec.ts b/server/src/repositories/filesystem.provider.spec.ts index 56f54b036..c9790767c 100644 --- a/server/src/repositories/filesystem.provider.spec.ts +++ b/server/src/repositories/filesystem.provider.spec.ts @@ -1,5 +1,5 @@ import mockfs from 'mock-fs'; -import { CrawlOptionsDto } from 'src/domain/library/library.dto'; +import { CrawlOptionsDto } from 'src/dtos/library.dto'; import { FilesystemProvider } from 'src/repositories/filesystem.provider'; interface Test { diff --git a/server/src/repositories/filesystem.provider.ts b/server/src/repositories/filesystem.provider.ts index 2da49625e..b4f231120 100644 --- a/server/src/repositories/filesystem.provider.ts +++ b/server/src/repositories/filesystem.provider.ts @@ -4,10 +4,7 @@ import { glob, globStream } from 'fast-glob'; import { constants, createReadStream, existsSync, mkdirSync } from 'node:fs'; import fs from 'node:fs/promises'; import path from 'node:path'; -import { mimeTypes } from 'src/domain/domain.constant'; -import { CrawlOptionsDto } from 'src/domain/library/library.dto'; -import { Instrumentation } from 'src/infra/instrumentation'; -import { ImmichLogger } from 'src/infra/logger'; +import { CrawlOptionsDto } from 'src/dtos/library.dto'; import { DiskUsage, IStorageRepository, @@ -15,7 +12,10 @@ import { ImmichZipStream, StorageEventType, WatchEvents, -} from 'src/interfaces/storage.repository'; +} from 'src/interfaces/storage.interface'; +import { Instrumentation } from 'src/utils/instrumentation'; +import { ImmichLogger } from 'src/utils/logger'; +import { mimeTypes } from 'src/utils/mime-types'; @Instrumentation() export class FilesystemProvider implements IStorageRepository { diff --git a/server/src/repositories/job.repository.ts b/server/src/repositories/job.repository.ts index a7637e0e6..b55996ed0 100644 --- a/server/src/repositories/job.repository.ts +++ b/server/src/repositories/job.repository.ts @@ -6,10 +6,78 @@ import { Job, JobsOptions, Processor, Queue, Worker, WorkerOptions } from 'bullm import { CronJob, CronTime } from 'cron'; import { setTimeout } from 'node:timers/promises'; import { bullConfig } from 'src/config'; -import { JOBS_TO_QUEUE, JobName, QueueName } from 'src/domain/job/job.constants'; -import { Instrumentation } from 'src/infra/instrumentation'; -import { ImmichLogger } from 'src/infra/logger'; -import { IJobRepository, JobCounts, JobItem, QueueCleanType, QueueStatus } from 'src/interfaces/job.repository'; +import { + IJobRepository, + JobCounts, + JobItem, + JobName, + QueueCleanType, + QueueName, + QueueStatus, +} from 'src/interfaces/job.interface'; +import { Instrumentation } from 'src/utils/instrumentation'; +import { ImmichLogger } from 'src/utils/logger'; + +export const JOBS_TO_QUEUE: Record = { + // misc + [JobName.ASSET_DELETION]: QueueName.BACKGROUND_TASK, + [JobName.ASSET_DELETION_CHECK]: QueueName.BACKGROUND_TASK, + [JobName.USER_DELETE_CHECK]: QueueName.BACKGROUND_TASK, + [JobName.USER_DELETION]: QueueName.BACKGROUND_TASK, + [JobName.DELETE_FILES]: QueueName.BACKGROUND_TASK, + [JobName.CLEAN_OLD_AUDIT_LOGS]: QueueName.BACKGROUND_TASK, + [JobName.PERSON_CLEANUP]: QueueName.BACKGROUND_TASK, + [JobName.USER_SYNC_USAGE]: QueueName.BACKGROUND_TASK, + + // conversion + [JobName.QUEUE_VIDEO_CONVERSION]: QueueName.VIDEO_CONVERSION, + [JobName.VIDEO_CONVERSION]: QueueName.VIDEO_CONVERSION, + + // thumbnails + [JobName.QUEUE_GENERATE_THUMBNAILS]: QueueName.THUMBNAIL_GENERATION, + [JobName.GENERATE_JPEG_THUMBNAIL]: QueueName.THUMBNAIL_GENERATION, + [JobName.GENERATE_WEBP_THUMBNAIL]: QueueName.THUMBNAIL_GENERATION, + [JobName.GENERATE_THUMBHASH_THUMBNAIL]: QueueName.THUMBNAIL_GENERATION, + [JobName.GENERATE_PERSON_THUMBNAIL]: QueueName.THUMBNAIL_GENERATION, + + // metadata + [JobName.QUEUE_METADATA_EXTRACTION]: QueueName.METADATA_EXTRACTION, + [JobName.METADATA_EXTRACTION]: QueueName.METADATA_EXTRACTION, + [JobName.LINK_LIVE_PHOTOS]: QueueName.METADATA_EXTRACTION, + + // storage template + [JobName.STORAGE_TEMPLATE_MIGRATION]: QueueName.STORAGE_TEMPLATE_MIGRATION, + [JobName.STORAGE_TEMPLATE_MIGRATION_SINGLE]: QueueName.STORAGE_TEMPLATE_MIGRATION, + + // migration + [JobName.QUEUE_MIGRATION]: QueueName.MIGRATION, + [JobName.MIGRATE_ASSET]: QueueName.MIGRATION, + [JobName.MIGRATE_PERSON]: QueueName.MIGRATION, + + // facial recognition + [JobName.QUEUE_FACE_DETECTION]: QueueName.FACE_DETECTION, + [JobName.FACE_DETECTION]: QueueName.FACE_DETECTION, + [JobName.QUEUE_FACIAL_RECOGNITION]: QueueName.FACIAL_RECOGNITION, + [JobName.FACIAL_RECOGNITION]: QueueName.FACIAL_RECOGNITION, + + // smart search + [JobName.QUEUE_SMART_SEARCH]: QueueName.SMART_SEARCH, + [JobName.SMART_SEARCH]: QueueName.SMART_SEARCH, + + // XMP sidecars + [JobName.QUEUE_SIDECAR]: QueueName.SIDECAR, + [JobName.SIDECAR_DISCOVERY]: QueueName.SIDECAR, + [JobName.SIDECAR_SYNC]: QueueName.SIDECAR, + [JobName.SIDECAR_WRITE]: QueueName.SIDECAR, + + // Library management + [JobName.LIBRARY_SCAN_ASSET]: QueueName.LIBRARY, + [JobName.LIBRARY_SCAN]: QueueName.LIBRARY, + [JobName.LIBRARY_DELETE]: QueueName.LIBRARY, + [JobName.LIBRARY_REMOVE_OFFLINE]: QueueName.LIBRARY, + [JobName.LIBRARY_QUEUE_SCAN_ALL]: QueueName.LIBRARY, + [JobName.LIBRARY_QUEUE_CLEANUP]: QueueName.LIBRARY, +}; @Instrumentation() @Injectable() diff --git a/server/src/repositories/library.repository.ts b/server/src/repositories/library.repository.ts index 19ac40bd5..d53354e25 100644 --- a/server/src/repositories/library.repository.ts +++ b/server/src/repositories/library.repository.ts @@ -1,10 +1,10 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { DummyValue, GenerateSql } from 'src/decorators'; -import { LibraryStatsResponseDto } from 'src/domain/library/library.dto'; +import { LibraryStatsResponseDto } from 'src/dtos/library.dto'; import { LibraryEntity, LibraryType } from 'src/entities/library.entity'; -import { Instrumentation } from 'src/infra/instrumentation'; -import { ILibraryRepository } from 'src/interfaces/library.repository'; +import { ILibraryRepository } from 'src/interfaces/library.interface'; +import { Instrumentation } from 'src/utils/instrumentation'; import { IsNull, Not } from 'typeorm'; import { Repository } from 'typeorm/repository/Repository.js'; diff --git a/server/src/repositories/machine-learning.repository.ts b/server/src/repositories/machine-learning.repository.ts index 46d16dcd7..3d8f0cac1 100644 --- a/server/src/repositories/machine-learning.repository.ts +++ b/server/src/repositories/machine-learning.repository.ts @@ -1,7 +1,6 @@ import { Injectable } from '@nestjs/common'; import { readFile } from 'node:fs/promises'; -import { CLIPConfig, ModelConfig, RecognitionConfig } from 'src/domain/smart-info/dto/model-config.dto'; -import { Instrumentation } from 'src/infra/instrumentation'; +import { CLIPConfig, ModelConfig, RecognitionConfig } from 'src/dtos/model-config.dto'; import { CLIPMode, DetectFaceResult, @@ -9,7 +8,8 @@ import { ModelType, TextModelInput, VisionModelInput, -} from 'src/interfaces/machine-learning.repository'; +} from 'src/interfaces/machine-learning.interface'; +import { Instrumentation } from 'src/utils/instrumentation'; const errorPrefix = 'Machine learning request'; diff --git a/server/src/repositories/media.repository.ts b/server/src/repositories/media.repository.ts index d0f4332b4..52a538909 100644 --- a/server/src/repositories/media.repository.ts +++ b/server/src/repositories/media.repository.ts @@ -4,16 +4,16 @@ import { Writable } from 'node:stream'; import { promisify } from 'node:util'; import sharp from 'sharp'; import { Colorspace } from 'src/entities/system-config.entity'; -import { Instrumentation } from 'src/infra/instrumentation'; -import { ImmichLogger } from 'src/infra/logger'; import { CropOptions, IMediaRepository, ResizeOptions, TranscodeOptions, VideoInfo, -} from 'src/interfaces/media.repository'; -import { handlePromiseError } from 'src/utils'; +} from 'src/interfaces/media.interface'; +import { Instrumentation } from 'src/utils/instrumentation'; +import { ImmichLogger } from 'src/utils/logger'; +import { handlePromiseError } from 'src/utils/misc'; const probe = promisify(ffmpeg.ffprobe); sharp.concurrency(0); diff --git a/server/src/repositories/metadata.repository.ts b/server/src/repositories/metadata.repository.ts index 5da664f14..511023a8e 100644 --- a/server/src/repositories/metadata.repository.ts +++ b/server/src/repositories/metadata.repository.ts @@ -6,21 +6,15 @@ import { getName } from 'i18n-iso-countries'; import { createReadStream, existsSync } from 'node:fs'; import { readFile } from 'node:fs/promises'; import readLine from 'node:readline'; +import { citiesFile, geodataAdmin1Path, geodataAdmin2Path, geodataCities500Path, geodataDatePath } from 'src/constants'; import { DummyValue, GenerateSql } from 'src/decorators'; -import { - citiesFile, - geodataAdmin1Path, - geodataAdmin2Path, - geodataCities500Path, - geodataDatePath, -} from 'src/domain/domain.constant'; import { ExifEntity } from 'src/entities/exif.entity'; import { GeodataPlacesEntity } from 'src/entities/geodata-places.entity'; import { SystemMetadataKey } from 'src/entities/system-metadata.entity'; -import { Instrumentation } from 'src/infra/instrumentation'; -import { ImmichLogger } from 'src/infra/logger'; -import { GeoPoint, IMetadataRepository, ImmichTags, ReverseGeocodeResult } from 'src/interfaces/metadata.repository'; -import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.repository'; +import { GeoPoint, IMetadataRepository, ImmichTags, ReverseGeocodeResult } from 'src/interfaces/metadata.interface'; +import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; +import { Instrumentation } from 'src/utils/instrumentation'; +import { ImmichLogger } from 'src/utils/logger'; import { DataSource, QueryRunner, Repository } from 'typeorm'; import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity.js'; diff --git a/server/src/repositories/move.repository.ts b/server/src/repositories/move.repository.ts index e567bcca3..a8416ff0a 100644 --- a/server/src/repositories/move.repository.ts +++ b/server/src/repositories/move.repository.ts @@ -2,8 +2,8 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { DummyValue, GenerateSql } from 'src/decorators'; import { MoveEntity, PathType } from 'src/entities/move.entity'; -import { Instrumentation } from 'src/infra/instrumentation'; -import { IMoveRepository, MoveCreate } from 'src/interfaces/move.repository'; +import { IMoveRepository, MoveCreate } from 'src/interfaces/move.interface'; +import { Instrumentation } from 'src/utils/instrumentation'; import { Repository } from 'typeorm'; @Instrumentation() diff --git a/server/src/repositories/partner.repository.ts b/server/src/repositories/partner.repository.ts index 4c4bbc0ed..8465493b5 100644 --- a/server/src/repositories/partner.repository.ts +++ b/server/src/repositories/partner.repository.ts @@ -1,8 +1,8 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { PartnerEntity } from 'src/entities/partner.entity'; -import { Instrumentation } from 'src/infra/instrumentation'; -import { IPartnerRepository, PartnerIds } from 'src/interfaces/partner.repository'; +import { IPartnerRepository, PartnerIds } from 'src/interfaces/partner.interface'; +import { Instrumentation } from 'src/utils/instrumentation'; import { DeepPartial, Repository } from 'typeorm'; @Instrumentation() diff --git a/server/src/repositories/person.repository.ts b/server/src/repositories/person.repository.ts index 6a0008c3d..867d6b174 100644 --- a/server/src/repositories/person.repository.ts +++ b/server/src/repositories/person.repository.ts @@ -4,8 +4,6 @@ import { ChunkedArray, DummyValue, GenerateSql } from 'src/decorators'; import { AssetFaceEntity } from 'src/entities/asset-face.entity'; import { AssetEntity } from 'src/entities/asset.entity'; import { PersonEntity } from 'src/entities/person.entity'; -import { asVector, paginate } from 'src/infra/infra.utils'; -import { Instrumentation } from 'src/infra/instrumentation'; import { AssetFaceId, IPersonRepository, @@ -14,8 +12,10 @@ import { PersonSearchOptions, PersonStatistics, UpdateFacesData, -} from 'src/interfaces/person.repository'; -import { Paginated, PaginationOptions } from 'src/utils'; +} from 'src/interfaces/person.interface'; +import { asVector } from 'src/utils/database'; +import { Instrumentation } from 'src/utils/instrumentation'; +import { Paginated, PaginationOptions, paginate } from 'src/utils/pagination'; import { FindManyOptions, FindOptionsRelations, FindOptionsSelect, In, Repository } from 'typeorm'; @Instrumentation() diff --git a/server/src/repositories/search.repository.ts b/server/src/repositories/search.repository.ts index aefb3c933..a151c0750 100644 --- a/server/src/repositories/search.repository.ts +++ b/server/src/repositories/search.repository.ts @@ -1,17 +1,13 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; +import { vectorExt } from 'src/database.config'; import { DummyValue, GenerateSql } from 'src/decorators'; -import { getCLIPModelInfo } from 'src/domain/smart-info/smart-info.constant'; import { AssetFaceEntity } from 'src/entities/asset-face.entity'; import { AssetEntity, AssetType } from 'src/entities/asset.entity'; import { GeodataPlacesEntity } from 'src/entities/geodata-places.entity'; import { SmartInfoEntity } from 'src/entities/smart-info.entity'; import { SmartSearchEntity } from 'src/entities/smart-search.entity'; -import { vectorExt } from 'src/infra/database.config'; -import { asVector, paginatedBuilder, searchAssetBuilder } from 'src/infra/infra.utils'; -import { Instrumentation } from 'src/infra/instrumentation'; -import { ImmichLogger } from 'src/infra/logger'; -import { DatabaseExtension } from 'src/interfaces/database.repository'; +import { DatabaseExtension } from 'src/interfaces/database.interface'; import { AssetSearchOptions, Embedding, @@ -20,8 +16,12 @@ import { ISearchRepository, SearchPaginationOptions, SmartSearchOptions, -} from 'src/interfaces/search.repository'; -import { Paginated, PaginationMode, PaginationResult } from 'src/utils'; +} from 'src/interfaces/search.interface'; +import { asVector, searchAssetBuilder } from 'src/utils/database'; +import { Instrumentation } from 'src/utils/instrumentation'; +import { ImmichLogger } from 'src/utils/logger'; +import { getCLIPModelInfo } from 'src/utils/misc'; +import { Paginated, PaginationMode, PaginationResult, paginatedBuilder } from 'src/utils/pagination'; import { isValidInteger } from 'src/validation'; import { Repository, SelectQueryBuilder } from 'typeorm'; diff --git a/server/src/repositories/server-info.repository.ts b/server/src/repositories/server-info.repository.ts index fcef0a396..5f14a881c 100644 --- a/server/src/repositories/server-info.repository.ts +++ b/server/src/repositories/server-info.repository.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; -import { Instrumentation } from 'src/infra/instrumentation'; -import { GitHubRelease, IServerInfoRepository } from 'src/interfaces/server-info.repository'; +import { GitHubRelease, IServerInfoRepository } from 'src/interfaces/server-info.interface'; +import { Instrumentation } from 'src/utils/instrumentation'; @Instrumentation() @Injectable() diff --git a/server/src/repositories/shared-link.repository.ts b/server/src/repositories/shared-link.repository.ts index 968b6dd73..48dbb3ab9 100644 --- a/server/src/repositories/shared-link.repository.ts +++ b/server/src/repositories/shared-link.repository.ts @@ -2,8 +2,8 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { DummyValue, GenerateSql } from 'src/decorators'; import { SharedLinkEntity } from 'src/entities/shared-link.entity'; -import { Instrumentation } from 'src/infra/instrumentation'; -import { ISharedLinkRepository } from 'src/interfaces/shared-link.repository'; +import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface'; +import { Instrumentation } from 'src/utils/instrumentation'; import { Repository } from 'typeorm'; @Instrumentation() diff --git a/server/src/repositories/system-config.repository.ts b/server/src/repositories/system-config.repository.ts index af6296ce0..baa3218b0 100644 --- a/server/src/repositories/system-config.repository.ts +++ b/server/src/repositories/system-config.repository.ts @@ -2,8 +2,8 @@ import { InjectRepository } from '@nestjs/typeorm'; import { readFile } from 'node:fs/promises'; import { Chunked, DummyValue, GenerateSql } from 'src/decorators'; import { SystemConfigEntity } from 'src/entities/system-config.entity'; -import { Instrumentation } from 'src/infra/instrumentation'; -import { ISystemConfigRepository } from 'src/interfaces/system-config.repository'; +import { ISystemConfigRepository } from 'src/interfaces/system-config.interface'; +import { Instrumentation } from 'src/utils/instrumentation'; import { In, Repository } from 'typeorm'; @Instrumentation() diff --git a/server/src/repositories/system-metadata.repository.ts b/server/src/repositories/system-metadata.repository.ts index 978dba9bd..80936e46f 100644 --- a/server/src/repositories/system-metadata.repository.ts +++ b/server/src/repositories/system-metadata.repository.ts @@ -1,7 +1,7 @@ import { InjectRepository } from '@nestjs/typeorm'; import { SystemMetadata, SystemMetadataEntity } from 'src/entities/system-metadata.entity'; -import { Instrumentation } from 'src/infra/instrumentation'; -import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.repository'; +import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; +import { Instrumentation } from 'src/utils/instrumentation'; import { Repository } from 'typeorm'; @Instrumentation() diff --git a/server/src/repositories/tag.repository.ts b/server/src/repositories/tag.repository.ts index a2ae74878..6fa827906 100644 --- a/server/src/repositories/tag.repository.ts +++ b/server/src/repositories/tag.repository.ts @@ -2,8 +2,8 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { AssetEntity } from 'src/entities/asset.entity'; import { TagEntity } from 'src/entities/tag.entity'; -import { Instrumentation } from 'src/infra/instrumentation'; -import { ITagRepository } from 'src/interfaces/tag.repository'; +import { ITagRepository } from 'src/interfaces/tag.interface'; +import { Instrumentation } from 'src/utils/instrumentation'; import { Repository } from 'typeorm'; @Instrumentation() diff --git a/server/src/repositories/user-token.repository.ts b/server/src/repositories/user-token.repository.ts index 31d6d2190..cbf3a3e3b 100644 --- a/server/src/repositories/user-token.repository.ts +++ b/server/src/repositories/user-token.repository.ts @@ -2,8 +2,8 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { DummyValue, GenerateSql } from 'src/decorators'; import { UserTokenEntity } from 'src/entities/user-token.entity'; -import { Instrumentation } from 'src/infra/instrumentation'; -import { IUserTokenRepository } from 'src/interfaces/user-token.repository'; +import { IUserTokenRepository } from 'src/interfaces/user-token.interface'; +import { Instrumentation } from 'src/utils/instrumentation'; import { Repository } from 'typeorm'; @Instrumentation() diff --git a/server/src/repositories/user.repository.ts b/server/src/repositories/user.repository.ts index bd94e71a5..f0e00d049 100644 --- a/server/src/repositories/user.repository.ts +++ b/server/src/repositories/user.repository.ts @@ -3,13 +3,13 @@ import { InjectRepository } from '@nestjs/typeorm'; import { DummyValue, GenerateSql } from 'src/decorators'; import { AssetEntity } from 'src/entities/asset.entity'; import { UserEntity } from 'src/entities/user.entity'; -import { Instrumentation } from 'src/infra/instrumentation'; import { IUserRepository, UserFindOptions, UserListFilter, UserStatsQueryResponse, -} from 'src/interfaces/user.repository'; +} from 'src/interfaces/user.interface'; +import { Instrumentation } from 'src/utils/instrumentation'; import { IsNull, Not, Repository } from 'typeorm'; @Instrumentation() diff --git a/server/src/domain/activity/activity.spec.ts b/server/src/services/activity.service.spec.ts similarity index 96% rename from server/src/domain/activity/activity.spec.ts rename to server/src/services/activity.service.spec.ts index 467540be3..e7049ea6c 100644 --- a/server/src/domain/activity/activity.spec.ts +++ b/server/src/services/activity.service.spec.ts @@ -1,7 +1,7 @@ import { BadRequestException } from '@nestjs/common'; -import { ReactionType } from 'src/domain/activity/activity.dto'; -import { ActivityService } from 'src/domain/activity/activity.service'; -import { IActivityRepository } from 'src/interfaces/activity.repository'; +import { ReactionType } from 'src/dtos/activity.dto'; +import { IActivityRepository } from 'src/interfaces/activity.interface'; +import { ActivityService } from 'src/services/activity.service'; import { activityStub } from 'test/fixtures/activity.stub'; import { authStub } from 'test/fixtures/auth.stub'; import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock'; diff --git a/server/src/domain/activity/activity.service.ts b/server/src/services/activity.service.ts similarity index 91% rename from server/src/domain/activity/activity.service.ts rename to server/src/services/activity.service.ts index 81d77fa8d..7589fb8cc 100644 --- a/server/src/domain/activity/activity.service.ts +++ b/server/src/services/activity.service.ts @@ -10,11 +10,11 @@ import { ReactionLevel, ReactionType, mapActivity, -} from 'src/domain/activity/activity.dto'; -import { AuthDto } from 'src/domain/auth/auth.dto'; +} from 'src/dtos/activity.dto'; +import { AuthDto } from 'src/dtos/auth.dto'; import { ActivityEntity } from 'src/entities/activity.entity'; -import { IAccessRepository } from 'src/interfaces/access.repository'; -import { IActivityRepository } from 'src/interfaces/activity.repository'; +import { IAccessRepository } from 'src/interfaces/access.interface'; +import { IActivityRepository } from 'src/interfaces/activity.interface'; @Injectable() export class ActivityService { diff --git a/server/src/domain/album/album.service.spec.ts b/server/src/services/album.service.spec.ts similarity index 98% rename from server/src/domain/album/album.service.spec.ts rename to server/src/services/album.service.spec.ts index 5b3a91ecf..48462fac4 100644 --- a/server/src/domain/album/album.service.spec.ts +++ b/server/src/services/album.service.spec.ts @@ -1,10 +1,10 @@ import { BadRequestException } from '@nestjs/common'; import _ from 'lodash'; -import { AlbumService } from 'src/domain/album/album.service'; -import { BulkIdErrorReason } from 'src/domain/asset/response-dto/asset-ids-response.dto'; -import { IAlbumRepository } from 'src/interfaces/album.repository'; -import { IAssetRepository } from 'src/interfaces/asset.repository'; -import { IUserRepository } from 'src/interfaces/user.repository'; +import { BulkIdErrorReason } from 'src/dtos/asset-ids.response.dto'; +import { IAlbumRepository } from 'src/interfaces/album.interface'; +import { IAssetRepository } from 'src/interfaces/asset.interface'; +import { IUserRepository } from 'src/interfaces/user.interface'; +import { AlbumService } from 'src/services/album.service'; import { albumStub } from 'test/fixtures/album.stub'; import { authStub } from 'test/fixtures/auth.stub'; import { userStub } from 'test/fixtures/user.stub'; diff --git a/server/src/domain/album/album.service.ts b/server/src/services/album.service.ts similarity index 93% rename from server/src/domain/album/album.service.ts rename to server/src/services/album.service.ts index 4d459ea44..483ddc3b0 100644 --- a/server/src/domain/album/album.service.ts +++ b/server/src/services/album.service.ts @@ -1,27 +1,27 @@ import { BadRequestException, Inject, Injectable } from '@nestjs/common'; import { AccessCore, Permission } from 'src/cores/access.core'; import { + AddUsersDto, AlbumCountResponseDto, + AlbumInfoDto, AlbumResponseDto, + CreateAlbumDto, + GetAlbumsDto, + UpdateAlbumDto, mapAlbum, mapAlbumWithAssets, mapAlbumWithoutAssets, -} from 'src/domain/album/album-response.dto'; -import { AddUsersDto } from 'src/domain/album/dto/album-add-users.dto'; -import { CreateAlbumDto } from 'src/domain/album/dto/album-create.dto'; -import { UpdateAlbumDto } from 'src/domain/album/dto/album-update.dto'; -import { AlbumInfoDto } from 'src/domain/album/dto/album.dto'; -import { GetAlbumsDto } from 'src/domain/album/dto/get-albums.dto'; -import { BulkIdErrorReason, BulkIdResponseDto, BulkIdsDto } from 'src/domain/asset/response-dto/asset-ids-response.dto'; -import { AuthDto } from 'src/domain/auth/auth.dto'; +} from 'src/dtos/album.dto'; +import { BulkIdErrorReason, BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; +import { AuthDto } from 'src/dtos/auth.dto'; import { AlbumEntity } from 'src/entities/album.entity'; import { AssetEntity } from 'src/entities/asset.entity'; import { UserEntity } from 'src/entities/user.entity'; -import { IAccessRepository } from 'src/interfaces/access.repository'; -import { AlbumAssetCount, AlbumInfoOptions, IAlbumRepository } from 'src/interfaces/album.repository'; -import { IAssetRepository } from 'src/interfaces/asset.repository'; -import { IUserRepository } from 'src/interfaces/user.repository'; -import { setUnion } from 'src/utils'; +import { IAccessRepository } from 'src/interfaces/access.interface'; +import { AlbumAssetCount, AlbumInfoOptions, IAlbumRepository } from 'src/interfaces/album.interface'; +import { IAssetRepository } from 'src/interfaces/asset.interface'; +import { IUserRepository } from 'src/interfaces/user.interface'; +import { setUnion } from 'src/utils/set'; @Injectable() export class AlbumService { diff --git a/server/src/domain/api-key/api-key.service.spec.ts b/server/src/services/api-key.service.spec.ts similarity index 94% rename from server/src/domain/api-key/api-key.service.spec.ts rename to server/src/services/api-key.service.spec.ts index 0400c13a2..3c8463c8f 100644 --- a/server/src/domain/api-key/api-key.service.spec.ts +++ b/server/src/services/api-key.service.spec.ts @@ -1,7 +1,7 @@ import { BadRequestException } from '@nestjs/common'; -import { APIKeyService } from 'src/domain/api-key/api-key.service'; -import { IKeyRepository } from 'src/interfaces/api-key.repository'; -import { ICryptoRepository } from 'src/interfaces/crypto.repository'; +import { IKeyRepository } from 'src/interfaces/api-key.interface'; +import { ICryptoRepository } from 'src/interfaces/crypto.interface'; +import { APIKeyService } from 'src/services/api-key.service'; import { keyStub } from 'test/fixtures/api-key.stub'; import { authStub } from 'test/fixtures/auth.stub'; import { newKeyRepositoryMock } from 'test/repositories/api-key.repository.mock'; diff --git a/server/src/domain/api-key/api-key.service.ts b/server/src/services/api-key.service.ts similarity index 89% rename from server/src/domain/api-key/api-key.service.ts rename to server/src/services/api-key.service.ts index 540c1d71f..5de908b4d 100644 --- a/server/src/domain/api-key/api-key.service.ts +++ b/server/src/services/api-key.service.ts @@ -1,9 +1,9 @@ import { BadRequestException, Inject, Injectable } from '@nestjs/common'; -import { APIKeyCreateDto, APIKeyCreateResponseDto, APIKeyResponseDto } from 'src/domain/api-key/api-key.dto'; -import { AuthDto } from 'src/domain/auth/auth.dto'; +import { APIKeyCreateDto, APIKeyCreateResponseDto, APIKeyResponseDto } from 'src/dtos/api-key.dto'; +import { AuthDto } from 'src/dtos/auth.dto'; import { APIKeyEntity } from 'src/entities/api-key.entity'; -import { IKeyRepository } from 'src/interfaces/api-key.repository'; -import { ICryptoRepository } from 'src/interfaces/crypto.repository'; +import { IKeyRepository } from 'src/interfaces/api-key.interface'; +import { ICryptoRepository } from 'src/interfaces/crypto.interface'; @Injectable() export class APIKeyService { diff --git a/server/src/apps/api.service.ts b/server/src/services/api.service.ts similarity index 83% rename from server/src/apps/api.service.ts rename to server/src/services/api.service.ts index 31134cefa..87107b23f 100644 --- a/server/src/apps/api.service.ts +++ b/server/src/services/api.service.ts @@ -3,16 +3,16 @@ import { Cron, CronExpression, Interval } from '@nestjs/schedule'; import { NextFunction, Request, Response } from 'express'; import { readFileSync } from 'node:fs'; import { join } from 'node:path'; -import { AuthService } from 'src/domain/auth/auth.service'; -import { DatabaseService } from 'src/domain/database/database.service'; -import { ONE_HOUR, WEB_ROOT } from 'src/domain/domain.constant'; -import { JobService } from 'src/domain/job/job.service'; -import { ServerInfoService } from 'src/domain/server-info/server-info.service'; -import { SharedLinkService } from 'src/domain/shared-link/shared-link.service'; -import { StorageService } from 'src/domain/storage/storage.service'; -import { SystemConfigService } from 'src/domain/system-config/system-config.service'; -import { ImmichLogger } from 'src/infra/logger'; -import { OpenGraphTags } from 'src/utils'; +import { ONE_HOUR, WEB_ROOT } from 'src/constants'; +import { AuthService } from 'src/services/auth.service'; +import { DatabaseService } from 'src/services/database.service'; +import { JobService } from 'src/services/job.service'; +import { ServerInfoService } from 'src/services/server-info.service'; +import { SharedLinkService } from 'src/services/shared-link.service'; +import { StorageService } from 'src/services/storage.service'; +import { SystemConfigService } from 'src/services/system-config.service'; +import { ImmichLogger } from 'src/utils/logger'; +import { OpenGraphTags } from 'src/utils/misc'; const render = (index: string, meta: OpenGraphTags) => { const tags = ` diff --git a/server/src/immich/api-v1/asset/asset.service.spec.ts b/server/src/services/asset-v1.service.spec.ts similarity index 90% rename from server/src/immich/api-v1/asset/asset.service.spec.ts rename to server/src/services/asset-v1.service.spec.ts index d10590475..898fb5a99 100644 --- a/server/src/immich/api-v1/asset/asset.service.spec.ts +++ b/server/src/services/asset-v1.service.spec.ts @@ -1,16 +1,15 @@ import { when } from 'jest-when'; -import { JobName } from 'src/domain/job/job.constants'; +import { AssetRejectReason, AssetUploadAction } from 'src/dtos/asset-v1-response.dto'; +import { CreateAssetDto } from 'src/dtos/asset-v1.dto'; import { ASSET_CHECKSUM_CONSTRAINT, AssetEntity, AssetType } from 'src/entities/asset.entity'; import { ExifEntity } from 'src/entities/exif.entity'; -import { IAssetRepositoryV1 } from 'src/immich/api-v1/asset/asset-repository'; -import { AssetService } from 'src/immich/api-v1/asset/asset.service'; -import { CreateAssetDto } from 'src/immich/api-v1/asset/dto/create-asset.dto'; -import { AssetRejectReason, AssetUploadAction } from 'src/immich/api-v1/asset/response-dto/asset-check-response.dto'; -import { IAssetRepository } from 'src/interfaces/asset.repository'; -import { IJobRepository } from 'src/interfaces/job.repository'; -import { ILibraryRepository } from 'src/interfaces/library.repository'; -import { IStorageRepository } from 'src/interfaces/storage.repository'; -import { IUserRepository } from 'src/interfaces/user.repository'; +import { IAssetRepositoryV1 } from 'src/interfaces/asset-v1.interface'; +import { IAssetRepository } from 'src/interfaces/asset.interface'; +import { IJobRepository, JobName } from 'src/interfaces/job.interface'; +import { ILibraryRepository } from 'src/interfaces/library.interface'; +import { IStorageRepository } from 'src/interfaces/storage.interface'; +import { IUserRepository } from 'src/interfaces/user.interface'; +import { AssetServiceV1 } from 'src/services/asset-v1.service'; import { assetStub } from 'test/fixtures/asset.stub'; import { authStub } from 'test/fixtures/auth.stub'; import { fileStub } from 'test/fixtures/file.stub'; @@ -61,7 +60,7 @@ const _getAsset_1 = () => { }; describe('AssetService', () => { - let sut: AssetService; + let sut: AssetServiceV1; let accessMock: IAccessRepositoryMock; let assetRepositoryMockV1: jest.Mocked; let assetMock: jest.Mocked; @@ -89,7 +88,7 @@ describe('AssetService', () => { storageMock = newStorageRepositoryMock(); userMock = newUserRepositoryMock(); - sut = new AssetService(accessMock, assetRepositoryMockV1, assetMock, jobMock, libraryMock, storageMock, userMock); + sut = new AssetServiceV1(accessMock, assetRepositoryMockV1, assetMock, jobMock, libraryMock, storageMock, userMock); when(assetRepositoryMockV1.get) .calledWith(assetStub.livePhotoStillAsset.id) diff --git a/server/src/immich/api-v1/asset/asset.service.ts b/server/src/services/asset-v1.service.ts similarity index 84% rename from server/src/immich/api-v1/asset/asset.service.ts rename to server/src/services/asset-v1.service.ts index 0a5722690..a24ddbd69 100644 --- a/server/src/immich/api-v1/asset/asset.service.ts +++ b/server/src/services/asset-v1.service.ts @@ -6,42 +6,45 @@ import { NotFoundException, } from '@nestjs/common'; import { AccessCore, Permission } from 'src/cores/access.core'; -import { UploadFile } from 'src/domain/asset/asset.service'; -import { AssetResponseDto, mapAsset } from 'src/domain/asset/response-dto/asset-response.dto'; -import { AuthDto } from 'src/domain/auth/auth.dto'; -import { mimeTypes } from 'src/domain/domain.constant'; -import { JobName } from 'src/domain/job/job.constants'; -import { ASSET_CHECKSUM_CONSTRAINT, AssetEntity, AssetType } from 'src/entities/asset.entity'; -import { LibraryType } from 'src/entities/library.entity'; -import { IAssetRepositoryV1 } from 'src/immich/api-v1/asset/asset-repository'; -import { AssetBulkUploadCheckDto } from 'src/immich/api-v1/asset/dto/asset-check.dto'; -import { AssetSearchDto } from 'src/immich/api-v1/asset/dto/asset-search.dto'; -import { CheckExistingAssetsDto } from 'src/immich/api-v1/asset/dto/check-existing-assets.dto'; -import { CreateAssetDto } from 'src/immich/api-v1/asset/dto/create-asset.dto'; -import { GetAssetThumbnailDto, GetAssetThumbnailFormatEnum } from 'src/immich/api-v1/asset/dto/get-asset-thumbnail.dto'; -import { ServeFileDto } from 'src/immich/api-v1/asset/dto/serve-file.dto'; +import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto'; import { AssetBulkUploadCheckResponseDto, + AssetFileUploadResponseDto, AssetRejectReason, AssetUploadAction, -} from 'src/immich/api-v1/asset/response-dto/asset-check-response.dto'; -import { AssetFileUploadResponseDto } from 'src/immich/api-v1/asset/response-dto/asset-file-upload-response.dto'; -import { CheckExistingAssetsResponseDto } from 'src/immich/api-v1/asset/response-dto/check-existing-assets-response.dto'; -import { CuratedLocationsResponseDto } from 'src/immich/api-v1/asset/response-dto/curated-locations-response.dto'; -import { CuratedObjectsResponseDto } from 'src/immich/api-v1/asset/response-dto/curated-objects-response.dto'; -import { ImmichLogger } from 'src/infra/logger'; -import { IAccessRepository } from 'src/interfaces/access.repository'; -import { IAssetRepository } from 'src/interfaces/asset.repository'; -import { IJobRepository } from 'src/interfaces/job.repository'; -import { ILibraryRepository } from 'src/interfaces/library.repository'; -import { IStorageRepository } from 'src/interfaces/storage.repository'; -import { IUserRepository } from 'src/interfaces/user.repository'; -import { CacheControl, ImmichFileResponse, getLivePhotoMotionFilename } from 'src/utils'; + CheckExistingAssetsResponseDto, + CuratedLocationsResponseDto, + CuratedObjectsResponseDto, +} from 'src/dtos/asset-v1-response.dto'; +import { + AssetBulkUploadCheckDto, + AssetSearchDto, + CheckExistingAssetsDto, + CreateAssetDto, + GetAssetThumbnailDto, + GetAssetThumbnailFormatEnum, + ServeFileDto, +} from 'src/dtos/asset-v1.dto'; +import { AuthDto } from 'src/dtos/auth.dto'; +import { ASSET_CHECKSUM_CONSTRAINT, AssetEntity, AssetType } from 'src/entities/asset.entity'; +import { LibraryType } from 'src/entities/library.entity'; +import { IAccessRepository } from 'src/interfaces/access.interface'; +import { IAssetRepositoryV1 } from 'src/interfaces/asset-v1.interface'; +import { IAssetRepository } from 'src/interfaces/asset.interface'; +import { IJobRepository, JobName } from 'src/interfaces/job.interface'; +import { ILibraryRepository } from 'src/interfaces/library.interface'; +import { IStorageRepository } from 'src/interfaces/storage.interface'; +import { IUserRepository } from 'src/interfaces/user.interface'; +import { UploadFile } from 'src/services/asset.service'; +import { CacheControl, ImmichFileResponse, getLivePhotoMotionFilename } from 'src/utils/file'; +import { ImmichLogger } from 'src/utils/logger'; +import { mimeTypes } from 'src/utils/mime-types'; import { QueryFailedError } from 'typeorm'; @Injectable() -export class AssetService { - readonly logger = new ImmichLogger(AssetService.name); +/** @deprecated */ +export class AssetServiceV1 { + readonly logger = new ImmichLogger(AssetServiceV1.name); private access: AccessCore; constructor( diff --git a/server/src/domain/asset/asset.service.spec.ts b/server/src/services/asset.service.spec.ts similarity index 97% rename from server/src/domain/asset/asset.service.spec.ts rename to server/src/services/asset.service.spec.ts index 7b49cfef9..a8e30a388 100644 --- a/server/src/domain/asset/asset.service.spec.ts +++ b/server/src/services/asset.service.spec.ts @@ -1,19 +1,17 @@ import { BadRequestException, UnauthorizedException } from '@nestjs/common'; import { when } from 'jest-when'; -import { AssetService, UploadFieldName } from 'src/domain/asset/asset.service'; -import { AssetJobName } from 'src/domain/asset/dto/asset-ids.dto'; -import { AssetStatsResponseDto } from 'src/domain/asset/dto/asset-statistics.dto'; -import { mapAsset } from 'src/domain/asset/response-dto/asset-response.dto'; -import { JobName } from 'src/domain/job/job.constants'; +import { mapAsset } from 'src/dtos/asset-response.dto'; +import { AssetJobName, AssetStatsResponseDto, UploadFieldName } from 'src/dtos/asset.dto'; import { AssetEntity, AssetType } from 'src/entities/asset.entity'; -import { IAssetStackRepository } from 'src/interfaces/asset-stack.repository'; -import { AssetStats, IAssetRepository, TimeBucketSize } from 'src/interfaces/asset.repository'; -import { ClientEvent, ICommunicationRepository } from 'src/interfaces/communication.repository'; -import { IJobRepository, JobItem } from 'src/interfaces/job.repository'; -import { IPartnerRepository } from 'src/interfaces/partner.repository'; -import { IStorageRepository } from 'src/interfaces/storage.repository'; -import { ISystemConfigRepository } from 'src/interfaces/system-config.repository'; -import { IUserRepository } from 'src/interfaces/user.repository'; +import { IAssetStackRepository } from 'src/interfaces/asset-stack.interface'; +import { AssetStats, IAssetRepository, TimeBucketSize } from 'src/interfaces/asset.interface'; +import { ClientEvent, ICommunicationRepository } from 'src/interfaces/communication.interface'; +import { IJobRepository, JobItem, JobName } from 'src/interfaces/job.interface'; +import { IPartnerRepository } from 'src/interfaces/partner.interface'; +import { IStorageRepository } from 'src/interfaces/storage.interface'; +import { ISystemConfigRepository } from 'src/interfaces/system-config.interface'; +import { IUserRepository } from 'src/interfaces/user.interface'; +import { AssetService } from 'src/services/asset.service'; import { assetStackStub, assetStub } from 'test/fixtures/asset.stub'; import { authStub } from 'test/fixtures/auth.stub'; import { faceStub } from 'test/fixtures/face.stub'; diff --git a/server/src/domain/asset/asset.service.ts b/server/src/services/asset.service.ts similarity index 91% rename from server/src/domain/asset/asset.service.ts rename to server/src/services/asset.service.ts index 38446b576..7020d5061 100644 --- a/server/src/domain/asset/asset.service.ts +++ b/server/src/services/asset.service.ts @@ -6,45 +6,48 @@ import sanitize from 'sanitize-filename'; import { AccessCore, Permission } from 'src/cores/access.core'; import { StorageCore, StorageFolder } from 'src/cores/storage.core'; import { SystemConfigCore } from 'src/cores/system-config.core'; -import { AssetJobName, AssetJobsDto } from 'src/domain/asset/dto/asset-ids.dto'; -import { UpdateStackParentDto } from 'src/domain/asset/dto/asset-stack.dto'; -import { AssetStatsDto, mapStats } from 'src/domain/asset/dto/asset-statistics.dto'; -import { AssetBulkDeleteDto, AssetBulkUpdateDto, UpdateAssetDto } from 'src/domain/asset/dto/asset.dto'; -import { MapMarkerDto } from 'src/domain/asset/dto/map-marker.dto'; -import { MemoryLaneDto } from 'src/domain/asset/dto/memory-lane.dto'; -import { TimeBucketAssetDto, TimeBucketDto } from 'src/domain/asset/dto/time-bucket.dto'; import { AssetResponseDto, MemoryLaneResponseDto, SanitizedAssetResponseDto, mapAsset, -} from 'src/domain/asset/response-dto/asset-response.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 { mimeTypes } from 'src/domain/domain.constant'; -import { JOBS_ASSET_PAGINATION_SIZE, JobName } from 'src/domain/job/job.constants'; -import { IAssetDeletionJob, ISidecarWriteJob } from 'src/domain/job/job.interface'; +} from 'src/dtos/asset-response.dto'; +import { + AssetBulkDeleteDto, + AssetBulkUpdateDto, + AssetJobName, + AssetJobsDto, + AssetStatsDto, + UpdateAssetDto, + UploadFieldName, + mapStats, +} from 'src/dtos/asset.dto'; +import { AuthDto } from 'src/dtos/auth.dto'; +import { MapMarkerDto, MapMarkerResponseDto, MemoryLaneDto } from 'src/dtos/search.dto'; +import { UpdateStackParentDto } from 'src/dtos/stack.dto'; +import { TimeBucketAssetDto, TimeBucketDto, TimeBucketResponseDto } from 'src/dtos/time-bucket.dto'; import { AssetEntity } from 'src/entities/asset.entity'; import { LibraryType } from 'src/entities/library.entity'; -import { ImmichLogger } from 'src/infra/logger'; -import { IAccessRepository } from 'src/interfaces/access.repository'; -import { IAssetStackRepository } from 'src/interfaces/asset-stack.repository'; -import { IAssetRepository, TimeBucketOptions } from 'src/interfaces/asset.repository'; -import { ClientEvent, ICommunicationRepository } from 'src/interfaces/communication.repository'; -import { IJobRepository, JobItem, JobStatus } from 'src/interfaces/job.repository'; -import { IPartnerRepository } from 'src/interfaces/partner.repository'; -import { IStorageRepository } from 'src/interfaces/storage.repository'; -import { ISystemConfigRepository } from 'src/interfaces/system-config.repository'; -import { IUserRepository } from 'src/interfaces/user.repository'; -import { usePagination } from 'src/utils'; - -export enum UploadFieldName { - ASSET_DATA = 'assetData', - LIVE_PHOTO_DATA = 'livePhotoData', - SIDECAR_DATA = 'sidecarData', - PROFILE_DATA = 'file', -} +import { IAccessRepository } from 'src/interfaces/access.interface'; +import { IAssetStackRepository } from 'src/interfaces/asset-stack.interface'; +import { IAssetRepository, TimeBucketOptions } from 'src/interfaces/asset.interface'; +import { ClientEvent, ICommunicationRepository } from 'src/interfaces/communication.interface'; +import { + IAssetDeletionJob, + IJobRepository, + ISidecarWriteJob, + JOBS_ASSET_PAGINATION_SIZE, + JobItem, + JobName, + JobStatus, +} from 'src/interfaces/job.interface'; +import { IPartnerRepository } from 'src/interfaces/partner.interface'; +import { IStorageRepository } from 'src/interfaces/storage.interface'; +import { ISystemConfigRepository } from 'src/interfaces/system-config.interface'; +import { IUserRepository } from 'src/interfaces/user.interface'; +import { ImmichLogger } from 'src/utils/logger'; +import { mimeTypes } from 'src/utils/mime-types'; +import { usePagination } from 'src/utils/pagination'; export interface UploadRequest { auth: AuthDto | null; diff --git a/server/src/domain/audit/audit.service.spec.ts b/server/src/services/audit.service.spec.ts similarity index 85% rename from server/src/domain/audit/audit.service.spec.ts rename to server/src/services/audit.service.spec.ts index ecda57b08..4af5c1f94 100644 --- a/server/src/domain/audit/audit.service.spec.ts +++ b/server/src/services/audit.service.spec.ts @@ -1,12 +1,12 @@ -import { AuditService } from 'src/domain/audit/audit.service'; import { DatabaseAction, EntityType } from 'src/entities/audit.entity'; -import { IAssetRepository } from 'src/interfaces/asset.repository'; -import { IAuditRepository } from 'src/interfaces/audit.repository'; -import { ICryptoRepository } from 'src/interfaces/crypto.repository'; -import { JobStatus } from 'src/interfaces/job.repository'; -import { IPersonRepository } from 'src/interfaces/person.repository'; -import { IStorageRepository } from 'src/interfaces/storage.repository'; -import { IUserRepository } from 'src/interfaces/user.repository'; +import { IAssetRepository } from 'src/interfaces/asset.interface'; +import { IAuditRepository } from 'src/interfaces/audit.interface'; +import { ICryptoRepository } from 'src/interfaces/crypto.interface'; +import { JobStatus } from 'src/interfaces/job.interface'; +import { IPersonRepository } from 'src/interfaces/person.interface'; +import { IStorageRepository } from 'src/interfaces/storage.interface'; +import { IUserRepository } from 'src/interfaces/user.interface'; +import { AuditService } from 'src/services/audit.service'; import { auditStub } from 'test/fixtures/audit.stub'; import { authStub } from 'test/fixtures/auth.stub'; import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock'; diff --git a/server/src/domain/audit/audit.service.ts b/server/src/services/audit.service.ts similarity index 90% rename from server/src/domain/audit/audit.service.ts rename to server/src/services/audit.service.ts index 91acb54d9..ff5e0d9c7 100644 --- a/server/src/domain/audit/audit.service.ts +++ b/server/src/services/audit.service.ts @@ -1,6 +1,7 @@ import { BadRequestException, Inject, Injectable } from '@nestjs/common'; import { DateTime } from 'luxon'; import { resolve } from 'node:path'; +import { AUDIT_LOG_MAX_DURATION } from 'src/constants'; import { AccessCore, Permission } from 'src/cores/access.core'; import { StorageCore, StorageFolder } from 'src/cores/storage.core'; import { @@ -10,22 +11,20 @@ import { FileChecksumResponseDto, FileReportItemDto, PathEntityType, -} from 'src/domain/audit/audit.dto'; -import { AuthDto } from 'src/domain/auth/auth.dto'; -import { AUDIT_LOG_MAX_DURATION } from 'src/domain/domain.constant'; -import { JOBS_ASSET_PAGINATION_SIZE } from 'src/domain/job/job.constants'; +} from 'src/dtos/audit.dto'; +import { AuthDto } from 'src/dtos/auth.dto'; import { DatabaseAction } from 'src/entities/audit.entity'; import { AssetPathType, PersonPathType, UserPathType } from 'src/entities/move.entity'; -import { ImmichLogger } from 'src/infra/logger'; -import { IAccessRepository } from 'src/interfaces/access.repository'; -import { IAssetRepository } from 'src/interfaces/asset.repository'; -import { IAuditRepository } from 'src/interfaces/audit.repository'; -import { ICryptoRepository } from 'src/interfaces/crypto.repository'; -import { JobStatus } from 'src/interfaces/job.repository'; -import { IPersonRepository } from 'src/interfaces/person.repository'; -import { IStorageRepository } from 'src/interfaces/storage.repository'; -import { IUserRepository } from 'src/interfaces/user.repository'; -import { usePagination } from 'src/utils'; +import { IAccessRepository } from 'src/interfaces/access.interface'; +import { IAssetRepository } from 'src/interfaces/asset.interface'; +import { IAuditRepository } from 'src/interfaces/audit.interface'; +import { ICryptoRepository } from 'src/interfaces/crypto.interface'; +import { JOBS_ASSET_PAGINATION_SIZE, JobStatus } from 'src/interfaces/job.interface'; +import { IPersonRepository } from 'src/interfaces/person.interface'; +import { IStorageRepository } from 'src/interfaces/storage.interface'; +import { IUserRepository } from 'src/interfaces/user.interface'; +import { ImmichLogger } from 'src/utils/logger'; +import { usePagination } from 'src/utils/pagination'; @Injectable() export class AuditService { diff --git a/server/src/domain/auth/auth.service.spec.ts b/server/src/services/auth.service.spec.ts similarity index 97% rename from server/src/domain/auth/auth.service.spec.ts rename to server/src/services/auth.service.spec.ts index e091cdcd9..30773f3f1 100644 --- a/server/src/domain/auth/auth.service.spec.ts +++ b/server/src/services/auth.service.spec.ts @@ -2,17 +2,17 @@ import { BadRequestException, UnauthorizedException } from '@nestjs/common'; import { IncomingHttpHeaders } from 'node:http'; import { Issuer, generators } from 'openid-client'; import { Socket } from 'socket.io'; -import { AuthType } from 'src/domain/auth/auth.constant'; -import { AuthDto, SignUpDto } from 'src/domain/auth/auth.dto'; -import { AuthService } from 'src/domain/auth/auth.service'; +import { AuthType } from 'src/constants'; +import { AuthDto, SignUpDto } from 'src/dtos/auth.dto'; import { UserEntity } from 'src/entities/user.entity'; -import { IKeyRepository } from 'src/interfaces/api-key.repository'; -import { ICryptoRepository } from 'src/interfaces/crypto.repository'; -import { ILibraryRepository } from 'src/interfaces/library.repository'; -import { ISharedLinkRepository } from 'src/interfaces/shared-link.repository'; -import { ISystemConfigRepository } from 'src/interfaces/system-config.repository'; -import { IUserTokenRepository } from 'src/interfaces/user-token.repository'; -import { IUserRepository } from 'src/interfaces/user.repository'; +import { IKeyRepository } from 'src/interfaces/api-key.interface'; +import { ICryptoRepository } from 'src/interfaces/crypto.interface'; +import { ILibraryRepository } from 'src/interfaces/library.interface'; +import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface'; +import { ISystemConfigRepository } from 'src/interfaces/system-config.interface'; +import { IUserTokenRepository } from 'src/interfaces/user-token.interface'; +import { IUserRepository } from 'src/interfaces/user.interface'; +import { AuthService } from 'src/services/auth.service'; import { keyStub } from 'test/fixtures/api-key.stub'; import { authStub, loginResponseStub } from 'test/fixtures/auth.stub'; import { sharedLinkStub } from 'test/fixtures/shared-link.stub'; diff --git a/server/src/domain/auth/auth.service.ts b/server/src/services/auth.service.ts similarity index 96% rename from server/src/domain/auth/auth.service.ts rename to server/src/services/auth.service.ts index 2d807a249..8563d8353 100644 --- a/server/src/domain/auth/auth.service.ts +++ b/server/src/services/auth.service.ts @@ -10,9 +10,6 @@ import cookieParser from 'cookie'; import { DateTime } from 'luxon'; import { IncomingHttpHeaders } from 'node:http'; import { ClientMetadata, Issuer, UserinfoResponse, custom, generators } from 'openid-client'; -import { AccessCore, Permission } from 'src/cores/access.core'; -import { SystemConfigCore } from 'src/cores/system-config.core'; -import { UserCore } from 'src/cores/user.core'; import { AuthType, IMMICH_ACCESS_COOKIE, @@ -21,7 +18,10 @@ import { IMMICH_IS_AUTHENTICATED, LOGIN_URL, MOBILE_REDIRECT, -} from 'src/domain/auth/auth.constant'; +} from 'src/constants'; +import { AccessCore, Permission } from 'src/cores/access.core'; +import { SystemConfigCore } from 'src/cores/system-config.core'; +import { UserCore } from 'src/cores/user.core'; import { AuthDeviceResponseDto, AuthDto, @@ -35,20 +35,20 @@ import { SignUpDto, mapLoginResponse, mapUserToken, -} from 'src/domain/auth/auth.dto'; -import { UserResponseDto, mapUser } from 'src/domain/user/response-dto/user-response.dto'; +} from 'src/dtos/auth.dto'; +import { UserResponseDto, mapUser } from 'src/dtos/user.dto'; import { SystemConfig } from 'src/entities/system-config.entity'; import { UserEntity } from 'src/entities/user.entity'; -import { ImmichLogger } from 'src/infra/logger'; -import { IAccessRepository } from 'src/interfaces/access.repository'; -import { IKeyRepository } from 'src/interfaces/api-key.repository'; -import { ICryptoRepository } from 'src/interfaces/crypto.repository'; -import { ILibraryRepository } from 'src/interfaces/library.repository'; -import { ISharedLinkRepository } from 'src/interfaces/shared-link.repository'; -import { ISystemConfigRepository } from 'src/interfaces/system-config.repository'; -import { IUserTokenRepository } from 'src/interfaces/user-token.repository'; -import { IUserRepository } from 'src/interfaces/user.repository'; -import { HumanReadableSize } from 'src/utils'; +import { IAccessRepository } from 'src/interfaces/access.interface'; +import { IKeyRepository } from 'src/interfaces/api-key.interface'; +import { ICryptoRepository } from 'src/interfaces/crypto.interface'; +import { ILibraryRepository } from 'src/interfaces/library.interface'; +import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface'; +import { ISystemConfigRepository } from 'src/interfaces/system-config.interface'; +import { IUserTokenRepository } from 'src/interfaces/user-token.interface'; +import { IUserRepository } from 'src/interfaces/user.interface'; +import { HumanReadableSize } from 'src/utils/bytes'; +import { ImmichLogger } from 'src/utils/logger'; export interface LoginDetails { isSecure: boolean; diff --git a/server/src/domain/database/database.service.spec.ts b/server/src/services/database.service.spec.ts similarity index 97% rename from server/src/domain/database/database.service.spec.ts rename to server/src/services/database.service.spec.ts index 191c0221b..6fa5e7fd8 100644 --- a/server/src/domain/database/database.service.spec.ts +++ b/server/src/services/database.service.spec.ts @@ -1,7 +1,7 @@ -import { DatabaseService } from 'src/domain/database/database.service'; -import { Version, VersionType } from 'src/domain/domain.constant'; -import { ImmichLogger } from 'src/infra/logger'; -import { DatabaseExtension, IDatabaseRepository, VectorIndex } from 'src/interfaces/database.repository'; +import { DatabaseExtension, IDatabaseRepository, VectorIndex } from 'src/interfaces/database.interface'; +import { DatabaseService } from 'src/services/database.service'; +import { ImmichLogger } from 'src/utils/logger'; +import { Version, VersionType } from 'src/utils/version'; import { newDatabaseRepositoryMock } from 'test/repositories/database.repository.mock'; describe(DatabaseService.name, () => { diff --git a/server/src/domain/database/database.service.ts b/server/src/services/database.service.ts similarity index 97% rename from server/src/domain/database/database.service.ts rename to server/src/services/database.service.ts index 7bb16a4e1..a333c0053 100644 --- a/server/src/domain/database/database.service.ts +++ b/server/src/services/database.service.ts @@ -1,6 +1,4 @@ import { Inject, Injectable } from '@nestjs/common'; -import { Version, VersionType } from 'src/domain/domain.constant'; -import { ImmichLogger } from 'src/infra/logger'; import { DatabaseExtension, DatabaseLock, @@ -8,7 +6,9 @@ import { VectorExtension, VectorIndex, extName, -} from 'src/interfaces/database.repository'; +} from 'src/interfaces/database.interface'; +import { ImmichLogger } from 'src/utils/logger'; +import { Version, VersionType } from 'src/utils/version'; @Injectable() export class DatabaseService { diff --git a/server/src/domain/download/download.service.spec.ts b/server/src/services/download.service.spec.ts similarity index 96% rename from server/src/domain/download/download.service.spec.ts rename to server/src/services/download.service.spec.ts index 6e1eafbec..babc21fa8 100644 --- a/server/src/domain/download/download.service.spec.ts +++ b/server/src/services/download.service.spec.ts @@ -1,10 +1,10 @@ import { BadRequestException } from '@nestjs/common'; import { when } from 'jest-when'; -import { DownloadResponseDto } from 'src/domain/download/download.dto'; -import { DownloadService } from 'src/domain/download/download.service'; -import { IAssetRepository } from 'src/interfaces/asset.repository'; -import { IStorageRepository } from 'src/interfaces/storage.repository'; -import { CacheControl, ImmichFileResponse } from 'src/utils'; +import { DownloadResponseDto } from 'src/dtos/download.dto'; +import { IAssetRepository } from 'src/interfaces/asset.interface'; +import { IStorageRepository } from 'src/interfaces/storage.interface'; +import { DownloadService } from 'src/services/download.service'; +import { CacheControl, ImmichFileResponse } from 'src/utils/file'; import { assetStub } from 'test/fixtures/asset.stub'; import { authStub } from 'test/fixtures/auth.stub'; import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock'; diff --git a/server/src/domain/download/download.service.ts b/server/src/services/download.service.ts similarity index 90% rename from server/src/domain/download/download.service.ts rename to server/src/services/download.service.ts index 8c0168a6c..b0b68a1e8 100644 --- a/server/src/domain/download/download.service.ts +++ b/server/src/services/download.service.ts @@ -1,15 +1,17 @@ import { BadRequestException, Inject, Injectable } from '@nestjs/common'; import { parse } from 'node:path'; import { AccessCore, Permission } from 'src/cores/access.core'; -import { AssetIdsDto } from 'src/domain/asset/dto/asset-ids.dto'; -import { AuthDto } from 'src/domain/auth/auth.dto'; -import { mimeTypes } from 'src/domain/domain.constant'; -import { DownloadArchiveInfo, DownloadInfoDto, DownloadResponseDto } from 'src/domain/download/download.dto'; +import { AssetIdsDto } from 'src/dtos/asset.dto'; +import { AuthDto } from 'src/dtos/auth.dto'; +import { DownloadArchiveInfo, DownloadInfoDto, DownloadResponseDto } from 'src/dtos/download.dto'; import { AssetEntity } from 'src/entities/asset.entity'; -import { IAccessRepository } from 'src/interfaces/access.repository'; -import { IAssetRepository } from 'src/interfaces/asset.repository'; -import { IStorageRepository, ImmichReadStream } from 'src/interfaces/storage.repository'; -import { CacheControl, HumanReadableSize, ImmichFileResponse, usePagination } from 'src/utils'; +import { IAccessRepository } from 'src/interfaces/access.interface'; +import { IAssetRepository } from 'src/interfaces/asset.interface'; +import { IStorageRepository, ImmichReadStream } from 'src/interfaces/storage.interface'; +import { HumanReadableSize } from 'src/utils/bytes'; +import { CacheControl, ImmichFileResponse } from 'src/utils/file'; +import { mimeTypes } from 'src/utils/mime-types'; +import { usePagination } from 'src/utils/pagination'; @Injectable() export class DownloadService { diff --git a/server/src/domain/job/job.service.spec.ts b/server/src/services/job.service.spec.ts similarity index 97% rename from server/src/domain/job/job.service.spec.ts rename to server/src/services/job.service.spec.ts index ec905b062..b680abaf1 100644 --- a/server/src/domain/job/job.service.spec.ts +++ b/server/src/services/job.service.spec.ts @@ -1,13 +1,20 @@ import { BadRequestException } from '@nestjs/common'; import { FeatureFlag, SystemConfigCore } from 'src/cores/system-config.core'; -import { JobCommand, JobName, QueueName } from 'src/domain/job/job.constants'; -import { JobService } from 'src/domain/job/job.service'; import { SystemConfig, SystemConfigKey } from 'src/entities/system-config.entity'; -import { IAssetRepository } from 'src/interfaces/asset.repository'; -import { ICommunicationRepository } from 'src/interfaces/communication.repository'; -import { IJobRepository, JobHandler, JobItem, JobStatus } from 'src/interfaces/job.repository'; -import { IPersonRepository } from 'src/interfaces/person.repository'; -import { ISystemConfigRepository } from 'src/interfaces/system-config.repository'; +import { IAssetRepository } from 'src/interfaces/asset.interface'; +import { ICommunicationRepository } from 'src/interfaces/communication.interface'; +import { + IJobRepository, + JobCommand, + JobHandler, + JobItem, + JobName, + JobStatus, + QueueName, +} from 'src/interfaces/job.interface'; +import { IPersonRepository } from 'src/interfaces/person.interface'; +import { ISystemConfigRepository } from 'src/interfaces/system-config.interface'; +import { JobService } from 'src/services/job.service'; import { assetStub } from 'test/fixtures/asset.stub'; import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock'; import { newCommunicationRepositoryMock } from 'test/repositories/communication.repository.mock'; diff --git a/server/src/domain/job/job.service.ts b/server/src/services/job.service.ts similarity index 94% rename from server/src/domain/job/job.service.ts rename to server/src/services/job.service.ts index 192b093c1..e6d9b0781 100644 --- a/server/src/domain/job/job.service.ts +++ b/server/src/services/job.service.ts @@ -1,15 +1,24 @@ import { BadRequestException, Inject, Injectable } from '@nestjs/common'; import { FeatureFlag, SystemConfigCore } from 'src/cores/system-config.core'; -import { mapAsset } from 'src/domain/asset/response-dto/asset-response.dto'; -import { ConcurrentQueueName, JobCommand, JobName, QueueName } from 'src/domain/job/job.constants'; -import { AllJobStatusResponseDto, JobCommandDto, JobStatusDto } from 'src/domain/job/job.dto'; +import { mapAsset } from 'src/dtos/asset-response.dto'; +import { AllJobStatusResponseDto, JobCommandDto, JobStatusDto } from 'src/dtos/job.dto'; import { AssetType } from 'src/entities/asset.entity'; -import { ImmichLogger } from 'src/infra/logger'; -import { IAssetRepository } from 'src/interfaces/asset.repository'; -import { ClientEvent, ICommunicationRepository } from 'src/interfaces/communication.repository'; -import { IJobRepository, JobHandler, JobItem, JobStatus, QueueCleanType } from 'src/interfaces/job.repository'; -import { IPersonRepository } from 'src/interfaces/person.repository'; -import { ISystemConfigRepository } from 'src/interfaces/system-config.repository'; +import { IAssetRepository } from 'src/interfaces/asset.interface'; +import { ClientEvent, ICommunicationRepository } from 'src/interfaces/communication.interface'; +import { + ConcurrentQueueName, + IJobRepository, + JobCommand, + JobHandler, + JobItem, + JobName, + JobStatus, + QueueCleanType, + QueueName, +} from 'src/interfaces/job.interface'; +import { IPersonRepository } from 'src/interfaces/person.interface'; +import { ISystemConfigRepository } from 'src/interfaces/system-config.interface'; +import { ImmichLogger } from 'src/utils/logger'; @Injectable() export class JobService { diff --git a/server/src/domain/library/library.service.spec.ts b/server/src/services/library.service.spec.ts similarity index 98% rename from server/src/domain/library/library.service.spec.ts rename to server/src/services/library.service.spec.ts index 3a8440fda..d00fef3db 100644 --- a/server/src/domain/library/library.service.spec.ts +++ b/server/src/services/library.service.spec.ts @@ -3,21 +3,20 @@ import { when } from 'jest-when'; import { R_OK } from 'node:constants'; import { Stats } from 'node:fs'; import { SystemConfigCore } from 'src/cores/system-config.core'; -import { JobName } from 'src/domain/job/job.constants'; -import { ILibraryFileJob, ILibraryOfflineJob, ILibraryRefreshJob } from 'src/domain/job/job.interface'; -import { mapLibrary } from 'src/domain/library/library.dto'; -import { LibraryService } from 'src/domain/library/library.service'; +import { ILibraryOfflineJob } from 'src/domain/job/job.interface'; +import { mapLibrary } from 'src/dtos/library.dto'; import { AssetType } from 'src/entities/asset.entity'; import { LibraryType } from 'src/entities/library.entity'; import { SystemConfig, SystemConfigKey } from 'src/entities/system-config.entity'; import { UserEntity } from 'src/entities/user.entity'; -import { IAssetRepository } from 'src/interfaces/asset.repository'; -import { ICryptoRepository } from 'src/interfaces/crypto.repository'; -import { IDatabaseRepository } from 'src/interfaces/database.repository'; -import { IJobRepository, JobStatus } from 'src/interfaces/job.repository'; -import { ILibraryRepository } from 'src/interfaces/library.repository'; -import { IStorageRepository, StorageEventType } from 'src/interfaces/storage.repository'; -import { ISystemConfigRepository } from 'src/interfaces/system-config.repository'; +import { IAssetRepository } from 'src/interfaces/asset.interface'; +import { ICryptoRepository } from 'src/interfaces/crypto.interface'; +import { IDatabaseRepository } from 'src/interfaces/database.interface'; +import { IJobRepository, ILibraryFileJob, ILibraryRefreshJob, JobName, JobStatus } from 'src/interfaces/job.interface'; +import { ILibraryRepository } from 'src/interfaces/library.interface'; +import { IStorageRepository, StorageEventType } from 'src/interfaces/storage.interface'; +import { ISystemConfigRepository } from 'src/interfaces/system-config.interface'; +import { LibraryService } from 'src/services/library.service'; import { assetStub } from 'test/fixtures/asset.stub'; import { authStub } from 'test/fixtures/auth.stub'; import { libraryStub } from 'test/fixtures/library.stub'; diff --git a/server/src/domain/library/library.service.ts b/server/src/services/library.service.ts similarity index 96% rename from server/src/domain/library/library.service.ts rename to server/src/services/library.service.ts index 654021947..536d26392 100644 --- a/server/src/domain/library/library.service.ts +++ b/server/src/services/library.service.ts @@ -1,5 +1,4 @@ import { BadRequestException, Inject, Injectable } from '@nestjs/common'; -import { OnEvent } from '@nestjs/event-emitter'; import { R_OK } from 'node:constants'; import { EventEmitter } from 'node:events'; import { Stats } from 'node:fs'; @@ -7,15 +6,8 @@ import path, { basename, parse } from 'node:path'; import picomatch from 'picomatch'; import { StorageCore } from 'src/cores/storage.core'; import { SystemConfigCore } from 'src/cores/system-config.core'; -import { mimeTypes } from 'src/domain/domain.constant'; -import { JOBS_ASSET_PAGINATION_SIZE, JobName } from 'src/domain/job/job.constants'; -import { - IBaseJob, - IEntityJob, - ILibraryFileJob, - ILibraryOfflineJob, - ILibraryRefreshJob, -} from 'src/domain/job/job.interface'; +import { OnEventInternal } from 'src/decorators'; +import { ILibraryOfflineJob } from 'src/domain/job/job.interface'; import { CreateLibraryDto, LibraryResponseDto, @@ -27,19 +19,30 @@ import { ValidateLibraryImportPathResponseDto, ValidateLibraryResponseDto, mapLibrary, -} from 'src/domain/library/library.dto'; +} from 'src/dtos/library.dto'; import { AssetEntity, AssetType } from 'src/entities/asset.entity'; -import { LibraryType } from 'src/entities/library.entity'; -import { ImmichLogger } from 'src/infra/logger'; -import { IAssetRepository, WithProperty } from 'src/interfaces/asset.repository'; -import { InternalEvent, InternalEventMap } from 'src/interfaces/communication.repository'; -import { ICryptoRepository } from 'src/interfaces/crypto.repository'; -import { DatabaseLock, IDatabaseRepository } from 'src/interfaces/database.repository'; -import { IJobRepository, JobStatus } from 'src/interfaces/job.repository'; -import { ILibraryRepository } from 'src/interfaces/library.repository'; -import { IStorageRepository, StorageEventType } from 'src/interfaces/storage.repository'; -import { ISystemConfigRepository } from 'src/interfaces/system-config.repository'; -import { handlePromiseError, usePagination } from 'src/utils'; +import { LibraryEntity, LibraryType } from 'src/entities/library.entity'; +import { IAssetRepository, WithProperty } from 'src/interfaces/asset.interface'; +import { InternalEvent, InternalEventMap } from 'src/interfaces/communication.interface'; +import { ICryptoRepository } from 'src/interfaces/crypto.interface'; +import { DatabaseLock, IDatabaseRepository } from 'src/interfaces/database.interface'; +import { + IBaseJob, + IEntityJob, + IJobRepository, + ILibraryFileJob, + ILibraryRefreshJob, + JOBS_ASSET_PAGINATION_SIZE, + JobName, + JobStatus, +} from 'src/interfaces/job.interface'; +import { ILibraryRepository } from 'src/interfaces/library.interface'; +import { IStorageRepository, StorageEventType } from 'src/interfaces/storage.interface'; +import { ISystemConfigRepository } from 'src/interfaces/system-config.interface'; +import { ImmichLogger } from 'src/utils/logger'; +import { mimeTypes } from 'src/utils/mime-types'; +import { handlePromiseError } from 'src/utils/misc'; +import { usePagination } from 'src/utils/pagination'; import { validateCronExpression } from 'src/validation'; const LIBRARY_SCAN_BATCH_SIZE = 1000; @@ -102,7 +105,7 @@ export class LibraryService extends EventEmitter { }); } - @OnEvent(InternalEvent.VALIDATE_CONFIG) + @OnEventInternal(InternalEvent.VALIDATE_CONFIG) validateConfig({ newConfig }: InternalEventMap[InternalEvent.VALIDATE_CONFIG]) { const { scan } = newConfig.library; if (!validateCronExpression(scan.cronExpression)) { diff --git a/server/src/domain/media/media.service.spec.ts b/server/src/services/media.service.spec.ts similarity index 99% rename from server/src/domain/media/media.service.spec.ts rename to server/src/services/media.service.spec.ts index 97a6d161b..4397730ab 100644 --- a/server/src/domain/media/media.service.spec.ts +++ b/server/src/services/media.service.spec.ts @@ -1,6 +1,4 @@ import { Stats } from 'node:fs'; -import { JobName } from 'src/domain/job/job.constants'; -import { MediaService } from 'src/domain/media/media.service'; import { AssetType } from 'src/entities/asset.entity'; import { ExifEntity } from 'src/entities/exif.entity'; import { @@ -12,14 +10,15 @@ import { TranscodePolicy, VideoCodec, } from 'src/entities/system-config.entity'; -import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.repository'; -import { ICryptoRepository } from 'src/interfaces/crypto.repository'; -import { IJobRepository, JobStatus } from 'src/interfaces/job.repository'; -import { IMediaRepository } from 'src/interfaces/media.repository'; -import { IMoveRepository } from 'src/interfaces/move.repository'; -import { IPersonRepository } from 'src/interfaces/person.repository'; -import { IStorageRepository } from 'src/interfaces/storage.repository'; -import { ISystemConfigRepository } from 'src/interfaces/system-config.repository'; +import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface'; +import { ICryptoRepository } from 'src/interfaces/crypto.interface'; +import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface'; +import { IMediaRepository } from 'src/interfaces/media.interface'; +import { IMoveRepository } from 'src/interfaces/move.interface'; +import { IPersonRepository } from 'src/interfaces/person.interface'; +import { IStorageRepository } from 'src/interfaces/storage.interface'; +import { ISystemConfigRepository } from 'src/interfaces/system-config.interface'; +import { MediaService } from 'src/services/media.service'; import { assetStub } from 'test/fixtures/asset.stub'; import { faceStub } from 'test/fixtures/face.stub'; import { probeStub } from 'test/fixtures/media.stub'; diff --git a/server/src/domain/media/media.service.ts b/server/src/services/media.service.ts similarity index 95% rename from server/src/domain/media/media.service.ts rename to server/src/services/media.service.ts index c254689b4..26aa2dce9 100644 --- a/server/src/domain/media/media.service.ts +++ b/server/src/services/media.service.ts @@ -1,19 +1,7 @@ import { Inject, Injectable, UnsupportedMediaTypeException } from '@nestjs/common'; import { StorageCore, StorageFolder } from 'src/cores/storage.core'; import { SystemConfigCore } from 'src/cores/system-config.core'; -import { JOBS_ASSET_PAGINATION_SIZE, JobName, QueueName } from 'src/domain/job/job.constants'; -import { IBaseJob, IEntityJob } from 'src/domain/job/job.interface'; -import { - H264Config, - HEVCConfig, - NVENCConfig, - QSVConfig, - RKMPPConfig, - ThumbnailConfig, - VAAPIConfig, - VP9Config, -} from 'src/domain/media/media.util'; -import { SystemConfigFFmpegDto } from 'src/domain/system-config/dto/system-config-ffmpeg.dto'; +import { SystemConfigFFmpegDto } from 'src/dtos/system-config.dto'; import { AssetEntity, AssetType } from 'src/entities/asset.entity'; import { AssetPathType } from 'src/entities/move.entity'; import { @@ -24,21 +12,35 @@ import { TranscodeTarget, VideoCodec, } from 'src/entities/system-config.entity'; -import { ImmichLogger } from 'src/infra/logger'; -import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.repository'; -import { ICryptoRepository } from 'src/interfaces/crypto.repository'; -import { IJobRepository, JobItem, JobStatus } from 'src/interfaces/job.repository'; +import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface'; +import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { - AudioStreamInfo, - IMediaRepository, - VideoCodecHWConfig, - VideoStreamInfo, -} from 'src/interfaces/media.repository'; -import { IMoveRepository } from 'src/interfaces/move.repository'; -import { IPersonRepository } from 'src/interfaces/person.repository'; -import { IStorageRepository } from 'src/interfaces/storage.repository'; -import { ISystemConfigRepository } from 'src/interfaces/system-config.repository'; -import { usePagination } from 'src/utils'; + IBaseJob, + IEntityJob, + IJobRepository, + JOBS_ASSET_PAGINATION_SIZE, + JobItem, + JobName, + JobStatus, + QueueName, +} from 'src/interfaces/job.interface'; +import { AudioStreamInfo, IMediaRepository, VideoCodecHWConfig, VideoStreamInfo } from 'src/interfaces/media.interface'; +import { IMoveRepository } from 'src/interfaces/move.interface'; +import { IPersonRepository } from 'src/interfaces/person.interface'; +import { IStorageRepository } from 'src/interfaces/storage.interface'; +import { ISystemConfigRepository } from 'src/interfaces/system-config.interface'; +import { ImmichLogger } from 'src/utils/logger'; +import { + H264Config, + HEVCConfig, + NVENCConfig, + QSVConfig, + RKMPPConfig, + ThumbnailConfig, + VAAPIConfig, + VP9Config, +} from 'src/utils/media'; +import { usePagination } from 'src/utils/pagination'; @Injectable() export class MediaService { diff --git a/server/src/domain/metadata/metadata.service.spec.ts b/server/src/services/metadata.service.spec.ts similarity index 97% rename from server/src/domain/metadata/metadata.service.spec.ts rename to server/src/services/metadata.service.spec.ts index 2108c5fc4..4dafa0ac5 100644 --- a/server/src/domain/metadata/metadata.service.spec.ts +++ b/server/src/services/metadata.service.spec.ts @@ -3,23 +3,22 @@ import { when } from 'jest-when'; import { randomBytes } from 'node:crypto'; import { Stats } from 'node:fs'; import { constants } from 'node:fs/promises'; -import { JobName } from 'src/domain/job/job.constants'; -import { MetadataService, Orientation } from 'src/domain/metadata/metadata.service'; import { AssetType } from 'src/entities/asset.entity'; import { ExifEntity } from 'src/entities/exif.entity'; import { SystemConfigKey } from 'src/entities/system-config.entity'; -import { IAlbumRepository } from 'src/interfaces/album.repository'; -import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.repository'; -import { ClientEvent, ICommunicationRepository } from 'src/interfaces/communication.repository'; -import { ICryptoRepository } from 'src/interfaces/crypto.repository'; -import { IDatabaseRepository } from 'src/interfaces/database.repository'; -import { IJobRepository, JobStatus } from 'src/interfaces/job.repository'; -import { IMediaRepository } from 'src/interfaces/media.repository'; -import { IMetadataRepository, ImmichTags } from 'src/interfaces/metadata.repository'; -import { IMoveRepository } from 'src/interfaces/move.repository'; -import { IPersonRepository } from 'src/interfaces/person.repository'; -import { IStorageRepository } from 'src/interfaces/storage.repository'; -import { ISystemConfigRepository } from 'src/interfaces/system-config.repository'; +import { IAlbumRepository } from 'src/interfaces/album.interface'; +import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface'; +import { ClientEvent, ICommunicationRepository } from 'src/interfaces/communication.interface'; +import { ICryptoRepository } from 'src/interfaces/crypto.interface'; +import { IDatabaseRepository } from 'src/interfaces/database.interface'; +import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface'; +import { IMediaRepository } from 'src/interfaces/media.interface'; +import { IMetadataRepository, ImmichTags } from 'src/interfaces/metadata.interface'; +import { IMoveRepository } from 'src/interfaces/move.interface'; +import { IPersonRepository } from 'src/interfaces/person.interface'; +import { IStorageRepository } from 'src/interfaces/storage.interface'; +import { ISystemConfigRepository } from 'src/interfaces/system-config.interface'; +import { MetadataService, Orientation } from 'src/services/metadata.service'; import { assetStub } from 'test/fixtures/asset.stub'; import { fileStub } from 'test/fixtures/file.stub'; import { probeStub } from 'test/fixtures/media.stub'; diff --git a/server/src/domain/metadata/metadata.service.ts b/server/src/services/metadata.service.ts similarity index 95% rename from server/src/domain/metadata/metadata.service.ts rename to server/src/services/metadata.service.ts index 0e949626c..849a12da9 100644 --- a/server/src/domain/metadata/metadata.service.ts +++ b/server/src/services/metadata.service.ts @@ -8,24 +8,32 @@ import path from 'node:path'; import { Subscription } from 'rxjs'; import { StorageCore } from 'src/cores/storage.core'; import { FeatureFlag, SystemConfigCore } from 'src/cores/system-config.core'; -import { JOBS_ASSET_PAGINATION_SIZE, JobName, QueueName } from 'src/domain/job/job.constants'; -import { IBaseJob, IEntityJob, ISidecarWriteJob } from 'src/domain/job/job.interface'; import { AssetEntity, AssetType } from 'src/entities/asset.entity'; import { ExifEntity } from 'src/entities/exif.entity'; -import { ImmichLogger } from 'src/infra/logger'; -import { IAlbumRepository } from 'src/interfaces/album.repository'; -import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.repository'; -import { ClientEvent, ICommunicationRepository } from 'src/interfaces/communication.repository'; -import { ICryptoRepository } from 'src/interfaces/crypto.repository'; -import { DatabaseLock, IDatabaseRepository } from 'src/interfaces/database.repository'; -import { IJobRepository, JobStatus } from 'src/interfaces/job.repository'; -import { IMediaRepository } from 'src/interfaces/media.repository'; -import { IMetadataRepository, ImmichTags } from 'src/interfaces/metadata.repository'; -import { IMoveRepository } from 'src/interfaces/move.repository'; -import { IPersonRepository } from 'src/interfaces/person.repository'; -import { IStorageRepository } from 'src/interfaces/storage.repository'; -import { ISystemConfigRepository } from 'src/interfaces/system-config.repository'; -import { handlePromiseError, usePagination } from 'src/utils'; +import { IAlbumRepository } from 'src/interfaces/album.interface'; +import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface'; +import { ClientEvent, ICommunicationRepository } from 'src/interfaces/communication.interface'; +import { ICryptoRepository } from 'src/interfaces/crypto.interface'; +import { DatabaseLock, IDatabaseRepository } from 'src/interfaces/database.interface'; +import { + IBaseJob, + IEntityJob, + IJobRepository, + ISidecarWriteJob, + JOBS_ASSET_PAGINATION_SIZE, + JobName, + JobStatus, + QueueName, +} from 'src/interfaces/job.interface'; +import { IMediaRepository } from 'src/interfaces/media.interface'; +import { IMetadataRepository, ImmichTags } from 'src/interfaces/metadata.interface'; +import { IMoveRepository } from 'src/interfaces/move.interface'; +import { IPersonRepository } from 'src/interfaces/person.interface'; +import { IStorageRepository } from 'src/interfaces/storage.interface'; +import { ISystemConfigRepository } from 'src/interfaces/system-config.interface'; +import { ImmichLogger } from 'src/utils/logger'; +import { handlePromiseError } from 'src/utils/misc'; +import { usePagination } from 'src/utils/pagination'; /** look for a date from these tags (in order) */ const EXIF_DATE_TAGS: Array = [ diff --git a/server/src/apps/microservices.service.ts b/server/src/services/microservices.service.ts similarity index 81% rename from server/src/apps/microservices.service.ts rename to server/src/services/microservices.service.ts index cfcd9cae5..07ef3f35f 100644 --- a/server/src/apps/microservices.service.ts +++ b/server/src/services/microservices.service.ts @@ -1,20 +1,19 @@ import { Injectable } from '@nestjs/common'; -import { AssetService } from 'src/domain/asset/asset.service'; -import { AuditService } from 'src/domain/audit/audit.service'; -import { DatabaseService } from 'src/domain/database/database.service'; -import { JobName } from 'src/domain/job/job.constants'; -import { IDeleteFilesJob } from 'src/domain/job/job.interface'; -import { JobService } from 'src/domain/job/job.service'; -import { LibraryService } from 'src/domain/library/library.service'; -import { MediaService } from 'src/domain/media/media.service'; -import { MetadataService } from 'src/domain/metadata/metadata.service'; -import { PersonService } from 'src/domain/person/person.service'; -import { SmartInfoService } from 'src/domain/smart-info/smart-info.service'; -import { StorageTemplateService } from 'src/domain/storage-template/storage-template.service'; -import { StorageService } from 'src/domain/storage/storage.service'; -import { SystemConfigService } from 'src/domain/system-config/system-config.service'; -import { UserService } from 'src/domain/user/user.service'; -import { otelSDK } from 'src/infra/instrumentation'; +import { IDeleteFilesJob, JobName } from 'src/interfaces/job.interface'; +import { AssetService } from 'src/services/asset.service'; +import { AuditService } from 'src/services/audit.service'; +import { DatabaseService } from 'src/services/database.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 { PersonService } from 'src/services/person.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 { UserService } from 'src/services/user.service'; +import { otelSDK } from 'src/utils/instrumentation'; @Injectable() export class MicroservicesService { diff --git a/server/src/domain/partner/partner.service.spec.ts b/server/src/services/partner.service.spec.ts similarity index 94% rename from server/src/domain/partner/partner.service.spec.ts rename to server/src/services/partner.service.spec.ts index 7d4821282..a3c4af736 100644 --- a/server/src/domain/partner/partner.service.spec.ts +++ b/server/src/services/partner.service.spec.ts @@ -1,9 +1,9 @@ import { BadRequestException } from '@nestjs/common'; -import { PartnerResponseDto } from 'src/domain/partner/partner.dto'; -import { PartnerService } from 'src/domain/partner/partner.service'; +import { PartnerResponseDto } from 'src/dtos/partner.dto'; import { UserAvatarColor } from 'src/entities/user.entity'; -import { IAccessRepository } from 'src/interfaces/access.repository'; -import { IPartnerRepository, PartnerDirection } from 'src/interfaces/partner.repository'; +import { IAccessRepository } from 'src/interfaces/access.interface'; +import { IPartnerRepository, PartnerDirection } from 'src/interfaces/partner.interface'; +import { PartnerService } from 'src/services/partner.service'; import { authStub } from 'test/fixtures/auth.stub'; import { partnerStub } from 'test/fixtures/partner.stub'; import { newPartnerRepositoryMock } from 'test/repositories/partner.repository.mock'; diff --git a/server/src/domain/partner/partner.service.ts b/server/src/services/partner.service.ts similarity index 89% rename from server/src/domain/partner/partner.service.ts rename to server/src/services/partner.service.ts index 34a88a7ea..14503cc7f 100644 --- a/server/src/domain/partner/partner.service.ts +++ b/server/src/services/partner.service.ts @@ -1,11 +1,11 @@ import { BadRequestException, Inject, Injectable } from '@nestjs/common'; import { AccessCore, Permission } from 'src/cores/access.core'; -import { AuthDto } from 'src/domain/auth/auth.dto'; -import { PartnerResponseDto, UpdatePartnerDto } from 'src/domain/partner/partner.dto'; -import { mapUser } from 'src/domain/user/response-dto/user-response.dto'; +import { AuthDto } from 'src/dtos/auth.dto'; +import { PartnerResponseDto, UpdatePartnerDto } from 'src/dtos/partner.dto'; +import { mapUser } from 'src/dtos/user.dto'; import { PartnerEntity } from 'src/entities/partner.entity'; -import { IAccessRepository } from 'src/interfaces/access.repository'; -import { IPartnerRepository, PartnerDirection, PartnerIds } from 'src/interfaces/partner.repository'; +import { IAccessRepository } from 'src/interfaces/access.interface'; +import { IPartnerRepository, PartnerDirection, PartnerIds } from 'src/interfaces/partner.interface'; @Injectable() export class PartnerService { diff --git a/server/src/domain/person/person.service.spec.ts b/server/src/services/person.service.spec.ts similarity index 98% rename from server/src/domain/person/person.service.spec.ts rename to server/src/services/person.service.spec.ts index 9da66e868..10e42e1b6 100644 --- a/server/src/domain/person/person.service.spec.ts +++ b/server/src/services/person.service.spec.ts @@ -1,21 +1,20 @@ import { BadRequestException, NotFoundException } from '@nestjs/common'; -import { BulkIdErrorReason } from 'src/domain/asset/response-dto/asset-ids-response.dto'; -import { JobName } from 'src/domain/job/job.constants'; -import { PersonResponseDto, mapFaces, mapPerson } from 'src/domain/person/person.dto'; -import { PersonService } from 'src/domain/person/person.service'; +import { BulkIdErrorReason } from 'src/dtos/asset-ids.response.dto'; +import { PersonResponseDto, mapFaces, mapPerson } from 'src/dtos/person.dto'; import { AssetFaceEntity } from 'src/entities/asset-face.entity'; import { Colorspace, SystemConfigKey } from 'src/entities/system-config.entity'; -import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.repository'; -import { ICryptoRepository } from 'src/interfaces/crypto.repository'; -import { IJobRepository, JobStatus } from 'src/interfaces/job.repository'; -import { IMachineLearningRepository } from 'src/interfaces/machine-learning.repository'; -import { IMediaRepository } from 'src/interfaces/media.repository'; -import { IMoveRepository } from 'src/interfaces/move.repository'; -import { IPersonRepository } from 'src/interfaces/person.repository'; -import { FaceSearchResult, ISearchRepository } from 'src/interfaces/search.repository'; -import { IStorageRepository } from 'src/interfaces/storage.repository'; -import { ISystemConfigRepository } from 'src/interfaces/system-config.repository'; -import { CacheControl, ImmichFileResponse } from 'src/utils'; +import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface'; +import { ICryptoRepository } from 'src/interfaces/crypto.interface'; +import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface'; +import { IMachineLearningRepository } from 'src/interfaces/machine-learning.interface'; +import { IMediaRepository } from 'src/interfaces/media.interface'; +import { IMoveRepository } from 'src/interfaces/move.interface'; +import { IPersonRepository } from 'src/interfaces/person.interface'; +import { FaceSearchResult, ISearchRepository } from 'src/interfaces/search.interface'; +import { IStorageRepository } from 'src/interfaces/storage.interface'; +import { ISystemConfigRepository } from 'src/interfaces/system-config.interface'; +import { PersonService } from 'src/services/person.service'; +import { CacheControl, ImmichFileResponse } from 'src/utils/file'; import { assetStub } from 'test/fixtures/asset.stub'; import { authStub } from 'test/fixtures/auth.stub'; import { faceStub } from 'test/fixtures/face.stub'; diff --git a/server/src/domain/person/person.service.ts b/server/src/services/person.service.ts similarity index 94% rename from server/src/domain/person/person.service.ts rename to server/src/services/person.service.ts index cd7ba9224..504716a55 100644 --- a/server/src/domain/person/person.service.ts +++ b/server/src/services/person.service.ts @@ -1,14 +1,11 @@ import { BadRequestException, Inject, Injectable, NotFoundException } from '@nestjs/common'; +import { FACE_THUMBNAIL_SIZE } from 'src/constants'; import { AccessCore, Permission } from 'src/cores/access.core'; import { StorageCore } from 'src/cores/storage.core'; import { SystemConfigCore } from 'src/cores/system-config.core'; -import { BulkIdErrorReason, BulkIdResponseDto } from 'src/domain/asset/response-dto/asset-ids-response.dto'; -import { AssetResponseDto, mapAsset } from 'src/domain/asset/response-dto/asset-response.dto'; -import { AuthDto } from 'src/domain/auth/auth.dto'; -import { mimeTypes } from 'src/domain/domain.constant'; -import { JOBS_ASSET_PAGINATION_SIZE, JobName, QueueName } from 'src/domain/job/job.constants'; -import { IBaseJob, IDeferrableJob, IEntityJob } from 'src/domain/job/job.interface'; -import { FACE_THUMBNAIL_SIZE } from 'src/domain/media/media.constant'; +import { BulkIdErrorReason, BulkIdResponseDto } from 'src/dtos/asset-ids.response.dto'; +import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto'; +import { AuthDto } from 'src/dtos/auth.dto'; import { AssetFaceResponseDto, AssetFaceUpdateDto, @@ -23,22 +20,34 @@ import { PersonUpdateDto, mapFaces, mapPerson, -} from 'src/domain/person/person.dto'; +} from 'src/dtos/person.dto'; import { PersonPathType } from 'src/entities/move.entity'; import { PersonEntity } from 'src/entities/person.entity'; -import { ImmichLogger } from 'src/infra/logger'; -import { IAccessRepository } from 'src/interfaces/access.repository'; -import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.repository'; -import { ICryptoRepository } from 'src/interfaces/crypto.repository'; -import { IJobRepository, JobItem, JobStatus } from 'src/interfaces/job.repository'; -import { IMachineLearningRepository } from 'src/interfaces/machine-learning.repository'; -import { CropOptions, IMediaRepository } from 'src/interfaces/media.repository'; -import { IMoveRepository } from 'src/interfaces/move.repository'; -import { IPersonRepository, UpdateFacesData } from 'src/interfaces/person.repository'; -import { ISearchRepository } from 'src/interfaces/search.repository'; -import { IStorageRepository } from 'src/interfaces/storage.repository'; -import { ISystemConfigRepository } from 'src/interfaces/system-config.repository'; -import { CacheControl, ImmichFileResponse, usePagination } from 'src/utils'; +import { IAccessRepository } from 'src/interfaces/access.interface'; +import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface'; +import { ICryptoRepository } from 'src/interfaces/crypto.interface'; +import { + IBaseJob, + IDeferrableJob, + IEntityJob, + IJobRepository, + JOBS_ASSET_PAGINATION_SIZE, + JobItem, + JobName, + JobStatus, + QueueName, +} from 'src/interfaces/job.interface'; +import { IMachineLearningRepository } from 'src/interfaces/machine-learning.interface'; +import { CropOptions, IMediaRepository } from 'src/interfaces/media.interface'; +import { IMoveRepository } from 'src/interfaces/move.interface'; +import { IPersonRepository, UpdateFacesData } from 'src/interfaces/person.interface'; +import { ISearchRepository } from 'src/interfaces/search.interface'; +import { IStorageRepository } from 'src/interfaces/storage.interface'; +import { ISystemConfigRepository } from 'src/interfaces/system-config.interface'; +import { CacheControl, ImmichFileResponse } from 'src/utils/file'; +import { ImmichLogger } from 'src/utils/logger'; +import { mimeTypes } from 'src/utils/mime-types'; +import { usePagination } from 'src/utils/pagination'; import { IsNull } from 'typeorm'; @Injectable() diff --git a/server/src/domain/search/search.service.spec.ts b/server/src/services/search.service.spec.ts similarity index 92% rename from server/src/domain/search/search.service.spec.ts rename to server/src/services/search.service.spec.ts index f72212809..72b543f2d 100644 --- a/server/src/domain/search/search.service.spec.ts +++ b/server/src/services/search.service.spec.ts @@ -1,14 +1,14 @@ -import { mapAsset } from 'src/domain/asset/response-dto/asset-response.dto'; -import { SearchDto } from 'src/domain/search/dto/search.dto'; -import { SearchService } from 'src/domain/search/search.service'; +import { mapAsset } from 'src/dtos/asset-response.dto'; +import { SearchDto } from 'src/dtos/search.dto'; import { SystemConfigKey } from 'src/entities/system-config.entity'; -import { IAssetRepository } from 'src/interfaces/asset.repository'; -import { IMachineLearningRepository } from 'src/interfaces/machine-learning.repository'; -import { IMetadataRepository } from 'src/interfaces/metadata.repository'; -import { IPartnerRepository } from 'src/interfaces/partner.repository'; -import { IPersonRepository } from 'src/interfaces/person.repository'; -import { ISearchRepository } from 'src/interfaces/search.repository'; -import { ISystemConfigRepository } from 'src/interfaces/system-config.repository'; +import { IAssetRepository } from 'src/interfaces/asset.interface'; +import { IMachineLearningRepository } from 'src/interfaces/machine-learning.interface'; +import { IMetadataRepository } from 'src/interfaces/metadata.interface'; +import { IPartnerRepository } from 'src/interfaces/partner.interface'; +import { IPersonRepository } from 'src/interfaces/person.interface'; +import { ISearchRepository } from 'src/interfaces/search.interface'; +import { ISystemConfigRepository } from 'src/interfaces/system-config.interface'; +import { SearchService } from 'src/services/search.service'; import { assetStub } from 'test/fixtures/asset.stub'; import { authStub } from 'test/fixtures/auth.stub'; import { personStub } from 'test/fixtures/person.stub'; diff --git a/server/src/domain/search/search.service.ts b/server/src/services/search.service.ts similarity index 90% rename from server/src/domain/search/search.service.ts rename to server/src/services/search.service.ts index cedee68d0..03fa154a3 100644 --- a/server/src/domain/search/search.service.ts +++ b/server/src/services/search.service.ts @@ -1,28 +1,29 @@ import { Inject, Injectable } from '@nestjs/common'; import { FeatureFlag, SystemConfigCore } from 'src/cores/system-config.core'; -import { AssetResponseDto, mapAsset } from 'src/domain/asset/response-dto/asset-response.dto'; -import { AuthDto } from 'src/domain/auth/auth.dto'; -import { PersonResponseDto } from 'src/domain/person/person.dto'; -import { SearchSuggestionRequestDto, SearchSuggestionType } from 'src/domain/search/dto/search-suggestion.dto'; +import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto'; +import { AuthDto } from 'src/dtos/auth.dto'; +import { PersonResponseDto } from 'src/dtos/person.dto'; import { MetadataSearchDto, PlacesResponseDto, SearchDto, SearchPeopleDto, SearchPlacesDto, + SearchResponseDto, + SearchSuggestionRequestDto, + SearchSuggestionType, SmartSearchDto, mapPlaces, -} from 'src/domain/search/dto/search.dto'; -import { SearchResponseDto } from 'src/domain/search/response-dto/search-response.dto'; +} from 'src/dtos/search.dto'; import { AssetOrder } from 'src/entities/album.entity'; import { AssetEntity } from 'src/entities/asset.entity'; -import { IAssetRepository } from 'src/interfaces/asset.repository'; -import { IMachineLearningRepository } from 'src/interfaces/machine-learning.repository'; -import { IMetadataRepository } from 'src/interfaces/metadata.repository'; -import { IPartnerRepository } from 'src/interfaces/partner.repository'; -import { IPersonRepository } from 'src/interfaces/person.repository'; -import { ISearchRepository, SearchExploreItem, SearchStrategy } from 'src/interfaces/search.repository'; -import { ISystemConfigRepository } from 'src/interfaces/system-config.repository'; +import { IAssetRepository } from 'src/interfaces/asset.interface'; +import { IMachineLearningRepository } from 'src/interfaces/machine-learning.interface'; +import { IMetadataRepository } from 'src/interfaces/metadata.interface'; +import { IPartnerRepository } from 'src/interfaces/partner.interface'; +import { IPersonRepository } from 'src/interfaces/person.interface'; +import { ISearchRepository, SearchExploreItem, SearchStrategy } from 'src/interfaces/search.interface'; +import { ISystemConfigRepository } from 'src/interfaces/system-config.interface'; @Injectable() export class SearchService { diff --git a/server/src/domain/server-info/server-info.service.spec.ts b/server/src/services/server-info.service.spec.ts similarity index 95% rename from server/src/domain/server-info/server-info.service.spec.ts rename to server/src/services/server-info.service.spec.ts index 1685f8bda..bbb608b21 100644 --- a/server/src/domain/server-info/server-info.service.spec.ts +++ b/server/src/services/server-info.service.spec.ts @@ -1,12 +1,12 @@ -import { serverVersion } from 'src/domain/domain.constant'; -import { ServerInfoService } from 'src/domain/server-info/server-info.service'; +import { serverVersion } from 'src/constants'; import { SystemMetadataKey } from 'src/entities/system-metadata.entity'; -import { ICommunicationRepository } from 'src/interfaces/communication.repository'; -import { IServerInfoRepository } from 'src/interfaces/server-info.repository'; -import { IStorageRepository } from 'src/interfaces/storage.repository'; -import { ISystemConfigRepository } from 'src/interfaces/system-config.repository'; -import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.repository'; -import { IUserRepository } from 'src/interfaces/user.repository'; +import { ICommunicationRepository } from 'src/interfaces/communication.interface'; +import { IServerInfoRepository } from 'src/interfaces/server-info.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 { IUserRepository } from 'src/interfaces/user.interface'; +import { ServerInfoService } from 'src/services/server-info.service'; import { newCommunicationRepositoryMock } from 'test/repositories/communication.repository.mock'; import { newStorageRepositoryMock } from 'test/repositories/storage.repository.mock'; import { newSystemConfigRepositoryMock } from 'test/repositories/system-config.repository.mock'; diff --git a/server/src/domain/server-info/server-info.service.ts b/server/src/services/server-info.service.ts similarity index 93% rename from server/src/domain/server-info/server-info.service.ts rename to server/src/services/server-info.service.ts index 13921949c..94195fd4a 100644 --- a/server/src/domain/server-info/server-info.service.ts +++ b/server/src/services/server-info.service.ts @@ -1,8 +1,8 @@ import { Inject, Injectable } from '@nestjs/common'; import { DateTime } from 'luxon'; +import { isDev, serverVersion } from 'src/constants'; import { StorageCore, StorageFolder } from 'src/cores/storage.core'; import { SystemConfigCore } from 'src/cores/system-config.core'; -import { Version, isDev, mimeTypes, serverVersion } from 'src/domain/domain.constant'; import { ServerConfigDto, ServerFeaturesDto, @@ -11,16 +11,18 @@ import { ServerPingResponse, ServerStatsResponseDto, UsageByUserDto, -} from 'src/domain/server-info/server-info.dto'; +} from 'src/dtos/server-info.dto'; import { SystemMetadataKey } from 'src/entities/system-metadata.entity'; -import { ImmichLogger } from 'src/infra/logger'; -import { ClientEvent, ICommunicationRepository } from 'src/interfaces/communication.repository'; -import { IServerInfoRepository } from 'src/interfaces/server-info.repository'; -import { IStorageRepository } from 'src/interfaces/storage.repository'; -import { ISystemConfigRepository } from 'src/interfaces/system-config.repository'; -import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.repository'; -import { IUserRepository, UserStatsQueryResponse } from 'src/interfaces/user.repository'; -import { asHumanReadable } from 'src/utils'; +import { ClientEvent, ICommunicationRepository } from 'src/interfaces/communication.interface'; +import { IServerInfoRepository } from 'src/interfaces/server-info.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 { IUserRepository, UserStatsQueryResponse } from 'src/interfaces/user.interface'; +import { asHumanReadable } from 'src/utils/bytes'; +import { ImmichLogger } from 'src/utils/logger'; +import { mimeTypes } from 'src/utils/mime-types'; +import { Version } from 'src/utils/version'; @Injectable() export class ServerInfoService { diff --git a/server/src/domain/shared-link/shared-link.service.spec.ts b/server/src/services/shared-link.service.spec.ts similarity index 97% rename from server/src/domain/shared-link/shared-link.service.spec.ts rename to server/src/services/shared-link.service.spec.ts index 926ec16b1..cad52928c 100644 --- a/server/src/domain/shared-link/shared-link.service.spec.ts +++ b/server/src/services/shared-link.service.spec.ts @@ -1,10 +1,10 @@ import { BadRequestException, ForbiddenException, UnauthorizedException } from '@nestjs/common'; import _ from 'lodash'; -import { AssetIdErrorReason } from 'src/domain/asset/response-dto/asset-ids-response.dto'; -import { SharedLinkService } from 'src/domain/shared-link/shared-link.service'; +import { AssetIdErrorReason } from 'src/dtos/asset-ids.response.dto'; import { SharedLinkType } from 'src/entities/shared-link.entity'; -import { ICryptoRepository } from 'src/interfaces/crypto.repository'; -import { ISharedLinkRepository } from 'src/interfaces/shared-link.repository'; +import { ICryptoRepository } from 'src/interfaces/crypto.interface'; +import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface'; +import { SharedLinkService } from 'src/services/shared-link.service'; import { albumStub } from 'test/fixtures/album.stub'; import { assetStub } from 'test/fixtures/asset.stub'; import { authStub } from 'test/fixtures/auth.stub'; diff --git a/server/src/domain/shared-link/shared-link.service.ts b/server/src/services/shared-link.service.ts similarity index 92% rename from server/src/domain/shared-link/shared-link.service.ts rename to server/src/services/shared-link.service.ts index e01deee7b..cea0e8414 100644 --- a/server/src/domain/shared-link/shared-link.service.ts +++ b/server/src/services/shared-link.service.ts @@ -1,20 +1,22 @@ import { BadRequestException, ForbiddenException, Inject, Injectable, UnauthorizedException } from '@nestjs/common'; import { AccessCore, Permission } from 'src/cores/access.core'; -import { AssetIdsDto } from 'src/domain/asset/dto/asset-ids.dto'; -import { AssetIdErrorReason, AssetIdsResponseDto } from 'src/domain/asset/response-dto/asset-ids-response.dto'; -import { AuthDto } from 'src/domain/auth/auth.dto'; +import { AssetIdErrorReason, AssetIdsResponseDto } from 'src/dtos/asset-ids.response.dto'; +import { AssetIdsDto } from 'src/dtos/asset.dto'; +import { AuthDto } from 'src/dtos/auth.dto'; import { + SharedLinkCreateDto, + SharedLinkEditDto, + SharedLinkPasswordDto, SharedLinkResponseDto, mapSharedLink, mapSharedLinkWithoutMetadata, -} from 'src/domain/shared-link/shared-link-response.dto'; -import { SharedLinkCreateDto, SharedLinkEditDto, SharedLinkPasswordDto } from 'src/domain/shared-link/shared-link.dto'; +} from 'src/dtos/shared-link.dto'; import { AssetEntity } from 'src/entities/asset.entity'; import { SharedLinkEntity, SharedLinkType } from 'src/entities/shared-link.entity'; -import { IAccessRepository } from 'src/interfaces/access.repository'; -import { ICryptoRepository } from 'src/interfaces/crypto.repository'; -import { ISharedLinkRepository } from 'src/interfaces/shared-link.repository'; -import { OpenGraphTags } from 'src/utils'; +import { IAccessRepository } from 'src/interfaces/access.interface'; +import { ICryptoRepository } from 'src/interfaces/crypto.interface'; +import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface'; +import { OpenGraphTags } from 'src/utils/misc'; @Injectable() export class SharedLinkService { diff --git a/server/src/domain/smart-info/smart-info.service.spec.ts b/server/src/services/smart-info.service.spec.ts similarity index 86% rename from server/src/domain/smart-info/smart-info.service.spec.ts rename to server/src/services/smart-info.service.spec.ts index 29ce75fb4..81d7935c3 100644 --- a/server/src/domain/smart-info/smart-info.service.spec.ts +++ b/server/src/services/smart-info.service.spec.ts @@ -1,14 +1,13 @@ -import { JobName } from 'src/domain/job/job.constants'; -import { cleanModelName, getCLIPModelInfo } from 'src/domain/smart-info/smart-info.constant'; -import { SmartInfoService } from 'src/domain/smart-info/smart-info.service'; import { AssetEntity } from 'src/entities/asset.entity'; import { SystemConfigKey } from 'src/entities/system-config.entity'; -import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.repository'; -import { IDatabaseRepository } from 'src/interfaces/database.repository'; -import { IJobRepository } from 'src/interfaces/job.repository'; -import { IMachineLearningRepository } from 'src/interfaces/machine-learning.repository'; -import { ISearchRepository } from 'src/interfaces/search.repository'; -import { ISystemConfigRepository } from 'src/interfaces/system-config.repository'; +import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface'; +import { IDatabaseRepository } from 'src/interfaces/database.interface'; +import { IJobRepository, JobName } from 'src/interfaces/job.interface'; +import { IMachineLearningRepository } from 'src/interfaces/machine-learning.interface'; +import { ISearchRepository } from 'src/interfaces/search.interface'; +import { ISystemConfigRepository } from 'src/interfaces/system-config.interface'; +import { SmartInfoService } from 'src/services/smart-info.service'; +import { getCLIPModelInfo } from 'src/utils/misc'; import { assetStub } from 'test/fixtures/asset.stub'; import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock'; import { newDatabaseRepositoryMock } from 'test/repositories/database.repository.mock'; @@ -124,16 +123,14 @@ describe(SmartInfoService.name, () => { }); }); - describe('cleanModelName', () => { - it('should clean name', () => { - expect(cleanModelName('ViT-B-32::openai')).toEqual('ViT-B-32__openai'); - expect(cleanModelName('M-CLIP/XLM-Roberta-Large-Vit-L-14')).toEqual('XLM-Roberta-Large-Vit-L-14'); - }); - }); - describe('getCLIPModelInfo', () => { it('should return the model info', () => { expect(getCLIPModelInfo('ViT-B-32__openai')).toEqual({ dimSize: 512 }); + expect(getCLIPModelInfo('M-CLIP/XLM-Roberta-Large-Vit-L-14')).toEqual({ dimSize: 768 }); + }); + + it('should clean the model name', () => { + expect(getCLIPModelInfo('ViT-B-32::openai')).toEqual({ dimSize: 512 }); }); it('should throw an error if the model is not present', () => { diff --git a/server/src/domain/smart-info/smart-info.service.ts b/server/src/services/smart-info.service.ts similarity index 87% rename from server/src/domain/smart-info/smart-info.service.ts rename to server/src/services/smart-info.service.ts index a7d470008..fb19e90a7 100644 --- a/server/src/domain/smart-info/smart-info.service.ts +++ b/server/src/services/smart-info.service.ts @@ -1,15 +1,21 @@ import { Inject, Injectable } from '@nestjs/common'; import { SystemConfigCore } from 'src/cores/system-config.core'; -import { JOBS_ASSET_PAGINATION_SIZE, JobName, QueueName } from 'src/domain/job/job.constants'; -import { IBaseJob, IEntityJob } from 'src/domain/job/job.interface'; -import { ImmichLogger } from 'src/infra/logger'; -import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.repository'; -import { DatabaseLock, IDatabaseRepository } from 'src/interfaces/database.repository'; -import { IJobRepository, JobStatus } from 'src/interfaces/job.repository'; -import { IMachineLearningRepository } from 'src/interfaces/machine-learning.repository'; -import { ISearchRepository } from 'src/interfaces/search.repository'; -import { ISystemConfigRepository } from 'src/interfaces/system-config.repository'; -import { usePagination } from 'src/utils'; +import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface'; +import { DatabaseLock, IDatabaseRepository } from 'src/interfaces/database.interface'; +import { + IBaseJob, + IEntityJob, + IJobRepository, + JOBS_ASSET_PAGINATION_SIZE, + JobName, + JobStatus, + QueueName, +} from 'src/interfaces/job.interface'; +import { IMachineLearningRepository } from 'src/interfaces/machine-learning.interface'; +import { ISearchRepository } from 'src/interfaces/search.interface'; +import { ISystemConfigRepository } from 'src/interfaces/system-config.interface'; +import { ImmichLogger } from 'src/utils/logger'; +import { usePagination } from 'src/utils/pagination'; @Injectable() export class SmartInfoService { diff --git a/server/src/domain/storage-template/storage-template.service.spec.ts b/server/src/services/storage-template.service.spec.ts similarity index 97% rename from server/src/domain/storage-template/storage-template.service.spec.ts rename to server/src/services/storage-template.service.spec.ts index ecadb3e86..1254705ae 100644 --- a/server/src/domain/storage-template/storage-template.service.spec.ts +++ b/server/src/services/storage-template.service.spec.ts @@ -1,19 +1,19 @@ import { when } from 'jest-when'; import { Stats } from 'node:fs'; import { SystemConfigCore, defaults } from 'src/cores/system-config.core'; -import { StorageTemplateService } from 'src/domain/storage-template/storage-template.service'; import { AssetPathType } from 'src/entities/move.entity'; import { SystemConfig, SystemConfigKey } from 'src/entities/system-config.entity'; -import { IAlbumRepository } from 'src/interfaces/album.repository'; -import { IAssetRepository } from 'src/interfaces/asset.repository'; -import { ICryptoRepository } from 'src/interfaces/crypto.repository'; -import { IDatabaseRepository } from 'src/interfaces/database.repository'; -import { JobStatus } from 'src/interfaces/job.repository'; -import { IMoveRepository } from 'src/interfaces/move.repository'; -import { IPersonRepository } from 'src/interfaces/person.repository'; -import { IStorageRepository } from 'src/interfaces/storage.repository'; -import { ISystemConfigRepository } from 'src/interfaces/system-config.repository'; -import { IUserRepository } from 'src/interfaces/user.repository'; +import { IAlbumRepository } from 'src/interfaces/album.interface'; +import { IAssetRepository } from 'src/interfaces/asset.interface'; +import { ICryptoRepository } from 'src/interfaces/crypto.interface'; +import { IDatabaseRepository } from 'src/interfaces/database.interface'; +import { JobStatus } from 'src/interfaces/job.interface'; +import { IMoveRepository } from 'src/interfaces/move.interface'; +import { IPersonRepository } from 'src/interfaces/person.interface'; +import { IStorageRepository } from 'src/interfaces/storage.interface'; +import { ISystemConfigRepository } from 'src/interfaces/system-config.interface'; +import { IUserRepository } from 'src/interfaces/user.interface'; +import { StorageTemplateService } from 'src/services/storage-template.service'; import { assetStub } from 'test/fixtures/asset.stub'; import { userStub } from 'test/fixtures/user.stub'; import { newAlbumRepositoryMock } from 'test/repositories/album.repository.mock'; diff --git a/server/src/domain/storage-template/storage-template.service.ts b/server/src/services/storage-template.service.ts similarity index 91% rename from server/src/domain/storage-template/storage-template.service.ts rename to server/src/services/storage-template.service.ts index b4af03a9f..39a0196f2 100644 --- a/server/src/domain/storage-template/storage-template.service.ts +++ b/server/src/services/storage-template.service.ts @@ -1,13 +1,8 @@ import { Inject, Injectable } from '@nestjs/common'; -import { OnEvent } from '@nestjs/event-emitter'; import handlebar from 'handlebars'; import { DateTime } from 'luxon'; import path from 'node:path'; import sanitize from 'sanitize-filename'; -import { StorageCore, StorageFolder } from 'src/cores/storage.core'; -import { SystemConfigCore } from 'src/cores/system-config.core'; -import { JOBS_ASSET_PAGINATION_SIZE } from 'src/domain/job/job.constants'; -import { IEntityJob } from 'src/domain/job/job.interface'; import { supportedDayTokens, supportedHourTokens, @@ -16,23 +11,27 @@ import { supportedSecondTokens, supportedWeekTokens, supportedYearTokens, -} from 'src/domain/system-config/system-config.constants'; +} from 'src/constants'; +import { StorageCore, StorageFolder } from 'src/cores/storage.core'; +import { SystemConfigCore } from 'src/cores/system-config.core'; +import { OnEventInternal } from 'src/decorators'; import { AssetEntity, AssetType } from 'src/entities/asset.entity'; import { AssetPathType } from 'src/entities/move.entity'; import { SystemConfig } from 'src/entities/system-config.entity'; -import { ImmichLogger } from 'src/infra/logger'; -import { IAlbumRepository } from 'src/interfaces/album.repository'; -import { IAssetRepository } from 'src/interfaces/asset.repository'; -import { InternalEvent, InternalEventMap } from 'src/interfaces/communication.repository'; -import { ICryptoRepository } from 'src/interfaces/crypto.repository'; -import { DatabaseLock, IDatabaseRepository } from 'src/interfaces/database.repository'; -import { JobStatus } from 'src/interfaces/job.repository'; -import { IMoveRepository } from 'src/interfaces/move.repository'; -import { IPersonRepository } from 'src/interfaces/person.repository'; -import { IStorageRepository } from 'src/interfaces/storage.repository'; -import { ISystemConfigRepository } from 'src/interfaces/system-config.repository'; -import { IUserRepository } from 'src/interfaces/user.repository'; -import { getLivePhotoMotionFilename, usePagination } from 'src/utils'; +import { IAlbumRepository } from 'src/interfaces/album.interface'; +import { IAssetRepository } from 'src/interfaces/asset.interface'; +import { InternalEvent, InternalEventMap } from 'src/interfaces/communication.interface'; +import { ICryptoRepository } from 'src/interfaces/crypto.interface'; +import { DatabaseLock, IDatabaseRepository } from 'src/interfaces/database.interface'; +import { IEntityJob, JOBS_ASSET_PAGINATION_SIZE, JobStatus } from 'src/interfaces/job.interface'; +import { IMoveRepository } from 'src/interfaces/move.interface'; +import { IPersonRepository } from 'src/interfaces/person.interface'; +import { IStorageRepository } from 'src/interfaces/storage.interface'; +import { ISystemConfigRepository } from 'src/interfaces/system-config.interface'; +import { IUserRepository } from 'src/interfaces/user.interface'; +import { getLivePhotoMotionFilename } from 'src/utils/file'; +import { ImmichLogger } from 'src/utils/logger'; +import { usePagination } from 'src/utils/pagination'; export interface MoveAssetMetadata { storageLabel: string | null; @@ -87,7 +86,7 @@ export class StorageTemplateService { ); } - @OnEvent(InternalEvent.VALIDATE_CONFIG) + @OnEventInternal(InternalEvent.VALIDATE_CONFIG) validate({ newConfig }: InternalEventMap[InternalEvent.VALIDATE_CONFIG]) { try { const { compiled } = this.compile(newConfig.storageTemplate.template); diff --git a/server/src/domain/storage/storage.service.spec.ts b/server/src/services/storage.service.spec.ts similarity index 90% rename from server/src/domain/storage/storage.service.spec.ts rename to server/src/services/storage.service.spec.ts index 2791c9630..977f632d5 100644 --- a/server/src/domain/storage/storage.service.spec.ts +++ b/server/src/services/storage.service.spec.ts @@ -1,5 +1,5 @@ -import { StorageService } from 'src/domain/storage/storage.service'; -import { IStorageRepository } from 'src/interfaces/storage.repository'; +import { IStorageRepository } from 'src/interfaces/storage.interface'; +import { StorageService } from 'src/services/storage.service'; import { newStorageRepositoryMock } from 'test/repositories/storage.repository.mock'; describe(StorageService.name, () => { diff --git a/server/src/domain/storage/storage.service.ts b/server/src/services/storage.service.ts similarity index 77% rename from server/src/domain/storage/storage.service.ts rename to server/src/services/storage.service.ts index 9df3dd778..81fdb4f41 100644 --- a/server/src/domain/storage/storage.service.ts +++ b/server/src/services/storage.service.ts @@ -1,9 +1,8 @@ import { Inject, Injectable } from '@nestjs/common'; import { StorageCore, StorageFolder } from 'src/cores/storage.core'; -import { IDeleteFilesJob } from 'src/domain/job/job.interface'; -import { ImmichLogger } from 'src/infra/logger'; -import { JobStatus } from 'src/interfaces/job.repository'; -import { IStorageRepository } from 'src/interfaces/storage.repository'; +import { IDeleteFilesJob, JobStatus } from 'src/interfaces/job.interface'; +import { IStorageRepository } from 'src/interfaces/storage.interface'; +import { ImmichLogger } from 'src/utils/logger'; @Injectable() export class StorageService { diff --git a/server/src/domain/system-config/system-config.service.spec.ts b/server/src/services/system-config.service.spec.ts similarity index 97% rename from server/src/domain/system-config/system-config.service.spec.ts rename to server/src/services/system-config.service.spec.ts index f81d726eb..122708a63 100644 --- a/server/src/domain/system-config/system-config.service.spec.ts +++ b/server/src/services/system-config.service.spec.ts @@ -1,7 +1,5 @@ import { BadRequestException } from '@nestjs/common'; import { defaults } from 'src/cores/system-config.core'; -import { QueueName } from 'src/domain/job/job.constants'; -import { SystemConfigService } from 'src/domain/system-config/system-config.service'; import { AudioCodec, CQMode, @@ -15,10 +13,12 @@ import { TranscodePolicy, VideoCodec, } from 'src/entities/system-config.entity'; -import { ImmichLogger } from 'src/infra/logger'; -import { ICommunicationRepository, ServerEvent } from 'src/interfaces/communication.repository'; -import { ISearchRepository } from 'src/interfaces/search.repository'; -import { ISystemConfigRepository } from 'src/interfaces/system-config.repository'; +import { ICommunicationRepository, ServerEvent } from 'src/interfaces/communication.interface'; +import { QueueName } from 'src/interfaces/job.interface'; +import { ISearchRepository } from 'src/interfaces/search.interface'; +import { ISystemConfigRepository } from 'src/interfaces/system-config.interface'; +import { SystemConfigService } from 'src/services/system-config.service'; +import { ImmichLogger } from 'src/utils/logger'; import { newCommunicationRepositoryMock } from 'test/repositories/communication.repository.mock'; import { newSystemConfigRepositoryMock } from 'test/repositories/system-config.repository.mock'; diff --git a/server/src/domain/system-config/system-config.service.ts b/server/src/services/system-config.service.ts similarity index 89% rename from server/src/domain/system-config/system-config.service.ts rename to server/src/services/system-config.service.ts index d69593d21..5842342db 100644 --- a/server/src/domain/system-config/system-config.service.ts +++ b/server/src/services/system-config.service.ts @@ -1,10 +1,6 @@ import { BadRequestException, Inject, Injectable } from '@nestjs/common'; -import { OnEvent } from '@nestjs/event-emitter'; import { instanceToPlain } from 'class-transformer'; import _ from 'lodash'; -import { SystemConfigCore } from 'src/cores/system-config.core'; -import { SystemConfigDto, mapConfig } from 'src/domain/system-config/dto/system-config.dto'; -import { SystemConfigTemplateStorageOptionDto } from 'src/domain/system-config/response-dto/system-config-template-storage-option.dto'; import { supportedDayTokens, supportedHourTokens, @@ -14,18 +10,21 @@ import { supportedSecondTokens, supportedWeekTokens, supportedYearTokens, -} from 'src/domain/system-config/system-config.constants'; +} from 'src/constants'; +import { SystemConfigCore } from 'src/cores/system-config.core'; +import { OnEventInternal } from 'src/decorators'; +import { SystemConfigDto, SystemConfigTemplateStorageOptionDto, mapConfig } from 'src/dtos/system-config.dto'; import { LogLevel, SystemConfig } from 'src/entities/system-config.entity'; -import { ImmichLogger } from 'src/infra/logger'; import { ClientEvent, ICommunicationRepository, InternalEvent, InternalEventMap, ServerEvent, -} from 'src/interfaces/communication.repository'; -import { ISearchRepository } from 'src/interfaces/search.repository'; -import { ISystemConfigRepository } from 'src/interfaces/system-config.repository'; +} from 'src/interfaces/communication.interface'; +import { ISearchRepository } from 'src/interfaces/search.interface'; +import { ISystemConfigRepository } from 'src/interfaces/system-config.interface'; +import { ImmichLogger } from 'src/utils/logger'; @Injectable() export class SystemConfigService { @@ -61,7 +60,7 @@ export class SystemConfigService { return mapConfig(config); } - @OnEvent(InternalEvent.VALIDATE_CONFIG) + @OnEventInternal(InternalEvent.VALIDATE_CONFIG) validateConfig({ newConfig, oldConfig }: InternalEventMap[InternalEvent.VALIDATE_CONFIG]) { if (!_.isEqual(instanceToPlain(newConfig.logging), oldConfig.logging) && this.getEnvLogLevel()) { throw new Error('Logging cannot be changed while the environment variable LOG_LEVEL is set.'); diff --git a/server/src/domain/tag/tag.service.spec.ts b/server/src/services/tag.service.spec.ts similarity index 97% rename from server/src/domain/tag/tag.service.spec.ts rename to server/src/services/tag.service.spec.ts index 9f630cdc0..2d684616a 100644 --- a/server/src/domain/tag/tag.service.spec.ts +++ b/server/src/services/tag.service.spec.ts @@ -1,9 +1,9 @@ import { BadRequestException } from '@nestjs/common'; import { when } from 'jest-when'; -import { AssetIdErrorReason } from 'src/domain/asset/response-dto/asset-ids-response.dto'; -import { TagService } from 'src/domain/tag/tag.service'; +import { AssetIdErrorReason } from 'src/dtos/asset-ids.response.dto'; import { TagType } from 'src/entities/tag.entity'; -import { ITagRepository } from 'src/interfaces/tag.repository'; +import { ITagRepository } from 'src/interfaces/tag.interface'; +import { TagService } from 'src/services/tag.service'; import { assetStub } from 'test/fixtures/asset.stub'; import { authStub } from 'test/fixtures/auth.stub'; import { tagResponseStub, tagStub } from 'test/fixtures/tag.stub'; diff --git a/server/src/domain/tag/tag.service.ts b/server/src/services/tag.service.ts similarity index 85% rename from server/src/domain/tag/tag.service.ts rename to server/src/services/tag.service.ts index b04e251f7..c04f9b14c 100644 --- a/server/src/domain/tag/tag.service.ts +++ b/server/src/services/tag.service.ts @@ -1,11 +1,10 @@ import { BadRequestException, Inject, Injectable } from '@nestjs/common'; -import { AssetIdsDto } from 'src/domain/asset/dto/asset-ids.dto'; -import { AssetIdErrorReason, AssetIdsResponseDto } from 'src/domain/asset/response-dto/asset-ids-response.dto'; -import { AssetResponseDto, mapAsset } from 'src/domain/asset/response-dto/asset-response.dto'; -import { AuthDto } from 'src/domain/auth/auth.dto'; -import { TagResponseDto, mapTag } from 'src/domain/tag/tag-response.dto'; -import { CreateTagDto, UpdateTagDto } from 'src/domain/tag/tag.dto'; -import { ITagRepository } from 'src/interfaces/tag.repository'; +import { AssetIdErrorReason, AssetIdsResponseDto } from 'src/dtos/asset-ids.response.dto'; +import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto'; +import { AssetIdsDto } from 'src/dtos/asset.dto'; +import { AuthDto } from 'src/dtos/auth.dto'; +import { CreateTagDto, TagResponseDto, UpdateTagDto, mapTag } from 'src/dtos/tag.dto'; +import { ITagRepository } from 'src/interfaces/tag.interface'; @Injectable() export class TagService { diff --git a/server/src/domain/trash/trash.service.spec.ts b/server/src/services/trash.service.spec.ts similarity index 92% rename from server/src/domain/trash/trash.service.spec.ts rename to server/src/services/trash.service.spec.ts index 021c8e526..e43926e4d 100644 --- a/server/src/domain/trash/trash.service.spec.ts +++ b/server/src/services/trash.service.spec.ts @@ -1,9 +1,8 @@ import { BadRequestException } from '@nestjs/common'; -import { JobName } from 'src/domain/job/job.constants'; -import { TrashService } from 'src/domain/trash/trash.service'; -import { IAssetRepository } from 'src/interfaces/asset.repository'; -import { ClientEvent, ICommunicationRepository } from 'src/interfaces/communication.repository'; -import { IJobRepository } from 'src/interfaces/job.repository'; +import { IAssetRepository } from 'src/interfaces/asset.interface'; +import { ClientEvent, ICommunicationRepository } from 'src/interfaces/communication.interface'; +import { IJobRepository, JobName } from 'src/interfaces/job.interface'; +import { TrashService } from 'src/services/trash.service'; import { assetStub } from 'test/fixtures/asset.stub'; import { authStub } from 'test/fixtures/auth.stub'; import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock'; diff --git a/server/src/domain/trash/trash.service.ts b/server/src/services/trash.service.ts similarity index 80% rename from server/src/domain/trash/trash.service.ts rename to server/src/services/trash.service.ts index 2043b4248..5f1ee29f7 100644 --- a/server/src/domain/trash/trash.service.ts +++ b/server/src/services/trash.service.ts @@ -1,14 +1,13 @@ import { Inject } from '@nestjs/common'; import { DateTime } from 'luxon'; import { AccessCore, Permission } from 'src/cores/access.core'; -import { BulkIdsDto } from 'src/domain/asset/response-dto/asset-ids-response.dto'; -import { AuthDto } from 'src/domain/auth/auth.dto'; -import { JOBS_ASSET_PAGINATION_SIZE, JobName } from 'src/domain/job/job.constants'; -import { IAccessRepository } from 'src/interfaces/access.repository'; -import { IAssetRepository } from 'src/interfaces/asset.repository'; -import { ClientEvent, ICommunicationRepository } from 'src/interfaces/communication.repository'; -import { IJobRepository } from 'src/interfaces/job.repository'; -import { usePagination } from 'src/utils'; +import { BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; +import { AuthDto } from 'src/dtos/auth.dto'; +import { IAccessRepository } from 'src/interfaces/access.interface'; +import { IAssetRepository } from 'src/interfaces/asset.interface'; +import { ClientEvent, ICommunicationRepository } from 'src/interfaces/communication.interface'; +import { IJobRepository, JOBS_ASSET_PAGINATION_SIZE, JobName } from 'src/interfaces/job.interface'; +import { usePagination } from 'src/utils/pagination'; export class TrashService { private access: AccessCore; diff --git a/server/src/domain/user/user.service.spec.ts b/server/src/services/user.service.spec.ts similarity index 96% rename from server/src/domain/user/user.service.spec.ts rename to server/src/services/user.service.spec.ts index 5b9a1de95..973f644d3 100644 --- a/server/src/domain/user/user.service.spec.ts +++ b/server/src/services/user.service.spec.ts @@ -5,19 +5,17 @@ import { NotFoundException, } from '@nestjs/common'; import { when } from 'jest-when'; -import { JobName } from 'src/domain/job/job.constants'; -import { UpdateUserDto } from 'src/domain/user/dto/update-user.dto'; -import { mapUser } from 'src/domain/user/response-dto/user-response.dto'; -import { UserService } from 'src/domain/user/user.service'; +import { UpdateUserDto, mapUser } from 'src/dtos/user.dto'; import { UserEntity, UserStatus } from 'src/entities/user.entity'; -import { IAlbumRepository } from 'src/interfaces/album.repository'; -import { ICryptoRepository } from 'src/interfaces/crypto.repository'; -import { IJobRepository } from 'src/interfaces/job.repository'; -import { ILibraryRepository } from 'src/interfaces/library.repository'; -import { IStorageRepository } from 'src/interfaces/storage.repository'; -import { ISystemConfigRepository } from 'src/interfaces/system-config.repository'; -import { IUserRepository } from 'src/interfaces/user.repository'; -import { CacheControl, ImmichFileResponse } from 'src/utils'; +import { IAlbumRepository } from 'src/interfaces/album.interface'; +import { ICryptoRepository } from 'src/interfaces/crypto.interface'; +import { IJobRepository, JobName } from 'src/interfaces/job.interface'; +import { ILibraryRepository } from 'src/interfaces/library.interface'; +import { IStorageRepository } from 'src/interfaces/storage.interface'; +import { ISystemConfigRepository } from 'src/interfaces/system-config.interface'; +import { IUserRepository } from 'src/interfaces/user.interface'; +import { UserService } from 'src/services/user.service'; +import { CacheControl, ImmichFileResponse } from 'src/utils/file'; import { authStub } from 'test/fixtures/auth.stub'; import { systemConfigStub } from 'test/fixtures/system-config.stub'; import { userStub } from 'test/fixtures/user.stub'; diff --git a/server/src/domain/user/user.service.ts b/server/src/services/user.service.ts similarity index 87% rename from server/src/domain/user/user.service.ts rename to server/src/services/user.service.ts index 349a775ec..6649927da 100644 --- a/server/src/domain/user/user.service.ts +++ b/server/src/services/user.service.ts @@ -4,27 +4,19 @@ import { randomBytes } from 'node:crypto'; import { StorageCore, StorageFolder } from 'src/cores/storage.core'; import { SystemConfigCore } from 'src/cores/system-config.core'; import { UserCore } from 'src/cores/user.core'; -import { AuthDto } from 'src/domain/auth/auth.dto'; -import { JobName } from 'src/domain/job/job.constants'; -import { IEntityJob } from 'src/domain/job/job.interface'; -import { CreateUserDto } from 'src/domain/user/dto/create-user.dto'; -import { DeleteUserDto } from 'src/domain/user/dto/delete-user.dto'; -import { UpdateUserDto } from 'src/domain/user/dto/update-user.dto'; -import { - CreateProfileImageResponseDto, - mapCreateProfileImageResponse, -} from 'src/domain/user/response-dto/create-profile-image-response.dto'; -import { UserResponseDto, mapUser } from 'src/domain/user/response-dto/user-response.dto'; +import { AuthDto } from 'src/dtos/auth.dto'; +import { CreateProfileImageResponseDto, mapCreateProfileImageResponse } from 'src/dtos/user-profile.dto'; +import { CreateUserDto, DeleteUserDto, UpdateUserDto, UserResponseDto, mapUser } from 'src/dtos/user.dto'; import { UserEntity, UserStatus } from 'src/entities/user.entity'; -import { ImmichLogger } from 'src/infra/logger'; -import { IAlbumRepository } from 'src/interfaces/album.repository'; -import { ICryptoRepository } from 'src/interfaces/crypto.repository'; -import { IJobRepository, JobStatus } from 'src/interfaces/job.repository'; -import { ILibraryRepository } from 'src/interfaces/library.repository'; -import { IStorageRepository } from 'src/interfaces/storage.repository'; -import { ISystemConfigRepository } from 'src/interfaces/system-config.repository'; -import { IUserRepository, UserFindOptions } from 'src/interfaces/user.repository'; -import { CacheControl, ImmichFileResponse } from 'src/utils'; +import { IAlbumRepository } from 'src/interfaces/album.interface'; +import { ICryptoRepository } from 'src/interfaces/crypto.interface'; +import { IEntityJob, IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface'; +import { ILibraryRepository } from 'src/interfaces/library.interface'; +import { IStorageRepository } from 'src/interfaces/storage.interface'; +import { ISystemConfigRepository } from 'src/interfaces/system-config.interface'; +import { IUserRepository, UserFindOptions } from 'src/interfaces/user.interface'; +import { CacheControl, ImmichFileResponse } from 'src/utils/file'; +import { ImmichLogger } from 'src/utils/logger'; @Injectable() export class UserService { diff --git a/server/src/utils.spec.ts b/server/src/utils.spec.ts deleted file mode 100644 index c5ae5f6e5..000000000 --- a/server/src/utils.spec.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { isDecimalNumber, isNumberInRange, parseLatitude, parseLongitude, toNumberOrNull } from 'src/utils'; - -describe('checks if a number is a decimal number', () => { - it('returns false for non-decimal numbers', () => { - expect(isDecimalNumber(Number.NaN)).toBe(false); - expect(isDecimalNumber(Number.POSITIVE_INFINITY)).toBe(false); - expect(isDecimalNumber(Number.NEGATIVE_INFINITY)).toBe(false); - }); - - it('returns true for decimal numbers', () => { - expect(isDecimalNumber(0)).toBe(true); - expect(isDecimalNumber(-0)).toBe(true); - expect(isDecimalNumber(10.123_45)).toBe(true); - expect(isDecimalNumber(Number.MAX_VALUE)).toBe(true); - expect(isDecimalNumber(Number.MIN_VALUE)).toBe(true); - }); -}); - -describe('checks if a number is within a range', () => { - it('returns false for numbers outside the range', () => { - expect(isNumberInRange(0, 10, 10)).toBe(false); - expect(isNumberInRange(0.01, 10, 10)).toBe(false); - expect(isNumberInRange(50.1, 0, 50)).toBe(false); - }); - - it('returns true for numbers inside the range', () => { - expect(isNumberInRange(0, 0, 50)).toBe(true); - expect(isNumberInRange(50, 0, 50)).toBe(true); - expect(isNumberInRange(-50.123_45, -50.123_45, 0)).toBe(true); - }); -}); - -describe('converts input to a number or null', () => { - it('returns null for invalid inputs', () => { - expect(toNumberOrNull(null)).toBeNull(); - // eslint-disable-next-line unicorn/no-useless-undefined - expect(toNumberOrNull(undefined)).toBeNull(); - expect(toNumberOrNull('')).toBeNull(); - expect(toNumberOrNull(Number.NaN)).toBeNull(); - }); - - it('returns a number for valid inputs', () => { - expect(toNumberOrNull(0)).toBeCloseTo(0); - expect(toNumberOrNull('0')).toBeCloseTo(0); - expect(toNumberOrNull('-123.45')).toBeCloseTo(-123.45); - }); -}); - -describe('parsing latitude from string input', () => { - it('returns null for invalid inputs', () => { - expect(parseLatitude('')).toBeNull(); - expect(parseLatitude('NaN')).toBeNull(); - expect(parseLatitude('Infinity')).toBeNull(); - expect(parseLatitude('-Infinity')).toBeNull(); - expect(parseLatitude('90.001')).toBeNull(); - expect(parseLatitude(-90.000_001)).toBeNull(); - expect(parseLatitude('1000')).toBeNull(); - expect(parseLatitude(-1000)).toBeNull(); - }); - - it('returns the numeric coordinate for valid inputs', () => { - expect(parseLatitude('90')).toBeCloseTo(90); - expect(parseLatitude('-90')).toBeCloseTo(-90); - expect(parseLatitude(89.999_999)).toBeCloseTo(89.999_999); - expect(parseLatitude('-89.9')).toBeCloseTo(-89.9); - expect(parseLatitude(0)).toBeCloseTo(0); - expect(parseLatitude('-0.0')).toBeCloseTo(-0); - }); -}); - -describe('parsing latitude from null input', () => { - it('returns null for null input', () => { - expect(parseLatitude(null)).toBeNull(); - }); -}); - -describe('parsing longitude from string input', () => { - it('returns null for invalid inputs', () => { - expect(parseLongitude('')).toBeNull(); - expect(parseLongitude('NaN')).toBeNull(); - expect(parseLongitude(Number.POSITIVE_INFINITY)).toBeNull(); - expect(parseLongitude('-Infinity')).toBeNull(); - expect(parseLongitude('180.001')).toBeNull(); - expect(parseLongitude('-180.000001')).toBeNull(); - expect(parseLongitude(1000)).toBeNull(); - expect(parseLongitude('-1000')).toBeNull(); - }); - - it('returns the numeric coordinate for valid inputs', () => { - expect(parseLongitude(180)).toBeCloseTo(180); - expect(parseLongitude('-180')).toBeCloseTo(-180); - expect(parseLongitude('179.999999')).toBeCloseTo(179.999_999); - expect(parseLongitude(-179.9)).toBeCloseTo(-179.9); - expect(parseLongitude('0')).toBeCloseTo(0); - expect(parseLongitude('-0.0')).toBeCloseTo(-0); - }); -}); - -describe('parsing longitude from null input', () => { - it('returns null for null input', () => { - expect(parseLongitude(null)).toBeNull(); - }); -}); diff --git a/server/src/utils.ts b/server/src/utils.ts deleted file mode 100644 index 863dde7ca..000000000 --- a/server/src/utils.ts +++ /dev/null @@ -1,181 +0,0 @@ -import { basename, extname } from 'node:path'; -import { ImmichLogger } from 'src/infra/logger'; - -const KiB = Math.pow(1024, 1); -const MiB = Math.pow(1024, 2); -const GiB = Math.pow(1024, 3); -const TiB = Math.pow(1024, 4); -const PiB = Math.pow(1024, 5); - -export const HumanReadableSize = { KiB, MiB, GiB, TiB, PiB }; - -export function asHumanReadable(bytes: number, precision = 1): string { - const units = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB']; - - let magnitude = 0; - let remainder = bytes; - while (remainder >= 1024) { - if (magnitude + 1 < units.length) { - magnitude++; - remainder /= 1024; - } else { - break; - } - } - - return `${remainder.toFixed(magnitude == 0 ? 0 : precision)} ${units[magnitude]}`; -} - -export const isConnectionAborted = (error: Error | any) => error.code === 'ECONNABORTED'; - -export function isDecimalNumber(number_: number): boolean { - return !Number.isNaN(number_) && Number.isFinite(number_); -} - -/** - * Check if `num` is a valid number and is between `start` and `end` (inclusive) - */ -export function isNumberInRange(number_: number, start: number, end: number): boolean { - return isDecimalNumber(number_) && number_ >= start && number_ <= end; -} - -export function toNumberOrNull(input: number | string | null | undefined): number | null { - if (input === null || input === undefined) { - return null; - } - - const number_ = typeof input === 'string' ? Number.parseFloat(input) : input; - return isDecimalNumber(number_) ? number_ : null; -} - -export function parseLatitude(input: string | number | null): number | null { - if (input === null) { - return null; - } - const latitude = typeof input === 'string' ? Number.parseFloat(input) : input; - - if (isNumberInRange(latitude, -90, 90)) { - return latitude; - } - return null; -} - -export function parseLongitude(input: string | number | null): number | null { - if (input === null) { - return null; - } - - const longitude = typeof input === 'string' ? Number.parseFloat(input) : input; - - if (isNumberInRange(longitude, -180, 180)) { - return longitude; - } - return null; -} - -// NOTE: The following Set utils have been added here, to easily determine where they are used. -// They should be replaced with native Set operations, when they are added to the language. -// Proposal reference: https://github.com/tc39/proposal-set-methods - -export const setUnion = (...sets: Set[]): Set => { - const union = new Set(sets[0]); - for (const set of sets.slice(1)) { - for (const element of set) { - union.add(element); - } - } - return union; -}; - -export const setDifference = (setA: Set, ...sets: Set[]): Set => { - const difference = new Set(setA); - for (const set of sets) { - for (const element of set) { - difference.delete(element); - } - } - return difference; -}; - -export const setIsSuperset = (set: Set, subset: Set): boolean => { - for (const element of subset) { - if (!set.has(element)) { - return false; - } - } - return true; -}; - -export const setIsEqual = (setA: Set, setB: Set): boolean => { - return setA.size === setB.size && setIsSuperset(setA, setB); -}; - -export const handlePromiseError = (promise: Promise, logger: ImmichLogger): void => { - promise.catch((error: Error | any) => logger.error(`Promise error: ${error}`, error?.stack)); -}; - -export enum CacheControl { - PRIVATE_WITH_CACHE = 'private_with_cache', - PRIVATE_WITHOUT_CACHE = 'private_without_cache', - NONE = 'none', -} - -export class ImmichFileResponse { - public readonly path!: string; - public readonly contentType!: string; - public readonly cacheControl!: CacheControl; - - constructor(response: ImmichFileResponse) { - Object.assign(this, response); - } -} - -export interface OpenGraphTags { - title: string; - description: string; - imageUrl?: string; -} - -export function getFileNameWithoutExtension(path: string): string { - return basename(path, extname(path)); -} - -export function getLivePhotoMotionFilename(stillName: string, motionName: string) { - return getFileNameWithoutExtension(stillName) + extname(motionName); -} - -export interface PaginationOptions { - take: number; - skip?: number; -} - -export enum PaginationMode { - LIMIT_OFFSET = 'limit-offset', - SKIP_TAKE = 'skip-take', -} - -export interface PaginatedBuilderOptions { - take: number; - skip?: number; - mode?: PaginationMode; -} - -export interface PaginationResult { - items: T[]; - hasNextPage: boolean; -} - -export type Paginated = Promise>; - -export async function* usePagination( - pageSize: number, - getNextPage: (pagination: PaginationOptions) => PaginationResult | Paginated, -) { - let hasNextPage = true; - - for (let skip = 0; hasNextPage; skip += pageSize) { - const result = await getNextPage({ take: pageSize, skip }); - hasNextPage = result.hasNextPage; - yield result.items; - } -} diff --git a/server/src/utils/bytes.ts b/server/src/utils/bytes.ts new file mode 100644 index 000000000..e837c81b9 --- /dev/null +++ b/server/src/utils/bytes.ts @@ -0,0 +1,24 @@ +const KiB = Math.pow(1024, 1); +const MiB = Math.pow(1024, 2); +const GiB = Math.pow(1024, 3); +const TiB = Math.pow(1024, 4); +const PiB = Math.pow(1024, 5); + +export const HumanReadableSize = { KiB, MiB, GiB, TiB, PiB }; + +export function asHumanReadable(bytes: number, precision = 1): string { + const units = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB']; + + let magnitude = 0; + let remainder = bytes; + while (remainder >= 1024) { + if (magnitude + 1 < units.length) { + magnitude++; + remainder /= 1024; + } else { + break; + } + } + + return `${remainder.toFixed(magnitude == 0 ? 0 : precision)} ${units[magnitude]}`; +} diff --git a/server/src/infra/infra.utils.ts b/server/src/utils/database.ts similarity index 75% rename from server/src/infra/infra.utils.ts rename to server/src/utils/database.ts index 2b75af379..7ec677756 100644 --- a/server/src/infra/infra.utils.ts +++ b/server/src/utils/database.ts @@ -1,18 +1,7 @@ import _ from 'lodash'; import { AssetEntity } from 'src/entities/asset.entity'; -import { AssetSearchBuilderOptions } from 'src/interfaces/search.repository'; -import { Paginated, PaginatedBuilderOptions, PaginationMode, PaginationOptions, PaginationResult } from 'src/utils'; -import { - Between, - FindManyOptions, - IsNull, - LessThanOrEqual, - MoreThanOrEqual, - Not, - ObjectLiteral, - Repository, - SelectQueryBuilder, -} from 'typeorm'; +import { AssetSearchBuilderOptions } from 'src/interfaces/search.interface'; +import { Between, IsNull, LessThanOrEqual, MoreThanOrEqual, Not, SelectQueryBuilder } from 'typeorm'; /** * Allows optional values unlike the regular Between and uses MoreThanOrEqual @@ -28,47 +17,6 @@ export function OptionalBetween(from?: T, to?: T) { } } -function paginationHelper(items: Entity[], take: number): PaginationResult { - const hasNextPage = items.length > take; - items.splice(take); - - return { items, hasNextPage }; -} - -export async function paginate( - repository: Repository, - { take, skip }: PaginationOptions, - searchOptions?: FindManyOptions, -): Paginated { - const items = await repository.find( - _.omitBy( - { - ...searchOptions, - // Take one more item to check if there's a next page - take: take + 1, - skip, - }, - _.isUndefined, - ), - ); - - return paginationHelper(items, take); -} - -export async function paginatedBuilder( - qb: SelectQueryBuilder, - { take, skip, mode }: PaginatedBuilderOptions, -): Paginated { - if (mode === PaginationMode.LIMIT_OFFSET) { - qb.limit(take + 1).offset(skip); - } else { - qb.take(take + 1).skip(skip); - } - - const items = await qb.getMany(); - return paginationHelper(items, take); -} - export const asVector = (embedding: number[], quote = false) => quote ? `'[${embedding.join(',')}]'` : `[${embedding.join(',')}]`; diff --git a/server/src/utils/file.ts b/server/src/utils/file.ts new file mode 100644 index 000000000..a80f17bea --- /dev/null +++ b/server/src/utils/file.ts @@ -0,0 +1,88 @@ +import { HttpException, StreamableFile } from '@nestjs/common'; +import { NextFunction, Response } from 'express'; +import { access, constants } from 'node:fs/promises'; +import { basename, extname, isAbsolute } from 'node:path'; +import { promisify } from 'node:util'; +import { ImmichReadStream } from 'src/interfaces/storage.interface'; +import { ImmichLogger } from 'src/utils/logger'; +import { isConnectionAborted } from 'src/utils/misc'; + +export function getFileNameWithoutExtension(path: string): string { + return basename(path, extname(path)); +} + +export function getLivePhotoMotionFilename(stillName: string, motionName: string) { + return getFileNameWithoutExtension(stillName) + extname(motionName); +} + +export enum CacheControl { + PRIVATE_WITH_CACHE = 'private_with_cache', + PRIVATE_WITHOUT_CACHE = 'private_without_cache', + NONE = 'none', +} + +export class ImmichFileResponse { + public readonly path!: string; + public readonly contentType!: string; + public readonly cacheControl!: CacheControl; + + constructor(response: ImmichFileResponse) { + Object.assign(this, response); + } +} +type SendFile = Parameters; +type SendFileOptions = SendFile[1]; + +const logger = new ImmichLogger('SendFile'); + +export const sendFile = async ( + res: Response, + next: NextFunction, + handler: () => Promise, +): Promise => { + const _sendFile = (path: string, options: SendFileOptions) => + promisify(res.sendFile).bind(res)(path, options); + + try { + const file = await handler(); + switch (file.cacheControl) { + case CacheControl.PRIVATE_WITH_CACHE: { + res.set('Cache-Control', 'private, max-age=86400, no-transform'); + break; + } + + case CacheControl.PRIVATE_WITHOUT_CACHE: { + res.set('Cache-Control', 'private, no-cache, no-transform'); + break; + } + } + + res.header('Content-Type', file.contentType); + + const options: SendFileOptions = { dotfiles: 'allow' }; + if (!isAbsolute(file.path)) { + options.root = process.cwd(); + } + + await access(file.path, constants.R_OK); + + return _sendFile(file.path, options); + } catch (error: Error | any) { + // ignore client-closed connection + if (isConnectionAborted(error)) { + return; + } + + // log non-http errors + if (error instanceof HttpException === false) { + logger.error(`Unable to send file: ${error.name}`, error.stack); + } + + res.header('Cache-Control', 'none'); + next(error); + } +}; + +export const asStreamableFile = ({ stream, type, length }: ImmichReadStream) => { + return new StreamableFile(stream, { type, length }); +}; diff --git a/server/src/infra/instrumentation.ts b/server/src/utils/instrumentation.ts similarity index 97% rename from server/src/infra/instrumentation.ts rename to server/src/utils/instrumentation.ts index a30f0523a..12d44aeac 100644 --- a/server/src/infra/instrumentation.ts +++ b/server/src/utils/instrumentation.ts @@ -13,9 +13,8 @@ import { snakeCase, startCase } from 'lodash'; import { OpenTelemetryModuleOptions } from 'nestjs-otel/lib/interfaces'; import { copyMetadataFromFunctionToFunction } from 'nestjs-otel/lib/opentelemetry.utils'; import { performance } from 'node:perf_hooks'; -import { excludePaths } from 'src/config'; +import { excludePaths, serverVersion } from 'src/constants'; import { DecorateAll } from 'src/decorators'; -import { serverVersion } from 'src/domain/domain.constant'; let metricsEnabled = process.env.IMMICH_METRICS === 'true'; const hostMetrics = diff --git a/server/src/infra/logger.ts b/server/src/utils/logger.ts similarity index 100% rename from server/src/infra/logger.ts rename to server/src/utils/logger.ts diff --git a/server/src/domain/media/media.util.ts b/server/src/utils/media.ts similarity index 99% rename from server/src/domain/media/media.util.ts rename to server/src/utils/media.ts index 3e58ca81a..5f1218766 100644 --- a/server/src/domain/media/media.util.ts +++ b/server/src/utils/media.ts @@ -1,4 +1,4 @@ -import { SystemConfigFFmpegDto } from 'src/domain/system-config/dto/system-config-ffmpeg.dto'; +import { SystemConfigFFmpegDto } from 'src/dtos/system-config.dto'; import { CQMode, ToneMapping, TranscodeHWAccel, TranscodeTarget, VideoCodec } from 'src/entities/system-config.entity'; import { AudioStreamInfo, @@ -7,7 +7,7 @@ import { VideoCodecHWConfig, VideoCodecSWConfig, VideoStreamInfo, -} from 'src/interfaces/media.repository'; +} from 'src/interfaces/media.interface'; class BaseConfig implements VideoCodecSWConfig { presets = ['veryslow', 'slower', 'slow', 'medium', 'fast', 'faster', 'veryfast', 'superfast', 'ultrafast']; diff --git a/server/src/domain/domain.constant.spec.ts b/server/src/utils/mime-types.spec.ts similarity index 68% rename from server/src/domain/domain.constant.spec.ts rename to server/src/utils/mime-types.spec.ts index e8a1c7b72..96c1921ba 100644 --- a/server/src/domain/domain.constant.spec.ts +++ b/server/src/utils/mime-types.spec.ts @@ -1,4 +1,4 @@ -import { mimeTypes, Version, VersionType } from 'src/domain/domain.constant'; +import { mimeTypes } from 'src/utils/mime-types'; describe('mimeTypes', () => { for (const { mimetype, extension } of [ @@ -196,74 +196,3 @@ describe('mimeTypes', () => { } }); }); - -describe('Version', () => { - const tests = [ - { this: new Version(0, 0, 1), other: new Version(0, 0, 0), compare: 1, type: VersionType.PATCH }, - { this: new Version(0, 1, 0), other: new Version(0, 0, 0), compare: 1, type: VersionType.MINOR }, - { this: new Version(1, 0, 0), other: new Version(0, 0, 0), compare: 1, type: VersionType.MAJOR }, - { this: new Version(0, 0, 0), other: new Version(0, 0, 1), compare: -1, type: VersionType.PATCH }, - { this: new Version(0, 0, 0), other: new Version(0, 1, 0), compare: -1, type: VersionType.MINOR }, - { this: new Version(0, 0, 0), other: new Version(1, 0, 0), compare: -1, type: VersionType.MAJOR }, - { this: new Version(0, 0, 0), other: new Version(0, 0, 0), compare: 0, type: VersionType.EQUAL }, - { this: new Version(0, 0, 1), other: new Version(0, 0, 1), compare: 0, type: VersionType.EQUAL }, - { this: new Version(0, 1, 0), other: new Version(0, 1, 0), compare: 0, type: VersionType.EQUAL }, - { this: new Version(1, 0, 0), other: new Version(1, 0, 0), compare: 0, type: VersionType.EQUAL }, - { this: new Version(1, 0), other: new Version(1, 0, 0), compare: 0, type: VersionType.EQUAL }, - { this: new Version(1, 0), other: new Version(1, 0, 1), compare: -1, type: VersionType.PATCH }, - { this: new Version(1, 1), other: new Version(1, 0, 1), compare: 1, type: VersionType.MINOR }, - { this: new Version(1), other: new Version(1, 0, 0), compare: 0, type: VersionType.EQUAL }, - { this: new Version(1), other: new Version(1, 0, 1), compare: -1, type: VersionType.PATCH }, - ]; - - describe('isOlderThan', () => { - for (const { this: thisVersion, other: otherVersion, compare, type } of tests) { - const expected = compare < 0 ? type : VersionType.EQUAL; - it(`should return '${expected}' when comparing ${thisVersion} to ${otherVersion}`, () => { - expect(thisVersion.isOlderThan(otherVersion)).toEqual(expected); - }); - } - }); - - describe('isEqual', () => { - for (const { this: thisVersion, other: otherVersion, compare } of tests) { - const bool = compare === 0; - it(`should return ${bool} when comparing ${thisVersion} to ${otherVersion}`, () => { - expect(thisVersion.isEqual(otherVersion)).toEqual(bool); - }); - } - }); - - describe('isNewerThan', () => { - for (const { this: thisVersion, other: otherVersion, compare, type } of tests) { - const expected = compare > 0 ? type : VersionType.EQUAL; - it(`should return ${expected} when comparing ${thisVersion} to ${otherVersion}`, () => { - expect(thisVersion.isNewerThan(otherVersion)).toEqual(expected); - }); - } - }); - - describe('fromString', () => { - const tests = [ - { scenario: 'leading v', value: 'v1.72.2', expected: new Version(1, 72, 2) }, - { scenario: 'uppercase v', value: 'V1.72.2', expected: new Version(1, 72, 2) }, - { scenario: 'missing v', value: '1.72.2', expected: new Version(1, 72, 2) }, - { scenario: 'large patch', value: '1.72.123', expected: new Version(1, 72, 123) }, - { scenario: 'large minor', value: '1.123.0', expected: new Version(1, 123, 0) }, - { scenario: 'large major', value: '123.0.0', expected: new Version(123, 0, 0) }, - { scenario: 'major bump', value: 'v2.0.0', expected: new Version(2, 0, 0) }, - { scenario: 'has dash', value: '14.10-1', expected: new Version(14, 10, 1) }, - { scenario: 'missing patch', value: '14.10', expected: new Version(14, 10, 0) }, - { scenario: 'only major', value: '14', expected: new Version(14, 0, 0) }, - ]; - - for (const { scenario, value, expected } of tests) { - it(`should correctly parse ${scenario}`, () => { - const actual = Version.fromString(value); - expect(actual.major).toEqual(expected.major); - expect(actual.minor).toEqual(expected.minor); - expect(actual.patch).toEqual(expected.patch); - }); - } - }); -}); diff --git a/server/src/domain/domain.constant.ts b/server/src/utils/mime-types.ts similarity index 56% rename from server/src/domain/domain.constant.ts rename to server/src/utils/mime-types.ts index 29c4837b6..789fc6a1c 100644 --- a/server/src/domain/domain.constant.ts +++ b/server/src/utils/mime-types.ts @@ -1,97 +1,6 @@ -import { Duration } from 'luxon'; -import { readFileSync } from 'node:fs'; -import { extname, join } from 'node:path'; +import { extname } from 'node:path'; import { AssetType } from 'src/entities/asset.entity'; -export const AUDIT_LOG_MAX_DURATION = Duration.fromObject({ days: 100 }); -export const ONE_HOUR = Duration.fromObject({ hours: 1 }); - -export interface IVersion { - major: number; - minor: number; - patch: number; -} - -export enum VersionType { - EQUAL = 0, - PATCH = 1, - MINOR = 2, - MAJOR = 3, -} - -export class Version implements IVersion { - public readonly types = ['major', 'minor', 'patch'] as const; - - constructor( - public major: number, - public minor: number = 0, - public patch: number = 0, - ) {} - - toString() { - return `${this.major}.${this.minor}.${this.patch}`; - } - - toJSON() { - const { major, minor, patch } = this; - return { major, minor, patch }; - } - - static fromString(version: string): Version { - const regex = /v?(?\d+)(?:\.(?\d+))?(?:[.-](?\d+))?/i; - const matchResult = version.match(regex); - if (matchResult) { - const { major, minor = '0', patch = '0' } = matchResult.groups as { [K in keyof IVersion]: string }; - return new Version(Number(major), Number(minor), Number(patch)); - } else { - throw new Error(`Invalid version format: ${version}`); - } - } - - private compare(version: Version): [number, VersionType] { - for (const [i, key] of this.types.entries()) { - const diff = this[key] - version[key]; - if (diff !== 0) { - return [diff > 0 ? 1 : -1, (VersionType.MAJOR - i) as VersionType]; - } - } - - return [0, VersionType.EQUAL]; - } - - isOlderThan(version: Version): VersionType { - const [bool, type] = this.compare(version); - return bool < 0 ? type : VersionType.EQUAL; - } - - isEqual(version: Version): boolean { - const [bool] = this.compare(version); - return bool === 0; - } - - isNewerThan(version: Version): VersionType { - const [bool, type] = this.compare(version); - return bool > 0 ? type : VersionType.EQUAL; - } -} - -export const envName = (process.env.NODE_ENV || 'development').toUpperCase(); -export const isDev = process.env.NODE_ENV === 'development'; - -const { version } = JSON.parse(readFileSync('./package.json', 'utf8')); -export const serverVersion = Version.fromString(version); - -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); - const image: Record = { '.3fr': ['image/3fr', 'image/x-hasselblad-3fr'], '.ari': ['image/ari', 'image/x-arriflex-ari'], diff --git a/server/src/immich/app.utils.ts b/server/src/utils/misc.ts similarity index 63% rename from server/src/immich/app.utils.ts rename to server/src/utils/misc.ts index f0e4ba661..3837c6279 100644 --- a/server/src/immich/app.utils.ts +++ b/server/src/utils/misc.ts @@ -1,4 +1,4 @@ -import { HttpException, INestApplication, StreamableFile } from '@nestjs/common'; +import { INestApplication } from '@nestjs/common'; import { DocumentBuilder, OpenAPIObject, @@ -7,75 +7,48 @@ import { SwaggerModule, } from '@nestjs/swagger'; import { SchemaObject } from '@nestjs/swagger/dist/interfaces/open-api-spec.interface'; -import { NextFunction, Response } from 'express'; import _ from 'lodash'; import { writeFileSync } from 'node:fs'; -import { access, constants } from 'node:fs/promises'; -import path, { isAbsolute } from 'node:path'; -import { promisify } from 'node:util'; -import { IMMICH_ACCESS_COOKIE, IMMICH_API_KEY_HEADER, IMMICH_API_KEY_NAME } from 'src/domain/auth/auth.constant'; -import { serverVersion } from 'src/domain/domain.constant'; -import { ImmichLogger } from 'src/infra/logger'; -import { ImmichReadStream } from 'src/interfaces/storage.repository'; +import path from 'node:path'; +import { + CLIP_MODEL_INFO, + IMMICH_ACCESS_COOKIE, + IMMICH_API_KEY_HEADER, + IMMICH_API_KEY_NAME, + serverVersion, +} from 'src/constants'; import { Metadata } from 'src/middleware/auth.guard'; -import { CacheControl, ImmichFileResponse, isConnectionAborted } from 'src/utils'; +import { ImmichLogger } from 'src/utils/logger'; -type SendFile = Parameters; -type SendFileOptions = SendFile[1]; +export const isConnectionAborted = (error: Error | any) => error.code === 'ECONNABORTED'; -const logger = new ImmichLogger('SendFile'); +export const handlePromiseError = (promise: Promise, logger: ImmichLogger): void => { + promise.catch((error: Error | any) => logger.error(`Promise error: ${error}`, error?.stack)); +}; -export const sendFile = async ( - res: Response, - next: NextFunction, - handler: () => Promise, -): Promise => { - const _sendFile = (path: string, options: SendFileOptions) => - promisify(res.sendFile).bind(res)(path, options); +export interface OpenGraphTags { + title: string; + description: string; + imageUrl?: string; +} - try { - const file = await handler(); - switch (file.cacheControl) { - case CacheControl.PRIVATE_WITH_CACHE: { - res.set('Cache-Control', 'private, max-age=86400, no-transform'); - break; - } - - case CacheControl.PRIVATE_WITHOUT_CACHE: { - res.set('Cache-Control', 'private, no-cache, no-transform'); - break; - } - } - - res.header('Content-Type', file.contentType); - - const options: SendFileOptions = { dotfiles: 'allow' }; - if (!isAbsolute(file.path)) { - options.root = process.cwd(); - } - - await access(file.path, constants.R_OK); - - return _sendFile(file.path, options); - } catch (error: Error | any) { - // ignore client-closed connection - if (isConnectionAborted(error)) { - return; - } - - // log non-http errors - if (error instanceof HttpException === false) { - logger.error(`Unable to send file: ${error.name}`, error.stack); - } - - res.header('Cache-Control', 'none'); - next(error); +function cleanModelName(modelName: string): string { + const token = modelName.split('/').at(-1); + if (!token) { + throw new Error(`Invalid model name: ${modelName}`); } -}; -export const asStreamableFile = ({ stream, type, length }: ImmichReadStream) => { - return new StreamableFile(stream, { type, length }); -}; + return token.replaceAll(':', '_'); +} + +export function getCLIPModelInfo(modelName: string) { + const modelInfo = CLIP_MODEL_INFO[cleanModelName(modelName)]; + if (!modelInfo) { + throw new Error(`Unknown CLIP model: ${modelName}`); + } + + return modelInfo; +} function sortKeys(target: T): T { if (!target || typeof target !== 'object' || Array.isArray(target)) { diff --git a/server/src/utils/pagination.ts b/server/src/utils/pagination.ts new file mode 100644 index 000000000..dec1a9de0 --- /dev/null +++ b/server/src/utils/pagination.ts @@ -0,0 +1,79 @@ +import _ from 'lodash'; +import { FindManyOptions, ObjectLiteral, Repository, SelectQueryBuilder } from 'typeorm'; + +export interface PaginationOptions { + take: number; + skip?: number; +} + +export enum PaginationMode { + LIMIT_OFFSET = 'limit-offset', + SKIP_TAKE = 'skip-take', +} + +export interface PaginatedBuilderOptions { + take: number; + skip?: number; + mode?: PaginationMode; +} + +export interface PaginationResult { + items: T[]; + hasNextPage: boolean; +} + +export type Paginated = Promise>; + +export async function* usePagination( + pageSize: number, + getNextPage: (pagination: PaginationOptions) => PaginationResult | Paginated, +) { + let hasNextPage = true; + + for (let skip = 0; hasNextPage; skip += pageSize) { + const result = await getNextPage({ take: pageSize, skip }); + hasNextPage = result.hasNextPage; + yield result.items; + } +} + +function paginationHelper(items: Entity[], take: number): PaginationResult { + const hasNextPage = items.length > take; + items.splice(take); + + return { items, hasNextPage }; +} + +export async function paginate( + repository: Repository, + { take, skip }: PaginationOptions, + searchOptions?: FindManyOptions, +): Paginated { + const items = await repository.find( + _.omitBy( + { + ...searchOptions, + // Take one more item to check if there's a next page + take: take + 1, + skip, + }, + _.isUndefined, + ), + ); + + return paginationHelper(items, take); +} + +export async function paginatedBuilder( + qb: SelectQueryBuilder, + { take, skip, mode }: PaginatedBuilderOptions, +): Paginated { + if (mode === PaginationMode.LIMIT_OFFSET) { + qb.limit(take + 1).offset(skip); + } else { + qb.take(take + 1).skip(skip); + } + + const items = await qb.getMany(); + return paginationHelper(items, take); +} diff --git a/server/src/utils/set.ts b/server/src/utils/set.ts new file mode 100644 index 000000000..971d3f7e5 --- /dev/null +++ b/server/src/utils/set.ts @@ -0,0 +1,36 @@ +// NOTE: The following Set utils have been added here, to easily determine where they are used. +// They should be replaced with native Set operations, when they are added to the language. +// Proposal reference: https://github.com/tc39/proposal-set-methods + +export const setUnion = (...sets: Set[]): Set => { + const union = new Set(sets[0]); + for (const set of sets.slice(1)) { + for (const element of set) { + union.add(element); + } + } + return union; +}; + +export const setDifference = (setA: Set, ...sets: Set[]): Set => { + const difference = new Set(setA); + for (const set of sets) { + for (const element of set) { + difference.delete(element); + } + } + return difference; +}; + +export const setIsSuperset = (set: Set, subset: Set): boolean => { + for (const element of subset) { + if (!set.has(element)) { + return false; + } + } + return true; +}; + +export const setIsEqual = (setA: Set, setB: Set): boolean => { + return setA.size === setB.size && setIsSuperset(setA, setB); +}; diff --git a/server/src/infra/sql-generator/index.ts b/server/src/utils/sql.ts similarity index 90% rename from server/src/infra/sql-generator/index.ts rename to server/src/utils/sql.ts index 10730fc36..1afe4d5a8 100644 --- a/server/src/infra/sql-generator/index.ts +++ b/server/src/utils/sql.ts @@ -5,11 +5,11 @@ import { Test } from '@nestjs/testing'; import { TypeOrmModule } from '@nestjs/typeorm'; import { mkdir, rm, writeFile } from 'node:fs/promises'; import { join } from 'node:path'; +import { format } from 'sql-formatter'; +import { databaseConfig } from 'src/database.config'; import { GENERATE_SQL_KEY, GenerateSqlQueries } from 'src/decorators'; import { databaseEntities } from 'src/entities'; -import { databaseConfig } from 'src/infra/database.config'; -import { SqlLogger } from 'src/infra/sql-generator/sql.logger'; -import { ISystemConfigRepository } from 'src/interfaces/system-config.repository'; +import { ISystemConfigRepository } from 'src/interfaces/system-config.interface'; import { AccessRepository } from 'src/repositories/access.repository'; import { AlbumRepository } from 'src/repositories/album.repository'; import { ApiKeyRepository } from 'src/repositories/api-key.repository'; @@ -26,6 +26,30 @@ import { SystemMetadataRepository } from 'src/repositories/system-metadata.repos import { TagRepository } from 'src/repositories/tag.repository'; import { UserTokenRepository } from 'src/repositories/user-token.repository'; import { UserRepository } from 'src/repositories/user.repository'; +import { Logger } from 'typeorm'; + +export class SqlLogger implements Logger { + queries: string[] = []; + errors: Array<{ error: string | Error; query: string }> = []; + + clear() { + this.queries = []; + this.errors = []; + } + + logQuery(query: string) { + this.queries.push(format(query, { language: 'postgresql' })); + } + + logQueryError(error: string | Error, query: string) { + this.errors.push({ error, query }); + } + + logQuerySlow() {} + logSchemaBuild() {} + logMigration() {} + log() {} +} const reflector = new Reflector(); const repositories = [ @@ -178,7 +202,7 @@ class SqlGenerator { } } -new SqlGenerator({ targetDir: './src/infra/sql' }) +new SqlGenerator({ targetDir: './src/queries' }) .run() .then(() => { console.log('Done'); diff --git a/server/src/utils/version.spec.ts b/server/src/utils/version.spec.ts new file mode 100644 index 000000000..34c8abb41 --- /dev/null +++ b/server/src/utils/version.spec.ts @@ -0,0 +1,72 @@ +import { Version, VersionType } from 'src/utils/version'; + +describe('Version', () => { + const tests = [ + { this: new Version(0, 0, 1), other: new Version(0, 0, 0), compare: 1, type: VersionType.PATCH }, + { this: new Version(0, 1, 0), other: new Version(0, 0, 0), compare: 1, type: VersionType.MINOR }, + { this: new Version(1, 0, 0), other: new Version(0, 0, 0), compare: 1, type: VersionType.MAJOR }, + { this: new Version(0, 0, 0), other: new Version(0, 0, 1), compare: -1, type: VersionType.PATCH }, + { this: new Version(0, 0, 0), other: new Version(0, 1, 0), compare: -1, type: VersionType.MINOR }, + { this: new Version(0, 0, 0), other: new Version(1, 0, 0), compare: -1, type: VersionType.MAJOR }, + { this: new Version(0, 0, 0), other: new Version(0, 0, 0), compare: 0, type: VersionType.EQUAL }, + { this: new Version(0, 0, 1), other: new Version(0, 0, 1), compare: 0, type: VersionType.EQUAL }, + { this: new Version(0, 1, 0), other: new Version(0, 1, 0), compare: 0, type: VersionType.EQUAL }, + { this: new Version(1, 0, 0), other: new Version(1, 0, 0), compare: 0, type: VersionType.EQUAL }, + { this: new Version(1, 0), other: new Version(1, 0, 0), compare: 0, type: VersionType.EQUAL }, + { this: new Version(1, 0), other: new Version(1, 0, 1), compare: -1, type: VersionType.PATCH }, + { this: new Version(1, 1), other: new Version(1, 0, 1), compare: 1, type: VersionType.MINOR }, + { this: new Version(1), other: new Version(1, 0, 0), compare: 0, type: VersionType.EQUAL }, + { this: new Version(1), other: new Version(1, 0, 1), compare: -1, type: VersionType.PATCH }, + ]; + + describe('isOlderThan', () => { + for (const { this: thisVersion, other: otherVersion, compare, type } of tests) { + const expected = compare < 0 ? type : VersionType.EQUAL; + it(`should return '${expected}' when comparing ${thisVersion} to ${otherVersion}`, () => { + expect(thisVersion.isOlderThan(otherVersion)).toEqual(expected); + }); + } + }); + + describe('isEqual', () => { + for (const { this: thisVersion, other: otherVersion, compare } of tests) { + const bool = compare === 0; + it(`should return ${bool} when comparing ${thisVersion} to ${otherVersion}`, () => { + expect(thisVersion.isEqual(otherVersion)).toEqual(bool); + }); + } + }); + + describe('isNewerThan', () => { + for (const { this: thisVersion, other: otherVersion, compare, type } of tests) { + const expected = compare > 0 ? type : VersionType.EQUAL; + it(`should return ${expected} when comparing ${thisVersion} to ${otherVersion}`, () => { + expect(thisVersion.isNewerThan(otherVersion)).toEqual(expected); + }); + } + }); + + describe('fromString', () => { + const tests = [ + { scenario: 'leading v', value: 'v1.72.2', expected: new Version(1, 72, 2) }, + { scenario: 'uppercase v', value: 'V1.72.2', expected: new Version(1, 72, 2) }, + { scenario: 'missing v', value: '1.72.2', expected: new Version(1, 72, 2) }, + { scenario: 'large patch', value: '1.72.123', expected: new Version(1, 72, 123) }, + { scenario: 'large minor', value: '1.123.0', expected: new Version(1, 123, 0) }, + { scenario: 'large major', value: '123.0.0', expected: new Version(123, 0, 0) }, + { scenario: 'major bump', value: 'v2.0.0', expected: new Version(2, 0, 0) }, + { scenario: 'has dash', value: '14.10-1', expected: new Version(14, 10, 1) }, + { scenario: 'missing patch', value: '14.10', expected: new Version(14, 10, 0) }, + { scenario: 'only major', value: '14', expected: new Version(14, 0, 0) }, + ]; + + for (const { scenario, value, expected } of tests) { + it(`should correctly parse ${scenario}`, () => { + const actual = Version.fromString(value); + expect(actual.major).toEqual(expected.major); + expect(actual.minor).toEqual(expected.minor); + expect(actual.patch).toEqual(expected.patch); + }); + } + }); +}); diff --git a/server/src/utils/version.ts b/server/src/utils/version.ts new file mode 100644 index 000000000..6eca12eb4 --- /dev/null +++ b/server/src/utils/version.ts @@ -0,0 +1,64 @@ +export type IVersion = { major: number; minor: number; patch: number }; + +export enum VersionType { + EQUAL = 0, + PATCH = 1, + MINOR = 2, + MAJOR = 3, +} + +export class Version implements IVersion { + public readonly types = ['major', 'minor', 'patch'] as const; + + constructor( + public major: number, + public minor: number = 0, + public patch: number = 0, + ) {} + + toString() { + return `${this.major}.${this.minor}.${this.patch}`; + } + + toJSON() { + const { major, minor, patch } = this; + return { major, minor, patch }; + } + + static fromString(version: string): Version { + const regex = /v?(?\d+)(?:\.(?\d+))?(?:[.-](?\d+))?/i; + const matchResult = version.match(regex); + if (matchResult) { + const { major, minor = '0', patch = '0' } = matchResult.groups as { [K in keyof IVersion]: string }; + return new Version(Number(major), Number(minor), Number(patch)); + } else { + throw new Error(`Invalid version format: ${version}`); + } + } + + private compare(version: Version): [number, VersionType] { + for (const [i, key] of this.types.entries()) { + const diff = this[key] - version[key]; + if (diff !== 0) { + return [diff > 0 ? 1 : -1, (VersionType.MAJOR - i) as VersionType]; + } + } + + return [0, VersionType.EQUAL]; + } + + isOlderThan(version: Version): VersionType { + const [bool, type] = this.compare(version); + return bool < 0 ? type : VersionType.EQUAL; + } + + isEqual(version: Version): boolean { + const [bool] = this.compare(version); + return bool === 0; + } + + isNewerThan(version: Version): VersionType { + const [bool, type] = this.compare(version); + return bool > 0 ? type : VersionType.EQUAL; + } +} diff --git a/server/test/fixtures/auth.stub.ts b/server/test/fixtures/auth.stub.ts index e0add44d2..2e56d0001 100644 --- a/server/test/fixtures/auth.stub.ts +++ b/server/test/fixtures/auth.stub.ts @@ -1,4 +1,4 @@ -import { AuthDto } from 'src/domain/auth/auth.dto'; +import { AuthDto } from 'src/dtos/auth.dto'; import { SharedLinkEntity } from 'src/entities/shared-link.entity'; import { UserTokenEntity } from 'src/entities/user-token.entity'; import { UserEntity } from 'src/entities/user.entity'; diff --git a/server/test/fixtures/library.stub.ts b/server/test/fixtures/library.stub.ts index b3fa4b7f0..dde250a7a 100644 --- a/server/test/fixtures/library.stub.ts +++ b/server/test/fixtures/library.stub.ts @@ -1,6 +1,6 @@ import { join } from 'node:path'; +import { APP_MEDIA_LOCATION } from 'src/constants'; import { THUMBNAIL_DIR } from 'src/cores/storage.core'; -import { APP_MEDIA_LOCATION } from 'src/domain/domain.constant'; import { LibraryEntity, LibraryType } from 'src/entities/library.entity'; import { userStub } from 'test/fixtures/user.stub'; diff --git a/server/test/fixtures/media.stub.ts b/server/test/fixtures/media.stub.ts index 1be792c25..5070586ac 100644 --- a/server/test/fixtures/media.stub.ts +++ b/server/test/fixtures/media.stub.ts @@ -1,4 +1,4 @@ -import { AudioStreamInfo, VideoFormat, VideoInfo, VideoStreamInfo } from 'src/interfaces/media.repository'; +import { AudioStreamInfo, VideoFormat, VideoInfo, VideoStreamInfo } from 'src/interfaces/media.interface'; const probeStubDefaultFormat: VideoFormat = { formatName: 'mov,mp4,m4a,3gp,3g2,mj2', diff --git a/server/test/fixtures/shared-link.stub.ts b/server/test/fixtures/shared-link.stub.ts index 6855c8c7a..34e3da515 100644 --- a/server/test/fixtures/shared-link.stub.ts +++ b/server/test/fixtures/shared-link.stub.ts @@ -1,8 +1,8 @@ -import { AlbumResponseDto } from 'src/domain/album/album-response.dto'; -import { AssetResponseDto } from 'src/domain/asset/response-dto/asset-response.dto'; -import { ExifResponseDto } from 'src/domain/asset/response-dto/exif-response.dto'; -import { SharedLinkResponseDto } from 'src/domain/shared-link/shared-link-response.dto'; -import { mapUser } from 'src/domain/user/response-dto/user-response.dto'; +import { AlbumResponseDto } from 'src/dtos/album.dto'; +import { AssetResponseDto } from 'src/dtos/asset-response.dto'; +import { ExifResponseDto } from 'src/dtos/exif.dto'; +import { SharedLinkResponseDto } from 'src/dtos/shared-link.dto'; +import { mapUser } from 'src/dtos/user.dto'; import { AssetOrder } from 'src/entities/album.entity'; import { AssetType } from 'src/entities/asset.entity'; import { SharedLinkEntity, SharedLinkType } from 'src/entities/shared-link.entity'; diff --git a/server/test/fixtures/tag.stub.ts b/server/test/fixtures/tag.stub.ts index 27123ddb6..537c65db4 100644 --- a/server/test/fixtures/tag.stub.ts +++ b/server/test/fixtures/tag.stub.ts @@ -1,4 +1,4 @@ -import { TagResponseDto } from 'src/domain/tag/tag-response.dto'; +import { TagResponseDto } from 'src/dtos/tag.dto'; import { TagEntity, TagType } from 'src/entities/tag.entity'; import { userStub } from 'test/fixtures/user.stub'; diff --git a/server/test/repositories/access.repository.mock.ts b/server/test/repositories/access.repository.mock.ts index 8d2233cf6..a61451277 100644 --- a/server/test/repositories/access.repository.mock.ts +++ b/server/test/repositories/access.repository.mock.ts @@ -1,5 +1,5 @@ import { AccessCore } from 'src/cores/access.core'; -import { IAccessRepository } from 'src/interfaces/access.repository'; +import { IAccessRepository } from 'src/interfaces/access.interface'; export interface IAccessRepositoryMock { activity: jest.Mocked; diff --git a/server/test/repositories/activity.repository.mock.ts b/server/test/repositories/activity.repository.mock.ts index 84cf7e124..276b57c6c 100644 --- a/server/test/repositories/activity.repository.mock.ts +++ b/server/test/repositories/activity.repository.mock.ts @@ -1,4 +1,4 @@ -import { IActivityRepository } from 'src/interfaces/activity.repository'; +import { IActivityRepository } from 'src/interfaces/activity.interface'; export const newActivityRepositoryMock = (): jest.Mocked => { return { diff --git a/server/test/repositories/album.repository.mock.ts b/server/test/repositories/album.repository.mock.ts index b50fe6750..626358d81 100644 --- a/server/test/repositories/album.repository.mock.ts +++ b/server/test/repositories/album.repository.mock.ts @@ -1,4 +1,4 @@ -import { IAlbumRepository } from 'src/interfaces/album.repository'; +import { IAlbumRepository } from 'src/interfaces/album.interface'; export const newAlbumRepositoryMock = (): jest.Mocked => { return { diff --git a/server/test/repositories/api-key.repository.mock.ts b/server/test/repositories/api-key.repository.mock.ts index d74a41028..32b8388a3 100644 --- a/server/test/repositories/api-key.repository.mock.ts +++ b/server/test/repositories/api-key.repository.mock.ts @@ -1,4 +1,4 @@ -import { IKeyRepository } from 'src/interfaces/api-key.repository'; +import { IKeyRepository } from 'src/interfaces/api-key.interface'; export const newKeyRepositoryMock = (): jest.Mocked => { return { diff --git a/server/test/repositories/asset-stack.repository.mock.ts b/server/test/repositories/asset-stack.repository.mock.ts index cf38d045f..76ada96cd 100644 --- a/server/test/repositories/asset-stack.repository.mock.ts +++ b/server/test/repositories/asset-stack.repository.mock.ts @@ -1,4 +1,4 @@ -import { IAssetStackRepository } from 'src/interfaces/asset-stack.repository'; +import { IAssetStackRepository } from 'src/interfaces/asset-stack.interface'; export const newAssetStackRepositoryMock = (): jest.Mocked => { return { diff --git a/server/test/repositories/asset.repository.mock.ts b/server/test/repositories/asset.repository.mock.ts index c72d00414..22d4f3707 100644 --- a/server/test/repositories/asset.repository.mock.ts +++ b/server/test/repositories/asset.repository.mock.ts @@ -1,4 +1,4 @@ -import { IAssetRepository } from 'src/interfaces/asset.repository'; +import { IAssetRepository } from 'src/interfaces/asset.interface'; export const newAssetRepositoryMock = (): jest.Mocked => { return { diff --git a/server/test/repositories/audit.repository.mock.ts b/server/test/repositories/audit.repository.mock.ts index 654f67794..9e4adf560 100644 --- a/server/test/repositories/audit.repository.mock.ts +++ b/server/test/repositories/audit.repository.mock.ts @@ -1,4 +1,4 @@ -import { IAuditRepository } from 'src/interfaces/audit.repository'; +import { IAuditRepository } from 'src/interfaces/audit.interface'; export const newAuditRepositoryMock = (): jest.Mocked => { return { diff --git a/server/test/repositories/communication.repository.mock.ts b/server/test/repositories/communication.repository.mock.ts index 3d4eaeb37..38284f377 100644 --- a/server/test/repositories/communication.repository.mock.ts +++ b/server/test/repositories/communication.repository.mock.ts @@ -1,4 +1,4 @@ -import { ICommunicationRepository } from 'src/interfaces/communication.repository'; +import { ICommunicationRepository } from 'src/interfaces/communication.interface'; export const newCommunicationRepositoryMock = (): jest.Mocked => { return { diff --git a/server/test/repositories/crypto.repository.mock.ts b/server/test/repositories/crypto.repository.mock.ts index 6cf4adffd..cbd90ec67 100644 --- a/server/test/repositories/crypto.repository.mock.ts +++ b/server/test/repositories/crypto.repository.mock.ts @@ -1,4 +1,4 @@ -import { ICryptoRepository } from 'src/interfaces/crypto.repository'; +import { ICryptoRepository } from 'src/interfaces/crypto.interface'; export const newCryptoRepositoryMock = (): jest.Mocked => { return { diff --git a/server/test/repositories/database.repository.mock.ts b/server/test/repositories/database.repository.mock.ts index 82abe16a8..704189571 100644 --- a/server/test/repositories/database.repository.mock.ts +++ b/server/test/repositories/database.repository.mock.ts @@ -1,5 +1,5 @@ -import { Version } from 'src/domain/domain.constant'; -import { IDatabaseRepository } from 'src/interfaces/database.repository'; +import { IDatabaseRepository } from 'src/interfaces/database.interface'; +import { Version } from 'src/utils/version'; export const newDatabaseRepositoryMock = (): jest.Mocked => { return { diff --git a/server/test/repositories/job.repository.mock.ts b/server/test/repositories/job.repository.mock.ts index 851da70da..9cd21fe87 100644 --- a/server/test/repositories/job.repository.mock.ts +++ b/server/test/repositories/job.repository.mock.ts @@ -1,4 +1,4 @@ -import { IJobRepository } from 'src/interfaces/job.repository'; +import { IJobRepository } from 'src/interfaces/job.interface'; export const newJobRepositoryMock = (): jest.Mocked => { return { diff --git a/server/test/repositories/library.repository.mock.ts b/server/test/repositories/library.repository.mock.ts index b3c62d681..6cdfb38f4 100644 --- a/server/test/repositories/library.repository.mock.ts +++ b/server/test/repositories/library.repository.mock.ts @@ -1,4 +1,4 @@ -import { ILibraryRepository } from 'src/interfaces/library.repository'; +import { ILibraryRepository } from 'src/interfaces/library.interface'; export const newLibraryRepositoryMock = (): jest.Mocked => { return { diff --git a/server/test/repositories/machine-learning.repository.mock.ts b/server/test/repositories/machine-learning.repository.mock.ts index 50da10cb4..bc35b4c85 100644 --- a/server/test/repositories/machine-learning.repository.mock.ts +++ b/server/test/repositories/machine-learning.repository.mock.ts @@ -1,4 +1,4 @@ -import { IMachineLearningRepository } from 'src/interfaces/machine-learning.repository'; +import { IMachineLearningRepository } from 'src/interfaces/machine-learning.interface'; export const newMachineLearningRepositoryMock = (): jest.Mocked => { return { diff --git a/server/test/repositories/media.repository.mock.ts b/server/test/repositories/media.repository.mock.ts index d1806c636..b904766ea 100644 --- a/server/test/repositories/media.repository.mock.ts +++ b/server/test/repositories/media.repository.mock.ts @@ -1,4 +1,4 @@ -import { IMediaRepository } from 'src/interfaces/media.repository'; +import { IMediaRepository } from 'src/interfaces/media.interface'; export const newMediaRepositoryMock = (): jest.Mocked => { return { diff --git a/server/test/repositories/metadata.repository.mock.ts b/server/test/repositories/metadata.repository.mock.ts index a5f13daf5..ec21ab8c1 100644 --- a/server/test/repositories/metadata.repository.mock.ts +++ b/server/test/repositories/metadata.repository.mock.ts @@ -1,4 +1,4 @@ -import { IMetadataRepository } from 'src/interfaces/metadata.repository'; +import { IMetadataRepository } from 'src/interfaces/metadata.interface'; export const newMetadataRepositoryMock = (): jest.Mocked => { return { diff --git a/server/test/repositories/move.repository.mock.ts b/server/test/repositories/move.repository.mock.ts index 265799ab6..b7adec2a7 100644 --- a/server/test/repositories/move.repository.mock.ts +++ b/server/test/repositories/move.repository.mock.ts @@ -1,4 +1,4 @@ -import { IMoveRepository } from 'src/interfaces/move.repository'; +import { IMoveRepository } from 'src/interfaces/move.interface'; export const newMoveRepositoryMock = (): jest.Mocked => { return { diff --git a/server/test/repositories/partner.repository.mock.ts b/server/test/repositories/partner.repository.mock.ts index 74c42ba8d..04370730b 100644 --- a/server/test/repositories/partner.repository.mock.ts +++ b/server/test/repositories/partner.repository.mock.ts @@ -1,4 +1,4 @@ -import { IPartnerRepository } from 'src/interfaces/partner.repository'; +import { IPartnerRepository } from 'src/interfaces/partner.interface'; export const newPartnerRepositoryMock = (): jest.Mocked => { return { diff --git a/server/test/repositories/person.repository.mock.ts b/server/test/repositories/person.repository.mock.ts index b4b817535..5b94fbc3d 100644 --- a/server/test/repositories/person.repository.mock.ts +++ b/server/test/repositories/person.repository.mock.ts @@ -1,4 +1,4 @@ -import { IPersonRepository } from 'src/interfaces/person.repository'; +import { IPersonRepository } from 'src/interfaces/person.interface'; export const newPersonRepositoryMock = (): jest.Mocked => { return { diff --git a/server/test/repositories/search.repository.mock.ts b/server/test/repositories/search.repository.mock.ts index eb6c13ea4..24e648ee2 100644 --- a/server/test/repositories/search.repository.mock.ts +++ b/server/test/repositories/search.repository.mock.ts @@ -1,4 +1,4 @@ -import { ISearchRepository } from 'src/interfaces/search.repository'; +import { ISearchRepository } from 'src/interfaces/search.interface'; export const newSearchRepositoryMock = (): jest.Mocked => { return { diff --git a/server/test/repositories/shared-link.repository.mock.ts b/server/test/repositories/shared-link.repository.mock.ts index ea084c1ed..2fcaf7aee 100644 --- a/server/test/repositories/shared-link.repository.mock.ts +++ b/server/test/repositories/shared-link.repository.mock.ts @@ -1,4 +1,4 @@ -import { ISharedLinkRepository } from 'src/interfaces/shared-link.repository'; +import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface'; export const newSharedLinkRepositoryMock = (): jest.Mocked => { return { diff --git a/server/test/repositories/storage.repository.mock.ts b/server/test/repositories/storage.repository.mock.ts index 72b87aed1..d5049999c 100644 --- a/server/test/repositories/storage.repository.mock.ts +++ b/server/test/repositories/storage.repository.mock.ts @@ -1,6 +1,6 @@ import { WatchOptions } from 'chokidar'; import { StorageCore } from 'src/cores/storage.core'; -import { IStorageRepository, StorageEventType, WatchEvents } from 'src/interfaces/storage.repository'; +import { IStorageRepository, StorageEventType, WatchEvents } from 'src/interfaces/storage.interface'; interface MockWatcherOptions { items?: Array<{ event: 'change' | 'add' | 'unlink' | 'error'; value: string }>; diff --git a/server/test/repositories/system-config.repository.mock.ts b/server/test/repositories/system-config.repository.mock.ts index 89ce6a050..0ef11ce18 100644 --- a/server/test/repositories/system-config.repository.mock.ts +++ b/server/test/repositories/system-config.repository.mock.ts @@ -1,5 +1,5 @@ import { SystemConfigCore } from 'src/cores/system-config.core'; -import { ISystemConfigRepository } from 'src/interfaces/system-config.repository'; +import { ISystemConfigRepository } from 'src/interfaces/system-config.interface'; export const newSystemConfigRepositoryMock = (reset = true): jest.Mocked => { if (reset) { diff --git a/server/test/repositories/system-info.repository.mock.ts b/server/test/repositories/system-info.repository.mock.ts index 93ec9c7dd..bdc11f9d6 100644 --- a/server/test/repositories/system-info.repository.mock.ts +++ b/server/test/repositories/system-info.repository.mock.ts @@ -1,4 +1,4 @@ -import { IServerInfoRepository } from 'src/interfaces/server-info.repository'; +import { IServerInfoRepository } from 'src/interfaces/server-info.interface'; export const newServerInfoRepositoryMock = (): jest.Mocked => { return { diff --git a/server/test/repositories/system-metadata.repository.mock.ts b/server/test/repositories/system-metadata.repository.mock.ts index f93278aaa..5ffc5dd89 100644 --- a/server/test/repositories/system-metadata.repository.mock.ts +++ b/server/test/repositories/system-metadata.repository.mock.ts @@ -1,4 +1,4 @@ -import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.repository'; +import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; export const newSystemMetadataRepositoryMock = (): jest.Mocked => { return { diff --git a/server/test/repositories/tag.repository.mock.ts b/server/test/repositories/tag.repository.mock.ts index 9f5283664..0c31c546c 100644 --- a/server/test/repositories/tag.repository.mock.ts +++ b/server/test/repositories/tag.repository.mock.ts @@ -1,4 +1,4 @@ -import { ITagRepository } from 'src/interfaces/tag.repository'; +import { ITagRepository } from 'src/interfaces/tag.interface'; export const newTagRepositoryMock = (): jest.Mocked => { return { diff --git a/server/test/repositories/user-token.repository.mock.ts b/server/test/repositories/user-token.repository.mock.ts index 23de88d35..b3fa7e73f 100644 --- a/server/test/repositories/user-token.repository.mock.ts +++ b/server/test/repositories/user-token.repository.mock.ts @@ -1,4 +1,4 @@ -import { IUserTokenRepository } from 'src/interfaces/user-token.repository'; +import { IUserTokenRepository } from 'src/interfaces/user-token.interface'; export const newUserTokenRepositoryMock = (): jest.Mocked => { return { diff --git a/server/test/repositories/user.repository.mock.ts b/server/test/repositories/user.repository.mock.ts index 4db0b16af..80d9a4cfd 100644 --- a/server/test/repositories/user.repository.mock.ts +++ b/server/test/repositories/user.repository.mock.ts @@ -1,5 +1,5 @@ import { UserCore } from 'src/cores/user.core'; -import { IUserRepository } from 'src/interfaces/user.repository'; +import { IUserRepository } from 'src/interfaces/user.interface'; export const newUserRepositoryMock = (reset = true): jest.Mocked => { if (reset) { diff --git a/server/test/utils.ts b/server/test/utils.ts index 777a6a00c..c7732eabc 100644 --- a/server/test/utils.ts +++ b/server/test/utils.ts @@ -5,17 +5,14 @@ import fs from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { EventEmitter } from 'node:stream'; -import { Server } from 'node:tls'; -import { ApiModule } from 'src/apps/api.module'; -import { ApiService } from 'src/apps/api.service'; -import { MicroservicesService } from 'src/apps/microservices.service'; -import { QueueName } from 'src/domain/job/job.constants'; -import { dataSource } from 'src/infra/database.config'; -import { InfraModule, InfraTestModule } from 'src/infra/infra.module'; -import { IJobRepository, JobItem, JobItemHandler } from 'src/interfaces/job.repository'; -import { IMediaRepository } from 'src/interfaces/media.repository'; -import { StorageEventType } from 'src/interfaces/storage.repository'; +import { AppTestModule } from 'src/app.module'; +import { dataSource } from 'src/database.config'; +import { IJobRepository, JobItem, JobItemHandler, QueueName } from 'src/interfaces/job.interface'; +import { IMediaRepository } from 'src/interfaces/media.interface'; +import { StorageEventType } from 'src/interfaces/storage.interface'; import { MediaRepository } from 'src/repositories/media.repository'; +import { ApiService } from 'src/services/api.service'; +import { MicroservicesService } from 'src/services/microservices.service'; import { EntityTarget, ObjectLiteral } from 'typeorm'; export const IMMICH_TEST_ASSET_PATH = process.env.IMMICH_TEST_ASSET_PATH as string; @@ -105,12 +102,7 @@ let app: INestApplication; export const testApp = { create: async (): Promise => { - const moduleFixture = await Test.createTestingModule({ - imports: [ApiModule], - providers: [ApiService, MicroservicesService], - }) - .overrideModule(InfraModule) - .useModule(InfraTestModule) + const moduleFixture = await Test.createTestingModule({ imports: [AppTestModule] }) .overrideProvider(IJobRepository) .useClass(JobMock) .overrideProvider(IMediaRepository) @@ -118,15 +110,11 @@ export const testApp = { .compile(); app = await moduleFixture.createNestApplication().init(); - await app.listen(0); + await app.get(ApiService).init(); await db.reset(); await app.get(ApiService).init(); await app.get(MicroservicesService).init(); - const port = app.getHttpServer().address().port; - const protocol = app instanceof Server ? 'https' : 'http'; - process.env.IMMICH_INSTANCE_URL = protocol + '://127.0.0.1:' + port; - return app; }, reset: async (options?: ResetOptions) => { diff --git a/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte b/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte index 4d09e1705..3a5105629 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte +++ b/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte @@ -5,20 +5,31 @@ import { getAssetJobName } from '$lib/utils'; import { clickOutside } from '$lib/utils/click-outside'; import { getContextMenuPosition } from '$lib/utils/context-menu'; - import { AssetJobName, AssetTypeEnum, type AssetResponseDto } from '@immich/sdk'; + import { AssetJobName, AssetTypeEnum, type AssetResponseDto, type AlbumResponseDto } from '@immich/sdk'; import { + mdiAccountCircleOutline, mdiAlertOutline, + mdiArchiveArrowDownOutline, + mdiArchiveArrowUpOutline, mdiArrowLeft, + mdiCogRefreshOutline, mdiContentCopy, + mdiDatabaseRefreshOutline, mdiDeleteOutline, mdiDotsVertical, + mdiFolderDownloadOutline, mdiHeart, mdiHeartOutline, + mdiImageAlbum, + mdiImageMinusOutline, + mdiImageOutline, + mdiImageRefreshOutline, mdiInformationOutline, mdiMagnifyMinusOutline, mdiMagnifyPlusOutline, mdiMotionPauseOutline, mdiPlaySpeed, + mdiPresentationPlay, mdiShareVariantOutline, } from '@mdi/js'; import { createEventDispatcher } from 'svelte'; @@ -26,6 +37,7 @@ import MenuOption from '../shared-components/context-menu/menu-option.svelte'; export let asset: AssetResponseDto; + export let album: AlbumResponseDto | null = null; export let showCopyButton: boolean; export let showZoomButton: boolean; export let showMotionPlayButton: boolean; @@ -42,6 +54,7 @@ | 'addToAlbum' | 'addToSharedAlbum' | 'asProfileImage' + | 'setAsAlbumCover' | 'download' | 'playSlideShow' | 'runJob' @@ -59,6 +72,7 @@ addToAlbum: void; addToSharedAlbum: void; asProfileImage: void; + setAsAlbumCover: void; runJob: AssetJobName; playSlideShow: void; unstack: void; @@ -173,37 +187,55 @@ {#if isShowAssetOptions} {#if showSlideshow} - onMenuClick('playSlideShow')} text="Slideshow" /> + onMenuClick('playSlideShow')} text="Slideshow" /> {/if} {#if showDownloadButton} - onMenuClick('download')} text="Download" /> + onMenuClick('download')} text="Download" /> {/if} - onMenuClick('addToAlbum')} text="Add to Album" /> - onMenuClick('addToSharedAlbum')} text="Add to Shared Album" /> + onMenuClick('addToAlbum')} text="Add to album" /> + onMenuClick('addToSharedAlbum')} + text="Add to shared album" + /> {#if isOwner} + {#if hasStackChildren} + onMenuClick('unstack')} text="Un-stack" /> + {/if} + {#if album} + onMenuClick('setAsAlbumCover')} + /> + {/if} + {#if asset.type === AssetTypeEnum.Image} + onMenuClick('asProfileImage')} + text="Set as profile picture" + /> + {/if} dispatch('toggleArchive')} + icon={asset.isArchived ? mdiArchiveArrowUpOutline : mdiArchiveArrowDownOutline} text={asset.isArchived ? 'Unarchive' : 'Archive'} /> - {#if asset.type === AssetTypeEnum.Image} - onMenuClick('asProfileImage')} text="As profile picture" /> - {/if} - - {#if hasStackChildren} - onMenuClick('unstack')} text="Un-Stack" /> - {/if} - +


onJobClick(AssetJobName.RefreshMetadata)} text={getAssetJobName(AssetJobName.RefreshMetadata)} /> onJobClick(AssetJobName.RegenerateThumbnail)} text={getAssetJobName(AssetJobName.RegenerateThumbnail)} /> {#if asset.type === AssetTypeEnum.Video} onJobClick(AssetJobName.TranscodeVideo)} text={getAssetJobName(AssetJobName.TranscodeVideo)} /> diff --git a/web/src/lib/components/asset-viewer/asset-viewer.svelte b/web/src/lib/components/asset-viewer/asset-viewer.svelte index f9d2f787b..51da30411 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer.svelte +++ b/web/src/lib/components/asset-viewer/asset-viewer.svelte @@ -97,6 +97,9 @@ let isShowActivity = false; let isLiked: ActivityResponseDto | null = null; let numberOfComments: number; + let fullscreenElement: Element; + + $: isFullScreen = fullscreenElement !== null; $: { if (asset.stackCount && asset.stack) { @@ -512,6 +515,8 @@ ]} /> + +
assetViewerHtmlElement.requestFullscreen()} onPrevious={() => navigateAsset('previous')} onNext={() => navigateAsset('next')} onClose={() => ($slideshowState = SlideshowState.StopSlideshow)} diff --git a/web/src/lib/components/asset-viewer/slideshow-bar.svelte b/web/src/lib/components/asset-viewer/slideshow-bar.svelte index 37cd7f701..03e8c9df4 100644 --- a/web/src/lib/components/asset-viewer/slideshow-bar.svelte +++ b/web/src/lib/components/asset-viewer/slideshow-bar.svelte @@ -3,23 +3,46 @@ import ProgressBar, { ProgressBarStatus } from '$lib/components/shared-components/progress-bar/progress-bar.svelte'; import SlideshowSettings from '$lib/components/slideshow-settings.svelte'; import { SlideshowNavigation, slideshowStore } from '$lib/stores/slideshow.store'; - import { mdiChevronLeft, mdiChevronRight, mdiClose, mdiCog, mdiPause, mdiPlay } from '@mdi/js'; + import { mdiChevronLeft, mdiChevronRight, mdiClose, mdiCog, mdiFullscreen, mdiPause, mdiPlay } from '@mdi/js'; import { onDestroy, onMount } from 'svelte'; + import { fly } from 'svelte/transition'; + export let isFullScreen: boolean; export let onNext = () => {}; export let onPrevious = () => {}; export let onClose = () => {}; + export let onSetToFullScreen = () => {}; const { restartProgress, stopProgress, slideshowDelay, showProgressBar, slideshowNavigation } = slideshowStore; let progressBarStatus: ProgressBarStatus; let progressBar: ProgressBar; let showSettings = false; + let showControls = true; + let timer: NodeJS.Timeout; + let isOverControls = false; let unsubscribeRestart: () => void; let unsubscribeStop: () => void; + const resetTimer = () => { + clearTimeout(timer); + document.body.style.cursor = ''; + showControls = true; + startTimer(); + }; + + const startTimer = () => { + timer = setTimeout(() => { + if (!isOverControls) { + showControls = false; + document.body.style.cursor = 'none'; + } + }, 10_000); + }; + onMount(() => { + startTimer(); unsubscribeRestart = restartProgress.subscribe((value) => { if (value) { progressBar.restart(value); @@ -52,19 +75,37 @@ }; -
- - (progressBarStatus === ProgressBarStatus.Paused ? progressBar.play() : progressBar.pause())} - title={progressBarStatus === ProgressBarStatus.Paused ? 'Play' : 'Pause'} - /> - - - (showSettings = !showSettings)} title="Next" /> -
+ +{#if showControls} + +
(isOverControls = true)} + on:mouseleave={() => (isOverControls = false)} + transition:fly={{ duration: 150 }} + > + + + (progressBarStatus === ProgressBarStatus.Paused ? progressBar.play() : progressBar.pause())} + title={progressBarStatus === ProgressBarStatus.Paused ? 'Play' : 'Pause'} + /> + + + (showSettings = !showSettings)} title="Next" /> + {#if !isFullScreen} + + {/if} +
+{/if} {#if showSettings} (showSettings = false)} /> {/if} diff --git a/web/src/lib/components/asset-viewer/video-viewer.svelte b/web/src/lib/components/asset-viewer/video-viewer.svelte index f5ff5f0fc..c53fcf600 100644 --- a/web/src/lib/components/asset-viewer/video-viewer.svelte +++ b/web/src/lib/components/asset-viewer/video-viewer.svelte @@ -9,7 +9,9 @@ export let assetId: string; + let element: HTMLVideoElement | undefined = undefined; let isVideoLoading = true; + const dispatch = createEventDispatcher<{ onVideoEnded: void; onVideoStarted: void }>(); const handleCanPlay = async (event: Event) => { @@ -29,6 +31,7 @@
diff --git a/web/src/lib/components/shared-components/combobox.svelte b/web/src/lib/components/shared-components/combobox.svelte index 19fd73d25..28a2b7253 100644 --- a/web/src/lib/components/shared-components/combobox.svelte +++ b/web/src/lib/components/shared-components/combobox.svelte @@ -18,6 +18,7 @@ import type { FormEventHandler } from 'svelte/elements'; import { shortcuts } from '$lib/utils/shortcut'; import { clickOutside } from '$lib/utils/click-outside'; + import { focusOutside } from '$lib/utils/focus-outside'; /** * Unique identifier for the combobox. @@ -40,6 +41,7 @@ let searchQuery = selectedOption?.label || ''; let selectedIndex: number | undefined; let optionRefs: HTMLElement[] = []; + let input: HTMLInputElement; const inputId = `combobox-${id}`; const listboxId = `listbox-${id}`; @@ -51,7 +53,6 @@ const dispatch = createEventDispatcher<{ select: ComboBoxOption | undefined; - click: void; }>(); const activate = () => { @@ -101,6 +102,8 @@ }; const onClear = () => { + input?.focus(); + selectedIndex = undefined; selectedOption = undefined; searchQuery = ''; dispatch('select', selectedOption); @@ -111,11 +114,16 @@
{ - if (e.relatedTarget instanceof Node && !e.currentTarget.contains(e.relatedTarget)) { - deactivate(); - } - }} + use:focusOutside={{ onFocusOut: deactivate }} + use:shortcuts={[ + { + shortcut: { key: 'Escape' }, + onShortcut: (event) => { + event.stopPropagation(); + closeDropdown(); + }, + }, + ]} >
{#if isActive} @@ -133,6 +141,7 @@ aria-controls={listboxId} aria-expanded={isOpen} autocomplete="off" + bind:this={input} class:!pl-8={isActive} class:!rounded-b-none={isOpen} class:cursor-pointer={!isActive} @@ -213,9 +222,7 @@ role="option" aria-selected={selectedIndex === 0} aria-disabled={true} - class:bg-gray-100={selectedIndex === 0} - class:dark:bg-gray-700={selectedIndex === 0} - class="text-left w-full px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-700 cursor-default" + class="text-left w-full px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-700 cursor-default aria-selected:bg-gray-100 aria-selected:dark:bg-gray-700" id={`${listboxId}-${0}`} on:click={() => closeDropdown()} > diff --git a/web/src/lib/components/shared-components/context-menu/context-menu.svelte b/web/src/lib/components/shared-components/context-menu/context-menu.svelte index c10371d83..aabf8c25c 100644 --- a/web/src/lib/components/shared-components/context-menu/context-menu.svelte +++ b/web/src/lib/components/shared-components/context-menu/context-menu.svelte @@ -23,12 +23,14 @@ diff --git a/web/src/lib/components/shared-components/context-menu/menu-option.svelte b/web/src/lib/components/shared-components/context-menu/menu-option.svelte index d2e2eb69b..6d5adc419 100644 --- a/web/src/lib/components/shared-components/context-menu/menu-option.svelte +++ b/web/src/lib/components/shared-components/context-menu/menu-option.svelte @@ -1,6 +1,9 @@

- {notificationInfo.message} + {notification.message}

diff --git a/web/src/lib/components/shared-components/notification/notification-list.svelte b/web/src/lib/components/shared-components/notification/notification-list.svelte index bf8d93d5f..d94ff5c14 100644 --- a/web/src/lib/components/shared-components/notification/notification-list.svelte +++ b/web/src/lib/components/shared-components/notification/notification-list.svelte @@ -11,9 +11,9 @@ {#if $notificationList.length > 0}
- {#each $notificationList as notificationInfo (notificationInfo.id)} + {#each $notificationList as notification (notification.id)}
- +
{/each}
diff --git a/web/src/lib/components/shared-components/notification/notification.ts b/web/src/lib/components/shared-components/notification/notification.ts index f1a214046..52e75bf41 100644 --- a/web/src/lib/components/shared-components/notification/notification.ts +++ b/web/src/lib/components/shared-components/notification/notification.ts @@ -6,57 +6,43 @@ export enum NotificationType { Warning = 'Warning', } -export class ImmichNotification { - id = Date.now() + Math.random(); - type!: NotificationType; - message!: string; - action!: NotificationAction; - timeout = 3000; -} +export type Notification = { + id: number; + type: NotificationType; + message: string; + /** The action to take when the notification is clicked */ + action: NotificationAction; + /** Timeout in miliseconds */ + timeout: number; +}; type DiscardAction = { type: 'discard' }; type NoopAction = { type: 'noop' }; type LinkAction = { type: 'link'; target: string }; export type NotificationAction = DiscardAction | NoopAction | LinkAction; -export class ImmichNotificationDto { - /** - * Notification type - * @type {NotificationType} [Info, Error] - */ - type: NotificationType = NotificationType.Info; - - /** - * Notification message - */ - message = ''; - - /** - * Timeout in miliseconds - */ - timeout?: number; - - /** - * The action to take when the notification is clicked - */ - action?: NotificationAction; -} +export type NotificationOptions = Partial> & { message: string }; function createNotificationList() { - const notificationList = writable([]); + const notificationList = writable([]); + let count = 1; - const show = (notificationInfo: ImmichNotificationDto) => { - const newNotification = new ImmichNotification(); - newNotification.message = notificationInfo.message; - newNotification.type = notificationInfo.type; - newNotification.timeout = notificationInfo.timeout || 3000; - newNotification.action = notificationInfo.action || { type: 'discard' }; + const show = (options: NotificationOptions) => { + notificationList.update((currentList) => { + currentList.push({ + id: count++, + type: NotificationType.Info, + action: { type: 'discard' }, + timeout: 3000, + ...options, + }); - notificationList.update((currentList) => [...currentList, newNotification]); + return currentList; + }); }; const removeNotificationById = (id: number) => { - notificationList.update((currentList) => currentList.filter((n) => n.id != id)); + notificationList.update((currentList) => currentList.filter((n) => n.id !== id)); }; return { diff --git a/web/src/lib/components/shared-components/search-bar/search-bar.svelte b/web/src/lib/components/shared-components/search-bar/search-bar.svelte index 8afa56df7..6c057483d 100644 --- a/web/src/lib/components/shared-components/search-bar/search-bar.svelte +++ b/web/src/lib/components/shared-components/search-bar/search-bar.svelte @@ -12,6 +12,7 @@ import { getMetadataSearchQuery } from '$lib/utils/metadata-search'; import { handlePromiseError } from '$lib/utils'; import { shortcut } from '$lib/utils/shortcut'; + import { focusOutside } from '$lib/utils/focus-outside'; export let value = ''; export let grayTheme: boolean; @@ -94,7 +95,7 @@ }} /> -
+
void; + selectAssets: (assets: AssetResponseDto[]) => void; removeAssetFromMultiselectGroup: (asset: AssetResponseDto) => void; addGroupToMultiselectGroup: (group: string) => void; removeGroupFromMultiselectGroup: (group: string) => void; @@ -76,6 +77,13 @@ export function createAssetInteractionStore(): AssetInteractionStore { selectedAssets.set(_selectedAssets); }; + const selectAssets = (assets: AssetResponseDto[]) => { + for (const asset of assets) { + _selectedAssets.add(asset); + } + selectedAssets.set(_selectedAssets); + }; + const removeAssetFromMultiselectGroup = (asset: AssetResponseDto) => { _selectedAssets.delete(asset); selectedAssets.set(_selectedAssets); @@ -123,6 +131,7 @@ export function createAssetInteractionStore(): AssetInteractionStore { return { selectAsset, + selectAssets, removeAssetFromMultiselectGroup, addGroupToMultiselectGroup, removeGroupFromMultiselectGroup, diff --git a/web/src/lib/stores/assets.store.ts b/web/src/lib/stores/assets.store.ts index 45b26c1a1..4cc92ab86 100644 --- a/web/src/lib/stores/assets.store.ts +++ b/web/src/lib/stores/assets.store.ts @@ -519,4 +519,4 @@ export class AssetStore { } } -export const isSelectAllCancelled = writable(false); +export const isSelectingAllAssets = writable(false); diff --git a/web/src/lib/utils.ts b/web/src/lib/utils.ts index 2cebba630..8f224e93b 100644 --- a/web/src/lib/utils.ts +++ b/web/src/lib/utils.ts @@ -12,6 +12,7 @@ import { unlinkOAuthAccount, type UserResponseDto, } from '@immich/sdk'; +import { mdiCogRefreshOutline, mdiDatabaseRefreshOutline, mdiImageRefreshOutline } from '@mdi/js'; interface DownloadRequestOptions { method?: 'GET' | 'POST' | 'PUT' | 'DELETE'; @@ -196,6 +197,16 @@ export const getAssetJobMessage = (job: AssetJobName) => { return messages[job]; }; +export const getAssetJobIcon = (job: AssetJobName) => { + const names: Record = { + [AssetJobName.RefreshMetadata]: mdiDatabaseRefreshOutline, + [AssetJobName.RegenerateThumbnail]: mdiImageRefreshOutline, + [AssetJobName.TranscodeVideo]: mdiCogRefreshOutline, + }; + + return names[job]; +}; + export const copyToClipboard = async (secret: string) => { try { await navigator.clipboard.writeText(secret); diff --git a/web/src/lib/utils/asset-utils.ts b/web/src/lib/utils/asset-utils.ts index f1a3d44be..27c99a473 100644 --- a/web/src/lib/utils/asset-utils.ts +++ b/web/src/lib/utils/asset-utils.ts @@ -1,4 +1,6 @@ import { notificationController, NotificationType } from '$lib/components/shared-components/notification/notification'; +import type { AssetInteractionStore } from '$lib/stores/asset-interaction.store'; +import { BucketPosition, isSelectingAllAssets, type AssetStore } from '$lib/stores/assets.store'; import { downloadManager } from '$lib/stores/download'; import { downloadRequest, getKey } from '$lib/utils'; import { @@ -13,6 +15,7 @@ import { type UserResponseDto, } from '@immich/sdk'; import { DateTime } from 'luxon'; +import { get } from 'svelte/store'; import { handleError } from './handle-error'; export const addAssetsToAlbum = async (albumId: string, assetIds: Array): Promise => @@ -224,6 +227,35 @@ export const getSelectedAssets = (assets: Set, user: UserRespo return ids; }; +export const selectAllAssets = async (assetStore: AssetStore, assetInteractionStore: AssetInteractionStore) => { + if (get(isSelectingAllAssets)) { + // Selection is already ongoing + return; + } + isSelectingAllAssets.set(true); + + try { + for (const bucket of assetStore.buckets) { + await assetStore.loadBucket(bucket.bucketDate, BucketPosition.Unknown); + + if (!get(isSelectingAllAssets)) { + break; // Cancelled + } + assetInteractionStore.selectAssets(bucket.assets); + + // We use setTimeout to allow the UI to update. Otherwise, this may + // cause a long delay between the start of 'select all' and the + // effective update of the UI, depending on the number of assets + // to select + await delay(0); + } + } catch (error) { + handleError(error, 'Error selecting all assets'); + } finally { + isSelectingAllAssets.set(false); + } +}; + export const delay = async (ms: number) => { return new Promise((resolve) => setTimeout(resolve, ms)); }; diff --git a/web/src/lib/utils/focus-outside.ts b/web/src/lib/utils/focus-outside.ts new file mode 100644 index 000000000..c8a4d574c --- /dev/null +++ b/web/src/lib/utils/focus-outside.ts @@ -0,0 +1,21 @@ +interface Options { + onFocusOut?: () => void; +} + +export function focusOutside(node: HTMLElement, options: Options = {}) { + const { onFocusOut } = options; + + const handleFocusOut = (event: FocusEvent) => { + if (onFocusOut && event.relatedTarget instanceof Node && !node.contains(event.relatedTarget as Node)) { + onFocusOut(); + } + }; + + node.addEventListener('focusout', handleFocusOut); + + return { + destroy() { + node.removeEventListener('focusout', handleFocusOut); + }, + }; +} diff --git a/web/src/routes/(user)/albums/[albumId]/+page.svelte b/web/src/routes/(user)/albums/[albumId]/+page.svelte index 2f74a7872..23e6a1b2e 100644 --- a/web/src/routes/(user)/albums/[albumId]/+page.svelte +++ b/web/src/routes/(user)/albums/[albumId]/+page.svelte @@ -64,11 +64,14 @@ mdiArrowLeft, mdiDeleteOutline, mdiDotsVertical, - mdiFileImagePlusOutline, mdiFolderDownloadOutline, mdiLink, mdiPlus, mdiShareVariantOutline, + mdiPresentationPlay, + mdiCogOutline, + mdiImageOutline, + mdiImagePlusOutline, } from '@mdi/js'; import { fly } from 'svelte/transition'; import type { PageData } from './$types'; @@ -385,23 +388,25 @@ assetInteractionStore.clearMultiselect()}> - + + {#if isAllUserOwned} + assetStore.triggerUpdate()} /> + {/if} + {#if isAllUserOwned} - assetStore.triggerUpdate()} /> + + assetStore.triggerUpdate()} /> {/if} - {#if isOwned || isAllUserOwned} {/if} {#if isAllUserOwned} - - {/if} @@ -410,9 +415,9 @@ goto(backUrl)}> (viewMode = ViewMode.SELECT_ASSETS)} - icon={mdiFileImagePlusOutline} + icon={mdiImagePlusOutline} /> {#if isOwned} @@ -421,11 +426,6 @@ on:click={() => (viewMode = ViewMode.SELECT_USERS)} icon={mdiShareVariantOutline} /> - (viewMode = ViewMode.CONFIRM_DELETE)} - icon={mdiDeleteOutline} - /> {/if} {#if album.assetCount > 0} @@ -436,9 +436,22 @@ {#if viewMode === ViewMode.ALBUM_OPTIONS} - - (viewMode = ViewMode.SELECT_THUMBNAIL)} text="Set album cover" /> - (viewMode = ViewMode.OPTIONS)} text="Options" /> + + (viewMode = ViewMode.SELECT_THUMBNAIL)} + /> + (viewMode = ViewMode.OPTIONS)} + /> + (viewMode = ViewMode.CONFIRM_DELETE)} + /> {/if} diff --git a/web/src/routes/(user)/archive/+page.svelte b/web/src/routes/(user)/archive/+page.svelte index 93dc09a50..2f943a4ff 100644 --- a/web/src/routes/(user)/archive/+page.svelte +++ b/web/src/routes/(user)/archive/+page.svelte @@ -31,14 +31,14 @@ assetStore.removeAssets(assetIds)} /> - + - assetStore.removeAssets(assetIds)} /> + assetStore.triggerUpdate()} /> - assetStore.triggerUpdate()} /> + assetStore.removeAssets(assetIds)} /> {/if} diff --git a/web/src/routes/(user)/favorites/+page.svelte b/web/src/routes/(user)/favorites/+page.svelte index 067859b69..1d080d100 100644 --- a/web/src/routes/(user)/favorites/+page.svelte +++ b/web/src/routes/(user)/favorites/+page.svelte @@ -34,16 +34,16 @@ assetStore.removeAssets(assetIds)} /> - + - assetStore.removeAssets(assetIds)} /> - assetStore.removeAssets(assetIds)} /> + assetStore.removeAssets(assetIds)} /> + assetStore.removeAssets(assetIds)} /> {/if} diff --git a/web/src/routes/(user)/people/[personId]/+page.svelte b/web/src/routes/(user)/people/[personId]/+page.svelte index ae99b0762..3c5c19944 100644 --- a/web/src/routes/(user)/people/[personId]/+page.svelte +++ b/web/src/routes/(user)/people/[personId]/+page.svelte @@ -44,7 +44,16 @@ type AssetResponseDto, type PersonResponseDto, } from '@immich/sdk'; - import { mdiArrowLeft, mdiDotsVertical, mdiPlus } from '@mdi/js'; + import { + mdiAccountBoxOutline, + mdiAccountMultipleCheckOutline, + mdiArrowLeft, + mdiCalendarEditOutline, + mdiDotsVertical, + mdiEyeOffOutline, + mdiEyeOutline, + mdiPlus, + } from '@mdi/js'; import { onMount } from 'svelte'; import type { PageData } from './$types'; import { listNavigation } from '$lib/utils/list-navigation'; @@ -395,18 +404,18 @@ assetInteractionStore.clearMultiselect()}> - + - $assetStore.removeAssets(assetIds)} /> + assetStore.triggerUpdate()} /> - assetStore.triggerUpdate()} /> - $assetStore.removeAssets(assetIds)} /> - + + $assetStore.removeAssets(assetIds)} /> + $assetStore.removeAssets(assetIds)} /> {:else} @@ -414,13 +423,26 @@ goto(previousRoute)}> - (viewMode = ViewMode.SELECT_PERSON)} /> - (viewMode = ViewMode.BIRTH_DATE)} /> - (viewMode = ViewMode.MERGE_PEOPLE)} /> + (viewMode = ViewMode.SELECT_PERSON)} + /> toggleHidePerson()} /> + (viewMode = ViewMode.BIRTH_DATE)} + /> + (viewMode = ViewMode.MERGE_PEOPLE)} + /> @@ -428,13 +450,13 @@ {#if viewMode === ViewMode.SELECT_PERSON} (viewMode = ViewMode.VIEW_ASSETS)}> - Select feature photo + Select featured photo {/if} {/if} -
+
{#key refreshAssetGrid} (handleEscapeKey = true)} /> - + - (handleEscapeKey = true)} - onAssetDelete={(assetIds) => assetStore.removeAssets(assetIds)} - /> + assetStore.triggerUpdate()} /> - assetStore.triggerUpdate()} /> - assetStore.removeAssets(assetIds)} /> {#if $selectedAssets.size > 1} assetStore.removeAssets(assetIds)} /> {/if} + assetStore.removeAssets(assetIds)} /> + (handleEscapeKey = true)} + onAssetDelete={(assetIds) => assetStore.removeAssets(assetIds)} + /> +
diff --git a/web/src/routes/(user)/search/+page.svelte b/web/src/routes/(user)/search/+page.svelte index 5774541e4..ab726b55c 100644 --- a/web/src/routes/(user)/search/+page.svelte +++ b/web/src/routes/(user)/search/+page.svelte @@ -209,18 +209,18 @@ (selectedAssets = new Set())}> - + - + - - + +
diff --git a/web/src/routes/(user)/trash/+page.svelte b/web/src/routes/(user)/trash/+page.svelte index fd176b7a1..66e3bb507 100644 --- a/web/src/routes/(user)/trash/+page.svelte +++ b/web/src/routes/(user)/trash/+page.svelte @@ -76,13 +76,13 @@
- Restore All + Restore all
(isShowEmptyConfirmation = true)}>
- Empty Trash + Empty trash
diff --git a/web/tailwind.config.cjs b/web/tailwind.config.cjs index 8dee52c5d..3d4176bd7 100644 --- a/web/tailwind.config.cjs +++ b/web/tailwind.config.cjs @@ -30,6 +30,9 @@ module.exports = { spacing: { 18: '4.5rem', }, + screens: { + tall: { raw: '(min-height: 800px)' }, + }, }, }, };