mirror of https://github.com/Xhofe/alist
feat: add lanzou driver
parent
57686d9df1
commit
1ab73e0742
|
@ -10,6 +10,7 @@ import (
|
||||||
_ "github.com/alist-org/alist/v3/drivers/baidu_photo"
|
_ "github.com/alist-org/alist/v3/drivers/baidu_photo"
|
||||||
_ "github.com/alist-org/alist/v3/drivers/ftp"
|
_ "github.com/alist-org/alist/v3/drivers/ftp"
|
||||||
_ "github.com/alist-org/alist/v3/drivers/google_drive"
|
_ "github.com/alist-org/alist/v3/drivers/google_drive"
|
||||||
|
_ "github.com/alist-org/alist/v3/drivers/lanzou"
|
||||||
_ "github.com/alist-org/alist/v3/drivers/local"
|
_ "github.com/alist-org/alist/v3/drivers/local"
|
||||||
_ "github.com/alist-org/alist/v3/drivers/mediatrack"
|
_ "github.com/alist-org/alist/v3/drivers/mediatrack"
|
||||||
_ "github.com/alist-org/alist/v3/drivers/onedrive"
|
_ "github.com/alist-org/alist/v3/drivers/onedrive"
|
||||||
|
|
|
@ -0,0 +1,171 @@
|
||||||
|
package lanzou
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/alist-org/alist/v3/drivers/base"
|
||||||
|
"github.com/alist-org/alist/v3/internal/driver"
|
||||||
|
"github.com/alist-org/alist/v3/internal/errs"
|
||||||
|
"github.com/alist-org/alist/v3/internal/model"
|
||||||
|
"github.com/alist-org/alist/v3/pkg/utils"
|
||||||
|
"github.com/go-resty/resty/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
var upClient = base.NewRestyClient().SetTimeout(120 * time.Second)
|
||||||
|
|
||||||
|
type LanZou struct {
|
||||||
|
Addition
|
||||||
|
model.Storage
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *LanZou) Config() driver.Config {
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *LanZou) GetAddition() driver.Additional {
|
||||||
|
return d.Addition
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *LanZou) Init(ctx context.Context, storage model.Storage) error {
|
||||||
|
d.Storage = storage
|
||||||
|
err := utils.Json.UnmarshalFromString(d.Storage.Addition, &d.Addition)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if d.IsCookie() {
|
||||||
|
if d.RootFolderID == "" {
|
||||||
|
d.RootFolderID = "-1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *LanZou) Drop(ctx context.Context) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取的大小和时间不准确
|
||||||
|
func (d *LanZou) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
||||||
|
if d.IsCookie() {
|
||||||
|
return d.GetFiles(ctx, dir.GetID())
|
||||||
|
} else {
|
||||||
|
return d.GetFileOrFolderByShareUrl(ctx, dir.GetID(), d.SharePassword)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *LanZou) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
||||||
|
downID := file.GetID()
|
||||||
|
pwd := d.SharePassword
|
||||||
|
if d.IsCookie() {
|
||||||
|
share, err := d.getFileShareUrlByID(ctx, file.GetID())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
downID = share.FID
|
||||||
|
pwd = share.Pwd
|
||||||
|
}
|
||||||
|
fileInfo, err := d.getFilesByShareUrl(ctx, downID, pwd, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &model.Link{
|
||||||
|
URL: fileInfo.Url,
|
||||||
|
Header: http.Header{
|
||||||
|
"User-Agent": []string{base.UserAgent},
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *LanZou) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
|
||||||
|
if d.IsCookie() {
|
||||||
|
_, err := d.post(d.BaseUrl+"/doupload.php", func(req *resty.Request) {
|
||||||
|
req.SetContext(ctx)
|
||||||
|
req.SetFormData(map[string]string{
|
||||||
|
"task": "2",
|
||||||
|
"parent_id": parentDir.GetID(),
|
||||||
|
"folder_name": dirName,
|
||||||
|
"folder_description": "",
|
||||||
|
})
|
||||||
|
}, nil)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return errs.NotImplement
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *LanZou) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
|
||||||
|
if d.IsCookie() {
|
||||||
|
if !srcObj.IsDir() {
|
||||||
|
_, err := d.post(d.BaseUrl+"/doupload.php", func(req *resty.Request) {
|
||||||
|
req.SetContext(ctx)
|
||||||
|
req.SetFormData(map[string]string{
|
||||||
|
"task": "20",
|
||||||
|
"folder_id": dstDir.GetID(),
|
||||||
|
"file_id": srcObj.GetID(),
|
||||||
|
})
|
||||||
|
}, nil)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return errs.NotImplement
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *LanZou) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
|
||||||
|
if d.IsCookie() {
|
||||||
|
if !srcObj.IsDir() {
|
||||||
|
_, err := d.post(d.BaseUrl+"/doupload.php", func(req *resty.Request) {
|
||||||
|
req.SetContext(ctx)
|
||||||
|
req.SetFormData(map[string]string{
|
||||||
|
"task": "46",
|
||||||
|
"file_id": srcObj.GetID(),
|
||||||
|
"file_name": newName,
|
||||||
|
"type": "2",
|
||||||
|
})
|
||||||
|
}, nil)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return errs.NotImplement
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *LanZou) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
|
||||||
|
return errs.NotImplement
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *LanZou) Remove(ctx context.Context, obj model.Obj) error {
|
||||||
|
if d.IsCookie() {
|
||||||
|
_, err := d.post(d.BaseUrl+"/doupload.php", func(req *resty.Request) {
|
||||||
|
req.SetContext(ctx)
|
||||||
|
if obj.IsDir() {
|
||||||
|
req.SetFormData(map[string]string{
|
||||||
|
"task": "3",
|
||||||
|
"folder_id": obj.GetID(),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
req.SetFormData(map[string]string{
|
||||||
|
"task": "6",
|
||||||
|
"file_id": obj.GetID(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, nil)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return errs.NotImplement
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *LanZou) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
|
||||||
|
if d.IsCookie() {
|
||||||
|
_, err := d._post(d.BaseUrl+"/fileup.php", func(req *resty.Request) {
|
||||||
|
req.SetFormData(map[string]string{
|
||||||
|
"task": "1",
|
||||||
|
"id": "WU_FILE_0",
|
||||||
|
"name": stream.GetName(),
|
||||||
|
"folder_id": dstDir.GetID(),
|
||||||
|
}).SetFileReader("upload_file", stream.GetName(), stream)
|
||||||
|
}, nil, true)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return errs.NotImplement
|
||||||
|
}
|
|
@ -0,0 +1,165 @@
|
||||||
|
package lanzou
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
"unicode"
|
||||||
|
)
|
||||||
|
|
||||||
|
const DAY time.Duration = 84600000000000
|
||||||
|
|
||||||
|
var timeSplitReg = regexp.MustCompile("([0-9.]*)\\s*([\u4e00-\u9fa5]+)")
|
||||||
|
|
||||||
|
func MustParseTime(str string) time.Time {
|
||||||
|
lastOpTime, err := time.ParseInLocation("2006-01-02 -07", str+" +08", time.Local)
|
||||||
|
if err != nil {
|
||||||
|
strs := timeSplitReg.FindStringSubmatch(str)
|
||||||
|
lastOpTime = time.Now()
|
||||||
|
if len(strs) == 3 {
|
||||||
|
i, _ := strconv.ParseInt(strs[1], 10, 64)
|
||||||
|
ti := time.Duration(-i)
|
||||||
|
switch strs[2] {
|
||||||
|
case "秒前":
|
||||||
|
lastOpTime = lastOpTime.Add(time.Second * ti)
|
||||||
|
case "分钟前":
|
||||||
|
lastOpTime = lastOpTime.Add(time.Minute * ti)
|
||||||
|
case "小时前":
|
||||||
|
lastOpTime = lastOpTime.Add(time.Hour * ti)
|
||||||
|
case "天前":
|
||||||
|
lastOpTime = lastOpTime.Add(DAY * ti)
|
||||||
|
case "昨天":
|
||||||
|
lastOpTime = lastOpTime.Add(-DAY)
|
||||||
|
case "前天":
|
||||||
|
lastOpTime = lastOpTime.Add(-DAY * 2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return lastOpTime
|
||||||
|
}
|
||||||
|
|
||||||
|
var sizeSplitReg = regexp.MustCompile(`(?i)([0-9.]+)\s*([bkm]+)`)
|
||||||
|
|
||||||
|
func SizeStrToInt64(size string) int64 {
|
||||||
|
strs := sizeSplitReg.FindStringSubmatch(size)
|
||||||
|
if len(strs) < 3 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
s, _ := strconv.ParseFloat(strs[1], 64)
|
||||||
|
switch strings.ToUpper(strs[2]) {
|
||||||
|
case "B":
|
||||||
|
return int64(s)
|
||||||
|
case "K":
|
||||||
|
return int64(s * (1 << 10))
|
||||||
|
case "M":
|
||||||
|
return int64(s * (1 << 20))
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除注释
|
||||||
|
func RemoveNotes(html []byte) []byte {
|
||||||
|
return regexp.MustCompile(`<!--.*?-->|//.*|/\*.*?\*/`).ReplaceAll(html, []byte{})
|
||||||
|
}
|
||||||
|
|
||||||
|
var findAcwScV2Reg = regexp.MustCompile(`arg1='([0-9A-Z]+)'`)
|
||||||
|
|
||||||
|
// 在页面被过多访问或其他情况下,有时候会先返回一个加密的页面,其执行计算出一个acw_sc__v2后放入页面后再重新访问页面才能获得正常页面
|
||||||
|
// 若该页面进行了js加密,则进行解密,计算acw_sc__v2,并加入cookie
|
||||||
|
func CalcAcwScV2(html string) (string, error) {
|
||||||
|
acwScV2s := findAcwScV2Reg.FindStringSubmatch(html)
|
||||||
|
if len(acwScV2s) != 2 {
|
||||||
|
return "", fmt.Errorf("无法匹配acw_sc__v2")
|
||||||
|
}
|
||||||
|
return HexXor(Unbox(acwScV2s[1]), "3000176000856006061501533003690027800375"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Unbox(hex string) string {
|
||||||
|
var box = []int{6, 28, 34, 31, 33, 18, 30, 23, 9, 8, 19, 38, 17, 24, 0, 5, 32, 21, 10, 22, 25, 14, 15, 3, 16, 27, 13, 35, 2, 29, 11, 26, 4, 36, 1, 39, 37, 7, 20, 12}
|
||||||
|
var newBox = make([]byte, len(hex))
|
||||||
|
for i := 0; i < len(box); i++ {
|
||||||
|
j := box[i]
|
||||||
|
if len(newBox) > j {
|
||||||
|
newBox[j] = hex[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return string(newBox)
|
||||||
|
}
|
||||||
|
|
||||||
|
func HexXor(hex1, hex2 string) string {
|
||||||
|
out := bytes.NewBuffer(make([]byte, len(hex1)))
|
||||||
|
for i := 0; i < len(hex1) && i < len(hex2); i += 2 {
|
||||||
|
v1, _ := strconv.ParseInt(hex1[i:i+2], 16, 64)
|
||||||
|
v2, _ := strconv.ParseInt(hex2[i:i+2], 16, 64)
|
||||||
|
out.WriteString(strconv.FormatInt(v1^v2, 16))
|
||||||
|
}
|
||||||
|
return out.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
var findDataReg = regexp.MustCompile(`data[:\s]+({[^}]+})`) // 查找json
|
||||||
|
var findKVReg = regexp.MustCompile(`'(.+?)':('?([^' },]*)'?)`) // 拆分kv
|
||||||
|
|
||||||
|
// 根据key查询js变量
|
||||||
|
func findJSVarFunc(key, data string) string {
|
||||||
|
values := regexp.MustCompile(`var ` + key + ` = '(.+?)';`).FindStringSubmatch(data)
|
||||||
|
if len(values) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return values[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析html中的JSON
|
||||||
|
func htmlJsonToMap(html string) (map[string]string, error) {
|
||||||
|
datas := findDataReg.FindStringSubmatch(html)
|
||||||
|
if len(datas) != 2 {
|
||||||
|
return nil, fmt.Errorf("not find data")
|
||||||
|
}
|
||||||
|
return jsonToMap(datas[1], html), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func jsonToMap(data, html string) map[string]string {
|
||||||
|
var param = make(map[string]string)
|
||||||
|
kvs := findKVReg.FindAllStringSubmatch(data, -1)
|
||||||
|
for _, kv := range kvs {
|
||||||
|
k, v := kv[1], kv[3]
|
||||||
|
if v == "" || strings.Contains(kv[2], "'") || IsNumber(kv[2]) {
|
||||||
|
param[k] = v
|
||||||
|
} else {
|
||||||
|
param[k] = findJSVarFunc(v, html)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return param
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsNumber(str string) bool {
|
||||||
|
for _, s := range str {
|
||||||
|
if !unicode.IsDigit(s) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
var findFromReg = regexp.MustCompile(`data : '(.+?)'`) // 查找from字符串
|
||||||
|
|
||||||
|
// 解析html中的from
|
||||||
|
func htmlFormToMap(html string) (map[string]string, error) {
|
||||||
|
froms := findFromReg.FindStringSubmatch(html)
|
||||||
|
if len(froms) != 2 {
|
||||||
|
return nil, fmt.Errorf("not find file sgin")
|
||||||
|
}
|
||||||
|
return fromToMap(froms[1]), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func fromToMap(from string) map[string]string {
|
||||||
|
var param = make(map[string]string)
|
||||||
|
for _, kv := range strings.Split(from, "&") {
|
||||||
|
kv := strings.SplitN(kv, "=", 2)[:2]
|
||||||
|
param[kv[0]] = kv[1]
|
||||||
|
}
|
||||||
|
return param
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
package lanzou
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/alist-org/alist/v3/internal/driver"
|
||||||
|
"github.com/alist-org/alist/v3/internal/op"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Addition struct {
|
||||||
|
Type string `json:"type" type:"select" options:"cookie,url" default:"cookie"`
|
||||||
|
Cookie string `json:"cookie" required:"true" help:"about 15 days valid"`
|
||||||
|
driver.RootID
|
||||||
|
SharePassword string `json:"share_password"`
|
||||||
|
BaseUrl string `json:"baseUrl" required:"true" default:"https://pc.woozooo.com"`
|
||||||
|
ShareUrl string `json:"shareUrl" required:"true" default:"https://pan.lanzouo.com"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Addition) IsCookie() bool {
|
||||||
|
return a.Type == "cookie"
|
||||||
|
}
|
||||||
|
|
||||||
|
var config = driver.Config{
|
||||||
|
Name: "Lanzou",
|
||||||
|
LocalSort: true,
|
||||||
|
DefaultRoot: "-1",
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
op.RegisterDriver(config, func() driver.Driver {
|
||||||
|
return &LanZou{}
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,131 @@
|
||||||
|
package lanzou
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/alist-org/alist/v3/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
type FilesOrFoldersResp struct {
|
||||||
|
Text []FileOrFolder `json:"text"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FileOrFolder struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
//Onof string `json:"onof"` // 是否存在提取码
|
||||||
|
//IsLock string `json:"is_lock"`
|
||||||
|
//IsCopyright int `json:"is_copyright"`
|
||||||
|
|
||||||
|
// 文件通用
|
||||||
|
ID string `json:"id"`
|
||||||
|
NameAll string `json:"name_all"`
|
||||||
|
Size string `json:"size"`
|
||||||
|
Time string `json:"time"`
|
||||||
|
//Icon string `json:"icon"`
|
||||||
|
//Downs string `json:"downs"`
|
||||||
|
//Filelock string `json:"filelock"`
|
||||||
|
//IsBakdownload int `json:"is_bakdownload"`
|
||||||
|
//Bakdownload string `json:"bakdownload"`
|
||||||
|
//IsDes int `json:"is_des"` // 是否存在描述
|
||||||
|
//IsIco int `json:"is_ico"`
|
||||||
|
|
||||||
|
// 文件夹
|
||||||
|
FolID string `json:"fol_id"`
|
||||||
|
//Folderlock string `json:"folderlock"`
|
||||||
|
//FolderDes string `json:"folder_des"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FileOrFolder) isFloder() bool {
|
||||||
|
return f.FolID != ""
|
||||||
|
}
|
||||||
|
func (f *FileOrFolder) ToObj() model.Obj {
|
||||||
|
obj := &model.Object{}
|
||||||
|
if f.isFloder() {
|
||||||
|
obj.ID = f.FolID
|
||||||
|
obj.Name = f.Name
|
||||||
|
obj.Modified = time.Now()
|
||||||
|
obj.IsFolder = true
|
||||||
|
} else {
|
||||||
|
obj.ID = f.ID
|
||||||
|
obj.Name = f.NameAll
|
||||||
|
obj.Modified = MustParseTime(f.Time)
|
||||||
|
obj.Size = SizeStrToInt64(f.Size)
|
||||||
|
}
|
||||||
|
return obj
|
||||||
|
}
|
||||||
|
|
||||||
|
type FileShareResp struct {
|
||||||
|
Info FileShare `json:"info"`
|
||||||
|
}
|
||||||
|
type FileShare struct {
|
||||||
|
Pwd string `json:"pwd"`
|
||||||
|
Onof string `json:"onof"`
|
||||||
|
Taoc string `json:"taoc"`
|
||||||
|
IsNewd string `json:"is_newd"`
|
||||||
|
|
||||||
|
// 文件
|
||||||
|
FID string `json:"f_id"`
|
||||||
|
|
||||||
|
// 文件夹
|
||||||
|
NewUrl string `json:"new_url"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Des string `json:"des"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FileOrFolderByShareUrlResp struct {
|
||||||
|
Text []FileOrFolderByShareUrl `json:"text"`
|
||||||
|
}
|
||||||
|
type FileOrFolderByShareUrl struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
NameAll string `json:"name_all"`
|
||||||
|
Size string `json:"size"`
|
||||||
|
Time string `json:"time"`
|
||||||
|
Duan string `json:"duan"`
|
||||||
|
//Icon string `json:"icon"`
|
||||||
|
//PIco int `json:"p_ico"`
|
||||||
|
//T int `json:"t"`
|
||||||
|
IsFloder bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FileOrFolderByShareUrl) ToObj() model.Obj {
|
||||||
|
return &model.Object{
|
||||||
|
ID: f.ID,
|
||||||
|
Name: f.NameAll,
|
||||||
|
Size: SizeStrToInt64(f.Size),
|
||||||
|
Modified: MustParseTime(f.Time),
|
||||||
|
IsFolder: f.IsFloder,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type FileShareInfoAndUrlResp[T string | int] struct {
|
||||||
|
Dom string `json:"dom"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
Inf T `json:"inf"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *FileShareInfoAndUrlResp[T]) GetBaseUrl() string {
|
||||||
|
return fmt.Sprint(u.Dom, "/file")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *FileShareInfoAndUrlResp[T]) GetDownloadUrl() string {
|
||||||
|
return fmt.Sprint(u.GetBaseUrl(), "/", u.URL)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通过分享链接获取文件信息和下载链接
|
||||||
|
type FileInfoAndUrlByShareUrl struct {
|
||||||
|
ID string
|
||||||
|
Name string
|
||||||
|
Size string
|
||||||
|
Time string
|
||||||
|
Url string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FileInfoAndUrlByShareUrl) ToObj() model.Obj {
|
||||||
|
return &model.Object{
|
||||||
|
ID: f.ID,
|
||||||
|
Name: f.Name,
|
||||||
|
Size: SizeStrToInt64(f.Size),
|
||||||
|
Modified: MustParseTime(f.Time),
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,416 @@
|
||||||
|
package lanzou
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"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"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (d *LanZou) get(url string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
|
||||||
|
return d.request(url, http.MethodGet, callback, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *LanZou) post(url string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
|
||||||
|
return d._post(url, callback, resp, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *LanZou) _post(url string, callback base.ReqCallback, resp interface{}, up bool) ([]byte, error) {
|
||||||
|
data, err := d.request(url, http.MethodPost, callback, up)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
switch utils.Json.Get(data, "zt").ToInt() {
|
||||||
|
case 1, 2, 4:
|
||||||
|
if resp != nil {
|
||||||
|
// 返回类型不统一,忽略错误
|
||||||
|
utils.Json.Unmarshal(data, resp)
|
||||||
|
}
|
||||||
|
return data, nil
|
||||||
|
default:
|
||||||
|
info := utils.Json.Get(data, "inf").ToString()
|
||||||
|
if info == "" {
|
||||||
|
info = utils.Json.Get(data, "info").ToString()
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf(info)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *LanZou) request(url string, method string, callback base.ReqCallback, up bool) ([]byte, error) {
|
||||||
|
var req *resty.Request
|
||||||
|
if up {
|
||||||
|
req = upClient.R()
|
||||||
|
} else {
|
||||||
|
req = base.RestyClient.R()
|
||||||
|
}
|
||||||
|
|
||||||
|
req.SetHeaders(map[string]string{
|
||||||
|
"Referer": "https://pc.woozooo.com",
|
||||||
|
})
|
||||||
|
|
||||||
|
if d.Cookie != "" {
|
||||||
|
req.SetHeader("cookie", d.Cookie)
|
||||||
|
}
|
||||||
|
|
||||||
|
if callback != nil {
|
||||||
|
callback(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := req.Execute(method, url)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.Body(), err
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
通过cookie获取数据
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 获取文件和文件夹,获取到的文件大小、更改时间不可信
|
||||||
|
func (d *LanZou) GetFiles(ctx context.Context, folderID string) ([]model.Obj, error) {
|
||||||
|
folders, err := d.getFolders(ctx, folderID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
files, err := d.getFiles(ctx, folderID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
objs := make([]model.Obj, 0, len(folders)+len(files))
|
||||||
|
for _, folder := range folders {
|
||||||
|
objs = append(objs, folder.ToObj())
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, file := range files {
|
||||||
|
objs = append(objs, file.ToObj())
|
||||||
|
}
|
||||||
|
return objs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通过ID获取文件夹
|
||||||
|
func (d *LanZou) getFolders(ctx context.Context, folderID string) ([]FileOrFolder, error) {
|
||||||
|
var resp FilesOrFoldersResp
|
||||||
|
_, err := d.post(d.BaseUrl+"/doupload.php", func(req *resty.Request) {
|
||||||
|
req.SetContext(ctx)
|
||||||
|
req.SetFormData(map[string]string{
|
||||||
|
"task": "47",
|
||||||
|
"folder_id": folderID,
|
||||||
|
})
|
||||||
|
}, &resp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return resp.Text, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通过ID获取文件
|
||||||
|
func (d *LanZou) getFiles(ctx context.Context, folderID string) ([]FileOrFolder, error) {
|
||||||
|
files := make([]FileOrFolder, 0)
|
||||||
|
for pg := 1; ; pg++ {
|
||||||
|
var resp FilesOrFoldersResp
|
||||||
|
_, err := d.post(d.BaseUrl+"/doupload.php", func(req *resty.Request) {
|
||||||
|
req.SetContext(ctx)
|
||||||
|
req.SetFormData(map[string]string{
|
||||||
|
"task": "5",
|
||||||
|
"folder_id": folderID,
|
||||||
|
"pg": strconv.Itoa(pg),
|
||||||
|
})
|
||||||
|
}, &resp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(resp.Text) == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
files = append(files, resp.Text...)
|
||||||
|
}
|
||||||
|
return files, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通过ID获取文件夹分享地址
|
||||||
|
func (d *LanZou) getFolderShareUrlByID(ctx context.Context, fileID string) (share FileShare, err error) {
|
||||||
|
var resp FileShareResp
|
||||||
|
_, err = d.post(d.BaseUrl+"/doupload.php", func(req *resty.Request) {
|
||||||
|
req.SetContext(ctx)
|
||||||
|
req.SetFormData(map[string]string{
|
||||||
|
"task": "18",
|
||||||
|
"file_id": fileID,
|
||||||
|
})
|
||||||
|
}, &resp)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
share = resp.Info
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通过ID获取文件分享地址
|
||||||
|
func (d *LanZou) getFileShareUrlByID(ctx context.Context, fileID string) (share FileShare, err error) {
|
||||||
|
var resp FileShareResp
|
||||||
|
_, err = d.post(d.BaseUrl+"/doupload.php", func(req *resty.Request) {
|
||||||
|
req.SetContext(ctx)
|
||||||
|
req.SetFormData(map[string]string{
|
||||||
|
"task": "22",
|
||||||
|
"file_id": fileID,
|
||||||
|
})
|
||||||
|
}, &resp)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
share = resp.Info
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
通过分享链接获取数据
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 判断类容
|
||||||
|
var isFileReg = regexp.MustCompile(`class="fileinfo"|id="file"|文件描述`)
|
||||||
|
var isFolderReg = regexp.MustCompile(`id="infos"`)
|
||||||
|
|
||||||
|
// 获取文件文件夹基础信息
|
||||||
|
var nameFindReg = regexp.MustCompile(`<title>(.+?) - 蓝奏云</title>|id="filenajax">(.+?)</div>|var filename = '(.+?)';|<div style="font-size.+?>([^<>].+?)</div>|<div class="filethetext".+?>([^<>]+?)</div>`)
|
||||||
|
var sizeFindReg = regexp.MustCompile(`(?i)大小\W*([0-9.]+\s*[bkm]+)`)
|
||||||
|
var timeFindReg = regexp.MustCompile(`\d+\s*[秒天分小][钟时]?前|[昨前]天|\d{4}-\d{2}-\d{2}`)
|
||||||
|
|
||||||
|
var findSubFolaerReg = regexp.MustCompile(`(folderlink|mbxfolder).+href="/(.+?)"(.+filename")?>(.+?)<`) // 查找分享文件夹子文件夹ID和名称
|
||||||
|
|
||||||
|
// 获取关键数据
|
||||||
|
var findDownPageParamReg = regexp.MustCompile(`<iframe.*?src="(.+?)"`)
|
||||||
|
|
||||||
|
// 通过分享链接获取文件或文件夹,如果是文件则会返回下载链接
|
||||||
|
func (d *LanZou) GetFileOrFolderByShareUrl(ctx context.Context, downID, pwd string) ([]model.Obj, error) {
|
||||||
|
pageData, err := d.get(fmt.Sprint(d.ShareUrl, "/", downID), func(req *resty.Request) { req.SetContext(ctx) }, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
pageData = RemoveNotes(pageData)
|
||||||
|
|
||||||
|
var objs []model.Obj
|
||||||
|
if !isFileReg.Match(pageData) {
|
||||||
|
files, err := d.getFolderByShareUrl(ctx, downID, pwd, pageData)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
objs = make([]model.Obj, 0, len(files))
|
||||||
|
for _, file := range files {
|
||||||
|
objs = append(objs, file.ToObj())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
file, err := d.getFilesByShareUrl(ctx, downID, pwd, pageData)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
objs = []model.Obj{file.ToObj()}
|
||||||
|
}
|
||||||
|
return objs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通过分享链接获取文件(下载链接也使用此方法)
|
||||||
|
// 参考 https://github.com/zaxtyson/LanZouCloud-API/blob/ab2e9ec715d1919bf432210fc16b91c6775fbb99/lanzou/api/core.py#L440
|
||||||
|
func (d *LanZou) getFilesByShareUrl(ctx context.Context, downID, pwd string, firstPageData []byte) (file FileInfoAndUrlByShareUrl, err error) {
|
||||||
|
if firstPageData == nil {
|
||||||
|
firstPageData, err = d.get(fmt.Sprint(d.ShareUrl, "/", downID), func(req *resty.Request) { req.SetContext(ctx) }, nil)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
firstPageData = RemoveNotes(firstPageData)
|
||||||
|
}
|
||||||
|
firstPageDataStr := string(firstPageData)
|
||||||
|
|
||||||
|
if strings.Contains(firstPageDataStr, "acw_sc__v2") {
|
||||||
|
var vs string
|
||||||
|
if vs, err = CalcAcwScV2(firstPageDataStr); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
firstPageData, err = d.get(fmt.Sprint(d.ShareUrl, "/", downID), func(req *resty.Request) {
|
||||||
|
req.SetCookie(&http.Cookie{
|
||||||
|
Name: "acw_sc__v2",
|
||||||
|
Value: vs,
|
||||||
|
})
|
||||||
|
req.SetContext(ctx)
|
||||||
|
}, nil)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
firstPageData = RemoveNotes(firstPageData)
|
||||||
|
firstPageDataStr = string(firstPageData)
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
param map[string]string
|
||||||
|
downloadUrl string
|
||||||
|
baseUrl string
|
||||||
|
)
|
||||||
|
|
||||||
|
// 需要密码
|
||||||
|
if strings.Contains(firstPageDataStr, "pwdload") || strings.Contains(firstPageDataStr, "passwddiv") {
|
||||||
|
param, err = htmlFormToMap(firstPageDataStr)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
param["p"] = pwd
|
||||||
|
var resp FileShareInfoAndUrlResp[string]
|
||||||
|
_, err = d.post(d.ShareUrl+"/ajaxm.php", func(req *resty.Request) { req.SetFormData(param).SetContext(ctx) }, &resp)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
file.Name = resp.Inf
|
||||||
|
baseUrl = resp.GetBaseUrl()
|
||||||
|
downloadUrl = resp.GetDownloadUrl()
|
||||||
|
} else {
|
||||||
|
urlpaths := findDownPageParamReg.FindStringSubmatch(firstPageDataStr)
|
||||||
|
if len(urlpaths) != 2 {
|
||||||
|
err = fmt.Errorf("not find file page param")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var nextPageData []byte
|
||||||
|
nextPageData, err = d.get(fmt.Sprint(d.ShareUrl, urlpaths[1]), func(req *resty.Request) { req.SetContext(ctx) }, nil)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
nextPageData = RemoveNotes(nextPageData)
|
||||||
|
nextPageDataStr := string(nextPageData)
|
||||||
|
|
||||||
|
param, err = htmlJsonToMap(nextPageDataStr)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var resp FileShareInfoAndUrlResp[int]
|
||||||
|
_, err = d.post(d.ShareUrl+"/ajaxm.php", func(req *resty.Request) { req.SetFormData(param).SetContext(ctx) }, &resp)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
baseUrl = resp.GetBaseUrl()
|
||||||
|
downloadUrl = resp.GetDownloadUrl()
|
||||||
|
|
||||||
|
names := nameFindReg.FindStringSubmatch(firstPageDataStr)
|
||||||
|
if len(names) > 1 {
|
||||||
|
for _, name := range names[1:] {
|
||||||
|
if name != "" {
|
||||||
|
file.Name = name
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sizes := sizeFindReg.FindStringSubmatch(firstPageDataStr)
|
||||||
|
if len(sizes) == 2 {
|
||||||
|
file.Size = sizes[1]
|
||||||
|
}
|
||||||
|
file.ID = downID
|
||||||
|
file.Time = timeFindReg.FindString(firstPageDataStr)
|
||||||
|
|
||||||
|
// 重定向获取真实链接
|
||||||
|
res, err := base.NoRedirectClient.R().SetHeaders(map[string]string{
|
||||||
|
"accept-language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
|
||||||
|
}).SetContext(ctx).Get(downloadUrl)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
file.Url = res.Header().Get("location")
|
||||||
|
|
||||||
|
// 触发验证
|
||||||
|
rPageDataStr := res.String()
|
||||||
|
if res.StatusCode() != 302 && strings.Contains(rPageDataStr, "网络异常") {
|
||||||
|
param, err = htmlJsonToMap(rPageDataStr)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
param["el"] = "2"
|
||||||
|
time.Sleep(time.Second * 2)
|
||||||
|
|
||||||
|
// 通过验证获取直连
|
||||||
|
var rUrl struct {
|
||||||
|
Url string `json:"url"`
|
||||||
|
}
|
||||||
|
_, err = d.post(fmt.Sprint(baseUrl, "/ajax.php"), func(req *resty.Request) { req.SetContext(ctx).SetFormData(param) }, &rUrl)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
file.Url = rUrl.Url
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通过分享链接获取文件夹
|
||||||
|
// 参考 https://github.com/zaxtyson/LanZouCloud-API/blob/ab2e9ec715d1919bf432210fc16b91c6775fbb99/lanzou/api/core.py#L1089
|
||||||
|
func (d *LanZou) getFolderByShareUrl(ctx context.Context, downID, pwd string, firstPageData []byte) ([]FileOrFolderByShareUrl, error) {
|
||||||
|
if firstPageData == nil {
|
||||||
|
var err error
|
||||||
|
firstPageData, err = d.get(fmt.Sprint(d.ShareUrl, "/", downID), func(req *resty.Request) { req.SetContext(ctx) }, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
firstPageData = RemoveNotes(firstPageData)
|
||||||
|
}
|
||||||
|
firstPageDataStr := string(firstPageData)
|
||||||
|
|
||||||
|
//
|
||||||
|
if strings.Contains(firstPageDataStr, "acw_sc__v2") {
|
||||||
|
vs, err := CalcAcwScV2(firstPageDataStr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
firstPageData, err = d.get(fmt.Sprint(d.ShareUrl, "/", downID), func(req *resty.Request) {
|
||||||
|
req.SetCookie(&http.Cookie{
|
||||||
|
Name: "acw_sc__v2",
|
||||||
|
Value: vs,
|
||||||
|
})
|
||||||
|
req.SetContext(ctx)
|
||||||
|
}, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
firstPageData = RemoveNotes(firstPageData)
|
||||||
|
firstPageDataStr = string(firstPageData)
|
||||||
|
}
|
||||||
|
|
||||||
|
from, err := htmlJsonToMap(firstPageDataStr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
from["pwd"] = pwd
|
||||||
|
|
||||||
|
files := make([]FileOrFolderByShareUrl, 0)
|
||||||
|
// vip获取文件夹
|
||||||
|
floders := findSubFolaerReg.FindAllStringSubmatch(firstPageDataStr, -1)
|
||||||
|
for _, floder := range floders {
|
||||||
|
if len(floder) == 5 {
|
||||||
|
files = append(files, FileOrFolderByShareUrl{
|
||||||
|
ID: floder[2],
|
||||||
|
NameAll: floder[4],
|
||||||
|
IsFloder: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for page := 1; ; page++ {
|
||||||
|
from["pg"] = strconv.Itoa(page)
|
||||||
|
var resp FileOrFolderByShareUrlResp
|
||||||
|
_, err := d.post(d.ShareUrl+"/filemoreajax.php", func(req *resty.Request) { req.SetFormData(from).SetContext(ctx) }, &resp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
files = append(files, resp.Text...)
|
||||||
|
if len(resp.Text) == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
time.Sleep(time.Millisecond * 600)
|
||||||
|
}
|
||||||
|
return files, nil
|
||||||
|
}
|
Loading…
Reference in New Issue