mirror of
https://github.com/immich-app/immich.git
synced 2025-06-03 13:44:16 -04:00
fix: out of memory error when uploading large assets on slow internet (#224)
This commit is contained in:
parent
360c1d9a15
commit
e6efc61b3b
@ -23,6 +23,8 @@ PODS:
|
|||||||
- Flutter
|
- Flutter
|
||||||
- FMDB (>= 2.7.5)
|
- FMDB (>= 2.7.5)
|
||||||
- Toast (4.0.0)
|
- Toast (4.0.0)
|
||||||
|
- url_launcher_ios (0.0.1):
|
||||||
|
- Flutter
|
||||||
- video_player_avfoundation (0.0.1):
|
- video_player_avfoundation (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- wakelock (0.0.1):
|
- wakelock (0.0.1):
|
||||||
@ -37,6 +39,7 @@ DEPENDENCIES:
|
|||||||
- path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`)
|
- path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`)
|
||||||
- photo_manager (from `.symlinks/plugins/photo_manager/ios`)
|
- photo_manager (from `.symlinks/plugins/photo_manager/ios`)
|
||||||
- sqflite (from `.symlinks/plugins/sqflite/ios`)
|
- sqflite (from `.symlinks/plugins/sqflite/ios`)
|
||||||
|
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
||||||
- video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/ios`)
|
- video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/ios`)
|
||||||
- wakelock (from `.symlinks/plugins/wakelock/ios`)
|
- wakelock (from `.symlinks/plugins/wakelock/ios`)
|
||||||
|
|
||||||
@ -63,6 +66,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/photo_manager/ios"
|
:path: ".symlinks/plugins/photo_manager/ios"
|
||||||
sqflite:
|
sqflite:
|
||||||
:path: ".symlinks/plugins/sqflite/ios"
|
:path: ".symlinks/plugins/sqflite/ios"
|
||||||
|
url_launcher_ios:
|
||||||
|
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
||||||
video_player_avfoundation:
|
video_player_avfoundation:
|
||||||
:path: ".symlinks/plugins/video_player_avfoundation/ios"
|
:path: ".symlinks/plugins/video_player_avfoundation/ios"
|
||||||
wakelock:
|
wakelock:
|
||||||
@ -80,6 +85,7 @@ SPEC CHECKSUMS:
|
|||||||
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
|
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
|
||||||
sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904
|
sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904
|
||||||
Toast: 91b396c56ee72a5790816f40d3a94dd357abc196
|
Toast: 91b396c56ee72a5790816f40d3a94dd357abc196
|
||||||
|
url_launcher_ios: 839c58cdb4279282219f5e248c3321761ff3c4de
|
||||||
video_player_avfoundation: e489aac24ef5cf7af82702979ed16f2a5ef84cff
|
video_player_avfoundation: e489aac24ef5cf7af82702979ed16f2a5ef84cff
|
||||||
wakelock: d0fc7c864128eac40eba1617cb5264d9c940b46f
|
wakelock: d0fc7c864128eac40eba1617cb5264d9c940b46f
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import 'package:dio/dio.dart';
|
import 'package:cancellation_token_http/http.dart';
|
||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
import 'package:photo_manager/photo_manager.dart';
|
import 'package:photo_manager/photo_manager.dart';
|
||||||
|
|
||||||
@ -12,7 +12,7 @@ class BackUpState extends Equatable {
|
|||||||
final BackUpProgressEnum backupProgress;
|
final BackUpProgressEnum backupProgress;
|
||||||
final List<String> allAssetOnDatabase;
|
final List<String> allAssetOnDatabase;
|
||||||
final double progressInPercentage;
|
final double progressInPercentage;
|
||||||
final CancelToken cancelToken;
|
final CancellationToken cancelToken;
|
||||||
final ServerInfo serverInfo;
|
final ServerInfo serverInfo;
|
||||||
|
|
||||||
/// All available albums on the device
|
/// All available albums on the device
|
||||||
@ -43,7 +43,7 @@ class BackUpState extends Equatable {
|
|||||||
BackUpProgressEnum? backupProgress,
|
BackUpProgressEnum? backupProgress,
|
||||||
List<String>? allAssetOnDatabase,
|
List<String>? allAssetOnDatabase,
|
||||||
double? progressInPercentage,
|
double? progressInPercentage,
|
||||||
CancelToken? cancelToken,
|
CancellationToken? cancelToken,
|
||||||
ServerInfo? serverInfo,
|
ServerInfo? serverInfo,
|
||||||
List<AvailableAlbum>? availableAlbums,
|
List<AvailableAlbum>? availableAlbums,
|
||||||
Set<AssetPathEntity>? selectedBackupAlbums,
|
Set<AssetPathEntity>? selectedBackupAlbums,
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import 'package:cancellation_token_http/http.dart';
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:hive_flutter/hive_flutter.dart';
|
import 'package:hive_flutter/hive_flutter.dart';
|
||||||
@ -19,7 +20,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
backupProgress: BackUpProgressEnum.idle,
|
backupProgress: BackUpProgressEnum.idle,
|
||||||
allAssetOnDatabase: const [],
|
allAssetOnDatabase: const [],
|
||||||
progressInPercentage: 0,
|
progressInPercentage: 0,
|
||||||
cancelToken: CancelToken(),
|
cancelToken: CancellationToken(),
|
||||||
serverInfo: ServerInfo(
|
serverInfo: ServerInfo(
|
||||||
diskAvailable: "0",
|
diskAvailable: "0",
|
||||||
diskAvailableRaw: 0,
|
diskAvailableRaw: 0,
|
||||||
@ -266,7 +267,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Perform Backup
|
// Perform Backup
|
||||||
state = state.copyWith(cancelToken: CancelToken());
|
state = state.copyWith(cancelToken: CancellationToken());
|
||||||
_backupService.backupAsset(assetsWillBeBackup, state.cancelToken, _onAssetUploaded, _onUploadProgress);
|
_backupService.backupAsset(assetsWillBeBackup, state.cancelToken, _onAssetUploaded, _onUploadProgress);
|
||||||
} else {
|
} else {
|
||||||
PhotoManager.openSetting();
|
PhotoManager.openSetting();
|
||||||
@ -274,7 +275,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void cancelBackup() {
|
void cancelBackup() {
|
||||||
state.cancelToken.cancel('Cancel Backup');
|
state.cancelToken.cancel();
|
||||||
state = state.copyWith(backupProgress: BackUpProgressEnum.idle, progressInPercentage: 0.0);
|
state = state.copyWith(backupProgress: BackUpProgressEnum.idle, progressInPercentage: 0.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,11 +8,11 @@ import 'package:hive/hive.dart';
|
|||||||
import 'package:immich_mobile/constants/hive_box.dart';
|
import 'package:immich_mobile/constants/hive_box.dart';
|
||||||
import 'package:immich_mobile/shared/services/network.service.dart';
|
import 'package:immich_mobile/shared/services/network.service.dart';
|
||||||
import 'package:immich_mobile/shared/models/device_info.model.dart';
|
import 'package:immich_mobile/shared/models/device_info.model.dart';
|
||||||
import 'package:immich_mobile/utils/dio_http_interceptor.dart';
|
|
||||||
import 'package:immich_mobile/utils/files_helper.dart';
|
import 'package:immich_mobile/utils/files_helper.dart';
|
||||||
import 'package:photo_manager/photo_manager.dart';
|
import 'package:photo_manager/photo_manager.dart';
|
||||||
import 'package:http_parser/http_parser.dart';
|
import 'package:http_parser/http_parser.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
|
import 'package:cancellation_token_http/http.dart' as http;
|
||||||
|
|
||||||
class BackupService {
|
class BackupService {
|
||||||
final NetworkService _networkService = NetworkService();
|
final NetworkService _networkService = NetworkService();
|
||||||
@ -26,17 +26,13 @@ class BackupService {
|
|||||||
return result.cast<String>();
|
return result.cast<String>();
|
||||||
}
|
}
|
||||||
|
|
||||||
backupAsset(Set<AssetEntity> assetList, CancelToken cancelToken, Function(String, String) singleAssetDoneCb,
|
backupAsset(Set<AssetEntity> assetList, http.CancellationToken cancelToken,
|
||||||
Function(int, int) uploadProgress) async {
|
Function(String, String) singleAssetDoneCb, Function(int, int) uploadProgress) async {
|
||||||
var dio = Dio();
|
|
||||||
dio.interceptors.add(AuthenticatedRequestInterceptor());
|
|
||||||
|
|
||||||
String deviceId = Hive.box(userInfoBox).get(deviceIdKey);
|
String deviceId = Hive.box(userInfoBox).get(deviceIdKey);
|
||||||
String savedEndpoint = Hive.box(userInfoBox).get(serverEndpointKey);
|
String savedEndpoint = Hive.box(userInfoBox).get(serverEndpointKey);
|
||||||
File? file;
|
File? file;
|
||||||
|
|
||||||
MultipartFile assetRawUploadData;
|
http.MultipartFile? thumbnailUploadData;
|
||||||
MultipartFile thumbnailUploadData;
|
|
||||||
|
|
||||||
for (var entity in assetList) {
|
for (var entity in assetList) {
|
||||||
try {
|
try {
|
||||||
@ -47,35 +43,27 @@ class BackupService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (file != null) {
|
if (file != null) {
|
||||||
FormData formData;
|
|
||||||
String originalFileName = await entity.titleAsync;
|
String originalFileName = await entity.titleAsync;
|
||||||
String fileNameWithoutPath = originalFileName.toString().split(".")[0];
|
String fileNameWithoutPath = originalFileName.toString().split(".")[0];
|
||||||
var fileExtension = p.extension(file.path);
|
var fileExtension = p.extension(file.path);
|
||||||
var mimeType = FileHelper.getMimeType(file.path);
|
var mimeType = FileHelper.getMimeType(file.path);
|
||||||
assetRawUploadData = await MultipartFile.fromFile(
|
var fileStream = file.openRead();
|
||||||
file.path,
|
var assetRawUploadData = http.MultipartFile(
|
||||||
|
"assetData",
|
||||||
|
fileStream,
|
||||||
|
file.lengthSync(),
|
||||||
filename: fileNameWithoutPath,
|
filename: fileNameWithoutPath,
|
||||||
contentType: MediaType(
|
contentType: MediaType(
|
||||||
mimeType["type"],
|
mimeType["type"],
|
||||||
mimeType["subType"],
|
mimeType["subType"],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
formData = FormData.fromMap({
|
|
||||||
'deviceAssetId': entity.id,
|
|
||||||
'deviceId': deviceId,
|
|
||||||
'assetType': _getAssetType(entity.type),
|
|
||||||
'createdAt': entity.createDateTime.toIso8601String(),
|
|
||||||
'modifiedAt': entity.modifiedDateTime.toIso8601String(),
|
|
||||||
'isFavorite': entity.isFavorite,
|
|
||||||
'fileExtension': fileExtension,
|
|
||||||
'duration': entity.videoDuration,
|
|
||||||
'assetData': [assetRawUploadData]
|
|
||||||
});
|
|
||||||
|
|
||||||
// Build thumbnail multipart data
|
// Build thumbnail multipart data
|
||||||
var thumbnailData = await entity.thumbnailDataWithSize(const ThumbnailSize(1440, 2560));
|
var thumbnailData = await entity.thumbnailDataWithSize(const ThumbnailSize(1440, 2560));
|
||||||
if (thumbnailData != null) {
|
if (thumbnailData != null) {
|
||||||
thumbnailUploadData = MultipartFile.fromBytes(
|
thumbnailUploadData = http.MultipartFile.fromBytes(
|
||||||
|
"thumbnailData",
|
||||||
List.from(thumbnailData),
|
List.from(thumbnailData),
|
||||||
filename: fileNameWithoutPath,
|
filename: fileNameWithoutPath,
|
||||||
contentType: MediaType(
|
contentType: MediaType(
|
||||||
@ -83,39 +71,37 @@ class BackupService {
|
|||||||
"jpeg",
|
"jpeg",
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Send thumbnail data if it is exist
|
|
||||||
formData = FormData.fromMap({
|
|
||||||
'deviceAssetId': entity.id,
|
|
||||||
'deviceId': deviceId,
|
|
||||||
'assetType': _getAssetType(entity.type),
|
|
||||||
'createdAt': entity.createDateTime.toIso8601String(),
|
|
||||||
'modifiedAt': entity.modifiedDateTime.toIso8601String(),
|
|
||||||
'isFavorite': entity.isFavorite,
|
|
||||||
'fileExtension': fileExtension,
|
|
||||||
'duration': entity.videoDuration,
|
|
||||||
'thumbnailData': [thumbnailUploadData],
|
|
||||||
'assetData': [assetRawUploadData]
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Response res = await dio.post(
|
var box = Hive.box(userInfoBox);
|
||||||
'$savedEndpoint/asset/upload',
|
|
||||||
data: formData,
|
var req = MultipartRequest('POST', Uri.parse('$savedEndpoint/asset/upload'),
|
||||||
cancelToken: cancelToken,
|
onProgress: ((bytes, totalBytes) => uploadProgress(bytes, totalBytes)));
|
||||||
onSendProgress: (sent, total) => uploadProgress(sent, total),
|
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();
|
||||||
|
|
||||||
|
if (thumbnailUploadData != null) {
|
||||||
|
req.files.add(thumbnailUploadData);
|
||||||
|
}
|
||||||
|
req.files.add(assetRawUploadData);
|
||||||
|
|
||||||
|
var res = await req.send(cancellationToken: cancelToken);
|
||||||
|
|
||||||
if (res.statusCode == 201) {
|
if (res.statusCode == 201) {
|
||||||
singleAssetDoneCb(entity.id, deviceId);
|
singleAssetDoneCb(entity.id, deviceId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} on DioError catch (e) {
|
} on http.CancelledException {
|
||||||
debugPrint("DioError backupAsset: ${e.response}");
|
debugPrint("Backup was cancelled by the user");
|
||||||
if (e.type == DioErrorType.cancel || e.type == DioErrorType.other) {
|
return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("ERROR backupAsset: ${e.toString()}");
|
debugPrint("ERROR backupAsset: ${e.toString()}");
|
||||||
continue;
|
continue;
|
||||||
@ -150,3 +136,35 @@ class BackupService {
|
|||||||
return DeviceInfoRemote.fromJson(res.toString());
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -141,6 +141,20 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.1"
|
version: "1.0.1"
|
||||||
|
cancellation_token:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: cancellation_token
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "1.4.0"
|
||||||
|
cancellation_token_http:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: cancellation_token_http
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.0"
|
||||||
characters:
|
characters:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -437,7 +451,7 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "0.15.0"
|
version: "0.15.0"
|
||||||
http:
|
http:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: http
|
name: http
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
|
@ -40,6 +40,8 @@ dependencies:
|
|||||||
equatable: ^2.0.3
|
equatable: ^2.0.3
|
||||||
image_picker: ^0.8.5+3
|
image_picker: ^0.8.5+3
|
||||||
url_launcher: ^6.1.3
|
url_launcher: ^6.1.3
|
||||||
|
http: 0.13.4
|
||||||
|
cancellation_token_http: ^1.1.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
Loading…
x
Reference in New Issue
Block a user