feat: add onedrive driver

This commit is contained in:
Noah Hsu
2022-08-30 21:52:06 +08:00
parent c95a7c2a04
commit f551dc76d0
21 changed files with 535 additions and 30 deletions

View File

@ -2,6 +2,7 @@ package drivers
import (
_ "github.com/alist-org/alist/v3/drivers/local"
_ "github.com/alist-org/alist/v3/drivers/onedrive"
_ "github.com/alist-org/alist/v3/drivers/virtual"
)

26
drivers/base/client.go Normal file
View File

@ -0,0 +1,26 @@
package base
import (
"net/http"
"time"
"github.com/go-resty/resty/v2"
)
var NoRedirectClient *resty.Client
var RestyClient = resty.New()
var HttpClient = &http.Client{}
var UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36"
var DefaultTimeout = time.Second * 10
func init() {
NoRedirectClient = resty.New().SetRedirectPolicy(
resty.RedirectPolicyFunc(func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
}),
)
NoRedirectClient.SetHeader("user-agent", UserAgent)
RestyClient.SetHeader("user-agent", UserAgent)
RestyClient.SetRetryCount(3)
RestyClient.SetTimeout(DefaultTimeout)
}

8
drivers/base/types.go Normal file
View File

@ -0,0 +1,8 @@
package base
type Json map[string]interface{}
type TokenResp struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
}

View File

@ -40,7 +40,6 @@ func (d *Local) Init(ctx context.Context, storage model.Storage) error {
}
if !utils.Exists(d.RootFolder) {
err = errors.Errorf("root folder %s not exists", d.RootFolder)
d.SetStatus(err.Error())
} else {
if !filepath.IsAbs(d.RootFolder) {
d.RootFolder, err = filepath.Abs(d.RootFolder)
@ -48,7 +47,6 @@ func (d *Local) Init(ctx context.Context, storage model.Storage) error {
return errors.Wrap(err, "error while get abs path")
}
}
d.SetStatus("OK")
}
operations.MustSaveDriverStorage(d)
return err

View File

@ -11,7 +11,7 @@ type Addition struct {
}
var config = driver.Config{
Name: "Local",
Name: "local",
OnlyLocal: true,
LocalSort: true,
NoCache: true,

157
drivers/onedrive/driver.go Normal file
View File

@ -0,0 +1,157 @@
package onedrive
import (
"context"
"net/http"
stdpath "path"
"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"
"github.com/pkg/errors"
)
type Onedrive struct {
model.Storage
Addition
AccessToken string
}
func (d *Onedrive) Config() driver.Config {
return config
}
func (d *Onedrive) GetAddition() driver.Additional {
return d.Addition
}
func (d *Onedrive) Init(ctx context.Context, storage model.Storage) error {
d.Storage = storage
err := utils.Json.UnmarshalFromString(d.Storage.Addition, &d.Addition)
if err != nil {
return errors.Wrap(err, "error while unmarshal addition")
}
return d.refreshToken()
}
func (d *Onedrive) Drop(ctx context.Context) error {
return nil
}
func (d *Onedrive) 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
}
objs := make([]model.Obj, len(files))
for i := 0; i < len(files); i++ {
objs[i] = fileToObj(files[i])
}
return objs, nil
}
func (d *Onedrive) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
f, err := d.GetFile(file.GetID())
if err != nil {
return nil, err
}
if f.File == nil {
return nil, errs.NotFile
}
return &model.Link{
URL: f.Url,
}, nil
}
func (d *Onedrive) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
url := d.GetMetaUrl(false, parentDir.GetID()) + "/children"
data := base.Json{
"name": dirName,
"folder": base.Json{},
"@microsoft.graph.conflictBehavior": "rename",
}
_, err := d.Request(url, http.MethodPost, func(req *resty.Request) {
req.SetBody(data)
}, nil)
return err
}
func (d *Onedrive) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
dst, err := d.GetFile(dstDir.GetID())
if err != nil {
return err
}
data := base.Json{
"parentReference": base.Json{
"id": dst.Id,
},
"name": srcObj.GetName(),
}
url := d.GetMetaUrl(false, srcObj.GetID())
_, err = d.Request(url, http.MethodPatch, func(req *resty.Request) {
req.SetBody(data)
}, nil)
return err
}
func (d *Onedrive) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
dstDir, err := d.GetFile(stdpath.Dir(srcObj.GetID()))
if err != nil {
return err
}
data := base.Json{
"parentReference": base.Json{
"id": dstDir.Id,
},
"name": newName,
}
url := d.GetMetaUrl(false, srcObj.GetID())
_, err = d.Request(url, http.MethodPatch, func(req *resty.Request) {
req.SetBody(data)
}, nil)
return err
}
func (d *Onedrive) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
dst, err := d.GetFile(dstDir.GetID())
if err != nil {
return err
}
data := base.Json{
"parentReference": base.Json{
"driveId": dst.ParentReference.DriveId,
"id": dst.Id,
},
"name": srcObj.GetName(),
}
url := d.GetMetaUrl(false, srcObj.GetID()) + "/copy"
_, err = d.Request(url, http.MethodPost, func(req *resty.Request) {
req.SetBody(data)
}, nil)
return err
}
func (d *Onedrive) Remove(ctx context.Context, obj model.Obj) error {
url := d.GetMetaUrl(false, obj.GetID())
_, err := d.Request(url, http.MethodDelete, nil, nil)
return err
}
func (d *Onedrive) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
var err error
if stream.GetSize() <= 4*1024*1024 {
err = d.upSmall(dstDir, stream)
} else {
err = d.upBig(ctx, dstDir, stream, up)
}
return err
}
func (d *Onedrive) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
return nil, errs.NotSupport
}
var _ driver.Driver = (*Onedrive)(nil)

