Skip to content

Commit 66c1a98

Browse files
committed
feat(Teldrive): Add driver Teldrive
[Official api docs](https://teldrive-docs.pages.dev/docs/api) implement: * copy * move * link (302 share and local proxy) * chunk upload * rename Not yet implement: - login (scan qrcode or auth by password) - refresh token
1 parent ffa03bf commit 66c1a98

File tree

5 files changed

+924
-0
lines changed

5 files changed

+924
-0
lines changed

drivers/all.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ import (
5858
_ "github.com/alist-org/alist/v3/drivers/sftp"
5959
_ "github.com/alist-org/alist/v3/drivers/smb"
6060
_ "github.com/alist-org/alist/v3/drivers/teambition"
61+
_ "github.com/alist-org/alist/v3/drivers/teldrive"
6162
_ "github.com/alist-org/alist/v3/drivers/terabox"
6263
_ "github.com/alist-org/alist/v3/drivers/thunder"
6364
_ "github.com/alist-org/alist/v3/drivers/thunder_browser"

drivers/teldrive/driver.go

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
package teldrive
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"github.com/alist-org/alist/v3/drivers/base"
7+
"github.com/alist-org/alist/v3/internal/driver"
8+
"github.com/alist-org/alist/v3/internal/errs"
9+
"github.com/alist-org/alist/v3/internal/model"
10+
"github.com/alist-org/alist/v3/internal/op"
11+
"github.com/alist-org/alist/v3/pkg/utils"
12+
"github.com/go-resty/resty/v2"
13+
"github.com/google/uuid"
14+
"math"
15+
"net/http"
16+
"net/url"
17+
"strings"
18+
)
19+
20+
type Teldrive struct {
21+
model.Storage
22+
Addition
23+
}
24+
25+
func (d *Teldrive) Config() driver.Config {
26+
return config
27+
}
28+
29+
func (d *Teldrive) GetAddition() driver.Additional {
30+
return &d.Addition
31+
}
32+
33+
func (d *Teldrive) Init(ctx context.Context) error {
34+
// TODO login / refresh token
35+
// op.MustSaveDriverStorage(d)
36+
if !strings.HasPrefix(d.Cookie, "access_token=") || d.Cookie == "" {
37+
return fmt.Errorf("cookie must start with 'access_token='")
38+
}
39+
if d.UploadConcurrency == 0 {
40+
d.UploadConcurrency = 4
41+
}
42+
if d.ChunkSize == 0 {
43+
d.ChunkSize = 10
44+
}
45+
if d.WebdavNative() {
46+
d.WebProxy = true
47+
} else {
48+
d.WebProxy = false
49+
}
50+
51+
op.MustSaveDriverStorage(d)
52+
return nil
53+
}
54+
55+
func (d *Teldrive) Drop(ctx context.Context) error {
56+
return nil
57+
}
58+
59+
func (d *Teldrive) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
60+
// TODO return the files list, required
61+
// endpoint /api/files, params ->page order sort path
62+
var listResp ListResp
63+
params := url.Values{}
64+
params.Set("path", dir.GetPath())
65+
//log.Info(dir.GetPath())
66+
pathname, err := utils.InjectQuery("/api/files", params)
67+
if err != nil {
68+
return nil, err
69+
}
70+
71+
err = d.request(http.MethodGet, pathname, nil, &listResp)
72+
if err != nil {
73+
return nil, err
74+
}
75+
76+
return utils.SliceConvert(listResp.Items, func(src Object) (model.Obj, error) {
77+
return &model.Object{
78+
ID: src.ID,
79+
Name: src.Name,
80+
Size: src.Size,
81+
IsFolder: src.Type == "folder",
82+
Modified: src.UpdatedAt,
83+
}, nil
84+
})
85+
}
86+
87+
func (d *Teldrive) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
88+
if !d.WebdavNative() {
89+
if shareObj, err := d.getShareFileById(file.GetID()); err == nil && shareObj != nil {
90+
return &model.Link{
91+
URL: d.Address + fmt.Sprintf("/api/shares/%s/files/%s/%s", shareObj.Id, file.GetID(), file.GetName()),
92+
}, nil
93+
}
94+
if err := d.createShareFile(file.GetID()); err != nil {
95+
return nil, err
96+
}
97+
shareObj, err := d.getShareFileById(file.GetID())
98+
if err != nil {
99+
return nil, err
100+
}
101+
return &model.Link{
102+
URL: d.Address + fmt.Sprintf("/api/shares/%s/files/%s/%s", shareObj.Id, file.GetID(), file.GetName()),
103+
}, nil
104+
}
105+
return &model.Link{
106+
URL: d.Address + "/api/files/" + file.GetID() + "/" + file.GetName(),
107+
Header: http.Header{
108+
"Cookie": {d.Cookie},
109+
},
110+
}, nil
111+
}
112+
113+
func (d *Teldrive) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
114+
return d.request(http.MethodPost, "/api/files/mkdir", func(req *resty.Request) {
115+
req.SetBody(map[string]interface{}{
116+
"path": parentDir.GetPath() + "/" + dirName,
117+
})
118+
}, nil)
119+
}
120+
121+
func (d *Teldrive) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
122+
body := base.Json{
123+
"ids": []string{srcObj.GetID()},
124+
"destinationParent": dstDir.GetID(),
125+
}
126+
return d.request(http.MethodPost, "/api/files/move", func(req *resty.Request) {
127+
req.SetBody(body)
128+
}, nil)
129+
}
130+
131+
func (d *Teldrive) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
132+
body := base.Json{
133+
"name": newName,
134+
}
135+
return d.request(http.MethodPatch, "/api/files/"+srcObj.GetID(), func(req *resty.Request) {
136+
req.SetBody(body)
137+
}, nil)
138+
}
139+
140+
func (d *Teldrive) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
141+
copyConcurrentLimit := 4
142+
copyManager := NewCopyManager(ctx, copyConcurrentLimit, d)
143+
copyManager.startWorkers()
144+
copyManager.G.Go(func() error {
145+
defer close(copyManager.TaskChan)
146+
return copyManager.generateTasks(ctx, srcObj, dstDir)
147+
})
148+
return copyManager.G.Wait()
149+
}
150+
151+
func (d *Teldrive) Remove(ctx context.Context, obj model.Obj) error {
152+
body := base.Json{
153+
"ids": []string{obj.GetID()},
154+
}
155+
return d.request(http.MethodPost, "/api/files/delete", func(req *resty.Request) {
156+
req.SetBody(body)
157+
}, nil)
158+
}
159+
160+
func (d *Teldrive) Put(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) error {
161+
fileId := uuid.New().String()
162+
chunkSizeInMB := d.ChunkSize
163+
chunkSize := chunkSizeInMB * 1024 * 1024 // Convert MB to bytes
164+
totalSize := file.GetSize()
165+
totalParts := int(math.Ceil(float64(totalSize) / float64(chunkSize)))
166+
retryCount := 0
167+
maxRetried := 3
168+
p := driver.NewProgress(totalSize, up)
169+
170+
// delete the upload task when finished or failed
171+
defer func() {
172+
_ = d.request(http.MethodDelete, "/api/uploads/"+fileId, nil, nil)
173+
}()
174+
175+
if obj, err := d.getFile(dstDir.GetPath(), file.GetName(), file.IsDir()); err == nil {
176+
if err = d.Remove(ctx, obj); err != nil {
177+
return err
178+
}
179+
}
180+
// start the upload process
181+
if err := d.request(http.MethodGet, "/api/uploads/"+fileId, nil, nil); err != nil {
182+
return err
183+
}
184+
if totalSize == 0 {
185+
return d.touch(file.GetName(), dstDir.GetPath())
186+
}
187+
188+
if totalParts <= 1 {
189+
return d.doSingleUpload(ctx, dstDir, file, p, retryCount, maxRetried, totalParts, fileId)
190+
}
191+
192+
return d.doMultiUpload(ctx, dstDir, file, p, maxRetried, totalParts, chunkSize, fileId)
193+
}
194+
195+
func (d *Teldrive) GetArchiveMeta(ctx context.Context, obj model.Obj, args model.ArchiveArgs) (model.ArchiveMeta, error) {
196+
// TODO get archive file meta-info, return errs.NotImplement to use an internal archive tool, optional
197+
return nil, errs.NotImplement
198+
}
199+
200+
func (d *Teldrive) ListArchive(ctx context.Context, obj model.Obj, args model.ArchiveInnerArgs) ([]model.Obj, error) {
201+
// TODO list args.InnerPath in the archive obj, return errs.NotImplement to use an internal archive tool, optional
202+
return nil, errs.NotImplement
203+
}
204+
205+
func (d *Teldrive) Extract(ctx context.Context, obj model.Obj, args model.ArchiveInnerArgs) (*model.Link, error) {
206+
// TODO return link of file args.InnerPath in the archive obj, return errs.NotImplement to use an internal archive tool, optional
207+
return nil, errs.NotImplement
208+
}
209+
210+
func (d *Teldrive) ArchiveDecompress(ctx context.Context, srcObj, dstDir model.Obj, args model.ArchiveDecompressArgs) ([]model.Obj, error) {
211+
// TODO extract args.InnerPath path in the archive srcObj to the dstDir location, optional
212+
// a folder with the same name as the archive file needs to be created to store the extracted results if args.PutIntoNewDir
213+
// return errs.NotImplement to use an internal archive tool
214+
return nil, errs.NotImplement
215+
}
216+
217+
//func (d *Teldrive) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
218+
// return nil, errs.NotSupport
219+
//}
220+
221+
var _ driver.Driver = (*Teldrive)(nil)

