Skip to content

Commit 36aecbf

Browse files
committed
feat(driver/cloudflare-imgbed): enhance cloudflare_imgbed API integration with improved error handling and pagination
1 parent 569dedf commit 36aecbf

3 files changed

Lines changed: 129 additions & 98 deletions

File tree

drivers/cloudflare_imgbed/driver.go

Lines changed: 78 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"github.com/OpenListTeam/OpenList/v4/internal/driver"
1010
"github.com/OpenListTeam/OpenList/v4/internal/errs"
1111
"github.com/OpenListTeam/OpenList/v4/internal/model"
12+
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
1213
"github.com/go-resty/resty/v2"
1314
)
1415

@@ -26,7 +27,8 @@ func (d *CFImgBed) GetAddition() driver.Additional {
2627
return &d.Addition
2728
}
2829

29-
// Init initializes the HTTP client with the configured Address and Token.
30+
// Init 使用 base 包提供的工厂方法初始化 HTTP 客户端,
31+
// 并设置 API 基础地址和鉴权请求头。
3032
func (d *CFImgBed) Init(ctx context.Context) error {
3133
d.client = base.NewRestyClient()
3234
d.client.SetBaseURL(strings.TrimRight(d.Address, "/")).
@@ -39,37 +41,41 @@ func (d *CFImgBed) Drop(ctx context.Context) error {
3941
return nil
4042
}
4143

42-
// apiError represents a generic error response from the CFImgBed API.
44+
// apiError 表示 CFImgBed API 返回的通用错误响应结构。
4345
type apiError struct {
4446
Error string `json:"error"`
4547
Message string `json:"message"`
4648
}
4749

48-
// buildReqPath constructs the path to send to the CFImgBed List API.
50+
// buildReqPath 根据挂载根路径和当前浏览目录,拼接出发送给 API 的请求路径。
4951
//
50-
// OpenList may call List() in two ways:
51-
// 1. List(nil) — initial load of the mount root
52-
// 2. List(obj) — where obj was returned by a previous List() call
52+
// OpenList 可能在两种场景下调用 List
53+
// 1. List(nil) — 首次加载挂载点根目录
54+
// 2. List(obj) — 用户点击进入某个子目录,obj 由上一次 List 返回
5355
//
54-
// When RootPath is set (e.g. "/telegram"), OpenList may pass a virtual root
55-
// dir object whose GetPath() already equals the root path itself. We must
56-
// detect this and avoid double-prepending rootPath.
56+
// 当设置了 RootPath(如 "/telegram")时,OpenList 首次调用的 dir 对象
57+
// 的 GetPath() 可能已经等于 rootPath 本身,此时不应重复拼接前缀。
5758
func buildReqPath(rootPath, dirPath string) string {
5859
rootPath = strings.Trim(rootPath, "/")
5960
dirPath = strings.Trim(dirPath, "/")
6061

6162
if dirPath == "" || dirPath == rootPath {
62-
// Either listing the real root, or OpenList passed the virtual root dir
63+
// 正在浏览根目录,或 OpenList 传入了虚拟根目录对象
6364
return rootPath
6465
}
6566
if rootPath == "" {
67+
// 未设置挂载前缀,直接使用目录路径
6668
return dirPath
6769
}
68-
// dirPath is a subfolder returned by a previous List call, prepend rootPath
70+
// 正常子目录:在目录路径前补上挂载根路径
6971
return rootPath + "/" + dirPath
7072
}
7173

72-
// List retrieves the file and directory listing for the given directory.
74+
// List 获取指定目录下的文件和子目录列表。
75+
//
76+
// 采用内部分页循环拉取,以防止单目录文件过多导致 API 响应超时或内存异常。
77+
// 每次请求 listPageSize 条记录,直到返回数量不足一页时退出循环,
78+
// 最终将所有分页结果汇总后一次性返回给 OpenList。
7379
func (d *CFImgBed) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
7480
rootPath := strings.Trim(d.GetRootPath(), "/")
7581

@@ -79,48 +85,69 @@ func (d *CFImgBed) List(ctx context.Context, dir model.Obj, args model.ListArgs)
7985
}
8086
reqPath := buildReqPath(rootPath, dirPath)
8187