31
drivers/onedrive/meta.go Normal file
View File

@ -0,0 +1,31 @@
package onedrive
import (
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/operations"
)
type Addition struct {
driver.RootFolderPath
Region string `json:"region" type:"select" required:"true" options:"global,cn,us,de"`
IsSharepoint bool `json:"is_sharepoint"`
ClientId string `json:"client_id" required:"true"`
ClientSecret string `json:"client_secret" required:"true"`
RedirectUri string `json:"redirect_uri" required:"true" default:"https://tool.nn.ci/onedrive/callback"`
RefreshToken string `json:"refresh_token" required:"true"`
SiteId string `json:"site_id"`
}
var config = driver.Config{
Name: "onedrive",
LocalSort: true,
DefaultRoot: "/",
}
func New() driver.Driver {
return &Onedrive{}
}
func init() {
operations.RegisterDriver(config, New)
}

48
drivers/onedrive/types.go Normal file
View File

@ -0,0 +1,48 @@
package onedrive
import (
"time"
"github.com/alist-org/alist/v3/internal/model"
)
type File struct {
Id string `json:"id"`
Name string `json:"name"`
Size int64 `json:"size"`
LastModifiedDateTime time.Time `json:"lastModifiedDateTime"`
Url string `json:"@microsoft.graph.downloadUrl"`
File *struct {
MimeType string `json:"mimeType"`
} `json:"file"`
Thumbnails []struct {
Medium struct {
Url string `json:"url"`
} `json:"medium"`
} `json:"thumbnails"`
ParentReference struct {
DriveId string `json:"driveId"`
} `json:"parentReference"`
}
func fileToObj(f File) *model.ObjectThumbnail {
thumb := ""
if len(f.Thumbnails) > 0 {
thumb = f.Thumbnails[0].Medium.Url
}
return &model.ObjectThumbnail{
Object: model.Object{
//ID: f.Id,
Name: f.Name,
Size: f.Size,
Modified: f.LastModifiedDateTime,
IsFolder: f.File == nil,
},
Thumbnail: model.Thumbnail{Thumbnail: thumb},
}
}
type Files struct {
Value []File `json:"value"`
NextLink string `json:"@odata.nextLink"`
}

217
drivers/onedrive/util.go Normal file
View File

