mirror of
https://github.com/immich-app/immich.git
synced 2026-07-20 21:02:55 +08:00
refactor: per asset backup flag
This commit is contained in:
parent
b21af78454
commit
e6db274aa9
BIN
mobile/drift_schemas/main/drift_schema_v31.json
Normal file
BIN
mobile/drift_schemas/main/drift_schema_v31.json
Normal file
Binary file not shown.
@ -6,7 +6,7 @@ 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/base_asset.model.dart';
|
||||
import 'package:immich_mobile/extensions/platform_extensions.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/mapper.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart';
|
||||
import 'package:immich_mobile/platform/native_sync_api.g.dart';
|
||||
@ -211,7 +211,7 @@ Future<List<_CloudIdMapping>> _fetchCloudIdMappings(Drift drift, String userId,
|
||||
return query.map((row) {
|
||||
return (
|
||||
remoteAssetId: row.read(drift.remoteAssetEntity.id)!,
|
||||
localAsset: row.readTable(drift.localAssetEntity).toDto(),
|
||||
localAsset: mapToLocalAsset(row.readTable(drift.localAssetEntity)),
|
||||
);
|
||||
}).get();
|
||||
}
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_album.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
|
||||
|
||||
@ -23,17 +22,3 @@ class LocalAlbumEntity extends Table with DriftDefaultsMixin {
|
||||
@override
|
||||
Set<Column> get primaryKey => {id};
|
||||
}
|
||||
|
||||
extension LocalAlbumEntityDataHelper on LocalAlbumEntityData {
|
||||
LocalAlbum toDto({int assetCount = 0}) {
|
||||
return LocalAlbum(
|
||||
id: id,
|
||||
name: name,
|
||||
updatedAt: updatedAt,
|
||||
assetCount: assetCount,
|
||||
backupSelection: backupSelection,
|
||||
linkedRemoteAlbumId: linkedRemoteAlbumId,
|
||||
isIosSharedAlbum: isIosSharedAlbum,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,12 +1,14 @@
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/utils/asset.mixin.dart';
|
||||
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
|
||||
|
||||
@TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_local_asset_checksum ON local_asset_entity (checksum)')
|
||||
@TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_local_asset_cloud_id ON local_asset_entity (i_cloud_id)')
|
||||
@TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_local_asset_created_at ON local_asset_entity (created_at)')
|
||||
@TableIndex.sql(
|
||||
'CREATE INDEX IF NOT EXISTS idx_local_asset_backup_candidate ON local_asset_entity (is_backup_candidate)',
|
||||
)
|
||||
class LocalAssetEntity extends Table with DriftDefaultsMixin, AssetEntityMixin {
|
||||
const LocalAssetEntity();
|
||||
|
||||
@ -16,6 +18,8 @@ class LocalAssetEntity extends Table with DriftDefaultsMixin, AssetEntityMixin {
|
||||
// Only used during backup to mirror the favorite status of the asset in the server
|
||||
BoolColumn get isFavorite => boolean().withDefault(const Constant(false))();
|
||||
|
||||
BoolColumn get isBackupCandidate => boolean().withDefault(const Constant(false))();
|
||||
|
||||
IntColumn get orientation => integer().withDefault(const Constant(0))();
|
||||
|
||||
TextColumn get iCloudId => text().nullable()();
|
||||
@ -31,26 +35,3 @@ class LocalAssetEntity extends Table with DriftDefaultsMixin, AssetEntityMixin {
|
||||
@override
|
||||
Set<Column> get primaryKey => {id};
|
||||
}
|
||||
|
||||
extension LocalAssetEntityDataDomainExtension on LocalAssetEntityData {
|
||||
LocalAsset toDto({String? remoteId}) => LocalAsset(
|
||||
id: id,
|
||||
name: name,
|
||||
checksum: checksum,
|
||||
type: type,
|
||||
createdAt: createdAt,
|
||||
updatedAt: updatedAt,
|
||||
durationMs: durationMs,
|
||||
isFavorite: isFavorite,
|
||||
height: height,
|
||||
width: width,
|
||||
remoteId: remoteId,
|
||||
orientation: orientation,
|
||||
playbackStyle: playbackStyle,
|
||||
adjustmentTime: adjustmentTime,
|
||||
latitude: latitude,
|
||||
longitude: longitude,
|
||||
cloudId: iCloudId,
|
||||
isEdited: false,
|
||||
);
|
||||
}
|
||||
|
||||
Binary file not shown.
@ -1,8 +1,6 @@
|
||||
import 'remote_asset.entity.dart';
|
||||
import 'stack.entity.dart';
|
||||
import 'local_asset.entity.dart';
|
||||
import 'local_album.entity.dart';
|
||||
import 'local_album_asset.entity.dart';
|
||||
|
||||
mergedAsset:
|
||||
SELECT
|
||||
@ -73,16 +71,7 @@ FROM
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM remote_asset_entity rae WHERE rae.checksum = lae.checksum AND rae.owner_id IN :user_ids
|
||||
)
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM local_album_asset_entity laa
|
||||
INNER JOIN local_album_entity la on laa.album_id = la.id
|
||||
WHERE laa.asset_id = lae.id AND la.backup_selection = 0 -- selected
|
||||
)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM local_album_asset_entity laa
|
||||
INNER JOIN local_album_entity la on laa.album_id = la.id
|
||||
WHERE laa.asset_id = lae.id AND la.backup_selection = 2 -- excluded
|
||||
)
|
||||
AND lae.is_backup_candidate
|
||||
ORDER BY created_at DESC
|
||||
LIMIT $limit;
|
||||
|
||||
@ -126,16 +115,7 @@ FROM
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM remote_asset_entity rae WHERE rae.checksum = lae.checksum AND rae.owner_id IN :user_ids
|
||||
)
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM local_album_asset_entity laa
|
||||
INNER JOIN local_album_entity la on laa.album_id = la.id
|
||||
WHERE laa.asset_id = lae.id AND la.backup_selection = 0 -- selected
|
||||
)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM local_album_asset_entity laa
|
||||
INNER JOIN local_album_entity la on laa.album_id = la.id
|
||||
WHERE laa.asset_id = lae.id AND la.backup_selection = 2 -- excluded
|
||||
)
|
||||
AND lae.is_backup_candidate
|
||||
)
|
||||
GROUP BY bucket_date
|
||||
ORDER BY bucket_date DESC;
|
||||
|
||||
Binary file not shown.
@ -1,4 +1,8 @@
|
||||
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/domain/models/user.model.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/partner.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart';
|
||||
|
||||
@ -13,3 +17,34 @@ User mapToUser(UserEntityData data) => User(
|
||||
|
||||
Partner mapToPartner(UserEntityData user, PartnerEntityData partner) =>
|
||||
Partner.fromUser(mapToUser(user), inTimeline: partner.inTimeline);
|
||||
|
||||
LocalAlbum mapToLocalAlbum(LocalAlbumEntityData data, {int assetCount = 0}) => LocalAlbum(
|
||||
id: data.id,
|
||||
name: data.name,
|
||||
updatedAt: data.updatedAt,
|
||||
assetCount: assetCount,
|
||||
backupSelection: data.backupSelection,
|
||||
linkedRemoteAlbumId: data.linkedRemoteAlbumId,
|
||||
isIosSharedAlbum: data.isIosSharedAlbum,
|
||||
);
|
||||
|
||||
LocalAsset mapToLocalAsset(LocalAssetEntityData data, {String? remoteId}) => LocalAsset(
|
||||
id: data.id,
|
||||
name: data.name,
|
||||
checksum: data.checksum,
|
||||
type: data.type,
|
||||
createdAt: data.createdAt,
|
||||
updatedAt: data.updatedAt,
|
||||
durationMs: data.durationMs,
|
||||
isFavorite: data.isFavorite,
|
||||
height: data.height,
|
||||
width: data.width,
|
||||
remoteId: remoteId,
|
||||
orientation: data.orientation,
|
||||
playbackStyle: data.playbackStyle,
|
||||
adjustmentTime: data.adjustmentTime,
|
||||
latitude: data.latitude,
|
||||
longitude: data.longitude,
|
||||
cloudId: data.iCloudId,
|
||||
isEdited: false,
|
||||
);
|
||||
|
||||
@ -2,9 +2,8 @@ import 'dart:async';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.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/entities/local_asset.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/mapper.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
||||
|
||||
@ -16,19 +15,6 @@ class DriftBackupRepository extends DriftDatabaseRepository {
|
||||
final Drift _db;
|
||||
const DriftBackupRepository(this._db) : super(_db);
|
||||
|
||||
_getExcludedSubquery() {
|
||||
return _db.localAlbumAssetEntity.selectOnly()
|
||||
..addColumns([_db.localAlbumAssetEntity.assetId])
|
||||
..join([
|
||||
innerJoin(
|
||||
_db.localAlbumEntity,
|
||||
_db.localAlbumAssetEntity.albumId.equalsExp(_db.localAlbumEntity.id),
|
||||
useColumns: false,
|
||||
),
|
||||
])
|
||||
..where(_db.localAlbumEntity.backupSelection.equalsValue(BackupSelection.excluded));
|
||||
}
|
||||
|
||||
/// Returns all backup-related counts in a single query.
|
||||
///
|
||||
/// - total: number of distinct assets in selected albums, excluding those that are also in excluded albums
|
||||
@ -45,31 +31,14 @@ class DriftBackupRepository extends DriftDatabaseRepository {
|
||||
FROM local_asset_entity lae
|
||||
LEFT JOIN main.remote_asset_entity rae
|
||||
ON lae.checksum = rae.checksum AND rae.owner_id = ?1
|
||||
WHERE EXISTS (
|
||||
SELECT 1
|
||||
FROM local_album_asset_entity laa
|
||||
INNER JOIN main.local_album_entity la on laa.album_id = la.id
|
||||
WHERE laa.asset_id = lae.id
|
||||
AND la.backup_selection = ?2
|
||||
)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM local_album_asset_entity laa
|
||||
INNER JOIN main.local_album_entity la on laa.album_id = la.id
|
||||
WHERE laa.asset_id = lae.id
|
||||
AND la.backup_selection = ?3
|
||||
);
|
||||
WHERE lae.is_backup_candidate;
|
||||
''';
|
||||
|
||||
final row = await _db
|
||||
.customSelect(
|
||||
sql,
|
||||
variables: [
|
||||
Variable.withString(userId),
|
||||
Variable.withInt(BackupSelection.selected.index),
|
||||
Variable.withInt(BackupSelection.excluded.index),
|
||||
],
|
||||
readsFrom: {_db.localAlbumAssetEntity, _db.localAlbumEntity, _db.localAssetEntity, _db.remoteAssetEntity},
|
||||
variables: [Variable.withString(userId)],
|
||||
readsFrom: {_db.localAssetEntity, _db.remoteAssetEntity},
|
||||
)
|
||||
.getSingle();
|
||||
|
||||
@ -82,29 +51,17 @@ class DriftBackupRepository extends DriftDatabaseRepository {
|
||||
}
|
||||
|
||||
Future<List<LocalAsset>> getCandidates(String userId, {bool onlyHashed = true}) async {
|
||||
final selectedAlbumIds = _db.localAlbumEntity.selectOnly(distinct: true)
|
||||
..addColumns([_db.localAlbumEntity.id])
|
||||
..where(_db.localAlbumEntity.backupSelection.equalsValue(BackupSelection.selected));
|
||||
|
||||
final query = _db.localAssetEntity.select()
|
||||
..where(
|
||||
(lae) =>
|
||||
existsQuery(
|
||||
_db.localAlbumAssetEntity.selectOnly()
|
||||
..addColumns([_db.localAlbumAssetEntity.assetId])
|
||||
..where(
|
||||
_db.localAlbumAssetEntity.albumId.isInQuery(selectedAlbumIds) &
|
||||
_db.localAlbumAssetEntity.assetId.equalsExp(lae.id),
|
||||
),
|
||||
) &
|
||||
lae.isBackupCandidate.equals(true) &
|
||||
notExistsQuery(
|
||||
_db.remoteAssetEntity.selectOnly()
|
||||
..addColumns([_db.remoteAssetEntity.checksum])
|
||||
..where(
|
||||
_db.remoteAssetEntity.checksum.equalsExp(lae.checksum) & _db.remoteAssetEntity.ownerId.equals(userId),
|
||||
),
|
||||
) &
|
||||
lae.id.isNotInQuery(_getExcludedSubquery()),
|
||||
),
|
||||
)
|
||||
..orderBy([(localAsset) => OrderingTerm.desc(localAsset.createdAt)]);
|
||||
|
||||
@ -112,6 +69,6 @@ class DriftBackupRepository extends DriftDatabaseRepository {
|
||||
query.where((lae) => lae.checksum.isNotNull());
|
||||
}
|
||||
|
||||
return query.map((localAsset) => localAsset.toDto()).get();
|
||||
return query.map(mapToLocalAsset).get();
|
||||
}
|
||||
}
|
||||
|
||||
@ -120,7 +120,7 @@ class Drift extends $Drift {
|
||||
}
|
||||
|
||||
@override
|
||||
int get schemaVersion => 30;
|
||||
int get schemaVersion => 31;
|
||||
|
||||
@override
|
||||
MigrationStrategy get migration => MigrationStrategy(
|
||||
@ -311,6 +311,10 @@ class Drift extends $Drift {
|
||||
from29To30: (m, v30) async {
|
||||
await m.alterTable(TableMigration(v30.settings));
|
||||
},
|
||||
from30To31: (m, v31) async {
|
||||
await m.addColumn(v31.localAssetEntity, v31.localAssetEntity.isBackupCandidate);
|
||||
await m.createIndex(v31.idxLocalAssetBackupCandidate);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@ -1,14 +1,15 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:immich_mobile/constants/constants.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/extensions/platform_extensions.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/local_album.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/mapper.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||
|
||||
enum SortLocalAlbumsBy { id, backupSelection, isIosSharedAlbum, name, assetCount, newestAsset }
|
||||
@ -47,26 +48,32 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository {
|
||||
query.orderBy(orderings);
|
||||
}
|
||||
|
||||
return query.map((row) => row.readTable(_db.localAlbumEntity).toDto(assetCount: row.read(assetCount) ?? 0)).get();
|
||||
return query
|
||||
.map((row) => mapToLocalAlbum(row.readTable(_db.localAlbumEntity), assetCount: row.read(assetCount) ?? 0))
|
||||
.get();
|
||||
}
|
||||
|
||||
Future<List<LocalAlbum>> getBackupAlbums() async {
|
||||
final query = _db.localAlbumEntity.select()
|
||||
..where((row) => row.backupSelection.equalsValue(BackupSelection.selected));
|
||||
|
||||
return query.map((row) => row.toDto()).get();
|
||||
return query.map(mapToLocalAlbum).get();
|
||||
}
|
||||
|
||||
Future<void> delete(String albumId) => transaction(() async {
|
||||
// Remove all assets that are only in this particular album
|
||||
// We cannot remove all assets in the album because they might be in other albums in iOS
|
||||
// That is not the case on Android since asset <-> album has one:one mapping
|
||||
final assetsToDelete = CurrentPlatform.isIOS ? await _getUniqueAssetsInAlbum(albumId) : await getAssetIds(albumId);
|
||||
await _deleteAssets(assetsToDelete);
|
||||
|
||||
await _db.managers.localAlbumEntity
|
||||
.filter((a) => a.id.equals(albumId) & a.backupSelection.equals(BackupSelection.none))
|
||||
.delete();
|
||||
final affectedAssetIds = await getAssetIds(albumId);
|
||||
final assetsToDelete = CurrentPlatform.isIOS ? await _getUniqueAssetsInAlbum(albumId) : affectedAssetIds;
|
||||
await _db.transaction(() async {
|
||||
await _deleteAssets(assetsToDelete);
|
||||
await _db.localAlbumAssetEntity.deleteWhere((f) => f.albumId.equals(albumId));
|
||||
await _db.managers.localAlbumEntity
|
||||
.filter((a) => a.id.equals(albumId) & a.backupSelection.equals(BackupSelection.none))
|
||||
.delete();
|
||||
await recomputeBackupCandidatesForAssets(affectedAssetIds);
|
||||
});
|
||||
});
|
||||
|
||||
Future<void> syncDeletes(String albumId, Iterable<String> assetIdsToKeep) async {
|
||||
@ -126,64 +133,93 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository {
|
||||
);
|
||||
}
|
||||
await _removeAssets(localAlbum.id, toDelete);
|
||||
await recomputeBackupCandidatesForAlbum(localAlbum.id);
|
||||
await recomputeBackupCandidatesForAssets(toDelete);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> updateAll(Iterable<LocalAlbum> albums) {
|
||||
return _db.transaction(() async {
|
||||
await _db.localAlbumEntity.update().write(const LocalAlbumEntityCompanion(marker_: Value(true)));
|
||||
|
||||
await _db.batch((batch) {
|
||||
for (final album in albums) {
|
||||
final companion = LocalAlbumEntityCompanion.insert(
|
||||
id: album.id,
|
||||
name: album.name,
|
||||
updatedAt: Value(album.updatedAt),
|
||||
backupSelection: album.backupSelection,
|
||||
isIosSharedAlbum: Value(album.isIosSharedAlbum),
|
||||
marker_: const Value(null),
|
||||
);
|
||||
|
||||
batch.insert(
|
||||
_db.localAlbumEntity,
|
||||
companion,
|
||||
onConflict: DoUpdate(
|
||||
(old) => LocalAlbumEntityCompanion(
|
||||
id: companion.id,
|
||||
name: companion.name,
|
||||
updatedAt: companion.updatedAt,
|
||||
isIosSharedAlbum: companion.isIosSharedAlbum,
|
||||
marker_: companion.marker_,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
await _markAllAlbums();
|
||||
await _upsertAndUnmarkIncoming(albums);
|
||||
if (CurrentPlatform.isAndroid) {
|
||||
// On Android, an asset can only be in one album
|
||||
// So, get the albums that are marked for deletion
|
||||
// On Android, an asset can only be in one album. So, get the albums that are marked for deletion
|
||||
// and delete all the assets that are in those albums
|
||||
final deleteSmt = _db.localAssetEntity.delete();
|
||||
deleteSmt.where((localAsset) {
|
||||
final subQuery = _db.localAlbumAssetEntity.selectOnly()
|
||||
..addColumns([_db.localAlbumAssetEntity.assetId])
|
||||
..join([
|
||||
innerJoin(_db.localAlbumEntity, _db.localAlbumAssetEntity.albumId.equalsExp(_db.localAlbumEntity.id)),
|
||||
]);
|
||||
subQuery.where(_db.localAlbumEntity.marker_.isNotNull());
|
||||
return localAsset.id.isInQuery(subQuery);
|
||||
});
|
||||
await deleteSmt.go();
|
||||
await _hardDeleteAssetsInMarkedAlbums();
|
||||
}
|
||||
|
||||
// Only remove albums that are not explicitly selected or excluded from backups
|
||||
await _db.localAlbumEntity.deleteWhere(
|
||||
(f) => f.marker_.isNotNull() & f.backupSelection.equalsValue(BackupSelection.none),
|
||||
);
|
||||
final affectedAssetIds = await _removeAssetsFromMarkedAlbums();
|
||||
await _deleteMarkedNoneAlbums();
|
||||
await recomputeBackupCandidatesForAssets(affectedAssetIds);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _markAllAlbums() async {
|
||||
await _db.localAlbumEntity.update().write(const LocalAlbumEntityCompanion(marker_: Value(true)));
|
||||
}
|
||||
|
||||
Future<void> _upsertAndUnmarkIncoming(Iterable<LocalAlbum> albums) async {
|
||||
await _db.batch((batch) {
|
||||
for (final album in albums) {
|
||||
final companion = LocalAlbumEntityCompanion.insert(
|
||||
id: album.id,
|
||||
name: album.name,
|
||||
updatedAt: Value(album.updatedAt),
|
||||
backupSelection: album.backupSelection,
|
||||
isIosSharedAlbum: Value(album.isIosSharedAlbum),
|
||||
marker_: const Value(null),
|
||||
);
|
||||
batch.insert(
|
||||
_db.localAlbumEntity,
|
||||
companion,
|
||||
onConflict: DoUpdate(
|
||||
(old) => LocalAlbumEntityCompanion(
|
||||
id: companion.id,
|
||||
name: companion.name,
|
||||
updatedAt: companion.updatedAt,
|
||||
isIosSharedAlbum: companion.isIosSharedAlbum,
|
||||
marker_: companion.marker_,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _hardDeleteAssetsInMarkedAlbums() async {
|
||||
final deleteSmt = _db.localAssetEntity.delete();
|
||||
deleteSmt.where((localAsset) {
|
||||
final subQuery = _db.localAlbumAssetEntity.selectOnly()
|
||||
..addColumns([_db.localAlbumAssetEntity.assetId])
|
||||
..join([innerJoin(_db.localAlbumEntity, _db.localAlbumAssetEntity.albumId.equalsExp(_db.localAlbumEntity.id))]);
|
||||
subQuery.where(_db.localAlbumEntity.marker_.isNotNull());
|
||||
return localAsset.id.isInQuery(subQuery);
|
||||
});
|
||||
await deleteSmt.go();
|
||||
}
|
||||
|
||||
Future<List<String>> _removeAssetsFromMarkedAlbums() async {
|
||||
final orphanedAlbumIds = _db.localAlbumEntity.selectOnly()
|
||||
..addColumns([_db.localAlbumEntity.id])
|
||||
..where(_db.localAlbumEntity.marker_.isNotNull());
|
||||
|
||||
final affectedAssetIds =
|
||||
await (_db.localAlbumAssetEntity.selectOnly(distinct: true)
|
||||
..addColumns([_db.localAlbumAssetEntity.assetId])
|
||||
..where(_db.localAlbumAssetEntity.albumId.isInQuery(orphanedAlbumIds)))
|
||||
.map((row) => row.read(_db.localAlbumAssetEntity.assetId)!)
|
||||
.get();
|
||||
|
||||
await (_db.localAlbumAssetEntity.delete()..where((f) => f.albumId.isInQuery(orphanedAlbumIds))).go();
|
||||
|
||||
return affectedAssetIds;
|
||||
}
|
||||
|
||||
Future<void> _deleteMarkedNoneAlbums() async {
|
||||
await _db.localAlbumEntity.deleteWhere(
|
||||
(f) => f.marker_.isNotNull() & f.backupSelection.equalsValue(BackupSelection.none),
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<LocalAsset>> getAssets(String albumId) {
|
||||
final query =
|
||||
_db.localAlbumAssetEntity.select().join([
|
||||
@ -191,7 +227,7 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository {
|
||||
])
|
||||
..where(_db.localAlbumAssetEntity.albumId.equals(albumId))
|
||||
..orderBy([OrderingTerm.asc(_db.localAssetEntity.id)]);
|
||||
return query.map((row) => row.readTable(_db.localAssetEntity).toDto()).get();
|
||||
return query.map((row) => mapToLocalAsset(row.readTable(_db.localAssetEntity))).get();
|
||||
}
|
||||
|
||||
Future<List<String>> getAssetIds(String albumId) {
|
||||
@ -232,6 +268,8 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
await recomputeBackupCandidatesForAssets(assetAlbums.keys);
|
||||
});
|
||||
}
|
||||
|
||||
@ -240,10 +278,65 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository {
|
||||
_db.localAlbumAssetEntity.select().join([
|
||||
innerJoin(_db.localAssetEntity, _db.localAlbumAssetEntity.assetId.equalsExp(_db.localAssetEntity.id)),
|
||||
])
|
||||
..where(_db.localAlbumAssetEntity.albumId.equals(albumId) & _db.localAssetEntity.checksum.isNull())
|
||||
..where(
|
||||
_db.localAlbumAssetEntity.albumId.equals(albumId) &
|
||||
_db.localAssetEntity.checksum.isNull() &
|
||||
_db.localAssetEntity.isBackupCandidate.equals(true),
|
||||
)
|
||||
..orderBy([OrderingTerm.desc(_db.localAssetEntity.createdAt)]);
|
||||
|
||||
return query.map((row) => row.readTable(_db.localAssetEntity).toDto()).get();
|
||||
return query.map((row) => mapToLocalAsset(row.readTable(_db.localAssetEntity))).get();
|
||||
}
|
||||
|
||||
Future<void> recomputeBackupCandidatesForAlbum(String albumId) => _recomputeBackupCandidates(
|
||||
whereClause: 'WHERE id IN (SELECT asset_id FROM local_album_asset_entity WHERE album_id = ?)',
|
||||
extraVariables: [Variable.withString(albumId)],
|
||||
);
|
||||
|
||||
Future<void> recomputeBackupCandidatesForAssets(Iterable<String> assetIds) async {
|
||||
final ids = assetIds.toList(growable: false);
|
||||
if (ids.isEmpty) {
|
||||
return;
|
||||
}
|
||||
for (final slice in ids.slices(kDriftMaxChunk)) {
|
||||
await _recomputeBackupCandidates(
|
||||
whereClause: 'WHERE id IN (${List.filled(slice.length, '?').join(',')})',
|
||||
extraVariables: slice.map(Variable.withString).toList(growable: false),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> recomputeAllBackupCandidates() => _recomputeBackupCandidates(whereClause: '', extraVariables: const []);
|
||||
|
||||
Future<void> _recomputeBackupCandidates({
|
||||
required String whereClause,
|
||||
required List<Variable<Object>> extraVariables,
|
||||
}) async {
|
||||
await _db.customUpdate(
|
||||
'''
|
||||
UPDATE local_asset_entity
|
||||
SET is_backup_candidate = (
|
||||
EXISTS (
|
||||
SELECT 1 FROM local_album_asset_entity laa
|
||||
INNER JOIN local_album_entity la ON la.id = laa.album_id
|
||||
WHERE laa.asset_id = local_asset_entity.id AND la.backup_selection = ?
|
||||
)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM local_album_asset_entity laa
|
||||
INNER JOIN local_album_entity la ON la.id = laa.album_id
|
||||
WHERE laa.asset_id = local_asset_entity.id AND la.backup_selection = ?
|
||||
)
|
||||
)
|
||||
$whereClause
|
||||
''',
|
||||
variables: [
|
||||
Variable.withInt(BackupSelection.selected.index),
|
||||
Variable.withInt(BackupSelection.excluded.index),
|
||||
...extraVariables,
|
||||
],
|
||||
updates: {_db.localAssetEntity},
|
||||
updateKind: UpdateKind.update,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> updateCloudMapping(Map<String, String> cloudMapping) {
|
||||
@ -424,7 +517,7 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository {
|
||||
..orderBy([OrderingTerm.desc(_db.localAssetEntity.createdAt)])
|
||||
..limit(1);
|
||||
|
||||
final results = await query.map((row) => row.readTable(_db.localAssetEntity).toDto()).get();
|
||||
final results = await query.map((row) => mapToLocalAsset(row.readTable(_db.localAssetEntity))).get();
|
||||
|
||||
return results.isNotEmpty ? results.first : null;
|
||||
}
|
||||
|
||||
@ -6,9 +6,8 @@ import 'package:immich_mobile/constants/constants.dart';
|
||||
import 'package:immich_mobile/constants/enums.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/entities/local_album.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/mapper.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||
|
||||
class RemovalCandidatesResult {
|
||||
@ -33,7 +32,7 @@ class DriftLocalAssetRepository extends DriftDatabaseRepository {
|
||||
])..where(_db.localAssetEntity.id.equals(id));
|
||||
|
||||
return query.map((row) {
|
||||
final asset = row.readTable(_db.localAssetEntity).toDto();
|
||||
final asset = mapToLocalAsset(row.readTable(_db.localAssetEntity));
|
||||
return asset.copyWith(remoteId: row.read(_db.remoteAssetEntity.id));
|
||||
});
|
||||
}
|
||||
@ -43,7 +42,7 @@ class DriftLocalAssetRepository extends DriftDatabaseRepository {
|
||||
Future<List<LocalAsset?>> getByChecksum(String checksum) {
|
||||
final query = _db.localAssetEntity.select()..where((lae) => lae.checksum.equals(checksum));
|
||||
|
||||
return query.map((row) => row.toDto()).get();
|
||||
return query.map(mapToLocalAsset).get();
|
||||
}
|
||||
|
||||
Stream<LocalAsset?> watch(String id) => _assetSelectable(id).watchSingleOrNull();
|
||||
@ -79,7 +78,7 @@ class DriftLocalAssetRepository extends DriftDatabaseRepository {
|
||||
Future<LocalAsset?> getById(String id) {
|
||||
final query = _db.localAssetEntity.select()..where((lae) => lae.id.equals(id));
|
||||
|
||||
return query.map((row) => row.toDto()).getSingleOrNull();
|
||||
return query.map(mapToLocalAsset).getSingleOrNull();
|
||||
}
|
||||
|
||||
Future<int> getCount() {
|
||||
@ -106,7 +105,7 @@ class DriftLocalAssetRepository extends DriftDatabaseRepository {
|
||||
if (backupSelection != null) {
|
||||
query.where((lae) => lae.backupSelection.equalsValue(backupSelection));
|
||||
}
|
||||
return query.map((localAlbum) => localAlbum.toDto()).get();
|
||||
return query.map(mapToLocalAlbum).get();
|
||||
}
|
||||
|
||||
Future<Map<String, List<LocalAsset>>> getAssetsFromBackupAlbums(Iterable<String> remoteIds) async {
|
||||
@ -138,7 +137,7 @@ class DriftLocalAssetRepository extends DriftDatabaseRepository {
|
||||
|
||||
for (final row in rows) {
|
||||
final albumId = row.readTable(_db.localAlbumAssetEntity).albumId;
|
||||
final asset = row.readTable(_db.localAssetEntity).toDto();
|
||||
final asset = mapToLocalAsset(row.readTable(_db.localAssetEntity));
|
||||
(result[albumId] ??= <LocalAsset>[]).add(asset);
|
||||
}
|
||||
}
|
||||
@ -200,7 +199,7 @@ class DriftLocalAssetRepository extends DriftDatabaseRepository {
|
||||
query.where(whereClause);
|
||||
|
||||
final rows = await query.get();
|
||||
final assets = rows.map((row) => row.readTable(_db.localAssetEntity).toDto()).toList();
|
||||
final assets = rows.map((row) => mapToLocalAsset(row.readTable(_db.localAssetEntity))).toList();
|
||||
final totalBytes = rows.fold<int>(0, (sum, row) {
|
||||
final fileSize = row.readTableOrNull(_db.remoteExifEntity)?.fileSize;
|
||||
return sum + (fileSize ?? 0);
|
||||
@ -211,7 +210,7 @@ class DriftLocalAssetRepository extends DriftDatabaseRepository {
|
||||
|
||||
Future<List<LocalAsset>> getEmptyCloudIdAssets() {
|
||||
final query = _db.localAssetEntity.select()..where((row) => row.iCloudId.isNull());
|
||||
return query.map((row) => row.toDto()).get();
|
||||
return query.map(mapToLocalAsset).get();
|
||||
}
|
||||
|
||||
Future<void> reconcileHashesFromCloudId() async {
|
||||
|
||||
@ -7,9 +7,9 @@ import 'package:immich_mobile/domain/models/album/album.model.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/models/timeline.model.dart';
|
||||
import 'package:immich_mobile/domain/services/timeline.service.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/mapper.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/map.repository.dart';
|
||||
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||
@ -175,7 +175,9 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
|
||||
..limit(count, offset: offset);
|
||||
|
||||
return query
|
||||
.map((row) => row.readTable(_db.localAssetEntity).toDto(remoteId: row.read(_db.remoteAssetEntity.id)))
|
||||
.map(
|
||||
(row) => mapToLocalAsset(row.readTable(_db.localAssetEntity), remoteId: row.read(_db.remoteAssetEntity.id)),
|
||||
)
|
||||
.get();
|
||||
}
|
||||
|
||||
|
||||
@ -3,10 +3,10 @@ import 'package:drift/drift.dart';
|
||||
import 'package:immich_mobile/constants/constants.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/entities/local_asset.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/trashed_local_asset.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/trashed_local_asset.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/mapper.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||
|
||||
typedef TrashedAsset = ({String albumId, LocalAsset asset});
|
||||
@ -198,6 +198,9 @@ class DriftTrashedLocalAssetRepository extends DriftDatabaseRepository {
|
||||
isFavorite: Value(e.isFavorite),
|
||||
orientation: Value(e.orientation),
|
||||
playbackStyle: Value(e.playbackStyle),
|
||||
// getToRestore only restores assets whose album is selected
|
||||
// TODO: Refactor getToRestore to not assume that and remove the backup candidate flag from here
|
||||
isBackupCandidate: const Value(true),
|
||||
);
|
||||
});
|
||||
|
||||
@ -283,7 +286,7 @@ class DriftTrashedLocalAssetRepository extends DriftDatabaseRepository {
|
||||
|
||||
for (final row in rows) {
|
||||
final albumId = row.readTable(_db.localAlbumAssetEntity).albumId;
|
||||
final asset = row.readTable(_db.localAssetEntity).toDto();
|
||||
final asset = mapToLocalAsset(row.readTable(_db.localAssetEntity));
|
||||
(result[albumId] ??= <LocalAsset>[]).add(asset);
|
||||
}
|
||||
|
||||
|
||||
@ -14,11 +14,12 @@ import 'package:immich_mobile/domain/models/timeline.model.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/settings.entity.drift.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/network.repository.dart';
|
||||
import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart';
|
||||
import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart';
|
||||
|
||||
const int targetVersion = 26;
|
||||
const int targetVersion = 27;
|
||||
|
||||
Future<void> migrateDatabaseIfNeeded(Drift drift) async {
|
||||
final int version = Store.get(StoreKey.version, targetVersion);
|
||||
@ -31,6 +32,10 @@ Future<void> migrateDatabaseIfNeeded(Drift drift) async {
|
||||
await _migrateTo26(drift);
|
||||
}
|
||||
|
||||
if (version < 27) {
|
||||
await DriftLocalAlbumRepository(drift).recomputeAllBackupCandidates();
|
||||
}
|
||||
|
||||
await Store.put(StoreKey.version, targetVersion);
|
||||
return;
|
||||
}
|
||||
|
||||
Binary file not shown.
BIN
mobile/test/drift/main/generated/schema_v31.dart
Normal file
BIN
mobile/test/drift/main/generated/schema_v31.dart
Normal file
Binary file not shown.
@ -14,10 +14,6 @@ void main() {
|
||||
sut = DriftBackupRepository(ctx.db);
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await ctx.dispose();
|
||||
});
|
||||
|
||||
group('getAllCounts', () {
|
||||
late String userId;
|
||||
|
||||
|
||||
366
mobile/test/medium/repositories/local_album_repository_test.dart
Normal file
366
mobile/test/medium/repositories/local_album_repository_test.dart
Normal file
@ -0,0 +1,366 @@
|
||||
import 'package:drift/drift.dart' show TableStatements, Value;
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/mapper.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart';
|
||||
|
||||
import '../repository_context.dart';
|
||||
|
||||
void main() {
|
||||
late MediumRepositoryContext ctx;
|
||||
late DriftLocalAlbumRepository sut;
|
||||
|
||||
setUp(() {
|
||||
ctx = MediumRepositoryContext();
|
||||
sut = DriftLocalAlbumRepository(ctx.db);
|
||||
});
|
||||
|
||||
group('recomputeAllBackupCandidates', () {
|
||||
test('sets flag true when asset is only in a selected album', () async {
|
||||
final selected = await ctx.newLocalAlbum(backupSelection: BackupSelection.selected);
|
||||
final asset = await ctx.newLocalAsset();
|
||||
await ctx.newLocalAlbumAsset(albumId: selected.id, assetId: asset.id, recomputeBackupCandidates: false);
|
||||
|
||||
expect(await ctx.isAssetBackupCandidate(asset.id), isFalse);
|
||||
await sut.recomputeAllBackupCandidates();
|
||||
expect(await ctx.isAssetBackupCandidate(asset.id), isTrue);
|
||||
});
|
||||
|
||||
test('keeps flag false when asset is only in a none / excluded album', () async {
|
||||
final none = await ctx.newLocalAlbum(backupSelection: BackupSelection.none);
|
||||
final excluded = await ctx.newLocalAlbum(backupSelection: BackupSelection.excluded);
|
||||
final inNone = await ctx.newLocalAsset();
|
||||
final inExcluded = await ctx.newLocalAsset();
|
||||
await ctx.newLocalAlbumAsset(albumId: none.id, assetId: inNone.id, recomputeBackupCandidates: false);
|
||||
await ctx.newLocalAlbumAsset(albumId: excluded.id, assetId: inExcluded.id, recomputeBackupCandidates: false);
|
||||
|
||||
await sut.recomputeAllBackupCandidates();
|
||||
expect(await ctx.isAssetBackupCandidate(inNone.id), isFalse);
|
||||
expect(await ctx.isAssetBackupCandidate(inExcluded.id), isFalse);
|
||||
});
|
||||
|
||||
test('keeps flag false when asset is in both a selected and an excluded album', () async {
|
||||
final selected = await ctx.newLocalAlbum(backupSelection: BackupSelection.selected);
|
||||
final excluded = await ctx.newLocalAlbum(backupSelection: BackupSelection.excluded);
|
||||
final asset = await ctx.newLocalAsset();
|
||||
await ctx.newLocalAlbumAsset(albumId: selected.id, assetId: asset.id, recomputeBackupCandidates: false);
|
||||
await ctx.newLocalAlbumAsset(albumId: excluded.id, assetId: asset.id, recomputeBackupCandidates: false);
|
||||
|
||||
await sut.recomputeAllBackupCandidates();
|
||||
expect(await ctx.isAssetBackupCandidate(asset.id), isFalse);
|
||||
});
|
||||
|
||||
test('flipping selection to excluded flips a candidate back to false', () async {
|
||||
final album = await ctx.newLocalAlbum(backupSelection: BackupSelection.selected);
|
||||
final asset = await ctx.newLocalAsset();
|
||||
await ctx.newLocalAlbumAsset(albumId: album.id, assetId: asset.id, recomputeBackupCandidates: false);
|
||||
await sut.recomputeAllBackupCandidates();
|
||||
expect(await ctx.isAssetBackupCandidate(asset.id), isTrue);
|
||||
|
||||
await (ctx.db.localAlbumEntity.update()..where((row) => row.id.equals(album.id))).write(
|
||||
const LocalAlbumEntityCompanion(backupSelection: Value(BackupSelection.excluded)),
|
||||
);
|
||||
await sut.recomputeAllBackupCandidates();
|
||||
expect(await ctx.isAssetBackupCandidate(asset.id), isFalse);
|
||||
});
|
||||
});
|
||||
|
||||
group('recomputeBackupCandidatesForAlbum', () {
|
||||
test('only touches assets in the given album', () async {
|
||||
final albumA = await ctx.newLocalAlbum(backupSelection: BackupSelection.selected);
|
||||
final albumB = await ctx.newLocalAlbum(backupSelection: BackupSelection.selected);
|
||||
final inAlbumA = await ctx.newLocalAsset();
|
||||
final inAlbumB = await ctx.newLocalAsset();
|
||||
await ctx.newLocalAlbumAsset(albumId: albumA.id, assetId: inAlbumA.id, recomputeBackupCandidates: false);
|
||||
await ctx.newLocalAlbumAsset(albumId: albumB.id, assetId: inAlbumB.id, recomputeBackupCandidates: false);
|
||||
await sut.recomputeBackupCandidatesForAlbum(albumA.id);
|
||||
|
||||
expect(await ctx.isAssetBackupCandidate(inAlbumA.id), isTrue);
|
||||
expect(
|
||||
await ctx.isAssetBackupCandidate(inAlbumB.id),
|
||||
isFalse,
|
||||
reason: 'asset in album B should be untouched by an album A-scoped recompute',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('recomputeBackupCandidatesForAssets', () {
|
||||
test('only touches the listed assets', () async {
|
||||
final selected = await ctx.newLocalAlbum(backupSelection: BackupSelection.selected);
|
||||
final touched = await ctx.newLocalAsset();
|
||||
final untouched = await ctx.newLocalAsset();
|
||||
await ctx.newLocalAlbumAsset(albumId: selected.id, assetId: touched.id, recomputeBackupCandidates: false);
|
||||
await ctx.newLocalAlbumAsset(albumId: selected.id, assetId: untouched.id, recomputeBackupCandidates: false);
|
||||
await sut.recomputeBackupCandidatesForAssets([touched.id]);
|
||||
|
||||
expect(await ctx.isAssetBackupCandidate(touched.id), isTrue);
|
||||
expect(await ctx.isAssetBackupCandidate(untouched.id), isFalse);
|
||||
});
|
||||
});
|
||||
|
||||
group('upsert', () {
|
||||
test('flipping selection via upsert recomputes the album', () async {
|
||||
final asset = await ctx.newLocalAsset();
|
||||
final album = mapToLocalAlbum(await ctx.newLocalAlbum(backupSelection: BackupSelection.none), assetCount: 1);
|
||||
await ctx.newLocalAlbumAsset(albumId: album.id, assetId: asset.id, recomputeBackupCandidates: false);
|
||||
await sut.recomputeAllBackupCandidates();
|
||||
expect(await ctx.isAssetBackupCandidate(asset.id), isFalse);
|
||||
|
||||
await sut.upsert(album.copyWith(backupSelection: BackupSelection.selected, updatedAt: DateTime.now()));
|
||||
expect(await ctx.isAssetBackupCandidate(asset.id), isTrue);
|
||||
});
|
||||
});
|
||||
|
||||
group('delete (iOS)', () {
|
||||
setUp(() {
|
||||
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
|
||||
});
|
||||
tearDown(() {
|
||||
debugDefaultTargetPlatformOverride = null;
|
||||
});
|
||||
|
||||
test('flips candidate flag when the only selected album is deleted', () async {
|
||||
final selected = await ctx.newLocalAlbum(backupSelection: BackupSelection.selected);
|
||||
final other = await ctx.newLocalAlbum(backupSelection: BackupSelection.none);
|
||||
final asset = await ctx.newLocalAsset();
|
||||
await ctx.newLocalAlbumAsset(albumId: selected.id, assetId: asset.id, recomputeBackupCandidates: false);
|
||||
await ctx.newLocalAlbumAsset(albumId: other.id, assetId: asset.id, recomputeBackupCandidates: false);
|
||||
await sut.recomputeAllBackupCandidates();
|
||||
expect(await ctx.isAssetBackupCandidate(asset.id), isTrue);
|
||||
|
||||
await sut.delete(selected.id);
|
||||
expect(await ctx.isAssetBackupCandidate(asset.id), isFalse, reason: 'survivor lost its only selected membership');
|
||||
expect(await ctx.albumAssetCount(selected.id), 0, reason: 'memberships must reflect the now-empty album');
|
||||
expect(await ctx.hasLocalAlbum(selected.id), isTrue, reason: 'selected album row is preserved as a bookmark');
|
||||
});
|
||||
|
||||
test('keeps candidate flag when another selected album remains', () async {
|
||||
final albumA = await ctx.newLocalAlbum(backupSelection: BackupSelection.selected);
|
||||
final albumB = await ctx.newLocalAlbum(backupSelection: BackupSelection.selected);
|
||||
final asset = await ctx.newLocalAsset();
|
||||
await ctx.newLocalAlbumAsset(albumId: albumA.id, assetId: asset.id, recomputeBackupCandidates: false);
|
||||
await ctx.newLocalAlbumAsset(albumId: albumB.id, assetId: asset.id, recomputeBackupCandidates: false);
|
||||
await sut.recomputeAllBackupCandidates();
|
||||
expect(await ctx.isAssetBackupCandidate(asset.id), isTrue);
|
||||
|
||||
await sut.delete(albumA.id);
|
||||
expect(await ctx.isAssetBackupCandidate(asset.id), isTrue, reason: 'albumB still keeps the asset a candidate');
|
||||
expect(await ctx.albumAssetCount(albumA.id), 0);
|
||||
});
|
||||
|
||||
test('deleting an excluded album can flip a flag from false to true', () async {
|
||||
final selected = await ctx.newLocalAlbum(backupSelection: BackupSelection.selected);
|
||||
final excluded = await ctx.newLocalAlbum(backupSelection: BackupSelection.excluded);
|
||||
final asset = await ctx.newLocalAsset();
|
||||
await ctx.newLocalAlbumAsset(albumId: selected.id, assetId: asset.id, recomputeBackupCandidates: false);
|
||||
await ctx.newLocalAlbumAsset(albumId: excluded.id, assetId: asset.id, recomputeBackupCandidates: false);
|
||||
await sut.recomputeAllBackupCandidates();
|
||||
expect(
|
||||
await ctx.isAssetBackupCandidate(asset.id),
|
||||
isFalse,
|
||||
reason: 'excluded suppresses an otherwise-candidate asset',
|
||||
);
|
||||
|
||||
await sut.delete(excluded.id);
|
||||
expect(
|
||||
await ctx.isAssetBackupCandidate(asset.id),
|
||||
isTrue,
|
||||
reason: 'with excluded gone, the selected membership wins',
|
||||
);
|
||||
expect(await ctx.albumAssetCount(excluded.id), 0);
|
||||
expect(await ctx.hasLocalAlbum(excluded.id), isTrue, reason: 'excluded album row is preserved as a bookmark');
|
||||
});
|
||||
|
||||
test('deleting a none album does not flip flags, and the row is removed', () async {
|
||||
final selected = await ctx.newLocalAlbum(backupSelection: BackupSelection.selected);
|
||||
final none = await ctx.newLocalAlbum(backupSelection: BackupSelection.none);
|
||||
final asset = await ctx.newLocalAsset();
|
||||
await ctx.newLocalAlbumAsset(albumId: selected.id, assetId: asset.id, recomputeBackupCandidates: false);
|
||||
await ctx.newLocalAlbumAsset(albumId: none.id, assetId: asset.id, recomputeBackupCandidates: false);
|
||||
await sut.recomputeAllBackupCandidates();
|
||||
expect(await ctx.isAssetBackupCandidate(asset.id), isTrue);
|
||||
|
||||
await sut.delete(none.id);
|
||||
expect(
|
||||
await ctx.isAssetBackupCandidate(asset.id),
|
||||
isTrue,
|
||||
reason: 'none membership never affected the predicate',
|
||||
);
|
||||
expect(await ctx.albumAssetCount(none.id), 0);
|
||||
expect(await ctx.hasLocalAlbum(none.id), isFalse, reason: 'none album row is hard-deleted');
|
||||
});
|
||||
});
|
||||
|
||||
group('updateAll (iOS)', () {
|
||||
setUp(() {
|
||||
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
|
||||
});
|
||||
tearDown(() {
|
||||
debugDefaultTargetPlatformOverride = null;
|
||||
});
|
||||
|
||||
test('drops selected memberships when album removed from device', () async {
|
||||
final selected = mapToLocalAlbum(await ctx.newLocalAlbum(backupSelection: BackupSelection.selected));
|
||||
final kept = mapToLocalAlbum(await ctx.newLocalAlbum(backupSelection: BackupSelection.none));
|
||||
final asset = await ctx.newLocalAsset();
|
||||
await ctx.newLocalAlbumAsset(albumId: selected.id, assetId: asset.id, recomputeBackupCandidates: false);
|
||||
await ctx.newLocalAlbumAsset(albumId: kept.id, assetId: asset.id, recomputeBackupCandidates: false);
|
||||
await sut.recomputeAllBackupCandidates();
|
||||
expect(await ctx.isAssetBackupCandidate(asset.id), isTrue);
|
||||
|
||||
await sut.updateAll([kept]);
|
||||
expect(await ctx.isAssetBackupCandidate(asset.id), isFalse, reason: 'survivor lost its only selected membership');
|
||||
expect(await ctx.albumAssetCount(selected.id), 0, reason: 'selected album must be empty');
|
||||
expect(await ctx.hasLocalAlbum(selected.id), isTrue, reason: 'selected album row preserved as bookmark');
|
||||
});
|
||||
|
||||
test('drops excluded memberships when album removed from device', () async {
|
||||
final excluded = mapToLocalAlbum(await ctx.newLocalAlbum(backupSelection: BackupSelection.excluded));
|
||||
final selected = mapToLocalAlbum(await ctx.newLocalAlbum(backupSelection: BackupSelection.selected));
|
||||
final asset = await ctx.newLocalAsset();
|
||||
await ctx.newLocalAlbumAsset(albumId: excluded.id, assetId: asset.id, recomputeBackupCandidates: false);
|
||||
await ctx.newLocalAlbumAsset(albumId: selected.id, assetId: asset.id, recomputeBackupCandidates: false);
|
||||
await sut.recomputeAllBackupCandidates();
|
||||
expect(
|
||||
await ctx.isAssetBackupCandidate(asset.id),
|
||||
isFalse,
|
||||
reason: 'excluded suppresses an otherwise-candidate asset',
|
||||
);
|
||||
|
||||
await sut.updateAll([selected]);
|
||||
expect(await ctx.isAssetBackupCandidate(asset.id), isTrue, reason: 'with excluded gone, selected wins');
|
||||
expect(await ctx.albumAssetCount(excluded.id), 0);
|
||||
expect(await ctx.hasLocalAlbum(excluded.id), isTrue, reason: 'excluded album row preserved as a bookmark');
|
||||
});
|
||||
|
||||
test('removes none rows entirely and leaves untouched albums alone', () async {
|
||||
final selected = mapToLocalAlbum(await ctx.newLocalAlbum(backupSelection: BackupSelection.selected));
|
||||
final none = mapToLocalAlbum(await ctx.newLocalAlbum(backupSelection: BackupSelection.none));
|
||||
final asset = await ctx.newLocalAsset();
|
||||
await ctx.newLocalAlbumAsset(albumId: selected.id, assetId: asset.id, recomputeBackupCandidates: false);
|
||||
await ctx.newLocalAlbumAsset(albumId: none.id, assetId: asset.id, recomputeBackupCandidates: false);
|
||||
await sut.recomputeAllBackupCandidates();
|
||||
expect(await ctx.isAssetBackupCandidate(asset.id), isTrue);
|
||||
|
||||
await sut.updateAll([selected]);
|
||||
|
||||
expect(
|
||||
await ctx.isAssetBackupCandidate(asset.id),
|
||||
isTrue,
|
||||
reason: 'none membership never affected the predicate',
|
||||
);
|
||||
expect(await ctx.hasLocalAlbum(none.id), isFalse, reason: 'none removed rows are hard-deleted');
|
||||
expect(await ctx.hasLocalAlbum(selected.id), isTrue);
|
||||
expect(await ctx.albumAssetCount(selected.id), 1, reason: 'kept album retains its membership');
|
||||
});
|
||||
});
|
||||
|
||||
group('delete (Android)', () {
|
||||
setUp(() {
|
||||
debugDefaultTargetPlatformOverride = TargetPlatform.android;
|
||||
});
|
||||
tearDown(() {
|
||||
debugDefaultTargetPlatformOverride = null;
|
||||
});
|
||||
|
||||
test('hard-deletes all member assets and preserves a selected album as a bookmark', () async {
|
||||
final selected = await ctx.newLocalAlbum(backupSelection: BackupSelection.selected);
|
||||
final other = await ctx.newLocalAlbum(backupSelection: BackupSelection.selected);
|
||||
final assetA = await ctx.newLocalAsset();
|
||||
final assetB = await ctx.newLocalAsset();
|
||||
final assetC = await ctx.newLocalAsset();
|
||||
await ctx.newLocalAlbumAsset(albumId: selected.id, assetId: assetA.id, recomputeBackupCandidates: false);
|
||||
await ctx.newLocalAlbumAsset(albumId: selected.id, assetId: assetB.id, recomputeBackupCandidates: false);
|
||||
await ctx.newLocalAlbumAsset(albumId: other.id, assetId: assetC.id, recomputeBackupCandidates: false);
|
||||
await sut.recomputeAllBackupCandidates();
|
||||
expect(await ctx.isAssetBackupCandidate(assetA.id), isTrue);
|
||||
|
||||
await sut.delete(selected.id);
|
||||
expect(await ctx.hasLocalAsset(assetA.id), isFalse, reason: 'Android removes every member asset of the album');
|
||||
expect(await ctx.hasLocalAsset(assetB.id), isFalse, reason: 'Android removes every member asset of the album');
|
||||
expect(await ctx.albumAssetCount(selected.id), 0, reason: 'cascade clears memberships of deleted assets');
|
||||
expect(await ctx.hasLocalAlbum(selected.id), isTrue, reason: 'selected album row is preserved as a bookmark');
|
||||
expect(await ctx.hasLocalAsset(assetC.id), isTrue, reason: 'a different album keeps its own distinct assets');
|
||||
});
|
||||
|
||||
test('hard-deletes member assets and removes a none album row', () async {
|
||||
final none = await ctx.newLocalAlbum(backupSelection: BackupSelection.none);
|
||||
final asset = await ctx.newLocalAsset();
|
||||
await ctx.newLocalAlbumAsset(albumId: none.id, assetId: asset.id, recomputeBackupCandidates: false);
|
||||
|
||||
await sut.delete(none.id);
|
||||
expect(await ctx.hasLocalAsset(asset.id), isFalse);
|
||||
expect(await ctx.albumAssetCount(none.id), 0);
|
||||
expect(await ctx.hasLocalAlbum(none.id), isFalse, reason: 'none album row is hard-deleted');
|
||||
});
|
||||
});
|
||||
|
||||
group('updateAll (Android)', () {
|
||||
setUp(() {
|
||||
debugDefaultTargetPlatformOverride = TargetPlatform.android;
|
||||
});
|
||||
tearDown(() {
|
||||
debugDefaultTargetPlatformOverride = null;
|
||||
});
|
||||
|
||||
test('hard-deletes assets of a removed selected album, preserving its row as a bookmark', () async {
|
||||
final removed = mapToLocalAlbum(await ctx.newLocalAlbum(backupSelection: BackupSelection.selected));
|
||||
final kept = mapToLocalAlbum(await ctx.newLocalAlbum(backupSelection: BackupSelection.selected));
|
||||
final removedAsset = await ctx.newLocalAsset();
|
||||
final keptAsset = await ctx.newLocalAsset();
|
||||
await ctx.newLocalAlbumAsset(albumId: removed.id, assetId: removedAsset.id, recomputeBackupCandidates: false);
|
||||
await ctx.newLocalAlbumAsset(albumId: kept.id, assetId: keptAsset.id, recomputeBackupCandidates: false);
|
||||
await sut.recomputeAllBackupCandidates();
|
||||
await sut.updateAll([kept]);
|
||||
|
||||
expect(
|
||||
await ctx.hasLocalAsset(removedAsset.id),
|
||||
isFalse,
|
||||
reason: 'removed album assets are gone from the device',
|
||||
);
|
||||
expect(await ctx.albumAssetCount(removed.id), 0, reason: 'cascade clears the removed album memberships');
|
||||
expect(await ctx.hasLocalAlbum(removed.id), isTrue, reason: 'selected removed row preserved as bookmark');
|
||||
expect(await ctx.isAssetBackupCandidate(keptAsset.id), isTrue, reason: 'surviving album assets are untouched');
|
||||
expect(await ctx.albumAssetCount(kept.id), 1);
|
||||
});
|
||||
|
||||
test('hard-deletes assets of a removed none album and removes the row', () async {
|
||||
final removed = mapToLocalAlbum(await ctx.newLocalAlbum(backupSelection: BackupSelection.none));
|
||||
final kept = mapToLocalAlbum(await ctx.newLocalAlbum(backupSelection: BackupSelection.selected));
|
||||
final removedAsset = await ctx.newLocalAsset();
|
||||
await ctx.newLocalAlbumAsset(albumId: removed.id, assetId: removedAsset.id, recomputeBackupCandidates: false);
|
||||
|
||||
await sut.updateAll([kept]);
|
||||
|
||||
expect(await ctx.hasLocalAsset(removedAsset.id), isFalse);
|
||||
expect(await ctx.hasLocalAlbum(removed.id), isFalse, reason: 'none removed rows are hard-deleted');
|
||||
expect(await ctx.hasLocalAlbum(kept.id), isTrue);
|
||||
});
|
||||
});
|
||||
|
||||
group('upsert selection flips', () {
|
||||
test('flipping selected to excluded flips a candidate back to false', () async {
|
||||
final asset = await ctx.newLocalAsset();
|
||||
final album = mapToLocalAlbum(await ctx.newLocalAlbum(backupSelection: BackupSelection.selected), assetCount: 1);
|
||||
await ctx.newLocalAlbumAsset(albumId: album.id, assetId: asset.id, recomputeBackupCandidates: false);
|
||||
await sut.recomputeAllBackupCandidates();
|
||||
expect(await ctx.isAssetBackupCandidate(asset.id), isTrue);
|
||||
|
||||
await sut.upsert(album.copyWith(backupSelection: BackupSelection.excluded, updatedAt: DateTime.now()));
|
||||
expect(await ctx.isAssetBackupCandidate(asset.id), isFalse);
|
||||
});
|
||||
|
||||
test('flipping selected to none flips a candidate back to false', () async {
|
||||
final asset = await ctx.newLocalAsset();
|
||||
final album = mapToLocalAlbum(await ctx.newLocalAlbum(backupSelection: BackupSelection.selected), assetCount: 1);
|
||||
await ctx.newLocalAlbumAsset(albumId: album.id, assetId: asset.id, recomputeBackupCandidates: false);
|
||||
await sut.recomputeAllBackupCandidates();
|
||||
expect(await ctx.isAssetBackupCandidate(asset.id), isTrue);
|
||||
|
||||
await sut.upsert(album.copyWith(backupSelection: BackupSelection.none, updatedAt: DateTime.now()));
|
||||
expect(await ctx.isAssetBackupCandidate(asset.id), isFalse);
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -15,10 +15,6 @@ void main() {
|
||||
sut = DriftLocalAssetRepository(ctx.db);
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await ctx.dispose();
|
||||
});
|
||||
|
||||
group('getRemovalCandidates', () {
|
||||
final cutoffDate = DateTime(2024, 1, 1);
|
||||
final beforeCutoff = DateTime(2023, 12, 31);
|
||||
|
||||
@ -12,10 +12,6 @@ void main() {
|
||||
sut = PartnerRepository(ctx.db);
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await ctx.dispose();
|
||||
});
|
||||
|
||||
group('search', () {
|
||||
test('sharedBy returns users the current user shares their library to', () async {
|
||||
final me = await ctx.newUser();
|
||||
|
||||
@ -12,10 +12,6 @@ void main() {
|
||||
sut = DriftPeopleRepository(ctx.db);
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await ctx.dispose();
|
||||
});
|
||||
|
||||
group('getAssetPeople', () {
|
||||
test('does not duplicate a person with multiple face records on the same asset', () async {
|
||||
// Regression check for #20585: a join on asset_face_entity returned one row
|
||||
|
||||
@ -13,10 +13,6 @@ void main() {
|
||||
sut = DriftRemoteAlbumRepository(ctx.db);
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await ctx.dispose();
|
||||
});
|
||||
|
||||
group('addAssets', () {
|
||||
test('sets the first added asset as thumbnail when the album has no thumbnail', () async {
|
||||
final user = await ctx.newUser();
|
||||
|
||||
@ -16,10 +16,6 @@ void main() {
|
||||
sut = await SettingsRepository.ensureInitialized(ctx.db);
|
||||
});
|
||||
|
||||
tearDownAll(() async {
|
||||
await ctx.dispose();
|
||||
});
|
||||
|
||||
setUp(() async {
|
||||
await ctx.db.delete(ctx.db.settingsEntity).go();
|
||||
await SettingsRepository.instance.refresh();
|
||||
|
||||
@ -18,10 +18,6 @@ void main() {
|
||||
sut = DriftTimelineRepository(ctx.db);
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await ctx.dispose();
|
||||
});
|
||||
|
||||
group('remoteAlbum assets', () {
|
||||
test('no duplicate assets when identical checksum appears in multiple local asset rows', () async {
|
||||
// Regression check for #23273: a LEFT OUTER JOIN on checksum would fan out and create duplicates
|
||||
|
||||
@ -0,0 +1,93 @@
|
||||
import 'package:drift/drift.dart' show TableOrViewStatements;
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
||||
import 'package:immich_mobile/infrastructure/mapper.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/trashed_local_asset.repository.dart';
|
||||
|
||||
import '../repository_context.dart';
|
||||
|
||||
void main() {
|
||||
late MediumRepositoryContext ctx;
|
||||
late DriftTrashedLocalAssetRepository sut;
|
||||
|
||||
setUp(() {
|
||||
ctx = MediumRepositoryContext();
|
||||
sut = DriftTrashedLocalAssetRepository(ctx.db);
|
||||
});
|
||||
|
||||
Future<int> trashedCount(String assetId) async =>
|
||||
await (ctx.db.trashedLocalAssetEntity.count(where: (row) => row.id.equals(assetId))).getSingle();
|
||||
|
||||
group('trash and restore lifecycle', () {
|
||||
test('trashing a candidate removes the local asset and cascades its album membership', () async {
|
||||
final album = await ctx.newLocalAlbum(backupSelection: BackupSelection.selected);
|
||||
final asset = await ctx.newLocalAsset();
|
||||
await ctx.newLocalAlbumAsset(albumId: album.id, assetId: asset.id);
|
||||
|
||||
expect(
|
||||
await ctx.isAssetBackupCandidate(asset.id),
|
||||
isTrue,
|
||||
reason: 'asset in a selected album starts as a candidate',
|
||||
);
|
||||
expect(await ctx.albumAssetCount(album.id), 1);
|
||||
|
||||
await sut.trashLocalAsset({
|
||||
album.id: [mapToLocalAsset(asset)],
|
||||
});
|
||||
|
||||
expect(await ctx.hasLocalAsset(asset.id), isFalse, reason: 'row moves out of local_asset_entity');
|
||||
expect(await ctx.albumAssetCount(album.id), 0, reason: 'FK cascade removes the album membership');
|
||||
expect(await trashedCount(asset.id), 1, reason: 'asset now lives in the trashed table');
|
||||
});
|
||||
|
||||
test('restoring re-inserts the asset and marks it a backup candidate', () async {
|
||||
final album = await ctx.newLocalAlbum(backupSelection: BackupSelection.selected);
|
||||
final asset = await ctx.newLocalAsset();
|
||||
await ctx.newLocalAlbumAsset(albumId: album.id, assetId: asset.id);
|
||||
await sut.trashLocalAsset({
|
||||
album.id: [mapToLocalAsset(asset)],
|
||||
});
|
||||
|
||||
await sut.applyRestoredAssets([asset.id]);
|
||||
|
||||
expect(await ctx.hasLocalAsset(asset.id), isTrue, reason: 'row returns to local_asset_entity');
|
||||
expect(await trashedCount(asset.id), 0, reason: 'trashed row is consumed');
|
||||
expect(await ctx.isAssetBackupCandidate(asset.id), isTrue, reason: 'restored asset is a candidate again');
|
||||
expect(await ctx.albumAssetCount(album.id), 0, reason: 'restore does not re-link membership');
|
||||
});
|
||||
});
|
||||
|
||||
group('getToRestore', () {
|
||||
test('returns trashed assets whose album is selected and whose remote copy is live again', () async {
|
||||
const checksum = 'shared-checksum';
|
||||
final owner = await ctx.newUser();
|
||||
final selected = await ctx.newLocalAlbum(backupSelection: BackupSelection.selected);
|
||||
final asset = await ctx.newLocalAsset(checksum: checksum);
|
||||
await ctx.newLocalAlbumAsset(albumId: selected.id, assetId: asset.id);
|
||||
await ctx.newRemoteAsset(checksum: checksum, ownerId: owner.id);
|
||||
await sut.trashLocalAsset({
|
||||
selected.id: [mapToLocalAsset(asset)],
|
||||
});
|
||||
|
||||
final toRestore = await sut.getToRestore();
|
||||
|
||||
expect(toRestore.map((a) => a.id), contains(asset.id));
|
||||
});
|
||||
|
||||
test('ignores trashed assets whose album is not selected', () async {
|
||||
const checksum = 'shared-checksum';
|
||||
final owner = await ctx.newUser();
|
||||
final none = await ctx.newLocalAlbum(backupSelection: BackupSelection.none);
|
||||
final asset = await ctx.newLocalAsset(checksum: checksum);
|
||||
await ctx.newLocalAlbumAsset(albumId: none.id, assetId: asset.id);
|
||||
await ctx.newRemoteAsset(checksum: checksum, ownerId: owner.id);
|
||||
await sut.trashLocalAsset({
|
||||
none.id: [mapToLocalAsset(asset)],
|
||||
});
|
||||
|
||||
final toRestore = await sut.getToRestore();
|
||||
|
||||
expect(toRestore, isEmpty);
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:drift/native.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
||||
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
@ -17,6 +18,7 @@ import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_asset_cloud_id.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart';
|
||||
import 'package:immich_mobile/utils/option.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
@ -25,12 +27,29 @@ import '../utils.dart';
|
||||
class MediumRepositoryContext {
|
||||
final Drift db;
|
||||
|
||||
MediumRepositoryContext() : db = Drift(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true));
|
||||
MediumRepositoryContext() : db = Drift(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true)) {
|
||||
addTearDown(dispose);
|
||||
}
|
||||
|
||||
Future<void> dispose() async {
|
||||
await db.close();
|
||||
}
|
||||
|
||||
Future<bool> isAssetBackupCandidate(String assetId) async =>
|
||||
await (db.localAssetEntity.select()..where((row) => row.id.equals(assetId)))
|
||||
.map((row) => row.isBackupCandidate)
|
||||
.getSingleOrNull() ==
|
||||
true;
|
||||
|
||||
Future<bool> hasLocalAsset(String assetId) async =>
|
||||
await (db.localAssetEntity.count(where: (row) => row.id.equals(assetId))).getSingle() == 1;
|
||||
|
||||
Future<bool> hasLocalAlbum(String albumId) async =>
|
||||
await (db.localAlbumEntity.count(where: (row) => row.id.equals(albumId))).getSingle() == 1;
|
||||
|
||||
Future<int> albumAssetCount(String albumId) async =>
|
||||
await (db.localAlbumAssetEntity.count(where: (row) => row.albumId.equals(albumId))).getSingle();
|
||||
|
||||
static Value<T> _resolveUndefined<T>(T? plain, Option<T>? option, T fallback) {
|
||||
if (plain != null) {
|
||||
return .new(plain);
|
||||
@ -214,8 +233,8 @@ class MediumRepositoryContext {
|
||||
}
|
||||
|
||||
Future<AssetFaceEntityData> newFace({String? assetId, String? personId, int? imageWidth, int? imageHeight}) {
|
||||
imageWidth ??= TestUtils.randInt(999) + 1;
|
||||
imageHeight ??= TestUtils.randInt(999) + 1;
|
||||
imageWidth ??= TestUtils.randInt(998) + 2;
|
||||
imageHeight ??= TestUtils.randInt(998) + 2;
|
||||
|
||||
final x1 = TestUtils.randInt(imageWidth - 1);
|
||||
final y1 = TestUtils.randInt(imageHeight - 1);
|
||||
@ -306,7 +325,16 @@ class MediumRepositoryContext {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> newLocalAlbumAsset({required String albumId, required String assetId}) => db
|
||||
.into(db.localAlbumAssetEntity)
|
||||
.insert(LocalAlbumAssetEntityCompanion(albumId: .new(albumId), assetId: .new(assetId)));
|
||||
Future<void> newLocalAlbumAsset({
|
||||
required String albumId,
|
||||
required String assetId,
|
||||
bool recomputeBackupCandidates = true,
|
||||
}) async {
|
||||
await db
|
||||
.into(db.localAlbumAssetEntity)
|
||||
.insert(LocalAlbumAssetEntityCompanion(albumId: .new(albumId), assetId: .new(assetId)));
|
||||
if (recomputeBackupCandidates) {
|
||||
await DriftLocalAlbumRepository(db).recomputeBackupCandidatesForAssets([assetId]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,10 +13,6 @@ void main() {
|
||||
sut = PartnerService(ctx.userRepository, ctx.partnerRepository, ctx.partnerApi);
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await ctx.dispose();
|
||||
});
|
||||
|
||||
group('getCandidates', () {
|
||||
test('returns the other users and excludes the current user', () async {
|
||||
final me = await ctx.newUser();
|
||||
|
||||
Loading…
Reference in New Issue
Block a user