Skip to content

Commit 202f77d

Browse files
committed
feat(driver): add Cloudflare Image Bed support
1 parent e28406f commit 202f77d

7 files changed

Lines changed: 383 additions & 1 deletion

File tree

drivers/all.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ import (
8282
_ "github.com/OpenListTeam/OpenList/v4/drivers/wopan"
8383
_ "github.com/OpenListTeam/OpenList/v4/drivers/wps"
8484
_ "github.com/OpenListTeam/OpenList/v4/drivers/yandex_disk"
85+
_ "github.com/OpenListTeam/OpenList/v4/drivers/cfimgbed"
8586
)
8687

8788
// All do nothing,just for import

drivers/cfimgbed/driver.go

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
package cfimgbed
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"strings"
7+
"time"
8+
9+
"github.com/OpenListTeam/OpenList/v4/internal/driver"
10+
"github.com/OpenListTeam/OpenList/v4/internal/errs"
11+
"github.com/OpenListTeam/OpenList/v4/internal/model"
12+
"github.com/go-resty/resty/v2"
13+
)
14+
15+
type CFImgBed struct {
16+
model.Storage
17+
Addition
18+
client *resty.Client
19+
}
20+
21+
func (d *CFImgBed) Config() driver.Config {
22+
return config
23+
}
24+
25+
func (d *CFImgBed) GetAddition() driver.Additional {
26+
return &d.Addition
27+
}
28+
29+
// Init initializes the HTTP client with the configured Address and Token.
30+
func (d *CFImgBed) Init(ctx context.Context) error {
31+
d.client = resty.New().
32+
SetBaseURL(strings.TrimRight(d.Address, "/")).
33+
SetTimeout(30*time.Second).
34+
SetHeader("Authorization", "Bearer "+d.Token).
35+
SetDebug(false)
36+
return nil
37+
}
38+
39+
func (d *CFImgBed) Drop(ctx context.Context) error {
40+
return nil
41+
}
42+
43+
// apiError represents a generic error response from the CFImgBed API.
44+
type apiError struct {
45+
Error string `json:"error"`
46+
Message string `json:"message"`
47+
}
48+
49+
// buildReqPath constructs the path to send to the CFImgBed List API.
50+
//
51+
// OpenList may call List() in two ways:
52+
// 1. List(nil) — initial load of the mount root
53+
// 2. List(obj) — where obj was returned by a previous List() call
54+
//
55+
// When RootPath is set (e.g. "/telegram"), OpenList may pass a virtual root
56+
// dir object whose GetPath() already equals the root path itself. We must
57+
// detect this and avoid double-prepending rootPath.
58+
func buildReqPath(rootPath, dirPath string) string {
59+
rootPath = strings.Trim(rootPath, "/")
60+
dirPath = strings.Trim(dirPath, "/")
61+
62+
if dirPath == "" || dirPath == rootPath {
63+
// Either listing the real root, or OpenList passed the virtual root dir
64+
return rootPath
65+
}
66+
if rootPath == "" {
67+
return dirPath
68+
}
69+
// dirPath is a subfolder returned by a previous List call, prepend rootPath
70+
return rootPath + "/" + dirPath
71+
}
72+
73+
// List retrieves the file and directory listing for the given directory.
74+
func (d *CFImgBed) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
75+
rootPath := strings.Trim(d.GetRootPath(), "/")
76+
77+
var dirPath string
78+
if dir != nil {
79+
dirPath = strings.Trim(dir.GetPath(), "/")
80+
}
81+
reqPath := buildReqPath(rootPath, dirPath)
82+
83+
var resp ListResponse
84+
var errResp apiError
85+
res, err := d.client.R().
86+
SetQueryParam("dir", reqPath).
87+
SetQueryParam("count", "-1").
88+
SetResult(&resp).
89+
SetError(&errResp).
90+
Get("/api/manage/list")
91+
92+
if err != nil {
93+
return nil, err
94+
}
95+
if res.IsError() {
96+
if errResp.Message != "" {
97+
return nil, fmt.Errorf("CFImgBed API error: %s", errResp.Message)
98+
}
99+
return nil, fmt.Errorf("CFImgBed API returned status %d", res.StatusCode())
100+
}
101+
102+
objs := make([]model.Obj, 0, len(resp.Directories)+len(resp.Files))
103+
104+
// Strip rootPath prefix from returned paths so that GetPath() is relative
105+
// to the OpenList mount point, not the CFImgBed root.
106+
for _, rawDir := range resp.Directories {
107+
cleanDir := strings.TrimRight(rawDir, "/")
108+
p := stripRootPrefix(cleanDir, rootPath)
109+
objs = append(objs, parseDir(p))
110+
}
111+
112+
for _, item := range resp.Files {
113+
p := stripRootPrefix(item.Name, rootPath)
114+
objs = append(objs, parseFile(FileItem{
115+
Name: p,
116+
Metadata: item.Metadata,
117+
}))
118+
}
119+
120+
return objs, nil
121+
}
122+
123+
// stripRootPrefix removes the rootPath prefix from a path returned by the API.
124+
// If rootPath is empty or the path doesn't start with rootPath/, return as-is.
125+
func stripRootPrefix(p, rootPath string) string {
126+
if rootPath == "" {
127+
return p
128+
}
129+
prefix := rootPath + "/"
130+
if strings.HasPrefix(p, prefix) {
131+
return strings.TrimPrefix(p, prefix)
132+
}
133+
return p
134+
}
135+
136+
// Link constructs a direct download URL for the given file object.
137+
// Format: {Address}/file/{rootPath}/{filePath} with no double slashes.
138+
func (d *CFImgBed) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
139+
rootPath := strings.Trim(d.GetRootPath(), "/")
140+
filePath := strings.Trim(file.GetPath(), "/")
141+
142+
var fullPath string
143+
if rootPath != "" && filePath != "" {
144+
fullPath = rootPath + "/" + filePath
145+
} else if rootPath != "" {
146+
fullPath = rootPath
147+
} else {
148+
fullPath = filePath
149+
}
150+
151+
link := strings.TrimRight(d.Address, "/") + "/file/" + fullPath
152+
return &model.Link{URL: link}, nil
153+
}
154+
155+
func (d *CFImgBed) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {
156+
return nil, errs.NotImplement
157+
}
158+
159+
func (d *CFImgBed) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
160+
return nil, errs.NotImplement
161+
}
162+
163+
func (d *CFImgBed) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) {
164+
return nil, errs.NotImplement
165+
}
166+
167+
func (d *CFImgBed) Copy(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
168+
return nil, errs.NotImplement
169+
}
170+
171+
func (d *CFImgBed) Remove(ctx context.Context, obj model.Obj) error {
172+
return errs.NotImplement
173+
}
174+
175+
func (d *CFImgBed) Put(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {
176+
return nil, errs.NotImplement
177+
}
178+
179+
func (d *CFImgBed) GetArchiveMeta(ctx context.Context, obj model.Obj, args model.ArchiveArgs) (model.ArchiveMeta, error) {
180+
return nil, errs.NotImplement
181+
}
182+
183+
func (d *CFImgBed) ListArchive(ctx context.Context, obj model.Obj, args model.ArchiveInnerArgs) ([]model.Obj, error) {
184+
return nil, errs.NotImplement
185+
}
186+
187+
func (d *CFImgBed) Extract(ctx context.Context, obj model.Obj, args model.ArchiveInnerArgs) (*model.Link, error) {
188+
return nil, errs.NotImplement
189+
}
190+
191+
func (d *CFImgBed) ArchiveDecompress(ctx context.Context, srcObj, dstDir model.Obj, args model.ArchiveDecompressArgs) ([]model.Obj, error) {
192+
return nil, errs.NotImplement
193+
}
194+
195+
func (d *CFImgBed) GetDetails(ctx context.Context) (*model.StorageDetails, error) {
196+
return nil, errs.NotImplement
197+
}
198+
199+
var _ driver.Driver = (*CFImgBed)(nil)

drivers/cfimgbed/meta.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package cfimgbed
2+
3+
import (
4+
"github.com/OpenListTeam/OpenList/v4/internal/driver"
5+
"github.com/OpenListTeam/OpenList/v4/internal/op"
6+
)
7+
8+
type Addition struct {
9+
driver.RootPath
10+
Address string `json:"address" type:"text" required:"true" default:"" help:"API 域名,如 https://img.example.com"`
11+
Token string `json:"token" type:"text" required:"true" default:"" help:"API 认证 Token"`
12+
}
13+
14+
var config = driver.Config{
15+
Name: "CFImgBed",
16+
LocalSort: false,
17+
OnlyProxy: false,
18+
NoCache: false,
19+
NoUpload: true,
20+
NeedMs: false,
21+
DefaultRoot: "/",
22+
CheckStatus: false,
23+
Alert: "",
24+
NoOverwriteUpload: false,
25+
NoLinkURL: false,
26+
}
27+
28+
func init() {
29+
op.RegisterDriver(func() driver.Driver {
30+
return &CFImgBed{}
31+
})
32+
}

drivers/cfimgbed/types.go

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
package cfimgbed
2+
3+
import (
4+
"fmt"
5+
"path"
6+
"strconv"
7+
"time"
8+
9+
"github.com/OpenListTeam/OpenList/v4/internal/model"
10+
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
11+
)
12+
13+
// File represents a file object parsed from the CFImgBed List API response.
14+
// It implements the model.Obj interface.
15+
type File struct {
16+
Path string
17+
Name_ string
18+
Size_ int64
19+
ModTime_ time.Time
20+
Mime_ string
21+
}
22+
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_ }
28+
func (f *File) IsDir() bool { return false }
29+
func (f *File) GetID() string { return f.Path }
30+
func (f *File) GetHash() utils.HashInfo { return utils.HashInfo{} }
31+
32+
// Dir represents a directory object parsed from the CFImgBed List API response.
33+
// It implements the model.Obj interface.
34+
type Dir struct {
35+
Path string
36+
Name_ string
37+
}
38+
39+
func (d *Dir) GetPath() string { return d.Path }
40+
func (d *Dir) GetName() string { return d.Name_ }
41+
func (d *Dir) ModTime() time.Time { return time.Time{} }
42+
func (d *Dir) CreateTime() time.Time { return time.Time{} }
43+
func (d *Dir) GetSize() int64 { return 0 }
44+
func (d *Dir) IsDir() bool { return true }
45+
func (d *Dir) GetID() string { return d.Path }
46+
func (d *Dir) GetHash() utils.HashInfo { return utils.HashInfo{} }
47+
48+
// Compile-time checks to ensure File and Dir implement model.Obj.
49+
var _ model.Obj = (*File)(nil)
50+
var _ model.Obj = (*Dir)(nil)
51+
52+
// ListResponse represents the JSON structure returned by the CFImgBed List API.
53+
type ListResponse struct {
54+
Files []FileItem `json:"files"`
55+
Directories []string `json:"directories"`
56+
}
57+
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
65+
type FileItem struct {
66+
Name string `json:"name"`
67+
Metadata map[string]interface{} `json:"metadata"`
68+
}
69+
70+
// getString safely extracts a string value from metadata, trying key in order.
71+
func getString(m map[string]interface{}, keys ...string) string {
72+
for _, k := range keys {
73+
if v, ok := m[k]; ok {
74+
switch val := v.(type) {
75+
case string:
76+
return val
77+
case float64:
78+
return strconv.FormatInt(int64(val), 10)
79+
default:
80+
return fmt.Sprintf("%v", val)
81+
}
82+
}
83+
}
84+
return ""
85+
}
86+
87+
// getInt64 safely extracts an int64 value from metadata, trying key in order.
88+
// Supports string, float64 (JSON number), and int64 types.
89+
func getInt64(m map[string]interface{}, keys ...string) int64 {
90+
for _, k := range keys {
91+
if v, ok := m[k]; ok {
92+
switch val := v.(type) {
93+
case string:
94+
n, _ := strconv.ParseInt(val, 10, 64)
95+
return n
96+
case float64:
97+
return int64(val)
98+
case int64:
99+
return val
100+
}
101+
}
102+
}
103+
return 0
104+
}
105+
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)
111+
func parseFile(item FileItem) *File {
112+
name := path.Base(item.Name)
113+
var size int64
114+
var modTime time.Time
115+
var mime string
116+
117+
if item.Metadata != nil {
118+
// Try FileSizeBytes (int) first, fall back to File-Size (string)
119+
size = getInt64(item.Metadata, "FileSizeBytes", "File-Size")
120+
121+
// Try FileType first, fall back to File-Mime
122+
mime = getString(item.Metadata, "FileType", "File-Mime")
123+
124+
// TimeStamp may be int or string depending on API version
125+
ts := getInt64(item.Metadata, "TimeStamp")
126+
if ts > 0 {
127+
modTime = time.UnixMilli(ts)
128+
}
129+
}
130+
131+
return &File{
132+
Path: item.Name,
133+
Name_: name,
134+
Size_: size,
135+
ModTime_: modTime,
136+
Mime_: mime,
137+
}
138+
}
139+
140+
// parseDir converts a directory path string from the API to a *Dir model.Obj.
141+
func parseDir(dirPath string) *Dir {
142+
return &Dir{
143+
Path: dirPath,
144+
Name_: path.Base(dirPath),
145+
}
146+
}

drivers/cfimgbed/util.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
package cfimgbed
2+
3+
// do others that not defined in Driver interface

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,3 +313,5 @@ replace github.com/ProtonMail/go-proton-api => github.com/henrybear327/go-proton
313313
replace github.com/cronokirby/saferith => github.com/Da3zKi7/saferith v0.33.0-fixed
314314

315315
// replace github.com/OpenListTeam/115-sdk-go => ../../OpenListTeam/115-sdk-go
316+
317+
replace github.com/OpenListTeam/OpenList/v4/drivers/cfimgbed => ./drivers/cfimgbed

0 commit comments

Comments
 (0)