Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(mobile): handle backup iCloud asset #5508

Merged
merged 10 commits into from
Dec 7, 2023
2 changes: 1 addition & 1 deletion mobile/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -169,4 +169,4 @@ SPEC CHECKSUMS:

PODFILE CHECKSUM: 599d8aeb73728400c15364e734525722250a5382

COCOAPODS: 1.11.3
COCOAPODS: 1.12.1
Original file line number Diff line number Diff line change
Expand Up @@ -342,7 +342,8 @@ class BackgroundService {

ApiService apiService = ApiService();
apiService.setAccessToken(Store.get(StoreKey.accessToken));
BackupService backupService = BackupService(apiService, db);
AppSettingsService settingService = AppSettingsService();
BackupService backupService = BackupService(apiService, db, settingService);
AppSettingsService settingsService = AppSettingsService();

final selectedAlbums = backupService.selectedAlbumsQuery().findAllSync();
Expand Down Expand Up @@ -452,9 +453,12 @@ class BackgroundService {
);

_cancellationToken = CancellationToken();
final pmProgressHandler = PMProgressHandler();

final bool ok = await backupService.backupAsset(
toUpload,
_cancellationToken!,
pmProgressHandler,
notifyTotalProgress ? _onAssetUploaded : (assetId, deviceId, isDup) {},
notifySingleProgress ? _onProgress : (sent, total) {},
notifySingleProgress ? _onSetCurrentBackupAsset : (asset) {},
Expand Down
18 changes: 13 additions & 5 deletions mobile/lib/modules/backup/models/backup_state.model.dart
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
// ignore_for_file: public_member_api_docs, sort_constructors_first

import 'package:cancellation_token_http/http.dart';
import 'package:collection/collection.dart';
import 'package:immich_mobile/shared/models/server_info/server_disk_info.model.dart';
import 'package:photo_manager/photo_manager.dart';

import 'package:immich_mobile/modules/backup/models/available_album.model.dart';
import 'package:immich_mobile/modules/backup/models/current_upload_asset.model.dart';
import 'package:immich_mobile/shared/models/server_info/server_disk_info.model.dart';

enum BackUpProgressEnum {
idle,
Expand All @@ -19,6 +21,7 @@ class BackUpState {
final BackUpProgressEnum backupProgress;
final List<String> allAssetsInDatabase;
final double progressInPercentage;
final double iCloudDownloadProgress;
final CancellationToken cancelToken;
final ServerDiskInfo serverInfo;
final bool autoBackup;
Expand All @@ -45,6 +48,7 @@ class BackUpState {
required this.backupProgress,
required this.allAssetsInDatabase,
required this.progressInPercentage,
required this.iCloudDownloadProgress,
required this.cancelToken,
required this.serverInfo,
required this.autoBackup,
Expand All @@ -64,6 +68,7 @@ class BackUpState {
BackUpProgressEnum? backupProgress,
List<String>? allAssetsInDatabase,
double? progressInPercentage,
double? iCloudDownloadProgress,
CancellationToken? cancelToken,
ServerDiskInfo? serverInfo,
bool? autoBackup,
Expand All @@ -82,6 +87,8 @@ class BackUpState {
backupProgress: backupProgress ?? this.backupProgress,
allAssetsInDatabase: allAssetsInDatabase ?? this.allAssetsInDatabase,
progressInPercentage: progressInPercentage ?? this.progressInPercentage,
iCloudDownloadProgress:
iCloudDownloadProgress ?? this.iCloudDownloadProgress,
cancelToken: cancelToken ?? this.cancelToken,
serverInfo: serverInfo ?? this.serverInfo,
autoBackup: autoBackup ?? this.autoBackup,
Expand All @@ -102,18 +109,18 @@ class BackUpState {

@override
String toString() {
return 'BackUpState(backupProgress: $backupProgress, allAssetsInDatabase: $allAssetsInDatabase, progressInPercentage: $progressInPercentage, cancelToken: $cancelToken, serverInfo: $serverInfo, autoBackup: $autoBackup, backgroundBackup: $backgroundBackup, backupRequireWifi: $backupRequireWifi, backupRequireCharging: $backupRequireCharging, backupTriggerDelay: $backupTriggerDelay, availableAlbums: $availableAlbums, selectedBackupAlbums: $selectedBackupAlbums, excludedBackupAlbums: $excludedBackupAlbums, allUniqueAssets: $allUniqueAssets, selectedAlbumsBackupAssetsIds: $selectedAlbumsBackupAssetsIds, currentUploadAsset: $currentUploadAsset)';
return 'BackUpState(backupProgress: $backupProgress, allAssetsInDatabase: $allAssetsInDatabase, progressInPercentage: $progressInPercentage, iCloudDownloadProgress: $iCloudDownloadProgress, cancelToken: $cancelToken, serverInfo: $serverInfo, autoBackup: $autoBackup, backgroundBackup: $backgroundBackup, backupRequireWifi: $backupRequireWifi, backupRequireCharging: $backupRequireCharging, backupTriggerDelay: $backupTriggerDelay, availableAlbums: $availableAlbums, selectedBackupAlbums: $selectedBackupAlbums, excludedBackupAlbums: $excludedBackupAlbums, allUniqueAssets: $allUniqueAssets, selectedAlbumsBackupAssetsIds: $selectedAlbumsBackupAssetsIds, currentUploadAsset: $currentUploadAsset)';
}

@override
bool operator ==(Object other) {
bool operator ==(covariant BackUpState other) {
if (identical(this, other)) return true;
final collectionEquals = const DeepCollectionEquality().equals;

return other is BackUpState &&
other.backupProgress == backupProgress &&
return other.backupProgress == backupProgress &&
collectionEquals(other.allAssetsInDatabase, allAssetsInDatabase) &&
other.progressInPercentage == progressInPercentage &&
other.iCloudDownloadProgress == iCloudDownloadProgress &&
other.cancelToken == cancelToken &&
other.serverInfo == serverInfo &&
other.autoBackup == autoBackup &&
Expand All @@ -137,6 +144,7 @@ class BackUpState {
return backupProgress.hashCode ^
allAssetsInDatabase.hashCode ^
progressInPercentage.hashCode ^
iCloudDownloadProgress.hashCode ^
cancelToken.hashCode ^
serverInfo.hashCode ^
autoBackup.hashCode ^
Expand Down
46 changes: 27 additions & 19 deletions mobile/lib/modules/backup/models/current_upload_asset.model.dart
Original file line number Diff line number Diff line change
@@ -1,78 +1,86 @@
// ignore_for_file: public_member_api_docs, sort_constructors_first
import 'dart:convert';

class CurrentUploadAsset {
final String id;
final DateTime fileCreatedAt;
final String fileName;
final String fileType;
final bool? iCloudAsset;

CurrentUploadAsset({
required this.id,
required this.fileCreatedAt,
required this.fileName,
required this.fileType,
this.iCloudAsset,
});

CurrentUploadAsset copyWith({
String? id,
DateTime? fileCreatedAt,
String? fileName,
String? fileType,
bool? iCloudAsset,
}) {
return CurrentUploadAsset(
id: id ?? this.id,
fileCreatedAt: fileCreatedAt ?? this.fileCreatedAt,
fileName: fileName ?? this.fileName,
fileType: fileType ?? this.fileType,
iCloudAsset: iCloudAsset ?? this.iCloudAsset,
);
}

Map<String, dynamic> toMap() {
final result = <String, dynamic>{};

result.addAll({'id': id});
result.addAll({'fileCreatedAt': fileCreatedAt.millisecondsSinceEpoch});
result.addAll({'fileName': fileName});
result.addAll({'fileType': fileType});

return result;
return <String, dynamic>{
'id': id,
'fileCreatedAt': fileCreatedAt.millisecondsSinceEpoch,
'fileName': fileName,
'fileType': fileType,
'iCloudAsset': iCloudAsset,
};
}

factory CurrentUploadAsset.fromMap(Map<String, dynamic> map) {
return CurrentUploadAsset(
id: map['id'] ?? '',
fileCreatedAt: DateTime.fromMillisecondsSinceEpoch(map['fileCreatedAt']),
fileName: map['fileName'] ?? '',
fileType: map['fileType'] ?? '',
id: map['id'] as String,
fileCreatedAt:
DateTime.fromMillisecondsSinceEpoch(map['fileCreatedAt'] as int),
fileName: map['fileName'] as String,
fileType: map['fileType'] as String,
iCloudAsset:
map['iCloudAsset'] != null ? map['iCloudAsset'] as bool : null,
);
}

String toJson() => json.encode(toMap());

factory CurrentUploadAsset.fromJson(String source) =>
CurrentUploadAsset.fromMap(json.decode(source));
CurrentUploadAsset.fromMap(json.decode(source) as Map<String, dynamic>);

@override
String toString() {
return 'CurrentUploadAsset(id: $id, fileCreatedAt: $fileCreatedAt, fileName: $fileName, fileType: $fileType)';
return 'CurrentUploadAsset(id: $id, fileCreatedAt: $fileCreatedAt, fileName: $fileName, fileType: $fileType, iCloudAsset: $iCloudAsset)';
}

@override
bool operator ==(Object other) {
bool operator ==(covariant CurrentUploadAsset other) {
if (identical(this, other)) return true;

return other is CurrentUploadAsset &&
other.id == id &&
return other.id == id &&
other.fileCreatedAt == fileCreatedAt &&
other.fileName == fileName &&
other.fileType == fileType;
other.fileType == fileType &&
other.iCloudAsset == iCloudAsset;
}

@override
int get hashCode {
return id.hashCode ^
fileCreatedAt.hashCode ^
fileName.hashCode ^
fileType.hashCode;
fileType.hashCode ^
iCloudAsset.hashCode;
}
}
11 changes: 11 additions & 0 deletions mobile/lib/modules/backup/providers/backup.provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,9 @@ class BackupNotifier extends StateNotifier<BackUpState> {
fileCreatedAt: DateTime.parse('2020-10-04'),
fileName: '...',
fileType: '...',
iCloudAsset: false,
),
iCloudDownloadProgress: 0.0,
),
);

Expand Down Expand Up @@ -444,9 +446,18 @@ class BackupNotifier extends StateNotifier<BackUpState> {

// Perform Backup
state = state.copyWith(cancelToken: CancellationToken());

final pmProgressHandler = PMProgressHandler();

pmProgressHandler.stream.listen((event) {
final double progress = event.progress;
state = state.copyWith(iCloudDownloadProgress: progress);
});

await _backupService.backupAsset(
assetsWillBeBackup,
state.cancelToken,
pmProgressHandler,
_onAssetUploaded,
_onUploadProgress,
_onSetCurrentBackupAsset,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -208,10 +208,12 @@ class ManualUploadNotifier extends StateNotifier<ManualUploadState> {
state.totalAssetsToUpload == 1;
state =
state.copyWith(showDetailedNotification: showDetailedNotification);
final pmProgressHandler = PMProgressHandler();

final bool ok = await ref.read(backupServiceProvider).backupAsset(
allUploadAssets,
state.cancelToken,
pmProgressHandler,
_onAssetUploaded,
_onProgress,
_onSetCurrentBackupAsset,
Expand Down
41 changes: 37 additions & 4 deletions mobile/lib/modules/backup/services/backup.service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import 'package:immich_mobile/modules/backup/models/backup_album.model.dart';
import 'package:immich_mobile/modules/backup/models/current_upload_asset.model.dart';
import 'package:immich_mobile/modules/backup/models/duplicated_asset.model.dart';
import 'package:immich_mobile/modules/backup/models/error_upload_asset.model.dart';
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
import 'package:immich_mobile/shared/models/store.dart';
import 'package:immich_mobile/shared/providers/api.provider.dart';
import 'package:immich_mobile/shared/providers/db.provider.dart';
Expand All @@ -26,6 +28,7 @@ final backupServiceProvider = Provider(
(ref) => BackupService(
ref.watch(apiServiceProvider),
ref.watch(dbProvider),
ref.watch(appSettingsServiceProvider),
),
);

Expand All @@ -34,8 +37,9 @@ class BackupService {
final ApiService _apiService;
final Isar _db;
final Logger _log = Logger("BackupService");
final AppSettingsService _appSetting;

BackupService(this._apiService, this._db);
BackupService(this._apiService, this._db, this._appSetting);

Future<List<String>?> getDeviceBackupAsset() async {
final String deviceId = Store.get(StoreKey.deviceId);
Expand Down Expand Up @@ -202,12 +206,16 @@ class BackupService {
Future<bool> backupAsset(
Iterable<AssetEntity> assetList,
http.CancellationToken cancelToken,
PMProgressHandler pmProgressHandler,
Function(String, String, bool) uploadSuccessCb,
Function(int, int) uploadProgressCb,
Function(CurrentUploadAsset) setCurrentUploadAssetCb,
Function(ErrorUploadAsset) errorCb, {
bool sortAssets = false,
}) async {
final bool isIgnoreIcloudAssets =
_appSetting.getSetting(AppSettingsEnum.ignoreIcloudAssets);

if (Platform.isAndroid &&
!(await Permission.accessMediaLocation.status).isGranted) {
// double check that permission is granted here, to guard against
Expand Down Expand Up @@ -241,10 +249,34 @@ class BackupService {

for (var entity in assetsToUpload) {
try {
if (entity.type == AssetType.video) {
file = await entity.originFile;
final isAvailableLocally = await entity.isLocallyAvailable();

// Handle getting files from iCloud
if (!isAvailableLocally && Platform.isIOS) {
// Skip iCloud assets if the user has disabled this feature
if (isIgnoreIcloudAssets) {
continue;
}

setCurrentUploadAssetCb(
CurrentUploadAsset(
id: entity.id,
fileCreatedAt: entity.createDateTime.year == 1970
? entity.modifiedDateTime
: entity.createDateTime,
fileName: await entity.titleAsync,
fileType: _getAssetType(entity.type),
iCloudAsset: true,
),
);

file = await entity.loadFile(progressHandler: pmProgressHandler);
} else {
file = await entity.originFile.timeout(const Duration(seconds: 5));
if (entity.type == AssetType.video) {
file = await entity.originFile;
} else {
file = await entity.originFile.timeout(const Duration(seconds: 5));
}
}

if (file != null) {
Expand Down Expand Up @@ -286,6 +318,7 @@ class BackupService {
: entity.createDateTime,
fileName: originalFileName,
fileType: _getAssetType(entity.type),
iCloudAsset: false,
),
);

Expand Down
Loading
Loading