immich/mobile/lib/domain/services/hash.service.dart
shenlong 84024f6cdc
refactor(mobile): simplify local sync and hash service (#18970)
* Hash service review changes

* local album repo test

* simplify local album repo method names

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2025-06-08 21:56:44 -05:00

121 lines
4.0 KiB
Dart

import 'dart:convert';
import 'package:immich_mobile/constants/constants.dart';
import 'package:immich_mobile/domain/interfaces/local_album.interface.dart';
import 'package:immich_mobile/domain/interfaces/local_asset.interface.dart';
import 'package:immich_mobile/domain/interfaces/storage.interface.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/platform/native_sync_api.g.dart';
import 'package:immich_mobile/presentation/pages/dev/dev_logger.dart';
import 'package:logging/logging.dart';
class HashService {
final int batchSizeLimit;
final int batchFileLimit;
final ILocalAlbumRepository _localAlbumRepository;
final ILocalAssetRepository _localAssetRepository;
final IStorageRepository _storageRepository;
final NativeSyncApi _nativeSyncApi;
final _log = Logger('HashService');
HashService({
required ILocalAlbumRepository localAlbumRepository,
required ILocalAssetRepository localAssetRepository,
required IStorageRepository storageRepository,
required NativeSyncApi nativeSyncApi,
this.batchSizeLimit = kBatchHashSizeLimit,
this.batchFileLimit = kBatchHashFileLimit,
}) : _localAlbumRepository = localAlbumRepository,
_localAssetRepository = localAssetRepository,
_storageRepository = storageRepository,
_nativeSyncApi = nativeSyncApi;
Future<void> hashAssets() async {
final Stopwatch stopwatch = Stopwatch()..start();
// Sorted by backupSelection followed by isCloud
final localAlbums = await _localAlbumRepository.getAll(
sortBy: {
SortLocalAlbumsBy.backupSelection,
SortLocalAlbumsBy.isIosSharedAlbum,
},
);
for (final album in localAlbums) {
final assetsToHash =
await _localAlbumRepository.getAssetsToHash(album.id);
if (assetsToHash.isNotEmpty) {
await _hashAssets(assetsToHash);
}
}
stopwatch.stop();
_log.info("Hashing took - ${stopwatch.elapsedMilliseconds}ms");
DLog.log("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(List<LocalAsset> assetsToHash) async {
int bytesProcessed = 0;
final toHash = <_AssetToPath>[];
for (final asset in assetsToHash) {
final file = await _storageRepository.getFileForAsset(asset);
if (file == null) {
continue;
}
bytesProcessed += await file.length();
toHash.add(_AssetToPath(asset: asset, path: file.path));
if (toHash.length >= batchFileLimit || bytesProcessed >= batchSizeLimit) {
await _processBatch(toHash);
toHash.clear();
bytesProcessed = 0;
}
}
await _processBatch(toHash);
}
/// Processes a batch of assets.
Future<void> _processBatch(List<_AssetToPath> toHash) async {
if (toHash.isEmpty) {
return;
}
_log.fine("Hashing ${toHash.length} files");
final hashed = <LocalAsset>[];
final hashes =
await _nativeSyncApi.hashPaths(toHash.map((e) => e.path).toList());
assert(
hashes.length == toHash.length,
"Hashes length does not match toHash length: ${hashes.length} != ${toHash.length}",
);
for (int i = 0; i < hashes.length; i++) {
final hash = hashes[i];
final asset = toHash[i].asset;
if (hash?.length == 20) {
hashed.add(asset.copyWith(checksum: base64.encode(hash!)));
} else {
_log.warning("Failed to hash file for ${asset.id}");
}
}
_log.fine("Hashed ${hashed.length}/${toHash.length} assets");
DLog.log("Hashed ${hashed.length}/${toHash.length} assets");
await _localAssetRepository.updateHashes(hashed);
}
}
class _AssetToPath {
final LocalAsset asset;
final String path;
const _AssetToPath({required this.asset, required this.path});
}