mirror of
https://github.com/AlistGo/alist.git
synced 2025-12-19 02:50:06 +08:00
feat(bitqiu): Add Bitqiu cloud drive support
- Implement the new Bitqiu cloud drive. - Add core driver logic, metadata handling, and utility functions. - Register the Bitqiu driver for use.
This commit is contained in:
parent
e2016dd031
commit
6ba97213d8
@ -21,6 +21,7 @@ import (
|
||||
_ "github.com/alist-org/alist/v3/drivers/baidu_netdisk"
|
||||
_ "github.com/alist-org/alist/v3/drivers/baidu_photo"
|
||||
_ "github.com/alist-org/alist/v3/drivers/baidu_share"
|
||||
_ "github.com/alist-org/alist/v3/drivers/bitqiu"
|
||||
_ "github.com/alist-org/alist/v3/drivers/chaoxing"
|
||||
_ "github.com/alist-org/alist/v3/drivers/cloudreve"
|
||||
_ "github.com/alist-org/alist/v3/drivers/cloudreve_v4"
|
||||
|
||||
282
drivers/bitqiu/driver.go
Normal file
282
drivers/bitqiu/driver.go
Normal file
@ -0,0 +1,282 @@
|
||||
package bitqiu
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http/cookiejar"
|
||||
"strconv"
|
||||
|
||||
"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/internal/op"
|
||||
streamPkg "github.com/alist-org/alist/v3/internal/stream"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const (
|
||||
baseURL = "https://pan.bitqiu.com"
|
||||
loginURL = baseURL + "/loginServer/login"
|
||||
listURL = baseURL + "/apiToken/cfi/fs/resources/pages"
|
||||
uploadInitializeURL = baseURL + "/apiToken/cfi/fs/upload/v2/initialize"
|
||||
|
||||
successCode = "10200"
|
||||
uploadSuccessCode = "30010"
|
||||
orgChannel = "default|default|default"
|
||||
)
|
||||
|
||||
type BitQiu struct {
|
||||
model.Storage
|
||||
Addition
|
||||
|
||||
client *resty.Client
|
||||
userID string
|
||||
}
|
||||
|
||||
func (d *BitQiu) Config() driver.Config {
|
||||
return config
|
||||
}
|
||||
|
||||
func (d *BitQiu) GetAddition() driver.Additional {
|
||||
return &d.Addition
|
||||
}
|
||||
|
||||
func (d *BitQiu) Init(ctx context.Context) error {
|
||||
if d.Addition.UserPlatform == "" {
|
||||
d.Addition.UserPlatform = uuid.NewString()
|
||||
op.MustSaveDriverStorage(d)
|
||||
}
|
||||
|
||||
if d.client == nil {
|
||||
jar, err := cookiejar.New(nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.client = base.NewRestyClient()
|
||||
d.client.SetBaseURL(baseURL)
|
||||
d.client.SetCookieJar(jar)
|
||||
}
|
||||
|
||||
return d.login(ctx)
|
||||
}
|
||||
|
||||
func (d *BitQiu) Drop(ctx context.Context) error {
|
||||
d.client = nil
|
||||
d.userID = ""
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *BitQiu) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
||||
if d.userID == "" {
|
||||
if err := d.login(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
parentID := d.resolveParentID(dir)
|
||||
dirPath := ""
|
||||
if dir != nil {
|
||||
dirPath = dir.GetPath()
|
||||
}
|
||||
pageSize := d.pageSize()
|
||||
orderType := d.orderType()
|
||||
desc := d.orderDesc()
|
||||
|
||||
var results []model.Obj
|
||||
page := 1
|
||||
for {
|
||||
form := map[string]string{
|
||||
"parentId": parentID,
|
||||
"limit": strconv.Itoa(pageSize),
|
||||
"orderType": orderType,
|
||||
"desc": desc,
|
||||
"model": "1",
|
||||
"userId": d.userID,
|
||||
"currentPage": strconv.Itoa(page),
|
||||
"page": strconv.Itoa(page),
|
||||
"org_channel": orgChannel,
|
||||
}
|
||||
var resp Response[ResourcePage]
|
||||
if err := d.postForm(ctx, listURL, form, &resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.Code != successCode {
|
||||
if resp.Code == "10401" || resp.Code == "10404" {
|
||||
if err := d.login(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
continue
|
||||
}
|
||||
return nil, fmt.Errorf("list failed: %s", resp.Message)
|
||||
}
|
||||
|
||||
objs, err := utils.SliceConvert(resp.Data.Data, func(item Resource) (model.Obj, error) {
|
||||
return item.toObject(parentID, dirPath)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
results = append(results, objs...)
|
||||
|
||||
if !resp.Data.HasNext || len(resp.Data.Data) == 0 {
|
||||
break
|
||||
}
|
||||
page++
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (d *BitQiu) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
||||
return nil, errs.NotImplement
|
||||
}
|
||||
|
||||
func (d *BitQiu) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {
|
||||
return nil, errs.NotImplement
|
||||
}
|
||||
|
||||
func (d *BitQiu) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
|
||||
return nil, errs.NotImplement
|
||||
}
|
||||
|
||||
func (d *BitQiu) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) {
|
||||
return nil, errs.NotImplement
|
||||
}
|
||||
|
||||
func (d *BitQiu) Copy(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
|
||||
return nil, errs.NotImplement
|
||||
}
|
||||
|
||||
func (d *BitQiu) Remove(ctx context.Context, obj model.Obj) error {
|
||||
return errs.NotImplement
|
||||
}
|
||||
|
||||
func (d *BitQiu) Put(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {
|
||||
if d.userID == "" {
|
||||
if err := d.login(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
up(0)
|
||||
_, md5sum, err := streamPkg.CacheFullInTempFileAndHash(file, utils.MD5)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
parentID := d.resolveParentID(dstDir)
|
||||
form := map[string]string{
|
||||
"parentId": parentID,
|
||||
"name": file.GetName(),
|
||||
"size": strconv.FormatInt(file.GetSize(), 10),
|
||||
"hash": md5sum,
|
||||
"sampleMd5": md5sum,
|
||||
"org_channel": orgChannel,
|
||||
}
|
||||
var resp Response[Resource]
|
||||
if err = d.postForm(ctx, uploadInitializeURL, form, &resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.Code != uploadSuccessCode {
|
||||
if resp.Code == successCode {
|
||||
return nil, fmt.Errorf("upload requires additional steps not implemented: %s", resp.Message)
|
||||
}
|
||||
return nil, fmt.Errorf("upload failed: %s", resp.Message)
|
||||
}
|
||||
|
||||
obj, err := resp.Data.toObject(parentID, dstDir.GetPath())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
up(100)
|
||||
return obj, nil
|
||||
}
|
||||
|
||||
func (d *BitQiu) login(ctx context.Context) error {
|
||||
if d.client == nil {
|
||||
return fmt.Errorf("client not initialized")
|
||||
}
|
||||
|
||||
form := map[string]string{
|
||||
"passport": d.Username,
|
||||
"password": utils.GetMD5EncodeStr(d.Password),
|
||||
"remember": "0",
|
||||
"captcha": "",
|
||||
"org_channel": orgChannel,
|
||||
}
|
||||
var resp Response[LoginData]
|
||||
if err := d.postForm(ctx, loginURL, form, &resp); err != nil {
|
||||
return err
|
||||
}
|
||||
if resp.Code != successCode {
|
||||
return fmt.Errorf("login failed: %s", resp.Message)
|
||||
}
|
||||
d.userID = strconv.FormatInt(resp.Data.UserID, 10)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *BitQiu) postForm(ctx context.Context, url string, form map[string]string, result interface{}) error {
|
||||
if d.client == nil {
|
||||
return fmt.Errorf("client not initialized")
|
||||
}
|
||||
req := d.client.R().
|
||||
SetContext(ctx).
|
||||
SetHeaders(d.commonHeaders()).
|
||||
SetFormData(form)
|
||||
if result != nil {
|
||||
req = req.SetResult(result)
|
||||
}
|
||||
_, err := req.Post(url)
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *BitQiu) commonHeaders() map[string]string {
|
||||
headers := map[string]string{
|
||||
"accept": "application/json, text/plain, */*",
|
||||
"accept-language": "en-US,en;q=0.9",
|
||||
"cache-control": "no-cache",
|
||||
"pragma": "no-cache",
|
||||
"user-platform": d.Addition.UserPlatform,
|
||||
"x-kl-saas-ajax-request": "Ajax_Request",
|
||||
"referer": baseURL + "/",
|
||||
"origin": baseURL,
|
||||
}
|
||||
return headers
|
||||
}
|
||||
|
||||
func (d *BitQiu) resolveParentID(dir model.Obj) string {
|
||||
if dir != nil && dir.GetID() != "" {
|
||||
return dir.GetID()
|
||||
}
|
||||
if root := d.Addition.GetRootId(); root != "" {
|
||||
return root
|
||||
}
|
||||
return config.DefaultRoot
|
||||
}
|
||||
|
||||
func (d *BitQiu) pageSize() int {
|
||||
if size, err := strconv.Atoi(d.Addition.PageSize); err == nil && size > 0 {
|
||||
return size
|
||||
}
|
||||
return 24
|
||||
}
|
||||
|
||||
func (d *BitQiu) orderType() string {
|
||||
if d.Addition.OrderType != "" {
|
||||
return d.Addition.OrderType
|
||||
}
|
||||
return "updateTime"
|
||||
}
|
||||
|
||||
func (d *BitQiu) orderDesc() string {
|
||||
if d.Addition.OrderDesc {
|
||||
return "1"
|
||||
}
|
||||
return "0"
|
||||
}
|
||||
|
||||
var _ driver.Driver = (*BitQiu)(nil)
|
||||
var _ driver.PutResult = (*BitQiu)(nil)
|
||||
27
drivers/bitqiu/meta.go
Normal file
27
drivers/bitqiu/meta.go
Normal file
@ -0,0 +1,27 @@
|
||||
package bitqiu
|
||||
|
||||
import (
|
||||
"github.com/alist-org/alist/v3/internal/driver"
|
||||
"github.com/alist-org/alist/v3/internal/op"
|
||||
)
|
||||
|
||||
type Addition struct {
|
||||
driver.RootID
|
||||
Username string `json:"username" required:"true"`
|
||||
Password string `json:"password" required:"true"`
|
||||
UserPlatform string `json:"user_platform" help:"Optional device identifier; auto-generated if empty."`
|
||||
OrderType string `json:"order_type" type:"select" options:"updateTime,createTime,name,size" default:"updateTime"`
|
||||
OrderDesc bool `json:"order_desc"`
|
||||
PageSize string `json:"page_size" default:"24" help:"Number of entries to request per page."`
|
||||
}
|
||||
|
||||
var config = driver.Config{
|
||||
Name: "BitQiu",
|
||||
DefaultRoot: "0",
|
||||
}
|
||||
|
||||
func init() {
|
||||
op.RegisterDriver(func() driver.Driver {
|
||||
return &BitQiu{}
|
||||
})
|
||||
}
|
||||
35
drivers/bitqiu/types.go
Normal file
35
drivers/bitqiu/types.go
Normal file
@ -0,0 +1,35 @@
|
||||
package bitqiu
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
type Response[T any] struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data T `json:"data"`
|
||||
}
|
||||
|
||||
type LoginData struct {
|
||||
UserID int64 `json:"userId"`
|
||||
}
|
||||
|
||||
type ResourcePage struct {
|
||||
CurrentPage int `json:"currentPage"`
|
||||
PageSize int `json:"pageSize"`
|
||||
TotalCount int `json:"totalCount"`
|
||||
TotalPageCount int `json:"totalPageCount"`
|
||||
Data []Resource `json:"data"`
|
||||
HasNext bool `json:"hasNext"`
|
||||
}
|
||||
|
||||
type Resource struct {
|
||||
ResourceID string `json:"resourceId"`
|
||||
ResourceUID string `json:"resourceUid"`
|
||||
ResourceType int `json:"resourceType"`
|
||||
ParentID string `json:"parentId"`
|
||||
Name string `json:"name"`
|
||||
ExtName string `json:"extName"`
|
||||
Size *json.Number `json:"size"`
|
||||
CreateTime *string `json:"createTime"`
|
||||
UpdateTime *string `json:"updateTime"`
|
||||
FileMD5 string `json:"fileMd5"`
|
||||
}
|
||||
60
drivers/bitqiu/util.go
Normal file
60
drivers/bitqiu/util.go
Normal file
@ -0,0 +1,60 @@
|
||||
package bitqiu
|
||||
|
||||
import (
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
)
|
||||
|
||||
type Object struct {
|
||||
model.Object
|
||||
ParentID string
|
||||
}
|
||||
|
||||
func (r Resource) toObject(parentID, parentPath string) (model.Obj, error) {
|
||||
id := r.ResourceID
|
||||
if id == "" {
|
||||
id = r.ResourceUID
|
||||
}
|
||||
obj := &Object{
|
||||
Object: model.Object{
|
||||
ID: id,
|
||||
Name: r.Name,
|
||||
IsFolder: r.ResourceType == 1,
|
||||
},
|
||||
ParentID: parentID,
|
||||
}
|
||||
if r.Size != nil {
|
||||
if size, err := (*r.Size).Int64(); err == nil {
|
||||
obj.Size = size
|
||||
}
|
||||
}
|
||||
if ct := parseBitQiuTime(r.CreateTime); !ct.IsZero() {
|
||||
obj.Ctime = ct
|
||||
}
|
||||
if mt := parseBitQiuTime(r.UpdateTime); !mt.IsZero() {
|
||||
obj.Modified = mt
|
||||
}
|
||||
if r.FileMD5 != "" {
|
||||
obj.HashInfo = utils.NewHashInfo(utils.MD5, strings.ToLower(r.FileMD5))
|
||||
}
|
||||
obj.SetPath(path.Join(parentPath, obj.Name))
|
||||
return obj, nil
|
||||
}
|
||||
|
||||
func parseBitQiuTime(value *string) time.Time {
|
||||
if value == nil {
|
||||
return time.Time{}
|
||||
}
|
||||
trimmed := strings.TrimSpace(*value)
|
||||
if trimmed == "" {
|
||||
return time.Time{}
|
||||
}
|
||||
if ts, err := time.ParseInLocation("2006-01-02 15:04:05", trimmed, time.Local); err == nil {
|
||||
return ts
|
||||
}
|
||||
return time.Time{}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user