mirror of
https://github.com/immich-app/immich.git
synced 2025-12-06 04:41:40 -08:00
Compare commits
2 Commits
refactor/t
...
hash-on-up
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
630fcf3516 | ||
|
|
c579e78413 |
167
mobile/lib/models/backup/bulk_upload_check_result.model.dart
Normal file
167
mobile/lib/models/backup/bulk_upload_check_result.model.dart
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
// ignore_for_file: public_member_api_docs, sort_constructors_first
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
|
||||||
|
class RejectResult {
|
||||||
|
final String localId;
|
||||||
|
final String remoteId;
|
||||||
|
|
||||||
|
RejectResult({
|
||||||
|
required this.localId,
|
||||||
|
required this.remoteId,
|
||||||
|
});
|
||||||
|
|
||||||
|
RejectResult copyWith({
|
||||||
|
String? localId,
|
||||||
|
String? remoteId,
|
||||||
|
}) {
|
||||||
|
return RejectResult(
|
||||||
|
localId: localId ?? this.localId,
|
||||||
|
remoteId: remoteId ?? this.remoteId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toMap() {
|
||||||
|
return <String, dynamic>{
|
||||||
|
'localId': localId,
|
||||||
|
'remoteId': remoteId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
factory RejectResult.fromMap(Map<String, dynamic> map) {
|
||||||
|
return RejectResult(
|
||||||
|
localId: map['localId'] as String,
|
||||||
|
remoteId: map['remoteId'] as String,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String toJson() => json.encode(toMap());
|
||||||
|
|
||||||
|
factory RejectResult.fromJson(String source) =>
|
||||||
|
RejectResult.fromMap(json.decode(source) as Map<String, dynamic>);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'RejectResult(localId: $localId, remoteId: $remoteId)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(covariant RejectResult other) {
|
||||||
|
if (identical(this, other)) return true;
|
||||||
|
|
||||||
|
return other.localId == localId && other.remoteId == remoteId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => localId.hashCode ^ remoteId.hashCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
class AcceptResult {
|
||||||
|
final String localId;
|
||||||
|
|
||||||
|
AcceptResult({
|
||||||
|
required this.localId,
|
||||||
|
});
|
||||||
|
|
||||||
|
AcceptResult copyWith({
|
||||||
|
String? localId,
|
||||||
|
}) {
|
||||||
|
return AcceptResult(
|
||||||
|
localId: localId ?? this.localId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toMap() {
|
||||||
|
return <String, dynamic>{
|
||||||
|
'localId': localId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
factory AcceptResult.fromMap(Map<String, dynamic> map) {
|
||||||
|
return AcceptResult(
|
||||||
|
localId: map['localId'] as String,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String toJson() => json.encode(toMap());
|
||||||
|
|
||||||
|
factory AcceptResult.fromJson(String source) =>
|
||||||
|
AcceptResult.fromMap(json.decode(source) as Map<String, dynamic>);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'AcceptResult(localId: $localId)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(covariant AcceptResult other) {
|
||||||
|
if (identical(this, other)) return true;
|
||||||
|
|
||||||
|
return other.localId == localId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => localId.hashCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
class BulkUploadCheckResult {
|
||||||
|
List<RejectResult> rejects;
|
||||||
|
List<AcceptResult> accepts;
|
||||||
|
|
||||||
|
BulkUploadCheckResult({
|
||||||
|
required this.rejects,
|
||||||
|
required this.accepts,
|
||||||
|
});
|
||||||
|
|
||||||
|
BulkUploadCheckResult copyWith({
|
||||||
|
List<RejectResult>? rejects,
|
||||||
|
List<AcceptResult>? accepts,
|
||||||
|
}) {
|
||||||
|
return BulkUploadCheckResult(
|
||||||
|
rejects: rejects ?? this.rejects,
|
||||||
|
accepts: accepts ?? this.accepts,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toMap() {
|
||||||
|
return <String, dynamic>{
|
||||||
|
'rejects': rejects.map((x) => x.toMap()).toList(),
|
||||||
|
'accepts': accepts.map((x) => x.toMap()).toList(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
factory BulkUploadCheckResult.fromMap(Map<String, dynamic> map) {
|
||||||
|
return BulkUploadCheckResult(
|
||||||
|
rejects: List<RejectResult>.from(
|
||||||
|
(map['rejects'] as List<int>).map<RejectResult>(
|
||||||
|
(x) => RejectResult.fromMap(x as Map<String, dynamic>),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
accepts: List<AcceptResult>.from(
|
||||||
|
(map['accepts'] as List<int>).map<AcceptResult>(
|
||||||
|
(x) => AcceptResult.fromMap(x as Map<String, dynamic>),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String toJson() => json.encode(toMap());
|
||||||
|
|
||||||
|
factory BulkUploadCheckResult.fromJson(String source) =>
|
||||||
|
BulkUploadCheckResult.fromMap(
|
||||||
|
json.decode(source) as Map<String, dynamic>,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() =>
|
||||||
|
'BulkUploadCheckResult(rejects: $rejects, accepts: $accepts)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(covariant BulkUploadCheckResult other) {
|
||||||
|
if (identical(this, other)) return true;
|
||||||
|
final listEquals = const DeepCollectionEquality().equals;
|
||||||
|
|
||||||
|
return listEquals(other.rejects, rejects) &&
|
||||||
|
listEquals(other.accepts, accepts);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => rejects.hashCode ^ accepts.hashCode;
|
||||||
|
}
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:cancellation_token_http/http.dart';
|
import 'package:cancellation_token_http/http.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
@@ -462,36 +460,39 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Set<BackupCandidate> assetsWillBeBackup = Set.from(state.allUniqueAssets);
|
Set<BackupCandidate> candidates = Set.from(state.allUniqueAssets);
|
||||||
// Remove item that has already been backed up
|
// Remove item that has already been backed up
|
||||||
for (final assetId in state.allAssetsInDatabase) {
|
for (final assetId in state.allAssetsInDatabase) {
|
||||||
assetsWillBeBackup.removeWhere((e) => e.asset.id == assetId);
|
candidates.removeWhere((e) => e.asset.id == assetId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (assetsWillBeBackup.isEmpty) {
|
if (candidates.isEmpty) {
|
||||||
state = state.copyWith(backupProgress: BackUpProgressEnum.idle);
|
state = state.copyWith(backupProgress: BackUpProgressEnum.idle);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Perform Backup
|
// Check with server for hash duplication
|
||||||
state = state.copyWith(cancelToken: CancellationToken());
|
final bulkCheckResult = await _backupService.checkBulkUpload(candidates);
|
||||||
|
|
||||||
final pmProgressHandler = Platform.isIOS ? PMProgressHandler() : null;
|
// // Perform Backup
|
||||||
|
// state = state.copyWith(cancelToken: CancellationToken());
|
||||||
|
|
||||||
pmProgressHandler?.stream.listen((event) {
|
// final pmProgressHandler = Platform.isIOS ? PMProgressHandler() : null;
|
||||||
final double progress = event.progress;
|
|
||||||
state = state.copyWith(iCloudDownloadProgress: progress);
|
|
||||||
});
|
|
||||||
|
|
||||||
await _backupService.backupAsset(
|
// pmProgressHandler?.stream.listen((event) {
|
||||||
assetsWillBeBackup,
|
// final double progress = event.progress;
|
||||||
state.cancelToken,
|
// state = state.copyWith(iCloudDownloadProgress: progress);
|
||||||
pmProgressHandler: pmProgressHandler,
|
// });
|
||||||
onSuccess: _onAssetUploaded,
|
|
||||||
onProgress: _onUploadProgress,
|
// await _backupService.backupAsset(
|
||||||
onCurrentAsset: _onSetCurrentBackupAsset,
|
// candidates,
|
||||||
onError: _onBackupError,
|
// state.cancelToken,
|
||||||
);
|
// pmProgressHandler: pmProgressHandler,
|
||||||
await notifyBackgroundServiceCanRun();
|
// onSuccess: _onAssetUploaded,
|
||||||
|
// onProgress: _onUploadProgress,
|
||||||
|
// onCurrentAsset: _onSetCurrentBackupAsset,
|
||||||
|
// onError: _onBackupError,
|
||||||
|
// );
|
||||||
|
// await notifyBackgroundServiceCanRun();
|
||||||
} else {
|
} else {
|
||||||
openAppSettings();
|
openAppSettings();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -361,8 +361,13 @@ class BackgroundService {
|
|||||||
UserService(apiService, db, syncSerive, partnerService);
|
UserService(apiService, db, syncSerive, partnerService);
|
||||||
AlbumService albumService =
|
AlbumService albumService =
|
||||||
AlbumService(apiService, userService, syncSerive, db);
|
AlbumService(apiService, userService, syncSerive, db);
|
||||||
BackupService backupService =
|
BackupService backupService = BackupService(
|
||||||
BackupService(apiService, db, settingService, albumService);
|
apiService,
|
||||||
|
db,
|
||||||
|
settingService,
|
||||||
|
albumService,
|
||||||
|
hashService,
|
||||||
|
);
|
||||||
|
|
||||||
final selectedAlbums = backupService.selectedAlbumsQuery().findAllSync();
|
final selectedAlbums = backupService.selectedAlbumsQuery().findAllSync();
|
||||||
final excludedAlbums = backupService.excludedAlbumsQuery().findAllSync();
|
final excludedAlbums = backupService.excludedAlbumsQuery().findAllSync();
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import 'package:immich_mobile/entities/backup_album.entity.dart';
|
|||||||
import 'package:immich_mobile/entities/duplicated_asset.entity.dart';
|
import 'package:immich_mobile/entities/duplicated_asset.entity.dart';
|
||||||
import 'package:immich_mobile/entities/store.entity.dart';
|
import 'package:immich_mobile/entities/store.entity.dart';
|
||||||
import 'package:immich_mobile/models/backup/backup_candidate.model.dart';
|
import 'package:immich_mobile/models/backup/backup_candidate.model.dart';
|
||||||
|
import 'package:immich_mobile/models/backup/bulk_upload_check_result.model.dart';
|
||||||
import 'package:immich_mobile/models/backup/current_upload_asset.model.dart';
|
import 'package:immich_mobile/models/backup/current_upload_asset.model.dart';
|
||||||
import 'package:immich_mobile/models/backup/error_upload_asset.model.dart';
|
import 'package:immich_mobile/models/backup/error_upload_asset.model.dart';
|
||||||
import 'package:immich_mobile/models/backup/success_upload_asset.model.dart';
|
import 'package:immich_mobile/models/backup/success_upload_asset.model.dart';
|
||||||
@@ -19,6 +20,7 @@ import 'package:immich_mobile/providers/db.provider.dart';
|
|||||||
import 'package:immich_mobile/services/album.service.dart';
|
import 'package:immich_mobile/services/album.service.dart';
|
||||||
import 'package:immich_mobile/services/api.service.dart';
|
import 'package:immich_mobile/services/api.service.dart';
|
||||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||||
|
import 'package:immich_mobile/services/hash.service.dart';
|
||||||
import 'package:isar/isar.dart';
|
import 'package:isar/isar.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:openapi/api.dart';
|
||||||
@@ -32,6 +34,7 @@ final backupServiceProvider = Provider(
|
|||||||
ref.watch(dbProvider),
|
ref.watch(dbProvider),
|
||||||
ref.watch(appSettingsServiceProvider),
|
ref.watch(appSettingsServiceProvider),
|
||||||
ref.watch(albumServiceProvider),
|
ref.watch(albumServiceProvider),
|
||||||
|
ref.watch(hashServiceProvider),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -42,14 +45,71 @@ class BackupService {
|
|||||||
final Logger _log = Logger("BackupService");
|
final Logger _log = Logger("BackupService");
|
||||||
final AppSettingsService _appSetting;
|
final AppSettingsService _appSetting;
|
||||||
final AlbumService _albumService;
|
final AlbumService _albumService;
|
||||||
|
final HashService _hashService;
|
||||||
|
|
||||||
BackupService(
|
BackupService(
|
||||||
this._apiService,
|
this._apiService,
|
||||||
this._db,
|
this._db,
|
||||||
this._appSetting,
|
this._appSetting,
|
||||||
this._albumService,
|
this._albumService,
|
||||||
|
this._hashService,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
Future<BulkUploadCheckResult> checkBulkUpload(
|
||||||
|
Set<BackupCandidate> candidates,
|
||||||
|
) async {
|
||||||
|
List<AssetBulkUploadCheckItem> assets = [];
|
||||||
|
|
||||||
|
final assetEntities = candidates.map((c) => c.asset).toList();
|
||||||
|
final hashedDeviceAssets =
|
||||||
|
await _hashService.getHashedAssetsFromAssetEntity(assetEntities);
|
||||||
|
|
||||||
|
for (final hashedAsset in hashedDeviceAssets) {
|
||||||
|
final AssetBulkUploadCheckItem item = AssetBulkUploadCheckItem(
|
||||||
|
id: hashedAsset.id.toString(),
|
||||||
|
checksum: hashedAsset.checksum,
|
||||||
|
);
|
||||||
|
|
||||||
|
assets.add(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
final response = await _apiService.assetsApi.checkBulkUpload(
|
||||||
|
AssetBulkUploadCheckDto(assets: assets),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response == null) {
|
||||||
|
return BulkUploadCheckResult(
|
||||||
|
rejects: [],
|
||||||
|
accepts: [],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final List<RejectResult> rejects = [];
|
||||||
|
final List<AcceptResult> accepts = [];
|
||||||
|
|
||||||
|
for (final result in response.results) {
|
||||||
|
if (result.action == AssetBulkUploadCheckResultActionEnum.reject) {
|
||||||
|
rejects.add(
|
||||||
|
RejectResult(
|
||||||
|
localId: result.id,
|
||||||
|
remoteId: result.assetId ?? "",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
accepts.add(
|
||||||
|
AcceptResult(
|
||||||
|
localId: result.id,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return BulkUploadCheckResult(
|
||||||
|
rejects: rejects,
|
||||||
|
accepts: accepts,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Future<List<String>?> getDeviceBackupAsset() async {
|
Future<List<String>?> getDeviceBackupAsset() async {
|
||||||
final String deviceId = Store.get(StoreKey.deviceId);
|
final String deviceId = Store.get(StoreKey.deviceId);
|
||||||
|
|
||||||
|
|||||||
@@ -19,8 +19,20 @@ class HashService {
|
|||||||
final BackgroundService _backgroundService;
|
final BackgroundService _backgroundService;
|
||||||
final _log = Logger('HashService');
|
final _log = Logger('HashService');
|
||||||
|
|
||||||
|
Future<List<Asset>> getHashedAssetsFromAssetEntity(
|
||||||
|
List<AssetEntity> assets,
|
||||||
|
) async {
|
||||||
|
final ids = assets
|
||||||
|
.map(Platform.isAndroid ? (a) => a.id.toInt() : (a) => a.id)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
final List<DeviceAsset?> hashes = await lookupHashes(ids);
|
||||||
|
|
||||||
|
return _mapAllHashedAssets(assets, hashes);
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns all assets that were successfully hashed
|
/// Returns all assets that were successfully hashed
|
||||||
Future<List<Asset>> getHashedAssets(
|
Future<List<Asset>> getHashedAssetsFromDeviceAlbum(
|
||||||
AssetPathEntity album, {
|
AssetPathEntity album, {
|
||||||
int start = 0,
|
int start = 0,
|
||||||
int end = 0x7fffffffffffffff,
|
int end = 0x7fffffffffffffff,
|
||||||
@@ -44,7 +56,7 @@ class HashService {
|
|||||||
final ids = assetEntities
|
final ids = assetEntities
|
||||||
.map(Platform.isAndroid ? (a) => a.id.toInt() : (a) => a.id)
|
.map(Platform.isAndroid ? (a) => a.id.toInt() : (a) => a.id)
|
||||||
.toList();
|
.toList();
|
||||||
final List<DeviceAsset?> hashes = await _lookupHashes(ids);
|
final List<DeviceAsset?> hashes = await lookupHashes(ids);
|
||||||
final List<DeviceAsset> toAdd = [];
|
final List<DeviceAsset> toAdd = [];
|
||||||
final List<String> toHash = [];
|
final List<String> toHash = [];
|
||||||
|
|
||||||
@@ -90,7 +102,7 @@ class HashService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Lookup hashes of assets by their local ID
|
/// Lookup hashes of assets by their local ID
|
||||||
Future<List<DeviceAsset?>> _lookupHashes(List<Object> ids) =>
|
Future<List<DeviceAsset?>> lookupHashes(List<Object> ids) =>
|
||||||
Platform.isAndroid
|
Platform.isAndroid
|
||||||
? _db.androidDeviceAssets.getAll(ids.cast())
|
? _db.androidDeviceAssets.getAll(ids.cast())
|
||||||
: _db.iOSDeviceAssets.getAllById(ids.cast());
|
: _db.iOSDeviceAssets.getAllById(ids.cast());
|
||||||
|
|||||||
@@ -566,8 +566,8 @@ class SyncService {
|
|||||||
.findAll();
|
.findAll();
|
||||||
assert(inDb.isSorted(Asset.compareByChecksum), "inDb not sorted!");
|
assert(inDb.isSorted(Asset.compareByChecksum), "inDb not sorted!");
|
||||||
final int assetCountOnDevice = await ape.assetCountAsync;
|
final int assetCountOnDevice = await ape.assetCountAsync;
|
||||||
final List<Asset> onDevice =
|
final List<Asset> onDevice = await _hashService
|
||||||
await _hashService.getHashedAssets(ape, excludedAssets: excludedAssets);
|
.getHashedAssetsFromDeviceAlbum(ape, excludedAssets: excludedAssets);
|
||||||
_removeDuplicates(onDevice);
|
_removeDuplicates(onDevice);
|
||||||
// _removeDuplicates sorts `onDevice` by checksum
|
// _removeDuplicates sorts `onDevice` by checksum
|
||||||
final (toAdd, toUpdate, toDelete) = _diffAssets(onDevice, inDb);
|
final (toAdd, toUpdate, toDelete) = _diffAssets(onDevice, inDb);
|
||||||
@@ -649,7 +649,8 @@ class SyncService {
|
|||||||
if (modified == null) {
|
if (modified == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
final List<Asset> newAssets = await _hashService.getHashedAssets(modified);
|
final List<Asset> newAssets =
|
||||||
|
await _hashService.getHashedAssetsFromDeviceAlbum(modified);
|
||||||
|
|
||||||
if (totalOnDevice != lastKnownTotal + newAssets.length) {
|
if (totalOnDevice != lastKnownTotal + newAssets.length) {
|
||||||
return false;
|
return false;
|
||||||
@@ -683,8 +684,8 @@ class SyncService {
|
|||||||
]) async {
|
]) async {
|
||||||
_log.info("Syncing a new local album to DB: ${ape.name}");
|
_log.info("Syncing a new local album to DB: ${ape.name}");
|
||||||
final Album a = Album.local(ape);
|
final Album a = Album.local(ape);
|
||||||
final assets =
|
final assets = await _hashService.getHashedAssetsFromDeviceAlbum(ape,
|
||||||
await _hashService.getHashedAssets(ape, excludedAssets: excludedAssets);
|
excludedAssets: excludedAssets);
|
||||||
_removeDuplicates(assets);
|
_removeDuplicates(assets);
|
||||||
final (existingInDb, updated) = await _linkWithExistingFromDb(assets);
|
final (existingInDb, updated) = await _linkWithExistingFromDb(assets);
|
||||||
_log.info(
|
_log.info(
|
||||||
|
|||||||
Reference in New Issue
Block a user