diff --git a/drivers/all.go b/drivers/all.go index 6d5b56e1..bde2fe9c 100644 --- a/drivers/all.go +++ b/drivers/all.go @@ -27,6 +27,7 @@ import ( _ "github.com/alist-org/alist/v3/drivers/sftp" _ "github.com/alist-org/alist/v3/drivers/smb" _ "github.com/alist-org/alist/v3/drivers/teambition" + _ "github.com/alist-org/alist/v3/drivers/terabox" _ "github.com/alist-org/alist/v3/drivers/thunder" _ "github.com/alist-org/alist/v3/drivers/uss" _ "github.com/alist-org/alist/v3/drivers/virtual" diff --git a/drivers/terabox/driver.go b/drivers/terabox/driver.go new file mode 100644 index 00000000..b7c4546e --- /dev/null +++ b/drivers/terabox/driver.go @@ -0,0 +1,214 @@ +package terbox + +import ( + "bytes" + "context" + "crypto/md5" + "encoding/hex" + "fmt" + "github.com/alist-org/alist/v3/drivers/base" + "github.com/alist-org/alist/v3/pkg/utils" + log "github.com/sirupsen/logrus" + "io" + "math" + "net/http" + "os" + stdpath "path" + "strconv" + "strings" + + "github.com/alist-org/alist/v3/internal/driver" + "github.com/alist-org/alist/v3/internal/model" +) + +type Terabox struct { + model.Storage + Addition +} + +func (d *Terabox) Config() driver.Config { + return config +} + +func (d *Terabox) GetAddition() driver.Additional { + return &d.Addition +} + +func (d *Terabox) Init(ctx context.Context) error { + _, err := d.request("https://www.terabox.com/api/check/login", http.MethodGet, nil, nil) + return err +} + +func (d *Terabox) Drop(ctx context.Context) error { + return nil +} + +func (d *Terabox) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) { + files, err := d.getFiles(dir.GetPath()) + if err != nil { + return nil, err + } + return utils.SliceConvert(files, func(src File) (model.Obj, error) { + return fileToObj(src), nil + }) +} + +func (d *Terabox) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) { + if d.DownloadAPI == "crack" { + return d.linkCrack(file, args) + } + return d.linkOfficial(file, args) +} + +func (d *Terabox) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error { + _, err := d.create(stdpath.Join(parentDir.GetPath(), dirName), 0, 1, "", "") + return err +} + +func (d *Terabox) Move(ctx context.Context, srcObj, dstDir model.Obj) error { + data := []base.Json{ + { + "path": srcObj.GetPath(), + "dest": dstDir.GetPath(), + "newname": srcObj.GetName(), + }, + } + _, err := d.manage("move", data) + return err +} + +func (d *Terabox) Rename(ctx context.Context, srcObj model.Obj, newName string) error { + data := []base.Json{ + { + "path": srcObj.GetPath(), + "newname": newName, + }, + } + _, err := d.manage("rename", data) + return err +} + +func (d *Terabox) Copy(ctx context.Context, srcObj, dstDir model.Obj) error { + data := []base.Json{ + { + "path": srcObj.GetPath(), + "dest": dstDir.GetPath(), + "newname": srcObj.GetName(), + }, + } + _, err := d.manage("copy", data) + return err +} + +func (d *Terabox) Remove(ctx context.Context, obj model.Obj) error { + data := []string{obj.GetPath()} + _, err := d.manage("delete", data) + return err +} + +func (d *Terabox) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error { + tempFile, err := utils.CreateTempFile(stream.GetReadCloser()) + if err != nil { + return err + } + defer func() { + _ = tempFile.Close() + _ = os.Remove(tempFile.Name()) + }() + var Default int64 = 4 * 1024 * 1024 + defaultByteData := make([]byte, Default) + count := int(math.Ceil(float64(stream.GetSize()) / float64(Default))) + // cal md5 + h1 := md5.New() + h2 := md5.New() + block_list := make([]string, 0) + left := stream.GetSize() + for i := 0; i < count; i++ { + byteSize := Default + var byteData []byte + if left < Default { + byteSize = left + byteData = make([]byte, byteSize) + } else { + byteData = defaultByteData + } + left -= byteSize + _, err = io.ReadFull(tempFile, byteData) + if err != nil { + return err + } + h1.Write(byteData) + h2.Write(byteData) + block_list = append(block_list, fmt.Sprintf("\"%s\"", hex.EncodeToString(h2.Sum(nil)))) + h2.Reset() + } + + _, err = tempFile.Seek(0, io.SeekStart) + if err != nil { + return err + } + + rawPath := stdpath.Join(dstDir.GetPath(), stream.GetName()) + path := encodeURIComponent(rawPath) + block_list_str := fmt.Sprintf("[%s]", strings.Join(block_list, ",")) + data := fmt.Sprintf("path=%s&size=%d&isdir=0&autoinit=1&block_list=%s", + path, stream.GetSize(), + block_list_str) + params := map[string]string{} + var precreateResp PrecreateResp + _, err = d.post("/api/precreate", params, data, &precreateResp) + if err != nil { + return err + } + log.Debugf("%+v", precreateResp) + if precreateResp.ReturnType == 2 { + return nil + } + params = map[string]string{ + "method": "upload", + "path": path, + "uploadid": precreateResp.Uploadid, + "app_id": "250528", + "web": "1", + "channel": "dubox", + "clienttype": "0", + } + left = stream.GetSize() + for i, partseq := range precreateResp.BlockList { + if utils.IsCanceled(ctx) { + return ctx.Err() + } + byteSize := Default + var byteData []byte + if left < Default { + byteSize = left + byteData = make([]byte, byteSize) + } else { + byteData = defaultByteData + } + left -= byteSize + _, err = io.ReadFull(tempFile, byteData) + if err != nil { + return err + } + u := "https://c-jp.terabox.com/rest/2.0/pcs/superfile2" + params["partseq"] = strconv.Itoa(partseq) + res, err := base.RestyClient.R(). + SetContext(ctx). + SetQueryParams(params). + SetFileReader("file", stream.GetName(), bytes.NewReader(byteData)). + SetHeader("Cookie", d.Cookie). + Post(u) + if err != nil { + return err + } + log.Debugln(res.String()) + if len(precreateResp.BlockList) > 0 { + up(i * 100 / len(precreateResp.BlockList)) + } + } + _, err = d.create(rawPath, stream.GetSize(), 0, precreateResp.Uploadid, block_list_str) + return err +} + +var _ driver.Driver = (*Terabox)(nil) diff --git a/drivers/terabox/meta.go b/drivers/terabox/meta.go new file mode 100644 index 00000000..797244b2 --- /dev/null +++ b/drivers/terabox/meta.go @@ -0,0 +1,25 @@ +package terbox + +import ( + "github.com/alist-org/alist/v3/internal/driver" + "github.com/alist-org/alist/v3/internal/op" +) + +type Addition struct { + driver.RootPath + Cookie string `json:"cookie" required:"true"` + DownloadAPI string `json:"download_api" type:"select" options:"official,crack" default:"official"` + OrderBy string `json:"order_by" type:"select" options:"name,time,size" default:"name"` + OrderDirection string `json:"order_direction" type:"select" options:"asc,desc" default:"asc"` +} + +var config = driver.Config{ + Name: "Terabox", + DefaultRoot: "/", +} + +func init() { + op.RegisterDriver(func() driver.Driver { + return &Terabox{} + }) +} diff --git a/drivers/terabox/types.go b/drivers/terabox/types.go new file mode 100644 index 00000000..e2015622 --- /dev/null +++ b/drivers/terabox/types.go @@ -0,0 +1,92 @@ +package terbox + +import ( + "github.com/alist-org/alist/v3/internal/model" + "strconv" + "time" +) + +type File struct { + //TkbindId int `json:"tkbind_id"` + //OwnerType int `json:"owner_type"` + //Category int `json:"category"` + //RealCategory string `json:"real_category"` + FsId int64 `json:"fs_id"` + ServerMtime int64 `json:"server_mtime"` + //OperId int `json:"oper_id"` + //ServerCtime int `json:"server_ctime"` + Thumbs struct { + //Icon string `json:"icon"` + Url3 string `json:"url3"` + //Url2 string `json:"url2"` + //Url1 string `json:"url1"` + } `json:"thumbs"` + //Wpfile int `json:"wpfile"` + //LocalMtime int `json:"local_mtime"` + Size int64 `json:"size"` + //ExtentTinyint7 int `json:"extent_tinyint7"` + Path string `json:"path"` + //Share int `json:"share"` + //ServerAtime int `json:"server_atime"` + //Pl int `json:"pl"` + //LocalCtime int `json:"local_ctime"` + ServerFilename string `json:"server_filename"` + //Md5 string `json:"md5"` + //OwnerId int `json:"owner_id"` + //Unlist int `json:"unlist"` + Isdir int `json:"isdir"` +} + +type ListResp struct { + Errno int `json:"errno"` + GuidInfo string `json:"guid_info"` + List []File `json:"list"` + RequestId int64 `json:"request_id"` + Guid int `json:"guid"` +} + +func fileToObj(f File) *model.ObjThumb { + return &model.ObjThumb{ + Object: model.Object{ + ID: strconv.FormatInt(f.FsId, 10), + Name: f.ServerFilename, + Size: f.Size, + Modified: time.Unix(f.ServerMtime, 0), + IsFolder: f.Isdir == 1, + }, + Thumbnail: model.Thumbnail{Thumbnail: f.Thumbs.Url3}, + } +} + +type DownloadResp struct { + Errno int `json:"errno"` + Dlink []struct { + Dlink string `json:"dlink"` + } `json:"dlink"` +} + +type DownloadResp2 struct { + Errno int `json:"errno"` + Info []struct { + Dlink string `json:"dlink"` + } `json:"info"` + RequestID int64 `json:"request_id"` +} + +type HomeInfoResp struct { + Errno int `json:"errno"` + Data struct { + Sign1 string `json:"sign1"` + Sign3 string `json:"sign3"` + Timestamp int `json:"timestamp"` + } `json:"data"` +} + +type PrecreateResp struct { + Path string `json:"path"` + Uploadid string `json:"uploadid"` + ReturnType int `json:"return_type"` + BlockList []int `json:"block_list"` + Errno int `json:"errno"` + RequestId int64 `json:"request_id"` +} diff --git a/drivers/terabox/util.go b/drivers/terabox/util.go new file mode 100644 index 00000000..bca7835c --- /dev/null +++ b/drivers/terabox/util.go @@ -0,0 +1,192 @@ +package terbox + +import ( + "encoding/base64" + "fmt" + "github.com/alist-org/alist/v3/drivers/base" + "github.com/alist-org/alist/v3/internal/model" + "github.com/alist-org/alist/v3/pkg/utils" + "github.com/go-resty/resty/v2" + "net/http" + "net/url" + "strconv" + "strings" + "time" +) + +func (d *Terabox) request(furl string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) { + req := base.RestyClient.R() + req.SetHeaders(map[string]string{ + "Cookie": d.Cookie, + "Accept": "application/json, text/plain, */*", + "Referer": "https://www.terabox.com/", + "User-Agent": base.UserAgent, + }) + req.SetQueryParam("app_id", "250528") + req.SetQueryParam("web", "1") + req.SetQueryParam("channel", "dubox") + req.SetQueryParam("clienttype", "0") + if callback != nil { + callback(req) + } + if resp != nil { + req.SetResult(resp) + } + res, err := req.Execute(method, furl) + if err != nil { + return nil, err + } + return res.Body(), nil +} + +func (d *Terabox) get(pathname string, params map[string]string, resp interface{}) ([]byte, error) { + return d.request("https://www.terabox.com"+pathname, http.MethodGet, func(req *resty.Request) { + req.SetQueryParams(params) + }, resp) +} + +func (d *Terabox) post(pathname string, params map[string]string, data interface{}, resp interface{}) ([]byte, error) { + return d.request("https://www.terabox.com"+pathname, http.MethodPost, func(req *resty.Request) { + req.SetQueryParams(params) + req.SetBody(data) + }, resp) +} + +func (d *Terabox) getFiles(dir string) ([]File, error) { + page := 1 + num := 100 + params := map[string]string{ + "dir": dir, + } + if d.OrderBy != "" { + params["order"] = d.OrderBy + if d.OrderDirection == "desc" { + params["desc"] = "1" + } + } + res := make([]File, 0) + for { + params["page"] = strconv.Itoa(page) + params["num"] = strconv.Itoa(num) + var resp ListResp + _, err := d.get("/api/list", params, &resp) + if err != nil { + return nil, err + } + if len(resp.List) == 0 { + break + } + res = append(res, resp.List...) + page++ + } + return res, nil +} + +func sign(s1, s2 string) string { + var a = make([]int, 256) + var p = make([]int, 256) + var o []byte + var v = len(s1) + for q := 0; q < 256; q++ { + a[q] = int(s1[(q % v) : (q%v)+1][0]) + p[q] = q + } + for u, q := 0, 0; q < 256; q++ { + u = (u + p[q] + a[q]) % 256 + p[q], p[u] = p[u], p[q] + } + for i, u, q := 0, 0, 0; q < len(s2); q++ { + i = (i + 1) % 256 + u = (u + p[i]) % 256 + p[i], p[u] = p[u], p[i] + k := p[((p[i] + p[u]) % 256)] + o = append(o, byte(int(s2[q])^k)) + } + return base64.StdEncoding.EncodeToString(o) +} + +func (d *Terabox) genSign() (string, error) { + var resp HomeInfoResp + _, err := d.get("/api/home/info", map[string]string{}, &resp) + if err != nil { + return "", err + } + return sign(resp.Data.Sign3, resp.Data.Sign1), nil +} + +func (d *Terabox) linkOfficial(file model.Obj, args model.LinkArgs) (*model.Link, error) { + var resp DownloadResp + signString, err := d.genSign() + if err != nil { + return nil, err + } + params := map[string]string{ + "type": "dlink", + "fidlist": fmt.Sprintf("[%s]", file.GetID()), + "sign": signString, + "vip": "2", + "timestamp": strconv.FormatInt(time.Now().Unix(), 10), + } + _, err = d.get("/api/download", params, &resp) + if err != nil { + return nil, err + } + res, err := base.NoRedirectClient.R().SetHeader("Cookie", d.Cookie).SetHeader("User-Agent", base.UserAgent).Get(resp.Dlink[0].Dlink) + if err != nil { + return nil, err + } + u := res.Header().Get("location") + return &model.Link{ + URL: u, + Header: http.Header{ + "User-Agent": []string{base.UserAgent}, + }, + }, nil +} + +func (d *Terabox) linkCrack(file model.Obj, args model.LinkArgs) (*model.Link, error) { + var resp DownloadResp2 + param := map[string]string{ + "target": fmt.Sprintf("[\"%s\"]", file.GetPath()), + "dlink": "1", + "origin": "dlna", + } + _, err := d.get("/api/filemetas", param, &resp) + if err != nil { + return nil, err + } + return &model.Link{ + URL: resp.Info[0].Dlink, + Header: http.Header{ + "User-Agent": []string{base.UserAgent}, + }, + }, nil +} + +func (d *Terabox) manage(opera string, filelist interface{}) ([]byte, error) { + params := map[string]string{ + "onnest": "fail", + "opera": opera, + } + marshal, err := utils.Json.Marshal(filelist) + if err != nil { + return nil, err + } + data := fmt.Sprintf("async=0&filelist=%s&ondup=newcopy", string(marshal)) + return d.post("/api/filemanager", params, data, nil) +} + +func (d *Terabox) create(path string, size int64, isdir int, uploadid, block_list string) ([]byte, error) { + params := map[string]string{} + data := fmt.Sprintf("path=%s&size=%d&isdir=%d", encodeURIComponent(path), size, isdir) + if uploadid != "" { + data += fmt.Sprintf("&uploadid=%s&block_list=%s", uploadid, block_list) + } + return d.post("/api/create", params, data, nil) +} + +func encodeURIComponent(str string) string { + r := url.QueryEscape(str) + r = strings.ReplaceAll(r, "+", "%20") + return r +}