mirror of
https://github.com/samsonjs/immich.git
synced 2026-04-27 15:07:45 +00:00
fix: free up space using small batch size to reliably work on Android (#26047)
* fix: free up space delete in small batch * fix: free up space delete in small batch
This commit is contained in:
parent
8a9b541dd0
commit
57485023ae
3 changed files with 89 additions and 6 deletions
|
|
@ -66,7 +66,7 @@ Now make sure that the local album is selected in the backup screen (steps 1-2 a
|
||||||
- **Keep on device:** You can choose to restrict removal to `Always keep` **All photos** or **All videos**, regardless of other settings. This setting can hamper freeing up space significantly — with 80 GB of videos and 40 GB photos, selecting `Always keep photos` retains thousands of photos on your device.
|
- **Keep on device:** You can choose to restrict removal to `Always keep` **All photos** or **All videos**, regardless of other settings. This setting can hamper freeing up space significantly — with 80 GB of videos and 40 GB photos, selecting `Always keep photos` retains thousands of photos on your device.
|
||||||
|
|
||||||
2. **Scan & Review:** Before any files are removed, you are presented with a review screen to verify which items will be deleted and how much storage is reclamable.
|
2. **Scan & Review:** Before any files are removed, you are presented with a review screen to verify which items will be deleted and how much storage is reclamable.
|
||||||
3. **Deletion:** Confirmed items are moved to your device's native Trash/Recycle Bin.
|
3. **Deletion:** Confirmed items are moved to your device's native Trash/Recycle Bin. For large queues, Immich processes deletion in batches for stability (`2000` assets per batch on Android, `10000` per batch on iOS).
|
||||||
|
|
||||||
:::info reclaim storage
|
:::info reclaim storage
|
||||||
To use the reclaimed space right away, you must empty the system/gallery trash manually outside of Immich.
|
To use the reclaimed space right away, you must empty the system/gallery trash manually outside of Immich.
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/constants/enums.dart';
|
import 'package:immich_mobile/constants/enums.dart';
|
||||||
|
import 'package:immich_mobile/extensions/platform_extensions.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
|
||||||
import 'package:immich_mobile/repositories/asset_media.repository.dart';
|
import 'package:immich_mobile/repositories/asset_media.repository.dart';
|
||||||
|
|
@ -9,6 +10,8 @@ final cleanupServiceProvider = Provider<CleanupService>((ref) {
|
||||||
});
|
});
|
||||||
|
|
||||||
class CleanupService {
|
class CleanupService {
|
||||||
|
static final int _deleteBatchSize = CurrentPlatform.isAndroid ? 2000 : 10000;
|
||||||
|
|
||||||
final DriftLocalAssetRepository _localAssetRepository;
|
final DriftLocalAssetRepository _localAssetRepository;
|
||||||
final AssetMediaRepository _assetMediaRepository;
|
final AssetMediaRepository _assetMediaRepository;
|
||||||
|
|
||||||
|
|
@ -35,13 +38,20 @@ class CleanupService {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
final deletedIds = await _assetMediaRepository.deleteAll(localIds);
|
int deletedCount = 0;
|
||||||
if (deletedIds.isNotEmpty) {
|
|
||||||
await _localAssetRepository.delete(deletedIds);
|
for (int index = 0; index < localIds.length; index += _deleteBatchSize) {
|
||||||
return deletedIds.length;
|
final end = index + _deleteBatchSize < localIds.length ? index + _deleteBatchSize : localIds.length;
|
||||||
|
final batch = localIds.sublist(index, end);
|
||||||
|
|
||||||
|
final deletedIds = await _assetMediaRepository.deleteAll(batch);
|
||||||
|
if (deletedIds.isNotEmpty) {
|
||||||
|
await _localAssetRepository.delete(deletedIds);
|
||||||
|
deletedCount += deletedIds.length;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0;
|
return deletedCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns album IDs that should be kept by default (e.g., messaging app albums)
|
/// Returns album IDs that should be kept by default (e.g., messaging app albums)
|
||||||
|
|
|
||||||
73
mobile/test/services/cleanup.service_test.dart
Normal file
73
mobile/test/services/cleanup.service_test.dart
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:immich_mobile/extensions/platform_extensions.dart';
|
||||||
|
import 'package:immich_mobile/services/cleanup.service.dart';
|
||||||
|
import 'package:mocktail/mocktail.dart';
|
||||||
|
|
||||||
|
import '../infrastructure/repository.mock.dart';
|
||||||
|
import '../repository.mocks.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
late CleanupService sut;
|
||||||
|
|
||||||
|
late MockDriftLocalAssetRepository localAssetRepository;
|
||||||
|
late MockAssetMediaRepository assetMediaRepository;
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
localAssetRepository = MockDriftLocalAssetRepository();
|
||||||
|
assetMediaRepository = MockAssetMediaRepository();
|
||||||
|
sut = CleanupService(localAssetRepository, assetMediaRepository);
|
||||||
|
});
|
||||||
|
|
||||||
|
group('CleanupService.deleteLocalAssets', () {
|
||||||
|
test('returns 0 and does nothing for empty input', () async {
|
||||||
|
final result = await sut.deleteLocalAssets([]);
|
||||||
|
|
||||||
|
expect(result, 0);
|
||||||
|
verifyNever(() => assetMediaRepository.deleteAll(any()));
|
||||||
|
verifyNever(() => localAssetRepository.delete(any()));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('deletes in a single batch when under limit', () async {
|
||||||
|
final ids = List.generate(999, (i) => 'asset-$i');
|
||||||
|
|
||||||
|
when(() => assetMediaRepository.deleteAll(any())).thenAnswer((invocation) async {
|
||||||
|
return (invocation.positionalArguments.first as List<String>).toList();
|
||||||
|
});
|
||||||
|
when(() => localAssetRepository.delete(any())).thenAnswer((_) async {});
|
||||||
|
|
||||||
|
final result = await sut.deleteLocalAssets(ids);
|
||||||
|
|
||||||
|
expect(result, ids.length);
|
||||||
|
verify(() => assetMediaRepository.deleteAll(ids)).called(1);
|
||||||
|
verify(() => localAssetRepository.delete(ids)).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('deletes in platform-specific batches when over limit', () async {
|
||||||
|
final batchSize = CurrentPlatform.isAndroid ? 2000 : 10000;
|
||||||
|
final ids = List.generate(batchSize * 2 + 501, (i) => 'asset-$i');
|
||||||
|
final capturedBatches = <List<String>>[];
|
||||||
|
|
||||||
|
when(() => assetMediaRepository.deleteAll(any())).thenAnswer((invocation) async {
|
||||||
|
final batch = (invocation.positionalArguments.first as List<String>).toList();
|
||||||
|
capturedBatches.add(batch);
|
||||||
|
return batch;
|
||||||
|
});
|
||||||
|
when(() => localAssetRepository.delete(any())).thenAnswer((_) async {});
|
||||||
|
|
||||||
|
final result = await sut.deleteLocalAssets(ids);
|
||||||
|
|
||||||
|
expect(result, ids.length);
|
||||||
|
expect(capturedBatches.length, 3);
|
||||||
|
expect(capturedBatches[0].length, batchSize);
|
||||||
|
expect(capturedBatches[1].length, batchSize);
|
||||||
|
expect(capturedBatches[2].length, 501);
|
||||||
|
expect(capturedBatches[0].first, 'asset-0');
|
||||||
|
expect(capturedBatches[0].last, 'asset-${batchSize - 1}');
|
||||||
|
expect(capturedBatches[1].first, 'asset-$batchSize');
|
||||||
|
expect(capturedBatches[1].last, 'asset-${batchSize * 2 - 1}');
|
||||||
|
expect(capturedBatches[2].first, 'asset-${batchSize * 2}');
|
||||||
|
expect(capturedBatches[2].last, 'asset-${batchSize * 2 + 500}');
|
||||||
|
verify(() => localAssetRepository.delete(any())).called(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue