Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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 @@ -82,6 +82,7 @@ import (
_ "github.com/OpenListTeam/OpenList/v4/drivers/wopan"
_ "github.com/OpenListTeam/OpenList/v4/drivers/wps"
_ "github.com/OpenListTeam/OpenList/v4/drivers/yandex_disk"
_ "github.com/OpenListTeam/OpenList/v4/drivers/cfimgbed"
)

// All do nothing,just for import
Expand Down
199 changes: 199 additions & 0 deletions drivers/cfimgbed/driver.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
package cfimgbed

import (
"context"
"fmt"
"strings"
"time"

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

// Init initializes the HTTP client with the configured Address and Token.
func (d *CFImgBed) Init(ctx context.Context) error {
d.client = resty.New().
SetBaseURL(strings.TrimRight(d.Address, "/")).
SetTimeout(30*time.Second).
SetHeader("Authorization", "Bearer "+d.Token).
SetDebug(false)
return nil
}

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

// apiError represents a generic error response from the CFImgBed API.
type apiError struct {
Error string `json:"error"`
Message string `json:"message"`
}

// buildReqPath constructs the path to send to the CFImgBed List API.
//
// OpenList may call List() in two ways:
// 1. List(nil) — initial load of the mount root
// 2. List(obj) — where obj was returned by a previous List() call
//
// When RootPath is set (e.g. "/telegram"), OpenList may pass a virtual root
// dir object whose GetPath() already equals the root path itself. We must
// detect this and avoid double-prepending rootPath.
func buildReqPath(rootPath, dirPath string) string {
rootPath = strings.Trim(rootPath, "/")
dirPath = strings.Trim(dirPath, "/")

if dirPath == "" || dirPath == rootPath {
// Either listing the real root, or OpenList passed the virtual root dir
return rootPath
}
if rootPath == "" {
return dirPath
}
// dirPath is a subfolder returned by a previous List call, prepend rootPath
return rootPath + "/" + dirPath
}

// List retrieves the file and directory listing for the given directory.
func (d *CFImgBed) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
rootPath := strings.Trim(d.GetRootPath(), "/")

var dirPath string
if dir != nil {
dirPath = strings.Trim(dir.GetPath(), "/")
}
reqPath := buildReqPath(rootPath, dirPath)

var resp ListResponse
var errResp apiError
res, err := d.client.R().
SetQueryParam("dir", reqPath).
SetQueryParam("count", "-1").
SetResult(&resp).
SetError(&errResp).
Get("/api/manage/list")

if err != nil {
return nil, err
}
if res.IsError() {
if errResp.Message != "" {
return nil, fmt.Errorf("CFImgBed API error: %s", errResp.Message)
}
return nil, fmt.Errorf("CFImgBed API returned status %d", res.StatusCode())
}

objs := make([]model.Obj, 0, len(resp.Directories)+len(resp.Files))

// Strip rootPath prefix from returned paths so that GetPath() is relative
// to the OpenList mount point, not the CFImgBed root.
for _, rawDir := range resp.Directories {
cleanDir := strings.TrimRight(rawDir, "/")
p := stripRootPrefix(cleanDir, rootPath)
objs = append(objs, parseDir(p))
}

for _, item := range resp.Files {
p := stripRootPrefix(item.Name, rootPath)
objs = append(objs, parseFile(FileItem{
Name: p,
Metadata: item.Metadata,
}))
}

return objs, nil
}

// stripRootPrefix removes the rootPath prefix from a path returned by the API.
// If rootPath is empty or the path doesn't start with rootPath/, return as-is.
func stripRootPrefix(p, rootPath string) string {
if rootPath == "" {
return p
}
prefix := rootPath + "/"
if strings.HasPrefix(p, prefix) {
return strings.TrimPrefix(p, prefix)
}
return p
}

// Link constructs a direct download URL for the given file object.
// Format: {Address}/file/{rootPath}/{filePath} with no double slashes.
func (d *CFImgBed) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
rootPath := strings.Trim(d.GetRootPath(), "/")
filePath := strings.Trim(file.GetPath(), "/")

var fullPath string
if rootPath != "" && filePath != "" {
fullPath = rootPath + "/" + filePath
} else if rootPath != "" {
fullPath = rootPath
} else {
fullPath = filePath
}

link := strings.TrimRight(d.Address, "/") + "/file/" + fullPath
return &model.Link{URL: link}, nil
}

func (d *CFImgBed) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {
return nil, errs.NotImplement
}

func (d *CFImgBed) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
return nil, errs.NotImplement
}

func (d *CFImgBed) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) {
return nil, errs.NotImplement
}

func (d *CFImgBed) Copy(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
return nil, errs.NotImplement
}

func (d *CFImgBed) Remove(ctx context.Context, obj model.Obj) error {
return errs.NotImplement
}

