mirror of https://github.com/Xhofe/alist
feat(seafile): improve features, support access to encrypted library, etc (#6160)
parent
2a17d0c2cd
commit
ac68079a76
|
@ -4,7 +4,6 @@ import (
|
|||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
@ -19,6 +18,7 @@ type Seafile struct {
|
|||
Addition
|
||||
|
||||
authorization string
|
||||
libraryMap map[string]*LibraryInfo
|
||||
}
|
||||
|
||||
func (d *Seafile) Config() driver.Config {
|
||||
|
@ -31,6 +31,8 @@ func (d *Seafile) GetAddition() driver.Additional {
|
|||
|
||||
func (d *Seafile) Init(ctx context.Context) error {
|
||||
d.Address = strings.TrimSuffix(d.Address, "/")
|
||||
d.RootFolderPath = utils.FixAndCleanPath(d.RootFolderPath)
|
||||
d.libraryMap = make(map[string]*LibraryInfo)
|
||||
return d.getToken()
|
||||
}
|
||||
|
||||
|
@ -38,10 +40,37 @@ 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) {
|
||||
func (d *Seafile) List(ctx context.Context, dir model.Obj, args model.ListArgs) (result []model.Obj, err error) {
|
||||
path := dir.GetPath()
|
||||
if path == d.RootFolderPath {
|
||||
libraries, err := d.listLibraries()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if path == "/" && d.RepoId == "" {
|
||||
return utils.SliceConvert(libraries, func(f LibraryItemResp) (model.Obj, error) {
|
||||
return &model.Object{
|
||||
Name: f.Name,
|
||||
Modified: time.Unix(f.Modified, 0),
|
||||
Size: f.Size,
|
||||
IsFolder: true,
|
||||
}, nil
|
||||
})
|
||||
}
|
||||
}
|
||||
var repo *LibraryInfo
|
||||
repo, path, err = d.getRepoAndPath(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if repo.Encrypted {
|
||||
err = d.decryptLibrary(repo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
var resp []RepoDirItemResp
|
||||
_, err := d.request(http.MethodGet, fmt.Sprintf("/api2/repos/%s/dir/", d.Addition.RepoId), func(req *resty.Request) {
|
||||
_, err = d.request(http.MethodGet, fmt.Sprintf("/api2/repos/%s/dir/", repo.Id), func(req *resty.Request) {
|
||||
req.SetResult(&resp).SetQueryParams(map[string]string{
|
||||
"p": path,
|
||||
})
|
||||
|
@ -63,9 +92,13 @@ func (d *Seafile) List(ctx context.Context, dir model.Obj, args model.ListArgs)
|
|||
}
|
||||
|
||||
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) {
|
||||
repo, path, err := d.getRepoAndPath(file.GetPath())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res, err := d.request(http.MethodGet, fmt.Sprintf("/api2/repos/%s/file/", repo.Id), func(req *resty.Request) {
|
||||
req.SetQueryParams(map[string]string{
|
||||
"p": file.GetPath(),
|
||||
"p": path,
|
||||
"reuse": "1",
|
||||
})
|
||||
})
|
||||
|
@ -78,9 +111,14 @@ func (d *Seafile) Link(ctx context.Context, file model.Obj, args model.LinkArgs)
|
|||
}
|
||||
|
||||
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) {
|
||||
repo, path, err := d.getRepoAndPath(parentDir.GetPath())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
path, _ = utils.JoinBasePath(path, dirName)
|
||||
_, err = d.request(http.MethodPost, fmt.Sprintf("/api2/repos/%s/dir/", repo.Id), func(req *resty.Request) {
|
||||
req.SetQueryParams(map[string]string{
|
||||
"p": filepath.Join(parentDir.GetPath(), dirName),
|
||||
"p": path,
|
||||
}).SetFormData(map[string]string{
|
||||
"operation": "mkdir",
|
||||
})
|
||||
|
@ -89,22 +127,34 @@ func (d *Seafile) MakeDir(ctx context.Context, parentDir model.Obj, dirName stri
|
|||
}
|
||||
|
||||
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) {
|
||||
repo, path, err := d.getRepoAndPath(srcObj.GetPath())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dstRepo, dstPath, err := d.getRepoAndPath(dstDir.GetPath())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = d.request(http.MethodPost, fmt.Sprintf("/api2/repos/%s/file/", repo.Id), func(req *resty.Request) {
|
||||
req.SetQueryParams(map[string]string{
|
||||
"p": srcObj.GetPath(),
|
||||
"p": path,
|
||||
}).SetFormData(map[string]string{
|
||||
"operation": "move",
|
||||
"dst_repo": d.Addition.RepoId,
|
||||
"dst_dir": dstDir.GetPath(),
|
||||
"dst_repo": dstRepo.Id,
|
||||
"dst_dir": dstPath,
|
||||
})
|
||||
}, 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) {
|
||||
repo, path, err := d.getRepoAndPath(srcObj.GetPath())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = d.request(http.MethodPost, fmt.Sprintf("/api2/repos/%s/file/", repo.Id), func(req *resty.Request) {
|
||||
req.SetQueryParams(map[string]string{
|
||||
"p": srcObj.GetPath(),
|
||||
"p": path,
|
||||
}).SetFormData(map[string]string{
|
||||
"operation": "rename",
|
||||
"newname": newName,
|
||||
|
@ -114,31 +164,47 @@ func (d *Seafile) Rename(ctx context.Context, srcObj model.Obj, newName string)
|
|||
}
|
||||
|
||||
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) {
|
||||
repo, path, err := d.getRepoAndPath(srcObj.GetPath())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dstRepo, dstPath, err := d.getRepoAndPath(dstDir.GetPath())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = d.request(http.MethodPost, fmt.Sprintf("/api2/repos/%s/file/", repo.Id), func(req *resty.Request) {
|
||||
req.SetQueryParams(map[string]string{
|
||||
"p": srcObj.GetPath(),
|
||||
"p": path,
|
||||
}).SetFormData(map[string]string{
|
||||
"operation": "copy",
|
||||
"dst_repo": d.Addition.RepoId,
|
||||
"dst_dir": dstDir.GetPath(),
|
||||
"dst_repo": dstRepo.Id,
|
||||
"dst_dir": dstPath,
|
||||
})
|
||||
})
|
||||
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) {
|
||||
repo, path, err := d.getRepoAndPath(obj.GetPath())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = d.request(http.MethodDelete, fmt.Sprintf("/api2/repos/%s/file/", repo.Id), func(req *resty.Request) {
|
||||
req.SetQueryParams(map[string]string{
|
||||
"p": obj.GetPath(),
|
||||
"p": path,
|
||||
})
|
||||
})
|
||||
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) {
|
||||
repo, path, err := d.getRepoAndPath(dstDir.GetPath())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
res, err := d.request(http.MethodGet, fmt.Sprintf("/api2/repos/%s/upload-link/", repo.Id), func(req *resty.Request) {
|
||||
req.SetQueryParams(map[string]string{
|
||||
"p": dstDir.GetPath(),
|
||||
"p": path,
|
||||
})
|
||||
})
|
||||
if err != nil {
|
||||
|
@ -150,7 +216,7 @@ func (d *Seafile) Put(ctx context.Context, dstDir model.Obj, stream model.FileSt
|
|||
_, err = d.request(http.MethodPost, u, func(req *resty.Request) {
|
||||
req.SetFileReader("file", stream.GetName(), stream).
|
||||
SetFormData(map[string]string{
|
||||
"parent_dir": dstDir.GetPath(),
|
||||
"parent_dir": path,
|
||||
"replace": "1",
|
||||
})
|
||||
})
|
||||
|
|
|
@ -11,7 +11,8 @@ type Addition struct {
|
|||
Address string `json:"address" required:"true"`
|
||||
UserName string `json:"username" required:"true"`
|
||||
Password string `json:"password" required:"true"`
|
||||
RepoId string `json:"repoId" required:"true"`
|
||||
RepoId string `json:"repoId" required:"false"`
|
||||
RepoPwd string `json:"repoPwd" required:"false"`
|
||||
}
|
||||
|
||||
var config = driver.Config{
|
||||
|
|
|
@ -1,14 +1,44 @@
|
|||
package seafile
|
||||
|
||||
import "time"
|
||||
|
||||
type AuthTokenResp struct {
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
type RepoDirItemResp struct {
|
||||
type RepoItemResp struct {
|
||||
Id string `json:"id"`
|
||||
Type string `json:"type"` // dir, file
|
||||
Type string `json:"type"` // repo, dir, file
|
||||
Name string `json:"name"`
|
||||
Size int64 `json:"size"`
|
||||
Modified int64 `json:"mtime"`
|
||||
Permission string `json:"permission"`
|
||||
}
|
||||
|
||||
type LibraryItemResp struct {
|
||||
RepoItemResp
|
||||
OwnerContactEmail string `json:"owner_contact_email"`
|
||||
OwnerName string `json:"owner_name"`
|
||||
Owner string `json:"owner"`
|
||||
ModifierEmail string `json:"modifier_email"`
|
||||
ModifierContactEmail string `json:"modifier_contact_email"`
|
||||
ModifierName string `json:"modifier_name"`
|
||||
Virtual bool `json:"virtual"`
|
||||
MtimeRelative string `json:"mtime_relative"`
|
||||
Encrypted bool `json:"encrypted"`
|
||||
Version int `json:"version"`
|
||||
HeadCommitId string `json:"head_commit_id"`
|
||||
Root string `json:"root"`
|
||||
Salt string `json:"salt"`
|
||||
SizeFormatted string `json:"size_formatted"`
|
||||
}
|
||||
|
||||
type RepoDirItemResp struct {
|
||||
RepoItemResp
|
||||
}
|
||||
|
||||
type LibraryInfo struct {
|
||||
LibraryItemResp
|
||||
decryptedTime time.Time
|
||||
decryptedSuccess bool
|
||||
}
|
|
@ -1,8 +1,13 @@
|
|||
package seafile
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/alist-org/alist/v3/internal/errs"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/alist-org/alist/v3/drivers/base"
|
||||
"github.com/go-resty/resty/v2"
|
||||
|
@ -60,3 +65,110 @@ func (d *Seafile) request(method string, pathname string, callback base.ReqCallb
|
|||
}
|
||||
return res.Body(), nil
|
||||
}
|
||||
|
||||
func (d *Seafile) getRepoAndPath(fullPath string) (repo *LibraryInfo, path string, err error) {
|
||||
libraryMap := d.libraryMap
|
||||
repoId := d.Addition.RepoId
|
||||
if repoId != "" {
|
||||
if len(repoId) == 36 /* uuid */ {
|
||||
for _, library := range libraryMap {
|
||||
if library.Id == repoId {
|
||||
return library, fullPath, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
var repoName string
|
||||
str := fullPath[1:]
|
||||
pos := strings.IndexRune(str, '/')
|
||||
if pos == -1 {
|
||||
repoName = str
|
||||
} else {
|
||||
repoName = str[:pos]
|
||||
}
|
||||
path = utils.FixAndCleanPath(fullPath[1+len(repoName):])
|
||||
if library, ok := libraryMap[repoName]; ok {
|
||||
return library, path, nil
|
||||
}
|
||||
}
|
||||
return nil, "", errs.ObjectNotFound
|
||||
}
|
||||
|
||||
func (d *Seafile) listLibraries() (resp []LibraryItemResp, err error) {
|
||||
repoId := d.Addition.RepoId
|
||||
if repoId == "" {
|
||||
_, err = d.request(http.MethodGet, "/api2/repos/", func(req *resty.Request) {
|
||||
req.SetResult(&resp)
|
||||
})
|
||||
} else {
|
||||
var oneResp LibraryItemResp
|
||||
_, err = d.request(http.MethodGet, fmt.Sprintf("/api2/repos/%s/", repoId), func(req *resty.Request) {
|
||||
req.SetResult(&oneResp)
|
||||
})
|
||||
if err == nil {
|
||||
resp = append(resp, oneResp)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
libraryMap := make(map[string]*LibraryInfo)
|
||||
var putLibraryMap func(library LibraryItemResp, index int)
|
||||
putLibraryMap = func(library LibraryItemResp, index int) {
|
||||
name := library.Name
|
||||
if index > 0 {
|
||||
name = fmt.Sprintf("%s (%d)", name, index)
|
||||
}
|
||||
if _, exist := libraryMap[name]; exist {
|
||||
putLibraryMap(library, index+1)
|
||||
} else {
|
||||
libraryInfo := LibraryInfo{}
|
||||
data, _ := utils.Json.Marshal(library)
|
||||
_ = utils.Json.Unmarshal(data, &libraryInfo)
|
||||
libraryMap[name] = &libraryInfo
|
||||
}
|
||||
}
|
||||
for _, library := range resp {
|
||||
putLibraryMap(library, 0)
|
||||
}
|
||||
d.libraryMap = libraryMap
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
var repoPwdNotConfigured = errors.New("library password not configured")
|
||||
var repoPwdIncorrect = errors.New("library password is incorrect")
|
||||
|
||||
func (d *Seafile) decryptLibrary(repo *LibraryInfo) (err error) {
|
||||
if !repo.Encrypted {
|
||||
return nil
|
||||
}
|
||||
if d.RepoPwd == "" {
|
||||
return repoPwdNotConfigured
|
||||
}
|
||||
now := time.Now()
|
||||
decryptedTime := repo.decryptedTime
|
||||
if repo.decryptedSuccess {
|
||||
if now.Sub(decryptedTime).Minutes() <= 30 {
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
if now.Sub(decryptedTime).Seconds() <= 10 {
|
||||
return repoPwdIncorrect
|
||||
}
|
||||
}
|
||||
var resp string
|
||||
_, err = d.request(http.MethodPost, fmt.Sprintf("/api2/repos/%s/", repo.Id), func(req *resty.Request) {
|
||||
req.SetResult(&resp).SetFormData(map[string]string{
|
||||
"password": d.RepoPwd,
|
||||
})
|
||||
})
|
||||
repo.decryptedTime = time.Now()
|
||||
if err != nil || !strings.Contains(resp, "success") {
|
||||
repo.decryptedSuccess = false
|
||||
return err
|
||||
}
|
||||
repo.decryptedSuccess = true
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
|
|
Loading…
Reference in New Issue