diff --git a/docs/linux-port/compat-off-baseline.txt b/docs/linux-port/compat-off-baseline.txt index 4b1653516cc4..5e497d2e8a98 100644 --- a/docs/linux-port/compat-off-baseline.txt +++ b/docs/linux-port/compat-off-baseline.txt @@ -2,18 +2,4 @@ # One "file: error CODE: message" per line. Regenerate with scripts/linux/compat-off-check.sh --update. src/Files.App/Data/Items/ContextMenu.cs: error CS0246: The type or namespace name 'MENU_ITEM_TYPE' could not be found (are you missing a using directive or an assembly reference?) src/Files.App/Data/Items/WidgetRecentItem.cs: error CS0246: The type or namespace name 'IShellItem' could not be found (are you missing a using directive or an assembly reference?) -src/Files.App/Utils/Storage/Enumerators/Win32StorageEnumerator.cs: error CS0246: The type or namespace name 'Win32PInvoke' could not be found (are you missing a using directive or an assembly reference?) -src/Files.App/Utils/Storage/Helpers/FileTypesHelper.cs: error CS0234: The type or namespace name 'FileSystem' does not exist in the namespace 'Windows.Win32.Storage' (are you missing an assembly reference?) -src/Files.App/Utils/Storage/Helpers/FilesystemResult.cs: error CS0122: 'BOOL' is inaccessible due to its protection level -src/Files.App/Utils/Storage/Helpers/FolderHelpers.cs: error CS0234: The type or namespace name 'FileSystem' does not exist in the namespace 'Windows.Win32.Storage' (are you missing an assembly reference?) -src/Files.App/Utils/Storage/Helpers/StorageHelpers.cs: error CS0234: The type or namespace name 'FileSystem' does not exist in the namespace 'Windows.Win32.Storage' (are you missing an assembly reference?) -src/Files.App/Utils/Storage/Search/FolderSearch.cs: error CS0234: The type or namespace name 'FileSystem' does not exist in the namespace 'Windows.Win32.Storage' (are you missing an assembly reference?) -src/Files.App/Utils/Storage/Search/FolderSearch.cs: error CS0246: The type or namespace name 'FindCloseSafeHandle' could not be found (are you missing a using directive or an assembly reference?) -src/Files.App/Utils/Storage/Search/FolderSearch.cs: error CS0246: The type or namespace name 'WIN32_FIND_DATAW' could not be found (are you missing a using directive or an assembly reference?) -src/Files.App/Utils/Storage/StorageItems/ShellStorageFile.cs: error CS0246: The type or namespace name 'SHOW_WINDOW_CMD' could not be found (are you missing a using directive or an assembly reference?) -src/Files.App/Utils/Storage/StorageItems/ShellStorageFolder.cs: error CS0246: The type or namespace name 'SHOW_WINDOW_CMD' could not be found (are you missing a using directive or an assembly reference?) -src/Files.App/Utils/Storage/StorageItems/StreamWithContentType.cs: error CS0122: 'IStream' is inaccessible due to its protection level -src/Files.App/Utils/Storage/StorageItems/StreamWithContentType.cs: error CS0122: 'STATSTG' is inaccessible due to its protection level -src/Files.App/Utils/Storage/StorageItems/StreamWithContentType.cs: error CS0122: 'STGMEDIUM' is inaccessible due to its protection level -src/Files.App/Utils/Storage/StorageItems/VirtualStorageItem.cs: error CS0234: The type or namespace name 'FileSystem' does not exist in the namespace 'Windows.Win32.Storage' (are you missing an assembly reference?) src/Files.App/ViewModels/ShellViewModel.cs: error CS0234: The type or namespace name 'Win32PInvoke' does not exist in the namespace 'Files.App.Helpers' (are you missing an assembly reference?) diff --git a/src/Files.App/Actions/FileSystem/CopyItemFromHomeAction.cs b/src/Files.App/Actions/FileSystem/CopyItemFromHomeAction.cs index 15477b11f12f..048aa65ffbb8 100644 --- a/src/Files.App/Actions/FileSystem/CopyItemFromHomeAction.cs +++ b/src/Files.App/Actions/FileSystem/CopyItemFromHomeAction.cs @@ -63,6 +63,7 @@ public async Task ExecuteAsync(object? parameter = null) return; } +#if WINDOWS if (storageItem is SystemStorageFolder or SystemStorageFile) { var standardItems = await new[] { storageItem }.ToStandardStorageItemsAsync(); @@ -70,6 +71,7 @@ public async Task ExecuteAsync(object? parameter = null) storageItem = standardItems.First(); } +#endif dataPackage.Properties.PackageFamilyName = Windows.ApplicationModel.Package.Current.Id.FamilyName; dataPackage.SetStorageItems(new[] { storageItem }, false); diff --git a/src/Files.App/Actions/Sidebar/CopyItemFromSidebarAction.cs b/src/Files.App/Actions/Sidebar/CopyItemFromSidebarAction.cs index 512d363ae2bf..27415b998e33 100644 --- a/src/Files.App/Actions/Sidebar/CopyItemFromSidebarAction.cs +++ b/src/Files.App/Actions/Sidebar/CopyItemFromSidebarAction.cs @@ -63,6 +63,7 @@ public async Task ExecuteAsync(object? parameter = null) return; } +#if WINDOWS if (storageItem is SystemStorageFolder or SystemStorageFile) { var standardItems = await new[] { storageItem }.ToStandardStorageItemsAsync(); @@ -70,6 +71,7 @@ public async Task ExecuteAsync(object? parameter = null) storageItem = standardItems.First(); } +#endif dataPackage.Properties.PackageFamilyName = Windows.ApplicationModel.Package.Current.Id.FamilyName; dataPackage.SetStorageItems(new[] { storageItem }, false); diff --git a/src/Files.App/Services/Windows/WindowsQuickAccessService.cs b/src/Files.App/Services/Windows/WindowsQuickAccessService.cs index 173518985fb6..4a479cc0dea1 100644 --- a/src/Files.App/Services/Windows/WindowsQuickAccessService.cs +++ b/src/Files.App/Services/Windows/WindowsQuickAccessService.cs @@ -54,6 +54,7 @@ private async Task UnpinFromSidebarAsync(string[] folderPaths, bool doUpda ?? throw new InvalidOperationException("The Windows Shell Home namespace returned an item without a path."); bool shouldUnpin = folderPaths.Contains(pathStr); +#if WINDOWS if (ShellStorageFolder.IsShellPath(pathStr)) { var folder = await ShellStorageFolder.FromPathAsync(pathStr); @@ -64,6 +65,7 @@ private async Task UnpinFromSidebarAsync(string[] folderPaths, bool doUpda (path.StartsWith(@"\\SHELL\\") && folderPaths.Any(x => x.StartsWith(@"\\SHELL\\")))); } +#endif if (!shouldUnpin) continue; diff --git a/src/Files.App/Utils/Shell/ShellHelpers.cs b/src/Files.App/Utils/Shell/ShellHelpers.cs index 812b48424707..3a2c6d663b9e 100644 --- a/src/Files.App/Utils/Shell/ShellHelpers.cs +++ b/src/Files.App/Utils/Shell/ShellHelpers.cs @@ -9,6 +9,7 @@ public static class ShellHelpers { public static string ResolveShellPath(string shPath) { +#if WINDOWS if (!ShellStorageFolder.IsShellPath(shPath)) return shPath; @@ -26,12 +27,19 @@ public static string ResolveShellPath(string shPath) return Constants.UserEnvironmentPaths.NetworkFolderPath; return shPath; +#else + return shPath; +#endif } public static bool IsSupportedShellPath(string shPath) { +#if WINDOWS return ShellStorageFolder.IsShellPath(shPath) && Constants.UserEnvironmentPaths.ShellPlaces.ContainsKey(GetShellPathLookupKey(shPath)); +#else + return false; +#endif } private static string GetShellPathLookupKey(string shPath) diff --git a/src/Files.App/Utils/Storage/Enumerators/UniversalStorageEnumerator.cs b/src/Files.App/Utils/Storage/Enumerators/UniversalStorageEnumerator.Windows.cs similarity index 100% rename from src/Files.App/Utils/Storage/Enumerators/UniversalStorageEnumerator.cs rename to src/Files.App/Utils/Storage/Enumerators/UniversalStorageEnumerator.Windows.cs diff --git a/src/Files.App/Utils/Storage/Enumerators/Win32StorageEnumerator.cs b/src/Files.App/Utils/Storage/Enumerators/Win32StorageEnumerator.Windows.cs similarity index 100% rename from src/Files.App/Utils/Storage/Enumerators/Win32StorageEnumerator.cs rename to src/Files.App/Utils/Storage/Enumerators/Win32StorageEnumerator.Windows.cs diff --git a/src/Files.App/Utils/Storage/Helpers/ErrorCodeConverter.cs b/src/Files.App/Utils/Storage/Helpers/ErrorCodeConverter.cs index a3be94bca180..4eb952ca63af 100644 --- a/src/Files.App/Utils/Storage/Helpers/ErrorCodeConverter.cs +++ b/src/Files.App/Utils/Storage/Helpers/ErrorCodeConverter.cs @@ -1,10 +1,21 @@ // Copyright (c) Files Community // Licensed under the MIT License. +using Files.Core.Storage.Enums; + namespace Files.App.Utils.Storage { public static class ErrorCodeConverter { + public static FileSystemStatusCode ToFileSystemStatusCode(this StorableStatus status) => status switch + { + StorableStatus.Success => FileSystemStatusCode.Success, + StorableStatus.NotFound => FileSystemStatusCode.NotFound, + StorableStatus.AccessDenied => FileSystemStatusCode.Unauthorized, + StorableStatus.NotMine or StorableStatus.Error => FileSystemStatusCode.Generic, + _ => FileSystemStatusCode.Generic, + }; + public static ReturnResult ToStatus(this FileSystemStatusCode errorCode) { switch (errorCode) diff --git a/src/Files.App/Utils/Storage/Helpers/FilePropertiesHelpers.cs b/src/Files.App/Utils/Storage/Helpers/FilePropertiesHelpers.cs index e11f3ed1a156..8c865d511b94 100644 --- a/src/Files.App/Utils/Storage/Helpers/FilePropertiesHelpers.cs +++ b/src/Files.App/Utils/Storage/Helpers/FilePropertiesHelpers.cs @@ -9,7 +9,9 @@ using Microsoft.UI.Xaml.Media.Animation; using System.Collections.Concurrent; using Windows.Graphics; +#if WINDOWS using Windows.Win32; +#endif namespace Files.App.Utils.Storage { @@ -135,8 +137,10 @@ public static void OpenPropertiesWindow(object item, IShellPage associatedInstan // WINUI3: Move window to cursor position (the window manager places the window on Linux) var pointerPosition = default(System.Drawing.Point); +#if WINDOWS if (!OperatingSystem.IsLinux()) PInvoke.GetCursorPos(out pointerPosition); +#endif // Null when no display is available, e.g. while monitors are detached var displayArea = OperatingSystem.IsLinux() diff --git a/src/Files.App/Utils/Storage/Helpers/FileTypesHelper.cs b/src/Files.App/Utils/Storage/Helpers/FileTypesHelper.cs index f20ea01fda1b..ec35c9171723 100644 --- a/src/Files.App/Utils/Storage/Helpers/FileTypesHelper.cs +++ b/src/Files.App/Utils/Storage/Helpers/FileTypesHelper.cs @@ -2,9 +2,11 @@ // SPDX-License-Identifier: MPL-2.0 using System.Collections.Concurrent; +#if WINDOWS using Windows.Win32; using Windows.Win32.Storage.FileSystem; using Windows.Win32.UI.Shell; +#endif namespace Files.App.Utils.Storage { @@ -25,6 +27,7 @@ public static unsafe string GetLocalizedTypeName(string? extension) if (!OperatingSystem.IsWindows()) return string.Empty; +#if WINDOWS if (typeNameCache.TryGetValue(extension, out var cached)) return cached; @@ -47,6 +50,9 @@ public static unsafe string GetLocalizedTypeName(string? extension) typeNameCache[extension] = typeName; return typeName; +#else + return string.Empty; +#endif } } } diff --git a/src/Files.App/Utils/Storage/Helpers/FilesystemResult.cs b/src/Files.App/Utils/Storage/Helpers/FilesystemResult.cs index c6a0eb1dfdc8..e0550dbbefbf 100644 --- a/src/Files.App/Utils/Storage/Helpers/FilesystemResult.cs +++ b/src/Files.App/Utils/Storage/Helpers/FilesystemResult.cs @@ -1,7 +1,9 @@ // Copyright (c) Files Community // Licensed under the MIT License. +#if WINDOWS using Windows.Win32.Foundation; +#endif namespace Files.App.Utils.Storage { @@ -18,8 +20,10 @@ public class FilesystemResult public static explicit operator FilesystemResult(bool res) => new(res ? FileSystemStatusCode.Success : FileSystemStatusCode.Generic); +#if WINDOWS public static implicit operator BOOL(FilesystemResult? res) => res?.ErrorCode is FileSystemStatusCode.Success; public static explicit operator FilesystemResult(BOOL res) => new(res ? FileSystemStatusCode.Success : FileSystemStatusCode.Generic); +#endif } public sealed class FilesystemResult : FilesystemResult diff --git a/src/Files.App/Utils/Storage/Helpers/FolderHelpers.cs b/src/Files.App/Utils/Storage/Helpers/FolderHelpers.cs index d0de8edac9b6..6dfe0e8e89bc 100644 --- a/src/Files.App/Utils/Storage/Helpers/FolderHelpers.cs +++ b/src/Files.App/Utils/Storage/Helpers/FolderHelpers.cs @@ -2,8 +2,12 @@ // Licensed under the MIT License. using System.IO; +#if WINDOWS using Windows.Win32; +#endif +#if WINDOWS using Windows.Win32.Storage.FileSystem; +#endif namespace Files.App.Utils.Storage { diff --git a/src/Files.App/Utils/Storage/Helpers/StorageFileExtensions.Linux.cs b/src/Files.App/Utils/Storage/Helpers/StorageFileExtensions.Linux.cs new file mode 100644 index 000000000000..ee289c28dcdc --- /dev/null +++ b/src/Files.App/Utils/Storage/Helpers/StorageFileExtensions.Linux.cs @@ -0,0 +1,94 @@ +// Copyright (c) Files Community +// Licensed under the MIT License. + +#if !WINDOWS +using System.IO; +using Windows.Storage; + +namespace Files.App.Utils.Storage +{ + public static partial class StorageFileExtensions + { + // LINUX-TODO(storage): retain legacy signatures until P4-B/E/G/H migrate to the resolver. + public static BaseStorageFile? AsBaseStorageFile(this IStorageItem? item) => item as BaseStorageFile; + public static BaseStorageFolder? AsBaseStorageFolder(this IStorageItem? item) => item as BaseStorageFolder; + + public static Task> ToStandardStorageItemsAsync(this IEnumerable items) + => Task.FromResult(items.ToList()); + + public static async Task DangerousGetFileFromPathAsync(string value, StorageFolderWithPath? rootFolder = null, StorageFolderWithPath? parentFolder = null) + { + var path = ResolveLegacyPath(value, parentFolder); + var result = await StorageHelpers.GetFileAsync(path); + ThrowLegacyResolutionError(result.ErrorCode, path); + return await OpenLegacyFileAsync(path); + } + + public static async Task DangerousGetFileWithPathFromPathAsync(string value, StorageFolderWithPath? rootFolder = null, StorageFolderWithPath? parentFolder = null) + { + var path = ResolveLegacyPath(value, parentFolder); + ThrowLegacyResolutionError((await StorageHelpers.GetFileAsync(path)).ErrorCode, path); + return new(await OpenLegacyFileAsync(path), path); + } + + public static async Task DangerousGetFolderFromPathAsync(string value, StorageFolderWithPath? rootFolder = null, StorageFolderWithPath? parentFolder = null) + { + var path = ResolveLegacyPath(value, parentFolder); + var result = await StorageHelpers.GetFolderAsync(path); + ThrowLegacyResolutionError(result.ErrorCode, path); + return await OpenLegacyFolderAsync(path); + } + + public static async Task DangerousGetFolderWithPathFromPathAsync(string value, StorageFolderWithPath? rootFolder = null, StorageFolderWithPath? parentFolder = null) + { + var path = ResolveLegacyPath(value, parentFolder); + ThrowLegacyResolutionError((await StorageHelpers.GetFolderAsync(path)).ErrorCode, path); + return new(await OpenLegacyFolderAsync(path), path); + } + + /// Opens a path the resolver already accepted as a legacy item; null when no legacy type can represent it (e.g. archive members). + internal static async Task OpenLegacyFileAsync(string path) + { + // Uno does not check existence or type, so only local regular files qualify + if (!Path.IsPathRooted(path) || !File.Exists(path)) + return null; + try { return await BaseStorageFile.GetFileFromPathAsync(path); } + catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or ArgumentException or NotSupportedException) { return null; } + } + + internal static async Task OpenLegacyFolderAsync(string path) + { + if (!Path.IsPathRooted(path) || !Directory.Exists(path)) + return null; + try { return await BaseStorageFolder.GetFolderFromPathAsync(path); } + catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or ArgumentException or NotSupportedException) { return null; } + } + + private static string ResolveLegacyPath(string path, StorageFolderWithPath? parentFolder) + => parentFolder is not null && !Path.IsPathRooted(path) && !FtpHelpers.IsFtpPath(path) + ? Path.GetFullPath(Path.Combine(parentFolder.Path, path)) : path; + + private static void ThrowLegacyResolutionError(FileSystemStatusCode status, string path) + { + if (status is FileSystemStatusCode.Success) + return; + if (status is FileSystemStatusCode.NotFound) + throw new FileNotFoundException("The storage item was not found.", path); + if (status is FileSystemStatusCode.Unauthorized) + throw new UnauthorizedAccessException("The storage item cannot be accessed."); + if (status is FileSystemStatusCode.NotAFile or FileSystemStatusCode.NotAFolder) + throw new ArgumentException("The storage item has a different kind.", nameof(path)); + throw new NotSupportedException("Use IStorableResolver instead of the legacy WinRT storage layer."); + } + + public static Task> GetFilesWithPathAsync(this StorageFolderWithPath parentFolder, uint maxNumberOfItems = uint.MaxValue) + => throw new NotSupportedException("Use IFolder.GetItemsAsync instead of the legacy WinRT storage layer."); + + public static Task> GetFoldersWithPathAsync(this StorageFolderWithPath parentFolder, uint maxNumberOfItems = uint.MaxValue) + => throw new NotSupportedException("Use IFolder.GetItemsAsync instead of the legacy WinRT storage layer."); + + public static Task?> GetFoldersWithPathAsync(this StorageFolderWithPath parentFolder, string nameFilter, uint maxNumberOfItems = uint.MaxValue) + => throw new NotSupportedException("Use IFolder.GetItemsAsync instead of the legacy WinRT storage layer."); + } +} +#endif diff --git a/src/Files.App/Utils/Storage/Helpers/StorageFileExtensions.Windows.cs b/src/Files.App/Utils/Storage/Helpers/StorageFileExtensions.Windows.cs new file mode 100644 index 000000000000..e430fe26d8a1 --- /dev/null +++ b/src/Files.App/Utils/Storage/Helpers/StorageFileExtensions.Windows.cs @@ -0,0 +1,188 @@ +// Copyright (c) Files Community +// Licensed under the MIT License. + +using System.IO; +using Windows.Storage; +using Windows.Storage.Search; +using WinRT; + +namespace Files.App.Utils.Storage +{ + public static partial class StorageFileExtensions + { + [DynamicWindowsRuntimeCast(typeof(StorageFile))] + public static BaseStorageFile? AsBaseStorageFile(this IStorageItem? item) + { + if (item is null || !item.IsOfType(StorageItemTypes.File)) + return null; + + return item is StorageFile file ? new SystemStorageFile(file) : item as BaseStorageFile; + } + + public static async Task> ToStandardStorageItemsAsync(this IEnumerable items) + { + var newItems = new List(); + foreach (var item in items) + { + try + { + if (item is null) + { + } + else if (item.IsOfType(StorageItemTypes.File)) + { + newItems.Add(await item.AsBaseStorageFile()!.ToStorageFileAsync()); + } + else if (item.IsOfType(StorageItemTypes.Folder)) + { + newItems.Add(await item.AsBaseStorageFolder()!.ToStorageFolderAsync()); + } + } + catch (NotSupportedException) + { + // Ignore items that can't be converted + } + } + return newItems; + } + + [DynamicWindowsRuntimeCast(typeof(StorageFolder))] + public static BaseStorageFolder? AsBaseStorageFolder(this IStorageItem? item) + { + if (item is not null && item.IsOfType(StorageItemTypes.Folder)) + return item is StorageFolder folder ? new SystemStorageFolder(folder) : item as BaseStorageFolder; + + return null; + } + + public async static Task DangerousGetFileFromPathAsync + (string value, StorageFolderWithPath? rootFolder = null, StorageFolderWithPath? parentFolder = null) + => (await DangerousGetFileWithPathFromPathAsync(value, rootFolder, parentFolder)).Item; + public async static Task DangerousGetFileWithPathFromPathAsync + (string value, StorageFolderWithPath? rootFolder = null, StorageFolderWithPath? parentFolder = null) + { + if (rootFolder is not null) + { + var rootItem = rootFolder.Item!; + var currComponents = GetDirectoryPathComponents(value); + + if (parentFolder is not null && value.IsSubPathOf(parentFolder.Path)) + { + var folder = parentFolder.Item!; + var prevComponents = GetDirectoryPathComponents(parentFolder.Path); + var path = parentFolder.Path; + foreach (var component in currComponents.ExceptBy(prevComponents, c => c.Path).SkipLast(1)) + { + folder = await folder.GetFolderAsync(component.Title ?? throw new InvalidOperationException("A path component is missing its title.")); + path = PathNormalization.Combine(path, folder.Name); + } + var file = await folder.GetFileAsync(currComponents.Last().Title ?? throw new InvalidOperationException("A path component is missing its title.")); + path = PathNormalization.Combine(path, file.Name); + return new StorageFileWithPath(file, path); + } + else if (value.IsSubPathOf(rootFolder.Path)) + { + var folder = rootItem; + var path = rootFolder.Path; + foreach (var component in currComponents.Skip(1).SkipLast(1)) + { + folder = await folder.GetFolderAsync(component.Title ?? throw new InvalidOperationException("A path component is missing its title.")); + path = PathNormalization.Combine(path, folder.Name); + } + var file = await folder.GetFileAsync(currComponents.Last().Title ?? throw new InvalidOperationException("A path component is missing its title.")); + path = PathNormalization.Combine(path, file.Name); + return new StorageFileWithPath(file, path); + } + } + + 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) + => (await parentFolder.Item!.GetFilesAsync(CommonFileQuery.DefaultQuery, 0, maxNumberOfItems)) + .Select(x => new StorageFileWithPath(x, string.IsNullOrEmpty(x.Path) ? PathNormalization.Combine(parentFolder.Path, x.Name) : x.Path)).ToList(); + + public async static Task DangerousGetFolderFromPathAsync + (string value, StorageFolderWithPath? rootFolder = null, StorageFolderWithPath? parentFolder = null) + => (await DangerousGetFolderWithPathFromPathAsync(value, rootFolder, parentFolder)).Item; + public async static Task DangerousGetFolderWithPathFromPathAsync + (string value, StorageFolderWithPath? rootFolder = null, StorageFolderWithPath? parentFolder = null) + { + // Archive paths can't be resolved by chaining WinRT GetFolderAsync from a network root/parent + // (an archive is not a real subfolder of the share); resolve them directly like local archives. + if (rootFolder is not null && !ZipStorageFolder.IsZipPath(value)) + { + var rootItem = rootFolder.Item!; + var currComponents = GetDirectoryPathComponents(value); + + if (rootFolder.Path == value) + { + return rootFolder; + } + else if (parentFolder is not null && value.IsSubPathOf(parentFolder.Path)) + { + var folder = parentFolder.Item!; + var prevComponents = GetDirectoryPathComponents(parentFolder.Path); + var path = parentFolder.Path; + foreach (var component in currComponents.ExceptBy(prevComponents, c => c.Path)) + { + folder = await folder.GetFolderAsync(component.Title ?? throw new InvalidOperationException("A path component is missing its title.")); + path = PathNormalization.Combine(path, folder.Name); + } + return new StorageFolderWithPath(folder, path); + } + else if (value.IsSubPathOf(rootFolder.Path)) + { + var folder = rootItem; + var path = rootFolder.Path; + foreach (var component in currComponents.Skip(1)) + { + folder = await folder.GetFolderAsync(component.Title ?? throw new InvalidOperationException("A path component is missing its title.")); + path = PathNormalization.Combine(path, folder.Name); + } + return new StorageFolderWithPath(folder, path); + } + } + + 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) + => (await parentFolder.Item!.GetFoldersAsync(CommonFolderQuery.DefaultQuery, 0, maxNumberOfItems)) + .Select(x => new StorageFolderWithPath(x, string.IsNullOrEmpty(x.Path) ? PathNormalization.Combine(parentFolder.Path, x.Name) : x.Path)).ToList(); + public async static Task?> GetFoldersWithPathAsync + (this StorageFolderWithPath parentFolder, string nameFilter, uint maxNumberOfItems = uint.MaxValue) + { + if (parentFolder is null) + return null; + + var folder = parentFolder.Item!; + + var queryOptions = new QueryOptions + { + ApplicationSearchFilter = $"System.FileName:{nameFilter}*" + }; + BaseStorageFolderQueryResult queryResult = folder.CreateFolderQueryWithOptions(queryOptions); + + return (await queryResult.GetFoldersAsync(0, maxNumberOfItems)) + .Select(x => new StorageFolderWithPath(x, string.IsNullOrEmpty(x.Path) ? PathNormalization.Combine(parentFolder.Path, x.Name) : x.Path)).ToList(); + } + + } +} diff --git a/src/Files.App/Utils/Storage/Helpers/StorageFileExtensions.cs b/src/Files.App/Utils/Storage/Helpers/StorageFileExtensions.cs index 16e1e96cab24..3956695ee652 100644 --- a/src/Files.App/Utils/Storage/Helpers/StorageFileExtensions.cs +++ b/src/Files.App/Utils/Storage/Helpers/StorageFileExtensions.cs @@ -5,12 +5,10 @@ using System.IO; using System.Text; using Windows.Storage; -using Windows.Storage.Search; -using WinRT; namespace Files.App.Utils.Storage { - public static class StorageFileExtensions + public static partial class StorageFileExtensions { private const int SINGLE_DOT_DIRECTORY_LENGTH = 2; private const int DOUBLE_DOT_DIRECTORY_LENGTH = 3; @@ -18,42 +16,6 @@ public static class StorageFileExtensions public static readonly ImmutableHashSet _ftpPaths = new HashSet() { "ftp:/", "ftps:/", "ftpes:/" }.ToImmutableHashSet(); - [DynamicWindowsRuntimeCast(typeof(StorageFile))] - public static BaseStorageFile? AsBaseStorageFile(this IStorageItem? item) - { - if (item is null || !item.IsOfType(StorageItemTypes.File)) - return null; - - return item is StorageFile file ? new SystemStorageFile(file) : item as BaseStorageFile; - } - - public static async Task> ToStandardStorageItemsAsync(this IEnumerable items) - { - var newItems = new List(); - foreach (var item in items) - { - try - { - if (item is null) - { - } - else if (item.IsOfType(StorageItemTypes.File)) - { - newItems.Add(await item.AsBaseStorageFile()!.ToStorageFileAsync()); - } - else if (item.IsOfType(StorageItemTypes.Folder)) - { - newItems.Add(await item.AsBaseStorageFolder()!.ToStorageFolderAsync()); - } - } - catch (NotSupportedException) - { - // Ignore items that can't be converted - } - } - return newItems; - } - public static bool AreItemsInSameDrive(this IEnumerable itemsPath, string? destinationPath) { try @@ -129,15 +91,6 @@ private static bool IsSamePathOrAncestor(string? trimmedItemPath, string trimmed trimmedDestinationPath[trimmedItemPath.Length] == Path.AltDirectorySeparatorChar); } - [DynamicWindowsRuntimeCast(typeof(StorageFolder))] - public static BaseStorageFolder? AsBaseStorageFolder(this IStorageItem? item) - { - if (item is not null && item.IsOfType(StorageItemTypes.Folder)) - return item is StorageFolder folder ? new SystemStorageFolder(folder) : item as BaseStorageFolder; - - return null; - } - public static List GetDirectoryPathComponents(string value) { List pathBoxItems = []; @@ -204,11 +157,19 @@ public static async Task> GetDirectoryPathComponentsWithDispla } else { +#if WINDOWS var path = item.Path!; BaseStorageFolder? folder = await FilesystemTasks.WrapNullable(() => DangerousGetFolderFromPathAsync(path)); if (!string.IsNullOrEmpty(folder?.DisplayName)) item.Title = folder.DisplayName; +#else + var path = item.Path!; + var folder = (await StorageHelpers.GetFolderAsync(path)).Result; + + if (!string.IsNullOrEmpty(folder?.Name)) + item.Title = folder.Name; +#endif } item.ChevronToolTip = string.Format(Strings.BreadcrumbBarChevronButtonToolTip.GetLocalizedResource(), item.Title); @@ -223,135 +184,6 @@ public static string GetResolvedPath(string path, bool isFtp) return ResolvePath(withoutEnvirnment, isFtp); } - public async static Task DangerousGetFileFromPathAsync - (string value, StorageFolderWithPath? rootFolder = null, StorageFolderWithPath? parentFolder = null) - => (await DangerousGetFileWithPathFromPathAsync(value, rootFolder, parentFolder)).Item; - public async static Task DangerousGetFileWithPathFromPathAsync - (string value, StorageFolderWithPath? rootFolder = null, StorageFolderWithPath? parentFolder = null) - { - if (rootFolder is not null) - { - var rootItem = rootFolder.Item!; - var currComponents = GetDirectoryPathComponents(value); - - if (parentFolder is not null && value.IsSubPathOf(parentFolder.Path)) - { - var folder = parentFolder.Item!; - var prevComponents = GetDirectoryPathComponents(parentFolder.Path); - var path = parentFolder.Path; - foreach (var component in currComponents.ExceptBy(prevComponents, c => c.Path).SkipLast(1)) - { - folder = await folder.GetFolderAsync(component.Title ?? throw new InvalidOperationException("A path component is missing its title.")); - path = PathNormalization.Combine(path, folder.Name); - } - var file = await folder.GetFileAsync(currComponents.Last().Title ?? throw new InvalidOperationException("A path component is missing its title.")); - path = PathNormalization.Combine(path, file.Name); - return new StorageFileWithPath(file, path); - } - else if (value.IsSubPathOf(rootFolder.Path)) - { - var folder = rootItem; - var path = rootFolder.Path; - foreach (var component in currComponents.Skip(1).SkipLast(1)) - { - folder = await folder.GetFolderAsync(component.Title ?? throw new InvalidOperationException("A path component is missing its title.")); - path = PathNormalization.Combine(path, folder.Name); - } - var file = await folder.GetFileAsync(currComponents.Last().Title ?? throw new InvalidOperationException("A path component is missing its title.")); - path = PathNormalization.Combine(path, file.Name); - return new StorageFileWithPath(file, path); - } - } - - 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) - => (await parentFolder.Item!.GetFilesAsync(CommonFileQuery.DefaultQuery, 0, maxNumberOfItems)) - .Select(x => new StorageFileWithPath(x, string.IsNullOrEmpty(x.Path) ? PathNormalization.Combine(parentFolder.Path, x.Name) : x.Path)).ToList(); - - public async static Task DangerousGetFolderFromPathAsync - (string value, StorageFolderWithPath? rootFolder = null, StorageFolderWithPath? parentFolder = null) - => (await DangerousGetFolderWithPathFromPathAsync(value, rootFolder, parentFolder)).Item; - public async static Task DangerousGetFolderWithPathFromPathAsync - (string value, StorageFolderWithPath? rootFolder = null, StorageFolderWithPath? parentFolder = null) - { - // Archive paths can't be resolved by chaining WinRT GetFolderAsync from a network root/parent - // (an archive is not a real subfolder of the share); resolve them directly like local archives. - if (rootFolder is not null && !ZipStorageFolder.IsZipPath(value)) - { - var rootItem = rootFolder.Item!; - var currComponents = GetDirectoryPathComponents(value); - - if (rootFolder.Path == value) - { - return rootFolder; - } - else if (parentFolder is not null && value.IsSubPathOf(parentFolder.Path)) - { - var folder = parentFolder.Item!; - var prevComponents = GetDirectoryPathComponents(parentFolder.Path); - var path = parentFolder.Path; - foreach (var component in currComponents.ExceptBy(prevComponents, c => c.Path)) - { - folder = await folder.GetFolderAsync(component.Title ?? throw new InvalidOperationException("A path component is missing its title.")); - path = PathNormalization.Combine(path, folder.Name); - } - return new StorageFolderWithPath(folder, path); - } - else if (value.IsSubPathOf(rootFolder.Path)) - { - var folder = rootItem; - var path = rootFolder.Path; - foreach (var component in currComponents.Skip(1)) - { - folder = await folder.GetFolderAsync(component.Title ?? throw new InvalidOperationException("A path component is missing its title.")); - path = PathNormalization.Combine(path, folder.Name); - } - return new StorageFolderWithPath(folder, path); - } - } - - 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) - => (await parentFolder.Item!.GetFoldersAsync(CommonFolderQuery.DefaultQuery, 0, maxNumberOfItems)) - .Select(x => new StorageFolderWithPath(x, string.IsNullOrEmpty(x.Path) ? PathNormalization.Combine(parentFolder.Path, x.Name) : x.Path)).ToList(); - public async static Task?> GetFoldersWithPathAsync - (this StorageFolderWithPath parentFolder, string nameFilter, uint maxNumberOfItems = uint.MaxValue) - { - if (parentFolder is null) - return null; - - var folder = parentFolder.Item!; - - var queryOptions = new QueryOptions - { - ApplicationSearchFilter = $"System.FileName:{nameFilter}*" - }; - BaseStorageFolderQueryResult queryResult = folder.CreateFolderQueryWithOptions(queryOptions); - - return (await queryResult.GetFoldersAsync(0, maxNumberOfItems)) - .Select(x => new StorageFolderWithPath(x, string.IsNullOrEmpty(x.Path) ? PathNormalization.Combine(parentFolder.Path, x.Name) : x.Path)).ToList(); - } - private static PathBoxItem GetPathItem(string component, string path) { var title = string.Empty; @@ -419,8 +251,10 @@ private static string ResolvePath(string path, bool isFtp) if (path.StartsWith("Settings")) return "Settings"; +#if WINDOWS if (ShellStorageFolder.IsShellPath(path)) return ShellHelpers.ResolveShellPath(path); +#endif var pathBuilder = new StringBuilder(path); var lastPathIndex = path.Length - 1; diff --git a/src/Files.App/Utils/Storage/Helpers/StorageHelpers.Storables.cs b/src/Files.App/Utils/Storage/Helpers/StorageHelpers.Storables.cs new file mode 100644 index 000000000000..98a8b481cb2c --- /dev/null +++ b/src/Files.App/Utils/Storage/Helpers/StorageHelpers.Storables.cs @@ -0,0 +1,42 @@ +// Copyright (c) Files Community +// Licensed under the MIT License. + +using Files.Core.Storage.Contracts; + +namespace Files.App.Helpers +{ + public static partial class StorageHelpers + { + public static async Task> GetStorableAsync(string path, CancellationToken cancellationToken = default) + { + var result = await Ioc.Default.GetRequiredService().TryGetAsync(path, cancellationToken); + return new(result.Item, result.Status.ToFileSystemStatusCode()); + } + + public static async Task> GetFileAsync(string path, CancellationToken cancellationToken = default) + { + var result = await GetStorableAsync(path, cancellationToken); + return new(result.Result as IFile, result && result.Result is not IFile ? FileSystemStatusCode.NotAFile : result.ErrorCode); + } + + public static async Task> GetFolderAsync(string path, CancellationToken cancellationToken = default) + { + var result = await GetStorableAsync(path, cancellationToken); + return new(result.Result as IFolder, result && result.Result is not IFolder ? FileSystemStatusCode.NotAFolder : result.ErrorCode); + } + + public static async Task> ToStorableResult(this IStorageItemWithPath item, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + var result = item.Storable is { } storable + ? new FilesystemResult(storable, FileSystemStatusCode.Success) + : await GetStorableAsync(item.Path, cancellationToken); + if (result && item.ItemType is FilesystemItemType.File && result.Result is not IFile) + return new(null, FileSystemStatusCode.NotAFile); + if (result && item.ItemType is FilesystemItemType.Directory or FilesystemItemType.Library && result.Result is not IFolder) + return new(null, FileSystemStatusCode.NotAFolder); + return result; + } + + } +} diff --git a/src/Files.App/Utils/Storage/Helpers/StorageHelpers.Windows.cs b/src/Files.App/Utils/Storage/Helpers/StorageHelpers.Windows.cs new file mode 100644 index 000000000000..2db99bf26d57 --- /dev/null +++ b/src/Files.App/Utils/Storage/Helpers/StorageHelpers.Windows.cs @@ -0,0 +1,206 @@ +// Copyright (c) Files Community +// Licensed under the MIT License. + +using Files.Shared.Helpers; +using System.Runtime.InteropServices; +using Windows.Storage; +using Windows.Storage.FileProperties; +using Windows.Win32; +using Windows.Win32.Storage.FileSystem; + +namespace Files.App.Helpers +{ + /// + /// related Helpers + /// + public static partial class StorageHelpers + { + public static async Task ToStorageItem(this IStorageItemWithPath item) + { + return (await item.ToStorageItemResult()).Result; + } + + public static async Task ToStorageItem(string path) where TRequested : IStorageItem + { + FilesystemResult? file = null; + FilesystemResult? folder = null; + + if (FileExtensionHelpers.IsShortcutOrUrlFile(path)) + { + // TODO: In the future, when IStorageItemWithPath will inherit from IStorageItem, + // we could implement this code here for getting .lnk files + // for now, we can't + return default; + } + + // Fast get attributes +#if WINDOWS + uint itemAttributes = PInvoke.GetFileAttributes(path); + bool exists = itemAttributes != PInvoke.INVALID_FILE_ATTRIBUTES; + bool isDirectory = exists && (itemAttributes & (uint)FILE_FLAGS_AND_ATTRIBUTES.FILE_ATTRIBUTE_DIRECTORY) != 0; +#else + bool isDirectory = System.IO.Directory.Exists(path); + bool exists = isDirectory || System.IO.File.Exists(path); +#endif + if (exists) // Exists on local storage + { + // Directory + if (isDirectory) + { + if (typeof(IStorageFile).IsAssignableFrom(typeof(TRequested))) // Wanted file + { + // NotAFile + return default; + } + else // Just get the directory + { + await GetFolderAsync(); + } + } + else // File + { + if (typeof(IStorageFolder).IsAssignableFrom(typeof(TRequested))) // Wanted directory + { + // NotAFile + return default; + } + else // Just get the file + { + await GetFileAsync(); + } + } + } + else // Does not exist or is not present on local storage + { + Debug.WriteLine($"Path does not exist. Trying to find storage item manually (HRESULT: {Marshal.GetLastWin32Error()})"); + + if (typeof(IStorageFile).IsAssignableFrom(typeof(TRequested))) + { + await GetFileAsync(); + } + else if (typeof(IStorageFolder).IsAssignableFrom(typeof(TRequested))) + { + await GetFolderAsync(); + } + else if (typeof(IStorageItem).IsAssignableFrom(typeof(TRequested))) + { + if (System.IO.Path.HasExtension(path)) // Possibly a file + { + await GetFileAsync(); + } + + if (file?.Result is null) // Possibly a folder + { + await GetFolderAsync(); + + if (file is null && folder?.Result is null) + { + // Try file because it wasn't checked + await GetFileAsync(); + } + } + } + } + + if (file?.Result is TRequested requestedFile) + { + return requestedFile; + } + else if (folder?.Result is TRequested requestedFolder) + { + return requestedFolder; + } + + return default; + + // Extensions + + async Task GetFileAsync() + { + var rootItem = await FilesystemTasks.WrapNullable(() => DriveHelpers.GetRootFromPathAsync(path)); + file = await FilesystemTasks.WrapNullable(() => StorageFileExtensions.DangerousGetFileFromPathAsync(path, rootItem.Result)); + } + + async Task GetFolderAsync() + { + var rootItem = await FilesystemTasks.WrapNullable(() => DriveHelpers.GetRootFromPathAsync(path)); + folder = await FilesystemTasks.WrapNullable(() => StorageFileExtensions.DangerousGetFolderFromPathAsync(path, rootItem.Result)); + } + } + + public static async Task GetFileSize(this IStorageFile file) + { + BasicProperties properties = await file.GetBasicPropertiesAsync(); + return (long)properties.Size; + } + + public static async Task> ToStorageItemResult(this IStorageItemWithPath item) + { + var returnedItem = new FilesystemResult(null, FileSystemStatusCode.Generic); + var rootItem = await FilesystemTasks.WrapNullable(() => DriveHelpers.GetRootFromPathAsync(item.Path)); + if (!string.IsNullOrEmpty(item.Path)) + { + returnedItem = (item.ItemType == FilesystemItemType.File) ? + ToType( + await FilesystemTasks.WrapNullable(() => StorageFileExtensions.DangerousGetFileFromPathAsync(item.Path, rootItem))) : + ToType( + await FilesystemTasks.WrapNullable(() => 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; + } + + public static IStorageItemWithPath FromPathAndType(string customPath, FilesystemItemType? itemType) + { + return (itemType == FilesystemItemType.File) ? + new StorageFileWithPath(null, customPath) : + new StorageFolderWithPath(null, customPath); + } + + public static async Task GetTypeFromPath(string path) + { + IStorageItem? item = await ToStorageItem(path); + + return item is null ? FilesystemItemType.File : (item.IsOfType(StorageItemTypes.Folder) ? FilesystemItemType.Directory : FilesystemItemType.File); + } + + public static bool Exists(string path) + { + if (OperatingSystem.IsLinux()) + return System.IO.Directory.Exists(path) || System.IO.File.Exists(path); + + return PInvoke.GetFileAttributes(path) != PInvoke.INVALID_FILE_ATTRIBUTES; + } + + public static IStorageItemWithPath? FromStorageItem(this IStorageItem? item, string? customPath = null, FilesystemItemType? itemType = null) + { + if (item is null) + { + return FromPathAndType(customPath + ?? throw new InvalidOperationException("A path is required when converting a missing storage item."), itemType); + } + else if (item.IsOfType(StorageItemTypes.File)) + { + return new StorageFileWithPath(item.AsBaseStorageFile(), GetPath(item, customPath)); + } + else if (item.IsOfType(StorageItemTypes.Folder)) + { + return new StorageFolderWithPath(item.AsBaseStorageFolder(), GetPath(item, customPath)); + } + return null; + + static string GetPath(IStorageItem item, string? customPath) + => !string.IsNullOrEmpty(item.Path) + ? item.Path + : customPath ?? string.Empty; + } + + public static FilesystemResult ToType(FilesystemResult result) where T : class + { + return new FilesystemResult(result.Result as T, result.ErrorCode); + } + } +} diff --git a/src/Files.App/Utils/Storage/Helpers/StorageHelpers.cs b/src/Files.App/Utils/Storage/Helpers/StorageHelpers.cs index 940b76b4af81..d89a5751f889 100644 --- a/src/Files.App/Utils/Storage/Helpers/StorageHelpers.cs +++ b/src/Files.App/Utils/Storage/Helpers/StorageHelpers.cs @@ -1,206 +1,62 @@ // Copyright (c) Files Community // Licensed under the MIT License. -using Files.Shared.Helpers; -using System.Runtime.InteropServices; +using Files.Core.Storage.Contracts; using Windows.Storage; -using Windows.Storage.FileProperties; -using Windows.Win32; -using Windows.Win32.Storage.FileSystem; namespace Files.App.Helpers { - /// - /// related Helpers - /// - public static class StorageHelpers + public static partial class StorageHelpers { +#if !WINDOWS + // LINUX-TODO(storage): remove these WinRT entry points as downstream work packages adopt ToStorableResult. public static async Task ToStorageItem(this IStorageItemWithPath item) - { - return (await item.ToStorageItemResult()).Result; - } + => (await item.ToStorageItemResult()).Result; public static async Task ToStorageItem(string path) where TRequested : IStorageItem { - FilesystemResult? file = null; - FilesystemResult? folder = null; - - if (FileExtensionHelpers.IsShortcutOrUrlFile(path)) - { - // TODO: In the future, when IStorageItemWithPath will inherit from IStorageItem, - // we could implement this code here for getting .lnk files - // for now, we can't + var result = await GetStorableAsync(path); + if (!result) return default; - } - - // Fast get attributes -#if WINDOWS - uint itemAttributes = PInvoke.GetFileAttributes(path); - bool exists = itemAttributes != PInvoke.INVALID_FILE_ATTRIBUTES; - bool isDirectory = exists && (itemAttributes & (uint)FILE_FLAGS_AND_ATTRIBUTES.FILE_ATTRIBUTE_DIRECTORY) != 0; -#else - bool isDirectory = System.IO.Directory.Exists(path); - bool exists = isDirectory || System.IO.File.Exists(path); -#endif - if (exists) // Exists on local storage - { - // Directory - if (isDirectory) - { - if (typeof(IStorageFile).IsAssignableFrom(typeof(TRequested))) // Wanted file - { - // NotAFile - return default; - } - else // Just get the directory - { - await GetFolderAsync(); - } - } - else // File - { - if (typeof(IStorageFolder).IsAssignableFrom(typeof(TRequested))) // Wanted directory - { - // NotAFile - return default; - } - else // Just get the file - { - await GetFileAsync(); - } - } - } - else // Does not exist or is not present on local storage - { - Debug.WriteLine($"Path does not exist. Trying to find storage item manually (HRESULT: {Marshal.GetLastWin32Error()})"); - - if (typeof(IStorageFile).IsAssignableFrom(typeof(TRequested))) - { - await GetFileAsync(); - } - else if (typeof(IStorageFolder).IsAssignableFrom(typeof(TRequested))) - { - await GetFolderAsync(); - } - else if (typeof(IStorageItem).IsAssignableFrom(typeof(TRequested))) - { - if (System.IO.Path.HasExtension(path)) // Possibly a file - { - await GetFileAsync(); - } - - if (file?.Result is null) // Possibly a folder - { - await GetFolderAsync(); - - if (file is null && folder?.Result is null) - { - // Try file because it wasn't checked - await GetFileAsync(); - } - } - } - } - - if (file?.Result is TRequested requestedFile) - { - return requestedFile; - } - else if (folder?.Result is TRequested requestedFolder) - { - return requestedFolder; - } - return default; - - // Extensions - - async Task GetFileAsync() - { - var rootItem = await FilesystemTasks.WrapNullable(() => DriveHelpers.GetRootFromPathAsync(path)); - file = await FilesystemTasks.WrapNullable(() => StorageFileExtensions.DangerousGetFileFromPathAsync(path, rootItem.Result)); - } - - async Task GetFolderAsync() - { - var rootItem = await FilesystemTasks.WrapNullable(() => DriveHelpers.GetRootFromPathAsync(path)); - folder = await FilesystemTasks.WrapNullable(() => StorageFileExtensions.DangerousGetFolderFromPathAsync(path, rootItem.Result)); - } - } - - public static async Task GetFileSize(this IStorageFile file) - { - BasicProperties properties = await file.GetBasicPropertiesAsync(); - return (long)properties.Size; + IStorageItem? item = result.Result is IFolder + ? await StorageFileExtensions.OpenLegacyFolderAsync(path) + : await StorageFileExtensions.OpenLegacyFileAsync(path); + return item is TRequested requested ? requested : default; } public static async Task> ToStorageItemResult(this IStorageItemWithPath item) { - var returnedItem = new FilesystemResult(null, FileSystemStatusCode.Generic); - var rootItem = await FilesystemTasks.WrapNullable(() => DriveHelpers.GetRootFromPathAsync(item.Path)); - if (!string.IsNullOrEmpty(item.Path)) - { - returnedItem = (item.ItemType == FilesystemItemType.File) ? - ToType( - await FilesystemTasks.WrapNullable(() => StorageFileExtensions.DangerousGetFileFromPathAsync(item.Path, rootItem))) : - ToType( - await FilesystemTasks.WrapNullable(() => 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; + var result = await item.ToStorableResult(); + if (!result) + return new(null, result.ErrorCode); + + IStorageItem? legacy = item.Item + ?? (result.Result is IFolder + ? await StorageFileExtensions.OpenLegacyFolderAsync(item.Path) + : await StorageFileExtensions.OpenLegacyFileAsync(item.Path)); + return new(legacy, legacy is null ? FileSystemStatusCode.Generic : FileSystemStatusCode.Success); } + public static async Task GetFileSize(this IStorageFile file) + => (long)(await file.GetBasicPropertiesAsync()).Size; + public static IStorageItemWithPath FromPathAndType(string customPath, FilesystemItemType? itemType) - { - return (itemType == FilesystemItemType.File) ? - new StorageFileWithPath(null, customPath) : - new StorageFolderWithPath(null, customPath); - } + => new StorableWithPath(customPath, itemType ?? FilesystemItemType.Directory); public static async Task GetTypeFromPath(string path) - { - IStorageItem? item = await ToStorageItem(path); - - return item is null ? FilesystemItemType.File : (item.IsOfType(StorageItemTypes.Folder) ? FilesystemItemType.Directory : FilesystemItemType.File); - } + => (await GetStorableAsync(path)).Result is IFolder ? FilesystemItemType.Directory : FilesystemItemType.File; public static bool Exists(string path) - { - if (OperatingSystem.IsLinux()) - return System.IO.Directory.Exists(path) || System.IO.File.Exists(path); - - return PInvoke.GetFileAttributes(path) != PInvoke.INVALID_FILE_ATTRIBUTES; - } + => System.IO.Directory.Exists(path) || System.IO.File.Exists(path); public static IStorageItemWithPath? FromStorageItem(this IStorageItem? item, string? customPath = null, FilesystemItemType? itemType = null) - { - if (item is null) - { - return FromPathAndType(customPath - ?? throw new InvalidOperationException("A path is required when converting a missing storage item."), itemType); - } - else if (item.IsOfType(StorageItemTypes.File)) - { - return new StorageFileWithPath(item.AsBaseStorageFile(), GetPath(item, customPath)); - } - else if (item.IsOfType(StorageItemTypes.Folder)) - { - return new StorageFolderWithPath(item.AsBaseStorageFolder(), GetPath(item, customPath)); - } - return null; - - static string GetPath(IStorageItem item, string? customPath) - => !string.IsNullOrEmpty(item.Path) - ? item.Path - : customPath ?? string.Empty; - } + => FromPathAndType(!string.IsNullOrEmpty(item?.Path) ? item.Path : customPath + ?? throw new InvalidOperationException("A path is required when converting a missing storage item."), + item is null ? itemType : item.IsOfType(StorageItemTypes.Folder) ? FilesystemItemType.Directory : FilesystemItemType.File); public static FilesystemResult ToType(FilesystemResult result) where T : class - { - return new FilesystemResult(result.Result as T, result.ErrorCode); - } + => new(result.Result as T, result.ErrorCode); +#endif } } diff --git a/src/Files.App/Utils/Storage/Operations/FilesystemOperations.Windows.cs b/src/Files.App/Utils/Storage/Operations/FilesystemOperations.Windows.cs index 8c6cce68274a..84349aa0f8fa 100644 --- a/src/Files.App/Utils/Storage/Operations/FilesystemOperations.Windows.cs +++ b/src/Files.App/Utils/Storage/Operations/FilesystemOperations.Windows.cs @@ -238,7 +238,7 @@ fsSourceFolder.Result is BaseStorageFolder sourceFolder && } else if (source.ItemType == FilesystemItemType.File) { - var fsResult = (FilesystemResult)await Task.Run(() => PInvoke.CopyFileFromApp(source.Path, destination, true)); + var fsResult = (FilesystemResult)(bool)await Task.Run(() => PInvoke.CopyFileFromApp(source.Path, destination, true)); if (!fsResult) { @@ -400,7 +400,7 @@ await DialogDisplayHelper.ShowDialogAsync( } else { - var fsResult = (FilesystemResult)await Task.Run(() => PInvoke.MoveFileFromApp(source.Path, destination)); + var fsResult = (FilesystemResult)(bool)await Task.Run(() => PInvoke.MoveFileFromApp(source.Path, destination)); if (!fsResult) { @@ -468,7 +468,7 @@ fsSourceFolder.Result is BaseStorageFolder sourceFolder && } else if (source.ItemType == FilesystemItemType.File) { - var fsResult = (FilesystemResult)await Task.Run(() => PInvoke.MoveFileFromApp(source.Path, destination)); + var fsResult = (FilesystemResult)(bool)await Task.Run(() => PInvoke.MoveFileFromApp(source.Path, destination)); if (!fsResult) { @@ -553,7 +553,7 @@ sourceResult.Result is BaseStorageFile file && if (permanently) { - fsResult = (FilesystemResult)PInvoke.DeleteFileFromApp(source.Path); + fsResult = (FilesystemResult)(bool)PInvoke.DeleteFileFromApp(source.Path); } if (!fsResult) { @@ -801,7 +801,7 @@ await storageHistory.SelectMany(item => item.Source).ToListAsync(), FilesystemResult fsResult = FileSystemStatusCode.InProgress; - fsResult = (FilesystemResult)await Task.Run(() => PInvoke.MoveFileFromApp(source.Path, destination)); + fsResult = (FilesystemResult)(bool)await Task.Run(() => PInvoke.MoveFileFromApp(source.Path, destination)); if (!fsResult) { diff --git a/src/Files.App/Utils/Storage/Search/FolderSearch.Linux.cs b/src/Files.App/Utils/Storage/Search/FolderSearch.Linux.cs index 965f72f50afb..543bf6b69b4d 100644 --- a/src/Files.App/Utils/Storage/Search/FolderSearch.Linux.cs +++ b/src/Files.App/Utils/Storage/Search/FolderSearch.Linux.cs @@ -1,6 +1,7 @@ // Copyright (c) Files Community // Licensed under the MIT License. +#if !WINDOWS using Files.Platform.Abstractions.Search; using Microsoft.Extensions.Logging; using System.IO; @@ -35,6 +36,9 @@ public static (string Pattern, bool Content) ParseLinuxQuery(string? query) return (text, false); } + private Task AddItemsAsync(string folder, IList results, CancellationToken token) + => SearchLinuxAsync(folder, results, token); + private async Task SearchLinuxAsync(string folder, IList results, CancellationToken token) { if (IsTagQuery(Query ?? string.Empty)) @@ -178,3 +182,4 @@ private static ListedItem CreateLinuxItem(string path, bool isDirectory, long le } } } +#endif diff --git a/src/Files.App/Utils/Storage/Search/FolderSearch.Windows.cs b/src/Files.App/Utils/Storage/Search/FolderSearch.Windows.cs new file mode 100644 index 000000000000..fbec1b502eb3 --- /dev/null +++ b/src/Files.App/Utils/Storage/Search/FolderSearch.Windows.cs @@ -0,0 +1,563 @@ +// Copyright (c) Files Community +// Licensed under the MIT License. + +using Files.Shared.Helpers; +using Microsoft.Extensions.Logging; +using System.IO; +using System.IO.Enumeration; +using System.Text.RegularExpressions; +using Windows.Storage; +using Windows.Storage.FileProperties; +using Windows.Storage.Search; +using Windows.Win32; +using Windows.Win32.Foundation; +using Windows.Win32.Storage.FileSystem; +using FileAttributes = System.IO.FileAttributes; + +namespace Files.App.Utils.Storage +{ + public sealed partial class FolderSearch + { + private async Task SearchAsync(BaseStorageFolder folder, IList results, CancellationToken token) + { + uint index = 0; + var stepSize = Math.Min(defaultStepSize, GetRemainingItemCount(results)); + if (stepSize == 0) + return; + + var options = ToQueryOptions(); + + var queryResult = folder.CreateItemQueryWithOptions(options); + var items = await queryResult.GetItemsAsync(0, stepSize).AsTask(token); + + while (items.Count > 0) + { + foreach (IStorageItem item in items) + { + if (token.IsCancellationRequested) + { + return; + } + + try + { + if (!item.Name.StartsWith('.') || UserSettingsService.FoldersSettingsService.ShowDotFiles) + { + var listedItem = await GetListedItemAsync(item); + AddResult(results, listedItem, token); + if (listedItem.ItemPath is not null) + indexedResultPaths.Add(listedItem.ItemPath); + } + } + catch (Exception ex) + { + App.Logger.LogWarning(ex, "Error creating ListedItem from StorageItem"); + } + } + + index += (uint)items.Count; + stepSize = Math.Min(defaultStepSize, GetRemainingItemCount(results)); + if (stepSize == 0) + return; + + items = await queryResult.GetItemsAsync(index, stepSize).AsTask(token); + } + } + + private async Task SearchTagsAsync(string folder, IList results, CancellationToken token) + { + var expression = ParseTagQuery(AQSQuery); + + if (expression.OrGroups.Count == 0) + { + return; + } + + var dbInstance = FileTagsHelper.GetDbInstance(); + var matches = dbInstance.GetAllUnderPath(folder) + .Where(x => MatchesTagExpression(x.Tags, expression)); + if (string.IsNullOrEmpty(folder)) + matches = matches.Where(x => !StorageTrashBinService.IsUnderTrashBin(x.FilePath)); + + foreach (var match in matches) + { + if (token.IsCancellationRequested) + return; + + (FindCloseSafeHandle? hFile, WIN32_FIND_DATAW findData) = await Task.Run(() => + { + WIN32_FIND_DATAW findDataTsk = default; + FindCloseSafeHandle hFileTsk; + unsafe + { + hFileTsk = PInvoke.FindFirstFileEx(match.FilePath, FINDEX_INFO_LEVELS.FindExInfoBasic, + &findDataTsk, FINDEX_SEARCH_OPS.FindExSearchNameMatch, FIND_FIRST_EX_FLAGS.FIND_FIRST_EX_LARGE_FETCH); + } + return (hFileTsk, findDataTsk); + }).WithTimeoutAsync(TimeSpan.FromSeconds(5)); + if (token.IsCancellationRequested) + { + hFile?.Dispose(); + return; + } + + if (hFile is { IsInvalid: false } tagSearchHandle) + { + using (tagSearchHandle) + { + string fileName = findData.cFileName.ToString(); + var isSystem = ((FileAttributes)findData.dwFileAttributes & FileAttributes.System) == FileAttributes.System; + var isHidden = ((FileAttributes)findData.dwFileAttributes & FileAttributes.Hidden) == FileAttributes.Hidden; + var startWithDot = fileName.StartsWith('.'); + + bool shouldBeListed = (!isHidden || + (UserSettingsService.FoldersSettingsService.ShowHiddenItems && + (!isSystem || UserSettingsService.FoldersSettingsService.ShowProtectedSystemFiles))) && + (!startWithDot || UserSettingsService.FoldersSettingsService.ShowDotFiles); + + if (shouldBeListed) + { + var item = GetListedItemAsync(match.FilePath, findData); + if (item is not null) + AddResult(results, item, token); + } + } + } + else + { + hFile?.Dispose(); + try + { + IStorageItem? item = (await GetStorageFileAsync(match.FilePath)).Result; + item ??= (await GetStorageFolderAsync(match.FilePath)).Result; + item = item + ?? throw new InvalidOperationException($"The search item '{match.FilePath}' could not be opened."); + if (!item.Name.StartsWith('.') || UserSettingsService.FoldersSettingsService.ShowDotFiles) + { + AddResult(results, await GetListedItemAsync(item), token); + } + } + catch (Exception ex) + { + App.Logger.LogWarning(ex, "Error creating ListedItem from StorageItem"); + } + } + } + } + + private async Task AddItemsAsync(string folder, IList results, CancellationToken token) + { + if (IsTagQuery(AQSQuery)) + { + await SearchTagsAsync(folder, results, token); + } + else + { + var workingFolder = await GetStorageFolderAsync(folder); + var storageFolder = workingFolder ? workingFolder.Result : null; + + if (IsAQSQuery) + { + if (storageFolder is not null) + await SearchAsync(storageFolder, results, token); + + return; + } + + if (storageFolder is SystemStorageFolder && !DriveHelpers.IsMtpPath(folder) && !await IsFullyIndexedAsync(storageFolder)) + { + await SearchWithWin32Async(folder, false, results, token); + return; + } + + var hiddenOnlyFromWin32 = false; + if (storageFolder is not null) + { + var countBefore = results.Count; + await SearchAsync(storageFolder, results, token); + hiddenOnlyFromWin32 = results.Count != countBefore; + } + + await SearchWithWin32Async(folder, hiddenOnlyFromWin32, results, token); + } + } + + private static async Task IsFullyIndexedAsync(BaseStorageFolder folder) + { + try + { + return await folder.GetIndexedStateAsync() == IndexedState.FullyIndexed; + } + catch (Exception) + { + return false; + } + } + + private Task SearchWithWin32Async(string folder, bool hiddenOnly, IList results, CancellationToken token) + { + return Task.Factory.StartNew( + () => SearchWithWin32(folder, hiddenOnly, results, token), + token, + TaskCreationOptions.LongRunning, + TaskScheduler.Default) + .WaitAsync(token); + } + + private void SearchWithWin32(string root, bool hiddenOnly, IList results, CancellationToken token) + { + var expression = FileSystemName.TranslateWin32Expression($"*{QueryWithWildcard}"); + var showHiddenItems = UserSettingsService.FoldersSettingsService.ShowHiddenItems; + var showProtectedSystemFiles = UserSettingsService.FoldersSettingsService.ShowProtectedSystemFiles; + var showDotFiles = UserSettingsService.FoldersSettingsService.ShowDotFiles; + + var pendingFolders = new Queue<(string Path, bool UseFindTimeout)>(); + pendingFolders.Enqueue((root, RequiresFindTimeout(root))); + + while (pendingFolders.TryDequeue(out var pendingFolder)) + { + var (folder, useFindTimeout) = pendingFolder; + if (token.IsCancellationRequested || results.Count >= UsedMaxItemCount) + return; + + using (var findHandle = FindFirstFile(folder, useFindTimeout, out var findData, token)) + { + if (findHandle is null || findHandle.IsInvalid) + continue; + + do + { + var fileName = findData.cFileName.ToString(); + if (fileName is "." or "..") + continue; + + var attributes = (FileAttributes)findData.dwFileAttributes; + var isDirectory = attributes.HasFlag(FileAttributes.Directory); + var reparseTag = attributes.HasFlag(FileAttributes.ReparsePoint) ? findData.dwReserved0 : 0; + + if (isDirectory && !IsNameSurrogateReparseTag(reparseTag)) + pendingFolders.Enqueue((Path.Combine(folder, fileName), useFindTimeout || IsCloudFilesReparseTag(reparseTag))); + + if (!FileSystemName.MatchesWin32Expression(expression, fileName)) + continue; + + var isSystem = attributes.HasFlag(FileAttributes.System); + var isHidden = attributes.HasFlag(FileAttributes.Hidden); + var startWithDot = fileName.StartsWith('.'); + var isShortcut = !isDirectory && FileExtensionHelpers.IsShortcutOrUrlFile(fileName); + + bool shouldBeListed = (hiddenOnly ? + (!isHidden && isShortcut) || (isHidden && showHiddenItems && (!isSystem || showProtectedSystemFiles)) : + !isHidden || (showHiddenItems && (!isSystem || showProtectedSystemFiles))) && + (!startWithDot || showDotFiles); + + var itemPath = Path.Combine(folder, fileName); + if (!shouldBeListed || (hiddenOnly && indexedResultPaths.Contains(itemPath))) + continue; + + try + { + if ((isShortcut ? GetShortcutItem(itemPath, findData) : GetListedItemAsync(itemPath, findData)) is { } item) + AddResult(results, item, token); + } + catch (Exception ex) + { + App.Logger.LogWarning(ex, "Error creating ListedItem from Win32 find data"); + } + } + while (!token.IsCancellationRequested && results.Count < UsedMaxItemCount && PInvoke.FindNextFile(findHandle, out findData)); + } + + RaiseSearchTickIfDue(token); + } + } + + private static FindCloseSafeHandle? FindFirstFile(string folder, bool useTimeout, out WIN32_FIND_DATAW findData, CancellationToken token) + { + if (!useTimeout) + return FindFirstFile(folder, out findData); + + var findTask = Task.Run(() => (Handle: FindFirstFile(folder, out var data), Data: data)); + try + { + if (findTask.Wait(TimeSpan.FromSeconds(5), token)) + { + (var handle, findData) = findTask.Result; + return handle; + } + } + catch (OperationCanceledException) + { + } + + _ = findTask.ContinueWith(t => t.Result.Handle.Dispose(), TaskContinuationOptions.OnlyOnRanToCompletion); + findData = default; + return null; + } + + private static unsafe FindCloseSafeHandle FindFirstFile(string folder, out WIN32_FIND_DATAW findData) + { + WIN32_FIND_DATAW data = default; + var handle = PInvoke.FindFirstFileEx(Path.Join(folder, "*"), FINDEX_INFO_LEVELS.FindExInfoBasic, + &data, FINDEX_SEARCH_OPS.FindExSearchNameMatch, FIND_FIRST_EX_FLAGS.FIND_FIRST_EX_LARGE_FETCH); + findData = data; + return handle; + } + + private static bool RequiresFindTimeout(string root) + { + if (DriveHelpers.IsNetworkPath(root)) + return true; + + try + { + // Virtual cloud drives such as Google Drive report Fixed but aren't NTFS + var drive = new DriveInfo(root); + return drive.DriveType is not System.IO.DriveType.Fixed + || drive.DriveFormat is not ("NTFS" or "ReFS") + || File.GetAttributes(root).HasFlag(FileAttributes.ReparsePoint); + } + catch (Exception) + { + return true; + } + } + + // Junctions, symlinks and mount points + private static bool IsNameSurrogateReparseTag(uint reparseTag) + => (reparseTag & 0x20000000) != 0; + + // IO_REPARSE_TAG_CLOUD and IO_REPARSE_TAG_CLOUD_1 to _F + private static bool IsCloudFilesReparseTag(uint reparseTag) + => (reparseTag & 0xFFFF0FFF) == 0x9000001A; + + private static ShortcutItem GetShortcutItem(string itemPath, WIN32_FIND_DATAW findData) + { + string fileName = findData.cFileName.ToString(); + var isUrl = FileExtensionHelpers.IsWebLinkFile(fileName); + var isHidden = ((FileAttributes)findData.dwFileAttributes & FileAttributes.Hidden) == FileAttributes.Hidden; + PInvoke.FileTimeToSystemTime(findData.ftLastWriteTime, out SYSTEMTIME modifiedTime); + PInvoke.FileTimeToSystemTime(findData.ftCreationTime, out SYSTEMTIME createdTime); + var fileSize = Win32FindDataExtensions.GetSize(findData); + + return new ShortcutItem(null) + { + PrimaryItemAttribute = StorageItemTypes.File, + FileExtension = fileName.Contains('.', StringComparison.Ordinal) ? Path.GetExtension(itemPath)! : string.Empty, + IsHiddenItem = isHidden, + Opacity = isHidden ? Constants.UI.DimItemOpacity : 1, + FileImage = null, + LoadFileIcon = false, + ItemNameRaw = fileName, + ItemDateModifiedReal = modifiedTime.ToDateTime(), + ItemDateCreatedReal = createdTime.ToDateTime(), + ItemType = isUrl ? Strings.ShortcutWebLinkFileType.GetLocalizedResource() : Strings.Shortcut.GetLocalizedResource(), + ItemPath = itemPath, + FileSize = fileSize.ToSizeString(), + FileSizeBytes = fileSize, + IsUrl = isUrl, + }; + } + + private ListedItem? GetListedItemAsync(string itemPath, WIN32_FIND_DATAW findData) + { + string fileName = findData.cFileName.ToString(); + ListedItem? listedItem = null; + var isHidden = ((FileAttributes)findData.dwFileAttributes & FileAttributes.Hidden) == FileAttributes.Hidden; + var isFolder = ((FileAttributes)findData.dwFileAttributes & FileAttributes.Directory) == FileAttributes.Directory; + PInvoke.FileTimeToSystemTime(findData.ftLastWriteTime, out SYSTEMTIME systemModifiedTimeOutput); + PInvoke.FileTimeToSystemTime(findData.ftCreationTime, out SYSTEMTIME systemCreatedTimeOutput); + + if (!isFolder) + { + string? itemFileExtension = null; + string? itemType = null; + long fileSize = Win32FindDataExtensions.GetSize(findData); + if (fileName.Contains('.', StringComparison.Ordinal)) + { + itemFileExtension = Path.GetExtension(itemPath); + itemType = itemFileExtension!.Trim('.') + " " + itemType; + } + + listedItem = new ListedItem(null) + { + PrimaryItemAttribute = StorageItemTypes.File, + ItemNameRaw = fileName, + ItemPath = itemPath, + ItemDateModifiedReal = systemModifiedTimeOutput.ToDateTime(), + ItemDateCreatedReal = systemCreatedTimeOutput.ToDateTime(), + IsHiddenItem = isHidden, + LoadFileIcon = false, + FileExtension = itemFileExtension, + ItemType = itemType, + Opacity = isHidden ? Constants.UI.DimItemOpacity : 1, + FileSize = fileSize.ToSizeString(), + FileSizeBytes = fileSize, + }; + } + else + { + if (fileName != "." && fileName != "..") + { + listedItem = new ListedItem(null) + { + PrimaryItemAttribute = StorageItemTypes.Folder, + ItemNameRaw = fileName, + ItemPath = itemPath, + ItemDateModifiedReal = systemModifiedTimeOutput.ToDateTime(), + ItemDateCreatedReal = systemCreatedTimeOutput.ToDateTime(), + IsHiddenItem = isHidden, + LoadFileIcon = false, + ItemType = folderTypeTextLocalized, + Opacity = isHidden ? Constants.UI.DimItemOpacity : 1 + }; + } + } + + return listedItem; + } + + private async Task GetListedItemAsync(IStorageItem item) + { + ListedItem? listedItem = null; + if (item.IsOfType(StorageItemTypes.Folder)) + { + var folder = item.AsBaseStorageFolder() + ?? throw new InvalidOperationException($"The search result '{item.Path}' could not be opened as a folder."); + + var props = await folder.GetBasicPropertiesAsync(); + if (folder is BinStorageFolder binFolder) + { + listedItem = new RecycleBinItem(null) + { + PrimaryItemAttribute = StorageItemTypes.Folder, + ItemNameRaw = folder.DisplayName, + ItemPath = folder.Path, + ItemDateModifiedReal = props.DateModified, + ItemDateCreatedReal = folder.DateCreated, + ItemType = folderTypeTextLocalized, + Opacity = 1, + FileSize = props.Size.ToSizeString(), + FileSizeBytes = (long)props.Size, + ItemDateDeletedReal = binFolder.DateDeleted, + ItemOriginalPath = binFolder.OriginalPath + }; + } + else + { + listedItem = new ListedItem(null) + { + PrimaryItemAttribute = StorageItemTypes.Folder, + ItemNameRaw = folder.DisplayName, + ItemPath = folder.Path, + ItemDateModifiedReal = props.DateModified, + ItemDateCreatedReal = folder.DateCreated, + ItemType = folderTypeTextLocalized, + Opacity = 1 + }; + } + } + else if (item.IsOfType(StorageItemTypes.File)) + { + var file = item.AsBaseStorageFile() + ?? throw new InvalidOperationException($"The search result '{item.Path}' could not be opened as a file."); + + var props = await file.GetBasicPropertiesAsync(); + string? itemFileExtension = null; + string? itemType = null; + if (file.Name.Contains('.', StringComparison.Ordinal)) + { + itemFileExtension = Path.GetExtension(file.Path); + itemType = itemFileExtension!.Trim('.') + " " + itemType; + } + + var itemSize = props.Size.ToSizeString(); + + if (file is BinStorageFile binFile) + { + listedItem = new RecycleBinItem(null) + { + PrimaryItemAttribute = StorageItemTypes.File, + ItemNameRaw = file.Name, + ItemPath = file.Path, + LoadFileIcon = false, + FileExtension = itemFileExtension, + FileSizeBytes = (long)props.Size, + FileSize = itemSize, + ItemDateModifiedReal = props.DateModified, + ItemDateCreatedReal = file.DateCreated, + ItemType = itemType, + Opacity = 1, + ItemDateDeletedReal = binFile.DateDeleted, + ItemOriginalPath = binFile.OriginalPath + }; + } + else if (FileExtensionHelpers.IsShortcutOrUrlFile(file.Path)) + { + var isUrl = FileExtensionHelpers.IsWebLinkFile(file.Path); + var shortcutItem = new ShortcutItem(null) + { + PrimaryItemAttribute = StorageItemTypes.File, + FileExtension = itemFileExtension, + IsHiddenItem = false, + Opacity = 1, + FileImage = null, + LoadFileIcon = false, + ItemNameRaw = file.Name, + ItemDateModifiedReal = props.DateModified, + ItemDateCreatedReal = file.DateCreated, + ItemType = isUrl ? Strings.ShortcutWebLinkFileType.GetLocalizedResource() : Strings.Shortcut.GetLocalizedResource(), + ItemPath = file.Path, + FileSize = itemSize, + FileSizeBytes = (long)props.Size, + IsUrl = isUrl, + }; + listedItem = shortcutItem; + } + else + { + listedItem = new ListedItem(null) + { + PrimaryItemAttribute = StorageItemTypes.File, + ItemNameRaw = file.Name, + ItemPath = file.Path, + LoadFileIcon = false, + FileExtension = itemFileExtension, + FileSizeBytes = (long)props.Size, + FileSize = itemSize, + ItemDateModifiedReal = props.DateModified, + ItemDateCreatedReal = file.DateCreated, + ItemType = itemType, + Opacity = 1 + }; + } + } + return listedItem + ?? throw new InvalidOperationException($"The search result '{item.Path}' is neither a file nor a folder."); + } + + private QueryOptions ToQueryOptions() + { + var query = new QueryOptions + { + FolderDepth = FolderDepth.Deep, + UserSearchFilter = AQSQuery ?? string.Empty, + }; + + query.IndexerOption = IndexerOption.UseIndexerWhenAvailable; + + query.SortOrder.Clear(); + query.SortOrder.Add(new SortEntry { PropertyName = "System.Search.Rank", AscendingOrder = false }); + + query.SetPropertyPrefetch(PropertyPrefetchOptions.BasicProperties, null); + + return query; + } + + private static Task> GetStorageFolderAsync(string path) + => FilesystemTasks.WrapNullable(() => StorageFileExtensions.DangerousGetFolderFromPathAsync(path)); + + private static Task> GetStorageFileAsync(string path) + => FilesystemTasks.WrapNullable(() => StorageFileExtensions.DangerousGetFileFromPathAsync(path)); + } +} diff --git a/src/Files.App/Utils/Storage/Search/FolderSearch.cs b/src/Files.App/Utils/Storage/Search/FolderSearch.cs index 177598c6f511..c91b3b69837b 100644 --- a/src/Files.App/Utils/Storage/Search/FolderSearch.cs +++ b/src/Files.App/Utils/Storage/Search/FolderSearch.cs @@ -1,33 +1,29 @@ // Copyright (c) Files Community // Licensed under the MIT License. -using Files.Shared.Helpers; using Microsoft.Extensions.Logging; using Microsoft.UI.Dispatching; using System.IO; -using System.IO.Enumeration; using System.Text.RegularExpressions; using Windows.Storage; -using Windows.Storage.FileProperties; -using Windows.Storage.Search; -using Windows.Win32; -using Windows.Win32.Foundation; -using Windows.Win32.Storage.FileSystem; -using FileAttributes = System.IO.FileAttributes; namespace Files.App.Utils.Storage { public sealed partial class FolderSearch { private IUserSettingsService UserSettingsService { get; } = Ioc.Default.GetRequiredService(); +#if WINDOWS private DrivesViewModel drivesViewModel = Ioc.Default.GetRequiredService(); private readonly IStorageTrashBinService StorageTrashBinService = Ioc.Default.GetRequiredService(); +#endif private readonly IFileTagsSettingsService fileTagsSettingsService = Ioc.Default.GetRequiredService(); private readonly ILogger logger = Ioc.Default.GetRequiredService>(); private static readonly string folderTypeTextLocalized = Strings.Folder.GetLocalizedResource(); +#if WINDOWS private const uint defaultStepSize = 500; +#endif public string? Query { get; set; } @@ -132,12 +128,9 @@ public async Task SearchAsync(IList results, CancellationToken token private async Task AddItemsForHomeAsync(IList results, CancellationToken token) { - if (OperatingSystem.IsLinux()) - { - await SearchLinuxAsync("Home", results, token); - return; - } - +#if !WINDOWS + await SearchLinuxAsync("Home", results, token); +#else if (IsTagQuery(AQSQuery)) { await SearchTagsAsync("", results, token); // Search tags everywhere, not only local drives @@ -149,6 +142,7 @@ private async Task AddItemsForHomeAsync(IList results, CancellationT await AddItemsAsync(drive.Path!, results, token); } } +#endif } public async Task> SearchAsync() @@ -206,52 +200,6 @@ private void RaiseSearchTickIfDue(CancellationToken token) private uint GetRemainingItemCount(IList results) => results.Count >= UsedMaxItemCount ? 0 : UsedMaxItemCount - (uint)results.Count; - private async Task SearchAsync(BaseStorageFolder folder, IList results, CancellationToken token) - { - uint index = 0; - var stepSize = Math.Min(defaultStepSize, GetRemainingItemCount(results)); - if (stepSize == 0) - return; - - var options = ToQueryOptions(); - - var queryResult = folder.CreateItemQueryWithOptions(options); - var items = await queryResult.GetItemsAsync(0, stepSize).AsTask(token); - - while (items.Count > 0) - { - foreach (IStorageItem item in items) - { - if (token.IsCancellationRequested) - { - return; - } - - try - { - if (!item.Name.StartsWith('.') || UserSettingsService.FoldersSettingsService.ShowDotFiles) - { - var listedItem = await GetListedItemAsync(item); - AddResult(results, listedItem, token); - if (listedItem.ItemPath is not null) - indexedResultPaths.Add(listedItem.ItemPath); - } - } - catch (Exception ex) - { - App.Logger.LogWarning(ex, "Error creating ListedItem from StorageItem"); - } - } - - index += (uint)items.Count; - stepSize = Math.Min(defaultStepSize, GetRemainingItemCount(results)); - if (stepSize == 0) - return; - - items = await queryResult.GetItemsAsync(index, stepSize).AsTask(token); - } - } - // Awaited so the final sort treats folder shortcuts as folders private async Task ResolveShortcutTargetsAsync(CancellationToken token) { @@ -413,506 +361,6 @@ private bool MatchesTagExpression(IEnumerable? fileTags, TagQueryExpress return false; } - private async Task SearchTagsAsync(string folder, IList results, CancellationToken token) - { - var expression = ParseTagQuery(AQSQuery); - - if (expression.OrGroups.Count == 0) - { - return; - } - - var dbInstance = FileTagsHelper.GetDbInstance(); - var matches = dbInstance.GetAllUnderPath(folder) - .Where(x => MatchesTagExpression(x.Tags, expression)); - if (string.IsNullOrEmpty(folder)) - matches = matches.Where(x => !StorageTrashBinService.IsUnderTrashBin(x.FilePath)); - - foreach (var match in matches) - { - if (token.IsCancellationRequested) - return; - - (FindCloseSafeHandle? hFile, WIN32_FIND_DATAW findData) = await Task.Run(() => - { - WIN32_FIND_DATAW findDataTsk = default; - FindCloseSafeHandle hFileTsk; - unsafe - { - hFileTsk = PInvoke.FindFirstFileEx(match.FilePath, FINDEX_INFO_LEVELS.FindExInfoBasic, - &findDataTsk, FINDEX_SEARCH_OPS.FindExSearchNameMatch, FIND_FIRST_EX_FLAGS.FIND_FIRST_EX_LARGE_FETCH); - } - return (hFileTsk, findDataTsk); - }).WithTimeoutAsync(TimeSpan.FromSeconds(5)); - if (token.IsCancellationRequested) - { - hFile?.Dispose(); - return; - } - - if (hFile is { IsInvalid: false } tagSearchHandle) - { - using (tagSearchHandle) - { - string fileName = findData.cFileName.ToString(); - var isSystem = ((FileAttributes)findData.dwFileAttributes & FileAttributes.System) == FileAttributes.System; - var isHidden = ((FileAttributes)findData.dwFileAttributes & FileAttributes.Hidden) == FileAttributes.Hidden; - var startWithDot = fileName.StartsWith('.'); - - bool shouldBeListed = (!isHidden || - (UserSettingsService.FoldersSettingsService.ShowHiddenItems && - (!isSystem || UserSettingsService.FoldersSettingsService.ShowProtectedSystemFiles))) && - (!startWithDot || UserSettingsService.FoldersSettingsService.ShowDotFiles); - - if (shouldBeListed) - { - var item = GetListedItemAsync(match.FilePath, findData); - if (item is not null) - AddResult(results, item, token); - } - } - } - else - { - hFile?.Dispose(); - try - { - IStorageItem? item = (await GetStorageFileAsync(match.FilePath)).Result; - item ??= (await GetStorageFolderAsync(match.FilePath)).Result; - item = item - ?? throw new InvalidOperationException($"The search item '{match.FilePath}' could not be opened."); - if (!item.Name.StartsWith('.') || UserSettingsService.FoldersSettingsService.ShowDotFiles) - { - AddResult(results, await GetListedItemAsync(item), token); - } - } - catch (Exception ex) - { - App.Logger.LogWarning(ex, "Error creating ListedItem from StorageItem"); - } - } - } - } - - private async Task AddItemsAsync(string folder, IList results, CancellationToken token) - { - if (OperatingSystem.IsLinux()) - { - await SearchLinuxAsync(folder, results, token); - return; - } - - if (IsTagQuery(AQSQuery)) - { - await SearchTagsAsync(folder, results, token); - } - else - { - var workingFolder = await GetStorageFolderAsync(folder); - var storageFolder = workingFolder ? workingFolder.Result : null; - - if (IsAQSQuery) - { - if (storageFolder is not null) - await SearchAsync(storageFolder, results, token); - - return; - } - - if (storageFolder is SystemStorageFolder && !DriveHelpers.IsMtpPath(folder) && !await IsFullyIndexedAsync(storageFolder)) - { - await SearchWithWin32Async(folder, false, results, token); - return; - } - - var hiddenOnlyFromWin32 = false; - if (storageFolder is not null) - { - var countBefore = results.Count; - await SearchAsync(storageFolder, results, token); - hiddenOnlyFromWin32 = results.Count != countBefore; - } - - await SearchWithWin32Async(folder, hiddenOnlyFromWin32, results, token); - } - } - - private static async Task IsFullyIndexedAsync(BaseStorageFolder folder) - { - try - { - return await folder.GetIndexedStateAsync() == IndexedState.FullyIndexed; - } - catch (Exception) - { - return false; - } - } - - private Task SearchWithWin32Async(string folder, bool hiddenOnly, IList results, CancellationToken token) - { - return Task.Factory.StartNew( - () => SearchWithWin32(folder, hiddenOnly, results, token), - token, - TaskCreationOptions.LongRunning, - TaskScheduler.Default) - .WaitAsync(token); - } - - private void SearchWithWin32(string root, bool hiddenOnly, IList results, CancellationToken token) - { - var expression = FileSystemName.TranslateWin32Expression($"*{QueryWithWildcard}"); - var showHiddenItems = UserSettingsService.FoldersSettingsService.ShowHiddenItems; - var showProtectedSystemFiles = UserSettingsService.FoldersSettingsService.ShowProtectedSystemFiles; - var showDotFiles = UserSettingsService.FoldersSettingsService.ShowDotFiles; - - var pendingFolders = new Queue<(string Path, bool UseFindTimeout)>(); - pendingFolders.Enqueue((root, RequiresFindTimeout(root))); - - while (pendingFolders.TryDequeue(out var pendingFolder)) - { - var (folder, useFindTimeout) = pendingFolder; - if (token.IsCancellationRequested || results.Count >= UsedMaxItemCount) - return; - - using (var findHandle = FindFirstFile(folder, useFindTimeout, out var findData, token)) - { - if (findHandle is null || findHandle.IsInvalid) - continue; - - do - { - var fileName = findData.cFileName.ToString(); - if (fileName is "." or "..") - continue; - - var attributes = (FileAttributes)findData.dwFileAttributes; - var isDirectory = attributes.HasFlag(FileAttributes.Directory); - var reparseTag = attributes.HasFlag(FileAttributes.ReparsePoint) ? findData.dwReserved0 : 0; - - if (isDirectory && !IsNameSurrogateReparseTag(reparseTag)) - pendingFolders.Enqueue((Path.Combine(folder, fileName), useFindTimeout || IsCloudFilesReparseTag(reparseTag))); - - if (!FileSystemName.MatchesWin32Expression(expression, fileName)) - continue; - - var isSystem = attributes.HasFlag(FileAttributes.System); - var isHidden = attributes.HasFlag(FileAttributes.Hidden); - var startWithDot = fileName.StartsWith('.'); - var isShortcut = !isDirectory && FileExtensionHelpers.IsShortcutOrUrlFile(fileName); - - bool shouldBeListed = (hiddenOnly ? - (!isHidden && isShortcut) || (isHidden && showHiddenItems && (!isSystem || showProtectedSystemFiles)) : - !isHidden || (showHiddenItems && (!isSystem || showProtectedSystemFiles))) && - (!startWithDot || showDotFiles); - - var itemPath = Path.Combine(folder, fileName); - if (!shouldBeListed || (hiddenOnly && indexedResultPaths.Contains(itemPath))) - continue; - - try - { - if ((isShortcut ? GetShortcutItem(itemPath, findData) : GetListedItemAsync(itemPath, findData)) is { } item) - AddResult(results, item, token); - } - catch (Exception ex) - { - App.Logger.LogWarning(ex, "Error creating ListedItem from Win32 find data"); - } - } - while (!token.IsCancellationRequested && results.Count < UsedMaxItemCount && PInvoke.FindNextFile(findHandle, out findData)); - } - - RaiseSearchTickIfDue(token); - } - } - - private static FindCloseSafeHandle? FindFirstFile(string folder, bool useTimeout, out WIN32_FIND_DATAW findData, CancellationToken token) - { - if (!useTimeout) - return FindFirstFile(folder, out findData); - - var findTask = Task.Run(() => (Handle: FindFirstFile(folder, out var data), Data: data)); - try - { - if (findTask.Wait(TimeSpan.FromSeconds(5), token)) - { - (var handle, findData) = findTask.Result; - return handle; - } - } - catch (OperationCanceledException) - { - } - - _ = findTask.ContinueWith(t => t.Result.Handle.Dispose(), TaskContinuationOptions.OnlyOnRanToCompletion); - findData = default; - return null; - } - - private static unsafe FindCloseSafeHandle FindFirstFile(string folder, out WIN32_FIND_DATAW findData) - { - WIN32_FIND_DATAW data = default; - var handle = PInvoke.FindFirstFileEx(Path.Join(folder, "*"), FINDEX_INFO_LEVELS.FindExInfoBasic, - &data, FINDEX_SEARCH_OPS.FindExSearchNameMatch, FIND_FIRST_EX_FLAGS.FIND_FIRST_EX_LARGE_FETCH); - findData = data; - return handle; - } - - private static bool RequiresFindTimeout(string root) - { - if (DriveHelpers.IsNetworkPath(root)) - return true; - - try - { - // Virtual cloud drives such as Google Drive report Fixed but aren't NTFS - var drive = new DriveInfo(root); - return drive.DriveType is not System.IO.DriveType.Fixed - || drive.DriveFormat is not ("NTFS" or "ReFS") - || File.GetAttributes(root).HasFlag(FileAttributes.ReparsePoint); - } - catch (Exception) - { - return true; - } - } - - // Junctions, symlinks and mount points - private static bool IsNameSurrogateReparseTag(uint reparseTag) - => (reparseTag & 0x20000000) != 0; - - // IO_REPARSE_TAG_CLOUD and IO_REPARSE_TAG_CLOUD_1 to _F - private static bool IsCloudFilesReparseTag(uint reparseTag) - => (reparseTag & 0xFFFF0FFF) == 0x9000001A; - - private static ShortcutItem GetShortcutItem(string itemPath, WIN32_FIND_DATAW findData) - { - string fileName = findData.cFileName.ToString(); - var isUrl = FileExtensionHelpers.IsWebLinkFile(fileName); - var isHidden = ((FileAttributes)findData.dwFileAttributes & FileAttributes.Hidden) == FileAttributes.Hidden; - PInvoke.FileTimeToSystemTime(findData.ftLastWriteTime, out SYSTEMTIME modifiedTime); - PInvoke.FileTimeToSystemTime(findData.ftCreationTime, out SYSTEMTIME createdTime); - var fileSize = Win32FindDataExtensions.GetSize(findData); - - return new ShortcutItem(null) - { - PrimaryItemAttribute = StorageItemTypes.File, - FileExtension = fileName.Contains('.', StringComparison.Ordinal) ? Path.GetExtension(itemPath)! : string.Empty, - IsHiddenItem = isHidden, - Opacity = isHidden ? Constants.UI.DimItemOpacity : 1, - FileImage = null, - LoadFileIcon = false, - ItemNameRaw = fileName, - ItemDateModifiedReal = modifiedTime.ToDateTime(), - ItemDateCreatedReal = createdTime.ToDateTime(), - ItemType = isUrl ? Strings.ShortcutWebLinkFileType.GetLocalizedResource() : Strings.Shortcut.GetLocalizedResource(), - ItemPath = itemPath, - FileSize = fileSize.ToSizeString(), - FileSizeBytes = fileSize, - IsUrl = isUrl, - }; - } - - private ListedItem? GetListedItemAsync(string itemPath, WIN32_FIND_DATAW findData) - { - string fileName = findData.cFileName.ToString(); - ListedItem? listedItem = null; - var isHidden = ((FileAttributes)findData.dwFileAttributes & FileAttributes.Hidden) == FileAttributes.Hidden; - var isFolder = ((FileAttributes)findData.dwFileAttributes & FileAttributes.Directory) == FileAttributes.Directory; - PInvoke.FileTimeToSystemTime(findData.ftLastWriteTime, out SYSTEMTIME systemModifiedTimeOutput); - PInvoke.FileTimeToSystemTime(findData.ftCreationTime, out SYSTEMTIME systemCreatedTimeOutput); - - if (!isFolder) - { - string? itemFileExtension = null; - string? itemType = null; - long fileSize = Win32FindDataExtensions.GetSize(findData); - if (fileName.Contains('.', StringComparison.Ordinal)) - { - itemFileExtension = Path.GetExtension(itemPath); - itemType = itemFileExtension!.Trim('.') + " " + itemType; - } - - listedItem = new ListedItem(null) - { - PrimaryItemAttribute = StorageItemTypes.File, - ItemNameRaw = fileName, - ItemPath = itemPath, - ItemDateModifiedReal = systemModifiedTimeOutput.ToDateTime(), - ItemDateCreatedReal = systemCreatedTimeOutput.ToDateTime(), - IsHiddenItem = isHidden, - LoadFileIcon = false, - FileExtension = itemFileExtension, - ItemType = itemType, - Opacity = isHidden ? Constants.UI.DimItemOpacity : 1, - FileSize = fileSize.ToSizeString(), - FileSizeBytes = fileSize, - }; - } - else - { - if (fileName != "." && fileName != "..") - { - listedItem = new ListedItem(null) - { - PrimaryItemAttribute = StorageItemTypes.Folder, - ItemNameRaw = fileName, - ItemPath = itemPath, - ItemDateModifiedReal = systemModifiedTimeOutput.ToDateTime(), - ItemDateCreatedReal = systemCreatedTimeOutput.ToDateTime(), - IsHiddenItem = isHidden, - LoadFileIcon = false, - ItemType = folderTypeTextLocalized, - Opacity = isHidden ? Constants.UI.DimItemOpacity : 1 - }; - } - } - - return listedItem; - } - - private async Task GetListedItemAsync(IStorageItem item) - { - ListedItem? listedItem = null; - if (item.IsOfType(StorageItemTypes.Folder)) - { - var folder = item.AsBaseStorageFolder() - ?? throw new InvalidOperationException($"The search result '{item.Path}' could not be opened as a folder."); - - var props = await folder.GetBasicPropertiesAsync(); - if (folder is BinStorageFolder binFolder) - { - listedItem = new RecycleBinItem(null) - { - PrimaryItemAttribute = StorageItemTypes.Folder, - ItemNameRaw = folder.DisplayName, - ItemPath = folder.Path, - ItemDateModifiedReal = props.DateModified, - ItemDateCreatedReal = folder.DateCreated, - ItemType = folderTypeTextLocalized, - Opacity = 1, - FileSize = props.Size.ToSizeString(), - FileSizeBytes = (long)props.Size, - ItemDateDeletedReal = binFolder.DateDeleted, - ItemOriginalPath = binFolder.OriginalPath - }; - } - else - { - listedItem = new ListedItem(null) - { - PrimaryItemAttribute = StorageItemTypes.Folder, - ItemNameRaw = folder.DisplayName, - ItemPath = folder.Path, - ItemDateModifiedReal = props.DateModified, - ItemDateCreatedReal = folder.DateCreated, - ItemType = folderTypeTextLocalized, - Opacity = 1 - }; - } - } - else if (item.IsOfType(StorageItemTypes.File)) - { - var file = item.AsBaseStorageFile() - ?? throw new InvalidOperationException($"The search result '{item.Path}' could not be opened as a file."); - - var props = await file.GetBasicPropertiesAsync(); - string? itemFileExtension = null; - string? itemType = null; - if (file.Name.Contains('.', StringComparison.Ordinal)) - { - itemFileExtension = Path.GetExtension(file.Path); - itemType = itemFileExtension!.Trim('.') + " " + itemType; - } - - var itemSize = props.Size.ToSizeString(); - - if (file is BinStorageFile binFile) - { - listedItem = new RecycleBinItem(null) - { - PrimaryItemAttribute = StorageItemTypes.File, - ItemNameRaw = file.Name, - ItemPath = file.Path, - LoadFileIcon = false, - FileExtension = itemFileExtension, - FileSizeBytes = (long)props.Size, - FileSize = itemSize, - ItemDateModifiedReal = props.DateModified, - ItemDateCreatedReal = file.DateCreated, - ItemType = itemType, - Opacity = 1, - ItemDateDeletedReal = binFile.DateDeleted, - ItemOriginalPath = binFile.OriginalPath - }; - } - else if (FileExtensionHelpers.IsShortcutOrUrlFile(file.Path)) - { - var isUrl = FileExtensionHelpers.IsWebLinkFile(file.Path); - var shortcutItem = new ShortcutItem(null) - { - PrimaryItemAttribute = StorageItemTypes.File, - FileExtension = itemFileExtension, - IsHiddenItem = false, - Opacity = 1, - FileImage = null, - LoadFileIcon = false, - ItemNameRaw = file.Name, - ItemDateModifiedReal = props.DateModified, - ItemDateCreatedReal = file.DateCreated, - ItemType = isUrl ? Strings.ShortcutWebLinkFileType.GetLocalizedResource() : Strings.Shortcut.GetLocalizedResource(), - ItemPath = file.Path, - FileSize = itemSize, - FileSizeBytes = (long)props.Size, - IsUrl = isUrl, - }; - listedItem = shortcutItem; - } - else - { - listedItem = new ListedItem(null) - { - PrimaryItemAttribute = StorageItemTypes.File, - ItemNameRaw = file.Name, - ItemPath = file.Path, - LoadFileIcon = false, - FileExtension = itemFileExtension, - FileSizeBytes = (long)props.Size, - FileSize = itemSize, - ItemDateModifiedReal = props.DateModified, - ItemDateCreatedReal = file.DateCreated, - ItemType = itemType, - Opacity = 1 - }; - } - } - return listedItem - ?? throw new InvalidOperationException($"The search result '{item.Path}' is neither a file nor a folder."); - } - - private QueryOptions ToQueryOptions() - { - var query = new QueryOptions - { - FolderDepth = FolderDepth.Deep, - UserSearchFilter = AQSQuery ?? string.Empty, - }; - - query.IndexerOption = IndexerOption.UseIndexerWhenAvailable; - - query.SortOrder.Clear(); - query.SortOrder.Add(new SortEntry { PropertyName = "System.Search.Rank", AscendingOrder = false }); - - query.SetPropertyPrefetch(PropertyPrefetchOptions.BasicProperties, null); - - return query; - } - - private static Task> GetStorageFolderAsync(string path) - => FilesystemTasks.WrapNullable(() => StorageFileExtensions.DangerousGetFolderFromPathAsync(path)); - private static Task> GetStorageFileAsync(string path) - => FilesystemTasks.WrapNullable(() => StorageFileExtensions.DangerousGetFileFromPathAsync(path)); } } diff --git a/src/Files.App/Utils/Storage/StorageBaseItems/BaseStorageFile.cs b/src/Files.App/Utils/Storage/StorageBaseItems/BaseStorageFile.cs index cd35f3d86c41..3414df304251 100644 --- a/src/Files.App/Utils/Storage/StorageBaseItems/BaseStorageFile.cs +++ b/src/Files.App/Utils/Storage/StorageBaseItems/BaseStorageFile.cs @@ -41,13 +41,18 @@ public StorageProvider? Provider public abstract IStorageItemExtraProperties Properties { get; } [MaybeNull] StorageItemContentProperties IStorageItemProperties.Properties +#if WINDOWS => this is SystemStorageFile file ? file.File.Properties : null; +#else + => null; +#endif public static implicit operator BaseStorageFile?(StorageFile? value) { return value is not null ? new SystemStorageFile(value) : null; } + public abstract IAsyncOperation ToStorageFileAsync(); public abstract bool IsEqual(IStorageItem item); @@ -165,12 +170,21 @@ IAsyncOperation IStorageFile.CopyAsync(IStorageFolder destinationFo return Task.FromResult(null).AsAsyncOperation(); } +#if WINDOWS public static IAsyncOperation GetFileFromPathAsync(string path) { return AsyncInfo.Run(async (cancellationToken) => await ZipStorageFile.FromPathAsync(path) ?? await FtpStorageFile.FromPathAsync(path) ?? await ShellStorageFile.FromPathAsync(path) ?? await NativeStorageFile.FromPathAsync(path) ?? await SystemStorageFile.FromPathAsync(path) ); } +#else + // LINUX-TODO(storage): remove legacy WinRT resolution after downstream consumers use IStorableResolver. + public static IAsyncOperation GetFileFromPathAsync(string path) + { + return AsyncInfo.Run(async (cancellationToken) + => await FtpStorageFile.FromPathAsync(path) ?? await SystemStorageFile.FromPathAsync(path)); + } +#endif public async Task ReadTextAsync(int maxLength = -1) { diff --git a/src/Files.App/Utils/Storage/StorageBaseItems/BaseStorageFolder.cs b/src/Files.App/Utils/Storage/StorageBaseItems/BaseStorageFolder.cs index 474f59d5363c..c25b7a8fa5a2 100644 --- a/src/Files.App/Utils/Storage/StorageBaseItems/BaseStorageFolder.cs +++ b/src/Files.App/Utils/Storage/StorageBaseItems/BaseStorageFolder.cs @@ -33,7 +33,11 @@ public StorageProvider? Provider [MaybeNull] StorageItemContentProperties IStorageItemProperties.Properties +#if WINDOWS => this is SystemStorageFolder folder ? folder.Folder.Properties : null; +#else + => null; +#endif public static implicit operator BaseStorageFolder?(StorageFolder? value) { @@ -43,6 +47,7 @@ value is not null : null; } + public abstract IAsyncOperation ToStorageFolderAsync(); public abstract bool IsEqual(IStorageItem item); @@ -139,12 +144,21 @@ IAsyncOperation IStorageFolder.GetFolderAsync(string name) }); } +#if WINDOWS public static IAsyncOperation GetFolderFromPathAsync(string path) { return AsyncInfo.Run(async (cancellationToken) => await ZipStorageFolder.FromPathAsync(path) ?? await FtpStorageFolder.FromPathAsync(path) ?? await ShellStorageFolder.FromPathAsync(path) ?? await SystemStorageFolder.FromPathAsync(path)); } +#else + // LINUX-TODO(storage): remove legacy WinRT resolution after downstream consumers use IStorableResolver. + public static IAsyncOperation GetFolderFromPathAsync(string path) + { + return AsyncInfo.Run(async (cancellationToken) + => await FtpStorageFolder.FromPathAsync(path) ?? await SystemStorageFolder.FromPathAsync(path)); + } +#endif public abstract IAsyncOperation?> GetFoldersAsync(); diff --git a/src/Files.App/Utils/Storage/StorageItems/FtpStorageFile.cs b/src/Files.App/Utils/Storage/StorageItems/FtpStorageFile.cs index 172195bd416b..64aab4378f2f 100644 --- a/src/Files.App/Utils/Storage/StorageItems/FtpStorageFile.cs +++ b/src/Files.App/Utils/Storage/StorageItems/FtpStorageFile.cs @@ -131,7 +131,12 @@ public override IAsyncOperation GetBasicPropertiesAsync() var inStream = await ftpClient.OpenRead(FtpPath, token: cancellationToken); var nsStream = new NonSeekableRandomAccessStreamForRead(inStream, (ulong)inStream.Length) { DisposeCallback = ftpClient.Dispose }; +#if WINDOWS return new StreamWithContentType(nsStream); +#else + // LINUX-TODO(storage): use the archive/FTP storable stream instead of the WinRT content stream. + throw new NotSupportedException("WinRT content streams are Windows-only."); +#endif }, ((IPasswordProtectedItem)this).RetryWithCredentialsAsync)); } public override IAsyncOperation OpenSequentialReadAsync() diff --git a/src/Files.App/Utils/Storage/StorageItems/NativeStorageFile.cs b/src/Files.App/Utils/Storage/StorageItems/NativeStorageFile.Windows.cs similarity index 100% rename from src/Files.App/Utils/Storage/StorageItems/NativeStorageFile.cs rename to src/Files.App/Utils/Storage/StorageItems/NativeStorageFile.Windows.cs diff --git a/src/Files.App/Utils/Storage/StorageItems/ShellStorageFile.cs b/src/Files.App/Utils/Storage/StorageItems/ShellStorageFile.Windows.cs similarity index 100% rename from src/Files.App/Utils/Storage/StorageItems/ShellStorageFile.cs rename to src/Files.App/Utils/Storage/StorageItems/ShellStorageFile.Windows.cs diff --git a/src/Files.App/Utils/Storage/StorageItems/ShellStorageFolder.cs b/src/Files.App/Utils/Storage/StorageItems/ShellStorageFolder.Windows.cs similarity index 100% rename from src/Files.App/Utils/Storage/StorageItems/ShellStorageFolder.cs rename to src/Files.App/Utils/Storage/StorageItems/ShellStorageFolder.Windows.cs diff --git a/src/Files.App/Utils/Storage/StorageItems/StreamWithContentType.cs b/src/Files.App/Utils/Storage/StorageItems/StorageStreams.cs similarity index 60% rename from src/Files.App/Utils/Storage/StorageItems/StreamWithContentType.cs rename to src/Files.App/Utils/Storage/StorageItems/StorageStreams.cs index e326951f1ac1..068ca9b7e62a 100644 --- a/src/Files.App/Utils/Storage/StorageItems/StreamWithContentType.cs +++ b/src/Files.App/Utils/Storage/StorageItems/StorageStreams.cs @@ -2,12 +2,9 @@ // Licensed under the MIT License. using System.IO; -using System.Runtime.InteropServices.Marshalling; using System.Runtime.InteropServices.WindowsRuntime; using Windows.Foundation; using Windows.Storage.Streams; -using Windows.Win32; -using Windows.Win32.System.Com; namespace Files.App.Utils.Storage { @@ -228,134 +225,4 @@ public void Dispose() } } - public sealed partial class StreamWithContentType : IRandomAccessStreamWithContentType - { - private IRandomAccessStream baseStream; - - public StreamWithContentType(IRandomAccessStream stream) - { - baseStream = stream; - } - - public IInputStream GetInputStreamAt(ulong position) => baseStream.GetInputStreamAt(position); - - public IOutputStream GetOutputStreamAt(ulong position) => baseStream.GetOutputStreamAt(position); - - public void Seek(ulong position) => baseStream.Seek(position); - - public IRandomAccessStream CloneStream() => baseStream.CloneStream(); - - public bool CanRead => baseStream.CanRead; - - public bool CanWrite => baseStream.CanWrite; - - public ulong Position => baseStream.Position; - - public ulong Size { get => baseStream.Size; set => baseStream.Size = value; } - - public IAsyncOperationWithProgress ReadAsync(IBuffer buffer, uint count, InputStreamOptions options) - { - return baseStream.ReadAsync(buffer, count, options); - } - - public IAsyncOperationWithProgress WriteAsync(IBuffer buffer) => baseStream.WriteAsync(buffer); - - public IAsyncOperation FlushAsync() => baseStream.FlushAsync(); - - public void Dispose() - { - baseStream.Dispose(); - } - - public string ContentType { get; set; } = "application/octet-stream"; - } - - public sealed partial class ComStreamWrapper : Stream - { - private IStream? iStream; - private STATSTG iStreamStat; - private STGMEDIUM medium; - - public ComStreamWrapper(IStream stream, STGMEDIUM medium) - { - iStream = stream; - this.medium = medium; - try - { - iStream.Stat(out iStreamStat, STATFLAG.STATFLAG_NONAME).ThrowOnFailure(); - } - catch - { - ReleaseResources(); - throw; - } - } - - public override bool CanRead => true; - - public override bool CanSeek => true; - - public override bool CanWrite => false; - - public override long Length => checked((long)iStreamStat.cbSize); - - public override long Position - { - get => Seek(0, SeekOrigin.Current); - set => Seek(value, SeekOrigin.Begin); - } - - public override void Flush() - { - } - - public override int Read(byte[] buffer, int offset, int count) - { - ObjectDisposedException.ThrowIf(iStream is null, this); - iStream.Read(buffer.AsSpan(offset, count), out uint bytesRead).ThrowOnFailure(); - return checked((int)bytesRead); - } - - public override long Seek(long offset, SeekOrigin origin) - { - ObjectDisposedException.ThrowIf(iStream is null, this); - iStream.Seek(offset, origin, out ulong newPosition).ThrowOnFailure(); - return checked((long)newPosition); - } - - public override void SetLength(long value) - { - throw new NotSupportedException(); - } - - public override void Write(byte[] buffer, int offset, int count) - { - throw new NotSupportedException(); - } - - protected override void Dispose(bool disposing) - { - if (iStream is null) - return; - - base.Dispose(disposing); - ReleaseResources(); - } - - private void ReleaseResources() - { - IStream? stream = iStream; - iStream = null; - try - { - if ((object?)stream is ComObject comObject) - comObject.FinalRelease(); - } - finally - { - PInvoke.ReleaseStgMedium(ref medium); - medium = default; - } - } - } } diff --git a/src/Files.App/Utils/Storage/StorageItems/StreamWithContentType.Windows.cs b/src/Files.App/Utils/Storage/StorageItems/StreamWithContentType.Windows.cs new file mode 100644 index 000000000000..23c8afe68211 --- /dev/null +++ b/src/Files.App/Utils/Storage/StorageItems/StreamWithContentType.Windows.cs @@ -0,0 +1,144 @@ +// Copyright (c) Files Community +// Licensed under the MIT License. + +using System.IO; +using System.Runtime.InteropServices.Marshalling; +using System.Runtime.InteropServices.WindowsRuntime; +using Windows.Foundation; +using Windows.Storage.Streams; +using Windows.Win32; +using Windows.Win32.System.Com; + +namespace Files.App.Utils.Storage +{ + public sealed partial class StreamWithContentType : IRandomAccessStreamWithContentType + { + private IRandomAccessStream baseStream; + + public StreamWithContentType(IRandomAccessStream stream) + { + baseStream = stream; + } + + public IInputStream GetInputStreamAt(ulong position) => baseStream.GetInputStreamAt(position); + + public IOutputStream GetOutputStreamAt(ulong position) => baseStream.GetOutputStreamAt(position); + + public void Seek(ulong position) => baseStream.Seek(position); + + public IRandomAccessStream CloneStream() => baseStream.CloneStream(); + + public bool CanRead => baseStream.CanRead; + + public bool CanWrite => baseStream.CanWrite; + + public ulong Position => baseStream.Position; + + public ulong Size { get => baseStream.Size; set => baseStream.Size = value; } + + public IAsyncOperationWithProgress ReadAsync(IBuffer buffer, uint count, InputStreamOptions options) + { + return baseStream.ReadAsync(buffer, count, options); + } + + public IAsyncOperationWithProgress WriteAsync(IBuffer buffer) => baseStream.WriteAsync(buffer); + + public IAsyncOperation FlushAsync() => baseStream.FlushAsync(); + + public void Dispose() + { + baseStream.Dispose(); + } + + public string ContentType { get; set; } = "application/octet-stream"; + } + + public sealed partial class ComStreamWrapper : Stream + { + private IStream? iStream; + private STATSTG iStreamStat; + private STGMEDIUM medium; + + public ComStreamWrapper(IStream stream, STGMEDIUM medium) + { + iStream = stream; + this.medium = medium; + try + { + iStream.Stat(out iStreamStat, STATFLAG.STATFLAG_NONAME).ThrowOnFailure(); + } + catch + { + ReleaseResources(); + throw; + } + } + + public override bool CanRead => true; + + public override bool CanSeek => true; + + public override bool CanWrite => false; + + public override long Length => checked((long)iStreamStat.cbSize); + + public override long Position + { + get => Seek(0, SeekOrigin.Current); + set => Seek(value, SeekOrigin.Begin); + } + + public override void Flush() + { + } + + public override int Read(byte[] buffer, int offset, int count) + { + ObjectDisposedException.ThrowIf(iStream is null, this); + iStream.Read(buffer.AsSpan(offset, count), out uint bytesRead).ThrowOnFailure(); + return checked((int)bytesRead); + } + + public override long Seek(long offset, SeekOrigin origin) + { + ObjectDisposedException.ThrowIf(iStream is null, this); + iStream.Seek(offset, origin, out ulong newPosition).ThrowOnFailure(); + return checked((long)newPosition); + } + + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotSupportedException(); + } + + protected override void Dispose(bool disposing) + { + if (iStream is null) + return; + + base.Dispose(disposing); + ReleaseResources(); + } + + private void ReleaseResources() + { + IStream? stream = iStream; + iStream = null; + try + { + if ((object?)stream is ComObject comObject) + comObject.FinalRelease(); + } + finally + { + PInvoke.ReleaseStgMedium(ref medium); + medium = default; + } + } + } +} diff --git a/src/Files.App/Utils/Storage/StorageItems/SystemStorageFile.cs b/src/Files.App/Utils/Storage/StorageItems/SystemStorageFile.cs index d45d887e340e..017bf61e7780 100644 --- a/src/Files.App/Utils/Storage/StorageItems/SystemStorageFile.cs +++ b/src/Files.App/Utils/Storage/StorageItems/SystemStorageFile.cs @@ -21,13 +21,30 @@ public sealed partial class SystemStorageFile : BaseStorageFile public override string Name => File.Name; public override string DisplayName => File.DisplayName; public override string ContentType => File.ContentType; +#if WINDOWS public override string DisplayType => File.DisplayType; +#else + // Uno does not implement these members and throws + public override string DisplayType => string.Empty; +#endif public override string FileType => File.FileType; +#if WINDOWS public override string FolderRelativeId => File.FolderRelativeId; +#else + public override string FolderRelativeId => string.Empty; +#endif public override DateTimeOffset DateCreated => File.DateCreated; +#if WINDOWS public override Windows.Storage.FileAttributes Attributes => File.Attributes; +#else + public override Windows.Storage.FileAttributes Attributes => default; +#endif +#if WINDOWS public override IStorageItemExtraProperties Properties => File.Properties; +#else + public override IStorageItemExtraProperties Properties => null!; +#endif public SystemStorageFile(StorageFile file) { @@ -108,6 +125,7 @@ public override IAsyncOperation GetBasicPropertiesAsync() return destFile; } } +#if WINDOWS catch (UnauthorizedAccessException) // shortcuts & .url { if (!string.IsNullOrEmpty(destFolder.Path)) @@ -128,6 +146,12 @@ public override IAsyncOperation GetBasicPropertiesAsync() } throw; } +#else + catch (UnauthorizedAccessException) + { + throw; + } +#endif }); } diff --git a/src/Files.App/Utils/Storage/StorageItems/SystemStorageFolder.cs b/src/Files.App/Utils/Storage/StorageItems/SystemStorageFolder.cs index cc61e4c6e2e7..3c4e5b3e130f 100644 --- a/src/Files.App/Utils/Storage/StorageItems/SystemStorageFolder.cs +++ b/src/Files.App/Utils/Storage/StorageItems/SystemStorageFolder.cs @@ -19,12 +19,29 @@ public sealed partial class SystemStorageFolder : BaseStorageFolder public override string Path => Folder.Path; public override string Name => Folder.Name; public override string DisplayName => Folder.DisplayName; +#if WINDOWS public override string DisplayType => Folder.DisplayType; +#else + // Uno does not implement these members and throws + public override string DisplayType => string.Empty; +#endif +#if WINDOWS public override string FolderRelativeId => Folder.FolderRelativeId; +#else + public override string FolderRelativeId => string.Empty; +#endif public override DateTimeOffset DateCreated => Folder.DateCreated; + #if WINDOWS public override FileAttributes Attributes => Folder.Attributes; +#else + public override FileAttributes Attributes => default; +#endif +#if WINDOWS public override IStorageItemExtraProperties Properties => Folder.Properties; +#else + public override IStorageItemExtraProperties Properties => null!; +#endif public SystemStorageFolder(StorageFolder folder) { @@ -34,6 +51,7 @@ public SystemStorageFolder(StorageFolder folder) public static IAsyncOperation FromPathAsync(string path) { +#if WINDOWS if (path.EndsWith(ShellLibraryItem.EXTENSION)) { try @@ -56,6 +74,7 @@ public static IAsyncOperation FromPathAsync(string path) App.Logger.LogWarning(e, null); } } +#endif return AsyncInfo.Run(async (cancellationToken) => new SystemStorageFolder(await StorageFolder.GetFolderFromPathAsync(path))); } diff --git a/src/Files.App/Utils/Storage/StorageItems/VirtualStorageFile.cs b/src/Files.App/Utils/Storage/StorageItems/VirtualStorageFile.Windows.cs similarity index 100% rename from src/Files.App/Utils/Storage/StorageItems/VirtualStorageFile.cs rename to src/Files.App/Utils/Storage/StorageItems/VirtualStorageFile.Windows.cs diff --git a/src/Files.App/Utils/Storage/StorageItems/VirtualStorageFolder.cs b/src/Files.App/Utils/Storage/StorageItems/VirtualStorageFolder.Windows.cs similarity index 100% rename from src/Files.App/Utils/Storage/StorageItems/VirtualStorageFolder.cs rename to src/Files.App/Utils/Storage/StorageItems/VirtualStorageFolder.Windows.cs diff --git a/src/Files.App/Utils/Storage/StorageItems/VirtualStorageItem.cs b/src/Files.App/Utils/Storage/StorageItems/VirtualStorageItem.Windows.cs similarity index 100% rename from src/Files.App/Utils/Storage/StorageItems/VirtualStorageItem.cs rename to src/Files.App/Utils/Storage/StorageItems/VirtualStorageItem.Windows.cs diff --git a/src/Files.App/ViewModels/ShellViewModel.cs b/src/Files.App/ViewModels/ShellViewModel.cs index e09c19570425..0fa2873c91ce 100644 --- a/src/Files.App/ViewModels/ShellViewModel.cs +++ b/src/Files.App/ViewModels/ShellViewModel.cs @@ -2315,7 +2315,7 @@ private async Task EnumerateItemsFromStandardFolderAsync(string path, Cance #if !WINDOWS return await EnumerateLinuxFolderAsync(path, cancellationToken, library); -#endif +#else // Flag to use FindFirstFileExFromApp or StorageFolder enumeration - Use storage folder for Box Drive (#4629) var isBoxFolder = CloudDrivesManager.Drives.FirstOrDefault(x => x.Text == "Box")?.Path?.TrimEnd('\\') is string boxFolder && path.StartsWith(boxFolder); @@ -2565,10 +2565,12 @@ await Task.Run(async () => return 0; } } +#endif } private async Task EnumFromStorageFolderAsync(string path, BaseStorageFolder? rootFolder, StorageFolderWithPath? currentStorageFolder, CancellationToken cancellationToken) { +#if WINDOWS if (rootFolder is null) return; @@ -2620,6 +2622,10 @@ await Task.Run(async () => if (rootFolder is IPasswordProtectedItem ppiu) ppiu.PasswordRequestedCallback = null; +#else + // LINUX-TODO(shell): enumerate storables for non-local locations in P4-E. + await Task.CompletedTask; +#endif } private void CheckForSolutionFile() @@ -3209,10 +3215,15 @@ await dispatcherQueue.EnqueueOrInvokeAsync(() => public Task AddFileOrFolderFromShellFile(ShellFileItem item) { +#if WINDOWS return item.IsFolder ? UniversalStorageEnumerator.AddFolderAsync(ShellStorageFolder.FromShellItem(item), currentStorageFolder, addFilesCTS.Token) : UniversalStorageEnumerator.AddFileAsync(ShellStorageFile.FromShellItem(item), currentStorageFolder, addFilesCTS.Token); +#else + // LINUX-TODO(shell): replace the Windows Shell item factory in P4-E. + return Task.FromResult(null); +#endif } private async Task AddFileOrFolderAsync(ListedItem? item) @@ -3233,6 +3244,7 @@ private async Task AddFileOrFolderAsync(ListedItem? item) { filesAndFolders.Add(item); +#if WINDOWS if (UserSettingsService.FoldersSettingsService.AreAlternateStreamsVisible) { // New file added, enumerate ADS @@ -3242,6 +3254,7 @@ private async Task AddFileOrFolderAsync(ListedItem? item) filesAndFolders.Add(adsItem); } } +#endif } enumFolderSemaphore.Release(); @@ -3249,6 +3262,7 @@ private async Task AddFileOrFolderAsync(ListedItem? item) private async Task AddFileOrFolderAsync(string fileOrFolderPath) { +#if WINDOWS FINDEX_INFO_LEVELS findInfoLevel = FINDEX_INFO_LEVELS.FindExInfoBasic; var additionalFlags = FIND_FIRST_EX_CASE_SENSITIVE; @@ -3291,6 +3305,11 @@ private async Task AddFileOrFolderAsync(ListedItem? item) await AddFileOrFolderAsync(listedItem); return listedItem; +#else + // LINUX-TODO(shell): replace the Win32 incremental item factory in P4-E. + await Task.CompletedTask; + return null; +#endif } private async Task<(ListedItem Item, CloudDriveSyncStatus? SyncStatus, long? Size, DateTimeOffset Created, DateTimeOffset Modified)?> GetFileOrFolderUpdateInfoAsync(ListedItem item, bool hasSyncStatus) diff --git a/src/Files.App/ViewModels/UserControls/Widgets/RecentFilesWidgetViewModel.cs b/src/Files.App/ViewModels/UserControls/Widgets/RecentFilesWidgetViewModel.cs index 8e5331799f1d..931d90408085 100644 --- a/src/Files.App/ViewModels/UserControls/Widgets/RecentFilesWidgetViewModel.cs +++ b/src/Files.App/ViewModels/UserControls/Widgets/RecentFilesWidgetViewModel.cs @@ -364,10 +364,12 @@ async void FlyoutClosed(object? sender, object args) } else { +#if WINDOWS var listedItem = await UniversalStorageEnumerator.AddFileAsync(file, null, default); var shellPage = ContentPageContext.ShellPage ?? throw new InvalidOperationException("The shell page is not available for opening properties."); FilePropertiesHelpers.OpenPropertiesWindow(listedItem!, shellPage); +#endif } } diff --git a/src/Files.App/Views/Layouts/BaseLayoutPage.cs b/src/Files.App/Views/Layouts/BaseLayoutPage.cs index 389df88f2735..bda8563f4639 100644 --- a/src/Files.App/Views/Layouts/BaseLayoutPage.cs +++ b/src/Files.App/Views/Layouts/BaseLayoutPage.cs @@ -1092,8 +1092,10 @@ protected virtual void FileList_DragItemsStarting(object sender, DragItemsStarti else { // Only support IStorageItem capable paths +#if WINDOWS var storageItemList = orderedItems.Where(x => !(x.IsHiddenItem && x.IsLinkItem && x.IsRecycleBinItem && x.IsShortcut)).Select(x => VirtualStorageItem.FromListedItem(x)).ToArray(); e.Data.SetStorageItems(storageItemList, false); +#endif } } finally