mirror of
https://github.com/immich-app/immich.git
synced 2026-05-29 11:02:38 -04:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4423a8f8a4 | |||
| 77fd2ba919 | |||
| 1318dafdc4 |
@@ -234,24 +234,13 @@ class RemoteAlbumService {
|
|||||||
final pendingAdds = <Future<void>>[];
|
final pendingAdds = <Future<void>>[];
|
||||||
final localById = {for (final a in localAssets) a.id: a};
|
final localById = {for (final a in localAssets) a.id: a};
|
||||||
|
|
||||||
|
final UploadCallbacks(:onProgress, :onSuccess, :onError, :onICloudProgress) = userCallbacks;
|
||||||
final wrappedCallbacks = UploadCallbacks(
|
final wrappedCallbacks = UploadCallbacks(
|
||||||
onProgress: (localId, filename, bytes, totalBytes) => _runUploadCallback(
|
onProgress: onProgress,
|
||||||
'Upload progress callback failed for $localId',
|
onICloudProgress: onICloudProgress,
|
||||||
() => userCallbacks.onProgress?.call(localId, filename, bytes, totalBytes),
|
onError: onError,
|
||||||
),
|
|
||||||
onICloudProgress: (localId, progress) => _runUploadCallback(
|
|
||||||
'iCloud progress callback failed for $localId',
|
|
||||||
() => userCallbacks.onICloudProgress?.call(localId, progress),
|
|
||||||
),
|
|
||||||
onError: (localId, errorMessage) => _runUploadCallback(
|
|
||||||
'Upload error callback failed for $localId',
|
|
||||||
() => userCallbacks.onError?.call(localId, errorMessage),
|
|
||||||
),
|
|
||||||
onSuccess: (localId, remoteId) {
|
onSuccess: (localId, remoteId) {
|
||||||
_runUploadCallback(
|
onSuccess?.call(localId, remoteId);
|
||||||
'Upload success callback failed for $localId',
|
|
||||||
() => userCallbacks.onSuccess?.call(localId, remoteId),
|
|
||||||
);
|
|
||||||
final source = localById[localId];
|
final source = localById[localId];
|
||||||
if (source == null) {
|
if (source == null) {
|
||||||
_logger.warning('Upload success for $localId but source LocalAsset missing; skipping album link');
|
_logger.warning('Upload success for $localId but source LocalAsset missing; skipping album link');
|
||||||
@@ -259,29 +248,22 @@ class RemoteAlbumService {
|
|||||||
}
|
}
|
||||||
pendingAdds.add(
|
pendingAdds.add(
|
||||||
_linkUploadedAssetToAlbum(albumId, remoteId, uploader, source)
|
_linkUploadedAssetToAlbum(albumId, remoteId, uploader, source)
|
||||||
.then<void>((added) {
|
.then<void>((added) => addedCount += added)
|
||||||
addedCount += added;
|
.onError(
|
||||||
})
|
(error, stack) =>
|
||||||
.catchError((Object error, StackTrace stack) {
|
_logger.warning('Failed to add uploaded asset $remoteId to album $albumId', error, stack),
|
||||||
_logger.warning('Failed to add uploaded asset $remoteId to album $albumId', error, stack);
|
),
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
await _uploadService.uploadManual(localAssets, callbacks: wrappedCallbacks);
|
await _uploadService.uploadManual(localAssets, cancelToken: null, callbacks: wrappedCallbacks);
|
||||||
await Future.wait(pendingAdds);
|
await Future.wait(pendingAdds);
|
||||||
return addedCount;
|
return addedCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
void _runUploadCallback(String message, void Function() callback) {
|
// TODO: this is a poorly designed flow; adding a "stub" just to satisfy FK constraints is hacky,
|
||||||
try {
|
// it goes out of its way to insert one at a time, and it swallows errors that should be surfaced to the user.
|
||||||
callback();
|
|
||||||
} catch (error, stack) {
|
|
||||||
_logger.warning(message, error, stack);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Links a freshly-uploaded asset to an album, ensuring the local DB
|
/// Links a freshly-uploaded asset to an album, ensuring the local DB
|
||||||
/// reflects the change without waiting for the next sync. We call the API
|
/// reflects the change without waiting for the next sync. We call the API
|
||||||
/// (server is the source of truth), then upsert a placeholder
|
/// (server is the source of truth), then upsert a placeholder
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'dart:async';
|
||||||
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';
|
||||||
@@ -5,66 +6,54 @@ import 'package:immich_mobile/extensions/platform_extensions.dart';
|
|||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:photo_manager/photo_manager.dart';
|
import 'package:photo_manager/photo_manager.dart';
|
||||||
|
|
||||||
|
typedef OnProgress = void Function(String id, double progress);
|
||||||
|
|
||||||
class StorageRepository {
|
class StorageRepository {
|
||||||
final log = Logger('StorageRepository');
|
static final log = Logger('StorageRepository');
|
||||||
|
|
||||||
StorageRepository();
|
const StorageRepository();
|
||||||
|
|
||||||
Future<File?> getFileForAsset(String assetId) async {
|
Future<File?> getAssetFile(String assetId, {OnProgress? onProgress, Completer<void>? cancelToken}) {
|
||||||
File? file;
|
return _getFileForAsset(assetId, isMotion: false, onProgress: onProgress, cancelToken: cancelToken);
|
||||||
final log = Logger('StorageRepository');
|
|
||||||
|
|
||||||
try {
|
|
||||||
final entity = await AssetEntity.fromId(assetId);
|
|
||||||
file = await entity?.originFile;
|
|
||||||
if (file == null) {
|
|
||||||
log.warning("Cannot get file for asset $assetId");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
final exists = await file.exists();
|
|
||||||
if (!exists) {
|
|
||||||
log.warning("File for asset $assetId does not exist");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
} catch (error, stackTrace) {
|
|
||||||
log.warning("Error getting file for asset $assetId", error, stackTrace);
|
|
||||||
}
|
|
||||||
return file;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<File?> getMotionFileForAsset(LocalAsset asset) async {
|
Future<File?> getMotionFile(String assetId, {OnProgress? onProgress, Completer<void>? cancelToken}) {
|
||||||
File? file;
|
return _getFileForAsset(assetId, isMotion: true, onProgress: onProgress, cancelToken: cancelToken);
|
||||||
final log = Logger('StorageRepository');
|
}
|
||||||
|
|
||||||
|
Future<File?> _getFileForAsset(
|
||||||
|
String assetId, {
|
||||||
|
bool isMotion = false,
|
||||||
|
OnProgress? onProgress,
|
||||||
|
Completer<void>? cancelToken,
|
||||||
|
}) async {
|
||||||
|
final entity = await AssetEntity.fromId(assetId);
|
||||||
|
if (entity == null) {
|
||||||
|
log.warning("Cannot get AssetEntity for asset $assetId");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
PMProgressHandler? progressHandler;
|
||||||
|
StreamSubscription<PMProgressState>? progressSubscription;
|
||||||
|
PMCancelToken? pmCancelToken;
|
||||||
|
if (cancelToken != null) {
|
||||||
|
progressHandler = PMProgressHandler();
|
||||||
|
progressSubscription = progressHandler.stream.listen((event) => onProgress?.call(assetId, event.progress));
|
||||||
|
pmCancelToken = PMCancelToken();
|
||||||
|
unawaited(cancelToken.future.then((_) => pmCancelToken!.cancelRequest()));
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final entity = await AssetEntity.fromId(asset.id);
|
return await entity.loadFile(withSubtype: isMotion, progressHandler: progressHandler, cancelToken: pmCancelToken);
|
||||||
file = await entity?.originFileWithSubtype;
|
|
||||||
if (file == null) {
|
|
||||||
log.warning(
|
|
||||||
"Cannot get motion file for asset ${asset.id}, name: ${asset.name}, created on: ${asset.createdAt}",
|
|
||||||
);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
final exists = await file.exists();
|
|
||||||
if (!exists) {
|
|
||||||
log.warning("Motion file for asset ${asset.id} does not exist");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
} catch (error, stackTrace) {
|
} catch (error, stackTrace) {
|
||||||
log.warning(
|
log.warning("Error loading file for asset $assetId", error, stackTrace);
|
||||||
"Error getting motion file for asset ${asset.id}, name: ${asset.name}, created on: ${asset.createdAt}",
|
return null;
|
||||||
error,
|
} finally {
|
||||||
stackTrace,
|
unawaited(progressSubscription?.cancel());
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return file;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<AssetEntity?> getAssetEntityForAsset(LocalAsset asset) async {
|
Future<AssetEntity?> getAssetEntityForAsset(LocalAsset asset) async {
|
||||||
final log = Logger('StorageRepository');
|
|
||||||
|
|
||||||
AssetEntity? entity;
|
AssetEntity? entity;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -99,39 +88,7 @@ class StorageRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<File?> loadFileFromCloud(String assetId, {PMProgressHandler? progressHandler}) async {
|
|
||||||
try {
|
|
||||||
final entity = await AssetEntity.fromId(assetId);
|
|
||||||
if (entity == null) {
|
|
||||||
log.warning("Cannot get AssetEntity for asset $assetId");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return await entity.loadFile(progressHandler: progressHandler);
|
|
||||||
} catch (error, stackTrace) {
|
|
||||||
log.warning("Error loading file from cloud for asset $assetId", error, stackTrace);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<File?> loadMotionFileFromCloud(String assetId, {PMProgressHandler? progressHandler}) async {
|
|
||||||
try {
|
|
||||||
final entity = await AssetEntity.fromId(assetId);
|
|
||||||
if (entity == null) {
|
|
||||||
log.warning("Cannot get AssetEntity for asset $assetId");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return await entity.loadFile(withSubtype: true, progressHandler: progressHandler);
|
|
||||||
} catch (error, stackTrace) {
|
|
||||||
log.warning("Error loading motion file from cloud for asset $assetId", error, stackTrace);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> clearCache() async {
|
Future<void> clearCache() async {
|
||||||
final log = Logger('StorageRepository');
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await PhotoManager.clearFileCache();
|
await PhotoManager.clearFileCache();
|
||||||
} catch (error, stackTrace) {
|
} catch (error, stackTrace) {
|
||||||
|
|||||||
@@ -6,13 +6,13 @@ import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
|||||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
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/platform_extensions.dart';
|
import 'package:immich_mobile/extensions/platform_extensions.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart';
|
|
||||||
import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart';
|
import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart';
|
||||||
import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart';
|
import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart';
|
||||||
import 'package:immich_mobile/providers/asset_viewer/video_player_provider.dart';
|
import 'package:immich_mobile/providers/asset_viewer/video_player_provider.dart';
|
||||||
import 'package:immich_mobile/providers/cast.provider.dart';
|
import 'package:immich_mobile/providers/cast.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/storage.provider.dart';
|
||||||
import 'package:immich_mobile/services/api.service.dart';
|
import 'package:immich_mobile/services/api.service.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:native_video_player/native_video_player.dart';
|
import 'package:native_video_player/native_video_player.dart';
|
||||||
@@ -108,7 +108,7 @@ class _NativeVideoViewerState extends ConsumerState<NativeVideoViewer> with Widg
|
|||||||
try {
|
try {
|
||||||
if (videoAsset.hasLocal && videoAsset.livePhotoVideoId == null) {
|
if (videoAsset.hasLocal && videoAsset.livePhotoVideoId == null) {
|
||||||
final id = videoAsset is LocalAsset ? videoAsset.id : (videoAsset as RemoteAsset).localId!;
|
final id = videoAsset is LocalAsset ? videoAsset.id : (videoAsset as RemoteAsset).localId!;
|
||||||
final file = await StorageRepository().getFileForAsset(id);
|
final file = await ref.read(storageRepositoryProvider).getAssetFile(id);
|
||||||
if (!mounted) {
|
if (!mounted) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'dart:async';
|
|||||||
|
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/extensions/platform_extensions.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
|
|
||||||
import 'package:immich_mobile/constants/constants.dart';
|
import 'package:immich_mobile/constants/constants.dart';
|
||||||
@@ -273,7 +274,7 @@ class DriftBackupNotifier extends StateNotifier<DriftBackupState> {
|
|||||||
onProgress: _handleForegroundBackupProgress,
|
onProgress: _handleForegroundBackupProgress,
|
||||||
onSuccess: _handleForegroundBackupSuccess,
|
onSuccess: _handleForegroundBackupSuccess,
|
||||||
onError: _handleForegroundBackupError,
|
onError: _handleForegroundBackupError,
|
||||||
onICloudProgress: _handleICloudProgress,
|
onICloudProgress: CurrentPlatform.isIOS ? _handleICloudProgress : null,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -282,7 +283,7 @@ class DriftBackupNotifier extends StateNotifier<DriftBackupState> {
|
|||||||
_cancelToken?.complete();
|
_cancelToken?.complete();
|
||||||
_cancelToken = null;
|
_cancelToken = null;
|
||||||
_uploadSpeedManager.clear();
|
_uploadSpeedManager.clear();
|
||||||
state = state.copyWith(uploadItems: {}, iCloudDownloadProgress: {});
|
state = state.copyWith(uploadItems: const {}, iCloudDownloadProgress: const {});
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleICloudProgress(String localAssetId, double progress) {
|
void _handleICloudProgress(String localAssetId, double progress) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart';
|
||||||
|
|
||||||
final storageRepositoryProvider = Provider<StorageRepository>((ref) => StorageRepository());
|
final storageRepositoryProvider = Provider<StorageRepository>((ref) => const StorageRepository());
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import 'package:immich_mobile/entities/store.entity.dart';
|
|||||||
import 'package:immich_mobile/infrastructure/repositories/network.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/network.repository.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:http/http.dart';
|
import 'package:http/http.dart';
|
||||||
import 'package:immich_mobile/utils/debug_print.dart';
|
|
||||||
|
|
||||||
final uploadRepositoryProvider = Provider((ref) => UploadRepository());
|
final uploadRepositoryProvider = Provider((ref) => UploadRepository());
|
||||||
|
|
||||||
@@ -20,21 +19,14 @@ class UploadRepository {
|
|||||||
void Function(TaskProgressUpdate)? onTaskProgress;
|
void Function(TaskProgressUpdate)? onTaskProgress;
|
||||||
|
|
||||||
UploadRepository() {
|
UploadRepository() {
|
||||||
FileDownloader().registerCallbacks(
|
final downloader = FileDownloader();
|
||||||
group: kBackupGroup,
|
for (final group in const [kBackupGroup, kBackupLivePhotoGroup, kManualUploadGroup]) {
|
||||||
taskStatusCallback: (update) => onUploadStatus?.call(update),
|
downloader.registerCallbacks(
|
||||||
taskProgressCallback: (update) => onTaskProgress?.call(update),
|
group: group,
|
||||||
);
|
taskStatusCallback: onUploadStatus,
|
||||||
FileDownloader().registerCallbacks(
|
taskProgressCallback: onTaskProgress,
|
||||||
group: kBackupLivePhotoGroup,
|
);
|
||||||
taskStatusCallback: (update) => onUploadStatus?.call(update),
|
}
|
||||||
taskProgressCallback: (update) => onTaskProgress?.call(update),
|
|
||||||
);
|
|
||||||
FileDownloader().registerCallbacks(
|
|
||||||
group: kManualUploadGroup,
|
|
||||||
taskStatusCallback: (update) => onUploadStatus?.call(update),
|
|
||||||
taskProgressCallback: (update) => onTaskProgress?.call(update),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> enqueueBackground(UploadTask task) {
|
Future<void> enqueueBackground(UploadTask task) {
|
||||||
@@ -66,28 +58,6 @@ class UploadRepository {
|
|||||||
return FileDownloader().start();
|
return FileDownloader().start();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> getUploadInfo() async {
|
|
||||||
final [enqueuedTasks, runningTasks, canceledTasks, waitingTasks, pausedTasks] = await Future.wait([
|
|
||||||
FileDownloader().database.allRecordsWithStatus(TaskStatus.enqueued, group: kBackupGroup),
|
|
||||||
FileDownloader().database.allRecordsWithStatus(TaskStatus.running, group: kBackupGroup),
|
|
||||||
FileDownloader().database.allRecordsWithStatus(TaskStatus.canceled, group: kBackupGroup),
|
|
||||||
FileDownloader().database.allRecordsWithStatus(TaskStatus.waitingToRetry, group: kBackupGroup),
|
|
||||||
FileDownloader().database.allRecordsWithStatus(TaskStatus.paused, group: kBackupGroup),
|
|
||||||
]);
|
|
||||||
|
|
||||||
dPrint(
|
|
||||||
() =>
|
|
||||||
"""
|
|
||||||
Upload Info:
|
|
||||||
Enqueued: ${enqueuedTasks.length}
|
|
||||||
Running: ${runningTasks.length}
|
|
||||||
Canceled: ${canceledTasks.length}
|
|
||||||
Waiting: ${waitingTasks.length}
|
|
||||||
Paused: ${pausedTasks.length}
|
|
||||||
""",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<UploadResult> uploadFile({
|
Future<UploadResult> uploadFile({
|
||||||
required File file,
|
required File file,
|
||||||
required String originalFileName,
|
required String originalFileName,
|
||||||
@@ -111,41 +81,30 @@ class UploadRepository {
|
|||||||
baseRequest.fields.addAll(fields);
|
baseRequest.fields.addAll(fields);
|
||||||
baseRequest.files.add(assetRawUploadData);
|
baseRequest.files.add(assetRawUploadData);
|
||||||
|
|
||||||
final response = await NetworkRepository.client.send(baseRequest);
|
final StreamedResponse(:statusCode, :stream) = await NetworkRepository.client.send(baseRequest);
|
||||||
final responseBodyString = await response.stream.bytesToString();
|
final responseBodyString = await stream.bytesToString();
|
||||||
|
|
||||||
if (![200, 201].contains(response.statusCode)) {
|
return switch ((statusCode, _tryJsonDecode(responseBodyString))) {
|
||||||
String? errorMessage;
|
(200 || 201, {'id': String id}) => UploadSuccess(remoteAssetId: id),
|
||||||
|
(413, _) => const UploadError(statusCode: 413, message: 'File is too large to upload'),
|
||||||
if (response.statusCode == 413) {
|
(_, {'message': String message}) => UploadError(statusCode: statusCode, message: message),
|
||||||
errorMessage = 'Error(413) File is too large to upload';
|
_ => UploadError(statusCode: statusCode, message: 'Upload failed with status $statusCode'),
|
||||||
return UploadResult.error(statusCode: response.statusCode, errorMessage: errorMessage);
|
};
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
final error = jsonDecode(responseBodyString);
|
|
||||||
errorMessage = error['message'] ?? error['error'];
|
|
||||||
} catch (_) {
|
|
||||||
errorMessage = responseBodyString.isNotEmpty
|
|
||||||
? responseBodyString
|
|
||||||
: 'Upload failed with status ${response.statusCode}';
|
|
||||||
}
|
|
||||||
|
|
||||||
return UploadResult.error(statusCode: response.statusCode, errorMessage: errorMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
final responseBody = jsonDecode(responseBodyString);
|
|
||||||
return UploadResult.success(remoteAssetId: responseBody['id'] as String);
|
|
||||||
} catch (e) {
|
|
||||||
return UploadResult.error(errorMessage: 'Failed to parse server response');
|
|
||||||
}
|
|
||||||
} on RequestAbortedException {
|
} on RequestAbortedException {
|
||||||
logger.warning("Upload $logContext was cancelled");
|
logger.warning("Upload $logContext was cancelled");
|
||||||
return UploadResult.cancelled();
|
return const UploadCancelled();
|
||||||
} catch (error, stackTrace) {
|
} catch (error, stackTrace) {
|
||||||
logger.warning("Error uploading $logContext: ${error.toString()}: $stackTrace");
|
logger.warning("Error uploading $logContext: ${error.toString()}: $stackTrace");
|
||||||
return UploadResult.error(errorMessage: error.toString());
|
return UploadError(message: error.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
Map? _tryJsonDecode(String s) {
|
||||||
|
try {
|
||||||
|
return (jsonDecode(s) as Map);
|
||||||
|
} catch (_) {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -180,30 +139,23 @@ class ProgressMultipartRequest extends MultipartRequest with Abortable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class UploadResult {
|
sealed class UploadResult {
|
||||||
final bool isSuccess;
|
const UploadResult();
|
||||||
final bool isCancelled;
|
}
|
||||||
final String? remoteAssetId;
|
|
||||||
final String? errorMessage;
|
final class UploadSuccess extends UploadResult {
|
||||||
|
final String remoteAssetId;
|
||||||
|
|
||||||
|
const UploadSuccess({required this.remoteAssetId});
|
||||||
|
}
|
||||||
|
|
||||||
|
final class UploadError extends UploadResult {
|
||||||
|
final String message;
|
||||||
final int? statusCode;
|
final int? statusCode;
|
||||||
|
|
||||||
const UploadResult({
|
const UploadError({required this.message, this.statusCode});
|
||||||
required this.isSuccess,
|
}
|
||||||
required this.isCancelled,
|
|
||||||
this.remoteAssetId,
|
final class UploadCancelled extends UploadResult {
|
||||||
this.errorMessage,
|
const UploadCancelled();
|
||||||
this.statusCode,
|
|
||||||
});
|
|
||||||
|
|
||||||
factory UploadResult.success({required String remoteAssetId}) {
|
|
||||||
return UploadResult(isSuccess: true, isCancelled: false, remoteAssetId: remoteAssetId);
|
|
||||||
}
|
|
||||||
|
|
||||||
factory UploadResult.error({String? errorMessage, int? statusCode}) {
|
|
||||||
return UploadResult(isSuccess: false, isCancelled: false, errorMessage: errorMessage, statusCode: statusCode);
|
|
||||||
}
|
|
||||||
|
|
||||||
factory UploadResult.cancelled() {
|
|
||||||
return const UploadResult(isSuccess: false, isCancelled: true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -266,8 +266,6 @@ class BackgroundUploadService {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
File? file;
|
|
||||||
|
|
||||||
/// iOS LivePhoto has two files: a photo and a video.
|
/// iOS LivePhoto has two files: a photo and a video.
|
||||||
/// They are uploaded separately, with video file being upload first, then returned with the assetId
|
/// They are uploaded separately, with video file being upload first, then returned with the assetId
|
||||||
/// The assetId is then used as a metadata for the photo file upload task.
|
/// The assetId is then used as a metadata for the photo file upload task.
|
||||||
@@ -278,11 +276,9 @@ class BackgroundUploadService {
|
|||||||
/// The cancel operation will only cancel the video group (normal group), the photo group will not
|
/// The cancel operation will only cancel the video group (normal group), the photo group will not
|
||||||
/// be touched, as the video file is already uploaded.
|
/// be touched, as the video file is already uploaded.
|
||||||
|
|
||||||
if (entity.isLivePhoto) {
|
final file = await (entity.isLivePhoto
|
||||||
file = await _storageRepository.getMotionFileForAsset(asset);
|
? _storageRepository.getMotionFile(asset.id)
|
||||||
} else {
|
: _storageRepository.getAssetFile(asset.id));
|
||||||
file = await _storageRepository.getFileForAsset(asset.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (file == null) {
|
if (file == null) {
|
||||||
_logger.warning("Failed to get file for asset ${asset.id} - ${asset.name}");
|
_logger.warning("Failed to get file for asset ${asset.id} - ${asset.name}");
|
||||||
@@ -330,7 +326,7 @@ class BackgroundUploadService {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
final file = await _storageRepository.getFileForAsset(asset.id);
|
final file = await _storageRepository.getAssetFile(asset.id);
|
||||||
if (file == null) {
|
if (file == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ import 'package:immich_mobile/repositories/asset_media.repository.dart';
|
|||||||
import 'package:immich_mobile/repositories/upload.repository.dart';
|
import 'package:immich_mobile/repositories/upload.repository.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
import 'package:photo_manager/photo_manager.dart' show PMProgressHandler;
|
|
||||||
|
|
||||||
/// Callbacks for upload progress and status updates
|
/// Callbacks for upload progress and status updates
|
||||||
class UploadCallbacks {
|
class UploadCallbacks {
|
||||||
@@ -99,7 +98,7 @@ class ForegroundUploadService {
|
|||||||
final requireWifi = _shouldRequireWiFi(asset);
|
final requireWifi = _shouldRequireWiFi(asset);
|
||||||
return requireWifi && !hasWifi;
|
return requireWifi && !hasWifi;
|
||||||
},
|
},
|
||||||
processItem: (asset) => _uploadSingleAsset(asset, cancelToken, callbacks: callbacks),
|
processItem: (asset) => _uploadSingleAsset(asset, cancelToken: cancelToken, callbacks: callbacks),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -125,14 +124,14 @@ class ForegroundUploadService {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
await _uploadSingleAsset(asset, cancelToken, callbacks: callbacks);
|
await _uploadSingleAsset(asset, cancelToken: cancelToken, callbacks: callbacks);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Manually upload picked local assets
|
/// Manually upload picked local assets
|
||||||
Future<void> uploadManual(
|
Future<void> uploadManual(
|
||||||
List<LocalAsset> localAssets, {
|
List<LocalAsset> localAssets, {
|
||||||
Completer<void>? cancelToken,
|
required Completer<void>? cancelToken,
|
||||||
UploadCallbacks callbacks = const UploadCallbacks(),
|
UploadCallbacks callbacks = const UploadCallbacks(),
|
||||||
}) async {
|
}) async {
|
||||||
if (localAssets.isEmpty) {
|
if (localAssets.isEmpty) {
|
||||||
@@ -142,7 +141,7 @@ class ForegroundUploadService {
|
|||||||
await _executeWithWorkerPool<LocalAsset>(
|
await _executeWithWorkerPool<LocalAsset>(
|
||||||
items: localAssets,
|
items: localAssets,
|
||||||
cancelToken: cancelToken,
|
cancelToken: cancelToken,
|
||||||
processItem: (asset) => _uploadSingleAsset(asset, cancelToken, callbacks: callbacks),
|
processItem: (asset) => _uploadSingleAsset(asset, cancelToken: cancelToken, callbacks: callbacks),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,11 +169,11 @@ class ForegroundUploadService {
|
|||||||
onProgress: (bytes, totalBytes) => onProgress?.call(fileId, bytes, totalBytes),
|
onProgress: (bytes, totalBytes) => onProgress?.call(fileId, bytes, totalBytes),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (result.isSuccess) {
|
return switch (result) {
|
||||||
onSuccess?.call(fileId);
|
UploadSuccess() => onSuccess?.call(fileId),
|
||||||
} else if (!result.isCancelled && result.errorMessage != null) {
|
UploadError(:final message) => onError?.call(fileId, message),
|
||||||
onError?.call(fileId, result.errorMessage!);
|
UploadCancelled() => null,
|
||||||
}
|
};
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -216,7 +215,7 @@ class ForegroundUploadService {
|
|||||||
|
|
||||||
final item = items[index];
|
final item = items[index];
|
||||||
|
|
||||||
if (shouldSkip?.call(item) ?? false) {
|
if (shouldSkip != null && shouldSkip(item)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -233,78 +232,48 @@ class ForegroundUploadService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _uploadSingleAsset(
|
Future<void> _uploadSingleAsset(
|
||||||
LocalAsset asset,
|
LocalAsset asset, {
|
||||||
Completer<void>? cancelToken, {
|
required Completer<void>? cancelToken,
|
||||||
required UploadCallbacks callbacks,
|
required UploadCallbacks callbacks,
|
||||||
}) async {
|
}) async {
|
||||||
File? file;
|
final UploadCallbacks(:onProgress, :onSuccess, :onError, :onICloudProgress) = callbacks;
|
||||||
|
File? assetFile;
|
||||||
File? livePhotoFile;
|
File? livePhotoFile;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final entity = await _storageRepository.getAssetEntityForAsset(asset);
|
final entity = await _storageRepository.getAssetEntityForAsset(asset);
|
||||||
if (entity == null) {
|
if (entity == null) {
|
||||||
callbacks.onError?.call(
|
onError?.call(
|
||||||
asset.localId!,
|
asset.localId!,
|
||||||
CurrentPlatform.isAndroid ? "asset_not_found_on_device_android".t() : "asset_not_found_on_device_ios".t(),
|
CurrentPlatform.isAndroid ? "asset_not_found_on_device_android".t() : "asset_not_found_on_device_ios".t(),
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final isAvailableLocally = await _storageRepository.isAssetAvailableLocally(asset.id);
|
File? file;
|
||||||
|
if (entity.isLivePhoto) {
|
||||||
if (!isAvailableLocally && CurrentPlatform.isIOS) {
|
file = await _storageRepository.getMotionFile(asset.id, cancelToken: cancelToken, onProgress: onICloudProgress);
|
||||||
_logger.info("Loading iCloud asset ${asset.id} - ${asset.name}");
|
|
||||||
|
|
||||||
// Create progress handler for iCloud download
|
|
||||||
PMProgressHandler? progressHandler;
|
|
||||||
StreamSubscription? progressSubscription;
|
|
||||||
|
|
||||||
progressHandler = PMProgressHandler();
|
|
||||||
progressSubscription = progressHandler.stream.listen((event) {
|
|
||||||
callbacks.onICloudProgress?.call(asset.localId!, event.progress);
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
file = await _storageRepository.loadFileFromCloud(asset.id, progressHandler: progressHandler);
|
|
||||||
if (entity.isLivePhoto) {
|
|
||||||
livePhotoFile = await _storageRepository.loadMotionFileFromCloud(
|
|
||||||
asset.id,
|
|
||||||
progressHandler: progressHandler,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
await progressSubscription.cancel();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Get files locally
|
|
||||||
file = await _storageRepository.getFileForAsset(asset.id);
|
|
||||||
if (file == null) {
|
if (file == null) {
|
||||||
_logger.warning("Failed to get file ${asset.id} - ${asset.name}");
|
_logger.warning("Failed to obtain motion part of the livePhoto - ${asset.name}");
|
||||||
callbacks.onError?.call(
|
onError?.call(
|
||||||
asset.localId!,
|
asset.localId!,
|
||||||
CurrentPlatform.isAndroid ? "asset_not_found_on_device_android".t() : "asset_not_found_on_device_ios".t(),
|
CurrentPlatform.isAndroid ? "asset_not_found_on_device_android".t() : "asset_not_found_on_device_ios".t(),
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
livePhotoFile = file;
|
||||||
// For live photos, get the motion video file
|
|
||||||
if (entity.isLivePhoto) {
|
|
||||||
livePhotoFile = await _storageRepository.getMotionFileForAsset(asset);
|
|
||||||
if (livePhotoFile == null) {
|
|
||||||
_logger.warning("Failed to obtain motion part of the livePhoto - ${asset.name}");
|
|
||||||
callbacks.onError?.call(
|
|
||||||
asset.localId!,
|
|
||||||
CurrentPlatform.isAndroid ? "asset_not_found_on_device_android".t() : "asset_not_found_on_device_ios".t(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
file = await _storageRepository.getAssetFile(asset.id, cancelToken: cancelToken, onProgress: onICloudProgress);
|
||||||
if (file == null) {
|
if (file == null) {
|
||||||
_logger.warning("Failed to obtain file from iCloud for asset ${asset.id} - ${asset.name}");
|
_logger.warning("Failed to get file ${asset.id} - ${asset.name}");
|
||||||
callbacks.onError?.call(asset.localId!, "asset_not_found_on_icloud".t());
|
onError?.call(
|
||||||
|
asset.localId!,
|
||||||
|
CurrentPlatform.isAndroid ? "asset_not_found_on_device_android".t() : "asset_not_found_on_device_ios".t(),
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
assetFile = file;
|
||||||
|
|
||||||
String fileName = await _assetMediaRepository.getOriginalFilename(asset.id) ?? asset.name;
|
String fileName = await _assetMediaRepository.getOriginalFilename(asset.id) ?? asset.name;
|
||||||
|
|
||||||
@@ -330,11 +299,9 @@ class ForegroundUploadService {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Upload live photo video first if available
|
// Upload live photo video first if available
|
||||||
String? livePhotoVideoId;
|
|
||||||
if (entity.isLivePhoto && livePhotoFile != null) {
|
if (entity.isLivePhoto && livePhotoFile != null) {
|
||||||
final livePhotoTitle = p.setExtension(originalFileName, p.extension(livePhotoFile.path));
|
final livePhotoTitle = p.setExtension(originalFileName, p.extension(livePhotoFile.path));
|
||||||
|
|
||||||
final onProgress = callbacks.onProgress;
|
|
||||||
final livePhotoResult = await _uploadRepository.uploadFile(
|
final livePhotoResult = await _uploadRepository.uploadFile(
|
||||||
file: livePhotoFile,
|
file: livePhotoFile,
|
||||||
originalFileName: livePhotoTitle,
|
originalFileName: livePhotoTitle,
|
||||||
@@ -346,15 +313,16 @@ class ForegroundUploadService {
|
|||||||
logContext: 'livePhotoVideo[${asset.localId}]',
|
logContext: 'livePhotoVideo[${asset.localId}]',
|
||||||
);
|
);
|
||||||
|
|
||||||
if (livePhotoResult.isSuccess && livePhotoResult.remoteAssetId != null) {
|
switch (livePhotoResult) {
|
||||||
livePhotoVideoId = livePhotoResult.remoteAssetId;
|
case UploadSuccess(:final remoteAssetId):
|
||||||
|
fields['livePhotoVideoId'] = remoteAssetId;
|
||||||
|
case UploadError(:final message):
|
||||||
|
onError?.call(asset.localId!, "Failed to upload live photo video: $message");
|
||||||
|
return;
|
||||||
|
case UploadCancelled():
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (livePhotoVideoId != null) {
|
|
||||||
fields['livePhotoVideoId'] = livePhotoVideoId;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add cloudId metadata only to the still image, not the motion video, becasue when the sync id happens, the motion video can get associated with the wrong still image.
|
// Add cloudId metadata only to the still image, not the motion video, becasue when the sync id happens, the motion video can get associated with the wrong still image.
|
||||||
if (CurrentPlatform.isIOS && asset.cloudId != null) {
|
if (CurrentPlatform.isIOS && asset.cloudId != null) {
|
||||||
fields['metadata'] = jsonEncode([
|
fields['metadata'] = jsonEncode([
|
||||||
@@ -371,7 +339,6 @@ class ForegroundUploadService {
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
final onProgress = callbacks.onProgress;
|
|
||||||
final result = await _uploadRepository.uploadFile(
|
final result = await _uploadRepository.uploadFile(
|
||||||
file: file,
|
file: file,
|
||||||
originalFileName: originalFileName,
|
originalFileName: originalFileName,
|
||||||
@@ -383,34 +350,33 @@ class ForegroundUploadService {
|
|||||||
logContext: 'asset[${asset.localId}]',
|
logContext: 'asset[${asset.localId}]',
|
||||||
);
|
);
|
||||||
|
|
||||||
if (result.isSuccess && result.remoteAssetId != null) {
|
switch (result) {
|
||||||
callbacks.onSuccess?.call(asset.localId!, result.remoteAssetId!);
|
case UploadSuccess(:final remoteAssetId):
|
||||||
} else if (result.isCancelled) {
|
onSuccess?.call(asset.localId!, remoteAssetId);
|
||||||
_logger.warning(() => "Backup was cancelled by the user");
|
case UploadCancelled():
|
||||||
shouldAbortUpload = true;
|
|
||||||
} else if (result.errorMessage != null) {
|
|
||||||
_logger.severe(
|
|
||||||
() =>
|
|
||||||
"Error(${result.statusCode}) uploading ${asset.localId} | $originalFileName | Created on ${asset.createdAt} | ${result.errorMessage}",
|
|
||||||
);
|
|
||||||
|
|
||||||
callbacks.onError?.call(asset.localId!, result.errorMessage!);
|
|
||||||
|
|
||||||
if (result.errorMessage == "Quota has been exceeded!") {
|
|
||||||
shouldAbortUpload = true;
|
shouldAbortUpload = true;
|
||||||
}
|
_logger.warning("Upload was cancelled by the user for asset ${asset.localId}");
|
||||||
|
case UploadError(:final message, :final statusCode):
|
||||||
|
shouldAbortUpload |= message == "Quota has been exceeded!";
|
||||||
|
_logger.severe(
|
||||||
|
"Error(${statusCode ?? 'unknown'}) uploading ${asset.localId} | $originalFileName | Created on ${asset.createdAt} | $message",
|
||||||
|
);
|
||||||
|
onError?.call(asset.localId!, message);
|
||||||
}
|
}
|
||||||
} catch (error, stackTrace) {
|
} catch (error, stackTrace) {
|
||||||
_logger.severe(() => "Error backup asset: ${error.toString()}", stackTrace);
|
_logger.severe("Asset backup failed", error, stackTrace);
|
||||||
callbacks.onError?.call(asset.localId!, error.toString());
|
onError?.call(asset.localId!, error.toString());
|
||||||
} finally {
|
} finally {
|
||||||
if (Platform.isIOS) {
|
if (Platform.isIOS) {
|
||||||
try {
|
unawaited(
|
||||||
await file?.delete();
|
Future.wait([
|
||||||
await livePhotoFile?.delete();
|
if (assetFile != null) assetFile.delete(),
|
||||||
} catch (error, stackTrace) {
|
if (livePhotoFile != null) livePhotoFile.delete(),
|
||||||
_logger.severe(() => "ERROR deleting file: ${error.toString()}", stackTrace);
|
]).onError((error, stackTrace) {
|
||||||
}
|
_logger.severe("Post-upload file cleanup failed", error, stackTrace);
|
||||||
|
return const [];
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -446,7 +412,7 @@ class ForegroundUploadService {
|
|||||||
logContext: 'shareIntent[$deviceAssetId]',
|
logContext: 'shareIntent[$deviceAssetId]',
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return UploadResult.error(errorMessage: e.toString());
|
return UploadError(message: e.toString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ void main() {
|
|||||||
|
|
||||||
when(() => mockEntity.isLivePhoto).thenReturn(false);
|
when(() => mockEntity.isLivePhoto).thenReturn(false);
|
||||||
when(() => mockStorageRepository.getAssetEntityForAsset(asset)).thenAnswer((_) async => mockEntity);
|
when(() => mockStorageRepository.getAssetEntityForAsset(asset)).thenAnswer((_) async => mockEntity);
|
||||||
when(() => mockStorageRepository.getFileForAsset(asset.id)).thenAnswer((_) async => mockFile);
|
when(() => mockStorageRepository.getAssetFile(asset.id)).thenAnswer((_) async => mockFile);
|
||||||
when(() => mockAssetMediaRepository.getOriginalFilename(asset.id)).thenAnswer((_) async => 'OriginalPhoto.jpg');
|
when(() => mockAssetMediaRepository.getOriginalFilename(asset.id)).thenAnswer((_) async => 'OriginalPhoto.jpg');
|
||||||
|
|
||||||
final task = await sut.getUploadTask(asset);
|
final task = await sut.getUploadTask(asset);
|
||||||
@@ -92,7 +92,7 @@ void main() {
|
|||||||
|
|
||||||
when(() => mockEntity.isLivePhoto).thenReturn(false);
|
when(() => mockEntity.isLivePhoto).thenReturn(false);
|
||||||
when(() => mockStorageRepository.getAssetEntityForAsset(asset)).thenAnswer((_) async => mockEntity);
|
when(() => mockStorageRepository.getAssetEntityForAsset(asset)).thenAnswer((_) async => mockEntity);
|
||||||
when(() => mockStorageRepository.getFileForAsset(asset.id)).thenAnswer((_) async => mockFile);
|
when(() => mockStorageRepository.getAssetFile(asset.id)).thenAnswer((_) async => mockFile);
|
||||||
when(() => mockAssetMediaRepository.getOriginalFilename(asset.id)).thenAnswer((_) async => null);
|
when(() => mockAssetMediaRepository.getOriginalFilename(asset.id)).thenAnswer((_) async => null);
|
||||||
|
|
||||||
final task = await sut.getUploadTask(asset);
|
final task = await sut.getUploadTask(asset);
|
||||||
@@ -109,7 +109,7 @@ void main() {
|
|||||||
|
|
||||||
when(() => mockEntity.isLivePhoto).thenReturn(true);
|
when(() => mockEntity.isLivePhoto).thenReturn(true);
|
||||||
when(() => mockStorageRepository.getAssetEntityForAsset(asset)).thenAnswer((_) async => mockEntity);
|
when(() => mockStorageRepository.getAssetEntityForAsset(asset)).thenAnswer((_) async => mockEntity);
|
||||||
when(() => mockStorageRepository.getMotionFileForAsset(asset)).thenAnswer((_) async => mockFile);
|
when(() => mockStorageRepository.getMotionFile(asset.id)).thenAnswer((_) async => mockFile);
|
||||||
when(
|
when(
|
||||||
() => mockAssetMediaRepository.getOriginalFilename(asset.id),
|
() => mockAssetMediaRepository.getOriginalFilename(asset.id),
|
||||||
).thenAnswer((_) async => 'OriginalLivePhoto.HEIC');
|
).thenAnswer((_) async => 'OriginalLivePhoto.HEIC');
|
||||||
@@ -130,7 +130,7 @@ void main() {
|
|||||||
|
|
||||||
when(() => mockEntity.isLivePhoto).thenReturn(true);
|
when(() => mockEntity.isLivePhoto).thenReturn(true);
|
||||||
when(() => mockStorageRepository.getAssetEntityForAsset(asset)).thenAnswer((_) async => mockEntity);
|
when(() => mockStorageRepository.getAssetEntityForAsset(asset)).thenAnswer((_) async => mockEntity);
|
||||||
when(() => mockStorageRepository.getFileForAsset(asset.id)).thenAnswer((_) async => mockFile);
|
when(() => mockStorageRepository.getAssetFile(asset.id)).thenAnswer((_) async => mockFile);
|
||||||
when(
|
when(
|
||||||
() => mockAssetMediaRepository.getOriginalFilename(asset.id),
|
() => mockAssetMediaRepository.getOriginalFilename(asset.id),
|
||||||
).thenAnswer((_) async => 'OriginalLivePhoto.HEIC');
|
).thenAnswer((_) async => 'OriginalLivePhoto.HEIC');
|
||||||
@@ -150,7 +150,7 @@ void main() {
|
|||||||
|
|
||||||
when(() => mockEntity.isLivePhoto).thenReturn(true);
|
when(() => mockEntity.isLivePhoto).thenReturn(true);
|
||||||
when(() => mockStorageRepository.getAssetEntityForAsset(asset)).thenAnswer((_) async => mockEntity);
|
when(() => mockStorageRepository.getAssetEntityForAsset(asset)).thenAnswer((_) async => mockEntity);
|
||||||
when(() => mockStorageRepository.getFileForAsset(asset.id)).thenAnswer((_) async => mockFile);
|
when(() => mockStorageRepository.getAssetFile(asset.id)).thenAnswer((_) async => mockFile);
|
||||||
when(() => mockAssetMediaRepository.getOriginalFilename(asset.id)).thenAnswer((_) async => null);
|
when(() => mockAssetMediaRepository.getOriginalFilename(asset.id)).thenAnswer((_) async => null);
|
||||||
|
|
||||||
final task = await sut.getLivePhotoUploadTask(asset, 'video-id-456');
|
final task = await sut.getLivePhotoUploadTask(asset, 'video-id-456');
|
||||||
@@ -194,7 +194,7 @@ void main() {
|
|||||||
|
|
||||||
when(() => mockEntity.isLivePhoto).thenReturn(false);
|
when(() => mockEntity.isLivePhoto).thenReturn(false);
|
||||||
when(() => mockStorageRepository.getAssetEntityForAsset(assetWithCloudId)).thenAnswer((_) async => mockEntity);
|
when(() => mockStorageRepository.getAssetEntityForAsset(assetWithCloudId)).thenAnswer((_) async => mockEntity);
|
||||||
when(() => mockStorageRepository.getFileForAsset(assetWithCloudId.id)).thenAnswer((_) async => mockFile);
|
when(() => mockStorageRepository.getAssetFile(assetWithCloudId.id)).thenAnswer((_) async => mockFile);
|
||||||
when(() => mockAssetMediaRepository.getOriginalFilename(assetWithCloudId.id)).thenAnswer((_) async => 'test.jpg');
|
when(() => mockAssetMediaRepository.getOriginalFilename(assetWithCloudId.id)).thenAnswer((_) async => 'test.jpg');
|
||||||
|
|
||||||
final task = await sutWithV24.getUploadTask(assetWithCloudId);
|
final task = await sutWithV24.getUploadTask(assetWithCloudId);
|
||||||
@@ -243,7 +243,7 @@ void main() {
|
|||||||
|
|
||||||
when(() => mockEntity.isLivePhoto).thenReturn(false);
|
when(() => mockEntity.isLivePhoto).thenReturn(false);
|
||||||
when(() => mockStorageRepository.getAssetEntityForAsset(assetWithCloudId)).thenAnswer((_) async => mockEntity);
|
when(() => mockStorageRepository.getAssetEntityForAsset(assetWithCloudId)).thenAnswer((_) async => mockEntity);
|
||||||
when(() => mockStorageRepository.getFileForAsset(assetWithCloudId.id)).thenAnswer((_) async => mockFile);
|
when(() => mockStorageRepository.getAssetFile(assetWithCloudId.id)).thenAnswer((_) async => mockFile);
|
||||||
when(() => mockAssetMediaRepository.getOriginalFilename(assetWithCloudId.id)).thenAnswer((_) async => 'test.jpg');
|
when(() => mockAssetMediaRepository.getOriginalFilename(assetWithCloudId.id)).thenAnswer((_) async => 'test.jpg');
|
||||||
|
|
||||||
final task = await sutAndroid.getUploadTask(assetWithCloudId);
|
final task = await sutAndroid.getUploadTask(assetWithCloudId);
|
||||||
@@ -281,7 +281,7 @@ void main() {
|
|||||||
|
|
||||||
when(() => mockEntity.isLivePhoto).thenReturn(false);
|
when(() => mockEntity.isLivePhoto).thenReturn(false);
|
||||||
when(() => mockStorageRepository.getAssetEntityForAsset(assetWithoutCloudId)).thenAnswer((_) async => mockEntity);
|
when(() => mockStorageRepository.getAssetEntityForAsset(assetWithoutCloudId)).thenAnswer((_) async => mockEntity);
|
||||||
when(() => mockStorageRepository.getFileForAsset(assetWithoutCloudId.id)).thenAnswer((_) async => mockFile);
|
when(() => mockStorageRepository.getAssetFile(assetWithoutCloudId.id)).thenAnswer((_) async => mockFile);
|
||||||
when(
|
when(
|
||||||
() => mockAssetMediaRepository.getOriginalFilename(assetWithoutCloudId.id),
|
() => mockAssetMediaRepository.getOriginalFilename(assetWithoutCloudId.id),
|
||||||
).thenAnswer((_) async => 'test.jpg');
|
).thenAnswer((_) async => 'test.jpg');
|
||||||
@@ -323,7 +323,7 @@ void main() {
|
|||||||
|
|
||||||
when(() => mockEntity.isLivePhoto).thenReturn(true);
|
when(() => mockEntity.isLivePhoto).thenReturn(true);
|
||||||
when(() => mockStorageRepository.getAssetEntityForAsset(assetWithCloudId)).thenAnswer((_) async => mockEntity);
|
when(() => mockStorageRepository.getAssetEntityForAsset(assetWithCloudId)).thenAnswer((_) async => mockEntity);
|
||||||
when(() => mockStorageRepository.getFileForAsset(assetWithCloudId.id)).thenAnswer((_) async => mockFile);
|
when(() => mockStorageRepository.getAssetFile(assetWithCloudId.id)).thenAnswer((_) async => mockFile);
|
||||||
when(
|
when(
|
||||||
() => mockAssetMediaRepository.getOriginalFilename(assetWithCloudId.id),
|
() => mockAssetMediaRepository.getOriginalFilename(assetWithCloudId.id),
|
||||||
).thenAnswer((_) async => 'livephoto.heic');
|
).thenAnswer((_) async => 'livephoto.heic');
|
||||||
|
|||||||
Reference in New Issue
Block a user