diff --git a/drivers/all.go b/drivers/all.go index 740ea350..b387fde9 100644 --- a/drivers/all.go +++ b/drivers/all.go @@ -16,6 +16,7 @@ import ( _ "github.com/alist-org/alist/v3/drivers/uss" _ "github.com/alist-org/alist/v3/drivers/virtual" _ "github.com/alist-org/alist/v3/drivers/webdav" + _ "github.com/alist-org/alist/v3/drivers/yandex_disk" ) // All do nothing,just for import diff --git a/drivers/yandex_disk/driver.go b/drivers/yandex_disk/driver.go new file mode 100644 index 00000000..fa496011 --- /dev/null +++ b/drivers/yandex_disk/driver.go @@ -0,0 +1,146 @@ +package yandex_disk + +import ( + "context" + "net/http" + "path" + "strconv" + + "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/pkg/utils" + "github.com/go-resty/resty/v2" +) + +type YandexDisk struct { + model.Storage + Addition + AccessToken string +} + +func (d *YandexDisk) Config() driver.Config { + return config +} + +func (d *YandexDisk) GetAddition() driver.Additional { + return d.Addition +} + +func (d *YandexDisk) Init(ctx context.Context, storage model.Storage) error { + d.Storage = storage + err := utils.Json.UnmarshalFromString(d.Storage.Addition, &d.Addition) + if err != nil { + return err + } + return d.refreshToken() +} + +func (d *YandexDisk) Drop(ctx context.Context) error { + return nil +} + +func (d *YandexDisk) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) { + files, err := d.getFiles(dir.GetPath()) + if err != nil { + return nil, err + } + return utils.SliceConvert(files, func(src File) (model.Obj, error) { + return fileToObj(src), nil + }) +} + +//func (d *YandexDisk) Get(ctx context.Context, path string) (model.Obj, error) { +// // this is optional +// return nil, errs.NotImplement +//} + +func (d *YandexDisk) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) { + var resp DownResp + _, err := d.request("/download", http.MethodGet, func(req *resty.Request) { + req.SetQueryParam("path", file.GetPath()) + }, &resp) + if err != nil { + return nil, err + } + link := model.Link{ + URL: resp.Href, + } + return &link, nil +} + +func (d *YandexDisk) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error { + _, err := d.request("", http.MethodPut, func(req *resty.Request) { + req.SetQueryParam("path", path.Join(parentDir.GetPath(), dirName)) + }, nil) + return err +} + +func (d *YandexDisk) Move(ctx context.Context, srcObj, dstDir model.Obj) error { + _, err := d.request("/move", http.MethodPost, func(req *resty.Request) { + req.SetQueryParams(map[string]string{ + "from": srcObj.GetPath(), + "path": path.Join(dstDir.GetPath(), srcObj.GetName()), + "overwrite": "true", + }) + }, nil) + return err +} + +func (d *YandexDisk) Rename(ctx context.Context, srcObj model.Obj, newName string) error { + _, err := d.request("/move", http.MethodPost, func(req *resty.Request) { + req.SetQueryParams(map[string]string{ + "from": srcObj.GetPath(), + "path": path.Join(path.Dir(srcObj.GetPath()), newName), + "overwrite": "true", + }) + }, nil) + return err +} + +func (d *YandexDisk) Copy(ctx context.Context, srcObj, dstDir model.Obj) error { + _, err := d.request("/copy", http.MethodPost, func(req *resty.Request) { + req.SetQueryParams(map[string]string{ + "from": srcObj.GetPath(), + "path": path.Join(dstDir.GetPath(), srcObj.GetName()), + "overwrite": "true", + }) + }, nil) + return err +} + +func (d *YandexDisk) Remove(ctx context.Context, obj model.Obj) error { + _, err := d.request("", http.MethodDelete, func(req *resty.Request) { + req.SetQueryParam("path", obj.GetPath()) + }, nil) + return err +} + +func (d *YandexDisk) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error { + var resp UploadResp + _, err := d.request("/upload", http.MethodGet, func(req *resty.Request) { + req.SetQueryParams(map[string]string{ + "path": path.Join(dstDir.GetPath(), stream.GetName()), + "overwrite": "true", + }) + }, &resp) + if err != nil { + return err + } + req, err := http.NewRequest(resp.Method, resp.Href, stream) + if err != nil { + return err + } + req.Header.Set("Content-Length", strconv.FormatInt(stream.GetSize(), 10)) + req.Header.Set("Content-Type", "application/octet-stream") + res, err := base.HttpClient.Do(req) + res.Body.Close() + return err +} + +func (d *YandexDisk) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) { + return nil, errs.NotSupport +} + +var _ driver.Driver = (*YandexDisk)(nil) diff --git a/drivers/yandex_disk/meta.go b/drivers/yandex_disk/meta.go new file mode 100644 index 00000000..0312a679 --- /dev/null +++ b/drivers/yandex_disk/meta.go @@ -0,0 +1,28 @@ +package yandex_disk + +import ( + "github.com/alist-org/alist/v3/internal/driver" + "github.com/alist-org/alist/v3/internal/op" +) + +type Addition struct { + RefreshToken string `json:"refresh_token" required:"true"` + OrderBy string `json:"order_by" type:"select" options:"name,path,created,modified,size" default:"name"` + OrderDirection string `json:"order_direction" type:"select" options:"asc,desc" default:"asc"` + driver.RootPath + ClientID string `json:"client_id" required:"true" default:"a78d5a69054042fa936f6c77f9a0ae8b"` + ClientSecret string `json:"client_secret" required:"true" default:"9c119bbb04b346d2a52aa64401936b2b"` +} + +var config = driver.Config{ + Name: "YandexDisk", + DefaultRoot: "/", +} + +func New() driver.Driver { + return &YandexDisk{} +} + +func init() { + op.RegisterDriver(config, New) +} diff --git a/drivers/yandex_disk/types.go b/drivers/yandex_disk/types.go new file mode 100644 index 00000000..111bd464 --- /dev/null +++ b/drivers/yandex_disk/types.go @@ -0,0 +1,87 @@ +package yandex_disk + +import ( + "time" + + "github.com/alist-org/alist/v3/internal/model" +) + +type TokenErrResp struct { + ErrorDescription string `json:"error_description"` + Error string `json:"error"` +} + +type ErrResp struct { + Message string `json:"message"` + Description string `json:"description"` + Error string `json:"error"` +} + +type File struct { + //AntivirusStatus string `json:"antivirus_status"` + Size int64 `json:"size"` + //CommentIds struct { + // PrivateResource string `json:"private_resource"` + // PublicResource string `json:"public_resource"` + //} `json:"comment_ids"` + Name string `json:"name"` + //Exif struct { + // DateTime time.Time `json:"date_time"` + //} `json:"exif"` + //Created time.Time `json:"created"` + //ResourceId string `json:"resource_id"` + Modified time.Time `json:"modified"` + //MimeType string `json:"mime_type"` + File string `json:"file"` + //MediaType string `json:"media_type"` + Preview string `json:"preview"` + Path string `json:"path"` + //Sha256 string `json:"sha256"` + Type string `json:"type"` + //Md5 string `json:"md5"` + //Revision int64 `json:"revision"` +} + +func fileToObj(f File) model.Obj { + return &model.Object{ + Name: f.Name, + Size: f.Size, + Modified: f.Modified, + IsFolder: f.Type == "dir", + } +} + +type FilesResp struct { + Embedded struct { + Sort string `json:"sort"` + Items []File `json:"items"` + Limit int `json:"limit"` + Offset int `json:"offset"` + Path string `json:"path"` + Total int `json:"total"` + } `json:"_embedded"` + Name string `json:"name"` + Exif struct { + } `json:"exif"` + ResourceId string `json:"resource_id"` + Created time.Time `json:"created"` + Modified time.Time `json:"modified"` + Path string `json:"path"` + CommentIds struct { + } `json:"comment_ids"` + Type string `json:"type"` + Revision int64 `json:"revision"` +} + +type DownResp struct { + Href string `json:"href"` + Method string `json:"method"` + Templated bool `json:"templated"` +} + +type UploadResp struct { + OperationId string `json:"operation_id"` + Href string `json:"href"` + Method string `json:"method"` + Templated bool `json:"templated"` +} diff --git a/drivers/yandex_disk/util.go b/drivers/yandex_disk/util.go new file mode 100644 index 00000000..c3ffc295 --- /dev/null +++ b/drivers/yandex_disk/util.go @@ -0,0 +1,98 @@ +package yandex_disk + +import ( + "errors" + "fmt" + "net/http" + "strconv" + + "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 *YandexDisk) refreshToken() error { + u := "https://oauth.yandex.com/token" + var resp base.TokenResp + var e TokenErrResp + _, err := base.RestyClient.R().SetResult(&resp).SetError(&e).SetFormData(map[string]string{ + "grant_type": "refresh_token", + "refresh_token": d.RefreshToken, + "client_id": d.ClientID, + "client_secret": d.ClientSecret, + }).Post(u) + if err != nil { + return err + } + if e.Error != "" { + return fmt.Errorf("%s : %s", e.Error, e.ErrorDescription) + } + d.AccessToken, d.RefreshToken = resp.AccessToken, resp.RefreshToken + op.MustSaveDriverStorage(d) + return nil +} + +func (d *YandexDisk) request(pathname string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) { + u := "https://cloud-api.yandex.net/v1/disk/resources" + pathname + req := base.RestyClient.R() + req.SetHeader("Authorization", "OAuth "+d.AccessToken) + if callback != nil { + callback(req) + } + if resp != nil { + req.SetResult(resp) + } + var e ErrResp + req.SetError(&e) + res, err := req.Execute(method, u) + if err != nil { + return nil, err + } + //log.Debug(res.String()) + if e.Error != "" { + if e.Error == "UnauthorizedError" { + err = d.refreshToken() + if err != nil { + return nil, err + } + return d.request(pathname, method, callback, resp) + } + return nil, errors.New(e.Description) + } + return res.Body(), nil +} + +func (d *YandexDisk) getFiles(path string) ([]File, error) { + limit := 100 + page := 1 + res := make([]File, 0) + for { + offset := (page - 1) * limit + query := map[string]string{ + "path": path, + "limit": strconv.Itoa(limit), + "offset": strconv.Itoa(offset), + } + if d.OrderBy != "" { + if d.OrderDirection == "desc" { + query["sort"] = "-" + d.OrderBy + } else { + query["sort"] = d.OrderBy + } + } + var resp FilesResp + _, err := d.request("", http.MethodGet, func(req *resty.Request) { + req.SetQueryParams(query) + }, &resp) + if err != nil { + return nil, err + } + res = append(res, resp.Embedded.Items...) + if resp.Embedded.Total <= offset+limit { + break + } + } + return res, nil +}