mirror of
				https://github.com/KevinMidboe/immich.git
				synced 2025-10-29 17:40:28 +00:00 
			
		
		
		
	* Added info box * Fixed upload endpoint doesn't report error status code * Added chip to show update error * Added chip to show failed upload * Add duplication check for upload * Better duplication-checking placement * Remove check for duplicated asset * Added failed backup status route * added page * Display error card with thumbnail * Improved styling * Set thumbnail with better quality * Remove force upload error
		
			
				
	
	
		
			221 lines
		
	
	
		
			7.0 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
			
		
		
	
	
			221 lines
		
	
	
		
			7.0 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
| import 'dart:async';
 | |
| import 'dart:convert';
 | |
| import 'dart:io';
 | |
| 
 | |
| import 'package:dio/dio.dart';
 | |
| import 'package:flutter/material.dart';
 | |
| import 'package:hive/hive.dart';
 | |
| import 'package:hooks_riverpod/hooks_riverpod.dart';
 | |
| import 'package:immich_mobile/constants/hive_box.dart';
 | |
| import 'package:immich_mobile/modules/backup/models/check_duplicate_asset_response.model.dart';
 | |
| import 'package:immich_mobile/modules/backup/models/current_upload_asset.model.dart';
 | |
| import 'package:immich_mobile/modules/backup/models/error_upload_asset.model.dart';
 | |
| import 'package:immich_mobile/shared/services/network.service.dart';
 | |
| import 'package:immich_mobile/shared/models/device_info.model.dart';
 | |
| import 'package:immich_mobile/utils/files_helper.dart';
 | |
| import 'package:photo_manager/photo_manager.dart';
 | |
| import 'package:http_parser/http_parser.dart';
 | |
| import 'package:path/path.dart' as p;
 | |
| import 'package:cancellation_token_http/http.dart' as http;
 | |
| 
 | |
| final backupServiceProvider =
 | |
|     Provider((ref) => BackupService(ref.watch(networkServiceProvider)));
 | |
| 
 | |
