feat: skip local hashing for iCloud assets, use server-computed checksum

Instead of downloading iCloud assets twice (once to hash, once to upload),
skip local hashing for iCloud-only assets and let the server return its
computed SHA1 checksum in the upload response. The mobile app stores this
checksum locally to prevent re-uploads.

Changes:
- Server returns checksum in upload response (created + duplicate)
- iOS native layer tags iCloud-only assets with ICLOUD_ONLY error
- Hash service skips iCloud assets (allowNetworkAccess: false)
- Upload result carries server checksum back to mobile
- Foreground/background upload services store server checksum
- Backup candidates include unhashed assets (onlyHashed: false)

https://claude.ai/code/session_01LEs74WpkJ1gWcJrFBsp1i2
This commit is contained in:
Claude
2026-03-17 18:52:32 +00:00
parent 34caed3b2b
commit 1811d42d79
7 changed files with 51 additions and 8 deletions
@@ -11,9 +11,11 @@ import 'package:immich_mobile/extensions/platform_extensions.dart';
import 'package:immich_mobile/extensions/network_capability_extensions.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/infrastructure/repositories/backup.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart';
import 'package:immich_mobile/platform/connectivity_api.g.dart';
import 'package:immich_mobile/providers/app_settings.provider.dart';
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
import 'package:immich_mobile/providers/infrastructure/storage.provider.dart';
import 'package:immich_mobile/repositories/asset_media.repository.dart';
@@ -37,6 +39,7 @@ final foregroundUploadServiceProvider = Provider((ref) {
return ForegroundUploadService(
ref.watch(uploadRepositoryProvider),
ref.watch(storageRepositoryProvider),
ref.watch(localAssetRepository),
ref.watch(backupRepositoryProvider),
ref.watch(connectivityApiProvider),
ref.watch(appSettingsServiceProvider),
@@ -53,6 +56,7 @@ class ForegroundUploadService {
ForegroundUploadService(
this._uploadRepository,
this._storageRepository,
this._localAssetRepository,
this._backupRepository,
this._connectivityApi,
this._appSettingsService,
@@ -61,6 +65,7 @@ class ForegroundUploadService {
final UploadRepository _uploadRepository;
final StorageRepository _storageRepository;
final DriftLocalAssetRepository _localAssetRepository;
final DriftBackupRepository _backupRepository;
final ConnectivityApi _connectivityApi;
final AppSettingsService _appSettingsService;
@@ -84,7 +89,7 @@ class ForegroundUploadService {
UploadCallbacks callbacks = const UploadCallbacks(),
bool useSequentialUpload = false,
}) async {
final candidates = await _backupRepository.getCandidates(userId);
final candidates = await _backupRepository.getCandidates(userId, onlyHashed: false);
if (candidates.isEmpty) {
return;
}
@@ -387,6 +392,10 @@ class ForegroundUploadService {
);
if (result.isSuccess && result.remoteAssetId != null) {
// Store server-computed checksum so iCloud-only assets don't need re-upload
if (result.checksum != null) {
await _localAssetRepository.updateHashes({asset.id: result.checksum!});
}
callbacks.onSuccess?.call(asset.localId!, result.remoteAssetId!);
} else if (result.isCancelled) {
_logger.warning(() => "Backup was cancelled by the user");