mirror of
https://github.com/immich-app/immich
synced 2025-10-17 18:19:27 +00:00
fix: process upload only after successful remote sync (#22360)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
This commit is contained in:
parent
fea5e6783c
commit
ee3c07d049
14 changed files with 156 additions and 73 deletions
|
|
@ -599,6 +599,7 @@
|
||||||
"backup_controller_page_turn_on": "Turn on foreground backup",
|
"backup_controller_page_turn_on": "Turn on foreground backup",
|
||||||
"backup_controller_page_uploading_file_info": "Uploading file info",
|
"backup_controller_page_uploading_file_info": "Uploading file info",
|
||||||
"backup_err_only_album": "Cannot remove the only album",
|
"backup_err_only_album": "Cannot remove the only album",
|
||||||
|
"backup_error_sync_failed": "Sync failed. Cannot start backup.",
|
||||||
"backup_info_card_assets": "assets",
|
"backup_info_card_assets": "assets",
|
||||||
"backup_manual_cancelled": "Cancelled",
|
"backup_manual_cancelled": "Cancelled",
|
||||||
"backup_manual_in_progress": "Upload already in progress. Try after sometime",
|
"backup_manual_in_progress": "Upload already in progress. Try after sometime",
|
||||||
|
|
|
||||||
|
|
@ -28,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/app_settings.service.dart';
|
||||||
import 'package:immich_mobile/services/auth.service.dart';
|
import 'package:immich_mobile/services/auth.service.dart';
|
||||||
import 'package:immich_mobile/services/localization.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/services/upload.service.dart';
|
||||||
import 'package:immich_mobile/utils/bootstrap.dart';
|
import 'package:immich_mobile/utils/bootstrap.dart';
|
||||||
import 'package:immich_mobile/utils/debug_print.dart';
|
import 'package:immich_mobile/utils/debug_print.dart';
|
||||||
|
|
@ -130,30 +129,33 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> onAndroidUpload() async {
|
Future<void> onAndroidUpload() async {
|
||||||
|
_logger.info('Android background processing started');
|
||||||
|
final sw = Stopwatch()..start();
|
||||||
try {
|
try {
|
||||||
_logger.info('Android background processing started');
|
if (!await _syncAssets(hashTimeout: Duration(minutes: _isBackupEnabled ? 3 : 6))) {
|
||||||
final sw = Stopwatch()..start();
|
_logger.warning("Remote sync did not complete successfully, skipping backup");
|
||||||
|
return;
|
||||||
await _syncAssets(hashTimeout: Duration(minutes: _isBackupEnabled ? 3 : 6));
|
}
|
||||||
await _handleBackup();
|
await _handleBackup();
|
||||||
|
|
||||||
sw.stop();
|
|
||||||
_logger.info("Android background processing completed in ${sw.elapsed.inSeconds}s");
|
|
||||||
} catch (error, stack) {
|
} catch (error, stack) {
|
||||||
_logger.severe("Failed to complete Android background processing", error, stack);
|
_logger.severe("Failed to complete Android background processing", error, stack);
|
||||||
} finally {
|
} finally {
|
||||||
|
sw.stop();
|
||||||
|
_logger.info("Android background processing completed in ${sw.elapsed.inSeconds}s");
|
||||||
await _cleanup();
|
await _cleanup();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> onIosUpload(bool isRefresh, int? maxSeconds) async {
|
Future<void> onIosUpload(bool isRefresh, int? maxSeconds) async {
|
||||||
|
_logger.info('iOS background upload started with maxSeconds: ${maxSeconds}s');
|
||||||
|
final sw = Stopwatch()..start();
|
||||||
try {
|
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);
|
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();
|
final backupFuture = _handleBackup();
|
||||||
if (maxSeconds != null) {
|
if (maxSeconds != null) {
|
||||||
|
|
@ -161,12 +163,11 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
|
||||||
} else {
|
} else {
|
||||||
await backupFuture;
|
await backupFuture;
|
||||||
}
|
}
|
||||||
|
|
||||||
sw.stop();
|
|
||||||
_logger.info("iOS background upload completed in ${sw.elapsed.inSeconds}s");
|
|
||||||
} catch (error, stack) {
|
} catch (error, stack) {
|
||||||
_logger.severe("Failed to complete iOS background upload", error, stack);
|
_logger.severe("Failed to complete iOS background upload", error, stack);
|
||||||
} finally {
|
} finally {
|
||||||
|
sw.stop();
|
||||||
|
_logger.info("iOS background upload completed in ${sw.elapsed.inSeconds}s");
|
||||||
await _cleanup();
|
await _cleanup();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -227,29 +228,20 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_isBackupEnabled) {
|
if (!_isBackupEnabled) {
|
||||||
_logger.info("[_handleBackup 1] Backup is disabled. Skipping backup routine");
|
_logger.info("Backup is disabled. Skipping backup routine");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.info("[_handleBackup 2] Enqueuing assets for backup from the background service");
|
|
||||||
|
|
||||||
final currentUser = _ref?.read(currentUserProvider);
|
final currentUser = _ref?.read(currentUserProvider);
|
||||||
if (currentUser == null) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.info("[_handleBackup 4] Resume backup from background");
|
|
||||||
if (Platform.isIOS) {
|
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() ?? false;
|
|
||||||
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
|
return _ref
|
||||||
?.read(uploadServiceProvider)
|
?.read(uploadServiceProvider)
|
||||||
|
|
@ -261,15 +253,15 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _syncAssets({Duration? hashTimeout}) async {
|
Future<bool> _syncAssets({Duration? hashTimeout}) async {
|
||||||
await _ref?.read(backgroundSyncProvider).syncLocal();
|
await _ref?.read(backgroundSyncProvider).syncLocal();
|
||||||
if (_isCleanedUp) {
|
if (_isCleanedUp) {
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
await _ref?.read(backgroundSyncProvider).syncRemote();
|
final isSuccess = await _ref?.read(backgroundSyncProvider).syncRemote() ?? false;
|
||||||
if (_isCleanedUp) {
|
if (_isCleanedUp) {
|
||||||
return;
|
return isSuccess;
|
||||||
}
|
}
|
||||||
|
|
||||||
var hashFuture = _ref?.read(backgroundSyncProvider).hashAssets();
|
var hashFuture = _ref?.read(backgroundSyncProvider).hashAssets();
|
||||||
|
|
@ -283,6 +275,7 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
|
||||||
}
|
}
|
||||||
|
|
||||||
await hashFuture;
|
await hashFuture;
|
||||||
|
return isSuccess;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ class SyncStreamService {
|
||||||
|
|
||||||
bool get isCancelled => _cancelChecker?.call() ?? false;
|
bool get isCancelled => _cancelChecker?.call() ?? false;
|
||||||
|
|
||||||
Future<void> sync() async {
|
Future<bool> sync() async {
|
||||||
_logger.info("Remote sync request for user");
|
_logger.info("Remote sync request for user");
|
||||||
// Start the sync stream and handle events
|
// Start the sync stream and handle events
|
||||||
bool shouldReset = false;
|
bool shouldReset = false;
|
||||||
|
|
@ -32,6 +32,7 @@ class SyncStreamService {
|
||||||
_logger.info("Resetting sync state as requested by server");
|
_logger.info("Resetting sync state as requested by server");
|
||||||
await _syncApiRepository.streamChanges(_handleEvents);
|
await _syncApiRepository.streamChanges(_handleEvents);
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _handleEvents(List<SyncEvent> events, Function() abort, Function() reset) async {
|
Future<void> _handleEvents(List<SyncEvent> events, Function() abort, Function() reset) async {
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ class BackgroundSyncManager {
|
||||||
final SyncCallback? onHashingComplete;
|
final SyncCallback? onHashingComplete;
|
||||||
final SyncErrorCallback? onHashingError;
|
final SyncErrorCallback? onHashingError;
|
||||||
|
|
||||||
Cancelable<void>? _syncTask;
|
Cancelable<bool?>? _syncTask;
|
||||||
Cancelable<void>? _syncWebsocketTask;
|
Cancelable<void>? _syncWebsocketTask;
|
||||||
Cancelable<void>? _deviceAlbumSyncTask;
|
Cancelable<void>? _deviceAlbumSyncTask;
|
||||||
Cancelable<void>? _linkedAlbumSyncTask;
|
Cancelable<void>? _linkedAlbumSyncTask;
|
||||||
|
|
@ -144,9 +144,9 @@ class BackgroundSyncManager {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> syncRemote() {
|
Future<bool> syncRemote() {
|
||||||
if (_syncTask != null) {
|
if (_syncTask != null) {
|
||||||
return _syncTask!.future;
|
return _syncTask!.future.then((result) => result ?? false).catchError((_) => false);
|
||||||
}
|
}
|
||||||
|
|
||||||
onRemoteSyncStart?.call();
|
onRemoteSyncStart?.call();
|
||||||
|
|
@ -156,6 +156,7 @@ class BackgroundSyncManager {
|
||||||
debugLabel: 'remote-sync',
|
debugLabel: 'remote-sync',
|
||||||
);
|
);
|
||||||
return _syncTask!
|
return _syncTask!
|
||||||
|
.then((result) => result ?? false)
|
||||||
.whenComplete(() {
|
.whenComplete(() {
|
||||||
onRemoteSyncComplete?.call();
|
onRemoteSyncComplete?.call();
|
||||||
_syncTask = null;
|
_syncTask = null;
|
||||||
|
|
@ -163,6 +164,7 @@ class BackgroundSyncManager {
|
||||||
.catchError((error) {
|
.catchError((error) {
|
||||||
onRemoteSyncError?.call(error.toString());
|
onRemoteSyncError?.call(error.toString());
|
||||||
_syncTask = null;
|
_syncTask = null;
|
||||||
|
return false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -110,7 +110,6 @@ class SyncApiRepository {
|
||||||
await onData(_parseLines(lines), abort, reset);
|
await onData(_parseLines(lines), abort, reset);
|
||||||
}
|
}
|
||||||
} catch (error, stack) {
|
} catch (error, stack) {
|
||||||
_logger.severe("Error processing stream", error, stack);
|
|
||||||
return Future.error(error, stack);
|
return Future.error(error, stack);
|
||||||
} finally {
|
} finally {
|
||||||
client.close();
|
client.close();
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
@ -8,6 +10,7 @@ import 'package:immich_mobile/entities/store.entity.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
|
import 'package:immich_mobile/generated/intl_keys.g.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/backup/backup_toggle_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/backup/backup_toggle_button.widget.dart';
|
||||||
import 'package:immich_mobile/providers/background_sync.provider.dart';
|
import 'package:immich_mobile/providers/background_sync.provider.dart';
|
||||||
import 'package:immich_mobile/providers/backup/backup_album.provider.dart';
|
import 'package:immich_mobile/providers/backup/backup_album.provider.dart';
|
||||||
|
|
@ -16,8 +19,7 @@ import 'package:immich_mobile/providers/sync_status.provider.dart';
|
||||||
import 'package:immich_mobile/providers/user.provider.dart';
|
import 'package:immich_mobile/providers/user.provider.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/widgets/backup/backup_info_card.dart';
|
import 'package:immich_mobile/widgets/backup/backup_info_card.dart';
|
||||||
import 'dart:async';
|
import 'package:logging/logging.dart';
|
||||||
|
|
||||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
|
|
@ -63,7 +65,10 @@ class _DriftBackupPageState extends ConsumerState<DriftBackupPage> {
|
||||||
.where((album) => album.backupSelection == BackupSelection.selected)
|
.where((album) => album.backupSelection == BackupSelection.selected)
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
|
final error = ref.watch(driftBackupProvider.select((p) => p.error));
|
||||||
|
|
||||||
final backupNotifier = ref.read(driftBackupProvider.notifier);
|
final backupNotifier = ref.read(driftBackupProvider.notifier);
|
||||||
|
final backupSyncManager = ref.read(backgroundSyncProvider);
|
||||||
|
|
||||||
Future<void> startBackup() async {
|
Future<void> startBackup() async {
|
||||||
final currentUser = Store.tryGet(StoreKey.currentUser);
|
final currentUser = Store.tryGet(StoreKey.currentUser);
|
||||||
|
|
@ -71,7 +76,14 @@ class _DriftBackupPageState extends ConsumerState<DriftBackupPage> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final syncSuccess = await backupSyncManager.syncRemote();
|
||||||
await backupNotifier.getBackupStatus(currentUser.id);
|
await backupNotifier.getBackupStatus(currentUser.id);
|
||||||
|
|
||||||
|
if (!syncSuccess) {
|
||||||
|
Logger("DriftBackupPage").warning("Remote sync did not complete successfully, skipping backup");
|
||||||
|
await backupNotifier.updateError(BackupError.syncFailed);
|
||||||
|
return;
|
||||||
|
}
|
||||||
await backupNotifier.startBackup(currentUser.id);
|
await backupNotifier.startBackup(currentUser.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -114,6 +126,26 @@ class _DriftBackupPageState extends ConsumerState<DriftBackupPage> {
|
||||||
const _RemainderCard(),
|
const _RemainderCard(),
|
||||||
const Divider(),
|
const Divider(),
|
||||||
BackupToggleButton(onStart: () async => await startBackup(), onStop: () async => await stopBackup()),
|
BackupToggleButton(onStart: () async => await startBackup(), onStop: () async => await stopBackup()),
|
||||||
|
switch (error) {
|
||||||
|
BackupError.none => const SizedBox.shrink(),
|
||||||
|
BackupError.syncFailed => Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 10),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
mainAxisSize: MainAxisSize.max,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.warning_rounded, color: context.colorScheme.error, fill: 1),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
IntlKeys.backup_error_sync_failed.t(),
|
||||||
|
style: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.error),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
},
|
||||||
TextButton.icon(
|
TextButton.icon(
|
||||||
icon: const Icon(Icons.info_outline_rounded),
|
icon: const Icon(Icons.info_outline_rounded),
|
||||||
onPressed: () => context.pushRoute(const DriftUploadDetailRoute()),
|
onPressed: () => context.pushRoute(const DriftUploadDetailRoute()),
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import 'package:immich_mobile/providers/user.provider.dart';
|
||||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||||
import 'package:immich_mobile/widgets/backup/drift_album_info_list_tile.dart';
|
import 'package:immich_mobile/widgets/backup/drift_album_info_list_tile.dart';
|
||||||
import 'package:immich_mobile/widgets/common/search_field.dart';
|
import 'package:immich_mobile/widgets/common/search_field.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
class DriftBackupAlbumSelectionPage extends ConsumerStatefulWidget {
|
class DriftBackupAlbumSelectionPage extends ConsumerStatefulWidget {
|
||||||
|
|
@ -112,7 +113,18 @@ class _DriftBackupAlbumSelectionPageState extends ConsumerState<DriftBackupAlbum
|
||||||
// Waits for hashing to be cancelled before starting a new one
|
// Waits for hashing to be cancelled before starting a new one
|
||||||
unawaited(nativeSync.cancelHashing().whenComplete(() => backgroundSync.hashAssets()));
|
unawaited(nativeSync.cancelHashing().whenComplete(() => backgroundSync.hashAssets()));
|
||||||
if (isBackupEnabled) {
|
if (isBackupEnabled) {
|
||||||
unawaited(backupNotifier.cancel().whenComplete(() => backupNotifier.startBackup(user.id)));
|
unawaited(
|
||||||
|
backupNotifier.cancel().whenComplete(
|
||||||
|
() => backgroundSync.syncRemote().then((success) {
|
||||||
|
if (success) {
|
||||||
|
return backupNotifier.startBackup(user.id);
|
||||||
|
} else {
|
||||||
|
Logger('DriftBackupAlbumSelectionPage').warning('Background sync failed, not starting backup');
|
||||||
|
backupNotifier.updateError(BackupError.syncFailed);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
|
@ -5,10 +7,12 @@ import 'package:immich_mobile/domain/models/store.model.dart';
|
||||||
import 'package:immich_mobile/entities/store.entity.dart';
|
import 'package:immich_mobile/entities/store.entity.dart';
|
||||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
import 'package:immich_mobile/providers/app_settings.provider.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';
|
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
|
||||||
import 'package:immich_mobile/providers/user.provider.dart';
|
import 'package:immich_mobile/providers/user.provider.dart';
|
||||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||||
import 'package:immich_mobile/widgets/settings/backup_settings/drift_backup_settings.dart';
|
import 'package:immich_mobile/widgets/settings/backup_settings/drift_backup_settings.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
class DriftBackupOptionsPage extends ConsumerWidget {
|
class DriftBackupOptionsPage extends ConsumerWidget {
|
||||||
|
|
@ -54,9 +58,19 @@ class DriftBackupOptionsPage extends ConsumerWidget {
|
||||||
);
|
);
|
||||||
|
|
||||||
final backupNotifier = ref.read(driftBackupProvider.notifier);
|
final backupNotifier = ref.read(driftBackupProvider.notifier);
|
||||||
backupNotifier.cancel().then((_) {
|
final backgroundSync = ref.read(backgroundSyncProvider);
|
||||||
backupNotifier.startBackup(currentUser.id);
|
unawaited(
|
||||||
});
|
backupNotifier.cancel().whenComplete(
|
||||||
|
() => backgroundSync.syncRemote().then((success) {
|
||||||
|
if (success) {
|
||||||
|
return backupNotifier.startBackup(currentUser.id);
|
||||||
|
} else {
|
||||||
|
Logger('DriftBackupOptionsPage').warning('Background sync failed, not starting backup');
|
||||||
|
backupNotifier.updateError(BackupError.syncFailed);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
|
|
|
||||||
|
|
@ -62,14 +62,24 @@ class SplashScreenPageState extends ConsumerState<SplashScreenPage> {
|
||||||
infoProvider.getServerInfo();
|
infoProvider.getServerInfo();
|
||||||
|
|
||||||
if (Store.isBetaTimelineEnabled) {
|
if (Store.isBetaTimelineEnabled) {
|
||||||
await Future.wait([backgroundManager.syncLocal(), backgroundManager.syncRemote()]);
|
bool syncSuccess = false;
|
||||||
await Future.wait([
|
await Future.wait([
|
||||||
backgroundManager.hashAssets().then((_) {
|
backgroundManager.syncLocal(),
|
||||||
_resumeBackup(backupProvider);
|
backgroundManager.syncRemote().then((success) => syncSuccess = success),
|
||||||
}),
|
|
||||||
_resumeBackup(backupProvider),
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
if (syncSuccess) {
|
||||||
|
await Future.wait([
|
||||||
|
backgroundManager.hashAssets().then((_) {
|
||||||
|
_resumeBackup(backupProvider);
|
||||||
|
}),
|
||||||
|
_resumeBackup(backupProvider),
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
backupProvider.updateError(BackupError.syncFailed);
|
||||||
|
await backgroundManager.hashAssets();
|
||||||
|
}
|
||||||
|
|
||||||
if (Store.get(StoreKey.syncAlbums, false)) {
|
if (Store.get(StoreKey.syncAlbums, false)) {
|
||||||
await backgroundManager.syncLinkedAlbum();
|
await backgroundManager.syncLinkedAlbum();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -148,17 +148,22 @@ class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> {
|
||||||
final isAlbumLinkedSyncEnable = _ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.syncAlbums);
|
final isAlbumLinkedSyncEnable = _ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.syncAlbums);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
bool syncSuccess = false;
|
||||||
await Future.wait([
|
await Future.wait([
|
||||||
_safeRun(backgroundManager.syncLocal(), "syncLocal"),
|
_safeRun(backgroundManager.syncLocal(), "syncLocal"),
|
||||||
_safeRun(backgroundManager.syncRemote(), "syncRemote"),
|
_safeRun(backgroundManager.syncRemote().then((success) => syncSuccess = success), "syncRemote"),
|
||||||
]);
|
|
||||||
|
|
||||||
await Future.wait([
|
|
||||||
_safeRun(backgroundManager.hashAssets(), "hashAssets").then((_) {
|
|
||||||
_resumeBackup();
|
|
||||||
}),
|
|
||||||
_resumeBackup(),
|
|
||||||
]);
|
]);
|
||||||
|
if (syncSuccess) {
|
||||||
|
await Future.wait([
|
||||||
|
_safeRun(backgroundManager.hashAssets(), "hashAssets").then((_) {
|
||||||
|
_resumeBackup();
|
||||||
|
}),
|
||||||
|
_resumeBackup(),
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
_ref.read(driftBackupProvider.notifier).updateError(BackupError.syncFailed);
|
||||||
|
await _safeRun(backgroundManager.hashAssets(), "hashAssets");
|
||||||
|
}
|
||||||
|
|
||||||
if (isAlbumLinkedSyncEnable) {
|
if (isAlbumLinkedSyncEnable) {
|
||||||
await _safeRun(backgroundManager.syncLinkedAlbum(), "syncLinkedAlbum");
|
await _safeRun(backgroundManager.syncLinkedAlbum(), "syncLinkedAlbum");
|
||||||
|
|
|
||||||
|
|
@ -91,6 +91,8 @@ class DriftUploadStatus {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum BackupError { none, syncFailed }
|
||||||
|
|
||||||
class DriftBackupState {
|
class DriftBackupState {
|
||||||
final int totalCount;
|
final int totalCount;
|
||||||
final int backupCount;
|
final int backupCount;
|
||||||
|
|
@ -101,6 +103,7 @@ class DriftBackupState {
|
||||||
final int enqueueTotalCount;
|
final int enqueueTotalCount;
|
||||||
|
|
||||||
final bool isCanceling;
|
final bool isCanceling;
|
||||||
|
final BackupError error;
|
||||||
|
|
||||||
final Map<String, DriftUploadStatus> uploadItems;
|
final Map<String, DriftUploadStatus> uploadItems;
|
||||||
|
|
||||||
|
|
@ -113,6 +116,7 @@ class DriftBackupState {
|
||||||
required this.enqueueTotalCount,
|
required this.enqueueTotalCount,
|
||||||
required this.isCanceling,
|
required this.isCanceling,
|
||||||
required this.uploadItems,
|
required this.uploadItems,
|
||||||
|
this.error = BackupError.none,
|
||||||
});
|
});
|
||||||
|
|
||||||
DriftBackupState copyWith({
|
DriftBackupState copyWith({
|
||||||
|
|
@ -124,6 +128,7 @@ class DriftBackupState {
|
||||||
int? enqueueTotalCount,
|
int? enqueueTotalCount,
|
||||||
bool? isCanceling,
|
bool? isCanceling,
|
||||||
Map<String, DriftUploadStatus>? uploadItems,
|
Map<String, DriftUploadStatus>? uploadItems,
|
||||||
|
BackupError? error,
|
||||||
}) {
|
}) {
|
||||||
return DriftBackupState(
|
return DriftBackupState(
|
||||||
totalCount: totalCount ?? this.totalCount,
|
totalCount: totalCount ?? this.totalCount,
|
||||||
|
|
@ -134,12 +139,13 @@ class DriftBackupState {
|
||||||
enqueueTotalCount: enqueueTotalCount ?? this.enqueueTotalCount,
|
enqueueTotalCount: enqueueTotalCount ?? this.enqueueTotalCount,
|
||||||
isCanceling: isCanceling ?? this.isCanceling,
|
isCanceling: isCanceling ?? this.isCanceling,
|
||||||
uploadItems: uploadItems ?? this.uploadItems,
|
uploadItems: uploadItems ?? this.uploadItems,
|
||||||
|
error: error ?? this.error,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'DriftBackupState(totalCount: $totalCount, backupCount: $backupCount, remainderCount: $remainderCount, processingCount: $processingCount, enqueueCount: $enqueueCount, enqueueTotalCount: $enqueueTotalCount, isCanceling: $isCanceling, uploadItems: $uploadItems)';
|
return 'DriftBackupState(totalCount: $totalCount, backupCount: $backupCount, remainderCount: $remainderCount, processingCount: $processingCount, enqueueCount: $enqueueCount, enqueueTotalCount: $enqueueTotalCount, isCanceling: $isCanceling, uploadItems: $uploadItems, error: $error)';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -154,7 +160,8 @@ class DriftBackupState {
|
||||||
other.enqueueCount == enqueueCount &&
|
other.enqueueCount == enqueueCount &&
|
||||||
other.enqueueTotalCount == enqueueTotalCount &&
|
other.enqueueTotalCount == enqueueTotalCount &&
|
||||||
other.isCanceling == isCanceling &&
|
other.isCanceling == isCanceling &&
|
||||||
mapEquals(other.uploadItems, uploadItems);
|
mapEquals(other.uploadItems, uploadItems) &&
|
||||||
|
other.error == error;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -166,7 +173,8 @@ class DriftBackupState {
|
||||||
enqueueCount.hashCode ^
|
enqueueCount.hashCode ^
|
||||||
enqueueTotalCount.hashCode ^
|
enqueueTotalCount.hashCode ^
|
||||||
isCanceling.hashCode ^
|
isCanceling.hashCode ^
|
||||||
uploadItems.hashCode;
|
uploadItems.hashCode ^
|
||||||
|
error.hashCode;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -186,6 +194,7 @@ class DriftBackupNotifier extends StateNotifier<DriftBackupState> {
|
||||||
enqueueTotalCount: 0,
|
enqueueTotalCount: 0,
|
||||||
isCanceling: false,
|
isCanceling: false,
|
||||||
uploadItems: {},
|
uploadItems: {},
|
||||||
|
error: BackupError.none,
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
{
|
{
|
||||||
|
|
@ -303,7 +312,12 @@ class DriftBackupNotifier extends StateNotifier<DriftBackupState> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> updateError(BackupError error) async {
|
||||||
|
state = state.copyWith(error: error);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> startBackup(String userId) {
|
Future<void> startBackup(String userId) {
|
||||||
|
state = state.copyWith(error: BackupError.none);
|
||||||
return _uploadService.startBackup(userId, _updateEnqueueCount);
|
return _uploadService.startBackup(userId, _updateEnqueueCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -313,7 +327,7 @@ class DriftBackupNotifier extends StateNotifier<DriftBackupState> {
|
||||||
|
|
||||||
Future<void> cancel() async {
|
Future<void> cancel() async {
|
||||||
dPrint(() => "Canceling backup tasks...");
|
dPrint(() => "Canceling backup tasks...");
|
||||||
state = state.copyWith(enqueueCount: 0, enqueueTotalCount: 0, isCanceling: true);
|
state = state.copyWith(enqueueCount: 0, enqueueTotalCount: 0, isCanceling: true, error: BackupError.none);
|
||||||
|
|
||||||
final activeTaskCount = await _uploadService.cancelBackup();
|
final activeTaskCount = await _uploadService.cancelBackup();
|
||||||
|
|
||||||
|
|
@ -329,6 +343,7 @@ class DriftBackupNotifier extends StateNotifier<DriftBackupState> {
|
||||||
|
|
||||||
Future<void> handleBackupResume(String userId) async {
|
Future<void> handleBackupResume(String userId) async {
|
||||||
_logger.info("Resuming backup tasks...");
|
_logger.info("Resuming backup tasks...");
|
||||||
|
state = state.copyWith(error: BackupError.none);
|
||||||
final tasks = await _uploadService.getActiveTasks(kBackupGroup);
|
final tasks = await _uploadService.getActiveTasks(kBackupGroup);
|
||||||
_logger.info("Found ${tasks.length} tasks");
|
_logger.info("Found ${tasks.length} tasks");
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,15 +14,6 @@ class ServerInfoService {
|
||||||
|
|
||||||
const ServerInfoService(this._apiService);
|
const ServerInfoService(this._apiService);
|
||||||
|
|
||||||
Future<bool> ping() async {
|
|
||||||
try {
|
|
||||||
await _apiService.serverInfoApi.pingServer().timeout(const Duration(seconds: 5));
|
|
||||||
return true;
|
|
||||||
} catch (e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<ServerDiskInfo?> getDiskInfo() async {
|
Future<ServerDiskInfo?> getDiskInfo() async {
|
||||||
try {
|
try {
|
||||||
final dto = await _apiService.serverInfoApi.getStorage();
|
final dto = await _apiService.serverInfoApi.getStorage();
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ Cancelable<T?> runInIsolateGentle<T>({
|
||||||
}
|
}
|
||||||
|
|
||||||
return workerManager.executeGentle((cancelledChecker) async {
|
return workerManager.executeGentle((cancelledChecker) async {
|
||||||
|
T? result;
|
||||||
await runZonedGuarded(
|
await runZonedGuarded(
|
||||||
() async {
|
() async {
|
||||||
BackgroundIsolateBinaryMessenger.ensureInitialized(token);
|
BackgroundIsolateBinaryMessenger.ensureInitialized(token);
|
||||||
|
|
@ -53,7 +54,7 @@ Cancelable<T?> runInIsolateGentle<T>({
|
||||||
|
|
||||||
try {
|
try {
|
||||||
HttpSSLOptions.apply(applyNative: false);
|
HttpSSLOptions.apply(applyNative: false);
|
||||||
return await computation(ref);
|
result = await computation(ref);
|
||||||
} on CanceledError {
|
} on CanceledError {
|
||||||
log.warning("Computation cancelled ${debugLabel == null ? '' : ' for $debugLabel'}");
|
log.warning("Computation cancelled ${debugLabel == null ? '' : ' for $debugLabel'}");
|
||||||
} catch (error, stack) {
|
} catch (error, stack) {
|
||||||
|
|
@ -83,12 +84,11 @@ Cancelable<T?> runInIsolateGentle<T>({
|
||||||
await Future.delayed(const Duration(seconds: 2));
|
await Future.delayed(const Duration(seconds: 2));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
|
||||||
},
|
},
|
||||||
(error, stack) {
|
(error, stack) {
|
||||||
dPrint(() => "Error in isolate $debugLabel zone: $error, $stack");
|
dPrint(() => "Error in isolate $debugLabel zone: $error, $stack");
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
return null;
|
return result;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,8 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/models/server_info/server_info.model.dart';
|
import 'package:immich_mobile/models/server_info/server_info.model.dart';
|
||||||
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
|
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
|
||||||
import 'package:immich_mobile/providers/cast.provider.dart';
|
import 'package:immich_mobile/providers/cast.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/setting.provider.dart';
|
|
||||||
import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/setting.provider.dart';
|
||||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||||
import 'package:immich_mobile/providers/sync_status.provider.dart';
|
import 'package:immich_mobile/providers/sync_status.provider.dart';
|
||||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||||
|
|
@ -168,8 +168,16 @@ class _BackupIndicator extends ConsumerWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
const widgetSize = 30.0;
|
const widgetSize = 30.0;
|
||||||
final indicatorIcon = _getBackupBadgeIcon(context, ref);
|
final hasError = ref.watch(driftBackupProvider.select((state) => state.error != BackupError.none));
|
||||||
final badgeBackground = context.colorScheme.surfaceContainer;
|
final indicatorIcon = hasError
|
||||||
|
? Icon(
|
||||||
|
Icons.warning_rounded,
|
||||||
|
size: 12,
|
||||||
|
color: context.colorScheme.error,
|
||||||
|
semanticLabel: 'backup_controller_page_backup'.tr(),
|
||||||
|
)
|
||||||
|
: _getBackupBadgeIcon(context, ref);
|
||||||
|
final badgeBackground = hasError ? context.colorScheme.errorContainer : context.colorScheme.surfaceContainer;
|
||||||
|
|
||||||
return InkWell(
|
return InkWell(
|
||||||
onTap: () => context.pushRoute(const DriftBackupRoute()),
|
onTap: () => context.pushRoute(const DriftBackupRoute()),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue