-
-
Notifications
You must be signed in to change notification settings - Fork 2k
feat(driver): add Cloudflare Image Bed support #2427
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
ZZ0YY
wants to merge
16
commits into
OpenListTeam:main
Choose a base branch
from
ZZ0YY:feat/cfimgbed
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 3 commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
202f77d
feat(driver): add Cloudflare Image Bed support
ZZ0YY 9bdaac8
fix: restore accidentally deleted file and name
ZZ0YY f828fc5
refactor: rename driver to cloudflare_imgbed and fix module structure
ZZ0YY 7858f49
fix: use base.NewRestyClient() and use e.g
ZZ0YY 569dedf
fix:go fmt
ZZ0YY 36aecbf
feat(driver/cloudflare-imgbed): enhance cloudflare_imgbed API integra…
ZZ0YY dc74222
refactor
j2rong4cn a54f30b
feat(cloudflare_imgbed): implement upload functionality and optimize …
ZZ0YY 719de68
Merge branch 'main' into feat/cfimgbed
ZZ0YY 1fdf6a3
refactor: simplify path handling logic
ZZ0YY 6013825
Merge branch 'feat/cfimgbed' of https://github.com/ZZ0YY/OpenList int…
ZZ0YY 865c19f
refactor(cloudflare_imgbed): streamline API endpoint constants and im…
j2rong4cn 7b5259b
refactor(cloudflare_imgbed): clean up upload logic and remove unused …
j2rong4cn 85f1189
docs: update help descriptions to English in cloudflare_imgbed
ZZ0YY 64b9947
Merge branch 'OpenListTeam:main' into ZZ0YY-patch-1
ZZ0YY 19a1dcb
docs: update help descriptions to English in cloudflare_imgbed
ZZ0YY File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,199 @@ | ||
| package cloudflare_imgbed | ||
|
|
||
| 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) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| 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" default:"" help:"API 域名,如 https://img.example.com"` | ||
| Token string `json:"token" type:"text" required:"true" default:"" help:"API 认证 Token"` | ||
|
ZZ0YY marked this conversation as resolved.
Outdated
|
||
| } | ||
|
|
||
| var config = driver.Config{ | ||
| Name: "cloudflare_imgbed", | ||
| 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{} | ||
| }) | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,146 @@ | ||
| package cloudflare_imgbed | ||
|
|
||
| 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), | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| package cloudflare_imgbed | ||
|
|
||
| // do others that not defined in Driver interface |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.