diff --git a/README.md b/README.md index b06d2644..61237af7 100755 --- a/README.md +++ b/README.md @@ -53,6 +53,7 @@ English | [中文](./README_cn.md) | [Contributing](./CONTRIBUTING.md) | [CODE_O - [x] FTP / SFTP - [x] [PikPak](https://www.mypikpak.com/) - [x] [S3](https://aws.amazon.com/s3/) + - [x] [Seafile](https://seafile.com/) - [x] [UPYUN Storage Service](https://www.upyun.com/products/file-storage) - [x] WebDav(Support OneDrive/SharePoint without API) - [x] Teambition([China](https://www.teambition.com/ ),[International](https://us.teambition.com/ )) diff --git a/drivers/all.go b/drivers/all.go index bde2fe9c..ebe67f04 100644 --- a/drivers/all.go +++ b/drivers/all.go @@ -24,6 +24,7 @@ import ( _ "github.com/alist-org/alist/v3/drivers/pikpak_share" _ "github.com/alist-org/alist/v3/drivers/quark" _ "github.com/alist-org/alist/v3/drivers/s3" + _ "github.com/alist-org/alist/v3/drivers/seafile" _ "github.com/alist-org/alist/v3/drivers/sftp" _ "github.com/alist-org/alist/v3/drivers/smb" _ "github.com/alist-org/alist/v3/drivers/teambition" diff --git a/drivers/seafile/driver.go b/drivers/seafile/driver.go new file mode 100644 index 00000000..49cf3386 --- /dev/null +++ b/drivers/seafile/driver.go @@ -0,0 +1,160 @@ +package seafile + +import ( + "context" + "fmt" + "net/http" + "path/filepath" + "strings" + "time" + + "github.com/alist-org/alist/v3/internal/driver" + "github.com/alist-org/alist/v3/internal/model" + "github.com/alist-org/alist/v3/pkg/utils" + "github.com/go-resty/resty/v2" +) + +type Seafile struct { + model.Storage + Addition + + authorization string +} + +func (d *Seafile) Config() driver.Config { + return config +} + +func (d *Seafile) GetAddition() driver.Additional { + return &d.Addition +} + +func (d *Seafile) Init(ctx context.Context) error { + d.Address = strings.TrimSuffix(d.Address, "/") + return d.getToken() +} + +func (d *Seafile) Drop(ctx context.Context) error { + return nil +} + +func (d *Seafile) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) { + path := dir.GetPath() + var resp []RepoDirItemResp + _, err := d.request(http.MethodGet, fmt.Sprintf("/api2/repos/%s/dir/", d.Addition.RepoId), func(req *resty.Request) { + req.SetResult(&resp).SetQueryParams(map[string]string{ + "p": path, + }) + }) + if err != nil { + return nil, err + } + return utils.SliceConvert(resp, func(f RepoDirItemResp) (model.Obj, error) { + return &model.ObjThumb{ + Object: model.Object{ + Name: f.Name, + Modified: time.Unix(f.Modified, 0), + Size: f.Size, + IsFolder: f.Type == "dir", + }, + // Thumbnail: model.Thumbnail{Thumbnail: f.Thumb}, + }, nil + }) +} + +func (d *Seafile) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) { + res, err := d.request(http.MethodGet, fmt.Sprintf("/api2/repos/%s/file/", d.Addition.RepoId), func(req *resty.Request) { + req.SetQueryParams(map[string]string{ + "p": file.GetPath(), + "reuse": "1", + }) + }) + if err != nil { + return nil, err + } + u := string(res) + u = u[1 : len(u)-1] // remove quotes + return &model.Link{URL: u}, nil +} + +func (d *Seafile) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error { + _, err := d.request(http.MethodPost, fmt.Sprintf("/api2/repos/%s/dir/", d.Addition.RepoId), func(req *resty.Request) { + req.SetQueryParams(map[string]string{ + "p": filepath.Join(parentDir.GetPath(), dirName), + }).SetFormData(map[string]string{ + "operation": "mkdir", + }) + }) + return err +} + +func (d *Seafile) Move(ctx context.Context, srcObj, dstDir model.Obj) error { + _, err := d.request(http.MethodPost, fmt.Sprintf("/api2/repos/%s/file/", d.Addition.RepoId), func(req *resty.Request) { + req.SetQueryParams(map[string]string{ + "p": srcObj.GetPath(), + }).SetFormData(map[string]string{ + "operation": "move", + "dst_repo": d.Addition.RepoId, + "dst_dir": dstDir.GetPath(), + }) + }, true) + return err +} + +func (d *Seafile) Rename(ctx context.Context, srcObj model.Obj, newName string) error { + _, err := d.request(http.MethodPost, fmt.Sprintf("/api2/repos/%s/file/", d.Addition.RepoId), func(req *resty.Request) { + req.SetQueryParams(map[string]string{ + "p": srcObj.GetPath(), + }).SetFormData(map[string]string{ + "operation": "rename", + "newname": newName, + }) + }, true) + return err +} + +func (d *Seafile) Copy(ctx context.Context, srcObj, dstDir model.Obj) error { + _, err := d.request(http.MethodPost, fmt.Sprintf("/api2/repos/%s/file/", d.Addition.RepoId), func(req *resty.Request) { + req.SetQueryParams(map[string]string{ + "p": srcObj.GetPath(), + }).SetFormData(map[string]string{ + "operation": "copy", + "dst_repo": d.Addition.RepoId, + "dst_dir": dstDir.GetPath(), + }) + }) + return err +} + +func (d *Seafile) Remove(ctx context.Context, obj model.Obj) error { + _, err := d.request(http.MethodDelete, fmt.Sprintf("/api2/repos/%s/file/", d.Addition.RepoId), func(req *resty.Request) { + req.SetQueryParams(map[string]string{ + "p": obj.GetPath(), + }) + }) + return err +} + +func (d *Seafile) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error { + res, err := d.request(http.MethodGet, fmt.Sprintf("/api2/repos/%s/upload-link/", d.Addition.RepoId), func(req *resty.Request) { + req.SetQueryParams(map[string]string{ + "p": dstDir.GetPath(), + }) + }) + if err != nil { + return err + } + + u := string(res) + u = u[1 : len(u)-1] // remove quotes + _, err = d.request(http.MethodPost, u, func(req *resty.Request) { + req.SetFileReader("file", stream.GetName(), stream). + SetFormData(map[string]string{ + "parent_dir": dstDir.GetPath(), + "replace": "1", + }) + }) + return err +} + +var _ driver.Driver = (*Seafile)(nil) diff --git a/drivers/seafile/meta.go b/drivers/seafile/meta.go new file mode 100644 index 00000000..e333fd90 --- /dev/null +++ b/drivers/seafile/meta.go @@ -0,0 +1,26 @@ +package seafile + +import ( + "github.com/alist-org/alist/v3/internal/driver" + "github.com/alist-org/alist/v3/internal/op" +) + +type Addition struct { + driver.RootPath + + Address string `json:"address" required:"true"` + UserName string `json:"username" required:"true"` + Password string `json:"password" required:"true"` + RepoId string `json:"repoId" required:"true"` +} + +var config = driver.Config{ + Name: "Seafile", + DefaultRoot: "/", +} + +func init() { + op.RegisterDriver(func() driver.Driver { + return &Seafile{} + }) +} diff --git a/drivers/seafile/types.go b/drivers/seafile/types.go new file mode 100644 index 00000000..5c5b528d --- /dev/null +++ b/drivers/seafile/types.go @@ -0,0 +1,14 @@ +package seafile + +type AuthTokenResp struct { + Token string `json:"token"` +} + +type RepoDirItemResp struct { + Id string `json:"id"` + Type string `json:"type"` // dir, file + Name string `json:"name"` + Size int64 `json:"size"` + Modified int64 `json:"mtime"` + Permission string `json:"permission"` +} diff --git a/drivers/seafile/util.go b/drivers/seafile/util.go new file mode 100644 index 00000000..7714cd0a --- /dev/null +++ b/drivers/seafile/util.go @@ -0,0 +1,48 @@ +package seafile + +import ( + "fmt" + "strings" + + "github.com/alist-org/alist/v3/drivers/base" +) + +func (d *Seafile) getToken() error { + var authResp AuthTokenResp + res, err := base.RestyClient.R(). + SetResult(&authResp). + SetFormData(map[string]string{ + "username": d.UserName, + "password": d.Password, + }). + Post(d.Address + "/api2/auth-token/") + if err != nil { + return err + } + if res.StatusCode() >= 400 { + return fmt.Errorf("get token failed: %s", res.String()) + } + d.authorization = fmt.Sprintf("Token %s", authResp.Token) + return nil +} + +func (d *Seafile) request(method string, pathname string, callback base.ReqCallback, noRedirect ...bool) ([]byte, error) { + full := pathname + if !strings.HasPrefix(pathname, "http") { + full = d.Address + pathname + } + req := base.RestyClient.R() + if len(noRedirect) > 0 && noRedirect[0] { + req = base.NoRedirectClient.R() + } + req.SetHeader("Authorization", d.authorization) + callback(req) + res, err := req.Execute(method, full) + if err != nil { + return nil, err + } + if res.StatusCode() >= 400 { + return nil, fmt.Errorf("request failed: %s", res.String()) + } + return res.Body(), nil +}