[3.6.5] - 2026-04-26
CLI polish + cross-provider correctness sweep
Drop-in patch driven by an end-to-end stress test of aeroftp-cli 3.6.4 against real provider accounts (S3, Backblaze, Storj, FTPS, Dropbox, Google Drive, OneDrive, Box, Koofr, Zoho WorkDrive, kDrive, Drime). The session surfaced four recurring correctness defects that span 8 providers, plus a dozen ergonomics rough edges that became visible only when the CLI is driven by an external AI agent rather than a human typing one command at a time. Fixes are surface-level and contained to the CLI binary plus per-provider trait impls; no architectural change.
Fixed
- FTP CLI now respects the server-provided home directory -
aeroftp-cliwas issuing a hardCWD /after login, which overrode the home directory negotiated by the FTP server (typically/home/useron non-chroot installations like default vsftpd) and landed the session at the filesystem root, which is usually non-writable. This surfaced in a head-to-head benchmark vs rclone where the sameput rel/path/file.txtworked under rclone but produced550 Permission deniedhere. Three coordinated fixes: the FTP provider skips the post-login CWD wheninitial_pathis bare/(aligning with rclone, lftp, FileZilla, ftp(1) and curl which all defer to PWD post-login); the CLI path resolver now returns empty for empty user input so the provider's canonical default kicks in instead of an artificial absolute root; andmkdir -ppreserves relative-vs-absolute semantics instead of always prefixing/, somkdir -p rel/pathissuesMKD rel/MKD rel/path(which the server can satisfy under the user's home) instead ofMKD /rel/path(which non-chroot servers reject for the same write-permission reason). Absolute paths like/etccontinue to target the filesystem root verbatim. Validated end-to-end against vsftpd in a Docker harness, no regression on the existing chroot case. - OAuth saved servers can now be renamed from the Edit dialog (issue #127) - In My Servers, hovering over an OAuth tile (Google Drive, Dropbox, OneDrive, Box, pCloud, Zoho WorkDrive, Yandex Disk, 4shared) and clicking the Edit pen now opens a form whose display name is editable, matching the behaviour of WebDAV / E2E / API providers. Two root causes: the
OAuthConnect"Active" branch (rendered when tokens already exist for the provider) skipped the Save toggle and the Connection Name input entirely, so there was no field to type into. And the OAuth save callback inConnectionScreensearched for the existing profile byname === saveName, so the moment the user changed the name the lookup failed and a brand-new profile was created next to the original instead of renaming it. Fix: render the Save toggle + name input in the Active branch (gated on the samewantToSaveflag the inactive branch uses), and prefer the expliciteditingProfileIdover the name-match heuristic when persisting an OAuth edit. OAuth-specific fields (clientId, clientSecret, scope, region) stay locked - renaming a saved server is purely a local label change. findglob matched as substring across 7 providers —aeroftp-cli find /path "*.txt"was returningreport.TXT.rtfand any file whose name contained the literaltxt, because the per-providerfind()implementations forwarded the pattern straight to the upstream search API (which is substring-by-name on most clouds) without re-applying glob semantics on the response. Affected: Dropbox, OneDrive, Box, Koofr, Zoho WorkDrive, Drime, kDrive. Fix: server-side query is now broad-prefiltered (glob characters stripped, only the literal portion sent), then the response is re-filtered client-side via the sharedmatches_find_patternhelper that powers the rest of the CLI. Google Drive (already filtered against its own catalog) gets the same belt-and-braces second pass.mkdir+putfailing on empty-prefix object stores —aeroftp-cli put file.bin s3://bucket/keyreturnedexit=2 "Parent does not exist"on a fresh bucket because the put plumbing was issuing a separatemkdircall before the upload, which is a hard error on S3 / Azure / Backblaze (no real directory primitive). Fix: skip the parent-directory pre-flight when the provider declaresis_object_store(). Validated end-to-end on Backblaze and Storj.lsof a missing FTPS path returnedexit=0with(empty directory)— the unhappy path was indistinguishable from an actually empty directory, so scripts piping the output couldn't tell. Fix: returnexit=2 "Path not found"when the upstreamLISTrejects the resolved path. The empty-directory case (path exists, no entries) still exits0.- Dropbox
getof a missing file took 3.5s + 3 retries before failing — thepath/lookup/not_foundJSON error was being classified as transient and retried, instead of as a permanent client error. Fix: detectpath/not_foundupstream and returnexit=2immediately. Drops the failure latency from ~3.5 s to ~150 ms. - Koofr WebDAV default Remote Path was
/dav/Koofr/(issue #126) — the discovery preset pairedserver: https://app.koofr.net/dav/KoofrwithbasePath: /dav/Koofr/, so the joined request URL doubled to/dav/Koofr/dav/Koofr/..., which Koofr rejected withInvalid credentials. Fix: defaultbasePathis now/. Existing saved profiles need to be edited once.
Added
- Inline rename for saved servers (issue #127) - In addition to unlocking rename via the Edit dialog, My Servers tiles now support a faster path: right-click → "Rename (F2)" turns the tile name into an editable input with a green check / grey X for confirm / cancel, plus an F2 hotkey that renames the currently hovered tile. Enter and blur commit, Escape cancels. Works in both grid and list view, and applies to every provider class (OAuth included). The right-click variant lives next to the existing Edit voice in the context menu.
- Protocol class label on My Servers tiles (issue #127 bonus) - Tiles now show a second small chip next to the protocol brand badge:
OAuth(indigo),API(sky),WebDAV(purple),E2E(emerald),S3(orange),Azure(blue),AeroCloud(cyan). FTP / FTPS / SFTP keep only the existing protocol badge to avoid duplication. The new label makes the WebDAV-Koofr vs API-Koofr distinction (and similar pairs across cloud providers) visible at a glance, parity with the Discover Services page.
Changed
- Throughput parity with rclone on FTP / SFTP / WebDAV - Head-to-head profiling on Docker loopback exposed three avoidable bottlenecks in the upload path. (1) The plain-FTP uploader was paying a 100ms-2s "TLS drain" sleep before
finalize_put_streamon every transfer; the sleep is a real workaround for a TLS/TCPclose_notifyrace but has no effect on plain FTP. Gating it behindtls_activecuts a single-100MB upload on Docker FTP from 2.74s to 0.72s (37 MB/s -> 138 MB/s, -74%) and a 500x10K bulk-of-small-files run from 14.68s to 2.72s (-82%, now ahead of rclone with--transfers 4). (2) The SFTP default buffer was 32 KiB - the conservative protocol minimum - which caps loopback throughput at ~35 MB/s. Raised to 256 KiB (OpenSSH 8+ and russh-sftp negotiate larger packets in practice anyway, and 1 MiB only adds another ~5 MB/s on top). 100MB Docker SFTP: 3.71s -> 2.37s (-36%).--chunk-size/--buffer-sizestill override. (3) The WebDAV streaming PUT body was built with the defaultReaderStreamcapacity of 8 KiB, churning syscalls. Bumped to 256 KiB. 100MB Docker WebDAV: 1.26s -> 0.84s (-33%). No protocol-level changes; tested in CI with the existing FTP/SFTP/WebDAV smoke jobs. aeroftp-clipolish pass for the AI-agent demo path:pgetis now a real subcommand (alias forget --segments 4); the banner cell stops being a lie.- Banner protocol count and the
connect"Unsupported protocol" error now derive from a singleSUPPORTED_URL_SCHEMESconstant, so they cannot drift apart again. - Subcommand help screens no longer reprint the ASCII banner. Banner is suppressed when stderr isn't a TTY, when
AEROFTP_NO_BANNERis set, or when--no-banneris on the command line. Top-level--helpstill shows it once. - 11 connection globals (
--bucket,--region,--container,--token,--tls,--key,--key-passphrase,--password-stdin,--insecure,--trust-host-key,--two-factor) move under aConnection optionsheading so per-subcommand help is no longer 30 random flags. 6 output flags move underOutput options. - 39 sentinel
default_value = "_"arguments now carryhide_default_value = trueso subcommand help stops rendering[default: _]for every URL/path positional. cleanupdescription rewritten to a single accurate sentence (was a confusing two-line run-on suggesting it handled duplicates, which it doesn't — that lives indedupe).dedupegained the missing description.tree --depthhelp no longer leaks the internal "TypeId mismatch" implementation note.Invalid URLerror now suggests--profile <name>when the input has no scheme, instead of leaking the rawurl::ParseErrormessage.
Skipped — flagged for follow-up
--excluderename:syncalready has a local--exclude(with-eshort) whoseVec<String>semantics differ from the global--exclude-global; unifying them would force a refactor of the per-command merge loop. Out of scope for a polish pass; tracked for a dedicated PR with proper test coverage.
Downloads:
- Windows:
.msiinstaller,.exe, or.zipportable (no installation required) - macOS:
.dmgdisk image - Linux:
.deb,.rpm,.snap, or.AppImage