diff --git a/.github/stale.yml b/.github/stale.yml index 33aa77dd..944e11b4 100644 --- a/.github/stale.yml +++ b/.github/stale.yml @@ -15,5 +15,5 @@ markComment: > for your contributions. # Comment to post when closing a stale issue. Set to `false` to disable closeComment: > - This issue was closed due to inactive more than 52 days. You can reopen or + This issue was closed due to inactive more than 52 days. You can reopen or recreate it if you think it should continue. Thank you for your contributions again. \ No newline at end of file diff --git a/drivers/aliyundrive_open/driver.go b/drivers/aliyundrive_open/driver.go new file mode 100644 index 00000000..dbcdbcaf --- /dev/null +++ b/drivers/aliyundrive_open/driver.go @@ -0,0 +1,234 @@ +package aliyundrive_open + +import ( + "context" + "io" + "math" + "net/http" + "time" + + "github.com/alist-org/alist/v3/drivers/base" + "github.com/alist-org/alist/v3/internal/driver" + "github.com/alist-org/alist/v3/internal/errs" + "github.com/alist-org/alist/v3/internal/model" + "github.com/alist-org/alist/v3/internal/op" + "github.com/alist-org/alist/v3/pkg/cron" + "github.com/alist-org/alist/v3/pkg/utils" + "github.com/go-resty/resty/v2" + log "github.com/sirupsen/logrus" +) + +type AliyundriveOpen struct { + model.Storage + Addition + base string + cron *cron.Cron + + AccessToken string + DriveId string +} + +func (d *AliyundriveOpen) Config() driver.Config { + return config +} + +func (d *AliyundriveOpen) GetAddition() driver.Additional { + return &d.Addition +} + +func (d *AliyundriveOpen) Init(ctx context.Context) error { + err := d.refreshToken() + if err != nil { + return err + } + res, err := d.request("/adrive/v1.0/user/getDriveInfo", http.MethodPost, nil) + if err != nil { + return err + } + d.DriveId = utils.Json.Get(res, "default_drive_id").ToString() + d.cron = cron.NewCron(time.Hour * 2) + d.cron.Do(func() { + err := d.refreshToken() + d.Status = err.Error() + op.MustSaveDriverStorage(d) + if err != nil { + log.Errorf("%+v", err) + } + }) + return nil +} + +func (d *AliyundriveOpen) Drop(ctx context.Context) error { + if d.cron != nil { + d.cron.Stop() + } + return nil +} + +func (d *AliyundriveOpen) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) { + files, err := d.getFiles(dir.GetID()) + if err != nil { + return nil, err + } + return utils.SliceConvert(files, func(src File) (model.Obj, error) { + return fileToObj(src), nil + }) +} + +func (d *AliyundriveOpen) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) { + res, err := d.request("/adrive/v1.0/openFile/getDownloadUrl", http.MethodPost, func(req *resty.Request) { + req.SetBody(base.Json{ + "drive_id": d.DriveId, + "file_id": file.GetID(), + }) + }) + if err != nil { + return nil, err + } + url := utils.Json.Get(res, "url").ToString() + return &model.Link{ + URL: url, + }, nil +} + +func (d *AliyundriveOpen) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error { + _, err := d.request("/adrive/v1.0/openFile/create", http.MethodPost, func(req *resty.Request) { + req.SetBody(base.Json{ + "drive_id": d.DriveId, + "parent_file_id": parentDir.GetID(), + "name": dirName, + "type": "folder", + "check_name_mode": "refuse", + }) + }) + return err +} + +func (d *AliyundriveOpen) Move(ctx context.Context, srcObj, dstDir model.Obj) error { + _, err := d.request("/adrive/v1.0/openFile/move", http.MethodPost, func(req *resty.Request) { + req.SetBody(base.Json{ + "drive_id": d.DriveId, + "file_id": srcObj.GetID(), + "to_parent_file_id": dstDir.GetID(), + "check_name_mode": "refuse", // optional:ignore,auto_rename,refuse + //"new_name": "newName", // The new name to use when a file of the same name exists + }) + }) + return err +} + +func (d *AliyundriveOpen) Rename(ctx context.Context, srcObj model.Obj, newName string) error { + _, err := d.request("/adrive/v1.0/openFile/update", http.MethodPost, func(req *resty.Request) { + req.SetBody(base.Json{ + "drive_id": d.DriveId, + "file_id": srcObj.GetID(), + "name": newName, + }) + }) + return err +} + +func (d *AliyundriveOpen) Copy(ctx context.Context, srcObj, dstDir model.Obj) error { + _, err := d.request("/adrive/v1.0/openFile/copy", http.MethodPost, func(req *resty.Request) { + req.SetBody(base.Json{ + "drive_id": d.DriveId, + "file_id": srcObj.GetID(), + "to_parent_file_id": dstDir.GetID(), + "auto_rename": true, + }) + }) + return err +} + +func (d *AliyundriveOpen) Remove(ctx context.Context, obj model.Obj) error { + _, err := d.request("/adrive/v1.0/openFile/recyclebin/trash", http.MethodPost, func(req *resty.Request) { + req.SetBody(base.Json{ + "drive_id": d.DriveId, + "file_id": obj.GetID(), + }) + }) + return err +} + +func (d *AliyundriveOpen) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error { + // rapid_upload is not currently supported + // 1. create + const DEFAULT int64 = 20971520 + createData := base.Json{ + "drive_id": d.DriveId, + "parent_file_id": dstDir.GetID(), + "name": stream.GetName(), + "type": "file", + "check_name_mode": "ignore", + } + count := 1 + if stream.GetSize() > DEFAULT { + count = int(math.Ceil(float64(stream.GetSize()) / float64(DEFAULT))) + partInfoList := make([]base.Json, 0, count) + for i := 1; i <= count; i++ { + partInfoList = append(partInfoList, base.Json{"part_number": i}) + } + createData["part_info_list"] = partInfoList + } + var createResp CreateResp + _, err := d.request("/adrive/v1.0/openFile/create", http.MethodPost, func(req *resty.Request) { + req.SetBody(createData).SetResult(&createResp) + }) + if err != nil { + return err + } + // 2. upload + for i, partInfo := range createResp.PartInfoList { + if utils.IsCanceled(ctx) { + return ctx.Err() + } + req, err := http.NewRequest("PUT", partInfo.UploadUrl, io.LimitReader(stream, DEFAULT)) + if err != nil { + return err + } + req = req.WithContext(ctx) + res, err := base.HttpClient.Do(req) + if err != nil { + return err + } + res.Body.Close() + if count > 0 { + up(i * 100 / count) + } + } + // 3. complete + _, err = d.request("/adrive/v1.0/openFile/complete", http.MethodPost, func(req *resty.Request) { + req.SetBody(base.Json{ + "drive_id": d.DriveId, + "file_id": createResp.FileId, + "upload_id": createResp.UploadId, + }) + }) + return err +} + +func (d *AliyundriveOpen) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) { + var resp base.Json + var uri string + data := base.Json{ + "drive_id": d.DriveId, + "file_id": args.Obj.GetID(), + } + switch args.Method { + case "video_preview": + uri = "/adrive/v1.0/openFile/getVideoPreviewPlayInfo" + data["category"] = "live_transcoding" + data["url_expire_sec"] = 14400 + default: + return nil, errs.NotSupport + } + _, err := d.request(uri, http.MethodPost, func(req *resty.Request) { + req.SetBody(data).SetResult(&resp) + }) + if err != nil { + return nil, err + } + return resp, nil +} + +var _ driver.Driver = (*AliyundriveOpen)(nil) diff --git a/drivers/aliyundrive_open/meta.go b/drivers/aliyundrive_open/meta.go new file mode 100644 index 00000000..6bf3b5cf --- /dev/null +++ b/drivers/aliyundrive_open/meta.go @@ -0,0 +1,35 @@ +package aliyundrive_open + +import ( + "github.com/alist-org/alist/v3/internal/driver" + "github.com/alist-org/alist/v3/internal/op" +) + +type Addition struct { + driver.RootID + RefreshToken string `json:"refresh_token" required:"true"` + OrderBy string `json:"order_by" type:"select" options:"name,size,updated_at,created_at"` + OrderDirection string `json:"order_direction" type:"select" options:"ASC,DESC"` + OauthTokenURL string `json:"oauth_token_url" default:"https://api.nn.ci/alist/ali_open/token"` + ClientID string `json:"client_id" required:"false"` + ClientSecret string `json:"client_secret" required:"false"` +} + +var config = driver.Config{ + Name: "AliyundriveOpen", + LocalSort: false, + OnlyLocal: false, + OnlyProxy: false, + NoCache: false, + NoUpload: false, + NeedMs: false, + DefaultRoot: "root", +} + +func init() { + op.RegisterDriver(func() driver.Driver { + return &AliyundriveOpen{ + base: "https://open.aliyundrive.com", + } + }) +} diff --git a/drivers/aliyundrive_open/types.go b/drivers/aliyundrive_open/types.go new file mode 100644 index 00000000..3aceb16e --- /dev/null +++ b/drivers/aliyundrive_open/types.go @@ -0,0 +1,67 @@ +package aliyundrive_open + +import ( + "time" + + "github.com/alist-org/alist/v3/internal/model" +) + +type ErrResp struct { + Code string `json:"code"` + Message string `json:"message"` +} + +type Files struct { + Items []File `json:"items"` + NextMarker string `json:"next_marker"` +} + +type File struct { + DriveId string `json:"drive_id"` + FileId string `json:"file_id"` + ParentFileId string `json:"parent_file_id"` + Name string `json:"name"` + Size int64 `json:"size"` + FileExtension string `json:"file_extension"` + ContentHash string `json:"content_hash"` + Category string `json:"category"` + Type string `json:"type"` + Thumbnail string `json:"thumbnail"` + Url string `json:"url"` + CreatedAt *time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +func fileToObj(f File) *model.ObjThumb { + return &model.ObjThumb{ + Object: model.Object{ + ID: f.FileId, + Name: f.Name, + Size: f.Size, + Modified: f.UpdatedAt, + IsFolder: f.Type == "folder", + }, + Thumbnail: model.Thumbnail{Thumbnail: f.Thumbnail}, + } +} + +type CreateResp struct { + //Type string `json:"type"` + //ParentFileId string `json:"parent_file_id"` + //DriveId string `json:"drive_id"` + FileId string `json:"file_id"` + //RevisionId string `json:"revision_id"` + //EncryptMode string `json:"encrypt_mode"` + //DomainId string `json:"domain_id"` + //FileName string `json:"file_name"` + UploadId string `json:"upload_id"` + //Location string `json:"location"` + RapidUpload bool `json:"rapid_upload"` + PartInfoList []struct { + Etag interface{} `json:"etag"` + PartNumber int `json:"part_number"` + PartSize interface{} `json:"part_size"` + UploadUrl string `json:"upload_url"` + ContentType string `json:"content_type"` + } `json:"part_info_list"` +} diff --git a/drivers/aliyundrive_open/util.go b/drivers/aliyundrive_open/util.go new file mode 100644 index 00000000..6d62db02 --- /dev/null +++ b/drivers/aliyundrive_open/util.go @@ -0,0 +1,100 @@ +package aliyundrive_open + +import ( + "errors" + "fmt" + "net/http" + + "github.com/alist-org/alist/v3/drivers/base" + "github.com/alist-org/alist/v3/internal/op" + "github.com/go-resty/resty/v2" +) + +// do others that not defined in Driver interface + +func (d *AliyundriveOpen) refreshToken() error { + url := d.base + "/oauth/access_token" + if d.OauthTokenURL != "" && d.ClientID == "" { + url = d.OauthTokenURL + } + var resp base.TokenResp + var e ErrResp + _, err := base.RestyClient.R(). + ForceContentType("application/json"). + SetBody(base.Json{ + "client_id": d.ClientID, + "client_secret": d.ClientSecret, + "grant_type": "refresh_token", + "refresh_token": d.RefreshToken, + }). + SetResult(&resp). + SetError(&e). + Post(url) + if err != nil { + return err + } + if e.Code != "" { + return fmt.Errorf("failed to refresh token: %s", e.Message) + } + if resp.RefreshToken == "" { + return errors.New("failed to refresh token: refresh token is empty") + } + d.RefreshToken, d.AccessToken = resp.RefreshToken, resp.AccessToken + op.MustSaveDriverStorage(d) + return nil +} + +func (d *AliyundriveOpen) request(uri, method string, callback base.ReqCallback) ([]byte, error) { + req := base.RestyClient.R() + // TODO check whether access_token is expired + req.SetHeader("Authorization", "Bearer "+d.AccessToken) + if method == http.MethodPost { + req.SetHeader("Content-Type", "application/json") + } + if callback != nil { + callback(req) + } + var e ErrResp + req.SetError(&e) + res, err := req.Execute(method, d.base+uri) + if err != nil { + return nil, err + } + if e.Code != "" { + return nil, fmt.Errorf("%s:%s", e.Code, e.Message) + } + return res.Body(), nil +} + +func (d *AliyundriveOpen) getFiles(fileId string) ([]File, error) { + marker := "first" + res := make([]File, 0) + for marker != "" { + if marker == "first" { + marker = "" + } + var resp Files + data := base.Json{ + "drive_id": d.DriveId, + "limit": 200, + "marker": marker, + "order_by": d.OrderBy, + "order_direction": d.OrderDirection, + "parent_file_id": fileId, + //"category": "", + //"type": "", + //"video_thumbnail_time": 120000, + //"video_thumbnail_width": 480, + //"image_thumbnail_width": 480, + } + _, err := d.request("/adrive/v1.0/openFile/list", http.MethodPost, func(req *resty.Request) { + req.SetBody(data).SetResult(&resp) + }) + if err != nil { + return nil, err + } + marker = resp.NextMarker + res = append(res, resp.Items...) + } + return res, nil +} diff --git a/drivers/all.go b/drivers/all.go index a141d466..eeffd956 100644 --- a/drivers/all.go +++ b/drivers/all.go @@ -9,6 +9,7 @@ import ( _ "github.com/alist-org/alist/v3/drivers/alist_v2" _ "github.com/alist-org/alist/v3/drivers/alist_v3" _ "github.com/alist-org/alist/v3/drivers/aliyundrive" + _ "github.com/alist-org/alist/v3/drivers/aliyundrive_open" _ "github.com/alist-org/alist/v3/drivers/aliyundrive_share" _ "github.com/alist-org/alist/v3/drivers/baidu_netdisk" _ "github.com/alist-org/alist/v3/drivers/baidu_photo" diff --git a/server/router.go b/server/router.go index 37dbd0e5..39c2cfbd 100644 --- a/server/router.go +++ b/server/router.go @@ -19,9 +19,9 @@ func Init(e *gin.Engine) { c.Redirect(302, conf.URL.Path) }) } + Cors(e) g := e.Group(conf.URL.Path) common.SecretKey = []byte(conf.Conf.JwtSecret) - Cors(g) g.Use(middlewares.StoragesLoaded) if conf.Conf.MaxConnections > 0 { g.Use(middlewares.MaxAllowed(conf.Conf.MaxConnections)) @@ -133,7 +133,7 @@ func _fs(g *gin.RouterGroup) { g.POST("/add_qbit", handles.AddQbittorrent) } -func Cors(r *gin.RouterGroup) { +func Cors(r *gin.Engine) { config := cors.DefaultConfig() config.AllowAllOrigins = true config.AllowHeaders = []string{"*"}