Skip to content

Commit 05aca27

Browse files
authored
Feature: Added file operation support for FTP (#13362)
1 parent a3be3ca commit 05aca27

File tree

9 files changed

+95
-10
lines changed

9 files changed

+95
-10
lines changed

src/Files.App/Utils/Storage/Operations/FilesystemOperations.cs

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -366,12 +366,17 @@ await DialogDisplayHelper.ShowDialogAsync(
366366
if (fsSourceFolder.Result is IPasswordProtectedItem ppis)
367367
ppis.PasswordRequestedCallback = UIFilesystemHelpers.RequestPassword;
368368

369-
// Moving folders using Storage API can result in data loss, copy instead
370-
//var fsResultMove = await FilesystemTasks.Wrap(() => MoveDirectoryAsync((BaseStorageFolder)fsSourceFolder, (BaseStorageFolder)fsDestinationFolder, fsSourceFolder.Result.Name, collision.Convert(), true));
371-
var fsResultMove = new FilesystemResult<BaseStorageFolder>(null, FileSystemStatusCode.Generic);
369+
var srcFolder = (BaseStorageFolder)fsSourceFolder;
370+
var fsResultMove = await FilesystemTasks.Wrap(() => srcFolder.MoveAsync(fsDestinationFolder.Result, collision).AsTask());
372371

373-
if (await DialogDisplayHelper.ShowDialogAsync("ErrorDialogThisActionCannotBeDone".GetLocalizedResource(), "ErrorDialogUnsupportedMoveOperation".GetLocalizedResource(), "OK", "Cancel".GetLocalizedResource()))
374-
fsResultMove = await FilesystemTasks.Wrap(() => CloneDirectoryAsync((BaseStorageFolder)fsSourceFolder, (BaseStorageFolder)fsDestinationFolder, fsSourceFolder.Result.Name, collision.Convert()));
372+
if (!fsResultMove) // Use generic move folder operation (move folder items one by one)
373+
{
374+
// Moving folders using Storage API can result in data loss, copy instead
375+
//var fsResultMove = await FilesystemTasks.Wrap(() => MoveDirectoryAsync((BaseStorageFolder)fsSourceFolder, (BaseStorageFolder)fsDestinationFolder, fsSourceFolder.Result.Name, collision.Convert(), true));
376+
377+
if (await DialogDisplayHelper.ShowDialogAsync("ErrorDialogThisActionCannotBeDone".GetLocalizedResource(), "ErrorDialogUnsupportedMoveOperation".GetLocalizedResource(), "OK", "Cancel".GetLocalizedResource()))
378+
fsResultMove = await FilesystemTasks.Wrap(() => CloneDirectoryAsync((BaseStorageFolder)fsSourceFolder, (BaseStorageFolder)fsDestinationFolder, fsSourceFolder.Result.Name, collision.Convert()));
379+
}
375380

376381
if (fsSourceFolder.Result is IPasswordProtectedItem ppiu)
377382
ppiu.PasswordRequestedCallback = null;
@@ -453,6 +458,14 @@ await DialogDisplayHelper.ShowDialogAsync(
453458
return null;
454459
}
455460

461+
bool sourceInCurrentFolder = PathNormalization.TrimPath(_associatedInstance.FilesystemViewModel.CurrentFolder.ItemPath) ==
462+
PathNormalization.GetParentDir(source.Path);
463+
if (fsProgress.Status == FileSystemStatusCode.Success && sourceInCurrentFolder)
464+
{
465+
await _associatedInstance.FilesystemViewModel.RemoveFileOrFolderAsync(source.Path);
466+
await _associatedInstance.FilesystemViewModel.ApplyFilesAndFoldersChangesAsync();
467+
}
468+
456469
var pathWithType = movedItem.FromStorageItem(destination, source.ItemType);
457470

458471
return new StorageHistory(FileOperationType.Move, source, pathWithType);
@@ -713,7 +726,6 @@ public async Task<IStorageHistory> RestoreFromTrashAsync(IStorageItemWithPath so
713726
if (fsResult)
714727
{
715728
// Moving folders using Storage API can result in data loss, copy instead
716-
717729
//fsResult = await FilesystemTasks.Wrap(() => MoveDirectoryAsync(sourceFolder.Result, destinationFolder.Result, Path.GetFileName(destination), CreationCollisionOption.FailIfExists, true));
718730

719731
if (await DialogDisplayHelper.ShowDialogAsync("ErrorDialogThisActionCannotBeDone".GetLocalizedResource(), "ErrorDialogUnsupportedMoveOperation".GetLocalizedResource(), "OK", "Cancel".GetLocalizedResource()))

src/Files.App/Utils/Storage/StorageBaseItems/BaseStorageFolder.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,10 @@ IAsyncOperation<StorageFolder> IStorageFolder.CreateFolderAsync(string desiredNa
184184
=> await (await CreateFolderAsync(desiredName, options)).ToStorageFolderAsync());
185185
}
186186

187+
public abstract IAsyncOperation<BaseStorageFolder> MoveAsync(IStorageFolder destinationFolder);
188+
189+
public abstract IAsyncOperation<BaseStorageFolder> MoveAsync(IStorageFolder destinationFolder, NameCollisionOption option);
190+
187191
public abstract IAsyncAction RenameAsync(string desiredName);
188192

189193
public abstract IAsyncAction RenameAsync(string desiredName, NameCollisionOption option);

src/Files.App/Utils/Storage/StorageBaseItems/IBaseStorageFolder.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ public interface IBaseStorageFolder : IStorageItem2, IStorageFolder, IStorageFol
4747

4848
new IAsyncOperation<BaseStorageFolder> CreateFolderAsync(string desiredName, CreationCollisionOption options);
4949

50+
IAsyncOperation<BaseStorageFolder> MoveAsync(IStorageFolder destinationFolder);
51+
IAsyncOperation<BaseStorageFolder> MoveAsync(IStorageFolder destinationFolder, NameCollisionOption option);
52+
5053
new BaseStorageItemQueryResult CreateItemQueryWithOptions(QueryOptions queryOptions);
5154

5255
new BaseStorageFileQueryResult CreateFileQueryWithOptions(QueryOptions queryOptions);

src/Files.App/Utils/Storage/StorageItems/FtpStorageFile.cs

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -183,9 +183,34 @@ public override IAsyncOperation<BaseStorageFile> CopyAsync(IStorageFolder destin
183183
}, ((IPasswordProtectedItem)this).RetryWithCredentials));
184184
}
185185

