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>
143 lines
5.3 KiB
Dart
143 lines
5.3 KiB
Dart
import 'package:flutter/services.dart';
|
|
import 'package:immich_mobile/constants/constants.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/extensions/platform_extensions.dart';
|
|
import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart';
|
|
import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart';
|
|
import 'package:immich_mobile/infrastructure/repositories/trashed_local_asset.repository.dart';
|
|
import 'package:immich_mobile/platform/native_sync_api.g.dart';
|
|
import 'package:logging/logging.dart';
|
|
|
|
const String _kHashCancelledCode = "HASH_CANCELLED";
|
|
|
|
class HashService {
|
|
final int _batchSize;
|
|
final DriftLocalAlbumRepository _localAlbumRepository;
|
|
final DriftLocalAssetRepository _localAssetRepository;
|
|
final DriftTrashedLocalAssetRepository _trashedLocalAssetRepository;
|
|
final NativeSyncApi _nativeSyncApi;
|
|
final bool Function()? _cancelChecker;
|
|
final _log = Logger('HashService');
|
|
|
|
HashService({
|
|
required DriftLocalAlbumRepository localAlbumRepository,
|
|
required DriftLocalAssetRepository localAssetRepository,
|
|
required DriftTrashedLocalAssetRepository trashedLocalAssetRepository,
|
|
required NativeSyncApi nativeSyncApi,
|
|
bool Function()? cancelChecker,
|
|
int? batchSize,
|
|
}) : _localAlbumRepository = localAlbumRepository,
|
|
_localAssetRepository = localAssetRepository,
|
|
_trashedLocalAssetRepository = trashedLocalAssetRepository,
|
|
_cancelChecker = cancelChecker,
|
|
_nativeSyncApi = nativeSyncApi,
|
|
_batchSize = batchSize ?? kBatchHashFileLimit;
|
|
|
|
bool get isCancelled => _cancelChecker?.call() ?? false;
|
|
|
|
Future<void> hashAssets() async {
|
|
_log.info("Starting hashing of assets");
|
|
final Stopwatch stopwatch = Stopwatch()..start();
|
|
try {
|
|
// Sorted by backupSelection followed by isCloud
|
|
final localAlbums = await _localAlbumRepository.getBackupAlbums();
|
|
|
|
for (final album in localAlbums) {
|
|
if (isCancelled) {
|
|
_log.warning("Hashing cancelled. Stopped processing albums.");
|
|
break;
|
|
}
|
|
|
|
final assetsToHash = await _localAlbumRepository.getAssetsToHash(album.id);
|
|
if (assetsToHash.isNotEmpty) {
|
|
await _hashAssets(album, assetsToHash);
|
|
}
|
|
}
|
|
if (CurrentPlatform.isAndroid && localAlbums.isNotEmpty) {
|
|
final backupAlbumIds = localAlbums.map((e) => e.id);
|
|
final trashedToHash = await _trashedLocalAssetRepository.getAssetsToHash(backupAlbumIds);
|
|
if (trashedToHash.isNotEmpty) {
|
|
final pseudoAlbum = LocalAlbum(id: '-pseudoAlbum', name: 'Trash', updatedAt: DateTime.now());
|
|
await _hashAssets(pseudoAlbum, trashedToHash, isTrashed: true);
|
|
}
|
|
}
|
|
} on PlatformException catch (e) {
|
|
if (e.code == _kHashCancelledCode) {
|
|
_log.warning("Hashing cancelled by platform");
|
|
return;
|
|
}
|
|
} catch (e, s) {
|
|
_log.severe("Error during hashing", e, s);
|
|
}
|
|
|
|
stopwatch.stop();
|
|
_log.info("Hashing took - ${stopwatch.elapsedMilliseconds}ms");
|
|
}
|
|
|
|
/// Processes a list of [LocalAsset]s, storing their hash and updating the assets in the DB
|
|
/// with hash for those that were successfully hashed. Hashes are looked up in a table
|
|
/// [LocalAssetHashEntity] by local id. Only missing entries are newly hashed and added to the DB.
|
|
Future<void> _hashAssets(LocalAlbum album, List<LocalAsset> assetsToHash, {bool isTrashed = false}) async {
|
|
final toHash = <String, LocalAsset>{};
|
|
|
|
for (final asset in assetsToHash) {
|
|
if (isCancelled) {
|
|
_log.warning("Hashing cancelled. Stopped processing assets.");
|
|
return;
|
|
}
|
|
|
|
toHash[asset.id] = asset;
|
|
if (toHash.length == _batchSize) {
|
|
await _processBatch(album, toHash, isTrashed);
|
|
toHash.clear();
|
|
}
|
|
}
|
|
|
|
await _processBatch(album, toHash, isTrashed);
|
|
}
|
|
|
|
/// Processes a batch of assets.
|
|
Future<void> _processBatch(LocalAlbum album, Map<String, LocalAsset> toHash, bool isTrashed) async {
|
|
if (toHash.isEmpty) {
|
|
return;
|
|
}
|
|
|
|
_log.fine("Hashing ${toHash.length} files");
|
|
|
|
final hashed = <String, String>{};
|
|
final hashResults = await _nativeSyncApi.hashAssets(
|
|
toHash.keys.toList(),
|
|
allowNetworkAccess: album.backupSelection == BackupSelection.selected,
|
|
);
|
|
assert(
|
|
hashResults.length == toHash.length,
|
|
"Hashes length does not match toHash length: ${hashResults.length} != ${toHash.length}",
|
|
);
|
|
|
|
for (int i = 0; i < hashResults.length; i++) {
|
|
if (isCancelled) {
|
|
_log.warning("Hashing cancelled. Stopped processing batch.");
|
|
return;
|
|
}
|
|
|
|
final hashResult = hashResults[i];
|
|
if (hashResult.hash != null) {
|
|
hashed[hashResult.assetId] = hashResult.hash!;
|
|
} else {
|
|
final asset = toHash[hashResult.assetId];
|
|
_log.warning(
|
|
"Failed to hash asset with id: ${hashResult.assetId}, name: ${asset?.name}, createdAt: ${asset?.createdAt}, from album: ${album.name}. Error: ${hashResult.error ?? "unknown"}",
|
|
);
|
|
}
|
|
}
|
|
|
|
_log.fine("Hashed ${hashed.length}/${toHash.length} assets");
|
|
if (isTrashed) {
|
|
await _trashedLocalAssetRepository.updateHashes(hashed);
|
|
} else {
|
|
await _localAssetRepository.updateHashes(hashed);
|
|
}
|
|
}
|
|
}
|