Skip to content

Commit 4322ca8

Browse files
axpnetaeroftp[bot]claude
committed
feat(transfer): expose GUI download segments knob
GTC-5 wires the user-facing intra-file range parallelism preset into the GUI download path. Settings > Transfers gains a new "Download Segments per File" select (Auto / 2 / 4 / 8 / 16) right after Concurrent Transfers, persisted in the app_settings vault entry under `downloadSegments` (default 0 = Auto). The resolved value is forwarded to `provider_download_file` and `provider_download_folder` via the existing `download_segments: Option<u32>` parameter that the backend already accepts (see provider_commands.rs:911 / :1281 / :1112 eligibility gate). Auto maps to `undefined` on the frontend, which the Rust handler treats as the legacy single-stream behaviour. Scope: - IN: provider_download_file, provider_download_folder (provider transport path: SFTP, S3, HTTP-based cloud). - OUT: rclone_crypt_provider_download_file / _folder. The crypt overlay reads the full ciphertext via download_to_bytes before decrypting in memory; it does not use provider.download() streaming, so the segmented helper would require a separate refactor (segmented download to .aerotmp, then streaming decrypt). Intentionally deferred for a later slice. - OUT: download_files_batch (FTP GUI batch on FtpDownloadExecutor) stays `download_segments: None` per the no-double-pool invariant already documented in lib.rs:4003 and the three sibling OAuth fallback sites. Translations: 4 new keys (settings.downloadSegments, downloadSegmentsAuto, downloadSegmentsN, downloadSegmentsDesc) in all 47 locales, npm run i18n:validate clean (0 warnings, 0 [NEEDS TRANSLATION]). `{n}` placeholder preserved literally; Armenian uses native Mesropian script. Validation: - npm run typecheck: green. - cargo check: green (no Rust changes). - Live SFTP @ axpbuntu, 64 MiB: seg=1 (Auto) 34.05s vs seg=4 11.41s, speedup 2.98x, byte-identical YES (cargo test --test integration_gtc_wan_segmented gtc_gui_sftp_segmented_byte_identical_vs_single_stream -- --ignored --nocapture --test-threads=1). Co-Authored-By: aeroftp[bot] <aeroftp[bot]@users.noreply.github.com> Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 8e5460b commit 4322ca8

50 files changed

