Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
202f77d
feat(driver): add Cloudflare Image Bed support
ZZ0YY May 1, 2026
9bdaac8
fix: restore accidentally deleted file and name
ZZ0YY May 1, 2026
f828fc5
refactor: rename driver to cloudflare_imgbed and fix module structure
ZZ0YY May 1, 2026
7858f49
fix: use base.NewRestyClient() and use e.g
ZZ0YY May 1, 2026
569dedf
fix:go fmt
ZZ0YY May 1, 2026
36aecbf
feat(driver/cloudflare-imgbed): enhance cloudflare_imgbed API integra…
ZZ0YY May 1, 2026
dc74222
refactor
j2rong4cn May 3, 2026
a54f30b
feat(cloudflare_imgbed): implement upload functionality and optimize …
ZZ0YY May 3, 2026
719de68
Merge branch 'main' into feat/cfimgbed
ZZ0YY May 3, 2026
1fdf6a3
refactor: simplify path handling logic
ZZ0YY May 3, 2026
6013825
Merge branch 'feat/cfimgbed' of https://github.com/ZZ0YY/OpenList int…
ZZ0YY May 3, 2026
865c19f
refactor(cloudflare_imgbed): streamline API endpoint constants and im…
j2rong4cn May 3, 2026
7b5259b
refactor(cloudflare_imgbed): clean up upload logic and remove unused …
j2rong4cn May 3, 2026
85f1189
docs: update help descriptions to English in cloudflare_imgbed
ZZ0YY May 4, 2026
64b9947
Merge branch 'OpenListTeam:main' into ZZ0YY-patch-1
ZZ0YY May 4, 2026
19a1dcb
docs: update help descriptions to English in cloudflare_imgbed
ZZ0YY May 4, 2026
57c8cd0
Merge branch 'main' into feat/cfimgbed
j2rong4cn May 27, 2026
89bd84c
feat(cloudflare_imgbed): add virtual directory support
j2rong4cn May 27, 2026
bd3c66a
Merge branch 'main' into feat/cfimgbed
j2rong4cn May 28, 2026
fd59e29
refactor(weak_cache): iImprove weak pointer cleanup handling
j2rong4cn May 28, 2026
50dd55d
fix bug
j2rong4cn May 28, 2026
488454a
Apply suggestions from code review
j2rong4cn May 28, 2026
e182152
Adds uploadFolder param and streams base64 sample
j2rong4cn May 28, 2026
8013801
refactor: add context parameter to doRequest and related calls
j2rong4cn May 28, 2026
a73075a
fix: update List method to check args.Refresh before virtual director…
j2rong4cn May 28, 2026
b1b1157
refactor: optimize List method and improve error handling in doRequest
j2rong4cn May 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions drivers/all.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
_ "github.com/OpenListTeam/OpenList/v4/drivers/baidu_photo"
_ "github.com/OpenListTeam/OpenList/v4/drivers/chaoxing"
_ "github.com/OpenListTeam/OpenList/v4/drivers/chunk"
_ "github.com/OpenListTeam/OpenList/v4/drivers/cloudflare_imgbed"
_ "github.com/OpenListTeam/OpenList/v4/drivers/cloudreve"
_ "github.com/OpenListTeam/OpenList/v4/drivers/cloudreve_v4"
_ "github.com/OpenListTeam/OpenList/v4/drivers/cnb_releases"
Expand Down
164 changes: 164 additions & 0 deletions drivers/cloudflare_imgbed/driver.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
package cloudflare_imgbed

import (
"context"
"fmt"
"net/http"
"path"
"strings"

"github.com/OpenListTeam/OpenList/v4/drivers/base"
"github.com/OpenListTeam/OpenList/v4/internal/cache"
"github.com/OpenListTeam/OpenList/v4/internal/driver"
"github.com/OpenListTeam/OpenList/v4/internal/errs"
"github.com/OpenListTeam/OpenList/v4/internal/model"
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
"github.com/go-resty/resty/v2"
)

type CFImgBed struct {
model.Storage
Addition
client *resty.Client
virtualDir *cache.WeakCacheMap[string, model.Object]
}

