mirror of https://github.com/Xhofe/alist
feat: add 123pan driver
parent
7290f9b301
commit
284274b37e
|
@ -0,0 +1,227 @@
|
|||
package _123
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/alist-org/alist/v3/drivers/base"
|
||||
"github.com/alist-org/alist/v3/internal/conf"
|
||||
"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/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/credentials"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/aws/aws-sdk-go/service/s3/s3manager"
|
||||
"github.com/go-resty/resty/v2"
|
||||
)
|
||||
|
||||
type Pan123 struct {
|
||||
model.Storage
|
||||
Addition
|
||||
AccessToken string
|
||||
}
|
||||
|
||||
func (d *Pan123) Config() driver.Config {
|
||||
return config
|
||||
}
|
||||
|
||||
func (d *Pan123) GetAddition() driver.Additional {
|
||||
return d.Addition
|
||||
}
|
||||
|
||||
func (d *Pan123) 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
|
||||
}
|
||||
return d.login()
|
||||
}
|
||||
|
||||
func (d *Pan123) Drop(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Pan123) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
||||
files, err := d.getFiles(dir.GetID())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return utils.SliceConvert(files, func(src File) (model.Obj, error) {
|
||||
return src, nil
|
||||
})
|
||||
}
|
||||
|
||||
//func (d *Pan123) Get(ctx context.Context, path string) (model.Obj, error) {
|
||||
// // TODO this is optional
|
||||
// return nil, errs.NotImplement
|
||||
//}
|
||||
|
||||
func (d *Pan123) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
||||
if f, ok := file.(File); ok {
|
||||
return &model.Link{
|
||||
URL: f.DownloadUrl,
|
||||
}, nil
|
||||
} else {
|
||||
return nil, fmt.Errorf("can't convert obj")
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Pan123) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
|
||||
data := base.Json{
|
||||
"driveId": 0,
|
||||
"etag": "",
|
||||
"fileName": dirName,
|
||||
"parentFileId": parentDir.GetID(),
|
||||
"size": 0,
|
||||
"type": 1,
|
||||
}
|
||||
_, err := d.request("https://www.123pan.com/api/file/upload_request", http.MethodPost, func(req *resty.Request) {
|
||||
req.SetBody(data)
|
||||
}, nil)
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *Pan123) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
|
||||
data := base.Json{
|
||||
"fileIdList": []base.Json{{"FileId": srcObj.GetID()}},
|
||||
"parentFileId": dstDir.GetID(),
|
||||
}
|
||||
_, err := d.request("https://www.123pan.com/api/file/mod_pid", http.MethodPost, func(req *resty.Request) {
|
||||
req.SetBody(data)
|
||||
}, nil)
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *Pan123) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
|
||||
data := base.Json{
|
||||
"driveId": 0,
|
||||
"fileId": srcObj.GetID(),
|
||||
"fileName": newName,
|
||||
}
|
||||
_, err := d.request("https://www.123pan.com/api/file/rename", http.MethodPost, func(req *resty.Request) {
|
||||
req.SetBody(data)
|
||||
}, nil)
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *Pan123) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
|
||||
return errs.NotSupport
|
||||
}
|
||||
|
||||
func (d *Pan123) Remove(ctx context.Context, obj model.Obj) error {
|
||||
if f, ok := obj.(File); ok {
|
||||
data := base.Json{
|
||||
"driveId": 0,
|
||||
"operation": true,
|
||||
"fileTrashInfoList": []File{f},
|
||||
}
|
||||
_, err := d.request("https://www.123pan.com/a/api/file/trash", http.MethodPost, func(req *resty.Request) {
|
||||
req.SetBody(data)
|
||||
}, nil)
|
||||
return err
|
||||
} else {
|
||||
return fmt.Errorf("can't convert obj")
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Pan123) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
|
||||
const DEFAULT int64 = 10485760
|
||||
var uploadFile io.Reader
|
||||
h := md5.New()
|
||||
if d.StreamUpload && stream.GetSize() > DEFAULT {
|
||||
// 只计算前10MIB
|
||||
buf := bytes.NewBuffer(make([]byte, 0, DEFAULT))
|
||||
if n, err := io.CopyN(io.MultiWriter(buf, h), stream, DEFAULT); err != io.EOF && n == 0 {
|
||||
return err
|
||||
}
|
||||
// 增加额外参数防止MD5碰撞
|
||||
h.Write([]byte(stream.GetName()))
|
||||
num := make([]byte, 8)
|
||||
binary.BigEndian.PutUint64(num, uint64(stream.GetSize()))
|
||||
h.Write(num)
|
||||
// 拼装
|
||||
uploadFile = io.MultiReader(buf, stream)
|
||||
} else {
|
||||
// 计算完整文件MD5
|
||||
tempFile, err := os.CreateTemp(conf.Conf.TempDir, "file-*")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
_ = tempFile.Close()
|
||||
_ = os.Remove(tempFile.Name())
|
||||
}()
|
||||
|
||||
if _, err = io.Copy(io.MultiWriter(tempFile, h), stream); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tempFile.Seek(0, io.SeekStart)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
uploadFile = tempFile
|
||||
}
|
||||
etag := hex.EncodeToString(h.Sum(nil))
|
||||
data := base.Json{
|
||||
"driveId": 0,
|
||||
"duplicate": 2, // 2->覆盖 1->重命名 0->默认
|
||||
"etag": etag,
|
||||
"fileName": stream.GetName(),
|
||||
"parentFileId": dstDir.GetID(),
|
||||
"size": stream.GetSize(),
|
||||
"type": 0,
|
||||
}
|
||||
var resp UploadResp
|
||||
_, err := d.request("https://www.123pan.com/api/file/upload_request", http.MethodPost, func(req *resty.Request) {
|
||||
req.SetBody(data)
|
||||
}, &resp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resp.Data.Key == "" {
|
||||
return nil
|
||||
}
|
||||
cfg := &aws.Config{
|
||||
Credentials: credentials.NewStaticCredentials(resp.Data.AccessKeyId, resp.Data.SecretAccessKey, resp.Data.SessionToken),
|
||||
Region: aws.String("123pan"),
|
||||
Endpoint: aws.String("file.123pan.com"),
|
||||
S3ForcePathStyle: aws.Bool(true),
|
||||
}
|
||||
s, err := session.NewSession(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
uploader := s3manager.NewUploader(s)
|
||||
input := &s3manager.UploadInput{
|
||||
Bucket: &resp.Data.Bucket,
|
||||
Key: &resp.Data.Key,
|
||||
Body: uploadFile,
|
||||
}
|
||||
_, err = uploader.Upload(input)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = d.request("https://www.123pan.com/api/file/upload_complete", http.MethodPost, func(req *resty.Request) {
|
||||
req.SetBody(base.Json{
|
||||
"fileId": resp.Data.FileId,
|
||||
})
|
||||
}, nil)
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *Pan123) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
|
||||
return nil, errs.NotSupport
|
||||
}
|
||||
|
||||
var _ driver.Driver = (*Pan123)(nil)
|
|
@ -0,0 +1,30 @@
|
|||
package _123
|
||||
|
||||
import (
|
||||
"github.com/alist-org/alist/v3/internal/driver"
|
||||
"github.com/alist-org/alist/v3/internal/op"
|
||||
)
|
||||
|
||||
type Addition struct {
|
||||
Username string `json:"username" required:"true"`
|
||||
Password string `json:"password" required:"true"`
|
||||
OrderBy string `json:"order_by" type:"select" options:"name,fileId,updateAt,createAt" default:"name"`
|
||||
OrderDirection string `json:"order_direction" type:"select" options:"asc,desc" default:"asc"`
|
||||
driver.RootFolderId
|
||||
// define other
|
||||
StreamUpload bool `json:"stream_upload"`
|
||||
//Field string `json:"field" type:"select" required:"true" options:"a,b,c" default:"a"`
|
||||
}
|
||||
|
||||
var config = driver.Config{
|
||||
Name: "123Pan",
|
||||
DefaultRoot: "0",
|
||||
}
|
||||
|
||||
func New() driver.Driver {
|
||||
return &Pan123{}
|
||||
}
|
||||
|
||||
func init() {
|
||||
op.RegisterDriver(config, New)
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
package _123
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
)
|
||||
|
||||
type BaseResp struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
type TokenResp struct {
|
||||
BaseResp
|
||||
Data struct {
|
||||
Token string `json:"token"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
type File struct {
|
||||
FileName string `json:"FileName"`
|
||||
Size int64 `json:"Size"`
|
||||
UpdateAt time.Time `json:"UpdateAt"`
|
||||
FileId int64 `json:"FileId"`
|
||||
Type int `json:"Type"`
|
||||
Etag string `json:"Etag"`
|
||||
S3KeyFlag string `json:"S3KeyFlag"`
|
||||
DownloadUrl string `json:"DownloadUrl"`
|
||||
}
|
||||
|
||||
func (f File) GetSize() int64 {
|
||||
return f.Size
|
||||
}
|
||||
|
||||
func (f File) GetName() string {
|
||||
return f.FileName
|
||||
}
|
||||
|
||||
func (f File) ModTime() time.Time {
|
||||
return f.UpdateAt
|
||||
}
|
||||
|
||||
func (f File) IsDir() bool {
|
||||
return f.Type == 1
|
||||
}
|
||||
|
||||
func (f File) GetID() string {
|
||||
return strconv.FormatInt(f.FileId, 10)
|
||||
}
|
||||
|
||||
var _ model.Obj = (*File)(nil)
|
||||
|
||||
//func (f File) Thumb() string {
|
||||
//
|
||||
//}
|
||||
//var _ model.Thumb = (*File)(nil)
|
||||
|
||||
type Files struct {
|
||||
BaseResp
|
||||
Data struct {
|
||||
InfoList []File `json:"InfoList"`
|
||||
Next string `json:"Next"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
type DownResp struct {
|
||||
BaseResp
|
||||
Data struct {
|
||||
DownloadUrl string `json:"DownloadUrl"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
type UploadResp struct {
|
||||
BaseResp
|
||||
Data struct {
|
||||
AccessKeyId string `json:"AccessKeyId"`
|
||||
Bucket string `json:"Bucket"`
|
||||
Key string `json:"Key"`
|
||||
SecretAccessKey string `json:"SecretAccessKey"`
|
||||
SessionToken string `json:"SessionToken"`
|
||||
FileId int64 `json:"FileId"`
|
||||
} `json:"data"`
|
||||
}
|
|
@ -0,0 +1,87 @@
|
|||
package _123
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/alist-org/alist/v3/drivers/base"
|
||||
"github.com/go-resty/resty/v2"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
)
|
||||
|
||||
// do others that not defined in Driver interface
|
||||
|
||||
func (d *Pan123) login() error {
|
||||
url := "https://www.123pan.com/api/user/sign_in"
|
||||
var resp TokenResp
|
||||
_, err := base.RestyClient.R().
|
||||
SetResult(&resp).
|
||||
SetBody(base.Json{
|
||||
"passport": d.Username,
|
||||
"password": d.Password,
|
||||
}).Post(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resp.Code != 200 {
|
||||
err = fmt.Errorf(resp.Message)
|
||||
} else {
|
||||
d.AccessToken = resp.Data.Token
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *Pan123) request(url string, method string, callback func(*resty.Request), resp interface{}) ([]byte, error) {
|
||||
req := base.RestyClient.R()
|
||||
req.SetHeader("Authorization", "Bearer "+d.AccessToken)
|
||||
if callback != nil {
|
||||
callback(req)
|
||||
}
|
||||
if resp != nil {
|
||||
req.SetResult(resp)
|
||||
}
|
||||
res, err := req.Execute(method, url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
body := res.Body()
|
||||
code := jsoniter.Get(body, "code").ToInt()
|
||||
if code != 0 {
|
||||
if code == 401 {
|
||||
err := d.login()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return d.request(url, method, callback, resp)
|
||||
}
|
||||
return nil, errors.New(jsoniter.Get(body, "message").ToString())
|
||||
}
|
||||
return body, nil
|
||||
}
|
||||
|
||||
func (d *Pan123) getFiles(parentId string) ([]File, error) {
|
||||
next := "0"
|
||||
res := make([]File, 0)
|
||||
for next != "-1" {
|
||||
var resp Files
|
||||
query := map[string]string{
|
||||
"driveId": "0",
|
||||
"limit": "100",
|
||||
"next": next,
|
||||
"orderBy": d.OrderBy,
|
||||
"orderDirection": d.OrderDirection,
|
||||
"parentFileId": parentId,
|
||||
"trashed": "false",
|
||||
}
|
||||
_, err := d.request("https://www.123pan.com/api/file/list/new", http.MethodGet, func(req *resty.Request) {
|
||||
req.SetQueryParams(query)
|
||||
}, &resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
next = resp.Data.Next
|
||||
res = append(res, resp.Data.InfoList...)
|
||||
}
|
||||
return res, nil
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package local
|
||||
package aliyundrive
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
package local
|
||||
package aliyundrive
|
||||
|
||||
import (
|
||||
"github.com/alist-org/alist/v3/internal/driver"
|
||||
|
@ -14,7 +14,7 @@ type Addition struct {
|
|||
}
|
||||
|
||||
var config = driver.Config{
|
||||
Name: "aliyundrive",
|
||||
Name: "Aliyundrive",
|
||||
DefaultRoot: "root",
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
package local
|
||||
package aliyundrive
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
package local
|
||||
package aliyundrive
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package drivers
|
||||
|
||||
import (
|
||||
_ "github.com/alist-org/alist/v3/drivers/123"
|
||||
_ "github.com/alist-org/alist/v3/drivers/aliyundrive"
|
||||
_ "github.com/alist-org/alist/v3/drivers/local"
|
||||
_ "github.com/alist-org/alist/v3/drivers/onedrive"
|
||||
|
|
|
@ -11,7 +11,7 @@ type Addition struct {
|
|||
}
|
||||
|
||||
var config = driver.Config{
|
||||
Name: "local",
|
||||
Name: "Local",
|
||||
OnlyLocal: true,
|
||||
LocalSort: true,
|
||||
NoCache: true,
|
||||
|
|
|
@ -17,7 +17,7 @@ type Addition struct {
|
|||
}
|
||||
|
||||
var config = driver.Config{
|
||||
Name: "onedrive",
|
||||
Name: "Onedrive",
|
||||
LocalSort: true,
|
||||
DefaultRoot: "/",
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
package local
|
||||
package pikpak
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
package local
|
||||
package pikpak
|
||||
|
||||
import (
|
||||
"github.com/alist-org/alist/v3/internal/driver"
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
package local
|
||||
package pikpak
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
package local
|
||||
package pikpak
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
package local
|
||||
package template
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
package local
|
||||
package template
|
||||
|
||||
import (
|
||||
"github.com/alist-org/alist/v3/internal/driver"
|
||||
|
@ -14,7 +14,7 @@ type Addition struct {
|
|||
}
|
||||
|
||||
var config = driver.Config{
|
||||
Name: "template",
|
||||
Name: "Template",
|
||||
LocalSort: false,
|
||||
OnlyLocal: false,
|
||||
OnlyProxy: false,
|
||||
|
|
|
@ -1 +1 @@
|
|||
package local
|
||||
package template
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
package local
|
||||
package template
|
||||
|
||||
// do others that not defined in Driver interface
|
||||
|
|
|
@ -14,7 +14,7 @@ type Addition struct {
|
|||
}
|
||||
|
||||
var config = driver.Config{
|
||||
Name: "virtual",
|
||||
Name: "Virtual",
|
||||
OnlyLocal: true,
|
||||
LocalSort: true,
|
||||
NeedMs: true,
|
||||
|
|
|
@ -43,7 +43,7 @@ func TestDown(t *testing.T) {
|
|||
ID: 0,
|
||||
MountPath: "/",
|
||||
Index: 0,
|
||||
Driver: "local",
|
||||
Driver: "Local",
|
||||
Status: "",
|
||||
Addition: `{"root_folder":"../../data"}`,
|
||||
Remark: "",
|
||||
|
|
|
@ -15,7 +15,7 @@ func initDevData() {
|
|||
err := op.CreateStorage(context.Background(), model.Storage{
|
||||
MountPath: "/",
|
||||
Index: 0,
|
||||
Driver: "local",
|
||||
Driver: "Local",
|
||||
Status: "",
|
||||
Addition: `{"root_folder":"."}`,
|
||||
})
|
||||
|
|
|
@ -25,8 +25,8 @@ func TestCreateStorage(t *testing.T) {
|
|||
storage model.Storage
|
||||
isErr bool
|
||||
}{
|
||||
{storage: model.Storage{Driver: "local", MountPath: "/local", Addition: `{"root_folder":"."}`}, isErr: false},
|
||||
{storage: model.Storage{Driver: "local", MountPath: "/local", Addition: `{"root_folder":"."}`}, isErr: true},
|
||||
{storage: model.Storage{Driver: "Local", MountPath: "/local", Addition: `{"root_folder":"."}`}, isErr: false},
|
||||
{storage: model.Storage{Driver: "Local", MountPath: "/local", Addition: `{"root_folder":"."}`}, isErr: true},
|
||||
{storage: model.Storage{Driver: "None", MountPath: "/none", Addition: `{"root_folder":"."}`}, isErr: true},
|
||||
}
|
||||
for _, storage := range storages {
|
||||
|
@ -70,11 +70,11 @@ func TestGetBalancedStorage(t *testing.T) {
|
|||
|
||||
func setupStorages(t *testing.T) {
|
||||
var storages = []model.Storage{
|
||||
{Driver: "local", MountPath: "/a/b", Index: 0, Addition: `{"root_folder":"."}`},
|
||||
{Driver: "local", MountPath: "/a/c", Index: 1, Addition: `{"root_folder":"."}`},
|
||||
{Driver: "local", MountPath: "/a/d", Index: 2, Addition: `{"root_folder":"."}`},
|
||||
{Driver: "local", MountPath: "/a/d/e", Index: 3, Addition: `{"root_folder":"."}`},
|
||||
{Driver: "local", MountPath: "/a/d/e.balance", Index: 4, Addition: `{"root_folder":"."}`},
|
||||
{Driver: "Local", MountPath: "/a/b", Index: 0, Addition: `{"root_folder":"."}`},
|
||||
{Driver: "Local", MountPath: "/a/c", Index: 1, Addition: `{"root_folder":"."}`},
|
||||
{Driver: "Local", MountPath: "/a/d", Index: 2, Addition: `{"root_folder":"."}`},
|
||||
{Driver: "Local", MountPath: "/a/d/e", Index: 3, Addition: `{"root_folder":"."}`},
|
||||
{Driver: "Local", MountPath: "/a/d/e.balance", Index: 4, Addition: `{"root_folder":"."}`},
|
||||
}
|
||||
for _, storage := range storages {
|
||||
err := op.CreateStorage(context.Background(), storage)
|
||||
|
|
Loading…
Reference in New Issue