ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

OpenViking 共享临时上传的时间戳目录布局设计与实现:一次根目录列举完成过期清理

2026/9/11 14:01:54 拓冰建站 浏览量
OpenViking 共享临时上传的时间戳目录布局设计与实现:一次根目录列举完成过期清理 OpenViking 共享临时上传的时间戳目录布局设计与实现一次根目录列举完成过期清理【免费下载链接】OpenVikingSelf-evolving Context Database for AI Agents. Unify Agent Memory, Knowledge RAG and Skills.项目地址: https://gitcode.com/GitHub_Trending/op/OpenViking共享临时上传shared temp upload是 OpenViking HTTP 服务器在分布式部署下处理本地文件上传的专用通道文件先上传到服务器托管的临时存储换取temp_file_id再由add_resource等 API 消费。本篇文章基于 docs/plans/2026-08-17-shared-temp-upload-timestamp-directory.md 实现计划结合 openviking/server/temp_upload_store.py、openviking/service/user_deletion.py 等源码与测试深入讲解时间戳前缀目录布局的设计动机、实现原理、清理算法与配套测试读完你可以理解 OpenViking 如何在**不依赖文件系统修改时间modTime**的前提下仅凭一次根目录列举就能判定并回收全部过期上传。一、设计动机为什么临时上传需要时间戳目录而非扁平对象在引入本方案之前共享上传在viking://upload根下以扁平的.content/.meta对象形式存放过期清理需要依赖对象本身的修改时间来推断存活时长。这在分布式、对象存储后端上存在两个痛点文件系统修改时间不可靠对象存储的 mtime 语义不统一复制、同步或后端实现差异都会污染它把清理逻辑建立在 mtime 上既不稳健也难以审计清理成本高逐个对象读取属性再判断过期面对大批量上传时扫描开销线性放大。计划文档给出的核心思路docs/plans/2026-08-17-shared-temp-upload-timestamp-directory.md是Store each shared temporary upload in a timestamp-prefixed directory so cleanup can determine expiry from one root listing without filesystem modification times.即将过期时间编码进目录名本身。目录名携带创建时间戳清理时只需要一次根目录列举解析每个一级目录名中的时间戳即可判断是否过期再递归删除过期目录——整个过程零 mtime 依赖。二、目录布局与 upload ID 格式2.1 目录层级共享上传存储在viking://upload内部根下源码常量_SHARED_UPLOAD_ROOT viking://upload见 openviking/server/temp_upload_store.py每次上传产生一个独立目录viking://upload/bucket/leaf/content viking://upload/bucket/leaf/metacontent上传文件本体先写入metaJSON 元数据后写入其存在即标记一次上传完成类似提交标记防止消费者读到半成品这两个对象不属于正常文件浏览面仅供内部使用。路径完全由temp_file_id推导而来消费者无需额外查询即可构造两个 URI。源码中的构造函数openviking/server/temp_upload_store.pydef _shared_content_uri(bucket: str, leaf: str) - str: return f{_SHARED_UPLOAD_ROOT}/{bucket}/{leaf}/content def _shared_meta_uri(bucket: str, leaf: str) - str: return f{_SHARED_UPLOAD_ROOT}/{bucket}/{leaf}/meta2.2 新旧两种 upload ID 格式计划文档最初设想的格式是定宽 Unix 毫秒时间戳 UUID13-digit-ms-uuid。当前仓库实现在此基础上进一步演进为小时分桶格式同时保留对旧格式的完整兼容两者可通过第一段前缀的长度区分openviking/server/temp_upload_store.py格式示例前缀长度解析函数新格式当前布局2026081715-3f2c…(32位hex)10 位YYYYMMDDHHUTC_split_shared_upload_id旧格式legacy 扁平布局1723881600000-3f2c…(32位hex)13 位毫秒时间戳_shared_upload_created_at新 ID 由_new_shared_upload_id()生成openviking/server/temp_upload_store.pydef _new_shared_upload_id() - str: return f{time.strftime(%Y%m%d%H, time.gmtime())}-{uuid.uuid4().hex}其中 10 位小时前缀YYYYMMDDHH同时承担两个职责作为存储分桶bucket共享上传按 UTC 小时归入viking://upload/YYYYMMDDHH/子目录_SHARED_BUCKET_SECONDS 3600见 openviking/server/temp_upload_store.py根目录永远不会退化为一个巨大的扁平目录作为新格式的标记旧格式以 13 位毫秒时间戳开头两种格式按第一段长度即可判别无需额外标记。_split_shared_upload_id()负责把新格式拆成(bucket, leaf)二元组并对合法性做严格校验前缀必须恰好 10 位纯数字、能通过strptime(%Y%m%d%H)解析叶子必须是 32 位小写 hexopenviking/server/temp_upload_store.py。在桶内叶子只使用 UUID hex不重复前缀因此目录路径不会越级嵌套。2.3 对外temp_file_id不变对 API 消费者而言返回的temp_file_id始终是shared_upload_id形式_parse_shared_temp_file_id校验前缀并禁止/、\见 openviking/server/temp_upload_store.py。目录布局是纯内部实现细节同一账号在其存活期内可重复消费该 ID参见 docs/en/api/02-resources.md 的说明。三、保存与解析路径content 先写、meta 后写3.1 保存save入口是TempUploadStore.save_upload()按upload_mode分发到_save_local单机或_save_shared共享见 openviking/server/temp_upload_store.py。_save_shared的核心步骤openviking/server/temp_upload_store.py先把上传流写入本地临时文件并累计字节数超过shared_max_size_bytes立即抛InvalidArgumentError生成新格式upload_id得到temp_file_id fshared_{upload_id}构造content与meta两个 URI元数据写入version、temp_file_id、account、user、original_filename、content_type、file_ext、size、storage_uri等字段openviking/server/temp_upload_store.py先写content再写metawrite_file_bytes之后才write_file保证消费端读取时 meta 一旦可见content 必然已完整落盘写入成功后调度一次异步清理_schedule_shared_cleanup若任一步骤失败递归删除整个viking://upload/bucket/leaf目录并重新抛出不留下孤儿数据finally中清理本地临时文件。VikingFS 写入使用auto_pathlockFalse——共享上传目录从不被并发修改省去不必要的路径锁开销。3.2 解析消费resolveresolve_for_consume()先把temp_file_id拆出shared_前缀openviking/server/temp_upload_store.py_resolve_shared的执行流程openviking/server/temp_upload_store.py读取 meta_read_shared_meta依据 upload_id 自身的格式精确选择一条路径、不做回退探测——新格式读viking://upload/bucket/leaf/meta旧格式读 legacy 扁平路径viking://upload/upload_id/meta_legacy_shared_meta_uri见 openviking/server/temp_upload_store.py校验 metatemp_file_id必须与请求一致、account必须属于当前账号否则抛PermissionDeniedErroropenviking/server/temp_upload_store.py校验content存在读取字节流到本地临时文件返回ResolvedTempUpload。测试test_read_shared_meta_uses_bucket_path_for_new_id与test_read_shared_meta_uses_flat_path_for_legacy_id分别验证了两种格式恰好一次读取、无 fallback 探测的行为tests/server/test_temp_upload_store_async_io.py。四、清理机制一次列举、三组分类、最老优先清理是这套设计的灵魂。_cleanup_shared_uploads()openviking/server/temp_upload_store.py实现时间戳 单次列举的过期判定一次根目录列举vfs.ls(viking://upload, show_all_hiddenTrue, node_limitLS_ALL_NODES, sort_byname, sort_orderasc)按名称升序天然得到最老在前的排列三组独立分类每个一级目录按名称判定归属——YYYYMMDDHH小时桶 → bucket 组过期时刻 bucket_start 3600 ttl_seconds一个桶内最晚上传也已过期时整桶过期13 位毫秒前缀 → legacy 扁平上传组过期时刻 created_at ttl_seconds其余畸形/外来目录 → invalid 组默认跳过仅当cleanup_invalid_dirsTrue时才尽力删除test_cleanup_skips_invalid_dirs_by_default/test_cleanup_removes_invalid_dirs_when_enabled覆盖这两种行为独立的最老优先扫描bucket 与 legacy 两组各自处理删除队首过期项在第一个仍未过期的条目处停止并记录其过期时刻作为due_at——这样一组的大量积压不会饿死另一组测试test_cleanup_buckets_not_starved_by_live_legacy专门验证递归删除_remove_shared_dir用vfs.remove_files(uri, recursiveTrue, auto_pathlockFalse)整目录删除openviking/server/temp_upload_store.py。原始上传从无向量索引记录删除时跳过 vector-store 清理以省去无谓的往返due_at节流清理结果中最小的存活过期时刻被记录到_SHARED_CLEANUP_DUE_AT[account_id]此后在该时刻之前的新请求不会再提交清理任务任一删除失败则清空due_at让下一次请求立即重试测试test_cleanup_remove_failure_clears_due_at_and_stops、test_cleanup_full_page_expired_clears_due_at验证。清理任务通过有界队列maxsize100交给单例后台线程ov-shared-upload-cleanup执行请求路径上只做节流判断与入队_schedule_shared_cleanup见 openviking/server/temp_upload_store.pyTTL 为 0、该账号已有排队任务、或now due_at时直接跳过避免为同一账号堆积重复任务。源码注释明确解释了这些模块级状态_SHARED_CLEANUP_DUE_AT/_SHARED_CLEANUP_PENDING为何必须放在模块作用域而非实例上——TempUploadStore并非单例HTTP 路由与 MCP 端点每次请求都会调用build(...)。五、用户删除联动按 meta 归属定向清理共享上传属于账号级数据用户删除时必须一并回收。UserDeletionService的删除流水线包含多个 stage任务记录、AGFS、向量库、上传、OAuth token、用量审计其中_delete_uploads()负责上传清理openviking/service/user_deletion.py列举viking://upload全部一级目录对每个目录读取其metauri/meta仅当meta.account 目标账号且meta.user 目标用户时才递归删除该目录。对于 meta 不可读的目录则记录 warning 并跳过绝不错删其他账号的数据。该实现与计划文档 Task 3 的目标一致从扫描根级 .meta 对象改为先列目录、再按目录读 meta、按归属定向删除。六、配置参数速查共享上传的行为由TempUploadConfigopenviking/server/config.py控制可在服务端配置中通过server.temp_upload.*路径设置见 docs/en/api/02-resources.md参数类型默认值说明default_modelocal/sharedlocal未显式指定upload_mode时使用的临时上传后端shared_max_size_bytesint512 * 1024 * 1024共享上传单文件大小上限ttl_secondsintge012 * 60 * 60共享上传保留时长0表示关闭 TTL 清理cleanup_invalid_dirsboolFalse是否在清理时删除名称畸形/外来的目录默认关闭以免误删客户端侧HTTP 客户端可通过在ovcli.conf设置upload.mode shared选用共享上传SDK 侧openviking.Config{UploadMode: shared}等效docs/en/api/02-resources.md。未显式指定时default_mode保持local既有客户端行为不变。七、测试与验证从写失败测试到全量回归计划文档采用 TDD 方式逐任务推进测试与验证命令如下# 清理行为Task 1 / Task 3 uv run --active pytest tests/server/test_temp_upload_store_async_io.py -q # 保存与解析路径Task 2 uv run --active pytest tests/server/test_api_resources.py -q -k shared_temp_upload # 聚焦回归 静态检查Task 4 uv run --active pytest tests/test_config_loader.py tests/server/test_temp_upload_store_async_io.py -q uv run --active pytest tests/server/test_api_resources.py -q -k shared_temp_upload uv run --active ruff check openviking/server/temp_upload_store.py openviking/service/user_deletion.py tests/server/test_temp_upload_store_async_io.py tests/server/test_api_resources.py仓库中与本文档对应的测试包括tests/server/test_temp_upload_store_async_io.py覆盖桶/legacy/invalid 三组清理、due_at节流与清除、删除失败重试、新老格式 ID 判别、meta 路径精确读取、事件循环安全_stream_upload_to_local_temp的asyncio.to_thread落盘等tests/server/test_api_resources.pytest_shared_temp_upload_can_be_added_repeatedly同一shared_id可重复消费、test_shared_temp_upload_failed_consume_is_retryable消费失败可重试、test_shared_upload_content_read_rejects_internal_scope内容读取拒绝内部作用域、test_temp_upload_uses_configured_shared_defaultdefault_modeshared生效等端到端断言。计划文档 Task 4 还要求同步更新 docs/en/api/02-resources.md、docs/zh/api/02-resources.md、docs/en/guides/01-configuration.md 与 docs/zh/guides/01-configuration.md在文档中只描述时间戳目录布局 单次列举清理不再引用对象修改时间。八、兼容性与演进要点新旧格式共存legacy 扁平上传在 TTL 内仍可被读取_legacy_shared_meta_uri保留读兼容并被归入 legacy 组单独清理legacy 条目永远不会被当作 invalid 目录删除测试test_cleanup_never_treats_legacy_flat_as_invalid验证。桶粒度过期小时桶的过期以bucket_start 3600 ttl计算语义是桶内最晚上传也已过期因此整桶递归删除是安全的当前小时的桶由于存在存活上传而始终被跳过测试test_cleanup_removes_expired_buckets_and_stops_at_live。不依赖 mtime所有过期判定都来自目录名中的时间戳10 位小时前缀或 13 位毫秒前缀与底层对象存储的修改时间语义完全解耦这正是本设计的核心收益。综上所述这套时间戳前缀目录 单次列举清理方案用目录名承担元数据职责把过期判定从读对象属性降维成解析目录名同时通过小时分桶、最老优先扫描与due_at节流保证了清理在根目录规模增长下的可扩展性是 OpenViking 分布式共享上传链路中一个精巧且自洽的基础设施设计。【免费下载链接】OpenVikingSelf-evolving Context Database for AI Agents. Unify Agent Memory, Knowledge RAG and Skills.项目地址: https://gitcode.com/GitHub_Trending/op/OpenViking创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考