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() {
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(
'downloading_media'.tr(),
'${'file_name'.tr()}: {filename}',

View File

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

View File

@ -2,35 +2,99 @@
import 'dart:convert';
import 'package:background_downloader/background_downloader.dart';
import 'package:collection/collection.dart';
import 'package:flutter/cupertino.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/providers/background_sync.provider.dart';
import 'package:immich_mobile/services/exp_backup.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 {
final int totalCount;
final int backupCount;
final int remainderCount;
final Map<String, ExpUploadStatus> uploadItems;
ExpBackupState({
required this.totalCount,
required this.backupCount,
required this.remainderCount,
required this.uploadItems,
});
ExpBackupState copyWith({
int? totalCount,
int? backupCount,
int? remainderCount,
Map<String, ExpUploadStatus>? uploadItems,
}) {
return ExpBackupState(
totalCount: totalCount ?? this.totalCount,
backupCount: backupCount ?? this.backupCount,
remainderCount: remainderCount ?? this.remainderCount,
uploadItems: uploadItems ?? this.uploadItems,
);
}
@ -39,6 +103,7 @@ class ExpBackupState {
'totalCount': totalCount,
'backupCount': backupCount,
'remainderCount': remainderCount,
'uploadItems': uploadItems,
};
}
@ -47,6 +112,14 @@ class ExpBackupState {
totalCount: map['totalCount'] as int,
backupCount: map['backupCount'] 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>);
@override
String toString() =>
'ExpBackupState(totalCount: $totalCount, backupCount: $backupCount, remainderCount: $remainderCount)';
String toString() {
return 'ExpBackupState(totalCount: $totalCount, backupCount: $backupCount, remainderCount: $remainderCount, uploadItems: $uploadItems)';
}
@override
bool operator ==(covariant ExpBackupState other) {
if (identical(this, other)) return true;
final mapEquals = const DeepCollectionEquality().equals;
return other.totalCount == totalCount &&
other.backupCount == backupCount &&
other.remainderCount == remainderCount;
other.remainderCount == remainderCount &&
mapEquals(other.uploadItems, uploadItems);
}
@override
int get hashCode =>
totalCount.hashCode ^ backupCount.hashCode ^ remainderCount.hashCode;
int get hashCode {
return totalCount.hashCode ^
backupCount.hashCode ^
remainderCount.hashCode ^
uploadItems.hashCode;
}
}
final expBackupProvider =
@ -92,6 +172,7 @@ class ExpBackupNotifier extends StateNotifier<ExpBackupState> {
totalCount: 0,
backupCount: 0,
remainderCount: 0,
uploadItems: {},
),
) {
{
@ -120,8 +201,6 @@ class ExpBackupNotifier extends StateNotifier<ExpBackupState> {
remainderCount: state.remainderCount - 1,
);
// TODO: find a better place to call this.
_backgroundSyncManager.syncRemote();
break;
default:
@ -130,10 +209,30 @@ class ExpBackupNotifier extends StateNotifier<ExpBackupState> {
}
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 {
await _backgroundSyncManager.syncRemote();
final [totalCount, backupCount, remainderCount] = await Future.wait([
_backupService.getTotalCount(),
_backupService.getBackupCount(),
@ -152,7 +251,6 @@ class ExpBackupNotifier extends StateNotifier<ExpBackupState> {
}
Future<void> cancel() async {
await _uploadService.cancel();
debugPrint("Cancel uploads");
await _backupService.cancel();
}
}

View File

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

View File

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

View File

@ -2,43 +2,53 @@ import 'dart:io';
import 'package:easy_localization/easy_localization.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/providers/backup/exp_backup.provider.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/icloud_download_progress_bar.dart';
import 'package:immich_mobile/widgets/backup/upload_progress_bar.dart';
import 'package:immich_mobile/widgets/backup/upload_stats.dart';
class CurrentUploadingAssetInfoBox extends StatelessWidget {
class CurrentUploadingAssetInfoBox extends ConsumerWidget {
const CurrentUploadingAssetInfoBox({super.key});
@override
Widget build(BuildContext context) {
return ListTile(
isThreeLine: true,
leading: Icon(
Icons.image_outlined,
color: context.primaryColor,
size: 30,
),
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(),
],
),
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(
dense: true,
leading: CircularProgressIndicator(
value: uploadItem.progress,
strokeWidth: 2,
),
title: Text(
uploadItem.filename,
style: context.textTheme.bodySmall,
overflow: TextOverflow.ellipsis,
),
trailing: Text(
'${(uploadItem.progress * 100).toInt()}%',
style: context.textTheme.bodySmall,
),
);
},
),
),
],
);
}
}

View File

@ -103,10 +103,6 @@ export class FileUploadInterceptor implements NestInterceptor {
}
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(
() => this.assetService.getUploadFilename(asRequest(request, file)),
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) => {
file.originalname = request.body['filename'] ?? file.filename;
return {
auth: request.user || null,
fieldName: file.fieldname as UploadFieldName,