Repository: AlistGo/alist (language: go)

Issue:
支持terabox网盘存储
### Please make sure of the following things

- [X] I have read the [documentation](https://alist.nn.ci).
- [X] I'm sure there are no duplicate issues or discussions.
- [X] I'm sure this feature is not implemented.
- [X] I'm sure it's a reasonable and popular requirement.

### Description of the feature / 需求描述

支持terabox网盘存储https://www.terabox.com/

### Suggested solution / 实现思路

API类似百度盘,用cookie登录，如列表接口是https://www.terabox.com/api/list

### Additional context / 附件

_No response_


Discussion / hints:
牛逼网盘
希望支持terabox

Fix patch:
diff --git a/drivers/all.go b/drivers/all.go
index 6d5b56e1e2f..bde2fe9c388 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 00000000000..b7c4546ece2
--- /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 00000000000..797244b2f74
--- /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 00000000000..e2015622cba
--- /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 00000000000..bca7835c33f
--- /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
+}


Test patch: