diff --git a/mobile/openapi/lib/model/asset_bulk_upload_check_result.dart b/mobile/openapi/lib/model/asset_bulk_upload_check_result.dart index 737186e5898ee..a016b357e7e6b 100644 --- a/mobile/openapi/lib/model/asset_bulk_upload_check_result.dart +++ b/mobile/openapi/lib/model/asset_bulk_upload_check_result.dart @@ -16,6 +16,7 @@ class AssetBulkUploadCheckResult { required this.action, this.assetId, required this.id, + this.isTrashed, this.reason, }); @@ -31,6 +32,14 @@ class AssetBulkUploadCheckResult { String id; + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + bool? isTrashed; + AssetBulkUploadCheckResultReasonEnum? reason; @override @@ -38,6 +47,7 @@ class AssetBulkUploadCheckResult { other.action == action && other.assetId == assetId && other.id == id && + other.isTrashed == isTrashed && other.reason == reason; @override @@ -46,10 +56,11 @@ class AssetBulkUploadCheckResult { (action.hashCode) + (assetId == null ? 0 : assetId!.hashCode) + (id.hashCode) + + (isTrashed == null ? 0 : isTrashed!.hashCode) + (reason == null ? 0 : reason!.hashCode); @override - String toString() => 'AssetBulkUploadCheckResult[action=$action, assetId=$assetId, id=$id, reason=$reason]'; + String toString() => 'AssetBulkUploadCheckResult[action=$action, assetId=$assetId, id=$id, isTrashed=$isTrashed, reason=$reason]'; Map toJson() { final json = {}; @@ -60,6 +71,11 @@ class AssetBulkUploadCheckResult { // json[r'assetId'] = null; } json[r'id'] = this.id; + if (this.isTrashed != null) { + json[r'isTrashed'] = this.isTrashed; + } else { + // json[r'isTrashed'] = null; + } if (this.reason != null) { json[r'reason'] = this.reason; } else { @@ -79,6 +95,7 @@ class AssetBulkUploadCheckResult { action: AssetBulkUploadCheckResultActionEnum.fromJson(json[r'action'])!, assetId: mapValueOfType(json, r'assetId'), id: mapValueOfType(json, r'id')!, + isTrashed: mapValueOfType(json, r'isTrashed'), reason: AssetBulkUploadCheckResultReasonEnum.fromJson(json[r'reason']), ); } diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 2325f24ee59d4..19d6b5055660b 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -7928,6 +7928,9 @@ "id": { "type": "string" }, + "isTrashed": { + "type": "boolean" + }, "reason": { "enum": [ "duplicate", diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 43777552c59bf..2afdf083433b2 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -395,6 +395,7 @@ export type AssetBulkUploadCheckResult = { action: Action; assetId?: string; id: string; + isTrashed?: boolean; reason?: Reason; }; export type AssetBulkUploadCheckResponseDto = { diff --git a/server/src/dtos/asset-media-response.dto.ts b/server/src/dtos/asset-media-response.dto.ts index 33fa080bc1607..5cd9b7e7d9b53 100644 --- a/server/src/dtos/asset-media-response.dto.ts +++ b/server/src/dtos/asset-media-response.dto.ts @@ -26,6 +26,7 @@ export class AssetBulkUploadCheckResult { action!: AssetUploadAction; reason?: AssetRejectReason; assetId?: string; + isTrashed?: boolean; } export class AssetBulkUploadCheckResponseDto { diff --git a/server/src/queries/asset.repository.sql b/server/src/queries/asset.repository.sql index 9b4b17425c409..da5ec1d4d1d53 100644 --- a/server/src/queries/asset.repository.sql +++ b/server/src/queries/asset.repository.sql @@ -493,6 +493,7 @@ LIMIT -- AssetRepository.getByChecksums SELECT "AssetEntity"."id" AS "AssetEntity_id", + "AssetEntity"."deletedAt" AS "AssetEntity_deletedAt", "AssetEntity"."checksum" AS "AssetEntity_checksum" FROM "assets" "AssetEntity" diff --git a/server/src/queries/metadata.repository.sql b/server/src/queries/metadata.repository.sql index 077b4644b824d..212527432072b 100644 --- a/server/src/queries/metadata.repository.sql +++ b/server/src/queries/metadata.repository.sql @@ -8,7 +8,7 @@ FROM LEFT JOIN "assets" "asset" ON "asset"."id" = "exif"."assetId" AND ("asset"."deletedAt" IS NULL) WHERE - "asset"."ownerId" = $1 + "asset"."ownerId" IN ($1) -- MetadataRepository.getStates SELECT DISTINCT @@ -18,7 +18,7 @@ FROM LEFT JOIN "assets" "asset" ON "asset"."id" = "exif"."assetId" AND ("asset"."deletedAt" IS NULL) WHERE - "asset"."ownerId" = $1 + "asset"."ownerId" IN ($1) AND "exif"."country" = $2 -- MetadataRepository.getCities @@ -29,7 +29,7 @@ FROM LEFT JOIN "assets" "asset" ON "asset"."id" = "exif"."assetId" AND ("asset"."deletedAt" IS NULL) WHERE - "asset"."ownerId" = $1 + "asset"."ownerId" IN ($1) AND "exif"."country" = $2 AND "exif"."state" = $3 @@ -41,7 +41,7 @@ FROM LEFT JOIN "assets" "asset" ON "asset"."id" = "exif"."assetId" AND ("asset"."deletedAt" IS NULL) WHERE - "asset"."ownerId" = $1 + "asset"."ownerId" IN ($1) AND "exif"."model" = $2 -- MetadataRepository.getCameraModels @@ -52,5 +52,5 @@ FROM LEFT JOIN "assets" "asset" ON "asset"."id" = "exif"."assetId" AND ("asset"."deletedAt" IS NULL) WHERE - "asset"."ownerId" = $1 + "asset"."ownerId" IN ($1) AND "exif"."make" = $2 diff --git a/server/src/repositories/asset.repository.ts b/server/src/repositories/asset.repository.ts index 3763cccd53c5d..059a05f9e770d 100644 --- a/server/src/repositories/asset.repository.ts +++ b/server/src/repositories/asset.repository.ts @@ -338,6 +338,7 @@ export class AssetRepository implements IAssetRepository { select: { id: true, checksum: true, + deletedAt: true, }, where: { ownerId, diff --git a/server/src/repositories/metadata.repository.ts b/server/src/repositories/metadata.repository.ts index 9902f04d9bfcf..f5933915ce241 100644 --- a/server/src/repositories/metadata.repository.ts +++ b/server/src/repositories/metadata.repository.ts @@ -55,7 +55,7 @@ export class MetadataRepository implements IMetadataRepository { } } - @GenerateSql({ params: [DummyValue.UUID] }) + @GenerateSql({ params: [[DummyValue.UUID]] }) async getCountries(userIds: string[]): Promise { const results = await this.exifRepository .createQueryBuilder('exif') @@ -68,7 +68,7 @@ export class MetadataRepository implements IMetadataRepository { return results.map(({ country }) => country).filter((item) => item !== ''); } - @GenerateSql({ params: [DummyValue.UUID, DummyValue.STRING] }) + @GenerateSql({ params: [[DummyValue.UUID], DummyValue.STRING] }) async getStates(userIds: string[], country: string | undefined): Promise { const query = this.exifRepository .createQueryBuilder('exif') @@ -86,7 +86,7 @@ export class MetadataRepository implements IMetadataRepository { return result.map(({ state }) => state).filter((item) => item !== ''); } - @GenerateSql({ params: [DummyValue.UUID, DummyValue.STRING, DummyValue.STRING] }) + @GenerateSql({ params: [[DummyValue.UUID], DummyValue.STRING, DummyValue.STRING] }) async getCities(userIds: string[], country: string | undefined, state: string | undefined): Promise { const query = this.exifRepository .createQueryBuilder('exif') @@ -108,7 +108,7 @@ export class MetadataRepository implements IMetadataRepository { return results.map(({ city }) => city).filter((item) => item !== ''); } - @GenerateSql({ params: [DummyValue.UUID, DummyValue.STRING] }) + @GenerateSql({ params: [[DummyValue.UUID], DummyValue.STRING] }) async getCameraMakes(userIds: string[], model: string | undefined): Promise { const query = this.exifRepository .createQueryBuilder('exif') @@ -125,7 +125,7 @@ export class MetadataRepository implements IMetadataRepository { return results.map(({ make }) => make).filter((item) => item !== ''); } - @GenerateSql({ params: [DummyValue.UUID, DummyValue.STRING] }) + @GenerateSql({ params: [[DummyValue.UUID], DummyValue.STRING] }) async getCameraModels(userIds: string[], make: string | undefined): Promise { const query = this.exifRepository .createQueryBuilder('exif') diff --git a/server/src/services/asset-media.service.spec.ts b/server/src/services/asset-media.service.spec.ts index 2f5192d84fcf6..9d6f0ff9cf547 100644 --- a/server/src/services/asset-media.service.spec.ts +++ b/server/src/services/asset-media.service.spec.ts @@ -589,8 +589,20 @@ describe(AssetMediaService.name, () => { }), ).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 }, + { + id: '1', + assetId: 'asset-1', + action: AssetUploadAction.REJECT, + reason: AssetRejectReason.DUPLICATE, + isTrashed: false, + }, + { + id: '2', + assetId: 'asset-2', + action: AssetUploadAction.REJECT, + reason: AssetRejectReason.DUPLICATE, + isTrashed: false, + }, ], }); diff --git a/server/src/services/asset-media.service.ts b/server/src/services/asset-media.service.ts index 76c6b49716413..30fb878cd0fa5 100644 --- a/server/src/services/asset-media.service.ts +++ b/server/src/services/asset-media.service.ts @@ -289,10 +289,10 @@ export class AssetMediaService { 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 = {}; + const checksumMap: Record = {}; - for (const { id, checksum } of results) { - checksumMap[checksum.toString('hex')] = id; + for (const { id, deletedAt, checksum } of results) { + checksumMap[checksum.toString('hex')] = { id, isTrashed: !!deletedAt }; } return { @@ -301,14 +301,13 @@ export class AssetMediaService { if (duplicate) { return { id, - assetId: duplicate, action: AssetUploadAction.REJECT, reason: AssetRejectReason.DUPLICATE, + assetId: duplicate.id, + isTrashed: duplicate.isTrashed, }; } - // TODO mime-check - return { id, action: AssetUploadAction.ACCEPT, diff --git a/web/src/lib/components/shared-components/upload-asset-preview.svelte b/web/src/lib/components/shared-components/upload-asset-preview.svelte index a7ba3430a02ed..d0abf12ab560a 100644 --- a/web/src/lib/components/shared-components/upload-asset-preview.svelte +++ b/web/src/lib/components/shared-components/upload-asset-preview.svelte @@ -15,6 +15,7 @@ mdiLoading, mdiOpenInNew, mdiRestart, + mdiTrashCan, } from '@mdi/js'; import { t } from 'svelte-i18n'; import { fade } from 'svelte/transition'; @@ -29,6 +30,10 @@ uploadAssetsStore.removeItem(uploadAsset.id); await fileUploadHandler([uploadAsset.file], uploadAsset.albumId); }; + + const asLink = (asset: UploadAsset) => { + return asset.isTrashed ? `${AppRoute.TRASH}/${asset.assetId}` : `${AppRoute.PHOTOS}/${uploadAsset.assetId}`; + };
{:else if uploadAsset.state === UploadState.DUPLICATED} - + {#if uploadAsset.isTrashed} + + {:else} + + {/if} {:else if uploadAsset.state === UploadState.DONE} {/if} @@ -56,7 +65,7 @@ {#if uploadAsset.state === UploadState.DUPLICATED && uploadAsset.assetId}
{ uploadAssetsStore.addItem({ id: 'asset-3', file: { name: 'asset3.jpg', size: 123_456 } as File }); uploadAssetsStore.updateItem('asset-3', { state: UploadState.DUPLICATED, assetId: 'asset-2' }); uploadAssetsStore.addItem({ id: 'asset-4', file: { name: 'asset3.jpg', size: 123_456 } as File }); - uploadAssetsStore.updateItem('asset-4', { state: UploadState.DONE }); + uploadAssetsStore.updateItem('asset-4', { state: UploadState.DUPLICATED, assetId: 'asset-2', isTrashed: true }); + uploadAssetsStore.addItem({ id: 'asset-10', file: { name: 'asset3.jpg', size: 123_456 } as File }); + uploadAssetsStore.updateItem('asset-10', { state: UploadState.DONE }); uploadAssetsStore.track('error'); uploadAssetsStore.track('success'); uploadAssetsStore.track('duplicate'); @@ -122,7 +124,7 @@ async function fileUploader(assetFile: File, albumId?: string, replaceAssetId?: formData.append(key, value); } - let responseData: AssetMediaResponseDto | undefined; + let responseData: { id: string; status: AssetMediaStatus; isTrashed?: boolean } | undefined; const key = getKey(); if (crypto?.subtle?.digest && !key) { uploadAssetsStore.updateItem(deviceAssetId, { message: $t('asset_hashing') }); @@ -138,7 +140,11 @@ async function fileUploader(assetFile: File, albumId?: string, replaceAssetId?: results: [checkUploadResult], } = await checkBulkUpload({ assetBulkUploadCheckDto: { assets: [{ id: assetFile.name, checksum }] } }); if (checkUploadResult.action === Action.Reject && checkUploadResult.assetId) { - responseData = { status: AssetMediaStatus.Duplicate, id: checkUploadResult.assetId }; + responseData = { + status: AssetMediaStatus.Duplicate, + id: checkUploadResult.assetId, + isTrashed: checkUploadResult.isTrashed, + }; } } catch (error) { console.error(`Error calculating sha1 file=${assetFile.name})`, error); @@ -185,6 +191,7 @@ async function fileUploader(assetFile: File, albumId?: string, replaceAssetId?: uploadAssetsStore.updateItem(deviceAssetId, { state: responseData.status === AssetMediaStatus.Duplicate ? UploadState.DUPLICATED : UploadState.DONE, assetId: responseData.id, + isTrashed: responseData.isTrashed, }); if (responseData.status !== AssetMediaStatus.Duplicate) {