diff --git a/mobile/lib/domain/utils/migrate_cloud_ids.dart b/mobile/lib/domain/utils/migrate_cloud_ids.dart index 1e7fe64021..5a008b073f 100644 --- a/mobile/lib/domain/utils/migrate_cloud_ids.dart +++ b/mobile/lib/domain/utils/migrate_cloud_ids.dart @@ -84,6 +84,7 @@ Future> _fetchCloudIdMappings(Drift drift, String userId) useColumns: false, ), ])..where( + // Only select assets that have a local cloud ID but either no remote cloud ID or a mismatched eTag drift.localAssetEntity.id.isNotNull() & drift.localAssetEntity.iCloudId.isNotNull() & drift.remoteAssetEntity.ownerId.equals(userId) & diff --git a/mobile/lib/models/server_info/server_version.model.dart b/mobile/lib/models/server_info/server_version.model.dart index 3aea98a80d..c8bf73db81 100644 --- a/mobile/lib/models/server_info/server_version.model.dart +++ b/mobile/lib/models/server_info/server_version.model.dart @@ -10,4 +10,8 @@ class ServerVersion extends SemVer { } ServerVersion.fromDto(ServerVersionResponseDto dto) : super(major: dto.major, minor: dto.minor, patch: dto.patch_); + + bool isAtLeast({int major = 0, int minor = 0, int patch = 0}) { + return this >= SemVer(major: major, minor: minor, patch: patch); + } } diff --git a/mobile/lib/pages/common/splash_screen.page.dart b/mobile/lib/pages/common/splash_screen.page.dart index 79db33104d..39c05f0be6 100644 --- a/mobile/lib/pages/common/splash_screen.page.dart +++ b/mobile/lib/pages/common/splash_screen.page.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/entities/store.entity.dart'; +import 'package:immich_mobile/extensions/platform_extensions.dart'; import 'package:immich_mobile/providers/auth.provider.dart'; import 'package:immich_mobile/providers/background_sync.provider.dart'; import 'package:immich_mobile/providers/backup/backup.provider.dart'; @@ -60,7 +61,8 @@ class SplashScreenPageState extends ConsumerState { (_) async { try { wsProvider.connect(); - unawaited(infoProvider.getServerInfo()); + await infoProvider.getServerInfo(); + final serverInfo = ref.read(serverInfoProvider); if (Store.isBetaTimelineEnabled) { bool syncSuccess = false; @@ -75,6 +77,9 @@ class SplashScreenPageState extends ConsumerState { _resumeBackup(backupProvider); }), _resumeBackup(backupProvider), + // Sync cloud IDs if server version is compatible + if (CurrentPlatform.isIOS && serverInfo.serverVersion.isAtLeast(major: 2, minor: 4)) + backgroundManager.syncCloudIds(), ]); } else { await backgroundManager.hashAssets(); diff --git a/mobile/lib/providers/app_life_cycle.provider.dart b/mobile/lib/providers/app_life_cycle.provider.dart index 4b1bf3e809..81d7c1d9ea 100644 --- a/mobile/lib/providers/app_life_cycle.provider.dart +++ b/mobile/lib/providers/app_life_cycle.provider.dart @@ -147,6 +147,7 @@ class AppLifeCycleNotifier extends StateNotifier { final backgroundManager = _ref.read(backgroundSyncProvider); final isAlbumLinkedSyncEnable = _ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.syncAlbums); + final serverInfo = _ref.read(serverInfoProvider); try { bool syncSuccess = false; @@ -160,6 +161,9 @@ class AppLifeCycleNotifier extends StateNotifier { _resumeBackup(); }), _resumeBackup(), + // Sync cloud IDs if server version is compatible + if (CurrentPlatform.isIOS && serverInfo.serverVersion.isAtLeast(major: 2, minor: 4)) + backgroundManager.syncCloudIds(), ]); } else { await _safeRun(backgroundManager.hashAssets(), "hashAssets"); diff --git a/mobile/lib/services/upload.service.dart b/mobile/lib/services/upload.service.dart index ffe84140d9..64a67e0ba1 100644 --- a/mobile/lib/services/upload.service.dart +++ b/mobile/lib/services/upload.service.dart @@ -15,10 +15,12 @@ import 'package:immich_mobile/extensions/platform_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/models/server_info/server_info.model.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/providers/backup/drift_backup.provider.dart'; import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; import 'package:immich_mobile/providers/infrastructure/storage.provider.dart'; +import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/repositories/asset_media.repository.dart'; import 'package:immich_mobile/repositories/upload.repository.dart'; import 'package:immich_mobile/services/api.service.dart'; @@ -35,6 +37,7 @@ final uploadServiceProvider = Provider((ref) { ref.watch(localAssetRepository), ref.watch(appSettingsServiceProvider), ref.watch(assetMediaRepositoryProvider), + ref.watch(serverInfoProvider), ); ref.onDispose(service.dispose); @@ -49,6 +52,7 @@ class UploadService { this._localAssetRepository, this._appSettingsService, this._assetMediaRepository, + this._serverInfo, ) { _uploadRepository.onUploadStatus = _onUploadCallback; _uploadRepository.onTaskProgress = _onTaskProgressCallback; @@ -60,6 +64,7 @@ class UploadService { final DriftLocalAssetRepository _localAssetRepository; final AppSettingsService _appSettingsService; final AssetMediaRepository _assetMediaRepository; + final ServerInfo _serverInfo; final Logger _logger = Logger('UploadService'); final StreamController _taskStatusController = StreamController.broadcast(); @@ -431,13 +436,18 @@ class UploadService { 'fileModifiedAt': modifiedAt.toUtc().toIso8601String(), 'isFavorite': isFavorite?.toString() ?? 'false', 'duration': '0', - 'metadata': jsonEncode([ - RemoteAssetMetadataItem( - key: RemoteAssetMetadataKey.mobileApp, - value: RemoteAssetMobileAppMetadata(cloudId: cloudId, eTag: eTag), - ), - ]), if (fields != null) ...fields, + // Include cloudId and eTag in metadata if available and server version supports it + if (CurrentPlatform.isIOS && + cloudId != null && + eTag != null && + _serverInfo.serverVersion.isAtLeast(major: 2, minor: 4)) + 'metadata': jsonEncode([ + RemoteAssetMetadataItem( + key: RemoteAssetMetadataKey.mobileApp, + value: RemoteAssetMobileAppMetadata(cloudId: cloudId, eTag: eTag), + ), + ]), }; return UploadTask( diff --git a/mobile/lib/widgets/settings/beta_sync_settings/sync_status_and_actions.dart b/mobile/lib/widgets/settings/beta_sync_settings/sync_status_and_actions.dart index c3f7742c7d..c89046da52 100644 --- a/mobile/lib/widgets/settings/beta_sync_settings/sync_status_and_actions.dart +++ b/mobile/lib/widgets/settings/beta_sync_settings/sync_status_and_actions.dart @@ -13,6 +13,7 @@ import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; import 'package:immich_mobile/providers/infrastructure/memory.provider.dart'; import 'package:immich_mobile/providers/infrastructure/storage.provider.dart'; import 'package:immich_mobile/providers/infrastructure/trash_sync.provider.dart'; +import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/providers/sync_status.provider.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/widgets/settings/beta_sync_settings/entity_count_tile.dart'; @@ -25,6 +26,8 @@ class SyncStatusAndActions extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final serverInfo = ref.read(serverInfoProvider); + Future exportDatabase() async { try { // WAL Checkpoint to ensure all changes are written to the database @@ -151,7 +154,7 @@ class SyncStatusAndActions extends HookConsumerWidget { ref.read(backgroundSyncProvider).hashAssets(); }, ), - if (CurrentPlatform.isIOS) + if (CurrentPlatform.isIOS && serverInfo.serverVersion.isAtLeast(major: 2, minor: 4)) ListTile( title: Text( "sync_cloud_ids".t(context: context),