186-
public override IAsyncAction MoveAsync(IStorageFolder destinationFolder) => throw new NotSupportedException();
187-
public override IAsyncAction MoveAsync(IStorageFolder destinationFolder, string desiredNewName) => throw new NotSupportedException();
188-
public override IAsyncAction MoveAsync(IStorageFolder destinationFolder, string desiredNewName, NameCollisionOption option) => throw new NotSupportedException();
186+
public override IAsyncAction MoveAsync(IStorageFolder destinationFolder)
187+
=> MoveAsync(destinationFolder, Name, NameCollisionOption.FailIfExists);
188+
public override IAsyncAction MoveAsync(IStorageFolder destinationFolder, string desiredNewName)
189+
=> MoveAsync(destinationFolder, desiredNewName, NameCollisionOption.FailIfExists);
190+
public override IAsyncAction MoveAsync(IStorageFolder destinationFolder, string desiredNewName, NameCollisionOption option)
191+
{
192+
return AsyncInfo.Run((cancellationToken) => SafetyExtensions.Wrap(async () =>
193+
{
194+
using var ftpClient = GetFtpClient();
195+
if (!await ftpClient.EnsureConnectedAsync())
196+
throw new IOException($"Failed to connect to FTP server.");
197+
198+
BaseStorageFolder destFolder = destinationFolder.AsBaseStorageFolder();
199+
200+
if (destFolder is FtpStorageFolder ftpFolder)
201+
{
202+
string destName = $"{ftpFolder.FtpPath}/{Name}";
203+
FtpRemoteExists ftpRemoteExists = option is NameCollisionOption.ReplaceExisting ? FtpRemoteExists.Overwrite : FtpRemoteExists.Skip;
204+
205+
bool isSuccessful = await ftpClient.MoveFile(FtpPath, destName, ftpRemoteExists, cancellationToken);
206+
if (!isSuccessful)
207+
throw new IOException($"Failed to move file from {Path} to {destFolder}.");
208+
}
209+
else
210+
throw new NotSupportedException();
211+
}, ((IPasswordProtectedItem)this).RetryWithCredentials));
212+
}
213+
189214

