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.
Files changed (39) hide show
  1. flyfile-0.1.0/.github/workflows/gh-release.yml +31 -0
  2. flyfile-0.1.0/.github/workflows/python-publish.yml +48 -0
  3. flyfile-0.1.0/.gitignore +9 -0
  4. flyfile-0.1.0/PKG-INFO +78 -0
  5. flyfile-0.1.0/README.md +61 -0
  6. flyfile-0.1.0/docs/README.md +37 -0
  7. flyfile-0.1.0/docs/api.md +43 -0
  8. flyfile-0.1.0/docs/architecture.md +69 -0
  9. flyfile-0.1.0/docs/cli.md +70 -0
  10. flyfile-0.1.0/docs/flaxkv2-feedback.md +42 -0
  11. flyfile-0.1.0/pyproject.toml +39 -0
  12. flyfile-0.1.0/scripts/bench.sh +51 -0
  13. flyfile-0.1.0/src/flyfile/__init__.py +1 -0
  14. flyfile-0.1.0/src/flyfile/cli/__init__.py +0 -0
  15. flyfile-0.1.0/src/flyfile/cli/main.py +407 -0
  16. flyfile-0.1.0/src/flyfile/cli/output.py +98 -0
  17. flyfile-0.1.0/src/flyfile/client/__init__.py +0 -0
  18. flyfile-0.1.0/src/flyfile/client/client.py +366 -0
  19. flyfile-0.1.0/src/flyfile/client/errors.py +54 -0
  20. flyfile-0.1.0/src/flyfile/client/transfer.py +231 -0
  21. flyfile-0.1.0/src/flyfile/core/__init__.py +1 -0
  22. flyfile-0.1.0/src/flyfile/core/compress.py +78 -0
  23. flyfile-0.1.0/src/flyfile/core/hashing.py +14 -0
  24. flyfile-0.1.0/src/flyfile/core/ids.py +26 -0
  25. flyfile-0.1.0/src/flyfile/core/tarstream.py +81 -0
  26. flyfile-0.1.0/src/flyfile/server/__init__.py +0 -0
  27. flyfile-0.1.0/src/flyfile/server/app.py +68 -0
  28. flyfile-0.1.0/src/flyfile/server/config.py +16 -0
  29. flyfile-0.1.0/src/flyfile/server/errors.py +18 -0
  30. flyfile-0.1.0/src/flyfile/server/relay.py +111 -0
  31. flyfile-0.1.0/src/flyfile/server/routes_objects.py +273 -0
  32. flyfile-0.1.0/src/flyfile/server/routes_uploads.py +111 -0
  33. flyfile-0.1.0/src/flyfile/server/store.py +262 -0
  34. flyfile-0.1.0/tests/__init__.py +0 -0
  35. flyfile-0.1.0/tests/conftest.py +61 -0
  36. flyfile-0.1.0/tests/test_api.py +198 -0
  37. flyfile-0.1.0/tests/test_c2c.py +85 -0
  38. flyfile-0.1.0/tests/test_cli.py +101 -0
  39. 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
@@ -0,0 +1,9 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ .venv/
5
+ dist/
6
+ build/
7
+ .pytest_cache/
8
+ .ruff_cache/
9
+ .mypy_cache/
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
+ ```
@@ -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