82-
var resp ListResponse
83-
var errResp apiError
84-
res, err := d.client.R().
85-
SetQueryParam("dir", reqPath).
86-
SetQueryParam("count", "-1").
87-
SetResult(&resp).
88-
SetError(&errResp).
89-
Get("/api/manage/list")
90-
91-
if err != nil {
92-
return nil, err
93-
}
94-
if res.IsError() {
95-
if errResp.Message != "" {
96-
return nil, fmt.Errorf("CFImgBed API error: %s", errResp.Message)
88+
// 用于去重:API 在分页时每个页面都可能重复返回相同的目录列表,
89+
// 使用 map 确保同一个目录对象只被添加一次。
90+
dirSeen := make(map[string]bool)
91+
objs := make([]model.Obj, 0)
92+
93+
// 分页拉取循环
94+
start := 0
95+
for {
96+
var resp ListResponse
97+
var errResp apiError
98+
res, err := d.client.R().
99+
SetQueryParam("dir", reqPath).
100+
SetQueryParam("start", fmt.Sprintf("%d", start)).
101+
SetQueryParam("count", fmt.Sprintf("%d", listPageSize)).
102+
SetResult(&resp).
103+
SetError(&errResp).
104+
Get("/api/manage/list")
105+
106+
if err != nil {
107+
return nil, err
108+
}
109+
if res.IsError() {
110+
if errResp.Message != "" {
111+
return nil, fmt.Errorf("CFImgBed API error: %s", errResp.Message)
112+
}
113+
return nil, fmt.Errorf("CFImgBed API returned status %d", res.StatusCode())
97114
}
98-
return nil, fmt.Errorf("CFImgBed API returned status %d", res.StatusCode())
99-
}
100115

101-
objs := make([]model.Obj, 0, len(resp.Directories)+len(resp.Files))
116+
// 裁剪 API 返回路径中的挂载根前缀,
117+
// 使 GetPath() 返回的是相对于 OpenList 挂载点的路径,而非图床的绝对路径。
118+
for _, rawDir := range resp.Directories {
119+
cleanDir := strings.TrimRight(rawDir, "/")
120+
p := stripRootPrefix(cleanDir, rootPath)
121+
// 目录去重:分页场景下不同页面可能返回相同的目录条目
122+
if !dirSeen[p] {
123+
dirSeen[p] = true
124+
objs = append(objs, parseDir(p))
125+
}
126+
}
102127

103-
// Strip rootPath prefix from returned paths so that GetPath() is relative
104-
// to the OpenList mount point, not the CFImgBed root.
105-
for _, rawDir := range resp.Directories {
106-
cleanDir := strings.TrimRight(rawDir, "/")
107-
p := stripRootPrefix(cleanDir, rootPath)
108-
objs = append(objs, parseDir(p))
109-
}
128+
for _, item := range resp.Files {
129+
p := stripRootPrefix(item.Name, rootPath)
130+
objs = append(objs, parseFile(FileItem{
131+
Name: p,
132+
Metadata: item.Metadata,
133+
}))
134+
}
135+
136+
// 判断是否已到最后一页:当返回的文件和目录总数小于请求的每页数量时,
137+
// 说明本页已经是最后一页,无需继续请求。
138+
fetched := len(resp.Files) + len(resp.Directories)
139+
if fetched < listPageSize {
140+
break
141+
}
110142

111-
for _, item := range resp.Files {
112-
p := stripRootPrefix(item.Name, rootPath)
113-
objs = append(objs, parseFile(FileItem{
114-
Name: p,
115-
Metadata: item.Metadata,
116-
}))
143+
start += listPageSize
117144
}
118145

119146
return objs, nil
120147
}
121148

122-
// stripRootPrefix removes the rootPath prefix from a path returned by the API.
123-
// If rootPath is empty or the path doesn't start with rootPath/, return as-is.
149+
// stripRootPrefix 移除 API 返回路径中的挂载根前缀。
150+
// 如果未设置 rootPath 或路径不以 rootPath/ 开头,则原样返回。
124151
func stripRootPrefix(p, rootPath string) string {
125152
if rootPath == "" {
126153
return p
@@ -132,12 +159,13 @@ func stripRootPrefix(p, rootPath string) string {
132159
return p
133160
}
134161

135-
// Link constructs a direct download URL for the given file object.
136-
// Format: {Address}/file/{rootPath}/{filePath} with no double slashes.
162+
// Link 拼装文件的直接下载/访问链接。
163+
// 路径中可能包含空格、中文、#、+ 等特殊字符,必须进行安全编码以生成有效 URL。
137164
func (d *CFImgBed) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
138165
rootPath := strings.Trim(d.GetRootPath(), "/")
139166
filePath := strings.Trim(file.GetPath(), "/")
140167

168+
// 拼接完整路径,避免出现双斜杠
141169
var fullPath string
142170
if rootPath != "" && filePath != "" {
143171
fullPath = rootPath + "/" + filePath
@@ -147,7 +175,8 @@ func (d *CFImgBed) Link(ctx context.Context, file model.Obj, args model.LinkArgs
147175
fullPath = filePath
148176
}
149177

150-
link := strings.TrimRight(d.Address, "/") + "/file/" + fullPath
178+
// 对路径进行安全编码,处理空格、特殊字符等可能导致链接失效的情况
179+
link := strings.TrimRight(d.Address, "/") + "/file/" + utils.EncodePath(fullPath)
151180
return &model.Link{URL: link}, nil
152181
}
153182

@@ -195,4 +224,5 @@ func (d *CFImgBed) GetDetails(ctx context.Context) (*model.StorageDetails, error
195224
return nil, errs.NotImplement
196225
}
197226

227+
// 编译时检查 CFImgBed 是否完整实现 driver.Driver 接口。
198228
var _ driver.Driver = (*CFImgBed)(nil)

drivers/cloudflare_imgbed/meta.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"github.com/OpenListTeam/OpenList/v4/internal/op"
66
)
77

8+
// Addition 定义驱动在 OpenList 前端管理界面中显示的表单配置项。
89
type Addition struct {
910
driver.RootPath
1011
Address string `json:"address" type:"text" required:"true" default:"" help:"API domain, https://img.example.com"`

drivers/cloudflare_imgbed/types.go

Lines changed: 50 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -10,64 +10,67 @@ import (
1010
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
1111
)
1212

13-
// File represents a file object parsed from the CFImgBed List API response.
14-
// It implements the model.Obj interface.
13+
// File 表示从 CFImgBed 列表 API 响应中解析出的文件对象,实现 model.Obj 接口。
1514
type File struct {
16-
Path string
17-
Name_ string
18-
Size_ int64
19-
ModTime_ time.Time
20-
Mime_ string
15+
path string // 文件相对路径,如 "example/image.jpg"
16+
name string // 显示名称(路径最后一段),如 "image.jpg"
17+
size int64 // 文件大小(字节)
18+
modTime time.Time // 最后修改时间(从 Unix 毫秒时间戳转换而来)
19+
mime string // MIME 类型,如 "image/jpeg"
2120
}
2221

23-
func (f *File) GetPath() string { return f.Path }
24-
func (f *File) GetName() string { return f.Name_ }
25-
func (f *File) ModTime() time.Time { return f.ModTime_ }
26-
func (f *File) CreateTime() time.Time { return f.ModTime_ }
27-
func (f *File) GetSize() int64 { return f.Size_ }
22+
func (f *File) GetPath() string { return f.path }
23+
func (f *File) GetName() string { return f.name }
24+
func (f *File) ModTime() time.Time { return f.modTime }
25+
func (f *File) CreateTime() time.Time { return f.modTime }
26+
func (f *File) GetSize() int64 { return f.size }
2827
func (f *File) IsDir() bool { return false }
29-
func (f *File) GetID() string { return f.Path }
28+
func (f *File) GetID() string { return f.path }
3029
func (f *File) GetHash() utils.HashInfo { return utils.HashInfo{} }
3130

32-
// Dir represents a directory object parsed from the CFImgBed List API response.
33-
// It implements the model.Obj interface.
31+
// Dir 表示从 CFImgBed 列表 API 响应中解析出的目录对象,实现 model.Obj 接口。
3432
type Dir struct {
35-
Path string
36-
Name_ string
33+
path string // 目录相对路径,如 "example/subfolder"
34+
name string // 显示名称(路径最后一段),如 "subfolder"
3735
}
3836

39-
func (d *Dir) GetPath() string { return d.Path }
40-
func (d *Dir) GetName() string { return d.Name_ }
37+
func (d *Dir) GetPath() string { return d.path }
38+
func (d *Dir) GetName() string { return d.name }
4139
func (d *Dir) ModTime() time.Time { return time.Time{} }
4240
func (d *Dir) CreateTime() time.Time { return time.Time{} }
4341
func (d *Dir) GetSize() int64 { return 0 }
4442
func (d *Dir) IsDir() bool { return true }
45-
func (d *Dir) GetID() string { return d.Path }
43+
func (d *Dir) GetID() string { return d.path }
4644
func (d *Dir) GetHash() utils.HashInfo { return utils.HashInfo{} }
4745

48-
// Compile-time checks to ensure File and Dir implement model.Obj.
46+
// 编译时检查 File Dir 是否完整实现 model.Obj 接口。
4947
var _ model.Obj = (*File)(nil)
5048
var _ model.Obj = (*Dir)(nil)
5149

52-
// ListResponse represents the JSON structure returned by the CFImgBed List API.
50+
// listPageSize 定义每次向 API 请求的最大条目数。
51+
// 采用内部分页循环拉取,以防止单目录文件过多导致 API 响应超时或内存异常。
52+
const listPageSize = 1000
53+
54+
// ListResponse 表示 CFImgBed 列表 API 返回的 JSON 结构。
5355
type ListResponse struct {
5456
Files []FileItem `json:"files"`
5557
Directories []string `json:"directories"`
5658
}
5759

58-
// FileItem represents a single file entry in the List API response.
59-
// Metadata uses map[string]interface{} because the actual API returns mixed types:
60-
// - TimeStamp: integer (e.g. 1774910085474) in newer versions
61-
// - FileSizeBytes: integer (e.g. 3936071)
62-
// - FileSize: string (e.g. "3.75") — human-readable size
63-
// - FileType: string (e.g. "audio/mpeg")
64-
// - Legacy fields may use string values for numbers
60+
// FileItem 表示列表 API 返回的单个文件条目。
61+
// 注意:Metadata 使用 map[string]interface{} 而非 map[string]string,
62+
// 因为实际 API 返回的字段类型不统一:
63+
// - TimeStamp: 可能是整数(如 1774910085474),也可能在旧版本中是字符串
64+
// - FileSizeBytes: 整数(如 3936071)
65+
// - FileSize: 字符串(如 "3.75")— 仅供人类阅读的格式化大小
66+
// - FileType: 字符串(如 "audio/mpeg")
6567
type FileItem struct {
6668
Name string `json:"name"`
6769
Metadata map[string]interface{} `json:"metadata"`
6870
}
6971

70-
// getString safely extracts a string value from metadata, trying key in order.
72+
// getString 从 metadata 中安全提取字符串值,按 keys 顺序依次尝试。
73+
// 支持 string 和 float64(JSON 数字反序列化后的默认类型)两种输入。
7174
func getString(m map[string]interface{}, keys ...string) string {
7275
for _, k := range keys {
7376
if v, ok := m[k]; ok {
@@ -84,8 +87,9 @@ func getString(m map[string]interface{}, keys ...string) string {
8487
return ""
8588
}
8689

87-
// getInt64 safely extracts an int64 value from metadata, trying key in order.
88-
// Supports string, float64 (JSON number), and int64 types.
90+
// getInt64 从 metadata 中安全提取 int64 值,按 keys 顺序依次尝试。
91+
// 同时兼容 string、float64(JSON 数字)和 int64 三种反序列化类型,
92+
// 确保在不同 API 版本下均能正确解析。
8993
func getInt64(m map[string]interface{}, keys ...string) int64 {
9094
for _, k := range keys {
9195
if v, ok := m[k]; ok {
@@ -103,44 +107,40 @@ func getInt64(m map[string]interface{}, keys ...string) int64 {
103107
return 0
104108
}
105109

106-
// parseFile converts an API FileItem to a *File model.Obj.
107-
// It tries multiple key names for each field to handle different API versions:
108-
// - Size: FileSizeBytes (int) > File-Size (string)
109-
// - MIME: FileType > File-Mime
110-
// - Time: TimeStamp (handles both int and string)
110+
// parseFile API 返回的 FileItem 转换为 *File 对象。
111+
// 字段提取策略(兼容新旧 API 版本):
112+
// - 文件大小:优先取 FileSizeBytesint),回退到 File-Sizestring
113+
// - MIME 类型:优先取 FileType,回退到 File-Mime
114+
// - 修改时间:取 TimeStamp(同时处理 int string 两种格式)
111115
func parseFile(item FileItem) *File {
112116
name := path.Base(item.Name)
113117
var size int64
114118
var modTime time.Time
115119
var mime string
116120

117121
if item.Metadata != nil {
118-
// Try FileSizeBytes (int) first, fall back to File-Size (string)
119122
size = getInt64(item.Metadata, "FileSizeBytes", "File-Size")
120-
121-
// Try FileType first, fall back to File-Mime
122123
mime = getString(item.Metadata, "FileType", "File-Mime")
123-
124-
// TimeStamp may be int or string depending on API version
125124
ts := getInt64(item.Metadata, "TimeStamp")
126125
if ts > 0 {
127126
modTime = time.UnixMilli(ts)
128127
}
129128
}
130129

131130
return &File{
132-
Path: item.Name,
133-
Name_: name,
134-
Size_: size,
135-
ModTime_: modTime,
136-
Mime_: mime,
131+
path: item.Name,
132+
name: name,
133+
size: size,
134+
modTime: modTime,
135+
mime: mime,
137136
}
138137
}
139138

140-
// parseDir converts a directory path string from the API to a *Dir model.Obj.
139+
// parseDir 将 API 返回的目录路径字符串转换为 *Dir 对象。
140+
// 显示名称取路径的最后一段(即最深层目录名)。
141141
func parseDir(dirPath string) *Dir {
142142
return &Dir{
143-
Path: dirPath,
144-
Name_: path.Base(dirPath),
143+
path: dirPath,
144+
name: path.Base(dirPath),
145145
}
146146
}

0 commit comments

Comments
 (0)