mirror of
https://github.com/immich-app/immich.git
synced 2025-07-07 10:14:08 -04:00
wip
This commit is contained in:
parent
f87ae08cd1
commit
3ccde454b1
@ -7,8 +7,10 @@ abstract interface class IBackupRepository implements IDatabaseRepository {
|
|||||||
|
|
||||||
Future<List<String>> getAssetIds(String albumId);
|
Future<List<String>> getAssetIds(String albumId);
|
||||||
|
|
||||||
/// Returns the total number of assets that are selected for backup.
|
Future<int> getTotalCount();
|
||||||
Future<int> getTotalCount(BackupSelection selection);
|
Future<int> getRemainderCount();
|
||||||
|
|
||||||
Future<int> getBackupCount();
|
Future<int> getBackupCount();
|
||||||
|
|
||||||
|
Future<List<LocalAlbum>> getBackupAlbums(BackupSelection selectionType);
|
||||||
|
Future<List<LocalAsset>> getCandidates();
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
|
// ignore: import_rule_photo_manager
|
||||||
|
import 'package:photo_manager/photo_manager.dart';
|
||||||
|
|
||||||
abstract interface class IStorageRepository {
|
abstract interface class IStorageRepository {
|
||||||
Future<File?> getFileForAsset(LocalAsset asset);
|
Future<File?> getFileForAsset(LocalAsset asset);
|
||||||
|
Future<AssetEntity?> getAssetEntityForAsset(LocalAsset asset);
|
||||||
}
|
}
|
||||||
|
@ -51,7 +51,9 @@ class DriftBackupRepository extends DriftDatabaseRepository
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<int> getTotalCount(BackupSelection selection) {
|
Future<int> getTotalCount() async {
|
||||||
|
final excludedAssetIds = await _getExcludedAssetIds();
|
||||||
|
|
||||||
final query = _db.localAlbumAssetEntity.selectOnly(distinct: true)
|
final query = _db.localAlbumAssetEntity.selectOnly(distinct: true)
|
||||||
..addColumns([_db.localAlbumAssetEntity.assetId])
|
..addColumns([_db.localAlbumAssetEntity.assetId])
|
||||||
..join([
|
..join([
|
||||||
@ -61,29 +63,147 @@ class DriftBackupRepository extends DriftDatabaseRepository
|
|||||||
),
|
),
|
||||||
])
|
])
|
||||||
..where(
|
..where(
|
||||||
_db.localAlbumEntity.backupSelection.equals(selection.index),
|
_db.localAlbumEntity.backupSelection
|
||||||
|
.equals(BackupSelection.selected.index) &
|
||||||
|
(excludedAssetIds.isEmpty
|
||||||
|
? const Constant(true)
|
||||||
|
: _db.localAlbumAssetEntity.assetId.isNotIn(excludedAssetIds)),
|
||||||
);
|
);
|
||||||
|
|
||||||
return query.get().then((rows) => rows.length);
|
return query.get().then((rows) => rows.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<int> getBackupCount() {
|
Future<int> getRemainderCount() async {
|
||||||
final query = _db.localAlbumEntity.select().join(
|
final excludedAssetIds = await _getExcludedAssetIds();
|
||||||
[
|
|
||||||
|
final query = _db.localAlbumAssetEntity.selectOnly(distinct: true)
|
||||||
|
..addColumns(
|
||||||
|
[_db.localAlbumAssetEntity.assetId],
|
||||||
|
)
|
||||||
|
..join([
|
||||||
innerJoin(
|
innerJoin(
|
||||||
_db.localAlbumAssetEntity,
|
_db.localAlbumEntity,
|
||||||
_db.localAlbumAssetEntity.albumId.equalsExp(_db.localAlbumEntity.id),
|
_db.localAlbumAssetEntity.albumId.equalsExp(_db.localAlbumEntity.id),
|
||||||
),
|
),
|
||||||
],
|
innerJoin(
|
||||||
)..where(
|
_db.localAssetEntity,
|
||||||
_db.localAlbumEntity.backupSelection.equals(
|
_db.localAlbumAssetEntity.assetId.equalsExp(_db.localAssetEntity.id),
|
||||||
BackupSelection.selected.index,
|
|
||||||
),
|
),
|
||||||
|
leftOuterJoin(
|
||||||
|
_db.remoteAssetEntity,
|
||||||
|
_db.localAssetEntity.checksum
|
||||||
|
.equalsExp(_db.remoteAssetEntity.checksum),
|
||||||
|
),
|
||||||
|
])
|
||||||
|
..where(
|
||||||
|
_db.localAlbumEntity.backupSelection
|
||||||
|
.equals(BackupSelection.selected.index) &
|
||||||
|
_db.remoteAssetEntity.checksum.isNull() &
|
||||||
|
(excludedAssetIds.isEmpty
|
||||||
|
? const Constant(true)
|
||||||
|
: _db.localAlbumAssetEntity.assetId.isNotIn(excludedAssetIds)),
|
||||||
);
|
);
|
||||||
|
|
||||||
return query.get().then((rows) => rows.length);
|
return query.get().then((rows) => rows.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<int> getBackupCount() async {
|
||||||
|
final excludedAssetIds = await _getExcludedAssetIds();
|
||||||
|
final query = _db.localAlbumAssetEntity.selectOnly(distinct: true)
|
||||||
|
..addColumns(
|
||||||
|
[_db.localAlbumAssetEntity.assetId],
|
||||||
|
)
|
||||||
|
..join([
|
||||||
|
innerJoin(
|
||||||
|
_db.localAlbumEntity,
|
||||||
|
_db.localAlbumAssetEntity.albumId.equalsExp(_db.localAlbumEntity.id),
|
||||||
|
),
|
||||||
|
innerJoin(
|
||||||
|
_db.localAssetEntity,
|
||||||
|
_db.localAlbumAssetEntity.assetId.equalsExp(_db.localAssetEntity.id),
|
||||||
|
),
|
||||||
|
innerJoin(
|
||||||
|
_db.remoteAssetEntity,
|
||||||
|
_db.localAssetEntity.checksum
|
||||||
|
.equalsExp(_db.remoteAssetEntity.checksum),
|
||||||
|
),
|
||||||
|
])
|
||||||
|
..where(
|
||||||
|
_db.localAlbumEntity.backupSelection
|
||||||
|
.equals(BackupSelection.selected.index) &
|
||||||
|
_db.remoteAssetEntity.checksum.isNotNull() &
|
||||||
|
(excludedAssetIds.isEmpty
|
||||||
|
? const Constant(true)
|
||||||
|
: _db.localAlbumAssetEntity.assetId.isNotIn(excludedAssetIds)),
|
||||||
|
);
|
||||||
|
|
||||||
|
return query.get().then((rows) => rows.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<String>> _getExcludedAssetIds() async {
|
||||||
|
final query = _db.localAlbumAssetEntity.selectOnly()
|
||||||
|
..addColumns([_db.localAlbumAssetEntity.assetId])
|
||||||
|
..join([
|
||||||
|
innerJoin(
|
||||||
|
_db.localAlbumEntity,
|
||||||
|
_db.localAlbumAssetEntity.albumId.equalsExp(_db.localAlbumEntity.id),
|
||||||
|
),
|
||||||
|
])
|
||||||
|
..where(
|
||||||
|
_db.localAlbumEntity.backupSelection
|
||||||
|
.equals(BackupSelection.excluded.index),
|
||||||
|
);
|
||||||
|
|
||||||
|
return query
|
||||||
|
.map((row) => row.read(_db.localAlbumAssetEntity.assetId)!)
|
||||||
|
.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<LocalAlbum>> getBackupAlbums(BackupSelection selectionType) {
|
||||||
|
final query = _db.localAlbumEntity.select()
|
||||||
|
..where(
|
||||||
|
(tbl) => tbl.backupSelection.equals(selectionType.index),
|
||||||
|
);
|
||||||
|
|
||||||
|
return query.map((localAlbum) => localAlbum.toDto(assetCount: 0)).get();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<LocalAsset>> getCandidates() async {
|
||||||
|
final excludedAssetIds = await _getExcludedAssetIds();
|
||||||
|
|
||||||
|
final query = _db.localAlbumAssetEntity.select(distinct: true).join(
|
||||||
|
[
|
||||||
|
innerJoin(
|
||||||
|
_db.localAlbumEntity,
|
||||||
|
_db.localAlbumAssetEntity.albumId.equalsExp(_db.localAlbumEntity.id),
|
||||||
|
),
|
||||||
|
innerJoin(
|
||||||
|
_db.localAssetEntity,
|
||||||
|
_db.localAlbumAssetEntity.assetId.equalsExp(_db.localAssetEntity.id),
|
||||||
|
),
|
||||||
|
leftOuterJoin(
|
||||||
|
_db.remoteAssetEntity,
|
||||||
|
_db.localAssetEntity.checksum
|
||||||
|
.equalsExp(_db.remoteAssetEntity.checksum),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)..where(
|
||||||
|
_db.localAlbumEntity.backupSelection
|
||||||
|
.equals(BackupSelection.selected.index) &
|
||||||
|
_db.remoteAssetEntity.checksum.isNull() &
|
||||||
|
(excludedAssetIds.isEmpty
|
||||||
|
? const Constant(true)
|
||||||
|
: _db.localAlbumAssetEntity.assetId.isNotIn(excludedAssetIds)),
|
||||||
|
);
|
||||||
|
|
||||||
|
return query
|
||||||
|
.map((row) => row.readTable(_db.localAssetEntity).toDto())
|
||||||
|
.get();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension on LocalAlbumEntityData {
|
extension on LocalAlbumEntityData {
|
||||||
|
@ -28,4 +28,24 @@ class StorageRepository implements IStorageRepository {
|
|||||||
}
|
}
|
||||||
return file;
|
return file;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<AssetEntity?> getAssetEntityForAsset(LocalAsset asset) async {
|
||||||
|
AssetEntity? entity;
|
||||||
|
try {
|
||||||
|
entity = await AssetEntity.fromId(asset.id);
|
||||||
|
if (entity == null) {
|
||||||
|
_log.warning(
|
||||||
|
"Cannot get AssetEntity for asset ${asset.id}, name: ${asset.name}, created on: ${asset.createdAt}",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
_log.warning(
|
||||||
|
"Error getting AssetEntity for asset ${asset.id}, name: ${asset.name}, created on: ${asset.createdAt}",
|
||||||
|
error,
|
||||||
|
stackTrace,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,7 @@ abstract interface class IUploadRepository {
|
|||||||
|
|
||||||
void enqueueAll(List<UploadTask> tasks);
|
void enqueueAll(List<UploadTask> tasks);
|
||||||
Future<bool> cancel(String id);
|
Future<bool> cancel(String id);
|
||||||
void cancelAll();
|
Future<bool> cancelAll();
|
||||||
Future<void> pauseAll();
|
Future<void> pauseAll();
|
||||||
Future<void> deleteAllTrackingRecords();
|
Future<void> deleteAllTrackingRecords();
|
||||||
Future<void> deleteRecordsWithIds(List<String> id);
|
Future<void> deleteRecordsWithIds(List<String> id);
|
||||||
|
@ -95,55 +95,34 @@ class ExpBackupPage extends HookConsumerWidget {
|
|||||||
[backupState.backupProgress],
|
[backupState.backupProgress],
|
||||||
);
|
);
|
||||||
|
|
||||||
void startBackup() {
|
Widget buildControlButtons() {
|
||||||
ref.watch(errorBackupListProvider.notifier).empty();
|
|
||||||
if (ref.watch(backupProvider).backupProgress !=
|
|
||||||
BackUpProgressEnum.inBackground) {
|
|
||||||
ref.watch(backupProvider.notifier).startBackupProcess();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget buildBackupButton() {
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(
|
padding: const EdgeInsets.only(
|
||||||
top: 24,
|
top: 24,
|
||||||
),
|
),
|
||||||
child: Container(
|
child: Column(
|
||||||
child: backupState.backupProgress == BackUpProgressEnum.inProgress ||
|
children: [
|
||||||
backupState.backupProgress ==
|
ElevatedButton(
|
||||||
BackUpProgressEnum.manualInProgress
|
onPressed: () => ref.read(expBackupProvider.notifier).backup(),
|
||||||
? ElevatedButton(
|
child: const Text(
|
||||||
style: ElevatedButton.styleFrom(
|
"backup_controller_page_start_backup",
|
||||||
foregroundColor: Colors.grey[50],
|
style: TextStyle(
|
||||||
backgroundColor: Colors.red[300],
|
fontSize: 16,
|
||||||
// padding: const EdgeInsets.all(14),
|
fontWeight: FontWeight.bold,
|
||||||
),
|
|
||||||
onPressed: () {
|
|
||||||
if (backupState.backupProgress ==
|
|
||||||
BackUpProgressEnum.manualInProgress) {
|
|
||||||
ref.read(manualUploadProvider.notifier).cancelBackup();
|
|
||||||
} else {
|
|
||||||
ref.read(backupProvider.notifier).cancelBackup();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: const Text(
|
|
||||||
"cancel",
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
).tr(),
|
|
||||||
)
|
|
||||||
: ElevatedButton(
|
|
||||||
onPressed: shouldBackup ? startBackup : null,
|
|
||||||
child: const Text(
|
|
||||||
"backup_controller_page_start_backup",
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
).tr(),
|
|
||||||
),
|
),
|
||||||
|
).tr(),
|
||||||
|
),
|
||||||
|
OutlinedButton(
|
||||||
|
onPressed: () => ref.read(expBackupProvider.notifier).cancel(),
|
||||||
|
child: const Text(
|
||||||
|
"cancel",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
).tr(),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -214,11 +193,11 @@ class ExpBackupPage extends HookConsumerWidget {
|
|||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
const BackupAlbumSelectionCard(),
|
const BackupAlbumSelectionCard(),
|
||||||
const TotalCard(),
|
const TotalCard(),
|
||||||
|
const BackupCard(),
|
||||||
const RemainderCard(),
|
const RemainderCard(),
|
||||||
const Divider(),
|
const Divider(),
|
||||||
|
buildControlButtons(),
|
||||||
const CurrentUploadingAssetInfoBox(),
|
const CurrentUploadingAssetInfoBox(),
|
||||||
if (!hasExclusiveAccess) buildBackgroundBackupInfo(),
|
|
||||||
buildBackupButton(),
|
|
||||||
]
|
]
|
||||||
: [
|
: [
|
||||||
const BackupAlbumSelectionCard(),
|
const BackupAlbumSelectionCard(),
|
||||||
@ -369,18 +348,33 @@ class TotalCard extends ConsumerWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class BackupCard extends ConsumerWidget {
|
||||||
|
const BackupCard({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final backupCount =
|
||||||
|
ref.watch(expBackupProvider.select((p) => p.backupCount));
|
||||||
|
|
||||||
|
return BackupInfoCard(
|
||||||
|
title: "backup_controller_page_backup".tr(),
|
||||||
|
subtitle: "backup_controller_page_backup_sub".tr(),
|
||||||
|
info: backupCount.toString(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class RemainderCard extends ConsumerWidget {
|
class RemainderCard extends ConsumerWidget {
|
||||||
const RemainderCard({super.key});
|
const RemainderCard({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final backupState = ref.watch(backupProvider);
|
final remainderCount =
|
||||||
|
ref.watch(expBackupProvider.select((p) => p.remainderCount));
|
||||||
return BackupInfoCard(
|
return BackupInfoCard(
|
||||||
title: "backup_controller_page_remainder".tr(),
|
title: "backup_controller_page_remainder".tr(),
|
||||||
subtitle: "backup_controller_page_remainder_sub".tr(),
|
subtitle: "backup_controller_page_remainder_sub".tr(),
|
||||||
info: backupState.availableAlbums.isEmpty
|
info: remainderCount.toString(),
|
||||||
? "..."
|
|
||||||
: "${max(0, backupState.allUniqueAssets.length - backupState.selectedAlbumsBackupAssetsIds.length)}",
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,33 +1,52 @@
|
|||||||
// ignore_for_file: public_member_api_docs, sort_constructors_first
|
// ignore_for_file: public_member_api_docs, sort_constructors_first
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:background_downloader/background_downloader.dart';
|
||||||
|
import 'package:flutter/cupertino.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/constants/constants.dart';
|
||||||
|
import 'package:immich_mobile/domain/utils/background_sync.dart';
|
||||||
|
import 'package:immich_mobile/providers/background_sync.provider.dart';
|
||||||
|
|
||||||
import 'package:immich_mobile/services/exp_backup.service.dart';
|
import 'package:immich_mobile/services/exp_backup.service.dart';
|
||||||
|
import 'package:immich_mobile/services/upload.service.dart';
|
||||||
|
|
||||||
class ExpBackupState {
|
class ExpBackupState {
|
||||||
final int totalCount;
|
final int totalCount;
|
||||||
|
final int backupCount;
|
||||||
|
final int remainderCount;
|
||||||
|
|
||||||
ExpBackupState({
|
ExpBackupState({
|
||||||
required this.totalCount,
|
required this.totalCount,
|
||||||
|
required this.backupCount,
|
||||||
|
required this.remainderCount,
|
||||||
});
|
});
|
||||||
|
|
||||||
ExpBackupState copyWith({
|
ExpBackupState copyWith({
|
||||||
int? totalCount,
|
int? totalCount,
|
||||||
|
int? backupCount,
|
||||||
|
int? remainderCount,
|
||||||
}) {
|
}) {
|
||||||
return ExpBackupState(
|
return ExpBackupState(
|
||||||
totalCount: totalCount ?? this.totalCount,
|
totalCount: totalCount ?? this.totalCount,
|
||||||
|
backupCount: backupCount ?? this.backupCount,
|
||||||
|
remainderCount: remainderCount ?? this.remainderCount,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, dynamic> toMap() {
|
Map<String, dynamic> toMap() {
|
||||||
return <String, dynamic>{
|
return <String, dynamic>{
|
||||||
'totalCount': totalCount,
|
'totalCount': totalCount,
|
||||||
|
'backupCount': backupCount,
|
||||||
|
'remainderCount': remainderCount,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
factory ExpBackupState.fromMap(Map<String, dynamic> map) {
|
factory ExpBackupState.fromMap(Map<String, dynamic> map) {
|
||||||
return ExpBackupState(
|
return ExpBackupState(
|
||||||
totalCount: map['totalCount'] as int,
|
totalCount: map['totalCount'] as int,
|
||||||
|
backupCount: map['backupCount'] as int,
|
||||||
|
remainderCount: map['remainderCount'] as int,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -37,37 +56,103 @@ class ExpBackupState {
|
|||||||
ExpBackupState.fromMap(json.decode(source) as Map<String, dynamic>);
|
ExpBackupState.fromMap(json.decode(source) as Map<String, dynamic>);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'ExpBackupState(totalCount: $totalCount)';
|
String toString() =>
|
||||||
|
'ExpBackupState(totalCount: $totalCount, backupCount: $backupCount, remainderCount: $remainderCount)';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(covariant ExpBackupState other) {
|
bool operator ==(covariant ExpBackupState other) {
|
||||||
if (identical(this, other)) return true;
|
if (identical(this, other)) return true;
|
||||||
|
|
||||||
return other.totalCount == totalCount;
|
return other.totalCount == totalCount &&
|
||||||
|
other.backupCount == backupCount &&
|
||||||
|
other.remainderCount == remainderCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode => totalCount.hashCode;
|
int get hashCode =>
|
||||||
|
totalCount.hashCode ^ backupCount.hashCode ^ remainderCount.hashCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
final expBackupProvider =
|
final expBackupProvider =
|
||||||
StateNotifierProvider<ExpBackupNotifier, ExpBackupState>((ref) {
|
StateNotifierProvider<ExpBackupNotifier, ExpBackupState>((ref) {
|
||||||
return ExpBackupNotifier(ref.watch(expBackupServiceProvider));
|
return ExpBackupNotifier(
|
||||||
|
ref.watch(expBackupServiceProvider),
|
||||||
|
ref.watch(uploadServiceProvider),
|
||||||
|
ref.watch(backgroundSyncProvider),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
class ExpBackupNotifier extends StateNotifier<ExpBackupState> {
|
class ExpBackupNotifier extends StateNotifier<ExpBackupState> {
|
||||||
ExpBackupNotifier(this._backupService)
|
ExpBackupNotifier(
|
||||||
: super(
|
this._backupService,
|
||||||
|
this._uploadService,
|
||||||
|
this._backgroundSyncManager,
|
||||||
|
) : super(
|
||||||
ExpBackupState(
|
ExpBackupState(
|
||||||
totalCount: 0,
|
totalCount: 0,
|
||||||
|
backupCount: 0,
|
||||||
|
remainderCount: 0,
|
||||||
),
|
),
|
||||||
);
|
) {
|
||||||
|
{
|
||||||
|
_uploadService.onUploadStatus = _uploadStatusCallback;
|
||||||
|
_uploadService.onTaskProgress = _taskProgressCallback;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final ExpBackupService _backupService;
|
final ExpBackupService _backupService;
|
||||||
|
final UploadService _uploadService;
|
||||||
|
final BackgroundSyncManager _backgroundSyncManager;
|
||||||
|
|
||||||
|
void _updateUploadStatus(TaskStatusUpdate task, TaskStatus status) async {
|
||||||
|
if (status == TaskStatus.canceled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _uploadStatusCallback(TaskStatusUpdate update) {
|
||||||
|
_updateUploadStatus(update, update.status);
|
||||||
|
|
||||||
|
switch (update.status) {
|
||||||
|
case TaskStatus.complete:
|
||||||
|
state = state.copyWith(
|
||||||
|
backupCount: state.backupCount + 1,
|
||||||
|
remainderCount: state.remainderCount - 1,
|
||||||
|
);
|
||||||
|
|
||||||
|
// TODO: find a better place to call this.
|
||||||
|
_backgroundSyncManager.syncRemote();
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _taskProgressCallback(TaskProgressUpdate update) {
|
||||||
|
debugPrint("[_taskProgressCallback] $update");
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> getBackupStatus() async {
|
Future<void> getBackupStatus() async {
|
||||||
final totalCount = await _backupService.getTotalCount();
|
final [totalCount, backupCount, remainderCount] = await Future.wait([
|
||||||
|
_backupService.getTotalCount(),
|
||||||
|
_backupService.getBackupCount(),
|
||||||
|
_backupService.getRemainderCount(),
|
||||||
|
]);
|
||||||
|
|
||||||
state = state.copyWith(totalCount: totalCount);
|
state = state.copyWith(
|
||||||
|
totalCount: totalCount,
|
||||||
|
backupCount: backupCount,
|
||||||
|
remainderCount: remainderCount,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> backup() async {
|
||||||
|
await _backupService.backup();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> cancel() async {
|
||||||
|
await _uploadService.cancel();
|
||||||
|
debugPrint("Cancel uploads");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -33,7 +33,7 @@ class UploadRepository implements IUploadRepository {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> deleteAllTrackingRecords() {
|
Future<void> deleteAllTrackingRecords() {
|
||||||
return FileDownloader().database.deleteAllRecords();
|
return FileDownloader().database.deleteAllRecords(group: kUploadGroup);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -42,8 +42,9 @@ class UploadRepository implements IUploadRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void cancelAll() {
|
Future<bool> cancelAll() {
|
||||||
return taskQueue.removeAll();
|
taskQueue.removeTasksWithGroup(kUploadGroup);
|
||||||
|
return FileDownloader().cancelAll(group: kUploadGroup);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -298,7 +298,7 @@ class BackupService {
|
|||||||
|
|
||||||
print("Uploading ${uploadTasks.length} assets");
|
print("Uploading ${uploadTasks.length} assets");
|
||||||
|
|
||||||
_uploadService.upload(uploadTasks);
|
_uploadService.enqueueTasks(uploadTasks);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> backupAsset(
|
Future<bool> backupAsset(
|
||||||
|
@ -1,31 +1,87 @@
|
|||||||
|
import 'package:background_downloader/background_downloader.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
import 'package:immich_mobile/domain/interfaces/backup.interface.dart';
|
import 'package:immich_mobile/domain/interfaces/backup.interface.dart';
|
||||||
import 'package:immich_mobile/domain/interfaces/local_album.interface.dart';
|
import 'package:immich_mobile/domain/interfaces/storage.interface.dart';
|
||||||
import 'package:immich_mobile/domain/models/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/infrastructure/repositories/backup.repository.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/storage.provider.dart';
|
||||||
|
import 'package:immich_mobile/services/upload.service.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
|
||||||
final expBackupServiceProvider = Provider<ExpBackupService>(
|
final expBackupServiceProvider = Provider<ExpBackupService>(
|
||||||
(ref) => ExpBackupService(
|
(ref) => ExpBackupService(
|
||||||
ref.watch(backupRepositoryProvider),
|
ref.watch(backupRepositoryProvider),
|
||||||
|
ref.watch(storageRepositoryProvider),
|
||||||
|
ref.watch(uploadServiceProvider),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
class ExpBackupService {
|
class ExpBackupService {
|
||||||
ExpBackupService(this._backupRepository);
|
ExpBackupService(
|
||||||
|
this._backupRepository,
|
||||||
|
this._storageRepository,
|
||||||
|
this._uploadService,
|
||||||
|
);
|
||||||
|
|
||||||
final IBackupRepository _backupRepository;
|
final IBackupRepository _backupRepository;
|
||||||
|
final IStorageRepository _storageRepository;
|
||||||
|
final UploadService _uploadService;
|
||||||
|
|
||||||
Future<int> getTotalCount() async {
|
Future<int> getTotalCount() {
|
||||||
final [selectedCount, excludedCount] = await Future.wait([
|
return _backupRepository.getTotalCount();
|
||||||
_backupRepository.getTotalCount(BackupSelection.selected),
|
}
|
||||||
_backupRepository.getTotalCount(BackupSelection.excluded),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return selectedCount - excludedCount;
|
Future<int> getRemainderCount() {
|
||||||
|
return _backupRepository.getRemainderCount();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<int> getBackupCount() {
|
Future<int> getBackupCount() {
|
||||||
return _backupRepository.getBackupCount();
|
return _backupRepository.getBackupCount();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> backup() async {
|
||||||
|
final candidates = await _backupRepository.getCandidates();
|
||||||
|
if (candidates.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const batchSize = 5;
|
||||||
|
for (int i = 0; i < candidates.length; i += batchSize) {
|
||||||
|
final batch = candidates.skip(i).take(batchSize).toList();
|
||||||
|
|
||||||
|
List<UploadTask> tasks = [];
|
||||||
|
for (final asset in batch) {
|
||||||
|
final task = await _getUploadTask(asset);
|
||||||
|
if (task != null) {
|
||||||
|
tasks.add(task);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tasks.isNotEmpty) {
|
||||||
|
_uploadService.enqueueTasks(tasks);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<UploadTask?> _getUploadTask(LocalAsset asset) async {
|
||||||
|
final entity = await _storageRepository.getAssetEntityForAsset(asset);
|
||||||
|
if (entity == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final file = await _storageRepository.getFileForAsset(asset);
|
||||||
|
if (file == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return _uploadService.buildUploadTask(
|
||||||
|
file,
|
||||||
|
originalFileName: asset.name,
|
||||||
|
deviceAssetId: asset.id,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> cancel() async {
|
||||||
|
await _uploadService.cancel();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -42,18 +42,15 @@ class UploadService {
|
|||||||
return FileDownloader().cancelTaskWithId(id);
|
return FileDownloader().cancelTaskWithId(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
void cancelAllUpload() {
|
Future<void> cancel() async {
|
||||||
return _uploadRepository.cancelAll();
|
await _uploadRepository.cancelAll();
|
||||||
|
await _uploadRepository.deleteAllTrackingRecords();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> pauseAllUploads() {
|
Future<void> pause() {
|
||||||
return _uploadRepository.pauseAll();
|
return _uploadRepository.pauseAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> deleteAllUploadTasks() {
|
|
||||||
return _uploadRepository.deleteAllTrackingRecords();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<List<TaskRecord>> getRecords() async {
|
Future<List<TaskRecord>> getRecords() async {
|
||||||
final all = await _uploadRepository.getRecords();
|
final all = await _uploadRepository.getRecords();
|
||||||
print('all record: all: ${all.length} records found');
|
print('all record: all: ${all.length} records found');
|
||||||
@ -64,7 +61,7 @@ class UploadService {
|
|||||||
return all;
|
return all;
|
||||||
}
|
}
|
||||||
|
|
||||||
void upload(List<UploadTask> tasks) {
|
void enqueueTasks(List<UploadTask> tasks) {
|
||||||
_uploadRepository.enqueueAll(tasks);
|
_uploadRepository.enqueueAll(tasks);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -111,6 +108,7 @@ class UploadService {
|
|||||||
|
|
||||||
return UploadTask(
|
return UploadTask(
|
||||||
taskId: id,
|
taskId: id,
|
||||||
|
displayName: filename,
|
||||||
httpRequestMethod: 'POST',
|
httpRequestMethod: 'POST',
|
||||||
url: url,
|
url: url,
|
||||||
headers: headers,
|
headers: headers,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user