immich/mobile/lib/modules/backup/services/backup.service.dart
Alex 58ec7553ea
Add information for uploading asset and error indication with error message for each failed upload. (#315)
* 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
2022-07-06 16:12:55 -05:00

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);
}
}