diff --git a/drivers/all.go b/drivers/all.go index 3acf041a..b8d88519 100644 --- a/drivers/all.go +++ b/drivers/all.go @@ -6,6 +6,7 @@ import ( _ "github.com/Xhofe/alist/drivers/189" _ "github.com/Xhofe/alist/drivers/alidrive" _ "github.com/Xhofe/alist/drivers/alist" + _ "github.com/Xhofe/alist/drivers/baidu" "github.com/Xhofe/alist/drivers/base" _ "github.com/Xhofe/alist/drivers/ftp" _ "github.com/Xhofe/alist/drivers/google" diff --git a/drivers/baidu/baidu.go b/drivers/baidu/baidu.go new file mode 100644 index 00000000..bb8d0c08 --- /dev/null +++ b/drivers/baidu/baidu.go @@ -0,0 +1,186 @@ +package baidu + +import ( + "fmt" + "github.com/Xhofe/alist/conf" + "github.com/Xhofe/alist/drivers/base" + "github.com/Xhofe/alist/model" + "github.com/Xhofe/alist/utils" + "github.com/go-resty/resty/v2" + jsoniter "github.com/json-iterator/go" + "path" + "strconv" +) + +func (driver Baidu) RefreshToken(account *model.Account) error { + err := driver.refreshToken(account) + if err != nil && err == base.ErrEmptyToken { + err = driver.refreshToken(account) + } + if err != nil { + account.Status = err.Error() + } + _ = model.SaveAccount(account) + return err +} + +func (driver Baidu) refreshToken(account *model.Account) error { + u := "https://openapi.baidu.com/oauth/2.0/token" + var resp base.TokenResp + var e TokenErrResp + _, err := base.RestyClient.R().SetResult(&resp).SetError(&e).SetQueryParams(map[string]string{ + "grant_type": "refresh_token", + "refresh_token": account.RefreshToken, + "client_id": account.ClientId, + "client_secret": account.ClientSecret, + }).Get(u) + if err != nil { + return err + } + if e.Error != "" { + return fmt.Errorf("%s : %s", e.Error, e.ErrorDescription) + } + if resp.RefreshToken == "" { + return base.ErrEmptyToken + } + account.Status = "work" + account.AccessToken, account.RefreshToken = resp.AccessToken, resp.RefreshToken + return nil +} + +func (driver Baidu) Request(pathname string, method int, headers, query, form map[string]string, data interface{}, resp interface{}, account *model.Account) ([]byte, error) { + u := "https://pan.baidu.com/rest/2.0" + pathname + req := base.RestyClient.R() + req.SetQueryParam("access_token", account.AccessToken) + if headers != nil { + req.SetHeaders(headers) + } + if query != nil { + req.SetQueryParams(query) + } + if form != nil { + req.SetFormData(form) + } + if data != nil { + req.SetBody(data) + } + if resp != nil { + req.SetResult(resp) + } + var res *resty.Response + var err error + switch method { + case base.Get: + res, err = req.Get(u) + case base.Post: + res, err = req.Post(u) + case base.Patch: + res, err = req.Patch(u) + case base.Delete: + res, err = req.Delete(u) + case base.Put: + res, err = req.Put(u) + default: + return nil, base.ErrNotSupport + } + if err != nil { + return nil, err + } + //log.Debug(res.String()) + errno := jsoniter.Get(res.Body(), "errno").ToInt() + if errno != 0 { + if errno == -6 { + err = driver.RefreshToken(account) + if err != nil { + return nil, err + } + return driver.Request(pathname, method, headers, query, form, data, resp, account) + } + return nil, fmt.Errorf("errno: %d, refer to https://pan.baidu.com/union/doc/", errno) + } + return res.Body(), nil +} + +func (driver Baidu) Get(pathname string, params map[string]string, resp interface{}, account *model.Account) ([]byte, error) { + return driver.Request(pathname, base.Get, nil, params, nil, nil, resp, account) +} + +func (driver Baidu) Post(pathname string, params map[string]string, data interface{}, resp interface{}, account *model.Account) ([]byte, error) { + return driver.Request(pathname, base.Post, nil, params, nil, data, resp, account) +} + +func (driver Baidu) manage(opera string, filelist interface{}, account *model.Account) ([]byte, error) { + params := map[string]string{ + "method": "filemanager", + "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 driver.Post("/xpan/file", params, data, nil, account) +} + +func (driver Baidu) GetFiles(dir string, account *model.Account) ([]model.File, error) { + dir = utils.Join(account.RootFolder, dir) + start := 0 + limit := 200 + params := map[string]string{ + "method": "list", + "dir": dir, + "web": "web", + } + if account.OrderBy != "" { + params["order"] = account.OrderBy + if account.OrderDirection == "desc" { + params["desc"] = "1" + } + } + res := make([]model.File, 0) + for { + params["start"] = strconv.Itoa(start) + params["limit"] = strconv.Itoa(limit) + start += limit + var resp ListResp + _, err := driver.Get("/xpan/file", params, &resp, account) + if err != nil { + return nil, err + } + if len(resp.List) == 0 { + break + } + for _, f := range resp.List { + file := model.File{ + Id: strconv.FormatInt(f.FsId, 10), + Name: f.ServerFilename, + Size: f.Size, + Driver: driver.Config().Name, + UpdatedAt: getTime(f.ServerMtime), + Thumbnail: f.Thumbs.Url3, + } + if f.Isdir == 1 { + file.Type = conf.FOLDER + } else { + file.Type = utils.GetFileType(path.Ext(f.ServerFilename)) + } + res = append(res, file) + } + } + return res, nil +} + +func (driver Baidu) create(path string, size uint64, isdir int, uploadid, block_list string, account *model.Account) ([]byte, error) { + params := map[string]string{ + "method": "create", + } + data := fmt.Sprintf("path=%s&size=%d&isdir=%d", path, size, isdir) + if uploadid != "" { + data += fmt.Sprintf("&uploadid=%s&block_list=%s", uploadid, block_list) + } + return driver.Post("/xpan/file", params, data, nil, account) +} + +func init() { + base.RegisterDriver(&Baidu{}) +} diff --git a/drivers/baidu/driver.go b/drivers/baidu/driver.go new file mode 100644 index 00000000..2e57b0ad --- /dev/null +++ b/drivers/baidu/driver.go @@ -0,0 +1,356 @@ +package baidu + +import ( + "bytes" + "crypto/md5" + "encoding/hex" + "fmt" + "github.com/Xhofe/alist/conf" + "github.com/Xhofe/alist/drivers/base" + "github.com/Xhofe/alist/model" + "github.com/Xhofe/alist/utils" + log "github.com/sirupsen/logrus" + "io" + "io/ioutil" + "math" + "net/http" + "os" + "path/filepath" + "strconv" + "strings" +) + +type Baidu struct{} + +func (driver Baidu) Config() base.DriverConfig { + return base.DriverConfig{ + Name: "Baidu.Disk", + } +} + +func (driver Baidu) Items() []base.Item { + return []base.Item{ + { + Name: "refresh_token", + Label: "refresh token", + Type: base.TypeString, + Required: true, + }, + { + Name: "root_folder", + Label: "root folder path", + Type: base.TypeString, + Default: "/", + Required: true, + }, + { + Name: "order_by", + Label: "order_by", + Type: base.TypeSelect, + Default: "name", + Values: "name,time,size", + Required: false, + }, + { + Name: "order_direction", + Label: "order_direction", + Type: base.TypeSelect, + Values: "asc,desc", + Default: "asc", + Required: false, + }, + { + Name: "client_id", + Label: "client id", + Default: "iYCeC9g08h5vuP9UqvPHKKSVrKFXGa1v", + Type: base.TypeString, + Required: true, + }, + { + Name: "client_secret", + Label: "client secret", + Default: "jXiFMOPVPCWlO2M5CwWQzffpNPaGTRBG", + Type: base.TypeString, + Required: true, + }, + } +} + +func (driver Baidu) Save(account *model.Account, old *model.Account) error { + return driver.RefreshToken(account) +} + +func (driver Baidu) File(path string, account *model.Account) (*model.File, error) { + path = utils.ParsePath(path) + if path == "/" { + return &model.File{ + Id: account.RootFolder, + Name: account.Name, + Size: 0, + Type: conf.FOLDER, + Driver: driver.Config().Name, + UpdatedAt: account.UpdatedAt, + }, nil + } + dir, name := filepath.Split(path) + files, err := driver.Files(dir, account) + if err != nil { + return nil, err + } + for _, file := range files { + if file.Name == name { + return &file, nil + } + } + return nil, base.ErrPathNotFound +} + +func (driver Baidu) Files(path string, account *model.Account) ([]model.File, error) { + path = utils.ParsePath(path) + cache, err := base.GetCache(path, account) + if err == nil { + files, _ := cache.([]model.File) + return files, nil + } + files, err := driver.GetFiles(path, account) + if err != nil { + return nil, err + } + if len(files) > 0 { + _ = base.SetCache(path, files, account) + } + return files, nil +} + +func (driver Baidu) Link(args base.Args, account *model.Account) (*base.Link, error) { + file, err := driver.File(args.Path, account) + if err != nil { + return nil, err + } + if file.IsDir() { + return nil, base.ErrNotFile + } + var resp DownloadResp + params := map[string]string{ + "method": "filemetas", + "fsids": fmt.Sprintf("[%s]", file.Id), + "dlink": "1", + } + _, err = driver.Get("/xpan/multimedia", params, &resp, account) + if err != nil { + return nil, err + } + u := fmt.Sprintf("%s&access_token=%s", resp.List[0].Dlink, account.AccessToken) + res, err := base.NoRedirectClient.R().Head(u) + if err != nil { + return nil, err + } + if res.StatusCode() == 302 { + u = res.Header().Get("location") + } + return &base.Link{ + Url: u, + Headers: []base.Header{ + {Name: "User-Agent", Value: "pan.baidu.com"}, + }}, nil +} + +func (driver Baidu) Path(path string, account *model.Account) (*model.File, []model.File, error) { + file, err := driver.File(path, account) + if err != nil { + return nil, nil, err + } + if !file.IsDir() { + return file, nil, nil + } + files, err := driver.Files(path, account) + if err != nil { + return nil, nil, err + } + return nil, files, nil +} + +func (driver Baidu) Proxy(r *http.Request, account *model.Account) { + r.Header.Set("User-Agent", "pan.baidu.com") +} + +func (driver Baidu) Preview(path string, account *model.Account) (interface{}, error) { + return nil, base.ErrNotSupport +} + +func (driver Baidu) MakeDir(path string, account *model.Account) error { + _, err := driver.create(utils.Join(account.RootFolder, path), 0, 1, "", "", account) + return err +} + +func (driver Baidu) Move(src string, dst string, account *model.Account) error { + path := utils.Join(account.RootFolder, src) + dest, newname := utils.Split(utils.Join(account.RootFolder, dst)) + data := []base.Json{ + { + "path": path, + "dest": dest, + "newname": newname, + }, + } + _, err := driver.manage("move", data, account) + return err +} + +func (driver Baidu) Rename(src string, dst string, account *model.Account) error { + path := utils.Join(account.RootFolder, src) + newname := utils.Base(dst) + data := []base.Json{ + { + "path": path, + "newname": newname, + }, + } + _, err := driver.manage("rename", data, account) + return err +} + +func (driver Baidu) Copy(src string, dst string, account *model.Account) error { + path := utils.Join(account.RootFolder, src) + dest, newname := utils.Split(utils.Join(account.RootFolder, dst)) + data := []base.Json{ + { + "path": path, + "dest": dest, + "newname": newname, + }, + } + _, err := driver.manage("copy", data, account) + return err +} + +func (driver Baidu) Delete(path string, account *model.Account) error { + path = utils.Join(account.RootFolder, path) + data := []string{path} + _, err := driver.manage("delete", data, account) + return err +} + +func (driver Baidu) Upload(file *model.FileStream, account *model.Account) error { + if file == nil { + return base.ErrEmptyFile + } + tempFile, err := ioutil.TempFile("data/temp", "file-*") + if err != nil { + return err + } + defer func() { + _ = tempFile.Close() + _ = os.Remove(tempFile.Name()) + }() + _, err = io.Copy(tempFile, file) + if err != nil { + return err + } + _, err = tempFile.Seek(0, io.SeekStart) + if err != nil { + return err + } + var Default uint64 = 4 * 1024 * 1024 + defaultByteData := make([]byte, Default) + count := int(math.Ceil(float64(file.GetSize()) / float64(Default))) + var SliceSize uint64 = 256 * 1024 + // cal md5 + h1 := md5.New() + h2 := md5.New() + block_list := make([]string, 0) + content_md5 := "" + slice_md5 := "" + left := file.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() + } + content_md5 = hex.EncodeToString(h1.Sum(nil)) + _, err = tempFile.Seek(0, io.SeekStart) + if err != nil { + return err + } + if file.GetSize() <= SliceSize { + slice_md5 = content_md5 + } else { + sliceData := make([]byte, SliceSize) + _, err = io.ReadFull(tempFile, sliceData) + if err != nil { + return err + } + h2.Write(sliceData) + slice_md5 = hex.EncodeToString(h2.Sum(nil)) + _, err = tempFile.Seek(0, io.SeekStart) + if err != nil { + return err + } + } + path := encodeURIComponent(utils.Join(account.RootFolder, file.ParentPath, file.Name)) + block_list_str := fmt.Sprintf("[%s]", strings.Join(block_list, ",")) + data := fmt.Sprintf("path=%s&size=%d&isdir=0&autoinit=1&block_list=%s&content-md5=%s&slice-md5=%s", + path, file.GetSize(), + block_list_str, + content_md5, slice_md5) + params := map[string]string{ + "method": "precreate", + } + var precreateResp PrecreateResp + _, err = driver.Post("/xpan/file", params, data, &precreateResp, account) + if err != nil { + return err + } + log.Debugf("%+v", precreateResp) + if precreateResp.ReturnType == 2 { + return nil + } + params = map[string]string{ + "method": "upload", + "access_token": account.AccessToken, + "type": "tmpfile", + "path": path, + "uploadid": precreateResp.Uploadid, + } + left = file.GetSize() + for _, partseq := range precreateResp.BlockList { + 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://d.pcs.baidu.com/rest/2.0/pcs/superfile2" + params["partseq"] = strconv.Itoa(partseq) + res, err := base.RestyClient.R().SetQueryParams(params).SetFileReader("file", file.Name, bytes.NewReader(byteData)).Post(u) + if err != nil { + return err + } + log.Debugln(res.String()) + } + _, err = driver.create(path, file.GetSize(), 0, precreateResp.Uploadid, block_list_str, account) + return err +} + +var _ base.Driver = (*Baidu)(nil) diff --git a/drivers/baidu/types.go b/drivers/baidu/types.go new file mode 100644 index 00000000..36a21053 --- /dev/null +++ b/drivers/baidu/types.go @@ -0,0 +1,84 @@ +package baidu + +type TokenErrResp struct { + ErrorDescription string `json:"error_description"` + Error string `json:"error"` +} + +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"` +} + +type DownloadResp struct { + Errmsg string `json:"errmsg"` + Errno int `json:"errno"` + List []struct { + //Category int `json:"category"` + //DateTaken int `json:"date_taken,omitempty"` + Dlink string `json:"dlink"` + //Filename string `json:"filename"` + //FsId int64 `json:"fs_id"` + //Height int `json:"height,omitempty"` + //Isdir int `json:"isdir"` + //Md5 string `json:"md5"` + //OperId int `json:"oper_id"` + //Path string `json:"path"` + //ServerCtime int `json:"server_ctime"` + //ServerMtime int `json:"server_mtime"` + //Size int `json:"size"` + //Thumbs struct { + // Icon string `json:"icon,omitempty"` + // Url1 string `json:"url1,omitempty"` + // Url2 string `json:"url2,omitempty"` + // Url3 string `json:"url3,omitempty"` + //} `json:"thumbs"` + //Width int `json:"width,omitempty"` + } `json:"list"` + //Names struct { + //} `json:"names"` + RequestId string `json:"request_id"` +} + +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/baidu/util.go b/drivers/baidu/util.go new file mode 100644 index 00000000..bf41c7f1 --- /dev/null +++ b/drivers/baidu/util.go @@ -0,0 +1,18 @@ +package baidu + +import ( + "net/url" + "strings" + "time" +) + +func getTime(t int64) *time.Time { + tm := time.Unix(t, 0) + return &tm +} + +func encodeURIComponent(str string) string { + r := url.QueryEscape(str) + r = strings.ReplaceAll(r, "+", "%20") + return r +} diff --git a/utils/file.go b/utils/file.go index 0608b983..cfeaa049 100644 --- a/utils/file.go +++ b/utils/file.go @@ -123,3 +123,7 @@ func Join(elem ...string) string { } return res } + +func Split(p string) (string, string) { + return path.Split(p) +}