Skip to content

Commit 8ef0d64

Browse files
authored
Merge pull request #9462 from okatu-loli/feat/360ai-drive
feat(driver): add 360ai drive
2 parents 06a08de + 0fe31e0 commit 8ef0d64

6 files changed

Lines changed: 2618 additions & 0 deletions

File tree

drivers/all.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ import (
8585
_ "github.com/alist-org/alist/v3/drivers/wopan"
8686
_ "github.com/alist-org/alist/v3/drivers/wukong"
8787
_ "github.com/alist-org/alist/v3/drivers/yandex_disk"
88+
_ "github.com/alist-org/alist/v3/drivers/yunpan360"
8889
)
8990

9091
// All do nothing,just for import

drivers/yunpan360/driver.go

Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,286 @@
1+
package yunpan360
2+
3+
import (
4+
"context"
5+
"errors"
6+
stdpath "path"
7+
"strings"
8+
"sync"
9+
"time"
10+
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/pkg/utils"
15+
)
16+
17+
type Yunpan360 struct {
18+
model.Storage
19+
Addition
20+
21+
authMu sync.Mutex
22+
cachedOpenAuth *OpenAuthInfo
23+
openAuthExpire time.Time
24+
25+
cachedCookieSession *CookieDownloadSession
26+
cookieSessionExpire time.Time
27+
}
28+
29+
func (d *Yunpan360) Config() driver.Config {
30+
return config
31+
}
32+
33+
func (d *Yunpan360) GetAddition() driver.Additional {
34+
return &d.Addition
35+
}
36+
37+
func (d *Yunpan360) Init(ctx context.Context) error {
38+
if d.PageSize <= 0 {
39+
d.PageSize = 100
40+
}
41+
d.RootFolderPath = utils.FixAndCleanPath(d.RootFolderPath)
42+
if d.RootFolderPath == "" {
43+
d.RootFolderPath = "/"
44+
}
45+
d.OrderDirection = strings.ToLower(strings.TrimSpace(d.OrderDirection))
46+
if d.OrderDirection != "desc" {
47+
d.OrderDirection = "asc"
48+
}
49+
d.AuthType = strings.ToLower(strings.TrimSpace(d.AuthType))
50+
if d.AuthType == "" {
51+
d.AuthType = authTypeCookie
52+
}
53+
d.SubChannel = strings.TrimSpace(d.SubChannel)
54+
if d.SubChannel == "" {
55+
d.SubChannel = defaultSubChannel
56+
}
57+
d.EcsEnv = strings.ToLower(strings.TrimSpace(d.EcsEnv))
58+
if d.EcsEnv == "" {
59+
d.EcsEnv = openEnvProd
60+
}
61+
d.Cookie = strings.TrimSpace(d.Cookie)
62+
d.APIKey = strings.TrimSpace(d.APIKey)
63+
d.OwnerQID = strings.TrimSpace(d.OwnerQID)
64+
d.DownloadToken = strings.TrimSpace(d.DownloadToken)
65+
d.cachedOpenAuth = nil
66+
d.openAuthExpire = time.Time{}
67+
d.cachedCookieSession = nil
68+
d.cookieSessionExpire = time.Time{}
69+
70+
switch d.authMode() {
71+
case authTypeAPIKey:
72+
if d.APIKey == "" {
73+
return errors.New("api_key is empty")
74+
}
75+
_, err := d.openUserInfo(ctx)
76+
return err
77+
case authTypeCookie:
78+
if d.Cookie == "" {
79+
return errors.New("cookie is empty")
80+
}
81+
// Web download URLs require browser-session headers; force local proxying
82+
// so AList can forward Referer/Origin instead of exposing a bare 302 URL.
83+
d.WebProxy = true
84+
_, err := d.listCookiePage(ctx, d.RootFolderPath, 0, 1)
85+
return err
86+
default:
87+
return errors.New("invalid auth_type")
88+
}
89+
}
90+
91+
func (d *Yunpan360) Drop(ctx context.Context) error {
92+
d.cachedOpenAuth = nil
93+
d.openAuthExpire = time.Time{}
94+
d.cachedCookieSession = nil
95+
d.cookieSessionExpire = time.Time{}
96+
return nil
97+
}
98+
99+
func (d *Yunpan360) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
100+
dirPath := dir.GetPath()
101+
if dirPath == "" {
102+
dirPath = d.RootFolderPath
103+
}
104+
105+
objs := make([]model.Obj, 0, d.PageSize)
106+
for page := 0; ; page++ {
107+
resp, err := d.listPage(ctx, dirPath, page, d.PageSize)
108+
if err != nil {
109+
return nil, err
110+
}
111+
pageObjs := resp.Objects(dirPath)
112+
for _, item := range pageObjs {
113+
objs = append(objs, item)
114+
}
115+
if len(pageObjs) == 0 {
116+
break
117+
}
118+
if d.authMode() == authTypeAPIKey {
119+
if len(pageObjs) < d.PageSize {
120+
break
121+
}
122+
continue
123+
}
124+
if !resp.GetHasNextPage() {
125+
break
126+
}
127+
}
128+
return objs, nil
129+
}
130+
131+
func (d *Yunpan360) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
132+
if d.authMode() == authTypeCookie {
133+
resp, err := d.cookieDownloadURL(ctx, file)
134+
if err != nil {
135+
return nil, err
136+
}
137+
downloadURL := strings.TrimSpace(resp.GetURL())
138+
if downloadURL == "" {
139+
return nil, errors.New("download url is empty")
140+
}
141+
return &model.Link{
142+
URL: downloadURL,
143+
Header: map[string][]string{
144+
"Accept": {"text/javascript, text/html, application/xml, text/xml, */*"},
145+
"Origin": {baseURL},
146+
"Referer": {baseURL + indexPath},
147+
},
148+
}, nil
149+
}
150+
if d.authMode() != authTypeAPIKey {
151+
return nil, errs.NotImplement
152+
}
153+
154+
resp, err := d.openDownloadURL(ctx, file)
155+
if err != nil {
156+
return nil, err
157+
}
158+
downloadURL := strings.TrimSpace(resp.GetURL())
159+
if downloadURL == "" {
160+
return nil, errors.New("download url is empty")
161+
}
162+
return &model.Link{URL: downloadURL}, nil
163+
}
164+
165+
func (d *Yunpan360) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {
166+
if d.authMode() == authTypeCookie {
167+
fullPath := ensureDirAPIPath(stdpath.Join(parentDir.GetPath(), dirName))
168+
resp, err := d.cookieMakeDir(ctx, fullPath)
169+
if err != nil {
170+
return nil, err
171+
}
172+
return &YunpanObject{
173+
Object: model.Object{
174+
ID: resp.Data.NID,
175+
Path: normalizeRemotePath(fullPath),
176+
Name: dirName,
177+
Size: 0,
178+
Modified: time.Now(),
179+
IsFolder: true,
180+
},
181+
}, nil
182+
}
183+
if d.authMode() != authTypeAPIKey {
184+
return nil, errs.NotImplement
185+
}
186+
187+
fullPath := ensureDirAPIPath(stdpath.Join(parentDir.GetPath(), dirName))
188+
resp, err := d.openMakeDir(ctx, fullPath)
189+
if err != nil {
190+
return nil, err
191+
}
192+
obj := &model.Object{
193+
ID: resp.Data.NID,
194+
Path: normalizeRemotePath(fullPath),
195+
Name: dirName,
196+
Size: 0,
197+
Modified: time.Now(),
198+
IsFolder: true,
199+
}
200+
return obj, nil
201+
}
202+
203+
func (d *Yunpan360) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
204+
if d.authMode() == authTypeCookie {
205+
srcPath := apiPathForObj(srcObj)
206+
dstPath := ensureDirAPIPath(dstDir.GetPath())
207+
if err := d.cookieMove(ctx, srcPath, dstPath); err != nil {
208+
return nil, err
209+
}
210+
return cloneObj(srcObj, stdpath.Join(dstDir.GetPath(), srcObj.GetName()), srcObj.GetName()), nil
211+
}
212+
if d.authMode() != authTypeAPIKey {
213+
return nil, errs.NotImplement
214+
}
215+
216+
srcPath := apiPathForObj(srcObj)
217+
dstPath := ensureDirAPIPath(dstDir.GetPath())
218+
if err := d.openMove(ctx, srcPath, dstPath); err != nil {
219+
return nil, err
220+
}
221+
return cloneObj(srcObj, stdpath.Join(dstDir.GetPath(), srcObj.GetName()), srcObj.GetName()), nil
222+
}
223+
224+
func (d *Yunpan360) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) {
225+
if d.authMode() == authTypeCookie {
226+
targetName := strings.TrimSuffix(strings.TrimSpace(newName), "/")
227+
if targetName == "" {
228+
return nil, errors.New("new name is empty")
229+
}
230+
if err := d.cookieRename(ctx, srcObj, targetName); err != nil {
231+
return nil, err
232+
}
233+
parentPath := stdpath.Dir(srcObj.GetPath())
234+
if parentPath == "." {
235+
parentPath = "/"
236+
}
237+
return cloneObj(srcObj, stdpath.Join(parentPath, targetName), targetName), nil
238+
}
239+
if d.authMode() != authTypeAPIKey {
240+
return nil, errs.NotImplement
241+
}
242+
243+
srcPath := apiPathForObj(srcObj)
244+
targetName := newName
245+
if srcObj.IsDir() {
246+
targetName = ensureDirSuffix(newName)
247+
}
248+
if err := d.openRename(ctx, srcPath, targetName); err != nil {
249+
return nil, err
250+
}
251+
252+
parentPath := stdpath.Dir(srcObj.GetPath())
253+
if parentPath == "." {
254+
parentPath = "/"
255+
}
256+
return cloneObj(srcObj, stdpath.Join(parentPath, strings.TrimSuffix(newName, "/")), strings.TrimSuffix(newName, "/")), nil
257+
}
258+
259+
func (d *Yunpan360) Remove(ctx context.Context, obj model.Obj) error {
260+
if d.authMode() == authTypeCookie {
261+
return d.cookieRecycle(ctx, obj)
262+
}
263+
if d.authMode() != authTypeAPIKey {
264+
return errs.NotImplement
265+
}
266+
return d.openDelete(ctx, apiPathForObj(obj))
267+
}
268+
269+
func (d *Yunpan360) Put(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {
270+
if d.authMode() == authTypeCookie {
271+
return nil, errs.NotImplement
272+
}
273+
if d.authMode() != authTypeAPIKey {
274+
return nil, errs.NotImplement
275+
}
276+
return d.putOpenFile(ctx, dstDir, file, up)
277+
}
278+
279+
func (d *Yunpan360) authMode() string {
280+
if d.AuthType == authTypeAPIKey {
281+
return authTypeAPIKey
282+
}
283+
return authTypeCookie
284+
}
285+
286+
var _ driver.Driver = (*Yunpan360)(nil)

drivers/yunpan360/meta.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package yunpan360
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.RootPath
10+
AuthType string `json:"auth_type" type:"select" options:"cookie,api_key" default:"cookie"`
11+
Cookie string `json:"cookie" type:"text" help:"Cookie copied from a logged-in yunpan.com session; used when auth_type=cookie"`
12+
OwnerQID string `json:"owner_qid" type:"text" help:"Optional owner_qid for cookie-mode download; leave empty to auto-detect"`
13+
DownloadToken string `json:"download_token" type:"text" help:"Optional web token for cookie-mode download; leave empty to auto-detect"`
14+
APIKey string `json:"api_key" type:"text" help:"360 AI YunPan API key; used when auth_type=api_key"`
15+
EcsEnv string `json:"ecs_env" type:"select" options:"prod,test,hgtest" default:"prod"`
16+
SubChannel string `json:"sub_channel" default:"open"`
17+
OrderDirection string `json:"order_direction" type:"select" options:"asc,desc" default:"asc"`
18+
PageSize int `json:"page_size" type:"number" default:"100" help:"List page size"`
19+
}
20+
21+
var config = driver.Config{
22+
Name: "360AIYunPan",
23+
LocalSort: false,
24+
CheckStatus: true,
25+
NoUpload: false,
26+
DefaultRoot: "/",
27+
Alert: "info|api_key mode supports list/link/upload/mkdir/rename/move/delete; cookie mode supports list/link/mkdir/rename/move/delete only, and forces web proxy because direct download URLs require web headers.",
28+
}
29+
30+
func init() {
31+
op.RegisterDriver(func() driver.Driver {
32+
return &Yunpan360{}
33+
})
34+
}

0 commit comments

Comments
 (0)