drivers/teldrive/meta.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package teldrive
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+
// Usually one of two
10+
driver.RootPath
11+
// define other
12+
Address string `json:"url" required:"true"`
13+
ChunkSize int64 `json:"chunk_size" type:"number" default:"4" help:"Chunk size in MiB"`
14+
Cookie string `json:"cookie" type:"string" required:"true" help:"access_token=xxx"`
15+
UploadConcurrency int64 `json:"upload_concurrency" type:"number" default:"4" help:"Concurrency upload requests"`
16+
}
17+
18+
var config = driver.Config{
19+
Name: "Teldrive",
20+
DefaultRoot: "/",
21+
}
22+
23+
func init() {
24+
op.RegisterDriver(func() driver.Driver {
25+
return &Teldrive{}
26+
})
27+
}

drivers/teldrive/types.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package teldrive
2+
3+
import (
4+
"context"
5+
"github.com/alist-org/alist/v3/internal/model"
6+
"golang.org/x/sync/errgroup"
7+
"golang.org/x/sync/semaphore"
8+
"time"
9+
)
10+
11+
type ErrResp struct {
12+
Code int `json:"code"`
13+
Message string `json:"message"`
14+
}
15+
16+
type Object struct {
17+
ID string `json:"id"`
18+
Name string `json:"name"`
19+
Type string `json:"type"`
20+
MimeType string `json:"mimeType"`
21+
Category string `json:"category,omitempty"`
22+
ParentId string `json:"parentId"`
23+
Size int64 `json:"size"`
24+
Encrypted bool `json:"encrypted"`
25+
UpdatedAt time.Time `json:"updatedAt"`
26+
}
27+
28+
type ListResp struct {
29+
Items []Object `json:"items"`
30+
Meta struct {
31+
Count int `json:"count"`
32+
TotalPages int `json:"totalPages"`
33+
CurrentPage int `json:"currentPage"`
34+
} `json:"meta"`
35+
}
36+
37+
type FilePart struct {
38+
Name string `json:"name"`
39+
PartId int `json:"partId"`
40+
PartNo int `json:"partNo"`
41+
ChannelId int `json:"channelId"`
42+
Size int `json:"size"`
43+
Encrypted bool `json:"encrypted"`
44+
Salt string `json:"salt"`
45+
}
46+
47+
type chunkTask struct {
48+
data []byte
49+
chunkIdx int
50+
fileName string
51+
}
52+
53+
type CopyManager struct {
54+
TaskChan chan CopyTask
55+
Sem *semaphore.Weighted
56+
G *errgroup.Group
57+
Ctx context.Context
58+
d *Teldrive
59+
}
60+
61+
type CopyTask struct {
62+
SrcObj model.Obj
63+
DstDir model.Obj
64+
}
65+
66+
type CustomProxy struct {
67+
model.Proxy
68+
}
69+
70+
type ShareObj struct {
71+
Id string `json:"id"`
72+
Protected bool `json:"protected"`
73+
UserId int `json:"userId"`
74+
Type string `json:"type"`
75+
Name string `json:"name"`
76+
ExpiresAt time.Time `json:"expiresAt"`
77+
}

0 commit comments

Comments
 (0)