func (d *CFImgBed) Config() driver.Config { return config }
func (d *CFImgBed) GetAddition() driver.Additional { return &d.Addition }

func (d *CFImgBed) Init(ctx context.Context) error {
d.UploadThread = min(d.UploadThread, 32)
if d.UploadThread < 1 {
d.UploadThread = 3
}
d.Address = strings.TrimRight(d.Address, "/")

d.client = base.NewRestyClient().
SetBaseURL(d.Address).
SetHeader("Authorization", "Bearer "+d.Token).
SetDebug(false)

// 连通性测试:尝试获取根目录单条数据
_, err := d.doRequest(ctx, http.MethodGet, listApi, func(req *resty.Request) {
req.SetQueryParams(map[string]string{
"start": "0",
"count": "1",
"dir": "/",
})
}, nil)
if err != nil {
return fmt.Errorf("init verification failed: %w", err)
}
d.virtualDir = cache.NewWeakCacheMap[string, model.Object]()
return nil
}

func (d *CFImgBed) Drop(ctx context.Context) error {
if d.virtualDir != nil {
d.virtualDir.Clear()
}
return nil
}
Comment thread
j2rong4cn marked this conversation as resolved.

func (d *CFImgBed) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
// if !args.Refresh && model.ObjHasMask(dir, model.Virtual) {
// if _, ok := d.virtualDir.Load(dir.GetPath()); ok {
// return nil, nil
// }
// }

var dirSeen map[string]bool
var fileSeen map[string]bool
var objs []model.Obj

start := 0
for {
var resp ListResponse
_, err := d.doRequest(ctx, http.MethodGet, listApi, func(req *resty.Request) {
req.SetQueryParams(map[string]string{
"dir": dir.GetPath(),
"start": fmt.Sprintf("%d", start),
"count": fmt.Sprintf("%d", listPageSize),
})
}, &resp)
if err != nil {
return nil, err
}
if len(resp.Files) == 0 && len(resp.Directories) == 0 {
break
}

if start == 0 {
dirSeen = make(map[string]bool, len(resp.Directories))
fileSeen = make(map[string]bool, len(resp.Files))
objs = make([]model.Obj, 0, len(resp.Directories)+len(resp.Files))
}

for _, rawDir := range resp.Directories {
rawDir = "/" + strings.TrimRight(rawDir, "/")
if !dirSeen[rawDir] {
dirSeen[rawDir] = true
objs = append(objs, &model.Object{
Path: rawDir,
Name: path.Base(rawDir),
Modified: d.Modified,
IsFolder: true,
})
}
}

for _, item := range resp.Files {
if !fileSeen[item.Name] {
fileSeen[item.Name] = true
objs = append(objs, parseFile(item))
}
}

// 如果当前获取的数量少于分页大小,说明已加载完毕
if len(resp.Files)+len(resp.Directories) < listPageSize {
break
}
start += listPageSize
}
return objs, nil
}

func (d *CFImgBed) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
return &model.Link{URL: d.Address + "/file" + utils.EncodePath(file.GetPath())}, nil
}

func (d *CFImgBed) Get(ctx context.Context, pathStr string) (model.Obj, error) {
fullPath := path.Join(d.RootFolderPath, pathStr)
if obj, found := d.virtualDir.Load(fullPath); found {
return obj, nil
}
return nil, errs.NotSupport
}

// MakeDir 在图床中通常是虚拟的,此处返回虚拟目录对象以支持上传时的路径展示
func (d *CFImgBed) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {
fullPath := path.Join(parentDir.GetPath(), dirName)
temp := &model.Object{
Path: fullPath,
Name: dirName,
IsFolder: true,
Modified: d.Modified,
Mask: model.Virtual,
}
d.virtualDir.Store(fullPath, temp)
return temp, nil
}

func (d *CFImgBed) Remove(ctx context.Context, obj model.Obj) error {
reqPath := obj.GetPath()
if model.ObjHasMask(obj, model.Virtual) {
d.virtualDir.Delete(reqPath)
return nil
}
_, err := d.doRequest(ctx, http.MethodPost, deleteApi+utils.EncodePath(reqPath), func(req *resty.Request) {
req.SetQueryParam("folder", fmt.Sprintf("%t", obj.IsDir()))
}, nil)
return err
}

