mirror of
https://github.com/immich-app/immich.git
synced 2026-07-20 21:02:55 +08:00
fix(mobile): back up files moved into a watched folder on android
This commit is contained in:
parent
557189d7a8
commit
7e2118901e
@ -210,17 +210,19 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository {
|
||||
await _deleteAssets(deletes);
|
||||
|
||||
await _upsertAssets(updates);
|
||||
// Drop every existing album link for each changed asset before re-adding the
|
||||
// ones the native side reports. A moved asset only reports its new album here,
|
||||
// so leaving the old link around makes the per-album delete sweep wipe the
|
||||
// asset entirely (it is still linked to a bucket it no longer lives in).
|
||||
await _db.batch((batch) async {
|
||||
for (final assetId in assetAlbums.keys) {
|
||||
batch.deleteWhere(_db.localAlbumAssetEntity, (f) => f.assetId.equals(assetId));
|
||||
}
|
||||
});
|
||||
// The ugly casting below is required for now because the generated code
|
||||
// casts the returned values from the platform during decoding them
|
||||
// and iterating over them causes the type to be List<Object?> instead of
|
||||
// List<String>
|
||||
await _db.batch((batch) async {
|
||||
assetAlbums.cast<String, List<Object?>>().forEach((assetId, albumIds) {
|
||||
for (final albumId in albumIds.cast<String?>().nonNulls) {
|
||||
batch.deleteWhere(_db.localAlbumAssetEntity, (f) => f.albumId.equals(albumId) & f.assetId.equals(assetId));
|
||||
}
|
||||
});
|
||||
});
|
||||
await _db.batch((batch) async {
|
||||
assetAlbums.cast<String, List<Object?>>().forEach((assetId, albumIds) {
|
||||
batch.insertAll(
|
||||
|
||||
@ -2,6 +2,7 @@ import 'package:drift/drift.dart';
|
||||
import 'package:drift/native.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart';
|
||||
|
||||
@ -35,4 +36,75 @@ void main() {
|
||||
expect(albums[3].id, '2'); // excluded
|
||||
});
|
||||
});
|
||||
|
||||
group('processDelta', () {
|
||||
// Regression for #22844: an asset moved (not copied) into a backup album on
|
||||
// Android was dropped. The delta reports only the asset's new album, and the
|
||||
// stale link to its old album made the per-album delete sweep wipe the asset.
|
||||
test('keeps an asset moved to another album that still holds other assets', () async {
|
||||
final localAlbumRepo = mediumFactory.getRepository<DriftLocalAlbumRepository>();
|
||||
|
||||
final moved = _localAsset('moved');
|
||||
final other = _localAsset('other');
|
||||
await localAlbumRepo.upsert(
|
||||
mediumFactory.localAlbum(id: 'src', backupSelection: BackupSelection.none),
|
||||
toUpsert: [moved, other],
|
||||
);
|
||||
final anchor = _localAsset('anchor');
|
||||
await localAlbumRepo.upsert(
|
||||
mediumFactory.localAlbum(id: 'dst', backupSelection: BackupSelection.selected),
|
||||
toUpsert: [anchor],
|
||||
);
|
||||
|
||||
// Delta reports the moved asset now living only in the destination album.
|
||||
await localAlbumRepo.processDelta(
|
||||
updates: [moved],
|
||||
deletes: [],
|
||||
assetAlbums: {
|
||||
'moved': ['dst'],
|
||||
},
|
||||
);
|
||||
|
||||
// Per-album delete sweep, as sync() runs on Android. src now physically
|
||||
// holds only `other`; dst holds `anchor` and the moved asset.
|
||||
await localAlbumRepo.syncDeletes('src', ['other']);
|
||||
await localAlbumRepo.syncDeletes('dst', ['anchor', 'moved']);
|
||||
|
||||
final dstIds = (await localAlbumRepo.getAssets('dst')).map((a) => a.id).toSet();
|
||||
final srcIds = await localAlbumRepo.getAssetIds('src');
|
||||
|
||||
expect(dstIds, contains('moved')); // survived and linked to the backed-up album
|
||||
expect(srcIds, isNot(contains('moved'))); // stale source link cleared
|
||||
expect(srcIds, contains('other')); // untouched asset stays put
|
||||
});
|
||||
|
||||
test('replaces album membership with exactly what the delta reports', () async {
|
||||
final localAlbumRepo = mediumFactory.getRepository<DriftLocalAlbumRepository>();
|
||||
|
||||
final moved = _localAsset('moved');
|
||||
await localAlbumRepo.upsert(mediumFactory.localAlbum(id: 'src'), toUpsert: [moved]);
|
||||
await localAlbumRepo.upsert(mediumFactory.localAlbum(id: 'dst'));
|
||||
|
||||
await localAlbumRepo.processDelta(
|
||||
updates: [moved],
|
||||
deletes: [],
|
||||
assetAlbums: {
|
||||
'moved': ['dst'],
|
||||
},
|
||||
);
|
||||
|
||||
expect(await localAlbumRepo.getAssetIds('src'), isEmpty);
|
||||
expect(await localAlbumRepo.getAssetIds('dst'), ['moved']);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
LocalAsset _localAsset(String id) => LocalAsset(
|
||||
id: id,
|
||||
name: '$id.jpg',
|
||||
type: AssetType.image,
|
||||
createdAt: DateTime(2024),
|
||||
updatedAt: DateTime(2024),
|
||||
playbackStyle: AssetPlaybackStyle.image,
|
||||
isEdited: false,
|
||||
);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user