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
@@ -162,7 +162,7 @@ class BackgroundUploadService {
await _storageRepository.clearCache();
shouldAbortQueuingTasks = false;
final candidates = await _backupRepository.getCandidates(userId);
final candidates = await _backupRepository.getCandidates(userId, onlyHashed: false);
if (candidates.isEmpty) {
_logger.info("No new backup candidates found, finishing background upload");
return;
@@ -210,6 +210,7 @@ class BackgroundUploadService {
switch (update.status) {
case TaskStatus.complete:
unawaited(_handleLivePhoto(update));
unawaited(_storeServerChecksum(update));
if (CurrentPlatform.isIOS) {
try {
@@ -227,6 +228,20 @@ class BackgroundUploadService {
}
}
Future<void> _storeServerChecksum(TaskStatusUpdate update) async {
try {
if (update.responseBody == null || update.responseBody!.isEmpty) return;
final response = jsonDecode(update.responseBody!);
final checksum = response['checksum'] as String?;
if (checksum == null) return;
final deviceAssetId = update.task.taskId;
if (deviceAssetId.isEmpty) return;
await _localAssetRepository.updateHashes({deviceAssetId: checksum});
} catch (e) {
_logger.warning('Failed to store server checksum: $e');
}
}
Future<void> _handleLivePhoto(TaskStatusUpdate update) async {
try {
if (update.task.metaData.isEmpty || update.task.metaData == '') {