Merge branch 'main' into fix/bring-back-delete-backed-up-only

This commit is contained in:
Alex
2025-10-02 11:30:47 -05:00
committed by GitHub
266 changed files with 12110 additions and 4362 deletions
@@ -84,8 +84,8 @@ class AssetService {
return 1.0;
}
Future<List<(String, String)>> getPlaces() {
return _remoteAssetRepository.getPlaces();
Future<List<(String, String)>> getPlaces(String userId) {
return _remoteAssetRepository.getPlaces(userId);
}
Future<(int local, int remote)> getAssetCounts() async {
@@ -10,11 +10,13 @@ import 'package:immich_mobile/constants/constants.dart';
import 'package:immich_mobile/domain/services/log.service.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/extensions/network_capability_extensions.dart';
import 'package:immich_mobile/extensions/platform_extensions.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/generated/intl_keys.g.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/logger_db.repository.dart';
import 'package:immich_mobile/platform/background_worker_api.g.dart';
import 'package:immich_mobile/platform/background_worker_lock_api.g.dart';
import 'package:immich_mobile/providers/app_settings.provider.dart';
import 'package:immich_mobile/providers/background_sync.provider.dart';
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
@@ -26,7 +28,6 @@ import 'package:immich_mobile/repositories/file_media.repository.dart';
import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:immich_mobile/services/auth.service.dart';
import 'package:immich_mobile/services/localization.service.dart';
import 'package:immich_mobile/services/server_info.service.dart';
import 'package:immich_mobile/services/upload.service.dart';
import 'package:immich_mobile/utils/bootstrap.dart';
import 'package:immich_mobile/utils/debug_print.dart';
@@ -58,7 +59,7 @@ class BackgroundWorkerFgService {
}
class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
late final ProviderContainer _ref;
ProviderContainer? _ref;
final Isar _isar;
final Drift _drift;
final DriftLogger _driftLogger;
@@ -83,36 +84,38 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
BackgroundWorkerFlutterApi.setUp(this);
}
bool get _isBackupEnabled => _ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableBackup);
bool get _isBackupEnabled => _ref?.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableBackup) ?? false;
Future<void> init() async {
try {
HttpSSLOptions.apply(applyNative: false);
await Future.wait([
loadTranslations(),
workerManager.init(dynamicSpawning: true),
_ref.read(authServiceProvider).setOpenApiServiceEndpoint(),
// Initialize the file downloader
FileDownloader().configure(
globalConfig: [
// maxConcurrent: 6, maxConcurrentByHost(server):6, maxConcurrentByGroup: 3
(Config.holdingQueue, (6, 6, 3)),
// On Android, if files are larger than 256MB, run in foreground service
(Config.runInForegroundIfFileLargerThan, 256),
],
),
FileDownloader().trackTasksInGroup(kDownloadGroupLivePhoto, markDownloadedComplete: false),
FileDownloader().trackTasks(),
_ref.read(fileMediaRepositoryProvider).enableBackgroundAccess(),
]);
await Future.wait(
[
loadTranslations(),
workerManager.init(dynamicSpawning: true),
_ref?.read(authServiceProvider).setOpenApiServiceEndpoint(),
// Initialize the file downloader
FileDownloader().configure(
globalConfig: [
// maxConcurrent: 6, maxConcurrentByHost(server):6, maxConcurrentByGroup: 3
(Config.holdingQueue, (6, 6, 3)),
// On Android, if files are larger than 256MB, run in foreground service
(Config.runInForegroundIfFileLargerThan, 256),
],
),
FileDownloader().trackTasksInGroup(kDownloadGroupLivePhoto, markDownloadedComplete: false),
FileDownloader().trackTasks(),
_ref?.read(fileMediaRepositoryProvider).enableBackgroundAccess(),
].nonNulls,
);
configureFileDownloaderNotifications();
if (Platform.isAndroid) {
await _backgroundHostApi.showNotification(
IntlKeys.uploading_media.t(),
IntlKeys.backup_background_service_in_progress_notification.t(),
IntlKeys.backup_background_service_default_notification.t(),
);
}
@@ -126,30 +129,33 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
@override
Future<void> onAndroidUpload() async {
_logger.info('Android background processing started');
final sw = Stopwatch()..start();
try {
_logger.info('Android background processing started');
final sw = Stopwatch()..start();
await _syncAssets(hashTimeout: Duration(minutes: _isBackupEnabled ? 3 : 6));
if (!await _syncAssets(hashTimeout: Duration(minutes: _isBackupEnabled ? 3 : 6))) {
_logger.warning("Remote sync did not complete successfully, skipping backup");
return;
}
await _handleBackup();
sw.stop();
_logger.info("Android background processing completed in ${sw.elapsed.inSeconds}s");
} catch (error, stack) {
_logger.severe("Failed to complete Android background processing", error, stack);
} finally {
sw.stop();
_logger.info("Android background processing completed in ${sw.elapsed.inSeconds}s");
await _cleanup();
}
}
@override
Future<void> onIosUpload(bool isRefresh, int? maxSeconds) async {
_logger.info('iOS background upload started with maxSeconds: ${maxSeconds}s');
final sw = Stopwatch()..start();
try {
_logger.info('iOS background upload started with maxSeconds: ${maxSeconds}s');
final sw = Stopwatch()..start();
final timeout = isRefresh ? const Duration(seconds: 5) : Duration(minutes: _isBackupEnabled ? 3 : 6);
await _syncAssets(hashTimeout: timeout);
if (!await _syncAssets(hashTimeout: timeout)) {
_logger.warning("Remote sync did not complete successfully, skipping backup");
return;
}
final backupFuture = _handleBackup();
if (maxSeconds != null) {
@@ -157,12 +163,11 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
} else {
await backupFuture;
}
sw.stop();
_logger.info("iOS background upload completed in ${sw.elapsed.inSeconds}s");
} catch (error, stack) {
_logger.severe("Failed to complete iOS background upload", error, stack);
} finally {
sw.stop();
_logger.info("iOS background upload completed in ${sw.elapsed.inSeconds}s");
await _cleanup();
}
}
@@ -178,15 +183,17 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
}
Future<void> _cleanup() async {
if (_isCleanedUp) {
// If ref is null, it means the service was never initialized properly
if (_isCleanedUp || _ref == null) {
return;
}
try {
final backgroundSyncManager = _ref.read(backgroundSyncProvider);
final nativeSyncApi = _ref.read(nativeSyncApiProvider);
_isCleanedUp = true;
_ref.dispose();
final backgroundSyncManager = _ref?.read(backgroundSyncProvider);
final nativeSyncApi = _ref?.read(nativeSyncApiProvider);
_ref?.dispose();
_ref = null;
_cancellationToken.cancel();
_logger.info("Cleaning up background worker");
@@ -199,14 +206,14 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
Store.dispose(),
_drift.close(),
_driftLogger.close(),
backgroundSyncManager.cancel(),
nativeSyncApi.cancelHashing(),
backgroundSyncManager?.cancel(),
nativeSyncApi?.cancelHashing(),
];
if (_isar.isOpen) {
cleanupFutures.add(_isar.close());
}
await Future.wait(cleanupFutures);
await Future.wait(cleanupFutures.nonNulls);
_logger.info("Background worker resources cleaned up");
} catch (error, stack) {
dPrint(() => 'Failed to cleanup background worker: $error with stack: $stack');
@@ -216,34 +223,28 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
Future<void> _handleBackup() async {
await runZonedGuarded(
() async {
if (!_isBackupEnabled || _isCleanedUp) {
_logger.info("[_handleBackup 1] Backup is disabled. Skipping backup routine");
if (_isCleanedUp) {
return;
}
_logger.info("[_handleBackup 2] Enqueuing assets for backup from the background service");
if (!_isBackupEnabled) {
_logger.info("Backup is disabled. Skipping backup routine");
return;
}
final currentUser = _ref.read(currentUserProvider);
final currentUser = _ref?.read(currentUserProvider);
if (currentUser == null) {
_logger.warning("[_handleBackup 3] No current user found. Skipping backup from background");
_logger.warning("No current user found. Skipping backup from background");
return;
}
_logger.info("[_handleBackup 4] Resume backup from background");
if (Platform.isIOS) {
return _ref.read(driftBackupProvider.notifier).handleBackupResume(currentUser.id);
return _ref?.read(driftBackupProvider.notifier).handleBackupResume(currentUser.id);
}
final canPing = await _ref.read(serverInfoServiceProvider).ping();
if (!canPing) {
_logger.warning("[_handleBackup 5] Server is not reachable. Skipping backup from background");
return;
}
final networkCapabilities = await _ref.read(connectivityApiProvider).getCapabilities();
final networkCapabilities = await _ref?.read(connectivityApiProvider).getCapabilities() ?? [];
return _ref
.read(uploadServiceProvider)
?.read(uploadServiceProvider)
.startBackupWithHttpClient(currentUser.id, networkCapabilities.hasWifi, _cancellationToken);
},
(error, stack) {
@@ -252,19 +253,19 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
);
}
Future<void> _syncAssets({Duration? hashTimeout}) async {
await _ref.read(backgroundSyncProvider).syncLocal();
Future<bool> _syncAssets({Duration? hashTimeout}) async {
await _ref?.read(backgroundSyncProvider).syncLocal();
if (_isCleanedUp) {
return;
return false;
}
await _ref.read(backgroundSyncProvider).syncRemote();
final isSuccess = await _ref?.read(backgroundSyncProvider).syncRemote() ?? false;
if (_isCleanedUp) {
return;
return isSuccess;
}
var hashFuture = _ref.read(backgroundSyncProvider).hashAssets();
if (hashTimeout != null) {
var hashFuture = _ref?.read(backgroundSyncProvider).hashAssets();
if (hashTimeout != null && hashFuture != null) {
hashFuture = hashFuture.timeout(
hashTimeout,
onTimeout: () {
@@ -274,6 +275,24 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
}
await hashFuture;
return isSuccess;
}
}
class BackgroundWorkerLockService {
final BackgroundWorkerLockApi _hostApi;
const BackgroundWorkerLockService(this._hostApi);
Future<void> lock() async {
if (CurrentPlatform.isAndroid) {
return _hostApi.lock();
}
}
Future<void> unlock() async {
if (CurrentPlatform.isAndroid) {
return _hostApi.unlock();
}
}
}
@@ -281,7 +281,7 @@ extension on Iterable<PlatformAlbum> {
(e) => LocalAlbum(
id: e.id,
name: e.name,
updatedAt: tryFromSecondsSinceEpoch(e.updatedAt) ?? DateTime.now(),
updatedAt: tryFromSecondsSinceEpoch(e.updatedAt, isUtc: true) ?? DateTime.timestamp(),
assetCount: e.assetCount,
),
).toList();
@@ -296,8 +296,8 @@ extension on Iterable<PlatformAsset> {
name: e.name,
checksum: null,
type: AssetType.values.elementAtOrNull(e.type) ?? AssetType.other,
createdAt: tryFromSecondsSinceEpoch(e.createdAt) ?? DateTime.now(),
updatedAt: tryFromSecondsSinceEpoch(e.updatedAt) ?? DateTime.now(),
createdAt: tryFromSecondsSinceEpoch(e.createdAt, isUtc: true) ?? DateTime.timestamp(),
updatedAt: tryFromSecondsSinceEpoch(e.updatedAt, isUtc: true) ?? DateTime.timestamp(),
width: e.width,
height: e.height,
durationInSeconds: e.durationInSeconds,
@@ -4,8 +4,8 @@ import 'package:immich_mobile/infrastructure/repositories/local_album.repository
import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart';
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
import 'package:immich_mobile/repositories/drift_album_api_repository.dart';
import 'package:logging/logging.dart';
import 'package:immich_mobile/utils/debug_print.dart';
import 'package:logging/logging.dart';
final syncLinkedAlbumServiceProvider = Provider(
(ref) => SyncLinkedAlbumService(
@@ -31,17 +31,19 @@ class SyncLinkedAlbumService {
selectedAlbums.map((localAlbum) async {
final linkedRemoteAlbumId = localAlbum.linkedRemoteAlbumId;
if (linkedRemoteAlbumId == null) {
_log.warning("No linked remote album ID found for local album: ${localAlbum.name}");
return;
}
final remoteAlbum = await _remoteAlbumRepository.get(linkedRemoteAlbumId);
if (remoteAlbum == null) {
_log.warning("Linked remote album not found for ID: $linkedRemoteAlbumId");
return;
}
// get assets that are uploaded but not in the remote album
final assetIds = await _remoteAlbumRepository.getLinkedAssetIds(userId, localAlbum.id, linkedRemoteAlbumId);
_log.fine("Syncing ${assetIds.length} assets to remote album: ${remoteAlbum.name}");
if (assetIds.isNotEmpty) {
final album = await _albumApiRepository.addAssets(remoteAlbum.id, assetIds);
await _remoteAlbumRepository.addAssets(remoteAlbum.id, album.added);
@@ -23,7 +23,7 @@ class SyncStreamService {
bool get isCancelled => _cancelChecker?.call() ?? false;
Future<void> sync() async {
Future<bool> sync() async {
_logger.info("Remote sync request for user");
// Start the sync stream and handle events
bool shouldReset = false;
@@ -32,6 +32,7 @@ class SyncStreamService {
_logger.info("Resetting sync state as requested by server");
await _syncApiRepository.streamChanges(_handleEvents);
}
return true;
}
Future<void> _handleEvents(List<SyncEvent> events, Function() abort, Function() reset) async {
@@ -59,7 +59,8 @@ class TimelineFactory {
TimelineService fromAssets(List<BaseAsset> assets) => TimelineService(_timelineRepository.fromAssets(assets));
TimelineService map(LatLngBounds bounds) => TimelineService(_timelineRepository.map(bounds, groupBy));
TimelineService map(String userId, LatLngBounds bounds) =>
TimelineService(_timelineRepository.map(userId, bounds, groupBy));
}
class TimelineService {
+5 -3
View File
@@ -21,7 +21,7 @@ class BackgroundSyncManager {
final SyncCallback? onHashingComplete;
final SyncErrorCallback? onHashingError;
Cancelable<void>? _syncTask;
Cancelable<bool?>? _syncTask;
Cancelable<void>? _syncWebsocketTask;
Cancelable<void>? _deviceAlbumSyncTask;
Cancelable<void>? _linkedAlbumSyncTask;
@@ -144,9 +144,9 @@ class BackgroundSyncManager {
});
}
Future<void> syncRemote() {
Future<bool> syncRemote() {
if (_syncTask != null) {
return _syncTask!.future;
return _syncTask!.future.then((result) => result ?? false).catchError((_) => false);
}
onRemoteSyncStart?.call();
@@ -156,6 +156,7 @@ class BackgroundSyncManager {
debugLabel: 'remote-sync',
);
return _syncTask!
.then((result) => result ?? false)
.whenComplete(() {
onRemoteSyncComplete?.call();
_syncTask = null;
@@ -163,6 +164,7 @@ class BackgroundSyncManager {
.catchError((error) {
onRemoteSyncError?.call(error.toString());
_syncTask = null;
return false;
});
}
@@ -2,10 +2,12 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/services/sync_linked_album.service.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:logging/logging.dart';
Future<void> syncLinkedAlbumsIsolated(ProviderContainer ref) {
final user = Store.tryGet(StoreKey.currentUser);
if (user == null) {
Logger("SyncLinkedAlbum").warning("No user logged in, skipping linked album sync");
return Future.value();
}
return ref.read(syncLinkedAlbumServiceProvider).syncLinkedAlbums(user.id);