diff --git a/mobile/android/kls_database.db b/mobile/android/kls_database.db new file mode 100644 index 0000000000..2fd5648887 Binary files /dev/null and b/mobile/android/kls_database.db differ diff --git a/mobile/lib/services/backup.service.dart b/mobile/lib/services/backup.service.dart index 8f958fff8c..9c10b6b7b1 100644 --- a/mobile/lib/services/backup.service.dart +++ b/mobile/lib/services/backup.service.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; +import 'package:background_downloader/background_downloader.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -33,6 +34,7 @@ final backupServiceProvider = Provider( class BackupService { final httpClient = http.Client(); + final _fileDownloader = FileDownloader(); final ApiService _apiService; final Isar _db; final Logger _log = Logger("BackupService"); @@ -242,121 +244,146 @@ class BackupService { ) : assetList.toList(); + final tasks = []; for (var entity in assetsToUpload) { - File? file; - File? livePhotoFile; + final isAvailableLocally = + await entity.isLocallyAvailable(isOrigin: true); - try { - final isAvailableLocally = - await entity.isLocallyAvailable(isOrigin: true); - - // Handle getting files from iCloud - if (!isAvailableLocally && Platform.isIOS) { - // Skip iCloud assets if the user has disabled this feature - if (isIgnoreIcloudAssets) { - continue; - } - - setCurrentUploadAssetCb( - CurrentUploadAsset( - id: entity.id, - fileCreatedAt: entity.createDateTime.year == 1970 - ? entity.modifiedDateTime - : entity.createDateTime, - fileName: await entity.titleAsync, - fileType: _getAssetType(entity.type), - iCloudAsset: true, - ), - ); - - file = await entity.loadFile(progressHandler: pmProgressHandler); - livePhotoFile = await entity.loadFile( - withSubtype: true, - progressHandler: pmProgressHandler, - ); - } else { - if (entity.type == AssetType.video) { - file = await entity.originFile; - } else { - file = await entity.originFile.timeout(const Duration(seconds: 5)); - if (entity.isLivePhoto) { - livePhotoFile = await entity.originFileWithSubtype - .timeout(const Duration(seconds: 5)); - } - } + // Handle getting files from iCloud + if (!isAvailableLocally && Platform.isIOS) { + // Skip iCloud assets if the user has disabled this feature + if (isIgnoreIcloudAssets) { + continue; } - if (file != null) { - String originalFileName = await entity.titleAsync; - var fileStream = file.openRead(); - var assetRawUploadData = http.MultipartFile( - "assetData", - fileStream, - file.lengthSync(), - filename: originalFileName, - ); + setCurrentUploadAssetCb( + CurrentUploadAsset( + id: entity.id, + fileCreatedAt: entity.createDateTime.year == 1970 + ? entity.modifiedDateTime + : entity.createDateTime, + fileName: await entity.titleAsync, + fileType: _getAssetType(entity.type), + iCloudAsset: true, + ), + ); + } - var req = MultipartRequest( - 'POST', - Uri.parse('$savedEndpoint/asset/upload'), - onProgress: ((bytes, totalBytes) => - uploadProgressCb(bytes, totalBytes)), - ); - req.headers["x-immich-user-token"] = Store.get(StoreKey.accessToken); - req.headers["Transfer-Encoding"] = "chunked"; + final files = []; + // TODO: This is silly to have to load the file just to access the path + // But there doesn't seem to be any other way to do it + final fileName = (await entity.originFile)?.path; + files.add(fileName); - req.fields['deviceAssetId'] = entity.id; - req.fields['deviceId'] = deviceId; - req.fields['fileCreatedAt'] = - entity.createDateTime.toUtc().toIso8601String(); - req.fields['fileModifiedAt'] = - entity.modifiedDateTime.toUtc().toIso8601String(); - req.fields['isFavorite'] = entity.isFavorite.toString(); - req.fields['duration'] = entity.videoDuration.toString(); + if (entity.isLivePhoto) { + final livePhotoFileName = (await entity.originFileWithSubtype)?.path; + if (livePhotoFileName != null) { + files.add(livePhotoFileName); + } + } - req.files.add(assetRawUploadData); + final url = '$savedEndpoint/asset/upload'; + final headers = { + 'x-immich-user-token': Store.get(StoreKey.accessToken), + 'Transfer-Encoding': 'chunked', + }; - var fileSize = file.lengthSync(); + final fields = { + 'deviceAssetId': entity.id, + 'deviceId': deviceId, + 'fileCreatedAt': entity.createDateTime.toUtc().toIso8601String(), + 'fileModifiedAt': entity.modifiedDateTime.toUtc().toIso8601String(), + 'isFavorite': entity.isFavorite.toString(), + 'duration': entity.videoDuration.toString(), + }; - if (entity.isLivePhoto) { - if (livePhotoFile != null) { - final livePhotoTitle = p.setExtension( - originalFileName, - p.extension(livePhotoFile.path), - ); - final fileStream = livePhotoFile.openRead(); - final livePhotoRawUploadData = http.MultipartFile( - "livePhotoData", - fileStream, - livePhotoFile.lengthSync(), - filename: livePhotoTitle, - ); - req.files.add(livePhotoRawUploadData); - fileSize += livePhotoFile.lengthSync(); - } else { - _log.warning( - "Failed to obtain motion part of the livePhoto - $originalFileName", - ); - } - } + if (files.length == 1) { + final String file = files.first; + final split = file.split('/'); + final name = split.last; + final directory = split.take(split.length - 1).join('/'); - setCurrentUploadAssetCb( - CurrentUploadAsset( - id: entity.id, - fileCreatedAt: entity.createDateTime.year == 1970 - ? entity.modifiedDateTime - : entity.createDateTime, - fileName: originalFileName, - fileType: _getAssetType(entity.type), - fileSize: fileSize, - iCloudAsset: false, - ), - ); + final task = UploadTask( + url: url, + group: 'backup', + fileField: 'assetData', + taskId: entity.id, + fields: fields, + headers: headers, + updates: Updates.statusAndProgress, + retries: 0, + httpRequestMethod: 'POST', + displayName: 'Immich', + filename: name, + directory: directory, + baseDirectory: BaseDirectory.root, + ); + tasks.add(task); + } else { + final task = MultiUploadTask( + url: url, + files: files, + headers: headers, + fields: fields, + updates: Updates.statusAndProgress, + group: 'backup', + taskId: entity.id, + retries: 0, + displayName: 'Immich', + httpRequestMethod: 'POST', + baseDirectory: BaseDirectory.root, + ); - var response = - await httpClient.send(req, cancellationToken: cancelToken); + print('created task $task for files $files'); - if (response.statusCode == 200) { + tasks.add(task); + } + } + + final permission = await _fileDownloader.permissions + .status(PermissionType.androidSharedStorage); + print('has permission $permission'); + + if (tasks.length == 1) { + final result = await _fileDownloader.upload( + tasks.first, + onProgress: (percent) => print('${percent * 100} done'), + onStatus: (status) => print('status $status'), + onElapsedTime: (t) => print('time is $t'), + elapsedTimeInterval: const Duration(seconds: 1), + ); + + print('$result is done with ${result.status}'); + print('result ${result.responseBody}'); + print('result ${result.responseHeaders}'); + } else { + final result = await _fileDownloader.uploadBatch( + tasks, + batchProgressCallback: (succeeded, failed) => + print('$succeeded succeeded, $failed failed'), + taskStatusCallback: (status) => print('status $status'), + taskProgressCallback: (update) => print('update $update'), + onElapsedTime: (t) => print('time is $t'), + elapsedTimeInterval: const Duration(seconds: 1), + ); + + print( + '$result is done with ${result.succeeded.length} succeeded and ${result.failed.length} failed', + ); + + for (final task in result.succeeded) { + final r = result.results[task]; + print('successful task $task with result $r'); + } + + for (final task in result.failed) { + final r = result.results[task]; + print('failed task $task with result $r'); + } + } + + /* + if (result.status == 200) { // asset is a duplicate (already exists on the server) duplicatedAssetIds.add(entity.id); uploadSuccessCb(entity.id, deviceId, true); @@ -409,6 +436,7 @@ class BackupService { } } } + */ if (duplicatedAssetIds.isNotEmpty) { await _saveDuplicatedAssetIds(duplicatedAssetIds); } diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index ed6afd7ff7..b3340c09b1 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -60,6 +60,7 @@ dependencies: octo_image: ^2.0.0 thumbhash: 0.1.0+1 async: ^2.11.0 + background_downloader: ^8.0.0 openapi: path: openapi @@ -82,6 +83,7 @@ dependency_overrides: #f url: https://github.com/Zverik/flutter-geolocator.git #f ref: floss #f path: geolocator_android + http: ^1.1.0 dev_dependencies: flutter_test: diff --git a/web/src/lib/components/elements/date-input.svelte b/web/src/lib/components/elements/date-input.svelte index 1f621b2464..ccd405c934 100644 --- a/web/src/lib/components/elements/date-input.svelte +++ b/web/src/lib/components/elements/date-input.svelte @@ -17,4 +17,9 @@ {value} on:input={(e) => (updatedValue = e.currentTarget.value)} on:blur={() => (value = updatedValue)} + on:keydown={(e) => { + if (e.key === 'Enter') { + value = updatedValue; + } + }} />