190215
public override IAsyncAction CopyAndReplaceAsync(IStorageFile fileToReplace) => throw new NotSupportedException();
191216
public override IAsyncAction MoveAndReplaceAsync(IStorageFile fileToReplace) => throw new NotSupportedException();
@@ -236,7 +261,7 @@ private AsyncFtpClient GetFtpClient()
236261
{
237262
string host = FtpHelpers.GetFtpHost(Path);
238263
ushort port = FtpHelpers.GetFtpPort(Path);
239-
var credentials = Credentials is not null ?
264+
var credentials = Credentials is not null ?
240265
new NetworkCredential(Credentials.UserName, Credentials.SecurePassword) :
241266
FtpManager.Credentials.Get(host, FtpManager.Anonymous);
242267

src/Files.App/Utils/Storage/StorageItems/FtpStorageFolder.cs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,36 @@ public override IAsyncOperation<BaseStorageFolder> CreateFolderAsync(string desi
264264
}, ((IPasswordProtectedItem)this).RetryWithCredentials));
265265
}
266266

267+
public override IAsyncOperation<BaseStorageFolder> MoveAsync(IStorageFolder destinationFolder)
268+
=> MoveAsync(destinationFolder, NameCollisionOption.FailIfExists);
269+
public override IAsyncOperation<BaseStorageFolder> MoveAsync(IStorageFolder destinationFolder, NameCollisionOption option)
270+
{
271+
return AsyncInfo.Run((cancellationToken) => SafetyExtensions.Wrap<BaseStorageFolder>(async () =>
272+
{
273+
using var ftpClient = GetFtpClient();
274+
if (!await ftpClient.EnsureConnectedAsync())
275+
throw new IOException($"Failed to connect to FTP server.");
276+
277+
BaseStorageFolder destFolder = destinationFolder.AsBaseStorageFolder();
278+
279+
if (destFolder is FtpStorageFolder ftpFolder)
280+
{
281+
string destName = $"{ftpFolder.FtpPath}/{Name}";
282+
FtpRemoteExists ftpRemoteExists = option is NameCollisionOption.ReplaceExisting ? FtpRemoteExists.Overwrite : FtpRemoteExists.Skip;
283+
284+
bool isSuccessful = await ftpClient.MoveDirectory(FtpPath, destName, ftpRemoteExists, token: cancellationToken);
285+
if (!isSuccessful)
286+
throw new IOException($"Failed to move folder from {Path} to {destFolder}.");
287+
288+
var folder = new FtpStorageFolder(new StorageFileWithPath(null, destName));
289+
((IPasswordProtectedItem)folder).CopyFrom(this);
290+
return folder;
291+
}
292+
else
293+
throw new NotSupportedException();
294+
}, ((IPasswordProtectedItem)this).RetryWithCredentials));
295+
}
296+
267297
public override IAsyncAction RenameAsync(string desiredName)
268298
=> RenameAsync(desiredName, NameCollisionOption.FailIfExists);
269299
public override IAsyncAction RenameAsync(string desiredName, NameCollisionOption option)

