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/115_share"
|
||||||
_ "github.com/alist-org/alist/v3/drivers/123"
|
_ "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_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/123_share"
|
||||||
_ "github.com/alist-org/alist/v3/drivers/139"
|
_ "github.com/alist-org/alist/v3/drivers/139"
|
||||||
_ "github.com/alist-org/alist/v3/drivers/189"
|
_ "github.com/alist-org/alist/v3/drivers/189"
|
||||||
|
|
|
@ -4,4 +4,5 @@ import "errors"
|
||||||
|
|
||||||
var (
|
var (
|
||||||
EmptyToken = errors.New("empty token")
|
EmptyToken = errors.New("empty token")
|
||||||
|
LinkIsDir = errors.New("link is dir")
|
||||||
)
|
)
|
||||||
|
|
|
@ -55,6 +55,21 @@ type FileStreamer interface {
|
||||||
|
|
||||||
type UpdateProgress func(percentage float64)
|
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 {
|
type URL interface {
|
||||||
URL() string
|
URL() string
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue