mirror of
https://github.com/immich-app/immich.git
synced 2026-05-18 03:10:24 +03:00
41f013387f
add schemas sync variants formatting initial implementation use existing db, wip move to separate folder fix table definitions wip wiring it up
367 lines
11 KiB
Dart
367 lines
11 KiB
Dart
// ignore_for_file: public_member_api_docs, sort_constructors_first
|
|
import 'dart:async';
|
|
|
|
import 'package:collection/collection.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/repositories/backup.repository.dart';
|
|
import 'package:immich_mobile/platform/upload_api.g.dart';
|
|
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
|
|
import 'package:immich_mobile/providers/user.provider.dart';
|
|
import 'package:immich_mobile/services/upload.service.dart';
|
|
import 'package:immich_mobile/utils/debug_print.dart';
|
|
import 'package:logging/logging.dart';
|
|
|
|
class EnqueueStatus {
|
|
final int enqueueCount;
|
|
final int totalCount;
|
|
|
|
const EnqueueStatus({required this.enqueueCount, required this.totalCount});
|
|
|
|
EnqueueStatus copyWith({int? enqueueCount, int? totalCount}) {
|
|
return EnqueueStatus(enqueueCount: enqueueCount ?? this.enqueueCount, totalCount: totalCount ?? this.totalCount);
|
|
}
|
|
|
|
@override
|
|
String toString() => 'EnqueueStatus(enqueueCount: $enqueueCount, totalCount: $totalCount)';
|
|
}
|
|
|
|
class DriftUploadStatus {
|
|
final String taskId;
|
|
final String filename;
|
|
final double progress;
|
|
final int fileSize;
|
|
final String networkSpeedAsString;
|
|
final bool? isFailed;
|
|
final String? error;
|
|
|
|
const DriftUploadStatus({
|
|
required this.taskId,
|
|
required this.filename,
|
|
required this.progress,
|
|
required this.fileSize,
|
|
required this.networkSpeedAsString,
|
|
this.isFailed,
|
|
this.error,
|
|
});
|
|
|
|
DriftUploadStatus copyWith({
|
|
String? taskId,
|
|
String? filename,
|
|
double? progress,
|
|
int? fileSize,
|
|
String? networkSpeedAsString,
|
|
bool? isFailed,
|
|
String? error,
|
|
}) {
|
|
return DriftUploadStatus(
|
|
taskId: taskId ?? this.taskId,
|
|
filename: filename ?? this.filename,
|
|
progress: progress ?? this.progress,
|
|
fileSize: fileSize ?? this.fileSize,
|
|
networkSpeedAsString: networkSpeedAsString ?? this.networkSpeedAsString,
|
|
isFailed: isFailed ?? this.isFailed,
|
|
error: error ?? this.error,
|
|
);
|
|
}
|
|
|
|
@override
|
|
String toString() {
|
|
return 'DriftUploadStatus(taskId: $taskId, filename: $filename, progress: $progress, fileSize: $fileSize, networkSpeedAsString: $networkSpeedAsString, isFailed: $isFailed, error: $error)';
|
|
}
|
|
|
|
@override
|
|
bool operator ==(covariant DriftUploadStatus other) {
|
|
if (identical(this, other)) return true;
|
|
|
|
return other.taskId == taskId &&
|
|
other.filename == filename &&
|
|
other.progress == progress &&
|
|
other.fileSize == fileSize &&
|
|
other.networkSpeedAsString == networkSpeedAsString &&
|
|
other.isFailed == isFailed &&
|
|
other.error == error;
|
|
}
|
|
|
|
@override
|
|
int get hashCode {
|
|
return taskId.hashCode ^
|
|
filename.hashCode ^
|
|
progress.hashCode ^
|
|
fileSize.hashCode ^
|
|
networkSpeedAsString.hashCode ^
|
|
isFailed.hashCode ^
|
|
error.hashCode;
|
|
}
|
|
}
|
|
|
|
enum BackupError { none, syncFailed }
|
|
|
|
class DriftBackupState {
|
|
final int totalCount;
|
|
final int backupCount;
|
|
final int remainderCount;
|
|
final int processingCount;
|
|
|
|
final int enqueueCount;
|
|
final int enqueueTotalCount;
|
|
|
|
final bool isSyncing;
|
|
final bool isCanceling;
|
|
final BackupError error;
|
|
|
|
final Map<String, DriftUploadStatus> uploadItems;
|
|
|
|
const DriftBackupState({
|
|
required this.totalCount,
|
|
required this.backupCount,
|
|
required this.remainderCount,
|
|
required this.processingCount,
|
|
required this.enqueueCount,
|
|
required this.enqueueTotalCount,
|
|
required this.isCanceling,
|
|
required this.isSyncing,
|
|
required this.uploadItems,
|
|
this.error = BackupError.none,
|
|
});
|
|
|
|
DriftBackupState copyWith({
|
|
int? totalCount,
|
|
int? backupCount,
|
|
int? remainderCount,
|
|
int? processingCount,
|
|
int? enqueueCount,
|
|
int? enqueueTotalCount,
|
|
bool? isCanceling,
|
|
bool? isSyncing,
|
|
Map<String, DriftUploadStatus>? uploadItems,
|
|
BackupError? error,
|
|
}) {
|
|
return DriftBackupState(
|
|
totalCount: totalCount ?? this.totalCount,
|
|
backupCount: backupCount ?? this.backupCount,
|
|
remainderCount: remainderCount ?? this.remainderCount,
|
|
processingCount: processingCount ?? this.processingCount,
|
|
enqueueCount: enqueueCount ?? this.enqueueCount,
|
|
enqueueTotalCount: enqueueTotalCount ?? this.enqueueTotalCount,
|
|
isCanceling: isCanceling ?? this.isCanceling,
|
|
isSyncing: isSyncing ?? this.isSyncing,
|
|
uploadItems: uploadItems ?? this.uploadItems,
|
|
error: error ?? this.error,
|
|
);
|
|
}
|
|
|
|
@override
|
|
String toString() {
|
|
return 'DriftBackupState(totalCount: $totalCount, backupCount: $backupCount, remainderCount: $remainderCount, processingCount: $processingCount, enqueueCount: $enqueueCount, enqueueTotalCount: $enqueueTotalCount, isCanceling: $isCanceling, isSyncing: $isSyncing, uploadItems: $uploadItems, error: $error)';
|
|
}
|
|
|
|
@override
|
|
bool operator ==(covariant DriftBackupState other) {
|
|
if (identical(this, other)) return true;
|
|
final mapEquals = const DeepCollectionEquality().equals;
|
|
|
|
return other.totalCount == totalCount &&
|
|
other.backupCount == backupCount &&
|
|
other.remainderCount == remainderCount &&
|
|
other.processingCount == processingCount &&
|
|
other.enqueueCount == enqueueCount &&
|
|
other.enqueueTotalCount == enqueueTotalCount &&
|
|
other.isCanceling == isCanceling &&
|
|
other.isSyncing == isSyncing &&
|
|
mapEquals(other.uploadItems, uploadItems) &&
|
|
other.error == error;
|
|
}
|
|
|
|
@override
|
|
int get hashCode {
|
|
return totalCount.hashCode ^
|
|
backupCount.hashCode ^
|
|
remainderCount.hashCode ^
|
|
processingCount.hashCode ^
|
|
enqueueCount.hashCode ^
|
|
enqueueTotalCount.hashCode ^
|
|
isCanceling.hashCode ^
|
|
isSyncing.hashCode ^
|
|
uploadItems.hashCode ^
|
|
error.hashCode;
|
|
}
|
|
}
|
|
|
|
final driftBackupProvider = StateNotifierProvider<DriftBackupNotifier, DriftBackupState>((ref) {
|
|
return DriftBackupNotifier(ref.watch(uploadServiceProvider));
|
|
});
|
|
|
|
class DriftBackupNotifier extends StateNotifier<DriftBackupState> {
|
|
late final StreamSubscription<UploadApiTaskStatus> _taskStatusStream;
|
|
late final StreamSubscription<UploadApiTaskProgress> _taskProgressStream;
|
|
|
|
DriftBackupNotifier(this._uploadService)
|
|
: super(
|
|
const DriftBackupState(
|
|
totalCount: 0,
|
|
backupCount: 0,
|
|
remainderCount: 0,
|
|
processingCount: 0,
|
|
enqueueCount: 0,
|
|
enqueueTotalCount: 0,
|
|
isCanceling: false,
|
|
isSyncing: false,
|
|
uploadItems: {},
|
|
error: BackupError.none,
|
|
),
|
|
) {
|
|
_taskStatusStream = _uploadService.taskStatusStream.listen(_handleTaskStatusUpdate);
|
|
_taskProgressStream = _uploadService.taskProgressStream.listen(_handleTaskProgressUpdate);
|
|
}
|
|
|
|
final UploadService _uploadService;
|
|
final _logger = Logger("DriftBackupNotifier");
|
|
|
|
@override
|
|
void dispose() {
|
|
unawaited(_taskStatusStream.cancel());
|
|
unawaited(_taskProgressStream.cancel());
|
|
super.dispose();
|
|
}
|
|
|
|
/// Remove upload item from state
|
|
void _removeUploadItem(String taskId) {
|
|
if (state.uploadItems.containsKey(taskId)) {
|
|
final updatedItems = Map<String, DriftUploadStatus>.from(state.uploadItems);
|
|
updatedItems.remove(taskId);
|
|
state = state.copyWith(uploadItems: updatedItems);
|
|
}
|
|
}
|
|
|
|
void _handleTaskStatusUpdate(UploadApiTaskStatus update) {
|
|
final taskId = update.id;
|
|
|
|
switch (update.status) {
|
|
case UploadApiStatus.uploadComplete:
|
|
state = state.copyWith(backupCount: state.backupCount + 1, remainderCount: state.remainderCount - 1);
|
|
|
|
// Remove the completed task from the upload items
|
|
if (state.uploadItems.containsKey(taskId)) {
|
|
Future.delayed(const Duration(milliseconds: 1000), () {
|
|
_removeUploadItem(taskId);
|
|
});
|
|
}
|
|
|
|
case UploadApiStatus.uploadFailed:
|
|
final currentItem = state.uploadItems[taskId];
|
|
if (currentItem == null) {
|
|
return;
|
|
}
|
|
|
|
// String? error;
|
|
// final exception = update.exception;
|
|
// if (exception != null && exception is TaskHttpException) {
|
|
// final message = tryJsonDecode(exception.description)?['message'] as String?;
|
|
// if (message != null) {
|
|
// final responseCode = exception.httpResponseCode;
|
|
// error = "${exception.exceptionType}, response code $responseCode: $message";
|
|
// }
|
|
// }
|
|
// error ??= update.exception?.toString();
|
|
|
|
// state = state.copyWith(
|
|
// uploadItems: {
|
|
// ...state.uploadItems,
|
|
// taskId: currentItem.copyWith(isFailed: true, error: error),
|
|
// },
|
|
// );
|
|
// _logger.fine("Upload failed for taskId: $taskId, exception: ${update.exception}");
|
|
break;
|
|
|
|
case UploadApiStatus.uploadPending:
|
|
state = state.copyWith(
|
|
uploadItems: {
|
|
...state.uploadItems,
|
|
taskId: DriftUploadStatus(
|
|
taskId: update.id.toString(),
|
|
filename: update.filename,
|
|
fileSize: 0,
|
|
networkSpeedAsString: "",
|
|
progress: 0.0,
|
|
),
|
|
},
|
|
);
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
void _handleTaskProgressUpdate(UploadApiTaskProgress update) {
|
|
final taskId = update.id;
|
|
final progress = update.progress;
|
|
final currentItem = state.uploadItems[taskId];
|
|
if (currentItem == null) {
|
|
return;
|
|
}
|
|
|
|
state = state.copyWith(
|
|
uploadItems: {
|
|
...state.uploadItems,
|
|
taskId: currentItem.copyWith(
|
|
progress: progress,
|
|
fileSize: update.totalBytes,
|
|
networkSpeedAsString: update.speed?.toStringAsFixed(2) ?? "",
|
|
),
|
|
},
|
|
);
|
|
}
|
|
|
|
Future<void> getBackupStatus(String userId) async {
|
|
final counts = await _uploadService.getBackupCounts(userId);
|
|
|
|
state = state.copyWith(
|
|
totalCount: counts.total,
|
|
backupCount: counts.total - counts.remainder,
|
|
remainderCount: counts.remainder,
|
|
processingCount: counts.processing,
|
|
);
|
|
}
|
|
|
|
void updateError(BackupError error) async {
|
|
state = state.copyWith(error: error);
|
|
}
|
|
|
|
void updateSyncing(bool isSyncing) async {
|
|
state = state.copyWith(isSyncing: isSyncing);
|
|
}
|
|
|
|
Future<void> startBackup() {
|
|
state = state.copyWith(error: BackupError.none);
|
|
return _uploadService.startBackup();
|
|
}
|
|
|
|
Future<void> cancel() async {
|
|
dPrint(() => "Canceling backup tasks...");
|
|
state = state.copyWith(enqueueCount: 0, enqueueTotalCount: 0, isCanceling: true, error: BackupError.none);
|
|
|
|
await _uploadService.cancelBackup();
|
|
dPrint(() => "All tasks canceled successfully.");
|
|
state = state.copyWith(isCanceling: false, uploadItems: {});
|
|
}
|
|
}
|
|
|
|
final driftBackupCandidateProvider = FutureProvider.autoDispose<List<LocalAsset>>((ref) async {
|
|
final user = ref.watch(currentUserProvider);
|
|
if (user == null) {
|
|
return [];
|
|
}
|
|
|
|
return ref.read(backupRepositoryProvider).getCandidates(user.id, onlyHashed: false);
|
|
});
|
|
|
|
final driftCandidateBackupAlbumInfoProvider = FutureProvider.autoDispose.family<List<LocalAlbum>, String>((
|
|
ref,
|
|
assetId,
|
|
) {
|
|
return ref.read(localAssetRepository).getSourceAlbums(assetId, backupSelection: BackupSelection.selected);
|
|
});
|