func (d *CFImgBed) Put(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {
return nil, errs.NotImplement
}

func (d *CFImgBed) GetArchiveMeta(ctx context.Context, obj model.Obj, args model.ArchiveArgs) (model.ArchiveMeta, error) {
return nil, errs.NotImplement
}

func (d *CFImgBed) ListArchive(ctx context.Context, obj model.Obj, args model.ArchiveInnerArgs) ([]model.Obj, error) {
return nil, errs.NotImplement
}

func (d *CFImgBed) Extract(ctx context.Context, obj model.Obj, args model.ArchiveInnerArgs) (*model.Link, error) {
return nil, errs.NotImplement
}

func (d *CFImgBed) ArchiveDecompress(ctx context.Context, srcObj, dstDir model.Obj, args model.ArchiveDecompressArgs) ([]model.Obj, error) {
return nil, errs.NotImplement
}

func (d *CFImgBed) GetDetails(ctx context.Context) (*model.StorageDetails, error) {
return nil, errs.NotImplement
}

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

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" default:"" help:"API 域名,如 https://img.example.com"`
Token string `json:"token" type:"text" required:"true" default:"" help:"API 认证 Token"`
}

var config = driver.Config{
Name: "CFImgBed",
LocalSort: false,
OnlyProxy: false,
NoCache: false,
NoUpload: true,
NeedMs: false,
DefaultRoot: "/",
CheckStatus: false,
Alert: "",
NoOverwriteUpload: false,
NoLinkURL: false,
}

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

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

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

// File represents a file object parsed from the CFImgBed List API response.
// It implements the model.Obj interface.
type File struct {
Path string
Name_ string
Size_ int64
ModTime_ time.Time
Mime_ string
}

func (f *File) GetPath() string { return f.Path }
func (f *File) GetName() string { return f.Name_ }
func (f *File) ModTime() time.Time { return f.ModTime_ }
func (f *File) CreateTime() time.Time { return f.ModTime_ }
func (f *File) GetSize() int64 { return f.Size_ }
func (f *File) IsDir() bool { return false }
func (f *File) GetID() string { return f.Path }
func (f *File) GetHash() utils.HashInfo { return utils.HashInfo{} }

// Dir represents a directory object parsed from the CFImgBed List API response.
// It implements the model.Obj interface.
type Dir struct {
Path string
Name_ string
}

func (d *Dir) GetPath() string { return d.Path }
func (d *Dir) GetName() string { return d.Name_ }
func (d *Dir) ModTime() time.Time { return time.Time{} }
func (d *Dir) CreateTime() time.Time { return time.Time{} }
func (d *Dir) GetSize() int64 { return 0 }
func (d *Dir) IsDir() bool { return true }
func (d *Dir) GetID() string { return d.Path }
func (d *Dir) GetHash() utils.HashInfo { return utils.HashInfo{} }

// Compile-time checks to ensure File and Dir implement model.Obj.
var _ model.Obj = (*File)(nil)
var _ model.Obj = (*Dir)(nil)

// ListResponse represents the JSON structure returned by the CFImgBed List API.
type ListResponse struct {
Files []FileItem `json:"files"`
Directories []string `json:"directories"`
}

// FileItem represents a single file entry in the List API response.
// Metadata uses map[string]interface{} because the actual API returns mixed types:
// - TimeStamp: integer (e.g. 1774910085474) in newer versions
// - FileSizeBytes: integer (e.g. 3936071)
// - FileSize: string (e.g. "3.75") — human-readable size
// - FileType: string (e.g. "audio/mpeg")
// - Legacy fields may use string values for numbers
type FileItem struct {
Name string `json:"name"`
Metadata map[string]interface{} `json:"metadata"`
}

// getString safely extracts a string value from metadata, trying key in order.
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 ""
}

// getInt64 safely extracts an int64 value from metadata, trying key in order.
// Supports string, float64 (JSON number), and int64 types.
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
}

// parseFile converts an API FileItem to a *File model.Obj.
// It tries multiple key names for each field to handle different API versions:
// - Size: FileSizeBytes (int) > File-Size (string)
// - MIME: FileType > File-Mime
// - Time: TimeStamp (handles both int and string)
func parseFile(item FileItem) *File {
name := path.Base(item.Name)
var size int64
var modTime time.Time
var mime string

if item.Metadata != nil {
// Try FileSizeBytes (int) first, fall back to File-Size (string)
size = getInt64(item.Metadata, "FileSizeBytes", "File-Size")

// Try FileType first, fall back to File-Mime
mime = getString(item.Metadata, "FileType", "File-Mime")

// TimeStamp may be int or string depending on API version
ts := getInt64(item.Metadata, "TimeStamp")
if ts > 0 {
modTime = time.UnixMilli(ts)
}
}

return &File{
Path: item.Name,
Name_: name,
Size_: size,
ModTime_: modTime,
Mime_: mime,
}
}

// parseDir converts a directory path string from the API to a *Dir model.Obj.
func parseDir(dirPath string) *Dir {
return &Dir{
Path: dirPath,
Name_: path.Base(dirPath),
}
}
3 changes: 3 additions & 0 deletions drivers/cfimgbed/util.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package cfimgbed

// do others that not defined in Driver interface
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -313,3 +313,5 @@ replace github.com/ProtonMail/go-proton-api => github.com/henrybear327/go-proton
replace github.com/cronokirby/saferith => github.com/Da3zKi7/saferith v0.33.0-fixed

// replace github.com/OpenListTeam/115-sdk-go => ../../OpenListTeam/115-sdk-go

replace github.com/OpenListTeam/OpenList/v4/drivers/cfimgbed => ./drivers/cfimgbed
Loading