diff --git a/src/Files.App/Data/Models/ItemViewModel.cs b/src/Files.App/Data/Models/ItemViewModel.cs index cc7d4951a1f1..6b13b19196c9 100644 --- a/src/Files.App/Data/Models/ItemViewModel.cs +++ b/src/Files.App/Data/Models/ItemViewModel.cs @@ -7,23 +7,19 @@ using Files.App.Utils.StorageItems; using Files.App.Helpers.StorageCache; using Files.App.Utils.Shell; -using Files.App.Storage.FtpStorage; using Files.App.ViewModels.Previews; using Files.Core.Services.SizeProvider; using Files.Shared.Cloud; using Files.Shared.EventArguments; using Files.Shared.Services; -using FluentFTP; using Microsoft.Extensions.Logging; using Microsoft.UI.Xaml.Data; using Microsoft.UI.Xaml.Media; using Microsoft.UI.Xaml.Media.Imaging; using System.Collections.Concurrent; using System.IO; -using System.Net; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -using System.Text; using System.Text.Json; using Vanara.Windows.Shell; using Windows.Foundation; @@ -144,20 +140,14 @@ public async Task SetWorkingDirectoryAsync(string? value) WorkingDirectory = value; - string? pathRoot; - if (FtpHelpers.IsFtpPath(WorkingDirectory)) - { - var rootIndex = FtpHelpers.GetRootIndex(WorkingDirectory); - pathRoot = rootIndex is -1 - ? WorkingDirectory - : WorkingDirectory.Substring(0, rootIndex); - } - else + string? pathRoot = null; + if (!FtpHelpers.IsFtpPath(WorkingDirectory)) { pathRoot = Path.GetPathRoot(WorkingDirectory); } GitDirectory = pathRoot is null ? null : GitHelpers.GetGitRepositoryPath(WorkingDirectory, pathRoot); + OnPropertyChanged(nameof(WorkingDirectory)); } @@ -1380,55 +1370,46 @@ private async Task RapidAddItemsToCollection(string? path, LibraryItem? library var stopwatch = new Stopwatch(); stopwatch.Start(); - if (FtpHelpers.IsFtpPath(path)) - { - // Recycle bin and network are enumerated by the fulltrust process - PageTypeUpdated?.Invoke(this, new PageTypeUpdatedEventArgs() { IsTypeCloudDrive = false }); - await EnumerateItemsFromSpecialFolderAsync(path); - } - else + var isRecycleBin = path.StartsWith(Constants.UserEnvironmentPaths.RecycleBinPath, StringComparison.Ordinal); + var enumerated = await EnumerateItemsFromStandardFolderAsync(path, addFilesCTS.Token, library); + + // Hide progressbar after enumeration + IsLoadingItems = false; + + switch (enumerated) { - var isRecycleBin = path.StartsWith(Constants.UserEnvironmentPaths.RecycleBinPath, StringComparison.Ordinal); - var enumerated = await EnumerateItemsFromStandardFolderAsync(path, addFilesCTS.Token, library); + // Enumerated with FindFirstFileExFromApp + // Is folder synced to cloud storage? + case 0: + currentStorageFolder ??= await FilesystemTasks.Wrap(() => StorageFileExtensions.DangerousGetFolderWithPathFromPathAsync(path)); + var syncStatus = await CheckCloudDriveSyncStatusAsync(currentStorageFolder?.Item); + PageTypeUpdated?.Invoke(this, new PageTypeUpdatedEventArgs() + { + IsTypeCloudDrive = syncStatus != CloudDriveSyncStatus.NotSynced && syncStatus != CloudDriveSyncStatus.Unknown, + IsTypeGitRepository = GitDirectory is not null + }); + WatchForDirectoryChanges(path, syncStatus); + if (GitDirectory is not null) + WatchForGitChanges(); + break; - // Hide progressbar after enumeration - IsLoadingItems = false; + // Enumerated with StorageFolder + case 1: + PageTypeUpdated?.Invoke(this, new PageTypeUpdatedEventArgs() { IsTypeCloudDrive = false, IsTypeRecycleBin = isRecycleBin }); + currentStorageFolder ??= await FilesystemTasks.Wrap(() => StorageFileExtensions.DangerousGetFolderWithPathFromPathAsync(path)); + WatchForStorageFolderChanges(currentStorageFolder?.Item); + break; - switch (enumerated) - { - // Enumerated with FindFirstFileExFromApp - // Is folder synced to cloud storage? - case 0: - currentStorageFolder ??= await FilesystemTasks.Wrap(() => StorageFileExtensions.DangerousGetFolderWithPathFromPathAsync(path)); - var syncStatus = await CheckCloudDriveSyncStatusAsync(currentStorageFolder?.Item); - PageTypeUpdated?.Invoke(this, new PageTypeUpdatedEventArgs() - { - IsTypeCloudDrive = syncStatus != CloudDriveSyncStatus.NotSynced && syncStatus != CloudDriveSyncStatus.Unknown, - IsTypeGitRepository = GitDirectory is not null - }); - WatchForDirectoryChanges(path, syncStatus); - if (GitDirectory is not null) - WatchForGitChanges(); - break; - - // Enumerated with StorageFolder - case 1: - PageTypeUpdated?.Invoke(this, new PageTypeUpdatedEventArgs() { IsTypeCloudDrive = false, IsTypeRecycleBin = isRecycleBin }); - currentStorageFolder ??= await FilesystemTasks.Wrap(() => StorageFileExtensions.DangerousGetFolderWithPathFromPathAsync(path)); - WatchForStorageFolderChanges(currentStorageFolder?.Item); - break; - - // Watch for changes using FTP in Box Drive folder (#7428) and network drives (#5869) - case 2: - PageTypeUpdated?.Invoke(this, new PageTypeUpdatedEventArgs() { IsTypeCloudDrive = false }); - WatchForWin32FolderChanges(path); - break; - - // Enumeration failed - case -1: - default: - break; - } + // Watch for changes using Win32 in Box Drive folder (#7428) and network drives (#5869) + case 2: + PageTypeUpdated?.Invoke(this, new PageTypeUpdatedEventArgs() { IsTypeCloudDrive = false }); + WatchForWin32FolderChanges(path); + break; + + // Enumeration failed + case -1: + default: + break; } await GetDefaultItemIcons(folderSettings.GetIconSize()); @@ -1455,99 +1436,6 @@ public void CloseWatcher() watcherCTS = new CancellationTokenSource(); } - public async Task EnumerateItemsFromSpecialFolderAsync(string path) - { - var isFtp = FtpHelpers.IsFtpPath(path); - - CurrentFolder = new ListedItem(null!) - { - PrimaryItemAttribute = StorageItemTypes.Folder, - ItemPropertiesInitialized = true, - ItemNameRaw = - path.StartsWith(Constants.UserEnvironmentPaths.RecycleBinPath, StringComparison.OrdinalIgnoreCase) ? "RecycleBin".GetLocalizedResource() : - path.StartsWith(Constants.UserEnvironmentPaths.NetworkFolderPath, StringComparison.OrdinalIgnoreCase) ? "Network".GetLocalizedResource() : - path.StartsWith(Constants.UserEnvironmentPaths.MyComputerPath, StringComparison.OrdinalIgnoreCase) ? "ThisPC".GetLocalizedResource() : - isFtp ? "FTP" : "Unknown", - ItemDateModifiedReal = DateTimeOffset.Now, // Fake for now - ItemDateCreatedReal = DateTimeOffset.Now, // Fake for now - ItemType = "Folder".GetLocalizedResource(), - FileImage = null, - LoadFileIcon = false, - ItemPath = path, - FileSize = null, - FileSizeBytes = 0 - }; - - if (!isFtp || !FtpHelpers.VerifyFtpPath(path)) - return; - - // TODO: Show invalid path dialog - - using var client = new AsyncFtpClient(); - client.Host = FtpHelpers.GetFtpHost(path); - client.Port = FtpHelpers.GetFtpPort(path); - client.Credentials = FtpManager.Credentials.Get(client.Host, FtpManager.Anonymous); - - static async Task WrappedAutoConnectFtpAsync(AsyncFtpClient client) - { - try - { - return await client.AutoConnect(); - } - catch (FtpAuthenticationException) - { - return null; - } - - throw new InvalidOperationException(); - } - - await Task.Run(async () => - { - try - { - if (!client.IsConnected && await WrappedAutoConnectFtpAsync(client) is null) - { - await dispatcherQueue.EnqueueOrInvokeAsync(async () => - { - var credentialDialogViewModel = new CredentialDialogViewModel(); - - if (await dialogService.ShowDialogAsync(credentialDialogViewModel) != DialogResult.Primary) - return; - - // Can't do more than that to mitigate immutability of strings. Perhaps convert DisposableArray to SecureString immediately? - if (!credentialDialogViewModel.IsAnonymous) - client.Credentials = new NetworkCredential(credentialDialogViewModel.UserName, Encoding.UTF8.GetString(credentialDialogViewModel.Password)); - }); - } - - if (!client.IsConnected && await WrappedAutoConnectFtpAsync(client) is null) - throw new InvalidOperationException(); - - FtpManager.Credentials[client.Host] = client.Credentials; - - var sampler = new IntervalSampler(500); - var list = await client.GetListing(FtpHelpers.GetFtpPath(path)); - - for (var i = 0; i < list.Length; i++) - { - filesAndFolders.Add(new FtpItem(list[i], path)); - - if (i == list.Length - 1 || sampler.CheckNow()) - { - await OrderFilesAndFoldersAsync(); - await ApplyFilesAndFoldersChangesAsync(); - } - } - } - catch - { - // Network issue - FtpManager.Credentials.Remove(client.Host); - } - }); - } - public async Task EnumerateItemsFromStandardFolderAsync(string path, CancellationToken cancellationToken, LibraryItem? library = null) { // Flag to use FindFirstFileExFromApp or StorageFolder enumeration - Use storage folder for Box Drive (#4629) @@ -1557,7 +1445,8 @@ public async Task EnumerateItemsFromStandardFolderAsync(string path, Cancel !path.StartsWith(@"\\?\", StringComparison.Ordinal) && !path.StartsWith(@"\\SHELL\", StringComparison.Ordinal) && !isWslDistro; - bool enumFromStorageFolder = isBoxFolder; + bool isFtp = FtpHelpers.IsFtpPath(path); + bool enumFromStorageFolder = isBoxFolder || isFtp; BaseStorageFolder? rootFolder = null; @@ -1585,7 +1474,7 @@ public async Task EnumerateItemsFromStandardFolderAsync(string path, Cancel } else { - var res = await FilesystemTasks.Wrap(() => StorageFileExtensions.DangerousGetFolderWithPathFromPathAsync(path)); + var res = await FilesystemTasks.Wrap(() => StorageFileExtensions.DangerousGetFolderWithPathFromPathAsync(path, workingRoot, currentStorageFolder)); if (res) { currentStorageFolder = res.Result; @@ -1756,12 +1645,15 @@ await Task.Run(async () => } } - private Task EnumFromStorageFolderAsync(string path, BaseStorageFolder? rootFolder, StorageFolderWithPath currentStorageFolder, CancellationToken cancellationToken) + private async Task EnumFromStorageFolderAsync(string path, BaseStorageFolder? rootFolder, StorageFolderWithPath currentStorageFolder, CancellationToken cancellationToken) { if (rootFolder is null) - return Task.CompletedTask; + return; - return Task.Run(async () => + if (rootFolder is IPasswordProtectedItem ppis) + ppis.PasswordRequestedCallback = UIFilesystemHelpers.RequestPassword; + + await Task.Run(async () => { List finalList = await UniversalStorageEnumerator.ListEntries( rootFolder, @@ -1782,6 +1674,9 @@ private Task EnumFromStorageFolderAsync(string path, BaseStorageFolder? rootFold await OrderFilesAndFoldersAsync(); await ApplyFilesAndFoldersChangesAsync(); }, cancellationToken); + + if (rootFolder is IPasswordProtectedItem ppiu) + ppiu.PasswordRequestedCallback = null; } private void CheckForSolutionFile() diff --git a/src/Files.App/Dialogs/CredentialDialog.xaml b/src/Files.App/Dialogs/CredentialDialog.xaml index 542cd7e98448..b9530e52862a 100644 --- a/src/Files.App/Dialogs/CredentialDialog.xaml +++ b/src/Files.App/Dialogs/CredentialDialog.xaml @@ -36,6 +36,7 @@ @@ -47,6 +48,7 @@ diff --git a/src/Files.App/Helpers/Storage/StorageHelpers.cs b/src/Files.App/Helpers/Storage/StorageHelpers.cs index 8f4bb369e70a..c1f8ca74696b 100644 --- a/src/Files.App/Helpers/Storage/StorageHelpers.cs +++ b/src/Files.App/Helpers/Storage/StorageHelpers.cs @@ -144,9 +144,9 @@ await FilesystemTasks.Wrap(() => StorageFileExtensions.DangerousGetFileFromPathA await FilesystemTasks.Wrap(() => StorageFileExtensions.DangerousGetFolderFromPathAsync(item.Path, rootItem))); } if (returnedItem.Result is null && item.Item is not null) - { returnedItem = new FilesystemResult(item.Item, FileSystemStatusCode.Success); - } + if (returnedItem.Result is IPasswordProtectedItem ppid && item.Item is IPasswordProtectedItem ppis) + ppid.Credentials = ppis.Credentials; return returnedItem; } diff --git a/src/Files.App/Helpers/UI/UIFilesystemHelpers.cs b/src/Files.App/Helpers/UI/UIFilesystemHelpers.cs index ac9ebee3d3b1..1184f14b1574 100644 --- a/src/Files.App/Helpers/UI/UIFilesystemHelpers.cs +++ b/src/Files.App/Helpers/UI/UIFilesystemHelpers.cs @@ -3,10 +3,13 @@ using Files.App.Dialogs; using Files.App.Utils.StorageItems; +using Files.App.Storage.FtpStorage; using Files.App.ViewModels.Dialogs; using Microsoft.Extensions.Logging; using System.Collections.Concurrent; using System.IO; +using System.Net; +using System.Text; using Windows.ApplicationModel.DataTransfer; using Windows.Storage; using Windows.System; @@ -414,5 +417,31 @@ public static void UpdateShortcutItemProperties(ShortcutItem item, string target item.WorkingDirectory = workingDir; item.RunAsAdmin = runAsAdmin; } + + public async static Task RequestPassword(IPasswordProtectedItem sender) + { + var path = ((IStorageItem)sender).Path; + var isFtp = FtpHelpers.IsFtpPath(path); + + var credentialDialogViewModel = new CredentialDialogViewModel() { CanBeAnonymous = isFtp, PasswordOnly = !isFtp }; + IDialogService dialogService = Ioc.Default.GetRequiredService(); + var dialogResult = await App.Window.DispatcherQueue.EnqueueOrInvokeAsync(() => + dialogService.ShowDialogAsync(credentialDialogViewModel)); + + if (dialogResult != DialogResult.Primary || credentialDialogViewModel.IsAnonymous) + return new(); + + // Can't do more than that to mitigate immutability of strings. Perhaps convert DisposableArray to SecureString immediately? + var credentials = new StorageCredential(credentialDialogViewModel.UserName, Encoding.UTF8.GetString(credentialDialogViewModel.Password)); + credentialDialogViewModel.Password?.Dispose(); + + if (isFtp) + { + var host = FtpHelpers.GetFtpHost(path); + FtpManager.Credentials[host] = new NetworkCredential(credentials.UserName, credentials.SecurePassword); + } + + return credentials; + } } } \ No newline at end of file diff --git a/src/Files.App/Utils/BaseStorage/IPasswordProtectedItem.cs b/src/Files.App/Utils/BaseStorage/IPasswordProtectedItem.cs new file mode 100644 index 000000000000..a2a1df16ad5e --- /dev/null +++ b/src/Files.App/Utils/BaseStorage/IPasswordProtectedItem.cs @@ -0,0 +1,44 @@ +using FluentFTP; +using SevenZip; +using System; +using Windows.Storage; + +namespace Files.App.Utils.StorageItems +{ + public interface IPasswordProtectedItem + { + StorageCredential Credentials { get; set; } + + Func> PasswordRequestedCallback { get; set; } + + async Task RetryWithCredentials(Func> func, Exception exception) + { + var handled = exception is SevenZipOpenFailedException szofex && szofex.Result is OperationResult.WrongPassword || + exception is ExtractionFailedException efex && efex.Result is OperationResult.WrongPassword || + exception is FtpAuthenticationException; + if (!handled || PasswordRequestedCallback is null) + throw exception; + + Credentials = await PasswordRequestedCallback(this); + return await func(); + } + + async Task RetryWithCredentials(Func func, Exception exception) + { + var handled = exception is SevenZipOpenFailedException szofex && szofex.Result is OperationResult.WrongPassword || + exception is ExtractionFailedException efex && efex.Result is OperationResult.WrongPassword || + exception is FtpAuthenticationException; + if (!handled || PasswordRequestedCallback is null) + throw exception; + + Credentials = await PasswordRequestedCallback(this); + await func(); + } + + void CopyFrom(IPasswordProtectedItem parent) + { + Credentials = parent.Credentials; + PasswordRequestedCallback = parent.PasswordRequestedCallback; + } + } +} diff --git a/src/Files.App/Utils/BaseStorage/StorageCredential.cs b/src/Files.App/Utils/BaseStorage/StorageCredential.cs new file mode 100644 index 000000000000..15a3897ca42f --- /dev/null +++ b/src/Files.App/Utils/BaseStorage/StorageCredential.cs @@ -0,0 +1,111 @@ +using System.Runtime.InteropServices; +using System.Security; + +namespace Files.App.Utils.StorageItems +{ + // Code from System.Net.NetworkCredential + public class StorageCredential + { + private string _userName = string.Empty; + private object? _password; + + public string UserName + { + get { return _userName; } + set { _userName = value ?? string.Empty; } + } + + public string Password + { + get + { + SecureString? sstr = _password as SecureString; + if (sstr != null) + { + return MarshalToString(sstr); + } + return (string?)_password ?? string.Empty; + } + set + { + SecureString? old = _password as SecureString; + _password = value; + old?.Dispose(); + } + } + + public SecureString SecurePassword + { + get + { + string? str = _password as string; + if (str != null) + { + return MarshalToSecureString(str); + } + SecureString? sstr = _password as SecureString; + return sstr != null ? sstr.Copy() : new SecureString(); + } + set + { + SecureString? old = _password as SecureString; + _password = value?.Copy(); + old?.Dispose(); + } + } + + public StorageCredential() + : this(string.Empty, string.Empty) + { + } + + public StorageCredential(string? userName, string? password) + { + UserName = userName; + Password = password; + } + + public StorageCredential(string? userName, SecureString? password) + { + UserName = userName; + SecurePassword = password; + } + + private static string MarshalToString(SecureString sstr) + { + if (sstr == null || sstr.Length == 0) + { + return string.Empty; + } + + IntPtr ptr = IntPtr.Zero; + string result = string.Empty; + try + { + ptr = Marshal.SecureStringToGlobalAllocUnicode(sstr); + result = Marshal.PtrToStringUni(ptr)!; + } + finally + { + if (ptr != IntPtr.Zero) + { + Marshal.ZeroFreeGlobalAllocUnicode(ptr); + } + } + return result; + } + + private unsafe SecureString MarshalToSecureString(string str) + { + if (string.IsNullOrEmpty(str)) + { + return new SecureString(); + } + + fixed (char* ptr = str) + { + return new SecureString(ptr, str.Length); + } + } + } +} diff --git a/src/Files.App/Utils/FilesystemOperations/FilesystemOperations.cs b/src/Files.App/Utils/FilesystemOperations/FilesystemOperations.cs index e81e2d6c19cf..08390dad2720 100644 --- a/src/Files.App/Utils/FilesystemOperations/FilesystemOperations.cs +++ b/src/Files.App/Utils/FilesystemOperations/FilesystemOperations.cs @@ -4,8 +4,11 @@ using Files.App.Utils.FilesystemHistory; using Files.App.Utils.StorageItems; using Files.Core.Helpers; +using Files.Core.Services; +using Files.Core.ViewModels.Dialogs; using Microsoft.UI.Xaml.Controls; using System.IO; +using System.Text; using Windows.Storage; namespace Files.App.Utils @@ -162,8 +165,14 @@ await DialogDisplayHelper.ShowDialogAsync( if (fsResult) { + if (fsSourceFolder.Result is IPasswordProtectedItem ppis) + ppis.PasswordRequestedCallback = UIFilesystemHelpers.RequestPassword; + var fsCopyResult = await FilesystemTasks.Wrap(() => CloneDirectoryAsync((BaseStorageFolder)fsSourceFolder, (BaseStorageFolder)fsDestinationFolder, fsSourceFolder.Result.Name, collision.Convert())); + if (fsSourceFolder.Result is IPasswordProtectedItem ppiu) + ppiu.PasswordRequestedCallback = null; + if (fsCopyResult == FileSystemStatusCode.AlreadyExists) { fsProgress.ReportStatus(FileSystemStatusCode.AlreadyExists); @@ -210,6 +219,9 @@ await DialogDisplayHelper.ShowDialogAsync( if (fsResult) { + if (sourceResult.Result is IPasswordProtectedItem ppis) + ppis.PasswordRequestedCallback = UIFilesystemHelpers.RequestPassword; + var file = (BaseStorageFile)sourceResult; var fsResultCopy = new FilesystemResult(null, FileSystemStatusCode.Generic); if (string.IsNullOrEmpty(file.Path) && collision == NameCollisionOption.GenerateUniqueName) @@ -233,6 +245,9 @@ await DialogDisplayHelper.ShowDialogAsync( fsResultCopy = await FilesystemTasks.Wrap(() => file.CopyAsync(destinationResult.Result, Path.GetFileName(file.Name), collision).AsTask()); } + if (sourceResult.Result is IPasswordProtectedItem ppiu) + ppiu.PasswordRequestedCallback = null; + if (fsResultCopy == FileSystemStatusCode.AlreadyExists) { fsProgress.ReportStatus(FileSystemStatusCode.AlreadyExists); @@ -352,6 +367,9 @@ await DialogDisplayHelper.ShowDialogAsync( if (fsResult) { + if (fsSourceFolder.Result is IPasswordProtectedItem ppis) + ppis.PasswordRequestedCallback = UIFilesystemHelpers.RequestPassword; + // Moving folders using Storage API can result in data loss, copy instead //var fsResultMove = await FilesystemTasks.Wrap(() => MoveDirectoryAsync((BaseStorageFolder)fsSourceFolder, (BaseStorageFolder)fsDestinationFolder, fsSourceFolder.Result.Name, collision.Convert(), true)); var fsResultMove = new FilesystemResult(null, FileSystemStatusCode.Generic); @@ -359,6 +377,9 @@ await DialogDisplayHelper.ShowDialogAsync( if (await DialogDisplayHelper.ShowDialogAsync("ErrorDialogThisActionCannotBeDone".GetLocalizedResource(), "ErrorDialogUnsupportedMoveOperation".GetLocalizedResource(), "OK", "Cancel".GetLocalizedResource())) fsResultMove = await FilesystemTasks.Wrap(() => CloneDirectoryAsync((BaseStorageFolder)fsSourceFolder, (BaseStorageFolder)fsDestinationFolder, fsSourceFolder.Result.Name, collision.Convert())); + if (fsSourceFolder.Result is IPasswordProtectedItem ppiu) + ppiu.PasswordRequestedCallback = null; + if (fsResultMove == FileSystemStatusCode.AlreadyExists) { fsProgress.ReportStatus(FileSystemStatusCode.AlreadyExists); @@ -401,9 +422,15 @@ await DialogDisplayHelper.ShowDialogAsync( if (fsResult) { + if (sourceResult.Result is IPasswordProtectedItem ppis) + ppis.PasswordRequestedCallback = UIFilesystemHelpers.RequestPassword; + var file = (BaseStorageFile)sourceResult; var fsResultMove = await FilesystemTasks.Wrap(() => file.MoveAsync(destinationResult.Result, Path.GetFileName(file.Name), collision).AsTask()); + if (sourceResult.Result is IPasswordProtectedItem ppiu) + ppiu.PasswordRequestedCallback = null; + if (fsResultMove == FileSystemStatusCode.AlreadyExists) { fsProgress.ReportStatus(FileSystemStatusCode.AlreadyExists); diff --git a/src/Files.App/Utils/FtpHelpers.cs b/src/Files.App/Utils/FtpHelpers.cs index 8a78f9172f26..e6f773c672dc 100644 --- a/src/Files.App/Utils/FtpHelpers.cs +++ b/src/Files.App/Utils/FtpHelpers.cs @@ -13,14 +13,7 @@ public static async Task EnsureConnectedAsync(this AsyncFtpClient ftpClien { if (!ftpClient.IsConnected) { - try - { - await ftpClient.Connect(); - } - catch - { - return false; - } + await ftpClient.Connect(); } return true; diff --git a/src/Files.App/Utils/StorageEnumerators/UniversalStorageEnumerator.cs b/src/Files.App/Utils/StorageEnumerators/UniversalStorageEnumerator.cs index 7a07d6edd08e..931fb821382c 100644 --- a/src/Files.App/Utils/StorageEnumerators/UniversalStorageEnumerator.cs +++ b/src/Files.App/Utils/StorageEnumerators/UniversalStorageEnumerator.cs @@ -7,6 +7,7 @@ using Files.App.Helpers; using Files.Core.Helpers; using Files.Core.Services.Settings; +using Microsoft.Extensions.Logging; using Microsoft.UI.Xaml.Media.Imaging; using System; using System.Collections.Generic; @@ -71,6 +72,11 @@ ex is UnauthorizedAccessException // If some unexpected exception is thrown - enumerate this folder file by file - just to be sure items = await EnumerateFileByFile(rootFolder, count, maxItemsToRetrieve); } + catch (Exception ex) + { + App.Logger.LogWarning(ex, "Error enumerating directory contents."); + break; + } foreach (var item in items) { var startWithDot = item.Name.StartsWith('.'); @@ -156,6 +162,11 @@ ex is UnauthorizedAccessException { continue; } + catch (Exception ex) + { + App.Logger.LogWarning(ex, "Error enumerating directory contents."); + break; + } tempList.Add(item); } return tempList; diff --git a/src/Files.App/Utils/StorageFileHelpers/StorageFileExtensions.cs b/src/Files.App/Utils/StorageFileHelpers/StorageFileExtensions.cs index d4eb79966f58..20d5efc977c2 100644 --- a/src/Files.App/Utils/StorageFileHelpers/StorageFileExtensions.cs +++ b/src/Files.App/Utils/StorageFileHelpers/StorageFileExtensions.cs @@ -190,13 +190,15 @@ public async static Task DangerousGetFileWithPathFromPathAs } } - if (parentFolder is not null && !Path.IsPathRooted(value) && !ShellStorageFolder.IsShellPath(value)) // "::{" not a valid root - { - // Relative path - var fullPath = Path.GetFullPath(Path.Combine(parentFolder.Path, value)); - return new StorageFileWithPath(await BaseStorageFile.GetFileFromPathAsync(fullPath)); - } - return new StorageFileWithPath(await BaseStorageFile.GetFileFromPathAsync(value)); + var fullPath = (parentFolder is not null && !FtpHelpers.IsFtpPath(value) && !Path.IsPathRooted(value) && !ShellStorageFolder.IsShellPath(value)) // "::{" not a valid root + ? Path.GetFullPath(Path.Combine(parentFolder.Path, value)) // Relative path + : value; + var item = await BaseStorageFile.GetFileFromPathAsync(fullPath); + + if (parentFolder is not null && parentFolder.Item is IPasswordProtectedItem ppis && item is IPasswordProtectedItem ppid) + ppid.Credentials = ppis.Credentials; + + return new StorageFileWithPath(item); } public async static Task> GetFilesWithPathAsync (this StorageFolderWithPath parentFolder, uint maxNumberOfItems = uint.MaxValue) @@ -242,16 +244,15 @@ public async static Task DangerousGetFolderWithPathFromPa } } - if (parentFolder is not null && !Path.IsPathRooted(value) && !ShellStorageFolder.IsShellPath(value)) // "::{" not a valid root - { - // Relative path - var fullPath = Path.GetFullPath(Path.Combine(parentFolder.Path, value)); - return new StorageFolderWithPath(await BaseStorageFolder.GetFolderFromPathAsync(fullPath)); - } - else - { - return new StorageFolderWithPath(await BaseStorageFolder.GetFolderFromPathAsync(value)); - } + var fullPath = (parentFolder is not null && !FtpHelpers.IsFtpPath(value) && !Path.IsPathRooted(value) && !ShellStorageFolder.IsShellPath(value)) // "::{" not a valid root + ? Path.GetFullPath(Path.Combine(parentFolder.Path, value)) // Relative path + : value; + var item = await BaseStorageFolder.GetFolderFromPathAsync(fullPath); + + if (parentFolder is not null && parentFolder.Item is IPasswordProtectedItem ppis && item is IPasswordProtectedItem ppid) + ppid.Credentials = ppis.Credentials; + + return new StorageFolderWithPath(item); } public async static Task> GetFoldersWithPathAsync (this StorageFolderWithPath parentFolder, uint maxNumberOfItems = uint.MaxValue) diff --git a/src/Files.App/Utils/StorageItems/FtpStorageFile.cs b/src/Files.App/Utils/StorageItems/FtpStorageFile.cs index 4fc041e8f841..dfab157b4fec 100644 --- a/src/Files.App/Utils/StorageItems/FtpStorageFile.cs +++ b/src/Files.App/Utils/StorageItems/FtpStorageFile.cs @@ -4,6 +4,7 @@ using Files.App.Storage.FtpStorage; using FluentFTP; using System.IO; +using System.Net; using System.Runtime.InteropServices.WindowsRuntime; using Windows.Foundation; using Windows.Storage; @@ -13,7 +14,7 @@ namespace Files.App.Utils.StorageItems { - public sealed class FtpStorageFile : BaseStorageFile + public sealed class FtpStorageFile : BaseStorageFile, IPasswordProtectedItem { public override string Path { get; } public override string Name { get; } @@ -40,6 +41,10 @@ public override string DisplayType public override Windows.Storage.FileAttributes Attributes { get; } = Windows.Storage.FileAttributes.Normal; public override IStorageItemExtraProperties Properties => new BaseBasicStorageItemExtraProperties(this); + public StorageCredential Credentials { get; set; } + + public Func> PasswordRequestedCallback { get; set; } + public FtpStorageFile(string path, string name, DateTimeOffset dateCreated) { Path = path; @@ -76,7 +81,7 @@ public override IAsyncOperation ToStorageFileAsync() public override IAsyncOperation GetBasicPropertiesAsync() { - return AsyncInfo.Run(async (cancellationToken) => + return AsyncInfo.Run((cancellationToken) => SafetyExtensions.Wrap(async () => { using var ftpClient = GetFtpClient(); if (!await ftpClient.EnsureConnectedAsync()) @@ -86,12 +91,12 @@ public override IAsyncOperation GetBasicPropertiesAsync() var item = await ftpClient.GetObjectInfo(FtpPath); return item is null ? new BaseBasicProperties() : new FtpFileBasicProperties(item); - }); + }, (_, _) => Task.FromResult(new BaseBasicProperties()))); } public override IAsyncOperation OpenAsync(FileAccessMode accessMode) { - return AsyncInfo.Run(async (cancellationToken) => + return AsyncInfo.Run((cancellationToken) => SafetyExtensions.Wrap(async () => { var ftpClient = GetFtpClient(); if (!await ftpClient.EnsureConnectedAsync()) @@ -111,13 +116,13 @@ public override IAsyncOperation OpenAsync(FileAccessMode ac { DisposeCallback = ftpClient.Dispose }; - }); + }, ((IPasswordProtectedItem)this).RetryWithCredentials)); } public override IAsyncOperation OpenAsync(FileAccessMode accessMode, StorageOpenOptions options) => OpenAsync(accessMode); public override IAsyncOperation OpenReadAsync() { - return AsyncInfo.Run(async (cancellationToken) => + return AsyncInfo.Run((cancellationToken) => SafetyExtensions.Wrap(async () => { var ftpClient = GetFtpClient(); if (!await ftpClient.EnsureConnectedAsync()) @@ -128,11 +133,11 @@ public override IAsyncOperation OpenReadAsyn var inStream = await ftpClient.OpenRead(FtpPath, token: cancellationToken); var nsStream = new NonSeekableRandomAccessStreamForRead(inStream, (ulong)inStream.Length) { DisposeCallback = ftpClient.Dispose }; return new StreamWithContentType(nsStream); - }); + }, ((IPasswordProtectedItem)this).RetryWithCredentials)); } public override IAsyncOperation OpenSequentialReadAsync() { - return AsyncInfo.Run(async (cancellationToken) => + return AsyncInfo.Run((cancellationToken) => SafetyExtensions.Wrap(async () => { var ftpClient = GetFtpClient(); if (!await ftpClient.EnsureConnectedAsync()) @@ -142,7 +147,7 @@ public override IAsyncOperation OpenSequentialReadAsync() var inStream = await ftpClient.OpenRead(FtpPath, token: cancellationToken); return new InputStreamWithDisposeCallback(inStream) { DisposeCallback = () => ftpClient.Dispose() }; - }); + }, ((IPasswordProtectedItem)this).RetryWithCredentials)); } public override IAsyncOperation OpenTransactedWriteAsync() => throw new NotSupportedException(); @@ -154,7 +159,7 @@ public override IAsyncOperation CopyAsync(IStorageFolder destin => CopyAsync(destinationFolder, desiredNewName, NameCollisionOption.FailIfExists); public override IAsyncOperation CopyAsync(IStorageFolder destinationFolder, string desiredNewName, NameCollisionOption option) { - return AsyncInfo.Run(async (cancellationToken) => + return AsyncInfo.Run((cancellationToken) => SafetyExtensions.Wrap(async () => { using var ftpClient = GetFtpClient(); if (!await ftpClient.EnsureConnectedAsync()) @@ -175,7 +180,7 @@ public override IAsyncOperation CopyAsync(IStorageFolder destin using var stream = await file.OpenStreamForWriteAsync(); return await ftpClient.DownloadStream(stream, FtpPath, token: cancellationToken) ? file : null; } - }); + }, ((IPasswordProtectedItem)this).RetryWithCredentials)); } public override IAsyncAction MoveAsync(IStorageFolder destinationFolder) => throw new NotSupportedException(); @@ -189,7 +194,7 @@ public override IAsyncAction RenameAsync(string desiredName) => RenameAsync(desiredName, NameCollisionOption.FailIfExists); public override IAsyncAction RenameAsync(string desiredName, NameCollisionOption option) { - return AsyncInfo.Run(async (cancellationToken) => + return AsyncInfo.Run((cancellationToken) => SafetyExtensions.Wrap(async () => { using var ftpClient = GetFtpClient(); if (!await ftpClient.EnsureConnectedAsync()) @@ -204,19 +209,19 @@ public override IAsyncAction RenameAsync(string desiredName, NameCollisionOption { // TODO: handle name generation } - }); + }, ((IPasswordProtectedItem)this).RetryWithCredentials)); } public override IAsyncAction DeleteAsync() { - return AsyncInfo.Run(async (cancellationToken) => + return AsyncInfo.Run((cancellationToken) => SafetyExtensions.Wrap(async () => { using var ftpClient = GetFtpClient(); if (await ftpClient.EnsureConnectedAsync()) { await ftpClient.DeleteFile(FtpPath, cancellationToken); } - }); + }, ((IPasswordProtectedItem)this).RetryWithCredentials)); } public override IAsyncAction DeleteAsync(StorageDeleteOption option) => DeleteAsync(); @@ -231,7 +236,9 @@ private AsyncFtpClient GetFtpClient() { string host = FtpHelpers.GetFtpHost(Path); ushort port = FtpHelpers.GetFtpPort(Path); - var credentials = FtpManager.Credentials.Get(host, FtpManager.Anonymous); + var credentials = Credentials is not null ? + new NetworkCredential(Credentials.UserName, Credentials.SecurePassword) : + FtpManager.Credentials.Get(host, FtpManager.Anonymous); return new(host, credentials, port); } diff --git a/src/Files.App/Utils/StorageItems/FtpStorageFolder.cs b/src/Files.App/Utils/StorageItems/FtpStorageFolder.cs index 74dbf03e3d13..baa2ae242895 100644 --- a/src/Files.App/Utils/StorageItems/FtpStorageFolder.cs +++ b/src/Files.App/Utils/StorageItems/FtpStorageFolder.cs @@ -5,6 +5,7 @@ using Files.App.Storage.FtpStorage; using FluentFTP; using System.IO; +using System.Net; using System.Runtime.InteropServices.WindowsRuntime; using Windows.Foundation; using Windows.Storage; @@ -13,7 +14,7 @@ namespace Files.App.Utils.StorageItems { - public sealed class FtpStorageFolder : BaseStorageFolder + public sealed class FtpStorageFolder : BaseStorageFolder, IPasswordProtectedItem { public override string Path { get; } public override string Name { get; } @@ -26,6 +27,10 @@ public sealed class FtpStorageFolder : BaseStorageFolder public override Windows.Storage.FileAttributes Attributes { get; } = Windows.Storage.FileAttributes.Directory; public override IStorageItemExtraProperties Properties => new BaseBasicStorageItemExtraProperties(this); + public StorageCredential Credentials { get; set; } + + public Func> PasswordRequestedCallback { get; set; } + public FtpStorageFolder(string path, string name, DateTimeOffset dateCreated) { Path = path; @@ -65,7 +70,7 @@ public static IAsyncOperation FromPathAsync(string path) public override IAsyncOperation GetBasicPropertiesAsync() { - return AsyncInfo.Run(async (cancellationToken) => + return AsyncInfo.Run((cancellationToken) => SafetyExtensions.Wrap(async () => { using var ftpClient = GetFtpClient(); if (!await ftpClient.EnsureConnectedAsync()) @@ -75,12 +80,12 @@ public override IAsyncOperation GetBasicPropertiesAsync() var item = await ftpClient.GetObjectInfo(FtpPath); return item is null ? new BaseBasicProperties() : new FtpFolderBasicProperties(item); - }); + }, (_, _) => Task.FromResult(new BaseBasicProperties()))); } public override IAsyncOperation GetItemAsync(string name) { - return AsyncInfo.Run(async (cancellationToken) => + return AsyncInfo.Run((cancellationToken) => SafetyExtensions.Wrap(async () => { using var ftpClient = GetFtpClient(); if (!await ftpClient.EnsureConnectedAsync()) @@ -93,15 +98,19 @@ public override IAsyncOperation GetItemAsync(string name) { if (item.Type is FtpObjectType.File) { - return new FtpStorageFile(Path, item); + var file = new FtpStorageFile(Path, item); + ((IPasswordProtectedItem)file).CopyFrom(this); + return file; } if (item.Type is FtpObjectType.Directory) { - return new FtpStorageFolder(Path, item); + var folder = new FtpStorageFolder(Path, item); + ((IPasswordProtectedItem)folder).CopyFrom(this); + return folder; } } return null; - }); + }, ((IPasswordProtectedItem)this).RetryWithCredentials)); } public override IAsyncOperation TryGetItemAsync(string name) { @@ -119,7 +128,7 @@ public override IAsyncOperation TryGetItemAsync(string name) } public override IAsyncOperation> GetItemsAsync() { - return AsyncInfo.Run(async (cancellationToken) => + return AsyncInfo.Run((cancellationToken) => SafetyExtensions.Wrap>(async () => { using var ftpClient = GetFtpClient(); if (!await ftpClient.EnsureConnectedAsync()) @@ -133,15 +142,19 @@ public override IAsyncOperation> GetItemsAsync() { if (item.Type is FtpObjectType.File) { - items.Add(new FtpStorageFile(Path, item)); + var file = new FtpStorageFile(Path, item); + ((IPasswordProtectedItem)file).CopyFrom(this); + items.Add(file); } else if (item.Type is FtpObjectType.Directory) { - items.Add(new FtpStorageFolder(Path, item)); + var folder = new FtpStorageFolder(Path, item); + ((IPasswordProtectedItem)folder).CopyFrom(this); + items.Add(folder); } } return (IReadOnlyList)items; - }); + }, ((IPasswordProtectedItem)this).RetryWithCredentials)); } public override IAsyncOperation> GetItemsAsync(uint startIndex, uint maxItemsToRetrieve) => AsyncInfo.Run>(async (cancellationToken) @@ -171,7 +184,7 @@ public override IAsyncOperation CreateFileAsync(string desiredN => CreateFileAsync(desiredName, CreationCollisionOption.FailIfExists); public override IAsyncOperation CreateFileAsync(string desiredName, CreationCollisionOption options) { - return AsyncInfo.Run(async (cancellationToken) => + return AsyncInfo.Run((cancellationToken) => SafetyExtensions.Wrap(async () => { using var ftpClient = GetFtpClient(); if (!await ftpClient.EnsureConnectedAsync()) @@ -200,7 +213,11 @@ public override IAsyncOperation CreateFileAsync(string desiredN while (result is FtpStatus.Skipped && ++attempt < 1024 && options == CreationCollisionOption.GenerateUniqueName); if (result is FtpStatus.Success) - return new FtpStorageFile(new StorageFileWithPath(null, $"{Path}/{finalName}")); + { + var file = new FtpStorageFile(new StorageFileWithPath(null, $"{Path}/{finalName}")); + ((IPasswordProtectedItem)file).CopyFrom(this); + return file; + } if (result is FtpStatus.Skipped) { @@ -211,14 +228,14 @@ public override IAsyncOperation CreateFileAsync(string desiredN } throw new IOException($"Failed to create file {remotePath}."); - }); + }, ((IPasswordProtectedItem)this).RetryWithCredentials)); } public override IAsyncOperation CreateFolderAsync(string desiredName) => CreateFolderAsync(desiredName, CreationCollisionOption.FailIfExists); public override IAsyncOperation CreateFolderAsync(string desiredName, CreationCollisionOption options) { - return AsyncInfo.Run(async (cancellationToken) => + return AsyncInfo.Run((cancellationToken) => SafetyExtensions.Wrap(async () => { using var ftpClient = GetFtpClient(); if (!await ftpClient.EnsureConnectedAsync()) @@ -229,7 +246,9 @@ public override IAsyncOperation CreateFolderAsync(string desi string fileName = $"{FtpPath}/{desiredName}"; if (await ftpClient.DirectoryExists(fileName)) { - return new FtpStorageFolder(new StorageFileWithPath(null, fileName)); + var item = new FtpStorageFolder(new StorageFileWithPath(null, fileName)); + ((IPasswordProtectedItem)item).CopyFrom(this); + return item; } bool replaceExisting = options is CreationCollisionOption.ReplaceExisting; @@ -239,15 +258,17 @@ public override IAsyncOperation CreateFolderAsync(string desi throw new IOException($"Failed to create folder {desiredName}."); } - return new FtpStorageFolder(new StorageFileWithPath(null, $"{Path}/{desiredName}")); - }); + var folder = new FtpStorageFolder(new StorageFileWithPath(null, $"{Path}/{desiredName}")); + ((IPasswordProtectedItem)folder).CopyFrom(this); + return folder; + }, ((IPasswordProtectedItem)this).RetryWithCredentials)); } public override IAsyncAction RenameAsync(string desiredName) => RenameAsync(desiredName, NameCollisionOption.FailIfExists); public override IAsyncAction RenameAsync(string desiredName, NameCollisionOption option) { - return AsyncInfo.Run(async (cancellationToken) => + return AsyncInfo.Run((cancellationToken) => SafetyExtensions.Wrap(async () => { using var ftpClient = GetFtpClient(); if (!await ftpClient.EnsureConnectedAsync()) @@ -262,19 +283,19 @@ public override IAsyncAction RenameAsync(string desiredName, NameCollisionOption { // TODO: handle name generation } - }); + }, ((IPasswordProtectedItem)this).RetryWithCredentials)); } public override IAsyncAction DeleteAsync() { - return AsyncInfo.Run(async (cancellationToken) => + return AsyncInfo.Run((cancellationToken) => SafetyExtensions.Wrap(async () => { using var ftpClient = GetFtpClient(); if (await ftpClient.EnsureConnectedAsync()) { await ftpClient.DeleteDirectory(FtpPath, cancellationToken); } - }); + }, ((IPasswordProtectedItem)this).RetryWithCredentials)); } public override IAsyncAction DeleteAsync(StorageDeleteOption option) => DeleteAsync(); @@ -304,7 +325,9 @@ private AsyncFtpClient GetFtpClient() { string host = FtpHelpers.GetFtpHost(Path); ushort port = FtpHelpers.GetFtpPort(Path); - var credentials = FtpManager.Credentials.Get(host, FtpManager.Anonymous); + var credentials = Credentials is not null ? + new NetworkCredential(Credentials.UserName, Credentials.SecurePassword) : + FtpManager.Credentials.Get(host, FtpManager.Anonymous); return new(host, credentials, port); } diff --git a/src/Files.App/Utils/StorageItems/ZipStorageFile.cs b/src/Files.App/Utils/StorageItems/ZipStorageFile.cs index 6fd9699a20d9..cab66f3b0dfe 100644 --- a/src/Files.App/Utils/StorageItems/ZipStorageFile.cs +++ b/src/Files.App/Utils/StorageItems/ZipStorageFile.cs @@ -19,7 +19,7 @@ namespace Files.App.Utils.StorageItems { - public sealed class ZipStorageFile : BaseStorageFile + public sealed class ZipStorageFile : BaseStorageFile, IPasswordProtectedItem { private readonly string containerPath; private readonly BaseStorageFile backingFile; @@ -50,6 +50,10 @@ public override string DisplayType private IStorageItemExtraProperties properties; public override IStorageItemExtraProperties Properties => properties ??= new BaseBasicStorageItemExtraProperties(this); + public StorageCredential Credentials { get; set; } = new(); + + public Func> PasswordRequestedCallback { get; set; } + public ZipStorageFile(string path, string containerPath) { Name = IO.Path.GetFileName(path.TrimEnd('\\', '/')); @@ -96,7 +100,7 @@ public static IAsyncOperation FromPathAsync(string path) public override IAsyncOperation OpenAsync(FileAccessMode accessMode) { - return AsyncInfo.Run(async (cancellationToken) => + return AsyncInfo.Run((cancellationToken) => SafetyExtensions.Wrap(async () => { bool rw = accessMode is FileAccessMode.ReadWrite; if (Path == containerPath) @@ -135,14 +139,14 @@ public override IAsyncOperation OpenAsync(FileAccessMode ac } throw new NotSupportedException("Can't open zip file as RW"); - }); + }, ((IPasswordProtectedItem)this).RetryWithCredentials)); } public override IAsyncOperation OpenAsync(FileAccessMode accessMode, StorageOpenOptions options) => OpenAsync(accessMode); public override IAsyncOperation OpenReadAsync() { - return AsyncInfo.Run(async (cancellationToken) => + return AsyncInfo.Run((cancellationToken) => SafetyExtensions.Wrap(async () => { if (Path == containerPath) { @@ -176,12 +180,12 @@ public override IAsyncOperation OpenReadAsyn DisposeCallback = () => zipFile.Dispose() }; return new StreamWithContentType(nsStream); - }); + }, ((IPasswordProtectedItem)this).RetryWithCredentials)); } public override IAsyncOperation OpenSequentialReadAsync() { - return AsyncInfo.Run(async (cancellationToken) => + return AsyncInfo.Run((cancellationToken) => SafetyExtensions.Wrap(async () => { if (Path == containerPath) { @@ -213,7 +217,7 @@ public override IAsyncOperation OpenSequentialReadAsync() { DisposeCallback = () => zipFile.Dispose() }; - }); + }, ((IPasswordProtectedItem)this).RetryWithCredentials)); } public override IAsyncOperation OpenTransactedWriteAsync() @@ -227,7 +231,7 @@ public override IAsyncOperation CopyAsync(IStorageFolder destin => CopyAsync(destinationFolder, desiredNewName, NameCollisionOption.FailIfExists); public override IAsyncOperation CopyAsync(IStorageFolder destinationFolder, string desiredNewName, NameCollisionOption option) { - return AsyncInfo.Run(async (cancellationToken) => + return AsyncInfo.Run((cancellationToken) => SafetyExtensions.Wrap(async () => { using SevenZipExtractor zipFile = await OpenZipFileAsync(); if (zipFile is null || zipFile.ArchiveFileData is null) @@ -256,14 +260,18 @@ public override IAsyncOperation CopyAsync(IStorageFolder destin { var destFile = await destFolder.CreateFileAsync(desiredNewName, option.Convert()); using var outStream = await destFile.OpenStreamForWriteAsync(); - await zipFile.ExtractFileAsync(entry.Index, outStream); + await SafetyExtensions.Wrap(() => zipFile.ExtractFileAsync(entry.Index, outStream), async (_, exception) => + { + await destFile.DeleteAsync(); + throw exception; + }); return destFile; } - }); + }, ((IPasswordProtectedItem)this).RetryWithCredentials)); } public override IAsyncAction CopyAndReplaceAsync(IStorageFile fileToReplace) { - return AsyncInfo.Run(async (cancellationToken) => + return AsyncInfo.Run((cancellationToken) => SafetyExtensions.Wrap(async () => { using SevenZipExtractor zipFile = await OpenZipFileAsync(); if (zipFile is null || zipFile.ArchiveFileData is null) @@ -282,7 +290,7 @@ public override IAsyncAction CopyAndReplaceAsync(IStorageFile fileToReplace) { await zipFile.ExtractFileAsync(entry.Index, outStream); } - }); + }, ((IPasswordProtectedItem)this).RetryWithCredentials)); } public override IAsyncAction MoveAsync(IStorageFolder destinationFolder) @@ -297,7 +305,7 @@ public override IAsyncAction MoveAndReplaceAsync(IStorageFile fileToReplace) public override IAsyncAction RenameAsync(string desiredName) => RenameAsync(desiredName, NameCollisionOption.FailIfExists); public override IAsyncAction RenameAsync(string desiredName, NameCollisionOption option) { - return AsyncInfo.Run(async (cancellationToken) => + return AsyncInfo.Run((cancellationToken) => SafetyExtensions.Wrap(async () => { if (Path == containerPath) { @@ -325,7 +333,7 @@ public override IAsyncAction RenameAsync(string desiredName, NameCollisionOption SevenZipCompressor compressor = new SevenZipCompressor() { CompressionMode = CompressionMode.Append }; compressor.SetFormatFromExistingArchive(archiveStream); var fileName = IO.Path.GetRelativePath(containerPath, IO.Path.Combine(IO.Path.GetDirectoryName(Path), desiredName)); - await compressor.ModifyArchiveAsync(archiveStream, new Dictionary() { { index, fileName } }, "", ms); + await compressor.ModifyArchiveAsync(archiveStream, new Dictionary() { { index, fileName } }, Credentials.Password, ms); } using (var archiveStream = await OpenZipFileAsync(FileAccessMode.ReadWrite)) { @@ -336,13 +344,13 @@ public override IAsyncAction RenameAsync(string desiredName, NameCollisionOption } } } - }); + }, ((IPasswordProtectedItem)this).RetryWithCredentials)); } public override IAsyncAction DeleteAsync() => DeleteAsync(StorageDeleteOption.Default); public override IAsyncAction DeleteAsync(StorageDeleteOption option) { - return AsyncInfo.Run(async (cancellationToken) => + return AsyncInfo.Run((cancellationToken) => SafetyExtensions.Wrap(async () => { if (Path == containerPath) { @@ -372,7 +380,7 @@ public override IAsyncAction DeleteAsync(StorageDeleteOption option) { SevenZipCompressor compressor = new SevenZipCompressor() { CompressionMode = CompressionMode.Append }; compressor.SetFormatFromExistingArchive(archiveStream); - await compressor.ModifyArchiveAsync(archiveStream, new Dictionary() { { index, null } }, "", ms); + await compressor.ModifyArchiveAsync(archiveStream, new Dictionary() { { index, null } }, Credentials.Password, ms); } using (var archiveStream = await OpenZipFileAsync(FileAccessMode.ReadWrite)) { @@ -383,7 +391,7 @@ public override IAsyncAction DeleteAsync(StorageDeleteOption option) } } } - }); + }, ((IPasswordProtectedItem)this).RetryWithCredentials)); } public override IAsyncOperation GetThumbnailAsync(ThumbnailMode mode) @@ -408,6 +416,10 @@ private static bool CheckAccess(string path) return zipFile.ArchiveFileData is not null; } } + catch (SevenZipOpenFailedException ex) + { + return ex.Result == OperationResult.WrongPassword; + } catch { return false; @@ -453,7 +465,7 @@ private IAsyncOperation OpenZipFileAsync() return AsyncInfo.Run(async (cancellationToken) => { var zipFile = await OpenZipFileAsync(FileAccessMode.Read); - return zipFile is not null ? new SevenZipExtractor(zipFile) : null; + return zipFile is not null ? new SevenZipExtractor(zipFile, Credentials.Password) : null; }); } diff --git a/src/Files.App/Utils/StorageItems/ZipStorageFolder.cs b/src/Files.App/Utils/StorageItems/ZipStorageFolder.cs index 0a44e79861b9..dbc1846f3cf5 100644 --- a/src/Files.App/Utils/StorageItems/ZipStorageFolder.cs +++ b/src/Files.App/Utils/StorageItems/ZipStorageFolder.cs @@ -6,6 +6,7 @@ using Files.Core.Helpers; using Files.Shared.Extensions; using SevenZip; +using SQLitePCL; using System; using System.Collections.Concurrent; using System.Collections.Generic; @@ -22,7 +23,7 @@ namespace Files.App.Utils.StorageItems { - public sealed class ZipStorageFolder : BaseStorageFolder, ICreateFileWithStream + public sealed class ZipStorageFolder : BaseStorageFolder, ICreateFileWithStream, IPasswordProtectedItem { private readonly string containerPath; private BaseStorageFile backingFile; @@ -37,6 +38,10 @@ public sealed class ZipStorageFolder : BaseStorageFolder, ICreateFileWithStream public override Windows.Storage.FileAttributes Attributes => Windows.Storage.FileAttributes.Directory; public override IStorageItemExtraProperties Properties => new BaseBasicStorageItemExtraProperties(this); + public StorageCredential Credentials { get; set; } = new(); + + public Func> PasswordRequestedCallback { get; set; } + public ZipStorageFolder(string path, string containerPath) { Name = IO.Path.GetFileName(path.TrimEnd('\\', '/')); @@ -55,7 +60,7 @@ public ZipStorageFolder(BaseStorageFile backingFile) } Name = IO.Path.GetFileName(backingFile.Path.TrimEnd('\\', '/')); Path = backingFile.Path; - containerPath = backingFile.Path; + this.containerPath = backingFile.Path; this.backingFile = backingFile; } public ZipStorageFolder(string path, string containerPath, ArchiveFileInfo entry, BaseStorageFile backingFile) : this(path, containerPath, entry) @@ -174,7 +179,7 @@ public override IAsyncOperation GetBasicPropertiesAsync() public override IAsyncOperation GetItemAsync(string name) { - return AsyncInfo.Run(async (cancellationToken) => + return AsyncInfo.Run((cancellationToken) => SafetyExtensions.Wrap(async () => { using SevenZipExtractor zipFile = await OpenZipFileAsync(); if (zipFile is null || zipFile.ArchiveFileData is null) @@ -193,12 +198,17 @@ public override IAsyncOperation GetItemAsync(string name) if (entry.IsDirectory) { - return new ZipStorageFolder(filePath, containerPath, entry, backingFile); + var folder = new ZipStorageFolder(filePath, containerPath, entry, backingFile); + ((IPasswordProtectedItem)folder).CopyFrom(this); + return folder; } - return new ZipStorageFile(filePath, containerPath, entry, backingFile); - }); + var file = new ZipStorageFile(filePath, containerPath, entry, backingFile); + ((IPasswordProtectedItem)file).CopyFrom(this); + return file; + }, ((IPasswordProtectedItem)this).RetryWithCredentials)); } + public override IAsyncOperation TryGetItemAsync(string name) { return AsyncInfo.Run(async (cancellationToken) => @@ -215,7 +225,7 @@ public override IAsyncOperation TryGetItemAsync(string name) } public override IAsyncOperation> GetItemsAsync() { - return AsyncInfo.Run>(async (cancellationToken) => + return AsyncInfo.Run((cancellationToken) => SafetyExtensions.Wrap>(async () => { using SevenZipExtractor zipFile = await OpenZipFileAsync(); if (zipFile is null || zipFile.ArchiveFileData is null) @@ -237,18 +247,22 @@ public override IAsyncOperation> GetItemsAsync() var itemPath = System.IO.Path.Combine(Path, split[0]); if (!items.Any(x => x.Path == itemPath)) { - items.Add(new ZipStorageFolder(itemPath, containerPath, entry, backingFile)); + var folder = new ZipStorageFolder(itemPath, containerPath, entry, backingFile); + ((IPasswordProtectedItem)folder).CopyFrom(this); + items.Add(folder); } } else { - items.Add(new ZipStorageFile(winPath, containerPath, entry, backingFile)); + var file = new ZipStorageFile(winPath, containerPath, entry, backingFile); + ((IPasswordProtectedItem)file).CopyFrom(this); + items.Add(file); } } } } return items; - }); + }, ((IPasswordProtectedItem)this).RetryWithCredentials)); } public override IAsyncOperation> GetItemsAsync(uint startIndex, uint maxItemsToRetrieve) => AsyncInfo.Run>(async (cancellationToken) @@ -290,7 +304,7 @@ public override IAsyncOperation CreateFolderAsync(string desi => CreateFolderAsync(desiredName, CreationCollisionOption.FailIfExists); public override IAsyncOperation CreateFolderAsync(string desiredName, CreationCollisionOption options) { - return AsyncInfo.Run(async (cancellationToken) => + return AsyncInfo.Run((cancellationToken) => SafetyExtensions.Wrap(async () => { var zipDesiredName = System.IO.Path.Combine(Path, desiredName); var item = await GetItemAsync(desiredName); @@ -310,7 +324,7 @@ public override IAsyncOperation CreateFolderAsync(string desi SevenZipCompressor compressor = new SevenZipCompressor() { CompressionMode = CompressionMode.Append }; compressor.SetFormatFromExistingArchive(archiveStream); var fileName = IO.Path.GetRelativePath(containerPath, zipDesiredName); - await compressor.CompressStreamDictionaryAsync(archiveStream, new Dictionary() { { fileName, null } }, "", ms); + await compressor.CompressStreamDictionaryAsync(archiveStream, new Dictionary() { { fileName, null } }, Credentials.Password, ms); } using (var archiveStream = await OpenZipFileAsync(FileAccessMode.ReadWrite)) { @@ -321,14 +335,16 @@ public override IAsyncOperation CreateFolderAsync(string desi } } - return new ZipStorageFolder(zipDesiredName, containerPath, backingFile); - }); + var folder = new ZipStorageFolder(zipDesiredName, containerPath, backingFile); + ((IPasswordProtectedItem)folder).CopyFrom(this); + return folder; + }, ((IPasswordProtectedItem)this).RetryWithCredentials)); } public override IAsyncAction RenameAsync(string desiredName) => RenameAsync(desiredName, NameCollisionOption.FailIfExists); public override IAsyncAction RenameAsync(string desiredName, NameCollisionOption option) { - return AsyncInfo.Run(async (cancellationToken) => + return AsyncInfo.Run((cancellationToken) => SafetyExtensions.Wrap(async () => { if (Path == containerPath) { @@ -359,7 +375,7 @@ public override IAsyncAction RenameAsync(string desiredName, NameCollisionOption var folderDes = IO.Path.Combine(IO.Path.GetDirectoryName(folderKey), desiredName); var entriesMap = new Dictionary(index.Select(x => new KeyValuePair(x.Index, IO.Path.Combine(folderDes, IO.Path.GetRelativePath(folderKey, x.Key))))); - await compressor.ModifyArchiveAsync(archiveStream, entriesMap, "", ms); + await compressor.ModifyArchiveAsync(archiveStream, entriesMap, Credentials.Password, ms); } using (var archiveStream = await OpenZipFileAsync(FileAccessMode.ReadWrite)) { @@ -370,13 +386,13 @@ public override IAsyncAction RenameAsync(string desiredName, NameCollisionOption } } } - }); + }, ((IPasswordProtectedItem)this).RetryWithCredentials)); } public override IAsyncAction DeleteAsync() => DeleteAsync(StorageDeleteOption.Default); public override IAsyncAction DeleteAsync(StorageDeleteOption option) { - return AsyncInfo.Run(async (cancellationToken) => + return AsyncInfo.Run((cancellationToken) => SafetyExtensions.Wrap(async () => { if (Path == containerPath) { @@ -407,7 +423,7 @@ public override IAsyncAction DeleteAsync(StorageDeleteOption option) SevenZipCompressor compressor = new SevenZipCompressor() { CompressionMode = CompressionMode.Append }; compressor.SetFormatFromExistingArchive(archiveStream); var entriesMap = new Dictionary(index.Select(x => new KeyValuePair(x.Index, null))); - await compressor.ModifyArchiveAsync(archiveStream, entriesMap, "", ms); + await compressor.ModifyArchiveAsync(archiveStream, entriesMap, Credentials.Password, ms); } using (var archiveStream = await OpenZipFileAsync(FileAccessMode.ReadWrite)) { @@ -418,7 +434,7 @@ public override IAsyncAction DeleteAsync(StorageDeleteOption option) } } } - }); + }, ((IPasswordProtectedItem)this).RetryWithCredentials)); } public override bool AreQueryOptionsSupported(QueryOptions queryOptions) => false; @@ -488,14 +504,22 @@ private static bool CheckAccess(string path) } private static bool CheckAccess(Stream stream) { - return SafetyExtensions.IgnoreExceptions(() => + try { using (SevenZipExtractor zipFile = new SevenZipExtractor(stream)) { //zipFile.IsStreamOwner = false; return zipFile.ArchiveFileData is not null; } - }); + } + catch (SevenZipOpenFailedException ex) + { + return ex.Result == OperationResult.WrongPassword; + } + catch + { + return false; + } } private static async Task CheckAccess(IStorageFile file) { @@ -549,7 +573,7 @@ private IAsyncOperation OpenZipFileAsync() return AsyncInfo.Run(async (cancellationToken) => { var zipFile = await OpenZipFileAsync(FileAccessMode.Read); - return zipFile is not null ? new SevenZipExtractor(zipFile) : null; + return zipFile is not null ? new SevenZipExtractor(zipFile, Credentials.Password) : null; }); } @@ -592,7 +616,7 @@ public IAsyncOperation CreateFileAsync(Stream contents, string public IAsyncOperation CreateFileAsync(Stream contents, string desiredName, CreationCollisionOption options) { - return AsyncInfo.Run(async (cancellationToken) => + return AsyncInfo.Run((cancellationToken) => SafetyExtensions.Wrap(async () => { var zipDesiredName = System.IO.Path.Combine(Path, desiredName); var item = await GetItemAsync(desiredName); @@ -612,7 +636,7 @@ public IAsyncOperation CreateFileAsync(Stream contents, string SevenZipCompressor compressor = new SevenZipCompressor() { CompressionMode = CompressionMode.Append }; compressor.SetFormatFromExistingArchive(archiveStream); var fileName = IO.Path.GetRelativePath(containerPath, zipDesiredName); - await compressor.CompressStreamDictionaryAsync(archiveStream, new Dictionary() { { fileName, contents } }, "", ms); + await compressor.CompressStreamDictionaryAsync(archiveStream, new Dictionary() { { fileName, contents } }, Credentials.Password, ms); } using (var archiveStream = await OpenZipFileAsync(FileAccessMode.ReadWrite)) { @@ -623,8 +647,10 @@ public IAsyncOperation CreateFileAsync(Stream contents, string } } - return new ZipStorageFile(zipDesiredName, containerPath, backingFile); - }); + var file = new ZipStorageFile(zipDesiredName, containerPath, backingFile); + ((IPasswordProtectedItem)file).CopyFrom(this); + return file; + }, ((IPasswordProtectedItem)this).RetryWithCredentials)); } private class ZipFolderBasicProperties : BaseBasicProperties diff --git a/src/Files.App/Views/LayoutModes/BaseLayout.cs b/src/Files.App/Views/LayoutModes/BaseLayout.cs index 9c3e5ba83ad6..ff0927e0c998 100644 --- a/src/Files.App/Views/LayoutModes/BaseLayout.cs +++ b/src/Files.App/Views/LayoutModes/BaseLayout.cs @@ -907,8 +907,8 @@ protected void FileList_DragItemsStarting(object sender, DragItemsStartingEventA { try { - var shellItemList = e.Items.OfType().Select(x => new VanaraWindowsShell.ShellItem(x.ItemPath)).ToArray(); - if (shellItemList[0].FileSystemPath is not null && !InstanceViewModel.IsPageTypeSearchResults) + var shellItemList = SafetyExtensions.IgnoreExceptions(() => e.Items.OfType().Select(x => new VanaraWindowsShell.ShellItem(x.ItemPath)).ToArray()); + if (shellItemList?[0].FileSystemPath is not null && !InstanceViewModel.IsPageTypeSearchResults) { var iddo = shellItemList[0].Parent.GetChildrenUIObjects(HWND.NULL, shellItemList); shellItemList.ForEach(x => x.Dispose()); diff --git a/src/Files.App/nupkgs/SevenZipSharp.1.0.0.nupkg b/src/Files.App/nupkgs/SevenZipSharp.1.0.0.nupkg index aacd1ea66163..ced7d4de30f6 100644 Binary files a/src/Files.App/nupkgs/SevenZipSharp.1.0.0.nupkg and b/src/Files.App/nupkgs/SevenZipSharp.1.0.0.nupkg differ diff --git a/src/Files.Core/ViewModels/Dialogs/CredentialDialogViewModel.cs b/src/Files.Core/ViewModels/Dialogs/CredentialDialogViewModel.cs index f30d2f273012..54fd51803f0b 100644 --- a/src/Files.Core/ViewModels/Dialogs/CredentialDialogViewModel.cs +++ b/src/Files.Core/ViewModels/Dialogs/CredentialDialogViewModel.cs @@ -19,6 +19,20 @@ public bool IsAnonymous set => SetProperty(ref _IsAnonymous, value); } + private bool _PasswordOnly; + public bool PasswordOnly + { + get => _PasswordOnly; + set => SetProperty(ref _PasswordOnly, value); + } + + private bool _CanBeAnonymous; + public bool CanBeAnonymous + { + get => _CanBeAnonymous; + set => SetProperty(ref _CanBeAnonymous, value); + } + public DisposableArray? Password { get; private set; } public IRelayCommand PrimaryButtonClickCommand { get; } diff --git a/src/Files.Shared/Extensions/SafetyExtensions.cs b/src/Files.Shared/Extensions/SafetyExtensions.cs index 9b384b0d1a8b..a7e06c8b4419 100644 --- a/src/Files.Shared/Extensions/SafetyExtensions.cs +++ b/src/Files.Shared/Extensions/SafetyExtensions.cs @@ -7,7 +7,6 @@ namespace Files.Shared.Extensions { - [Obsolete("This class will be replaced with SafeWrapper.")] public static class SafetyExtensions { public static bool IgnoreExceptions(Action action, ILogger? logger = null) @@ -63,5 +62,29 @@ public static async Task IgnoreExceptions(Func action, ILogger? logg return default; } } + + public static async Task Wrap(Func> inputTask, Func>, Exception, Task> onFailed) + { + try + { + return await inputTask(); + } + catch (Exception ex) + { + return await onFailed(inputTask, ex); + } + } + + public static async Task Wrap(Func inputTask, Func, Exception, Task> onFailed) + { + try + { + await inputTask(); + } + catch (Exception ex) + { + await onFailed(inputTask, ex); + } + } } }