From d5cf8e4bfee968164248bd533b010cce558a05e8 Mon Sep 17 00:00:00 2001 From: Min Idzelis Date: Fri, 24 May 2024 21:02:22 -0400 Subject: [PATCH] refactor(server): move checkExistingAssets(), checkBulkUpdate() remove getAllAssets() (#9715) * Refactor controller methods, non-breaking change * Remove getAllAssets * used imports * sync:sql * missing mock * Removing remaining references * chore: remove unused code --------- Co-authored-by: Jason Rasmussen --- e2e/src/api/specs/asset.e2e-spec.ts | 21 ---- e2e/src/api/specs/trash.e2e-spec.ts | 18 +-- e2e/src/cli/specs/upload.e2e-spec.ts | 82 +++++++------- e2e/src/utils.ts | 3 - mobile/openapi/README.md | Bin 27342 -> 27259 bytes mobile/openapi/lib/api/asset_api.dart | Bin 39871 -> 36377 bytes open-api/immich-openapi-specs.json | 103 ------------------ open-api/typescript-sdk/README.md | 5 +- open-api/typescript-sdk/src/fetch-client.ts | 31 ------ .../src/controllers/asset-media.controller.ts | 42 ++++++- server/src/controllers/asset-v1.controller.ts | 58 +--------- server/src/dtos/asset-media-response.dto.ts | 25 +++++ server/src/dtos/asset-media.dto.ts | 31 +++++- server/src/dtos/asset-v1-response.dto.ts | 25 ----- server/src/dtos/asset-v1.dto.ts | 62 +---------- server/src/interfaces/asset-v1.interface.ts | 4 - server/src/interfaces/asset.interface.ts | 2 + server/src/queries/asset.repository.sql | 14 +++ server/src/queries/library.repository.sql | 37 ------- .../src/repositories/asset-v1.repository.ts | 60 +--------- server/src/repositories/asset.repository.ts | 27 +++++ server/src/repositories/library.repository.ts | 15 --- .../src/services/asset-media.service.spec.ts | 29 ++++- server/src/services/asset-media.service.ts | 63 ++++++++++- server/src/services/asset-v1.service.spec.ts | 32 ------ server/src/services/asset-v1.service.ts | 67 +----------- .../repositories/asset.repository.mock.ts | 2 + 27 files changed, 286 insertions(+), 572 deletions(-) diff --git a/e2e/src/api/specs/asset.e2e-spec.ts b/e2e/src/api/specs/asset.e2e-spec.ts index 5703d2ae72..c110440132 100644 --- a/e2e/src/api/specs/asset.e2e-spec.ts +++ b/e2e/src/api/specs/asset.e2e-spec.ts @@ -699,27 +699,6 @@ describe('/asset', () => { }); }); - describe('GET /asset', () => { - it('should return stack data', async () => { - const { status, body } = await request(app).get('/asset').set('Authorization', `Bearer ${stackUser.accessToken}`); - - const stack = body.find((asset: AssetResponseDto) => asset.id === stackAssets[0].id); - - expect(status).toBe(200); - expect(stack).toEqual( - expect.objectContaining({ - stackCount: 3, - stack: - // Response includes children at the root level - expect.arrayContaining([ - expect.objectContaining({ id: stackAssets[1].id }), - expect.objectContaining({ id: stackAssets[2].id }), - ]), - }), - ); - }); - }); - describe('PUT /asset', () => { it('should require authentication', async () => { const { status, body } = await request(app).put('/asset'); diff --git a/e2e/src/api/specs/trash.e2e-spec.ts b/e2e/src/api/specs/trash.e2e-spec.ts index e86f6d497a..3049ff1511 100644 --- a/e2e/src/api/specs/trash.e2e-spec.ts +++ b/e2e/src/api/specs/trash.e2e-spec.ts @@ -1,4 +1,4 @@ -import { LoginResponseDto, getAllAssets } from '@immich/sdk'; +import { LoginResponseDto, getAssetInfo, getAssetStatistics } from '@immich/sdk'; import { Socket } from 'socket.io-client'; import { errorDto } from 'src/responses'; import { app, asBearerAuth, utils } from 'src/utils'; @@ -31,16 +31,16 @@ describe('/trash', () => { const { id: assetId } = await utils.createAsset(admin.accessToken); await utils.deleteAssets(admin.accessToken, [assetId]); - const before = await getAllAssets({}, { headers: asBearerAuth(admin.accessToken) }); - expect(before).toStrictEqual([expect.objectContaining({ id: assetId, isTrashed: true })]); + const before = await getAssetInfo({ id: assetId }, { headers: asBearerAuth(admin.accessToken) }); + expect(before).toStrictEqual(expect.objectContaining({ id: assetId, isTrashed: true })); const { status } = await request(app).post('/trash/empty').set('Authorization', `Bearer ${admin.accessToken}`); expect(status).toBe(204); await utils.waitForWebsocketEvent({ event: 'assetDelete', id: assetId }); - const after = await getAllAssets({}, { headers: asBearerAuth(admin.accessToken) }); - expect(after.length).toBe(0); + const after = await getAssetStatistics({ isTrashed: true }, { headers: asBearerAuth(admin.accessToken) }); + expect(after.total).toBe(0); }); }); @@ -56,14 +56,14 @@ describe('/trash', () => { const { id: assetId } = await utils.createAsset(admin.accessToken); await utils.deleteAssets(admin.accessToken, [assetId]); - const before = await getAllAssets({}, { headers: asBearerAuth(admin.accessToken) }); - expect(before).toStrictEqual([expect.objectContaining({ id: assetId, isTrashed: true })]); + const before = await getAssetInfo({ id: assetId }, { headers: asBearerAuth(admin.accessToken) }); + expect(before).toStrictEqual(expect.objectContaining({ id: assetId, isTrashed: true })); const { status } = await request(app).post('/trash/restore').set('Authorization', `Bearer ${admin.accessToken}`); expect(status).toBe(204); - const after = await getAllAssets({}, { headers: asBearerAuth(admin.accessToken) }); - expect(after).toStrictEqual([expect.objectContaining({ id: assetId, isTrashed: false })]); + const after = await getAssetInfo({ id: assetId }, { headers: asBearerAuth(admin.accessToken) }); + expect(after).toStrictEqual(expect.objectContaining({ id: assetId, isTrashed: false })); }); }); diff --git a/e2e/src/cli/specs/upload.e2e-spec.ts b/e2e/src/cli/specs/upload.e2e-spec.ts index 89f38feadc..db2b6c5341 100644 --- a/e2e/src/cli/specs/upload.e2e-spec.ts +++ b/e2e/src/cli/specs/upload.e2e-spec.ts @@ -1,4 +1,4 @@ -import { LoginResponseDto, getAllAlbums, getAllAssets } from '@immich/sdk'; +import { LoginResponseDto, getAllAlbums, getAssetStatistics } from '@immich/sdk'; import { readFileSync } from 'node:fs'; import { mkdir, readdir, rm, symlink } from 'node:fs/promises'; import { asKeyAuth, immichCli, testAssetDir, utils } from 'src/utils'; @@ -28,8 +28,8 @@ describe(`immich upload`, () => { ); expect(exitCode).toBe(0); - const assets = await getAllAssets({}, { headers: asKeyAuth(key) }); - expect(assets.length).toBe(1); + const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) }); + expect(assets.total).toBe(1); }); it('should skip a duplicate file', async () => { @@ -40,8 +40,8 @@ describe(`immich upload`, () => { ); expect(first.exitCode).toBe(0); - const assets = await getAllAssets({}, { headers: asKeyAuth(key) }); - expect(assets.length).toBe(1); + const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) }); + expect(assets.total).toBe(1); const second = await immichCli(['upload', `${testAssetDir}/albums/nature/silver_fir.jpg`]); expect(second.stderr).toBe(''); @@ -60,8 +60,8 @@ describe(`immich upload`, () => { 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); + const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) }); + expect(assets.total).toBe(0); }); it('should have accurate dry run', async () => { @@ -76,8 +76,8 @@ describe(`immich upload`, () => { ); expect(exitCode).toBe(0); - const assets = await getAllAssets({}, { headers: asKeyAuth(key) }); - expect(assets.length).toBe(0); + const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) }); + expect(assets.total).toBe(0); }); it('dry run should handle duplicates', async () => { @@ -88,8 +88,8 @@ describe(`immich upload`, () => { ); expect(first.exitCode).toBe(0); - const assets = await getAllAssets({}, { headers: asKeyAuth(key) }); - expect(assets.length).toBe(1); + const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) }); + expect(assets.total).toBe(1); const second = await immichCli(['upload', `${testAssetDir}/albums/nature/`, '--dry-run']); expect(second.stderr).toBe(''); @@ -112,8 +112,8 @@ describe(`immich upload`, () => { ); expect(exitCode).toBe(0); - const assets = await getAllAssets({}, { headers: asKeyAuth(key) }); - expect(assets.length).toBe(9); + const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) }); + expect(assets.total).toBe(9); }); }); @@ -135,8 +135,8 @@ describe(`immich upload`, () => { expect(stderr).toBe(''); expect(exitCode).toBe(0); - const assets = await getAllAssets({}, { headers: asKeyAuth(key) }); - expect(assets.length).toBe(9); + const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) }); + expect(assets.total).toBe(9); const albums = await getAllAlbums({}, { headers: asKeyAuth(key) }); expect(albums.length).toBe(1); @@ -151,8 +151,8 @@ describe(`immich upload`, () => { expect(response1.stderr).toBe(''); expect(response1.exitCode).toBe(0); - const assets1 = await getAllAssets({}, { headers: asKeyAuth(key) }); - expect(assets1.length).toBe(9); + const assets1 = await getAssetStatistics({}, { headers: asKeyAuth(key) }); + expect(assets1.total).toBe(9); const albums1 = await getAllAlbums({}, { headers: asKeyAuth(key) }); expect(albums1.length).toBe(0); @@ -167,8 +167,8 @@ describe(`immich upload`, () => { expect(response2.stderr).toBe(''); expect(response2.exitCode).toBe(0); - const assets2 = await getAllAssets({}, { headers: asKeyAuth(key) }); - expect(assets2.length).toBe(9); + const assets2 = await getAssetStatistics({}, { headers: asKeyAuth(key) }); + expect(assets2.total).toBe(9); const albums2 = await getAllAlbums({}, { headers: asKeyAuth(key) }); expect(albums2.length).toBe(1); @@ -193,8 +193,8 @@ describe(`immich upload`, () => { expect(stderr).toBe(''); expect(exitCode).toBe(0); - const assets = await getAllAssets({}, { headers: asKeyAuth(key) }); - expect(assets.length).toBe(0); + const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) }); + expect(assets.total).toBe(0); const albums = await getAllAlbums({}, { headers: asKeyAuth(key) }); expect(albums.length).toBe(0); @@ -219,8 +219,8 @@ describe(`immich upload`, () => { expect(stderr).toBe(''); expect(exitCode).toBe(0); - const assets = await getAllAssets({}, { headers: asKeyAuth(key) }); - expect(assets.length).toBe(9); + const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) }); + expect(assets.total).toBe(9); const albums = await getAllAlbums({}, { headers: asKeyAuth(key) }); expect(albums.length).toBe(1); @@ -245,8 +245,8 @@ describe(`immich upload`, () => { expect(stderr).toBe(''); expect(exitCode).toBe(0); - const assets = await getAllAssets({}, { headers: asKeyAuth(key) }); - expect(assets.length).toBe(0); + const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) }); + expect(assets.total).toBe(0); const albums = await getAllAlbums({}, { headers: asKeyAuth(key) }); expect(albums.length).toBe(0); @@ -276,8 +276,8 @@ describe(`immich upload`, () => { expect(stderr).toBe(''); expect(exitCode).toBe(0); - const assets = await getAllAssets({}, { headers: asKeyAuth(key) }); - expect(assets.length).toBe(9); + const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) }); + expect(assets.total).toBe(9); }); it('should have accurate dry run', async () => { @@ -302,8 +302,8 @@ describe(`immich upload`, () => { expect(stderr).toBe(''); expect(exitCode).toBe(0); - const assets = await getAllAssets({}, { headers: asKeyAuth(key) }); - expect(assets.length).toBe(0); + const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) }); + expect(assets.total).toBe(0); }); }); @@ -328,8 +328,8 @@ describe(`immich upload`, () => { ); expect(exitCode).toBe(0); - const assets = await getAllAssets({}, { headers: asKeyAuth(key) }); - expect(assets.length).toBe(1); + const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) }); + expect(assets.total).toBe(1); }); it('should throw an error if attempting dry run', async () => { @@ -344,8 +344,8 @@ describe(`immich upload`, () => { expect(stderr).toEqual(`error: option '-n, --dry-run' cannot be used with option '-h, --skip-hash'`); expect(exitCode).not.toBe(0); - const assets = await getAllAssets({}, { headers: asKeyAuth(key) }); - expect(assets.length).toBe(0); + const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) }); + expect(assets.total).toBe(0); }); }); @@ -367,8 +367,8 @@ describe(`immich upload`, () => { ); expect(exitCode).toBe(0); - const assets = await getAllAssets({}, { headers: asKeyAuth(key) }); - expect(assets.length).toBe(9); + const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) }); + expect(assets.total).toBe(9); }); it('should reject string argument', async () => { @@ -408,8 +408,8 @@ describe(`immich upload`, () => { ); expect(exitCode).toBe(0); - const assets = await getAllAssets({}, { headers: asKeyAuth(key) }); - expect(assets.length).toBe(8); + const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) }); + expect(assets.total).toBe(8); }); it('should ignore assets matching glob pattern', async () => { @@ -429,8 +429,8 @@ describe(`immich upload`, () => { ); expect(exitCode).toBe(0); - const assets = await getAllAssets({}, { headers: asKeyAuth(key) }); - expect(assets.length).toBe(1); + const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) }); + expect(assets.total).toBe(1); }); it('should have accurate dry run', async () => { @@ -451,8 +451,8 @@ describe(`immich upload`, () => { ); expect(exitCode).toBe(0); - const assets = await getAllAssets({}, { headers: asKeyAuth(key) }); - expect(assets.length).toBe(0); + const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) }); + expect(assets.total).toBe(0); }); }); }); diff --git a/e2e/src/utils.ts b/e2e/src/utils.ts index 9f001910fe..be4faab707 100644 --- a/e2e/src/utils.ts +++ b/e2e/src/utils.ts @@ -17,7 +17,6 @@ import { createSharedLink, createUser, deleteAssets, - getAllAssets, getAllJobsStatus, getAssetInfo, getConfigDefaults, @@ -340,8 +339,6 @@ export const utils = { getAssetInfo: (accessToken: string, id: string) => getAssetInfo({ id }, { headers: asBearerAuth(accessToken) }), - getAllAssets: (accessToken: string) => getAllAssets({}, { headers: asBearerAuth(accessToken) }), - metadataSearch: async (accessToken: string, dto: MetadataSearchDto) => { return searchMetadata({ metadataSearchDto: dto }, { headers: asBearerAuth(accessToken) }); }, diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 402c3ccca070c7e6603d889d6499f7a6546e36b8..dbbbdc2fee08b960c01a192bbfdceb8f143f3af4 100644 GIT binary patch delta 14 WcmX?imGSo##tpj!H}eV|i~s;S2nO2# delta 38 ucmex;h4I`~#tpj!CqERG=1xy7NzBPfEG|whnLJTgc(Rn3$mT7A$0Gnx?ho() diff --git a/mobile/openapi/lib/api/asset_api.dart b/mobile/openapi/lib/api/asset_api.dart index 326237a93f526b2d9d4a331c12731e325c551206..58af13bd3d54a621ca879b26cdf603f60f276e6c 100644 GIT binary patch delta 14 WcmdnLooVJArVSRNo9`(-tN;Kpy#~1e delta 1000 zcmbO^hiU(IrVSRNa*oBtsU@y?C7C6a3Q4Is`FZIICHV>^8L0}T#i>P;b40W4GSmF> z^HO~iOOi9JxD*uh_4O4L6kJ0R(-rd56jBmP5)~42ic%9(Dix9wlQU9N6!P=H1|;WX zrskDQZd4DN{K1Kpr8qmYVDfz@F_x0V?9|B{{G^#n3sNS}F;?LP(upOhDNd5ewOJv2Q1y(^i-F3%*g?#?P3jDC!59y4Yu^u5}<{kR4R#_ zIIWO5lYe&^C4iEW6*Q@WQi>Hgy`UxpE0kmbmxDw%ToS^C#Vb%49KW0UyB{!4w)6S7 J*+BGV1pr91SzG`B diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 9809611535..82e328bc47 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -969,109 +969,6 @@ "Asset" ] }, - "get": { - "description": "Get all AssetEntity belong to the user", - "operationId": "getAllAssets", - "parameters": [ - { - "name": "if-none-match", - "in": "header", - "description": "ETag of data already cached on the client", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "isArchived", - "required": false, - "in": "query", - "schema": { - "type": "boolean" - } - }, - { - "name": "isFavorite", - "required": false, - "in": "query", - "schema": { - "type": "boolean" - } - }, - { - "name": "skip", - "required": false, - "in": "query", - "schema": { - "type": "integer" - } - }, - { - "name": "take", - "required": false, - "in": "query", - "schema": { - "type": "integer" - } - }, - { - "name": "updatedAfter", - "required": false, - "in": "query", - "schema": { - "format": "date-time", - "type": "string" - } - }, - { - "name": "updatedBefore", - "required": false, - "in": "query", - "schema": { - "format": "date-time", - "type": "string" - } - }, - { - "name": "userId", - "required": false, - "in": "query", - "schema": { - "format": "uuid", - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/AssetResponseDto" - }, - "type": "array" - } - } - }, - "description": "" - } - }, - "security": [ - { - "bearer": [] - }, - { - "cookie": [] - }, - { - "api_key": [] - } - ], - "tags": [ - "Asset" - ] - }, "put": { "operationId": "updateAssets", "parameters": [], diff --git a/open-api/typescript-sdk/README.md b/open-api/typescript-sdk/README.md index 9d74360b20..91b702d43e 100644 --- a/open-api/typescript-sdk/README.md +++ b/open-api/typescript-sdk/README.md @@ -13,15 +13,14 @@ npm i --save @immich/sdk For a more detailed example, check out the [`@immich/cli`](https://github.com/immich-app/immich/tree/main/cli). ```typescript -import { getAllAlbums, getAllAssets, getMyUserInfo, init } from "@immich/sdk"; +import { getAllAlbums, getMyUserInfo, init } from "@immich/sdk"; const API_KEY = ""; // process.env.IMMICH_API_KEY init({ baseUrl: "https://demo.immich.app/api", apiKey: API_KEY }); const user = await getMyUserInfo(); -const assets = await getAllAssets({ take: 1000 }); const albums = await getAllAlbums({}); -console.log({ user, assets, albums }); +console.log({ user, albums }); ``` diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 29ae94c428..adbae62bbd 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -1338,37 +1338,6 @@ export function deleteAssets({ assetBulkDeleteDto }: { body: assetBulkDeleteDto }))); } -/** - * Get all AssetEntity belong to the user - */ -export function getAllAssets({ ifNoneMatch, isArchived, isFavorite, skip, take, updatedAfter, updatedBefore, userId }: { - ifNoneMatch?: string; - isArchived?: boolean; - isFavorite?: boolean; - skip?: number; - take?: number; - updatedAfter?: string; - updatedBefore?: string; - userId?: string; -}, opts?: Oazapfts.RequestOpts) { - return oazapfts.ok(oazapfts.fetchJson<{ - status: 200; - data: AssetResponseDto[]; - }>(`/asset${QS.query(QS.explode({ - isArchived, - isFavorite, - skip, - take, - updatedAfter, - updatedBefore, - userId - }))}`, { - ...opts, - headers: oazapfts.mergeHeaders(opts?.headers, { - "if-none-match": ifNoneMatch - }) - })); -} export function updateAssets({ assetBulkUpdateDto }: { assetBulkUpdateDto: AssetBulkUpdateDto; }, opts?: Oazapfts.RequestOpts) { diff --git a/server/src/controllers/asset-media.controller.ts b/server/src/controllers/asset-media.controller.ts index cd0847142b..064a6d22ab 100644 --- a/server/src/controllers/asset-media.controller.ts +++ b/server/src/controllers/asset-media.controller.ts @@ -1,10 +1,12 @@ import { Body, Controller, + HttpCode, HttpStatus, Inject, Param, ParseFilePipe, + Post, Put, Res, UploadedFiles, @@ -13,8 +15,18 @@ import { import { ApiConsumes, ApiTags } from '@nestjs/swagger'; import { Response } from 'express'; import { EndpointLifecycle } from 'src/decorators'; -import { AssetMediaResponseDto, AssetMediaStatusEnum } from 'src/dtos/asset-media-response.dto'; -import { AssetMediaReplaceDto, UploadFieldName } from 'src/dtos/asset-media.dto'; +import { + AssetBulkUploadCheckResponseDto, + AssetMediaResponseDto, + AssetMediaStatusEnum, + CheckExistingAssetsResponseDto, +} from 'src/dtos/asset-media-response.dto'; +import { + AssetBulkUploadCheckDto, + AssetMediaReplaceDto, + CheckExistingAssetsDto, + UploadFieldName, +} from 'src/dtos/asset-media.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { Auth, Authenticated } from 'src/middleware/auth.guard'; @@ -53,4 +65,30 @@ export class AssetMediaController { } return responseDto; } + + /** + * Checks if multiple assets exist on the server and returns all existing - used by background backup + */ + @Post('exist') + @HttpCode(HttpStatus.OK) + @Authenticated() + checkExistingAssets( + @Auth() auth: AuthDto, + @Body() dto: CheckExistingAssetsDto, + ): Promise { + return this.service.checkExistingAssets(auth, dto); + } + + /** + * Checks if assets exist by checksums + */ + @Post('bulk-upload-check') + @HttpCode(HttpStatus.OK) + @Authenticated() + checkBulkUpload( + @Auth() auth: AuthDto, + @Body() dto: AssetBulkUploadCheckDto, + ): Promise { + return this.service.bulkUploadCheck(auth, dto); + } } diff --git a/server/src/controllers/asset-v1.controller.ts b/server/src/controllers/asset-v1.controller.ts index ba29e462cb..380aaca390 100644 --- a/server/src/controllers/asset-v1.controller.ts +++ b/server/src/controllers/asset-v1.controller.ts @@ -2,7 +2,6 @@ import { Body, Controller, Get, - HttpCode, HttpStatus, Inject, Next, @@ -16,20 +15,8 @@ import { } from '@nestjs/common'; import { ApiBody, ApiConsumes, ApiHeader, ApiTags } from '@nestjs/swagger'; import { NextFunction, Response } from 'express'; -import { AssetResponseDto } from 'src/dtos/asset-response.dto'; -import { - AssetBulkUploadCheckResponseDto, - AssetFileUploadResponseDto, - CheckExistingAssetsResponseDto, -} from 'src/dtos/asset-v1-response.dto'; -import { - AssetBulkUploadCheckDto, - AssetSearchDto, - CheckExistingAssetsDto, - CreateAssetDto, - GetAssetThumbnailDto, - ServeFileDto, -} from 'src/dtos/asset-v1.dto'; +import { AssetFileUploadResponseDto } from 'src/dtos/asset-v1-response.dto'; +import { CreateAssetDto, GetAssetThumbnailDto, ServeFileDto } from 'src/dtos/asset-v1.dto'; import { AuthDto, ImmichHeader } from 'src/dtos/auth.dto'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { AssetUploadInterceptor } from 'src/middleware/asset-upload.interceptor'; @@ -109,45 +96,4 @@ export class AssetControllerV1 { ) { await sendFile(res, next, () => this.service.serveThumbnail(auth, id, dto), this.logger); } - - /** - * Get all AssetEntity belong to the user - */ - @Get('/') - @ApiHeader({ - name: 'if-none-match', - description: 'ETag of data already cached on the client', - required: false, - schema: { type: 'string' }, - }) - @Authenticated() - getAllAssets(@Auth() auth: AuthDto, @Query() dto: AssetSearchDto): Promise { - return this.service.getAllAssets(auth, dto); - } - - /** - * Checks if multiple assets exist on the server and returns all existing - used by background backup - */ - @Post('/exist') - @HttpCode(HttpStatus.OK) - @Authenticated() - checkExistingAssets( - @Auth() auth: AuthDto, - @Body() dto: CheckExistingAssetsDto, - ): Promise { - return this.service.checkExistingAssets(auth, dto); - } - - /** - * Checks if assets exist by checksums - */ - @Post('/bulk-upload-check') - @HttpCode(HttpStatus.OK) - @Authenticated() - checkBulkUpload( - @Auth() auth: AuthDto, - @Body() dto: AssetBulkUploadCheckDto, - ): Promise { - return this.service.bulkUploadCheck(auth, dto); - } } diff --git a/server/src/dtos/asset-media-response.dto.ts b/server/src/dtos/asset-media-response.dto.ts index 7b65772f76..66e2e3160a 100644 --- a/server/src/dtos/asset-media-response.dto.ts +++ b/server/src/dtos/asset-media-response.dto.ts @@ -9,3 +9,28 @@ export class AssetMediaResponseDto { status!: AssetMediaStatusEnum; id!: string; } + +export enum AssetUploadAction { + ACCEPT = 'accept', + REJECT = 'reject', +} + +export enum AssetRejectReason { + DUPLICATE = 'duplicate', + UNSUPPORTED_FORMAT = 'unsupported-format', +} + +export class AssetBulkUploadCheckResult { + id!: string; + action!: AssetUploadAction; + reason?: AssetRejectReason; + assetId?: string; +} + +export class AssetBulkUploadCheckResponseDto { + results!: AssetBulkUploadCheckResult[]; +} + +export class CheckExistingAssetsResponseDto { + existingIds!: string[]; +} diff --git a/server/src/dtos/asset-media.dto.ts b/server/src/dtos/asset-media.dto.ts index a30ff8a107..2f8fa105cb 100644 --- a/server/src/dtos/asset-media.dto.ts +++ b/server/src/dtos/asset-media.dto.ts @@ -1,5 +1,6 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsNotEmpty, IsString } from 'class-validator'; +import { Type } from 'class-transformer'; +import { ArrayNotEmpty, IsArray, IsNotEmpty, IsString, ValidateNested } from 'class-validator'; import { Optional, ValidateDate } from 'src/validation'; export enum UploadFieldName { @@ -33,3 +34,31 @@ export class AssetMediaReplaceDto { @ApiProperty({ type: 'string', format: 'binary' }) [UploadFieldName.ASSET_DATA]!: any; } + +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 CheckExistingAssetsDto { + @ArrayNotEmpty() + @IsString({ each: true }) + @IsNotEmpty({ each: true }) + deviceAssetIds!: string[]; + + @IsNotEmpty() + deviceId!: string; +} diff --git a/server/src/dtos/asset-v1-response.dto.ts b/server/src/dtos/asset-v1-response.dto.ts index 687b336428..f628b708dc 100644 --- a/server/src/dtos/asset-v1-response.dto.ts +++ b/server/src/dtos/asset-v1-response.dto.ts @@ -1,29 +1,4 @@ -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[]; -} diff --git a/server/src/dtos/asset-v1.dto.ts b/server/src/dtos/asset-v1.dto.ts index 83c34ab0f7..0ec68e677a 100644 --- a/server/src/dtos/asset-v1.dto.ts +++ b/server/src/dtos/asset-v1.dto.ts @@ -1,68 +1,8 @@ import { ApiProperty } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; -import { ArrayNotEmpty, IsArray, IsEnum, IsInt, IsNotEmpty, IsString, IsUUID, ValidateNested } from 'class-validator'; +import { IsEnum, IsNotEmpty, IsString } from 'class-validator'; import { UploadFieldName } from 'src/dtos/asset.dto'; import { Optional, ValidateBoolean, ValidateDate } 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 { @IsNotEmpty() @IsString() diff --git a/server/src/interfaces/asset-v1.interface.ts b/server/src/interfaces/asset-v1.interface.ts index 799a303ba6..73d90019e2 100644 --- a/server/src/interfaces/asset-v1.interface.ts +++ b/server/src/interfaces/asset-v1.interface.ts @@ -1,4 +1,3 @@ -import { AssetSearchDto, CheckExistingAssetsDto } from 'src/dtos/asset-v1.dto'; import { AssetEntity } from 'src/entities/asset.entity'; export interface AssetCheck { @@ -12,10 +11,7 @@ export interface AssetOwnerCheck extends AssetCheck { export interface IAssetRepositoryV1 { get(id: string): Promise; - getAllByUserId(userId: string, dto: AssetSearchDto): 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.interface.ts b/server/src/interfaces/asset.interface.ts index 88f494c15b..63a2c5a770 100644 --- a/server/src/interfaces/asset.interface.ts +++ b/server/src/interfaces/asset.interface.ts @@ -168,8 +168,10 @@ export interface IAssetRepository { getByIdsWithAllRelations(ids: string[]): Promise; getByDayOfYear(ownerIds: string[], monthDay: MonthDay): Promise; getByChecksum(libraryId: string | null, checksum: Buffer): Promise; + getByChecksums(userId: string, checksums: Buffer[]): Promise; getUploadAssetIdByChecksum(ownerId: string, checksum: Buffer): Promise; getByAlbumId(pagination: PaginationOptions, albumId: string): Paginated; + getByDeviceIds(ownerId: string, deviceId: string, deviceAssetIds: string[]): Promise; getByUserId(pagination: PaginationOptions, userId: string, options?: AssetSearchOptions): Paginated; getById( id: string, diff --git a/server/src/queries/asset.repository.sql b/server/src/queries/asset.repository.sql index 6829c1445d..9615ebbcef 100644 --- a/server/src/queries/asset.repository.sql +++ b/server/src/queries/asset.repository.sql @@ -482,6 +482,20 @@ WHERE LIMIT 1 +-- AssetRepository.getByChecksums +SELECT + "AssetEntity"."id" AS "AssetEntity_id", + "AssetEntity"."checksum" AS "AssetEntity_checksum" +FROM + "assets" "AssetEntity" +WHERE + ( + ("AssetEntity"."ownerId" = $1) + AND ( + "AssetEntity"."checksum" IN ($2, $3, $4, $5, $6, $7, $8, $9, $10) + ) + ) + -- AssetRepository.getUploadAssetIdByChecksum SELECT "AssetEntity"."id" AS "AssetEntity_id" diff --git a/server/src/queries/library.repository.sql b/server/src/queries/library.repository.sql index 7f1e0eff93..b63bb35fd9 100644 --- a/server/src/queries/library.repository.sql +++ b/server/src/queries/library.repository.sql @@ -74,43 +74,6 @@ WHERE ((("LibraryEntity"."ownerId" = $1))) AND ("LibraryEntity"."deletedAt" IS NULL) --- LibraryRepository.getAllByUserId -SELECT - "LibraryEntity"."id" AS "LibraryEntity_id", - "LibraryEntity"."name" AS "LibraryEntity_name", - "LibraryEntity"."ownerId" AS "LibraryEntity_ownerId", - "LibraryEntity"."importPaths" AS "LibraryEntity_importPaths", - "LibraryEntity"."exclusionPatterns" AS "LibraryEntity_exclusionPatterns", - "LibraryEntity"."createdAt" AS "LibraryEntity_createdAt", - "LibraryEntity"."updatedAt" AS "LibraryEntity_updatedAt", - "LibraryEntity"."deletedAt" AS "LibraryEntity_deletedAt", - "LibraryEntity"."refreshedAt" AS "LibraryEntity_refreshedAt", - "LibraryEntity__LibraryEntity_owner"."id" AS "LibraryEntity__LibraryEntity_owner_id", - "LibraryEntity__LibraryEntity_owner"."name" AS "LibraryEntity__LibraryEntity_owner_name", - "LibraryEntity__LibraryEntity_owner"."isAdmin" AS "LibraryEntity__LibraryEntity_owner_isAdmin", - "LibraryEntity__LibraryEntity_owner"."email" AS "LibraryEntity__LibraryEntity_owner_email", - "LibraryEntity__LibraryEntity_owner"."storageLabel" AS "LibraryEntity__LibraryEntity_owner_storageLabel", - "LibraryEntity__LibraryEntity_owner"."oauthId" AS "LibraryEntity__LibraryEntity_owner_oauthId", - "LibraryEntity__LibraryEntity_owner"."profileImagePath" AS "LibraryEntity__LibraryEntity_owner_profileImagePath", - "LibraryEntity__LibraryEntity_owner"."shouldChangePassword" AS "LibraryEntity__LibraryEntity_owner_shouldChangePassword", - "LibraryEntity__LibraryEntity_owner"."createdAt" AS "LibraryEntity__LibraryEntity_owner_createdAt", - "LibraryEntity__LibraryEntity_owner"."deletedAt" AS "LibraryEntity__LibraryEntity_owner_deletedAt", - "LibraryEntity__LibraryEntity_owner"."status" AS "LibraryEntity__LibraryEntity_owner_status", - "LibraryEntity__LibraryEntity_owner"."updatedAt" AS "LibraryEntity__LibraryEntity_owner_updatedAt", - "LibraryEntity__LibraryEntity_owner"."quotaSizeInBytes" AS "LibraryEntity__LibraryEntity_owner_quotaSizeInBytes", - "LibraryEntity__LibraryEntity_owner"."quotaUsageInBytes" AS "LibraryEntity__LibraryEntity_owner_quotaUsageInBytes" -FROM - "libraries" "LibraryEntity" - LEFT JOIN "users" "LibraryEntity__LibraryEntity_owner" ON "LibraryEntity__LibraryEntity_owner"."id" = "LibraryEntity"."ownerId" - AND ( - "LibraryEntity__LibraryEntity_owner"."deletedAt" IS NULL - ) -WHERE - ((("LibraryEntity"."ownerId" = $1))) - AND ("LibraryEntity"."deletedAt" IS NULL) -ORDER BY - "LibraryEntity"."createdAt" ASC - -- LibraryRepository.getAll SELECT "LibraryEntity"."id" AS "LibraryEntity_id", diff --git a/server/src/repositories/asset-v1.repository.ts b/server/src/repositories/asset-v1.repository.ts index 4e346d5fdf..1278c17e95 100644 --- a/server/src/repositories/asset-v1.repository.ts +++ b/server/src/repositories/asset-v1.repository.ts @@ -1,9 +1,7 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { AssetSearchDto, CheckExistingAssetsDto } from 'src/dtos/asset-v1.dto'; import { AssetEntity } from 'src/entities/asset.entity'; -import { AssetCheck, AssetOwnerCheck, IAssetRepositoryV1 } from 'src/interfaces/asset-v1.interface'; -import { OptionalBetween } from 'src/utils/database'; +import { AssetCheck, IAssetRepositoryV1 } from 'src/interfaces/asset-v1.interface'; import { In } from 'typeorm/find-options/operator/In.js'; import { Repository } from 'typeorm/repository/Repository.js'; @@ -11,36 +9,6 @@ import { Repository } from 'typeorm/repository/Repository.js'; export class AssetRepositoryV1 implements IAssetRepositoryV1 { constructor(@InjectRepository(AssetEntity) private assetRepository: Repository) {} - /** - * Retrieves all assets by user ID. - * - * @param ownerId - The ID of the owner. - * @param dto - The AssetSearchDto object containing search criteria. - * @returns A Promise that resolves to an array of AssetEntity objects. - */ - getAllByUserId(ownerId: string, dto: AssetSearchDto): Promise { - return this.assetRepository.find({ - where: { - ownerId, - isVisible: true, - isFavorite: dto.isFavorite, - isArchived: dto.isArchived, - updatedAt: OptionalBetween(dto.updatedAfter, dto.updatedBefore), - }, - relations: { - exifInfo: true, - tags: true, - stack: { assets: true }, - }, - skip: dto.skip || 0, - take: dto.take, - order: { - fileCreatedAt: 'DESC', - }, - withDeleted: true, - }); - } - get(id: string): Promise { return this.assetRepository.findOne({ where: { id }, @@ -73,30 +41,4 @@ export class AssetRepositoryV1 implements IAssetRepositoryV1 { withDeleted: true, }); } - - async getExistingAssets(ownerId: string, checkDuplicateAssetDto: CheckExistingAssetsDto): Promise { - const assets = await this.assetRepository.find({ - select: { deviceAssetId: true }, - where: { - deviceAssetId: In(checkDuplicateAssetDto.deviceAssetIds), - deviceId: checkDuplicateAssetDto.deviceId, - ownerId, - }, - withDeleted: true, - }); - return assets.map((asset) => asset.deviceAssetId); - } - - getByOriginalPath(originalPath: string): Promise { - return this.assetRepository.findOne({ - select: { - id: true, - ownerId: true, - checksum: true, - }, - where: { - originalPath, - }, - }); - } } diff --git a/server/src/repositories/asset.repository.ts b/server/src/repositories/asset.repository.ts index 9a68b4e708..ee8d9dbb93 100644 --- a/server/src/repositories/asset.repository.ts +++ b/server/src/repositories/asset.repository.ts @@ -157,6 +157,18 @@ export class AssetRepository implements IAssetRepository { }); } + getByDeviceIds(ownerId: string, deviceId: string, deviceAssetIds: string[]): Promise { + return this.repository.find({ + select: { deviceAssetId: true }, + where: { + deviceAssetId: In(deviceAssetIds), + deviceId, + ownerId, + }, + withDeleted: true, + }); + } + getByUserId( pagination: PaginationOptions, userId: string, @@ -300,6 +312,21 @@ export class AssetRepository implements IAssetRepository { }); } + @GenerateSql({ params: [DummyValue.UUID, DummyValue.BUFFER] }) + getByChecksums(ownerId: string, checksums: Buffer[]): Promise { + return this.repository.find({ + select: { + id: true, + checksum: true, + }, + where: { + ownerId, + checksum: In(checksums), + }, + withDeleted: true, + }); + } + @GenerateSql({ params: [DummyValue.UUID, DummyValue.BUFFER] }) async getUploadAssetIdByChecksum(ownerId: string, checksum: Buffer): Promise { const asset = await this.repository.findOne({ diff --git a/server/src/repositories/library.repository.ts b/server/src/repositories/library.repository.ts index ca70780720..30338315e8 100644 --- a/server/src/repositories/library.repository.ts +++ b/server/src/repositories/library.repository.ts @@ -39,21 +39,6 @@ export class LibraryRepository implements ILibraryRepository { return this.repository.countBy({ ownerId }); } - @GenerateSql({ params: [DummyValue.UUID] }) - getAllByUserId(ownerId: string): Promise { - return this.repository.find({ - where: { - ownerId, - }, - relations: { - owner: true, - }, - order: { - createdAt: 'ASC', - }, - }); - } - @GenerateSql({ params: [] }) getAll(withDeleted = false): Promise { return this.repository.find({ diff --git a/server/src/services/asset-media.service.spec.ts b/server/src/services/asset-media.service.spec.ts index 1779635630..a75ece42f5 100644 --- a/server/src/services/asset-media.service.spec.ts +++ b/server/src/services/asset-media.service.spec.ts @@ -1,5 +1,5 @@ import { Stats } from 'node:fs'; -import { AssetMediaStatusEnum } from 'src/dtos/asset-media-response.dto'; +import { AssetMediaStatusEnum, AssetRejectReason, AssetUploadAction } from 'src/dtos/asset-media-response.dto'; import { AssetMediaReplaceDto } from 'src/dtos/asset-media.dto'; import { ASSET_CHECKSUM_CONSTRAINT, AssetEntity, AssetType } from 'src/entities/asset.entity'; import { ExifEntity } from 'src/entities/exif.entity'; @@ -277,4 +277,31 @@ describe('AssetMediaService', () => { expect(userMock.updateUsage).not.toHaveBeenCalled(); }); }); + describe('bulkUploadCheck', () => { + it('should accept hex and base64 checksums', async () => { + const file1 = Buffer.from('d2947b871a706081be194569951b7db246907957', 'hex'); + const file2 = Buffer.from('53be335e99f18a66ff12e9a901c7a6171dd76573', 'hex'); + + assetMock.getByChecksums.mockResolvedValue([ + { id: 'asset-1', checksum: file1 } as AssetEntity, + { id: 'asset-2', checksum: file2 } as AssetEntity, + ]); + + await expect( + sut.bulkUploadCheck(authStub.admin, { + assets: [ + { id: '1', checksum: file1.toString('hex') }, + { id: '2', checksum: file2.toString('base64') }, + ], + }), + ).resolves.toEqual({ + results: [ + { id: '1', assetId: 'asset-1', action: AssetUploadAction.REJECT, reason: AssetRejectReason.DUPLICATE }, + { id: '2', assetId: 'asset-2', action: AssetUploadAction.REJECT, reason: AssetRejectReason.DUPLICATE }, + ], + }); + + expect(assetMock.getByChecksums).toHaveBeenCalledWith(authStub.admin.user.id, [file1, file2]); + }); + }); }); diff --git a/server/src/services/asset-media.service.ts b/server/src/services/asset-media.service.ts index ef8c46c5b7..ddb8f105a3 100644 --- a/server/src/services/asset-media.service.ts +++ b/server/src/services/asset-media.service.ts @@ -1,7 +1,19 @@ import { BadRequestException, Inject, Injectable, InternalServerErrorException } from '@nestjs/common'; import { AccessCore, Permission } from 'src/cores/access.core'; -import { AssetMediaResponseDto, AssetMediaStatusEnum } from 'src/dtos/asset-media-response.dto'; -import { AssetMediaReplaceDto, UploadFieldName } from 'src/dtos/asset-media.dto'; +import { + AssetBulkUploadCheckResponseDto, + AssetMediaResponseDto, + AssetMediaStatusEnum, + AssetRejectReason, + AssetUploadAction, + CheckExistingAssetsResponseDto, +} from 'src/dtos/asset-media-response.dto'; +import { + AssetBulkUploadCheckDto, + AssetMediaReplaceDto, + CheckExistingAssetsDto, + UploadFieldName, +} from 'src/dtos/asset-media.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { ASSET_CHECKSUM_CONSTRAINT, AssetEntity } from 'src/entities/asset.entity'; import { IAccessRepository } from 'src/interfaces/access.interface'; @@ -12,8 +24,8 @@ import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { IStorageRepository } from 'src/interfaces/storage.interface'; import { IUserRepository } from 'src/interfaces/user.interface'; import { mimeTypes } from 'src/utils/mime-types'; +import { fromChecksum } from 'src/utils/request'; import { QueryFailedError } from 'typeorm'; - export interface UploadRequest { auth: AuthDto | null; fieldName: UploadFieldName; @@ -174,4 +186,49 @@ export class AssetMediaService { throw new BadRequestException('Quota has been exceeded!'); } } + + async checkExistingAssets( + auth: AuthDto, + checkExistingAssetsDto: CheckExistingAssetsDto, + ): Promise { + const assets = await this.assetRepository.getByDeviceIds( + auth.user.id, + checkExistingAssetsDto.deviceId, + checkExistingAssetsDto.deviceAssetIds, + ); + return { + existingIds: assets.map((asset) => asset.id), + }; + } + + async bulkUploadCheck(auth: AuthDto, dto: AssetBulkUploadCheckDto): Promise { + const checksums: Buffer[] = dto.assets.map((asset) => fromChecksum(asset.checksum)); + const results = await this.assetRepository.getByChecksums(auth.user.id, checksums); + const checksumMap: Record = {}; + + for (const { id, checksum } of results) { + checksumMap[checksum.toString('hex')] = id; + } + + return { + results: dto.assets.map(({ id, checksum }) => { + const duplicate = checksumMap[fromChecksum(checksum).toString('hex')]; + if (duplicate) { + return { + id, + assetId: duplicate, + action: AssetUploadAction.REJECT, + reason: AssetRejectReason.DUPLICATE, + }; + } + + // TODO mime-check + + return { + id, + action: AssetUploadAction.ACCEPT, + }; + }), + }; + } } diff --git a/server/src/services/asset-v1.service.spec.ts b/server/src/services/asset-v1.service.spec.ts index 89485d2669..b359bbf487 100644 --- a/server/src/services/asset-v1.service.spec.ts +++ b/server/src/services/asset-v1.service.spec.ts @@ -1,4 +1,3 @@ -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'; @@ -74,10 +73,7 @@ describe('AssetService', () => { beforeEach(() => { assetRepositoryMockV1 = { get: vitest.fn(), - getAllByUserId: vitest.fn(), getAssetsByChecksums: vitest.fn(), - getExistingAssets: vitest.fn(), - getByOriginalPath: vitest.fn(), }; accessMock = newAccessRepositoryMock(); @@ -194,32 +190,4 @@ describe('AssetService', () => { ); }); }); - - describe('bulkUploadCheck', () => { - it('should accept hex and base64 checksums', async () => { - const file1 = Buffer.from('d2947b871a706081be194569951b7db246907957', 'hex'); - const file2 = Buffer.from('53be335e99f18a66ff12e9a901c7a6171dd76573', 'hex'); - - assetRepositoryMockV1.getAssetsByChecksums.mockResolvedValue([ - { id: 'asset-1', checksum: file1 }, - { id: 'asset-2', checksum: file2 }, - ]); - - await expect( - sut.bulkUploadCheck(authStub.admin, { - assets: [ - { id: '1', checksum: file1.toString('hex') }, - { id: '2', checksum: file2.toString('base64') }, - ], - }), - ).resolves.toEqual({ - results: [ - { id: '1', assetId: 'asset-1', action: AssetUploadAction.REJECT, reason: AssetRejectReason.DUPLICATE }, - { id: '2', assetId: 'asset-2', action: AssetUploadAction.REJECT, reason: AssetRejectReason.DUPLICATE }, - ], - }); - - expect(assetRepositoryMockV1.getAssetsByChecksums).toHaveBeenCalledWith(authStub.admin.user.id, [file1, file2]); - }); - }); }); diff --git a/server/src/services/asset-v1.service.ts b/server/src/services/asset-v1.service.ts index 9346204506..32841b0214 100644 --- a/server/src/services/asset-v1.service.ts +++ b/server/src/services/asset-v1.service.ts @@ -6,23 +6,8 @@ import { NotFoundException, } from '@nestjs/common'; import { AccessCore, Permission } from 'src/cores/access.core'; -import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto'; -import { - AssetBulkUploadCheckResponseDto, - AssetFileUploadResponseDto, - AssetRejectReason, - AssetUploadAction, - CheckExistingAssetsResponseDto, -} from 'src/dtos/asset-v1-response.dto'; -import { - AssetBulkUploadCheckDto, - AssetSearchDto, - CheckExistingAssetsDto, - CreateAssetDto, - GetAssetThumbnailDto, - GetAssetThumbnailFormatEnum, - ServeFileDto, -} from 'src/dtos/asset-v1.dto'; +import { AssetFileUploadResponseDto } from 'src/dtos/asset-v1-response.dto'; +import { 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 { IAccessRepository } from 'src/interfaces/access.interface'; @@ -36,7 +21,6 @@ import { IUserRepository } from 'src/interfaces/user.interface'; import { UploadFile } from 'src/services/asset-media.service'; import { CacheControl, ImmichFileResponse, getLivePhotoMotionFilename } from 'src/utils/file'; import { mimeTypes } from 'src/utils/mime-types'; -import { fromChecksum } from 'src/utils/request'; import { QueryFailedError } from 'typeorm'; @Injectable() @@ -112,13 +96,6 @@ export class AssetServiceV1 { } } - public async getAllAssets(auth: AuthDto, dto: AssetSearchDto): Promise { - const userId = dto.userId || auth.user.id; - await this.access.requirePermission(auth, Permission.TIMELINE_READ, userId); - const assets = await this.assetRepositoryV1.getAllByUserId(userId, dto); - return assets.map((asset) => mapAsset(asset, { withStack: true, auth })); - } - async serveThumbnail(auth: AuthDto, assetId: string, dto: GetAssetThumbnailDto): Promise { await this.access.requirePermission(auth, Permission.ASSET_VIEW, assetId); @@ -159,46 +136,6 @@ export class AssetServiceV1 { }); } - async checkExistingAssets( - auth: AuthDto, - checkExistingAssetsDto: CheckExistingAssetsDto, - ): Promise { - return { - existingIds: await this.assetRepositoryV1.getExistingAssets(auth.user.id, checkExistingAssetsDto), - }; - } - - async bulkUploadCheck(auth: AuthDto, dto: AssetBulkUploadCheckDto): Promise { - const checksums: Buffer[] = dto.assets.map((asset) => fromChecksum(asset.checksum)); - const results = await this.assetRepositoryV1.getAssetsByChecksums(auth.user.id, checksums); - const checksumMap: Record = {}; - - for (const { id, checksum } of results) { - checksumMap[checksum.toString('hex')] = id; - } - - return { - results: dto.assets.map(({ id, checksum }) => { - const duplicate = checksumMap[fromChecksum(checksum).toString('hex')]; - if (duplicate) { - return { - id, - assetId: duplicate, - action: AssetUploadAction.REJECT, - reason: AssetRejectReason.DUPLICATE, - }; - } - - // TODO mime-check - - return { - id, - action: AssetUploadAction.ACCEPT, - }; - }), - }; - } - private getThumbnailPath(asset: AssetEntity, format: GetAssetThumbnailFormatEnum) { switch (format) { case GetAssetThumbnailFormatEnum.WEBP: { diff --git a/server/test/repositories/asset.repository.mock.ts b/server/test/repositories/asset.repository.mock.ts index 1ad8e31ce2..abe56495db 100644 --- a/server/test/repositories/asset.repository.mock.ts +++ b/server/test/repositories/asset.repository.mock.ts @@ -10,10 +10,12 @@ export const newAssetRepositoryMock = (): Mocked => { getByIds: vitest.fn().mockResolvedValue([]), getByIdsWithAllRelations: vitest.fn().mockResolvedValue([]), getByAlbumId: vitest.fn(), + getByDeviceIds: vitest.fn(), getByUserId: vitest.fn(), getById: vitest.fn(), getWithout: vitest.fn(), getByChecksum: vitest.fn(), + getByChecksums: vitest.fn(), getUploadAssetIdByChecksum: vitest.fn(), getWith: vitest.fn(), getRandom: vitest.fn(),