feat(driver): add streamtape storage driver - #2921
Conversation
- Port Streamtape storage driver from AList to OpenList - Register streamtape driver in drivers/all.go
There was a problem hiding this comment.
Pull request overview
Adds a new Streamtape storage driver to OpenList (ported from AList), wiring it into the driver registry and providing Streamtape API interactions for listing, linking, uploads, and “Other” operations.
Changes:
- Introduces the
drivers/streamtapedriver implementation (API calls, listing, link generation, upload/remote-upload helpers). - Registers the Streamtape driver in
drivers/all.go. - Makes
build.shmore robust whengit logis unavailable (falls back to"unknown").
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| drivers/streamtape/driver.go | Implements Streamtape storage driver operations (List/Link/Put/PutURL/Other) and range-shaping behavior. |
| drivers/streamtape/util.go | Adds Streamtape API helper, ID encoding/decoding helpers, and upload response parsing. |
| drivers/streamtape/types.go | Defines API response/result types used by the driver. |
| drivers/streamtape/meta.go | Declares driver addition fields and driver config/registration. |
| drivers/all.go | Registers the new Streamtape driver via blank import. |
| build.sh | Avoids failing when git log cannot run by providing a default commit string. |
Suppressed comments (1)
drivers/streamtape/driver.go:226
- Casting
partSize(int64) tointcan overflow on 32-bit platforms or very large configured part sizes. Clamp to the platform max-int before converting to avoid wraparound and negative/incorrect part sizes.
link.Concurrency = concurrency
link.PartSize = int(partSize)
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
- Trim whitespace from JSON raw results before checking for null - Prevent empty ID prefixing in encodeFileID - Change ExtID and URL fields in remoteDlStatusItem to any - Update RangeMode help text and full mode range shaping behavior - Clamp partSize to platform max int to prevent 32-bit overflow - Return explicit error when upload ID cannot be determined
|
Thanks for the suggestions! All feedback has been addressed in the latest commit 5f7fa16. |
pikachuren
left a comment
There was a problem hiding this comment.
🙏 感谢贡献
感谢 @gurglegift 提交此PR!我已完成代码评审,以下是评审结果。
🤖 AI 自动审核声明
本评审报告由 AI 自动生成,当前使用 Claude Opus 5 模型进行分析,部分复杂场景可能辅助使用 ChatGPT、DeepSeek 等模型进行交叉验证。
⚠️ AI 分析结果仅供参考,可能存在误判或遗漏。如您发现任何问题或有不同意见,欢迎随时提出讨论和纠正。
⚠️ 重要提醒:即使 AI 评审认为代码质量良好且建议合并,最终是否合并仍需由项目维护者进行人工判定。项目维护者会综合考虑代码质量、项目规划、技术方向、团队资源等多方面因素做出决策。
📖 PR背景与需求
PR标题:feat(driver): add streamtape storage driver
关联Issue:未关联明确Issue
需求说明:
从 AList 项目移植 Streamtape 存储驱动到 OpenList,为 OpenList 增加对 Streamtape 云存储服务的原生支持。Streamtape 是一个文件托管和视频流媒体服务,支持在线预览、远程下载等功能。
预期目标:
- 完整实现 Streamtape API 的核心功能(列表、上传、下载、删除、移动、重命名等)
- 支持 Streamtape 的特色功能(远程下载、缩略图、视频转换状态查询)
- 提供灵活的 Range 请求策略以优化流媒体播放体验
- 确保驱动行为与 OpenList 现有驱动规范一致
AI辅助说明:
作者已声明使用 AI(Antigravity)辅助代码生成,并已完成审查验证。
📋 问题摘要
- ✅ 优点:功能完整、架构清晰、Range策略灵活
⚠️ 安全性:1个P1级别问题(SHA256参数未实际使用)⚠️ 代码质量:2个改进建议(错误处理、重试机制)- 💡 改进建议:3个可选优化(测试覆盖、文档补充、性能优化)
📂 逐文件分析
build.sh
改动意图:
修复构建脚本在没有 git 仓库环境下的兼容性问题。
代码修改逻辑:
# 修改前
gitCommit=$(git log --pretty=format:"%h" -1)
# 修改后
gitCommit=$(git log --pretty=format:"%h" -1 2>/dev/null || echo "unknown")当 git log 失败时(例如非 git 仓库、浅克隆等情况),使用 "unknown" 作为默认值,避免构建中断。
合理性评估:
- ✅ 优点:提升了构建脚本的健壮性,支持更多构建场景(Docker构建、CI/CD环境等)
- ✅ 向下兼容:不影响正常 git 仓库的构建流程
- ✅ 最佳实践:符合防御性编程原则
drivers/all.go
改动意图:
在全局驱动注册列表中注册 Streamtape 驱动。
代码修改逻辑:
_ "github.com/OpenListTeam/OpenList/v4/drivers/streamtape"按字母顺序插入在 sftp 和 strm 之间,符合 OpenList 的代码组织规范。
合理性评估:
- ✅ 优点:注册方式标准,位置正确
- ✅ 规范性:遵循 OpenList 驱动注册机制
drivers/streamtape/driver.go (545行,核心文件)
改动意图:
实现 Streamtape 驱动的完整功能逻辑,包括文件操作、流媒体优化和特色功能。
代码修改逻辑:
1. 初始化与配置验证
func (d *Streamtape) Init(ctx context.Context) error {
// 验证必需配置
if strings.TrimSpace(d.APILogin) == "" || strings.TrimSpace(d.APIKey) == "" {
return errors.New("api_login and api_key are required")
}
// 测试账号连接
var account accountInfo
if err := d.callAPI(ctx, "/account/info", nil, &account); err != nil {
return err
}
return nil
}2. Range策略实现(流媒体优化的核心)
func (d *Streamtape) applyRangeStrategy(link *model.Link, size int64) {
switch mode {
case "full":
// 不限制Range,允许透明流式传输
return
case "percent":
// 按文件大小百分比设置分片大小
partSize := size * int64(percent) / 100
link.Concurrency = 1
link.PartSize = int(partSize)
default: // "chunk"
// 固定大小分片,支持并发
partSize := int64(chunkMB) * 1024 * 1024
link.Concurrency = concurrency
link.PartSize = int(partSize)
}
}设计亮点:
- full模式:适合小文件或稳定网络,不做Range限制
- percent模式:根据文件大小动态调整分片,适合不同规模文件
- chunk模式:固定大小+并发,平衡性能和稳定性
3. 下载链接获取(防止频率限制)
func (d *Streamtape) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
// 第一步:获取下载ticket
var ticket dlTicketResult
if err := d.callAPI(ctx, "/file/dlticket", map[string]string{"file": fileID}, &ticket); err != nil {
return nil, err
}
// 第二步:等待服务器要求的时间
if ticket.WaitTime > 0 {
timer := time.NewTimer(time.Duration(ticket.WaitTime+1) * time.Second)
select {
case <-ctx.Done():
timer.Stop()
return nil, ctx.Err()
case <-timer.C:
}
}
// 第三步:最多重试3次获取下载链接
for i := 0; i < 3; i++ {
err = d.callAPI(ctx, "/file/dl", params, &dl)
if err == nil {
break
}
// 从错误信息中提取需要等待的秒数
waitSeconds = extractWaitSecondsFromErr(err)
if waitSeconds <= 0 {
return nil, err
}
// 动态等待
timer := time.NewTimer(time.Duration(waitSeconds+1) * time.Second)
select {
case <-ctx.Done():
timer.Stop()
return nil, ctx.Err()
case <-timer.C:
}
}
}设计亮点:
- ✅ 遵守 Streamtape API 频率限制规则
- ✅ 支持上下文取消(优雅中断)
- ✅ 智能重试机制(从错误消息中提取等待时间)
4. 上传功能(带回退机制)
func (d *Streamtape) Put(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {
// 上传文件
res, err := base.RestyClient.R().
SetContext(ctx).
SetFileReader("file1", file.GetName(), reader).
Post(uploadURL.URL)
// 从响应中提取文件ID
uploadedID := extractFileIDFromUploadBody(res.Body())
// 如果提取失败,回退到目录扫描
if uploadedID == "" {
list, listErr := d.List(ctx, &model.Object{ID: encodeFolderID(folderID), IsFolder: true}, model.ListArgs{})
if listErr == nil {
for _, obj := range list {
if obj.GetName() == file.GetName() && (file.GetSize() <= 0 || obj.GetSize() == file.GetSize()) {
return obj, nil
}
}
}
return nil, errors.New("uploaded file ID not found in response or directory scan")
}
}设计亮点:
- ✅ 主流程:从上传响应中解析文件ID
- ✅ 回退机制:当响应解析失败时,通过目录扫描匹配文件名和大小
- ✅ 错误处理:明确告知无法定位上传结果
5. 特色功能实现
func (d *Streamtape) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
switch strings.ToLower(args.Method) {
case "remotedl_status": // 查询远程下载状态
return d.remoteDlStatus(ctx, args)
case "remotedl_remove": // 取消远程下载
return d.remoteDlRemove(ctx, args)
case "file_info": // 获取文件详细信息
return d.fileInfo(ctx, args)
case "thumbnail": // 获取视频缩略图
return d.thumbnail(ctx, args)
case "conversion_status": // 查询视频转换状态
return d.conversionStatus(ctx, args)
default:
return nil, errs.NotSupport
}
}合理性评估:
✅ 优点:
- 功能完整:实现了 Streamtape API 的核心功能和特色功能
- 流媒体优化:灵活的 Range 策略适应不同播放场景
- 智能重试:下载链接获取支持动态等待和重试
- 上下文支持:所有长时间操作都支持优雅取消
- 错误处理:清晰的错误传递和友好的错误信息
- 回退机制:上传失败时通过目录扫描确认结果
- SHA256参数未实际使用
// 当前实现
if d.Sha256 != "" {
params["sha256"] = d.Sha256 // 仅传递给API,未做客户端验证
}问题:
- 配置中声明了
Sha256字段(用于上传验证),但实际代码中只是将其传递给API,没有在客户端进行任何验证 - 如果API不支持或忽略该参数,用户期望的完整性校验将失效
建议:
- 要么在上传前/后计算文件SHA256并验证
- 要么从配置中移除该字段,避免误导用户
- Move操作限制不够友好
if folderID == "" || folderID == "0" {
return nil, fmt.Errorf("streamtape move to root is not supported by API")
}问题:虽然在 meta.go 中已有 Alert 提示,但用户实际操作时仍会遇到错误
建议:在 Init 阶段输出警告日志,或在前端显示更明显的提示
💡 可选改进(P2/P3):
- 补充单元测试(P2)
// 建议测试用例
- TestApplyRangeStrategy(验证三种模式的分片计算)
- TestExtractWaitSecondsFromErr(验证错误消息解析)
- TestExtractFileIDFromUploadBody(验证ID提取逻辑)
- TestEncodeDecode(验证ID编码/解码的可逆性)- 补充用户文档(P3)
建议在drivers/streamtape/README.md中说明:
- 如何获取 API Login 和 API Key
- Range 策略的适用场景
- 远程下载功能的使用方法
- 视频转换状态查询的触发条件
- 性能优化(P3)
// 当前实现:每次List都完整遍历
func (d *Streamtape) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
// 建议:支持分页或限制返回数量
}drivers/streamtape/meta.go
改动意图:
定义驱动的元数据、配置结构和注册信息。
代码修改逻辑:
配置结构:
type Addition struct {
driver.RootID
APILogin string `json:"api_login" required:"true"`
APIKey string `json:"api_key" required:"true"`
RangeMode string `json:"range_mode" type:"select" options:"chunk,full,percent" default:"chunk"`
RangeChunkMB int `json:"range_chunk_mb" type:"number" default:"8"`
RangeConcurrency int `json:"range_concurrency" type:"number" default:"4"`
RangePercent int `json:"range_percent" type:"number" default:"15"`
EnableRangeControl bool `json:"enable_range_control" default:"true"`
Sha256 string `json:"sha256"`
}驱动配置:
var config = driver.Config{
Name: "Streamtape",
LocalSort: false, // 依赖API排序
OnlyProxy: true, // 必须通过代理(Streamtape不支持直接客户端访问)
NoCache: false, // 允许缓存
NoUpload: false, // 支持上传
DefaultRoot: "0", // 默认根目录ID
Alert: "warning|Moving files to root folder is not supported by Streamtape API",
ProxyRangeOption: true, // 启用代理Range选项
}合理性评估:
- ✅ 配置清晰:字段命名直观,帮助文本完善
- ✅ 默认值合理:chunk模式8MB分片、4并发、15%百分比都是经过测试的推荐值
- ✅ 警告机制:明确告知用户API限制
⚠️ SHA256字段:如前所述,该字段未被实际使用,建议移除或实现验证
drivers/streamtape/types.go
改动意图:
定义 Streamtape API 的请求/响应数据结构。
代码修改逻辑:
定义了8个核心数据结构:
apiResponse:通用API响应包装accountInfo:账号信息listFolderResult:文件夹列表dlTicketResult、dlResult:下载链接获取remoteDlStatusResult:远程下载状态fileInfoResult:文件详细信息conversionResult:视频转换状态
合理性评估:
- ✅ 结构完整:覆盖了所有API端点
- ✅ 类型准确:正确使用
int64、interface{}、json.RawMessage等类型 - ✅ 命名规范:遵循 Go 命名约定
drivers/streamtape/util.go
改动意图:
提供辅助函数,包括API调用、ID编解码、对象构建等。
代码修改逻辑:
1. API调用封装
func (d *Streamtape) callAPI(ctx context.Context, endpoint string, params map[string]string, out any) error {
// 自动添加认证参数
query := map[string]string{
"login": d.APILogin,
"key": d.APIKey,
}
// 合并用户参数
for k, v := range params {
if strings.TrimSpace(v) == "" {
continue // 跳过空参数
}
query[k] = v
}
// 发送请求
var resp apiResponse
r, err := base.RestyClient.R().
SetContext(ctx).
SetQueryParams(query).
SetResult(&resp).
Get(apiBase + endpoint)
// 检查HTTP状态
if r.StatusCode() != http.StatusOK {
return fmt.Errorf("streamtape http error: %d", r.StatusCode())
}
// 检查API状态
if resp.Status != 200 {
return fmt.Errorf("streamtape api error: status=%d msg=%s", resp.Status, resp.Msg)
}
// 解析结果
if out != nil && len(resp.Result) > 0 {
if err := json.Unmarshal(resp.Result, out); err != nil {
return fmt.Errorf("decode streamtape result failed: %w", err)
}
}
return nil
}设计亮点:
- ✅ 自动处理认证
- ✅ 过滤空参数
- ✅ 分层错误检查(HTTP层、API层、解析层)
- ✅ 上下文支持
2. ID编解码
// 文件夹ID:d:xxx
func encodeFolderID(id string) string {
if id == "" || id == "0" || id == "/" {
return "d:0"
}
return "d:" + id
}
// 文件ID:f:xxx
func encodeFileID(id string) string {
if id == "" {
return ""
}
return "f:" + id
}
// 远程上传ID:ru:xxx
func encodeRemoteUploadID(id string) string {
return "ru:" + id
}设计亮点:
- ✅ 类型前缀明确区分不同资源
- ✅ 处理边界情况(空ID、根目录)
- ✅ 一致的编码规则
3. 上传响应解析
func extractFileIDFromUploadBody(body []byte) string {
var resp apiResponse
if err := json.Unmarshal(body, &resp); err != nil {
return ""
}
var result map[string]any
if err := json.Unmarshal(resp.Result, &result); err != nil {
return ""
}
// 尝试多个可能的字段名
for _, key := range []string{"file", "fileid", "id", "linkid"} {
if v, ok := result[key]; ok {
if s, ok := v.(string); ok && s != "" {
return s
}
}
}
return ""
}设计亮点:
- ✅ 容错性强:尝试多个可能的字段名
- ✅ 类型安全:检查类型断言
- ✅ 失败静默:返回空字符串而非panic
合理性评估:
- ✅ 辅助函数完善:覆盖了所有必要的转换和解析逻辑
- ✅ 错误处理稳健:所有解析失败都返回零值而非panic
- ✅ 代码复用性好:清晰的职责划分
🎯 总体评价
功能性:⭐⭐⭐⭐⭐ - 功能完整,覆盖核心操作和特色功能
安全性:⭐⭐⭐⭐ - 整体安全,但SHA256参数未实际使用(P1问题)
代码质量:⭐⭐⭐⭐⭐ - 代码结构清晰,错误处理完善,遵循最佳实践
实现方案:⭐⭐⭐⭐⭐ - Range策略设计优秀,智能重试机制完善,上下文支持到位
技术亮点:
- 流媒体优化:三种Range策略(full/percent/chunk)灵活适配不同场景
- 智能等待:下载链接获取时动态解析等待时间并自动重试
- 回退机制:上传结果解析失败时通过目录扫描确认
- 上下文友好:所有长时间操作都支持优雅取消
- 错误处理完善:分层错误检查,清晰的错误信息
建议操作:
- 🔄 Request Changes(需要小幅修改)
理由:
此PR整体质量优秀,功能实现完整且技术方案先进。然而存在一个P1级别问题(SHA256参数未实际使用)需要修复:
- 方案1:实现客户端SHA256验证(推荐)
- 方案2:从配置中移除该字段并更新文档
修复后该PR将达到教科书级别,强烈建议合并。
📝 详细修改建议
必须修复(P1)
1. SHA256参数处理
当前问题:
// drivers/streamtape/meta.go
Sha256 string `json:"sha256" help:"Expected SHA256 hash for upload verification (optional)"`
// drivers/streamtape/driver.go
if d.Sha256 != "" {
params["sha256"] = d.Sha256 // 仅传递,未验证
}修复建议(二选一):
方案A:实现完整验证(推荐)
func (d *Streamtape) Put(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {
var actualSHA256 string
// 如果配置了期望的SHA256,计算实际SHA256
if d.Sha256 != "" {
hash := sha256.New()
teeReader := io.TeeReader(file, hash)
// 使用teeReader进行上传...
actualSHA256 = hex.EncodeToString(hash.Sum(nil))
// 验证
if !strings.EqualFold(actualSHA256, d.Sha256) {
return nil, fmt.Errorf("sha256 mismatch: expected=%s actual=%s", d.Sha256, actualSHA256)
}
}
// 继续正常上传流程...
}方案B:移除该字段
// 从 Addition 结构体中删除 Sha256 字段
// 从 API 调用中移除相关参数可选改进(P2)
2. 补充单元测试
// drivers/streamtape/driver_test.go
package streamtape
import (
"testing"
)
func TestApplyRangeStrategy(t *testing.T) {
tests := []struct{
name string
mode string
size int64
wantPartSize int
wantConcurrency int
}{
{"full mode", "full", 100*1024*1024, 0, 0},
{"chunk mode", "chunk", 100*1024*1024, 8*1024*1024, 4},
{"percent mode", "percent", 100*1024*1024, 15*1024*1024, 1},
}
// 测试实现...
}
func TestExtractWaitSecondsFromErr(t *testing.T) {
tests := []struct{
err error
want int
}{
{errors.New("wait 10 more seconds"), 10},
{errors.New("wait 1 more second"), 1},
{errors.New("other error"), 0},
}
// 测试实现...
}3. Move操作友好提示
func (d *Streamtape) Init(ctx context.Context) error {
// 现有验证...
// 添加警告日志
log.Warn("Streamtape does not support moving files to root folder. Please ensure all move operations target a non-root destination.")
return nil
}可选优化(P3)
4. 补充README文档
创建 drivers/streamtape/README.md:
# Streamtape Driver
## Configuration
### Required Fields
- `api_login`: Your Streamtape API login (found in account settings)
- `api_key`: Your Streamtape API key (found in account settings)
### Range Control Options
**range_mode**: Strategy for handling range requests
- `chunk` (default): Fixed-size chunks with concurrency
- `full`: No driver-level range shaping, transparent streaming
- `percent`: Part size based on file percentage
**range_chunk_mb**: Chunk size in MB for `chunk` mode (default: 8)
**range_concurrency**: Concurrent requests for `chunk` mode (default: 4)
**range_percent**: Percentage for `percent` mode (default: 15)
### Special Features
**Remote Downloads**:POST /api/fs/other
{
"method": "remotedl_status",
"data": {"id": "upload_id"}
}
**Thumbnails**:
POST /api/fs/other
{
"method": "thumbnail"
}
## Limitations
- Moving files to root folder is not supported by Streamtape API
- Copy operation is not implemented
📊 对比分析
与之前评审的 PDS 驱动(PR #2663)相比:
| 维度 | Streamtape (PR #2921) | PDS (PR #2663) |
|---|---|---|
| 代码规模 | ~800行 | ~800行 |
| 功能完整性 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| 安全性 | ⭐⭐⭐⭐(SHA256未验证) | ⭐⭐⭐(uploadToken未签名) |
| 特色功能 | 远程下载、缩略图、Range优化 | 客户端直传、OAuth刷新 |
| 测试覆盖 | 未包含 | 未包含 |
| 文档完善度 | 缺少README | 配套PR已补充 |
结论:两个PR质量都很高,Streamtape的安全问题相对较轻(仅影响可选功能),修复后质量优于PDS。
✅ 总结
此PR展现了优秀的工程实践:
- ✅ 功能完整且实现精良
- ✅ Range策略设计先进,适配流媒体场景
- ✅ 智能重试和回退机制完善
- ✅ 代码结构清晰,易于维护
修复SHA256参数问题后,此PR将达到教科书级别,强烈建议合并。
修复优先级:
- P1(必须):SHA256参数处理
- P2(建议):补充单元测试、Move操作提示
- P3(可选):补充README文档、性能优化
期待看到修复后的版本!🎉
|
文档建议补充到doc项目,而不是README |
| builtAt="$(date +'%F %T %z')" | ||
| gitAuthor="The OpenList Projects Contributors <noreply@openlist.team>" | ||
| gitCommit=$(git log --pretty=format:"%h" -1) | ||
| gitCommit=$(git log --pretty=format:"%h" -1 2>/dev/null || echo "unknown") |
| RangeConcurrency int `json:"range_concurrency" type:"number" default:"4" help:"Chunk mode concurrent upstream requests"` | ||
| RangePercent int `json:"range_percent" type:"number" default:"15" help:"Percent mode part size percentage (1-100)"` | ||
| EnableRangeControl bool `json:"enable_range_control" default:"true" help:"Enable driver-level range shaping for smoother streaming"` | ||
| Sha256 string `json:"sha256" help:"Expected SHA256 hash for upload verification (optional)"` |
Summary / 摘要
Testing / 测试
Checklist / 检查清单
gofmtorgo fmt.AI Disclosure / AI 使用声明
Tools used / 使用工具:
Usage scope / 使用范围:
Code generation / 代码生成
I have reviewed and validated all AI-assisted content included in this PR.