AlistGo/alist · warning · MoveBetweenTwoStorages

can't move files between two storages, try to copy

Error message

can't move files between two storages, try to copy

What it means

errs.MoveBetweenTwoStorages is returned when a move operation is requested across two different storage mounts. Filesystem-level move between storages is not a single atomic remote operation, so the framework refuses it and the message itself tells you the remedy: copy instead.

Source

Thrown at internal/errs/errors.go:15

package errs

import (
	"errors"
	"fmt"

	pkgerr "github.com/pkg/errors"
)

var (
	NotImplement = errors.New("not implement")
	NotSupport   = errors.New("not support")
	RelativePath = errors.New("access using relative path is not allowed")

	MoveBetweenTwoStorages = errors.New("can't move files between two storages, try to copy")
	UploadNotSupported     = errors.New("upload not supported")

	MetaNotFound     = errors.New("meta not found")
	StorageNotFound  = errors.New("storage not found")
	StreamIncomplete = errors.New("upload/download stream incomplete, possible network issue")
	StreamPeekFail   = errors.New("StreamPeekFail")

	UnknownArchiveFormat      = errors.New("unknown archive format")
	WrongArchivePassword      = errors.New("wrong archive password")
	DriverExtractNotSupported = errors.New("driver extraction not supported")
)

// NewErr wrap constant error with an extra message
// use errors.Is(err1, StorageNotFound) to check if err belongs to any internal error
func NewErr(err error, format string, a ...any) error {
	return fmt.Errorf("%w; %s", err, fmt.Sprintf(format, a...))
}

View on GitHub (pinned to 843d9dc814)

Solutions

  1. Use copy: copy the object to the destination storage, then delete the source after verifying the copy
  2. Verify the destination path stays inside the same storage if a true move was intended (fix the typo)
  3. For frequent cross-storage needs, script copy+delete explicitly with error checks between steps
  4. Some frontends do this automatically — enable that option if available

Example fix

// before
err := fsMove(ctx, "/cloud-a/docs/x.txt", "/cloud-b/docs/x.txt")

// after
copyErr := fsCopy(ctx, "/cloud-a/docs/x.txt", "/cloud-b/docs/x.txt")
if copyErr != nil { return copyErr }
err := fsRemove(ctx, "/cloud-a/docs/x.txt")
Defensive patterns

Strategy: fallback

Validate before calling

if srcStoragePath.Mount != dstStoragePath.Mount { return errs.MoveBetweenTwoStorages }

Type guard

func isCrossStorageMove(err error) bool { return errors.Is(err, errs.MoveBetweenTwoStorages) }

Try / catch

err := fsMove(ctx, srcPath, dstPath)
if errors.Is(err, errs.MoveBetweenTwoStorages) {
    if err := fsCopy(ctx, srcPath, dstPath); err != nil { return err }
    return fsRemove(ctx, srcPath)
}

Prevention

When it happens

Trigger: Calling Move/Rename with a source object on storage A and a destination path resolving to storage B — e.g. moving /mnt/cloud-a/file.txt to /mnt/cloud-b/. Detected during path resolution when src and dst storage identities differ.

Common situations: Users dragging files between two mounted storages in the web UI; scripts using a single virtual tree spanning multiple backends; renaming with a path that accidentally crosses a mount boundary (typos in the destination root).

Related errors


AI-assisted analysis of AlistGo/alist@843d9dc814 (2026-08-15). Data as JSON: /api/errors/7887142248a652e9. Report an issue: GitHub.