fixes fixes

This commit is contained in:
shenlong-tanwen
2025-12-03 23:42:32 +05:30
parent e76b6baf23
commit b06f591b7a
8 changed files with 51 additions and 10 deletions

View File

@@ -401,8 +401,10 @@ class NativeSyncApiImpl: ImmichPlugin, NativeSyncApi, FlutterPlugin {
var mappings: [String: String?] = [:] var mappings: [String: String?] = [:]
let result = PHPhotoLibrary.shared().cloudIdentifierMappings(forLocalIdentifiers: assetIds) let result = PHPhotoLibrary.shared().cloudIdentifierMappings(forLocalIdentifiers: assetIds)
for (key, value) in result { for (key, value) in result {
let id = try? value.get().stringValue // Ignores invalid cloud ids of the format "GUID:ID:". Valid Ids are of the form "GUID:ID:HASH"
mappings[key] = id if let cloudId = try? value.get().stringValue, !cloudId.hasSuffix(":") {
mappings[key] = cloudId
}
} }
return mappings; return mappings;
} }

View File

@@ -1,3 +1,5 @@
import 'package:immich_mobile/constants/constants.dart';
part 'local_asset.model.dart'; part 'local_asset.model.dart';
part 'remote_asset.model.dart'; part 'remote_asset.model.dart';

View File

