diff --git a/README.md b/README.md index 6b8ca60e..2e9fb207 100755 --- a/README.md +++ b/README.md @@ -71,6 +71,7 @@ English | [中文](./README_cn.md) | [Contributing](./CONTRIBUTING.md) | [CODE_O - [x] [Baidu photo](https://photo.baidu.com/) - [x] SMB - [x] [115](https://115.com/) + - [X] Cloudreve - [x] Easy to deploy and out-of-the-box - [x] File preview (PDF, markdown, code, plain text, ...) - [x] Image preview in gallery mode diff --git a/README_cn.md b/README_cn.md index 5038a27d..f44c4cd8 100644 --- a/README_cn.md +++ b/README_cn.md @@ -69,6 +69,7 @@ - [x] [一刻相册](https://photo.baidu.com/) - [x] SMB - [x] [115](https://115.com/) + - [X] Cloudreve - [x] 部署方便,开箱即用 - [x] 文件预览(PDF、markdown、代码、纯文本……) - [x] 画廊模式下的图像预览 diff --git a/drivers/all.go b/drivers/all.go index ebe67f04..a141d466 100644 --- a/drivers/all.go +++ b/drivers/all.go @@ -12,6 +12,7 @@ import ( _ "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" + _ "github.com/alist-org/alist/v3/drivers/cloudreve" _ "github.com/alist-org/alist/v3/drivers/ftp" _ "github.com/alist-org/alist/v3/drivers/google_drive" _ "github.com/alist-org/alist/v3/drivers/google_photo" diff --git a/drivers/cloudreve/driver.go b/drivers/cloudreve/driver.go new file mode 100644 index 00000000..d0a02f03 --- /dev/null +++ b/drivers/cloudreve/driver.go @@ -0,0 +1,184 @@ +package cloudreve + +import ( + "context" + "github.com/alist-org/alist/v3/drivers/base" + "github.com/alist-org/alist/v3/pkg/utils" + "github.com/go-resty/resty/v2" + "io" + "net/http" + "strconv" + + "github.com/alist-org/alist/v3/internal/driver" + "github.com/alist-org/alist/v3/internal/model" +) + +type Cloudreve struct { + model.Storage + Addition + Cookie string +} + +func (d *Cloudreve) Config() driver.Config { + return config +} + +func (d *Cloudreve) GetAddition() driver.Additional { + return &d.Addition +} + +func (d *Cloudreve) Init(ctx context.Context) error { + return d.login() +} + +func (d *Cloudreve) Drop(ctx context.Context) error { + d.Cookie = "" + return nil +} + +func (d *Cloudreve) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) { + var r DirectoryResp + err := d.request(http.MethodGet, "/directory"+dir.GetPath(), nil, &r) + if err != nil { + return nil, err + } + + return utils.SliceConvert(r.Objects, func(src Object) (model.Obj, error) { + return objectToObj(src), nil + }) +} + +func (d *Cloudreve) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) { + var dUrl string + err := d.request(http.MethodPut, "/file/download/"+file.GetID(), nil, &dUrl) + if err != nil { + return nil, err + } + return &model.Link{ + URL: dUrl, + }, nil +} + +func (d *Cloudreve) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error { + return d.request(http.MethodPut, "/directory", func(req *resty.Request) { + req.SetBody(base.Json{ + "path": parentDir.GetPath() + "/" + dirName, + }) + }, nil) +} + +func (d *Cloudreve) Move(ctx context.Context, srcObj, dstDir model.Obj) error { + body := base.Json{ + "action": "move", + "src_dir": srcObj.GetPath(), + "dst": dstDir.GetPath(), + "src": convertSrc(srcObj), + } + return d.request(http.MethodPatch, "/object", func(req *resty.Request) { + req.SetBody(body) + }, nil) +} + +func (d *Cloudreve) Rename(ctx context.Context, srcObj model.Obj, newName string) error { + body := base.Json{ + "action": "rename", + "new_name": newName, + "src": convertSrc(srcObj), + } + return d.request(http.MethodPatch, "/object/rename", func(req *resty.Request) { + req.SetBody(body) + }, nil) +} + +func (d *Cloudreve) Copy(ctx context.Context, srcObj, dstDir model.Obj) error { + body := base.Json{ + "src_dir": srcObj.GetPath(), + "dst": dstDir.GetPath(), + "src": convertSrc(srcObj), + } + return d.request(http.MethodPost, "/object/copy", func(req *resty.Request) { + req.SetBody(body) + }, nil) +} + +func (d *Cloudreve) Remove(ctx context.Context, obj model.Obj) error { + body := convertSrc(obj) + err := d.request(http.MethodDelete, "/object", func(req *resty.Request) { + req.SetBody(body) + }, nil) + return err +} + +func (d *Cloudreve) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error { + if stream.GetReadCloser() == http.NoBody { + return d.create(ctx, dstDir, stream) + } + var r DirectoryResp + err := d.request(http.MethodGet, "/directory"+dstDir.GetPath(), nil, &r) + if err != nil { + return err + } + uploadBody := base.Json{ + "path": dstDir.GetPath(), + "size": stream.GetSize(), + "name": stream.GetName(), + "policy_id": r.Policy.Id, + "last_modified": stream.ModTime().Unix(), + } + var u UploadInfo + err = d.request(http.MethodPut, "/file/upload", func(req *resty.Request) { + req.SetBody(uploadBody) + }, &u) + if err != nil { + return err + } + var chunkSize = u.ChunkSize + var buf []byte + var chunk int + for { + var n int + buf = make([]byte, chunkSize) + n, err = io.ReadAtLeast(stream, buf, chunkSize) + if err != nil && err != io.ErrUnexpectedEOF { + if err == io.EOF { + return nil + } + return err + } + + if n == 0 { + break + } + buf = buf[:n] + err = d.request(http.MethodPost, "/file/upload/"+u.SessionID+"/"+strconv.Itoa(chunk), func(req *resty.Request) { + req.SetHeader("Content-Type", "application/octet-stream") + req.SetHeader("Content-Length", strconv.Itoa(n)) + req.SetBody(buf) + }, nil) + if err != nil { + break + } + chunk++ + + } + return err +} + +func (d *Cloudreve) create(ctx context.Context, dir model.Obj, file model.Obj) error { + body := base.Json{"path": dir.GetPath() + "/" + file.GetName()} + if file.IsDir() { + err := d.request(http.MethodPut, "directory", func(req *resty.Request) { + req.SetBody(body) + }, nil) + return err + } + return d.request(http.MethodPost, "/file/create", func(req *resty.Request) { + req.SetBody(body) + }, nil) +} + +//func (d *Cloudreve) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) { +// return nil, errs.NotSupport +//} + +var _ driver.Driver = (*Cloudreve)(nil) diff --git a/drivers/cloudreve/meta.go b/drivers/cloudreve/meta.go new file mode 100644 index 00000000..f87c4c26 --- /dev/null +++ b/drivers/cloudreve/meta.go @@ -0,0 +1,26 @@ +package cloudreve + +import ( + "github.com/alist-org/alist/v3/internal/driver" + "github.com/alist-org/alist/v3/internal/op" +) + +type Addition struct { + // Usually one of two + driver.RootPath + // define other + Address string `json:"address" required:"true"` + Username string `json:"username" required:"true"` + Password string `json:"password" required:"true"` +} + +var config = driver.Config{ + Name: "Cloudreve", + DefaultRoot: "/", +} + +func init() { + op.RegisterDriver(func() driver.Driver { + return &Cloudreve{} + }) +} diff --git a/drivers/cloudreve/types.go b/drivers/cloudreve/types.go new file mode 100644 index 00000000..a1ce39db --- /dev/null +++ b/drivers/cloudreve/types.go @@ -0,0 +1,54 @@ +package cloudreve + +import ( + "github.com/alist-org/alist/v3/internal/model" + "time" +) + +type Resp struct { + Code int `json:"code"` + Msg string `json:"msg"` + Data interface{} `json:"data"` +} + +type Policy struct { + Id string `json:"id"` + Name string `json:"name"` + Type string `json:"type"` + MaxSize int `json:"max_size"` + FileType []string `json:"file_type"` +} + +type UploadInfo struct { + SessionID string `json:"sessionID"` + ChunkSize int `json:"chunkSize"` + Expires int `json:"expires"` +} + +type DirectoryResp struct { + Parent string `json:"parent"` + Objects []Object `json:"objects"` + Policy Policy `json:"policy"` +} + +type Object struct { + Id string `json:"id"` + Name string `json:"name"` + Path string `json:"path"` + Pic string `json:"pic"` + Size int `json:"size"` + Type string `json:"type"` + Date time.Time `json:"date"` + CreateDate time.Time `json:"create_date"` + SourceEnabled bool `json:"source_enabled"` +} + +func objectToObj(f Object) *model.Object { + return &model.Object{ + ID: f.Id, + Name: f.Name, + Size: int64(f.Size), + Modified: f.Date, + IsFolder: f.Type == "dir", + } +} diff --git a/drivers/cloudreve/util.go b/drivers/cloudreve/util.go new file mode 100644 index 00000000..ab7499ea --- /dev/null +++ b/drivers/cloudreve/util.go @@ -0,0 +1,96 @@ +package cloudreve + +import ( + "errors" + "github.com/alist-org/alist/v3/drivers/base" + "github.com/alist-org/alist/v3/internal/model" + "github.com/alist-org/alist/v3/pkg/cookie" + "github.com/go-resty/resty/v2" + json "github.com/json-iterator/go" + "net/http" +) + +// do others that not defined in Driver interface + +const loginPath = "/user/session" + +func (d *Cloudreve) request(method string, path string, callback base.ReqCallback, out interface{}) error { + u := d.Address + "/api/v3" + path + req := base.RestyClient.R() + req.SetHeaders(map[string]string{ + "Cookie": "cloudreve-session=" + d.Cookie, + "Accept": "application/json, text/plain, */*", + "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36", + }) + + var r Resp + + req.SetResult(&r) + + if callback != nil { + callback(req) + } + + resp, err := req.Execute(method, u) + if err != nil { + return err + } + if !resp.IsSuccess() { + return errors.New(resp.String()) + } + + if r.Code != 0 { + + // 刷新 cookie + if r.Code == http.StatusUnauthorized && path != loginPath { + err = d.login() + if err != nil { + return err + } + return d.request(method, path, callback, out) + } + + return errors.New(r.Msg) + } + sess := cookie.GetCookie(resp.Cookies(), "cloudreve-session") + if sess != nil { + d.Cookie = sess.Value + } + if out != nil && r.Data != nil { + var marshal []byte + marshal, err = json.Marshal(r.Data) + if err != nil { + return err + } + err = json.Unmarshal(marshal, out) + if err != nil { + return err + } + } + + return nil +} + +func (d *Cloudreve) login() error { + return d.request(http.MethodPost, loginPath, func(req *resty.Request) { + req.SetBody(base.Json{ + "username": d.Addition.Username, + "Password": d.Addition.Password, + "captchaCode": "", + }) + }, nil) +} + +func convertSrc(obj model.Obj) map[string]interface{} { + m := make(map[string]interface{}) + var dirs []string + var items []string + if obj.IsDir() { + dirs = append(dirs, obj.GetID()) + } else { + items = append(items, obj.GetID()) + } + m["dirs"] = dirs + m["items"] = items + return m +}