diff --git a/drivers/all.go b/drivers/all.go index cf0fb8e5..a913a028 100644 --- a/drivers/all.go +++ b/drivers/all.go @@ -8,7 +8,9 @@ import ( _ "github.com/Xhofe/alist/drivers/alidrive" _ "github.com/Xhofe/alist/drivers/alist" _ "github.com/Xhofe/alist/drivers/baidu" + _ "github.com/Xhofe/alist/drivers/baiduphoto" "github.com/Xhofe/alist/drivers/base" + _ "github.com/Xhofe/alist/drivers/chaoxing" _ "github.com/Xhofe/alist/drivers/ftp" _ "github.com/Xhofe/alist/drivers/google" _ "github.com/Xhofe/alist/drivers/lanzou" @@ -25,7 +27,6 @@ import ( _ "github.com/Xhofe/alist/drivers/webdav" _ "github.com/Xhofe/alist/drivers/xunlei" _ "github.com/Xhofe/alist/drivers/yandex" - _ "github.com/Xhofe/alist/drivers/baiduphoto" log "github.com/sirupsen/logrus" "strings" ) diff --git a/drivers/chaoxing/chaoxing.go b/drivers/chaoxing/chaoxing.go new file mode 100644 index 00000000..730a9a76 --- /dev/null +++ b/drivers/chaoxing/chaoxing.go @@ -0,0 +1,90 @@ +package template + +import ( + "errors" + "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" + "path" +) + +func LoginOrRefreshToken(account *model.Account) error { + return nil +} + +func Request(u string, method int, headers, query, form map[string]string, data interface{}, resp interface{}, account *model.Account) ([]byte, error) { + req := base.RestyClient.R() + req.SetHeaders(map[string]string{ + "Authorization": "Bearer" + account.AccessToken, + "Accept": "application/json, text/plain, */*", + }) + 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 e Resp + var err error + var res *resty.Response + req.SetError(&e) + switch method { + case base.Get: + res, err = req.Get(u) + case base.Post: + res, err = req.Post(u) + case base.Delete: + res, err = req.Delete(u) + case base.Patch: + res, err = req.Patch(u) + case base.Put: + res, err = req.Put(u) + default: + return nil, base.ErrNotSupport + } + if err != nil { + return nil, err + } + if e.Code >= 400 { + if e.Code == 401 { + err = LoginOrRefreshToken(account) + if err != nil { + return nil, err + } + return Request(u, method, headers, query, form, data, resp, account) + } + return nil, errors.New(e.Message) + } + return res.Body(), nil +} + +func (driver ChaoxingDrive) formatFile(f *File) *model.File { + file := model.File{ + Id: f.Id, + Name: f.FileName, + Size: f.Size, + Driver: driver.Config().Name, + UpdatedAt: f.UpdatedAt, + } + if f.File { + file.Type = utils.GetFileType(path.Ext(f.FileName)) + } else { + file.Type = conf.FOLDER + } + return &file +} + +func init() { + base.RegisterDriver(&ChaoxingDrive{}) +} diff --git a/drivers/chaoxing/driver.go b/drivers/chaoxing/driver.go new file mode 100644 index 00000000..7811a1b1 --- /dev/null +++ b/drivers/chaoxing/driver.go @@ -0,0 +1,261 @@ +package template + +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/robfig/cron/v3" + log "github.com/sirupsen/logrus" + "path/filepath" + "strings" +) + +type ChaoxingDrive struct { + base.Base +} + +func (driver ChaoxingDrive) Config() base.DriverConfig { + return base.DriverConfig{ + Name: "ChaoXing", + OnlyProxy: true, + OnlyLocal: false, + ApiProxy: false, + NoNeedSetLink: false, + NoCors: false, + LocalSort: false, + } +} + +func (driver ChaoxingDrive) Items() []base.Item { + // TODO fill need info + return []base.Item{ + { + Name: "username", + Label: "手机号/超星号", + Type: base.TypeString, + Required: true, + }, + { + Name: "password", + Label: "enc (加密后的密码)", + Type: base.TypeString, + Required: true, + }, + { + Name: "root_folder", + Label: "根目录id", + Type: base.TypeString, + Default: "", + Required: false, + }, + { + Name: "limit", + Label: "目录文件上限", + Type: base.TypeNumber, + Default: "50", + Required: false, + }, + } +} + +// Save 用户更新账号信息 +func (driver ChaoxingDrive) Save(account *model.Account, old *model.Account) error { + // TODO test available or init + if old != nil { + conf.Cron.Remove(cron.EntryID(old.CronId)) + } + if account == nil { + return nil + } + if account.Limit <= 0 { + account.Limit = 50 + } + + // 登录网盘并取得 Enc 字符串 ( enc保存在 account.AccessSecret 中; cookie 以键值对的形式保存在 account.AccessToken 中) + err := driver.GetEnc(account) + if err != nil { + return err + } + + // 每隔一天重新获取一次 Enc(和 + cronId, err := conf.Cron.AddFunc("@every 24h", func() { + id := account.ID + log.Debugf("ali account id: %d", id) + newAccount, err := model.GetAccountById(id) + log.Debugf("ali account: %+v", newAccount) + if err != nil { + return + } + err = driver.GetEnc(newAccount) + _ = model.SaveAccount(newAccount) + }) + + // 记录当前计划任务的id + account.CronId = int(cronId) + err = model.SaveAccount(account) + + if err != nil { + return err + } + return nil +} + +// File 通过用户路径获取到文件对象(主要是id号) +func (driver ChaoxingDrive) File(path string, account *model.Account) (*model.File, error) { + path = utils.ParsePath(path) + if path == "/" { + return &model.File{ + Id: "", + 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 +} + +// Files 列出所有文件 +func (driver ChaoxingDrive) Files(path string, account *model.Account) ([]model.File, error) { + path = utils.ParsePath(path) + // 先从缓存中获取结果 + cache, err := base.GetCache(path, account) + var fileList []model.File + if err == nil { + // 缓存命中,将目录信息保存到变量中 + fileList = cache.([]model.File) + } else { + // 缓存未命中 + //尝试获取上级目录的id(递归地尝试从上级目录的缓存中读取信息,直到获取到根目录为止) + file, err := driver.File(path, account) + if err != nil { + return nil, err + } + //列出上级目录的文件 + fileList, err = driver.ListFile(file.Id, account) + if err != nil { + return nil, err + } + // 缓存数据 + if len(fileList) > 0 { + _ = base.SetCache(path, fileList, account) + } + } + files := make([]model.File, 0) + for _, file := range fileList { + files = append(files, file) + } + return files, nil +} + +var api_file_link = "https://pan-yz.chaoxing.com/download/downloadfile?fleid=%s&puid=1" + +// Link 返回传入路径对应的文件的直链(本地除外),并包含需要携带的请求头 +func (driver ChaoxingDrive) Link(args base.Args, account *model.Account) (*base.Link, error) { + // TODO get file link + file, e := driver.File(args.Path, account) + if e != nil { + return nil, e + } + url := fmt.Sprintf(api_file_link, file.Id[:strings.Index(file.Id, "_")]) + //var resp base.Json + //var err Resp + + // https://pan-yz.chaoxing.com/download/downloadfile?fleid=582519780768600064&puid=1 + //_, e = chaoxingClient.R().SetResult(&resp).SetError(&err). + // SetHeader("Cookie", account.AccessToken). + // SetHeader("Referer", "https://pan-yz.chaoxing.com/"). + // Get(url) + return &base.Link{ + Headers: []base.Header{ + { + Name: "Referer", + Value: "https://pan-yz.chaoxing.com/", + }, + //{ + // Name: "Cookie", + // Value: account.AccessToken, + //}, + }, + Url: url, + }, nil +} + +// Path 通过调用上述的File与Files函数判断是文件还是文件夹,并进行返回,当是文件时附带文件的直链。 +func (driver ChaoxingDrive) Path(path string, account *model.Account) (*model.File, []model.File, error) { + path = utils.ParsePath(path) + 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 +} + +// Optional function +//func (driver ChaoxingDrive) Preview(path string, account *model.Account) (interface{}, error) { +// //TODO preview interface if driver support +// return nil, base.ErrNotImplement +//} + +func (driver ChaoxingDrive) MakeDir(path string, account *model.Account) error { + // 三个参数缺一不可,均不能写死 + // https://pan-yz.chaoxing.com/opt/newfolder?parentId=205255741446029312&name=test&puid=54351295 + + return base.ErrNotImplement +} + +func (driver ChaoxingDrive) Move(src string, dst string, account *model.Account) error { + // 注意 folderid 是由 {id}_{puid} 组成的(截取前两段即可) + // https://pan-yz.chaoxing.com/opt/moveres?folderid=762268051373813760_54351295&resids=762263362701209600, + return base.ErrNotImplement +} + +func (driver ChaoxingDrive) Rename(src string, dst string, account *model.Account) error { + // resid 就是 fileid + // https://pan-yz.chaoxing.com/opt/rename?resid=762263362701209600&name=test.pdf&puid=54351295 + return base.ErrNotImplement +} + +// 超星网盘不支持复制 +//func (driver ChaoxingDrive) Copy(src string, dst string, account *model.Account) error { +// //TODO copy file/dir +// return base.ErrNotImplement +//} + +// Delete 这个函数太危险了,不想实现 +func (driver ChaoxingDrive) Delete(path string, account *model.Account) error { + // 删除单个文件 + // https://pan-yz.chaoxing.com/opt/delres?resids=762268051373813760&resourcetype=0&puids=54351295 + // 删除多个文件 + // https://pan-yz.chaoxing.com/opt/delres?resids=762269933587513344,762269920078848000,&resourcetype=0,0,&puids=54351295,54351295, + return base.ErrNotImplement +} + +//func (driver ChaoxingDrive) Upload(file *model.FileStream, account *model.Account) error { +// //TODO upload file +// return base.ErrNotImplement +//} + +var _ base.Driver = (*ChaoxingDrive)(nil) diff --git a/drivers/chaoxing/types.go b/drivers/chaoxing/types.go new file mode 100644 index 00000000..0ae73a9b --- /dev/null +++ b/drivers/chaoxing/types.go @@ -0,0 +1,27 @@ +package template + +import "time" + +// write all struct here + +// TYPE_CX_FILE type=1 文件 +var TYPE_CX_FILE = int64(1) + +// TYPE_CX_FOLDER type=2 文件夹 +var TYPE_CX_FOLDER = int64(2) + +// TYPE_CX_SHARED_ROOT type=4 共享文件的根目录 +var TYPE_CX_SHARED_ROOT = int64(4) + +type Resp struct { + Code int `json:"code"` + Message string `json:"message"` +} + +type File struct { + Id string `json:"id"` + FileName string `json:"file_name"` + Size int64 `json:"size"` + File bool `json:"file"` + UpdatedAt *time.Time `json:"updated_at"` +} diff --git a/drivers/chaoxing/util.go b/drivers/chaoxing/util.go new file mode 100644 index 00000000..f18aa85e --- /dev/null +++ b/drivers/chaoxing/util.go @@ -0,0 +1,151 @@ +package template + +import ( + "encoding/json" + "fmt" + "github.com/Xhofe/alist/conf" + "github.com/Xhofe/alist/drivers/base" + "github.com/Xhofe/alist/model" + "github.com/Xhofe/alist/utils" + "github.com/bitly/go-simplejson" + "github.com/go-resty/resty/v2" + "regexp" + "strconv" + "strings" + "time" +) + +// write util func here, such as cal sign + +var chaoxingClient = resty.New() + +var form_login_fmt = "fid=-1&uname=%s&password=%s&t=true&forbidotherlogin=0&validate=&doubleFactorLogin=0" + +var api_list_root = "https://pan-yz.chaoxing.com/opt/listres?page=1&size=%d&enc=%s" +var api_list_file = "https://pan-yz.chaoxing.com/opt/listres?puid=%s&shareid=%s&parentId=%s&page=1&size=%d&enc=%s" +var api_list_shared_root = "https://pan-yz.chaoxing.com/opt/listres?puid=0&shareid=-1&parentId=0&page=1&size=%d&enc=%s" + +var reg_enc_fmt = regexp.MustCompile("enc[ ]*=\"(.*)\"") + +func (driver ChaoxingDrive) Login(account *model.Account) error { + url := "https://passport2.chaoxing.com/fanyalogin" + var resp base.Json + var err Resp + + req_body := fmt.Sprintf(form_login_fmt, account.Username, account.Password) + + loginReq, e := chaoxingClient.R().SetBody(req_body). + SetResult(&resp).SetError(&err). + SetHeader("Content-Length", strconv.FormatInt(int64(len(req_body)), 10)). + SetHeader("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8"). + SetHeader("Host", "passport2.chaoxing.com"). + SetHeader("User-Agent", " Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.69 Safari/537.36"). + SetHeader("X-Requested-With", "XMLHttpRequest"). + Post(url) + + if e != nil { + return e + } + + account.AccessToken = "" + for _, cookie := range loginReq.Cookies() { + //route=9d169c0aea4b7c89fa0d073417b5645f; + account.AccessToken += fmt.Sprintf("%s=%s; ", cookie.Name, cookie.Value) + } + + return nil +} + +func (driver ChaoxingDrive) GetEnc(account *model.Account) error { + url := "https://pan-yz.chaoxing.com/" + + encReq, e := chaoxingClient.R().SetHeader("Cookie", account.AccessToken).Get(url) + if e != nil { + return e + } + + //直接读取响应 + sresp := string(encReq.Body()) + submatch := reg_enc_fmt.FindAllStringSubmatch(sresp, 1) + //第一次获取失败,可能是未登录 + if len(submatch) == 0 { + e = driver.Login(account) + if e != nil { + return e + } + encReq, e = chaoxingClient.R().SetHeader("Cookie", account.AccessToken).Get(url) + if e != nil { + return e + } + sresp = string(encReq.Body()) + submatch = reg_enc_fmt.FindAllStringSubmatch(sresp, 1) + if len(submatch) == 0 { + account.Status = "failed" + return fmt.Errorf("登录失败,服务器返回信息:%s", sresp) + } + } + enc := submatch[0][1] + account.AccessSecret = enc + account.Status = "work" + return nil +} + +func (driver ChaoxingDrive) ListFile(folder_id string, account *model.Account) ([]model.File, error) { + var url string + //按规则解析 id 号 + folder_id_info := strings.Split(folder_id, "_") + if len(folder_id_info) == 3 { + folder_id = folder_id_info[0] + folder_puid := folder_id_info[1] + folder_shareid := folder_id_info[2] + if folder_id == "0" { + //访问“共享给我的文件夹” + url = fmt.Sprintf(api_list_shared_root, account.Limit, account.AccessSecret) + } else { + //访问其他目录 + url = fmt.Sprintf(api_list_file, folder_puid, folder_shareid, folder_id, account.Limit, account.AccessSecret) + } + } else { + //id无法解析为三段,应当是访问根目录(此时为 "") + url = fmt.Sprintf(api_list_root, account.Limit, account.AccessSecret) + } + + listFileReq, e := chaoxingClient.R().SetHeader("Cookie", account.AccessToken).Post(url) + resp, e := simplejson.NewJson(listFileReq.Body()) + if e != nil || resp == nil { + return nil, e + } + + files := make([]model.File, 0) + array, _ := resp.Get("list").Array() + + for _, file := range array { + var f = model.File{} + file_ := file.(map[string]interface{}) + //f.Id = file_["id"].(string) + f.Id = fmt.Sprintf("%s_%s_%s", file_["id"].(string), file_["puid"].(json.Number).String(), file_["shareid"].(json.Number).String()) + f.Name = file_["name"].(string) + f_server_type, _ := file_["type"].(json.Number).Int64() + if f_server_type != TYPE_CX_SHARED_ROOT { + f.Size, _ = file_["filesize"].(json.Number).Int64() + } + // 为文件分配类型 + switch f_server_type { + case TYPE_CX_FILE: + { + f.Type = utils.GetFileType(file_["suffix"].(string)) + } + case TYPE_CX_FOLDER: + f.Type = conf.FOLDER + case TYPE_CX_SHARED_ROOT: + f.Type = conf.FOLDER + } + modifyDate, e := time.Parse("2006-01-02 15:04:05", file_["modifyDate"].(string)) + if e == nil { + f.UpdatedAt = &modifyDate + } + f.Thumbnail = file_["thumbnail"].(string) + files = append(files, f) + } + return files, nil +} diff --git a/go.mod b/go.mod index c1733935..0ca84268 100644 --- a/go.mod +++ b/go.mod @@ -87,3 +87,8 @@ require ( gopkg.in/yaml.v2 v2.4.0 // indirect k8s.io/apimachinery v0.0.0-20191123233150-4c4803ed55e3 // indirect ) + +require ( + github.com/bitly/go-simplejson v0.5.0 + github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 // indirect +) \ No newline at end of file diff --git a/go.sum b/go.sum index cfb8beeb..fae74f0b 100644 --- a/go.sum +++ b/go.sum @@ -1010,3 +1010,7 @@ rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI= sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU= +github.com/bitly/go-simplejson v0.5.0 h1:6IH+V8/tVMab511d5bn4M7EwGXZf9Hj6i2xSwkNEM+Y= +github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= +github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= +github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= \ No newline at end of file