@@ -8,6 +8,7 @@ import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/extensions/platform_extensions.dart'; import 'package:immich_mobile/extensions/platform_extensions.dart';
import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/local_album.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/infrastructure/repositories/storage.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/trashed_local_asset.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/trashed_local_asset.repository.dart';
import 'package:immich_mobile/platform/native_sync_api.g.dart'; import 'package:immich_mobile/platform/native_sync_api.g.dart';
@@ -18,6 +19,7 @@ import 'package:logging/logging.dart';
class LocalSyncService { class LocalSyncService {
final DriftLocalAlbumRepository _localAlbumRepository; final DriftLocalAlbumRepository _localAlbumRepository;
final DriftLocalAssetRepository _localAssetRepository;
final NativeSyncApi _nativeSyncApi; final NativeSyncApi _nativeSyncApi;
final DriftTrashedLocalAssetRepository _trashedLocalAssetRepository; final DriftTrashedLocalAssetRepository _trashedLocalAssetRepository;
final LocalFilesManagerRepository _localFilesManager; final LocalFilesManagerRepository _localFilesManager;
@@ -26,11 +28,13 @@ class LocalSyncService {
LocalSyncService({ LocalSyncService({
required DriftLocalAlbumRepository localAlbumRepository, required DriftLocalAlbumRepository localAlbumRepository,
required DriftLocalAssetRepository localAssetRepository,
required DriftTrashedLocalAssetRepository trashedLocalAssetRepository, required DriftTrashedLocalAssetRepository trashedLocalAssetRepository,
required LocalFilesManagerRepository localFilesManager, required LocalFilesManagerRepository localFilesManager,
required StorageRepository storageRepository, required StorageRepository storageRepository,
required NativeSyncApi nativeSyncApi, required NativeSyncApi nativeSyncApi,
}) : _localAlbumRepository = localAlbumRepository, }) : _localAlbumRepository = localAlbumRepository,
_localAssetRepository = localAssetRepository,
_trashedLocalAssetRepository = trashedLocalAssetRepository, _trashedLocalAssetRepository = trashedLocalAssetRepository,
_localFilesManager = localFilesManager, _localFilesManager = localFilesManager,
_storageRepository = storageRepository, _storageRepository = storageRepository,
@@ -47,6 +51,12 @@ class LocalSyncService {
_log.warning("syncTrashedAssets cannot proceed because MANAGE_MEDIA permission is missing"); _log.warning("syncTrashedAssets cannot proceed because MANAGE_MEDIA permission is missing");
} }
} }
if (CurrentPlatform.isIOS) {
final assets = await _localAssetRepository.getEmptyCloudIdAssets();
await _mapIosCloudIds(assets);
}
if (full || await _nativeSyncApi.shouldFullSync()) { if (full || await _nativeSyncApi.shouldFullSync()) {
_log.fine("Full sync request from ${full ? "user" : "native"}"); _log.fine("Full sync request from ${full ? "user" : "native"}");
return await fullSync(); return await fullSync();

View File

@@ -1,5 +1,6 @@
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/constants.dart';
import 'package:immich_mobile/domain/models/asset/asset_metadata.model.dart'; import 'package:immich_mobile/domain/models/asset/asset_metadata.model.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart';
@@ -32,7 +33,7 @@ Future<void> syncCloudIds(ProviderContainer ref) async {
for (final mapping in mappingsToUpdate) { for (final mapping in mappingsToUpdate) {
final mobileMeta = AssetMetadataUpsertItemDto( final mobileMeta = AssetMetadataUpsertItemDto(
key: AssetMetadataKey.mobileApp, key: AssetMetadataKey.mobileApp,
value: RemoteAssetMobileAppMetadata(cloudId: mapping.cloudId), value: RemoteAssetMobileAppMetadata(cloudId: mapping.cloudId, eTag: mapping.eTag),
); );
try { try {
await assetApi.updateAssetMetadata(mapping.assetId, AssetMetadataUpsertDto(items: [mobileMeta])); await assetApi.updateAssetMetadata(mapping.assetId, AssetMetadataUpsertDto(items: [mobileMeta]));
@@ -51,7 +52,7 @@ Future<void> _populateCloudIds(Drift drift) async {
await DriftLocalAlbumRepository(drift).updateCloudMapping(cloudMapping); await DriftLocalAlbumRepository(drift).updateCloudMapping(cloudMapping);
} }
typedef _CloudIdMapping = ({String assetId, String cloudId}); typedef _CloudIdMapping = ({String assetId, String cloudId, String eTag});
Future<List<_CloudIdMapping>> _fetchCloudIdMappings(Drift drift, String userId) async { Future<List<_CloudIdMapping>> _fetchCloudIdMappings(Drift drift, String userId) async {
final query = final query =
@@ -67,16 +68,31 @@ Future<List<_CloudIdMapping>> _fetchCloudIdMappings(Drift drift, String userId)
useColumns: false, useColumns: false,
), ),
]) ])
..addColumns([drift.remoteAssetEntity.id, drift.localAssetEntity.iCloudId]) ..addColumns([
drift.remoteAssetEntity.id,
drift.localAssetEntity.iCloudId,
drift.localAssetEntity.createdAt,
drift.localAssetEntity.adjustmentTime,
drift.localAssetEntity.latitude,
drift.localAssetEntity.longitude,
])
..where( ..where(
drift.localAssetEntity.id.isNotNull() & drift.localAssetEntity.id.isNotNull() &
drift.localAssetEntity.iCloudId.isNotNull() & drift.localAssetEntity.iCloudId.isNotNull() &
drift.remoteAssetEntity.ownerId.equals(userId) & drift.remoteAssetEntity.ownerId.equals(userId) &
drift.remoteAssetCloudIdEntity.cloudId.isNull(), drift.remoteAssetCloudIdEntity.cloudId.isNull(),
); );
return query return query.map((row) {
.map( final createdAt = row.read(drift.localAssetEntity.createdAt)!;
(row) => (assetId: row.read(drift.remoteAssetEntity.id)!, cloudId: row.read(drift.localAssetEntity.iCloudId)!), final adjustmentTime = row.read(drift.localAssetEntity.adjustmentTime);
) final latitude = row.read(drift.localAssetEntity.latitude);
.get(); final longitude = row.read(drift.localAssetEntity.longitude);
final eTag =
"${createdAt.millisecondsSinceEpoch ~/ 1000}$kUploadETagDelimiter${(adjustmentTime?.millisecondsSinceEpoch ?? 0) ~/ 1000}$kUploadETagDelimiter${latitude ?? 0}$kUploadETagDelimiter${longitude ?? 0}";
return (
assetId: row.read(drift.remoteAssetEntity.id)!,
cloudId: row.read(drift.localAssetEntity.iCloudId)!,
eTag: eTag,
);
}).get();
} }

View File

