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

View File

@@ -72,6 +72,48 @@ function getExt(name) {
class JobTable: Reactor.Component {
this var jobs = [];
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() {
var me = this;
@@ -109,10 +151,12 @@ class JobTable: Reactor.Component {
function clearAllJobs() {
this.jobs = [];
this.job_map = {};
this.next_conflict_batch_id = 1;
this.remembered_write_strategy = {};
this.update();
}
function send(path, is_remote) {
function send(path, is_remote, conflict_batch_id = null) {
var to;
var show_hidden;
if (is_remote) {
@@ -123,13 +167,15 @@ class JobTable: Reactor.Component {
show_hidden = file_transfer.local_folder_view.show_hidden;
}
if (!to) return;
if (conflict_batch_id == null) conflict_batch_id = this.nextConflictBatchId();
to += handler.get_path_sep(!is_remote) + getFileName(is_remote, path);
var id = handler.get_next_job_id();
this.jobs.push({ type: "transfer",
id: id, path: path, to: to,
include_hidden: show_hidden,
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];
handler.send_files(id, 0, path, to, 0, show_hidden, is_remote);
@@ -141,7 +187,8 @@ class JobTable: Reactor.Component {
var job = { type: "transfer",
id: id, path: path, to: to,
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.job_map[id] = this.jobs[this.jobs.length - 1];
handler.update_next_job_id(id + 1);
@@ -230,6 +277,7 @@ class JobTable: Reactor.Component {
else return translate("Waiting");
}
}
if (job.err == "cancel") return translate("Cancel");
if (!job.entries) return translate("Waiting");
var i = job.file_num + 1;
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) {
var job = this.job_map[id];
if (!job) return;
if (job.finished && job.err == "cancel") return;
if (job.type == "del-file"){
job.finished = true;
job.err = err;
@@ -269,7 +319,6 @@ class JobTable: Reactor.Component {
this.updateJob(job);
return;
}
if (!job) return;
if (file_num < job.file_num) return;
job.file_num = file_num;
var n = job.num_entries || job.entries.length;
@@ -601,8 +650,9 @@ class FolderView : Reactor.Component {
event click $(.send) () {
var rows = this.getCurrentRows();
if (!rows || rows.length == 0) return;
var conflict_batch_id = file_transfer.job_table.nextConflictBatchId();
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) {
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"): "";
msgbox("custom-skip", "Confirm Write Strategy", "<div .form> \
<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><button|checkbox(remember) {ts}>" + translate('Do this for all conflicts') + "</button></div> \
</div>", "", function(res=null) {
var current_job = jt.job_map[id];
if (!current_job || current_job.finished) return;
if (!res) {
jt.updateJobStatus(id, -1, "cancel");
handler.cancel_job(id);
} 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); //
}
jt.cancelTransferConflictBatch(id);
return;
}
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);
});
}