Skip to content

Commit 4689f50

Browse files
committed
merge(transfer): GUI segmented downloads (GTC-0..GTC-2)
Wires intra-file range parallelism into the GUI command surface, matching the CLI pget path. Five commits: - bootstrap GUI<->CLI transfer parity harness (GTC-0) - ProviderDownloadExecutor segmented (GTC-1) - provider_download_file segmented (GTC-2) - read_range self-dial on SFTP/FTP cloned workers (clone-pool workers were unconnected so read_range failed; download() already self-dialled, lifted the same guard into read_range) - live-WAN integration test against axpbuntu (SFTP 2.52x, FTP 1.71x, S3 2.15x; byte-identical; .aerotmp cleanup verified) PD-SFTP-1 6/6 + PD-FTP-1 1/1 Docker regression green after the read_range fix. Frontend wiring is GTC-5 scope; downstream callers that pass download_segments=None see no behaviour change.
2 parents c21e74a + 4d05e88 commit 4689f50

14 files changed

Lines changed: 1996 additions & 17 deletions

src-tauri/src/bin/aeroftp_cli.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4963,6 +4963,9 @@ async fn run_shared_provider_download_batch(
49634963
max_concurrent: None,
49644964
retry_count: None,
49654965
timeout_seconds: None,
4966+
// CLI segmented downloads use the dedicated `pget` path, not
4967+
// the GUI provider executor, so the executor stays single-stream.
4968+
download_segments: None,
49664969
});
49674970

49684971
let entries: Vec<TransferEntry> = files
@@ -5164,6 +5167,9 @@ async fn run_shared_provider_upload_batch(
51645167
max_concurrent: None,
51655168
retry_count: None,
51665169
timeout_seconds: None,
5170+
// CLI segmented downloads use the dedicated `pget` path, not
5171+
// the GUI provider executor, so the executor stays single-stream.
5172+
download_segments: None,
51675173
});
51685174

51695175
let entries: Vec<TransferEntry> = files

src-tauri/src/lib.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3997,6 +3997,10 @@ async fn download_files_batch(
39973997
max_concurrent: params.max_concurrent,
39983998
retry_count: params.retry_count,
39993999
timeout_seconds: params.timeout_seconds,
4000+
// GTC-1: FTP GUI batch stays on `FtpDownloadExecutor`
4001+
// (no-double-pool invariant); the segments knob only
4002+
// matters on the `ProviderDownloadExecutor` path.
4003+
download_segments: None,
40004004
},
40014005
);
40024006

@@ -4225,6 +4229,10 @@ async fn upload_files_batch(
42254229
max_concurrent: params.max_concurrent,
42264230
retry_count: params.retry_count,
42274231
timeout_seconds: params.timeout_seconds,
4232+
// GTC-1: FTP GUI batch stays on `FtpDownloadExecutor`
4233+
// (no-double-pool invariant); the segments knob only
4234+
// matters on the `ProviderDownloadExecutor` path.
4235+
download_segments: None,
42284236
},
42294237
);
42304238

