From 11cade0802e35f1bc6411cba1b3f0ca41a5c49c5 Mon Sep 17 00:00:00 2001 From: Val Packett Date: Mon, 8 Apr 2024 03:56:52 -0300 Subject: [PATCH 01/23] feat(server): add isReadOnly property to the Library model --- mobile/openapi/doc/UpdateLibraryDto.md | 1 + .../openapi/lib/model/update_library_dto.dart | 19 ++++++++++++++- .../openapi/test/update_library_dto_test.dart | 5 ++++ open-api/immich-openapi-specs.json | 8 +++++++ open-api/typescript-sdk/src/fetch-client.ts | 1 + server/src/dtos/library.dto.ts | 6 +++++ server/src/entities/library.entity.ts | 3 +++ .../1712293452361-AddLibraryReadOnly.ts | 14 +++++++++++ server/src/queries/library.repository.sql | 8 ++++++- server/test/fixtures/library.stub.ts | 23 +++++++++++++++++++ 10 files changed, 86 insertions(+), 2 deletions(-) create mode 100644 server/src/migrations/1712293452361-AddLibraryReadOnly.ts diff --git a/mobile/openapi/doc/UpdateLibraryDto.md b/mobile/openapi/doc/UpdateLibraryDto.md index 0f0e2652b81c0..737d113920a31 100644 --- a/mobile/openapi/doc/UpdateLibraryDto.md +++ b/mobile/openapi/doc/UpdateLibraryDto.md @@ -10,6 +10,7 @@ Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- **exclusionPatterns** | **List** | | [optional] [default to const []] **importPaths** | **List** | | [optional] [default to const []] +**isReadOnly** | **bool** | | [optional] **isVisible** | **bool** | | [optional] **name** | **String** | | [optional] diff --git a/mobile/openapi/lib/model/update_library_dto.dart b/mobile/openapi/lib/model/update_library_dto.dart index b870f240fe08d..7bf571872d687 100644 --- a/mobile/openapi/lib/model/update_library_dto.dart +++ b/mobile/openapi/lib/model/update_library_dto.dart @@ -15,6 +15,7 @@ class UpdateLibraryDto { UpdateLibraryDto({ this.exclusionPatterns = const [], this.importPaths = const [], + this.isReadOnly, this.isVisible, this.name, }); @@ -23,6 +24,14 @@ class UpdateLibraryDto { List importPaths; + /// + /// 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? isReadOnly; + /// /// 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 @@ -43,6 +52,7 @@ class UpdateLibraryDto { bool operator ==(Object other) => identical(this, other) || other is UpdateLibraryDto && _deepEquality.equals(other.exclusionPatterns, exclusionPatterns) && _deepEquality.equals(other.importPaths, importPaths) && + other.isReadOnly == isReadOnly && other.isVisible == isVisible && other.name == name; @@ -51,16 +61,22 @@ class UpdateLibraryDto { // ignore: unnecessary_parenthesis (exclusionPatterns.hashCode) + (importPaths.hashCode) + + (isReadOnly == null ? 0 : isReadOnly!.hashCode) + (isVisible == null ? 0 : isVisible!.hashCode) + (name == null ? 0 : name!.hashCode); @override - String toString() => 'UpdateLibraryDto[exclusionPatterns=$exclusionPatterns, importPaths=$importPaths, isVisible=$isVisible, name=$name]'; + String toString() => 'UpdateLibraryDto[exclusionPatterns=$exclusionPatterns, importPaths=$importPaths, isReadOnly=$isReadOnly, isVisible=$isVisible, name=$name]'; Map toJson() { final json = {}; json[r'exclusionPatterns'] = this.exclusionPatterns; json[r'importPaths'] = this.importPaths; + if (this.isReadOnly != null) { + json[r'isReadOnly'] = this.isReadOnly; + } else { + // json[r'isReadOnly'] = null; + } if (this.isVisible != null) { json[r'isVisible'] = this.isVisible; } else { @@ -88,6 +104,7 @@ class UpdateLibraryDto { importPaths: json[r'importPaths'] is Iterable ? (json[r'importPaths'] as Iterable).cast().toList(growable: false) : const [], + isReadOnly: mapValueOfType(json, r'isReadOnly'), isVisible: mapValueOfType(json, r'isVisible'), name: mapValueOfType(json, r'name'), ); diff --git a/mobile/openapi/test/update_library_dto_test.dart b/mobile/openapi/test/update_library_dto_test.dart index 222eb333bcf1f..4d4cd6a4bab18 100644 --- a/mobile/openapi/test/update_library_dto_test.dart +++ b/mobile/openapi/test/update_library_dto_test.dart @@ -26,6 +26,11 @@ void main() { // TODO }); + // bool isReadOnly + test('to test the property `isReadOnly`', () async { + // TODO + }); + // bool isVisible test('to test the property `isVisible`', () async { // TODO diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 4f666b303cf1e..2d13f586cdd17 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -8778,6 +8778,10 @@ }, "type": "array" }, + "isReadOnly": { + "nullable": true, + "type": "boolean" + }, "name": { "type": "string" }, @@ -8803,6 +8807,7 @@ "exclusionPatterns", "id", "importPaths", + "isReadOnly", "name", "ownerId", "refreshedAt", @@ -11183,6 +11188,9 @@ }, "type": "array" }, + "isReadOnly": { + "type": "boolean" + }, "isVisible": { "type": "boolean" }, diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 1bf219162dd8b..36963074471ce 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -439,6 +439,7 @@ export type CreateLibraryDto = { export type UpdateLibraryDto = { exclusionPatterns?: string[]; importPaths?: string[]; + isReadOnly?: boolean; isVisible?: boolean; name?: string; }; diff --git a/server/src/dtos/library.dto.ts b/server/src/dtos/library.dto.ts index b693d35adf854..83d88c22c8992 100644 --- a/server/src/dtos/library.dto.ts +++ b/server/src/dtos/library.dto.ts @@ -43,6 +43,9 @@ export class UpdateLibraryDto { @ValidateBoolean({ optional: true }) isVisible?: boolean; + @ValidateBoolean({ optional: true }) + isReadOnly?: boolean; + @Optional() @IsString({ each: true }) @IsNotEmpty({ each: true }) @@ -128,6 +131,8 @@ export class LibraryResponseDto { createdAt!: Date; updatedAt!: Date; refreshedAt!: Date | null; + + isReadOnly!: boolean | null; } export class LibraryStatsResponseDto { @@ -160,5 +165,6 @@ export function mapLibrary(entity: LibraryEntity): LibraryResponseDto { assetCount, importPaths: entity.importPaths, exclusionPatterns: entity.exclusionPatterns, + isReadOnly: entity.isReadOnly, }; } diff --git a/server/src/entities/library.entity.ts b/server/src/entities/library.entity.ts index 8be560a8893f2..12fc0a22a8acc 100644 --- a/server/src/entities/library.entity.ts +++ b/server/src/entities/library.entity.ts @@ -53,6 +53,9 @@ export class LibraryEntity { @Column({ type: 'boolean', default: true }) isVisible!: boolean; + + @Column({ type: 'boolean', nullable: true }) + isReadOnly!: boolean | null; } export enum LibraryType { diff --git a/server/src/migrations/1712293452361-AddLibraryReadOnly.ts b/server/src/migrations/1712293452361-AddLibraryReadOnly.ts new file mode 100644 index 0000000000000..d624f5bb9f74d --- /dev/null +++ b/server/src/migrations/1712293452361-AddLibraryReadOnly.ts @@ -0,0 +1,14 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class AddLibraryReadOnly1712293452361 implements MigrationInterface { + name = 'AddLibraryReadOnly1712293452361' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "libraries" ADD "isReadOnly" boolean`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "libraries" DROP COLUMN "isReadOnly"`); + } + +} diff --git a/server/src/queries/library.repository.sql b/server/src/queries/library.repository.sql index 93a6fc97fb107..eb418ee745834 100644 --- a/server/src/queries/library.repository.sql +++ b/server/src/queries/library.repository.sql @@ -17,6 +17,7 @@ FROM "LibraryEntity"."deletedAt" AS "LibraryEntity_deletedAt", "LibraryEntity"."refreshedAt" AS "LibraryEntity_refreshedAt", "LibraryEntity"."isVisible" AS "LibraryEntity_isVisible", + "LibraryEntity"."isReadOnly" AS "LibraryEntity_isReadOnly", "LibraryEntity__LibraryEntity_owner"."id" AS "LibraryEntity__LibraryEntity_owner_id", "LibraryEntity__LibraryEntity_owner"."name" AS "LibraryEntity__LibraryEntity_owner_name", "LibraryEntity__LibraryEntity_owner"."avatarColor" AS "LibraryEntity__LibraryEntity_owner_avatarColor", @@ -90,7 +91,8 @@ SELECT "LibraryEntity"."updatedAt" AS "LibraryEntity_updatedAt", "LibraryEntity"."deletedAt" AS "LibraryEntity_deletedAt", "LibraryEntity"."refreshedAt" AS "LibraryEntity_refreshedAt", - "LibraryEntity"."isVisible" AS "LibraryEntity_isVisible" + "LibraryEntity"."isVisible" AS "LibraryEntity_isVisible", + "LibraryEntity"."isReadOnly" AS "LibraryEntity_isReadOnly" FROM "libraries" "LibraryEntity" WHERE @@ -133,6 +135,7 @@ SELECT "LibraryEntity"."deletedAt" AS "LibraryEntity_deletedAt", "LibraryEntity"."refreshedAt" AS "LibraryEntity_refreshedAt", "LibraryEntity"."isVisible" AS "LibraryEntity_isVisible", + "LibraryEntity"."isReadOnly" AS "LibraryEntity_isReadOnly", "LibraryEntity__LibraryEntity_owner"."id" AS "LibraryEntity__LibraryEntity_owner_id", "LibraryEntity__LibraryEntity_owner"."name" AS "LibraryEntity__LibraryEntity_owner_name", "LibraryEntity__LibraryEntity_owner"."avatarColor" AS "LibraryEntity__LibraryEntity_owner_avatarColor", @@ -179,6 +182,7 @@ SELECT "LibraryEntity"."deletedAt" AS "LibraryEntity_deletedAt", "LibraryEntity"."refreshedAt" AS "LibraryEntity_refreshedAt", "LibraryEntity"."isVisible" AS "LibraryEntity_isVisible", + "LibraryEntity"."isReadOnly" AS "LibraryEntity_isReadOnly", "LibraryEntity__LibraryEntity_owner"."id" AS "LibraryEntity__LibraryEntity_owner_id", "LibraryEntity__LibraryEntity_owner"."name" AS "LibraryEntity__LibraryEntity_owner_name", "LibraryEntity__LibraryEntity_owner"."avatarColor" AS "LibraryEntity__LibraryEntity_owner_avatarColor", @@ -219,6 +223,7 @@ SELECT "LibraryEntity"."deletedAt" AS "LibraryEntity_deletedAt", "LibraryEntity"."refreshedAt" AS "LibraryEntity_refreshedAt", "LibraryEntity"."isVisible" AS "LibraryEntity_isVisible", + "LibraryEntity"."isReadOnly" AS "LibraryEntity_isReadOnly", "LibraryEntity__LibraryEntity_owner"."id" AS "LibraryEntity__LibraryEntity_owner_id", "LibraryEntity__LibraryEntity_owner"."name" AS "LibraryEntity__LibraryEntity_owner_name", "LibraryEntity__LibraryEntity_owner"."avatarColor" AS "LibraryEntity__LibraryEntity_owner_avatarColor", @@ -259,6 +264,7 @@ SELECT "libraries"."deletedAt" AS "libraries_deletedAt", "libraries"."refreshedAt" AS "libraries_refreshedAt", "libraries"."isVisible" AS "libraries_isVisible", + "libraries"."isReadOnly" AS "libraries_isReadOnly", COUNT("assets"."id") FILTER ( WHERE "assets"."type" = 'IMAGE' diff --git a/server/test/fixtures/library.stub.ts b/server/test/fixtures/library.stub.ts index dde250a7a16aa..1db359b1731cd 100644 --- a/server/test/fixtures/library.stub.ts +++ b/server/test/fixtures/library.stub.ts @@ -17,6 +17,7 @@ export const libraryStub = { updatedAt: new Date('2022-01-01'), refreshedAt: null, isVisible: true, + isReadOnly: null, exclusionPatterns: [], }), externalLibrary1: Object.freeze({ @@ -31,6 +32,7 @@ export const libraryStub = { updatedAt: new Date('2023-01-01'), refreshedAt: null, isVisible: true, + isReadOnly: null, exclusionPatterns: [], }), externalLibrary2: Object.freeze({ @@ -45,6 +47,22 @@ export const libraryStub = { updatedAt: new Date('2022-01-01'), refreshedAt: null, isVisible: true, + isReadOnly: null, + exclusionPatterns: [], + }), + externalLibraryNotReadOnly: Object.freeze({ + id: 'library-id-not-read-only', + name: 'library-external-not-read-only', + assets: [], + owner: userStub.admin, + ownerId: 'admin_id', + type: LibraryType.EXTERNAL, + importPaths: [], + createdAt: new Date('2021-01-01'), + updatedAt: new Date('2022-01-01'), + refreshedAt: null, + isVisible: true, + isReadOnly: false, exclusionPatterns: [], }), externalLibraryWithImportPaths1: Object.freeze({ @@ -59,6 +77,7 @@ export const libraryStub = { updatedAt: new Date('2023-01-01'), refreshedAt: null, isVisible: true, + isReadOnly: null, exclusionPatterns: [], }), externalLibraryWithImportPaths2: Object.freeze({ @@ -73,6 +92,7 @@ export const libraryStub = { updatedAt: new Date('2023-01-01'), refreshedAt: null, isVisible: true, + isReadOnly: null, exclusionPatterns: [], }), externalLibraryWithExclusionPattern: Object.freeze({ @@ -87,6 +107,7 @@ export const libraryStub = { updatedAt: new Date('2023-01-01'), refreshedAt: null, isVisible: true, + isReadOnly: null, exclusionPatterns: ['**/dir1/**'], }), patternPath: Object.freeze({ @@ -101,6 +122,7 @@ export const libraryStub = { updatedAt: new Date('2023-01-01'), refreshedAt: null, isVisible: true, + isReadOnly: null, exclusionPatterns: ['**/dir1/**'], }), hasImmichPaths: Object.freeze({ @@ -115,6 +137,7 @@ export const libraryStub = { updatedAt: new Date('2023-01-01'), refreshedAt: null, isVisible: true, + isReadOnly: null, exclusionPatterns: ['**/dir1/**'], }), }; From c9f82dbd30ef958965a1ff4e4a30d4e4a49aa53a Mon Sep 17 00:00:00 2001 From: Val Packett Date: Sat, 20 Apr 2024 01:22:52 -0300 Subject: [PATCH 02/23] feat(server,web): allow deletion in external libraries marked not-read-only --- server/src/repositories/asset.repository.ts | 2 +- server/src/services/asset.service.ts | 8 +++++--- .../asset-viewer/asset-viewer-nav-bar.svelte | 2 +- .../photos-page/actions/delete-assets.svelte | 2 +- .../admin/library-management/+page.svelte | 19 +++++++++++++++++++ 5 files changed, 27 insertions(+), 6 deletions(-) diff --git a/server/src/repositories/asset.repository.ts b/server/src/repositories/asset.repository.ts index ddc666edd3daa..76b9c4086f679 100644 --- a/server/src/repositories/asset.repository.ts +++ b/server/src/repositories/asset.repository.ts @@ -254,7 +254,7 @@ export class AssetRepository implements IAssetRepository { @Chunked() async softDeleteAll(ids: string[]): Promise { - await this.repository.softDelete({ id: In(ids), isExternal: false }); + await this.repository.softDelete({ id: In(ids) }); } @Chunked() diff --git a/server/src/services/asset.service.ts b/server/src/services/asset.service.ts index a26b7f5a73096..441f75c5a9bb7 100644 --- a/server/src/services/asset.service.ts +++ b/server/src/services/asset.service.ts @@ -372,8 +372,10 @@ export class AssetService { return JobStatus.FAILED; } - // Ignore requests that are not from external library job but is for an external asset - if (!fromExternal && (!asset.library || asset.library.type === LibraryType.EXTERNAL)) { + // Ignore requests that are not from external library job but is for an external read-only asset + const isReadOnlyLibrary = !asset.library || asset.library.isReadOnly || + (asset.library.isReadOnly === null && asset.library.type === LibraryType.EXTERNAL); + if (!fromExternal && isReadOnlyLibrary) { return JobStatus.SKIPPED; } @@ -406,7 +408,7 @@ export class AssetService { } const files = [asset.thumbnailPath, asset.previewPath, asset.encodedVideoPath]; - if (!(asset.isExternal || asset.isReadOnly)) { + if (!isReadOnlyLibrary) { files.push(asset.sidecarPath, asset.originalPath); } 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 6772ff5db00fe..643cd028beed6 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 @@ -193,7 +193,7 @@ {/if} {#if isOwner} - {#if !asset.isReadOnly || !asset.isExternal} + {#if !asset.isReadOnly || !asset.isExternal || (true /* XXX */)} dispatch('delete')} title="Delete" /> {/if}
{ loading = true; - const ids = [...getOwnedAssets()].filter((a) => !a.isExternal).map((a) => a.id); + const ids = [...getOwnedAssets()].map((a) => a.id); await deleteAssets(force, onAssetDelete, ids); clearSelect(); isShowConfirmation = false; diff --git a/web/src/routes/admin/library-management/+page.svelte b/web/src/routes/admin/library-management/+page.svelte index 5bd3cfb09e86d..b213d9e6ee547 100644 --- a/web/src/routes/admin/library-management/+page.svelte +++ b/web/src/routes/admin/library-management/+page.svelte @@ -243,6 +243,19 @@ updateLibraryIndex = selectedLibraryIndex; }; + const onToggleReadOnlyClicked = async () => { + const library = libraries[selectedLibraryIndex]; + const prevValue = library.isReadOnly == null ? (library.type === LibraryType.External) : library.isReadOnly; + + try { + await updateLibrary({ id: library.id, updateLibraryDto: { ...library, isReadOnly: !prevValue } }); + closeAll(); + await readLibraryList(); + } catch (error) { + handleError(error, 'Unable to update library'); + } + }; + const onEditImportPathClicked = () => { closeAll(); editImportPaths = selectedLibraryIndex; @@ -397,6 +410,12 @@ onMenuExit()}> onRenameClicked()} text={`Rename`} /> + onToggleReadOnlyClicked()} + text={(selectedLibrary?.isReadOnly || + (selectedLibrary?.isReadOnly == null && selectedLibrary?.type === LibraryType.External)) + ? 'Allow file deletion' : 'Disallow file deletion'} + /> {#if selectedLibrary && selectedLibrary.type === LibraryType.External} onEditImportPathClicked()} text="Edit Import Paths" /> From 49ac5463dfbf58d89144957d4ab98c92744011df Mon Sep 17 00:00:00 2001 From: Val Packett Date: Sat, 20 Apr 2024 04:54:53 -0300 Subject: [PATCH 03/23] chore(server): add test for deleting external asset --- server/src/services/asset.service.spec.ts | 33 +++++++++++++++++++ server/test/fixtures/asset.stub.ts | 40 +++++++++++++++++++++++ 2 files changed, 73 insertions(+) diff --git a/server/src/services/asset.service.spec.ts b/server/src/services/asset.service.spec.ts index 1516ef89613a9..6334381fb0ec4 100755 --- a/server/src/services/asset.service.spec.ts +++ b/server/src/services/asset.service.spec.ts @@ -691,6 +691,39 @@ describe(AssetService.name, () => { expect(assetMock.remove).not.toHaveBeenCalled(); }); + it('should process assets from external non-read-only library without fromExternal flag', async () => { + when(assetMock.getById) + .calledWith(assetStub.externalDeletable.id, { + faces: { + person: true, + }, + library: true, + stack: { assets: true }, + exifInfo: true, + }) + .mockResolvedValue(assetStub.externalDeletable); + + await sut.handleAssetDeletion({ id: assetStub.externalDeletable.id }); + + expect(assetMock.remove).toHaveBeenCalledWith(assetStub.externalDeletable); + expect(jobMock.queue.mock.calls).toEqual([ + [ + { + name: JobName.DELETE_FILES, + data: { + files: [ + assetStub.externalDeletable.thumbnailPath, + assetStub.externalDeletable.previewPath, + assetStub.externalDeletable.encodedVideoPath, + assetStub.externalDeletable.sidecarPath, + assetStub.externalDeletable.originalPath, + ], + }, + }, + ], + ]); + }); + it('should process assets from external library with fromExternal flag', async () => { assetMock.getById.mockResolvedValue(assetStub.external); diff --git a/server/test/fixtures/asset.stub.ts b/server/test/fixtures/asset.stub.ts index ce2b07067215e..f33a40570e482 100644 --- a/server/test/fixtures/asset.stub.ts +++ b/server/test/fixtures/asset.stub.ts @@ -255,6 +255,46 @@ export const assetStub = { } as ExifEntity, }), + externalDeletable: Object.freeze({ + id: 'asset-id', + deviceAssetId: 'device-asset-id', + fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'), + fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'), + owner: userStub.user1, + ownerId: 'user-id', + deviceId: 'device-id', + originalPath: '/ext/photo.jpg', + previewPath: '/uploads/user-id/thumbs/path.jpg', + checksum: Buffer.from('file hash', 'utf8'), + type: AssetType.IMAGE, + thumbnailPath: '/uploads/user-id/webp/path.ext', + thumbhash: Buffer.from('blablabla', 'base64'), + encodedVideoPath: null, + createdAt: new Date('2023-02-23T05:06:29.716Z'), + updatedAt: new Date('2023-02-23T05:06:29.716Z'), + localDateTime: new Date('2023-02-23T05:06:29.716Z'), + isFavorite: true, + isArchived: false, + isReadOnly: false, + isExternal: true, + duration: null, + isVisible: true, + livePhotoVideo: null, + livePhotoVideoId: null, + isOffline: false, + libraryId: 'library-id', + library: libraryStub.externalLibraryNotReadOnly, + tags: [], + sharedLinks: [], + originalFileName: 'asset-id.jpg', + faces: [], + deletedAt: null, + sidecarPath: null, + exifInfo: { + fileSizeInByte: 5000, + } as ExifEntity, + }), + offline: Object.freeze({ id: 'asset-id', deviceAssetId: 'device-asset-id', From ab787e7c2d17639dfad35440ab7425e2b8887307 Mon Sep 17 00:00:00 2001 From: Ben McCann <322311+benmccann@users.noreply.github.com> Date: Sat, 27 Apr 2024 11:59:08 -0700 Subject: [PATCH 04/23] address code review feedback --- mobile/openapi/doc/LibraryResponseDto.md | 1 + .../openapi/lib/model/library_response_dto.dart | 10 +++++++++- .../openapi/test/library_response_dto_test.dart | 5 +++++ open-api/immich-openapi-specs.json | 1 - open-api/typescript-sdk/src/fetch-client.ts | 1 + server/src/dtos/library.dto.ts | 2 +- server/src/entities/library.entity.ts | 4 ++-- server/src/services/asset.service.ts | 7 ++----- .../asset-viewer/asset-viewer-nav-bar.svelte | 2 +- .../routes/admin/library-management/+page.svelte | 15 ++++++++------- 10 files changed, 30 insertions(+), 18 deletions(-) diff --git a/mobile/openapi/doc/LibraryResponseDto.md b/mobile/openapi/doc/LibraryResponseDto.md index e7283c11b6d7b..3671c5a061c52 100644 --- a/mobile/openapi/doc/LibraryResponseDto.md +++ b/mobile/openapi/doc/LibraryResponseDto.md @@ -13,6 +13,7 @@ Name | Type | Description | Notes **exclusionPatterns** | **List** | | [default to const []] **id** | **String** | | **importPaths** | **List** | | [default to const []] +**isReadOnly** | **bool** | | **name** | **String** | | **ownerId** | **String** | | **refreshedAt** | [**DateTime**](DateTime.md) | | diff --git a/mobile/openapi/lib/model/library_response_dto.dart b/mobile/openapi/lib/model/library_response_dto.dart index eadd3049fb335..de963ed70b781 100644 --- a/mobile/openapi/lib/model/library_response_dto.dart +++ b/mobile/openapi/lib/model/library_response_dto.dart @@ -18,6 +18,7 @@ class LibraryResponseDto { this.exclusionPatterns = const [], required this.id, this.importPaths = const [], + required this.isReadOnly, required this.name, required this.ownerId, required this.refreshedAt, @@ -35,6 +36,8 @@ class LibraryResponseDto { List importPaths; + bool isReadOnly; + String name; String ownerId; @@ -52,6 +55,7 @@ class LibraryResponseDto { _deepEquality.equals(other.exclusionPatterns, exclusionPatterns) && other.id == id && _deepEquality.equals(other.importPaths, importPaths) && + other.isReadOnly == isReadOnly && other.name == name && other.ownerId == ownerId && other.refreshedAt == refreshedAt && @@ -66,6 +70,7 @@ class LibraryResponseDto { (exclusionPatterns.hashCode) + (id.hashCode) + (importPaths.hashCode) + + (isReadOnly.hashCode) + (name.hashCode) + (ownerId.hashCode) + (refreshedAt == null ? 0 : refreshedAt!.hashCode) + @@ -73,7 +78,7 @@ class LibraryResponseDto { (updatedAt.hashCode); @override - String toString() => 'LibraryResponseDto[assetCount=$assetCount, createdAt=$createdAt, exclusionPatterns=$exclusionPatterns, id=$id, importPaths=$importPaths, name=$name, ownerId=$ownerId, refreshedAt=$refreshedAt, type=$type, updatedAt=$updatedAt]'; + String toString() => 'LibraryResponseDto[assetCount=$assetCount, createdAt=$createdAt, exclusionPatterns=$exclusionPatterns, id=$id, importPaths=$importPaths, isReadOnly=$isReadOnly, name=$name, ownerId=$ownerId, refreshedAt=$refreshedAt, type=$type, updatedAt=$updatedAt]'; Map toJson() { final json = {}; @@ -82,6 +87,7 @@ class LibraryResponseDto { json[r'exclusionPatterns'] = this.exclusionPatterns; json[r'id'] = this.id; json[r'importPaths'] = this.importPaths; + json[r'isReadOnly'] = this.isReadOnly; json[r'name'] = this.name; json[r'ownerId'] = this.ownerId; if (this.refreshedAt != null) { @@ -111,6 +117,7 @@ class LibraryResponseDto { importPaths: json[r'importPaths'] is Iterable ? (json[r'importPaths'] as Iterable).cast().toList(growable: false) : const [], + isReadOnly: mapValueOfType(json, r'isReadOnly')!, name: mapValueOfType(json, r'name')!, ownerId: mapValueOfType(json, r'ownerId')!, refreshedAt: mapDateTime(json, r'refreshedAt', r''), @@ -168,6 +175,7 @@ class LibraryResponseDto { 'exclusionPatterns', 'id', 'importPaths', + 'isReadOnly', 'name', 'ownerId', 'refreshedAt', diff --git a/mobile/openapi/test/library_response_dto_test.dart b/mobile/openapi/test/library_response_dto_test.dart index 9fd196d1b051d..76e375f5c2991 100644 --- a/mobile/openapi/test/library_response_dto_test.dart +++ b/mobile/openapi/test/library_response_dto_test.dart @@ -41,6 +41,11 @@ void main() { // TODO }); + // bool isReadOnly + test('to test the property `isReadOnly`', () async { + // TODO + }); + // String name test('to test the property `name`', () async { // TODO diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 2d13f586cdd17..187c579207572 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -8779,7 +8779,6 @@ "type": "array" }, "isReadOnly": { - "nullable": true, "type": "boolean" }, "name": { diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 36963074471ce..7cee2139ae160 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -422,6 +422,7 @@ export type LibraryResponseDto = { exclusionPatterns: string[]; id: string; importPaths: string[]; + isReadOnly: boolean; name: string; ownerId: string; refreshedAt: string | null; diff --git a/server/src/dtos/library.dto.ts b/server/src/dtos/library.dto.ts index 83d88c22c8992..3a86583650dbd 100644 --- a/server/src/dtos/library.dto.ts +++ b/server/src/dtos/library.dto.ts @@ -132,7 +132,7 @@ export class LibraryResponseDto { updatedAt!: Date; refreshedAt!: Date | null; - isReadOnly!: boolean | null; + isReadOnly!: boolean; } export class LibraryStatsResponseDto { diff --git a/server/src/entities/library.entity.ts b/server/src/entities/library.entity.ts index 12fc0a22a8acc..603f000a28501 100644 --- a/server/src/entities/library.entity.ts +++ b/server/src/entities/library.entity.ts @@ -54,8 +54,8 @@ export class LibraryEntity { @Column({ type: 'boolean', default: true }) isVisible!: boolean; - @Column({ type: 'boolean', nullable: true }) - isReadOnly!: boolean | null; + @Column({ type: 'boolean' }) + isReadOnly!: boolean; } export enum LibraryType { diff --git a/server/src/services/asset.service.ts b/server/src/services/asset.service.ts index 441f75c5a9bb7..ed681042b0d1c 100644 --- a/server/src/services/asset.service.ts +++ b/server/src/services/asset.service.ts @@ -372,10 +372,7 @@ export class AssetService { return JobStatus.FAILED; } - // Ignore requests that are not from external library job but is for an external read-only asset - const isReadOnlyLibrary = !asset.library || asset.library.isReadOnly || - (asset.library.isReadOnly === null && asset.library.type === LibraryType.EXTERNAL); - if (!fromExternal && isReadOnlyLibrary) { + if (asset.isReadOnly) { return JobStatus.SKIPPED; } @@ -408,7 +405,7 @@ export class AssetService { } const files = [asset.thumbnailPath, asset.previewPath, asset.encodedVideoPath]; - if (!isReadOnlyLibrary) { + if (!asset.isReadOnly) { files.push(asset.sidecarPath, asset.originalPath); } 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 643cd028beed6..991fc12914d9e 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 @@ -193,7 +193,7 @@ {/if} {#if isOwner} - {#if !asset.isReadOnly || !asset.isExternal || (true /* XXX */)} + {#if !asset.isReadOnly} dispatch('delete')} title="Delete" /> {/if}
{ const library = libraries[selectedLibraryIndex]; - const prevValue = library.isReadOnly == null ? (library.type === LibraryType.External) : library.isReadOnly; + const prevValue = library.isReadOnly == null ? library.type === LibraryType.External : library.isReadOnly; try { - await updateLibrary({ id: library.id, updateLibraryDto: { ...library, isReadOnly: !prevValue } }); + await updateLibrary({ id: library.id, updateLibraryDto: { isReadOnly: !prevValue } }); closeAll(); await readLibraryList(); } catch (error) { @@ -406,18 +406,19 @@ - {#if showContextMenu} + {#if showContextMenu && selectedLibrary} onMenuExit()}> onRenameClicked()} text={`Rename`} /> onToggleReadOnlyClicked()} - text={(selectedLibrary?.isReadOnly || - (selectedLibrary?.isReadOnly == null && selectedLibrary?.type === LibraryType.External)) - ? 'Allow file deletion' : 'Disallow file deletion'} + text={selectedLibrary.isReadOnly || + (selectedLibrary.isReadOnly == null && selectedLibrary.type === LibraryType.External) + ? 'Allow file deletion' + : 'Disallow file deletion'} /> - {#if selectedLibrary && selectedLibrary.type === LibraryType.External} + {#if selectedLibrary.type === LibraryType.External} onEditImportPathClicked()} text="Edit Import Paths" /> onScanSettingClicked()} text="Scan Settings" />
From 3168643c30eb29aa6f51a038839fe42babb9cfae Mon Sep 17 00:00:00 2001 From: Ben McCann <322311+benmccann@users.noreply.github.com> Date: Tue, 30 Apr 2024 14:54:29 -0700 Subject: [PATCH 05/23] update migration --- ...braryReadOnly.ts => 1714513984514-AddLibraryReadOnly.ts} | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) rename server/src/migrations/{1712293452361-AddLibraryReadOnly.ts => 1714513984514-AddLibraryReadOnly.ts} (72%) diff --git a/server/src/migrations/1712293452361-AddLibraryReadOnly.ts b/server/src/migrations/1714513984514-AddLibraryReadOnly.ts similarity index 72% rename from server/src/migrations/1712293452361-AddLibraryReadOnly.ts rename to server/src/migrations/1714513984514-AddLibraryReadOnly.ts index d624f5bb9f74d..0a0eb3fa1f771 100644 --- a/server/src/migrations/1712293452361-AddLibraryReadOnly.ts +++ b/server/src/migrations/1714513984514-AddLibraryReadOnly.ts @@ -1,10 +1,10 @@ import { MigrationInterface, QueryRunner } from "typeorm"; -export class AddLibraryReadOnly1712293452361 implements MigrationInterface { - name = 'AddLibraryReadOnly1712293452361' +export class AddLibraryReadOnly1714513984514 implements MigrationInterface { + name = 'AddLibraryReadOnly1714513984514' public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "libraries" ADD "isReadOnly" boolean`); + await queryRunner.query(`ALTER TABLE "libraries" ADD "isReadOnly" boolean NOT NULL`); } public async down(queryRunner: QueryRunner): Promise { From 26e2e844657a38a6460c697f60e92cd52975d0e8 Mon Sep 17 00:00:00 2001 From: Ben McCann <322311+benmccann@users.noreply.github.com> Date: Tue, 30 Apr 2024 15:06:39 -0700 Subject: [PATCH 06/23] fix when not defined --- server/src/services/asset.service.spec.ts | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/server/src/services/asset.service.spec.ts b/server/src/services/asset.service.spec.ts index 086c91f686c66..c5c1aaf5f2651 100755 --- a/server/src/services/asset.service.spec.ts +++ b/server/src/services/asset.service.spec.ts @@ -692,16 +692,7 @@ describe(AssetService.name, () => { }); it('should process assets from external non-read-only library without fromExternal flag', async () => { - when(assetMock.getById) - .calledWith(assetStub.externalDeletable.id, { - faces: { - person: true, - }, - library: true, - stack: { assets: true }, - exifInfo: true, - }) - .mockResolvedValue(assetStub.externalDeletable); + assetMock.getById.mockResolvedValue(assetStub.externalDeletable); await sut.handleAssetDeletion({ id: assetStub.externalDeletable.id }); From 277d00859f2e6dce7f45cca3c8291b9ad111e20a Mon Sep 17 00:00:00 2001 From: Ben McCann <322311+benmccann@users.noreply.github.com> Date: Tue, 30 Apr 2024 15:46:13 -0700 Subject: [PATCH 07/23] revert line change causing test failure --- server/src/services/asset.service.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/src/services/asset.service.ts b/server/src/services/asset.service.ts index 0cdaada9d0b3f..64ba80d2cc203 100644 --- a/server/src/services/asset.service.ts +++ b/server/src/services/asset.service.ts @@ -372,7 +372,8 @@ export class AssetService { return JobStatus.FAILED; } - if (asset.isReadOnly) { + // Ignore requests that are not from external library job but is for an external asset + if (!fromExternal && (!asset.library || asset.library.type === LibraryType.EXTERNAL)) { return JobStatus.SKIPPED; } From 082559bb397a83d6564d93265f28d8952134393f Mon Sep 17 00:00:00 2001 From: Ben McCann <322311+benmccann@users.noreply.github.com> Date: Tue, 30 Apr 2024 15:55:00 -0700 Subject: [PATCH 08/23] cannot be null --- web/src/routes/admin/library-management/+page.svelte | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/web/src/routes/admin/library-management/+page.svelte b/web/src/routes/admin/library-management/+page.svelte index a339835a7999d..8fa13ae58f5e4 100644 --- a/web/src/routes/admin/library-management/+page.svelte +++ b/web/src/routes/admin/library-management/+page.svelte @@ -412,10 +412,7 @@ onRenameClicked()} text={`Rename`} /> onToggleReadOnlyClicked()} - text={selectedLibrary.isReadOnly || - (selectedLibrary.isReadOnly == null && selectedLibrary.type === LibraryType.External) - ? 'Allow file deletion' - : 'Disallow file deletion'} + text={selectedLibrary.isReadOnly ? 'Allow file deletion' : 'Disallow file deletion'} /> {#if selectedLibrary.type === LibraryType.External} From 6c1a020092ac20f6aec546dec9d93428d0e53e70 Mon Sep 17 00:00:00 2001 From: Ben McCann <322311+benmccann@users.noreply.github.com> Date: Tue, 30 Apr 2024 16:09:58 -0700 Subject: [PATCH 09/23] remove newly added test, which is failing and non-sensical --- server/src/services/asset.service.spec.ts | 24 ----------------------- 1 file changed, 24 deletions(-) diff --git a/server/src/services/asset.service.spec.ts b/server/src/services/asset.service.spec.ts index c5c1aaf5f2651..49753923a203f 100755 --- a/server/src/services/asset.service.spec.ts +++ b/server/src/services/asset.service.spec.ts @@ -691,30 +691,6 @@ describe(AssetService.name, () => { expect(assetMock.remove).not.toHaveBeenCalled(); }); - it('should process assets from external non-read-only library without fromExternal flag', async () => { - assetMock.getById.mockResolvedValue(assetStub.externalDeletable); - - await sut.handleAssetDeletion({ id: assetStub.externalDeletable.id }); - - expect(assetMock.remove).toHaveBeenCalledWith(assetStub.externalDeletable); - expect(jobMock.queue.mock.calls).toEqual([ - [ - { - name: JobName.DELETE_FILES, - data: { - files: [ - assetStub.externalDeletable.thumbnailPath, - assetStub.externalDeletable.previewPath, - assetStub.externalDeletable.encodedVideoPath, - assetStub.externalDeletable.sidecarPath, - assetStub.externalDeletable.originalPath, - ], - }, - }, - ], - ]); - }); - it('should process assets from external library with fromExternal flag', async () => { assetMock.getById.mockResolvedValue(assetStub.external); From 51b752511db05a7e1618fcb7859cc046341d737b Mon Sep 17 00:00:00 2001 From: Ben McCann <322311+benmccann@users.noreply.github.com> Date: Tue, 30 Apr 2024 16:19:03 -0700 Subject: [PATCH 10/23] update stubs --- server/test/fixtures/asset.stub.ts | 2 +- server/test/fixtures/library.stub.ts | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/server/test/fixtures/asset.stub.ts b/server/test/fixtures/asset.stub.ts index 3410bc325adfa..4852ac7287e93 100644 --- a/server/test/fixtures/asset.stub.ts +++ b/server/test/fixtures/asset.stub.ts @@ -45,7 +45,7 @@ export const assetStub = { sharedLinks: [], faces: [], sidecarPath: null, - isReadOnly: false, + isReadOnly: true, deletedAt: null, isOffline: false, isExternal: false, diff --git a/server/test/fixtures/library.stub.ts b/server/test/fixtures/library.stub.ts index 1db359b1731cd..64d164b75adb0 100644 --- a/server/test/fixtures/library.stub.ts +++ b/server/test/fixtures/library.stub.ts @@ -17,7 +17,7 @@ export const libraryStub = { updatedAt: new Date('2022-01-01'), refreshedAt: null, isVisible: true, - isReadOnly: null, + isReadOnly: false, exclusionPatterns: [], }), externalLibrary1: Object.freeze({ @@ -32,7 +32,7 @@ export const libraryStub = { updatedAt: new Date('2023-01-01'), refreshedAt: null, isVisible: true, - isReadOnly: null, + isReadOnly: true, exclusionPatterns: [], }), externalLibrary2: Object.freeze({ @@ -47,7 +47,7 @@ export const libraryStub = { updatedAt: new Date('2022-01-01'), refreshedAt: null, isVisible: true, - isReadOnly: null, + isReadOnly: true, exclusionPatterns: [], }), externalLibraryNotReadOnly: Object.freeze({ @@ -77,7 +77,7 @@ export const libraryStub = { updatedAt: new Date('2023-01-01'), refreshedAt: null, isVisible: true, - isReadOnly: null, + isReadOnly: true, exclusionPatterns: [], }), externalLibraryWithImportPaths2: Object.freeze({ @@ -92,7 +92,7 @@ export const libraryStub = { updatedAt: new Date('2023-01-01'), refreshedAt: null, isVisible: true, - isReadOnly: null, + isReadOnly: true, exclusionPatterns: [], }), externalLibraryWithExclusionPattern: Object.freeze({ @@ -107,7 +107,7 @@ export const libraryStub = { updatedAt: new Date('2023-01-01'), refreshedAt: null, isVisible: true, - isReadOnly: null, + isReadOnly: true, exclusionPatterns: ['**/dir1/**'], }), patternPath: Object.freeze({ @@ -122,7 +122,7 @@ export const libraryStub = { updatedAt: new Date('2023-01-01'), refreshedAt: null, isVisible: true, - isReadOnly: null, + isReadOnly: true, exclusionPatterns: ['**/dir1/**'], }), hasImmichPaths: Object.freeze({ @@ -137,7 +137,7 @@ export const libraryStub = { updatedAt: new Date('2023-01-01'), refreshedAt: null, isVisible: true, - isReadOnly: null, + isReadOnly: true, exclusionPatterns: ['**/dir1/**'], }), }; From 9bf1a307a94f64814789ebfd48ae3fc0954cb856 Mon Sep 17 00:00:00 2001 From: Ben McCann <322311+benmccann@users.noreply.github.com> Date: Tue, 30 Apr 2024 16:30:16 -0700 Subject: [PATCH 11/23] default library should not be readonly --- server/src/cores/user.core.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/server/src/cores/user.core.ts b/server/src/cores/user.core.ts index e8596db3e7629..56ca88b8920cb 100644 --- a/server/src/cores/user.core.ts +++ b/server/src/cores/user.core.ts @@ -101,6 +101,7 @@ export class UserCore { type: LibraryType.UPLOAD, importPaths: [], exclusionPatterns: [], + isReadOnly: false, isVisible: true, }); From a1027685071b225ae78c562e8b821ec86fb39100 Mon Sep 17 00:00:00 2001 From: Ben McCann <322311+benmccann@users.noreply.github.com> Date: Tue, 30 Apr 2024 16:34:23 -0700 Subject: [PATCH 12/23] update migration --- server/src/migrations/1714513984514-AddLibraryReadOnly.ts | 3 ++- server/src/services/asset.service.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/server/src/migrations/1714513984514-AddLibraryReadOnly.ts b/server/src/migrations/1714513984514-AddLibraryReadOnly.ts index 0a0eb3fa1f771..4420797908107 100644 --- a/server/src/migrations/1714513984514-AddLibraryReadOnly.ts +++ b/server/src/migrations/1714513984514-AddLibraryReadOnly.ts @@ -4,7 +4,8 @@ export class AddLibraryReadOnly1714513984514 implements MigrationInterface { name = 'AddLibraryReadOnly1714513984514' public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "libraries" ADD "isReadOnly" boolean NOT NULL`); + await queryRunner.query(`ALTER TABLE "libraries" ADD "isReadOnly" boolean NOT NULL DEFAULT false`); + await queryRunner.query(`UPDATE libraries SET "isReadOnly" = true WHERE type = 'EXTERNAL'`); } public async down(queryRunner: QueryRunner): Promise { diff --git a/server/src/services/asset.service.ts b/server/src/services/asset.service.ts index 64ba80d2cc203..45184b6dedfaa 100644 --- a/server/src/services/asset.service.ts +++ b/server/src/services/asset.service.ts @@ -358,7 +358,7 @@ export class AssetService { async handleAssetDeletion(job: IAssetDeletionJob): Promise { const { id, fromExternal } = job; - + console.error('hanndling asset deletion\n\n\n'); const asset = await this.assetRepository.getById(id, { faces: { person: true, From e4faa1a36cb29dfb36bb1380eb8e34893c84d168 Mon Sep 17 00:00:00 2001 From: Ben McCann <322311+benmccann@users.noreply.github.com> Date: Tue, 30 Apr 2024 16:45:19 -0700 Subject: [PATCH 13/23] update stubs --- server/test/fixtures/asset.stub.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/test/fixtures/asset.stub.ts b/server/test/fixtures/asset.stub.ts index 4852ac7287e93..412d31f040ed9 100644 --- a/server/test/fixtures/asset.stub.ts +++ b/server/test/fixtures/asset.stub.ts @@ -45,7 +45,7 @@ export const assetStub = { sharedLinks: [], faces: [], sidecarPath: null, - isReadOnly: true, + isReadOnly: false, deletedAt: null, isOffline: false, isExternal: false, @@ -235,7 +235,7 @@ export const assetStub = { localDateTime: new Date('2023-02-23T05:06:29.716Z'), isFavorite: true, isArchived: false, - isReadOnly: false, + isReadOnly: true, isExternal: true, duration: null, isVisible: true, From 40b0d3fd7156b929bb92a59379c1f16a099aa1f5 Mon Sep 17 00:00:00 2001 From: Ben McCann <322311+benmccann@users.noreply.github.com> Date: Tue, 30 Apr 2024 16:51:02 -0700 Subject: [PATCH 14/23] set readonly when creating library --- mobile/openapi/doc/CreateLibraryDto.md | 1 + .../openapi/lib/model/create_library_dto.dart | 19 ++++++++++++++++++- .../openapi/test/create_library_dto_test.dart | 5 +++++ open-api/immich-openapi-specs.json | 3 +++ open-api/typescript-sdk/src/fetch-client.ts | 1 + server/src/dtos/library.dto.ts | 3 +++ server/src/entities/library.entity.ts | 2 +- server/src/services/library.service.ts | 5 +++++ 8 files changed, 37 insertions(+), 2 deletions(-) diff --git a/mobile/openapi/doc/CreateLibraryDto.md b/mobile/openapi/doc/CreateLibraryDto.md index 01a2a0f917761..6e7fa548b6bad 100644 --- a/mobile/openapi/doc/CreateLibraryDto.md +++ b/mobile/openapi/doc/CreateLibraryDto.md @@ -10,6 +10,7 @@ Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- **exclusionPatterns** | **List** | | [optional] [default to const []] **importPaths** | **List** | | [optional] [default to const []] +**isReadOnly** | **bool** | | [optional] **isVisible** | **bool** | | [optional] **name** | **String** | | [optional] **ownerId** | **String** | | diff --git a/mobile/openapi/lib/model/create_library_dto.dart b/mobile/openapi/lib/model/create_library_dto.dart index 93fb89b701835..0366e35741144 100644 --- a/mobile/openapi/lib/model/create_library_dto.dart +++ b/mobile/openapi/lib/model/create_library_dto.dart @@ -15,6 +15,7 @@ class CreateLibraryDto { CreateLibraryDto({ this.exclusionPatterns = const [], this.importPaths = const [], + this.isReadOnly, this.isVisible, this.name, required this.ownerId, @@ -25,6 +26,14 @@ class CreateLibraryDto { List importPaths; + /// + /// 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? isReadOnly; + /// /// 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 @@ -49,6 +58,7 @@ class CreateLibraryDto { bool operator ==(Object other) => identical(this, other) || other is CreateLibraryDto && _deepEquality.equals(other.exclusionPatterns, exclusionPatterns) && _deepEquality.equals(other.importPaths, importPaths) && + other.isReadOnly == isReadOnly && other.isVisible == isVisible && other.name == name && other.ownerId == ownerId && @@ -59,18 +69,24 @@ class CreateLibraryDto { // ignore: unnecessary_parenthesis (exclusionPatterns.hashCode) + (importPaths.hashCode) + + (isReadOnly == null ? 0 : isReadOnly!.hashCode) + (isVisible == null ? 0 : isVisible!.hashCode) + (name == null ? 0 : name!.hashCode) + (ownerId.hashCode) + (type.hashCode); @override - String toString() => 'CreateLibraryDto[exclusionPatterns=$exclusionPatterns, importPaths=$importPaths, isVisible=$isVisible, name=$name, ownerId=$ownerId, type=$type]'; + String toString() => 'CreateLibraryDto[exclusionPatterns=$exclusionPatterns, importPaths=$importPaths, isReadOnly=$isReadOnly, isVisible=$isVisible, name=$name, ownerId=$ownerId, type=$type]'; Map toJson() { final json = {}; json[r'exclusionPatterns'] = this.exclusionPatterns; json[r'importPaths'] = this.importPaths; + if (this.isReadOnly != null) { + json[r'isReadOnly'] = this.isReadOnly; + } else { + // json[r'isReadOnly'] = null; + } if (this.isVisible != null) { json[r'isVisible'] = this.isVisible; } else { @@ -100,6 +116,7 @@ class CreateLibraryDto { importPaths: json[r'importPaths'] is Iterable ? (json[r'importPaths'] as Iterable).cast().toList(growable: false) : const [], + isReadOnly: mapValueOfType(json, r'isReadOnly'), isVisible: mapValueOfType(json, r'isVisible'), name: mapValueOfType(json, r'name'), ownerId: mapValueOfType(json, r'ownerId')!, diff --git a/mobile/openapi/test/create_library_dto_test.dart b/mobile/openapi/test/create_library_dto_test.dart index 1dd77af251e2d..6a010dd8d3307 100644 --- a/mobile/openapi/test/create_library_dto_test.dart +++ b/mobile/openapi/test/create_library_dto_test.dart @@ -26,6 +26,11 @@ void main() { // TODO }); + // bool isReadOnly + test('to test the property `isReadOnly`', () async { + // TODO + }); + // bool isVisible test('to test the property `isVisible`', () async { // TODO diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 3d983613ba432..8d2b99f78cc31 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -7681,6 +7681,9 @@ }, "type": "array" }, + "isReadOnly": { + "type": "boolean" + }, "isVisible": { "type": "boolean" }, diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 3d620d5449023..30c4705fc45b2 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -434,6 +434,7 @@ export type LibraryResponseDto = { export type CreateLibraryDto = { exclusionPatterns?: string[]; importPaths?: string[]; + isReadOnly?: boolean; isVisible?: boolean; name?: string; ownerId: string; diff --git a/server/src/dtos/library.dto.ts b/server/src/dtos/library.dto.ts index 3a86583650dbd..dc85a73ffbe47 100644 --- a/server/src/dtos/library.dto.ts +++ b/server/src/dtos/library.dto.ts @@ -19,6 +19,9 @@ export class CreateLibraryDto { @ValidateBoolean({ optional: true }) isVisible?: boolean; + @ValidateBoolean({ optional: true }) + isReadOnly?: boolean; + @Optional() @IsString({ each: true }) @IsNotEmpty({ each: true }) diff --git a/server/src/entities/library.entity.ts b/server/src/entities/library.entity.ts index 603f000a28501..a447e38d91cf8 100644 --- a/server/src/entities/library.entity.ts +++ b/server/src/entities/library.entity.ts @@ -54,7 +54,7 @@ export class LibraryEntity { @Column({ type: 'boolean', default: true }) isVisible!: boolean; - @Column({ type: 'boolean' }) + @Column({ type: 'boolean', default: false }) isReadOnly!: boolean; } diff --git a/server/src/services/library.service.ts b/server/src/services/library.service.ts index 00909e9837fc0..db10c23e6ac01 100644 --- a/server/src/services/library.service.ts +++ b/server/src/services/library.service.ts @@ -249,12 +249,17 @@ export class LibraryService { if (!dto.name) { dto.name = 'New External Library'; } + dto.isReadOnly ??= true; break; } case LibraryType.UPLOAD: { if (!dto.name) { dto.name = 'New Upload Library'; } + dto.isReadOnly ??= false; + if (dto.isReadOnly) { + throw new BadRequestException('Upload libraries cannot be readonly'); + } if (dto.importPaths && dto.importPaths.length > 0) { throw new BadRequestException('Upload libraries cannot have import paths'); } From c5241876096c18026668edfc70cb275ee8cf05dc Mon Sep 17 00:00:00 2001 From: Ben McCann <322311+benmccann@users.noreply.github.com> Date: Tue, 30 Apr 2024 20:05:43 -0700 Subject: [PATCH 15/23] remove debug line --- server/src/services/asset.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/services/asset.service.ts b/server/src/services/asset.service.ts index 45184b6dedfaa..64ba80d2cc203 100644 --- a/server/src/services/asset.service.ts +++ b/server/src/services/asset.service.ts @@ -358,7 +358,7 @@ export class AssetService { async handleAssetDeletion(job: IAssetDeletionJob): Promise { const { id, fromExternal } = job; - console.error('hanndling asset deletion\n\n\n'); + const asset = await this.assetRepository.getById(id, { faces: { person: true, From 7c4e148fdc1a6392f9c7b583d6d22ff211d9dd96 Mon Sep 17 00:00:00 2001 From: Ben McCann <322311+benmccann@users.noreply.github.com> Date: Tue, 30 Apr 2024 20:07:24 -0700 Subject: [PATCH 16/23] cannot be null --- web/src/routes/admin/library-management/+page.svelte | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/web/src/routes/admin/library-management/+page.svelte b/web/src/routes/admin/library-management/+page.svelte index 8fa13ae58f5e4..81671c5201f43 100644 --- a/web/src/routes/admin/library-management/+page.svelte +++ b/web/src/routes/admin/library-management/+page.svelte @@ -245,10 +245,9 @@ const onToggleReadOnlyClicked = async () => { const library = libraries[selectedLibraryIndex]; - const prevValue = library.isReadOnly == null ? library.type === LibraryType.External : library.isReadOnly; try { - await updateLibrary({ id: library.id, updateLibraryDto: { isReadOnly: !prevValue } }); + await updateLibrary({ id: library.id, updateLibraryDto: { isReadOnly: !library.isReadOnly } }); closeAll(); await readLibraryList(); } catch (error) { From d2c7dd4a458287c0eabbfd0d4f91785172a86f74 Mon Sep 17 00:00:00 2001 From: Ben McCann <322311+benmccann@users.noreply.github.com> Date: Tue, 30 Apr 2024 20:09:57 -0700 Subject: [PATCH 17/23] update filter --- web/src/lib/components/photos-page/actions/delete-assets.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/lib/components/photos-page/actions/delete-assets.svelte b/web/src/lib/components/photos-page/actions/delete-assets.svelte index 9d413ca7f647b..e568a89acb1dc 100644 --- a/web/src/lib/components/photos-page/actions/delete-assets.svelte +++ b/web/src/lib/components/photos-page/actions/delete-assets.svelte @@ -34,7 +34,7 @@ const handleDelete = async () => { loading = true; - const ids = [...getOwnedAssets()].map((a) => a.id); + const ids = [...getOwnedAssets()].filter((a) => !a.isReadOnly).map((a) => a.id); await deleteAssets(force, onAssetDelete, ids); clearSelect(); isShowConfirmation = false; From 12776128ed96fe3b5912b451d42ece586d090123 Mon Sep 17 00:00:00 2001 From: Ben McCann <322311+benmccann@users.noreply.github.com> Date: Tue, 30 Apr 2024 20:32:01 -0700 Subject: [PATCH 18/23] toggle asset flag on server --- server/src/interfaces/asset.interface.ts | 43 +++++++++++---------- server/src/repositories/asset.repository.ts | 4 ++ server/src/services/library.service.ts | 4 ++ 3 files changed, 30 insertions(+), 21 deletions(-) diff --git a/server/src/interfaces/asset.interface.ts b/server/src/interfaces/asset.interface.ts index cad83f09d4024..cbb1cdc8380f4 100644 --- a/server/src/interfaces/asset.interface.ts +++ b/server/src/interfaces/asset.interface.ts @@ -151,41 +151,42 @@ export const IAssetRepository = 'IAssetRepository'; export interface IAssetRepository { create(asset: AssetCreate): Promise; + deleteAll(ownerId: string): Promise; + findLivePhotoMatch(options: LivePhotoSearchOptions): Promise; + getAll(pagination: PaginationOptions, options?: AssetSearchOptions): Paginated; + getAllByDeviceId(userId: string, deviceId: string): Promise; + getAssetIdByCity(userId: string, options: AssetExploreFieldOptions): Promise>; + getAssetIdByTag(userId: string, options: AssetExploreFieldOptions): Promise>; + getAllForUserFullSync(options: AssetFullSyncOptions): Promise; getByIds( ids: string[], relations?: FindOptionsRelations, select?: FindOptionsSelect, ): Promise; - getByIdsWithAllRelations(ids: string[]): Promise; - getByDayOfYear(ownerIds: string[], monthDay: MonthDay): Promise; - getByChecksum(libraryId: string, checksum: Buffer): Promise; getByAlbumId(pagination: PaginationOptions, albumId: string): Paginated; + getByChecksum(libraryId: string, checksum: Buffer): Promise; + getByDayOfYear(ownerIds: string[], monthDay: MonthDay): Promise; + getByIdsWithAllRelations(ids: string[]): Promise; + getByLibraryIdAndOriginalPath(libraryId: string, originalPath: string): Promise; getByUserId(pagination: PaginationOptions, userId: string, options?: AssetSearchOptions): Paginated; getById(id: string, relations?: FindOptionsRelations): Promise; - getWithout(pagination: PaginationOptions, property: WithoutProperty): Paginated; - getWith(pagination: PaginationOptions, property: WithProperty, libraryId?: string): Paginated; - getRandom(userId: string, count: number): Promise; + getChangedDeltaSync(options: AssetDeltaSyncOptions): Promise; + getExternalLibraryAssetPaths(pagination: PaginationOptions, libraryId: string): Paginated; getFirstAssetForAlbumId(albumId: string): Promise; getLastUpdatedAssetForAlbumId(albumId: string): Promise; - getExternalLibraryAssetPaths(pagination: PaginationOptions, libraryId: string): Paginated; - getByLibraryIdAndOriginalPath(libraryId: string, originalPath: string): Promise; - deleteAll(ownerId: string): Promise; - getAll(pagination: PaginationOptions, options?: AssetSearchOptions): Paginated; - getAllByDeviceId(userId: string, deviceId: string): Promise; - updateAll(ids: string[], options: Partial): Promise; - update(asset: AssetUpdateOptions): Promise; - remove(asset: AssetEntity): Promise; - softDeleteAll(ids: string[]): Promise; - restoreAll(ids: string[]): Promise; - findLivePhotoMatch(options: LivePhotoSearchOptions): Promise; getMapMarkers(ownerIds: string[], options?: MapMarkerSearchOptions): Promise; + getRandom(userId: string, count: number): Promise; getStatistics(ownerId: string, options: AssetStatsOptions): Promise; getTimeBuckets(options: TimeBucketOptions): Promise; getTimeBucket(timeBucket: string, options: TimeBucketOptions): Promise; + getWithout(pagination: PaginationOptions, property: WithoutProperty): Paginated; + getWith(pagination: PaginationOptions, property: WithProperty, libraryId?: string): Paginated; + remove(asset: AssetEntity): Promise; + restoreAll(ids: string[]): Promise; + setReadOnlyForLibrary(libraryId: string, isReadOnly: boolean): Promise; + softDeleteAll(ids: string[]): Promise; + updateAll(ids: string[], options: Partial): Promise; + update(asset: AssetUpdateOptions): Promise; upsertExif(exif: Partial): Promise; upsertJobStatus(jobStatus: Partial): Promise; - getAssetIdByCity(userId: string, options: AssetExploreFieldOptions): Promise>; - getAssetIdByTag(userId: string, options: AssetExploreFieldOptions): Promise>; - getAllForUserFullSync(options: AssetFullSyncOptions): Promise; - getChangedDeltaSync(options: AssetDeltaSyncOptions): Promise; } diff --git a/server/src/repositories/asset.repository.ts b/server/src/repositories/asset.repository.ts index 42dac813608c5..0c8c662aae125 100644 --- a/server/src/repositories/asset.repository.ts +++ b/server/src/repositories/asset.repository.ts @@ -201,6 +201,10 @@ export class AssetRepository implements IAssetRepository { ); } + async setReadOnlyForLibrary(libraryId: string, isReadOnly: boolean): Promise { + await this.repository.update({ library: { id: libraryId } }, { isReadOnly }); + } + getAll(pagination: PaginationOptions, options: AssetSearchOptions = {}): Paginated { let builder = this.repository.createQueryBuilder('asset'); builder = searchAssetBuilder(builder, options); diff --git a/server/src/services/library.service.ts b/server/src/services/library.service.ts index db10c23e6ac01..a3e89077bf6a3 100644 --- a/server/src/services/library.service.ts +++ b/server/src/services/library.service.ts @@ -364,6 +364,10 @@ export class LibraryService { } } + if (dto.isReadOnly !== undefined) { + await this.assetRepository.setReadOnlyForLibrary(id, dto.isReadOnly); + } + return mapLibrary(library); } From f4017e48d785f4d203403aa66516bbfb32e2b40d Mon Sep 17 00:00:00 2001 From: Ben McCann <322311+benmccann@users.noreply.github.com> Date: Tue, 30 Apr 2024 20:35:54 -0700 Subject: [PATCH 19/23] initialize correctly on asset creation --- server/src/services/library.service.ts | 2 +- server/src/services/metadata.service.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/server/src/services/library.service.ts b/server/src/services/library.service.ts index a3e89077bf6a3..4da73762e5c34 100644 --- a/server/src/services/library.service.ts +++ b/server/src/services/library.service.ts @@ -512,7 +512,7 @@ export class LibraryService { type: assetType, originalFileName, sidecarPath, - isReadOnly: true, + isReadOnly: library?.isReadOnly ?? false, isExternal: true, }); assetId = addedAsset.id; diff --git a/server/src/services/metadata.service.ts b/server/src/services/metadata.service.ts index e28f1cd474248..f5912576a63c3 100644 --- a/server/src/services/metadata.service.ts +++ b/server/src/services/metadata.service.ts @@ -440,7 +440,7 @@ export class MetadataService { originalPath: motionPath, originalFileName: asset.originalFileName, isVisible: false, - isReadOnly: false, + isReadOnly: asset.isReadOnly, deviceAssetId: 'NONE', deviceId: 'NONE', }); From 4b6c84adfff4d05167c552c0344895001e8c14ed Mon Sep 17 00:00:00 2001 From: Ben McCann <322311+benmccann@users.noreply.github.com> Date: Tue, 30 Apr 2024 21:35:45 -0700 Subject: [PATCH 20/23] update tests --- server/src/services/library.service.spec.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server/src/services/library.service.spec.ts b/server/src/services/library.service.spec.ts index 2122b03208111..3384ab8027b31 100644 --- a/server/src/services/library.service.spec.ts +++ b/server/src/services/library.service.spec.ts @@ -368,7 +368,7 @@ describe(LibraryService.name, () => { type: AssetType.IMAGE, originalFileName: 'photo.jpg', sidecarPath: null, - isReadOnly: true, + isReadOnly: false, isExternal: true, }, ], @@ -416,7 +416,7 @@ describe(LibraryService.name, () => { type: AssetType.IMAGE, originalFileName: 'photo.jpg', sidecarPath: '/data/user1/photo.jpg.xmp', - isReadOnly: true, + isReadOnly: false, isExternal: true, }, ], @@ -463,7 +463,7 @@ describe(LibraryService.name, () => { type: AssetType.VIDEO, originalFileName: 'video.mp4', sidecarPath: null, - isReadOnly: true, + isReadOnly: false, isExternal: true, }, ], From 81a15e763ef41c7cdc79aa541626542018e12e2d Mon Sep 17 00:00:00 2001 From: Ben McCann <322311+benmccann@users.noreply.github.com> Date: Tue, 30 Apr 2024 21:39:04 -0700 Subject: [PATCH 21/23] update mock --- server/test/repositories/asset.repository.mock.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/server/test/repositories/asset.repository.mock.ts b/server/test/repositories/asset.repository.mock.ts index f09d6b619eb70..1581b97a11745 100644 --- a/server/test/repositories/asset.repository.mock.ts +++ b/server/test/repositories/asset.repository.mock.ts @@ -32,6 +32,7 @@ export const newAssetRepositoryMock = (): Mocked => { getTimeBucket: vitest.fn(), getTimeBuckets: vitest.fn(), restoreAll: vitest.fn(), + setReadOnlyForLibrary: vitest.fn(), softDeleteAll: vitest.fn(), getAssetIdByCity: vitest.fn(), getAssetIdByTag: vitest.fn(), From aae4459d3794fde162bc307c1e51f7e56ce7cf2c Mon Sep 17 00:00:00 2001 From: Ben McCann <322311+benmccann@users.noreply.github.com> Date: Wed, 1 May 2024 07:33:27 -0700 Subject: [PATCH 22/23] use isReadOnly field --- server/src/services/library.service.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/server/src/services/library.service.ts b/server/src/services/library.service.ts index 4da73762e5c34..2a374a88c8db5 100644 --- a/server/src/services/library.service.ts +++ b/server/src/services/library.service.ts @@ -249,14 +249,12 @@ export class LibraryService { if (!dto.name) { dto.name = 'New External Library'; } - dto.isReadOnly ??= true; break; } case LibraryType.UPLOAD: { if (!dto.name) { dto.name = 'New Upload Library'; } - dto.isReadOnly ??= false; if (dto.isReadOnly) { throw new BadRequestException('Upload libraries cannot be readonly'); } @@ -276,6 +274,7 @@ export class LibraryService { type: dto.type, importPaths: dto.importPaths ?? [], exclusionPatterns: dto.exclusionPatterns ?? [], + isReadOnly: dto.isReadOnly ?? dto.type === LibraryType.EXTERNAL, isVisible: dto.isVisible ?? true, }); From 3e19aef1f8f05adff6c8fbc248dc85ab1384ad77 Mon Sep 17 00:00:00 2001 From: Ben McCann <322311+benmccann@users.noreply.github.com> Date: Wed, 1 May 2024 10:38:37 -0700 Subject: [PATCH 23/23] only set isReadOnly for visible assets --- server/src/repositories/asset.repository.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/repositories/asset.repository.ts b/server/src/repositories/asset.repository.ts index 0c8c662aae125..91cfe4fbdcde1 100644 --- a/server/src/repositories/asset.repository.ts +++ b/server/src/repositories/asset.repository.ts @@ -202,7 +202,7 @@ export class AssetRepository implements IAssetRepository { } async setReadOnlyForLibrary(libraryId: string, isReadOnly: boolean): Promise { - await this.repository.update({ library: { id: libraryId } }, { isReadOnly }); + await this.repository.update({ library: { id: libraryId }, isVisible: true }, { isReadOnly }); } getAll(pagination: PaginationOptions, options: AssetSearchOptions = {}): Paginated {