src/Files.App/Utils/Storage/StorageItems/ShellStorageFolder.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,9 @@ public override IAsyncOperation<BaseStorageFile> CreateFileAsync(string desiredN
231231
public override IAsyncOperation<BaseStorageFolder> CreateFolderAsync(string desiredName, CreationCollisionOption options)
232232
=> throw new NotSupportedException();
233233

234+
public override IAsyncOperation<BaseStorageFolder> MoveAsync(IStorageFolder destinationFolder) => throw new NotSupportedException();
235+
public override IAsyncOperation<BaseStorageFolder> MoveAsync(IStorageFolder destinationFolder, NameCollisionOption option) => throw new NotSupportedException();
236+
234237
public override IAsyncAction RenameAsync(string desiredName) => throw new NotSupportedException();
235238
public override IAsyncAction RenameAsync(string desiredName, NameCollisionOption option) => throw new NotSupportedException();
236239

src/Files.App/Utils/Storage/StorageItems/SystemStorageFolder.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,9 @@ public override IAsyncOperation<BaseStorageFolder> CreateFolderAsync(string desi
8686
public override IAsyncOperation<BaseStorageFolder> CreateFolderAsync(string desiredName, CreationCollisionOption options)
8787
=> AsyncInfo.Run<BaseStorageFolder>(async (cancellationToken) => new SystemStorageFolder(await Folder.CreateFolderAsync(desiredName, options)));
8888

89+
public override IAsyncOperation<BaseStorageFolder> MoveAsync(IStorageFolder destinationFolder) => throw new NotSupportedException();
90+
public override IAsyncOperation<BaseStorageFolder> MoveAsync(IStorageFolder destinationFolder, NameCollisionOption option) => throw new NotSupportedException();
91+
8992
public override IAsyncAction RenameAsync(string desiredName) => Folder.RenameAsync(desiredName);
9093
public override IAsyncAction RenameAsync(string desiredName, NameCollisionOption option) => Folder.RenameAsync(desiredName, option);
9194

src/Files.App/Utils/Storage/StorageItems/VirtualStorageFolder.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ public override IAsyncOperation<BaseStorageFolder> CreateFolderAsync(string desi
8787
=> CreateFolderAsync(desiredName, CreationCollisionOption.FailIfExists);
8888
public override IAsyncOperation<BaseStorageFolder> CreateFolderAsync(string desiredName, CreationCollisionOption options)
8989
=> throw new NotSupportedException();
90+
public override IAsyncOperation<BaseStorageFolder> MoveAsync(IStorageFolder destinationFolder) => throw new NotSupportedException();
91+
public override IAsyncOperation<BaseStorageFolder> MoveAsync(IStorageFolder destinationFolder, NameCollisionOption option) => throw new NotSupportedException();
9092

9193
public override IAsyncAction RenameAsync(string desiredName)
9294
=> RenameAsync(desiredName, NameCollisionOption.FailIfExists);

src/Files.App/Utils/Storage/StorageItems/ZipStorageFolder.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,9 @@ public override IAsyncOperation<BaseStorageFolder> CreateFolderAsync(string desi
333333
}, ((IPasswordProtectedItem)this).RetryWithCredentials));
334334
}
335335

336+
public override IAsyncOperation<BaseStorageFolder> MoveAsync(IStorageFolder destinationFolder) => throw new NotSupportedException();
337+
public override IAsyncOperation<BaseStorageFolder> MoveAsync(IStorageFolder destinationFolder, NameCollisionOption option) => throw new NotSupportedException();
338+
336339
public override IAsyncAction RenameAsync(string desiredName) => RenameAsync(desiredName, NameCollisionOption.FailIfExists);
337340
public override IAsyncAction RenameAsync(string desiredName, NameCollisionOption option)
338341
{

0 commit comments

Comments
 (0)