fix: filename interpretation

This commit is contained in:
Alex Tran 2025-06-11 16:39:56 -05:00 committed by Alex
parent 3ccde454b1
commit 12a472ebbe
No known key found for this signature in database
GPG Key ID: 53CD082B3A5E1082
8 changed files with 173 additions and 49 deletions

View File

@ -156,7 +156,21 @@ class ImmichAppState extends ConsumerState<ImmichApp>
} }
void _configureFileDownloaderNotifications() { void _configureFileDownloaderNotifications() {
FileDownloader().configureNotification( FileDownloader().configureNotificationForGroup(
downloadGroupImage,
running: TaskNotification(
'downloading_media'.tr(),
'${'file_name'.tr()}: {filename}',
),
complete: TaskNotification(
'download_finished'.tr(),
'${'file_name'.tr()}: {filename}',
),
progressBar: true,
);
FileDownloader().configureNotificationForGroup(
downloadGroupVideo,
running: TaskNotification( running: TaskNotification(
'downloading_media'.tr(), 'downloading_media'.tr(),
'${'file_name'.tr()}: {filename}', '${'file_name'.tr()}: {filename}',

View File

@ -197,7 +197,7 @@ class ExpBackupPage extends HookConsumerWidget {
const RemainderCard(), const RemainderCard(),
const Divider(), const Divider(),
buildControlButtons(), buildControlButtons(),
const CurrentUploadingAssetInfoBox(), // const CurrentUploadingAssetInfoBox(),
] ]
: [ : [
const BackupAlbumSelectionCard(), const BackupAlbumSelectionCard(),

View File

@ -2,35 +2,99 @@
import 'dart:convert'; import 'dart:convert';
import 'package:background_downloader/background_downloader.dart'; import 'package:background_downloader/background_downloader.dart';
import 'package:collection/collection.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/constants.dart';
import 'package:immich_mobile/domain/utils/background_sync.dart'; import 'package:immich_mobile/domain/utils/background_sync.dart';
import 'package:immich_mobile/providers/background_sync.provider.dart'; import 'package:immich_mobile/providers/background_sync.provider.dart';
import 'package:immich_mobile/services/exp_backup.service.dart'; import 'package:immich_mobile/services/exp_backup.service.dart';
import 'package:immich_mobile/services/upload.service.dart'; import 'package:immich_mobile/services/upload.service.dart';
class ExpUploadStatus {
final String taskId;
final String filename;
final double progress;
ExpUploadStatus({
required this.taskId,
required this.filename,
required this.progress,
});
ExpUploadStatus copyWith({
String? taskId,
String? filename,
double? progress,
}) {
return ExpUploadStatus(
taskId: taskId ?? this.taskId,
filename: filename ?? this.filename,
progress: progress ?? this.progress,
);
}
Map<String, dynamic> toMap() {
return <String, dynamic>{
'taskId': taskId,
'filename': filename,
'progress': progress,
};
}
factory ExpUploadStatus.fromMap(Map<String, dynamic> map) {
return ExpUploadStatus(
taskId: map['taskId'] as String,
filename: map['filename'] as String,
progress: map['progress'] as double,
);
}
String toJson() => json.encode(toMap());
factory ExpUploadStatus.fromJson(String source) =>
ExpUploadStatus.fromMap(json.decode(source) as Map<String, dynamic>);
@override
String toString() =>
'ExpUploadStatus(taskId: $taskId, filename: $filename, progress: $progress)';
@override
bool operator ==(covariant ExpUploadStatus other) {
if (identical(this, other)) return true;
return other.taskId == taskId &&
other.filename == filename &&
other.progress == progress;
}
@override
int get hashCode => taskId.hashCode ^ filename.hashCode ^ progress.hashCode;
}
class ExpBackupState { class ExpBackupState {
final int totalCount; final int totalCount;
final int backupCount; final int backupCount;
final int remainderCount; final int remainderCount;
final Map<String, ExpUploadStatus> uploadItems;
ExpBackupState({ ExpBackupState({
required this.totalCount, required this.totalCount,
required this.backupCount, required this.backupCount,
required this.remainderCount, required this.remainderCount,
required this.uploadItems,
}); });
ExpBackupState copyWith({ ExpBackupState copyWith({
int? totalCount, int? totalCount,
int? backupCount, int? backupCount,
int? remainderCount, int? remainderCount,
Map<String, ExpUploadStatus>? uploadItems,
}) { }) {
return ExpBackupState( return ExpBackupState(
totalCount: totalCount ?? this.totalCount, totalCount: totalCount ?? this.totalCount,
backupCount: backupCount ?? this.backupCount, backupCount: backupCount ?? this.backupCount,
remainderCount: remainderCount ?? this.remainderCount, remainderCount: remainderCount ?? this.remainderCount,
uploadItems: uploadItems ?? this.uploadItems,
); );
} }
@ -39,6 +103,7 @@ class ExpBackupState {
'totalCount': totalCount, 'totalCount': totalCount,
'backupCount': backupCount, 'backupCount': backupCount,
'remainderCount': remainderCount, 'remainderCount': remainderCount,
'uploadItems': uploadItems,
}; };
} }
@ -47,6 +112,14 @@ class ExpBackupState {
totalCount: map['totalCount'] as int, totalCount: map['totalCount'] as int,
backupCount: map['backupCount'] as int, backupCount: map['backupCount'] as int,
remainderCount: map['remainderCount'] as int, remainderCount: map['remainderCount'] as int,
uploadItems: Map<String, ExpUploadStatus>.from(
(map['uploadItems'] as Map<String, dynamic>).map(
(key, value) => MapEntry(
key,
ExpUploadStatus.fromMap(value as Map<String, dynamic>),
),
),
),
); );
} }
@ -56,21 +129,28 @@ class ExpBackupState {
ExpBackupState.fromMap(json.decode(source) as Map<String, dynamic>); ExpBackupState.fromMap(json.decode(source) as Map<String, dynamic>);
@override @override
String toString() => String toString() {
'ExpBackupState(totalCount: $totalCount, backupCount: $backupCount, remainderCount: $remainderCount)'; return 'ExpBackupState(totalCount: $totalCount, backupCount: $backupCount, remainderCount: $remainderCount, uploadItems: $uploadItems)';
}
@override @override
bool operator ==(covariant ExpBackupState other) { bool operator ==(covariant ExpBackupState other) {
if (identical(this, other)) return true; if (identical(this, other)) return true;
final mapEquals = const DeepCollectionEquality().equals;
return other.totalCount == totalCount && return other.totalCount == totalCount &&
other.backupCount == backupCount && other.backupCount == backupCount &&
other.remainderCount == remainderCount; other.remainderCount == remainderCount &&
mapEquals(other.uploadItems, uploadItems);
} }
@override @override
int get hashCode => int get hashCode {
totalCount.hashCode ^ backupCount.hashCode ^ remainderCount.hashCode; return totalCount.hashCode ^
backupCount.hashCode ^
remainderCount.hashCode ^
uploadItems.hashCode;
}
} }
final expBackupProvider = final expBackupProvider =
@ -92,6 +172,7 @@ class ExpBackupNotifier extends StateNotifier<ExpBackupState> {
totalCount: 0, totalCount: 0,
backupCount: 0, backupCount: 0,
remainderCount: 0, remainderCount: 0,
uploadItems: {},
), ),
) { ) {
{ {
@ -120,8 +201,6 @@ class ExpBackupNotifier extends StateNotifier<ExpBackupState> {
remainderCount: state.remainderCount - 1, remainderCount: state.remainderCount - 1,
); );
// TODO: find a better place to call this.
_backgroundSyncManager.syncRemote();
break; break;
default: default:
@ -130,10 +209,30 @@ class ExpBackupNotifier extends StateNotifier<ExpBackupState> {
} }
void _taskProgressCallback(TaskProgressUpdate update) { void _taskProgressCallback(TaskProgressUpdate update) {
debugPrint("[_taskProgressCallback] $update"); final uploadStatus = ExpUploadStatus(
taskId: update.task.taskId,
filename: update.task.displayName,
progress: update.progress,
);
state = state.copyWith(
uploadItems: {
for (final entry in state.uploadItems.entries)
if (entry.key == update.task.taskId)
entry.key: uploadStatus
else
entry.key: entry.value,
if (!state.uploadItems.containsKey(update.task.taskId))
update.task.taskId: uploadStatus,
},
);
print(update.task.taskId);
} }
Future<void> getBackupStatus() async { Future<void> getBackupStatus() async {
await _backgroundSyncManager.syncRemote();
final [totalCount, backupCount, remainderCount] = await Future.wait([ final [totalCount, backupCount, remainderCount] = await Future.wait([
_backupService.getTotalCount(), _backupService.getTotalCount(),
_backupService.getBackupCount(), _backupService.getBackupCount(),
@ -152,7 +251,6 @@ class ExpBackupNotifier extends StateNotifier<ExpBackupState> {
} }
Future<void> cancel() async { Future<void> cancel() async {
await _uploadService.cancel(); await _backupService.cancel();
debugPrint("Cancel uploads");
} }
} }

View File

@ -26,6 +26,7 @@ class ExpBackupService {
final IBackupRepository _backupRepository; final IBackupRepository _backupRepository;
final IStorageRepository _storageRepository; final IStorageRepository _storageRepository;
final UploadService _uploadService; final UploadService _uploadService;
bool shouldCancel = false;
Future<int> getTotalCount() { Future<int> getTotalCount() {
return _backupRepository.getTotalCount(); return _backupRepository.getTotalCount();
@ -40,6 +41,8 @@ class ExpBackupService {
} }
Future<void> backup() async { Future<void> backup() async {
shouldCancel = false;
final candidates = await _backupRepository.getCandidates(); final candidates = await _backupRepository.getCandidates();
if (candidates.isEmpty) { if (candidates.isEmpty) {
return; return;
@ -47,6 +50,10 @@ class ExpBackupService {
const batchSize = 5; const batchSize = 5;
for (int i = 0; i < candidates.length; i += batchSize) { for (int i = 0; i < candidates.length; i += batchSize) {
if (shouldCancel) {
break;
}
final batch = candidates.skip(i).take(batchSize).toList(); final batch = candidates.skip(i).take(batchSize).toList();
List<UploadTask> tasks = []; List<UploadTask> tasks = [];
@ -57,7 +64,7 @@ class ExpBackupService {
} }
} }
if (tasks.isNotEmpty) { if (tasks.isNotEmpty && !shouldCancel) {
_uploadService.enqueueTasks(tasks); _uploadService.enqueueTasks(tasks);
} }
} }
@ -82,6 +89,7 @@ class ExpBackupService {
} }
Future<void> cancel() async { Future<void> cancel() async {
shouldCancel = true;
await _uploadService.cancel(); await _uploadService.cancel();
} }
} }

View File

@ -108,7 +108,7 @@ class UploadService {
return UploadTask( return UploadTask(
taskId: id, taskId: id,
displayName: filename, displayName: originalFileName ?? filename,
httpRequestMethod: 'POST', httpRequestMethod: 'POST',
url: url, url: url,
headers: headers, headers: headers,

View File

@ -2,43 +2,53 @@ import 'dart:io';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/providers/backup/exp_backup.provider.dart';
import 'package:immich_mobile/widgets/backup/asset_info_table.dart'; import 'package:immich_mobile/widgets/backup/asset_info_table.dart';
import 'package:immich_mobile/widgets/backup/error_chip.dart'; import 'package:immich_mobile/widgets/backup/error_chip.dart';
import 'package:immich_mobile/widgets/backup/icloud_download_progress_bar.dart'; import 'package:immich_mobile/widgets/backup/icloud_download_progress_bar.dart';
import 'package:immich_mobile/widgets/backup/upload_progress_bar.dart'; import 'package:immich_mobile/widgets/backup/upload_progress_bar.dart';
import 'package:immich_mobile/widgets/backup/upload_stats.dart'; import 'package:immich_mobile/widgets/backup/upload_stats.dart';
class CurrentUploadingAssetInfoBox extends StatelessWidget { class CurrentUploadingAssetInfoBox extends ConsumerWidget {
const CurrentUploadingAssetInfoBox({super.key}); const CurrentUploadingAssetInfoBox({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, WidgetRef ref) {
final uploadItems =
ref.watch(expBackupProvider.select((state) => state.uploadItems));
return Column(
children: [
if (uploadItems.isNotEmpty)
Container(
constraints: const BoxConstraints(maxHeight: 200),
child: ListView.builder(
shrinkWrap: true,
itemCount: uploadItems.length,
itemBuilder: (context, index) {
final uploadItem = uploadItems.values.elementAt(index);
return ListTile( return ListTile(
isThreeLine: true, dense: true,
leading: Icon( leading: CircularProgressIndicator(
Icons.image_outlined, value: uploadItem.progress,
color: context.primaryColor, strokeWidth: 2,
size: 30, ),
title: Text(
uploadItem.filename,
style: context.textTheme.bodySmall,
overflow: TextOverflow.ellipsis,
),
trailing: Text(
'${(uploadItem.progress * 100).toInt()}%',
style: context.textTheme.bodySmall,
),
);
},
),
), ),
title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"backup_controller_page_uploading_file_info",
style: context.textTheme.titleSmall,
).tr(),
const BackupErrorChip(),
], ],
),
subtitle: Column(
children: [
if (Platform.isIOS) const IcloudDownloadProgressBar(),
const BackupUploadProgressBar(),
const BackupUploadStats(),
const BackupAssetInfoTable(),
],
),
); );
} }
} }

View File

@ -103,10 +103,6 @@ export class FileUploadInterceptor implements NestInterceptor {
} }
private filename(request: AuthRequest, file: Express.Multer.File, callback: DiskStorageCallback) { private filename(request: AuthRequest, file: Express.Multer.File, callback: DiskStorageCallback) {
console.log(
'FileUploadInterceptor.filename called with file:',
this.assetService.getUploadFilename(asRequest(request, file)),
);
return callbackify( return callbackify(
() => this.assetService.getUploadFilename(asRequest(request, file)), () => this.assetService.getUploadFilename(asRequest(request, file)),
callback as Callback<string>, callback as Callback<string>,

View File

@ -191,8 +191,6 @@ export function mapToUploadFile(file: ImmichFile): UploadFile {
} }
export const asRequest = (request: AuthRequest, file: Express.Multer.File) => { export const asRequest = (request: AuthRequest, file: Express.Multer.File) => {
file.originalname = request.body['filename'] ?? file.filename;
return { return {
auth: request.user || null, auth: request.user || null,
fieldName: file.fieldname as UploadFieldName, fieldName: file.fieldname as UploadFieldName,