WIP upload,mkdir,update

This commit is contained in:
ns2kracy 2025-03-03 00:00:44 +08:00
parent 5c1eebdc33
commit 98db73d759
No known key found for this signature in database
GPG Key ID: 0774F76059AAAB0F
2 changed files with 443 additions and 859 deletions

View File

@ -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, &copyResp)
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,

View File

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