mirror of
https://github.com/immich-app/immich.git
synced 2025-12-12 15:50:43 -08:00
* feature(mobile, beta, Android): handle remote asset trash/restore events and rescan media - Handle move to trash and restore from trash for remote assets on Android - Trigger MediaScannerConnection to rescan affected media files * feature(mobile, beta, Android): fix rescan * fix imports * fix checking conditions * refactor naming * fix line breaks * refactor code rollback changes in BackgroundServicePlugin * refactor code (use separate TrashService) * refactor code * parallelize restoreFromTrash calls with Future.wait format trash.provider.dart * try to re-format trash.provider.dart * re-format trash.provider.dart * rename TrashService to TrashSyncService to avoid duplicated names revert changes in original trash.provider.dart * refactor code (minor nitpicks) * process restoreFromTrash sequentially instead of Future.wait * group local assets by checksum before moving to trash delete LocalAssetEntity records when moved to trash refactor code * fix format * use checksum for asset restoration refactro code * fix format * sync trash only for backup-selected assets * feat(db): add local_trashed_asset table and integrate with restoration flow - Add new `local_trashed_asset` table to store metadata of trashed assets - Save trashed asset info into `local_trashed_asset` before deletion - Use `local_trashed_asset` as source for asset restoration - Implement file restoration by `mediaId` * resolve merge conflicts * fix index creating on migration * rework trashed assets handling - add new table trashed_local_asset - mirror trashed assets data in trashed_local_asset. - compute checksums for assets trashed out-of-app. - restore assets present in trashed_local_asset and non-trashed in remote_asset. - simplify moving-to-trash logic based on remote_asset events. * resolve merge conflicts use updated approach for calculating checksums * use CurrentPlatform instead _platform fix mocks * revert redundant changes * Include trashed items in getMediaChanges Process trashed items delta during incremental sync * fix merge conflicts * fix format * trashed_local_asset table mirror of local_asset table structure trashed_local_asset<->local_asset transfer data on move to trash or restore refactor code * refactor and format code * refactor TrashedAsset model fix missed data transfering * refactor code remove unused model * fix label * fix merge conflicts * optimize, refactor code remove redundant code and checking getTrashedAssetsForAlbum for iOS tests for hash trashed assets * format code * fix migration fix tests * fix generated file * reuse exist checksums on trash data update handle restoration errors fix import * format code * sync_stream.service depend on repos refactor assets restoration update dependencies in tests * remove trashed asset model remove trash_sync.service refactor DriftTrashedLocalAssetRepository, LocalSyncService * rework fetching trashed assets data on native side optimize handling trashed assets in local sync service refactor code * update NativeSyncApi on iOS side remove unused code * optimize sync trashed assets call in full sync mode refactor code * fix format * remove albumIds from getTrashedAssets params fix upsert in trashed local asset repo refactor code * fix getTrashedAssets params * fix(trash-sync): clean up NativeSyncApiImplBase and correct applyDelta * refactor(trash-sync): optimize performance and fix minor issues * refactor(trash-sync): add missed index * feat(trash-sync): remove sinceLastCheckpoint param from getTrashedAssets * fix(trash-sync): fix target table * fix(trash-sync): remove unused extension * fix(trash-sync): remove unused code * fix(trash-sync): refactor code * fix(trash-sync): reformat file * fix(trash_sync): refactor code * fix(trash_sync): improve moving to trash * refactor(trash_sync): integrate MANAGE_MEDIA permission request into login flow and advanced settings * refactor(trash_sync): add additional checking for experimental trash sync flag and MANAGE_MEDIA permission. * refactor(trash_sync): resolve merge conflicts * refactor(trash_sync): fix format * resolve merge conflicts add await for alert dialog add missed request * refactor(trash_sync): rework MANAGE_MEDIA info widget show rationale text in permission request alert dialog refactor setting getter * fix(trash_sync): restore missing text values * fix(trash_sync): format file * fix(trash_sync): check backup enabled and remove remote asset existence check * fix(trash_sync): remove checking backup enabled test(trash_sync): cover sync-stream trash/restore paths and dedupe mocks * test(trash_sync): cover trash/restore flows for local_sync_service * chore(e2e): restore test-assets submodule pointer --------- Co-authored-by: Peter Ombodi <peter.ombodi@gmail.com> Co-authored-by: Alex <alex.tran1502@gmail.com>
262 lines
8.4 KiB
Dart
262 lines
8.4 KiB
Dart
import 'package:drift/drift.dart';
|
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
|
import 'package:immich_mobile/domain/models/exif.model.dart';
|
|
import 'package:immich_mobile/domain/models/stack.model.dart';
|
|
import 'package:immich_mobile/infrastructure/entities/exif.entity.dart' hide ExifInfo;
|
|
import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart';
|
|
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart';
|
|
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart';
|
|
import 'package:immich_mobile/infrastructure/entities/stack.entity.drift.dart';
|
|
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
|
import 'package:maplibre_gl/maplibre_gl.dart';
|
|
|
|
class RemoteAssetRepository extends DriftDatabaseRepository {
|
|
final Drift _db;
|
|
|
|
const RemoteAssetRepository(this._db) : super(_db);
|
|
|
|
/// For testing purposes
|
|
Future<List<RemoteAsset>> getSome(String userId) {
|
|
final query = _db.remoteAssetEntity.select()
|
|
..where(
|
|
(row) =>
|
|
_db.remoteAssetEntity.ownerId.equals(userId) &
|
|
_db.remoteAssetEntity.deletedAt.isNull() &
|
|
_db.remoteAssetEntity.visibility.equalsValue(AssetVisibility.timeline),
|
|
)
|
|
..orderBy([(row) => OrderingTerm.desc(row.createdAt)])
|
|
..limit(10);
|
|
|
|
return query.map((row) => row.toDto()).get();
|
|
}
|
|
|
|
SingleOrNullSelectable<RemoteAsset?> _assetSelectable(String id) {
|
|
final query =
|
|
_db.remoteAssetEntity.select().addColumns([_db.localAssetEntity.id]).join([
|
|
leftOuterJoin(
|
|
_db.localAssetEntity,
|
|
_db.remoteAssetEntity.checksum.equalsExp(_db.localAssetEntity.checksum),
|
|
useColumns: false,
|
|
),
|
|
])
|
|
..where(_db.remoteAssetEntity.id.equals(id))
|
|
..limit(1);
|
|
|
|
return query.map((row) {
|
|
final asset = row.readTable(_db.remoteAssetEntity).toDto();
|
|
return asset.copyWith(localId: row.read(_db.localAssetEntity.id));
|
|
});
|
|
}
|
|
|
|
Stream<RemoteAsset?> watch(String id) {
|
|
return _assetSelectable(id).watchSingleOrNull();
|
|
}
|
|
|
|
Future<RemoteAsset?> get(String id) {
|
|
return _assetSelectable(id).getSingleOrNull();
|
|
}
|
|
|
|
Future<RemoteAsset?> getByChecksum(String checksum) {
|
|
final query = _db.remoteAssetEntity.select()..where((row) => row.checksum.equals(checksum));
|
|
|
|
return query.map((row) => row.toDto()).getSingleOrNull();
|
|
}
|
|
|
|
Future<List<RemoteAsset>> getStackChildren(RemoteAsset asset) {
|
|
final stackId = asset.stackId;
|
|
if (stackId == null) {
|
|
return Future.value(const []);
|
|
}
|
|
|
|
final query = _db.remoteAssetEntity.select()
|
|
..where((row) => row.stackId.equals(stackId) & row.id.equals(asset.id).not())
|
|
..orderBy([(row) => OrderingTerm.desc(row.createdAt)]);
|
|
|
|
return query.map((row) => row.toDto()).get();
|
|
}
|
|
|
|
Future<ExifInfo?> getExif(String id) {
|
|
return _db.managers.remoteExifEntity
|
|
.filter((row) => row.assetId.id.equals(id))
|
|
.map((row) => row.toDto())
|
|
.getSingleOrNull();
|
|
}
|
|
|
|
Future<List<(String, String)>> getPlaces(String userId) {
|
|
final asset = Subquery(
|
|
_db.remoteAssetEntity.select()
|
|
..where((row) => row.ownerId.equals(userId))
|
|
..orderBy([(row) => OrderingTerm.desc(row.createdAt)]),
|
|
"asset",
|
|
);
|
|
|
|
final query =
|
|
asset.selectOnly().join([
|
|
innerJoin(
|
|
_db.remoteExifEntity,
|
|
_db.remoteExifEntity.assetId.equalsExp(asset.ref(_db.remoteAssetEntity.id)),
|
|
useColumns: false,
|
|
),
|
|
])
|
|
..addColumns([_db.remoteExifEntity.city, _db.remoteExifEntity.assetId])
|
|
..where(
|
|
_db.remoteExifEntity.city.isNotNull() &
|
|
asset.ref(_db.remoteAssetEntity.deletedAt).isNull() &
|
|
asset.ref(_db.remoteAssetEntity.visibility).equals(AssetVisibility.timeline.index),
|
|
)
|
|
..groupBy([_db.remoteExifEntity.city])
|
|
..orderBy([OrderingTerm.asc(_db.remoteExifEntity.city)]);
|
|
|
|
return query.map((row) {
|
|
final assetId = row.read(_db.remoteExifEntity.assetId);
|
|
final city = row.read(_db.remoteExifEntity.city);
|
|
return (city!, assetId!);
|
|
}).get();
|
|
}
|
|
|
|
Future<void> updateFavorite(List<String> ids, bool isFavorite) {
|
|
return _db.batch((batch) async {
|
|
for (final id in ids) {
|
|
batch.update(
|
|
_db.remoteAssetEntity,
|
|
RemoteAssetEntityCompanion(isFavorite: Value(isFavorite)),
|
|
where: (e) => e.id.equals(id),
|
|
);
|
|
}
|
|
});
|
|
}
|
|
|
|
Future<void> updateVisibility(List<String> ids, AssetVisibility visibility) {
|
|
return _db.batch((batch) async {
|
|
for (final id in ids) {
|
|
batch.update(
|
|
_db.remoteAssetEntity,
|
|
RemoteAssetEntityCompanion(visibility: Value(visibility)),
|
|
where: (e) => e.id.equals(id),
|
|
);
|
|
}
|
|
});
|
|
}
|
|
|
|
Future<void> trash(List<String> ids) {
|
|
return _db.batch((batch) async {
|
|
for (final id in ids) {
|
|
batch.update(
|
|
_db.remoteAssetEntity,
|
|
RemoteAssetEntityCompanion(deletedAt: Value(DateTime.now())),
|
|
where: (e) => e.id.equals(id),
|
|
);
|
|
}
|
|
});
|
|
}
|
|
|
|
Future<void> restoreTrash(List<String> ids) {
|
|
return _db.batch((batch) async {
|
|
for (final id in ids) {
|
|
batch.update(
|
|
_db.remoteAssetEntity,
|
|
const RemoteAssetEntityCompanion(deletedAt: Value(null)),
|
|
where: (e) => e.id.equals(id),
|
|
);
|
|
}
|
|
});
|
|
}
|
|
|
|
Future<void> delete(List<String> ids) {
|
|
return _db.batch((batch) {
|
|
for (final id in ids) {
|
|
batch.deleteWhere(_db.remoteAssetEntity, (row) => row.id.equals(id));
|
|
}
|
|
});
|
|
}
|
|
|
|
Future<void> updateLocation(List<String> ids, LatLng location) {
|
|
return _db.batch((batch) async {
|
|
for (final id in ids) {
|
|
batch.update(
|
|
_db.remoteExifEntity,
|
|
RemoteExifEntityCompanion(latitude: Value(location.latitude), longitude: Value(location.longitude)),
|
|
where: (e) => e.assetId.equals(id),
|
|
);
|
|
}
|
|
});
|
|
}
|
|
|
|
Future<void> updateDateTime(List<String> ids, DateTime dateTime) {
|
|
return _db.batch((batch) async {
|
|
for (final id in ids) {
|
|
batch.update(
|
|
_db.remoteExifEntity,
|
|
RemoteExifEntityCompanion(dateTimeOriginal: Value(dateTime)),
|
|
where: (e) => e.assetId.equals(id),
|
|
);
|
|
batch.update(
|
|
_db.remoteAssetEntity,
|
|
RemoteAssetEntityCompanion(createdAt: Value(dateTime)),
|
|
where: (e) => e.id.equals(id),
|
|
);
|
|
}
|
|
});
|
|
}
|
|
|
|
Future<void> stack(String userId, StackResponse stack) {
|
|
return _db.transaction(() async {
|
|
final stackIds = await _db.managers.stackEntity
|
|
.filter((row) => row.primaryAssetId.isIn(stack.assetIds))
|
|
.map((row) => row.id)
|
|
.get();
|
|
|
|
await _db.batch((batch) {
|
|
for (final stackId in stackIds) {
|
|
batch.deleteWhere(_db.stackEntity, (row) => row.id.equals(stackId));
|
|
}
|
|
});
|
|
|
|
await _db.batch((batch) {
|
|
final companion = StackEntityCompanion(ownerId: Value(userId), primaryAssetId: Value(stack.primaryAssetId));
|
|
|
|
batch.insert(_db.stackEntity, companion.copyWith(id: Value(stack.id)), onConflict: DoUpdate((_) => companion));
|
|
|
|
for (final assetId in stack.assetIds) {
|
|
batch.update(
|
|
_db.remoteAssetEntity,
|
|
RemoteAssetEntityCompanion(stackId: Value(stack.id)),
|
|
where: (e) => e.id.equals(assetId),
|
|
);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
Future<void> unStack(List<String> stackIds) {
|
|
return _db.transaction(() async {
|
|
await _db.batch((batch) {
|
|
for (final stackId in stackIds) {
|
|
batch.deleteWhere(_db.stackEntity, (row) => row.id.equals(stackId));
|
|
}
|
|
});
|
|
|
|
// TODO: delete this after adding foreign key on stackId
|
|
await _db.batch((batch) {
|
|
for (final stackId in stackIds) {
|
|
batch.update(
|
|
_db.remoteAssetEntity,
|
|
const RemoteAssetEntityCompanion(stackId: Value(null)),
|
|
where: (e) => e.stackId.equals(stackId),
|
|
);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
Future<void> updateDescription(String assetId, String description) async {
|
|
await (_db.remoteExifEntity.update()..where((row) => row.assetId.equals(assetId))).write(
|
|
RemoteExifEntityCompanion(description: Value(description)),
|
|
);
|
|
}
|
|
|
|
Future<int> getCount() {
|
|
return _db.managers.remoteAssetEntity.count();
|
|
}
|
|
}
|