@@ -127,6 +127,11 @@ class DriftLocalAssetRepository extends DriftDatabaseRepository {
return result; return result;
} }
Future<List<LocalAsset>> getEmptyCloudIdAssets() {
final query = _db.localAssetEntity.select()..where((row) => row.iCloudId.isNull());
return query.map((row) => row.toDto()).get();
}
Future<Map<String, String>> getHashMappingFromCloudId() async { Future<Map<String, String>> getHashMappingFromCloudId() async {
final createdAt = coalesce([_db.localAssetEntity.createdAt.strftime('%s'), const Constant('0')]); final createdAt = coalesce([_db.localAssetEntity.createdAt.strftime('%s'), const Constant('0')]);
final adjustmentTime = coalesce([_db.localAssetEntity.adjustmentTime.strftime('%s'), const Constant('0')]); final adjustmentTime = coalesce([_db.localAssetEntity.adjustmentTime.strftime('%s'), const Constant('0')]);

View File

@@ -131,6 +131,7 @@ class _AssetPropertiesSectionState extends ConsumerState<_AssetPropertiesSection
final albums = await ref.read(assetServiceProvider).getSourceAlbums(asset.id); final albums = await ref.read(assetServiceProvider).getSourceAlbums(asset.id);
properties.add(_PropertyItem(label: 'Album', value: albums.map((a) => a.name).join(', '))); properties.add(_PropertyItem(label: 'Album', value: albums.map((a) => a.name).join(', ')));
if (CurrentPlatform.isIOS) { if (CurrentPlatform.isIOS) {
properties.add(_PropertyItem(label: 'Cloud ID', value: asset.cloudId));
properties.add(_PropertyItem(label: 'Adjustment Time', value: asset.adjustmentTime?.toString())); properties.add(_PropertyItem(label: 'Adjustment Time', value: asset.adjustmentTime?.toString()));
} }
properties.add( properties.add(

View File

@@ -32,6 +32,7 @@ final syncStreamRepositoryProvider = Provider((ref) => SyncStreamRepository(ref.
final localSyncServiceProvider = Provider( final localSyncServiceProvider = Provider(
(ref) => LocalSyncService( (ref) => LocalSyncService(
localAlbumRepository: ref.watch(localAlbumRepository), localAlbumRepository: ref.watch(localAlbumRepository),
localAssetRepository: ref.watch(localAssetRepository),
trashedLocalAssetRepository: ref.watch(trashedLocalAssetRepository), trashedLocalAssetRepository: ref.watch(trashedLocalAssetRepository),
localFilesManager: ref.watch(localFilesManagerRepositoryProvider), localFilesManager: ref.watch(localFilesManagerRepositoryProvider),
storageRepository: ref.watch(storageRepositoryProvider), storageRepository: ref.watch(storageRepositoryProvider),

View File

@@ -9,6 +9,7 @@ import 'package:immich_mobile/domain/services/store.service.dart';
import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/local_album.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/infrastructure/repositories/storage.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/trashed_local_asset.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/trashed_local_asset.repository.dart';
@@ -25,6 +26,7 @@ import '../../repository.mocks.dart';
void main() { void main() {
late LocalSyncService sut; late LocalSyncService sut;
late DriftLocalAlbumRepository mockLocalAlbumRepository; late DriftLocalAlbumRepository mockLocalAlbumRepository;
late DriftLocalAssetRepository mockLocalAssetRepository;
late DriftTrashedLocalAssetRepository mockTrashedLocalAssetRepository; late DriftTrashedLocalAssetRepository mockTrashedLocalAssetRepository;
late LocalFilesManagerRepository mockLocalFilesManager; late LocalFilesManagerRepository mockLocalFilesManager;
late StorageRepository mockStorageRepository; late StorageRepository mockStorageRepository;
@@ -47,6 +49,7 @@ void main() {
setUp(() async { setUp(() async {
mockLocalAlbumRepository = MockLocalAlbumRepository(); mockLocalAlbumRepository = MockLocalAlbumRepository();
mockLocalAssetRepository = MockLocalAssetRepository();
mockTrashedLocalAssetRepository = MockTrashedLocalAssetRepository(); mockTrashedLocalAssetRepository = MockTrashedLocalAssetRepository();
mockLocalFilesManager = MockLocalFilesManagerRepository(); mockLocalFilesManager = MockLocalFilesManagerRepository();
mockStorageRepository = MockStorageRepository(); mockStorageRepository = MockStorageRepository();
@@ -66,6 +69,7 @@ void main() {
sut = LocalSyncService( sut = LocalSyncService(
localAlbumRepository: mockLocalAlbumRepository, localAlbumRepository: mockLocalAlbumRepository,
localAssetRepository: mockLocalAssetRepository,
trashedLocalAssetRepository: mockTrashedLocalAssetRepository, trashedLocalAssetRepository: mockTrashedLocalAssetRepository,
localFilesManager: mockLocalFilesManager, localFilesManager: mockLocalFilesManager,
storageRepository: mockStorageRepository, storageRepository: mockStorageRepository,