feat(drivers): add huggingface cloud storage driver - #2799
Conversation
实现了HuggingFace仓库作为存储驱动的完整功能,支持模型/数据集/空间三种仓库类型,支持代理配置、LFS大文件上传、目录创建和删除等操作,同时在drivers/all.go中注册该驱动。
重新导入三个编写过程中因意外丢失的存储驱动
清理仓库中不再需要的.tmp/CloudPaste和.tmp/OpenList-Frontend临时子模块
已改正,感谢提醒 |
|
HF仓库已经支持S3访问了,数据集还不支持 |
所以主要是支持模型和数据集,只是对接API的话顺手做全可能更好,毕竟小白不一定知道HF支持S3,S3配置也有点难度。 |
我看官方说法是桶支持,但标准的模型、数据集几乎不支持 |
确实,不过我试了一下,桶目前是和数据集和模型共享存储空间的 |
当这吃的是私人仓库空间好像,我打算是白嫖公共空间用来挂载AI数据 |
xrgzs
left a comment
There was a problem hiding this comment.
go fmt跑一下,代码格式有问题- 自建请求客户端,无适配OpenList Conf代理配置逻辑,建议先检查一下是否真的需要,否则直接用base中的即可。(让AI看一遍
internal/net) - 文档弄一下:https://github.com/OpenListTeam/OpenList-Docs
| return d.lfsUploadAndCommit(ctx, tmp, filePath, fileName, sha256Hex, size, up) | ||
| } | ||
| return d.streamCommit(ctx, tmp, filePath, fileName, size, up) | ||
| } |
There was a problem hiding this comment.
全量落盘,未使用框架的流式缓存工具 internal/stream.CacheFullAndHash,建议整改
| // hfClient is a dedicated HTTP client for HF API requests, | ||
| // separate from base.HttpClient to avoid keep-alive connection reuse issues. | ||
| func hfClient() *http.Client { | ||
| return &http.Client{ | ||
| Timeout: 30 * time.Minute, | ||
| Transport: &http.Transport{ | ||
| TLSClientConfig: &tls.Config{InsecureSkipVerify: false}, | ||
| DisableKeepAlives: true, | ||
| MaxIdleConns: 0, | ||
| }, | ||
| } | ||
| } |
| // s3Client returns a shared HTTP client for S3 LFS uploads. | ||
| func s3Client() *http.Client { | ||
| s3HTTPClientOnce.Do(func() { | ||
| s3HTTPClient = &http.Client{ | ||
| Timeout: 30 * time.Minute, | ||
| Transport: &http.Transport{ | ||
| TLSClientConfig: &tls.Config{ | ||
| InsecureSkipVerify: false, | ||
| MinVersion: tls.VersionTLS12, | ||
| }, | ||
| DialContext: (&net.Dialer{ | ||
| Timeout: 30 * time.Second, | ||
| KeepAlive: 30 * time.Second, | ||
| }).DialContext, | ||
| ForceAttemptHTTP2: false, | ||
| MaxIdleConns: 2, | ||
| MaxIdleConnsPerHost: 2, | ||
| IdleConnTimeout: 90 * time.Second, | ||
| ResponseHeaderTimeout: 5 * time.Minute, | ||
| ExpectContinueTimeout: 5 * time.Second, | ||
| }, | ||
| } | ||
| }) | ||
| return s3HTTPClient | ||
| } |
文档已提交 OpenListTeam/OpenList-Docs#351 hfClient() 与 s3Client() 的自定义 Transport 现在都会调用 netutil.SetProxyIfConfigured(transport)(internal/net/util.go 中定义),读取 conf.Conf.ProxyAddress 并设置 transport.Proxy,因此两个客户端都会遵循 OpenList Conf 中配置的代理。 关于"是否真的需要自定义客户端,还是直接复用 base 中的":我们排查了 internal/net 和 drivers/base/client.go,结论是这两个客户端有必要保留,理由如下: base.HttpClient(即 net.NewHttpClient())默认带 DisableKeepAlives=false、开启 HTTP/2,且无 S3 上传所需的连接参数定制; |
|
感谢 review,三条意见都已在提交 752f237 中处理完毕:
关于"是否真的需要自定义客户端、能否直接复用 base.HttpClient":排查 internal/net 与 drivers/base/client.go 后,结论是保留自定义客户端是合理的: base.HttpClient(即 net.NewHttpClient())不提供 DisableKeepAlives、禁用 HTTP/2、以及面向 S3 上传的连接参数定制;
|
PIKACHUIM
left a comment
There was a problem hiding this comment.
🙏 感谢贡献
感谢 @WUHINS 提交此PR!我已完成代码评审,以下是评审结果。
🤖 AI 自动审核声明
本评审报告由 AI 自动生成,当前使用 Claude Opus 5 模型进行分析,部分复杂场景可能辅助使用 ChatGPT、DeepSeek 等模型进行交叉验证。
⚠️ AI 分析结果仅供参考,可能存在误判或遗漏。如您发现任何问题或有不同意见,欢迎随时提出讨论和纠正。
⚠️ 重要提醒:即使 AI 评审认为代码质量良好且建议合并,最终是否合并仍需由项目维护者进行人工判定。项目维护者会综合考虑代码质量、项目规划、技术方向、团队资源等多方面因素做出决策。
📖 PR背景与需求
PR标题:feat(drivers): add huggingface cloud storage driver
关联Issue:无
需求说明:实现 HuggingFace 仓库作为 OpenList 存储驱动的完整功能,支持模型(model) / 数据集(dataset) / 空间(space) 三种仓库类型。文件上传时,小于 5MB 的文件走 base64 NDJSON 内联提交,大于等于 5MB 的文件自动切换到 HuggingFace LFS(S3 presigned URL 直传)。
预期目标:用户可以将 HuggingFace 仓库作为 OpenList 的存储后端,支持文件上传、下载、列表、删除、目录创建等完整功能,并支持代理配置。
📋 问题摘要
- ✅ 功能性:功能设计完整,实现了 HuggingFace 存储驱动的核心功能
⚠️ 安全性:需要加强错误处理和资源管理- 💡 代码质量:实现较为完整,但有多处可优化点
📂 逐文件分析
drivers/huggingface/driver.go
改动意图:实现 HuggingFace 驱动的核心逻辑,包括文件上传(小文件 base64 内联提交、大文件 LFS 上传)、文件列表、下载链接生成、目录创建、文件删除等功能。
代码修改逻辑:
- LFS 上传流程:
Put()→stream.CacheFullAndHash()计算 SHA256 →preupload()检测 → LFS batch API 获取 S3 presigned URL →streamUpload()PUT 直传 S3(支持 fallback 到 POST)→verify(可选)→doCommitLFS()NDJSON commit 提交 lfsFile 引用 - 小型文件流式提交:
streamCommit()使用io.Pipe+ base64 编码 + NDJSON 内嵌,避免写临时文件 - 自定义 HTTP 客户端:
s3Client()单例共享连接池(5 分钟 ResponseHeaderTimeout),hfClient()独立客户端(DisableKeepAlives)用于 HF API context.WithoutCancel保护 S3 上传 body 不被任务取消中断- 过滤未签名的
x-amz-*头部,避免 S3 403(X-Amz-SignedHeaders=host)
合理性评估:
-
✅ 优点:
- 完整实现了 HuggingFace 存储驱动的核心功能
- LFS 上传流程设计合理,支持 S3 presigned URL 直传
- 使用
context.WithoutCancel保护 S3 上传不被中断 - 过滤未签名的
x-amz-*头部,避免 S3 签名错误 - 小文件流式提交避免写临时文件,节省磁盘空间
-
⚠️ 问题:-
资源泄漏风险:
streamCommit()中的io.Pipe在 goroutine 中写入,如果写入失败或cache.Seek()失败,pw.CloseWithError(err)会正确关闭,但如果后续 HTTP 请求构建失败(http.NewRequestWithContext()返回错误),goroutine 可能仍在写入,导致pr未被消费完。建议在 HTTP 请求构建失败时也关闭pr。 -
错误处理不完整:
lfsUploadAndCommit()中,如果obj.Actions为 nil 或没有 "upload" action,会直接调用doCommitLFS(),但此时文件内容可能还未上传到 LFS,这可能导致提交失败或文件内容丢失。建议增加明确的错误检查。 -
hfClient()DisableKeepAlives:设置DisableKeepAlives: true会导致每次请求都创建新连接,对于频繁的 HF API 调用(如列表、下载等)会降低性能。建议保留 KeepAlive,仅在特定场景(如 S3 上传)使用独立客户端。 -
缺少超时配置:
s3Client()设置了ResponseHeaderTimeout: 5 * time.Minute,但hfClient()设置了Timeout: 30 * time.Minute,这对于列表等快速操作来说过长。建议根据操作类型设置不同的超时时间。 -
缺少重试机制:对于网络操作(如 S3 上传、LFS batch API 调用),建议增加重试机制,提高上传成功率。
-
详细建议:
-
修复
streamCommit()资源泄漏风险:func (d *HuggingFace) streamCommit(ctx context.Context, cache model.File, filePath, fileName string, size int64, up driver.UpdateProgress) error { headerLine := fmt.Sprintf(`{"key":"header","value":{"summary":"Upload %s"}}`, fileName) + "\n" filePrefix := fmt.Sprintf(`{"key":"file","value":{"path":"%s","content":"`, filePath) fileSuffix := `","encoding":"base64"}}` + "\n" b64Len := calculateBase64Length(size) contentLength := int64(len(headerLine)+len(filePrefix)) + b64Len + int64(len(fileSuffix)) pr, pw := io.Pipe() errChan := make(chan error, 1) go func() { defer pw.Close() if _, err := cache.Seek(0, io.SeekStart); err != nil { errChan <- err _ = pw.CloseWithError(err) return } encoder := base64.NewEncoder(base64.StdEncoding, pw) if _, err := io.Copy(encoder, cache); err != nil { errChan <- err _ = pw.CloseWithError(err) return } _ = encoder.Close() errChan <- nil }() body := io.MultiReader( strings.NewReader(headerLine), strings.NewReader(filePrefix), pr, strings.NewReader(fileSuffix), ) req, err := http.NewRequestWithContext(ctx, http.MethodPost, d.apiURL(fmt.Sprintf("/commit/%s", d.Ref)), driver.NewLimitedUploadStream(ctx, &driver.ReaderUpdatingProgress{ Reader: &driver.SimpleReaderWithSize{Reader: body, Size: contentLength}, UpdateProgress: up, })) if err != nil { pr.Close() // 确保关闭 reader return fmt.Errorf("stream commit request build: %w", err) } // ... 继续后续逻辑 }
-
增强
lfsUploadAndCommit()错误检查:obj := batchResp.Objects[0] if obj.Error != nil { return fmt.Errorf("lfs batch error: %v", obj.Error) } if obj.Actions == nil { // 文件已存在于 LFS,直接提交引用 return d.doCommitLFS(ctx, filePath, sha256Hex, size) } uploadAction, ok := obj.Actions["upload"] if !ok { // 没有 upload action,可能文件已存在 return d.doCommitLFS(ctx, filePath, sha256Hex, size) }
-
优化
hfClient()连接管理:func hfClient() *http.Client { transport := &http.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: false}, DisableKeepAlives: false, // 保留 KeepAlive 提高性能 MaxIdleConns: 10, MaxIdleConnsPerHost: 2, IdleConnTimeout: 90 * time.Second, ResponseHeaderTimeout: 30 * time.Second, } netutil.SetProxyIfConfigured(transport) return &http.Client{ Timeout: 60 * time.Second, // 减少超时时间 Transport: transport, } }
-
缺少单元测试:建议补充单元测试覆盖核心功能(文件上传、列表、删除等)。
drivers/huggingface/meta.go
改动意图:定义 HuggingFace 驱动的元数据和配置项。
代码修改逻辑:
- 定义
Addition结构体,包含ApiToken、RepoID、Ref、RepoType、HFProxy等配置项 - 定义
config变量,设置驱动名称、本地排序、默认根路径
合理性评估:
-
✅ 优点:
- 配置项定义清晰,
help标签提供了使用说明 - 支持可选的 API token,兼容公开仓库
- 支持代理配置,方便国内用户使用
- 配置项定义清晰,
-
💡 建议:
RepoType的 help 标签可以补充更多说明(如 "model 用于模型仓库,dataset 用于数据集,space 用于应用空间")
drivers/huggingface/types.go
改动意图:定义 HuggingFace API 的请求和响应类型。
代码修改逻辑:
- 定义
TreeEntry、ErrorResponse、PreuploadFile、PreuploadResponse、LFSBatchRequest、LFSBatchResponse等类型
合理性评估:
-
✅ 优点:
- 类型定义完整,覆盖了 HuggingFace API 的主要数据结构
TreeEntry.toModelObj()正确处理了目录的 size(设置为 0)
-
⚠️ 问题:TreeEntry没有Modified字段,导致所有文件的修改时间都是 Unix 纪元(1970-01-01)。建议检查 HuggingFace API 是否提供修改时间字段,如果有则解析;如果没有则在注释中说明。
drivers/huggingface/util.go
改动意图:提供工具函数,包括 API URL 构建、错误处理、路径处理等。
代码修改逻辑:
apiURL()、resolveURL()、apiRepoType()等函数用于构建 API 请求 URLtoHFError()将 HTTP 响应转换为错误信息relativePath()处理路径标准化lastIndexByte()查找最后一个字节的位置
合理性评估:
-
✅ 优点:
- 工具函数设计合理,提高了代码复用性
toHFError()正确解析了 HuggingFace API 的错误响应
-
💡 建议:
lastIndexByte()可以使用标准库的strings.LastIndexByte()替代,减少自定义实现
🎯 总体评价
功能性:⭐⭐⭐⭐ - 功能设计完整,实现了 HuggingFace 存储驱动的核心功能
安全性:⭐⭐⭐ - 需要加强错误处理和资源管理,避免资源泄漏
代码质量:⭐⭐⭐ - 实现较为完整,但有多处可优化点,缺少单元测试
实现方案:⭐⭐⭐⭐ - 实现方案合理,LFS 上传流程设计正确
建议操作:
- ✅ Approve(建议合并)
- 🔄 Request Changes(需要修改)
- ❌ Close(建议关闭)
理由:此 PR 功能完整、实现合理,但存在以下需要修改的问题:
- P1(必须修改):
streamCommit()中的io.Pipe资源泄漏风险 - P1(必须修改):
lfsUploadAndCommit()中缺少错误检查,可能导致文件上传失败 - P2(建议修改):优化
hfClient()连接管理,提高性能 - P2(建议修改):补充单元测试覆盖核心功能
建议先修复 P1 问题后合并,P2 问题可在后续 PR 中优化。
Next Steps / 后续建议:
- 修复资源泄漏风险和错误检查问题(P1)
- 补充单元测试,覆盖文件上传、列表、删除等核心功能
- 考虑增加重试机制,提高上传成功率
- 优化 HTTP 客户端配置,提高性能
- 补充文档说明 HuggingFace 驱动的使用方法和限制
再次感谢你的贡献!这是一个很有价值的功能,期待看到修复后的版本。👏
Summary / 摘要
实现了 HuggingFace 仓库作为 OpenList 存储驱动的完整功能。
用户可感知的变化
HuggingFace驱动类型,支持模型(model) / 数据集(dataset) / 空间(space) 三种仓库repo_id(必填)、api_token、ref(分支/标签/commit SHA,默认 main)、repo_type(默认 model)、hf_proxy(代理地址)重要实现变化
新增 5 个文件:
drivers/huggingface/driver.go(核心逻辑)、meta.go(驱动元数据)、types.go(API 类型)、util.go(工具函数),以及在drivers/all.go注册LFS 上传流程:saveStream 写临时文件 + SHA256 → preupload 检测 → LFS batch API 获取 S3 presigned URL → PUT 直传 S3(支持 fallback 到 POST)→ verify(可选)→ NDJSON commit 提交 lfsFile 引用
小型文件流式提交:io.Pipe + base64 编码 + NDJSON 内嵌,避免写临时文件
自定义 HTTP 客户端:
s3Client()单例共享连接池(5 分钟 ResponseHeaderTimeout),hfClient()独立客户端(DisableKeepAlives)用于 HF APIcontext.WithoutCancel保护 S3 上传 body 不被任务取消中断过滤未签名的
x-amz-*头部,避免 S3 403(X-Amz-SignedHeaders=host)saveStream 增加
fi.Size() != stream.GetSize()校验,提前捕获数据不完整This PR has breaking changes.
/ 此 PR 包含破坏性变更。
This PR changes public API, config, storage format, or migration behavior.
/ 此 PR 修改了公开 API、配置、存储格式或迁移行为。
This PR requires corresponding changes in related repositories.
/ 此 PR 需要关联仓库同步修改。
Related repository PRs / 关联仓库 PR:
Related Issues / 关联 Issue
无
Testing / 测试
go build -o OpenList.exe .— 编译通过,生成 ~124MB 可执行文件Checklist / 检查清单
/ 我已阅读 CONTRIBUTING。
/ 我确认此贡献符合仓库许可证、贡献规范和行为准则。
gofmt,go fmt, orprettierwhere applicable./ 我已按适用情况使用
gofmt、go fmt或prettier格式化变更代码。/ 我已在适用情况下请求相关维护者或代码所有者审查。
AI Disclosure / AI 使用声明
/ 此 PR 包含 AI 辅助内容。
Tools used / 使用工具:
Usage scope / 使用范围:
Code generation / 代码生成
Refactoring / 重构
Documentation / 文档
Tests / 测试
Translation / 翻译
Review assistance / 审查辅助
I have reviewed and validated all AI-assisted content included in this PR.
/ 我已审核并验证此 PR 中的所有 AI 辅助内容。
I have ensured that all AI-assisted commits include
Co-Authored-Byattribution./ 我已确保所有 AI 辅助提交都包含
Co-Authored-By归属信息。I can reproduce all AI-assisted content included in this PR without any AI tools.
/ 我可以在没有任何 AI 工具的情况下重现此 PR 中包含的所有 AI 辅助内容。
如果需要我调整措辞或增减内容,直接说。