diff --git a/drivers/seafile/driver.go b/drivers/seafile/driver.go index 49cf3386058..6d1f16dad3b 100644 --- a/drivers/seafile/driver.go +++ b/drivers/seafile/driver.go @@ -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", }) }) diff --git a/drivers/seafile/meta.go b/drivers/seafile/meta.go index e333fd905bb..9d84aee182f 100644 --- a/drivers/seafile/meta.go +++ b/drivers/seafile/meta.go @@ -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{ diff --git a/drivers/seafile/types.go b/drivers/seafile/types.go index 5c5b528d31f..47cb322df4a 100644 --- a/drivers/seafile/types.go +++ b/drivers/seafile/types.go @@ -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 +} \ No newline at end of file diff --git a/drivers/seafile/util.go b/drivers/seafile/util.go index 23255545c3a..681953103c3 100644 --- a/drivers/seafile/util.go +++ b/drivers/seafile/util.go @@ -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 +} + +