files-community/Files · error · IOException

Failed to move folder from {Path} to {destFolder}.

Error message

Failed to move folder from {Path} to {destFolder}.

What it means

Thrown by FtpStorageFolder.MoveAsync (Files.App/Utils) when AsyncFtpClient.MoveDirectory returns false after a successful connection. The directory rename did not complete: destination collision under skip mode, source missing, permission denied, or the server rejecting the move (e.g. cross-root renames).

Source

Thrown at src/Files.App/Utils/Storage/StorageItems/FtpStorageFolder.cs:284

			=> MoveAsync(destinationFolder, NameCollisionOption.FailIfExists);
		public override IAsyncOperation<BaseStorageFolder> MoveAsync(IStorageFolder destinationFolder, NameCollisionOption option)
		{
			return AsyncInfo.Run((cancellationToken) => SafetyExtensions.Wrap<BaseStorageFolder>(async () =>
			{
				using var ftpClient = GetFtpClient();
				if (!await ftpClient.EnsureConnectedAsync())
					throw new IOException($"Failed to connect to FTP server.");

				BaseStorageFolder destFolder = destinationFolder.AsBaseStorageFolder();

				if (destFolder is FtpStorageFolder ftpFolder)
				{
					string destName = $"{ftpFolder.FtpPath}/{Name}";
					FtpRemoteExists ftpRemoteExists = option is NameCollisionOption.ReplaceExisting ? FtpRemoteExists.Overwrite : FtpRemoteExists.Skip;

					bool isSuccessful = await ftpClient.MoveDirectory(FtpPath, destName, ftpRemoteExists, token: cancellationToken);
					if (!isSuccessful)
						throw new IOException($"Failed to move folder from {Path} to {destFolder}.");

					var folder = new FtpStorageFolder(new StorageFileWithPath(null, destName));
					((IPasswordProtectedItem)folder).CopyFrom(this);
					return folder;
				}
				else
					throw new NotSupportedException();
			}, ((IPasswordProtectedItem)this).RetryWithCredentialsAsync));
		}

		public override IAsyncAction RenameAsync(string desiredName)
			=> RenameAsync(desiredName, NameCollisionOption.FailIfExists);
		public override IAsyncAction RenameAsync(string desiredName, NameCollisionOption option)
		{
			return AsyncInfo.Run((cancellationToken) => SafetyExtensions.WrapAsync(async () =>
			{
				using var ftpClient = GetFtpClient();
				if (!await ftpClient.EnsureConnectedAsync())

View on GitHub (pinned to 68c68a58d4)

Solutions

  1. Use NameCollisionOption.ReplaceExisting if overwriting the destination is acceptable.
  2. Pre-check destination existence with DirectoryExists and resolve collisions before moving.
  3. Inspect ftpClient.LastReply for the 5xx code explaining the failure.
  4. Confirm the source FtpPath still exists immediately before the move.
  5. Catch IOException, distinguish collision vs permission, and retry with the right strategy.

Example fix

// before
await folder.MoveAsync(destFolder, NameCollisionOption.FailIfExists);

// after
var option = await DestFolderExists(destFolder, folder.Name)
    ? NameCollisionOption.ReplaceExisting
    : NameCollisionOption.FailIfExists;
await folder.MoveAsync(destFolder, option);
Defensive patterns

Strategy: try-catch

Validate before calling

// Resolve destination collisions before moving a folder.
bool exists = await ftpClient.DirectoryExists($"{ftpFolder.FtpPath}/{folder.Name}", ct);
var option = exists ? NameCollisionOption.ReplaceExisting : NameCollisionOption.FailIfExists;

Try / catch

try { await folder.MoveAsync(destFolder, option); }
catch (IOException ex) when (ex.Message.Contains("Failed to move folder"))
{ var reply = ftpClient.LastReply; /* inspect 5xx */ throw; }

Prevention

When it happens

Trigger: MoveDirectory returning false inside MoveAsync. With NameCollisionOption other than ReplaceExisting, FtpRemoteExists.Skip is used so an existing destination directory causes failure. Insufficient permission, missing source, or unsupported cross-filesystem rename also yield false.

Common situations: Moving a folder onto an existing name without ReplaceExisting; source folder deleted between list and move; lack of write permission at destination; servers that forbid renaming non-empty directories.

Related errors


AI-assisted analysis of files-community/Files@68c68a58d4 (2026-08-13). Data as JSON: /api/errors/556b30700c33c256. Report an issue: GitHub.