From a1da58b282d93bf5859cc9165e9dbd88dcf8350a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kov=C3=A1cs=20B=C3=A1lint=20Hunor?= Date: Mon, 5 Oct 2026 20:45:09 +0300 Subject: [PATCH 1/5] Linux: resolver-based storage helpers; Windows enumeration/search isolated (P4-C) Co-Authored-By: GPT-6.1-sol (OpenAI Codex) Claude-Session: https://claude.ai/code/session_01LeKXJc3DnK3PzAqwgA72NY --- .../FileSystem/CopyItemFromHomeAction.cs | 2 + .../Sidebar/CopyItemFromSidebarAction.cs | 2 + .../Helpers/Navigation/NavigationHelpers.cs | 6 + src/Files.App/Helpers/TransferHelpers.cs | 12 +- src/Files.App/Services/Git/LibGit2Service.cs | 7 +- .../Windows/WindowsQuickAccessService.cs | 2 + src/Files.App/Utils/Shell/ShellHelpers.cs | 8 + ... => UniversalStorageEnumerator.Windows.cs} | 0 ...r.cs => Win32StorageEnumerator.Windows.cs} | 0 .../Storage/Helpers/ErrorCodeConverter.cs | 11 + .../Storage/Helpers/FilePropertiesHelpers.cs | 4 + .../Utils/Storage/Helpers/FileTypesHelper.cs | 6 + .../Utils/Storage/Helpers/FilesystemResult.cs | 4 + .../Utils/Storage/Helpers/FolderHelpers.cs | 4 + .../Helpers/StorageFileExtensions.Linux.cs | 74 ++ .../Helpers/StorageFileExtensions.Windows.cs | 188 +++++ .../Storage/Helpers/StorageFileExtensions.cs | 188 +---- .../Helpers/StorageHelpers.Storables.cs | 42 ++ .../Storage/Helpers/StorageHelpers.Windows.cs | 206 ++++++ .../Utils/Storage/Helpers/StorageHelpers.cs | 195 +----- .../Storage/Operations/FilesystemHelpers.cs | 2 + .../Operations/FilesystemOperations.cs | 10 +- .../Storage/Search/FolderSearch.Linux.cs | 5 + .../Storage/Search/FolderSearch.Windows.cs | 642 +++++++++++++++++ .../Utils/Storage/Search/FolderSearch.cs | 650 +----------------- .../StorageBaseItems/BaseStorageFile.cs | 14 + .../StorageBaseItems/BaseStorageFolder.cs | 14 + .../Storage/StorageItems/BaseQueryResults.cs | 2 + .../Storage/StorageItems/FtpStorageFile.cs | 5 + ...geFile.cs => NativeStorageFile.Windows.cs} | 0 ...ageFile.cs => ShellStorageFile.Windows.cs} | 0 ...older.cs => ShellStorageFolder.Windows.cs} | 0 ...amWithContentType.cs => StorageStreams.cs} | 133 ---- .../StreamWithContentType.Windows.cs | 144 ++++ ...geFile.cs => SystemStorageFile.Windows.cs} | 0 ...lder.cs => SystemStorageFolder.Windows.cs} | 0 ...eFile.cs => VirtualStorageFile.Windows.cs} | 0 ...der.cs => VirtualStorageFolder.Windows.cs} | 0 ...eItem.cs => VirtualStorageItem.Windows.cs} | 0 .../Storage/StorageItems/ZipStorageFile.cs | 15 + .../Storage/StorageItems/ZipStorageFolder.cs | 5 + src/Files.App/ViewModels/ShellViewModel.cs | 21 +- .../Widgets/RecentFilesWidgetViewModel.cs | 2 + src/Files.App/Views/Layouts/BaseLayoutPage.cs | 2 + 44 files changed, 1489 insertions(+), 1138 deletions(-) rename src/Files.App/Utils/Storage/Enumerators/{UniversalStorageEnumerator.cs => UniversalStorageEnumerator.Windows.cs} (100%) rename src/Files.App/Utils/Storage/Enumerators/{Win32StorageEnumerator.cs => Win32StorageEnumerator.Windows.cs} (100%) create mode 100644 src/Files.App/Utils/Storage/Helpers/StorageFileExtensions.Linux.cs create mode 100644 src/Files.App/Utils/Storage/Helpers/StorageFileExtensions.Windows.cs create mode 100644 src/Files.App/Utils/Storage/Helpers/StorageHelpers.Storables.cs create mode 100644 src/Files.App/Utils/Storage/Helpers/StorageHelpers.Windows.cs create mode 100644 src/Files.App/Utils/Storage/Search/FolderSearch.Windows.cs rename src/Files.App/Utils/Storage/StorageItems/{NativeStorageFile.cs => NativeStorageFile.Windows.cs} (100%) rename src/Files.App/Utils/Storage/StorageItems/{ShellStorageFile.cs => ShellStorageFile.Windows.cs} (100%) rename src/Files.App/Utils/Storage/StorageItems/{ShellStorageFolder.cs => ShellStorageFolder.Windows.cs} (100%) rename src/Files.App/Utils/Storage/StorageItems/{StreamWithContentType.cs => StorageStreams.cs} (60%) create mode 100644 src/Files.App/Utils/Storage/StorageItems/StreamWithContentType.Windows.cs rename src/Files.App/Utils/Storage/StorageItems/{SystemStorageFile.cs => SystemStorageFile.Windows.cs} (100%) rename src/Files.App/Utils/Storage/StorageItems/{SystemStorageFolder.cs => SystemStorageFolder.Windows.cs} (100%) rename src/Files.App/Utils/Storage/StorageItems/{VirtualStorageFile.cs => VirtualStorageFile.Windows.cs} (100%) rename src/Files.App/Utils/Storage/StorageItems/{VirtualStorageFolder.cs => VirtualStorageFolder.Windows.cs} (100%) rename src/Files.App/Utils/Storage/StorageItems/{VirtualStorageItem.cs => VirtualStorageItem.Windows.cs} (100%) 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/Helpers/Navigation/NavigationHelpers.cs b/src/Files.App/Helpers/Navigation/NavigationHelpers.cs index a598c57a79cc..f3248a2dc044 100644 --- a/src/Files.App/Helpers/Navigation/NavigationHelpers.cs +++ b/src/Files.App/Helpers/Navigation/NavigationHelpers.cs @@ -654,10 +654,12 @@ private static async Task OpenDirectory(string path, IShellPag // File.Exists distinguishes an archive root (real file on disk) from an inner path like "archive.zip\sub". await STATask.RunPooled(() => { +#if WINDOWS if (folder.Item is SystemStorageFolder || (folder.Item is ZipStorageFolder && File.Exists(folder.Path))) return WindowsRecentItemsService.Add(folder.Path); +#endif return false; }, App.Logger); }); @@ -692,8 +694,10 @@ private static async Task OpenFile(string path, IShellPage ass { var childFileResult = await associatedInstance.ShellViewModel.GetFileWithPathFromPathAsync(shortcutInfo.TargetPath); // Add location to Recent Items List +#if WINDOWS if (childFileResult.Result is { Item: SystemStorageFile } childFile) WindowsRecentItemsService.Add(childFile.Path); +#endif } await Win32Helper.InvokeWin32ComponentAsync(shortcutInfo.TargetPath, associatedInstance, $"{args} {shortcutInfo.Arguments}", shortcutInfo.RunAsAdmin, shortcutInfo.WorkingDirectory); } @@ -714,9 +718,11 @@ private static async Task OpenFile(string path, IShellPage ass { var file = childFile!; // Add location to Recent Items List +#if WINDOWS if (file.Item is SystemStorageFile) WindowsRecentItemsService.Add(file.Path); +#endif if (openViaApplicationPicker) { var storageFile = file.Item diff --git a/src/Files.App/Helpers/TransferHelpers.cs b/src/Files.App/Helpers/TransferHelpers.cs index 0906260bc28c..3ab9420584dc 100644 --- a/src/Files.App/Helpers/TransferHelpers.cs +++ b/src/Files.App/Helpers/TransferHelpers.cs @@ -56,7 +56,11 @@ await itemsToTransfer.ParallelForEachAsync(async storable => throw new SystemIO.IOException($"Failed to process {storable.Id} in cutting/copying to the clipboard.", (int)result.ErrorCode); }, 10, statusCenterItem?.CancellationToken ?? CancellationToken.None); - var standardObjectsOnly = items.All(x => x is StorageFile or StorageFolder or SystemStorageFile or SystemStorageFolder); + var standardObjectsOnly = items.All(x => x is StorageFile or StorageFolder +#if WINDOWS + or SystemStorageFile or SystemStorageFolder +#endif + ); if (standardObjectsOnly) items = new(await items.ToStandardStorageItemsAsync()); @@ -171,7 +175,11 @@ await MainWindow.Instance.DispatcherQueue.EnqueueOrInvokeAsync(() => } }, 10, statusCenterItem?.CancellationToken ?? CancellationToken.None); - var standardObjectsOnly = items.All(x => x is StorageFile or StorageFolder or SystemStorageFile or SystemStorageFolder); + var standardObjectsOnly = items.All(x => x is StorageFile or StorageFolder +#if WINDOWS + or SystemStorageFile or SystemStorageFolder +#endif + ); if (standardObjectsOnly) items = new(await items.ToStandardStorageItemsAsync()); diff --git a/src/Files.App/Services/Git/LibGit2Service.cs b/src/Files.App/Services/Git/LibGit2Service.cs index 37670435a13a..effcf7e83192 100644 --- a/src/Files.App/Services/Git/LibGit2Service.cs +++ b/src/Files.App/Services/Git/LibGit2Service.cs @@ -54,8 +54,11 @@ public bool IsExecutingGitAction if (string.IsNullOrWhiteSpace(path) || path.Equals(root, StringComparison.OrdinalIgnoreCase) || - path.Equals("Home", StringComparison.OrdinalIgnoreCase) || - ShellStorageFolder.IsShellPath(path)) + path.Equals("Home", StringComparison.OrdinalIgnoreCase) +#if WINDOWS + || ShellStorageFolder.IsShellPath(path) +#endif + ) { return null; } 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..0d4bdc432e35 --- /dev/null +++ b/src/Files.App/Utils/Storage/Helpers/StorageFileExtensions.Linux.cs @@ -0,0 +1,74 @@ +// 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 null; + } + + 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(null, 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 null; + } + + 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(null, path); + } + + 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.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..70d11bf8c517 100644 --- a/src/Files.App/Utils/Storage/Helpers/StorageHelpers.cs +++ b/src/Files.App/Utils/Storage/Helpers/StorageHelpers.cs @@ -1,206 +1,49 @@ // 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 - 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; - } - + await GetStorableAsync(path); 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; + var result = await item.ToStorableResult(); + return new(null, result ? FileSystemStatusCode.Generic : result.ErrorCode); } + 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/FilesystemHelpers.cs b/src/Files.App/Utils/Storage/Operations/FilesystemHelpers.cs index f3ad8aa9ebae..b37071b8c52a 100644 --- a/src/Files.App/Utils/Storage/Operations/FilesystemHelpers.cs +++ b/src/Files.App/Utils/Storage/Operations/FilesystemHelpers.cs @@ -830,6 +830,7 @@ public static async Task> GetDraggedStorageIte // workaround for pasting folders from remote desktop (#12318) try { +#if WINDOWS if (hasVirtualItems && packageView.Contains("FileContents")) { var dataObject = ShellDataObject.GetClipboard(); @@ -849,6 +850,7 @@ public static async Task> GetDraggedStorageIte } } } +#endif } catch (Exception ex) { diff --git a/src/Files.App/Utils/Storage/Operations/FilesystemOperations.cs b/src/Files.App/Utils/Storage/Operations/FilesystemOperations.cs index 8c6cce68274a..84349aa0f8fa 100644 --- a/src/Files.App/Utils/Storage/Operations/FilesystemOperations.cs +++ b/src/Files.App/Utils/Storage/Operations/FilesystemOperations.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 15ff4d0282ae..e6e022585e55 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)) @@ -183,3 +187,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..f6c3ad56b335 --- /dev/null +++ b/src/Files.App/Utils/Storage/Search/FolderSearch.Windows.cs @@ -0,0 +1,642 @@ +// Copyright (c) Files Community +// Licensed under the MIT License. + +using Files.Shared.Helpers; +using Microsoft.Extensions.Logging; +using System.IO; +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) + { + //var sampler = new IntervalSampler(500); + uint index = 0; + var stepSize = Math.Min(defaultStepSize, UsedMaxItemCount); + 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) + results.Add(await GetListedItemAsync(item)); + } + catch (Exception ex) + { + App.Logger.LogWarning(ex, "Error creating ListedItem from StorageItem"); + } + + if (results.Count == 32 || results.Count % 300 == 0 /*|| sampler.CheckNow()*/) + { + SearchTick?.Invoke(this, EventArgs.Empty); + } + } + + index += (uint)items.Count; + stepSize = Math.Min(defaultStepSize, UsedMaxItemCount - (uint)results.Count); + items = await queryResult.GetItemsAsync(index, stepSize).AsTask(token); + } + } + + private async Task SearchTagsAsync(string folder, IList results, CancellationToken token) + { + //var sampler = new IntervalSampler(500); + 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 && !token.IsCancellationRequested) + results.Add(item); + } + } + } + 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) + { + var listedItem = await GetListedItemAsync(item); + if (!token.IsCancellationRequested) + results.Add(listedItem); + } + } + catch (Exception ex) + { + App.Logger.LogWarning(ex, "Error creating ListedItem from StorageItem"); + } + } + + if (token.IsCancellationRequested) + return; + + if (results.Count == 32 || results.Count % 300 == 0 /*|| sampler.CheckNow()*/) + { + SearchTick?.Invoke(this, EventArgs.Empty); + } + } + } + + 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 hiddenOnlyFromWin32 = false; + if (workingFolder) + { + var storageFolder = workingFolder.Result + ?? throw new InvalidOperationException($"The search folder '{folder}' could not be opened."); + await SearchAsync(storageFolder, results, token); + hiddenOnlyFromWin32 = (results.Count != 0); + } + + if (!IsAQSQuery) + { + await SearchWithWin32Async(folder, hiddenOnlyFromWin32, UsedMaxItemCount - (uint)results.Count, results, token); + } + } + } + + private async Task SearchWithWin32Async(string folder, bool hiddenOnly, uint maxItemCount, IList results, CancellationToken token) + { + //var sampler = new IntervalSampler(500); + if (token.IsCancellationRequested) + return; + + (FindCloseSafeHandle? hFile, WIN32_FIND_DATAW findData) = await Task.Run(() => + { + WIN32_FIND_DATAW findDataTsk = default; + FindCloseSafeHandle hFileTsk; + unsafe + { + hFileTsk = PInvoke.FindFirstFileEx($"{folder}\\*{QueryWithWildcard}", 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; + } + + var pendingShortcuts = new List<(string Path, WIN32_FIND_DATAW FindData)>(); + + if (hFile is { IsInvalid: false } findHandle) + { + // Always enter the delegate so the find handle is disposed; cancellation is checked before mutations. + await Task.Run(() => + { + using (findHandle) + { + var hasNextFile = false; + do + { + if (token.IsCancellationRequested) + break; + + if (results.Count >= maxItemCount) + break; + + string fileName = findData.cFileName.ToString(); + var itemPath = Path.Combine(folder, fileName); + var isSystem = ((FileAttributes)findData.dwFileAttributes & FileAttributes.System) == FileAttributes.System; + var isHidden = ((FileAttributes)findData.dwFileAttributes & FileAttributes.Hidden) == FileAttributes.Hidden; + var startWithDot = fileName.StartsWith('.'); + var isShortcut = FileExtensionHelpers.IsShortcutOrUrlFile(fileName); + + bool shouldBeListed = (hiddenOnly ? + (!isHidden && isShortcut) || (isHidden && UserSettingsService.FoldersSettingsService.ShowHiddenItems && (!isSystem || UserSettingsService.FoldersSettingsService.ShowProtectedSystemFiles)) : + !isHidden || (UserSettingsService.FoldersSettingsService.ShowHiddenItems && (!isSystem || UserSettingsService.FoldersSettingsService.ShowProtectedSystemFiles))) && + (!startWithDot || UserSettingsService.FoldersSettingsService.ShowDotFiles); + + if (shouldBeListed) + { + if (isShortcut) + { + pendingShortcuts.Add((itemPath, findData)); + } + else + { + var item = GetListedItemAsync(itemPath, findData); + if (item is not null && !token.IsCancellationRequested) + results.Add(item); + } + } + + if (!token.IsCancellationRequested && (results.Count == 32 || results.Count % 300 == 0 /*|| sampler.CheckNow()*/)) + SearchTick?.Invoke(this, EventArgs.Empty); + hasNextFile = PInvoke.FindNextFile(findHandle, out findData); + } while (hasNextFile); + } + }); + } + else + { + hFile?.Dispose(); + } + + foreach (var (itemPath, itemFindData) in pendingShortcuts) + { + if (results.Count >= maxItemCount || token.IsCancellationRequested) + break; + + string shortcutFileName = itemFindData.cFileName.ToString(); + var isUrl = FileExtensionHelpers.IsWebLinkFile(shortcutFileName); + var shortcutFindData = itemFindData; + var isHidden = ((FileAttributes)shortcutFindData.dwFileAttributes & FileAttributes.Hidden) == FileAttributes.Hidden; + PInvoke.FileTimeToSystemTime(shortcutFindData.ftLastWriteTime, out SYSTEMTIME modifiedTime); + PInvoke.FileTimeToSystemTime(shortcutFindData.ftCreationTime, out SYSTEMTIME createdTime); + var fileSize = Win32FindDataExtensions.GetSize(shortcutFindData); + var itemFileExtension = shortcutFileName.Contains('.', StringComparison.Ordinal) ? Path.GetExtension(itemPath)! : string.Empty; + + var shortcutItem = new ShortcutItem(null) + { + PrimaryItemAttribute = StorageItemTypes.File, + FileExtension = itemFileExtension, + IsHiddenItem = isHidden, + Opacity = isHidden ? Constants.UI.DimItemOpacity : 1, + FileImage = null, + LoadFileIcon = false, + ItemNameRaw = shortcutFileName, + ItemDateModifiedReal = modifiedTime.ToDateTime(), + ItemDateCreatedReal = createdTime.ToDateTime(), + ItemType = isUrl ? Strings.ShortcutWebLinkFileType.GetLocalizedResource() : Strings.Shortcut.GetLocalizedResource(), + ItemPath = itemPath, + FileSize = fileSize.ToSizeString(), + FileSizeBytes = fileSize, + IsUrl = isUrl, + }; + + if (results.Any(r => string.Equals(r.ItemPath, itemPath, StringComparison.OrdinalIgnoreCase))) + continue; + + if (MaxItemCount == 0) + { + _ = FileOperationsHelpers.ParseLinkAsync(itemPath).ContinueWith((t) => + { + if (t.IsCompletedSuccessfully && t.Result is not null) + { + _ = FilesystemTasks.Wrap(() => MainWindow.Instance.DispatcherQueue.EnqueueOrInvokeAsync(() => + { + shortcutItem.TargetPath = t.Result.TargetPath; + shortcutItem.Arguments = t.Result.Arguments; + shortcutItem.WorkingDirectory = t.Result.WorkingDirectory; + shortcutItem.RunAsAdmin = t.Result.RunAsAdmin; + shortcutItem.ShowWindowCommand = t.Result.ShowWindowCommand; + shortcutItem.PrimaryItemAttribute = t.Result.IsFolder ? StorageItemTypes.Folder : StorageItemTypes.File; + })); + } + }); + } + else + { + var iconResult = await FileThumbnailHelper.GetIconAsync( + itemPath, + Constants.ShellIconSizes.Small, + false, + IconOptions.ReturnIconOnly); + if (iconResult is not null) + shortcutItem.FileImage = await iconResult.ToBitmapAsync(); + } + + if (token.IsCancellationRequested) + break; + + results.Add(shortcutItem); + + if (!token.IsCancellationRequested && (results.Count == 32 || results.Count % 300 == 0)) + { + SearchTick?.Invoke(this, EventArgs.Empty); + } + } + if (token.IsCancellationRequested) + return; + + (FindCloseSafeHandle? hSubDir, WIN32_FIND_DATAW subDirData) = await Task.Run(() => + { + WIN32_FIND_DATAW subDirDataTsk = default; + FindCloseSafeHandle hSubDirTsk; + unsafe + { + hSubDirTsk = PInvoke.FindFirstFileEx($"{folder}\\*", FINDEX_INFO_LEVELS.FindExInfoBasic, + &subDirDataTsk, FINDEX_SEARCH_OPS.FindExSearchNameMatch, FIND_FIRST_EX_FLAGS.FIND_FIRST_EX_LARGE_FETCH); + } + return (hSubDirTsk, subDirDataTsk); + }).WithTimeoutAsync(TimeSpan.FromSeconds(5)); + if (token.IsCancellationRequested) + { + hSubDir?.Dispose(); + return; + } + + if (hSubDir is { IsInvalid: false } subDirectoryHandle) + { + var subDirectories = new List(); + + // Always enter the delegate so the find handle is disposed; cancellation is checked before mutations. + await Task.Run(() => + { + using (subDirectoryHandle) + { + var hasNextDir = false; + do + { + if (token.IsCancellationRequested) + break; + + string subDirName = subDirData.cFileName.ToString(); + var isDirectory = ((FileAttributes)subDirData.dwFileAttributes & FileAttributes.Directory) == FileAttributes.Directory; + if (isDirectory && subDirName != "." && subDirName != "..") + subDirectories.Add(Path.Combine(folder, subDirName)); + + hasNextDir = PInvoke.FindNextFile(subDirectoryHandle, out subDirData); + } while (hasNextDir); + } + }); + + foreach (var subDir in subDirectories) + { + if (results.Count >= maxItemCount || token.IsCancellationRequested) + break; + + await SearchWithWin32Async(subDir, hiddenOnly, maxItemCount - (uint)results.Count, results, token); + } + } + else + { + hSubDir?.Dispose(); + } + } + + 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 + }; + } + } + + if (listedItem is not null && MaxItemCount > 0) // Only load icon for searchbox suggestions + { + _ = FileThumbnailHelper.GetIconAsync( + listedItem.ItemPath, + Constants.ShellIconSizes.Small, + isFolder, + IconOptions.ReturnIconOnly) + .ContinueWith((t) => + { + if (t.IsCompletedSuccessfully && t.Result is not null) + { + _ = FilesystemTasks.Wrap(() => MainWindow.Instance.DispatcherQueue.EnqueueOrInvokeAsync(async () => + { + var bitmapImage = await t.Result.ToBitmapAsync(); + if (bitmapImage is not null) + listedItem.FileImage = bitmapImage; + }, Microsoft.UI.Dispatching.DispatcherQueuePriority.Low)); + } + }); + } + + 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, + }; + if (MaxItemCount == 0) + { + _ = FileOperationsHelpers.ParseLinkAsync(file.Path).ContinueWith((t) => + { + if (t.IsCompletedSuccessfully && t.Result is not null) + { + _ = FilesystemTasks.Wrap(() => MainWindow.Instance.DispatcherQueue.EnqueueOrInvokeAsync(() => + { + shortcutItem.TargetPath = t.Result.TargetPath; + shortcutItem.Arguments = t.Result.Arguments; + shortcutItem.WorkingDirectory = t.Result.WorkingDirectory; + shortcutItem.RunAsAdmin = t.Result.RunAsAdmin; + shortcutItem.ShowWindowCommand = t.Result.ShowWindowCommand; + shortcutItem.PrimaryItemAttribute = t.Result.IsFolder ? StorageItemTypes.Folder : StorageItemTypes.File; + })); + } + }); + } + 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 + }; + } + } + if (listedItem is not null && MaxItemCount > 0) // Only load icon for searchbox suggestions + { + var iconResult = await FileThumbnailHelper.GetIconAsync( + item.Path, + Constants.ShellIconSizes.Small, + item.IsOfType(StorageItemTypes.Folder), + IconOptions.ReturnIconOnly); + + if (iconResult is not null) + listedItem.FileImage = await iconResult.ToBitmapAsync(); + } + 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); + query.SetThumbnailPrefetch(ThumbnailMode.ListView, 24, ThumbnailOptions.UseCurrentScale); + + 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 4df8e3b5495e..3a5fdeabb536 100644 --- a/src/Files.App/Utils/Storage/Search/FolderSearch.cs +++ b/src/Files.App/Utils/Storage/Search/FolderSearch.cs @@ -1,31 +1,26 @@ // Copyright (c) Files Community // Licensed under the MIT License. -using Files.Shared.Helpers; using Microsoft.Extensions.Logging; -using System.IO; 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; } @@ -104,12 +99,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 @@ -121,6 +113,7 @@ private async Task AddItemsForHomeAsync(IList results, CancellationT await AddItemsAsync(drive.Path!, results, token); } } +#endif } public async Task> SearchAsync() @@ -150,47 +143,6 @@ public async Task> SearchAsync() return results; } - private async Task SearchAsync(BaseStorageFolder folder, IList results, CancellationToken token) - { - //var sampler = new IntervalSampler(500); - uint index = 0; - var stepSize = Math.Min(defaultStepSize, UsedMaxItemCount); - 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) - results.Add(await GetListedItemAsync(item)); - } - catch (Exception ex) - { - App.Logger.LogWarning(ex, "Error creating ListedItem from StorageItem"); - } - - if (results.Count == 32 || results.Count % 300 == 0 /*|| sampler.CheckNow()*/) - { - SearchTick?.Invoke(this, EventArgs.Empty); - } - } - - index += (uint)items.Count; - stepSize = Math.Min(defaultStepSize, UsedMaxItemCount - (uint)results.Count); - items = await queryResult.GetItemsAsync(index, stepSize).AsTask(token); - } - } - private async Task AddItemsForLibraryAsync(LibraryLocationItem library, IList results, CancellationToken token) { foreach (var folder in library.Folders) @@ -301,592 +253,6 @@ private bool MatchesTagExpression(IEnumerable? fileTags, TagQueryExpress return false; } - private async Task SearchTagsAsync(string folder, IList results, CancellationToken token) - { - //var sampler = new IntervalSampler(500); - 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 && !token.IsCancellationRequested) - results.Add(item); - } - } - } - 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) - { - var listedItem = await GetListedItemAsync(item); - if (!token.IsCancellationRequested) - results.Add(listedItem); - } - } - catch (Exception ex) - { - App.Logger.LogWarning(ex, "Error creating ListedItem from StorageItem"); - } - } - - if (token.IsCancellationRequested) - return; - - if (results.Count == 32 || results.Count % 300 == 0 /*|| sampler.CheckNow()*/) - { - SearchTick?.Invoke(this, EventArgs.Empty); - } - } - } - - 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 hiddenOnlyFromWin32 = false; - if (workingFolder) - { - var storageFolder = workingFolder.Result - ?? throw new InvalidOperationException($"The search folder '{folder}' could not be opened."); - await SearchAsync(storageFolder, results, token); - hiddenOnlyFromWin32 = (results.Count != 0); - } - - if (!IsAQSQuery) - { - await SearchWithWin32Async(folder, hiddenOnlyFromWin32, UsedMaxItemCount - (uint)results.Count, results, token); - } - } - } - - private async Task SearchWithWin32Async(string folder, bool hiddenOnly, uint maxItemCount, IList results, CancellationToken token) - { - //var sampler = new IntervalSampler(500); - if (token.IsCancellationRequested) - return; - - (FindCloseSafeHandle? hFile, WIN32_FIND_DATAW findData) = await Task.Run(() => - { - WIN32_FIND_DATAW findDataTsk = default; - FindCloseSafeHandle hFileTsk; - unsafe - { - hFileTsk = PInvoke.FindFirstFileEx($"{folder}\\*{QueryWithWildcard}", 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; - } - - var pendingShortcuts = new List<(string Path, WIN32_FIND_DATAW FindData)>(); - - if (hFile is { IsInvalid: false } findHandle) - { - // Always enter the delegate so the find handle is disposed; cancellation is checked before mutations. - await Task.Run(() => - { - using (findHandle) - { - var hasNextFile = false; - do - { - if (token.IsCancellationRequested) - break; - - if (results.Count >= maxItemCount) - break; - - string fileName = findData.cFileName.ToString(); - var itemPath = Path.Combine(folder, fileName); - var isSystem = ((FileAttributes)findData.dwFileAttributes & FileAttributes.System) == FileAttributes.System; - var isHidden = ((FileAttributes)findData.dwFileAttributes & FileAttributes.Hidden) == FileAttributes.Hidden; - var startWithDot = fileName.StartsWith('.'); - var isShortcut = FileExtensionHelpers.IsShortcutOrUrlFile(fileName); - - bool shouldBeListed = (hiddenOnly ? - (!isHidden && isShortcut) || (isHidden && UserSettingsService.FoldersSettingsService.ShowHiddenItems && (!isSystem || UserSettingsService.FoldersSettingsService.ShowProtectedSystemFiles)) : - !isHidden || (UserSettingsService.FoldersSettingsService.ShowHiddenItems && (!isSystem || UserSettingsService.FoldersSettingsService.ShowProtectedSystemFiles))) && - (!startWithDot || UserSettingsService.FoldersSettingsService.ShowDotFiles); - - if (shouldBeListed) - { - if (isShortcut) - { - pendingShortcuts.Add((itemPath, findData)); - } - else - { - var item = GetListedItemAsync(itemPath, findData); - if (item is not null && !token.IsCancellationRequested) - results.Add(item); - } - } - - if (!token.IsCancellationRequested && (results.Count == 32 || results.Count % 300 == 0 /*|| sampler.CheckNow()*/)) - SearchTick?.Invoke(this, EventArgs.Empty); - hasNextFile = PInvoke.FindNextFile(findHandle, out findData); - } while (hasNextFile); - } - }); - } - else - { - hFile?.Dispose(); - } - - foreach (var (itemPath, itemFindData) in pendingShortcuts) - { - if (results.Count >= maxItemCount || token.IsCancellationRequested) - break; - - string shortcutFileName = itemFindData.cFileName.ToString(); - var isUrl = FileExtensionHelpers.IsWebLinkFile(shortcutFileName); - var shortcutFindData = itemFindData; - var isHidden = ((FileAttributes)shortcutFindData.dwFileAttributes & FileAttributes.Hidden) == FileAttributes.Hidden; - PInvoke.FileTimeToSystemTime(shortcutFindData.ftLastWriteTime, out SYSTEMTIME modifiedTime); - PInvoke.FileTimeToSystemTime(shortcutFindData.ftCreationTime, out SYSTEMTIME createdTime); - var fileSize = Win32FindDataExtensions.GetSize(shortcutFindData); - var itemFileExtension = shortcutFileName.Contains('.', StringComparison.Ordinal) ? Path.GetExtension(itemPath)! : string.Empty; - - var shortcutItem = new ShortcutItem(null) - { - PrimaryItemAttribute = StorageItemTypes.File, - FileExtension = itemFileExtension, - IsHiddenItem = isHidden, - Opacity = isHidden ? Constants.UI.DimItemOpacity : 1, - FileImage = null, - LoadFileIcon = false, - ItemNameRaw = shortcutFileName, - ItemDateModifiedReal = modifiedTime.ToDateTime(), - ItemDateCreatedReal = createdTime.ToDateTime(), - ItemType = isUrl ? Strings.ShortcutWebLinkFileType.GetLocalizedResource() : Strings.Shortcut.GetLocalizedResource(), - ItemPath = itemPath, - FileSize = fileSize.ToSizeString(), - FileSizeBytes = fileSize, - IsUrl = isUrl, - }; - - if (results.Any(r => string.Equals(r.ItemPath, itemPath, StringComparison.OrdinalIgnoreCase))) - continue; - - if (MaxItemCount == 0) - { - _ = FileOperationsHelpers.ParseLinkAsync(itemPath).ContinueWith((t) => - { - if (t.IsCompletedSuccessfully && t.Result is not null) - { - _ = FilesystemTasks.Wrap(() => MainWindow.Instance.DispatcherQueue.EnqueueOrInvokeAsync(() => - { - shortcutItem.TargetPath = t.Result.TargetPath; - shortcutItem.Arguments = t.Result.Arguments; - shortcutItem.WorkingDirectory = t.Result.WorkingDirectory; - shortcutItem.RunAsAdmin = t.Result.RunAsAdmin; - shortcutItem.ShowWindowCommand = t.Result.ShowWindowCommand; - shortcutItem.PrimaryItemAttribute = t.Result.IsFolder ? StorageItemTypes.Folder : StorageItemTypes.File; - })); - } - }); - } - else - { - var iconResult = await FileThumbnailHelper.GetIconAsync( - itemPath, - Constants.ShellIconSizes.Small, - false, - IconOptions.ReturnIconOnly); - if (iconResult is not null) - shortcutItem.FileImage = await iconResult.ToBitmapAsync(); - } - - if (token.IsCancellationRequested) - break; - - results.Add(shortcutItem); - - if (!token.IsCancellationRequested && (results.Count == 32 || results.Count % 300 == 0)) - { - SearchTick?.Invoke(this, EventArgs.Empty); - } - } - if (token.IsCancellationRequested) - return; - - (FindCloseSafeHandle? hSubDir, WIN32_FIND_DATAW subDirData) = await Task.Run(() => - { - WIN32_FIND_DATAW subDirDataTsk = default; - FindCloseSafeHandle hSubDirTsk; - unsafe - { - hSubDirTsk = PInvoke.FindFirstFileEx($"{folder}\\*", FINDEX_INFO_LEVELS.FindExInfoBasic, - &subDirDataTsk, FINDEX_SEARCH_OPS.FindExSearchNameMatch, FIND_FIRST_EX_FLAGS.FIND_FIRST_EX_LARGE_FETCH); - } - return (hSubDirTsk, subDirDataTsk); - }).WithTimeoutAsync(TimeSpan.FromSeconds(5)); - if (token.IsCancellationRequested) - { - hSubDir?.Dispose(); - return; - } - - if (hSubDir is { IsInvalid: false } subDirectoryHandle) - { - var subDirectories = new List(); - - // Always enter the delegate so the find handle is disposed; cancellation is checked before mutations. - await Task.Run(() => - { - using (subDirectoryHandle) - { - var hasNextDir = false; - do - { - if (token.IsCancellationRequested) - break; - - string subDirName = subDirData.cFileName.ToString(); - var isDirectory = ((FileAttributes)subDirData.dwFileAttributes & FileAttributes.Directory) == FileAttributes.Directory; - if (isDirectory && subDirName != "." && subDirName != "..") - subDirectories.Add(Path.Combine(folder, subDirName)); - - hasNextDir = PInvoke.FindNextFile(subDirectoryHandle, out subDirData); - } while (hasNextDir); - } - }); - - foreach (var subDir in subDirectories) - { - if (results.Count >= maxItemCount || token.IsCancellationRequested) - break; - - await SearchWithWin32Async(subDir, hiddenOnly, maxItemCount - (uint)results.Count, results, token); - } - } - else - { - hSubDir?.Dispose(); - } - } - - 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 - }; - } - } - - if (listedItem is not null && MaxItemCount > 0) // Only load icon for searchbox suggestions - { - _ = FileThumbnailHelper.GetIconAsync( - listedItem.ItemPath, - Constants.ShellIconSizes.Small, - isFolder, - IconOptions.ReturnIconOnly) - .ContinueWith((t) => - { - if (t.IsCompletedSuccessfully && t.Result is not null) - { - _ = FilesystemTasks.Wrap(() => MainWindow.Instance.DispatcherQueue.EnqueueOrInvokeAsync(async () => - { - var bitmapImage = await t.Result.ToBitmapAsync(); - if (bitmapImage is not null) - listedItem.FileImage = bitmapImage; - }, Microsoft.UI.Dispatching.DispatcherQueuePriority.Low)); - } - }); - } - - 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, - }; - if (MaxItemCount == 0) - { - _ = FileOperationsHelpers.ParseLinkAsync(file.Path).ContinueWith((t) => - { - if (t.IsCompletedSuccessfully && t.Result is not null) - { - _ = FilesystemTasks.Wrap(() => MainWindow.Instance.DispatcherQueue.EnqueueOrInvokeAsync(() => - { - shortcutItem.TargetPath = t.Result.TargetPath; - shortcutItem.Arguments = t.Result.Arguments; - shortcutItem.WorkingDirectory = t.Result.WorkingDirectory; - shortcutItem.RunAsAdmin = t.Result.RunAsAdmin; - shortcutItem.ShowWindowCommand = t.Result.ShowWindowCommand; - shortcutItem.PrimaryItemAttribute = t.Result.IsFolder ? StorageItemTypes.Folder : StorageItemTypes.File; - })); - } - }); - } - 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 - }; - } - } - if (listedItem is not null && MaxItemCount > 0) // Only load icon for searchbox suggestions - { - var iconResult = await FileThumbnailHelper.GetIconAsync( - item.Path, - Constants.ShellIconSizes.Small, - item.IsOfType(StorageItemTypes.Folder), - IconOptions.ReturnIconOnly); - - if (iconResult is not null) - listedItem.FileImage = await iconResult.ToBitmapAsync(); - } - 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); - query.SetThumbnailPrefetch(ThumbnailMode.ListView, 24, ThumbnailOptions.UseCurrentScale); - - 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..7ee52f728141 100644 --- a/src/Files.App/Utils/Storage/StorageBaseItems/BaseStorageFile.cs +++ b/src/Files.App/Utils/Storage/StorageBaseItems/BaseStorageFile.cs @@ -39,6 +39,7 @@ public StorageProvider? Provider public abstract string FolderRelativeId { get; } public abstract IStorageItemExtraProperties Properties { get; } +#if WINDOWS [MaybeNull] StorageItemContentProperties IStorageItemProperties.Properties => this is SystemStorageFile file ? file.File.Properties : null; @@ -48,6 +49,13 @@ StorageItemContentProperties IStorageItemProperties.Properties return value is not null ? new SystemStorageFile(value) : null; } +#else + [MaybeNull] + StorageItemContentProperties IStorageItemProperties.Properties + => null; + +#endif + public abstract IAsyncOperation ToStorageFileAsync(); public abstract bool IsEqual(IStorageItem item); @@ -165,12 +173,18 @@ 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) + => throw new NotSupportedException("Use IStorableResolver instead of the legacy WinRT storage layer."); +#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..5b60401da332 100644 --- a/src/Files.App/Utils/Storage/StorageBaseItems/BaseStorageFolder.cs +++ b/src/Files.App/Utils/Storage/StorageBaseItems/BaseStorageFolder.cs @@ -31,6 +31,7 @@ public StorageProvider? Provider public abstract IStorageItemExtraProperties Properties { get; } +#if WINDOWS [MaybeNull] StorageItemContentProperties IStorageItemProperties.Properties => this is SystemStorageFolder folder ? folder.Folder.Properties : null; @@ -43,6 +44,13 @@ value is not null : null; } +#else + [MaybeNull] + StorageItemContentProperties IStorageItemProperties.Properties + => null; + +#endif + public abstract IAsyncOperation ToStorageFolderAsync(); public abstract bool IsEqual(IStorageItem item); @@ -139,12 +147,18 @@ 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) + => throw new NotSupportedException("Use IStorableResolver instead of the legacy WinRT storage layer."); +#endif public abstract IAsyncOperation?> GetFoldersAsync(); diff --git a/src/Files.App/Utils/Storage/StorageItems/BaseQueryResults.cs b/src/Files.App/Utils/Storage/StorageItems/BaseQueryResults.cs index 27b906bd6875..1d9d3cd3e233 100644 --- a/src/Files.App/Utils/Storage/StorageItems/BaseQueryResults.cs +++ b/src/Files.App/Utils/Storage/StorageItems/BaseQueryResults.cs @@ -166,6 +166,7 @@ public virtual IAsyncOperation> GetFoldersAsync public virtual StorageFolderQueryResult? ToStorageFolderQueryResult() => null; } +#if WINDOWS public sealed class SystemStorageItemQueryResult : BaseStorageItemQueryResult { private StorageItemQueryResult StorageItemQueryResult { get; } @@ -278,4 +279,5 @@ public override IAsyncOperation> GetFoldersAsyn public override StorageFolderQueryResult ToStorageFolderQueryResult() => StorageFolderQueryResult; } +#endif } diff --git a/src/Files.App/Utils/Storage/StorageItems/FtpStorageFile.cs b/src/Files.App/Utils/Storage/StorageItems/FtpStorageFile.cs index 043d19733dc4..0954e25531c5 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.Windows.cs similarity index 100% rename from src/Files.App/Utils/Storage/StorageItems/SystemStorageFile.cs rename to src/Files.App/Utils/Storage/StorageItems/SystemStorageFile.Windows.cs diff --git a/src/Files.App/Utils/Storage/StorageItems/SystemStorageFolder.cs b/src/Files.App/Utils/Storage/StorageItems/SystemStorageFolder.Windows.cs similarity index 100% rename from src/Files.App/Utils/Storage/StorageItems/SystemStorageFolder.cs rename to src/Files.App/Utils/Storage/StorageItems/SystemStorageFolder.Windows.cs 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/Utils/Storage/StorageItems/ZipStorageFile.cs b/src/Files.App/Utils/Storage/StorageItems/ZipStorageFile.cs index 59ba26636d8e..f896f646b7d1 100644 --- a/src/Files.App/Utils/Storage/StorageItems/ZipStorageFile.cs +++ b/src/Files.App/Utils/Storage/StorageItems/ZipStorageFile.cs @@ -214,7 +214,12 @@ public override IAsyncOperation GetBasicPropertiesAsync() } var hFile = Win32Helper.OpenFileForRead(containerPath); +#if WINDOWS return hFile.IsInvalid ? null : new StreamWithContentType(new FileStream(hFile, FileAccess.Read).AsRandomAccessStream()); +#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 } SevenZipExtractor? zipFile = await OpenZipFileAsync(); @@ -237,7 +242,12 @@ public override IAsyncOperation GetBasicPropertiesAsync() { DisposeCallback = () => zipFile.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)); } @@ -268,7 +278,12 @@ public override IAsyncOperation GetBasicPropertiesAsync() } ms.Position = 0; var nsStream = new NonSeekableRandomAccessStreamForRead(ms, (ulong)entry.Size); +#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 } } return null; diff --git a/src/Files.App/Utils/Storage/StorageItems/ZipStorageFolder.cs b/src/Files.App/Utils/Storage/StorageItems/ZipStorageFolder.cs index b72ef63f46ab..c94bcf10ab52 100644 --- a/src/Files.App/Utils/Storage/StorageItems/ZipStorageFolder.cs +++ b/src/Files.App/Utils/Storage/StorageItems/ZipStorageFolder.cs @@ -228,8 +228,13 @@ public override IAsyncOperation GetBasicPropertiesAsync() { if (Path == containerPath) { +#if WINDOWS var zipFile = new SystemStorageFile(await StorageFile.GetFileFromPathAsync(Path)); return await zipFile.GetBasicPropertiesAsync(); +#else + // LINUX-TODO(archives): use ArchiveFolder metadata for the archive root. + throw new NotSupportedException("Legacy archive metadata is Windows-only."); +#endif } if (CurrentEncoding is not null) diff --git a/src/Files.App/ViewModels/ShellViewModel.cs b/src/Files.App/ViewModels/ShellViewModel.cs index a31118048ca8..9d2a03ff32f9 100644 --- a/src/Files.App/ViewModels/ShellViewModel.cs +++ b/src/Files.App/ViewModels/ShellViewModel.cs @@ -2292,7 +2292,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); @@ -2542,10 +2542,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; @@ -2597,6 +2599,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() @@ -3186,10 +3192,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) @@ -3210,6 +3221,7 @@ private async Task AddFileOrFolderAsync(ListedItem? item) { filesAndFolders.Add(item); +#if WINDOWS if (UserSettingsService.FoldersSettingsService.AreAlternateStreamsVisible) { // New file added, enumerate ADS @@ -3219,6 +3231,7 @@ private async Task AddFileOrFolderAsync(ListedItem? item) filesAndFolders.Add(adsItem); } } +#endif } enumFolderSemaphore.Release(); @@ -3226,6 +3239,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; @@ -3268,6 +3282,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 7b9e768cb2b4..0ffb2fb6a541 100644 --- a/src/Files.App/Views/Layouts/BaseLayoutPage.cs +++ b/src/Files.App/Views/Layouts/BaseLayoutPage.cs @@ -1086,8 +1086,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 From 7812d42f54099a446f3b3359e5a97c458449afe6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kov=C3=A1cs=20B=C3=A1lint=20Hunor?= Date: Mon, 5 Oct 2026 21:08:42 +0300 Subject: [PATCH 2/5] Linux: working legacy item on resolver success, SystemStorageFile/Folder wrappers on Linux Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01LeKXJc3DnK3PzAqwgA72NY --- src/Files.App/Files.App.csproj | 3 +++ .../Helpers/StorageFileExtensions.Linux.cs | 23 +++++++++++++++---- .../Utils/Storage/Helpers/StorageHelpers.cs | 19 ++++++++++++--- .../StorageBaseItems/BaseStorageFile.cs | 12 ++++------ .../StorageBaseItems/BaseStorageFolder.cs | 12 ++++------ .../Storage/StorageItems/BaseQueryResults.cs | 2 -- .../StorageItems/SystemStorageFile.Windows.cs | 7 ++++++ .../SystemStorageFolder.Windows.cs | 2 ++ 8 files changed, 55 insertions(+), 25 deletions(-) diff --git a/src/Files.App/Files.App.csproj b/src/Files.App/Files.App.csproj index 9d86c429d060..4054cda5d890 100644 --- a/src/Files.App/Files.App.csproj +++ b/src/Files.App/Files.App.csproj @@ -267,6 +267,9 @@ + + + diff --git a/src/Files.App/Utils/Storage/Helpers/StorageFileExtensions.Linux.cs b/src/Files.App/Utils/Storage/Helpers/StorageFileExtensions.Linux.cs index 0d4bdc432e35..ee47d91175f8 100644 --- a/src/Files.App/Utils/Storage/Helpers/StorageFileExtensions.Linux.cs +++ b/src/Files.App/Utils/Storage/Helpers/StorageFileExtensions.Linux.cs @@ -21,14 +21,14 @@ public static Task> ToStandardStorageItemsAsync(this IEnumera var path = ResolveLegacyPath(value, parentFolder); var result = await StorageHelpers.GetFileAsync(path); ThrowLegacyResolutionError(result.ErrorCode, path); - return null; + 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(null, path); + return new(await OpenLegacyFileAsync(path), path); } public static async Task DangerousGetFolderFromPathAsync(string value, StorageFolderWithPath? rootFolder = null, StorageFolderWithPath? parentFolder = null) @@ -36,14 +36,27 @@ public static async Task DangerousGetFileWithPathFromPathAs var path = ResolveLegacyPath(value, parentFolder); var result = await StorageHelpers.GetFolderAsync(path); ThrowLegacyResolutionError(result.ErrorCode, path); - return null; + 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(null, 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) + { + 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) + { + 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) @@ -52,6 +65,8 @@ private static string ResolveLegacyPath(string path, StorageFolderWithPath? pare 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) diff --git a/src/Files.App/Utils/Storage/Helpers/StorageHelpers.cs b/src/Files.App/Utils/Storage/Helpers/StorageHelpers.cs index 70d11bf8c517..d89a5751f889 100644 --- a/src/Files.App/Utils/Storage/Helpers/StorageHelpers.cs +++ b/src/Files.App/Utils/Storage/Helpers/StorageHelpers.cs @@ -15,14 +15,27 @@ public static partial class StorageHelpers public static async Task ToStorageItem(string path) where TRequested : IStorageItem { - await GetStorableAsync(path); - return default; + var result = await GetStorableAsync(path); + if (!result) + return default; + + 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 result = await item.ToStorableResult(); - return new(null, result ? FileSystemStatusCode.Generic : result.ErrorCode); + 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) diff --git a/src/Files.App/Utils/Storage/StorageBaseItems/BaseStorageFile.cs b/src/Files.App/Utils/Storage/StorageBaseItems/BaseStorageFile.cs index 7ee52f728141..92d92ac8bc8b 100644 --- a/src/Files.App/Utils/Storage/StorageBaseItems/BaseStorageFile.cs +++ b/src/Files.App/Utils/Storage/StorageBaseItems/BaseStorageFile.cs @@ -39,7 +39,6 @@ public StorageProvider? Provider public abstract string FolderRelativeId { get; } public abstract IStorageItemExtraProperties Properties { get; } -#if WINDOWS [MaybeNull] StorageItemContentProperties IStorageItemProperties.Properties => this is SystemStorageFile file ? file.File.Properties : null; @@ -49,12 +48,6 @@ StorageItemContentProperties IStorageItemProperties.Properties return value is not null ? new SystemStorageFile(value) : null; } -#else - [MaybeNull] - StorageItemContentProperties IStorageItemProperties.Properties - => null; - -#endif public abstract IAsyncOperation ToStorageFileAsync(); @@ -183,7 +176,10 @@ IAsyncOperation IStorageFile.CopyAsync(IStorageFolder destinationFo #else // LINUX-TODO(storage): remove legacy WinRT resolution after downstream consumers use IStorableResolver. public static IAsyncOperation GetFileFromPathAsync(string path) - => throw new NotSupportedException("Use IStorableResolver instead of the legacy WinRT storage layer."); + { + 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 5b60401da332..9f5935afb35e 100644 --- a/src/Files.App/Utils/Storage/StorageBaseItems/BaseStorageFolder.cs +++ b/src/Files.App/Utils/Storage/StorageBaseItems/BaseStorageFolder.cs @@ -31,7 +31,6 @@ public StorageProvider? Provider public abstract IStorageItemExtraProperties Properties { get; } -#if WINDOWS [MaybeNull] StorageItemContentProperties IStorageItemProperties.Properties => this is SystemStorageFolder folder ? folder.Folder.Properties : null; @@ -44,12 +43,6 @@ value is not null : null; } -#else - [MaybeNull] - StorageItemContentProperties IStorageItemProperties.Properties - => null; - -#endif public abstract IAsyncOperation ToStorageFolderAsync(); @@ -157,7 +150,10 @@ IAsyncOperation IStorageFolder.GetFolderAsync(string name) #else // LINUX-TODO(storage): remove legacy WinRT resolution after downstream consumers use IStorableResolver. public static IAsyncOperation GetFolderFromPathAsync(string path) - => throw new NotSupportedException("Use IStorableResolver instead of the legacy WinRT storage layer."); + { + 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/BaseQueryResults.cs b/src/Files.App/Utils/Storage/StorageItems/BaseQueryResults.cs index 1d9d3cd3e233..27b906bd6875 100644 --- a/src/Files.App/Utils/Storage/StorageItems/BaseQueryResults.cs +++ b/src/Files.App/Utils/Storage/StorageItems/BaseQueryResults.cs @@ -166,7 +166,6 @@ public virtual IAsyncOperation> GetFoldersAsync public virtual StorageFolderQueryResult? ToStorageFolderQueryResult() => null; } -#if WINDOWS public sealed class SystemStorageItemQueryResult : BaseStorageItemQueryResult { private StorageItemQueryResult StorageItemQueryResult { get; } @@ -279,5 +278,4 @@ public override IAsyncOperation> GetFoldersAsyn public override StorageFolderQueryResult ToStorageFolderQueryResult() => StorageFolderQueryResult; } -#endif } diff --git a/src/Files.App/Utils/Storage/StorageItems/SystemStorageFile.Windows.cs b/src/Files.App/Utils/Storage/StorageItems/SystemStorageFile.Windows.cs index d45d887e340e..f75a47c68c3c 100644 --- a/src/Files.App/Utils/Storage/StorageItems/SystemStorageFile.Windows.cs +++ b/src/Files.App/Utils/Storage/StorageItems/SystemStorageFile.Windows.cs @@ -108,6 +108,7 @@ public override IAsyncOperation GetBasicPropertiesAsync() return destFile; } } +#if WINDOWS catch (UnauthorizedAccessException) // shortcuts & .url { if (!string.IsNullOrEmpty(destFolder.Path)) @@ -128,6 +129,12 @@ public override IAsyncOperation GetBasicPropertiesAsync() } throw; } +#else + catch (UnauthorizedAccessException) + { + throw; + } +#endif }); } diff --git a/src/Files.App/Utils/Storage/StorageItems/SystemStorageFolder.Windows.cs b/src/Files.App/Utils/Storage/StorageItems/SystemStorageFolder.Windows.cs index cc61e4c6e2e7..cfffbe9483ff 100644 --- a/src/Files.App/Utils/Storage/StorageItems/SystemStorageFolder.Windows.cs +++ b/src/Files.App/Utils/Storage/StorageItems/SystemStorageFolder.Windows.cs @@ -34,6 +34,7 @@ public SystemStorageFolder(StorageFolder folder) public static IAsyncOperation FromPathAsync(string path) { +#if WINDOWS if (path.EndsWith(ShellLibraryItem.EXTENSION)) { try @@ -56,6 +57,7 @@ public static IAsyncOperation FromPathAsync(string path) App.Logger.LogWarning(e, null); } } +#endif return AsyncInfo.Run(async (cancellationToken) => new SystemStorageFolder(await StorageFolder.GetFolderFromPathAsync(path))); } From b09be39462b6e244ca264566831d1015d1b7f703 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kov=C3=A1cs=20B=C3=A1lint=20Hunor?= Date: Mon, 5 Oct 2026 21:16:09 +0300 Subject: [PATCH 3/5] Refresh compat-off baseline after storage wrappers Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01LeKXJc3DnK3PzAqwgA72NY --- docs/linux-port/compat-off-baseline.txt | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/docs/linux-port/compat-off-baseline.txt b/docs/linux-port/compat-off-baseline.txt index 30c8382d1b40..1c7d0b24827b 100644 --- a/docs/linux-port/compat-off-baseline.txt +++ b/docs/linux-port/compat-off-baseline.txt @@ -11,21 +11,7 @@ src/Files.App/Data/Models/RemovableDevice.cs: error CS0234: The type or namespac src/Files.App/Data/Models/RemovableDevice.cs: error CS0234: The type or namespace name 'Win32PInvoke' does not exist in the namespace 'Files.App.Helpers' (are you missing an assembly reference?) src/Files.App/Helpers/UI/UIFilesystemHelpers.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/UserControls/Toolbar.xaml.cs: error CS0246: The type or namespace name 'OpenWithMenu' 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/Helpers/SyncRootHelpers.cs: error CS0234: The type or namespace name 'WinRT' does not exist in the namespace 'Windows.Win32.System' (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?) src/Files.App/ViewModels/UserControls/NavigationToolbarViewModel.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/UserControls/Widgets/QuickAccessWidgetViewModel.cs: error CS0234: The type or namespace name 'WinRT' does not exist in the namespace 'Windows.Win32.System' (are you missing an assembly reference?) From 1b1f7e293d39d929b895d5ab1f225afc93015821 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kov=C3=A1cs=20B=C3=A1lint=20Hunor?= Date: Mon, 5 Oct 2026 22:22:21 +0300 Subject: [PATCH 4/5] Linux: safe SystemStorage members, existence-checked legacy items, drop .Windows suffix on wrappers Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01LeKXJc3DnK3PzAqwgA72NY --- src/Files.App/Files.App.csproj | 3 --- .../Helpers/StorageFileExtensions.Linux.cs | 5 +++++ .../Storage/Search/FolderSearch.Windows.cs | 3 ++- .../Storage/StorageBaseItems/BaseStorageFile.cs | 4 ++++ .../StorageBaseItems/BaseStorageFolder.cs | 4 ++++ ...rageFile.Windows.cs => SystemStorageFile.cs} | 17 +++++++++++++++++ ...Folder.Windows.cs => SystemStorageFolder.cs} | 17 +++++++++++++++++ 7 files changed, 49 insertions(+), 4 deletions(-) rename src/Files.App/Utils/Storage/StorageItems/{SystemStorageFile.Windows.cs => SystemStorageFile.cs} (96%) rename src/Files.App/Utils/Storage/StorageItems/{SystemStorageFolder.Windows.cs => SystemStorageFolder.cs} (96%) diff --git a/src/Files.App/Files.App.csproj b/src/Files.App/Files.App.csproj index 4054cda5d890..9d86c429d060 100644 --- a/src/Files.App/Files.App.csproj +++ b/src/Files.App/Files.App.csproj @@ -267,9 +267,6 @@ - - - diff --git a/src/Files.App/Utils/Storage/Helpers/StorageFileExtensions.Linux.cs b/src/Files.App/Utils/Storage/Helpers/StorageFileExtensions.Linux.cs index ee47d91175f8..ee289c28dcdc 100644 --- a/src/Files.App/Utils/Storage/Helpers/StorageFileExtensions.Linux.cs +++ b/src/Files.App/Utils/Storage/Helpers/StorageFileExtensions.Linux.cs @@ -49,12 +49,17 @@ public static async Task DangerousGetFolderWithPathFromPa /// 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; } } diff --git a/src/Files.App/Utils/Storage/Search/FolderSearch.Windows.cs b/src/Files.App/Utils/Storage/Search/FolderSearch.Windows.cs index b5710a431c64..fbec1b502eb3 100644 --- a/src/Files.App/Utils/Storage/Search/FolderSearch.Windows.cs +++ b/src/Files.App/Utils/Storage/Search/FolderSearch.Windows.cs @@ -558,5 +558,6 @@ private static Task> GetStorageFolderAsync(s => FilesystemTasks.WrapNullable(() => StorageFileExtensions.DangerousGetFolderFromPathAsync(path)); private static Task> GetStorageFileAsync(string path) - => FilesystemTasks.WrapNullable(() => StorageFileExtensions.DangerousGetFileFromPathAsync(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 92d92ac8bc8b..3414df304251 100644 --- a/src/Files.App/Utils/Storage/StorageBaseItems/BaseStorageFile.cs +++ b/src/Files.App/Utils/Storage/StorageBaseItems/BaseStorageFile.cs @@ -41,7 +41,11 @@ 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) { diff --git a/src/Files.App/Utils/Storage/StorageBaseItems/BaseStorageFolder.cs b/src/Files.App/Utils/Storage/StorageBaseItems/BaseStorageFolder.cs index 9f5935afb35e..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) { diff --git a/src/Files.App/Utils/Storage/StorageItems/SystemStorageFile.Windows.cs b/src/Files.App/Utils/Storage/StorageItems/SystemStorageFile.cs similarity index 96% rename from src/Files.App/Utils/Storage/StorageItems/SystemStorageFile.Windows.cs rename to src/Files.App/Utils/Storage/StorageItems/SystemStorageFile.cs index f75a47c68c3c..017bf61e7780 100644 --- a/src/Files.App/Utils/Storage/StorageItems/SystemStorageFile.Windows.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) { diff --git a/src/Files.App/Utils/Storage/StorageItems/SystemStorageFolder.Windows.cs b/src/Files.App/Utils/Storage/StorageItems/SystemStorageFolder.cs similarity index 96% rename from src/Files.App/Utils/Storage/StorageItems/SystemStorageFolder.Windows.cs rename to src/Files.App/Utils/Storage/StorageItems/SystemStorageFolder.cs index cfffbe9483ff..3c4e5b3e130f 100644 --- a/src/Files.App/Utils/Storage/StorageItems/SystemStorageFolder.Windows.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) { From 35330020e26c5dd6fadaf507ed15038150de8b9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kov=C3=A1cs=20B=C3=A1lint=20Hunor?= Date: Mon, 5 Oct 2026 22:28:16 +0300 Subject: [PATCH 5/5] Refresh compat-off baseline after merge Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01LeKXJc3DnK3PzAqwgA72NY --- docs/linux-port/compat-off-baseline.txt | 14 -------------- 1 file changed, 14 deletions(-) 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?)