var _ driver.Driver = (*CFImgBed)(nil)
27 changes: 27 additions & 0 deletions drivers/cloudflare_imgbed/meta.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package cloudflare_imgbed

import (
"github.com/OpenListTeam/OpenList/v4/internal/driver"
"github.com/OpenListTeam/OpenList/v4/internal/op"
)

type Addition struct {
driver.RootPath
Address string `json:"address" required:"true" help:"Backend API address of the image hosting service, e.g., https://img.example.com"`
Token string `json:"token" required:"true" help:"Authentication Token"`
SmallChannelName string `json:"smallChannelName" help:"Channel name for regular files (typically <20MB)"`
LargeChannelName string `json:"largeChannelName" help:"Channel name for large files"`
LargeChannelType string `json:"largeChannelType" type:"select" options:",huggingface" help:"Special type for large file channels (select 'huggingface' for direct upload to HuggingFace)"`
UploadThread int `json:"uploadThread" type:"number" default:"3" help:"Concurrent thread count for HuggingFace chunked direct upload"`
}

var config = driver.Config{
Name: "cloudflare_imgbed",
LocalSort: true,
NoUpload: false,
DefaultRoot: "/",
}

func init() {
op.RegisterDriver(func() driver.Driver { return &CFImgBed{} })
}
115 changes: 115 additions & 0 deletions drivers/cloudflare_imgbed/types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package cloudflare_imgbed

import (
"fmt"
"path"
"strconv"
"strings"
"time"

"github.com/OpenListTeam/OpenList/v4/internal/model"
)

const listPageSize = 1000

// ListResponse 列表接口响应
type ListResponse struct {
Files []FileItem `json:"files"`
Directories []string `json:"directories"`
}

type FileItem struct {
Name string `json:"name"`
Metadata map[string]interface{} `json:"metadata"` // 存储文件大小、哈希、时间戳等
}

type apiError struct {
Error string `json:"error"`
Message string `json:"message"`
}

// standardUploadResp 标准上传成功返回的数组
type standardUploadResp []struct {
Src string `json:"src"`
}

// hfGetUrlResp 获取 HF 直传授权地址的响应
type hfGetUrlResp struct {
Success bool `json:"success"`
FullID string `json:"fullId"`
FilePath string `json:"filePath"`
ChannelName string `json:"channelName"`
Repo string `json:"repo"`
NeedsLfs bool `json:"needsLfs"` // 是否需要进行 LFS 物理上传
AlreadyExists bool `json:"alreadyExists"` // 是否秒传成功
Oid string `json:"oid"` // Git LFS 对象 ID (SHA256)
UploadAction *UploadAction `json:"uploadAction"`
}

type UploadAction struct {
Href string `json:"href"`
Header map[string]string `json:"header"`
}

type hfCommitResp struct {
Success bool `json:"success"`
Src string `json:"src"`
FileUrl string `json:"fileUrl"`
FullID string `json:"fullId"`
}

// 辅助函数:从 map 中安全提取字符串/数值
func getString(m map[string]interface{}, keys ...string) string {
for _, k := range keys {
if v, ok := m[k]; ok {
switch val := v.(type) {
case string:
return val
case float64:
return strconv.FormatInt(int64(val), 10)
default:
return fmt.Sprintf("%v", val)
}
}
}
return ""
}

func getInt64(m map[string]interface{}, keys ...string) int64 {
for _, k := range keys {
if v, ok := m[k]; ok {
switch val := v.(type) {
case string:
n, _ := strconv.ParseInt(val, 10, 64)
return n
case float64:
return int64(val)
case int64:
return val
}
}
}
return 0
}

func parseFile(item FileItem) *model.Object {
name := path.Base(item.Name)
var size int64
var modTime time.Time

if item.Metadata != nil {
size = getInt64(item.Metadata, "FileSizeBytes", "File-Size")
ts := getInt64(item.Metadata, "TimeStamp")
if ts > 0 {
modTime = time.UnixMilli(ts)
}
}

return &model.Object{
Path: "/" + strings.TrimRight(item.Name, "/"),
Name: name,
Size: size,
Modified: modTime,
IsFolder: false,
}
}
Loading
Loading