@ -0,0 +1,217 @@
package onedrive
import (
"bytes"
"context"
"fmt"
"io"
"net/http"
stdpath "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/internal/operations"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/go-resty/resty/v2"
jsoniter "github.com/json-iterator/go"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
)
type Host struct {
Oauth string
Api string
}
var onedriveHostMap = map[string]Host{
"global": {
Oauth: "https://login.microsoftonline.com",
Api: "https://graph.microsoft.com",
},
"cn": {
Oauth: "https://login.chinacloudapi.cn",
Api: "https://microsoftgraph.chinacloudapi.cn",
},
"us": {
Oauth: "https://login.microsoftonline.us",
Api: "https://graph.microsoft.us",
},
"de": {
Oauth: "https://login.microsoftonline.de",
Api: "https://graph.microsoft.de",
},
}
func (d *Onedrive) GetMetaUrl(auth bool, path string) string {
host, _ := onedriveHostMap[d.Region]
if auth {
return host.Oauth
}
if d.IsSharepoint {
if path == "/" || path == "\\" {
return fmt.Sprintf("%s/v1.0/sites/%s/drive/root", host.Api, d.SiteId)
} else {
return fmt.Sprintf("%s/v1.0/sites/%s/drive/root:%s:", host.Api, d.SiteId, path)
}
} else {
if path == "/" || path == "\\" {
return fmt.Sprintf("%s/v1.0/me/drive/root", host.Api)
} else {
return fmt.Sprintf("%s/v1.0/me/drive/root:%s:", host.Api, path)
}
}
}
func (d *Onedrive) refreshToken() error {
var err error
for i := 0; i < 3; i++ {
err = d._refreshToken()
if err == nil {
break
}
}
return err
}
type TokenErr struct {
Error string `json:"error"`
ErrorDescription string `json:"error_description"`
}
func (d *Onedrive) _refreshToken() error {
url := d.GetMetaUrl(true, "") + "/common/oauth2/v2.0/token"
var resp base.TokenResp
var e TokenErr
_, err := base.RestyClient.R().SetResult(&resp).SetError(&e).SetFormData(map[string]string{
"grant_type": "refresh_token",
"client_id": d.ClientId,
"client_secret": d.ClientSecret,
"redirect_uri": d.RedirectUri,
"refresh_token": d.RefreshToken,
}).Post(url)
if err != nil {
return err
}
if e.Error != "" {
return fmt.Errorf("%s", e.ErrorDescription)
}
if resp.RefreshToken == "" {
return errs.EmptyToken
}
d.RefreshToken, d.AccessToken = resp.RefreshToken, resp.AccessToken
operations.MustSaveDriverStorage(d)
return nil
}
type RespErr struct {
Error struct {
Code string `json:"code"`
Message string `json:"message"`
} `json:"error"`
}
func (d *Onedrive) Request(url string, method string, callback func(*resty.Request), resp interface{}) ([]byte, error) {
req := base.RestyClient.R()
req.SetHeader("Authorization", "Bearer "+d.AccessToken)
if callback != nil {
callback(req)
}
if resp != nil {
req.SetResult(resp)
}
var e RespErr
req.SetError(&e)
res, err := req.Execute(method, url)
if err != nil {
return nil, errors.WithStack(err)
}
if e.Error.Code != "" {
if e.Error.Code == "InvalidAuthenticationToken" {
err = d.refreshToken()
if err != nil {
return nil, err
}
return d.Request(url, method, callback, resp)
}
return nil, errors.New(e.Error.Message)
}
return res.Body(), nil
}
func (d *Onedrive) GetFiles(path string) ([]File, error) {
var res []File
nextLink := d.GetMetaUrl(false, path) + "/children?$expand=thumbnails"
for nextLink != "" {
var files Files
_, err := d.Request(nextLink, http.MethodGet, nil, &files)
if err != nil {
return nil, err
}
res = append(res, files.Value...)
nextLink = files.NextLink
}
return res, nil
}
func (d *Onedrive) GetFile(path string) (*File, error) {
var file File
u := d.GetMetaUrl(false, path)
_, err := d.Request(u, http.MethodGet, nil, &file)
return &file, err
}
func (d *Onedrive) upSmall(dstDir model.Obj, stream model.FileStreamer) error {
url := d.GetMetaUrl(false, stdpath.Join(dstDir.GetID(), stream.GetName())) + "/content"
data, err := io.ReadAll(stream)
if err != nil {
return err
}
_, err = d.Request(url, http.MethodPut, func(req *resty.Request) {
req.SetBody(data)
}, nil)
return err
}
func (d *Onedrive) upBig(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
url := d.GetMetaUrl(false, stdpath.Join(dstDir.GetID(), stream.GetName())) + "/createUploadSession"
res, err := d.Request(url, http.MethodPost, nil, nil)
if err != nil {
return err
}
uploadUrl := jsoniter.Get(res, "uploadUrl").ToString()
var finish int64 = 0
const DEFAULT = 4 * 1024 * 1024
for finish < stream.GetSize() {
if utils.IsCanceled(ctx) {
return ctx.Err()
}
log.Debugf("upload: %d", finish)
var byteSize int64 = DEFAULT
left := stream.GetSize() - finish
if left < DEFAULT {
byteSize = left
}
byteData := make([]byte, byteSize)
n, err := io.ReadFull(stream, byteData)
log.Debug(err, n)
if err != nil {
return err
}
req, err := http.NewRequest("PUT", uploadUrl, bytes.NewBuffer(byteData))
req.Header.Set("Content-Length", strconv.Itoa(int(byteSize)))
req.Header.Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", finish, finish+byteSize-1, stream.GetSize()))
finish += byteSize
res, err := base.HttpClient.Do(req)
if res.StatusCode != 201 && res.StatusCode != 202 {
data, _ := io.ReadAll(res.Body)
res.Body.Close()
return errors.New(string(data))
}
res.Body.Close()
up(int(finish / stream.GetSize()))
}
return nil
}

View File

@ -35,10 +35,6 @@ func (d *Virtual) Drop(ctx context.Context) error {
return nil
}
func (d *Virtual) GetStorage() model.Storage {
return d.Storage
}
func (d *Virtual) GetAddition() driver.Additional {
return d.Addition
}

View File

@ -14,7 +14,7 @@ type Addition struct {
}
var config = driver.Config{
Name: "Virtual",
Name: "virtual",
OnlyLocal: true,
LocalSort: true,
NeedMs: true,