diff --git a/drivers/all.go b/drivers/all.go index 3eb7e813..000d0ca6 100644 --- a/drivers/all.go +++ b/drivers/all.go @@ -77,6 +77,7 @@ import ( _ "github.com/alist-org/alist/v3/drivers/webdav" _ "github.com/alist-org/alist/v3/drivers/weiyun" _ "github.com/alist-org/alist/v3/drivers/wopan" + _ "github.com/alist-org/alist/v3/drivers/wps" _ "github.com/alist-org/alist/v3/drivers/yandex_disk" ) diff --git a/drivers/wps/driver.go b/drivers/wps/driver.go new file mode 100644 index 00000000..870e6a20 --- /dev/null +++ b/drivers/wps/driver.go @@ -0,0 +1,139 @@ +package wps + +import ( + "context" + "fmt" + "net/http" + + "github.com/alist-org/alist/v3/internal/driver" + "github.com/alist-org/alist/v3/internal/errs" + "github.com/alist-org/alist/v3/internal/model" +) + +type Wps struct { + model.Storage + Addition + companyID string +} + +func (d *Wps) Config() driver.Config { + return config +} + +func (d *Wps) GetAddition() driver.Additional { + return &d.Addition +} + +func (d *Wps) Init(ctx context.Context) error { + if d.Cookie == "" { + return fmt.Errorf("cookie is empty") + } + return d.ensureCompanyID(ctx) +} + +func (d *Wps) Drop(ctx context.Context) error { + return nil +} + +func (d *Wps) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) { + basePath := "/" + if dir != nil { + if p := dir.GetPath(); p != "" { + basePath = p + } + } + node, err := d.resolvePath(ctx, basePath) + if err != nil { + return nil, err + } + if node.kind == "root" { + groups, err := d.getGroups(ctx) + if err != nil { + return nil, err + } + res := make([]model.Obj, 0, len(groups)) + for _, g := range groups { + path := joinPath(basePath, g.Name) + obj := &Obj{ + id: path, + name: g.Name, + ctime: parseTime(0), + mtime: parseTime(0), + isDir: true, + path: path, + } + res = append(res, obj) + } + return res, nil + } + if node.kind != "group" && node.kind != "folder" { + return nil, nil + } + parentID := int64(0) + if node.file != nil && node.kind == "folder" { + parentID = node.file.ID + } + files, err := d.getFiles(ctx, node.group.GroupID, parentID) + if err != nil { + return nil, err + } + res := make([]model.Obj, 0, len(files)) + for _, f := range files { + res = append(res, fileToObj(basePath, f)) + } + return res, nil +} + +func (d *Wps) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) { + path := file.GetPath() + node, err := d.resolvePath(ctx, path) + if err != nil { + return nil, err + } + if node.kind != "file" || node.file == nil { + return nil, errs.NotSupport + } + if node.file.FilePerms.Download == 0 { + return nil, fmt.Errorf("no download permission") + } + url := fmt.Sprintf("%s/3rd/drive/api/v5/groups/%d/files/%d/download?support_checksums=sha1", endpoint, node.group.GroupID, node.file.ID) + var resp downloadResp + _, err = d.request(ctx).SetResult(&resp).Get(url) + if err != nil { + return nil, err + } + if resp.URL == "" { + return nil, fmt.Errorf("empty download url") + } + link := &model.Link{ + URL: resp.URL, + Header: http.Header{}, + } + return link, nil +} + +func (d *Wps) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error { + return errs.NotSupport +} + +func (d *Wps) Move(ctx context.Context, srcObj, dstDir model.Obj) error { + return errs.NotSupport +} + +func (d *Wps) Rename(ctx context.Context, srcObj model.Obj, newName string) error { + return errs.NotSupport +} + +func (d *Wps) Copy(ctx context.Context, srcObj, dstDir model.Obj) error { + return errs.NotSupport +} + +func (d *Wps) Remove(ctx context.Context, obj model.Obj) error { + return errs.NotSupport +} + +func (d *Wps) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error { + return errs.NotSupport +} + +var _ driver.Driver = (*Wps)(nil) diff --git a/drivers/wps/meta.go b/drivers/wps/meta.go new file mode 100644 index 00000000..430c175d --- /dev/null +++ b/drivers/wps/meta.go @@ -0,0 +1,30 @@ +package wps + +import ( + "github.com/alist-org/alist/v3/internal/driver" + "github.com/alist-org/alist/v3/internal/op" +) + +type Addition struct { + Cookie string `json:"cookie" required:"true" help:"kso_sid=xxxxx"` +} + +var config = driver.Config{ + Name: "Wps", + LocalSort: false, + OnlyLocal: false, + OnlyProxy: false, + NoCache: false, + NoUpload: true, + NeedMs: false, + DefaultRoot: "/", + CheckStatus: false, + Alert: "", + NoOverwriteUpload: false, +} + +func init() { + op.RegisterDriver(func() driver.Driver { + return &Wps{} + }) +} diff --git a/drivers/wps/types.go b/drivers/wps/types.go new file mode 100644 index 00000000..c9627f18 --- /dev/null +++ b/drivers/wps/types.go @@ -0,0 +1,94 @@ +package wps + +import ( + "time" + + "github.com/alist-org/alist/v3/pkg/utils" +) + +type workspaceResp struct { + Companies []struct { + ID int64 `json:"id"` + } `json:"companies"` +} + +type Group struct { + CompanyID int64 `json:"company_id"` + GroupID int64 `json:"group_id"` + Name string `json:"name"` + Type string `json:"type"` +} + +type groupsResp struct { + Groups []Group `json:"groups"` +} + +type filePerms struct { + Download int `json:"download"` +} + +type FileInfo struct { + GroupID int64 `json:"groupid"` + ParentID int64 `json:"parentid"` + Name string `json:"fname"` + Size int64 `json:"fsize"` + Type string `json:"ftype"` + Ctime int64 `json:"ctime"` + Mtime int64 `json:"mtime"` + ID int64 `json:"id"` + Deleted bool `json:"deleted"` + FilePerms filePerms `json:"file_perms_acl"` +} + +type filesResp struct { + Files []FileInfo `json:"files"` +} + +type downloadResp struct { + URL string `json:"url"` + Result string `json:"result"` +} + +type Obj struct { + id string + name string + size int64 + ctime time.Time + mtime time.Time + isDir bool + hash utils.HashInfo + path string + canDownload bool +} + +func (o *Obj) GetSize() int64 { + return o.size +} + +func (o *Obj) GetName() string { + return o.name +} + +func (o *Obj) ModTime() time.Time { + return o.mtime +} + +func (o *Obj) CreateTime() time.Time { + return o.ctime +} + +func (o *Obj) IsDir() bool { + return o.isDir +} + +func (o *Obj) GetHash() utils.HashInfo { + return o.hash +} + +func (o *Obj) GetID() string { + return o.id +} + +func (o *Obj) GetPath() string { + return o.path +} diff --git a/drivers/wps/util.go b/drivers/wps/util.go new file mode 100644 index 00000000..833de1d1 --- /dev/null +++ b/drivers/wps/util.go @@ -0,0 +1,149 @@ +package wps + +import ( + "context" + "fmt" + "strconv" + "strings" + "time" + + "github.com/alist-org/alist/v3/drivers/base" + "github.com/go-resty/resty/v2" +) + +const endpoint = "https://365.kdocs.cn" + +type resolvedNode struct { + kind string + group Group + file *FileInfo +} + +func (d *Wps) request(ctx context.Context) *resty.Request { + return base.RestyClient.R(). + SetHeader("Cookie", d.Cookie). + SetHeader("Accept", "application/json"). + SetContext(ctx) +} + +func (d *Wps) ensureCompanyID(ctx context.Context) error { + if d.companyID != "" { + return nil + } + var resp workspaceResp + _, err := d.request(ctx).SetResult(&resp).Get(endpoint + "/3rd/plussvr/compose/v1/users/self/workspaces?fields=name&comp_status=active") + if err != nil { + return err + } + if len(resp.Companies) == 0 { + return fmt.Errorf("no company id") + } + d.companyID = strconv.FormatInt(resp.Companies[0].ID, 10) + return nil +} + +func (d *Wps) getGroups(ctx context.Context) ([]Group, error) { + if err := d.ensureCompanyID(ctx); err != nil { + return nil, err + } + var resp groupsResp + url := fmt.Sprintf("%s/3rd/plus/groups/v1/companies/%s/users/self/groups/private", endpoint, d.companyID) + _, err := d.request(ctx).SetResult(&resp).Get(url) + if err != nil { + return nil, err + } + return resp.Groups, nil +} + +func (d *Wps) getFiles(ctx context.Context, groupID, parentID int64) ([]FileInfo, error) { + var resp filesResp + url := fmt.Sprintf("%s/3rd/drive/api/v5/groups/%d/files", endpoint, groupID) + _, err := d.request(ctx).SetQueryParam("parentid", strconv.FormatInt(parentID, 10)).SetResult(&resp).Get(url) + if err != nil { + return nil, err + } + return resp.Files, nil +} + +func parseTime(v int64) time.Time { + if v <= 0 { + return time.Time{} + } + return time.Unix(v, 0) +} + +func joinPath(basePath, name string) string { + if basePath == "" || basePath == "/" { + return "/" + name + } + return strings.TrimRight(basePath, "/") + "/" + name +} + +func (d *Wps) resolvePath(ctx context.Context, path string) (*resolvedNode, error) { + clean := strings.TrimSpace(path) + if clean == "" { + clean = "/" + } + clean = strings.Trim(clean, "/") + if clean == "" { + return &resolvedNode{kind: "root"}, nil + } + segs := strings.Split(clean, "/") + groups, err := d.getGroups(ctx) + if err != nil { + return nil, err + } + var grp *Group + for i := range groups { + if groups[i].Name == segs[0] { + grp = &groups[i] + break + } + } + if grp == nil { + return nil, fmt.Errorf("group not found") + } + if len(segs) == 1 { + return &resolvedNode{kind: "group", group: *grp}, nil + } + parentID := int64(0) + var last FileInfo + for i := 1; i < len(segs); i++ { + files, err := d.getFiles(ctx, grp.GroupID, parentID) + if err != nil { + return nil, err + } + var found *FileInfo + for j := range files { + if files[j].Name == segs[i] { + found = &files[j] + break + } + } + if found == nil { + return nil, fmt.Errorf("path not found") + } + last = *found + parentID = found.ID + } + kind := "file" + if last.Type == "folder" { + kind = "folder" + } + return &resolvedNode{kind: kind, group: *grp, file: &last}, nil +} + +func fileToObj(basePath string, f FileInfo) *Obj { + name := f.Name + path := joinPath(basePath, name) + return &Obj{ + id: path, + name: name, + size: f.Size, + ctime: parseTime(f.Ctime), + mtime: parseTime(f.Mtime), + isDir: f.Type == "folder", + path: path, + canDownload: f.FilePerms.Download != 0, + } +}