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 基础地址和鉴权请求头。
3032func (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 返回的通用错误响应结构。
4345type 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 本身,此时不应重复拼接前缀。
5758func 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。
7379func (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/ 开头,则原样返回。
124151func 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。
137164func (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 接口。
198228var _ driver.Driver = (* CFImgBed )(nil )
0 commit comments