diff --git a/mobile/lib/domain/models/asset/trashed_asset.model.dart b/mobile/lib/domain/models/asset/trashed_asset.model.dart deleted file mode 100644 index 44732b3b01..0000000000 --- a/mobile/lib/domain/models/asset/trashed_asset.model.dart +++ /dev/null @@ -1,117 +0,0 @@ -import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; - -class TrashedAsset extends LocalAsset { - final String albumId; - - const TrashedAsset({ - required this.albumId, - required super.id, - super.remoteId, - required super.name, - super.checksum, - required super.type, - required super.createdAt, - required super.updatedAt, - super.width, - super.height, - super.durationInSeconds, - super.isFavorite = false, - super.livePhotoVideoId, - super.orientation = 0, - }); - - @override - TrashedAsset copyWith({ - String? id, - String? remoteId, - String? name, - String? checksum, - AssetType? type, - DateTime? createdAt, - DateTime? updatedAt, - int? width, - int? height, - int? durationInSeconds, - bool? isFavorite, - String? livePhotoVideoId, - int? orientation, - String? albumId, - }) { - return TrashedAsset( - id: id ?? this.id, - remoteId: remoteId ?? this.remoteId, - name: name ?? this.name, - checksum: checksum ?? this.checksum, - type: type ?? this.type, - createdAt: createdAt ?? this.createdAt, - updatedAt: updatedAt ?? this.updatedAt, - width: width ?? this.width, - height: height ?? this.height, - durationInSeconds: durationInSeconds ?? this.durationInSeconds, - isFavorite: isFavorite ?? this.isFavorite, - livePhotoVideoId: livePhotoVideoId ?? this.livePhotoVideoId, - orientation: orientation ?? this.orientation, - albumId: albumId ?? this.albumId, - ); - } - - @override - bool operator ==(Object other) => - identical(this, other) || - other is TrashedAsset && - runtimeType == other.runtimeType && - // LocalAsset identity - id == other.id && - name == other.name && - remoteId == other.remoteId && - checksum == other.checksum && - type == other.type && - createdAt == other.createdAt && - updatedAt == other.updatedAt && - width == other.width && - height == other.height && - durationInSeconds == other.durationInSeconds && - isFavorite == other.isFavorite && - livePhotoVideoId == other.livePhotoVideoId && - orientation == other.orientation && - // TrashedAsset extras - albumId == other.albumId; - - @override - int get hashCode => Object.hash( - id, - name, - remoteId, - checksum, - type, - createdAt, - updatedAt, - width, - height, - durationInSeconds, - isFavorite, - livePhotoVideoId, - orientation, - albumId, - ); - - @override - String toString() { - return 'TrashedAsset(' - 'id: $id, ' - 'remoteId: $remoteId, ' - 'name: $name, ' - 'checksum: $checksum, ' - 'type: $type, ' - 'createdAt: $createdAt, ' - 'updatedAt: $updatedAt, ' - 'width: $width, ' - 'height: $height, ' - 'durationInSeconds: $durationInSeconds, ' - 'isFavorite: $isFavorite, ' - 'livePhotoVideoId: $livePhotoVideoId, ' - 'orientation: $orientation, ' - 'albumId: $albumId, ' - ')'; - } -} diff --git a/mobile/lib/domain/services/local_sync.service.dart b/mobile/lib/domain/services/local_sync.service.dart index 7fec2d4d4f..5ca5c43e9f 100644 --- a/mobile/lib/domain/services/local_sync.service.dart +++ b/mobile/lib/domain/services/local_sync.service.dart @@ -4,7 +4,6 @@ import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:immich_mobile/domain/models/album/local_album.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; -import 'package:immich_mobile/domain/models/asset/trashed_asset.model.dart'; import 'package:immich_mobile/extensions/platform_extensions.dart'; import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/trashed_local_asset.repository.dart'; @@ -299,15 +298,9 @@ class LocalSyncService { if (trashUpdates.isEmpty) { return Future.value(); } - final trashedAssets = []; - for (final update in trashUpdates) { - final albums = delta.assetAlbums.cast>(); - for (final String id in albums[update.id]!.cast().nonNulls) { - trashedAssets.add(update.toTrashedAsset(id)); - } - } - _log.info("updateLocalTrashChanges trashedAssets: ${trashedAssets.map((e) => e.id)}"); - await _trashedLocalAssetRepository.saveTrashedAssets(trashedAssets); + final trashedAssets = delta.toTrashedAssets(); + _log.info("updateLocalTrashChanges trashedAssets: ${trashedAssets.map((e) => e.asset.id)}"); + await _trashedLocalAssetRepository.applyDelta(trashedAssets); await _applyRemoteRestoreToLocal(); } @@ -321,7 +314,7 @@ class LocalSyncService { _log.info("syncDeviceTrashSnapshot prepare, album: ${album.id}/${album.name}"); final trashedPlatformAssets = await _nativeSyncApi.getTrashedAssetsForAlbum(album.id); final trashedAssets = trashedPlatformAssets.toTrashedAssets(album.id); - await _trashedLocalAssetRepository.applyTrashSnapshot(trashedAssets, album.id); + await _trashedLocalAssetRepository.applyTrashSnapshot(trashedAssets); } await _applyRemoteRestoreToLocal(); } @@ -352,33 +345,22 @@ extension on Iterable { extension on Iterable { List toLocalAssets() { - return map( - (e) => LocalAsset( - id: e.id, - name: e.name, - checksum: null, - type: AssetType.values.elementAtOrNull(e.type) ?? AssetType.other, - createdAt: tryFromSecondsSinceEpoch(e.createdAt, isUtc: true) ?? DateTime.timestamp(), - updatedAt: tryFromSecondsSinceEpoch(e.updatedAt, isUtc: true) ?? DateTime.timestamp(), - width: e.width, - height: e.height, - durationInSeconds: e.durationInSeconds, - orientation: e.orientation, - isFavorite: e.isFavorite, - ), - ).toList(); + return map((e) => e.toLocalAsset()).toList(); + } + + Iterable toTrashedAssets(String albumId) { + return map((e) => (albumId: albumId, asset: e.toLocalAsset())); } } extension on PlatformAsset { - TrashedAsset toTrashedAsset(String albumId) => TrashedAsset( + LocalAsset toLocalAsset() => LocalAsset( id: id, name: name, checksum: null, type: AssetType.values.elementAtOrNull(type) ?? AssetType.other, - createdAt: tryFromSecondsSinceEpoch(createdAt) ?? DateTime.now(), - updatedAt: tryFromSecondsSinceEpoch(updatedAt) ?? DateTime.now(), - albumId: albumId, + createdAt: tryFromSecondsSinceEpoch(createdAt, isUtc: true) ?? DateTime.timestamp(), + updatedAt: tryFromSecondsSinceEpoch(createdAt, isUtc: true) ?? DateTime.timestamp(), width: width, height: height, durationInSeconds: durationInSeconds, @@ -387,8 +369,12 @@ extension on PlatformAsset { ); } -extension PlatformAssetsExtension on Iterable { - Iterable toTrashedAssets(String albumId) { - return map((e) => e.toTrashedAsset(albumId)); +extension SyncDeltaExtension on SyncDelta { + Iterable toTrashedAssets() { + return updates.map((e) { + final albums = assetAlbums.cast>(); + final albumId = albums[e.id]!.cast().first; + return (albumId: albumId, asset: e.toLocalAsset()); + }); } } diff --git a/mobile/lib/domain/services/trash_sync.service.dart b/mobile/lib/domain/services/trash_sync.service.dart deleted file mode 100644 index b2c54df1e2..0000000000 --- a/mobile/lib/domain/services/trash_sync.service.dart +++ /dev/null @@ -1,53 +0,0 @@ -import 'package:immich_mobile/extensions/platform_extensions.dart'; -import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart'; -import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart'; -import 'package:immich_mobile/infrastructure/repositories/trashed_local_asset.repository.dart'; -import 'package:immich_mobile/repositories/local_files_manager.repository.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:logging/logging.dart'; - -class TrashSyncService { - final AppSettingsService _appSettingsService; - final DriftLocalAssetRepository _localAssetRepository; - final DriftTrashedLocalAssetRepository _trashedLocalAssetRepository; - final LocalFilesManagerRepository _localFilesManager; - final StorageRepository _storageRepository; - final Logger _logger = Logger('TrashService'); - - TrashSyncService({ - required AppSettingsService appSettingsService, - required DriftLocalAssetRepository localAssetRepository, - required DriftTrashedLocalAssetRepository trashedLocalAssetRepository, - required LocalFilesManagerRepository localFilesManager, - required StorageRepository storageRepository, - }) : _appSettingsService = appSettingsService, - _localAssetRepository = localAssetRepository, - _trashedLocalAssetRepository = trashedLocalAssetRepository, - _localFilesManager = localFilesManager, - _storageRepository = storageRepository; - - bool get isTrashSyncMode => - CurrentPlatform.isAndroid && _appSettingsService.getSetting(AppSettingsEnum.manageLocalMediaAndroid); - - Future handleRemoteTrashed(Iterable checksums) async { - if (checksums.isEmpty) { - return Future.value(); - } else { - final localAssetsToTrash = await _localAssetRepository.getAssetsFromBackupAlbums(checksums); - if (localAssetsToTrash.isNotEmpty) { - final mediaUrls = await Future.wait( - localAssetsToTrash.values - .expand((e) => e) - .map((localAsset) => _storageRepository.getAssetEntityForAsset(localAsset).then((e) => e?.getMediaUrl())), - ); - _logger.info("Moving to trash ${mediaUrls.join(", ")} assets"); - final result = await _localFilesManager.moveToTrash(mediaUrls.nonNulls.toList()); - if (result) { - await _trashedLocalAssetRepository.trashLocalAsset(localAssetsToTrash); - } - } else { - _logger.info("No assets found in backup-enabled albums for assets: $checksums"); - } - } - } -} diff --git a/mobile/lib/infrastructure/entities/trashed_local_asset.entity.dart b/mobile/lib/infrastructure/entities/trashed_local_asset.entity.dart index 10a4cefe66..3b58e85bc8 100644 --- a/mobile/lib/infrastructure/entities/trashed_local_asset.entity.dart +++ b/mobile/lib/infrastructure/entities/trashed_local_asset.entity.dart @@ -1,5 +1,5 @@ import 'package:drift/drift.dart'; -import 'package:immich_mobile/domain/models/asset/trashed_asset.model.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/infrastructure/entities/trashed_local_asset.entity.drift.dart'; import 'package:immich_mobile/infrastructure/utils/asset.mixin.dart'; import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; @@ -23,10 +23,9 @@ class TrashedLocalAssetEntity extends Table with DriftDefaultsMixin, AssetEntity } extension TrashedLocalAssetEntityDataDomainExtension on TrashedLocalAssetEntityData { - TrashedAsset toDto() => TrashedAsset( + LocalAsset toLocalAsset() => LocalAsset( id: id, name: name, - albumId: albumId, checksum: checksum, type: type, createdAt: createdAt, diff --git a/mobile/lib/infrastructure/repositories/trashed_local_asset.repository.dart b/mobile/lib/infrastructure/repositories/trashed_local_asset.repository.dart index d653c58a6b..dcc3a03d98 100644 --- a/mobile/lib/infrastructure/repositories/trashed_local_asset.repository.dart +++ b/mobile/lib/infrastructure/repositories/trashed_local_asset.repository.dart @@ -2,13 +2,14 @@ import 'package:collection/collection.dart'; import 'package:drift/drift.dart'; import 'package:immich_mobile/domain/models/album/local_album.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; -import 'package:immich_mobile/domain/models/asset/trashed_asset.model.dart'; import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/trashed_local_asset.entity.dart'; import 'package:immich_mobile/infrastructure/entities/trashed_local_asset.entity.drift.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart'; +typedef TrashedAsset = ({String albumId, LocalAsset asset}); + class DriftTrashedLocalAssetRepository extends DriftDatabaseRepository { final Drift _db; @@ -30,12 +31,12 @@ class DriftTrashedLocalAssetRepository extends DriftDatabaseRepository { }); } - Future> getAssetsToHash(Iterable albumIds) { + Future> getAssetsToHash(Iterable albumIds) { final query = _db.trashedLocalAssetEntity.select()..where((r) => r.albumId.isIn(albumIds) & r.checksum.isNull()); - return query.map((row) => row.toDto()).get(); + return query.map((row) => row.toLocalAsset()).get(); } - Future> getToRestore() async { + Future> getToRestore() async { final selectedAlbumIds = (_db.selectOnly(_db.localAlbumEntity) ..addColumns([_db.localAlbumEntity.id]) ..where(_db.localAlbumEntity.backupSelection.equalsValue(BackupSelection.selected))); @@ -52,55 +53,54 @@ class DriftTrashedLocalAssetRepository extends DriftDatabaseRepository { )) .get(); - return rows.map((result) => result.readTable(_db.trashedLocalAssetEntity).toDto()); + return rows.map((result) => result.readTable(_db.trashedLocalAssetEntity).toLocalAsset()); } /// Applies resulted snapshot of trashed assets: /// - upserts incoming rows /// - deletes rows that are not present in the snapshot - Future applyTrashSnapshot(Iterable assets, String albumId) async { - if (assets.isEmpty) { + Future applyTrashSnapshot(Iterable trashedAssets) async { + if (trashedAssets.isEmpty) { await _db.delete(_db.trashedLocalAssetEntity).go(); return; } - Map localChecksumById = await _getCachedChecksums(assets); + final assetIds = trashedAssets.map((e) => e.asset.id).toSet(); + Map localChecksumById = await _getCachedChecksums(assetIds); + return _db.transaction(() async { await _db.batch((batch) { - for (final asset in assets) { - final effectiveChecksum = localChecksumById[asset.id] ?? asset.checksum; + for (final item in trashedAssets) { + final effectiveChecksum = localChecksumById[item.asset.id] ?? item.asset.checksum; final companion = TrashedLocalAssetEntityCompanion.insert( - id: asset.id, - albumId: albumId, + id: item.asset.id, + albumId: item.albumId, checksum: effectiveChecksum == null ? const Value.absent() : Value(effectiveChecksum), - name: asset.name, - type: asset.type, - createdAt: Value(asset.createdAt), - updatedAt: Value(asset.updatedAt), - width: Value(asset.width), - height: Value(asset.height), - durationInSeconds: Value(asset.durationInSeconds), - isFavorite: Value(asset.isFavorite), - orientation: Value(asset.orientation), + name: item.asset.name, + type: item.asset.type, + createdAt: Value(item.asset.createdAt), + updatedAt: Value(item.asset.updatedAt), + width: Value(item.asset.width), + height: Value(item.asset.height), + durationInSeconds: Value(item.asset.durationInSeconds), + isFavorite: Value(item.asset.isFavorite), + orientation: Value(item.asset.orientation), ); batch.insert<$TrashedLocalAssetEntityTable, TrashedLocalAssetEntityData>( _db.trashedLocalAssetEntity, companion, - onConflict: DoUpdate((_) => companion, where: (old) => old.updatedAt.isNotValue(asset.updatedAt)), + onConflict: DoUpdate((_) => companion, where: (old) => old.updatedAt.isNotValue(item.asset.updatedAt)), ); } }); - final keepIds = assets.map((asset) => asset.id); - - if (keepIds.length <= 32000) { - await (_db.delete(_db.trashedLocalAssetEntity)..where((row) => row.id.isNotIn(keepIds))).go(); + if (assetIds.length <= 32000) { + await (_db.delete(_db.trashedLocalAssetEntity)..where((row) => row.id.isNotIn(assetIds))).go(); } else { - final keepIdsSet = keepIds.toSet(); final existingIds = await (_db.selectOnly( _db.trashedLocalAssetEntity, )..addColumns([_db.trashedLocalAssetEntity.id])).map((r) => r.read(_db.trashedLocalAssetEntity.id)!).get(); - final idToDelete = existingIds.where((id) => !keepIdsSet.contains(id)); + final idToDelete = existingIds.where((id) => !assetIds.contains(id)); await _db.batch((batch) { for (final id in idToDelete) { batch.deleteWhere(_db.trashedLocalAssetEntity, (row) => row.id.equals(id)); @@ -110,33 +110,33 @@ class DriftTrashedLocalAssetRepository extends DriftDatabaseRepository { }); } - Future saveTrashedAssets(Iterable assets) async { - if (assets.isEmpty) { + Future applyDelta(Iterable trashedAssets) async { + if (trashedAssets.isEmpty) { return; } - - Map localChecksumById = await _getCachedChecksums(assets); + final assetIds = trashedAssets.map((e) => e.asset.id).toSet(); + Map localChecksumById = await _getCachedChecksums(assetIds); await _db.batch((batch) { - for (final asset in assets) { - final effectiveChecksum = localChecksumById[asset.id] ?? asset.checksum; + for (final item in trashedAssets) { + final effectiveChecksum = localChecksumById[item.asset.id] ?? item.asset.checksum; final companion = TrashedLocalAssetEntityCompanion.insert( - id: asset.id, - albumId: asset.albumId, - name: asset.name, - type: asset.type, + id: item.asset.id, + albumId: item.albumId, + name: item.asset.name, + type: item.asset.type, checksum: effectiveChecksum == null ? const Value.absent() : Value(effectiveChecksum), - createdAt: Value(asset.createdAt), - width: Value(asset.width), - height: Value(asset.height), - durationInSeconds: Value(asset.durationInSeconds), - isFavorite: Value(asset.isFavorite), - orientation: Value(asset.orientation), + createdAt: Value(item.asset.createdAt), + width: Value(item.asset.width), + height: Value(item.asset.height), + durationInSeconds: Value(item.asset.durationInSeconds), + isFavorite: Value(item.asset.isFavorite), + orientation: Value(item.asset.orientation), ); batch.insert<$TrashedLocalAssetEntityTable, TrashedLocalAssetEntityData>( _db.trashedLocalAssetEntity, companion, - onConflict: DoUpdate((_) => companion, where: (old) => old.updatedAt.isNotValue(asset.updatedAt)), + onConflict: DoUpdate((_) => companion, where: (old) => old.updatedAt.isNotValue(item.asset.updatedAt)), ); } }); @@ -171,7 +171,7 @@ class DriftTrashedLocalAssetRepository extends DriftDatabaseRepository { TrashedLocalAssetEntityCompanion( id: Value(asset.id), name: Value(asset.name), - albumId: Value(entry.key), + // albumId: Value(entry.key), checksum: asset.checksum == null ? const Value.absent() : Value(asset.checksum), type: Value(asset.type), width: Value(asset.width), @@ -237,17 +237,21 @@ class DriftTrashedLocalAssetRepository extends DriftDatabaseRepository { } //attempt to reuse existing checksums - Future> _getCachedChecksums(Iterable trashUpdates) async { - final ids = trashUpdates.map((e) => e.id).toSet(); - final rows = - await (_db.selectOnly(_db.localAssetEntity) - ..where(_db.localAssetEntity.id.isIn(ids) & _db.localAssetEntity.checksum.isNotNull()) - ..addColumns([_db.localAssetEntity.id, _db.localAssetEntity.checksum])) - .get(); + Future> _getCachedChecksums(Set assetIds) async { + final localChecksumById = {}; + + for (final slice in assetIds.slices(32000)) { + final rows = + await (_db.selectOnly(_db.localAssetEntity) + ..where(_db.localAssetEntity.id.isIn(slice) & _db.localAssetEntity.checksum.isNotNull()) + ..addColumns([_db.localAssetEntity.id, _db.localAssetEntity.checksum])) + .get(); + + for (final r in rows) { + localChecksumById[r.read(_db.localAssetEntity.id)!] = r.read(_db.localAssetEntity.checksum)!; + } + } - final localChecksumById = { - for (final r in rows) r.read(_db.localAssetEntity.id)!: r.read(_db.localAssetEntity.checksum)!, - }; return localChecksumById; } }