mirror of
https://github.com/rclone/rclone.git
synced 2025-04-24 13:14:13 +08:00
WIP upload,mkdir,update
This commit is contained in:
parent
5c1eebdc33
commit
98db73d759
@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
@ -47,25 +48,6 @@ const (
|
||||
|
||||
ItemTypeFile = "file"
|
||||
ItemTypeFolder = "folder"
|
||||
|
||||
VipIdentityMember = "member"
|
||||
VipIdentityVip = "vip"
|
||||
VipIdentitySVip = "svip"
|
||||
|
||||
FeatureCode1080p = "hd.1080p"
|
||||
FeatureCode1440p = "hd.1080p.plus"
|
||||
|
||||
TrialStatusNoTrial = "noTrial"
|
||||
TrialStatusOnTrial = "onTrial"
|
||||
TrialStatusEndTrial = "endTrial"
|
||||
TrialStatusAllowTrial = "allowTrial"
|
||||
|
||||
CheckNameModeIgnore = "ignore"
|
||||
CheckNameModeAutoRename = "auto_rename"
|
||||
CheckNameModeRefuse = "refuse"
|
||||
|
||||
RemoveWayDelete = "delete"
|
||||
RemoveWayTrash = "trash"
|
||||
)
|
||||
|
||||
// Globals
|
||||
@ -103,66 +85,6 @@ func init() {
|
||||
encoder.EncodeBackSlash |
|
||||
encoder.EncodeInvalidUtf8, // /*?:<>\"|
|
||||
},
|
||||
{
|
||||
Name: "root_folder_id",
|
||||
Help: "Root folder ID",
|
||||
Advanced: true,
|
||||
Default: "",
|
||||
},
|
||||
{
|
||||
Name: "client_id",
|
||||
Help: "Client ID (OpenAPI)",
|
||||
Advanced: true,
|
||||
Default: "",
|
||||
},
|
||||
{
|
||||
Name: "client_secret",
|
||||
Help: "Client Secret (OpenAPI)",
|
||||
Advanced: true,
|
||||
Default: "",
|
||||
},
|
||||
{
|
||||
Name: "chunk_size",
|
||||
Help: "Chunk size for uploads",
|
||||
Advanced: true,
|
||||
Default: defaultChunkSize,
|
||||
},
|
||||
{
|
||||
Name: "remove_way",
|
||||
Help: "Way to remove files: delete or trash",
|
||||
Advanced: true,
|
||||
Default: RemoveWayTrash,
|
||||
Examples: []fs.OptionExample{
|
||||
{
|
||||
Value: RemoveWayDelete,
|
||||
Help: "Delete files permanently",
|
||||
},
|
||||
{
|
||||
Value: RemoveWayTrash,
|
||||
Help: "Move files to the trash",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "check_name_mode",
|
||||
Help: "How to handle duplicate file names",
|
||||
Advanced: true,
|
||||
Default: CheckNameModeRefuse,
|
||||
Examples: []fs.OptionExample{
|
||||
{
|
||||
Value: CheckNameModeRefuse,
|
||||
Help: "Refuse to create duplicates",
|
||||
},
|
||||
{
|
||||
Value: CheckNameModeAutoRename,
|
||||
Help: "Auto rename duplicates",
|
||||
},
|
||||
{
|
||||
Value: CheckNameModeIgnore,
|
||||
Help: "Ignore duplicates",
|
||||
},
|
||||
},
|
||||
},
|
||||
}...),
|
||||
})
|
||||
}
|
||||
@ -200,9 +122,9 @@ type Object struct {
|
||||
// Lists the directory required calling the user function on each item found
|
||||
//
|
||||
// If the user fn ever returns true then it early exits with found = true
|
||||
func (f *Fs) listAll(ctx context.Context, directoryID string) (items []api.FileItem, err error) {
|
||||
func (f *Fs) listAll(ctx context.Context, directoryID string) (items aliyunpan.FileList, err error) {
|
||||
// Convert to API items
|
||||
param := &openapi.FileListParam{
|
||||
param := &aliyunpan.FileListParam{
|
||||
DriveId: f.driveID,
|
||||
ParentFileId: directoryID,
|
||||
OrderBy: "name",
|
||||
@ -214,22 +136,7 @@ func (f *Fs) listAll(ctx context.Context, directoryID string) (items []api.FileI
|
||||
return nil, fmt.Errorf("error listing directory: %v", apiErr)
|
||||
}
|
||||
|
||||
// Convert openapi.FileItem to api.FileItem
|
||||
for _, item := range result.Items {
|
||||
fileItem := api.FileItem{
|
||||
DriveID: item.DriveId,
|
||||
ParentFileID: item.ParentFileId,
|
||||
FileID: item.FileId,
|
||||
Name: item.Name,
|
||||
Type: item.Type,
|
||||
Size: item.Size,
|
||||
CreatedAt: parseTime(item.CreatedAt),
|
||||
UpdatedAt: parseTime(item.UpdatedAt),
|
||||
}
|
||||
items = append(items, fileItem)
|
||||
}
|
||||
|
||||
return items, nil
|
||||
return result.FileList, nil
|
||||
}
|
||||
|
||||
func parseTime(timeStr string) time.Time {
|
||||
@ -245,25 +152,11 @@ func parseTime(timeStr string) time.Time {
|
||||
|
||||
// deleteObject removes an object by ID
|
||||
func (f *Fs) deleteObject(ctx context.Context, id string) error {
|
||||
path := "/adrive/v1.0/openFile"
|
||||
if f.opt.RemoveWay == RemoveWayTrash {
|
||||
path += "/recyclebin/trash"
|
||||
} else if f.opt.RemoveWay == RemoveWayDelete {
|
||||
path += "/delete"
|
||||
}
|
||||
_, err := f.openClient.FileDelete(&aliyunpan.FileBatchActionParam{
|
||||
DriveId: f.driveID,
|
||||
FileId: id,
|
||||
})
|
||||
|
||||
opts := rest.Opts{
|
||||
Method: "DELETE",
|
||||
Path: path,
|
||||
NoResponse: true,
|
||||
}
|
||||
|
||||
req := api.DeleteFileRequest{
|
||||
DriveID: f.driveID,
|
||||
FileID: id,
|
||||
}
|
||||
|
||||
_, err := f.client.CallJSON(ctx, &opts, &req, nil)
|
||||
return err
|
||||
}
|
||||
|
||||
@ -273,21 +166,22 @@ func (f *Fs) deleteObject(ctx context.Context, id string) error {
|
||||
// Returns the object, leaf, dirID and error.
|
||||
//
|
||||
// Used to create new objects
|
||||
func (f *Fs) createObject(ctx context.Context, remote string, modTime time.Time, size int64) (o *Object, leaf string, dirID string, err error) {
|
||||
func (f *Fs) createObject(ctx context.Context, remote string, modTime time.Time, size int64) (o *Object, leaf string, directoryID string, err error) {
|
||||
// Create the directory for the object if it doesn't exist
|
||||
leaf, dirID, err = f.dirCache.FindPath(ctx, remote, true)
|
||||
leaf, directoryID, err = f.dirCache.FindPath(ctx, remote, true)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// Temporary Object under construction
|
||||
// Create a new object
|
||||
o = &Object{
|
||||
fs: f,
|
||||
remote: remote,
|
||||
parentID: dirID,
|
||||
size: size,
|
||||
modTime: modTime,
|
||||
parentID: directoryID,
|
||||
}
|
||||
return o, leaf, dirID, nil
|
||||
|
||||
return o, leaf, directoryID, nil
|
||||
}
|
||||
|
||||
// purgeCheck removes the root directory, if check is set then it
|
||||
@ -314,13 +208,16 @@ func (f *Fs) purgeCheck(ctx context.Context, dir string) error {
|
||||
// Return an Object from a path
|
||||
//
|
||||
// If it can't be found it returns the error fs.ErrorObjectNotFound.
|
||||
func (f *Fs) newObjectWithInfo(ctx context.Context, remote string, info *api.FileItem) (fs.Object, error) {
|
||||
func (f *Fs) newObjectWithInfo(ctx context.Context, remote string, info *aliyunpan.FileEntity) (fs.Object, error) {
|
||||
o := &Object{
|
||||
fs: f,
|
||||
remote: remote,
|
||||
fs: f,
|
||||
remote: remote,
|
||||
id: info.FileId,
|
||||
parentID: info.ParentFileId,
|
||||
}
|
||||
var err error
|
||||
if info != nil {
|
||||
// Set info
|
||||
err = o.setMetaData(info)
|
||||
} else {
|
||||
err = o.readMetaData(ctx) // reads info and meta, returning an error
|
||||
@ -331,75 +228,43 @@ func (f *Fs) newObjectWithInfo(ctx context.Context, remote string, info *api.Fil
|
||||
return o, nil
|
||||
}
|
||||
|
||||
// getUserInfo gets UserInfo from API
|
||||
func (f *Fs) getUserInfo(ctx context.Context) (info *aliyunpan.UserInfo, err error) {
|
||||
info, apiErr := f.openClient.GetUserInfo()
|
||||
if apiErr != nil {
|
||||
return nil, fmt.Errorf("failed to get userinfo: %v", apiErr)
|
||||
}
|
||||
return info, nil
|
||||
}
|
||||
|
||||
// getDriveInfo gets DriveInfo from API
|
||||
func (f *Fs) getDriveInfo(ctx context.Context) error {
|
||||
userInfo, err := f.getUserInfo(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get drive info: %w", err)
|
||||
}
|
||||
f.driveID = userInfo.FileDriveId
|
||||
return nil
|
||||
}
|
||||
|
||||
// getSpaceInfo gets SpaceInfo from API
|
||||
func (f *Fs) getSpaceInfo(ctx context.Context) (info *aliyunpan.UserInfo, err error) {
|
||||
return f.getUserInfo(ctx)
|
||||
}
|
||||
|
||||
// getVipInfo gets VipInfo from API
|
||||
func (f *Fs) getVipInfo(ctx context.Context) (info *aliyunpan.UserInfo, err error) {
|
||||
return f.getUserInfo(ctx)
|
||||
}
|
||||
|
||||
// moveObject moves an object by ID
|
||||
func (f *Fs) move(ctx context.Context, id, leaf, directoryID string) (info *api.FileItem, err error) {
|
||||
opts := rest.Opts{
|
||||
Method: "POST",
|
||||
Path: "/adrive/v1.0/openFile/move",
|
||||
func (f *Fs) move(ctx context.Context, id, leaf, directoryID string) (info *aliyunpan.FileEntity, err error) {
|
||||
// Use OpenAPI client for file move operation
|
||||
moveFileParam := &aliyunpan.FileMoveParam{
|
||||
DriveId: f.driveID,
|
||||
FileId: id,
|
||||
ToParentFileId: directoryID,
|
||||
}
|
||||
|
||||
move := api.CopyFileRequest{
|
||||
DriveID: f.driveID,
|
||||
FileID: id,
|
||||
ToParentFileID: directoryID,
|
||||
NewName: leaf,
|
||||
CheckNameMode: f.opt.CheckNameMode,
|
||||
result, apiErr := f.openClient.FileMove(moveFileParam)
|
||||
if apiErr != nil {
|
||||
return nil, fmt.Errorf("error moving file: %v", apiErr)
|
||||
}
|
||||
|
||||
var resp *http.Response
|
||||
err = f.pacer.Call(func() (bool, error) {
|
||||
resp, err = f.client.CallJSON(ctx, &opts, &move, &info)
|
||||
return shouldRetry(ctx, resp, err)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
// Convert to FileEntity
|
||||
fileEntity, apiErr := f.openClient.FileInfoById(f.driveID, result.FileId)
|
||||
if apiErr != nil {
|
||||
return nil, fmt.Errorf("error getting moved file: %v", apiErr)
|
||||
}
|
||||
|
||||
return info, nil
|
||||
return fileEntity, nil
|
||||
}
|
||||
|
||||
// setMetaData sets the metadata from info
|
||||
func (o *Object) setMetaData(info *api.FileItem) (err error) {
|
||||
if info.Type == ItemTypeFolder {
|
||||
func (o *Object) setMetaData(info *aliyunpan.FileEntity) (err error) {
|
||||
if info.FileType == ItemTypeFolder {
|
||||
return fs.ErrorIsDir
|
||||
}
|
||||
if info.Type != ItemTypeFile {
|
||||
return fmt.Errorf("%q is %q: %w", o.remote, info.Type, fs.ErrorNotAFile)
|
||||
if info.FileType != ItemTypeFile {
|
||||
return fmt.Errorf("%q is %q: %w", o.remote, info.FileType, fs.ErrorNotAFile)
|
||||
}
|
||||
o.hasMetaData = true
|
||||
o.size = int64(info.Size)
|
||||
o.modTime = info.CreatedAt
|
||||
o.id = info.FileID
|
||||
o.parentID = info.ParentFileID
|
||||
o.size = int64(info.FileSize)
|
||||
o.sha1 = info.ContentHash
|
||||
o.modTime = parseTime(info.CreatedAt)
|
||||
o.id = info.FileId
|
||||
o.parentID = info.ParentFileId
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -423,10 +288,10 @@ func (o *Object) readMetaData(ctx context.Context) (err error) {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var info api.FileItem
|
||||
var info aliyunpan.FileEntity
|
||||
for _, item := range list {
|
||||
if item.Type == ItemTypeFile && strings.EqualFold(item.Name, leaf) {
|
||||
info = item
|
||||
if item.FileType == ItemTypeFile && strings.EqualFold(item.FileName, leaf) {
|
||||
info = *item
|
||||
break
|
||||
}
|
||||
}
|
||||
@ -436,23 +301,31 @@ func (o *Object) readMetaData(ctx context.Context) (err error) {
|
||||
|
||||
// ------------------------------------------------------------
|
||||
|
||||
// About implements fs.Abouter.
|
||||
// About gets quota information
|
||||
func (f *Fs) About(ctx context.Context) (usage *fs.Usage, err error) {
|
||||
about, err := f.getSpaceInfo(ctx)
|
||||
about, err := f.openClient.GetUserInfo()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
usage = &fs.Usage{
|
||||
Used: fs.NewUsageValue(about.TotalSize),
|
||||
Total: fs.NewUsageValue(about.TotalSize),
|
||||
Free: fs.NewUsageValue(about.TotalSize - about.UsedSize),
|
||||
Used: fs.NewUsageValue(int64(about.UsedSize)),
|
||||
Total: fs.NewUsageValue(int64(about.TotalSize)),
|
||||
Free: fs.NewUsageValue(int64(about.TotalSize - about.UsedSize)),
|
||||
}
|
||||
|
||||
return usage, nil
|
||||
}
|
||||
|
||||
// Copy implements fs.Copier.
|
||||
// Copy src to this remote using server-side copy operations.
|
||||
//
|
||||
// This is stored with the remote path given.
|
||||
//
|
||||
// It returns the destination Object and a possible error.
|
||||
//
|
||||
// Will only be called if src.Fs().Name() == f.Name()
|
||||
//
|
||||
// If it isn't possible then return fs.ErrorCantCopy
|
||||
func (f *Fs) Copy(ctx context.Context, src fs.Object, remote string) (fs.Object, error) {
|
||||
srcObj, ok := src.(*Object)
|
||||
if !ok {
|
||||
@ -476,27 +349,21 @@ func (f *Fs) Copy(ctx context.Context, src fs.Object, remote string) (fs.Object,
|
||||
return nil, err
|
||||
}
|
||||
|
||||
request := api.CopyFileRequest{
|
||||
DriveID: f.driveID,
|
||||
FileID: srcObj.id,
|
||||
ToParentFileID: directoryID,
|
||||
}
|
||||
|
||||
opts := rest.Opts{
|
||||
Method: "POST",
|
||||
Path: "/adrive/v1.0/openFile/copy",
|
||||
}
|
||||
|
||||
var resp *http.Response
|
||||
var copyResp api.CopyFileResponse
|
||||
err = f.pacer.Call(func() (bool, error) {
|
||||
resp, err = f.client.CallJSON(ctx, &opts, &request, ©Resp)
|
||||
return shouldRetry(ctx, resp, err)
|
||||
item, err := f.openClient.FileCopy(&aliyunpan.FileCopyParam{
|
||||
DriveId: f.driveID,
|
||||
ToParentFileId: directoryID,
|
||||
FileId: srcObj.id,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := dstObj.readMetaData(ctx); err != nil {
|
||||
|
||||
info, err := f.openClient.FileInfoById(f.driveID, item.FileId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = dstObj.setMetaData(info)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return dstObj, nil
|
||||
@ -541,29 +408,35 @@ func (f *Fs) Features() *fs.Features {
|
||||
|
||||
// CreateDir implements dircache.DirCacher.
|
||||
func (f *Fs) CreateDir(ctx context.Context, pathID string, leaf string) (newID string, err error) {
|
||||
var resp *http.Response
|
||||
var info *api.FileItem
|
||||
var info *aliyunpan.FileEntity
|
||||
|
||||
opts := rest.Opts{
|
||||
Method: "POST",
|
||||
Path: "/adrive/v1.0/openFile/create",
|
||||
}
|
||||
|
||||
req := api.CreateFileRequest{
|
||||
DriveID: f.driveID,
|
||||
ParentFileID: pathID,
|
||||
// TODO
|
||||
_, err = f.openClient.CreateUploadFile(&aliyunpan.CreateFileUploadParam{
|
||||
DriveId: f.driveID,
|
||||
ParentFileId: pathID,
|
||||
Name: leaf,
|
||||
Type: ItemTypeFolder,
|
||||
CheckNameMode: "refuse",
|
||||
}
|
||||
err = f.pacer.Call(func() (bool, error) {
|
||||
resp, err = f.client.CallJSON(ctx, &opts, &req, &info)
|
||||
return shouldRetry(ctx, resp, err)
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return info.FileID, nil
|
||||
|
||||
// req := api.CreateFileRequest{
|
||||
// DriveID: f.driveID,
|
||||
// ParentFileID: pathID,
|
||||
// Name: leaf,
|
||||
// Type: ItemTypeFolder,
|
||||
// CheckNameMode: "refuse",
|
||||
// }
|
||||
// err = f.pacer.Call(func() (bool, error) {
|
||||
// resp, err = f.client.CallJSON(ctx, &opts, &req, &info)
|
||||
// return shouldRetry(ctx, resp, err)
|
||||
// })
|
||||
// if err != nil {
|
||||
// return "", err
|
||||
// }
|
||||
return info.FileId, nil
|
||||
}
|
||||
|
||||
// FindLeaf finds a directory of name leaf in the folder with ID pathID
|
||||
@ -574,8 +447,8 @@ func (f *Fs) FindLeaf(ctx context.Context, pathID, leaf string) (pathIDOut strin
|
||||
return "", false, err
|
||||
}
|
||||
for _, item := range items {
|
||||
if strings.EqualFold(item.Name, leaf) {
|
||||
pathIDOut = item.FileID
|
||||
if strings.EqualFold(item.FileName, leaf) {
|
||||
pathIDOut = item.FileId
|
||||
found = true
|
||||
break
|
||||
}
|
||||
@ -591,15 +464,36 @@ func (f *Fs) List(ctx context.Context, dir string) (entries fs.DirEntries, err e
|
||||
return nil, err
|
||||
}
|
||||
|
||||
list, err := f.listAll(ctx, directoryID)
|
||||
for _, info := range list {
|
||||
remote := path.Join(dir, info.Name)
|
||||
if info.Type == ItemTypeFolder {
|
||||
f.dirCache.Put(remote, info.FileID)
|
||||
d := fs.NewDir(remote, info.UpdatedAt).SetID(info.FileID).SetParentID(dir)
|
||||
// Get directory entries from the dirID
|
||||
fileList, err := f.openClient.FileListGetAll(&aliyunpan.FileListParam{
|
||||
DriveId: f.driveID,
|
||||
ParentFileId: directoryID,
|
||||
OrderBy: "name",
|
||||
OrderDirection: "ASC",
|
||||
}, -1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
entries = make(fs.DirEntries, 0, len(fileList))
|
||||
for _, item := range fileList {
|
||||
remote := path.Join(dir, item.FileName)
|
||||
if item.FileType == ItemTypeFolder {
|
||||
// Parse UpdatedAt time
|
||||
modTime := time.Now()
|
||||
if item.UpdatedAt != "" {
|
||||
parsedTime, err := time.Parse(time.RFC3339, item.UpdatedAt)
|
||||
if err == nil {
|
||||
modTime = parsedTime.UTC()
|
||||
} else {
|
||||
fs.Debugf(remote, "Failed to parse UpdatedAt time %q: %v", item.UpdatedAt, err)
|
||||
}
|
||||
}
|
||||
|
||||
d := fs.NewDir(remote, modTime).SetID(item.FileId).SetParentID(directoryID)
|
||||
entries = append(entries, d)
|
||||
} else {
|
||||
o, createErr := f.newObjectWithInfo(ctx, remote, &info)
|
||||
o, createErr := f.newObjectWithInfo(ctx, remote, item)
|
||||
if createErr == nil {
|
||||
entries = append(entries, o)
|
||||
}
|
||||
@ -623,6 +517,17 @@ func (f *Fs) Put(ctx context.Context, in io.Reader, src fs.ObjectInfo, options .
|
||||
// TODO
|
||||
o := &Object{}
|
||||
return o, o.Update(ctx, in, src, options...)
|
||||
|
||||
// existingObj, err := f.NewObject(ctx, src.Remote())
|
||||
// switch {
|
||||
// case err == nil:
|
||||
// return existingObj.Update(ctx, in, src, options...)
|
||||
// case errors.Is(err, fs.ErrorObjectNotFound):
|
||||
// // Not found so create it
|
||||
// return f.PutUnchecked(ctx, in, src, options...)
|
||||
// default:
|
||||
// return nil, err
|
||||
// }
|
||||
}
|
||||
|
||||
// PutStream uploads to the remote path with the modTime given of indeterminate size
|
||||
@ -630,10 +535,125 @@ func (f *Fs) PutStream(ctx context.Context, in io.Reader, src fs.ObjectInfo, opt
|
||||
return f.Put(ctx, in, src, options...)
|
||||
}
|
||||
|
||||
// PutUnchecked uploads the object
|
||||
func (f *Fs) PutUnchecked(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (fs.Object, error) {
|
||||
remote := src.Remote()
|
||||
modTime := src.ModTime(ctx)
|
||||
size := src.Size()
|
||||
|
||||
// Create a new object
|
||||
o, leaf, directoryID, err := f.createObject(ctx, remote, modTime, size)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Save the object in the parent directory
|
||||
tempFile, err := os.CreateTemp("", "rclone-adrive-*")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create temp file: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
_ = tempFile.Close()
|
||||
_ = os.Remove(tempFile.Name())
|
||||
}()
|
||||
|
||||
// Copy the contents to the temp file
|
||||
_, err = io.Copy(tempFile, in)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to write to temp file: %w", err)
|
||||
}
|
||||
|
||||
// Seek to the beginning for reading
|
||||
_, err = tempFile.Seek(0, io.SeekStart)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to seek temp file: %w", err)
|
||||
}
|
||||
|
||||
// Create the file upload param
|
||||
// uploadParam := aliyunpan.FileUploadParam{
|
||||
// DriveId: f.driveID,
|
||||
// Name: leaf,
|
||||
// Size: size,
|
||||
// ContentHash: "", // Will be calculated by the SDK
|
||||
// ParentFileId: directoryID,
|
||||
// CheckNameMode: f.opt.CheckNameMode,
|
||||
// CreateTime: modTime,
|
||||
// LocalFilePath: tempFile.Name(),
|
||||
// Content: nil, // Not using content directly to avoid memory issues
|
||||
// }
|
||||
|
||||
// Upload the file
|
||||
// TODO
|
||||
result, apiErr := f.openClient.CreateUploadFile(&aliyunpan.CreateFileUploadParam{
|
||||
DriveId: f.driveID,
|
||||
Name: leaf,
|
||||
Size: size,
|
||||
ContentHash: "", // Will be calculated by the SDK
|
||||
ParentFileId: directoryID,
|
||||
CheckNameMode: f.opt.CheckNameMode,
|
||||
})
|
||||
if apiErr != nil {
|
||||
return nil, fmt.Errorf("error uploading file: %v", apiErr)
|
||||
}
|
||||
|
||||
// Get file metadata
|
||||
FileEntity, apiErr := o.fs.openClient.FileInfoById(o.fs.driveID, result.FileId)
|
||||
if apiErr != nil {
|
||||
return nil, fmt.Errorf("error getting file metadata after upload: %v", apiErr)
|
||||
}
|
||||
|
||||
// Set metadata of object
|
||||
o.id = FileEntity.FileId
|
||||
o.sha1 = FileEntity.ContentHash
|
||||
o.size = size
|
||||
o.modTime = modTime
|
||||
o.parentID = directoryID
|
||||
o.hasMetaData = true
|
||||
|
||||
return o, nil
|
||||
}
|
||||
|
||||
// Mkdir creates the container if it doesn't exist
|
||||
func (f *Fs) Mkdir(ctx context.Context, dir string) error {
|
||||
_, err := f.dirCache.FindDir(ctx, dir, true)
|
||||
return err
|
||||
if dir == "" || dir == "." {
|
||||
return nil
|
||||
}
|
||||
|
||||
parts := strings.Split(strings.Trim(dir, "/"), "/")
|
||||
parentID := f.opt.RootFolderID
|
||||
|
||||
// Create each directory in the path if it doesn't exist
|
||||
for i, part := range parts {
|
||||
currentPath := strings.Join(parts[:i+1], "/")
|
||||
|
||||
// Try to get the directory first
|
||||
info, err := f.openClient.FileInfoByPath(f.driveID, currentPath)
|
||||
if err != nil {
|
||||
// Directory doesn't exist, create it
|
||||
createParam := &aliyunpan.FileCreateFolderParam{
|
||||
DriveId: f.driveID,
|
||||
Name: part,
|
||||
ParentFileId: parentID,
|
||||
CheckNameMode: f.opt.CheckNameMode,
|
||||
}
|
||||
|
||||
result, apiErr := f.openClient.FileCreateFolder(createParam)
|
||||
if apiErr != nil {
|
||||
return fmt.Errorf("error creating directory %q: %v", part, apiErr)
|
||||
}
|
||||
|
||||
// Use the new directory ID as parent for the next iteration
|
||||
parentID = result.FileId
|
||||
} else {
|
||||
// Directory exists, use its ID as parent for the next iteration
|
||||
if info.FileType != ItemTypeFolder {
|
||||
return fmt.Errorf("%q already exists but is not a directory", currentPath)
|
||||
}
|
||||
parentID = info.FileId
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Rmdir implements fs.Fs.
|
||||
@ -680,24 +700,23 @@ func (f *Fs) Shutdown(ctx context.Context) error {
|
||||
|
||||
// UserInfo implements fs.UserInfoer.
|
||||
func (f *Fs) UserInfo(ctx context.Context) (userInfo map[string]string, err error) {
|
||||
user, err := f.getUserInfo(ctx)
|
||||
user, err := f.openClient.GetUserInfo()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
userInfo = map[string]string{
|
||||
"Name": user.Name,
|
||||
"Avatar": user.Avatar,
|
||||
"Phone": user.Phone,
|
||||
"UserName": user.UserName,
|
||||
"Email": user.Email,
|
||||
"Phone": user.Phone,
|
||||
"Role": string(user.Role),
|
||||
"Status": string(user.Status),
|
||||
"Nickname": user.Nickname,
|
||||
}
|
||||
|
||||
if vip, err := f.getVipInfo(ctx); err == nil {
|
||||
userInfo["Identity"] = string(vip.Identity)
|
||||
userInfo["Level"] = vip.Level
|
||||
userInfo["Expire"] = time.Time(vip.Expire).String()
|
||||
userInfo["ThirdPartyVip"] = strconv.FormatBool(vip.ThirdPartyVip)
|
||||
userInfo["ThirdPartyVipExpire"] = strconv.Itoa(int(vip.ThirdPartyVipExpire))
|
||||
}
|
||||
userInfo["Expire"] = user.ThirdPartyVipExpire
|
||||
userInfo["ThirdPartyVip"] = strconv.FormatBool(user.ThirdPartyVip)
|
||||
userInfo["ThirdPartyVipExpire"] = user.ThirdPartyVipExpire
|
||||
|
||||
return userInfo, nil
|
||||
}
|
||||
@ -770,47 +789,141 @@ func (o *Object) SetModTime(ctx context.Context, t time.Time) error {
|
||||
}
|
||||
|
||||
// Open implements fs.Object.
|
||||
func (o *Object) Open(ctx context.Context, options ...fs.OpenOption) (in io.ReadCloser, err error) {
|
||||
opts := rest.Opts{
|
||||
Method: "POST",
|
||||
Path: "/adrive/v1.0/openFile/getDownloadUrl",
|
||||
func (o *Object) Open(ctx context.Context, options ...fs.OpenOption) (io.ReadCloser, error) {
|
||||
if o.id == "" {
|
||||
return nil, errors.New("can't open without object ID")
|
||||
}
|
||||
|
||||
req := api.GetDownloadURLRequest{
|
||||
DriveID: o.fs.driveID,
|
||||
FileID: o.id,
|
||||
}
|
||||
|
||||
var resp *http.Response
|
||||
var download api.GetDownloadURLResponse
|
||||
err = o.fs.pacer.Call(func() (bool, error) {
|
||||
resp, err = o.fs.client.CallJSON(ctx, &opts, &req, &download)
|
||||
return shouldRetry(ctx, resp, err)
|
||||
// Get download URL from OpenAPI
|
||||
downloadUrl, apiErr := o.fs.openClient.GetFileDownloadUrl(&aliyunpan.GetFileDownloadUrlParam{
|
||||
DriveId: o.fs.driveID,
|
||||
FileId: o.id,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if apiErr != nil {
|
||||
return nil, fmt.Errorf("error getting download URL: %v", apiErr)
|
||||
}
|
||||
|
||||
fs.FixRangeOption(options, o.size)
|
||||
if downloadUrl == nil || downloadUrl.Url == "" {
|
||||
return nil, errors.New("empty download URL")
|
||||
}
|
||||
|
||||
// TODO
|
||||
resp, err := http.Get(downloadUrl.Url)
|
||||
|
||||
fs.FixRangeOption(options, o.Size())
|
||||
|
||||
opts = rest.Opts{
|
||||
Method: download.Method,
|
||||
RootURL: download.URL,
|
||||
Options: options,
|
||||
}
|
||||
err = o.fs.pacer.Call(func() (bool, error) {
|
||||
resp, err = o.fs.client.Call(ctx, &opts)
|
||||
return shouldRetry(ctx, resp, err)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp.Body, err
|
||||
}
|
||||
|
||||
// Update implements fs.Object.
|
||||
func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) error {
|
||||
// return o.upload(ctx, in, src, options...)
|
||||
// TODO
|
||||
func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (err error) {
|
||||
size := src.Size()
|
||||
modTime := src.ModTime(ctx)
|
||||
remote := o.Remote()
|
||||
|
||||
// Save the object in the parent directory
|
||||
tempFile, err := os.CreateTemp("", "rclone-adrive-*")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create temp file: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
_ = tempFile.Close()
|
||||
_ = os.Remove(tempFile.Name())
|
||||
}()
|
||||
|
||||
// Copy the contents to the temp file
|
||||
_, err = io.Copy(tempFile, in)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to write to temp file: %w", err)
|
||||
}
|
||||
|
||||
// Seek to the beginning for reading
|
||||
_, err = tempFile.Seek(0, io.SeekStart)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to seek temp file: %w", err)
|
||||
}
|
||||
|
||||
// Get leaf name and parent ID
|
||||
var leaf, directoryID string
|
||||
if o.id != "" {
|
||||
// Try to get the current parent directory from the existing object
|
||||
info, err := o.Stat(ctx)
|
||||
if err != nil {
|
||||
if errors.Is(err, fs.ErrorObjectNotFound) {
|
||||
// If the object doesn't exist, we need to find the directory to create it in
|
||||
remoteDir := path.Dir(remote)
|
||||
remoteName := path.Base(remote)
|
||||
leaf, directoryID, err = o.fs.resolvePath(ctx, remoteDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error resolving path: %w", err)
|
||||
}
|
||||
leaf = remoteName
|
||||
} else {
|
||||
return fmt.Errorf("error getting current object info: %w", err)
|
||||
}
|
||||
} else {
|
||||
// Use the existing parent ID and name
|
||||
directoryID = info.ParentFileId
|
||||
leaf = info.FileName
|
||||
}
|
||||
} else {
|
||||
// Find the directory for this object
|
||||
remoteDir := path.Dir(remote)
|
||||
remoteName := path.Base(remote)
|
||||
leaf, directoryID, err = o.fs.resolvePath(ctx, remoteDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error resolving path: %w", err)
|
||||
}
|
||||
leaf = remoteName
|
||||
}
|
||||
|
||||
// Create the file upload param
|
||||
uploadParam := &aliyunpan.FileUploadParam{
|
||||
DriveId: o.fs.driveID,
|
||||
Name: leaf,
|
||||
Size: size,
|
||||
ContentHash: "", // Will be calculated by the SDK
|
||||
ParentFileId: directoryID,
|
||||
CheckNameMode: o.fs.opt.CheckNameMode,
|
||||
CreateTime: modTime,
|
||||
LocalFilePath: tempFile.Name(),
|
||||
Content: nil, // Not using content directly to avoid memory issues
|
||||
}
|
||||
|
||||
var fileID string
|
||||
var apiErr *aliyunpan.ApiError
|
||||
|
||||
// If updating an existing file
|
||||
if o.id != "" {
|
||||
// Set the file ID for update
|
||||
uploadParam.FileId = o.id
|
||||
|
||||
// Update existing file
|
||||
fileID, apiErr = o.fs.openClient.FileUpload(uploadParam)
|
||||
} else {
|
||||
// Upload new file
|
||||
fileID, apiErr = o.fs.openClient.FileUpload(uploadParam)
|
||||
}
|
||||
|
||||
if apiErr != nil {
|
||||
return fmt.Errorf("error uploading file: %v", apiErr)
|
||||
}
|
||||
|
||||
// Get file metadata
|
||||
FileEntity, apiErr := o.fs.openClient.FileInfoById(o.fs.driveID, fileID)
|
||||
if apiErr != nil {
|
||||
return fmt.Errorf("error getting file metadata after upload: %v", apiErr)
|
||||
}
|
||||
|
||||
// Set metadata of object
|
||||
o.id = FileEntity.FileId
|
||||
o.sha1 = FileEntity.ContentHash
|
||||
o.size = size
|
||||
o.modTime = modTime
|
||||
o.parentID = directoryID
|
||||
o.hasMetaData = true
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -824,6 +937,62 @@ func (o *Object) ParentID() string {
|
||||
return o.parentID
|
||||
}
|
||||
|
||||
// Stat returns the stat info of the object
|
||||
func (o *Object) Stat(ctx context.Context) (info *aliyunpan.FileEntity, err error) {
|
||||
// Try to get from id
|
||||
if o.id != "" {
|
||||
info, err = o.fs.openClient.FileInfoById(o.fs.driveID, o.id)
|
||||
if err != nil {
|
||||
// Check for file not found type errors
|
||||
return nil, fs.ErrorObjectNotFound
|
||||
}
|
||||
return info, nil
|
||||
}
|
||||
|
||||
// Otherwise get by path
|
||||
leaf, directoryID, err := o.fs.resolvePath(ctx, o.remote)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error resolving path: %w", err)
|
||||
}
|
||||
|
||||
info, err = o.fs.openClient.FileInfoByPath(o.fs.driveID, path.Join(directoryID, leaf))
|
||||
if err != nil {
|
||||
// Check for file not found type errors
|
||||
return nil, fs.ErrorObjectNotFound
|
||||
}
|
||||
|
||||
return info, nil
|
||||
}
|
||||
|
||||
// resolvePath resolves a remote path to its leaf and directory ID
|
||||
func (f *Fs) resolvePath(ctx context.Context, remote string) (leaf, directoryID string, err error) {
|
||||
// Find the parent directory
|
||||
if remote == "" {
|
||||
return "", f.opt.RootFolderID, nil
|
||||
}
|
||||
|
||||
// Split the path into directory and leaf name
|
||||
dir, leaf := path.Split(remote)
|
||||
if dir == "" {
|
||||
// No directory, so use root
|
||||
return leaf, f.opt.RootFolderID, nil
|
||||
}
|
||||
|
||||
// Find the directory ID
|
||||
dir = strings.TrimSuffix(dir, "/")
|
||||
info, err := f.openClient.FileInfoByPath(f.driveID, dir)
|
||||
if err != nil {
|
||||
// Directory not found
|
||||
return "", "", fs.ErrorDirNotFound
|
||||
}
|
||||
|
||||
if info.FileType != ItemTypeFolder {
|
||||
return "", "", fs.ErrorIsFile
|
||||
}
|
||||
|
||||
return leaf, info.FileId, nil
|
||||
}
|
||||
|
||||
// Options defines the configuration for this backend
|
||||
type Options struct {
|
||||
Enc encoder.MultiEncoder `config:"encoding"`
|
||||
@ -940,7 +1109,8 @@ func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, e
|
||||
}
|
||||
|
||||
// Update the OpenAPI client with the new token
|
||||
newToken := openapi.ApiToken{
|
||||
// TODO
|
||||
_ = openapi.ApiToken{
|
||||
AccessToken: token.AccessToken,
|
||||
ExpiredAt: time.Now().Add(time.Duration(token.Expiry.Unix()-time.Now().Unix()) * time.Second).Unix(),
|
||||
}
|
||||
@ -1014,20 +1184,6 @@ func (f *Fs) CallWithPacer(ctx context.Context, opts *rest.Opts, pacer *fs.Pacer
|
||||
return resp, err
|
||||
}
|
||||
|
||||
// CallJSON makes an API call and decodes the JSON return packet into response
|
||||
func (f *Fs) CallJSON(ctx context.Context, opts *rest.Opts, request interface{}, response interface{}) (resp *http.Response, err error) {
|
||||
return f.CallJSONWithPacer(ctx, opts, f.pacer, request, response)
|
||||
}
|
||||
|
||||
// CallJSONWithPacer makes an API call and decodes the JSON return packet into response using the pacer passed in
|
||||
func (f *Fs) CallJSONWithPacer(ctx context.Context, opts *rest.Opts, pacer *fs.Pacer, request interface{}, response interface{}) (resp *http.Response, err error) {
|
||||
err = pacer.Call(func() (bool, error) {
|
||||
resp, err = f.client.CallJSON(ctx, opts, request, response)
|
||||
return shouldRetry(ctx, resp, err)
|
||||
})
|
||||
return resp, err
|
||||
}
|
||||
|
||||
var retryErrorCodes = []int{
|
||||
403,
|
||||
404,
|
||||
|
@ -2,34 +2,8 @@ package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
// 2017-05-03T07:26:10-07:00
|
||||
timeFormat = `"` + time.RFC3339 + `"`
|
||||
)
|
||||
|
||||
// Time represents date and time information for the
|
||||
// box API, by using RFC3339
|
||||
type Time time.Time
|
||||
|
||||
// MarshalJSON turns a Time into JSON (in UTC)
|
||||
func (t *Time) MarshalJSON() (out []byte, err error) {
|
||||
timeString := (*time.Time)(t).Format(timeFormat)
|
||||
return []byte(timeString), nil
|
||||
}
|
||||
|
||||
// UnmarshalJSON turns JSON into a Time
|
||||
func (t *Time) UnmarshalJSON(data []byte) error {
|
||||
newT, err := time.Parse(timeFormat, string(data))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*t = Time(newT)
|
||||
return nil
|
||||
}
|
||||
|
||||
type Error struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
@ -46,549 +20,3 @@ func (e *Error) Error() string {
|
||||
|
||||
// Check Error satisfies the error interface
|
||||
var _ error = (*Error)(nil)
|
||||
|
||||
type AuthorizeRequest struct {
|
||||
ClientID string `json:"client_id"`
|
||||
ClientSecret *string `json:"client_secret"`
|
||||
GrantType string `json:"grant_type"`
|
||||
Code *string `json:"code"`
|
||||
RefreshToken *string `json:"refresh_token"`
|
||||
CodeVerifier *string `json:"code_verifier"`
|
||||
}
|
||||
|
||||
type Token struct {
|
||||
TokenType string `json:"token_type"`
|
||||
AccessToken string `json:"access_token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
ExpiresIn int64 `json:"expires_in"`
|
||||
}
|
||||
|
||||
// Expiry returns expiry from expires in, so it should be called on retrieval
|
||||
// e must be non-nil.
|
||||
func (e *Token) Expiry() (t time.Time) {
|
||||
if v := e.ExpiresIn; v != 0 {
|
||||
return time.Now().Add(time.Duration(v) * time.Second)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// UserInfo represents the user information returned by the /oauth/users/info endpoint
|
||||
type UserInfo struct {
|
||||
ID string `json:"id"` // User ID, unique identifier
|
||||
Name string `json:"name"` // Nickname, returns default nickname if not set
|
||||
Avatar string `json:"avatar"` // Avatar URL, empty if not set
|
||||
Phone string `json:"phone"` // Phone number, requires user:phone permission
|
||||
}
|
||||
|
||||
// DriveInfo represents the user and drive information returned by the /adrive/v1.0/user/getDriveInfo endpoint
|
||||
type DriveInfo struct {
|
||||
UserID string `json:"user_id"` // User ID, unique identifier
|
||||
Name string `json:"name"` // Nickname
|
||||
Avatar string `json:"avatar"` // Avatar URL
|
||||
DefaultDriveID string `json:"default_drive_id"` // Default drive ID
|
||||
ResourceDriveID string `json:"resource_drive_id"` // Resource library drive ID, only returned if authorized
|
||||
BackupDriveID string `json:"backup_drive_id"` // Backup drive ID, only returned if authorized
|
||||
FolderID string `json:"folder_id"` // Folder ID, only returned if authorized
|
||||
}
|
||||
|
||||
// PersonalSpaceInfo represents the user's personal space usage information
|
||||
type PersonalSpaceInfo struct {
|
||||
UsedSize int64 `json:"used_size"` // Used space in bytes
|
||||
TotalSize int64 `json:"total_size"` // Total space in bytes
|
||||
}
|
||||
|
||||
// SpaceInfo represents the response from /adrive/v1.0/user/getSpaceInfo endpoint
|
||||
type SpaceInfo struct {
|
||||
PersonalSpaceInfo PersonalSpaceInfo `json:"personal_space_info"` // Personal space usage information
|
||||
}
|
||||
|
||||
// VipIdentity represents the VIP status of a user
|
||||
type VipIdentity string
|
||||
|
||||
const (
|
||||
VipIdentityMember VipIdentity = "member" // Regular member
|
||||
VipIdentityVIP VipIdentity = "vip" // VIP member
|
||||
VipIdentitySVIP VipIdentity = "svip" // Super VIP member
|
||||
)
|
||||
|
||||
// VipInfo represents the response from /business/v1.0/user/getVipInfo endpoint
|
||||
type VipInfo struct {
|
||||
Identity VipIdentity `json:"identity"` // VIP status: member, vip, or svip
|
||||
Level string `json:"level,omitempty"` // Storage level (e.g., "20TB", "8TB")
|
||||
Expire Time `json:"expire"` // Expiration timestamp in seconds
|
||||
ThirdPartyVip bool `json:"thirdPartyVip"` // Whether third-party VIP benefits are active
|
||||
ThirdPartyVipExpire int64 `json:"thirdPartyVipExpire"` // Third-party VIP benefits expiration timestamp
|
||||
}
|
||||
|
||||
// Scope represents a single permission scope
|
||||
type Scope struct {
|
||||
Scope string `json:"scope"` // Permission scope identifier
|
||||
}
|
||||
|
||||
// UserScopes represents the response from /oauth/users/scopes endpoint
|
||||
type UserScopes struct {
|
||||
ID string `json:"id"` // User ID
|
||||
Scopes []Scope `json:"scopes"` // List of permission scopes
|
||||
}
|
||||
|
||||
// TrialStatus represents the trial status of a VIP feature
|
||||
type TrialStatus string
|
||||
|
||||
const (
|
||||
TrialStatusNoTrial TrialStatus = "noTrial" // Trial not allowed
|
||||
TrialStatusOnTrial TrialStatus = "onTrial" // Trial in progress
|
||||
TrialStatusEndTrial TrialStatus = "endTrial" // Trial ended
|
||||
TrialStatusAllowTrial TrialStatus = "allowTrial" // Trial allowed but not started
|
||||
)
|
||||
|
||||
// FeatureCode represents the available VIP features
|
||||
type FeatureCode string
|
||||
|
||||
const (
|
||||
FeatureCode1080p FeatureCode = "hd.1080p" // 1080p HD feature
|
||||
FeatureCode1080pPlus FeatureCode = "hd.1080p.plus" // 1440p HD feature
|
||||
)
|
||||
|
||||
// VipFeature represents a single VIP feature with its trial status
|
||||
type VipFeature struct {
|
||||
Code FeatureCode `json:"code"` // Feature identifier
|
||||
Intercept bool `json:"intercept"` // Whether the feature is intercepted
|
||||
TrialStatus TrialStatus `json:"trialStatus"` // Current trial status
|
||||
TrialDuration int64 `json:"trialDuration"` // Trial duration in minutes
|
||||
TrialStartTime int64 `json:"trialStartTime"` // Trial start timestamp
|
||||
}
|
||||
|
||||
// VipFeatureList represents the response from /business/v1.0/vip/feature/list endpoint
|
||||
type VipFeatureList struct {
|
||||
Result []VipFeature `json:"result"` // List of VIP features
|
||||
}
|
||||
|
||||
// VipFeatureTrialRequest represents the request body for /business/v1.0/vip/feature/trial endpoint
|
||||
type VipFeatureTrialRequest struct {
|
||||
FeatureCode FeatureCode `json:"featureCode"` // Feature code to start trial for
|
||||
}
|
||||
|
||||
// VipFeatureTrialResponse represents the response from /business/v1.0/vip/feature/trial endpoint
|
||||
type VipFeatureTrialResponse struct {
|
||||
TrialStatus TrialStatus `json:"trialStatus"` // Current trial status
|
||||
TrialDuration int64 `json:"trialDuration"` // Trial duration in minutes
|
||||
TrialStartTime int64 `json:"trialStartTime"` // Trial start timestamp
|
||||
}
|
||||
|
||||
// DriveFile represents a file in a specific drive
|
||||
type DriveFile struct {
|
||||
DriveID string `json:"drive_id"` // Drive ID
|
||||
FileID string `json:"file_id"` // File ID
|
||||
}
|
||||
|
||||
// CreateFastTransferRequest represents the request body for /adrive/v1.0/openFile/createFastTransfer endpoint
|
||||
type CreateFastTransferRequest struct {
|
||||
DriveFileList []DriveFile `json:"drive_file_list"` // List of drive files to share [1,100]
|
||||
}
|
||||
|
||||
// CreateFastTransferResponse represents the response from /adrive/v1.0/openFile/createFastTransfer endpoint
|
||||
type CreateFastTransferResponse struct {
|
||||
Expiration Time `json:"expiration"` // Fast transfer expiration time
|
||||
CreatorID string `json:"creator_id"` // ID of the fast transfer creator
|
||||
ShareID string `json:"share_id"` // Share ID
|
||||
ShareURL string `json:"share_url"` // Share URL
|
||||
DriveFileList []DriveFile `json:"drive_file_list"` // List of shared drive files
|
||||
}
|
||||
|
||||
// FileType represents the type of a file
|
||||
type FileType string
|
||||
|
||||
const (
|
||||
FileTypeFile FileType = "file" // Regular file
|
||||
FileTypeFolder FileType = "folder" // Directory/folder
|
||||
)
|
||||
|
||||
// FileCategory represents the category of a file
|
||||
type FileCategory string
|
||||
|
||||
const (
|
||||
FileCategoryVideo FileCategory = "video" // Video files
|
||||
FileCategoryDoc FileCategory = "doc" // Document files
|
||||
FileCategoryAudio FileCategory = "audio" // Audio files
|
||||
FileCategoryZip FileCategory = "zip" // Archive files
|
||||
FileCategoryOthers FileCategory = "others" // Other files
|
||||
FileCategoryImage FileCategory = "image" // Image files
|
||||
)
|
||||
|
||||
// OrderDirection represents the sort order direction
|
||||
type OrderDirection string
|
||||
|
||||
const (
|
||||
OrderDirectionASC OrderDirection = "ASC" // Ascending order
|
||||
OrderDirectionDESC OrderDirection = "DESC" // Descending order
|
||||
)
|
||||
|
||||
// OrderBy represents the field to sort by
|
||||
type OrderBy string
|
||||
|
||||
const (
|
||||
OrderByCreatedAt OrderBy = "created_at" // Sort by creation time
|
||||
OrderByUpdatedAt OrderBy = "updated_at" // Sort by update time
|
||||
OrderByName OrderBy = "name" // Sort by name
|
||||
OrderBySize OrderBy = "size" // Sort by size
|
||||
OrderByNameEnhanced OrderBy = "name_enhanced" // Sort by name with enhanced number handling
|
||||
)
|
||||
|
||||
// VideoMediaMetadata represents video file metadata
|
||||
type VideoMediaMetadata struct {
|
||||
// Add video metadata fields as needed
|
||||
}
|
||||
|
||||
// VideoPreviewMetadata represents video preview metadata
|
||||
type VideoPreviewMetadata struct {
|
||||
// Add video preview metadata fields as needed
|
||||
}
|
||||
|
||||
// FileItem represents a file or folder item in the drive
|
||||
type FileItem struct {
|
||||
DriveID string `json:"drive_id"` // Drive ID
|
||||
FileID string `json:"file_id"` // File ID
|
||||
ParentFileID string `json:"parent_file_id"` // Parent folder ID
|
||||
Name string `json:"name"` // File name
|
||||
Size int64 `json:"size"` // File size in bytes
|
||||
FileExtension string `json:"file_extension"` // File extension
|
||||
ContentHash string `json:"content_hash"` // File content hash
|
||||
Category FileCategory `json:"category"` // File category
|
||||
Type FileType `json:"type"` // File type (file/folder)
|
||||
Thumbnail string `json:"thumbnail,omitempty"` // Thumbnail URL
|
||||
URL string `json:"url,omitempty"` // Preview/download URL for files under 5MB
|
||||
CreatedAt time.Time `json:"created_at"` // Creation time
|
||||
UpdatedAt time.Time `json:"updated_at"` // Last update time
|
||||
PlayCursor string `json:"play_cursor,omitempty"` // Playback progress
|
||||
VideoMediaMetadata *VideoMediaMetadata `json:"video_media_metadata,omitempty"` // Video metadata
|
||||
VideoPreviewMetadata *VideoPreviewMetadata `json:"video_preview_metadata,omitempty"` // Video preview metadata
|
||||
}
|
||||
|
||||
// ListFileRequest represents the request body for /adrive/v1.0/openFile/list endpoint
|
||||
type ListFileRequest struct {
|
||||
DriveID string `json:"drive_id"` // Drive ID
|
||||
Limit int `json:"limit,omitempty"` // Max items to return (default 50, max 100)
|
||||
Marker string `json:"marker,omitempty"` // Pagination marker
|
||||
OrderBy OrderBy `json:"order_by,omitempty"` // Sort field
|
||||
OrderDirection OrderDirection `json:"order_direction,omitempty"` // Sort direction
|
||||
ParentFileID string `json:"parent_file_id"` // Parent folder ID (root for root folder)
|
||||
Category string `json:"category,omitempty"` // File categories (comma-separated)
|
||||
Type FileType `json:"type,omitempty"` // Filter by type
|
||||
VideoThumbnailTime int64 `json:"video_thumbnail_time,omitempty"` // Video thumbnail timestamp (ms)
|
||||
VideoThumbnailWidth int `json:"video_thumbnail_width,omitempty"` // Video thumbnail width
|
||||
ImageThumbnailWidth int `json:"image_thumbnail_width,omitempty"` // Image thumbnail width
|
||||
Fields string `json:"fields,omitempty"` // Fields to return
|
||||
}
|
||||
|
||||
// ListFileResponse represents the response from file listing endpoints
|
||||
type ListFileResponse struct {
|
||||
Items []FileItem `json:"items"` // List of files/folders
|
||||
NextMarker string `json:"next_marker,omitempty"` // Next page marker
|
||||
}
|
||||
|
||||
// SearchFileRequest represents the request body for /adrive/v1.0/openFile/search endpoint
|
||||
type SearchFileRequest struct {
|
||||
DriveID string `json:"drive_id"` // Drive ID
|
||||
Limit int `json:"limit,omitempty"` // Max items to return (default 100, max 100)
|
||||
Marker string `json:"marker,omitempty"` // Pagination marker
|
||||
Query string `json:"query"` // Search query
|
||||
OrderBy string `json:"order_by,omitempty"` // Sort order
|
||||
VideoThumbnailTime int64 `json:"video_thumbnail_time,omitempty"` // Video thumbnail timestamp (ms)
|
||||
VideoThumbnailWidth int `json:"video_thumbnail_width,omitempty"` // Video thumbnail width
|
||||
ImageThumbnailWidth int `json:"image_thumbnail_width,omitempty"` // Image thumbnail width
|
||||
ReturnTotalCount bool `json:"return_total_count,omitempty"` // Whether to return total count
|
||||
}
|
||||
|
||||
// SearchFileResponse represents the response from the search endpoint
|
||||
type SearchFileResponse struct {
|
||||
Items []FileItem `json:"items"` // Search results
|
||||
NextMarker string `json:"next_marker,omitempty"` // Next page marker
|
||||
TotalCount int64 `json:"total_count,omitempty"` // Total number of matches
|
||||
}
|
||||
|
||||
// StarredFileRequest represents the request body for /adrive/v1.0/openFile/starredList endpoint
|
||||
type StarredFileRequest struct {
|
||||
DriveID string `json:"drive_id"` // Drive ID
|
||||
Limit int `json:"limit,omitempty"` // Max items to return (default 100, max 100)
|
||||
Marker string `json:"marker,omitempty"` // Pagination marker
|
||||
OrderBy OrderBy `json:"order_by,omitempty"` // Sort field
|
||||
OrderDirection OrderDirection `json:"order_direction,omitempty"` // Sort direction
|
||||
Type FileType `json:"type,omitempty"` // Filter by type
|
||||
VideoThumbnailTime int64 `json:"video_thumbnail_time,omitempty"` // Video thumbnail timestamp (ms)
|
||||
VideoThumbnailWidth int `json:"video_thumbnail_width,omitempty"` // Video thumbnail width
|
||||
ImageThumbnailWidth int `json:"image_thumbnail_width,omitempty"` // Image thumbnail width
|
||||
}
|
||||
|
||||
// GetFileRequest represents the request body for /adrive/v1.0/openFile/get endpoint
|
||||
type GetFileRequest struct {
|
||||
DriveID string `json:"drive_id"` // Drive ID
|
||||
FileID string `json:"file_id"` // File ID
|
||||
VideoThumbnailTime int64 `json:"video_thumbnail_time,omitempty"` // Video thumbnail timestamp (ms)
|
||||
VideoThumbnailWidth int `json:"video_thumbnail_width,omitempty"` // Video thumbnail width
|
||||
ImageThumbnailWidth int `json:"image_thumbnail_width,omitempty"` // Image thumbnail width
|
||||
Fields string `json:"fields,omitempty"` // Specific fields to return (comma-separated)
|
||||
}
|
||||
|
||||
// GetFileByPathRequest represents the request body for /adrive/v1.0/openFile/get_by_path endpoint
|
||||
type GetFileByPathRequest struct {
|
||||
DriveID string `json:"drive_id"` // Drive ID
|
||||
FilePath string `json:"file_path"` // File path (e.g., /folder/file.txt)
|
||||
}
|
||||
|
||||
// BatchGetFileRequest represents the request body for /adrive/v1.0/openFile/batch/get endpoint
|
||||
type BatchGetFileRequest struct {
|
||||
FileList []DriveFile `json:"file_list"` // List of files to get details for
|
||||
VideoThumbnailTime int64 `json:"video_thumbnail_time,omitempty"` // Video thumbnail timestamp (ms)
|
||||
VideoThumbnailWidth int `json:"video_thumbnail_width,omitempty"` // Video thumbnail width
|
||||
ImageThumbnailWidth int `json:"image_thumbnail_width,omitempty"` // Image thumbnail width
|
||||
}
|
||||
|
||||
// BatchGetFileResponse represents the response from the batch get endpoint
|
||||
type BatchGetFileResponse struct {
|
||||
Items []FileItem `json:"items"` // List of file details
|
||||
}
|
||||
|
||||
// FileDetailExtended represents a file with additional path information
|
||||
type FileDetailExtended struct {
|
||||
FileItem
|
||||
IDPath string `json:"id_path,omitempty"` // Path using IDs (e.g., root:/64de0fb2...)
|
||||
NamePath string `json:"name_path,omitempty"` // Path using names (e.g., root:/folder/file.txt)
|
||||
}
|
||||
|
||||
// GetDownloadURLRequest represents the request body for /adrive/v1.0/openFile/getDownloadUrl endpoint
|
||||
type GetDownloadURLRequest struct {
|
||||
DriveID string `json:"drive_id"` // Drive ID
|
||||
FileID string `json:"file_id"` // File ID
|
||||
ExpireSec int64 `json:"expire_sec,omitempty"` // URL expiration time in seconds (default 900, max 14400 for premium apps)
|
||||
}
|
||||
|
||||
// GetDownloadURLResponse represents the response from the download URL endpoint
|
||||
type GetDownloadURLResponse struct {
|
||||
URL string `json:"url"` // Download URL
|
||||
Expiration Time `json:"expiration"` // URL expiration time
|
||||
Method string `json:"method"` // Download method
|
||||
Description string `json:"description"` // Additional information about download speed and privileges
|
||||
}
|
||||
|
||||
// CheckNameMode represents how to handle naming conflicts
|
||||
type CheckNameMode string
|
||||
|
||||
const (
|
||||
CheckNameModeAutoRename CheckNameMode = "auto_rename" // Automatically rename if file exists
|
||||
CheckNameModeRefuse CheckNameMode = "refuse" // Don't create if file exists
|
||||
CheckNameModeIgnore CheckNameMode = "ignore" // Create even if file exists
|
||||
)
|
||||
|
||||
// PartInfo represents information about a file part for multipart upload
|
||||
type PartInfo struct {
|
||||
PartNumber int `json:"part_number"` // Part sequence number (1-based)
|
||||
UploadURL string `json:"upload_url,omitempty"` // Upload URL for this part
|
||||
PartSize int64 `json:"part_size,omitempty"` // Size of this part
|
||||
Etag string `json:"etag,omitempty"` // ETag returned after part upload
|
||||
}
|
||||
|
||||
// StreamInfo represents stream information for special file formats (e.g., livp)
|
||||
type StreamInfo struct {
|
||||
ContentHash string `json:"content_hash,omitempty"` // Content hash
|
||||
ContentHashName string `json:"content_hash_name,omitempty"` // Hash algorithm name
|
||||
ProofVersion string `json:"proof_version,omitempty"` // Proof version
|
||||
ProofCode string `json:"proof_code,omitempty"` // Proof code
|
||||
ContentMD5 string `json:"content_md5,omitempty"` // Content MD5
|
||||
PreHash string `json:"pre_hash,omitempty"` // Pre-hash for large files
|
||||
Size int64 `json:"size,omitempty"` // Stream size
|
||||
PartInfoList []PartInfo `json:"part_info_list,omitempty"` // Part information list
|
||||
}
|
||||
|
||||
// CreateFileRequest represents the request body for /adrive/v1.0/openFile/create endpoint
|
||||
type CreateFileRequest struct {
|
||||
DriveID string `json:"drive_id"` // Drive ID
|
||||
ParentFileID string `json:"parent_file_id"` // Parent folder ID (root for root directory)
|
||||
Name string `json:"name"` // File name (UTF-8, max 1024 bytes)
|
||||
Type FileType `json:"type"` // File type (file/folder)
|
||||
CheckNameMode CheckNameMode `json:"check_name_mode"` // How to handle name conflicts
|
||||
PartInfoList []PartInfo `json:"part_info_list,omitempty"` // Part information for multipart upload (max 10000)
|
||||
StreamsInfo []StreamInfo `json:"streams_info,omitempty"` // Stream information (for special formats)
|
||||
PreHash string `json:"pre_hash,omitempty"` // First 1KB SHA1 for quick duplicate check
|
||||
Size int64 `json:"size,omitempty"` // File size in bytes
|
||||
ContentHash string `json:"content_hash,omitempty"` // Full file content hash
|
||||
ContentHashName string `json:"content_hash_name,omitempty"` // Hash algorithm (default: sha1)
|
||||
ProofCode string `json:"proof_code,omitempty"` // Proof code for duplicate check
|
||||
ProofVersion string `json:"proof_version,omitempty"` // Proof version (fixed: v1)
|
||||
LocalCreatedAt *Time `json:"local_created_at,omitempty"` // Local creation time
|
||||
LocalModifiedAt *Time `json:"local_modified_at,omitempty"` // Local modification time
|
||||
}
|
||||
|
||||
// CreateFileResponse represents the response from the file creation endpoint
|
||||
type CreateFileResponse struct {
|
||||
DriveID string `json:"drive_id"` // Drive ID
|
||||
FileID string `json:"file_id"` // File ID
|
||||
FileName string `json:"file_name"` // File name
|
||||
ParentFileID string `json:"parent_file_id"` // Parent folder ID
|
||||
Status string `json:"status"` // Status
|
||||
UploadID string `json:"upload_id"` // Upload ID (empty for folders)
|
||||
Available bool `json:"available"` // Whether the file is available
|
||||
Exist bool `json:"exist"` // Whether a file with same name exists
|
||||
RapidUpload bool `json:"rapid_upload"` // Whether rapid upload was used
|
||||
PartInfoList []PartInfo `json:"part_info_list"` // Part information list
|
||||
}
|
||||
|
||||
// GetUploadURLRequest represents the request body for /adrive/v1.0/openFile/getUploadUrl endpoint
|
||||
type GetUploadURLRequest struct {
|
||||
DriveID string `json:"drive_id"` // Drive ID
|
||||
FileID string `json:"file_id"` // File ID
|
||||
UploadID string `json:"upload_id"` // Upload ID from file creation
|
||||
PartInfoList []PartInfo `json:"part_info_list"` // Part information list
|
||||
}
|
||||
|
||||
// GetUploadURLResponse represents the response from the upload URL endpoint
|
||||
type GetUploadURLResponse struct {
|
||||
DriveID string `json:"drive_id"` // Drive ID
|
||||
FileID string `json:"file_id"` // File ID
|
||||
UploadID string `json:"upload_id"` // Upload ID
|
||||
CreatedAt Time `json:"created_at"` // Creation time
|
||||
PartInfoList []PartInfo `json:"part_info_list"` // Part information with URLs
|
||||
}
|
||||
|
||||
// ListUploadedPartsRequest represents the request body for /adrive/v1.0/openFile/listUploadedParts endpoint
|
||||
type ListUploadedPartsRequest struct {
|
||||
DriveID string `json:"drive_id"` // Drive ID
|
||||
FileID string `json:"file_id"` // File ID
|
||||
UploadID string `json:"upload_id"` // Upload ID
|
||||
PartNumberMarker string `json:"part_number_marker,omitempty"` // Marker for pagination
|
||||
}
|
||||
|
||||
// ListUploadedPartsResponse represents the response from the list uploaded parts endpoint
|
||||
type ListUploadedPartsResponse struct {
|
||||
DriveID string `json:"drive_id"` // Drive ID
|
||||
UploadID string `json:"upload_id"` // Upload ID
|
||||
ParallelUpload bool `json:"parallelUpload"` // Whether parallel upload is enabled
|
||||
UploadedParts []PartInfo `json:"uploaded_parts"` // List of uploaded parts
|
||||
NextPartNumberMarker string `json:"next_part_number_marker"` // Marker for next page
|
||||
}
|
||||
|
||||
// CompleteUploadRequest represents the request body for /adrive/v1.0/openFile/complete endpoint
|
||||
type CompleteUploadRequest struct {
|
||||
DriveID string `json:"drive_id"` // Drive ID
|
||||
FileID string `json:"file_id"` // File ID
|
||||
UploadID string `json:"upload_id"` // Upload ID
|
||||
}
|
||||
|
||||
// CompleteUploadResponse represents the response from the complete upload endpoint
|
||||
type CompleteUploadResponse struct {
|
||||
DriveID string `json:"drive_id"` // Drive ID
|
||||
FileID string `json:"file_id"` // File ID
|
||||
Name string `json:"name"` // File name
|
||||
Size int64 `json:"size"` // File size
|
||||
FileExtension string `json:"file_extension"` // File extension
|
||||
ContentHash string `json:"content_hash"` // Content hash
|
||||
Category FileCategory `json:"category"` // File category
|
||||
Type FileType `json:"type"` // File type
|
||||
Thumbnail string `json:"thumbnail,omitempty"` // Thumbnail URL
|
||||
URL string `json:"url,omitempty"` // Preview URL
|
||||
DownloadURL string `json:"download_url,omitempty"` // Download URL
|
||||
CreatedAt Time `json:"created_at"` // Creation time
|
||||
UpdatedAt Time `json:"updated_at"` // Last update time
|
||||
}
|
||||
|
||||
// UpdateFileRequest represents the request body for /adrive/v1.0/openFile/update endpoint
|
||||
type UpdateFileRequest struct {
|
||||
DriveID string `json:"drive_id"` // Drive ID
|
||||
FileID string `json:"file_id"` // File ID
|
||||
Name string `json:"name,omitempty"` // New file name
|
||||
CheckNameMode CheckNameMode `json:"check_name_mode,omitempty"` // How to handle name conflicts
|
||||
Starred *bool `json:"starred,omitempty"` // Whether to star/unstar the file
|
||||
}
|
||||
|
||||
// UpdateFileResponse represents the response from the file update endpoint
|
||||
type UpdateFileResponse struct {
|
||||
DriveID string `json:"drive_id"` // Drive ID
|
||||
FileID string `json:"file_id"` // File ID
|
||||
Name string `json:"name"` // File name
|
||||
Size int64 `json:"size"` // File size
|
||||
FileExtension string `json:"file_extension"` // File extension
|
||||
ContentHash string `json:"content_hash"` // Content hash
|
||||
Category FileCategory `json:"category"` // File category
|
||||
Type FileType `json:"type"` // File type (file/folder)
|
||||
CreatedAt Time `json:"created_at"` // Creation time
|
||||
UpdatedAt Time `json:"updated_at"` // Last update time
|
||||
}
|
||||
|
||||
// MoveFileRequest represents the request body for /adrive/v1.0/openFile/move endpoint
|
||||
type MoveFileRequest struct {
|
||||
DriveID string `json:"drive_id"` // Current drive ID
|
||||
FileID string `json:"file_id"` // File ID to move
|
||||
ToDriveID string `json:"to_drive_id,omitempty"` // Target drive ID (defaults to current drive_id)
|
||||
ToParentFileID string `json:"to_parent_file_id"` // Target parent folder ID (root for root directory)
|
||||
CheckNameMode CheckNameMode `json:"check_name_mode,omitempty"` // How to handle name conflicts (default: refuse)
|
||||
NewName string `json:"new_name,omitempty"` // New name to use if there's a conflict
|
||||
}
|
||||
|
||||
// MoveFileResponse represents the response from the file move endpoint
|
||||
type MoveFileResponse struct {
|
||||
DriveID string `json:"drive_id"` // Drive ID
|
||||
FileID string `json:"file_id"` // File ID
|
||||
AsyncTaskID string `json:"async_task_id,omitempty"` // Async task ID for folder moves
|
||||
Exist bool `json:"exist"` // Whether file already exists in target
|
||||
}
|
||||
|
||||
// CopyFileRequest represents the request body for /adrive/v1.0/openFile/copy endpoint
|
||||
type CopyFileRequest struct {
|
||||
DriveID string `json:"drive_id"` // Drive ID
|
||||
FileID string `json:"file_id"` // File ID to copy
|
||||
ToParentFileID string `json:"to_parent_file_id"` // Target parent folder ID (root for root directory)
|
||||
CheckNameMode string `json:"check_name_mode,omitempty"` // The process mode when exists file/directory with the same name
|
||||
NewName string `json:"auto_rename,omitempty"` // New name when there's a conflict
|
||||
}
|
||||
|
||||
// CopyFileResponse represents the response from the file copy endpoint
|
||||
type CopyFileResponse struct {
|
||||
DriveID string `json:"drive_id"` // Drive ID
|
||||
FileID string `json:"file_id"` // File ID
|
||||
AsyncTaskID string `json:"async_task_id,omitempty"` // Async task ID for folder copies
|
||||
}
|
||||
|
||||
// TaskState represents the state of an async task
|
||||
type TaskState string
|
||||
|
||||
const (
|
||||
TaskStateSucceed TaskState = "Succeed" // Task completed successfully
|
||||
TaskStateRunning TaskState = "Running" // Task is still running
|
||||
TaskStateFailed TaskState = "Failed" // Task failed
|
||||
)
|
||||
|
||||
// TrashFileRequest represents the request body for /adrive/v1.0/openFile/recyclebin/trash endpoint
|
||||
type TrashFileRequest struct {
|
||||
DriveID string `json:"drive_id"` // Drive ID
|
||||
FileID string `json:"file_id"` // File ID to move to trash
|
||||
}
|
||||
|
||||
// TrashFileResponse represents the response from the trash file endpoint
|
||||
type TrashFileResponse struct {
|
||||
DriveID string `json:"drive_id"` // Drive ID
|
||||
FileID string `json:"file_id"` // File ID
|
||||
AsyncTaskID string `json:"async_task_id,omitempty"` // Async task ID for folder operations
|
||||
}
|
||||
|
||||
// DeleteFileRequest represents the request body for /adrive/v1.0/openFile/delete endpoint
|
||||
type DeleteFileRequest struct {
|
||||
DriveID string `json:"drive_id"` // Drive ID
|
||||
FileID string `json:"file_id"` // File ID to delete
|
||||
}
|
||||
|
||||
// DeleteFileResponse represents the response from the delete file endpoint
|
||||
type DeleteFileResponse struct {
|
||||
DriveID string `json:"drive_id"` // Drive ID
|
||||
FileID string `json:"file_id"` // File ID
|
||||
AsyncTaskID string `json:"async_task_id,omitempty"` // Async task ID for folder operations
|
||||
}
|
||||
|
||||
// GetAsyncTaskRequest represents the request body for /adrive/v1.0/openFile/async_task/get endpoint
|
||||
type GetAsyncTaskRequest struct {
|
||||
AsyncTaskID string `json:"async_task_id"` // Async task ID to query
|
||||
}
|
||||
|
||||
// GetAsyncTaskResponse represents the response from the get async task endpoint
|
||||
type GetAsyncTaskResponse struct {
|
||||
State TaskState `json:"state"` // Task state (Succeed/Running/Failed)
|
||||
AsyncTaskID string `json:"async_task_id"` // Async task ID
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user