Skip to content

Commit 9ed77a5

Browse files
KirCutexrgzs
andauthored
feat(driver): add AList v3 (#1721)
* feat(driver/openlist): compatible with AList v3 * Revert "feat(driver/openlist): compatible with AList v3" This reverts commit 90f3f80. * feat(driver): add AList v3 * Revert "feat(patch): add migration from Alist V3 driver to OpenList (#919)" Signed-off-by: MadDogOwner <xiaoran@xrgzs.top> --------- Signed-off-by: MadDogOwner <xiaoran@xrgzs.top> Co-authored-by: MadDogOwner <xiaoran@xrgzs.top>
1 parent 7d6d3b8 commit 9ed77a5

File tree

7 files changed

+648
-41
lines changed

7 files changed

+648
-41
lines changed

drivers/alist_v3/driver.go

Lines changed: 379 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,379 @@
1+
package alist_v3
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"io"
7+
"net/http"
8+
"net/url"
9+
"path"
10+
"strings"
11+
12+
"github.com/OpenListTeam/OpenList/v4/drivers/base"
13+
"github.com/OpenListTeam/OpenList/v4/internal/conf"
14+
"github.com/OpenListTeam/OpenList/v4/internal/driver"
15+
"github.com/OpenListTeam/OpenList/v4/internal/errs"
16+
"github.com/OpenListTeam/OpenList/v4/internal/model"
17+
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
18+
"github.com/OpenListTeam/OpenList/v4/server/common"
19+
"github.com/go-resty/resty/v2"
20+
log "github.com/sirupsen/logrus"
21+
)
22+
23+
type AListV3 struct {
24+
model.Storage
25+
Addition
26+
}
27+
28+
func (d *AListV3) Config() driver.Config {
29+
return config
30+
}
31+
32+
func (d *AListV3) GetAddition() driver.Additional {
33+
return &d.Addition
34+
}
35+
36+
func (d *AListV3) Init(ctx context.Context) error {
37+
d.Addition.Address = strings.TrimSuffix(d.Addition.Address, "/")
38+
var resp common.Resp[MeResp]
39+
_, _, err := d.request("/me", http.MethodGet, func(req *resty.Request) {
40+
req.SetResult(&resp)
41+
})
42+
if err != nil {
43+
return err
44+
}
45+
// if the username is not empty and the username is not the same as the current username, then login again
46+
if d.Username != resp.Data.Username {
47+
err = d.login()
48+
if err != nil {
49+
return err
50+
}
51+
}
52+
// re-get the user info
53+
_, _, err = d.request("/me", http.MethodGet, func(req *resty.Request) {
54+
req.SetResult(&resp)
55+
})
56+
if err != nil {
57+
return err
58+
}
59+
if utils.SliceContains(resp.Data.Role, model.GUEST) {
60+
u := d.Address + "/api/public/settings"
61+
res, err := base.RestyClient.R().Get(u)
62+
if err != nil {
63+
return err
64+
}
65+
allowMounted := utils.Json.Get(res.Body(), "data", conf.AllowMounted).ToString() == "true"
66+
if !allowMounted {
67+
return fmt.Errorf("the site does not allow mounted")
68+
}
69+
}
70+
return err
71+
}
72+
73+
func (d *AListV3) Drop(ctx context.Context) error {
74+
return nil
75+
}
76+
77+
func (d *AListV3) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
78+
var resp common.Resp[FsListResp]
79+
_, _, err := d.request("/fs/list", http.MethodPost, func(req *resty.Request) {
80+
req.SetResult(&resp).SetBody(ListReq{
81+
PageReq: model.PageReq{
82+
Page: 1,
83+
PerPage: 0,
84+
},
85+
Path: dir.GetPath(),
86+
Password: d.MetaPassword,
87+
Refresh: false,
88+
})
89+
})
90+
if err != nil {
91+
return nil, err
92+
}
93+
var files []model.Obj
94+
for _, f := range resp.Data.Content {
95+
file := model.ObjThumb{
96+
Object: model.Object{
97+
Name: f.Name,
98+
Modified: f.Modified,
99+
Ctime: f.Created,
100+
Size: f.Size,
101+
IsFolder: f.IsDir,
102+
HashInfo: utils.FromString(f.HashInfo),
103+
},
104+
Thumbnail: model.Thumbnail{Thumbnail: f.Thumb},
105+
}
106+
files = append(files, &file)
107+
}
108+
return files, nil
109+
}
110+
111+
func (d *AListV3) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
112+
var resp common.Resp[FsGetResp]
113+
headers := map[string]string{
114+
"User-Agent": base.UserAgent,
115+
}
116+
// if PassUAToUpsteam is true, then pass the user-agent to the upstream
117+
if d.PassUAToUpsteam {
118+
userAgent := args.Header.Get("user-agent")
119+
if userAgent != "" {
120+
headers["User-Agent"] = userAgent
121+
}
122+
}
123+
// if PassIPToUpsteam is true, then pass the ip address to the upstream
124+
if d.PassIPToUpsteam {
125+
ip := args.IP
126+
if ip != "" {
127+
headers["X-Forwarded-For"] = ip
128+
headers["X-Real-Ip"] = ip
129+
}
130+
}
131+
_, _, err := d.request("/fs/get", http.MethodPost, func(req *resty.Request) {
132+
req.SetResult(&resp).SetBody(FsGetReq{
133+
Path: file.GetPath(),
134+
Password: d.MetaPassword,
135+
}).SetHeaders(headers)
136+
})
137+
if err != nil {
138+
return nil, err
139+
}
140+
return &model.Link{
141+
URL: resp.Data.RawURL,
142+
}, nil
143+
}
144+
145+
func (d *AListV3) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
146+
_, _, err := d.request("/fs/mkdir", http.MethodPost, func(req *resty.Request) {
147+
req.SetBody(MkdirOrLinkReq{
148+
Path: path.Join(parentDir.GetPath(), dirName),
149+
})
150+
})
151+
return err
152+
}
153+
154+
func (d *AListV3) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
155+
_, _, err := d.request("/fs/move", http.MethodPost, func(req *resty.Request) {
156+
req.SetBody(MoveCopyReq{
157+
SrcDir: path.Dir(srcObj.GetPath()),
158+
DstDir: dstDir.GetPath(),
159+
Names: []string{srcObj.GetName()},
160+
})
161+
})
162+
return err
163+
}
164+
165+
func (d *AListV3) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
166+
_, _, err := d.request("/fs/rename", http.MethodPost, func(req *resty.Request) {
167+
req.SetBody(RenameReq{
168+
Path: srcObj.GetPath(),
169+
Name: newName,
170+
})
171+
})
172+
return err
173+
}
174+
175+
func (d *AListV3) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
176+
_, _, err := d.request("/fs/copy", http.MethodPost, func(req *resty.Request) {
177+
req.SetBody(MoveCopyReq{
178+
SrcDir: path.Dir(srcObj.GetPath()),
179+
DstDir: dstDir.GetPath(),
180+
Names: []string{srcObj.GetName()},
181+
})
182+
})
183+
return err
184+
}
185+
186+
func (d *AListV3) Remove(ctx context.Context, obj model.Obj) error {
187+
_, _, err := d.request("/fs/remove", http.MethodPost, func(req *resty.Request) {
188+
req.SetBody(RemoveReq{
189+
Dir: path.Dir(obj.GetPath()),
190+
Names: []string{obj.GetName()},
191+
})
192+
})
193+
return err
194+
}
195+
196+
func (d *AListV3) Put(ctx context.Context, dstDir model.Obj, s model.FileStreamer, up driver.UpdateProgress) error {
197+
reader := driver.NewLimitedUploadStream(ctx, &driver.ReaderUpdatingProgress{
198+
Reader: s,
199+
UpdateProgress: up,
200+
})
201+
req, err := http.NewRequestWithContext(ctx, http.MethodPut, d.Address+"/api/fs/put", reader)
202+
if err != nil {
203+
return err
204+
}
205+
req.Header.Set("Authorization", d.Token)
206+
req.Header.Set("File-Path", path.Join(dstDir.GetPath(), s.GetName()))
207+
req.Header.Set("Password", d.MetaPassword)
208+
if md5 := s.GetHash().GetHash(utils.MD5); len(md5) > 0 {
209+
req.Header.Set("X-File-Md5", md5)
210+
}
211+
if sha1 := s.GetHash().GetHash(utils.SHA1); len(sha1) > 0 {
212+
req.Header.Set("X-File-Sha1", sha1)
213+
}
214+
if sha256 := s.GetHash().GetHash(utils.SHA256); len(sha256) > 0 {
215+
req.Header.Set("X-File-Sha256", sha256)
216+
}
217+
218+
req.ContentLength = s.GetSize()
219+
// client := base.NewHttpClient()
220+
// client.Timeout = time.Hour * 6
221+
res, err := base.HttpClient.Do(req)
222+
if err != nil {
223+
return err
224+
}
225+
226+
bytes, err := io.ReadAll(res.Body)
227+
if err != nil {
228+
return err
229+
}
230+
log.Debugf("[openlist] response body: %s", string(bytes))
231+
if res.StatusCode >= 400 {
232+
return fmt.Errorf("request failed, status: %s", res.Status)
233+
}
234+
code := utils.Json.Get(bytes, "code").ToInt()
235+
if code != 200 {
236+
if code == 401 || code == 403 {
237+
err = d.login()
238+
if err != nil {
239+
return err
240+
}
241+
}
242+
return fmt.Errorf("request failed,code: %d, message: %s", code, utils.Json.Get(bytes, "message").ToString())
243+
}
244+
return nil
245+
}
246+
247+
func (d *AListV3) GetArchiveMeta(ctx context.Context, obj model.Obj, args model.ArchiveArgs) (model.ArchiveMeta, error) {
248+
if !d.ForwardArchiveReq {
249+
return nil, errs.NotImplement
250+
}
251+
var resp common.Resp[ArchiveMetaResp]
252+
_, code, err := d.request("/fs/archive/meta", http.MethodPost, func(req *resty.Request) {
253+
req.SetResult(&resp).SetBody(ArchiveMetaReq{
254+
ArchivePass: args.Password,
255+
Password: d.MetaPassword,
256+
Path: obj.GetPath(),
257+
Refresh: false,
258+
})
259+
})
260+
if code == 202 {
261+
return nil, errs.WrongArchivePassword
262+
}
263+
if err != nil {
264+
return nil, err
265+
}
266+
var tree []model.ObjTree
267+
if resp.Data.Content != nil {
268+
tree = make([]model.ObjTree, 0, len(resp.Data.Content))
269+
for _, content := range resp.Data.Content {
270+
tree = append(tree, &content)
271+
}
272+
}
273+
return &model.ArchiveMetaInfo{
274+
Comment: resp.Data.Comment,
275+
Encrypted: resp.Data.Encrypted,
276+
Tree: tree,
277+
}, nil
278+
}
279+
280+
func (d *AListV3) ListArchive(ctx context.Context, obj model.Obj, args model.ArchiveInnerArgs) ([]model.Obj, error) {
281+
if !d.ForwardArchiveReq {
282+
return nil, errs.NotImplement
283+
}
284+
var resp common.Resp[ArchiveListResp]
285+
_, code, err := d.request("/fs/archive/list", http.MethodPost, func(req *resty.Request) {
286+
req.SetResult(&resp).SetBody(ArchiveListReq{
287+
ArchiveMetaReq: ArchiveMetaReq{
288+
ArchivePass: args.Password,
289+
Password: d.MetaPassword,
290+
Path: obj.GetPath(),
291+
Refresh: false,
292+
},
293+
PageReq: model.PageReq{
294+
Page: 1,
295+
PerPage: 0,
296+
},
297+
InnerPath: args.InnerPath,
298+
})
299+
})
300+
if code == 202 {
301+
return nil, errs.WrongArchivePassword
302+
}
303+
if err != nil {
304+
return nil, err
305+
}
306+
var files []model.Obj
307+
for _, f := range resp.Data.Content {
308+
file := model.ObjThumb{
309+
Object: model.Object{
310+
Name: f.Name,
311+
Modified: f.Modified,
312+
Ctime: f.Created,
313+
Size: f.Size,
314+
IsFolder: f.IsDir,
315+
HashInfo: utils.FromString(f.HashInfo),
316+
},
317+
Thumbnail: model.Thumbnail{Thumbnail: f.Thumb},
318+
}
319+
files = append(files, &file)
320+
}
321+
return files, nil
322+
}
323+
324+
func (d *AListV3) Extract(ctx context.Context, obj model.Obj, args model.ArchiveInnerArgs) (*model.Link, error) {
325+
if !d.ForwardArchiveReq {
326+
return nil, errs.NotSupport
327+
}
328+
var resp common.Resp[ArchiveMetaResp]
329+
_, _, err := d.request("/fs/archive/meta", http.MethodPost, func(req *resty.Request) {
330+
req.SetResult(&resp).SetBody(ArchiveMetaReq{
331+
ArchivePass: args.Password,
332+
Password: d.MetaPassword,
333+
Path: obj.GetPath(),
334+
Refresh: false,
335+
})
336+
})
337+
if err != nil {
338+
return nil, err
339+
}
340+
return &model.Link{
341+
URL: fmt.Sprintf("%s?inner=%s&pass=%s&sign=%s",
342+
resp.Data.RawURL,
343+
utils.EncodePath(args.InnerPath, true),
344+
url.QueryEscape(args.Password),
345+
resp.Data.Sign),
346+
}, nil
347+
}
348+
349+
func (d *AListV3) ArchiveDecompress(ctx context.Context, srcObj, dstDir model.Obj, args model.ArchiveDecompressArgs) error {
350+
if !d.ForwardArchiveReq {
351+
return errs.NotImplement
352+
}
353+
dir, name := path.Split(srcObj.GetPath())
354+
_, _, err := d.request("/fs/archive/decompress", http.MethodPost, func(req *resty.Request) {
355+
req.SetBody(DecompressReq{
356+
ArchivePass: args.Password,
357+
CacheFull: args.CacheFull,
358+
DstDir: dstDir.GetPath(),
359+
InnerPath: args.InnerPath,
360+
Name: []string{name},
361+
PutIntoNewDir: args.PutIntoNewDir,
362+
SrcDir: dir,
363+
})
364+
})
365+
return err
366+
}
367+
368+
func (d *AListV3) ResolveLinkCacheMode(_ string) driver.LinkCacheMode {
369+
var mode driver.LinkCacheMode
370+
if d.PassIPToUpsteam {
371+
mode |= driver.LinkCacheIP
372+
}
373+
if d.PassUAToUpsteam {
374+
mode |= driver.LinkCacheUA
375+
}
376+
return mode
377+
}
378+
379+
var _ driver.Driver = (*AListV3)(nil)

0 commit comments

Comments
 (0)