diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/MessagesImplBase.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/MessagesImplBase.kt index 1abd9c39c8..0e42a8c9d1 100644 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/MessagesImplBase.kt +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/MessagesImplBase.kt @@ -235,7 +235,9 @@ open class NativeSyncApiImplBase(context: Context) { } } + // This method is only implemented on iOS; on Android, we do not have a concept of cloud IDs + @Suppress("unused", "UNUSED_PARAMETER") fun getCloudIdForAssetIds(assetIds: List): Map { - throw IllegalStateException("Method not supported on Android.") + return emptyMap() } } diff --git a/mobile/lib/infrastructure/entities/local_asset.entity.drift.dart b/mobile/lib/infrastructure/entities/local_asset.entity.drift.dart index d0fe742463..23cdd92ec9 100644 --- a/mobile/lib/infrastructure/entities/local_asset.entity.drift.dart +++ b/mobile/lib/infrastructure/entities/local_asset.entity.drift.dart @@ -21,6 +21,7 @@ typedef $$LocalAssetEntityTableCreateCompanionBuilder = i0.Value checksum, i0.Value isFavorite, i0.Value orientation, + i0.Value cloudId, }); typedef $$LocalAssetEntityTableUpdateCompanionBuilder = i1.LocalAssetEntityCompanion Function({ @@ -35,6 +36,7 @@ typedef $$LocalAssetEntityTableUpdateCompanionBuilder = i0.Value checksum, i0.Value isFavorite, i0.Value orientation, + i0.Value cloudId, }); class $$LocalAssetEntityTableFilterComposer @@ -101,6 +103,11 @@ class $$LocalAssetEntityTableFilterComposer column: $table.orientation, builder: (column) => i0.ColumnFilters(column), ); + + i0.ColumnFilters get cloudId => $composableBuilder( + column: $table.cloudId, + builder: (column) => i0.ColumnFilters(column), + ); } class $$LocalAssetEntityTableOrderingComposer @@ -166,6 +173,11 @@ class $$LocalAssetEntityTableOrderingComposer column: $table.orientation, builder: (column) => i0.ColumnOrderings(column), ); + + i0.ColumnOrderings get cloudId => $composableBuilder( + column: $table.cloudId, + builder: (column) => i0.ColumnOrderings(column), + ); } class $$LocalAssetEntityTableAnnotationComposer @@ -215,6 +227,9 @@ class $$LocalAssetEntityTableAnnotationComposer column: $table.orientation, builder: (column) => column, ); + + i0.GeneratedColumn get cloudId => + $composableBuilder(column: $table.cloudId, builder: (column) => column); } class $$LocalAssetEntityTableTableManager @@ -268,6 +283,7 @@ class $$LocalAssetEntityTableTableManager i0.Value checksum = const i0.Value.absent(), i0.Value isFavorite = const i0.Value.absent(), i0.Value orientation = const i0.Value.absent(), + i0.Value cloudId = const i0.Value.absent(), }) => i1.LocalAssetEntityCompanion( name: name, type: type, @@ -280,6 +296,7 @@ class $$LocalAssetEntityTableTableManager checksum: checksum, isFavorite: isFavorite, orientation: orientation, + cloudId: cloudId, ), createCompanionCallback: ({ @@ -294,6 +311,7 @@ class $$LocalAssetEntityTableTableManager i0.Value checksum = const i0.Value.absent(), i0.Value isFavorite = const i0.Value.absent(), i0.Value orientation = const i0.Value.absent(), + i0.Value cloudId = const i0.Value.absent(), }) => i1.LocalAssetEntityCompanion.insert( name: name, type: type, @@ -306,6 +324,7 @@ class $$LocalAssetEntityTableTableManager checksum: checksum, isFavorite: isFavorite, orientation: orientation, + cloudId: cloudId, ), withReferenceMapper: (p0) => p0 .map((e) => (e.readTable(table), i0.BaseReferences(db, table, e))) @@ -473,6 +492,17 @@ class $LocalAssetEntityTable extends i3.LocalAssetEntity requiredDuringInsert: false, defaultValue: const i4.Constant(0), ); + static const i0.VerificationMeta _cloudIdMeta = const i0.VerificationMeta( + 'cloudId', + ); + @override + late final i0.GeneratedColumn cloudId = i0.GeneratedColumn( + 'cloud_id', + aliasedName, + true, + type: i0.DriftSqlType.string, + requiredDuringInsert: false, + ); @override List get $columns => [ name, @@ -486,6 +516,7 @@ class $LocalAssetEntityTable extends i3.LocalAssetEntity checksum, isFavorite, orientation, + cloudId, ]; @override String get aliasedName => _alias ?? actualTableName; @@ -566,6 +597,12 @@ class $LocalAssetEntityTable extends i3.LocalAssetEntity ), ); } + if (data.containsKey('cloud_id')) { + context.handle( + _cloudIdMeta, + cloudId.isAcceptableOrUnknown(data['cloud_id']!, _cloudIdMeta), + ); + } return context; } @@ -624,6 +661,10 @@ class $LocalAssetEntityTable extends i3.LocalAssetEntity i0.DriftSqlType.int, data['${effectivePrefix}orientation'], )!, + cloudId: attachedDatabase.typeMapping.read( + i0.DriftSqlType.string, + data['${effectivePrefix}cloud_id'], + ), ); } @@ -653,6 +694,7 @@ class LocalAssetEntityData extends i0.DataClass final String? checksum; final bool isFavorite; final int orientation; + final String? cloudId; const LocalAssetEntityData({ required this.name, required this.type, @@ -665,6 +707,7 @@ class LocalAssetEntityData extends i0.DataClass this.checksum, required this.isFavorite, required this.orientation, + this.cloudId, }); @override Map toColumns(bool nullToAbsent) { @@ -692,6 +735,9 @@ class LocalAssetEntityData extends i0.DataClass } map['is_favorite'] = i0.Variable(isFavorite); map['orientation'] = i0.Variable(orientation); + if (!nullToAbsent || cloudId != null) { + map['cloud_id'] = i0.Variable(cloudId); + } return map; } @@ -714,6 +760,7 @@ class LocalAssetEntityData extends i0.DataClass checksum: serializer.fromJson(json['checksum']), isFavorite: serializer.fromJson(json['isFavorite']), orientation: serializer.fromJson(json['orientation']), + cloudId: serializer.fromJson(json['cloudId']), ); } @override @@ -733,6 +780,7 @@ class LocalAssetEntityData extends i0.DataClass 'checksum': serializer.toJson(checksum), 'isFavorite': serializer.toJson(isFavorite), 'orientation': serializer.toJson(orientation), + 'cloudId': serializer.toJson(cloudId), }; } @@ -748,6 +796,7 @@ class LocalAssetEntityData extends i0.DataClass i0.Value checksum = const i0.Value.absent(), bool? isFavorite, int? orientation, + i0.Value cloudId = const i0.Value.absent(), }) => i1.LocalAssetEntityData( name: name ?? this.name, type: type ?? this.type, @@ -762,6 +811,7 @@ class LocalAssetEntityData extends i0.DataClass checksum: checksum.present ? checksum.value : this.checksum, isFavorite: isFavorite ?? this.isFavorite, orientation: orientation ?? this.orientation, + cloudId: cloudId.present ? cloudId.value : this.cloudId, ); LocalAssetEntityData copyWithCompanion(i1.LocalAssetEntityCompanion data) { return LocalAssetEntityData( @@ -782,6 +832,7 @@ class LocalAssetEntityData extends i0.DataClass orientation: data.orientation.present ? data.orientation.value : this.orientation, + cloudId: data.cloudId.present ? data.cloudId.value : this.cloudId, ); } @@ -798,7 +849,8 @@ class LocalAssetEntityData extends i0.DataClass ..write('id: $id, ') ..write('checksum: $checksum, ') ..write('isFavorite: $isFavorite, ') - ..write('orientation: $orientation') + ..write('orientation: $orientation, ') + ..write('cloudId: $cloudId') ..write(')')) .toString(); } @@ -816,6 +868,7 @@ class LocalAssetEntityData extends i0.DataClass checksum, isFavorite, orientation, + cloudId, ); @override bool operator ==(Object other) => @@ -831,7 +884,8 @@ class LocalAssetEntityData extends i0.DataClass other.id == this.id && other.checksum == this.checksum && other.isFavorite == this.isFavorite && - other.orientation == this.orientation); + other.orientation == this.orientation && + other.cloudId == this.cloudId); } class LocalAssetEntityCompanion @@ -847,6 +901,7 @@ class LocalAssetEntityCompanion final i0.Value checksum; final i0.Value isFavorite; final i0.Value orientation; + final i0.Value cloudId; const LocalAssetEntityCompanion({ this.name = const i0.Value.absent(), this.type = const i0.Value.absent(), @@ -859,6 +914,7 @@ class LocalAssetEntityCompanion this.checksum = const i0.Value.absent(), this.isFavorite = const i0.Value.absent(), this.orientation = const i0.Value.absent(), + this.cloudId = const i0.Value.absent(), }); LocalAssetEntityCompanion.insert({ required String name, @@ -872,6 +928,7 @@ class LocalAssetEntityCompanion this.checksum = const i0.Value.absent(), this.isFavorite = const i0.Value.absent(), this.orientation = const i0.Value.absent(), + this.cloudId = const i0.Value.absent(), }) : name = i0.Value(name), type = i0.Value(type), id = i0.Value(id); @@ -887,6 +944,7 @@ class LocalAssetEntityCompanion i0.Expression? checksum, i0.Expression? isFavorite, i0.Expression? orientation, + i0.Expression? cloudId, }) { return i0.RawValuesInsertable({ if (name != null) 'name': name, @@ -900,6 +958,7 @@ class LocalAssetEntityCompanion if (checksum != null) 'checksum': checksum, if (isFavorite != null) 'is_favorite': isFavorite, if (orientation != null) 'orientation': orientation, + if (cloudId != null) 'cloud_id': cloudId, }); } @@ -915,6 +974,7 @@ class LocalAssetEntityCompanion i0.Value? checksum, i0.Value? isFavorite, i0.Value? orientation, + i0.Value? cloudId, }) { return i1.LocalAssetEntityCompanion( name: name ?? this.name, @@ -928,6 +988,7 @@ class LocalAssetEntityCompanion checksum: checksum ?? this.checksum, isFavorite: isFavorite ?? this.isFavorite, orientation: orientation ?? this.orientation, + cloudId: cloudId ?? this.cloudId, ); } @@ -969,6 +1030,9 @@ class LocalAssetEntityCompanion if (orientation.present) { map['orientation'] = i0.Variable(orientation.value); } + if (cloudId.present) { + map['cloud_id'] = i0.Variable(cloudId.value); + } return map; } @@ -985,7 +1049,8 @@ class LocalAssetEntityCompanion ..write('id: $id, ') ..write('checksum: $checksum, ') ..write('isFavorite: $isFavorite, ') - ..write('orientation: $orientation') + ..write('orientation: $orientation, ') + ..write('cloudId: $cloudId') ..write(')')) .toString(); } diff --git a/mobile/lib/infrastructure/repositories/db.repository.dart b/mobile/lib/infrastructure/repositories/db.repository.dart index 61440f9594..8013c60d5c 100644 --- a/mobile/lib/infrastructure/repositories/db.repository.dart +++ b/mobile/lib/infrastructure/repositories/db.repository.dart @@ -122,8 +122,10 @@ class Drift extends $Drift implements IDatabaseRepository { }, from7To8: (m, v8) async { await m.create(v8.storeEntity); + }, + from8To9: (m, v9) async { // Add cloudId column to local_asset_entity - await m.addColumn(v6.localAssetEntity, v6.localAssetEntity.cloudId); + await m.addColumn(v9.localAssetEntity, v9.localAssetEntity.cloudId); }, from8To9: (m, v9) async { await m.addColumn(v9.localAlbumEntity, v9.localAlbumEntity.linkedRemoteAlbumId); diff --git a/mobile/lib/platform/native_sync_api.g.dart b/mobile/lib/platform/native_sync_api.g.dart index 6fc96f5046..9682ba38a7 100644 --- a/mobile/lib/platform/native_sync_api.g.dart +++ b/mobile/lib/platform/native_sync_api.g.dart @@ -495,4 +495,32 @@ class NativeSyncApi { return (pigeonVar_replyList[0] as List?)!.cast(); } } + + Future> getCloudIdForAssetIds(List assetIds) async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.immich_mobile.NativeSyncApi.getCloudIdForAssetIds$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([assetIds]); + final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as Map?)!.cast(); + } + } } diff --git a/mobile/lib/utils/migration.dart b/mobile/lib/utils/migration.dart index cdead7c949..e630e641f5 100644 --- a/mobile/lib/utils/migration.dart +++ b/mobile/lib/utils/migration.dart @@ -35,7 +35,7 @@ import 'package:logging/logging.dart'; // ignore: import_rule_photo_manager import 'package:photo_manager/photo_manager.dart'; -const int targetVersion = 14; +const int targetVersion = 15; Future migrateDatabaseIfNeeded(Isar db, Drift drift) async { final hasVersion = Store.tryGet(StoreKey.version) != null; @@ -70,6 +70,10 @@ Future migrateDatabaseIfNeeded(Isar db, Drift drift) async { await Store.populateCache(); } + if (version < 15) { + await _updateCloudId(drift); + } + if (targetVersion >= 12) { await Store.put(StoreKey.version, targetVersion); return; @@ -179,6 +183,20 @@ Future migrateDeviceAssetToSqlite(Isar db, Drift drift) async { } } +Future _updateCloudId(Drift drift) async { + // Android do not have a concept of cloud IDs + if (Platform.isAndroid) { + return; + } + + final query = drift.localAssetEntity.selectOnly() + ..addColumns([drift.localAssetEntity.id]) + ..where(drift.localAssetEntity.cloudId.isNull()); + final ids = await query.map((row) => row.read(drift.localAssetEntity.id)!).get(); + final cloudMapping = await NativeSyncApi().getCloudIdForAssetIds(ids); + await DriftLocalAlbumRepository(drift).updateCloudMapping(cloudMapping); +} + Future migrateBackupAlbumsToSqlite(Isar db, Drift drift) async { try { final isarBackupAlbums = await db.backupAlbums.where().findAll(); diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index a97e0333b9..c642dc93a4 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -4797,9 +4797,6 @@ export enum Permission { export enum AssetMetadataKey { MobileApp = "mobile-app" } -export enum AssetMetadataKey { - MobileApp = "mobile-app" -} export enum AssetMediaStatus { Created = "created", Replaced = "replaced", diff --git a/server/src/schema/migrations/1753888027393-AssetMetadataTables.ts b/server/src/schema/migrations/1753888027393-AssetMetadataTables.ts deleted file mode 100644 index ba0bad9d9a..0000000000 --- a/server/src/schema/migrations/1753888027393-AssetMetadataTables.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { Kysely, sql } from 'kysely'; - -export async function up(db: Kysely): Promise { - await sql`CREATE OR REPLACE FUNCTION asset_metadata_audit() - RETURNS TRIGGER - LANGUAGE PLPGSQL - AS $$ - BEGIN - INSERT INTO asset_metadata_audit ("assetId", "key") - SELECT "assetId", "key" - FROM OLD; - RETURN NULL; - END - $$;`.execute(db); - await sql`CREATE TABLE "asset_metadata_audit" ( - "id" uuid NOT NULL DEFAULT immich_uuid_v7(), - "assetId" uuid NOT NULL, - "key" character varying NOT NULL, - "deletedAt" timestamp with time zone NOT NULL DEFAULT clock_timestamp(), - CONSTRAINT "asset_metadata_audit_pkey" PRIMARY KEY ("id") -);`.execute(db); - await sql`CREATE INDEX "asset_metadata_audit_assetId_idx" ON "asset_metadata_audit" ("assetId");`.execute(db); - await sql`CREATE INDEX "asset_metadata_audit_key_idx" ON "asset_metadata_audit" ("key");`.execute(db); - await sql`CREATE INDEX "asset_metadata_audit_deletedAt_idx" ON "asset_metadata_audit" ("deletedAt");`.execute(db); - await sql`CREATE TABLE "asset_metadata" ( - "assetId" uuid NOT NULL, - "key" character varying NOT NULL, - "value" jsonb NOT NULL, - "updateId" uuid NOT NULL DEFAULT immich_uuid_v7(), - "updatedAt" timestamp with time zone NOT NULL DEFAULT now(), - CONSTRAINT "asset_metadata_assetId_fkey" FOREIGN KEY ("assetId") REFERENCES "asset" ("id") ON UPDATE CASCADE ON DELETE CASCADE, - CONSTRAINT "asset_metadata_pkey" PRIMARY KEY ("assetId", "key") -);`.execute(db); - await sql`CREATE INDEX "asset_metadata_updateId_idx" ON "asset_metadata" ("updateId");`.execute(db); - await sql`CREATE INDEX "asset_metadata_updatedAt_idx" ON "asset_metadata" ("updatedAt");`.execute(db); - await sql`CREATE OR REPLACE TRIGGER "asset_metadata_audit" - AFTER DELETE ON "asset_metadata" - REFERENCING OLD TABLE AS "old" - FOR EACH STATEMENT - WHEN (pg_trigger_depth() = 0) - EXECUTE FUNCTION asset_metadata_audit();`.execute(db); - await sql`CREATE OR REPLACE TRIGGER "asset_metadata_updated_at" - BEFORE UPDATE ON "asset_metadata" - FOR EACH ROW - EXECUTE FUNCTION updated_at();`.execute(db); - await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('function_asset_metadata_audit', '{"type":"function","name":"asset_metadata_audit","sql":"CREATE OR REPLACE FUNCTION asset_metadata_audit()\\n RETURNS TRIGGER\\n LANGUAGE PLPGSQL\\n AS $$\\n BEGIN\\n INSERT INTO asset_metadata_audit (\\"assetId\\", \\"key\\")\\n SELECT \\"assetId\\", \\"key\\"\\n FROM OLD;\\n RETURN NULL;\\n END\\n $$;"}'::jsonb);`.execute(db); - await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_asset_metadata_audit', '{"type":"trigger","name":"asset_metadata_audit","sql":"CREATE OR REPLACE TRIGGER \\"asset_metadata_audit\\"\\n AFTER DELETE ON \\"asset_metadata\\"\\n REFERENCING OLD TABLE AS \\"old\\"\\n FOR EACH STATEMENT\\n WHEN (pg_trigger_depth() = 0)\\n EXECUTE FUNCTION asset_metadata_audit();"}'::jsonb);`.execute(db); - await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_asset_metadata_updated_at', '{"type":"trigger","name":"asset_metadata_updated_at","sql":"CREATE OR REPLACE TRIGGER \\"asset_metadata_updated_at\\"\\n BEFORE UPDATE ON \\"asset_metadata\\"\\n FOR EACH ROW\\n EXECUTE FUNCTION updated_at();"}'::jsonb);`.execute(db); -} - -export async function down(db: Kysely): Promise { - await sql`DROP TABLE "asset_metadata_audit";`.execute(db); - await sql`DROP TABLE "asset_metadata";`.execute(db); - await sql`DROP FUNCTION asset_metadata_audit;`.execute(db); - await sql`DELETE FROM "migration_overrides" WHERE "name" = 'function_asset_metadata_audit';`.execute(db); - await sql`DELETE FROM "migration_overrides" WHERE "name" = 'trigger_asset_metadata_audit';`.execute(db); - await sql`DELETE FROM "migration_overrides" WHERE "name" = 'trigger_asset_metadata_updated_at';`.execute(db); -}