mirror of
https://github.com/immich-app/immich.git
synced 2026-06-15 19:39:20 -07:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6823bc9d72 |
@@ -104,7 +104,6 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
|
||||
Future<void> onAndroidUpload(int? maxMinutes) async {
|
||||
final hashTimeout = Duration(minutes: _isBackupEnabled ? 3 : 6);
|
||||
final backupTimeout = maxMinutes != null ? Duration(minutes: maxMinutes - 1) : null;
|
||||
await _optimizeDB();
|
||||
return _backgroundLoop(
|
||||
hashTimeout: hashTimeout,
|
||||
backupTimeout: backupTimeout,
|
||||
@@ -124,11 +123,6 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only for Background Processing tasks
|
||||
if (maxSeconds == null) {
|
||||
await _optimizeDB();
|
||||
}
|
||||
|
||||
// Run sync local, sync remote, hash and backup concurrently so the bg
|
||||
// refresh task (20s budget) can make progress on all four instead of
|
||||
// racing them sequentially. Phases are independent at the data layer:
|
||||
@@ -199,14 +193,6 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _optimizeDB() async {
|
||||
try {
|
||||
await (_drift.optimize(allTables: true), _driftLogger.optimize()).wait;
|
||||
} catch (error, stack) {
|
||||
dPrint(() => "Error during background worker optimize: $error, $stack");
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _cleanup() async {
|
||||
await runZonedGuarded(_handleCleanup, (error, stack) {
|
||||
dPrint(() => "Error during background worker cleanup: $error, $stack");
|
||||
@@ -235,7 +221,7 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
|
||||
if (nativeSyncApi != null) nativeSyncApi.cancelHashing(),
|
||||
]);
|
||||
await workerManagerPatch.dispose().catchError((_) async {});
|
||||
await Future.wait([LogService.I.dispose(), Store.dispose()]);
|
||||
await Future.wait([LogService.I.dispose(), Store.dispose(), _drift.optimize(allTables: true)]);
|
||||
await _drift.close();
|
||||
await _driftLogger.close();
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ import 'package:drift/drift.dart';
|
||||
import 'package:drift_sqlite_async/drift_sqlite_async.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/log.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/logger_db.repository.drift.dart';
|
||||
import 'package:immich_mobile/utils/debug_print.dart';
|
||||
import 'package:sqlite_async/sqlite_async.dart';
|
||||
|
||||
@DriftDatabase(tables: [LogMessageEntity])
|
||||
@@ -14,14 +13,6 @@ class DriftLogger extends $DriftLogger {
|
||||
@override
|
||||
int get schemaVersion => 1;
|
||||
|
||||
Future<void> optimize() async {
|
||||
try {
|
||||
await customStatement('PRAGMA optimize=0x10002');
|
||||
} catch (error) {
|
||||
dPrint(() => 'Failed to optimize logger database: $error');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
MigrationStrategy get migration => MigrationStrategy(
|
||||
beforeOpen: (details) async {
|
||||
|
||||
+4
-4
@@ -366,18 +366,18 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: drift
|
||||
sha256: "8033500116b24398fba0cca0369cc31678cd627c01e41753a61186911cea743e"
|
||||
sha256: "6cc0b623c0e83f7080524d8396e9301b1d78b9c66a4fdceeb0f798211303254c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.33.0"
|
||||
version: "2.34.0"
|
||||
drift_dev:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: drift_dev
|
||||
sha256: b3dd5b75e30522a91da8abda9f5bb17230cb038097f6d15fa75d42bb563428aa
|
||||
sha256: "9cfff1576b49725da0d32c040651a41ae195e8c4af8d8da301593e41d7abc2f7"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.33.0"
|
||||
version: "2.34.0"
|
||||
drift_sqlite_async:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
||||
+2
-2
@@ -19,7 +19,7 @@ dependencies:
|
||||
crypto: ^3.0.7
|
||||
device_info_plus: ^12.4.0
|
||||
diacritic: ^0.1.6
|
||||
drift: ^2.32.1
|
||||
drift: ^2.34.0
|
||||
drift_sqlite_async: 0.3.1
|
||||
dynamic_color: ^1.8.1
|
||||
easy_localization: ^3.0.8
|
||||
@@ -96,7 +96,7 @@ dev_dependencies:
|
||||
auto_route_generator: ^10.5.0
|
||||
build_runner: ^2.13.1
|
||||
# Drift generator
|
||||
drift_dev: ^2.32.1
|
||||
drift_dev: ^2.34.0
|
||||
fake_async: ^1.3.3
|
||||
file: ^7.0.1 # for MemoryFileSystem
|
||||
flutter_launcher_icons: ^0.14.4
|
||||
|
||||
@@ -369,26 +369,6 @@ describe(DuplicateService.name, () => {
|
||||
expect(mocks.job.queueAll).toHaveBeenCalledWith([{ name: JobName.SidecarWrite, data: { id: asset1.id } }]);
|
||||
});
|
||||
|
||||
it('should not merge metadata when multiple assets are kept', async () => {
|
||||
const asset1 = AssetFactory.create({ isFavorite: true });
|
||||
const asset2 = AssetFactory.create();
|
||||
mocks.access.duplicate.checkOwnerAccess.mockResolvedValue(new Set(['group-1']));
|
||||
mocks.duplicateRepository.get.mockResolvedValue({
|
||||
duplicateId: 'group-1',
|
||||
assets: [asset1 as unknown as MapAsset, asset2 as unknown as MapAsset],
|
||||
});
|
||||
|
||||
const result = await sut.resolve(authStub.admin, {
|
||||
groups: [{ duplicateId: 'group-1', keepAssetIds: [asset1.id, asset2.id], trashAssetIds: [] }],
|
||||
});
|
||||
|
||||
expect(result[0].success).toBe(true);
|
||||
expect(mocks.album.addAssetIdsToAlbums).not.toHaveBeenCalled();
|
||||
expect(mocks.tag.replaceAssetTags).not.toHaveBeenCalled();
|
||||
expect(mocks.asset.updateAllExif).not.toHaveBeenCalled();
|
||||
expect(mocks.asset.updateAll).toHaveBeenCalledWith([asset1.id, asset2.id], { duplicateId: null });
|
||||
});
|
||||
|
||||
// NOTE: The following integration-style tests are covered by E2E tests instead
|
||||
// to avoid complex mock setup. The validation and error-handling logic above
|
||||
// is thoroughly unit tested.
|
||||
|
||||
@@ -156,51 +156,51 @@ export class DuplicateService extends BaseService {
|
||||
}
|
||||
}
|
||||
|
||||
// Only merge metadata into the keeper when exactly one asset can absorb trashed duplicates.
|
||||
if (idsToKeep.length === 1 && idsToTrash.length > 0) {
|
||||
const assetAlbumMap = await this.albumRepository.getByAssetIds(auth.user.id, [...groupAssetIds]);
|
||||
const assetAlbumMap = await this.albumRepository.getByAssetIds(auth.user.id, [...groupAssetIds]);
|
||||
|
||||
const { assetUpdate, exifUpdate, mergedAlbumIds, mergedTagIds, mergedTagValues } = this.getSyncMergeResult(
|
||||
duplicateGroup.assets,
|
||||
assetAlbumMap,
|
||||
);
|
||||
const { assetUpdate, exifUpdate, mergedAlbumIds, mergedTagIds, mergedTagValues } = this.getSyncMergeResult(
|
||||
duplicateGroup.assets,
|
||||
assetAlbumMap,
|
||||
);
|
||||
|
||||
if (mergedAlbumIds.length > 0) {
|
||||
const allowedAlbumIds = await this.checkAccess({
|
||||
auth,
|
||||
permission: Permission.AlbumAssetCreate,
|
||||
ids: mergedAlbumIds,
|
||||
});
|
||||
if (mergedAlbumIds.length > 0) {
|
||||
const allowedAlbumIds = await this.checkAccess({
|
||||
auth,
|
||||
permission: Permission.AlbumAssetCreate,
|
||||
ids: mergedAlbumIds,
|
||||
});
|
||||
|
||||
const allowedShareIds = await this.checkAccess({
|
||||
auth,
|
||||
permission: Permission.AssetShare,
|
||||
ids: idsToKeep,
|
||||
});
|
||||
const allowedShareIds = await this.checkAccess({
|
||||
auth,
|
||||
permission: Permission.AssetShare,
|
||||
ids: idsToKeep,
|
||||
});
|
||||
|
||||
if (allowedAlbumIds.size > 0 && allowedShareIds.size > 0) {
|
||||
await this.albumRepository.addAssetIdsToAlbums(
|
||||
[...allowedAlbumIds].flatMap((albumId) => [...allowedShareIds].map((assetId) => ({ albumId, assetId }))),
|
||||
);
|
||||
}
|
||||
if (allowedAlbumIds.size > 0 && allowedShareIds.size > 0) {
|
||||
await this.albumRepository.addAssetIdsToAlbums(
|
||||
[...allowedAlbumIds].flatMap((albumId) => [...allowedShareIds].map((assetId) => ({ albumId, assetId }))),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (mergedTagIds.length > 0) {
|
||||
const allowedTagIds = await this.checkAccess({
|
||||
auth,
|
||||
permission: Permission.TagAsset,
|
||||
ids: mergedTagIds,
|
||||
});
|
||||
if (mergedTagIds.length > 0) {
|
||||
const allowedTagIds = await this.checkAccess({
|
||||
auth,
|
||||
permission: Permission.TagAsset,
|
||||
ids: mergedTagIds,
|
||||
});
|
||||
|
||||
if (allowedTagIds.size > 0) {
|
||||
await Promise.all(
|
||||
idsToKeep.map((assetId) => this.tagRepository.replaceAssetTags(assetId, [...allowedTagIds])),
|
||||
);
|
||||
if (allowedTagIds.size > 0) {
|
||||
// Replace tags for each keeper asset to ensure all merged tags are applied
|
||||
await Promise.all(idsToKeep.map((assetId) => this.tagRepository.replaceAssetTags(assetId, [...allowedTagIds])));
|
||||
|
||||
await this.assetRepository.updateAllExif(idsToKeep, { tags: mergedTagValues });
|
||||
}
|
||||
// Update asset_exif.tags so the subsequent SidecarWrite + MetadataExtraction
|
||||
// cycle preserves the merged tags (updateAllExif locks the property automatically)
|
||||
await this.assetRepository.updateAllExif(idsToKeep, { tags: mergedTagValues });
|
||||
}
|
||||
}
|
||||
|
||||
if (idsToKeep.length > 0) {
|
||||
const hasExifUpdate = Object.keys(exifUpdate).length > 0;
|
||||
const hasTagUpdate = mergedTagIds.length > 0;
|
||||
|
||||
@@ -213,8 +213,6 @@ export class DuplicateService extends BaseService {
|
||||
}
|
||||
|
||||
await this.assetRepository.updateAll(idsToKeep, { duplicateId: null, ...assetUpdate });
|
||||
} else if (idsToKeep.length > 0) {
|
||||
await this.assetRepository.updateAll(idsToKeep, { duplicateId: null });
|
||||
}
|
||||
|
||||
if (idsToTrash.length > 0) {
|
||||
|
||||
Reference in New Issue
Block a user