mfatsftp 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mfatsftp-0.3.0/.gitignore +17 -0
- mfatsftp-0.3.0/CHANGELOG.md +138 -0
- mfatsftp-0.3.0/CONTRIBUTING.md +75 -0
- mfatsftp-0.3.0/LICENSE +675 -0
- mfatsftp-0.3.0/PKG-INFO +420 -0
- mfatsftp-0.3.0/README.md +382 -0
- mfatsftp-0.3.0/SECURITY.md +39 -0
- mfatsftp-0.3.0/docs/api.md +379 -0
- mfatsftp-0.3.0/docs/changelog.md +2 -0
- mfatsftp-0.3.0/docs/conf.py +38 -0
- mfatsftp-0.3.0/docs/contributing.md +3 -0
- mfatsftp-0.3.0/docs/guide.md +4 -0
- mfatsftp-0.3.0/docs/index.md +14 -0
- mfatsftp-0.3.0/docs/reference.rst +45 -0
- mfatsftp-0.3.0/docs/requirements.txt +3 -0
- mfatsftp-0.3.0/docs/security.md +2 -0
- mfatsftp-0.3.0/examples/list_remote.py +29 -0
- mfatsftp-0.3.0/examples/transfer.py +54 -0
- mfatsftp-0.3.0/pyproject.toml +70 -0
- mfatsftp-0.3.0/src/mfatsftp/__init__.py +54 -0
- mfatsftp-0.3.0/src/mfatsftp/client.py +2614 -0
- mfatsftp-0.3.0/src/mfatsftp/protocol.py +473 -0
- mfatsftp-0.3.0/src/mfatsftp/py.typed +0 -0
- mfatsftp-0.3.0/src/mfatsftp/transport.py +270 -0
- mfatsftp-0.3.0/tests/__init__.py +0 -0
- mfatsftp-0.3.0/tests/conftest.py +15 -0
- mfatsftp-0.3.0/tests/fake_server.py +222 -0
- mfatsftp-0.3.0/tests/real_server.py +125 -0
- mfatsftp-0.3.0/tests/sftp_stdio_server.py +19 -0
- mfatsftp-0.3.0/tests/test_client.py +115 -0
- mfatsftp-0.3.0/tests/test_docs.py +78 -0
- mfatsftp-0.3.0/tests/test_highlevel.py +411 -0
- mfatsftp-0.3.0/tests/test_large_files.py +620 -0
- mfatsftp-0.3.0/tests/test_live_ssh.py +47 -0
- mfatsftp-0.3.0/tests/test_open_and_rename_requests.py +197 -0
- mfatsftp-0.3.0/tests/test_paramiko_parity.py +331 -0
- mfatsftp-0.3.0/tests/test_protocol.py +165 -0
- mfatsftp-0.3.0/tests/test_symlink.py +131 -0
- mfatsftp-0.3.0/tests/test_transport.py +50 -0
- mfatsftp-0.3.0/tests/test_transport_subprocess.py +64 -0
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. The format is based on
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/), and this project adheres to
|
|
5
|
+
[Semantic Versioning](https://semver.org/).
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.3.0] - 2026-10-18
|
|
10
|
+
|
|
11
|
+
paramiko parity for the SFTP client, a documentation site, strict typing, and
|
|
12
|
+
a security fix.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
- Reply timeouts: `SFTPClient.settimeout()`/`gettimeout()` (also on
|
|
16
|
+
`SFTPFile`, with `setblocking`), raising the new `SFTPTimeoutError`
|
|
17
|
+
(a `TimeoutError`).
|
|
18
|
+
- Line I/O on `SFTPFile`: `readline`, `readlines`, iteration, `writelines`.
|
|
19
|
+
- Write buffering via `open(..., bufsize=N)` (`1` = line-buffered) and
|
|
20
|
+
`SFTPFile.set_pipelined()`; `flush()` now sends and confirms buffered data.
|
|
21
|
+
- `chdir`/`getcwd` (an emulated working directory, as in paramiko).
|
|
22
|
+
- `listdir_iter` (streaming directory listing).
|
|
23
|
+
- `readv` on `SFTPClient` and `SFTPFile` (many ranges, one pipelined batch).
|
|
24
|
+
- `SFTPClient.max_concurrent_requests` and per-call
|
|
25
|
+
`max_concurrent_prefetch_requests` on `get`/`getfo` to cap requests in flight.
|
|
26
|
+
- paramiko compatibility: `prefetch=` on `get`/`getfo`, `SFTPFile.prefetch()`
|
|
27
|
+
(a no-op), `SFTPFile.chown()`, `SFTPAttributes.attr` (extended attributes,
|
|
28
|
+
previously discarded) and `SFTPAttributes.from_stat()`.
|
|
29
|
+
- A garbage-collected `SFTPFile` sends its buffered writes and closes its
|
|
30
|
+
handle without waiting for replies.
|
|
31
|
+
- Documentation site (Sphinx, with a reference generated from the
|
|
32
|
+
docstrings), ready for Read the Docs; `CONTRIBUTING.md` and `SECURITY.md`.
|
|
33
|
+
- Tests that fail when a public member lacks a docstring or an entry in
|
|
34
|
+
`docs/api.md`, or when a code sample in the docs does not compile.
|
|
35
|
+
- A release workflow that publishes to PyPI when a `v*` tag is pushed.
|
|
36
|
+
|
|
37
|
+
### Changed
|
|
38
|
+
- Type hints pass `mypy --strict`; CI checks them, along with the docs build.
|
|
39
|
+
Stream arguments are typed `IO[bytes]` and `env` as `Dict[str, str]`.
|
|
40
|
+
|
|
41
|
+
### Fixed
|
|
42
|
+
- `put_dir` now gives small files exactly their local permission bits. The
|
|
43
|
+
server's umask could strip bits from them before, for example group write.
|
|
44
|
+
- The docs gave OpenSSH 9.6 as the first version with
|
|
45
|
+
`users-groups-by-id@openssh.com`; it is 9.1.
|
|
46
|
+
|
|
47
|
+
### Security
|
|
48
|
+
- `connect()` / `build_ssh_argv()` refuse a host that is empty or starts with
|
|
49
|
+
`-`. Before, a host such as `-oProxyCommand=…` from untrusted input was read
|
|
50
|
+
by ssh as an option and could run a local command.
|
|
51
|
+
|
|
52
|
+
## [0.2.0] - 2026-10-18
|
|
53
|
+
|
|
54
|
+
Brings the client up to date with the SFTP code in sshPilot, adds the
|
|
55
|
+
convenience API a standalone library needs, and documents all of it.
|
|
56
|
+
|
|
57
|
+
### Added
|
|
58
|
+
- Pipelined transfers: reads and writes keep a window of requests in flight
|
|
59
|
+
that grows from the measured round-trip time. Chunk sizes come from the
|
|
60
|
+
server's `limits@openssh.com`. A server that returns less than it
|
|
61
|
+
advertised gets the missing pieces fetched together.
|
|
62
|
+
- Batch operations that cost about one round trip per 100 paths:
|
|
63
|
+
`stat_many`, `listdir_many`, `mkdir_many`, `remove_many`, `rmdir_many`,
|
|
64
|
+
`read_small_files`, `atomic_upload_many` (with `AtomicUploadItem`).
|
|
65
|
+
Directory listings keep several READDIRs in flight.
|
|
66
|
+
- `rmtree` (like `rm -r`, never follows symlinks) and `walk` (like `os.walk`).
|
|
67
|
+
- `getfo` / `putfo`, a `confirm` option on `put`/`putfo`, and `copy`
|
|
68
|
+
(server-side with `copy-data`, read + write otherwise).
|
|
69
|
+
- Metadata: `fstat`, `setstat`, `fsetstat`, `chown`, `utime`, `truncate`,
|
|
70
|
+
`symlink`, `readlink`.
|
|
71
|
+
- OpenSSH extensions: `atomic_rename` (falls back when `posix-rename` is
|
|
72
|
+
missing or rejected), `hardlink`, `copy_data`, `statvfs` (`SFTPStatVFS`),
|
|
73
|
+
`users_groups_by_id`, and `supports_*()` checks for each.
|
|
74
|
+
- `SFTPFile`: `seek`, `tell`, `truncate`, `stat`, `chmod`, `utime`,
|
|
75
|
+
`readinto` (so `io.BufferedReader`/`io.TextIOWrapper` can wrap it),
|
|
76
|
+
`readable`/`writable`/`seekable`, `closed`, `handle`.
|
|
77
|
+
- `open(..., create_mode=0o600)` sets a new file's permissions in the open
|
|
78
|
+
request.
|
|
79
|
+
- `SFTPClient.closed`, `SFTPClient.extensions`, `max_read_length`,
|
|
80
|
+
`max_write_length`, `max_open_handles`.
|
|
81
|
+
- `SFTPAttributes.is_file()` and an `ls -l`-style `str()`.
|
|
82
|
+
- Exception subclasses that match Python's built-ins: `SFTPNoSuchFileError`
|
|
83
|
+
(`FileNotFoundError`), `SFTPPermissionError` (`PermissionError`),
|
|
84
|
+
`SFTPConnectionError` (`ConnectionError`).
|
|
85
|
+
- `PipelinedWriter` (via `pipelined_writer`) with an `on_ack` progress hook.
|
|
86
|
+
- Full API reference (`docs/api.md`), an expanded README, and
|
|
87
|
+
`examples/transfer.py`.
|
|
88
|
+
- Tests against the real OpenSSH `sftp-server` binary (including simulated
|
|
89
|
+
high-latency links), and an opt-in end-to-end test through `ssh`
|
|
90
|
+
(`MFATSFTP_TEST_HOST`).
|
|
91
|
+
|
|
92
|
+
### Changed
|
|
93
|
+
- `get`/`put`/`get_dir`/`put_dir` use the pipelined and batched paths. A
|
|
94
|
+
tree of many small files now costs a few round trips per directory level
|
|
95
|
+
instead of several per file.
|
|
96
|
+
- `put_dir` keeps local permission bits and modification times, and writes
|
|
97
|
+
small files through a temporary name and a rename.
|
|
98
|
+
- `makedirs` now raises if something other than a directory is in the way,
|
|
99
|
+
instead of silently ignoring every error.
|
|
100
|
+
- `open()` modes now follow Python: `r+` no longer creates a missing file,
|
|
101
|
+
`w+` truncates, `x` fails if the file exists, `a` starts at the end, and an
|
|
102
|
+
unknown mode raises `ValueError`.
|
|
103
|
+
- `SFTPFile.write` returns the number of bytes written.
|
|
104
|
+
- `from_command`/`connect`: `timeout` now limits login plus handshake and
|
|
105
|
+
raises `TimeoutError`. The default is `None` (no limit). Before, the
|
|
106
|
+
handshake could hang indefinitely whatever the value.
|
|
107
|
+
- Packaging: SPDX license expression (`GPL-3.0-or-later`) per PEP 639,
|
|
108
|
+
Python 3.14 classifier, ruff configuration. CI lints, builds and checks
|
|
109
|
+
the package, and runs the real-server tests.
|
|
110
|
+
- License changed from MIT to GPL-3.0-or-later.
|
|
111
|
+
|
|
112
|
+
### Fixed
|
|
113
|
+
- Reads no longer stop short on large files, and writes larger than one
|
|
114
|
+
packet no longer drop the session. OpenSSH caps a READ at about 255 KiB and
|
|
115
|
+
closes the connection on a message over 256 KiB.
|
|
116
|
+
- Filenames that are not valid UTF-8 are kept intact (`surrogateescape`)
|
|
117
|
+
instead of being replaced, so such files can be opened, renamed and
|
|
118
|
+
deleted.
|
|
119
|
+
- `get_dir` refuses server-listed names containing a path separator or equal
|
|
120
|
+
to `.`/`..`, so a hostile server cannot make it write outside the target
|
|
121
|
+
directory.
|
|
122
|
+
- A failed connection attempt now closes the ssh process's pipes.
|
|
123
|
+
|
|
124
|
+
## [0.1.0] - initial extraction
|
|
125
|
+
### Added
|
|
126
|
+
- Pure-Python SFTP v3 wire-protocol codec (`mfatsftp.protocol`).
|
|
127
|
+
- `SFTPClient` / `SFTPFile` over a stream pair, with a paramiko-compatible
|
|
128
|
+
surface (`open`/`file`, `stat`/`lstat`, `listdir`/`listdir_attr`,
|
|
129
|
+
`mkdir`/`makedirs`/`rmdir`, `remove`/`unlink`, `rename`/`posix_rename`,
|
|
130
|
+
`chmod`, `realpath`/`normalize`, `get`/`put`/`get_dir`/`put_dir`).
|
|
131
|
+
- `connect()` / `from_command()` / `build_ssh_argv()` transport over the system
|
|
132
|
+
OpenSSH (`ssh -s <host> sftp`), with clean subprocess teardown.
|
|
133
|
+
- In-memory SFTP server test fixture; protocol, client and transport test suites.
|
|
134
|
+
|
|
135
|
+
[Unreleased]: https://github.com/mfat/mfatsftp/compare/v0.3.0...HEAD
|
|
136
|
+
[0.3.0]: https://github.com/mfat/mfatsftp/releases/tag/v0.3.0
|
|
137
|
+
[0.2.0]: https://github.com/mfat/mfatsftp/commit/09978e8
|
|
138
|
+
[0.1.0]: https://github.com/mfat/mfatsftp/commit/aa26356
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
Thanks for helping improve mfatsftp. Bug reports, fixes and documentation
|
|
4
|
+
improvements are all welcome.
|
|
5
|
+
|
|
6
|
+
## Setting up
|
|
7
|
+
|
|
8
|
+
```sh
|
|
9
|
+
git clone https://github.com/mfat/mfatsftp
|
|
10
|
+
cd mfatsftp
|
|
11
|
+
python -m venv .venv && . .venv/bin/activate
|
|
12
|
+
pip install -e ".[dev]"
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Most tests drive the real OpenSSH `sftp-server` binary over pipes. Install it
|
|
16
|
+
if it's missing:
|
|
17
|
+
|
|
18
|
+
- Debian/Ubuntu: `sudo apt install openssh-sftp-server`
|
|
19
|
+
- Fedora: `sudo dnf install openssh-server`
|
|
20
|
+
- macOS: already present at `/usr/libexec/sftp-server`
|
|
21
|
+
|
|
22
|
+
Tests that need it are skipped, not failed, when it isn't there.
|
|
23
|
+
|
|
24
|
+
## Checks
|
|
25
|
+
|
|
26
|
+
These are the checks CI runs. Please run them before opening a pull request:
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
pytest # tests
|
|
30
|
+
ruff check src tests examples # lint
|
|
31
|
+
mypy --strict src/mfatsftp # types (the package ships py.typed)
|
|
32
|
+
sphinx-build -W -b html docs docs/_build/html # docs (pip install -r docs/requirements.txt)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
To test a real ssh login as well, point `MFATSFTP_TEST_HOST` at a host you
|
|
36
|
+
can reach without a password prompt (for example `localhost` with your key in
|
|
37
|
+
`~/.ssh/authorized_keys`):
|
|
38
|
+
|
|
39
|
+
```sh
|
|
40
|
+
MFATSFTP_TEST_HOST=localhost pytest tests/test_live_ssh.py
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Where things are
|
|
44
|
+
|
|
45
|
+
| Path | What |
|
|
46
|
+
|---|---|
|
|
47
|
+
| `src/mfatsftp/protocol.py` | SFTP v3 wire codec: pure functions, no I/O |
|
|
48
|
+
| `src/mfatsftp/client.py` | `SFTPClient`, `SFTPFile`, `PipelinedWriter`: requests, pipelining, transfers |
|
|
49
|
+
| `src/mfatsftp/transport.py` | `connect` / `from_command`: running `ssh -s host sftp` |
|
|
50
|
+
| `tests/real_server.py` | helpers for tests against `sftp-server`, including a simulated slow link |
|
|
51
|
+
| `tests/fake_server.py` | in-memory SFTP server for protocol-level tests |
|
|
52
|
+
| `docs/` | Sphinx site; `docs/api.md` is the hand-written reference |
|
|
53
|
+
|
|
54
|
+
## Guidelines
|
|
55
|
+
|
|
56
|
+
- **Tests:** add a test for every fix or feature. Prefer the real-server
|
|
57
|
+
fixture (`client` in `tests/conftest.py`) over the fake server.
|
|
58
|
+
- **Speed:** if you change how requests are sent, check the round trips with
|
|
59
|
+
the simulated slow link (`start_client(delay)`), as in
|
|
60
|
+
`tests/test_large_files.py`.
|
|
61
|
+
- **Docs:** public methods need a docstring and an entry in `docs/api.md`.
|
|
62
|
+
`tests/test_docs.py` fails otherwise. User-visible changes go under
|
|
63
|
+
"Unreleased" in `CHANGELOG.md`.
|
|
64
|
+
- **Dependencies:** the package has no runtime dependencies beyond the
|
|
65
|
+
standard library, and it should stay that way.
|
|
66
|
+
- **Compatibility:** Python 3.9+ on Linux and macOS.
|
|
67
|
+
|
|
68
|
+
## Reporting bugs
|
|
69
|
+
|
|
70
|
+
Open an issue with your Python version, OS, `ssh -V` output, the server
|
|
71
|
+
software if known, and a minimal script that shows the problem. For security
|
|
72
|
+
problems, see [SECURITY.md](https://github.com/mfat/mfatsftp/blob/main/SECURITY.md) instead.
|
|
73
|
+
|
|
74
|
+
By contributing you agree that your contributions are licensed under the
|
|
75
|
+
project's license (GPL-3.0-or-later).
|