@@ -4750,6 +4758,10 @@ async fn download_folder(
47504758
max_concurrent: params.max_concurrent,
47514759
retry_count: params.retry_count,
47524760
timeout_seconds: params.timeout_seconds,
4761+
// GTC-1: FTP GUI batch stays on `FtpDownloadExecutor`
4762+
// (no-double-pool invariant); the segments knob only
4763+
// matters on the `ProviderDownloadExecutor` path.
4764+
download_segments: None,
47534765
},
47544766
);
47554767
info!(
@@ -5280,6 +5292,10 @@ async fn upload_folder(
52805292
max_concurrent: params.max_concurrent,
52815293
retry_count: params.retry_count,
52825294
timeout_seconds: params.timeout_seconds,
5295+
// GTC-1: FTP GUI batch stays on `FtpDownloadExecutor`
5296+
// (no-double-pool invariant); the segments knob only
5297+
// matters on the `ProviderDownloadExecutor` path.
5298+
download_segments: None,
52835299
},
52845300
);
52855301
info!(

src-tauri/src/provider_commands.rs

Lines changed: 122 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -906,6 +906,7 @@ pub async fn provider_pwd(state: State<'_, ProviderState>) -> Result<String, Str
906906
}
907907

908908
/// Download a file from the remote server
909+
#[allow(clippy::too_many_arguments)]
909910
#[tauri::command]
910911
pub async fn provider_download_file(
911912
app: AppHandle,
@@ -914,6 +915,7 @@ pub async fn provider_download_file(
914915
local_path: String,
915916
modified: Option<String>,
916917
use_delta: Option<bool>,
918+
download_segments: Option<u32>,
917919
) -> Result<String, String> {
918920
let mut provider_lock = state.provider.lock().await;
919921

@@ -960,7 +962,7 @@ pub async fn provider_download_file(
960962
let fname_progress = filename.clone();
961963

962964
let dl_start_time = std::time::Instant::now();
963-
let progress_cb: Option<Box<dyn Fn(u64, u64) + Send>> = if file_size > 0 {
965+
let mut progress_cb: Option<Box<dyn Fn(u64, u64) + Send>> = if file_size > 0 {
964966
Some(Box::new(move |transferred: u64, total: u64| {
965967
let pct = if total > 0 {
966968
((transferred as f64 / total as f64) * 100.0) as u8
@@ -1090,25 +1092,122 @@ pub async fn provider_download_file(
10901092
// Resume-aware download: if provider supports resume and a partial .aerotmp exists,
10911093
// use resume_download to continue from where we left off. This avoids re-downloading
10921094
// data on S3/Azure (pay-per-GB) and all other HTTP-based providers.
1093-
let result = if provider.supports_resume() {
1094-
let tmp_path = format!("{}.aerotmp", local_path);
1095-
let offset = tokio::fs::metadata(&tmp_path)
1095+
let tmp_path = format!("{}.aerotmp", local_path);
1096+
let partial_offset = if provider.supports_resume() {
1097+
tokio::fs::metadata(&tmp_path)
10961098
.await
10971099
.map(|m| m.len())
1098-
.unwrap_or(0);
1099-
if offset > 0 {
1100-
info!(
1101-
"Resuming download from offset {} bytes: {}",
1102-
offset, filename
1103-
);
1104-
provider
1105-
.resume_download(&remote_path, &local_path, offset, progress_cb)
1106-
.await
1107-
} else {
1108-
provider
1109-
.download(&remote_path, &local_path, progress_cb)
1110-
.await
1100+
.unwrap_or(0)
1101+
} else {
1102+
0
1103+
};
1104+
1105+
// GTC-2: opportunistic intra-file range parallelism on the single-file
1106+
// path. Gated on no-resume (`partial_offset == 0`) because the segmented
1107+
// engine pre-allocates and overwrites its own `.aerotmp`; a partial
1108+
// legacy resume must not be silently dropped. On hard failure we fall
1109+
// through to the legacy single-stream branch below.
1110+
let mut segmented_result: Option<Result<(), String>> = None;
1111+
if partial_offset == 0 {
1112+
if let Some(requested) = download_segments {
1113+
if let Some(segments) =
1114+
crate::provider_transfer_executor::provider_segmented_download_eligible(
1115+
provider.as_ref(),
1116+
file_size,
1117+
requested,
1118+
requested as usize,
1119+
)
1120+
{
1121+
info!(
1122+
"Segmented download: {} segments on {} ({} bytes)",
1123+
segments, filename, file_size
1124+
);
1125+
let cancel = tokio_util::sync::CancellationToken::new();
1126+
let outcome =
1127+
crate::provider_transfer_executor::run_provider_segmented_download(
1128+
provider.as_ref(),
1129+
&remote_path,
1130+
&local_path,
1131+
file_size,
1132+
segments,
1133+
progress_cb.take(),
1134+
cancel,
1135+
)
1136+
.await;
1137+
if let Err(ref e) = outcome {
1138+
warn!(
1139+
"Segmented download failed, falling back to single-stream: {}",
1140+
e
1141+
);
1142+
}
1143+
segmented_result = Some(outcome);
1144+
}
11111145
}
1146+
}
1147+
1148+
// If segmented ran (success or hard error) the original progress_cb has
1149+
// been moved into it. Build a fresh callback for the legacy fallback so
1150+
// the user still sees per-byte progress.
1151+
if segmented_result.is_some() && progress_cb.is_none() && file_size > 0 {
1152+
let app_progress_fb = app.clone();
1153+
let tid_fb = transfer_id.clone();
1154+
let fname_fb = filename.clone();
1155+
let start_fb = std::time::Instant::now();
1156+
progress_cb = Some(Box::new(move |transferred: u64, total: u64| {
1157+
let pct = if total > 0 {
1158+
((transferred as f64 / total as f64) * 100.0) as u8
1159+
} else {
1160+
0
1161+
};
1162+
let elapsed = start_fb.elapsed().as_secs_f64();
1163+
let speed = if elapsed > 0.1 {
1164+
(transferred as f64 / elapsed) as u64
1165+
} else {
1166+
0
1167+
};
1168+
let eta = if speed > 0 && transferred < total {
1169+
((total - transferred) as f64 / speed as f64) as u64
1170+
} else {
1171+
0
1172+
};
1173+
let _ = app_progress_fb.emit(
1174+
"transfer_event",
1175+
crate::TransferEvent {
1176+
event_type: "progress".to_string(),
1177+
transfer_id: tid_fb.clone(),
1178+
filename: fname_fb.clone(),
1179+
direction: "download".to_string(),
1180+
message: None,
1181+
progress: Some(crate::TransferProgress {
1182+
transfer_id: tid_fb.clone(),
1183+
filename: fname_fb.clone(),
1184+
direction: "download".to_string(),
1185+
percentage: pct,
1186+
transferred,
1187+
total,
1188+
speed_bps: speed,
1189+
eta_seconds: eta as u32,
1190+
total_files: None,
1191+
path: None,
1192+
}),
1193+
path: None,
1194+
delta_stats: None,
1195+
fallback_reason: None,
1196+
},
1197+
);
1198+
}));
1199+
}
1200+
1201+
let result = if let Some(Ok(())) = &segmented_result {
1202+
Ok(())
1203+
} else if partial_offset > 0 {
1204+
info!(
1205+
"Resuming download from offset {} bytes: {}",
1206+
partial_offset, filename
1207+
);
1208+
provider
1209+
.resume_download(&remote_path, &local_path, partial_offset, progress_cb)
1210+
.await
11121211
} else {
11131212
provider
11141213
.download(&remote_path, &local_path, progress_cb)
@@ -1179,11 +1278,13 @@ pub async fn provider_download_folder(
11791278
max_concurrent: Option<u32>,
11801279
retry_count: Option<u32>,
11811280
timeout_seconds: Option<u64>,
1281+
download_segments: Option<u32>,
11821282
) -> Result<String, String> {
11831283
let runtime_settings = resolve_provider_transfer_settings(TransferSettingsInput {
11841284
max_concurrent,
11851285
retry_count,
11861286
timeout_seconds,
1287+
download_segments,
11871288
});
11881289

11891290
// Capture current pwd so we can restore it after folder scan changes it
@@ -1236,6 +1337,10 @@ pub async fn provider_upload_folder(
12361337
max_concurrent,
12371338
retry_count,
12381339
timeout_seconds,
1340+
// Upload-side intra-file parallelism is a separate slice (out
1341+
// of scope for GTC-1); upload paths keep single-stream legacy
1342+
// behaviour regardless of the requested segments knob.
1343+
download_segments: None,
12391344
});
12401345

12411346
// Capture current pwd so we can restore it after upload

0 commit comments

Comments
 (0)