2025-09-18 13:55:56 +03:00
|
|
|
import 'package:collection/collection.dart';
|
|
|
|
|
import 'package:drift/drift.dart';
|
|
|
|
|
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
2025-09-24 16:58:56 +03:00
|
|
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
2025-09-18 13:55:56 +03:00
|
|
|
import 'package:immich_mobile/domain/models/asset/trashed_asset.model.dart';
|
2025-09-24 16:58:56 +03:00
|
|
|
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart';
|
2025-09-18 13:55:56 +03:00
|
|
|
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';
|
2025-09-24 16:58:56 +03:00
|
|
|
import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart';
|
2025-09-18 13:55:56 +03:00
|
|
|
|
|
|
|
|
class DriftTrashedLocalAssetRepository extends DriftDatabaseRepository {
|
|
|
|
|
final Drift _db;
|
|
|
|
|
|
|
|
|
|
const DriftTrashedLocalAssetRepository(this._db) : super(_db);
|
|
|
|
|
|
2025-10-06 11:41:34 +03:00
|
|
|
Future<void> updateHashes(Map<String, String> hashes) {
|
|
|
|
|
if (hashes.isEmpty) {
|
2025-09-18 13:55:56 +03:00
|
|
|
return Future.value();
|
|
|
|
|
}
|
2025-09-19 17:55:20 +03:00
|
|
|
final now = DateTime.now();
|
2025-09-18 13:55:56 +03:00
|
|
|
return _db.batch((batch) async {
|
2025-10-06 11:41:34 +03:00
|
|
|
for (final entry in hashes.entries) {
|
2025-09-18 13:55:56 +03:00
|
|
|
batch.update(
|
|
|
|
|
_db.trashedLocalAssetEntity,
|
2025-10-06 11:41:34 +03:00
|
|
|
TrashedLocalAssetEntityCompanion(checksum: Value(entry.value), updatedAt: Value(now)),
|
|
|
|
|
where: (e) => e.id.equals(entry.key),
|
2025-09-18 13:55:56 +03:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-06 11:41:34 +03:00
|
|
|
Future<Iterable<TrashedAsset>> getAssetsToHash(Iterable<String> albumIds) {
|
2025-09-25 13:11:14 +03:00
|
|
|
final query = _db.trashedLocalAssetEntity.select()..where((r) => r.albumId.isIn(albumIds) & r.checksum.isNull());
|
2025-09-24 16:58:56 +03:00
|
|
|
return query.map((row) => row.toDto()).get();
|
2025-09-18 13:55:56 +03:00
|
|
|
}
|
|
|
|
|
|
2025-09-24 16:58:56 +03:00
|
|
|
Future<Iterable<TrashedAsset>> getToRestore() async {
|
2025-10-06 11:41:34 +03:00
|
|
|
final selectedAlbumIds = (_db.selectOnly(_db.localAlbumEntity)
|
|
|
|
|
..addColumns([_db.localAlbumEntity.id])
|
|
|
|
|
..where(_db.localAlbumEntity.backupSelection.equalsValue(BackupSelection.selected)));
|
|
|
|
|
|
|
|
|
|
final rows =
|
|
|
|
|
await (_db.select(_db.trashedLocalAssetEntity).join([
|
|
|
|
|
innerJoin(
|
|
|
|
|
_db.remoteAssetEntity,
|
|
|
|
|
_db.remoteAssetEntity.checksum.equalsExp(_db.trashedLocalAssetEntity.checksum),
|
|
|
|
|
),
|
|
|
|
|
])..where(
|
|
|
|
|
_db.trashedLocalAssetEntity.albumId.isInQuery(selectedAlbumIds) &
|
|
|
|
|
_db.remoteAssetEntity.deletedAt.isNull(),
|
|
|
|
|
))
|
|
|
|
|
.get();
|
|
|
|
|
|
|
|
|
|
return rows.map((result) => result.readTable(_db.trashedLocalAssetEntity).toDto());
|
2025-09-18 13:55:56 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Applies resulted snapshot of trashed assets:
|
|
|
|
|
/// - upserts incoming rows
|
|
|
|
|
/// - deletes rows that are not present in the snapshot
|
|
|
|
|
Future<void> applyTrashSnapshot(Iterable<TrashedAsset> assets, String albumId) async {
|
|
|
|
|
if (assets.isEmpty) {
|
|
|
|
|
await _db.delete(_db.trashedLocalAssetEntity).go();
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-10-06 18:28:31 +03:00
|
|
|
Map<String, String> localChecksumById = await _getCachedChecksums(assets);
|
2025-09-18 13:55:56 +03:00
|
|
|
return _db.transaction(() async {
|
2025-10-06 11:41:34 +03:00
|
|
|
await _db.batch((batch) {
|
|
|
|
|
for (final asset in assets) {
|
2025-10-06 18:28:31 +03:00
|
|
|
final effectiveChecksum = localChecksumById[asset.id] ?? asset.checksum;
|
2025-10-06 11:41:34 +03:00
|
|
|
final companion = TrashedLocalAssetEntityCompanion.insert(
|
|
|
|
|
id: asset.id,
|
|
|
|
|
albumId: albumId,
|
2025-10-06 18:28:31 +03:00
|
|
|
checksum: effectiveChecksum == null ? const Value.absent() : Value(effectiveChecksum),
|
2025-10-06 11:41:34 +03:00
|
|
|
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),
|
|
|
|
|
);
|
2025-09-18 13:55:56 +03:00
|
|
|
|
2025-10-06 11:41:34 +03:00
|
|
|
batch.insert<$TrashedLocalAssetEntityTable, TrashedLocalAssetEntityData>(
|
|
|
|
|
_db.trashedLocalAssetEntity,
|
|
|
|
|
companion,
|
|
|
|
|
onConflict: DoUpdate((_) => companion, where: (old) => old.updatedAt.isNotValue(asset.updatedAt)),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
});
|
2025-09-18 13:55:56 +03:00
|
|
|
|
|
|
|
|
final keepIds = assets.map((asset) => asset.id);
|
2025-10-06 11:41:34 +03:00
|
|
|
|
|
|
|
|
if (keepIds.length <= 32000) {
|
|
|
|
|
await (_db.delete(_db.trashedLocalAssetEntity)..where((row) => row.id.isNotIn(keepIds))).go();
|
2025-09-18 13:55:56 +03:00
|
|
|
} else {
|
2025-10-06 11:41:34 +03:00
|
|
|
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));
|
|
|
|
|
await _db.batch((batch) {
|
|
|
|
|
for (final id in idToDelete) {
|
|
|
|
|
batch.deleteWhere(_db.trashedLocalAssetEntity, (row) => row.id.equals(id));
|
|
|
|
|
}
|
|
|
|
|
});
|
2025-09-18 13:55:56 +03:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-06 18:28:31 +03:00
|
|
|
Future<void> saveTrashedAssets(Iterable<TrashedAsset> assets) async {
|
|
|
|
|
if (assets.isEmpty) {
|
2025-09-19 17:55:20 +03:00
|
|
|
return;
|
|
|
|
|
}
|
2025-10-06 11:41:34 +03:00
|
|
|
|
2025-10-06 18:28:31 +03:00
|
|
|
Map<String, String> localChecksumById = await _getCachedChecksums(assets);
|
|
|
|
|
|
2025-10-06 11:41:34 +03:00
|
|
|
await _db.batch((batch) {
|
2025-10-06 18:28:31 +03:00
|
|
|
for (final asset in assets) {
|
|
|
|
|
final effectiveChecksum = localChecksumById[asset.id] ?? asset.checksum;
|
2025-10-06 11:41:34 +03:00
|
|
|
final companion = TrashedLocalAssetEntityCompanion.insert(
|
|
|
|
|
id: asset.id,
|
|
|
|
|
albumId: asset.albumId,
|
|
|
|
|
name: asset.name,
|
|
|
|
|
type: asset.type,
|
2025-10-06 18:28:31 +03:00
|
|
|
checksum: effectiveChecksum == null ? const Value.absent() : Value(effectiveChecksum),
|
2025-10-06 11:41:34 +03:00
|
|
|
createdAt: Value(asset.createdAt),
|
|
|
|
|
width: Value(asset.width),
|
|
|
|
|
height: Value(asset.height),
|
|
|
|
|
durationInSeconds: Value(asset.durationInSeconds),
|
|
|
|
|
isFavorite: Value(asset.isFavorite),
|
|
|
|
|
orientation: Value(asset.orientation),
|
|
|
|
|
);
|
|
|
|
|
batch.insert<$TrashedLocalAssetEntityTable, TrashedLocalAssetEntityData>(
|
|
|
|
|
_db.trashedLocalAssetEntity,
|
|
|
|
|
companion,
|
|
|
|
|
onConflict: DoUpdate((_) => companion, where: (old) => old.updatedAt.isNotValue(asset.updatedAt)),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
});
|
2025-09-19 17:55:20 +03:00
|
|
|
}
|
|
|
|
|
|
2025-09-18 13:55:56 +03:00
|
|
|
Stream<int> watchCount() {
|
2025-10-06 11:41:34 +03:00
|
|
|
return (_db.selectOnly(_db.trashedLocalAssetEntity)..addColumns([_db.trashedLocalAssetEntity.id.count()]))
|
|
|
|
|
.watchSingle()
|
|
|
|
|
.map((row) => row.read<int>(_db.trashedLocalAssetEntity.id.count()) ?? 0);
|
2025-09-18 13:55:56 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Stream<int> watchHashedCount() {
|
2025-10-06 11:41:34 +03:00
|
|
|
return (_db.selectOnly(_db.trashedLocalAssetEntity)
|
|
|
|
|
..addColumns([_db.trashedLocalAssetEntity.id.count()])
|
|
|
|
|
..where(_db.trashedLocalAssetEntity.checksum.isNotNull()))
|
2025-09-18 13:55:56 +03:00
|
|
|
.watchSingle()
|
2025-10-06 11:41:34 +03:00
|
|
|
.map((row) => row.read<int>(_db.trashedLocalAssetEntity.id.count()) ?? 0);
|
2025-09-18 13:55:56 +03:00
|
|
|
}
|
|
|
|
|
|
2025-09-24 16:58:56 +03:00
|
|
|
Future<void> trashLocalAsset(Map<AlbumId, List<LocalAsset>> assetsByAlbums) async {
|
|
|
|
|
if (assetsByAlbums.isEmpty) {
|
|
|
|
|
return;
|
2025-09-18 13:55:56 +03:00
|
|
|
}
|
2025-09-24 16:58:56 +03:00
|
|
|
|
|
|
|
|
final companions = <TrashedLocalAssetEntityCompanion>[];
|
|
|
|
|
final idToDelete = <String>{};
|
|
|
|
|
|
2025-10-06 11:41:34 +03:00
|
|
|
for (final entry in assetsByAlbums.entries) {
|
|
|
|
|
for (final asset in entry.value) {
|
2025-09-24 16:58:56 +03:00
|
|
|
idToDelete.add(asset.id);
|
|
|
|
|
companions.add(
|
|
|
|
|
TrashedLocalAssetEntityCompanion(
|
|
|
|
|
id: Value(asset.id),
|
|
|
|
|
name: Value(asset.name),
|
2025-10-06 11:41:34 +03:00
|
|
|
albumId: Value(entry.key),
|
2025-09-24 16:58:56 +03:00
|
|
|
checksum: asset.checksum == null ? const Value.absent() : Value(asset.checksum),
|
|
|
|
|
type: Value(asset.type),
|
|
|
|
|
width: Value(asset.width),
|
|
|
|
|
height: Value(asset.height),
|
|
|
|
|
durationInSeconds: Value(asset.durationInSeconds),
|
|
|
|
|
isFavorite: Value(asset.isFavorite),
|
|
|
|
|
orientation: Value(asset.orientation),
|
2025-10-06 18:28:31 +03:00
|
|
|
createdAt: Value(asset.createdAt),
|
|
|
|
|
updatedAt: Value(asset.updatedAt),
|
2025-09-24 16:58:56 +03:00
|
|
|
),
|
|
|
|
|
);
|
2025-09-18 13:55:56 +03:00
|
|
|
}
|
2025-10-06 11:41:34 +03:00
|
|
|
}
|
2025-09-24 16:58:56 +03:00
|
|
|
|
|
|
|
|
await _db.transaction(() async {
|
2025-10-06 11:41:34 +03:00
|
|
|
await _db.batch((batch) {
|
|
|
|
|
for (final slice in companions.slices(32000)) {
|
2025-09-24 16:58:56 +03:00
|
|
|
batch.insertAllOnConflictUpdate(_db.trashedLocalAssetEntity, slice);
|
2025-10-06 11:41:34 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (final id in idToDelete) {
|
|
|
|
|
batch.deleteWhere(_db.localAssetEntity, (row) => row.id.equals(id));
|
|
|
|
|
}
|
|
|
|
|
});
|
2025-09-24 16:58:56 +03:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-07 14:18:45 +03:00
|
|
|
Future<void> applyRestoredAssets(Iterable<String> ids) async {
|
2025-09-24 16:58:56 +03:00
|
|
|
if (ids.isEmpty) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
final trashedAssets = await (_db.select(_db.trashedLocalAssetEntity)..where((tbl) => tbl.id.isIn(ids))).get();
|
|
|
|
|
|
|
|
|
|
if (trashedAssets.isEmpty) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
final localAssets = trashedAssets.map((e) {
|
|
|
|
|
return LocalAssetEntityCompanion.insert(
|
|
|
|
|
id: e.id,
|
|
|
|
|
name: e.name,
|
|
|
|
|
type: e.type,
|
|
|
|
|
createdAt: Value(e.createdAt),
|
|
|
|
|
updatedAt: Value(e.updatedAt),
|
|
|
|
|
width: Value(e.width),
|
|
|
|
|
height: Value(e.height),
|
|
|
|
|
durationInSeconds: Value(e.durationInSeconds),
|
|
|
|
|
checksum: Value(e.checksum),
|
|
|
|
|
isFavorite: Value(e.isFavorite),
|
|
|
|
|
orientation: Value(e.orientation),
|
|
|
|
|
);
|
|
|
|
|
}).toList();
|
|
|
|
|
|
|
|
|
|
await _db.transaction(() async {
|
|
|
|
|
await _db.batch((batch) {
|
|
|
|
|
batch.insertAllOnConflictUpdate(_db.localAssetEntity, localAssets);
|
2025-10-06 11:41:34 +03:00
|
|
|
for (final id in ids) {
|
|
|
|
|
batch.deleteWhere(_db.trashedLocalAssetEntity, (row) => row.id.equals(id));
|
2025-09-24 16:58:56 +03:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
2025-09-18 13:55:56 +03:00
|
|
|
}
|
2025-10-06 18:28:31 +03:00
|
|
|
|
|
|
|
|
//attempt to reuse existing checksums
|
|
|
|
|
Future<Map<String, String>> _getCachedChecksums(Iterable<TrashedAsset> trashUpdates) async {
|
|
|
|
|
final ids = trashUpdates.map((e) => e.id).toSet();
|
2025-10-06 18:58:02 +03:00
|
|
|
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();
|
2025-10-06 18:28:31 +03:00
|
|
|
|
|
|
|
|
final localChecksumById = {
|
2025-10-06 18:58:02 +03:00
|
|
|
for (final r in rows) r.read(_db.localAssetEntity.id)!: r.read(_db.localAssetEntity.checksum)!,
|
2025-10-06 18:28:31 +03:00
|
|
|
};
|
|
|
|
|
return localChecksumById;
|
|
|
|
|
}
|
2025-09-18 13:55:56 +03:00
|
|
|
}
|