flyfile 0.1.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.
- flyfile-0.1.0/.github/workflows/gh-release.yml +31 -0
- flyfile-0.1.0/.github/workflows/python-publish.yml +48 -0
- flyfile-0.1.0/.gitignore +9 -0
- flyfile-0.1.0/PKG-INFO +78 -0
- flyfile-0.1.0/README.md +61 -0
- flyfile-0.1.0/docs/README.md +37 -0
- flyfile-0.1.0/docs/api.md +43 -0
- flyfile-0.1.0/docs/architecture.md +69 -0
- flyfile-0.1.0/docs/cli.md +70 -0
- flyfile-0.1.0/docs/flaxkv2-feedback.md +42 -0
- flyfile-0.1.0/pyproject.toml +39 -0
- flyfile-0.1.0/scripts/bench.sh +51 -0
- flyfile-0.1.0/src/flyfile/__init__.py +1 -0
- flyfile-0.1.0/src/flyfile/cli/__init__.py +0 -0
- flyfile-0.1.0/src/flyfile/cli/main.py +407 -0
- flyfile-0.1.0/src/flyfile/cli/output.py +98 -0
- flyfile-0.1.0/src/flyfile/client/__init__.py +0 -0
- flyfile-0.1.0/src/flyfile/client/client.py +366 -0
- flyfile-0.1.0/src/flyfile/client/errors.py +54 -0
- flyfile-0.1.0/src/flyfile/client/transfer.py +231 -0
- flyfile-0.1.0/src/flyfile/core/__init__.py +1 -0
- flyfile-0.1.0/src/flyfile/core/compress.py +78 -0
- flyfile-0.1.0/src/flyfile/core/hashing.py +14 -0
- flyfile-0.1.0/src/flyfile/core/ids.py +26 -0
- flyfile-0.1.0/src/flyfile/core/tarstream.py +81 -0
- flyfile-0.1.0/src/flyfile/server/__init__.py +0 -0
- flyfile-0.1.0/src/flyfile/server/app.py +68 -0
- flyfile-0.1.0/src/flyfile/server/config.py +16 -0
- flyfile-0.1.0/src/flyfile/server/errors.py +18 -0
- flyfile-0.1.0/src/flyfile/server/relay.py +111 -0
- flyfile-0.1.0/src/flyfile/server/routes_objects.py +273 -0
- flyfile-0.1.0/src/flyfile/server/routes_uploads.py +111 -0
- flyfile-0.1.0/src/flyfile/server/store.py +262 -0
- flyfile-0.1.0/tests/__init__.py +0 -0
- flyfile-0.1.0/tests/conftest.py +61 -0
- flyfile-0.1.0/tests/test_api.py +198 -0
- flyfile-0.1.0/tests/test_c2c.py +85 -0
- flyfile-0.1.0/tests/test_cli.py +101 -0
- flyfile-0.1.0/tests/test_lifecycle.py +57 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
on:
|
|
2
|
+
push:
|
|
3
|
+
tags:
|
|
4
|
+
- 'v*'
|
|
5
|
+
|
|
6
|
+
name: Create Release
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: write
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
build:
|
|
13
|
+
name: Create Release
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
steps:
|
|
16
|
+
- name: Checkout code
|
|
17
|
+
uses: actions/checkout@v4
|
|
18
|
+
|
|
19
|
+
- name: Extract tag name
|
|
20
|
+
id: extract_tag
|
|
21
|
+
run: echo "TAG_NAME=$(echo ${GITHUB_REF#refs/tags/})" >> $GITHUB_ENV
|
|
22
|
+
|
|
23
|
+
- name: Create Release and Generate Notes
|
|
24
|
+
id: create_release
|
|
25
|
+
uses: softprops/action-gh-release@v1
|
|
26
|
+
with:
|
|
27
|
+
token: ${{ secrets.GITHUB_TOKEN }}
|
|
28
|
+
name: Release ${{ env.TAG_NAME }}
|
|
29
|
+
draft: false
|
|
30
|
+
prerelease: false
|
|
31
|
+
generate_release_notes: true
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
name: Upload Python Package
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_dispatch:
|
|
5
|
+
push:
|
|
6
|
+
tags:
|
|
7
|
+
- 'v*'
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
release-build:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
|
|
17
|
+
- uses: actions/setup-python@v5
|
|
18
|
+
with:
|
|
19
|
+
python-version: "3.10"
|
|
20
|
+
|
|
21
|
+
- name: build release distributions
|
|
22
|
+
run: |
|
|
23
|
+
python -m pip install --upgrade pip
|
|
24
|
+
pip install build
|
|
25
|
+
python -m build
|
|
26
|
+
|
|
27
|
+
- name: upload dists
|
|
28
|
+
uses: actions/upload-artifact@v4
|
|
29
|
+
with:
|
|
30
|
+
name: release-dists
|
|
31
|
+
path: dist/
|
|
32
|
+
|
|
33
|
+
pypi-publish:
|
|
34
|
+
runs-on: ubuntu-latest
|
|
35
|
+
needs:
|
|
36
|
+
- release-build
|
|
37
|
+
permissions:
|
|
38
|
+
id-token: write
|
|
39
|
+
|
|
40
|
+
steps:
|
|
41
|
+
- name: Retrieve release distributions
|
|
42
|
+
uses: actions/download-artifact@v4
|
|
43
|
+
with:
|
|
44
|
+
name: release-dists
|
|
45
|
+
path: dist/
|
|
46
|
+
|
|
47
|
+
- name: Publish release distributions to PyPI
|
|
48
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
flyfile-0.1.0/.gitignore
ADDED
flyfile-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: flyfile
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Agent-native data transfer: push/pull/send anything between agents and machines
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Requires-Dist: fastapi>=0.110
|
|
7
|
+
Requires-Dist: flaxkv2>=0.2.14
|
|
8
|
+
Requires-Dist: httpx>=0.27
|
|
9
|
+
Requires-Dist: pyyaml>=6.0
|
|
10
|
+
Requires-Dist: typer>=0.12
|
|
11
|
+
Requires-Dist: uvicorn[standard]>=0.29
|
|
12
|
+
Requires-Dist: zstandard>=0.22
|
|
13
|
+
Provides-Extra: dev
|
|
14
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
15
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# flyfile
|
|
19
|
+
|
|
20
|
+
Agent-native data transfer. Push/pull anything (text, files, directories) through a central
|
|
21
|
+
server, or stream it directly client-to-client. Built for AI agents: JSON output everywhere,
|
|
22
|
+
stable exit codes, content-addressed dedup, burn-after-read.
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install flyfile
|
|
26
|
+
|
|
27
|
+
# server (single worker; data lives in LMDB via flaxkv2)
|
|
28
|
+
flyfile serve --port 8632 --token SECRET
|
|
29
|
+
|
|
30
|
+
# client
|
|
31
|
+
export FLYFILE_SERVER=http://host:8632 FLYFILE_TOKEN=SECRET
|
|
32
|
+
echo "build log" | flyfile push - --name buildlog --tag ci --ttl 2h
|
|
33
|
+
flyfile push ./model.bin --reads 1 # burn after one read
|
|
34
|
+
flyfile push ./dataset/ # dirs stream as tar, no temp files
|
|
35
|
+
flyfile ls --tag ci --json
|
|
36
|
+
flyfile preview k3x9m2pq # peek without consuming reads
|
|
37
|
+
flyfile pull k3x9m2pq -o ./model.bin
|
|
38
|
+
|
|
39
|
+
# client → client (server relays the stream, nothing is stored)
|
|
40
|
+
flyfile send ./results/ # prints: code: amber-falcon
|
|
41
|
+
flyfile recv amber-falcon # on the other machine
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Agent contract
|
|
45
|
+
|
|
46
|
+
- **JSON everywhere**: `--json`, or automatic when stdout is not a TTY. Progress goes to
|
|
47
|
+
stderr, data to stdout. Never prompts.
|
|
48
|
+
- **Exit codes (stable)**: 0 ok · 2 usage · 3 not found · 4 auth · 5 conflict ·
|
|
49
|
+
6 expired/burned · 7 network (retryable) · 1 other.
|
|
50
|
+
- **Errors** are JSON on stderr: `{"error", "message", "retryable", "suggestion"}`.
|
|
51
|
+
- **Idempotent push**: content-addressed (sha256). Re-pushing the same bytes is instant
|
|
52
|
+
(`"deduped": true`).
|
|
53
|
+
- **`flyfile preview <id>`** reads the head of an object (or a dir's file manifest)
|
|
54
|
+
without downloading and without consuming burn-after-read counts.
|
|
55
|
+
- **`flyfile schema [cmd]`** dumps the command tree as JSON for introspection.
|
|
56
|
+
- **`flyfile send --json`** emits NDJSON events; the `code` event arrives before the
|
|
57
|
+
transfer starts, so an agent can hand it to the receiver immediately.
|
|
58
|
+
|
|
59
|
+
## Design notes
|
|
60
|
+
|
|
61
|
+
- Storage is [flaxkv2](https://github.com/KenyonY/flaxkv) (LMDB): metadata and 8 MiB
|
|
62
|
+
content chunks in one env. The LMDB file does not shrink after deletes (free pages are
|
|
63
|
+
reused; file size ≈ historical peak).
|
|
64
|
+
- Compression (zstd-3) happens on the *client*; the server stores/relays compressed bytes.
|
|
65
|
+
Each 8 MiB chunk of a large upload is an independent zstd frame, so parallel upload,
|
|
66
|
+
resume, and parallel download all work per-chunk.
|
|
67
|
+
- Burn-after-read: the read is claimed atomically when chunk 0 (or `/content`) is fetched.
|
|
68
|
+
Later chunks of an in-flight parallel download are served during a grace period
|
|
69
|
+
(default 15 min) even after the object burns.
|
|
70
|
+
- Run exactly **one** uvicorn worker: relay pairing and the burn-claim lock are in-process.
|
|
71
|
+
|
|
72
|
+
## Development
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
uv venv && uv pip install -e ".[dev]"
|
|
76
|
+
.venv/bin/pytest
|
|
77
|
+
scripts/bench.sh # 1 GiB loopback throughput smoke test
|
|
78
|
+
```
|
flyfile-0.1.0/README.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# flyfile
|
|
2
|
+
|
|
3
|
+
Agent-native data transfer. Push/pull anything (text, files, directories) through a central
|
|
4
|
+
server, or stream it directly client-to-client. Built for AI agents: JSON output everywhere,
|
|
5
|
+
stable exit codes, content-addressed dedup, burn-after-read.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install flyfile
|
|
9
|
+
|
|
10
|
+
# server (single worker; data lives in LMDB via flaxkv2)
|
|
11
|
+
flyfile serve --port 8632 --token SECRET
|
|
12
|
+
|
|
13
|
+
# client
|
|
14
|
+
export FLYFILE_SERVER=http://host:8632 FLYFILE_TOKEN=SECRET
|
|
15
|
+
echo "build log" | flyfile push - --name buildlog --tag ci --ttl 2h
|
|
16
|
+
flyfile push ./model.bin --reads 1 # burn after one read
|
|
17
|
+
flyfile push ./dataset/ # dirs stream as tar, no temp files
|
|
18
|
+
flyfile ls --tag ci --json
|
|
19
|
+
flyfile preview k3x9m2pq # peek without consuming reads
|
|
20
|
+
flyfile pull k3x9m2pq -o ./model.bin
|
|
21
|
+
|
|
22
|
+
# client → client (server relays the stream, nothing is stored)
|
|
23
|
+
flyfile send ./results/ # prints: code: amber-falcon
|
|
24
|
+
flyfile recv amber-falcon # on the other machine
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Agent contract
|
|
28
|
+
|
|
29
|
+
- **JSON everywhere**: `--json`, or automatic when stdout is not a TTY. Progress goes to
|
|
30
|
+
stderr, data to stdout. Never prompts.
|
|
31
|
+
- **Exit codes (stable)**: 0 ok · 2 usage · 3 not found · 4 auth · 5 conflict ·
|
|
32
|
+
6 expired/burned · 7 network (retryable) · 1 other.
|
|
33
|
+
- **Errors** are JSON on stderr: `{"error", "message", "retryable", "suggestion"}`.
|
|
34
|
+
- **Idempotent push**: content-addressed (sha256). Re-pushing the same bytes is instant
|
|
35
|
+
(`"deduped": true`).
|
|
36
|
+
- **`flyfile preview <id>`** reads the head of an object (or a dir's file manifest)
|
|
37
|
+
without downloading and without consuming burn-after-read counts.
|
|
38
|
+
- **`flyfile schema [cmd]`** dumps the command tree as JSON for introspection.
|
|
39
|
+
- **`flyfile send --json`** emits NDJSON events; the `code` event arrives before the
|
|
40
|
+
transfer starts, so an agent can hand it to the receiver immediately.
|
|
41
|
+
|
|
42
|
+
## Design notes
|
|
43
|
+
|
|
44
|
+
- Storage is [flaxkv2](https://github.com/KenyonY/flaxkv) (LMDB): metadata and 8 MiB
|
|
45
|
+
content chunks in one env. The LMDB file does not shrink after deletes (free pages are
|
|
46
|
+
reused; file size ≈ historical peak).
|
|
47
|
+
- Compression (zstd-3) happens on the *client*; the server stores/relays compressed bytes.
|
|
48
|
+
Each 8 MiB chunk of a large upload is an independent zstd frame, so parallel upload,
|
|
49
|
+
resume, and parallel download all work per-chunk.
|
|
50
|
+
- Burn-after-read: the read is claimed atomically when chunk 0 (or `/content`) is fetched.
|
|
51
|
+
Later chunks of an in-flight parallel download are served during a grace period
|
|
52
|
+
(default 15 min) even after the object burns.
|
|
53
|
+
- Run exactly **one** uvicorn worker: relay pairing and the burn-claim lock are in-process.
|
|
54
|
+
|
|
55
|
+
## Development
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
uv venv && uv pip install -e ".[dev]"
|
|
59
|
+
.venv/bin/pytest
|
|
60
|
+
scripts/bench.sh # 1 GiB loopback throughput smoke test
|
|
61
|
+
```
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# flyfile 文档
|
|
2
|
+
|
|
3
|
+
面向 AI agent 的数据传输系统:客户端 ↔ 服务端存取 + 客户端 → 客户端直传,
|
|
4
|
+
支持字符串/文件/文件夹,阅后即焚/TTL/永久,元数据可查询。
|
|
5
|
+
|
|
6
|
+
## 文档目录
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
docs/
|
|
10
|
+
├── README.md # 本文档:总览与目录
|
|
11
|
+
├── architecture.md # 架构:存储模型、传输机制、生命周期、C2C 中转
|
|
12
|
+
├── cli.md # CLI 参考:命令、退出码契约、agent 使用范式
|
|
13
|
+
├── api.md # HTTP API 参考
|
|
14
|
+
└── flaxkv2-feedback.md # dogfooding 过程中发现的 flaxkv2 改进点
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## 核心心智模型
|
|
18
|
+
|
|
19
|
+
- **一切皆 object**:text / file / dir 统一对象模型,dir 以流式 tar 传输(不落临时文件)
|
|
20
|
+
- **元数据一等公民**:name/tags/description/sha256/TTL/阅后即焚次数全部可查询
|
|
21
|
+
- **CLI 即 API 契约**:JSON 输出 + 稳定退出码,agent 直接消费
|
|
22
|
+
- **chunk 原生**:存储与传输的最小单元都是 8 MiB 块——并行、续传、秒传共用一套机制
|
|
23
|
+
- **单一存储引擎**:flaxkv2 (LMDB),元数据与数据块同一个 env,跨表原子写
|
|
24
|
+
|
|
25
|
+
## 快速开始
|
|
26
|
+
|
|
27
|
+
见根目录 [README.md](../README.md)。
|
|
28
|
+
|
|
29
|
+
## 性能基线(loopback,1 GiB 不可压随机数据)
|
|
30
|
+
|
|
31
|
+
| 操作 | 吞吐 |
|
|
32
|
+
|---|---|
|
|
33
|
+
| push(4 并发分块) | ~420 MiB/s |
|
|
34
|
+
| pull(4 并发分块) | ~467 MiB/s |
|
|
35
|
+
| 重复 push(秒传) | 0.7s(纯本地 hash 时间) |
|
|
36
|
+
|
|
37
|
+
运行 `scripts/bench.sh` 复现。实际瓶颈在网络带宽,不在 flyfile。
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# HTTP API 参考
|
|
2
|
+
|
|
3
|
+
前缀 `/api/v1`,除 `/health` 外均需 `Authorization: Bearer <token>`(token 为空则不校验)。
|
|
4
|
+
错误体统一:`{"error": "<code>", "message": "...", "retryable": bool, "suggestion"?}`。
|
|
5
|
+
|
|
6
|
+
## 对象
|
|
7
|
+
|
|
8
|
+
| 方法 | 路径 | 说明 |
|
|
9
|
+
|---|---|---|
|
|
10
|
+
| HEAD | `/blobs/{sha256}` | 秒传探测:200 已有 / 404 |
|
|
11
|
+
| PUT | `/objects/stream` | 流式上传。元数据走 `x-ff-*` header(见下),body 为存储态字节流(chunked,无需 Content-Length)。返回 201 + 对象 JSON(含 `deduped`) |
|
|
12
|
+
| GET | `/objects` | 查询。`name`(glob)、`tag`(可重复,AND)、`kind`、`created_after/before`(epoch 秒)、`limit`(默认 50)、`offset` |
|
|
13
|
+
| GET | `/objects/{id}` | 元数据(含 num_chunks/chunking/compression,供下载规划)。404 不存在 / 410 已死 |
|
|
14
|
+
| GET | `/objects/{id}/chunks/{idx}` | 下载第 idx 块(存储态字节)。**idx=0 原子认领 1 次 read**,失败 410;idx>0 不检查(宽限期语义) |
|
|
15
|
+
| GET | `/objects/{id}/content` | 单流下载(全部块拼接,存储态),计 1 次 read。响应头 `x-ff-compression/sha256/size/kind/name` |
|
|
16
|
+
| GET | `/objects/{id}/preview?bytes=N` | 不计 read。text/file → `{is_text, text, hex_head}`;dir → `{manifest}` |
|
|
17
|
+
| PATCH | `/objects/{id}` | 更新 name/tags/description/manifest/ttl |
|
|
18
|
+
| DELETE | `/objects/{id}` | 删除,blob refcount-- |
|
|
19
|
+
|
|
20
|
+
`x-ff-*` 上传 header:`name`(URL-quoted)、`kind`(text/file/dir)、`compression`(none/zstd)、
|
|
21
|
+
`tags`(逗号分隔,各自 URL-quoted)、`description`(URL-quoted)、`ttl`(秒)、`max-reads`、`content-type`。
|
|
22
|
+
|
|
23
|
+
## 分块上传
|
|
24
|
+
|
|
25
|
+
| 方法 | 路径 | 说明 |
|
|
26
|
+
|---|---|---|
|
|
27
|
+
| POST | `/uploads` | `{sha256, size, chunk_size, compression, meta}`。blob 已存在 → 201 `{object, deduped: true}`(秒传);否则 `{upload_id, num_chunks}` |
|
|
28
|
+
| PUT | `/uploads/{id}/chunks/{idx}` | body 为该块存储态字节(aligned:独立 zstd frame)。幂等 |
|
|
29
|
+
| GET | `/uploads/{id}` | `{received: [...]}` 供断点续传 |
|
|
30
|
+
| POST | `/uploads/{id}/complete` | 服务端流式解压校验 sha256。422 校验失败(清理会话);409 缺块 |
|
|
31
|
+
|
|
32
|
+
## C2C 中转
|
|
33
|
+
|
|
34
|
+
| 方法 | 路径 | 说明 |
|
|
35
|
+
|---|---|---|
|
|
36
|
+
| POST | `/channels/{code}/send?timeout=N` | body 流式转发给接收端,元数据走 `x-ff-*`。409 code 被占 / 408 无人接收 |
|
|
37
|
+
| GET | `/channels/{code}/recv?timeout=N` | 流式响应,`x-ff-*` 回填。408 无发送方 |
|
|
38
|
+
| GET | `/channels/{code}` | `{state: none / waiting_sender / waiting_receiver / active}` |
|
|
39
|
+
|
|
40
|
+
## 其它
|
|
41
|
+
|
|
42
|
+
- `GET /health`(免认证):`{status, version}`
|
|
43
|
+
- `GET /api/v1/stats`:`{objects, blobs, stored_bytes, logical_bytes}`
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# 架构
|
|
2
|
+
|
|
3
|
+
## 存储模型(flaxkv2 / LMDB,单 env 四个 sub_db)
|
|
4
|
+
|
|
5
|
+
| sub_db | key | value |
|
|
6
|
+
|---|---|---|
|
|
7
|
+
| objects | 对象 id(8 字符 nanoid) | 元数据 dict(name/kind/tags/sha256/expires_at/max_reads/read_count/...) |
|
|
8
|
+
| blobs | sha256(未压缩内容) | {size, stored_size, compression, chunking, num_chunks, refcount} |
|
|
9
|
+
| chunks | `{sha256}:{idx:08d}` | 该块的存储态字节(zstd frame 或 raw) |
|
|
10
|
+
| uploads | upload_id | 分块上传会话(received 列表、meta) |
|
|
11
|
+
|
|
12
|
+
- **对象与 blob 分离 + refcount**:同内容多对象共享一份数据(dedup / 秒传)
|
|
13
|
+
- **查询走全扫描 + Python 过滤**:单租户万级对象毫秒级,不维护二级索引(KISS)
|
|
14
|
+
- **跨表原子写**用 `cross_db_txn`;**条件更新**(阅后即焚认领)用进程内锁串行化——
|
|
15
|
+
cross_db_txn 是"收集-提交"模型,事务内不可读,应用层锁在单 worker 下正确且最简
|
|
16
|
+
- **已知取舍**:LMDB 文件删除后不收缩(空闲页复用,大小 ≈ 历史峰值水位)
|
|
17
|
+
|
|
18
|
+
## 传输机制
|
|
19
|
+
|
|
20
|
+
压缩(zstd-3)永远发生在**客户端**,服务端只存/转发存储态字节(零压缩 CPU)。
|
|
21
|
+
跳过压缩:扩展名黑名单(zip/jpg/mp4/...)或前 128KiB 试压比 >0.95。
|
|
22
|
+
|
|
23
|
+
两种 blob 形态(`chunking` 字段):
|
|
24
|
+
|
|
25
|
+
| | aligned | stream |
|
|
26
|
+
|---|---|---|
|
|
27
|
+
| 产生方式 | 分块上传(文件) | 流式上传(stdin/text/dir-tar) |
|
|
28
|
+
| 块边界 | 每 8MiB **原始**偏移一块,各自独立 zstd frame | 单一 zstd 流按 8MiB **存储**字节切块 |
|
|
29
|
+
| 下载 | 可并行拉块 + pwrite 定点写 + 按块续传 | 顺序流式解压 |
|
|
30
|
+
|
|
31
|
+
多个独立 zstd frame 拼接仍是合法 zstd 流,所以两种形态共用同一套顺序解压代码。
|
|
32
|
+
|
|
33
|
+
**push 决策树**(客户端):
|
|
34
|
+
- stdin / dir → `PUT /objects/stream`(边 tar 边压边传,无临时文件)
|
|
35
|
+
- 文件 → 本地 sha256 → `POST /uploads`(服务端已有 blob 则秒传返回)→
|
|
36
|
+
并发 4 PUT 各块(独立 frame,幂等可重传)→ `complete`(服务端流式解压校验 sha256)
|
|
37
|
+
- 续传:`~/.cache/flyfile/uploads.json` 按 (path,size,mtime,sha256) 找回会话,只补缺失块
|
|
38
|
+
|
|
39
|
+
**pull 决策树**:aligned 且多块 → 并行拉块、解压后 pwrite 到 `.part` 预分配文件、
|
|
40
|
+
`.part.json` 记进度、完成后整体 sha256 校验再原子 rename;否则单流 `/content`。
|
|
41
|
+
dir 边解压边 untar,事后校验 sha256。
|
|
42
|
+
|
|
43
|
+
**chunk 级网络重试**:分块路径的每个 PUT/GET 对传输层错误(连接被掐、瞬断)做
|
|
44
|
+
3 次指数退避重试——WAN 上单连接瞬断是常态(跨洋实测 13 块并发上传曾被掐掉 3 块),
|
|
45
|
+
分块 + 幂等使重试天然安全;应用层错误(4xx/410)不重试直接抛。
|
|
46
|
+
|
|
47
|
+
## 生命周期(阅后即焚 / TTL)
|
|
48
|
+
|
|
49
|
+
双保险:
|
|
50
|
+
1. **惰性过滤**:所有读路径检查 `expires_at` / `read_count < max_reads`,
|
|
51
|
+
语义正确性不依赖后台任务
|
|
52
|
+
2. **后台 sweeper**(60s 一轮):物理删除已死对象 → blob refcount-- → 为 0 删全部 chunk
|
|
53
|
+
→ 清理 24h 僵尸上传会话
|
|
54
|
+
|
|
55
|
+
read 计数在下载**开始**时原子认领(`/content` 或 chunk 0),失败返回 410。
|
|
56
|
+
并发 N+1 个请求恰好 N 个成功。**宽限期**:焚毁后 15 分钟内 chunk 仍可按 idx>0 拉取,
|
|
57
|
+
保证在途并行下载能收尾;宽限期内不接受新的读(认领已失败)。
|
|
58
|
+
|
|
59
|
+
## C2C 中转(relay)
|
|
60
|
+
|
|
61
|
+
`POST /channels/{code}/send` ↔ `GET /channels/{code}/recv`,进程内
|
|
62
|
+
`asyncio.Queue(16 × ≤1MiB)` 配对转发,**数据不经过存储层**,队列满即背压。
|
|
63
|
+
先到方等待对方(默认 300s 超时 408)。取件码 `word-word` 由客户端生成。
|
|
64
|
+
|
|
65
|
+
## 约束
|
|
66
|
+
|
|
67
|
+
- **单 uvicorn worker**:relay 配对与阅后即焚锁都在进程内。垂直扩展够用
|
|
68
|
+
(瓶颈在网络 I/O,事件循环 + to_thread 足以打满带宽)
|
|
69
|
+
- 认证是单 token Bearer,v1 单租户;token 为空则完全开放(仅限内网)
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# CLI 参考
|
|
2
|
+
|
|
3
|
+
命令 `flyfile`(别名 `ff`)。完整参数用 `flyfile <cmd> --help` 或 `flyfile schema <cmd>`(JSON)。
|
|
4
|
+
|
|
5
|
+
## Agent 契约
|
|
6
|
+
|
|
7
|
+
- **JSON 输出**:`--json` 强制;stdout 非 TTY 时自动。数据走 stdout,进度/提示走 stderr,永不交互
|
|
8
|
+
- **错误**:stderr 一行 JSON `{"error", "message", "retryable", "suggestion"}`
|
|
9
|
+
- **退出码(跨版本稳定)**:
|
|
10
|
+
|
|
11
|
+
| 码 | 含义 | | 码 | 含义 |
|
|
12
|
+
|---|---|---|---|---|
|
|
13
|
+
| 0 | 成功 | | 5 | 冲突(目标已存在 / channel 被占) |
|
|
14
|
+
| 2 | 用法错误 | | 6 | 已过期或已焚毁 (410) |
|
|
15
|
+
| 3 | 不存在 | | 7 | 网络错误(retryable=true) |
|
|
16
|
+
| 4 | 认证失败 | | 1 | 其他 |
|
|
17
|
+
|
|
18
|
+
- **幂等**:push 内容寻址(重复 push 秒传,`"deduped": true`);chunk 重传幂等;`rm --if-exists` 不存在时退 0
|
|
19
|
+
|
|
20
|
+
## 配置
|
|
21
|
+
|
|
22
|
+
优先级:命令行 `--server/--token` > 环境变量 `FLYFILE_SERVER/FLYFILE_TOKEN` >
|
|
23
|
+
`~/.flyfile/config.yaml`(`flyfile config set server http://host:18632`)。
|
|
24
|
+
|
|
25
|
+
## 命令
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
# 存取
|
|
29
|
+
echo "text" | flyfile push - --name note --ttl 2h # stdin → text 对象
|
|
30
|
+
flyfile push model.bin --reads 1 --tag ckpt # 阅后即焚
|
|
31
|
+
flyfile push ./dataset/ --description "训练集 v2" # 目录(流式 tar)
|
|
32
|
+
flyfile pull <id或名字> # text → stdout;file/dir → ./name
|
|
33
|
+
flyfile pull big100 -o out.bin --force # 名字解析:唯一命中直接用
|
|
34
|
+
flyfile pull "log*" --latest # glob + 多命中取最新;歧义时报 exit 5 并列候选
|
|
35
|
+
flyfile pull <id> -o - # 强制 stdout(可接管道)
|
|
36
|
+
|
|
37
|
+
# 查询 / 管理(info/preview 不消耗 read 次数;均支持 id 或名字 + --latest)
|
|
38
|
+
flyfile ls --tag ckpt --kind file --since 2d --json
|
|
39
|
+
flyfile info <id>
|
|
40
|
+
flyfile preview <id> --bytes 4096 # 文本头部 / dir 文件清单
|
|
41
|
+
flyfile rm <id> [--if-exists]
|
|
42
|
+
flyfile stats
|
|
43
|
+
|
|
44
|
+
# C2C 直传(不落服务器存储)
|
|
45
|
+
flyfile send ./results/ --json # NDJSON: {"event":"code","code":"amber-falcon"} → done
|
|
46
|
+
flyfile recv amber-falcon -o ./results/
|
|
47
|
+
|
|
48
|
+
# 服务端 / 自省
|
|
49
|
+
flyfile serve --port 8632 --data-dir /data --token SECRET
|
|
50
|
+
flyfile schema [cmd] # 命令树 JSON
|
|
51
|
+
flyfile version
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Agent 使用范式
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
# 1. 传结果给另一台机器上的 agent:send 的 code 事件在传输开始前就输出,
|
|
58
|
+
# 拿到 code 立即通过其它信道告知对方,无需等待传输完成
|
|
59
|
+
flyfile send big.tar --json | while read -r ev; do ... done
|
|
60
|
+
|
|
61
|
+
# 2. 先看再拉:preview 不消耗阅后即焚次数
|
|
62
|
+
flyfile preview k3x9m2pq --json | jq -r .text
|
|
63
|
+
flyfile pull k3x9m2pq
|
|
64
|
+
|
|
65
|
+
# 3. 判断可重试:exit code 7 或 error body 里 retryable=true 才值得重试
|
|
66
|
+
flyfile pull $ID || { [ $? -eq 7 ] && retry; }
|
|
67
|
+
|
|
68
|
+
# 4. 管道串联
|
|
69
|
+
flyfile pull $LOG_ID -o - | grep ERROR | flyfile push - --name errors --ttl 1h
|
|
70
|
+
```
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# flaxkv2 dogfooding 反馈(来自 flyfile 开发,flaxkv2 0.2.14)
|
|
2
|
+
|
|
3
|
+
flyfile 把 flaxkv2 用作唯一存储引擎(元数据 + 8MiB 数据块)。总体验证结论:
|
|
4
|
+
**能胜任**——8MiB value 写 2.6 GB/s / 读 4.4 GB/s(页缓存加成),map_size 自动扩容、
|
|
5
|
+
cross_db_txn、多线程并发、写删后空闲页复用全部符合预期。以下是过程中发现的改进点:
|
|
6
|
+
|
|
7
|
+
## 1. 缺少 `pop(key, default)` 方法
|
|
8
|
+
|
|
9
|
+
`RawLmdbDict` 有 `get/__contains__/__delitem__` 但没有 `pop`——这是最常用的
|
|
10
|
+
"存在即删"字典习语。flyfile 只能自己包一个 try/del/except KeyError(`store.py` 的 `_pop`)。
|
|
11
|
+
建议在 `BaseLmdbDict` 补 `pop`(顺手可加 `setdefault`)。
|
|
12
|
+
|
|
13
|
+
## 2. 实例缓存 + close 的语义陷阱(最重要)
|
|
14
|
+
|
|
15
|
+
`db_instance_manager` 让同进程内同 (name, path, sub_db) 返回**同一个实例**。
|
|
16
|
+
后果:A 处 `close()` 会把 B 处正在用的"另一个"db 一起关掉(`self._db = None`,
|
|
17
|
+
后续访问报 `AttributeError: 'NoneType' object has no attribute 'get'`)。
|
|
18
|
+
|
|
19
|
+
flyfile 测试里踩到:测试代码对同一 data_dir 开了第二个 Store 并 close,
|
|
20
|
+
把正在运行的服务端句柄关了。对使用者来说这是很难排查的 spooky action at a distance。
|
|
21
|
+
|
|
22
|
+
建议之一:close 也走引用计数(第 N 次 close 才真关);或 close 后自动失效重开;
|
|
23
|
+
或至少在文档里显著标注"同参数实例是共享的,close 影响所有持有者"。
|
|
24
|
+
|
|
25
|
+
## 3. `keys()` / `items()` 返回完整 list
|
|
26
|
+
|
|
27
|
+
大库上容易误用导致内存峰值。`keys_iter()/items_iter()` 已经存在且好用(还支持
|
|
28
|
+
prefix/start/stop),但默认名字 `items()` 更顺手、更危险。建议文档强调,或让
|
|
29
|
+
`items()` 返回迭代器(破坏兼容,可放大版本)。
|
|
30
|
+
|
|
31
|
+
## 4. cross_db_txn 是"收集-提交"模型,事务内不可读
|
|
32
|
+
|
|
33
|
+
做"读-判断-写"条件更新(flyfile 的阅后即焚计数认领)时用不上,只能退回应用层锁。
|
|
34
|
+
LMDB 本身的写事务是支持事务内读的;如果 cross_db_txn 能提供 `txn.get()`(在同一
|
|
35
|
+
write txn 里读),条件更新就能做到真正的存储层原子。这是把 flaxkv2 用于
|
|
36
|
+
"并发状态机"类场景(计数器、队列认领、乐观锁)的关键缺口。
|
|
37
|
+
|
|
38
|
+
## 5. 小点
|
|
39
|
+
|
|
40
|
+
- `keys_count()` 很实用,但藏得深,SKILL.md 没提
|
|
41
|
+
- 8MiB bytes value 实测无问题,SKILL.md 中"不要存大对象(超过几 MB 考虑外存)"
|
|
42
|
+
的表述可以放宽为具体数字(例如"单 value 建议 ≤16MiB,分块场景实测 8MiB 无压力")
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "flyfile"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Agent-native data transfer: push/pull/send anything between agents and machines"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"fastapi>=0.110",
|
|
13
|
+
"uvicorn[standard]>=0.29",
|
|
14
|
+
"httpx>=0.27",
|
|
15
|
+
"typer>=0.12",
|
|
16
|
+
"zstandard>=0.22",
|
|
17
|
+
"flaxkv2>=0.2.14",
|
|
18
|
+
"pyyaml>=6.0",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[project.optional-dependencies]
|
|
22
|
+
dev = ["pytest>=8", "pytest-asyncio>=0.23"]
|
|
23
|
+
|
|
24
|
+
[project.scripts]
|
|
25
|
+
flyfile = "flyfile.cli.main:app"
|
|
26
|
+
ff = "flyfile.cli.main:app"
|
|
27
|
+
|
|
28
|
+
[tool.hatch.build.targets.wheel]
|
|
29
|
+
packages = ["src/flyfile"]
|
|
30
|
+
|
|
31
|
+
[tool.pytest.ini_options]
|
|
32
|
+
testpaths = ["tests"]
|
|
33
|
+
asyncio_mode = "auto"
|
|
34
|
+
|
|
35
|
+
[tool.ruff]
|
|
36
|
+
line-length = 120
|
|
37
|
+
|
|
38
|
+
[tool.ruff.lint]
|
|
39
|
+
select = ["E4", "E7", "E9", "F", "B006"]
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# 1 GiB loopback 吞吐冒烟:目标是瓶颈在磁盘/回环而不在 flyfile 代码。
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
cd "$(dirname "$0")/.."
|
|
5
|
+
|
|
6
|
+
PY=.venv/bin/python
|
|
7
|
+
FF=.venv/bin/flyfile
|
|
8
|
+
WORK=$(mktemp -d)
|
|
9
|
+
DATA=$WORK/data
|
|
10
|
+
PORT=18632
|
|
11
|
+
trap 'kill $SERVER_PID 2>/dev/null || true; rm -rf "$WORK"' EXIT
|
|
12
|
+
|
|
13
|
+
echo "== generating 1 GiB random file =="
|
|
14
|
+
head -c 1G /dev/urandom > "$WORK/big.bin"
|
|
15
|
+
|
|
16
|
+
$FF serve --host 127.0.0.1 --port $PORT --data-dir "$DATA" --token bench >/dev/null 2>&1 &
|
|
17
|
+
SERVER_PID=$!
|
|
18
|
+
for i in $(seq 50); do curl -sf "http://127.0.0.1:$PORT/health" >/dev/null && break; sleep 0.2; done
|
|
19
|
+
|
|
20
|
+
export FLYFILE_SERVER=http://127.0.0.1:$PORT FLYFILE_TOKEN=bench
|
|
21
|
+
|
|
22
|
+
echo "== push (chunked parallel, incompressible -> raw) =="
|
|
23
|
+
T0=$(date +%s.%N)
|
|
24
|
+
ID=$($FF push "$WORK/big.bin" --json | $PY -c "import sys,json;print(json.load(sys.stdin)['id'])")
|
|
25
|
+
T1=$(date +%s.%N)
|
|
26
|
+
$PY -c "print(f'push: {1024/($T1-$T0):.0f} MiB/s ({$T1-$T0:.1f}s)')"
|
|
27
|
+
|
|
28
|
+
echo "== pull (parallel chunk fetch) =="
|
|
29
|
+
T0=$(date +%s.%N)
|
|
30
|
+
$FF pull "$ID" -o "$WORK/big.out" --json >/dev/null
|
|
31
|
+
T1=$(date +%s.%N)
|
|
32
|
+
$PY -c "print(f'pull: {1024/($T1-$T0):.0f} MiB/s ({$T1-$T0:.1f}s)')"
|
|
33
|
+
|
|
34
|
+
cmp "$WORK/big.bin" "$WORK/big.out" && echo "integrity: ok"
|
|
35
|
+
|
|
36
|
+
echo "== dedup re-push =="
|
|
37
|
+
T0=$(date +%s.%N)
|
|
38
|
+
$FF push "$WORK/big.bin" --json | $PY -c "import sys,json;d=json.load(sys.stdin);assert d['deduped'],d"
|
|
39
|
+
T1=$(date +%s.%N)
|
|
40
|
+
$PY -c "print(f'dedup push: {$T1-$T0:.2f}s (hash only)')"
|
|
41
|
+
|
|
42
|
+
echo "== LMDB file size =="
|
|
43
|
+
du -sh "$DATA"
|
|
44
|
+
|
|
45
|
+
echo "== churn: write+delete 5 rounds, file size should plateau =="
|
|
46
|
+
for r in $(seq 5); do
|
|
47
|
+
head -c 128M /dev/urandom > "$WORK/churn.bin"
|
|
48
|
+
CID=$($FF push "$WORK/churn.bin" --json | $PY -c "import sys,json;print(json.load(sys.stdin)['id'])")
|
|
49
|
+
$FF rm "$CID" >/dev/null
|
|
50
|
+
echo "round $r: $(du -sh "$DATA" | cut -f1)"
|
|
51
|
+
done
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
File without changes
|