|
| 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) |
0 commit comments