Lines changed: 271 additions & 47 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/App.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,7 @@ const App: React.FC = () => {
371371
compactMode, showHiddenFiles, showToastNotifications, confirmBeforeDelete,
372372
showStatusBar, defaultLocalPath, fontSize, fontFamily, doubleClickAction, rememberLastFolder, visibleColumns,
373373
sortFoldersFirst, showFileExtensions, timeoutSeconds, maxConcurrentTransfers, retryCount,
374+
downloadSegments,
374375
fileExistsAction, swapPanels,
375376
showActivityLog, showConnectionScreen,
376377
showSettingsPanel, setShowSettingsPanel, setShowConnectionScreen,
@@ -5811,6 +5812,9 @@ interface UpdateVerificationInfo {
58115812
maxConcurrent: effectiveMaxConcurrentTransfers,
58125813
retryCount,
58135814
timeoutSeconds,
5815+
// GTC-5: intra-file range parallelism per download.
5816+
// 0 = Auto (backend = legacy single-stream).
5817+
downloadSegments: downloadSegments > 0 ? downloadSegments : undefined,
58145818
});
58155819
// pwd is restored by the backend (provider_download_folder saves/restores pwd)
58165820
} else {
@@ -5875,7 +5879,14 @@ interface UpdateVerificationInfo {
58755879
});
58765880
} else if (isProvider) {
58775881
// Use provider command for file download
5878-
await invoke('provider_download_file', { remotePath: remoteFilePath, localPath: localFilePath, modified: remoteModified });
5882+
await invoke('provider_download_file', {
5883+
remotePath: remoteFilePath,
5884+
localPath: localFilePath,
5885+
modified: remoteModified,
5886+
// GTC-5: intra-file range parallelism per download.
5887+
// 0 = Auto (backend = legacy single-stream).
5888+
downloadSegments: downloadSegments > 0 ? downloadSegments : undefined,
5889+
});
58795890
} else {
58805891
const params: DownloadParams = { remote_path: remoteFilePath, local_path: localFilePath, modified: remoteModified };
58815892
await invoke('download_file', { params });

src/components/SettingsPanel.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,8 @@ interface AppSettings {
213213
// Transfers
214214
maxConcurrentTransfers: number;
215215
retryCount: number;
216+
/** Intra-file range parallelism per download. 0 = Auto. */
217+
downloadSegments: number;
216218
// File Handling
217219
fileExistsAction: 'ask' | 'overwrite' | 'skip' | 'rename' | 'resume' | 'overwrite_if_newer' | 'overwrite_if_different' | 'skip_if_identical';
218220
preserveTimestamps: boolean;
@@ -256,6 +258,7 @@ const defaultSettings: AppSettings = {
256258
ftpMode: 'passive',
257259
maxConcurrentTransfers: 5,
258260
retryCount: 3,
261+
downloadSegments: 0,
259262
fileExistsAction: 'ask',
260263
preserveTimestamps: true,
261264
transferMode: 'auto',
@@ -2831,6 +2834,18 @@ export const SettingsPanel: React.FC<SettingsPanelProps> = ({ isOpen, onClose, o
28312834
</select>
28322835
</div>
28332836

2837+
<div>
2838+
<label className="block text-sm font-medium mb-1">{t('settings.downloadSegments')}</label>
2839+
<select value={settings.downloadSegments} onChange={(e) => updateSetting('downloadSegments', parseInt(e.target.value))} className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 text-sm">
2840+
{[0, 2, 4, 8, 16].map((n) => (
2841+
<option key={n} value={n}>
2842+
{n === 0 ? t('settings.downloadSegmentsAuto') : t('settings.downloadSegmentsN', { n })}
2843+
</option>
2844+
))}
2845+
</select>
2846+
<p className="text-xs text-gray-500 mt-1">{t('settings.downloadSegmentsDesc')}</p>
2847+
</div>
2848+
28342849
<div>
28352850
<label className="block text-sm font-medium mb-1">{t('settings.retryCountOnError')}</label>
28362851
<select value={settings.retryCount} onChange={(e) => updateSetting('retryCount', parseInt(e.target.value))} className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 text-sm">

src/hooks/useSettings.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,9 @@ export interface AppSettings {
8080
timeoutSeconds: number;
8181
maxConcurrentTransfers: number;
8282
retryCount: number;
83+
/** Intra-file range parallelism per download. 0 = Auto (backend decides,
84+
* today: single-stream). Supported values: 0, 2, 4, 8, 16. */
85+
downloadSegments: number;
8386
fileExistsAction: 'ask' | 'overwrite' | 'skip' | 'rename' | 'resume' | 'overwrite_if_newer' | 'overwrite_if_different' | 'skip_if_identical';
8487
swapPanels: boolean;
8588
lastLocalPath?: string;
@@ -116,6 +119,7 @@ const DEFAULTS: AppSettings = {
116119
timeoutSeconds: 30,
117120
maxConcurrentTransfers: 5,
118121
retryCount: 3,
122+
downloadSegments: 0,
119123
fileExistsAction: 'ask',
120124
swapPanels: false,
121125
disableUpdateChecks: false,
@@ -145,6 +149,7 @@ export const useSettings = () => {
145149
const [timeoutSeconds, setTimeoutSeconds] = useState(DEFAULTS.timeoutSeconds);
146150
const [maxConcurrentTransfers, setMaxConcurrentTransfers] = useState(DEFAULTS.maxConcurrentTransfers);
147151
const [retryCount, setRetryCount] = useState(DEFAULTS.retryCount);
152+
const [downloadSegments, setDownloadSegments] = useState(DEFAULTS.downloadSegments);
148153
const [fileExistsAction, setFileExistsAction] = useState<AppSettings['fileExistsAction']>(DEFAULTS.fileExistsAction);
149154
const [swapPanels, setSwapPanels] = useState(DEFAULTS.swapPanels);
150155
const [disableUpdateChecks, setDisableUpdateChecks] = useState(DEFAULTS.disableUpdateChecks);
@@ -174,6 +179,9 @@ export const useSettings = () => {
174179
if (typeof parsed.timeoutSeconds === 'number') setTimeoutSeconds(parsed.timeoutSeconds);
175180
if (typeof parsed.maxConcurrentTransfers === 'number') setMaxConcurrentTransfers(parsed.maxConcurrentTransfers);
176181
if (typeof parsed.retryCount === 'number') setRetryCount(parsed.retryCount);
182+
if (typeof parsed.downloadSegments === 'number' && [0, 2, 4, 8, 16].includes(parsed.downloadSegments)) {
183+
setDownloadSegments(parsed.downloadSegments);
184+
}
177185
if (
178186
typeof parsed.fileExistsAction === 'string' &&
179187
['ask', 'overwrite', 'skip', 'rename', 'resume', 'overwrite_if_newer', 'overwrite_if_different', 'skip_if_identical'].includes(parsed.fileExistsAction)
@@ -269,6 +277,7 @@ export const useSettings = () => {
269277
timeoutSeconds,
270278
maxConcurrentTransfers,
271279
retryCount,
280+
downloadSegments,
272281
fileExistsAction,
273282
swapPanels,
274283
disableUpdateChecks,
@@ -298,6 +307,7 @@ export const useSettings = () => {
298307
setTimeoutSeconds,
299308
setMaxConcurrentTransfers,
300309
setRetryCount,
310+
setDownloadSegments,
301311
setFileExistsAction,
302312
setSwapPanels,
303313
setDisableUpdateChecks,

src/i18n/locales/bg.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -998,7 +998,11 @@
998998
"bridgeUnsupportedProto": "{protocol} не се поддържа от {app}",
999999
"bridgeSecretFull": "Идентификационните данни се възстановяват и повторно се шифроват в хранилището на AeroFTP.",
10001000
"bridgeSecretLimited": "Само част от идентификационните данни може да бъде възстановена; може да се наложи да въведете отново някои тайни.",
1001-
"bridgeSecretMetadata": "Само метаданни за връзката: тайните остават в ключодържателя/агента на {app} и трябва да бъдат въведени отново."
1001+
"bridgeSecretMetadata": "Само метаданни за връзката: тайните остават в ключодържателя/агента на {app} и трябва да бъдат въведени отново.",
1002+
"downloadSegments": "Сегменти за изтегляне на файл",
1003+
"downloadSegmentsAuto": "Auto (единичен поток)",
1004+
"downloadSegmentsN": "{n} сегмента на файл",
1005+
"downloadSegmentsDesc": "Разделя всяко изтегляне на паралелни байтови диапазони. Auto запазва текущото поведение с единичен поток. Действа при доставчици, които поддържат range заявки (SFTP, S3, HTTP) и при файлове, достатъчно големи, за да има полза."
10021006
},
10031007
"devtools": {
10041008
"title": "AeroTools",

src/i18n/locales/bn.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -998,7 +998,11 @@
998998
"bridgeUnsupportedProto": "{protocol} {app} দ্বারা সমর্থিত নয়",
999999
"bridgeSecretFull": "শংসাপত্রগুলো পুনরুদ্ধার করে AeroFTP ভল্টে পুনরায় এনক্রিপ্ট করা হয়।",
10001000
"bridgeSecretLimited": "শংসাপত্রের শুধুমাত্র একটি অংশ পুনরুদ্ধার করা যাবে; আপনাকে কিছু গোপন তথ্য পুনরায় লিখতে হতে পারে।",
1001-
"bridgeSecretMetadata": "শুধুমাত্র সংযোগ মেটাডেটা: গোপন তথ্য {app}-এর কীচেইন/এজেন্টে থেকে যায় এবং পুনরায় লিখতে হবে।"
1001+
"bridgeSecretMetadata": "শুধুমাত্র সংযোগ মেটাডেটা: গোপন তথ্য {app}-এর কীচেইন/এজেন্টে থেকে যায় এবং পুনরায় লিখতে হবে।",
1002+
"downloadSegments": "প্রতি ফাইলে ডাউনলোড সেগমেন্ট",
1003+
"downloadSegmentsAuto": "Auto (একক স্ট্রিম)",
1004+
"downloadSegmentsN": "প্রতি ফাইলে {n} সেগমেন্ট",
1005+
"downloadSegmentsDesc": "প্রতিটি ডাউনলোডকে সমান্তরাল বাইট রেঞ্জে ভাগ করুন। Auto বর্তমান একক-স্ট্রিম আচরণ বজায় রাখে। যেসব প্রোভাইডার রেঞ্জ রিকোয়েস্ট সমর্থন করে (SFTP, S3, HTTP) এবং ফাইল যথেষ্ট বড় হলে কার্যকর।"
10021006
},
10031007
"devtools": {
10041008
"title": "AeroTools",

src/i18n/locales/ca.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -998,7 +998,11 @@
998998
"bridgeUnsupportedProto": "{protocol} no és compatible amb {app}",
999999
"bridgeSecretFull": "Les credencials es recuperen i es tornen a xifrar al vault d'AeroFTP.",
10001000
"bridgeSecretLimited": "Només es pot recuperar una part de les credencials; pot ser que hagis de tornar a introduir alguns secrets.",
1001-
"bridgeSecretMetadata": "Només metadades de connexió: els secrets es queden al clauer/agent de {app} i s'han de tornar a introduir."
1001+
"bridgeSecretMetadata": "Només metadades de connexió: els secrets es queden al clauer/agent de {app} i s'han de tornar a introduir.",
1002+
"downloadSegments": "Segments de descàrrega per fitxer",
1003+
"downloadSegmentsAuto": "Auto (flux únic)",
1004+
"downloadSegmentsN": "{n} segments per fitxer",
1005+
"downloadSegmentsDesc": "Divideix cada descàrrega en intervals de bytes paral·lels. Auto manté el comportament actual de flux únic. Efectiu en proveïdors compatibles amb sol·licituds de rang (SFTP, S3, HTTP) per a fitxers prou grans com per beneficiar-se'n."
10021006
},
10031007
"devtools": {
10041008
"title": "AeroTools",

src/i18n/locales/cs.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -998,7 +998,11 @@
998998
"bridgeUnsupportedProto": "{protocol} není podporován v {app}",
999999
"bridgeSecretFull": "Přihlašovací údaje se obnoví a znovu zašifrují do trezoru AeroFTP.",
10001000
"bridgeSecretLimited": "Lze obnovit jen část přihlašovacích údajů; některá tajemství bude možná nutné zadat znovu.",
1001-
"bridgeSecretMetadata": "Pouze metadata připojení: tajemství zůstávají v klíčence/agentovi {app} a musí být zadána znovu."
1001+
"bridgeSecretMetadata": "Pouze metadata připojení: tajemství zůstávají v klíčence/agentovi {app} a musí být zadána znovu.",
1002+
"downloadSegments": "Segmenty stahování na soubor",
1003+
"downloadSegmentsAuto": "Auto (jeden proud)",
1004+
"downloadSegmentsN": "{n} segmentů na soubor",
1005+
"downloadSegmentsDesc": "Rozdělí každé stahování na paralelní bajtové rozsahy. Auto zachovává současné chování s jedním proudem. Funguje u poskytovatelů, kteří podporují požadavky na rozsah (SFTP, S3, HTTP), a u souborů dostatečně velkých, aby z toho měly užitek."
10021006
},
10031007
"devtools": {
10041008
"title": "AeroTools",

src/i18n/locales/cy.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -998,7 +998,11 @@
998998
"bridgeUnsupportedProto": "Nid yw {app} yn cefnogi {protocol}",
999999
"bridgeSecretFull": "Caiff y manylion adnabod eu hadfer a'u hailamgryptio i mewn i gladd AeroFTP.",
10001000
"bridgeSecretLimited": "Dim ond rhan o'r manylion adnabod y gellir ei hadfer; efallai y bydd angen i chi ailosod rhai cyfrinachau.",
1001-
"bridgeSecretMetadata": "Metadata cysylltiad yn unig: mae'r cyfrinachau'n aros yng nghadwyn allweddi/asiant {app} a rhaid eu hailosod."
1001+
"bridgeSecretMetadata": "Metadata cysylltiad yn unig: mae'r cyfrinachau'n aros yng nghadwyn allweddi/asiant {app} a rhaid eu hailosod.",
1002+
"downloadSegments": "Segmentau lawrlwytho fesul ffeil",
1003+
"downloadSegmentsAuto": "Auto (un ffrwd)",
1004+
"downloadSegmentsN": "{n} segment fesul ffeil",
1005+
"downloadSegmentsDesc": "Rhannu pob lawrlwythiad yn ystodau beit cyfochrog. Mae Auto yn cadw'r ymddygiad un ffrwd presennol. Effeithiol gyda darparwyr sy'n cefnogi ceisiadau ystod (SFTP, S3, HTTP) ar gyfer ffeiliau digon mawr i elwa."
10021006
},
10031007
"devtools": {
10041008
"title": "AeroTools",

src/i18n/locales/da.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -998,7 +998,11 @@
998998
"bridgeUnsupportedProto": "{protocol} understøttes ikke af {app}",
999999
"bridgeSecretFull": "Loginoplysningerne gendannes og krypteres på ny i AeroFTP-vaulten.",
10001000
"bridgeSecretLimited": "Kun en del af loginoplysningerne kan gendannes; du skal muligvis indtaste nogle hemmeligheder igen.",
1001-
"bridgeSecretMetadata": "Kun forbindelsesmetadata: hemmelighederne forbliver i {app}'s nøglering/agent og skal indtastes igen."
1001+
"bridgeSecretMetadata": "Kun forbindelsesmetadata: hemmelighederne forbliver i {app}'s nøglering/agent og skal indtastes igen.",
1002+
"downloadSegments": "Download-segmenter pr. fil",
1003+
"downloadSegmentsAuto": "Auto (enkelt stream)",
1004+
"downloadSegmentsN": "{n} segmenter pr. fil",
1005+
"downloadSegmentsDesc": "Opdel hver download i parallelle byte-intervaller. Auto bevarer den nuværende adfærd med enkelt stream. Virker hos udbydere, der understøtter range-anmodninger (SFTP, S3, HTTP), for filer der er store nok til at have gavn af det."
10021006
},
10031007
"devtools": {
10041008
"title": "AeroTools",

src/i18n/locales/de.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -998,7 +998,11 @@
998998
"bridgeUnsupportedProto": "{protocol} wird von {app} nicht unterstützt",
999999
"bridgeSecretFull": "Die Zugangsdaten werden wiederhergestellt und erneut in den AeroFTP-Vault verschlüsselt.",
10001000
"bridgeSecretLimited": "Nur ein Teil der Zugangsdaten kann wiederhergestellt werden; einige Geheimnisse müssen Sie möglicherweise erneut eingeben.",
1001-
"bridgeSecretMetadata": "Nur Verbindungsmetadaten: Die Geheimnisse bleiben im Schlüsselbund/Agenten von {app} und müssen erneut eingegeben werden."
1001+
"bridgeSecretMetadata": "Nur Verbindungsmetadaten: Die Geheimnisse bleiben im Schlüsselbund/Agenten von {app} und müssen erneut eingegeben werden.",
1002+
"downloadSegments": "Download-Segmente pro Datei",
1003+
"downloadSegmentsAuto": "Auto (einzelner Stream)",
1004+
"downloadSegmentsN": "{n} Segmente pro Datei",
1005+
"downloadSegmentsDesc": "Teilt jeden Download in parallele Byte-Bereiche auf. Auto behält das bisherige Verhalten mit einem einzelnen Stream bei. Wirksam bei Anbietern, die Range-Anfragen unterstützen (SFTP, S3, HTTP), und bei Dateien, die groß genug sind, um davon zu profitieren."
10021006
},
10031007
"devtools": {
10041008
"title": "AeroTools",

0 commit comments

Comments
 (0)