feat: add 123pan driver

pull/1604/head
Noah Hsu 2022-09-01 22:13:37 +08:00
parent 7290f9b301
commit 284274b37e
23 changed files with 456 additions and 26 deletions

227
drivers/123/driver.go Normal file
View File

@ -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)

30
drivers/123/meta.go Normal file
View File

@ -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)
}

85
drivers/123/types.go Normal file
View File

@ -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"`
}

87
drivers/123/util.go Normal file
View File

@ -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
}

View File

@ -1,4 +1,4 @@
package local
package aliyundrive
import (
"bytes"

View File

@ -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",
}

View File

@ -1,4 +1,4 @@
package local
package aliyundrive
import (
"time"

View File

@ -1,4 +1,4 @@
package local
package aliyundrive
import (
"errors"

View File

@ -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"

View File

@ -11,7 +11,7 @@ type Addition struct {
}
var config = driver.Config{
Name: "local",
Name: "Local",
OnlyLocal: true,
LocalSort: true,
NoCache: true,

View File

@ -17,7 +17,7 @@ type Addition struct {
}
var config = driver.Config{
Name: "onedrive",
Name: "Onedrive",
LocalSort: true,
DefaultRoot: "/",
}

View File

@ -1,4 +1,4 @@
package local
package pikpak
import (
"context"

View File

@ -1,4 +1,4 @@
package local
package pikpak
import (
"github.com/alist-org/alist/v3/internal/driver"

View File

@ -1,4 +1,4 @@
package local
package pikpak
import (
"strconv"

View File

@ -1,4 +1,4 @@
package local
package pikpak
import (
"errors"

View File

@ -1,4 +1,4 @@
package local
package template
import (
"context"

View File

@ -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,

View File

@ -1 +1 @@
package local
package template

View File

@ -1,3 +1,3 @@
package local
package template
// do others that not defined in Driver interface

View File

@ -14,7 +14,7 @@ type Addition struct {
}
var config = driver.Config{
Name: "virtual",
Name: "Virtual",
OnlyLocal: true,
LocalSort: true,
NeedMs: true,

View File

@ -43,7 +43,7 @@ func TestDown(t *testing.T) {
ID: 0,
MountPath: "/",
Index: 0,
Driver: "local",
Driver: "Local",
Status: "",
Addition: `{"root_folder":"../../data"}`,
Remark: "",

View File

@ -15,7 +15,7 @@ func initDevData() {
err := op.CreateStorage(context.Background(), model.Storage{
MountPath: "/",
Index: 0,
Driver: "local",
Driver: "Local",
Status: "",
Addition: `{"root_folder":"."}`,
})

View File

@ -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)