Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
128 changes: 128 additions & 0 deletions drivers/cloudflare_imgbed/driver.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package cloudflare_imgbed

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

"github.com/OpenListTeam/OpenList/v4/drivers/base"
"github.com/OpenListTeam/OpenList/v4/internal/driver"
"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
}

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(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)
}
return nil
}

func (d *CFImgBed) Drop(ctx context.Context) error { return nil }

func (d *CFImgBed) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
reqPath := dir.GetPath()

dirSeen := make(map[string]bool)
fileSeen := make(map[string]bool)
objs := make([]model.Obj, 0)

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

for _, rawDir := range resp.Directories {
Comment thread
j2rong4cn marked this conversation as resolved.
cleanDir := strings.TrimRight(rawDir, "/")
if !dirSeen[cleanDir] {
dirSeen[cleanDir] = true
objs = append(objs, &model.Object{
Path: cleanDir,
Name: path.Base(cleanDir),
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
}

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

func (d *CFImgBed) Remove(ctx context.Context, obj model.Obj) error {
reqPath := obj.GetPath()
_, err := d.doRequest(http.MethodPost, deleteApi, func(req *resty.Request) {
req.SetBody(map[string]string{
"path": reqPath,
}).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" type:"text" required:"true" help:"Backend API address of the image hosting service, e.g., https://img.example.com"`
Token string `json:"token" type:"text" required:"true" help:"Authentication Token"`
SmallChannelName string `json:"smallChannelName" type:"text" help:"Channel name for regular files (typically <20MB)"`
LargeChannelName string `json:"largeChannelName" type:"text" 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{} })
}
114 changes: 114 additions & 0 deletions drivers/cloudflare_imgbed/types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package cloudflare_imgbed

import (
"fmt"
"path"
"strconv"
"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: item.Name,
Name: name,
Size: size,
Modified: modTime,
IsFolder: false,
}
}
Loading