From 1120caca1099d47338775fce5bff57903b993722 Mon Sep 17 00:00:00 2001 From: Santo Shakil Date: Sat, 9 May 2026 00:33:04 +0600 Subject: [PATCH] fix(mobile): self-heal stale linked album cache on 400 if the server forgets an album that mobile still has cached, every upload hits 400 on addAssets and spams severe forever. catch that 400, drop the cache row, fk cascade nulls the link. next manage pass recreates or re-links by name. --- .../services/sync_linked_album.service.dart | 43 ++++++++++++------- .../drift_album_api_repository.dart | 33 ++++++++++---- 2 files changed, 51 insertions(+), 25 deletions(-) diff --git a/mobile/lib/domain/services/sync_linked_album.service.dart b/mobile/lib/domain/services/sync_linked_album.service.dart index 3bc76083b8..ff60e0ef74 100644 --- a/mobile/lib/domain/services/sync_linked_album.service.dart +++ b/mobile/lib/domain/services/sync_linked_album.service.dart @@ -39,24 +39,35 @@ class SyncLinkedAlbumService { await Future.wait( selectedAlbums.map((localAlbum) async { - final linkedRemoteAlbumId = localAlbum.linkedRemoteAlbumId; - if (linkedRemoteAlbumId == null) { - _log.warning("No linked remote album ID found for local album: ${localAlbum.name}"); - return; - } + try { + final linkedRemoteAlbumId = localAlbum.linkedRemoteAlbumId; + if (linkedRemoteAlbumId == null) { + _log.warning("No linked remote album ID found for local album: ${localAlbum.name}"); + return; + } - final remoteAlbum = await _remoteAlbumRepository.get(linkedRemoteAlbumId); - if (remoteAlbum == null) { - _log.warning("Linked remote album not found for ID: $linkedRemoteAlbumId"); - return; - } + final remoteAlbum = await _remoteAlbumRepository.get(linkedRemoteAlbumId); + if (remoteAlbum == null) { + _log.warning("Linked remote album not found for ID: $linkedRemoteAlbumId"); + return; + } - // get assets that are uploaded but not in the remote album - final assetIds = await _remoteAlbumRepository.getLinkedAssetIds(userId, localAlbum.id, linkedRemoteAlbumId); - _log.fine("Syncing ${assetIds.length} assets to remote album: ${remoteAlbum.name}"); - if (assetIds.isNotEmpty) { - final album = await _albumApiRepository.addAssets(remoteAlbum.id, assetIds); - await _remoteAlbumRepository.addAssets(remoteAlbum.id, album.added); + // get assets that are uploaded but not in the remote album + final assetIds = await _remoteAlbumRepository.getLinkedAssetIds(userId, localAlbum.id, linkedRemoteAlbumId); + _log.fine("Syncing ${assetIds.length} assets to remote album: ${remoteAlbum.name}"); + if (assetIds.isNotEmpty) { + final album = await _albumApiRepository.addAssets(remoteAlbum.id, assetIds); + await _remoteAlbumRepository.addAssets(remoteAlbum.id, album.added); + } + } on RemoteAlbumNotFoundException catch (e) { + // server doesn't have the linked album anymore. drop the cached row; + // KeyAction.setNull on LocalAlbumEntity.linkedRemoteAlbumId nulls + // the link via FK cascade, and the next manageLinkedAlbums run + // will recreate or re-link by name. + _log.warning("Pruning stale linked album for ${localAlbum.name} (server returned 'Album not found' for ${e.albumId})"); + await _remoteAlbumRepository.deleteAlbum(e.albumId); + } catch (error, stack) { + _log.severe("Linked album sync failed for ${localAlbum.name}", error, stack); } }), ); diff --git a/mobile/lib/repositories/drift_album_api_repository.dart b/mobile/lib/repositories/drift_album_api_repository.dart index a0c7a3732a..2ae6e23891 100644 --- a/mobile/lib/repositories/drift_album_api_repository.dart +++ b/mobile/lib/repositories/drift_album_api_repository.dart @@ -42,17 +42,24 @@ class DriftAlbumApiRepository extends ApiRepository { } Future<({List added, List failed})> addAssets(String albumId, Iterable assetIds) async { - final response = await checkNull(_api.addAssetsToAlbum(albumId, BulkIdsDto(ids: assetIds.toList()))); - final List added = [], failed = []; - for (final dto in response) { - if (dto.success) { - added.add(dto.id); - } else { - failed.add(dto.id); + try { + final response = await checkNull(_api.addAssetsToAlbum(albumId, BulkIdsDto(ids: assetIds.toList()))); + final List added = [], failed = []; + for (final dto in response) { + if (dto.success) { + added.add(dto.id); + } else { + failed.add(dto.id); + } } - } - return (added: added, failed: failed); + return (added: added, failed: failed); + } on ApiException catch (e) { + if (e.code == 400 && (e.message?.contains('"message":"Album not found"') ?? false)) { + throw RemoteAlbumNotFoundException(albumId); + } + rethrow; + } } Future updateAlbum( @@ -104,6 +111,14 @@ class DriftAlbumApiRepository extends ApiRepository { } } +class RemoteAlbumNotFoundException implements Exception { + final String albumId; + const RemoteAlbumNotFoundException(this.albumId); + + @override + String toString() => 'RemoteAlbumNotFoundException: $albumId'; +} + extension on AlbumResponseDto { RemoteAlbum toRemoteAlbum(final UserDto user) { return RemoteAlbum(