mirror of
https://github.com/rclone/rclone.git
synced 2025-04-19 18:31:10 +08:00
namecrane: support server-sided dirmove/move, links
Adds support for server-sided DirMove and Move, required for certain applications using mounts, and full support for public link generation (with expiration)
This commit is contained in:
parent
e9da24a89c
commit
314d78d8c3
@ -33,10 +33,14 @@ const (
|
||||
|
||||
apiUpload = "api/upload"
|
||||
apiFiles = "api/v1/filestorage/files"
|
||||
apiMoveFiles = "api/v1/filestorage/move-files"
|
||||
apiEditFile = "api/v1/filestorage/{fileId}/edit"
|
||||
apiGetFileLink = "api/v1/filestorage/{fileId}/getlink"
|
||||
apiFolder = "api/v1/filestorage/folder"
|
||||
apiFolders = "api/v1/filestorage/folders"
|
||||
apiPutFolder = "api/v1/filestorage/folder-put"
|
||||
apiDeleteFolder = "api/v1/filestorage/delete-folder"
|
||||
apiPatchFolder = "api/v1/filestorage/folder-patch"
|
||||
apiFileDownload = "api/v1/filestorage/%s/download"
|
||||
)
|
||||
|
||||
@ -439,6 +443,10 @@ func (n *Namecrane) Find(ctx context.Context, file string) (*Folder, *File, erro
|
||||
}
|
||||
|
||||
folder = &folders[0]
|
||||
|
||||
if name == "" {
|
||||
return folder, nil, nil
|
||||
}
|
||||
} else {
|
||||
var err error
|
||||
|
||||
@ -530,6 +538,175 @@ func (n *Namecrane) DeleteFolder(ctx context.Context, folder string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type moveFilesRequest struct {
|
||||
NewFolder string `json:"newFolder"`
|
||||
FileIDs []string `json:"fileIDs"`
|
||||
}
|
||||
|
||||
// MoveFiles moves files to the specified folder
|
||||
func (n *Namecrane) MoveFiles(ctx context.Context, folder string, fileIDs ...string) error {
|
||||
res, err := n.doRequest(ctx, http.MethodPost, apiMoveFiles, moveFilesRequest{
|
||||
NewFolder: folder,
|
||||
FileIDs: fileIDs,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("%w: %d", ErrUnexpectedStatus, res.StatusCode)
|
||||
}
|
||||
|
||||
var response FolderResponse
|
||||
|
||||
if err := res.Decode(&response); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !response.Success {
|
||||
return fmt.Errorf("failed to create directory, status: %d, response: %s", res.StatusCode, response.Message)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type editFileRequest struct {
|
||||
NewFilename string `json:"newFilename"`
|
||||
}
|
||||
|
||||
// RenameFile will rename the specified file to the new name
|
||||
func (n *Namecrane) RenameFile(ctx context.Context, fileID string, name string) error {
|
||||
res, err := n.doRequest(ctx, http.MethodPost, apiEditFile, editFileRequest{
|
||||
NewFilename: name,
|
||||
}, WithURLParameter("fileId", fileID))
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("%w: %d", ErrUnexpectedStatus, res.StatusCode)
|
||||
}
|
||||
|
||||
var response defaultResponse
|
||||
|
||||
if err := res.Decode(&response); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !response.Success {
|
||||
return fmt.Errorf("failed to create directory, status: %d, response: %s", res.StatusCode, response.Message)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type EditFileParams struct {
|
||||
Password string `json:"password"`
|
||||
Published bool `json:"published"`
|
||||
PublishedUntil time.Time `json:"publishedUntil"`
|
||||
ShortLink string `json:"shortLink"`
|
||||
PublicDownloadLink string `json:"publicDownloadLink"`
|
||||
}
|
||||
|
||||
// EditFile updates a file on the backend
|
||||
func (n *Namecrane) EditFile(ctx context.Context, fileID string, params EditFileParams) error {
|
||||
res, err := n.doRequest(ctx, http.MethodPost, apiEditFile, params, WithURLParameter("fileId", fileID))
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("%w: %d", ErrUnexpectedStatus, res.StatusCode)
|
||||
}
|
||||
|
||||
var response defaultResponse
|
||||
|
||||
if err := res.Decode(&response); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !response.Success {
|
||||
return fmt.Errorf("failed to create directory, status: %d, response: %s", res.StatusCode, response.Message)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type linkResponse struct {
|
||||
defaultResponse
|
||||
PublicLink string `json:"publicLink"`
|
||||
ShortLink string `json:"shortLink"`
|
||||
IsPublic bool `json:"isPublic"`
|
||||
}
|
||||
|
||||
// GetLink creates a short link and public link to a file
|
||||
// This is combined with EditFile to make it public
|
||||
func (n *Namecrane) GetLink(ctx context.Context, fileID string) (string, string, error) {
|
||||
res, err := n.doRequest(ctx, http.MethodGet, apiGetFileLink, nil, WithURLParameter("fileId", fileID))
|
||||
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return "", "", fmt.Errorf("%w: %d", ErrUnexpectedStatus, res.StatusCode)
|
||||
}
|
||||
|
||||
var response linkResponse
|
||||
|
||||
if err := res.Decode(&response); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
if !response.Success {
|
||||
return "", "", fmt.Errorf("failed to create directory, status: %d, response: %s", res.StatusCode, response.Message)
|
||||
}
|
||||
|
||||
return response.ShortLink, response.PublicLink, nil
|
||||
}
|
||||
|
||||
type patchFolderRequest struct {
|
||||
folderRequest
|
||||
ParentFolder string `json:"parentFolder"`
|
||||
Folder string `json:"folder"`
|
||||
NewFolderName string `json:"newFolderName,omitempty"`
|
||||
NewParentFolder string `json:"newParentFolder,omitempty"`
|
||||
}
|
||||
|
||||
func (n *Namecrane) MoveFolder(ctx context.Context, folder, newParentFolder string) error {
|
||||
_, subfolder := n.parsePath(folder)
|
||||
|
||||
res, err := n.doRequest(ctx, http.MethodPost, apiPatchFolder, patchFolderRequest{
|
||||
//ParentFolder: parent,
|
||||
Folder: folder,
|
||||
NewParentFolder: newParentFolder,
|
||||
NewFolderName: subfolder,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("%w: %d (%s)", ErrUnexpectedStatus, res.StatusCode, string(res.Data()))
|
||||
}
|
||||
|
||||
var response defaultResponse
|
||||
|
||||
if err := res.Decode(&response); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !response.Success {
|
||||
return fmt.Errorf("failed to move directory, status: %d, response: %s", res.StatusCode, response.Message)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// apiUrl joins the base API URL with the path specified
|
||||
func (n *Namecrane) apiUrl(subPath string) (string, error) {
|
||||
u, err := url.Parse(n.apiURL)
|
||||
@ -560,6 +737,23 @@ func WithHeader(key, value string) RequestOpt {
|
||||
}
|
||||
}
|
||||
|
||||
// WithURLParameter replaces a URL parameter encased in {} with the value
|
||||
func WithURLParameter(key string, value any) RequestOpt {
|
||||
return func(r *http.Request) {
|
||||
var valStr string
|
||||
switch v := value.(type) {
|
||||
case string:
|
||||
valStr = v
|
||||
case int:
|
||||
valStr = strconv.Itoa(v)
|
||||
default:
|
||||
valStr = fmt.Sprintf("%v", v)
|
||||
}
|
||||
|
||||
r.URL.Path = strings.Replace(r.URL.Path, "{"+key+"}", valStr, -1)
|
||||
}
|
||||
}
|
||||
|
||||
func doHttpRequest(ctx context.Context, client *http.Client, method, u string, body any, opts ...RequestOpt) (*Response, error) {
|
||||
var bodyReader io.Reader
|
||||
var jsonBody bool
|
||||
@ -581,6 +775,8 @@ func doHttpRequest(ctx context.Context, client *http.Client, method, u string, b
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fs.Debugf(nil, "body: %s", string(b))
|
||||
|
||||
bodyReader = bytes.NewReader(b)
|
||||
|
||||
jsonBody = true
|
||||
|
@ -343,10 +343,16 @@ func (f *Fs) Sortable() bool {
|
||||
func (f *Fs) NewObject(ctx context.Context, remote string) (fs.Object, error) {
|
||||
fs.Debugf(f, "New object %s", remote)
|
||||
|
||||
remote = path.Join(f.root, remote)
|
||||
|
||||
folder, file, err := f.client.Find(ctx, remote)
|
||||
|
||||
if err != nil {
|
||||
fs.Debugf(f, "Unable to find existing file, not necessarily a bad thing: %s", err.Error())
|
||||
if errors.Is(err, ErrNoFile) {
|
||||
return nil, fs.ErrorObjectNotFound
|
||||
}
|
||||
|
||||
fs.Debugf(f, "Unable to find existing file at %s, not necessarily a bad thing: %s", remote, err.Error())
|
||||
}
|
||||
|
||||
return &Object{
|
||||
@ -357,6 +363,13 @@ func (f *Fs) NewObject(ctx context.Context, remote string) (fs.Object, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (f *Fs) newObject(remote string) *Object {
|
||||
return &Object{
|
||||
fs: f,
|
||||
remote: remote,
|
||||
}
|
||||
}
|
||||
|
||||
func (f *Fs) Put(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (fs.Object, error) {
|
||||
remote := src.Remote()
|
||||
|
||||
@ -381,6 +394,181 @@ func (f *Fs) Put(ctx context.Context, in io.Reader, src fs.ObjectInfo, options .
|
||||
}, nil
|
||||
}
|
||||
|
||||
// DirMove moves src, srcRemote to this remote at dstRemote
|
||||
// using server-side move operations.
|
||||
//
|
||||
// Will only be called if src.Fs().Name() == f.Name()
|
||||
//
|
||||
// If it isn't possible then return fs.ErrorCantDirMove
|
||||
//
|
||||
// If destination exists then return fs.ErrorDirExists
|
||||
func (f *Fs) DirMove(ctx context.Context, src fs.Fs, srcRemote, dstRemote string) error {
|
||||
srcFs, ok := src.(*Fs)
|
||||
if !ok {
|
||||
fs.Debugf(srcFs, "Can't move directory - not same remote type")
|
||||
return fs.ErrorCantDirMove
|
||||
}
|
||||
|
||||
fs.Debugf(f, "Moving directory %s to %s", srcRemote, dstRemote)
|
||||
|
||||
srcRemote = path.Join(srcFs.root, srcRemote)
|
||||
dstRemote = path.Join(f.root, dstRemote)
|
||||
|
||||
// Check source remote for the folder to move
|
||||
folder, _, err := f.client.Find(ctx, srcRemote)
|
||||
|
||||
if err != nil || folder == nil {
|
||||
return fs.ErrorDirNotFound
|
||||
}
|
||||
|
||||
// Confirm that the parent folder exists in the destination path
|
||||
parent, _, err := f.client.Find(ctx, dstRemote)
|
||||
|
||||
if errors.Is(err, ErrNoFile) {
|
||||
// If the parent does not exist, create it (equivalent to MkdirAll)
|
||||
parent, err = f.client.CreateFolder(ctx, dstRemote)
|
||||
|
||||
if err != nil {
|
||||
return fs.ErrorDirNotFound
|
||||
}
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check dest path for existing folder (dstRemote + folder.Name)
|
||||
existing, _, _ := f.client.Find(ctx, path.Join(dstRemote, folder.Name))
|
||||
|
||||
if existing != nil {
|
||||
return fs.ErrorDirExists
|
||||
}
|
||||
|
||||
// Use server side move
|
||||
err = f.client.MoveFolder(ctx, folder.Path, parent.Path)
|
||||
|
||||
if err != nil {
|
||||
// not quite clear, but probably trying to move directory across file system
|
||||
// boundaries. Copying might still work.
|
||||
fs.Debugf(src, "Can't move dir: %v: trying copy", err)
|
||||
return fs.ErrorCantDirMove
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Move src to this remote using server-side move 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.ErrorCantMove
|
||||
func (f *Fs) Move(ctx context.Context, src fs.Object, remote string) (fs.Object, error) {
|
||||
srcObj, ok := src.(*Object)
|
||||
if !ok {
|
||||
fs.Debugf(src, "Can't move - not same remote type")
|
||||
return nil, fs.ErrorCantMove
|
||||
}
|
||||
|
||||
remote = path.Join(f.root, remote)
|
||||
|
||||
// Temporary Object under construction
|
||||
dstObj := f.newObject(remote)
|
||||
|
||||
// Check if the destination is a folder
|
||||
_, err := dstObj.Stat(ctx)
|
||||
|
||||
if errors.Is(err, ErrNoFile) {
|
||||
// OK
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if dstObj.folder != nil {
|
||||
return nil, errors.New("can't move file onto non-file")
|
||||
}
|
||||
|
||||
newFolder, _ := f.client.parsePath(remote)
|
||||
|
||||
baseFolder, _, err := f.client.Find(ctx, newFolder)
|
||||
|
||||
if err != nil && errors.Is(err, ErrNoFile) {
|
||||
baseFolder, err = f.client.CreateFolder(ctx, newFolder)
|
||||
|
||||
if err != nil {
|
||||
fs.Debugf(f, "Unable to create parent directory due to error %s", err.Error())
|
||||
return nil, fs.ErrorDirNotFound
|
||||
}
|
||||
} else if err != nil {
|
||||
fs.Debugf(f, "Unable to get parent directory due to error %s", err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = f.client.MoveFiles(ctx, baseFolder.Path, srcObj.file.ID)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, err = dstObj.Stat(ctx)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return dstObj, nil
|
||||
}
|
||||
|
||||
// PublicLink generates a public link to the remote path (usually readable by anyone)
|
||||
func (f *Fs) PublicLink(ctx context.Context, remote string, expire fs.Duration, unlink bool) (string, error) {
|
||||
remote = path.Join(f.root, remote)
|
||||
|
||||
_, file, err := f.client.Find(ctx, remote)
|
||||
|
||||
if errors.Is(err, ErrNoFile) {
|
||||
return "", fs.ErrorObjectNotFound
|
||||
}
|
||||
|
||||
// Unlink just sets published to false
|
||||
if unlink {
|
||||
err = f.client.EditFile(ctx, file.ID, EditFileParams{
|
||||
Published: false,
|
||||
PublishedUntil: time.Time{},
|
||||
})
|
||||
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// Generate the link
|
||||
shortLink, publicLink, err := f.client.GetLink(ctx, file.ID)
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
publicLink = strings.TrimRight(f.apiURL, "/") + "/" + publicLink
|
||||
|
||||
params := EditFileParams{
|
||||
ShortLink: shortLink,
|
||||
PublicDownloadLink: publicLink,
|
||||
Published: true,
|
||||
}
|
||||
|
||||
if expire.IsSet() {
|
||||
params.PublishedUntil = time.Now().Add(time.Duration(expire))
|
||||
}
|
||||
|
||||
// Set the file to public
|
||||
err = f.client.EditFile(ctx, file.ID, params)
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return publicLink, nil
|
||||
}
|
||||
|
||||
func (o *Object) ModTime(ctx context.Context) time.Time {
|
||||
if o.file != nil {
|
||||
return o.file.DateAdded
|
||||
@ -508,6 +696,9 @@ func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, op
|
||||
|
||||
// Remove deletes the file represented by the object from the remote.
|
||||
func (o *Object) Remove(ctx context.Context) error {
|
||||
if o.file == nil {
|
||||
return fs.ErrorNotAFile
|
||||
}
|
||||
return o.fs.client.DeleteFiles(ctx, o.file.ID)
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user