diff --git a/drivers/all.go b/drivers/all.go index eb5e4720..9304853a 100644 --- a/drivers/all.go +++ b/drivers/all.go @@ -24,6 +24,7 @@ import ( _ "github.com/alist-org/alist/v3/drivers/mediatrack" _ "github.com/alist-org/alist/v3/drivers/mega" _ "github.com/alist-org/alist/v3/drivers/onedrive" + _ "github.com/alist-org/alist/v3/drivers/onedrive_app" _ "github.com/alist-org/alist/v3/drivers/pikpak" _ "github.com/alist-org/alist/v3/drivers/pikpak_share" _ "github.com/alist-org/alist/v3/drivers/quark" diff --git a/drivers/local/driver.go b/drivers/local/driver.go index e3943ee0..3535df94 100644 --- a/drivers/local/driver.go +++ b/drivers/local/driver.go @@ -6,7 +6,6 @@ import ( "errors" "fmt" "io" - "io/ioutil" "net/http" "os" stdpath "path" @@ -68,7 +67,7 @@ func (d *Local) GetAddition() driver.Additional { func (d *Local) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) { fullPath := dir.GetPath() - rawFiles, err := ioutil.ReadDir(fullPath) + rawFiles, err := readDir(fullPath) if err != nil { return nil, err } diff --git a/drivers/local/util.go b/drivers/local/util.go index 1d0eb8da..d782783d 100644 --- a/drivers/local/util.go +++ b/drivers/local/util.go @@ -3,10 +3,12 @@ package local import ( "bytes" "fmt" - ffmpeg "github.com/u2takey/ffmpeg-go" "io/fs" "os" "path/filepath" + "sort" + + ffmpeg "github.com/u2takey/ffmpeg-go" ) func isSymlinkDir(f fs.FileInfo, path string) bool { @@ -39,3 +41,17 @@ func GetSnapshot(videoPath string, frameNum int) (imgData *bytes.Buffer, err err } return srcBuf, nil } + +func readDir(dirname string) ([]fs.FileInfo, error) { + f, err := os.Open(dirname) + if err != nil { + return nil, err + } + list, err := f.Readdir(-1) + f.Close() + if err != nil { + return nil, err + } + sort.Slice(list, func(i, j int) bool { return list[i].Name() < list[j].Name() }) + return list, nil +} diff --git a/drivers/onedrive_app/driver.go b/drivers/onedrive_app/driver.go new file mode 100644 index 00000000..ac6f232e --- /dev/null +++ b/drivers/onedrive_app/driver.go @@ -0,0 +1,160 @@ +package onedrive_app + +import ( + "context" + "fmt" + "net/http" + "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" +) + +type OnedriveAPP struct { + model.Storage + Addition + AccessToken string +} + +func (d *OnedriveAPP) Config() driver.Config { + return config +} + +func (d *OnedriveAPP) GetAddition() driver.Additional { + return &d.Addition +} + +func (d *OnedriveAPP) Init(ctx context.Context) error { + if d.ChunkSize < 1 { + d.ChunkSize = 5 + } + return d.accessToken() +} + +func (d *OnedriveAPP) Drop(ctx context.Context) error { + return nil +} + +func (d *OnedriveAPP) 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, dir.GetID()), nil + }) +} + +func (d *OnedriveAPP) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) { + f, err := d.GetFile(file.GetPath()) + if err != nil { + return nil, err + } + if f.File == nil { + return nil, errs.NotFile + } + return &model.Link{ + URL: f.Url, + }, nil +} + +func (d *OnedriveAPP) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error { + url := d.GetMetaUrl(false, parentDir.GetPath()) + "/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 *OnedriveAPP) Move(ctx context.Context, srcObj, dstDir model.Obj) error { + parentPath := "" + if dstDir.GetID() == "" { + parentPath = dstDir.GetPath() + if utils.PathEqual(parentPath, "/") { + parentPath = path.Join("/drive/root", parentPath) + } else { + parentPath = path.Join("/drive/root:/", parentPath) + } + } + data := base.Json{ + "parentReference": base.Json{ + "id": dstDir.GetID(), + "path": parentPath, + }, + "name": srcObj.GetName(), + } + url := d.GetMetaUrl(false, srcObj.GetPath()) + _, err := d.Request(url, http.MethodPatch, func(req *resty.Request) { + req.SetBody(data) + }, nil) + return err +} + +func (d *OnedriveAPP) Rename(ctx context.Context, srcObj model.Obj, newName string) error { + var parentID string + if o, ok := srcObj.(*Object); ok { + parentID = o.ParentID + } else { + return fmt.Errorf("srcObj is not Object") + } + if parentID == "" { + parentID = "root" + } + data := base.Json{ + "parentReference": base.Json{ + "id": parentID, + }, + "name": newName, + } + url := d.GetMetaUrl(false, srcObj.GetPath()) + _, err := d.Request(url, http.MethodPatch, func(req *resty.Request) { + req.SetBody(data) + }, nil) + return err +} + +func (d *OnedriveAPP) Copy(ctx context.Context, srcObj, dstDir model.Obj) error { + dst, err := d.GetFile(dstDir.GetPath()) + 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.GetPath()) + "/copy" + _, err = d.Request(url, http.MethodPost, func(req *resty.Request) { + req.SetBody(data) + }, nil) + return err +} + +func (d *OnedriveAPP) Remove(ctx context.Context, obj model.Obj) error { + url := d.GetMetaUrl(false, obj.GetPath()) + _, err := d.Request(url, http.MethodDelete, nil, nil) + return err +} + +func (d *OnedriveAPP) 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(ctx, dstDir, stream) + } else { + err = d.upBig(ctx, dstDir, stream, up) + } + return err +} + +var _ driver.Driver = (*OnedriveAPP)(nil) diff --git a/drivers/onedrive_app/meta.go b/drivers/onedrive_app/meta.go new file mode 100644 index 00000000..21ae4f15 --- /dev/null +++ b/drivers/onedrive_app/meta.go @@ -0,0 +1,28 @@ +package onedrive_app + +import ( + "github.com/alist-org/alist/v3/internal/driver" + "github.com/alist-org/alist/v3/internal/op" +) + +type Addition struct { + driver.RootPath + Region string `json:"region" type:"select" required:"true" options:"global,cn,us,de" default:"global"` + ClientID string `json:"client_id" required:"true"` + ClientSecret string `json:"client_secret" required:"true"` + TenantID string `json:"tenant_id"` + Email string `json:"email"` + ChunkSize int64 `json:"chunk_size" type:"number" default:"5"` +} + +var config = driver.Config{ + Name: "OnedriveAPP", + LocalSort: true, + DefaultRoot: "/", +} + +func init() { + op.RegisterDriver(func() driver.Driver { + return &OnedriveAPP{} + }) +} diff --git a/drivers/onedrive_app/types.go b/drivers/onedrive_app/types.go new file mode 100644 index 00000000..7179e4b4 --- /dev/null +++ b/drivers/onedrive_app/types.go @@ -0,0 +1,74 @@ +package onedrive_app + +import ( + "time" + + "github.com/alist-org/alist/v3/internal/model" +) + +type Host struct { + Oauth string + Api string +} + +type TokenErr struct { + Error string `json:"error"` + ErrorDescription string `json:"error_description"` +} + +type RespErr struct { + Error struct { + Code string `json:"code"` + Message string `json:"message"` + } `json:"error"` +} + +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"` +} + +type Object struct { + model.ObjThumb + ParentID string +} + +func fileToObj(f File, parentID string) *Object { + thumb := "" + if len(f.Thumbnails) > 0 { + thumb = f.Thumbnails[0].Medium.Url + } + return &Object{ + ObjThumb: model.ObjThumb{ + Object: model.Object{ + ID: f.Id, + Name: f.Name, + Size: f.Size, + Modified: f.LastModifiedDateTime, + IsFolder: f.File == nil, + }, + Thumbnail: model.Thumbnail{Thumbnail: thumb}, + //Url: model.Url{Url: f.Url}, + }, + ParentID: parentID, + } +} + +type Files struct { + Value []File `json:"value"` + NextLink string `json:"@odata.nextLink"` +} diff --git a/drivers/onedrive_app/util.go b/drivers/onedrive_app/util.go new file mode 100644 index 00000000..1c70a9e2 --- /dev/null +++ b/drivers/onedrive_app/util.go @@ -0,0 +1,196 @@ +package onedrive_app + +import ( + "bytes" + "context" + "errors" + "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/op" + "github.com/alist-org/alist/v3/pkg/utils" + "github.com/go-resty/resty/v2" + jsoniter "github.com/json-iterator/go" + log "github.com/sirupsen/logrus" +) + +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 *OnedriveAPP) GetMetaUrl(auth bool, path string) string { + host, _ := onedriveHostMap[d.Region] + path = utils.EncodePath(path, true) + if auth { + return host.Oauth + } + if path == "/" || path == "\\" { + return fmt.Sprintf("%s/v1.0/users/%s/drive/root", host.Api, d.Email) + } + return fmt.Sprintf("%s/v1.0/users/%s/drive/root:%s:", host.Api, d.Email, path) +} + +func (d *OnedriveAPP) accessToken() error { + var err error + for i := 0; i < 3; i++ { + err = d._accessToken() + if err == nil { + break + } + } + return err +} + +func (d *OnedriveAPP) _accessToken() error { + url := d.GetMetaUrl(true, "") + "/" + d.TenantID + "/oauth2/token" + var resp base.TokenResp + var e TokenErr + _, err := base.RestyClient.R().SetResult(&resp).SetError(&e).SetFormData(map[string]string{ + "grant_type": "client_credentials", + "client_id": d.ClientID, + "client_secret": d.ClientSecret, + "resource": "https://graph.microsoft.com/", + "scope": "https://graph.microsoft.com/.default", + }).Post(url) + if err != nil { + return err + } + if e.Error != "" { + return fmt.Errorf("%s", e.ErrorDescription) + } + if resp.AccessToken == "" { + return errs.EmptyToken + } + d.AccessToken = resp.AccessToken + op.MustSaveDriverStorage(d) + return nil +} + +func (d *OnedriveAPP) Request(url string, method string, callback base.ReqCallback, 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, err + } + if e.Error.Code != "" { + if e.Error.Code == "InvalidAuthenticationToken" { + err = d.accessToken() + 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 *OnedriveAPP) getFiles(path string) ([]File, error) { + var res []File + nextLink := d.GetMetaUrl(false, path) + "/children?$top=5000&$expand=thumbnails($select=medium)&$select=id,name,size,lastModifiedDateTime,content.downloadUrl,file,parentReference" + 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 *OnedriveAPP) 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 *OnedriveAPP) upSmall(ctx context.Context, dstDir model.Obj, stream model.FileStreamer) error { + url := d.GetMetaUrl(false, stdpath.Join(dstDir.GetPath(), 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).SetContext(ctx) + }, nil) + return err +} + +func (d *OnedriveAPP) upBig(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error { + url := d.GetMetaUrl(false, stdpath.Join(dstDir.GetPath(), 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 + DEFAULT := d.ChunkSize * 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)) + if err != nil { + return err + } + req = req.WithContext(ctx) + 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 * 100 / stream.GetSize())) + } + return nil +} diff --git a/pkg/aria2/rpc/client.go b/pkg/aria2/rpc/client.go index 67c58dd6..041e9d4f 100644 --- a/pkg/aria2/rpc/client.go +++ b/pkg/aria2/rpc/client.go @@ -4,8 +4,8 @@ import ( "context" "encoding/base64" "errors" - "io/ioutil" "net/url" + "os" "time" ) @@ -89,7 +89,7 @@ func (c *client) AddURI(uris []string, options ...interface{}) (gid string, err // If a file with the same name already exists, it is overwritten! // If the file cannot be saved successfully or --rpc-save-upload-metadata is false, the downloads added by this method are not saved by --save-session. func (c *client) AddTorrent(filename string, options ...interface{}) (gid string, err error) { - co, err := ioutil.ReadFile(filename) + co, err := os.ReadFile(filename) if err != nil { return } @@ -120,7 +120,7 @@ func (c *client) AddTorrent(filename string, options ...interface{}) (gid string // If a file with the same name already exists, it is overwritten! // If the file cannot be saved successfully or --rpc-save-upload-metadata is false, the downloads added by this method are not saved by --save-session. func (c *client) AddMetalink(filename string, options ...interface{}) (gid []string, err error) { - co, err := ioutil.ReadFile(filename) + co, err := os.ReadFile(filename) if err != nil { return } diff --git a/server/common/proxy.go b/server/common/proxy.go index 0489e3fc..2453c3b8 100644 --- a/server/common/proxy.go +++ b/server/common/proxy.go @@ -3,7 +3,6 @@ package common import ( "fmt" "io" - "io/ioutil" "net/http" "net/url" "os" @@ -61,7 +60,8 @@ func Proxy(w http.ResponseWriter, r *http.Request, link *model.Link, file model. if err != nil { return err } - w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"; filename*=UTF-8''%s`, file.GetName(), url.QueryEscape(file.GetName()))) + filename := file.GetName() + w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"; filename*=UTF-8''%s`, filename, url.PathEscape(filename))) http.ServeContent(w, r, file.GetName(), fileStat.ModTime(), f) return nil } else { @@ -93,7 +93,7 @@ func Proxy(w http.ResponseWriter, r *http.Request, link *model.Link, file model. } w.WriteHeader(res.StatusCode) if res.StatusCode >= 400 { - all, _ := ioutil.ReadAll(res.Body) + all, _ := io.ReadAll(res.Body) msg := string(all) log.Debugln(msg) return errors.New(msg) diff --git a/server/handles/fsread.go b/server/handles/fsread.go index 6a171430..d393bd2f 100644 --- a/server/handles/fsread.go +++ b/server/handles/fsread.go @@ -302,6 +302,7 @@ func FsGet(c *gin.Context) { related = filterRelated(sameLevelFiles, obj) } parentMeta, _ := op.GetNearestMeta(parentPath) + thumb, _ := model.GetThumb(obj) common.SuccessResp(c, FsGetResp{ ObjResp: ObjResp{ Name: obj.GetName(), @@ -310,6 +311,7 @@ func FsGet(c *gin.Context) { Modified: obj.ModTime(), Sign: common.Sign(obj, parentPath, isEncrypt(meta, reqPath)), Type: utils.GetFileType(obj.GetName()), + Thumb: thumb, }, RawURL: rawURL, Readme: getReadme(meta, reqPath),