| class BackupService {
 | |
|   final NetworkService _networkService;
 | |
| 
 | |
|   BackupService(this._networkService);
 | |
| 
 | |
|   Future<List<String>> getDeviceBackupAsset() async {
 | |
|     String deviceId = Hive.box(userInfoBox).get(deviceIdKey);
 | |
| 
 | |
|     Response response =
 | |
|         await _networkService.getRequest(url: "asset/$deviceId");
 | |
|     List<dynamic> result = jsonDecode(response.toString());
 | |
| 
 | |
|     return result.cast<String>();
 | |
|   }
 | |
| 
 | |
|   Future<bool> checkDuplicateAsset(String deviceAssetId) async {
 | |
|     String deviceId = Hive.box(userInfoBox).get(deviceIdKey);
 | |
| 
 | |
|     try {
 | |
|       Response response =
 | |
|           await _networkService.postRequest(url: "asset/check", data: {
 | |
|         "deviceId": deviceId,
 | |
|         "deviceAssetId": deviceAssetId,
 | |
|       });
 | |
| 
 | |
|       if (response.statusCode == 200) {
 | |
|         var result = CheckDuplicateAssetResponse.fromJson(response.toString());
 | |
| 
 | |
|         return result.isExist;
 | |
|       } else {
 | |
|         return false;
 | |
|       }
 | |
|     } catch (e) {
 | |
|       return false;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   backupAsset(
 | |
|     Set<AssetEntity> assetList,
 | |
|     http.CancellationToken cancelToken,
 | |
|     Function(String, String) singleAssetDoneCb,
 | |
|     Function(int, int) uploadProgressCb,
 | |
|     Function(CurrentUploadAsset) setCurrentUploadAssetCb,
 | |
|     Function(ErrorUploadAsset) errorCb,
 | |
|   ) async {
 | |
|     String deviceId = Hive.box(userInfoBox).get(deviceIdKey);
 | |
|     String savedEndpoint = Hive.box(userInfoBox).get(serverEndpointKey);
 | |
|     File? file;
 | |
| 
 | |
|     for (var entity in assetList) {
 | |
|       try {
 | |
|         if (entity.type == AssetType.video) {
 | |
|           file = await entity.originFile;
 | |
|         } else {
 | |
|           file = await entity.originFile.timeout(const Duration(seconds: 5));
 | |
|         }
 | |
| 
 | |
|         if (file != null) {
 | |
|           String originalFileName = await entity.titleAsync;
 | |
|           String fileNameWithoutPath =
 | |
|               originalFileName.toString().split(".")[0];
 | |
|           var fileExtension = p.extension(file.path);
 | |
|           var mimeType = FileHelper.getMimeType(file.path);
 | |
|           var fileStream = file.openRead();
 | |
|           var assetRawUploadData = http.MultipartFile(
 | |
|             "assetData",
 | |
|             fileStream,
 | |
|             file.lengthSync(),
 | |
|             filename: fileNameWithoutPath,
 | |
|             contentType: MediaType(
 | |
|               mimeType["type"],
 | |
|               mimeType["subType"],
 | |
|             ),
 | |
|           );
 | |
| 
 | |
|           var box = Hive.box(userInfoBox);
 | |
| 
 | |
|           var req = MultipartRequest(
 | |
|               'POST', Uri.parse('$savedEndpoint/asset/upload'),
 | |
|               onProgress: ((bytes, totalBytes) =>
 | |
|                   uploadProgressCb(bytes, totalBytes)));
 | |
|           req.headers["Authorization"] = "Bearer ${box.get(accessTokenKey)}";
 | |
| 
 | |
|           req.fields['deviceAssetId'] = entity.id;
 | |
|           req.fields['deviceId'] = deviceId;
 | |
|           req.fields['assetType'] = _getAssetType(entity.type);
 | |
|           req.fields['createdAt'] = entity.createDateTime.toIso8601String();
 | |
|           req.fields['modifiedAt'] = entity.modifiedDateTime.toIso8601String();
 | |
|           req.fields['isFavorite'] = entity.isFavorite.toString();
 | |
|           req.fields['fileExtension'] = fileExtension;
 | |
|           req.fields['duration'] = entity.videoDuration.toString();
 | |
| 
 | |
|           req.files.add(assetRawUploadData);
 | |
| 
 | |
|           setCurrentUploadAssetCb(
 | |
|             CurrentUploadAsset(
 | |
|               id: entity.id,
 | |
|               createdAt: entity.createDateTime,
 | |
|               fileName: originalFileName,
 | |
|               fileType: _getAssetType(entity.type),
 | |
|             ),
 | |
|           );
 | |
| 
 | |
|           var response = await req.send(cancellationToken: cancelToken);
 | |
| 
 | |
|           if (response.statusCode == 201) {
 | |
|             singleAssetDoneCb(entity.id, deviceId);
 | |
|           } else {
 | |
|             var data = await response.stream.bytesToString();
 | |
|             var error = jsonDecode(data);
 | |
| 
 | |
|             debugPrint(
 | |
|                 "Error(${error['statusCode']}) uploading ${entity.id} | $originalFileName | Created on ${entity.createDateTime} | ${error['error']}");
 | |
| 
 | |
|             errorCb(ErrorUploadAsset(
 | |
|               asset: entity,
 | |
|               id: entity.id,
 | |
|               createdAt: entity.createDateTime,
 | |
|               fileName: originalFileName,
 | |
|               fileType: _getAssetType(entity.type),
 | |
|               errorMessage: error['error'],
 | |
|             ));
 | |
|             continue;
 | |
|           }
 | |
|         }
 | |
|       } on http.CancelledException {
 | |
|         debugPrint("Backup was cancelled by the user");
 | |
|         return;
 | |
|       } catch (e) {
 | |
|         debugPrint("ERROR backupAsset: ${e.toString()}");
 | |
|         continue;
 | |
|       } finally {
 | |
|         if (Platform.isIOS) {
 | |
|           file?.deleteSync();
 | |
|         }
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   void sendBackupRequest(AssetEntity entity) {}
 | |
| 
 | |
|   String _getAssetType(AssetType assetType) {
 | |
|     switch (assetType) {
 | |
|       case AssetType.audio:
 | |
|         return "AUDIO";
 | |
|       case AssetType.image:
 | |
|         return "IMAGE";
 | |
|       case AssetType.video:
 | |
|         return "VIDEO";
 | |
|       case AssetType.other:
 | |
|         return "OTHER";
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   Future<DeviceInfoRemote> setAutoBackup(
 | |
|       bool status, String deviceId, String deviceType) async {
 | |
|     var res = await _networkService.patchRequest(url: 'device-info', data: {
 | |
|       "isAutoBackup": status,
 | |
|       "deviceId": deviceId,
 | |
|       "deviceType": deviceType,
 | |
|     });
 | |
| 
 | |
|     return DeviceInfoRemote.fromJson(res.toString());
 | |
|   }
 | |
| }
 | |
| 
 | |
| class MultipartRequest extends http.MultipartRequest {
 | |
|   /// Creates a new [MultipartRequest].
 | |
|   MultipartRequest(
 | |
|     String method,
 | |
|     Uri url, {
 | |
|     required this.onProgress,
 | |
|   }) : super(method, url);
 | |
| 
 | |
|   final void Function(int bytes, int totalBytes) onProgress;
 | |
| 
 | |
|   /// Freezes all mutable fields and returns a
 | |
|   /// single-subscription [http.ByteStream]
 | |
|   /// that will emit the request body.
 | |
|   @override
 | |
|   http.ByteStream finalize() {
 | |
|     final byteStream = super.finalize();
 | |
| 
 | |
|     final total = contentLength;
 | |
|     var bytes = 0;
 | |
| 
 | |
|     final t = StreamTransformer.fromHandlers(
 | |
|       handleData: (List<int> data, EventSink<List<int>> sink) {
 | |
|         bytes += data.length;
 | |
|         onProgress.call(bytes, total);
 | |
|         sink.add(data);
 | |
|       },
 | |
|     );
 | |
|     final stream = byteStream.transform(t);
 | |
|     return http.ByteStream(stream);
 | |
|   }
 | |
| }
 |