mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-02-17 14:07:28 +08:00
feat(fs): delegate win --server file reading to CM (#13736)
- Route Windows server-to-client file reads through CM instead of the connection layer - Add FS IPC commands (ReadFile, CancelRead, SendConfirmForRead, ReadAllFiles) and CM data messages (ReadJobInitResult, FileBlockFromCM, FileReadDone, FileReadError, FileDigestFromCM, AllFilesResult) - Track pending read validations and read jobs to coordinate CM-driven file transfers and clean them up on completion, cancellation, and errors - Enforce a configurable file-transfer-max-files limit for ReadAllFiles and add stronger file name/path validation on the CM side - Improve Flutter file transfer UX and robustness: - Use explicit percent/percentText progress fields - Derive speed and cancel actions from the active job - Handle job errors via FileModel.handleJobError and complete pending recursive tasks on failure - Wrap recursive directory operations in try/catch and await sendRemoveEmptyDir when removing empty directories Signed-off-by: fufesou <linlong1266@gmail.com>
This commit is contained in:
@@ -282,11 +282,9 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
item.state != JobState.inProgress,
|
||||
child: LinearPercentIndicator(
|
||||
animateFromLastPercent: true,
|
||||
center: Text(
|
||||
'${(item.finishedSize / item.totalSize * 100).toStringAsFixed(0)}%',
|
||||
),
|
||||
center: Text(item.percentText),
|
||||
barRadius: Radius.circular(15),
|
||||
percent: item.finishedSize / item.totalSize,
|
||||
percent: item.percent,
|
||||
progressColor: MyTheme.accent,
|
||||
backgroundColor: Theme.of(context).hoverColor,
|
||||
lineHeight: kDesktopFileTransferRowHeight,
|
||||
|
||||
@@ -355,15 +355,21 @@ class _FileManagerPageState extends State<FileManagerPage> {
|
||||
return Offstage();
|
||||
}
|
||||
|
||||
switch (jobTable.last.state) {
|
||||
// Find the first job that is in progress (the one actually transferring data)
|
||||
// Rust backend processes jobs sequentially, so the first inProgress job is the active one
|
||||
final activeJob = jobTable
|
||||
.firstWhereOrNull((job) => job.state == JobState.inProgress) ??
|
||||
jobTable.last;
|
||||
|
||||
switch (activeJob.state) {
|
||||
case JobState.inProgress:
|
||||
return BottomSheetBody(
|
||||
leading: CircularProgressIndicator(),
|
||||
title: translate("Waiting"),
|
||||
text:
|
||||
"${translate("Speed")}: ${readableFileSize(jobTable.last.speed)}/s",
|
||||
"${translate("Speed")}: ${readableFileSize(activeJob.speed)}/s",
|
||||
onCanceled: () {
|
||||
model.jobController.cancelJob(jobTable.last.id);
|
||||
model.jobController.cancelJob(activeJob.id);
|
||||
jobTable.clear();
|
||||
},
|
||||
);
|
||||
@@ -371,7 +377,7 @@ class _FileManagerPageState extends State<FileManagerPage> {
|
||||
return BottomSheetBody(
|
||||
leading: Icon(Icons.check),
|
||||
title: "${translate("Successful")}!",
|
||||
text: jobTable.last.display(),
|
||||
text: activeJob.display(),
|
||||
onCanceled: () => jobTable.clear(),
|
||||
);
|
||||
case JobState.error:
|
||||
|
||||
@@ -275,7 +275,7 @@ class TransferJobSerdeData {
|
||||
: this(
|
||||
connId: d['connId'] ?? 0,
|
||||
id: int.tryParse(d['id'].toString()) ?? 0,
|
||||
path: d['path'] ?? '',
|
||||
path: d['dataSource'] ?? '',
|
||||
isRemote: d['isRemote'] ?? false,
|
||||
totalSize: d['totalSize'] ?? 0,
|
||||
finishedSize: d['finishedSize'] ?? 0,
|
||||
|
||||
@@ -113,6 +113,34 @@ class FileModel {
|
||||
fileFetcher.tryCompleteEmptyDirsTask(evt['value'], evt['is_local']);
|
||||
}
|
||||
|
||||
// This method fixes a deadlock that occurred when the previous code directly
|
||||
// called jobController.jobError(evt) in the job_error event handler.
|
||||
//
|
||||
// The problem with directly calling jobController.jobError():
|
||||
// 1. fetchDirectoryRecursiveToRemove(jobID) registers readRecursiveTasks[jobID]
|
||||
// and waits for completion
|
||||
// 2. If the remote has no permission (or some other errors), it returns a FileTransferError
|
||||
// 3. The error triggers job_error event, which called jobController.jobError()
|
||||
// 4. jobController.jobError() calls getJob(jobID) to find the job in jobTable
|
||||
// 5. But addDeleteDirJob() is called AFTER fetchDirectoryRecursiveToRemove(),
|
||||
// so the job doesn't exist yet in jobTable
|
||||
// 6. Result: jobController.jobError() does nothing useful, and
|
||||
// readRecursiveTasks[jobID] never completes, causing a 2s timeout
|
||||
//
|
||||
// Solution: Before calling jobController.jobError(), we first check if there's
|
||||
// a pending readRecursiveTasks with this ID and complete it with the error.
|
||||
void handleJobError(Map<String, dynamic> evt) {
|
||||
final id = int.tryParse(evt['id']?.toString() ?? '');
|
||||
if (id != null) {
|
||||
final err = evt['err']?.toString() ?? 'Unknown error';
|
||||
fileFetcher.tryCompleteRecursiveTaskWithError(id, err);
|
||||
}
|
||||
// Always call jobController.jobError(evt) to ensure all error events are processed,
|
||||
// even if the event does not have a valid job ID. This allows for generic error handling
|
||||
// or logging of unexpected errors.
|
||||
jobController.jobError(evt);
|
||||
}
|
||||
|
||||
Future<void> postOverrideFileConfirm(Map<String, dynamic> evt) async {
|
||||
evtLoop.pushEvent(
|
||||
_FileDialogEvent(WeakReference(this), FileDialogType.overwrite, evt));
|
||||
@@ -591,8 +619,21 @@ class FileController {
|
||||
} else if (item.isDirectory) {
|
||||
title = translate("Not an empty directory");
|
||||
dialogManager?.showLoading(translate("Waiting"));
|
||||
final fd = await fileFetcher.fetchDirectoryRecursiveToRemove(
|
||||
jobID, item.path, items.isLocal, true);
|
||||
final FileDirectory fd;
|
||||
try {
|
||||
fd = await fileFetcher.fetchDirectoryRecursiveToRemove(
|
||||
jobID, item.path, items.isLocal, true);
|
||||
} catch (e) {
|
||||
dialogManager?.dismissAll();
|
||||
final dm = dialogManager;
|
||||
if (dm != null) {
|
||||
msgBox(sessionId, 'custom-error-nook-nocancel-hasclose',
|
||||
translate("Error"), e.toString(), '', dm);
|
||||
} else {
|
||||
debugPrint("removeAction error msgbox failed: $e");
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (fd.path.isEmpty) {
|
||||
fd.path = item.path;
|
||||
}
|
||||
@@ -606,7 +647,7 @@ class FileController {
|
||||
item.name,
|
||||
false);
|
||||
if (confirm == true) {
|
||||
sendRemoveEmptyDir(
|
||||
await sendRemoveEmptyDir(
|
||||
item.path,
|
||||
0,
|
||||
deleteJobId,
|
||||
@@ -647,7 +688,7 @@ class FileController {
|
||||
// handle remove res;
|
||||
if (item.isDirectory &&
|
||||
res['file_num'] == (entries.length - 1).toString()) {
|
||||
sendRemoveEmptyDir(item.path, i, deleteJobId);
|
||||
await sendRemoveEmptyDir(item.path, i, deleteJobId);
|
||||
}
|
||||
} else {
|
||||
jobController.updateJobStatus(deleteJobId,
|
||||
@@ -660,7 +701,7 @@ class FileController {
|
||||
final res = await jobController.jobResultListener.start();
|
||||
if (item.isDirectory &&
|
||||
res['file_num'] == (entries.length - 1).toString()) {
|
||||
sendRemoveEmptyDir(item.path, i, deleteJobId);
|
||||
await sendRemoveEmptyDir(item.path, i, deleteJobId);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -755,9 +796,9 @@ class FileController {
|
||||
fileNum: fileNum);
|
||||
}
|
||||
|
||||
void sendRemoveEmptyDir(String path, int fileNum, int actId) {
|
||||
Future<void> sendRemoveEmptyDir(String path, int fileNum, int actId) async {
|
||||
history.removeWhere((element) => element.contains(path));
|
||||
bind.sessionRemoveAllEmptyDirs(
|
||||
await bind.sessionRemoveAllEmptyDirs(
|
||||
sessionId: sessionId, actId: actId, path: path, isRemote: !isLocal);
|
||||
}
|
||||
|
||||
@@ -1275,6 +1316,15 @@ class FileFetcher {
|
||||
}
|
||||
}
|
||||
|
||||
// Complete a pending recursive read task with an error.
|
||||
// See FileModel.handleJobError() for why this is necessary.
|
||||
void tryCompleteRecursiveTaskWithError(int id, String error) {
|
||||
final completer = readRecursiveTasks.remove(id);
|
||||
if (completer != null && !completer.isCompleted) {
|
||||
completer.completeError(error);
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<FileDirectory>> readEmptyDirs(
|
||||
String path, bool isLocal, bool showHidden) async {
|
||||
try {
|
||||
@@ -1438,6 +1488,10 @@ class JobProgress {
|
||||
var err = "";
|
||||
int lastTransferredSize = 0;
|
||||
|
||||
double get percent =>
|
||||
totalSize > 0 ? (finishedSize.toDouble() / totalSize) : 0.0;
|
||||
String get percentText => '${(percent * 100).toStringAsFixed(0)}%';
|
||||
|
||||
clear() {
|
||||
type = JobType.none;
|
||||
state = JobState.none;
|
||||
|
||||
@@ -363,7 +363,7 @@ class FfiModel with ChangeNotifier {
|
||||
parent.target?.fileModel.refreshAll();
|
||||
}
|
||||
} else if (name == 'job_error') {
|
||||
parent.target?.fileModel.jobController.jobError(evt);
|
||||
parent.target?.fileModel.handleJobError(evt);
|
||||
} else if (name == 'override_file_confirm') {
|
||||
parent.target?.fileModel.postOverrideFileConfirm(evt);
|
||||
} else if (name == 'load_last_job') {
|
||||
|
||||
Reference in New Issue
Block a user