refact: file transfer, do this for all conflicts(tasks) (#15385)

Signed-off-by: fufesou <linlong1266@gmail.com>
This commit is contained in:
fufesou
2026-06-23 11:23:42 +08:00
committed by GitHub
parent 456817b4f4
commit 0c6df924d1
2 changed files with 236 additions and 32 deletions

View File

@@ -142,12 +142,22 @@ class FileModel {
} }
Future<void> postOverrideFileConfirm(Map<String, dynamic> evt) async { Future<void> postOverrideFileConfirm(Map<String, dynamic> evt) async {
final id = int.tryParse(evt['id']?.toString() ?? '');
if (id == null || !jobController.hasTransferConflictJob(id)) {
debugPrint("Ignore stale override confirm event: $evt");
return;
}
evtLoop.pushEvent( evtLoop.pushEvent(
_FileDialogEvent(WeakReference(this), FileDialogType.overwrite, evt)); _FileDialogEvent(WeakReference(this), FileDialogType.overwrite, evt));
} }
Future<void> overrideFileConfirm(Map<String, dynamic> evt, Future<void> overrideFileConfirm(Map<String, dynamic> evt,
{bool? overrideConfirm, bool skip = false}) async { {bool? overrideConfirm, bool skip = false}) async {
final id = int.tryParse(evt['id']?.toString() ?? '') ?? 0;
if (id == 0 || !jobController.hasTransferConflictJob(id)) {
debugPrint("Ignore override confirm for inactive job: $evt");
return;
}
// If `skip == true`, it means to skip this file without showing dialog. // If `skip == true`, it means to skip this file without showing dialog.
// Because `resp` may be null after the user operation or the last remembered operation, // Because `resp` may be null after the user operation or the last remembered operation,
// and we should distinguish them. // and we should distinguish them.
@@ -156,15 +166,12 @@ class FileModel {
? await showFileConfirmDialog(translate("Overwrite"), ? await showFileConfirmDialog(translate("Overwrite"),
"${evt['read_path']}", true, evt['is_identical'] == "true") "${evt['read_path']}", true, evt['is_identical'] == "true")
: null); : null);
final id = int.tryParse(evt['id']) ?? 0; if (!jobController.hasTransferConflictJob(id)) {
debugPrint("Ignore override confirm result for inactive job: $evt");
return;
}
if (false == resp) { if (false == resp) {
final jobIndex = jobController.getJob(id); await jobController.cancelTransferConflictBatch(id);
if (jobIndex != -1) {
await jobController.cancelJob(id);
final job = jobController.jobTable[jobIndex];
job.state = JobState.done;
jobController.jobTable.refresh();
}
} else { } else {
var need_override = false; var need_override = false;
if (resp == null) { if (resp == null) {
@@ -176,6 +183,7 @@ class FileModel {
} }
// Update the loop config. // Update the loop config.
if (fileConfirmCheckboxRemember) { if (fileConfirmCheckboxRemember) {
jobController.rememberTransferConflictBatch(id, resp);
evtLoop.setSkip(!need_override); evtLoop.setSkip(!need_override);
} }
await bind.sessionSetConfirmOverrideFile( await bind.sessionSetConfirmOverrideFile(
@@ -285,6 +293,8 @@ class FileModel {
final isWindows = otherSideData.options.isWindows; final isWindows = otherSideData.options.isWindows;
final showHidden = otherSideData.options.showHidden; final showHidden = otherSideData.options.showHidden;
final jobID = jobController.addTransferJob(entry, false); final jobID = jobController.addTransferJob(entry, false);
jobController.registerTransferConflictBatch([jobID],
batchId: int.tryParse(obj['batchId']?.toString() ?? ''));
webSendLocalFiles( webSendLocalFiles(
handleIndex: handleIndex, handleIndex: handleIndex,
actId: jobID, actId: jobID,
@@ -570,8 +580,15 @@ class FileController {
final toPath = otherSideData.directory.path; final toPath = otherSideData.directory.path;
final isWindows = otherSideData.options.isWindows; final isWindows = otherSideData.options.isWindows;
final showHidden = otherSideData.options.showHidden; final showHidden = otherSideData.options.showHidden;
final transferJobs = <(Entry, int)>[];
final transferJobIds = <int>[];
for (var from in items.items) { for (var from in items.items) {
final jobID = jobController.addTransferJob(from, isRemoteToLocal); final jobID = jobController.addTransferJob(from, isRemoteToLocal);
transferJobs.add((from, jobID));
transferJobIds.add(jobID);
}
jobController.registerTransferConflictBatch(transferJobIds);
for (final (from, jobID) in transferJobs) {
bind.sessionSendFiles( bind.sessionSendFiles(
sessionId: sessionId, sessionId: sessionId,
actId: jobID, actId: jobID,
@@ -917,6 +934,10 @@ class JobController {
static final JobID jobID = JobID(); static final JobID jobID = JobID();
final jobTable = List<JobProgress>.empty(growable: true).obs; final jobTable = List<JobProgress>.empty(growable: true).obs;
final jobResultListener = JobResultListener<Map<String, dynamic>>(); final jobResultListener = JobResultListener<Map<String, dynamic>>();
int _nextTransferConflictBatchId = 1;
final Map<int, int> _transferConflictJobToBatch = {};
int? _transferConflictRememberBatchId;
bool? _transferConflictRememberOverrideConfirm;
final GetSessionID getSessionID; final GetSessionID getSessionID;
final GetDialogManager getDialogManager; final GetDialogManager getDialogManager;
SessionID get sessionId => getSessionID(); SessionID get sessionId => getSessionID();
@@ -929,6 +950,57 @@ class JobController {
return jobTable.indexWhere((element) => element.id == id); return jobTable.indexWhere((element) => element.id == id);
} }
void registerTransferConflictBatch(Iterable<int> jobIds, {int? batchId}) {
final ids = jobIds.toList(growable: false);
if (ids.isEmpty) {
return;
}
batchId ??= _nextTransferConflictBatchId++;
if (batchId >= _nextTransferConflictBatchId) {
_nextTransferConflictBatchId = batchId + 1;
}
for (final jobId in ids) {
_transferConflictJobToBatch[jobId] = batchId;
}
}
int? transferConflictBatchId(int jobId) {
return _transferConflictJobToBatch[jobId];
}
bool hasTransferConflictJob(int jobId) {
return transferConflictBatchId(jobId) != null;
}
bool isTransferConflictRememberBatch(int? batchId) {
return batchId != null && batchId == _transferConflictRememberBatchId;
}
bool? transferConflictRememberOverrideConfirm(int? batchId) {
if (!isTransferConflictRememberBatch(batchId)) {
return null;
}
return _transferConflictRememberOverrideConfirm;
}
void rememberTransferConflictBatch(int jobId, bool? overrideConfirm) {
_transferConflictRememberBatchId = _transferConflictJobToBatch[jobId];
_transferConflictRememberOverrideConfirm = overrideConfirm;
}
void unregisterTransferConflictJob(int jobId) {
final batchId = _transferConflictJobToBatch.remove(jobId);
if (batchId == null) {
return;
}
if (!_transferConflictJobToBatch.containsValue(batchId)) {
if (_transferConflictRememberBatchId == batchId) {
_transferConflictRememberBatchId = null;
_transferConflictRememberOverrideConfirm = null;
}
}
}
// return jobID // return jobID
int addTransferJob(Entry from, bool isRemoteToLocal) { int addTransferJob(Entry from, bool isRemoteToLocal) {
final jobID = JobController.jobID.next(); final jobID = JobController.jobID.next();
@@ -1000,7 +1072,10 @@ class JobController {
id = int.parse(evt['id']); id = int.parse(evt['id']);
} catch (_) {} } catch (_) {}
final jobIndex = getJob(id); final jobIndex = getJob(id);
if (jobIndex == -1) return true; if (jobIndex == -1) {
unregisterTransferConflictJob(id);
return true;
}
final job = jobTable[jobIndex]; final job = jobTable[jobIndex];
job.recvJobRes = true; job.recvJobRes = true;
if (job.type == JobType.deleteFile) { if (job.type == JobType.deleteFile) {
@@ -1026,6 +1101,9 @@ class JobController {
job.state = JobState.done; job.state = JobState.done;
} }
jobTable.refresh(); jobTable.refresh();
if (job.state == JobState.done || job.state == JobState.error) {
unregisterTransferConflictJob(id);
}
if (job.type == JobType.deleteDir) { if (job.type == JobType.deleteDir) {
return job.state == JobState.done; return job.state == JobState.done;
} else { } else {
@@ -1035,9 +1113,15 @@ class JobController {
void jobError(Map<String, dynamic> evt) { void jobError(Map<String, dynamic> evt) {
final err = evt['err'].toString(); final err = evt['err'].toString();
int jobIndex = getJob(int.parse(evt['id'])); final id = int.tryParse(evt['id']?.toString() ?? '');
if (id == null) {
debugPrint("Ignore job error with invalid id: $evt");
return;
}
int jobIndex = getJob(id);
if (jobIndex != -1) { if (jobIndex != -1) {
final job = jobTable[jobIndex]; final job = jobTable[jobIndex];
if (job.state == JobState.done && job.err == "cancel") return;
job.state = JobState.error; job.state = JobState.error;
job.err = err; job.err = err;
job.recvJobRes = true; job.recvJobRes = true;
@@ -1060,6 +1144,11 @@ class JobController {
} }
} }
jobTable.refresh(); jobTable.refresh();
if (job.state == JobState.done || job.state == JobState.error) {
unregisterTransferConflictJob(job.id);
}
} else {
unregisterTransferConflictJob(id);
} }
if (err == _kOneWayFileTransferError) { if (err == _kOneWayFileTransferError) {
if (DateTime.now().millisecondsSinceEpoch - _lastTimeShowMsgbox > 3000) { if (DateTime.now().millisecondsSinceEpoch - _lastTimeShowMsgbox > 3000) {
@@ -1096,9 +1185,42 @@ class JobController {
} }
Future<void> cancelJob(int id) async { Future<void> cancelJob(int id) async {
unregisterTransferConflictJob(id);
await bind.sessionCancelJob(sessionId: sessionId, actId: id); await bind.sessionCancelJob(sessionId: sessionId, actId: id);
} }
Future<void> cancelTransferConflictBatch(int jobId) async {
final batchId = _transferConflictJobToBatch[jobId];
final batchJobIds = batchId == null ? [jobId] : <int>[];
if (batchId != null) {
for (final entry in _transferConflictJobToBatch.entries) {
if (entry.value == batchId) {
batchJobIds.add(entry.key);
}
}
for (final id in batchJobIds) {
unregisterTransferConflictJob(id);
}
}
final jobIdsToCancel = batchJobIds.toSet();
for (final job in jobTable) {
if (!jobIdsToCancel.contains(job.id) || job.state == JobState.done) {
continue;
}
job.state = JobState.done;
job.err = "cancel";
job.recvJobRes = true;
}
jobTable.refresh();
for (final id in batchJobIds) {
try {
await bind.sessionCancelJob(sessionId: sessionId, actId: id);
} catch (e) {
debugPrint("Failed to cancel transfer job $id in conflict batch: $e");
}
}
}
Future<void> loadLastJob(Map<String, dynamic> evt) async { Future<void> loadLastJob(Map<String, dynamic> evt) async {
debugPrint("load last job: $evt"); debugPrint("load last job: $evt");
Map<String, dynamic> jobDetail = json.decode(evt['value']); Map<String, dynamic> jobDetail = json.decode(evt['value']);
@@ -1145,7 +1267,7 @@ class JobController {
..state = JobState.paused; ..state = JobState.paused;
jobTable.add(jobProgress); jobTable.add(jobProgress);
} }
registerTransferConflictBatch([currJobId]);
await bind.sessionAddJob( await bind.sessionAddJob(
sessionId: sessionId, sessionId: sessionId,
isRemote: isRemote, isRemote: isRemote,
@@ -1193,6 +1315,9 @@ class JobController {
void clear() { void clear() {
jobTable.clear(); jobTable.clear();
_transferConflictJobToBatch.clear();
_transferConflictRememberBatchId = null;
_transferConflictRememberOverrideConfirm = null;
jobResultListener.clear(); jobResultListener.clear();
} }
} }
@@ -1535,6 +1660,9 @@ class JobProgress {
String display() { String display() {
if (type == JobType.transfer) { if (type == JobType.transfer) {
if (state == JobState.done && err == "cancel") {
return translate("Cancel");
}
if (state == JobState.done && err == "skipped") { if (state == JobState.done && err == "skipped") {
return translate("Skipped"); return translate("Skipped");
} }
@@ -1844,21 +1972,44 @@ class _FileDialogEvent extends BaseEvent<FileDialogType, Map<String, dynamic>> {
class FileDialogEventLoop class FileDialogEventLoop
extends BaseEventLoop<FileDialogType, Map<String, dynamic>> { extends BaseEventLoop<FileDialogType, Map<String, dynamic>> {
int? _batchId;
bool? _overrideConfirm; bool? _overrideConfirm;
bool _skip = false; bool _skip = false;
@override @override
Future<void> onPreConsume( Future<void> onPreConsume(
BaseEvent<FileDialogType, Map<String, dynamic>> evt) async { BaseEvent<FileDialogType, Map<String, dynamic>> evt) async {
var event = evt as _FileDialogEvent; final event = evt as _FileDialogEvent;
final model = event.fileModel.target;
final jobId = int.tryParse(evt.data['id']?.toString() ?? '');
final batchId = model == null || jobId == null
? null
: model.jobController.transferConflictBatchId(jobId);
final keepRemembered = model != null &&
model.jobController.isTransferConflictRememberBatch(batchId);
// The loop only preloads the remembered batch choice. The model updates it
// after the user answers the current overwrite dialog.
if (_batchId != batchId && !keepRemembered) {
_batchId = batchId;
_overrideConfirm = null;
_skip = false;
} else {
_batchId = batchId;
}
if (keepRemembered) {
_overrideConfirm =
model.jobController.transferConflictRememberOverrideConfirm(batchId);
_skip = _overrideConfirm == null;
}
event.setOverrideConfirm(_overrideConfirm); event.setOverrideConfirm(_overrideConfirm);
event.setSkip(_skip); event.setSkip(_skip);
debugPrint( debugPrint(
"FileDialogEventLoop: consuming<jobId: ${evt.data['id']} overrideConfirm: $_overrideConfirm, skip: $_skip>"); "FileDialogEventLoop: consuming<jobId: ${evt.data['id']} batchId: $_batchId overrideConfirm: $_overrideConfirm, skip: $_skip>");
} }
@override @override
Future<void> onEventsClear() { Future<void> onEventsClear() {
_batchId = null;
_overrideConfirm = null; _overrideConfirm = null;
_skip = false; _skip = false;
return super.onEventsClear(); return super.onEventsClear();

View File

@@ -72,6 +72,48 @@ function getExt(name) {
class JobTable: Reactor.Component { class JobTable: Reactor.Component {
this var jobs = []; this var jobs = [];
this var job_map = {}; this var job_map = {};
this var next_conflict_batch_id = 1;
this var remembered_write_strategy = {};
function nextConflictBatchId() {
return this.next_conflict_batch_id++;
}
function getRememberedWriteStrategy(conflict_batch_id) {
var is_override = this.remembered_write_strategy[conflict_batch_id];
if (is_override == true || is_override == false) return is_override;
return null;
}
function rememberWriteStrategy(conflict_batch_id, is_override) {
this.remembered_write_strategy[conflict_batch_id] = is_override;
}
function cancelTransferJob(job) {
job.finished = true;
job.err = "cancel";
this.updateJob(job);
}
function cancelTransferConflictBatch(id) {
var job = this.job_map[id];
if (!job) return;
var conflict_batch_id = job.conflict_batch_id;
if (conflict_batch_id == null) {
this.cancelTransferJob(job);
handler.cancel_job(job.id);
refreshDir(!job.is_remote);
return;
}
delete this.remembered_write_strategy[conflict_batch_id];
for (var i = 0; i < this.jobs.length; ++i) {
var current_job = this.jobs[i];
if (current_job.conflict_batch_id != conflict_batch_id || current_job.finished) continue;
this.cancelTransferJob(current_job);
handler.cancel_job(current_job.id);
}
refreshDir(!job.is_remote);
}
function render() { function render() {
var me = this; var me = this;
@@ -109,10 +151,12 @@ class JobTable: Reactor.Component {
function clearAllJobs() { function clearAllJobs() {
this.jobs = []; this.jobs = [];
this.job_map = {}; this.job_map = {};
this.next_conflict_batch_id = 1;
this.remembered_write_strategy = {};
this.update(); this.update();
} }
function send(path, is_remote) { function send(path, is_remote, conflict_batch_id = null) {
var to; var to;
var show_hidden; var show_hidden;
if (is_remote) { if (is_remote) {
@@ -123,13 +167,15 @@ class JobTable: Reactor.Component {
show_hidden = file_transfer.local_folder_view.show_hidden; show_hidden = file_transfer.local_folder_view.show_hidden;
} }
if (!to) return; if (!to) return;
if (conflict_batch_id == null) conflict_batch_id = this.nextConflictBatchId();
to += handler.get_path_sep(!is_remote) + getFileName(is_remote, path); to += handler.get_path_sep(!is_remote) + getFileName(is_remote, path);
var id = handler.get_next_job_id(); var id = handler.get_next_job_id();
this.jobs.push({ type: "transfer", this.jobs.push({ type: "transfer",
id: id, path: path, to: to, id: id, path: path, to: to,
include_hidden: show_hidden, include_hidden: show_hidden,
is_remote: is_remote, is_remote: is_remote,
is_last: false is_last: false,
conflict_batch_id: conflict_batch_id
}); });
this.job_map[id] = this.jobs[this.jobs.length - 1]; this.job_map[id] = this.jobs[this.jobs.length - 1];
handler.send_files(id, 0, path, to, 0, show_hidden, is_remote); handler.send_files(id, 0, path, to, 0, show_hidden, is_remote);
@@ -141,7 +187,8 @@ class JobTable: Reactor.Component {
var job = { type: "transfer", var job = { type: "transfer",
id: id, path: path, to: to, id: id, path: path, to: to,
include_hidden: show_hidden, include_hidden: show_hidden,
is_remote: is_remote, is_last: true, file_num: file_num }; is_remote: is_remote, is_last: true, file_num: file_num,
conflict_batch_id: this.nextConflictBatchId() };
this.jobs.push(job); this.jobs.push(job);
this.job_map[id] = this.jobs[this.jobs.length - 1]; this.job_map[id] = this.jobs[this.jobs.length - 1];
handler.update_next_job_id(id + 1); handler.update_next_job_id(id + 1);
@@ -230,6 +277,7 @@ class JobTable: Reactor.Component {
else return translate("Waiting"); else return translate("Waiting");
} }
} }
if (job.err == "cancel") return translate("Cancel");
if (!job.entries) return translate("Waiting"); if (!job.entries) return translate("Waiting");
var i = job.file_num + 1; var i = job.file_num + 1;
var n = job.num_entries || job.entries.length; var n = job.num_entries || job.entries.length;
@@ -262,6 +310,8 @@ class JobTable: Reactor.Component {
function updateJobStatus(id, file_num = -1, err = null, speed = null, finished_size = 0) { function updateJobStatus(id, file_num = -1, err = null, speed = null, finished_size = 0) {
var job = this.job_map[id]; var job = this.job_map[id];
if (!job) return;
if (job.finished && job.err == "cancel") return;
if (job.type == "del-file"){ if (job.type == "del-file"){
job.finished = true; job.finished = true;
job.err = err; job.err = err;
@@ -269,7 +319,6 @@ class JobTable: Reactor.Component {
this.updateJob(job); this.updateJob(job);
return; return;
} }
if (!job) return;
if (file_num < job.file_num) return; if (file_num < job.file_num) return;
job.file_num = file_num; job.file_num = file_num;
var n = job.num_entries || job.entries.length; var n = job.num_entries || job.entries.length;
@@ -601,8 +650,9 @@ class FolderView : Reactor.Component {
event click $(.send) () { event click $(.send) () {
var rows = this.getCurrentRows(); var rows = this.getCurrentRows();
if (!rows || rows.length == 0) return; if (!rows || rows.length == 0) return;
var conflict_batch_id = file_transfer.job_table.nextConflictBatchId();
for (var i = 0; i < rows.length; ++i) { for (var i = 0; i < rows.length; ++i) {
file_transfer.job_table.send(rows[i][0], this.is_remote); file_transfer.job_table.send(rows[i][0], this.is_remote, conflict_batch_id);
} }
} }
@@ -780,6 +830,13 @@ handler.confirmDeleteFiles = function(id, i, name) {
handler.overrideFileConfirm = function(id, file_num, to, is_upload, is_identical) { handler.overrideFileConfirm = function(id, file_num, to, is_upload, is_identical) {
var jt = file_transfer.job_table; var jt = file_transfer.job_table;
var job = jt.job_map[id];
if (!job || job.finished) return;
var remembered = jt.getRememberedWriteStrategy(job.conflict_batch_id);
if (remembered == true || remembered == false) {
handler.set_write_override(id, file_num, remembered, true, is_upload);
return;
}
var identical_msg = is_identical ? translate("identical_file_tip"): ""; var identical_msg = is_identical ? translate("identical_file_tip"): "";
msgbox("custom-skip", "Confirm Write Strategy", "<div .form> \ msgbox("custom-skip", "Confirm Write Strategy", "<div .form> \
<div>" + translate('Overwrite') + " " + translate('files') + ".</div> \ <div>" + translate('Overwrite') + " " + translate('files') + ".</div> \
@@ -788,22 +845,18 @@ handler.overrideFileConfirm = function(id, file_num, to, is_upload, is_identical
<div>" + identical_msg + "</div> \ <div>" + identical_msg + "</div> \
<div><button|checkbox(remember) {ts}>" + translate('Do this for all conflicts') + "</button></div> \ <div><button|checkbox(remember) {ts}>" + translate('Do this for all conflicts') + "</button></div> \
</div>", "", function(res=null) { </div>", "", function(res=null) {
var current_job = jt.job_map[id];
if (!current_job || current_job.finished) return;
if (!res) { if (!res) {
jt.updateJobStatus(id, -1, "cancel"); jt.cancelTransferConflictBatch(id);
handler.cancel_job(id); return;
} else if (res.skip) {
if (res.remember){
handler.set_write_override(id,file_num,false,true, is_upload); //
} else {
handler.set_write_override(id,file_num,false,false,is_upload); //
}
} else {
if (res.remember){
handler.set_write_override(id,file_num,true,true,is_upload); //
} else {
handler.set_write_override(id,file_num,true,false,is_upload); //
}
} }
var is_override = !res.skip;
var remember = res.remember ? true : false;
if (remember) {
jt.rememberWriteStrategy(current_job.conflict_batch_id, is_override);
}
handler.set_write_override(id, file_num, is_override, remember, is_upload);
}); });
} }