Skip to content

Commit 9b697e4

Browse files
darkizoneclaude
andcommitted
feat(driver): add Darkibox driver
Add driver for Darkibox (https://darkibox.com/), an XFileSharing-based video hosting platform. Supported operations: - List files and folders (with pagination) - Upload files (via upload server) - Download files (via direct link API with quality selection) - Create and delete folders - Delete and move files - Account validation on init Closes #8079 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 2f903c4 commit 9b697e4

5 files changed

Lines changed: 494 additions & 0 deletions

File tree

drivers/all.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727
_ "github.com/alist-org/alist/v3/drivers/cloudreve"
2828
_ "github.com/alist-org/alist/v3/drivers/cloudreve_v4"
2929
_ "github.com/alist-org/alist/v3/drivers/crypt"
30+
_ "github.com/alist-org/alist/v3/drivers/darkibox"
3031
_ "github.com/alist-org/alist/v3/drivers/doubao"
3132
_ "github.com/alist-org/alist/v3/drivers/doubao_new"
3233
_ "github.com/alist-org/alist/v3/drivers/doubao_share"

drivers/darkibox/driver.go

Lines changed: 299 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,299 @@
1+
package darkibox
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"net/http"
7+
"strconv"
8+
"time"
9+
10+
"github.com/alist-org/alist/v3/drivers/base"
11+
"github.com/alist-org/alist/v3/internal/driver"
12+
"github.com/alist-org/alist/v3/internal/errs"
13+
"github.com/alist-org/alist/v3/internal/model"
14+
"github.com/alist-org/alist/v3/internal/op"
15+
)
16+
17+
type Darkibox struct {
18+
model.Storage
19+
Addition
20+
}
21+
22+
func (d *Darkibox) Config() driver.Config {
23+
return config
24+
}
25+
26+
func (d *Darkibox) GetAddition() driver.Additional {
27+
return &d.Addition
28+
}
29+
30+
func (d *Darkibox) Init(ctx context.Context) error {
31+
if d.APIKey == "" {
32+
return fmt.Errorf("API key is required")
33+
}
34+
if d.RootFolderID == "" {
35+
d.RootFolderID = "0"
36+
}
37+
38+
// Verify API key by calling account/info
39+
var account accountInfoResult
40+
if err := d.callAPI(ctx, "/account/info", nil, &account); err != nil {
41+
return fmt.Errorf("failed to verify API key: %w", err)
42+
}
43+
44+
op.MustSaveDriverStorage(d)
45+
return nil
46+
}
47+
48+
func (d *Darkibox) Drop(ctx context.Context) error {
49+
return nil
50+
}
51+
52+
func (d *Darkibox) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
53+
folderID := d.RootFolderID
54+
if dir.GetID() != "" {
55+
folderID = folderIDFromObjID(dir.GetID())
56+
}
57+
58+
var objects []model.Obj
59+
60+
// List sub-folders via /api/folder/list
61+
var folders folderListResult
62+
if err := d.callAPI(ctx, "/folder/list", map[string]string{
63+
"fld_id": fldIDStr(folderID),
64+
}, &folders); err != nil {
65+
return nil, fmt.Errorf("list folders failed: %w", err)
66+
}
67+
for _, f := range folders.Folders {
68+
objects = append(objects, &model.Object{
69+
ID: encodeFolderID(f.FldID),
70+
Name: f.Name,
71+
IsFolder: true,
72+
})
73+
}
74+
75+
// List files via /api/file/list (paginated)
76+
page := 1
77+
for {
78+
var files fileListResult
79+
if err := d.callAPI(ctx, "/file/list", map[string]string{
80+
"fld_id": fldIDStr(folderID),
81+
"per_page": "200",
82+
"page": strconv.Itoa(page),
83+
}, &files); err != nil {
84+
return nil, fmt.Errorf("list files failed: %w", err)
85+
}
86+
87+
for _, f := range files.Files {
88+
modified := time.Now()
89+
if f.Uploaded != "" {
90+
if t, err := time.Parse("2006-01-02 15:04:05", f.Uploaded); err == nil {
91+
modified = t
92+
}
93+
}
94+
name := f.Name
95+
if name == "" {
96+
name = f.Title
97+
}
98+
objects = append(objects, &model.Object{
99+
ID: encodeFileID(f.FileCode),
100+
Name: name,
101+
Size: f.Size,
102+
Modified: modified,
103+
IsFolder: false,
104+
})
105+
}
106+
107+
// Check if there are more pages
108+
if len(files.Files) < 200 {
109+
break
110+
}
111+
page++
112+
}
113+
114+
return objects, nil
115+
}
116+
117+
func (d *Darkibox) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
118+
if file.IsDir() {
119+
return nil, errs.NotFile
120+
}
121+
122+
fileCode := fileCodeFromObjID(file.GetID())
123+
if fileCode == "" {
124+
return nil, fmt.Errorf("empty file code")
125+
}
126+
127+
var result directLinkResult
128+
if err := d.callAPI(ctx, "/file/direct_link", map[string]string{
129+
"file_code": fileCode,
130+
}, &result); err != nil {
131+
return nil, fmt.Errorf("failed to get direct link: %w", err)
132+
}
133+
134+
// Find the original quality version, fall back to first available
135+
var dlURL string
136+
for _, v := range result.Versions {
137+
if v.Name == "o" {
138+
dlURL = v.URL
139+
break
140+
}
141+
}
142+
if dlURL == "" && len(result.Versions) > 0 {
143+
dlURL = result.Versions[0].URL
144+
}
145+
if dlURL == "" {
146+
return nil, fmt.Errorf("no download URL available for file %s", fileCode)
147+
}
148+
149+
return &model.Link{
150+
URL: dlURL,
151+
}, nil
152+
}
153+
154+
func (d *Darkibox) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {
155+
parentID := d.RootFolderID
156+
if parentDir.GetID() != "" {
157+
parentID = folderIDFromObjID(parentDir.GetID())
158+
}
159+
160+
var result folderCreateResult
161+
if err := d.callAPI(ctx, "/folder/create", map[string]string{
162+
"name": dirName,
163+
"parent_id": fldIDStr(parentID),
164+
}, &result); err != nil {
165+
return nil, fmt.Errorf("create folder failed: %w", err)
166+
}
167+
168+
return &model.Object{
169+
ID: encodeFolderID(result.FldID),
170+
Name: dirName,
171+
IsFolder: true,
172+
}, nil
173+
}
174+
175+
func (d *Darkibox) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
176+
if srcObj.IsDir() {
177+
return nil, errs.NotImplement
178+
}
179+
180+
fileCode := fileCodeFromObjID(srcObj.GetID())
181+
if fileCode == "" {
182+
return nil, fmt.Errorf("empty file code")
183+
}
184+
185+
dstFolderID := d.RootFolderID
186+
if dstDir.GetID() != "" {
187+
dstFolderID = folderIDFromObjID(dstDir.GetID())
188+
}
189+
190+
if err := d.callAPI(ctx, "/file/move", map[string]string{
191+
"file_code": fileCode,
192+
"to_folder": fldIDStr(dstFolderID),
193+
}, nil); err != nil {
194+
return nil, fmt.Errorf("move file failed: %w", err)
195+
}
196+
197+
return &model.Object{
198+
ID: srcObj.GetID(),
199+
Name: srcObj.GetName(),
200+
Size: srcObj.GetSize(),
201+
Modified: srcObj.ModTime(),
202+
IsFolder: false,
203+
}, nil
204+
}
205+
206+
func (d *Darkibox) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) {
207+
return nil, errs.NotImplement
208+
}
209+
210+
func (d *Darkibox) Copy(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
211+
return nil, errs.NotImplement
212+
}
213+
214+
func (d *Darkibox) Remove(ctx context.Context, obj model.Obj) error {
215+
if obj.IsDir() {
216+
folderID := folderIDFromObjID(obj.GetID())
217+
return d.callAPI(ctx, "/folder/delete", map[string]string{
218+
"fld_id": folderID,
219+
}, nil)
220+
}
221+
222+
fileCode := fileCodeFromObjID(obj.GetID())
223+
return d.callAPI(ctx, "/file/delete", map[string]string{
224+
"file_code": fileCode,
225+
}, nil)
226+
}
227+
228+
func (d *Darkibox) Put(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {
229+
folderID := d.RootFolderID
230+
if dstDir.GetID() != "" {
231+
folderID = folderIDFromObjID(dstDir.GetID())
232+
}
233+
234+
// Step 1: Get the upload server URL
235+
var server uploadServerResult
236+
if err := d.callAPI(ctx, "/upload/server", nil, &server); err != nil {
237+
return nil, fmt.Errorf("get upload server failed: %w", err)
238+
}
239+
if server.URL == "" {
240+
return nil, fmt.Errorf("no upload server URL returned")
241+
}
242+
243+
// Step 2: Upload the file to the upload server
244+
reader := driver.NewLimitedUploadStream(ctx, &driver.ReaderUpdatingProgress{
245+
Reader: file,
246+
UpdateProgress: up,
247+
})
248+
249+
res, err := base.RestyClient.R().
250+
SetContext(ctx).
251+
SetMultipartField("file", file.GetName(), "", reader).
252+
SetMultipartFormData(map[string]string{
253+
"key": d.APIKey,
254+
"fld_id": fldIDStr(folderID),
255+
}).
256+
Post(server.URL)
257+
if err != nil {
258+
return nil, fmt.Errorf("upload failed: %w", err)
259+
}
260+
if res.StatusCode() != http.StatusOK {
261+
return nil, fmt.Errorf("upload failed: http %d", res.StatusCode())
262+
}
263+
264+
// Try to parse upload response to get the file code
265+
var uploadResp uploadResult
266+
if err := base.RestyClient.JSONUnmarshal(res.Body(), &uploadResp); err == nil && len(uploadResp.Files) > 0 {
267+
uf := uploadResp.Files[0]
268+
return &model.Object{
269+
ID: encodeFileID(uf.FileCode),
270+
Name: file.GetName(),
271+
Size: file.GetSize(),
272+
IsFolder: false,
273+
}, nil
274+
}
275+
276+
return &model.Object{
277+
Name: file.GetName(),
278+
Size: file.GetSize(),
279+
IsFolder: false,
280+
}, nil
281+
}
282+
283+
func (d *Darkibox) GetArchiveMeta(ctx context.Context, obj model.Obj, args model.ArchiveArgs) (model.ArchiveMeta, error) {
284+
return nil, errs.NotImplement
285+
}
286+
287+
func (d *Darkibox) ListArchive(ctx context.Context, obj model.Obj, args model.ArchiveInnerArgs) ([]model.Obj, error) {
288+
return nil, errs.NotImplement
289+
}
290+
291+
func (d *Darkibox) Extract(ctx context.Context, obj model.Obj, args model.ArchiveInnerArgs) (*model.Link, error) {
292+
return nil, errs.NotImplement
293+
}
294+
295+
func (d *Darkibox) ArchiveDecompress(ctx context.Context, srcObj, dstDir model.Obj, args model.ArchiveDecompressArgs) ([]model.Obj, error) {
296+
return nil, errs.NotImplement
297+
}
298+
299+
var _ driver.Driver = (*Darkibox)(nil)

drivers/darkibox/meta.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package darkibox
2+
3+
import (
4+
"github.com/alist-org/alist/v3/internal/driver"
5+
"github.com/alist-org/alist/v3/internal/op"
6+
)
7+
8+
type Addition struct {
9+
driver.RootID
10+
APIKey string `json:"api_key" required:"true" help:"API key from your Darkibox account"`
11+
}
12+
13+
var config = driver.Config{
14+
Name: "Darkibox",
15+
LocalSort: false,
16+
OnlyLocal: false,
17+
OnlyProxy: true,
18+
NoCache: false,
19+
NoUpload: false,
20+
DefaultRoot: "0",
21+
}
22+
23+
func init() {
24+
op.RegisterDriver(func() driver.Driver {
25+
return &Darkibox{}
26+
})
27+
}

0 commit comments

Comments
 (0)