mirror of https://github.com/Xhofe/alist
feat: add `Seafile` driver (#2964)
* feat: add Seafile driver * docs: add Seafile support * refactor: optimization * fix: close redirect on `move` and `rename` Co-authored-by: Noah Hsu <i@nn.ci>pull/2997/head
parent
0ad9e17196
commit
48e6f3bb23
|
@ -53,6 +53,7 @@ English | [中文](./README_cn.md) | [Contributing](./CONTRIBUTING.md) | [CODE_O
|
||||||
- [x] FTP / SFTP
|
- [x] FTP / SFTP
|
||||||
- [x] [PikPak](https://www.mypikpak.com/)
|
- [x] [PikPak](https://www.mypikpak.com/)
|
||||||
- [x] [S3](https://aws.amazon.com/s3/)
|
- [x] [S3](https://aws.amazon.com/s3/)
|
||||||
|
- [x] [Seafile](https://seafile.com/)
|
||||||
- [x] [UPYUN Storage Service](https://www.upyun.com/products/file-storage)
|
- [x] [UPYUN Storage Service](https://www.upyun.com/products/file-storage)
|
||||||
- [x] WebDav(Support OneDrive/SharePoint without API)
|
- [x] WebDav(Support OneDrive/SharePoint without API)
|
||||||
- [x] Teambition([China](https://www.teambition.com/ ),[International](https://us.teambition.com/ ))
|
- [x] Teambition([China](https://www.teambition.com/ ),[International](https://us.teambition.com/ ))
|
||||||
|
|
|
@ -24,6 +24,7 @@ import (
|
||||||
_ "github.com/alist-org/alist/v3/drivers/pikpak_share"
|
_ "github.com/alist-org/alist/v3/drivers/pikpak_share"
|
||||||
_ "github.com/alist-org/alist/v3/drivers/quark"
|
_ "github.com/alist-org/alist/v3/drivers/quark"
|
||||||
_ "github.com/alist-org/alist/v3/drivers/s3"
|
_ "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/sftp"
|
||||||
_ "github.com/alist-org/alist/v3/drivers/smb"
|
_ "github.com/alist-org/alist/v3/drivers/smb"
|
||||||
_ "github.com/alist-org/alist/v3/drivers/teambition"
|
_ "github.com/alist-org/alist/v3/drivers/teambition"
|
||||||
|
|
|
@ -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)
|
|
@ -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{}
|
||||||
|
})
|
||||||
|
}
|
|
@ -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"`
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
Loading…
Reference in New Issue