mirror of https://github.com/Xhofe/alist
feat(123_open): add new driver support for 123 Open (#9246)
- Implement new driver for 123 Open service, enabling file operations such as listing, uploading, moving, and removing files. - Introduce token management for authentication and authorization. - Add API integration for various file operations and actions. - Include utility functions for handling API requests and responses. - Register the new driver in the existing drivers' list.pull/9249/head
parent
46de9e9ebb
commit
52da07e8a7
|
@ -0,0 +1,191 @@
|
|||
package _123Open
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
const (
|
||||
// baseurl
|
||||
ApiBaseURL = "https://open-api.123pan.com"
|
||||
|
||||
// auth
|
||||
ApiToken = "/api/v1/access_token"
|
||||
|
||||
// file list
|
||||
ApiFileList = "/api/v2/file/list"
|
||||
|
||||
// direct link
|
||||
ApiGetDirectLink = "/api/v1/direct-link/url"
|
||||
|
||||
// mkdir
|
||||
ApiMakeDir = "/upload/v1/file/mkdir"
|
||||
|
||||
// remove
|
||||
ApiRemove = "/api/v1/file/trash"
|
||||
|
||||
// upload
|
||||
ApiUploadDomainURL = "/upload/v2/file/domain"
|
||||
ApiSingleUploadURL = "/upload/v2/file/single/create"
|
||||
ApiCreateUploadURL = "/upload/v2/file/create"
|
||||
ApiUploadSliceURL = "/upload/v2/file/slice"
|
||||
ApiUploadCompleteURL = "/upload/v2/file/upload_complete"
|
||||
|
||||
// move
|
||||
ApiMove = "/api/v1/file/move"
|
||||
|
||||
// rename
|
||||
ApiRename = "/api/v1/file/name"
|
||||
)
|
||||
|
||||
type Response[T any] struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data T `json:"data"`
|
||||
}
|
||||
|
||||
type TokenResp struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data TokenData `json:"data"`
|
||||
}
|
||||
|
||||
type TokenData struct {
|
||||
AccessToken string `json:"accessToken"`
|
||||
ExpiredAt string `json:"expiredAt"`
|
||||
}
|
||||
|
||||
type FileListResp struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data FileListData `json:"data"`
|
||||
}
|
||||
|
||||
type FileListData struct {
|
||||
LastFileId int64 `json:"lastFileId"`
|
||||
FileList []File `json:"fileList"`
|
||||
}
|
||||
|
||||
type DirectLinkResp struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data DirectLinkData `json:"data"`
|
||||
}
|
||||
|
||||
type DirectLinkData struct {
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
type MakeDirRequest struct {
|
||||
Name string `json:"name"`
|
||||
ParentID int64 `json:"parentID"`
|
||||
}
|
||||
|
||||
type MakeDirResp struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data MakeDirData `json:"data"`
|
||||
}
|
||||
|
||||
type MakeDirData struct {
|
||||
DirID int64 `json:"dirID"`
|
||||
}
|
||||
|
||||
type RemoveRequest struct {
|
||||
FileIDs []int64 `json:"fileIDs"`
|
||||
}
|
||||
|
||||
type UploadCreateResp struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data UploadCreateData `json:"data"`
|
||||
}
|
||||
|
||||
type UploadCreateData struct {
|
||||
FileID int64 `json:"fileId"`
|
||||
Reuse bool `json:"reuse"`
|
||||
PreuploadID string `json:"preuploadId"`
|
||||
SliceSize int64 `json:"sliceSize"`
|
||||
Servers []string `json:"servers"`
|
||||
}
|
||||
|
||||
type UploadUrlResp struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data UploadUrlData `json:"data"`
|
||||
}
|
||||
|
||||
type UploadUrlData struct {
|
||||
PresignedURL string `json:"presignedUrl"`
|
||||
}
|
||||
|
||||
type UploadCompleteResp struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data UploadCompleteData `json:"data"`
|
||||
}
|
||||
|
||||
type UploadCompleteData struct {
|
||||
FileID int `json:"fileID"`
|
||||
Completed bool `json:"completed"`
|
||||
}
|
||||
|
||||
func (d *Open123) Request(endpoint string, method string, setup func(*resty.Request), result any) (*resty.Response, error) {
|
||||
client := resty.New()
|
||||
token, err := d.tm.getToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req := client.R().
|
||||
SetHeader("Authorization", "Bearer "+token).
|
||||
SetHeader("Platform", "open_platform").
|
||||
SetHeader("Content-Type", "application/json").
|
||||
SetResult(result)
|
||||
|
||||
if setup != nil {
|
||||
setup(req)
|
||||
}
|
||||
|
||||
switch method {
|
||||
case http.MethodGet:
|
||||
return req.Get(ApiBaseURL + endpoint)
|
||||
case http.MethodPost:
|
||||
return req.Post(ApiBaseURL + endpoint)
|
||||
case http.MethodPut:
|
||||
return req.Put(ApiBaseURL + endpoint)
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported method: %s", method)
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Open123) RequestTo(fullURL string, method string, setup func(*resty.Request), result any) (*resty.Response, error) {
|
||||
client := resty.New()
|
||||
|
||||
token, err := d.tm.getToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req := client.R().
|
||||
SetHeader("Authorization", "Bearer "+token).
|
||||
SetHeader("Platform", "open_platform").
|
||||
SetHeader("Content-Type", "application/json").
|
||||
SetResult(result)
|
||||
|
||||
if setup != nil {
|
||||
setup(req)
|
||||
}
|
||||
|
||||
switch method {
|
||||
case http.MethodGet:
|
||||
return req.Get(fullURL)
|
||||
case http.MethodPost:
|
||||
return req.Post(fullURL)
|
||||
case http.MethodPut:
|
||||
return req.Put(fullURL)
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported method: %s", method)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,277 @@
|
|||
package _123Open
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"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/stream"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"net/http"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type Open123 struct {
|
||||
model.Storage
|
||||
Addition
|
||||
|
||||
UploadThread int
|
||||
tm *tokenManager
|
||||
}
|
||||
|
||||
func (d *Open123) Config() driver.Config {
|
||||
return config
|
||||
}
|
||||
|
||||
func (d *Open123) GetAddition() driver.Additional {
|
||||
return &d.Addition
|
||||
}
|
||||
|
||||
func (d *Open123) Init(ctx context.Context) error {
|
||||
d.tm = newTokenManager(d.ClientID, d.ClientSecret)
|
||||
|
||||
if _, err := d.tm.getToken(); err != nil {
|
||||
return fmt.Errorf("token 初始化失败: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Open123) Drop(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Open123) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
||||
parentFileId, err := strconv.ParseInt(dir.GetID(), 10, 64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fileLastId := int64(0)
|
||||
var results []File
|
||||
|
||||
for fileLastId != -1 {
|
||||
files, err := d.getFiles(parentFileId, 100, fileLastId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, f := range files.Data.FileList {
|
||||
if f.Trashed == 0 {
|
||||
results = append(results, f)
|
||||
}
|
||||
}
|
||||
fileLastId = files.Data.LastFileId
|
||||
}
|
||||
|
||||
objs := make([]model.Obj, 0, len(results))
|
||||
for _, f := range results {
|
||||
objs = append(objs, f)
|
||||
}
|
||||
return objs, nil
|
||||
}
|
||||
|
||||
func (d *Open123) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
||||
if file.IsDir() {
|
||||
return nil, errs.LinkIsDir
|
||||
}
|
||||
|
||||
fileID := file.GetID()
|
||||
|
||||
var result DirectLinkResp
|
||||
url := fmt.Sprintf("%s?fileID=%s", ApiGetDirectLink, fileID)
|
||||
_, err := d.Request(url, http.MethodGet, nil, &result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if result.Code != 0 {
|
||||
return nil, fmt.Errorf("get link failed: %s", result.Message)
|
||||
}
|
||||
|
||||
return &model.Link{
|
||||
URL: result.Data.URL,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *Open123) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {
|
||||
parentID, err := strconv.ParseInt(parentDir.GetID(), 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid parent ID: %w", err)
|
||||
}
|
||||
|
||||
var result MakeDirResp
|
||||
reqBody := MakeDirRequest{
|
||||
Name: dirName,
|
||||
ParentID: parentID,
|
||||
}
|
||||
|
||||
_, err = d.Request(ApiMakeDir, http.MethodPost, func(r *resty.Request) {
|
||||
r.SetBody(reqBody)
|
||||
}, &result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if result.Code != 0 {
|
||||
return nil, fmt.Errorf("mkdir failed: %s", result.Message)
|
||||
}
|
||||
|
||||
newDir := File{
|
||||
FileId: result.Data.DirID,
|
||||
FileName: dirName,
|
||||
Type: 1,
|
||||
ParentFileId: int(parentID),
|
||||
Size: 0,
|
||||
Trashed: 0,
|
||||
}
|
||||
return newDir, nil
|
||||
}
|
||||
|
||||
func (d *Open123) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
|
||||
srcID, err := strconv.ParseInt(srcObj.GetID(), 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid src file ID: %w", err)
|
||||
}
|
||||
dstID, err := strconv.ParseInt(dstDir.GetID(), 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid dest dir ID: %w", err)
|
||||
}
|
||||
|
||||
var result Response[any]
|
||||
reqBody := map[string]interface{}{
|
||||
"fileIDs": []int64{srcID},
|
||||
"toParentFileID": dstID,
|
||||
}
|
||||
|
||||
_, err = d.Request(ApiMove, http.MethodPost, func(r *resty.Request) {
|
||||
r.SetBody(reqBody)
|
||||
}, &result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if result.Code != 0 {
|
||||
return nil, fmt.Errorf("move failed: %s", result.Message)
|
||||
}
|
||||
|
||||
files, err := d.getFiles(dstID, 100, 0)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("move succeed but failed to get target dir: %w", err)
|
||||
}
|
||||
for _, f := range files.Data.FileList {
|
||||
if f.FileId == srcID {
|
||||
return f, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("move succeed but file not found in target dir")
|
||||
}
|
||||
|
||||
func (d *Open123) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) {
|
||||
srcID, err := strconv.ParseInt(srcObj.GetID(), 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid file ID: %w", err)
|
||||
}
|
||||
|
||||
var result Response[any]
|
||||
reqBody := map[string]interface{}{
|
||||
"fileId": srcID,
|
||||
"fileName": newName,
|
||||
}
|
||||
|
||||
_, err = d.Request(ApiRename, http.MethodPut, func(r *resty.Request) {
|
||||
r.SetBody(reqBody)
|
||||
}, &result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if result.Code != 0 {
|
||||
return nil, fmt.Errorf("rename failed: %s", result.Message)
|
||||
}
|
||||
|
||||
parentID := 0
|
||||
if file, ok := srcObj.(File); ok {
|
||||
parentID = file.ParentFileId
|
||||
}
|
||||
files, err := d.getFiles(int64(parentID), 100, 0)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("rename succeed but failed to get parent dir: %w", err)
|
||||
}
|
||||
for _, f := range files.Data.FileList {
|
||||
if f.FileId == srcID {
|
||||
return f, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("rename succeed but file not found in parent dir")
|
||||
}
|
||||
|
||||
func (d *Open123) Copy(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
|
||||
return nil, errs.NotSupport
|
||||
}
|
||||
|
||||
func (d *Open123) Remove(ctx context.Context, obj model.Obj) error {
|
||||
idStr := obj.GetID()
|
||||
id, err := strconv.ParseInt(idStr, 10, 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid file ID: %w", err)
|
||||
}
|
||||
|
||||
var result Response[any]
|
||||
reqBody := RemoveRequest{
|
||||
FileIDs: []int64{id},
|
||||
}
|
||||
|
||||
_, err = d.Request(ApiRemove, http.MethodPost, func(r *resty.Request) {
|
||||
r.SetBody(reqBody)
|
||||
}, &result)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if result.Code != 0 {
|
||||
return fmt.Errorf("remove failed: %s", result.Message)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Open123) Put(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) error {
|
||||
parentFileId, err := strconv.ParseInt(dstDir.GetID(), 10, 64)
|
||||
etag := file.GetHash().GetHash(utils.MD5)
|
||||
|
||||
if len(etag) < utils.MD5.Width {
|
||||
up = model.UpdateProgressWithRange(up, 50, 100)
|
||||
_, etag, err = stream.CacheFullInTempFileAndHash(file, utils.MD5)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
createResp, err := d.create(parentFileId, file.GetName(), etag, file.GetSize(), 2, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if createResp.Data.Reuse {
|
||||
return nil
|
||||
}
|
||||
|
||||
return d.Upload(ctx, file, parentFileId, createResp, up)
|
||||
}
|
||||
|
||||
func (d *Open123) GetArchiveMeta(ctx context.Context, obj model.Obj, args model.ArchiveArgs) (model.ArchiveMeta, error) {
|
||||
return nil, errs.NotSupport
|
||||
}
|
||||
|
||||
func (d *Open123) ListArchive(ctx context.Context, obj model.Obj, args model.ArchiveInnerArgs) ([]model.Obj, error) {
|
||||
return nil, errs.NotSupport
|
||||
}
|
||||
|
||||
func (d *Open123) Extract(ctx context.Context, obj model.Obj, args model.ArchiveInnerArgs) (*model.Link, error) {
|
||||
return nil, errs.NotSupport
|
||||
}
|
||||
|
||||
func (d *Open123) ArchiveDecompress(ctx context.Context, srcObj, dstDir model.Obj, args model.ArchiveDecompressArgs) ([]model.Obj, error) {
|
||||
return nil, errs.NotSupport
|
||||
}
|
||||
|
||||
//func (d *Open123) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
|
||||
// return nil, errs.NotSupport
|
||||
//}
|
||||
|
||||
var _ driver.Driver = (*Open123)(nil)
|
|
@ -0,0 +1,33 @@
|
|||
package _123Open
|
||||
|
||||
import (
|
||||
"github.com/alist-org/alist/v3/internal/driver"
|
||||
"github.com/alist-org/alist/v3/internal/op"
|
||||
)
|
||||
|
||||
type Addition struct {
|
||||
driver.RootID
|
||||
|
||||
ClientID string `json:"client_id" required:"true" label:"Client ID"`
|
||||
ClientSecret string `json:"client_secret" required:"true" label:"Client Secret"`
|
||||
}
|
||||
|
||||
var config = driver.Config{
|
||||
Name: "123 Open",
|
||||
LocalSort: false,
|
||||
OnlyLocal: false,
|
||||
OnlyProxy: false,
|
||||
NoCache: false,
|
||||
NoUpload: false,
|
||||
NeedMs: false,
|
||||
DefaultRoot: "0",
|
||||
CheckStatus: false,
|
||||
Alert: "",
|
||||
NoOverwriteUpload: false,
|
||||
}
|
||||
|
||||
func init() {
|
||||
op.RegisterDriver(func() driver.Driver {
|
||||
return &Open123{}
|
||||
})
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
package _123Open
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const tokenURL = ApiBaseURL + ApiToken
|
||||
|
||||
type tokenManager struct {
|
||||
clientID string
|
||||
clientSecret string
|
||||
|
||||
mu sync.Mutex
|
||||
accessToken string
|
||||
expireTime time.Time
|
||||
}
|
||||
|
||||
func newTokenManager(clientID, clientSecret string) *tokenManager {
|
||||
return &tokenManager{
|
||||
clientID: clientID,
|
||||
clientSecret: clientSecret,
|
||||
}
|
||||
}
|
||||
|
||||
func (tm *tokenManager) getToken() (string, error) {
|
||||
tm.mu.Lock()
|
||||
defer tm.mu.Unlock()
|
||||
|
||||
if tm.accessToken != "" && time.Now().Before(tm.expireTime.Add(-5*time.Minute)) {
|
||||
return tm.accessToken, nil
|
||||
}
|
||||
|
||||
reqBody := map[string]string{
|
||||
"clientID": tm.clientID,
|
||||
"clientSecret": tm.clientSecret,
|
||||
}
|
||||
body, _ := json.Marshal(reqBody)
|
||||
req, err := http.NewRequest("POST", tokenURL, bytes.NewBuffer(body))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
req.Header.Set("Platform", "open_platform")
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var result TokenResp
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if result.Code != 0 {
|
||||
return "", fmt.Errorf("get token failed: %s", result.Message)
|
||||
}
|
||||
|
||||
tm.accessToken = result.Data.AccessToken
|
||||
expireAt, err := time.Parse(time.RFC3339, result.Data.ExpiredAt)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("parse expire time failed: %w", err)
|
||||
}
|
||||
tm.expireTime = expireAt
|
||||
|
||||
return tm.accessToken, nil
|
||||
}
|
||||
|
||||
func (tm *tokenManager) buildHeaders() (http.Header, error) {
|
||||
token, err := tm.getToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
header := http.Header{}
|
||||
header.Set("Authorization", "Bearer "+token)
|
||||
header.Set("Platform", "open_platform")
|
||||
header.Set("Content-Type", "application/json")
|
||||
return header, nil
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
package _123Open
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
"time"
|
||||
)
|
||||
|
||||
type File struct {
|
||||
FileName string `json:"filename"`
|
||||
Size int64 `json:"size"`
|
||||
CreateAt string `json:"createAt"`
|
||||
UpdateAt string `json:"updateAt"`
|
||||
FileId int64 `json:"fileId"`
|
||||
Type int `json:"type"`
|
||||
Etag string `json:"etag"`
|
||||
S3KeyFlag string `json:"s3KeyFlag"`
|
||||
ParentFileId int `json:"parentFileId"`
|
||||
Category int `json:"category"`
|
||||
Status int `json:"status"`
|
||||
Trashed int `json:"trashed"`
|
||||
}
|
||||
|
||||
func (f File) GetID() string {
|
||||
return fmt.Sprint(f.FileId)
|
||||
}
|
||||
|
||||
func (f File) GetName() string {
|
||||
return f.FileName
|
||||
}
|
||||
|
||||
func (f File) GetSize() int64 {
|
||||
return f.Size
|
||||
}
|
||||
|
||||
func (f File) IsDir() bool {
|
||||
return f.Type == 1
|
||||
}
|
||||
|
||||
func (f File) GetModified() string {
|
||||
return f.UpdateAt
|
||||
}
|
||||
|
||||
func (f File) GetThumb() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (f File) ModTime() time.Time {
|
||||
t, err := time.Parse("2006-01-02 15:04:05", f.UpdateAt)
|
||||
if err != nil {
|
||||
return time.Time{}
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
func (f File) CreateTime() time.Time {
|
||||
t, err := time.Parse("2006-01-02 15:04:05", f.CreateAt)
|
||||
if err != nil {
|
||||
return time.Time{}
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
func (f File) GetHash() utils.HashInfo {
|
||||
return utils.NewHashInfo(utils.MD5, f.Etag)
|
||||
}
|
||||
|
||||
func (f File) GetPath() string {
|
||||
return ""
|
||||
}
|
|
@ -0,0 +1,282 @@
|
|||
package _123Open
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/alist-org/alist/v3/drivers/base"
|
||||
"github.com/alist-org/alist/v3/internal/driver"
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
"github.com/alist-org/alist/v3/internal/stream"
|
||||
"github.com/alist-org/alist/v3/pkg/http_range"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"golang.org/x/sync/errgroup"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (d *Open123) create(parentFileID int64, filename, etag string, size int64, duplicate int, containDir bool) (*UploadCreateResp, error) {
|
||||
var resp UploadCreateResp
|
||||
|
||||
_, err := d.Request(ApiCreateUploadURL, http.MethodPost, func(req *resty.Request) {
|
||||
body := base.Json{
|
||||
"parentFileID": parentFileID,
|
||||
"filename": filename,
|
||||
"etag": etag,
|
||||
"size": size,
|
||||
}
|
||||
if duplicate > 0 {
|
||||
body["duplicate"] = duplicate
|
||||
}
|
||||
if containDir {
|
||||
body["containDir"] = true
|
||||
}
|
||||
req.SetBody(body)
|
||||
}, &resp)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
func (d *Open123) GetUploadDomains() ([]string, error) {
|
||||
var resp struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data []string `json:"data"`
|
||||
}
|
||||
|
||||
_, err := d.Request(ApiUploadDomainURL, http.MethodGet, nil, &resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.Code != 0 {
|
||||
return nil, fmt.Errorf("get upload domain failed: %s", resp.Message)
|
||||
}
|
||||
return resp.Data, nil
|
||||
}
|
||||
|
||||
func (d *Open123) UploadSingle(ctx context.Context, createResp *UploadCreateResp, file model.FileStreamer, parentID int64) error {
|
||||
domain := createResp.Data.Servers[0]
|
||||
|
||||
etag := file.GetHash().GetHash(utils.MD5)
|
||||
if len(etag) < utils.MD5.Width {
|
||||
_, _, err := stream.CacheFullInTempFileAndHash(file, utils.MD5)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
reader, err := file.RangeRead(http_range.Range{Start: 0, Length: file.GetSize()})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
reader = driver.NewLimitedUploadStream(ctx, reader)
|
||||
|
||||
var b bytes.Buffer
|
||||
mw := multipart.NewWriter(&b)
|
||||
mw.WriteField("parentFileID", fmt.Sprint(parentID))
|
||||
mw.WriteField("filename", file.GetName())
|
||||
mw.WriteField("etag", etag)
|
||||
mw.WriteField("size", fmt.Sprint(file.GetSize()))
|
||||
fw, _ := mw.CreateFormFile("file", file.GetName())
|
||||
_, err = io.Copy(fw, reader)
|
||||
mw.Close()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", domain+ApiSingleUploadURL, &b)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+d.tm.accessToken)
|
||||
req.Header.Set("Platform", "open_platform")
|
||||
req.Header.Set("Content-Type", mw.FormDataContentType())
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var result struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data struct {
|
||||
FileID int64 `json:"fileID"`
|
||||
Completed bool `json:"completed"`
|
||||
} `json:"data"`
|
||||
}
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
if err := json.Unmarshal(body, &result); err != nil {
|
||||
return fmt.Errorf("unmarshal response error: %v, body: %s", err, string(body))
|
||||
}
|
||||
if result.Code != 0 {
|
||||
return fmt.Errorf("upload failed: %s", result.Message)
|
||||
}
|
||||
if !result.Data.Completed || result.Data.FileID == 0 {
|
||||
return fmt.Errorf("upload incomplete or missing fileID")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Open123) Upload(ctx context.Context, file model.FileStreamer, parentID int64, createResp *UploadCreateResp, up driver.UpdateProgress) error {
|
||||
if cacher, ok := file.(interface{ CacheFullInTempFile() (model.File, error) }); ok {
|
||||
if _, err := cacher.CacheFullInTempFile(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
size := file.GetSize()
|
||||
chunkSize := createResp.Data.SliceSize
|
||||
uploadNums := (size + chunkSize - 1) / chunkSize
|
||||
uploadDomain := createResp.Data.Servers[0]
|
||||
|
||||
if d.UploadThread <= 0 {
|
||||
cpuCores := runtime.NumCPU()
|
||||
threads := cpuCores * 2
|
||||
if threads < 4 {
|
||||
threads = 4
|
||||
}
|
||||
if threads > 16 {
|
||||
threads = 16
|
||||
}
|
||||
d.UploadThread = threads
|
||||
fmt.Printf("[Upload] Auto set upload concurrency: %d (CPU cores=%d)\n", d.UploadThread, cpuCores)
|
||||
}
|
||||
|
||||
fmt.Printf("[Upload] File size: %d bytes, chunk size: %d bytes, total slices: %d, concurrency: %d\n",
|
||||
size, chunkSize, uploadNums, d.UploadThread)
|
||||
|
||||
if size <= 1<<30 {
|
||||
return d.UploadSingle(ctx, createResp, file, parentID)
|
||||
}
|
||||
|
||||
if createResp.Data.Reuse {
|
||||
up(100)
|
||||
return nil
|
||||
}
|
||||
|
||||
client := resty.New()
|
||||
semaphore := make(chan struct{}, d.UploadThread)
|
||||
threadG, _ := errgroup.WithContext(ctx)
|
||||
|
||||
var progressArr = make([]int64, uploadNums)
|
||||
|
||||
for partIndex := int64(0); partIndex < uploadNums; partIndex++ {
|
||||
partIndex := partIndex
|
||||
semaphore <- struct{}{}
|
||||
|
||||
threadG.Go(func() error {
|
||||
defer func() { <-semaphore }()
|
||||
offset := partIndex * chunkSize
|
||||
length := min(chunkSize, size-offset)
|
||||
partNumber := partIndex + 1
|
||||
|
||||
fmt.Printf("[Slice %d] Starting read from offset %d, length %d\n", partNumber, offset, length)
|
||||
reader, err := file.RangeRead(http_range.Range{Start: offset, Length: length})
|
||||
if err != nil {
|
||||
return fmt.Errorf("[Slice %d] RangeRead error: %v", partNumber, err)
|
||||
}
|
||||
|
||||
buf := make([]byte, length)
|
||||
n, err := io.ReadFull(reader, buf)
|
||||
if err != nil && err != io.EOF {
|
||||
return fmt.Errorf("[Slice %d] Read error: %v", partNumber, err)
|
||||
}
|
||||
buf = buf[:n]
|
||||
hash := md5.Sum(buf)
|
||||
sliceMD5Str := hex.EncodeToString(hash[:])
|
||||
|
||||
body := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(body)
|
||||
writer.WriteField("preuploadID", createResp.Data.PreuploadID)
|
||||
writer.WriteField("sliceNo", strconv.FormatInt(partNumber, 10))
|
||||
writer.WriteField("sliceMD5", sliceMD5Str)
|
||||
partName := fmt.Sprintf("%s.part%d", file.GetName(), partNumber)
|
||||
fw, _ := writer.CreateFormFile("slice", partName)
|
||||
fw.Write(buf)
|
||||
writer.Close()
|
||||
|
||||
resp, err := client.R().
|
||||
SetHeader("Authorization", "Bearer "+d.tm.accessToken).
|
||||
SetHeader("Platform", "open_platform").
|
||||
SetHeader("Content-Type", writer.FormDataContentType()).
|
||||
SetBody(body.Bytes()).
|
||||
Post(uploadDomain + ApiUploadSliceURL)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("[Slice %d] Upload HTTP error: %v", partNumber, err)
|
||||
}
|
||||
if resp.StatusCode() != 200 {
|
||||
return fmt.Errorf("[Slice %d] Upload failed with status: %s, resp: %s", partNumber, resp.Status(), resp.String())
|
||||
}
|
||||
|
||||
progressArr[partIndex] = length
|
||||
var totalUploaded int64 = 0
|
||||
for _, v := range progressArr {
|
||||
totalUploaded += v
|
||||
}
|
||||
if up != nil {
|
||||
percent := float64(totalUploaded) / float64(size) * 100
|
||||
up(percent)
|
||||
}
|
||||
|
||||
fmt.Printf("[Slice %d] MD5: %s\n", partNumber, sliceMD5Str)
|
||||
fmt.Printf("[Slice %d] Upload finished\n", partNumber)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
if err := threadG.Wait(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var completeResp struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data struct {
|
||||
Completed bool `json:"completed"`
|
||||
FileID int64 `json:"fileID"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
for {
|
||||
reqBody := fmt.Sprintf(`{"preuploadID":"%s"}`, createResp.Data.PreuploadID)
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", uploadDomain+ApiUploadCompleteURL, bytes.NewBufferString(reqBody))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+d.tm.accessToken)
|
||||
req.Header.Set("Platform", "open_platform")
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
|
||||
if err := json.Unmarshal(body, &completeResp); err != nil {
|
||||
return fmt.Errorf("completion response unmarshal error: %v, body: %s", err, string(body))
|
||||
}
|
||||
if completeResp.Code != 0 {
|
||||
return fmt.Errorf("completion API returned error code %d: %s", completeResp.Code, completeResp.Message)
|
||||
}
|
||||
if completeResp.Data.Completed && completeResp.Data.FileID != 0 {
|
||||
fmt.Printf("[Upload] Upload completed successfully. FileID: %d\n", completeResp.Data.FileID)
|
||||
break
|
||||
}
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
up(100)
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
package _123Open
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func (d *Open123) getFiles(parentFileId int64, limit int, lastFileId int64) (*FileListResp, error) {
|
||||
var result FileListResp
|
||||
url := fmt.Sprintf("%s?parentFileId=%d&limit=%d&lastFileId=%d", ApiFileList, parentFileId, limit, lastFileId)
|
||||
|
||||
_, err := d.Request(url, http.MethodGet, nil, &result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if result.Code != 0 {
|
||||
return nil, fmt.Errorf("list error: %s", result.Message)
|
||||
}
|
||||
return &result, nil
|
||||
}
|
|
@ -6,6 +6,7 @@ import (
|
|||
_ "github.com/alist-org/alist/v3/drivers/115_share"
|
||||
_ "github.com/alist-org/alist/v3/drivers/123"
|
||||
_ "github.com/alist-org/alist/v3/drivers/123_link"
|
||||
_ "github.com/alist-org/alist/v3/drivers/123_open"
|
||||
_ "github.com/alist-org/alist/v3/drivers/123_share"
|
||||
_ "github.com/alist-org/alist/v3/drivers/139"
|
||||
_ "github.com/alist-org/alist/v3/drivers/189"
|
||||
|
|
|
@ -4,4 +4,5 @@ import "errors"
|
|||
|
||||
var (
|
||||
EmptyToken = errors.New("empty token")
|
||||
LinkIsDir = errors.New("link is dir")
|
||||
)
|
||||
|
|
|
@ -55,6 +55,21 @@ type FileStreamer interface {
|
|||
|
||||
type UpdateProgress func(percentage float64)
|
||||
|
||||
// Reference implementation from OpenListTeam:
|
||||
// https://github.com/OpenListTeam/OpenList/blob/a703b736c9346c483bae56905a39bc07bf781cff/internal/model/obj.go#L58
|
||||
func UpdateProgressWithRange(inner UpdateProgress, start, end float64) UpdateProgress {
|
||||
return func(p float64) {
|
||||
if p < 0 {
|
||||
p = 0
|
||||
}
|
||||
if p > 100 {
|
||||
p = 100
|
||||
}
|
||||
scaled := start + (end-start)*(p/100.0)
|
||||
inner(scaled)
|
||||
}
|
||||
}
|
||||
|
||||
type URL interface {
|
||||
URL() string
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue