ffl-python 0.1.4__tar.gz → 0.1.5__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.
- {ffl_python-0.1.4 → ffl_python-0.1.5}/PKG-INFO +71 -63
- ffl_python-0.1.5/README.md +198 -0
- {ffl_python-0.1.4 → ffl_python-0.1.5}/pyproject.toml +1 -1
- ffl_python-0.1.5/src/ffl/bin/ffl.com +4 -0
- {ffl_python-0.1.4 → ffl_python-0.1.5}/src/ffl/events.py +16 -7
- {ffl_python-0.1.4 → ffl_python-0.1.5}/src/ffl/models.py +2 -0
- {ffl_python-0.1.4 → ffl_python-0.1.5}/src/ffl/parsing.py +5 -5
- {ffl_python-0.1.4 → ffl_python-0.1.5}/src/ffl_python.egg-info/PKG-INFO +71 -63
- {ffl_python-0.1.4 → ffl_python-0.1.5}/tests/test_events.py +53 -6
- {ffl_python-0.1.4 → ffl_python-0.1.5}/tests/test_hook.py +11 -1
- {ffl_python-0.1.4 → ffl_python-0.1.5}/tests/test_session.py +26 -0
- ffl_python-0.1.4/README.md +0 -190
- ffl_python-0.1.4/src/ffl/bin/ffl.com +0 -4
- {ffl_python-0.1.4 → ffl_python-0.1.5}/LICENSE +0 -0
- {ffl_python-0.1.4 → ffl_python-0.1.5}/setup.cfg +0 -0
- {ffl_python-0.1.4 → ffl_python-0.1.5}/setup.py +0 -0
- {ffl_python-0.1.4 → ffl_python-0.1.5}/src/ffl/__init__.py +0 -0
- {ffl_python-0.1.4 → ffl_python-0.1.5}/src/ffl/_generated.py +0 -0
- {ffl_python-0.1.4 → ffl_python-0.1.5}/src/ffl/_runtime.py +0 -0
- {ffl_python-0.1.4 → ffl_python-0.1.5}/src/ffl/client.py +0 -0
- {ffl_python-0.1.4 → ffl_python-0.1.5}/src/ffl/errors.py +0 -0
- {ffl_python-0.1.4 → ffl_python-0.1.5}/src/ffl/py.typed +0 -0
- {ffl_python-0.1.4 → ffl_python-0.1.5}/src/ffl_python.egg-info/SOURCES.txt +0 -0
- {ffl_python-0.1.4 → ffl_python-0.1.5}/src/ffl_python.egg-info/dependency_links.txt +0 -0
- {ffl_python-0.1.4 → ffl_python-0.1.5}/src/ffl_python.egg-info/requires.txt +0 -0
- {ffl_python-0.1.4 → ffl_python-0.1.5}/src/ffl_python.egg-info/top_level.txt +0 -0
- {ffl_python-0.1.4 → ffl_python-0.1.5}/tests/test_auth.py +0 -0
- {ffl_python-0.1.4 → ffl_python-0.1.5}/tests/test_e2ee.py +0 -0
- {ffl_python-0.1.4 → ffl_python-0.1.5}/tests/test_folder.py +0 -0
- {ffl_python-0.1.4 → ffl_python-0.1.5}/tests/test_packaging.py +0 -0
- {ffl_python-0.1.4 → ffl_python-0.1.5}/tests/test_port.py +0 -0
- {ffl_python-0.1.4 → ffl_python-0.1.5}/tests/test_qr.py +0 -0
- {ffl_python-0.1.4 → ffl_python-0.1.5}/tests/test_recipient_auth.py +0 -0
- {ffl_python-0.1.4 → ffl_python-0.1.5}/tests/test_relay.py +0 -0
- {ffl_python-0.1.4 → ffl_python-0.1.5}/tests/test_stream.py +0 -0
- {ffl_python-0.1.4 → ffl_python-0.1.5}/tests/test_transfer.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ffl-python
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.5
|
|
4
4
|
Summary: Python binding for FastFileLink, backed by the portable ffl.com APE
|
|
5
5
|
Author: ffl-python contributors
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -20,13 +20,24 @@ Dynamic: license-file
|
|
|
20
20
|
|
|
21
21
|
# ffl-python
|
|
22
22
|
|
|
23
|
-
Python binding for
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
`ffl-python` is the Python binding for the [FastFileLink](https://github.com/nuwainfo/ffl)
|
|
24
|
+
CLI (FFL), which turns a file, folder, or stream into a browser-ready HTTPS link so the
|
|
25
|
+
recipient can download it without installing anything. It prefers a direct QUIC/WebRTC P2P
|
|
26
|
+
connection and falls back to a relayed/tunneled HTTPS link, with optional end-to-end
|
|
27
|
+
encryption. See the FFL repository for the full protocol and CLI details.
|
|
26
28
|
|
|
27
|
-
The
|
|
28
|
-
|
|
29
|
-
|
|
29
|
+
The package bundles the portable `ffl.com` APE and runs it behind a Python API,
|
|
30
|
+
powered by [APEBind](https://github.com/nuwainfo/apebind),
|
|
31
|
+
so callers do not need to locate or install a separate FFL
|
|
32
|
+
|
|
33
|
+
## Installation
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install ffl-python
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
This installs the `ffl` package with the bundled `ffl.com` APE included -- no separate
|
|
40
|
+
FFL install or PATH setup required.
|
|
30
41
|
|
|
31
42
|
## Development
|
|
32
43
|
|
|
@@ -42,14 +53,6 @@ python -m venv .venv
|
|
|
42
53
|
# or: ./scripts/build.sh # Linux/macOS
|
|
43
54
|
```
|
|
44
55
|
|
|
45
|
-
The test directly imports `ffl`, shares a binary file, and downloads it through the
|
|
46
|
-
bundled `ffl.com` APE. It requires working network access to FastFileLink. A wheel is a
|
|
47
|
-
build artifact, not the source of truth.
|
|
48
|
-
|
|
49
|
-
The integration suite covers ordinary, E2EE, relay, pickup-code, and public-key
|
|
50
|
-
transfers; text, bytes, folders, multiple files, QR images, hooks, explicit ports, and
|
|
51
|
-
session shutdown, and Basic Auth download.
|
|
52
|
-
|
|
53
56
|
## Share
|
|
54
57
|
|
|
55
58
|
```python
|
|
@@ -77,10 +80,6 @@ with ffl.share_text("hello", name="hello.txt") as session:
|
|
|
77
80
|
print(session.link)
|
|
78
81
|
```
|
|
79
82
|
|
|
80
|
-
Optional-value FFL flags use natural Python values. For example, `receipt=True` emits
|
|
81
|
-
`--receipt` without a value, while `receipt="me@example.com"` emits the flag with the
|
|
82
|
-
address.
|
|
83
|
-
|
|
84
83
|
### Stream a source without a temporary file
|
|
85
84
|
|
|
86
85
|
`share_stream()` passes a binary file-like object directly to FFL stdin. It is useful
|
|
@@ -93,6 +92,25 @@ with open("backup.tar", "rb") as source:
|
|
|
93
92
|
print(session.link)
|
|
94
93
|
```
|
|
95
94
|
|
|
95
|
+
### Events
|
|
96
|
+
|
|
97
|
+
`session.on(name, listener)` follows FFL's integrated events:
|
|
98
|
+
|
|
99
|
+
| Semantic name | FFL event |
|
|
100
|
+
| --- | --- |
|
|
101
|
+
| `ready` | `/share/available` |
|
|
102
|
+
| `started` | `/transfer/create` |
|
|
103
|
+
| `progress` | `/transfer/progress` |
|
|
104
|
+
| `completed` | `/transfer/complete` |
|
|
105
|
+
| `failed` | `/transfer/fail` |
|
|
106
|
+
|
|
107
|
+
HTTP, WebRTC, and direct P2P also emit their own events, such as
|
|
108
|
+
`/download/complete`. Those stay on `session.on_raw()`. `completed` follows
|
|
109
|
+
`/transfer/complete`, so one transfer notifies that listener once. Each accepted
|
|
110
|
+
hook POST is answered with HTTP 200 and `{}`.
|
|
111
|
+
|
|
112
|
+
The bundled `ffl.com` emits these names.
|
|
113
|
+
|
|
96
114
|
## Download
|
|
97
115
|
|
|
98
116
|
```python
|
|
@@ -154,9 +172,6 @@ print(result.public_key_path)
|
|
|
154
172
|
print(result.private_key_path)
|
|
155
173
|
```
|
|
156
174
|
|
|
157
|
-
`keygen()` has a 60-second process timeout and verifies that the key paths reported by
|
|
158
|
-
FFL exist.
|
|
159
|
-
|
|
160
175
|
## Version and raw access
|
|
161
176
|
|
|
162
177
|
```python
|
|
@@ -167,44 +182,37 @@ result = ffl.raw(["download", "--help"])
|
|
|
167
182
|
`raw()` is the escape hatch for new FFL options or commands that the semantic API has
|
|
168
183
|
not adopted yet.
|
|
169
184
|
|
|
170
|
-
##
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
- `src/ffl/_generated.py`
|
|
205
|
-
- `src/ffl/_runtime.py`
|
|
206
|
-
- `src/ffl/bin/ffl.com`
|
|
207
|
-
- `src/ffl/py.typed`
|
|
208
|
-
|
|
209
|
-
It deliberately does not overwrite `client.py`, `models.py`, parsing logic, tests, or the
|
|
210
|
-
semantic schema.
|
|
185
|
+
## Compared to magic-wormhole
|
|
186
|
+
|
|
187
|
+
[magic-wormhole](https://github.com/magic-wormhole/magic-wormhole) is the other Python
|
|
188
|
+
tool commonly reached for to move a file between two machines. Both are ad hoc,
|
|
189
|
+
non-account-based transfers you can drive from Python, but they differ in shape:
|
|
190
|
+
|
|
191
|
+
- **Binding vs. native library.** `ffl-python` is a subprocess wrapper around the
|
|
192
|
+
separate `ffl.com` CLI binary -- WebRTC/QUIC, NAT traversal, and relay/tunnel fallback
|
|
193
|
+
all happen in that external process. `wormhole` is a native, in-process Python package
|
|
194
|
+
(Twisted-based); no external binary is involved. In practice, though, wormhole's own
|
|
195
|
+
*file*-transfer path is also driven through its CLI machinery rather than a stable
|
|
196
|
+
public library call -- `wormhole.create()` covers generic message exchange, not file
|
|
197
|
+
transfer directly.
|
|
198
|
+
- **Recipient experience.** An FFL share is an HTTPS link: the recipient opens it in a
|
|
199
|
+
browser and downloads, no install required. A wormhole transfer is a short code
|
|
200
|
+
(e.g. `7-crossbow-clockwork`) exchanged out-of-band; the recipient needs the
|
|
201
|
+
`wormhole` CLI installed to redeem it.
|
|
202
|
+
- **Transport.** FFL tries a direct WebRTC/QUIC P2P connection first, falls back to a
|
|
203
|
+
plain P2P TCP connection, and falls back again to a relayed/tunneled HTTPS link if no
|
|
204
|
+
P2P path is reachable at all. wormhole's own transit protocol does direct TCP with a
|
|
205
|
+
relay fallback, with the connection authenticated by a SPAKE2 PAKE key derived from the
|
|
206
|
+
code.
|
|
207
|
+
- **Security default.** wormhole is end-to-end encrypted on every transfer by
|
|
208
|
+
construction of the code exchange. FFL's end-to-end encryption is opt-in (`--e2ee`);
|
|
209
|
+
without it, data in transit is only as protected as the HTTPS connection to FFL's
|
|
210
|
+
relay/tunnel infrastructure.
|
|
211
|
+
- **Feature surface.** FFL adds application-layer conveniences wormhole doesn't have: a
|
|
212
|
+
secondary pickup-code/public-key recipient check layered on top of the link,
|
|
213
|
+
receipt-confirmation emails, a pluggable choice of tunnel backend when P2P isn't
|
|
214
|
+
reachable (built-in `default`, plus Cloudflare, ngrok, Localtunnel, Loophole, Dev
|
|
215
|
+
Tunnel, Bore, or a self-hosted sish tunnel via `--preferred-tunnel`, with custom
|
|
216
|
+
tunnels configurable in `~/.fastfilelink/tunnels.json`), and general SOCKS5/HTTP
|
|
217
|
+
proxy configuration (wormhole only knows how to route through Tor, via `--tor`).
|
|
218
|
+
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# ffl-python
|
|
2
|
+
|
|
3
|
+
`ffl-python` is the Python binding for the [FastFileLink](https://github.com/nuwainfo/ffl)
|
|
4
|
+
CLI (FFL), which turns a file, folder, or stream into a browser-ready HTTPS link so the
|
|
5
|
+
recipient can download it without installing anything. It prefers a direct QUIC/WebRTC P2P
|
|
6
|
+
connection and falls back to a relayed/tunneled HTTPS link, with optional end-to-end
|
|
7
|
+
encryption. See the FFL repository for the full protocol and CLI details.
|
|
8
|
+
|
|
9
|
+
The package bundles the portable `ffl.com` APE and runs it behind a Python API,
|
|
10
|
+
powered by [APEBind](https://github.com/nuwainfo/apebind),
|
|
11
|
+
so callers do not need to locate or install a separate FFL
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install ffl-python
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
This installs the `ffl` package with the bundled `ffl.com` APE included -- no separate
|
|
20
|
+
FFL install or PATH setup required.
|
|
21
|
+
|
|
22
|
+
## Development
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
python -m venv .venv
|
|
26
|
+
.venv/Scripts/python -m pip install -e ".[dev]" # Windows
|
|
27
|
+
# or: .venv/bin/python -m pip install -e ".[dev]"
|
|
28
|
+
|
|
29
|
+
.\scripts\test.ps1 # Windows
|
|
30
|
+
# or: ./scripts/test.sh # Linux/macOS
|
|
31
|
+
|
|
32
|
+
.\scripts\build.ps1 # Windows
|
|
33
|
+
# or: ./scripts/build.sh # Linux/macOS
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Share
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
import ffl
|
|
40
|
+
|
|
41
|
+
with ffl.share("release.zip", max_downloads=1, timeout_seconds=1800) as session:
|
|
42
|
+
print(session.link)
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
`share()` always requests a foreground FFL process and disables clipboard side effects,
|
|
46
|
+
which gives library callers a deterministic `ShareSession` they can stop or keep alive.
|
|
47
|
+
FFL's runtime-owned `--json` output is used internally to wait until `session.link` is
|
|
48
|
+
ready.
|
|
49
|
+
|
|
50
|
+
Multiple files are accepted directly:
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
session = ffl.share(["one.txt", "two.txt"], name="files.zip")
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Text and bytes helpers own their temporary file until the share session is closed:
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
with ffl.share_text("hello", name="hello.txt") as session:
|
|
60
|
+
print(session.link)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Stream a source without a temporary file
|
|
64
|
+
|
|
65
|
+
`share_stream()` passes a binary file-like object directly to FFL stdin. It is useful
|
|
66
|
+
for database dumps, generated artifacts, and other data that should not first be
|
|
67
|
+
materialized as a separate temporary file:
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
with open("backup.tar", "rb") as source:
|
|
71
|
+
with ffl.share_stream(source, name="backup.tar") as session:
|
|
72
|
+
print(session.link)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Events
|
|
76
|
+
|
|
77
|
+
`session.on(name, listener)` follows FFL's integrated events:
|
|
78
|
+
|
|
79
|
+
| Semantic name | FFL event |
|
|
80
|
+
| --- | --- |
|
|
81
|
+
| `ready` | `/share/available` |
|
|
82
|
+
| `started` | `/transfer/create` |
|
|
83
|
+
| `progress` | `/transfer/progress` |
|
|
84
|
+
| `completed` | `/transfer/complete` |
|
|
85
|
+
| `failed` | `/transfer/fail` |
|
|
86
|
+
|
|
87
|
+
HTTP, WebRTC, and direct P2P also emit their own events, such as
|
|
88
|
+
`/download/complete`. Those stay on `session.on_raw()`. `completed` follows
|
|
89
|
+
`/transfer/complete`, so one transfer notifies that listener once. Each accepted
|
|
90
|
+
hook POST is answered with HTTP 200 and `{}`.
|
|
91
|
+
|
|
92
|
+
The bundled `ffl.com` emits these names.
|
|
93
|
+
|
|
94
|
+
## Download
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
result = ffl.download("https://example.fastfilelink/...", output_path="download.bin")
|
|
98
|
+
print(result.output_path)
|
|
99
|
+
print(result.transfer_mode)
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
`download()` waits for the foreground FFL process to finish and returns a
|
|
103
|
+
`DownloadResult`. For cancellation, progress, or caller-controlled timeouts, use
|
|
104
|
+
`start_download()` and manage its `DownloadSession` explicitly.
|
|
105
|
+
|
|
106
|
+
### Stream a download
|
|
107
|
+
|
|
108
|
+
`download_stream()` exposes FFL stdout as binary chunks without buffering the whole
|
|
109
|
+
file in memory:
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
with ffl.download_stream("https://example.fastfilelink/...") as transfer:
|
|
113
|
+
with open("output.bin", "wb") as target:
|
|
114
|
+
for chunk in transfer.iter_bytes():
|
|
115
|
+
target.write(chunk)
|
|
116
|
+
|
|
117
|
+
result = transfer.wait()
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Consume `iter_bytes()` through EOF before calling `wait()`. Calling `wait()` with
|
|
121
|
+
unconsumed streamed stdout raises `RuntimeError`; this avoids silently discarding
|
|
122
|
+
binary data or deadlocking when the child process fills its stdout pipe.
|
|
123
|
+
|
|
124
|
+
## Authentication secrets
|
|
125
|
+
|
|
126
|
+
For shares protected with HTTP Basic Auth, FFL supports `FFL_AUTH_PASSWORD`. Set it in
|
|
127
|
+
the application environment and pass only `auth_user` to keep the password out of the
|
|
128
|
+
FFL command line:
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
export FFL_AUTH_PASSWORD='use-your-secret-manager'
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
```powershell
|
|
135
|
+
$env:FFL_AUTH_PASSWORD = 'use-your-secret-manager'
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
with ffl.share("release.zip", auth_user="deploy") as session:
|
|
140
|
+
print(session.link)
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Do not also pass `auth_password=` when using this pattern: FFL gives the explicit CLI
|
|
144
|
+
option precedence over `FFL_AUTH_PASSWORD`. The environment variable applies to the
|
|
145
|
+
sharing side; pass download credentials explicitly when downloading a protected link.
|
|
146
|
+
|
|
147
|
+
## Key generation
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
result = ffl.keygen("alice")
|
|
151
|
+
print(result.public_key_path)
|
|
152
|
+
print(result.private_key_path)
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Version and raw access
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
print(ffl.version())
|
|
159
|
+
result = ffl.raw(["download", "--help"])
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
`raw()` is the escape hatch for new FFL options or commands that the semantic API has
|
|
163
|
+
not adopted yet.
|
|
164
|
+
|
|
165
|
+
## Compared to magic-wormhole
|
|
166
|
+
|
|
167
|
+
[magic-wormhole](https://github.com/magic-wormhole/magic-wormhole) is the other Python
|
|
168
|
+
tool commonly reached for to move a file between two machines. Both are ad hoc,
|
|
169
|
+
non-account-based transfers you can drive from Python, but they differ in shape:
|
|
170
|
+
|
|
171
|
+
- **Binding vs. native library.** `ffl-python` is a subprocess wrapper around the
|
|
172
|
+
separate `ffl.com` CLI binary -- WebRTC/QUIC, NAT traversal, and relay/tunnel fallback
|
|
173
|
+
all happen in that external process. `wormhole` is a native, in-process Python package
|
|
174
|
+
(Twisted-based); no external binary is involved. In practice, though, wormhole's own
|
|
175
|
+
*file*-transfer path is also driven through its CLI machinery rather than a stable
|
|
176
|
+
public library call -- `wormhole.create()` covers generic message exchange, not file
|
|
177
|
+
transfer directly.
|
|
178
|
+
- **Recipient experience.** An FFL share is an HTTPS link: the recipient opens it in a
|
|
179
|
+
browser and downloads, no install required. A wormhole transfer is a short code
|
|
180
|
+
(e.g. `7-crossbow-clockwork`) exchanged out-of-band; the recipient needs the
|
|
181
|
+
`wormhole` CLI installed to redeem it.
|
|
182
|
+
- **Transport.** FFL tries a direct WebRTC/QUIC P2P connection first, falls back to a
|
|
183
|
+
plain P2P TCP connection, and falls back again to a relayed/tunneled HTTPS link if no
|
|
184
|
+
P2P path is reachable at all. wormhole's own transit protocol does direct TCP with a
|
|
185
|
+
relay fallback, with the connection authenticated by a SPAKE2 PAKE key derived from the
|
|
186
|
+
code.
|
|
187
|
+
- **Security default.** wormhole is end-to-end encrypted on every transfer by
|
|
188
|
+
construction of the code exchange. FFL's end-to-end encryption is opt-in (`--e2ee`);
|
|
189
|
+
without it, data in transit is only as protected as the HTTPS connection to FFL's
|
|
190
|
+
relay/tunnel infrastructure.
|
|
191
|
+
- **Feature surface.** FFL adds application-layer conveniences wormhole doesn't have: a
|
|
192
|
+
secondary pickup-code/public-key recipient check layered on top of the link,
|
|
193
|
+
receipt-confirmation emails, a pluggable choice of tunnel backend when P2P isn't
|
|
194
|
+
reachable (built-in `default`, plus Cloudflare, ngrok, Localtunnel, Loophole, Dev
|
|
195
|
+
Tunnel, Bore, or a self-hosted sish tunnel via `--preferred-tunnel`, with custom
|
|
196
|
+
tunnels configurable in `~/.fastfilelink/tunnels.json`), and general SOCKS5/HTTP
|
|
197
|
+
proxy configuration (wormhole only knows how to route through Tor, via `--tor`).
|
|
198
|
+
|
|
@@ -31,11 +31,17 @@ from urllib.error import URLError
|
|
|
31
31
|
from urllib.request import Request, urlopen
|
|
32
32
|
|
|
33
33
|
|
|
34
|
+
# FFL posts one integrated /transfer/* event for every HTTP, WebRTC, and
|
|
35
|
+
# direct P2P transfer. Semantic listeners bind to those names, plus
|
|
36
|
+
# /share/available. Transport-specific events stay on the raw channel, so one
|
|
37
|
+
# transfer notifies each semantic listener once.
|
|
38
|
+
_HOOK_OK_BODY = b'{}'
|
|
34
39
|
_SEMANTIC_EVENT_NAMES = {
|
|
35
|
-
'/
|
|
36
|
-
'/
|
|
37
|
-
'/
|
|
38
|
-
'/
|
|
40
|
+
'/share/available': 'ready',
|
|
41
|
+
'/transfer/create': 'started',
|
|
42
|
+
'/transfer/progress': 'progress',
|
|
43
|
+
'/transfer/complete': 'completed',
|
|
44
|
+
'/transfer/fail': 'failed',
|
|
39
45
|
}
|
|
40
46
|
|
|
41
47
|
|
|
@@ -176,12 +182,15 @@ class FFLHookEventChannel:
|
|
|
176
182
|
try:
|
|
177
183
|
channel._publish(body)
|
|
178
184
|
published = True
|
|
179
|
-
self.send_response(
|
|
185
|
+
self.send_response(200)
|
|
186
|
+
self.send_header('Content-Type', 'application/json')
|
|
187
|
+
self.send_header('Content-Length', str(len(_HOOK_OK_BODY)))
|
|
188
|
+
self.end_headers()
|
|
189
|
+
self.wfile.write(_HOOK_OK_BODY)
|
|
180
190
|
except Exception as error:
|
|
181
191
|
channel._record_error(error)
|
|
182
192
|
self.send_response(500)
|
|
183
|
-
|
|
184
|
-
self.end_headers()
|
|
193
|
+
self.end_headers()
|
|
185
194
|
|
|
186
195
|
if published and channel._forward_url is not None:
|
|
187
196
|
channel._forward_async(body)
|
|
@@ -42,12 +42,12 @@ class FFLResultParser:
|
|
|
42
42
|
|
|
43
43
|
@classmethod
|
|
44
44
|
def _detect_transfer_mode(cls, output: str) -> TransferMode:
|
|
45
|
-
if
|
|
46
|
-
'P2P direct' in output
|
|
47
|
-
or 'P2P TCP' in output
|
|
48
|
-
or 'WebRTC P2P' in output
|
|
49
|
-
):
|
|
45
|
+
if 'P2P direct' in output or 'WebRTC P2P' in output:
|
|
50
46
|
return TransferMode.WEBRTC_P2P
|
|
47
|
+
if 'P2P UDP/QUIC' in output:
|
|
48
|
+
return TransferMode.P2P_QUIC
|
|
49
|
+
if 'P2P TCP' in output:
|
|
50
|
+
return TransferMode.P2P_TCP
|
|
51
51
|
if 'HTTP fallback' in output:
|
|
52
52
|
return TransferMode.HTTP_FALLBACK
|
|
53
53
|
if (
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ffl-python
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.5
|
|
4
4
|
Summary: Python binding for FastFileLink, backed by the portable ffl.com APE
|
|
5
5
|
Author: ffl-python contributors
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -20,13 +20,24 @@ Dynamic: license-file
|
|
|
20
20
|
|
|
21
21
|
# ffl-python
|
|
22
22
|
|
|
23
|
-
Python binding for
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
`ffl-python` is the Python binding for the [FastFileLink](https://github.com/nuwainfo/ffl)
|
|
24
|
+
CLI (FFL), which turns a file, folder, or stream into a browser-ready HTTPS link so the
|
|
25
|
+
recipient can download it without installing anything. It prefers a direct QUIC/WebRTC P2P
|
|
26
|
+
connection and falls back to a relayed/tunneled HTTPS link, with optional end-to-end
|
|
27
|
+
encryption. See the FFL repository for the full protocol and CLI details.
|
|
26
28
|
|
|
27
|
-
The
|
|
28
|
-
|
|
29
|
-
|
|
29
|
+
The package bundles the portable `ffl.com` APE and runs it behind a Python API,
|
|
30
|
+
powered by [APEBind](https://github.com/nuwainfo/apebind),
|
|
31
|
+
so callers do not need to locate or install a separate FFL
|
|
32
|
+
|
|
33
|
+
## Installation
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install ffl-python
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
This installs the `ffl` package with the bundled `ffl.com` APE included -- no separate
|
|
40
|
+
FFL install or PATH setup required.
|
|
30
41
|
|
|
31
42
|
## Development
|
|
32
43
|
|
|
@@ -42,14 +53,6 @@ python -m venv .venv
|
|
|
42
53
|
# or: ./scripts/build.sh # Linux/macOS
|
|
43
54
|
```
|
|
44
55
|
|
|
45
|
-
The test directly imports `ffl`, shares a binary file, and downloads it through the
|
|
46
|
-
bundled `ffl.com` APE. It requires working network access to FastFileLink. A wheel is a
|
|
47
|
-
build artifact, not the source of truth.
|
|
48
|
-
|
|
49
|
-
The integration suite covers ordinary, E2EE, relay, pickup-code, and public-key
|
|
50
|
-
transfers; text, bytes, folders, multiple files, QR images, hooks, explicit ports, and
|
|
51
|
-
session shutdown, and Basic Auth download.
|
|
52
|
-
|
|
53
56
|
## Share
|
|
54
57
|
|
|
55
58
|
```python
|
|
@@ -77,10 +80,6 @@ with ffl.share_text("hello", name="hello.txt") as session:
|
|
|
77
80
|
print(session.link)
|
|
78
81
|
```
|
|
79
82
|
|
|
80
|
-
Optional-value FFL flags use natural Python values. For example, `receipt=True` emits
|
|
81
|
-
`--receipt` without a value, while `receipt="me@example.com"` emits the flag with the
|
|
82
|
-
address.
|
|
83
|
-
|
|
84
83
|
### Stream a source without a temporary file
|
|
85
84
|
|
|
86
85
|
`share_stream()` passes a binary file-like object directly to FFL stdin. It is useful
|
|
@@ -93,6 +92,25 @@ with open("backup.tar", "rb") as source:
|
|
|
93
92
|
print(session.link)
|
|
94
93
|
```
|
|
95
94
|
|
|
95
|
+
### Events
|
|
96
|
+
|
|
97
|
+
`session.on(name, listener)` follows FFL's integrated events:
|
|
98
|
+
|
|
99
|
+
| Semantic name | FFL event |
|
|
100
|
+
| --- | --- |
|
|
101
|
+
| `ready` | `/share/available` |
|
|
102
|
+
| `started` | `/transfer/create` |
|
|
103
|
+
| `progress` | `/transfer/progress` |
|
|
104
|
+
| `completed` | `/transfer/complete` |
|
|
105
|
+
| `failed` | `/transfer/fail` |
|
|
106
|
+
|
|
107
|
+
HTTP, WebRTC, and direct P2P also emit their own events, such as
|
|
108
|
+
`/download/complete`. Those stay on `session.on_raw()`. `completed` follows
|
|
109
|
+
`/transfer/complete`, so one transfer notifies that listener once. Each accepted
|
|
110
|
+
hook POST is answered with HTTP 200 and `{}`.
|
|
111
|
+
|
|
112
|
+
The bundled `ffl.com` emits these names.
|
|
113
|
+
|
|
96
114
|
## Download
|
|
97
115
|
|
|
98
116
|
```python
|
|
@@ -154,9 +172,6 @@ print(result.public_key_path)
|
|
|
154
172
|
print(result.private_key_path)
|
|
155
173
|
```
|
|
156
174
|
|
|
157
|
-
`keygen()` has a 60-second process timeout and verifies that the key paths reported by
|
|
158
|
-
FFL exist.
|
|
159
|
-
|
|
160
175
|
## Version and raw access
|
|
161
176
|
|
|
162
177
|
```python
|
|
@@ -167,44 +182,37 @@ result = ffl.raw(["download", "--help"])
|
|
|
167
182
|
`raw()` is the escape hatch for new FFL options or commands that the semantic API has
|
|
168
183
|
not adopted yet.
|
|
169
184
|
|
|
170
|
-
##
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
- `src/ffl/_generated.py`
|
|
205
|
-
- `src/ffl/_runtime.py`
|
|
206
|
-
- `src/ffl/bin/ffl.com`
|
|
207
|
-
- `src/ffl/py.typed`
|
|
208
|
-
|
|
209
|
-
It deliberately does not overwrite `client.py`, `models.py`, parsing logic, tests, or the
|
|
210
|
-
semantic schema.
|
|
185
|
+
## Compared to magic-wormhole
|
|
186
|
+
|
|
187
|
+
[magic-wormhole](https://github.com/magic-wormhole/magic-wormhole) is the other Python
|
|
188
|
+
tool commonly reached for to move a file between two machines. Both are ad hoc,
|
|
189
|
+
non-account-based transfers you can drive from Python, but they differ in shape:
|
|
190
|
+
|
|
191
|
+
- **Binding vs. native library.** `ffl-python` is a subprocess wrapper around the
|
|
192
|
+
separate `ffl.com` CLI binary -- WebRTC/QUIC, NAT traversal, and relay/tunnel fallback
|
|
193
|
+
all happen in that external process. `wormhole` is a native, in-process Python package
|
|
194
|
+
(Twisted-based); no external binary is involved. In practice, though, wormhole's own
|
|
195
|
+
*file*-transfer path is also driven through its CLI machinery rather than a stable
|
|
196
|
+
public library call -- `wormhole.create()` covers generic message exchange, not file
|
|
197
|
+
transfer directly.
|
|
198
|
+
- **Recipient experience.** An FFL share is an HTTPS link: the recipient opens it in a
|
|
199
|
+
browser and downloads, no install required. A wormhole transfer is a short code
|
|
200
|
+
(e.g. `7-crossbow-clockwork`) exchanged out-of-band; the recipient needs the
|
|
201
|
+
`wormhole` CLI installed to redeem it.
|
|
202
|
+
- **Transport.** FFL tries a direct WebRTC/QUIC P2P connection first, falls back to a
|
|
203
|
+
plain P2P TCP connection, and falls back again to a relayed/tunneled HTTPS link if no
|
|
204
|
+
P2P path is reachable at all. wormhole's own transit protocol does direct TCP with a
|
|
205
|
+
relay fallback, with the connection authenticated by a SPAKE2 PAKE key derived from the
|
|
206
|
+
code.
|
|
207
|
+
- **Security default.** wormhole is end-to-end encrypted on every transfer by
|
|
208
|
+
construction of the code exchange. FFL's end-to-end encryption is opt-in (`--e2ee`);
|
|
209
|
+
without it, data in transit is only as protected as the HTTPS connection to FFL's
|
|
210
|
+
relay/tunnel infrastructure.
|
|
211
|
+
- **Feature surface.** FFL adds application-layer conveniences wormhole doesn't have: a
|
|
212
|
+
secondary pickup-code/public-key recipient check layered on top of the link,
|
|
213
|
+
receipt-confirmation emails, a pluggable choice of tunnel backend when P2P isn't
|
|
214
|
+
reachable (built-in `default`, plus Cloudflare, ngrok, Localtunnel, Loophole, Dev
|
|
215
|
+
Tunnel, Bore, or a self-hosted sish tunnel via `--preferred-tunnel`, with custom
|
|
216
|
+
tunnels configurable in `~/.fastfilelink/tunnels.json`), and general SOCKS5/HTTP
|
|
217
|
+
proxy configuration (wormhole only knows how to route through Tor, via `--tor`).
|
|
218
|
+
|
|
@@ -11,7 +11,7 @@ import pytest
|
|
|
11
11
|
from ffl.events import FFLHookEventChannel
|
|
12
12
|
|
|
13
13
|
|
|
14
|
-
def _event_body(index: int, name: str = '/
|
|
14
|
+
def _event_body(index: int, name: str = '/transfer/progress') -> bytes:
|
|
15
15
|
return json.dumps({'event': name, 'data': {'index': index}}).encode('utf-8')
|
|
16
16
|
|
|
17
17
|
|
|
@@ -20,7 +20,7 @@ def test_hook_history_is_bounded_and_exposes_semantic_event_names():
|
|
|
20
20
|
try:
|
|
21
21
|
channel._publish(_event_body(1))
|
|
22
22
|
channel._publish(_event_body(2))
|
|
23
|
-
channel._publish(_event_body(3, '/
|
|
23
|
+
channel._publish(_event_body(3, '/transfer/complete'))
|
|
24
24
|
|
|
25
25
|
assert [event.data['index'] for event in channel.history] == [2, 3]
|
|
26
26
|
assert channel.history[-1].semantic_name == 'completed'
|
|
@@ -36,6 +36,47 @@ def test_hook_history_is_bounded_and_exposes_semantic_event_names():
|
|
|
36
36
|
channel.close()
|
|
37
37
|
|
|
38
38
|
|
|
39
|
+
def test_integrated_transfer_events_map_once():
|
|
40
|
+
channel = FFLHookEventChannel()
|
|
41
|
+
try:
|
|
42
|
+
names = (
|
|
43
|
+
'/download/complete',
|
|
44
|
+
'/webrtc/transfer/complete',
|
|
45
|
+
'/p2p/transfer/complete',
|
|
46
|
+
'/hook/transfer/complete',
|
|
47
|
+
'/transfer/complete',
|
|
48
|
+
'/share/available',
|
|
49
|
+
'/transfer/create',
|
|
50
|
+
'/transfer/progress',
|
|
51
|
+
'/download/progress',
|
|
52
|
+
'/transfer/fail',
|
|
53
|
+
)
|
|
54
|
+
for index, name in enumerate(names, start=1):
|
|
55
|
+
channel._publish(_event_body(index, name))
|
|
56
|
+
|
|
57
|
+
def named(semantic):
|
|
58
|
+
return [
|
|
59
|
+
event.name for event in channel.history if event.semantic_name == semantic
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
assert named('completed') == ['/transfer/complete']
|
|
63
|
+
assert named('progress') == ['/transfer/progress']
|
|
64
|
+
assert named('ready') == ['/share/available']
|
|
65
|
+
assert named('started') == ['/transfer/create']
|
|
66
|
+
assert named('failed') == ['/transfer/fail']
|
|
67
|
+
|
|
68
|
+
replayed = []
|
|
69
|
+
replayed_event = threading.Event()
|
|
70
|
+
channel.on_semantic(
|
|
71
|
+
'completed',
|
|
72
|
+
lambda event: (replayed.append(event.name), replayed_event.set()),
|
|
73
|
+
)
|
|
74
|
+
assert replayed_event.wait(timeout=1)
|
|
75
|
+
assert replayed == ['/transfer/complete']
|
|
76
|
+
finally:
|
|
77
|
+
channel.close()
|
|
78
|
+
|
|
79
|
+
|
|
39
80
|
def test_live_event_iterator_preserves_events_after_history_eviction():
|
|
40
81
|
channel = FFLHookEventChannel(event_history_limit=2)
|
|
41
82
|
iterator = channel.events()
|
|
@@ -69,8 +110,12 @@ def test_hook_response_does_not_wait_for_forwarded_webhook():
|
|
|
69
110
|
def do_POST(self) -> None:
|
|
70
111
|
forwarded.set()
|
|
71
112
|
release_forward.wait(timeout=5)
|
|
72
|
-
|
|
113
|
+
body = b'{}'
|
|
114
|
+
self.send_response(200)
|
|
115
|
+
self.send_header('Content-Type', 'application/json')
|
|
116
|
+
self.send_header('Content-Length', str(len(body)))
|
|
73
117
|
self.end_headers()
|
|
118
|
+
self.wfile.write(body)
|
|
74
119
|
|
|
75
120
|
def log_message(self, format_string: str, *args) -> None:
|
|
76
121
|
del format_string, args
|
|
@@ -84,7 +129,8 @@ def test_hook_response_does_not_wait_for_forwarded_webhook():
|
|
|
84
129
|
started = time.monotonic()
|
|
85
130
|
request = Request(channel.url, data=_event_body(1), method='POST')
|
|
86
131
|
with urlopen(request, timeout=1) as response:
|
|
87
|
-
assert response.status ==
|
|
132
|
+
assert response.status == 200
|
|
133
|
+
assert response.read() == b'{}'
|
|
88
134
|
|
|
89
135
|
assert time.monotonic() - started < 0.5
|
|
90
136
|
assert forwarded.wait(timeout=1)
|
|
@@ -114,7 +160,7 @@ def test_hook_response_does_not_wait_for_a_local_listener():
|
|
|
114
160
|
release_listener = threading.Event()
|
|
115
161
|
channel = FFLHookEventChannel()
|
|
116
162
|
channel.on(
|
|
117
|
-
'/
|
|
163
|
+
'/transfer/progress',
|
|
118
164
|
lambda event: (listener_started.set(), release_listener.wait(timeout=5)),
|
|
119
165
|
)
|
|
120
166
|
|
|
@@ -122,7 +168,8 @@ def test_hook_response_does_not_wait_for_a_local_listener():
|
|
|
122
168
|
started = time.monotonic()
|
|
123
169
|
request = Request(channel.url, data=_event_body(1), method='POST')
|
|
124
170
|
with urlopen(request, timeout=1) as response:
|
|
125
|
-
assert response.status ==
|
|
171
|
+
assert response.status == 200
|
|
172
|
+
assert response.read() == b'{}'
|
|
126
173
|
|
|
127
174
|
assert time.monotonic() - started < 0.5
|
|
128
175
|
assert listener_started.wait(timeout=1)
|
|
@@ -15,8 +15,12 @@ class _HookHandler(BaseHTTPRequestHandler):
|
|
|
15
15
|
content_length = int(self.headers.get('Content-Length', '0'))
|
|
16
16
|
self.__class__.requests.append((self.path, self.rfile.read(content_length)))
|
|
17
17
|
self.__class__.received_event.set()
|
|
18
|
-
|
|
18
|
+
body = b'{}'
|
|
19
|
+
self.send_response(200)
|
|
20
|
+
self.send_header('Content-Type', 'application/json')
|
|
21
|
+
self.send_header('Content-Length', str(len(body)))
|
|
19
22
|
self.end_headers()
|
|
23
|
+
self.wfile.write(body)
|
|
20
24
|
|
|
21
25
|
def log_message(self, format_string: str, *args) -> None:
|
|
22
26
|
del format_string, args
|
|
@@ -50,6 +54,12 @@ def test_share_sends_events_to_hook_url(tmp_path: Path, monkeypatch):
|
|
|
50
54
|
if event.name == '/hook/server/endpoints/register'
|
|
51
55
|
]
|
|
52
56
|
assert endpoint_events
|
|
57
|
+
completed = [
|
|
58
|
+
event.name
|
|
59
|
+
for event in session.event_history
|
|
60
|
+
if event.semantic_name == 'completed'
|
|
61
|
+
]
|
|
62
|
+
assert completed == ['/transfer/complete']
|
|
53
63
|
replayed = []
|
|
54
64
|
session.on_raw('/hook/server/endpoints/register', replayed.append)
|
|
55
65
|
assert replayed == endpoint_events
|
|
@@ -286,4 +286,30 @@ def test_download_parser_recognizes_p2p_tcp_output(tmp_path: Path):
|
|
|
286
286
|
|
|
287
287
|
result = FFLResultParser.parse_download(process, None, tmp_path)
|
|
288
288
|
|
|
289
|
+
assert result.transfer_mode is ffl.TransferMode.P2P_TCP
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def test_download_parser_recognizes_p2p_quic_output(tmp_path: Path):
|
|
293
|
+
process = ffl.APEProcessResult(
|
|
294
|
+
('download', 'https://example.test/file'),
|
|
295
|
+
0,
|
|
296
|
+
'Using P2P UDP/QUIC download\nDownloaded: file.bin',
|
|
297
|
+
'',
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
result = FFLResultParser.parse_download(process, None, tmp_path)
|
|
301
|
+
|
|
302
|
+
assert result.transfer_mode is ffl.TransferMode.P2P_QUIC
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def test_download_parser_recognizes_webrtc_p2p_output(tmp_path: Path):
|
|
306
|
+
process = ffl.APEProcessResult(
|
|
307
|
+
('download', 'https://example.test/file'),
|
|
308
|
+
0,
|
|
309
|
+
'Using WebRTC P2P download\nDownloaded: file.bin',
|
|
310
|
+
'',
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
result = FFLResultParser.parse_download(process, None, tmp_path)
|
|
314
|
+
|
|
289
315
|
assert result.transfer_mode is ffl.TransferMode.WEBRTC_P2P
|
ffl_python-0.1.4/README.md
DELETED
|
@@ -1,190 +0,0 @@
|
|
|
1
|
-
# ffl-python
|
|
2
|
-
|
|
3
|
-
Python binding for FastFileLink. The package bundles the portable `ffl.com` APE and
|
|
4
|
-
runs it behind a Python API; callers do not need to locate or install a separate FFL
|
|
5
|
-
binary.
|
|
6
|
-
|
|
7
|
-
The low-level command grammar is generated by APEBind from `binding/ffl.apebind.yaml`.
|
|
8
|
-
The public API in `src/ffl/client.py` is intentionally handwritten so FFL-specific
|
|
9
|
-
library semantics stay explicit and reviewable.
|
|
10
|
-
|
|
11
|
-
## Development
|
|
12
|
-
|
|
13
|
-
```bash
|
|
14
|
-
python -m venv .venv
|
|
15
|
-
.venv/Scripts/python -m pip install -e ".[dev]" # Windows
|
|
16
|
-
# or: .venv/bin/python -m pip install -e ".[dev]"
|
|
17
|
-
|
|
18
|
-
.\scripts\test.ps1 # Windows
|
|
19
|
-
# or: ./scripts/test.sh # Linux/macOS
|
|
20
|
-
|
|
21
|
-
.\scripts\build.ps1 # Windows
|
|
22
|
-
# or: ./scripts/build.sh # Linux/macOS
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
The test directly imports `ffl`, shares a binary file, and downloads it through the
|
|
26
|
-
bundled `ffl.com` APE. It requires working network access to FastFileLink. A wheel is a
|
|
27
|
-
build artifact, not the source of truth.
|
|
28
|
-
|
|
29
|
-
The integration suite covers ordinary, E2EE, relay, pickup-code, and public-key
|
|
30
|
-
transfers; text, bytes, folders, multiple files, QR images, hooks, explicit ports, and
|
|
31
|
-
session shutdown, and Basic Auth download.
|
|
32
|
-
|
|
33
|
-
## Share
|
|
34
|
-
|
|
35
|
-
```python
|
|
36
|
-
import ffl
|
|
37
|
-
|
|
38
|
-
with ffl.share("release.zip", max_downloads=1, timeout_seconds=1800) as session:
|
|
39
|
-
print(session.link)
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
`share()` always requests a foreground FFL process and disables clipboard side effects,
|
|
43
|
-
which gives library callers a deterministic `ShareSession` they can stop or keep alive.
|
|
44
|
-
FFL's runtime-owned `--json` output is used internally to wait until `session.link` is
|
|
45
|
-
ready.
|
|
46
|
-
|
|
47
|
-
Multiple files are accepted directly:
|
|
48
|
-
|
|
49
|
-
```python
|
|
50
|
-
session = ffl.share(["one.txt", "two.txt"], name="files.zip")
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
Text and bytes helpers own their temporary file until the share session is closed:
|
|
54
|
-
|
|
55
|
-
```python
|
|
56
|
-
with ffl.share_text("hello", name="hello.txt") as session:
|
|
57
|
-
print(session.link)
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
Optional-value FFL flags use natural Python values. For example, `receipt=True` emits
|
|
61
|
-
`--receipt` without a value, while `receipt="me@example.com"` emits the flag with the
|
|
62
|
-
address.
|
|
63
|
-
|
|
64
|
-
### Stream a source without a temporary file
|
|
65
|
-
|
|
66
|
-
`share_stream()` passes a binary file-like object directly to FFL stdin. It is useful
|
|
67
|
-
for database dumps, generated artifacts, and other data that should not first be
|
|
68
|
-
materialized as a separate temporary file:
|
|
69
|
-
|
|
70
|
-
```python
|
|
71
|
-
with open("backup.tar", "rb") as source:
|
|
72
|
-
with ffl.share_stream(source, name="backup.tar") as session:
|
|
73
|
-
print(session.link)
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
## Download
|
|
77
|
-
|
|
78
|
-
```python
|
|
79
|
-
result = ffl.download("https://example.fastfilelink/...", output_path="download.bin")
|
|
80
|
-
print(result.output_path)
|
|
81
|
-
print(result.transfer_mode)
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
`download()` waits for the foreground FFL process to finish and returns a
|
|
85
|
-
`DownloadResult`. For cancellation, progress, or caller-controlled timeouts, use
|
|
86
|
-
`start_download()` and manage its `DownloadSession` explicitly.
|
|
87
|
-
|
|
88
|
-
### Stream a download
|
|
89
|
-
|
|
90
|
-
`download_stream()` exposes FFL stdout as binary chunks without buffering the whole
|
|
91
|
-
file in memory:
|
|
92
|
-
|
|
93
|
-
```python
|
|
94
|
-
with ffl.download_stream("https://example.fastfilelink/...") as transfer:
|
|
95
|
-
with open("output.bin", "wb") as target:
|
|
96
|
-
for chunk in transfer.iter_bytes():
|
|
97
|
-
target.write(chunk)
|
|
98
|
-
|
|
99
|
-
result = transfer.wait()
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
Consume `iter_bytes()` through EOF before calling `wait()`. Calling `wait()` with
|
|
103
|
-
unconsumed streamed stdout raises `RuntimeError`; this avoids silently discarding
|
|
104
|
-
binary data or deadlocking when the child process fills its stdout pipe.
|
|
105
|
-
|
|
106
|
-
## Authentication secrets
|
|
107
|
-
|
|
108
|
-
For shares protected with HTTP Basic Auth, FFL supports `FFL_AUTH_PASSWORD`. Set it in
|
|
109
|
-
the application environment and pass only `auth_user` to keep the password out of the
|
|
110
|
-
FFL command line:
|
|
111
|
-
|
|
112
|
-
```bash
|
|
113
|
-
export FFL_AUTH_PASSWORD='use-your-secret-manager'
|
|
114
|
-
```
|
|
115
|
-
|
|
116
|
-
```powershell
|
|
117
|
-
$env:FFL_AUTH_PASSWORD = 'use-your-secret-manager'
|
|
118
|
-
```
|
|
119
|
-
|
|
120
|
-
```python
|
|
121
|
-
with ffl.share("release.zip", auth_user="deploy") as session:
|
|
122
|
-
print(session.link)
|
|
123
|
-
```
|
|
124
|
-
|
|
125
|
-
Do not also pass `auth_password=` when using this pattern: FFL gives the explicit CLI
|
|
126
|
-
option precedence over `FFL_AUTH_PASSWORD`. The environment variable applies to the
|
|
127
|
-
sharing side; pass download credentials explicitly when downloading a protected link.
|
|
128
|
-
|
|
129
|
-
## Key generation
|
|
130
|
-
|
|
131
|
-
```python
|
|
132
|
-
result = ffl.keygen("alice")
|
|
133
|
-
print(result.public_key_path)
|
|
134
|
-
print(result.private_key_path)
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
`keygen()` has a 60-second process timeout and verifies that the key paths reported by
|
|
138
|
-
FFL exist.
|
|
139
|
-
|
|
140
|
-
## Version and raw access
|
|
141
|
-
|
|
142
|
-
```python
|
|
143
|
-
print(ffl.version())
|
|
144
|
-
result = ffl.raw(["download", "--help"])
|
|
145
|
-
```
|
|
146
|
-
|
|
147
|
-
`raw()` is the escape hatch for new FFL options or commands that the semantic API has
|
|
148
|
-
not adopted yet.
|
|
149
|
-
|
|
150
|
-
## WSL2
|
|
151
|
-
|
|
152
|
-
If an operation fails with `TLSError([0x6300])`, WSL may be routing the bundled
|
|
153
|
-
`.com` APE through Windows interop. Run the following in WSL, then restart the WSL
|
|
154
|
-
session:
|
|
155
|
-
|
|
156
|
-
```bash
|
|
157
|
-
sudo sh -c 'echo -1 > /proc/sys/fs/binfmt_misc/WSLInterop'
|
|
158
|
-
```
|
|
159
|
-
|
|
160
|
-
## Updating the FFL binding
|
|
161
|
-
|
|
162
|
-
Install APEBind from its source project. When adopting a new `ffl.com`, first inspect the
|
|
163
|
-
CLI into a raw discovery file:
|
|
164
|
-
|
|
165
|
-
```bash
|
|
166
|
-
./scripts/inspect.sh /path/to/ffl.com
|
|
167
|
-
```
|
|
168
|
-
|
|
169
|
-
This writes `binding/ffl.discovered.apebind.yaml` and automatically applies the hidden
|
|
170
|
-
command seeds in `binding/ffl.commands.yaml`. Review the discovered-schema diff, then
|
|
171
|
-
manually merge CLI changes into the canonical semantic contract
|
|
172
|
-
`binding/ffl.apebind.yaml`. Automatic inspection never overwrites the semantic contract.
|
|
173
|
-
|
|
174
|
-
After reviewing the semantic schema, regenerate the low-level binding:
|
|
175
|
-
|
|
176
|
-
```bash
|
|
177
|
-
./scripts/regenerate.sh /path/to/ffl.com
|
|
178
|
-
# Windows: .\scripts\regenerate.ps1 D:\ffl.com
|
|
179
|
-
```
|
|
180
|
-
|
|
181
|
-
The script invokes APEBind into an isolated temporary project and replaces only these
|
|
182
|
-
machine-owned files:
|
|
183
|
-
|
|
184
|
-
- `src/ffl/_generated.py`
|
|
185
|
-
- `src/ffl/_runtime.py`
|
|
186
|
-
- `src/ffl/bin/ffl.com`
|
|
187
|
-
- `src/ffl/py.typed`
|
|
188
|
-
|
|
189
|
-
It deliberately does not overwrite `client.py`, `models.py`, parsing logic, tests, or the
|
|
190
|
-
semantic schema.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|