throw on unsupported

This commit is contained in:
shenlong-tanwen 2026-08-13 08:13:47 +05:30
parent ffdd360bd7
commit de28149d7d
8 changed files with 52 additions and 39 deletions

View file

@ -144,7 +144,7 @@ class NativeSyncApiImpl: ImmichPlugin, NativeSyncApi, FlutterPlugin {
private func getMediaChanges() throws -> SyncDelta {
guard #available(iOS 16, *) else {
throw PigeonError(code: "UNSUPPORTED_OS", message: "This feature requires iOS 16 or later.", details: nil)
throw PigeonError(code: kUnSupportedOSError, message: "This feature requires iOS 16 or later.", details: nil)
}
guard PHPhotoLibrary.authorizationStatus(for: .readWrite) == .authorized else {
@ -437,7 +437,7 @@ class NativeSyncApiImpl: ImmichPlugin, NativeSyncApi, FlutterPlugin {
}
func getTrashedAssets() throws -> [String: [PlatformAsset]] {
throw PigeonError(code: "UNSUPPORTED_OS", message: "This feature not supported on iOS.", details: nil)
throw PigeonError(code: kUnSupportedOSError, message: "This feature not supported on iOS.", details: nil)
}
func restoreFromTrashById(mediaId: String, type: Int64, completion: @escaping (Result<Bool, Error>) -> Void) {
@ -477,9 +477,7 @@ class NativeSyncApiImpl: ImmichPlugin, NativeSyncApi, FlutterPlugin {
func getCloudIdForAssetIds(assetIds: [String]) throws -> [CloudIdResult] {
guard #available(iOS 16, *) else {
return assetIds.map {
CloudIdResult(assetId: $0, error: "Cloud identifiers require iOS 16", errorKind: .unsupported)
}
throw PigeonError(code: kUnSupportedOSError, message: "This feature requires iOS 16 or later.", details: nil)
}
var mappings: [CloudIdResult] = []

View file

@ -108,7 +108,7 @@ class LocalSyncService {
await updateAlbum(dbAlbum, album);
}
await _mapIosCloudIds(newAssets);
await _mapCloudIds(newAssets);
}
await _nativeSyncApi.checkpointSync();
} on PlatformException catch (e, s) {
@ -167,7 +167,7 @@ class LocalSyncService {
: <LocalAsset>[];
await _localAlbumRepository.upsert(album, toUpsert: assets);
await _mapIosCloudIds(assets);
await _mapCloudIds(assets);
_log.fine("Successfully added device album ${album.name}");
} catch (e, s) {
_log.warning("Error while adding device album", e, s);
@ -249,7 +249,7 @@ class LocalSyncService {
toUpsert: newAssets,
);
await _mapIosCloudIds(newAssets);
await _mapCloudIds(newAssets);
return true;
} catch (e, s) {
_log.warning("Error on fast syncing local album: ${dbAlbum.name}", e, s);
@ -281,7 +281,7 @@ class LocalSyncService {
if (dbAlbum.assetCount == 0) {
_log.fine("Device album ${deviceAlbum.name} is empty. Adding assets to DB.");
await _localAlbumRepository.upsert(updatedDeviceAlbum, toUpsert: assetsInDevice);
await _mapIosCloudIds(assetsInDevice);
await _mapCloudIds(assetsInDevice);
return true;
}
@ -319,7 +319,7 @@ class LocalSyncService {
}
await _localAlbumRepository.upsert(updatedDeviceAlbum, toUpsert: assetsToUpsert, toDelete: assetsToDelete);
await _mapIosCloudIds(assetsToUpsert);
await _mapCloudIds(assetsToUpsert);
return true;
} catch (e, s) {
@ -328,12 +328,17 @@ class LocalSyncService {
return true;
}
Future<void> _mapIosCloudIds(List<LocalAsset> assets) async {
Future<void> _mapCloudIds(List<LocalAsset> assets) async {
if (!CurrentPlatform.isIOS || assets.isEmpty) {
return;
}
await resolveCloudIds(_nativeSyncApi, _localAlbumRepository, assets.map((a) => a.id).toList());
await resolveCloudIds(
_nativeSyncApi,
_localAlbumRepository,
assets.map((a) => a.id).toList(),
cancellation: _cancellation,
);
}
bool _assetsEqual(LocalAsset a, LocalAsset b) {

View file

@ -1,9 +1,13 @@
import 'dart:async';
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart';
import 'package:immich_mobile/platform/native_sync_api.g.dart';
import 'package:logging/logging.dart';
@visibleForTesting
const kCloudIdChunkSize = 5000;
Future<void> resolveCloudIds(
@ -14,31 +18,36 @@ Future<void> resolveCloudIds(
}) async {
final logger = Logger('resolveCloudIds');
for (int offset = 0; offset < assetIds.length; offset += kCloudIdChunkSize) {
for (final batch in assetIds.slices(kCloudIdChunkSize)) {
if (cancellation?.isCompleted ?? false) {
logger.warning('Cloud ID resolution cancelled after $offset of ${assetIds.length} assets');
logger.warning('Cloud ID resolution cancelled');
return;
}
final end = offset + kCloudIdChunkSize;
final chunk = assetIds.sublist(offset, end > assetIds.length ? assetIds.length : end);
final cloudMapping = <String, String>{};
for (final result in await nativeSyncApi.getCloudIdForAssetIds(chunk)) {
if (result.cloudId != null) {
cloudMapping[result.assetId] = result.cloudId!;
continue;
}
if (result.errorKind == CloudIdErrorKind.unsupported) {
logger.warning('Cloud IDs unavailable: ${result.error ?? "unsupported"}');
final List<CloudIdResult> results;
try {
results = await nativeSyncApi.getCloudIdForAssetIds(batch);
} on PlatformException catch (error, stack) {
if (error.code == kUnSupportedOSError) {
logger.warning('Cloud IDs are unavailable on this device. Skipping resolution.', error, stack);
return;
}
logger.fine(
'Cannot fetch cloudId for asset with id: ${result.assetId}. '
'Reason: ${result.errorKind?.name ?? "unknown"}. Error: ${result.error ?? "unknown"}',
);
logger.warning('Cannot fetch cloudIds for ${batch.length} assets. Skipping batch.', error, stack);
continue;
}
final cloudMapping = <String, String>{};
for (final CloudIdResult(:assetId, :cloudId, :error, :errorKind) in results) {
if (cloudId == null) {
logger.fine(
'Cannot fetch cloudId for asset with id: $assetId. '
'Reason: ${errorKind?.name ?? "unknown"}. Error: ${error ?? "unknown"}',
);
continue;
}
cloudMapping[assetId] = cloudId;
}
await albumRepository.updateCloudMapping(cloudMapping);

View file

@ -135,7 +135,7 @@ class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> {
unawaited(_resumeBackup());
}),
_resumeBackup(),
_safeRun(backgroundManager.syncCloudIds(), "syncCloudIds"),
_safeRun(backgroundManager.syncCloudIds, "syncCloudIds"),
]);
} else {
await _safeRun(backgroundManager.hashAssets, "hashAssets");

View file

@ -11,6 +11,8 @@ import 'package:pigeon/pigeon.dart';
dartPackageName: 'immich_mobile',
),
)
const String kUnSupportedOSError = 'UNSUPPORTED_OS';
enum PlatformAssetPlaybackStyle { unknown, image, video, imageAnimated, livePhoto, videoLooping }
class PlatformAsset {

View file

@ -1362,10 +1362,10 @@ packages:
dependency: "direct dev"
description:
name: pigeon
sha256: "04cfefc8add8b47ddf9ccac8b92bb4edeb67c87f185c623ba0db118ac99334ad"
sha256: f90254ef7b22db026fd8d5fd44718fd60697d50968a144a8bc251e5e95d79e10
url: "https://pub.dev"
source: hosted
version: "26.3.4"
version: "27.3.0"
pinput:
dependency: "direct main"
description:

View file

@ -110,7 +110,7 @@ dev_dependencies:
sdk: flutter
mocktail: ^1.0.5
# Type safe platform code
pigeon: ^26.3.4
pigeon: ^27.3.0
# cast 2.1.0 declares a loose bonsoir range but its code targets the 5.x API.
# Pin bonsoir to 5.x until cast releases a version compatible with bonsoir 6.x.

View file

@ -1,6 +1,7 @@
import 'dart:async';
import 'package:drift/drift.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:immich_mobile/domain/utils/cloud_id_resolver.dart';
import 'package:immich_mobile/domain/utils/migrate_cloud_ids.dart';
@ -84,11 +85,9 @@ void main() {
test('stops after the first chunk when cloud IDs are unsupported', () async {
final ids = List.generate(kCloudIdChunkSize + 1, (i) => 'asset-$i');
when(() => mockNativeSyncApi.getCloudIdForAssetIds(any())).thenAnswer(
(invocation) async => (invocation.positionalArguments.first as List<String>)
.map((id) => CloudIdResult(assetId: id, error: 'needs iOS 16', errorKind: .unsupported))
.toList(),
);
when(
() => mockNativeSyncApi.getCloudIdForAssetIds(any()),
).thenThrow(PlatformException(code: kUnSupportedOSError));
await resolveCloudIds(mockNativeSyncApi, albumRepository, ids);