文件存储
每个应用的数据后端在其表之外都附带一块私有对象存储。数据后端保存的是你的时序数据行,而文件存储保存的是一切放不进数据行的内容:相机帧、PDF 报表、固件二进制块、音频片段、导出文件。
这两者本就是为配合使用而设计的。存储一个文件时会直接返回一个永久 URL,而推荐的用法是在同一步操作中就把该 URL 写入表的某一列——随后仪表板组件无需任何额外工作即可渲染出这张图片:
Python
info = await ironflock.files.put("part-1.jpg", jpeg_bytes, content_type="image/jpeg")
await ironflock.publish_to_table("inspections", part_id="1", photo_url=info.url)完整的客户端 API——读取、列举、用量、共享链接、大对象、错误码——记录在 SDK 参考中。本页介绍平台一侧的内容:存储是如何声明、治理、共享和运维的。
工作原理
- 可选地在
.ironflock/data-template.yml中声明一个files:段。 - 当用户在某个项目中安装你的应用时,IronFlock 会为它配置一块私有存储区域——正如它配置项目数据库一样。
- 你的边缘代码通过 SDK 的
filesAPI 存储和读取对象。 - 卸载应用会彻底移除其存储:每一个对象和每一份凭据,正如数据库 schema 被删除一样。
与数据库一样,存储也是按项目划分的。同一个应用安装在两个项目中会得到两块完全独立的存储区域,而作为应用开发者,你对二者都无法访问——数据属于运行你应用的用户。
零配置也是一种有效的配置:即使应用没有 files: 段,它仍会获得一个名为 default 的命名空间,因此 files.put(...) 对每个应用都开箱即用。
在数据模板中声明存储
files: 段在 data-template.yml 中与 data: 平级:
files:
description: Camera frames and generated inspection reports.
# 应用为自己“建议”的存储预算,单位为字节(此处为 5 GiB)。
# 项目用户可以覆盖它;实际强制执行的是用户的设置。
quotaBytes: 5368709120
namespaces:
- name: frames
description: Raw camera frames, one JPEG per inspected part.
contentTypes: ["image/jpeg"]
maxObjectBytes: 20971520
retention: { deleteAfter: 30 days }
- name: reports
description: Generated PDF inspection reports.
contentTypes: ["application/pdf"]
private: true命名空间
命名空间是一个承载策略的键前缀。它并不是一个独立的存储桶——一个应用的每个命名空间都位于该应用唯一的那块存储区域之内,命名空间的名称只是成为每个对象存储路径的第一段。
这一区别告诉你何时应该声明命名空间:
- **只是想组织文件?**在
default命名空间内使用键路径——2026/03/part-1.jpg。文件夹式的键是常规做法。 - **需要为一组对象应用不同的规则?**那就声明一个命名空间。规则是命名空间唯一带来的东西。
命名空间可以承载的规则:
| 字段 | 含义 |
|---|---|
name | 小写字母、数字和连字符,且以字母开头。sys、system、ironflock 和 ironflock-* 为保留名 |
description | 展示给用户,并可供 AI 智能体读取 |
private | 将该命名空间排除在跨应用访问之外。默认为 false(共享)——与表完全一致 |
contentTypes | 允许的 MIME 类型,可使用通配符(image/*)。默认:任意 |
maxObjectBytes | 单个对象的最大大小,上限为 5 GiB。默认 100 MiB |
retention.deleteAfter | 早于此时长的对象会被自动删除(30 days、2 weeks、1 year、……) |
存储预算
quotaBytes 是为整个应用声明一次的,而不是按命名空间声明。命名空间只是一个键前缀,因此并不存在什么东西可以让按前缀划分的预算去强制执行——该配额作用于应用唯一的那块存储区域,并且由对象存储本身在每一条写入路径上强制执行,包括直接上传。
存在两个数值,而它们的区别是有意为之的:
- 建议配额——你的模板所请求的值。在应用首次安装时应用。
- 强制配额——项目用户所设置的值。一旦用户在应用的存储设置中更改了预算,他们的值就会生效,而且重新部署你的应用不会重置它。
如果你的模板未做任何声明,则应用平台默认值(开发后端为 1 GiB,生产后端为 10 GiB)。
保留
超过其命名空间 deleteAfter 时限的对象会被自动移除——这是表的 dropAfter 策略在文件一侧的对应物。在对象存储支持的情况下,保留在对象存储内部运行;在其不支持的情况下(本地一体机),则作为平台的每日任务运行,因此所声明的保留在任何地方的行为都一致。
永久 URL 与仪表板
每个已存储的对象都有一个形如 https://files.ironflock.com/f/<backend>/<namespace>/<key> 的稳定 URL。有三个特性使它成为写入表列的正确选择:
- **它永不过期。**该 URL 是一个纯粹的地址;它在对象的整个生命周期内都保持有效。
- **它不是公开链接。**每一次请求都会经过一个鉴权代理,校验请求方是否已登录、并且在此数据后端上持有 READ 权限——每一次请求都会重新校验。撤销某个用户的访问权限,会立即取消其获取任何文件的能力。
- **它可以在
<img>中渲染。**浏览器会自动发送其会话 cookie,因此仪表板组件可以在<img src>、<video src>或下载链接中使用该 URL,完全不涉及 JavaScript。
若要把文件交给项目之外的人,SDK 的 share_url 则会签发一个会过期的 bearer 链接——关于何时使用哪一种,参见共享对象。
大文件
不超过 6 MiB 的传输会作为单次调用经由消息系统传输。更大的内容则通过 HTTPS 在设备与对象存储之间直接传输——SDK 会自动切换,以流式方式从磁盘读取和写入磁盘,因此一个数 GB 的文件永远不必装进内存。单次上传的上限为 5 GiB。
直连路径要求设备能够访问对象存储主机(s3.ironflock.com),而不只是消息路由器。如果工厂代理只放行通往路由器的流量,大文件传输会以明确的错误码 PRESIGN_UNREACHABLE 失败,而不是一个笼统的错误——而设备时钟偏差超过 15 分钟则会以 CLOCK_SKEW 失败,这提示你应检查 NTP,而不是凭据。
在应用之间共享文件
跨应用文件访问依托与跨应用表访问相同的授权。只有一个开关:当项目用户授予应用 B 访问应用 A 数据的权限时(即应用设置中的 data_access 授权),该授权同时覆盖 A 的表和 A 的非私有文件命名空间。撤销它会同时撤销两者。
作为提供方,你的应用所控制的是每个命名空间的 private 标志:
private: false(默认)——持有数据访问授权的应用可以读取它(但永远无法写入)。private: true——即使已获授权,该命名空间对其他应用也完全不可见。
这与消费其他应用的数据完全一致:命名空间和表的默认值相同。没有项目用户的授权,任何内容都不会被共享——private: 只是收窄已获授权的读取方能看到的范围,它本身并不是授权。
用户视角
项目用户在两处查看和管理你应用的存储:
- 数据视图在每个应用的表和视图旁展示一个 Files 条目——一份可搜索的列表,列出每个已存储对象及其大小、类型和修改日期,并支持逐个文件下载。
- 应用的存储设置展示用量(字节数和对象数量)、与你应用建议值并列的强制预算、一个更改预算的控件,以及一个 Delete all files 操作——这是清空所有表在文件一侧的孪生功能。删除需要输入应用名称来确认,且无法撤销。
- AI 助手可以代表用户列出、搜索和读取这些文件。适用同样的
DATABACKEND/READ权限检查,因此绝不会显示用户自己无法打开的文件。它搜索的是文件路径而非文件内容,并且能读取文本文件、图像和 PDF — 压缩包等其他二进制格式则无法读取。
与表一样,这是用户的数据:他们可以检查它、限制它并删除它,而无需你的参与。
直接 S3 访问
对于 SDK 之外的一切用途——使用 DuckDB 的分析师、每晚的 rclone 备份、一条 BI 流水线——项目用户可以从应用的存储设置中按应用签发只读 S3 凭据。
每份凭据的作用范围都限定在那一个应用的存储区域内:为应用 A 签发的凭据在结构上就无法用于应用 B 的文件——B 的存储策略根本就不会提及它。一个项目可以为每个应用持有多份凭据(每个使用者一份:一个 CI 任务、一个备份脚本、一台笔记本电脑),撤销其中一份不会影响其他凭据继续工作。这也是密钥轮换的思路:如果某个密钥泄露,就签发第二份凭据,把使用者切换过去,再撤销第一份——其他任何使用者都不会受到干扰。
密钥只在创建时显示一次。签发需要对该应用数据后端的读取权限——与最初让你能够读取这些文件的权限相同,因此凭据永远不会扩大任何人的访问范围。
# rclone
rclone config create myapp s3 provider=Other \
endpoint=https://s3.ironflock.com \
access_key_id=ifs-key-3317-x7k2m secret_access_key=<shown once>
rclone ls myapp:if-1042-3317# DuckDB
CREATE SECRET (TYPE S3, KEY_ID 'ifs-key-3317-x7k2m', SECRET '<shown once>',
ENDPOINT 's3.ironflock.com');
SELECT * FROM read_parquet('s3://if-1042-3317/exports/*.parquet');存储设置会显示端点以及供客户端指向的确切桶名。请注意,顶层的桶列举(不带参数的 aws s3 ls)在设计上不返回任何内容——该凭据并不拥有任何桶;它只是被授予了对某一个桶的访问权限。请如上所示直接寻址该桶。
本地一体机
文件存储在本地一体机上的工作方式完全相同,但因一体机的设计而带来三处差异:
- 文件在一体机自身的地址下以同源方式提供(
/files/...)——无需额外的 DNS 名称,无需额外的证书,并且在纯 HTTP 的安装中也能工作。永久 URL 和<img src>的行为与在云端完全一致。 - 一体机的存储后端会通过平台以流式方式提供文件下载,而不是重定向到一个独立的存储主机,因此存储服务绝不会作为第二个源被暴露出来。
- 一体机上不提供直接 S3 凭据——内嵌的对象存储无法表达按凭据划分的访问授权。该部分在那里的存储设置中根本不会出现。其余一切,包括通过 SDK 的大文件行为以及所声明的保留,都保持相同。
一台物理隔离的一体机会在本地处理所有文件流量:应用徽标、仪表板图片和文件下载都无需互联网连接。
限制一览
| 限制 | 值 | 来源 |
|---|---|---|
| 内联传输(单次调用) | 6 MiB | 在运行时给出;可能在服务端调高 |
| 单个对象 | 默认 100 MiB,上限 5 GiB | 每个命名空间的 maxObjectBytes |
| 单次上传 | 5 GiB | 对象存储单次 PUT 的上限;分片上传尚不可用 |
| 存储预算 | 默认开发环境 1 GiB / 生产环境 10 GiB | 由模板建议,由用户决定 |
| 键的长度与字符 | UTF-8,以 / 分隔的路径 | ../、控制字符和保留前缀会被拒绝 |