From e7b8c445e955eac4817794964fe43793540c08da Mon Sep 17 00:00:00 2001 From: 0x5BFA <62196528+0x5bfa@users.noreply.github.com> Date: Mon, 8 Jul 2024 07:32:42 +0900 Subject: [PATCH 1/4] Init --- src/Files.App.CsWin32/NativeMethods.json | 7 +- src/Files.App.CsWin32/NativeMethods.txt | 11 ++ src/Files.App.CsWin32/Windows.Win32.Extras.cs | 34 ++++ .../Windows.Win32.MONITORENUMPROC.cs | 10 - .../Windows.Win32.WNDPROC.cs | 10 - .../Watchers/RecycleBinWatcher.cs | 108 +++++++++++ .../FileSystem/EmptyRecycleBinAction.cs | 34 +++- .../FileSystem/RestoreAllRecycleBinAction.cs | 40 +++- .../FileSystem/RestoreRecycleBinAction.cs | 32 +++- .../Contracts/IWindowsRecycleBinService.cs | 67 +++++++ src/Files.App/Data/Items/ListedItem.cs | 4 +- src/Files.App/Data/Items/LocationItem.cs | 12 +- src/Files.App/GlobalUsings.cs | 6 +- .../Helpers/Application/AppLifecycleHelper.cs | 1 + .../Helpers/Win32/Win32Helper.Shell.cs | 19 -- .../Helpers/Win32/Win32PInvoke.Methods.cs | 9 +- .../Helpers/Win32/Win32PInvoke.Structs.cs | 12 -- src/Files.App/Services/App/FileTagsService.cs | 8 +- .../Windows/WindowsRecycleBinService.cs | 145 ++++++++++++++ .../Utils/RecycleBin/RecycleBinHelpers.cs | 180 ------------------ .../Utils/RecycleBin/RecycleBinManager.cs | 113 ----------- .../Storage/Operations/FilesystemHelpers.cs | 19 +- .../Operations/FilesystemOperations.cs | 8 +- .../Operations/ShellFilesystemOperations.cs | 8 +- .../Utils/Storage/Search/FolderSearch.cs | 3 +- .../Properties/Items/FolderProperties.cs | 4 +- src/Files.App/ViewModels/ShellViewModel.cs | 13 +- src/Files.App/Views/Layouts/BaseLayoutPage.cs | 3 +- .../Contracts/ITrashWatcher.cs | 4 +- src/Files.Core.Storage/Contracts/IWatcher.cs | 4 +- 30 files changed, 531 insertions(+), 397 deletions(-) create mode 100644 src/Files.App.CsWin32/Windows.Win32.Extras.cs delete mode 100644 src/Files.App.CsWin32/Windows.Win32.MONITORENUMPROC.cs delete mode 100644 src/Files.App.CsWin32/Windows.Win32.WNDPROC.cs create mode 100644 src/Files.App.Storage/Watchers/RecycleBinWatcher.cs create mode 100644 src/Files.App/Data/Contracts/IWindowsRecycleBinService.cs create mode 100644 src/Files.App/Services/Windows/WindowsRecycleBinService.cs delete mode 100644 src/Files.App/Utils/RecycleBin/RecycleBinHelpers.cs delete mode 100644 src/Files.App/Utils/RecycleBin/RecycleBinManager.cs diff --git a/src/Files.App.CsWin32/NativeMethods.json b/src/Files.App.CsWin32/NativeMethods.json index 40ae5356353c..9ed0c8e95460 100644 --- a/src/Files.App.CsWin32/NativeMethods.json +++ b/src/Files.App.CsWin32/NativeMethods.json @@ -1,5 +1,10 @@ { "$schema": "https://aka.ms/CsWin32.schema.json", "allowMarshaling": false, - "public": true + "public": true, + "comInterop": { + "preserveSigMethods": [ + "IEnumShellItems.Next" + ] + } } diff --git a/src/Files.App.CsWin32/NativeMethods.txt b/src/Files.App.CsWin32/NativeMethods.txt index ff3a58961d46..c4af40440c13 100644 --- a/src/Files.App.CsWin32/NativeMethods.txt +++ b/src/Files.App.CsWin32/NativeMethods.txt @@ -121,3 +121,14 @@ WindowsDeleteString IPreviewHandler AssocQueryString GetModuleHandle +SHEmptyRecycleBin +SHFileOperation +SHGetFolderPath +SHGFP_TYPE +SHGetKnownFolderItem +SHQUERYRBINFO +SHQueryRecycleBin +FileOperation +IFileOperation +IShellItem2 +PSGetPropertyKeyFromName diff --git a/src/Files.App.CsWin32/Windows.Win32.Extras.cs b/src/Files.App.CsWin32/Windows.Win32.Extras.cs new file mode 100644 index 000000000000..357578425b8f --- /dev/null +++ b/src/Files.App.CsWin32/Windows.Win32.Extras.cs @@ -0,0 +1,34 @@ +// Copyright (c) 2024 Files Community +// Licensed under the MIT License. See the LICENSE. + +using System; +using System.Runtime.InteropServices; +using Windows.Win32.Foundation; + +namespace Windows.Win32 +{ + namespace Graphics.Gdi + { + [UnmanagedFunctionPointer(CallingConvention.Winapi)] + public unsafe delegate BOOL MONITORENUMPROC([In] HMONITOR param0, [In] HDC param1, [In][Out] RECT* param2, [In] LPARAM param3); + } + + namespace UI.WindowsAndMessaging + { + [UnmanagedFunctionPointer(CallingConvention.Winapi)] + public delegate LRESULT WNDPROC(HWND hWnd, uint msg, WPARAM wParam, LPARAM lParam); + } + + namespace UI.Shell + { + public static partial class FOLDERID + { + public readonly static Guid FOLDERID_RecycleBinFolder = new(0xB7534046, 0x3ECB, 0x4C18, 0xBE, 0x4E, 0x64, 0xCD, 0x4C, 0xB7, 0xD6, 0xAC); + } + + public static partial class BHID + { + public readonly static Guid BHID_EnumItems = new(0x94f60519, 0x2850, 0x4924, 0xaa, 0x5a, 0xd1, 0x5e, 0x84, 0x86, 0x80, 0x39); + } + } +} diff --git a/src/Files.App.CsWin32/Windows.Win32.MONITORENUMPROC.cs b/src/Files.App.CsWin32/Windows.Win32.MONITORENUMPROC.cs deleted file mode 100644 index c2914bd3b3f1..000000000000 --- a/src/Files.App.CsWin32/Windows.Win32.MONITORENUMPROC.cs +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright (c) 2024 Files Community -// Licensed under the MIT License. See the LICENSE. - -using System.Runtime.InteropServices; -using Windows.Win32.Foundation; - -namespace Windows.Win32.Graphics.Gdi; - -[UnmanagedFunctionPointer(CallingConvention.Winapi)] -public unsafe delegate BOOL MONITORENUMPROC([In] HMONITOR param0, [In] HDC param1, [In][Out] RECT* param2, [In] LPARAM param3); diff --git a/src/Files.App.CsWin32/Windows.Win32.WNDPROC.cs b/src/Files.App.CsWin32/Windows.Win32.WNDPROC.cs deleted file mode 100644 index b0d6eed42443..000000000000 --- a/src/Files.App.CsWin32/Windows.Win32.WNDPROC.cs +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright (c) 2024 Files Community -// Licensed under the MIT License. See the LICENSE. - -using System.Runtime.InteropServices; -using Windows.Win32.Foundation; - -namespace Windows.Win32.UI.WindowsAndMessaging; - -[UnmanagedFunctionPointer(CallingConvention.Winapi)] -public delegate LRESULT WNDPROC(HWND hWnd, uint msg, WPARAM wParam, LPARAM lParam); diff --git a/src/Files.App.Storage/Watchers/RecycleBinWatcher.cs b/src/Files.App.Storage/Watchers/RecycleBinWatcher.cs new file mode 100644 index 000000000000..1583e35e8b92 --- /dev/null +++ b/src/Files.App.Storage/Watchers/RecycleBinWatcher.cs @@ -0,0 +1,108 @@ +// Copyright (c) 2024 Files Community +// Licensed under the MIT License. See the LICENSE. + +using System.Security.Principal; + +namespace Files.App.Storage.Watchers +{ + public class RecycleBinWatcher : ITrashWatcher + { + private readonly List _watchers = []; + + /// + public event EventHandler? ItemAdded; + + /// + public event EventHandler? ItemDeleted; + + /// + public event EventHandler? ItemChanged; + + /// + public event EventHandler? ItemRenamed; + + /// + public event EventHandler? RefreshRequested; + + /// + /// Initializes an instance of class. + /// + public RecycleBinWatcher() + { + StartWatcher(); + } + + /// + public void StartWatcher() + { + // NOTE: + // SHChangeNotifyRegister only works if recycle bin is open in File Explorer. + // Create file system watcher to monitor recycle bin folder(s) instead. + + // Listen changes only on the Recycle Bin that the current logon user has + var sid = WindowsIdentity.GetCurrent().User?.ToString() ?? string.Empty; + if (string.IsNullOrEmpty(sid)) + return; + + // TODO: Use IStorageDevicesService to enumerate drives + foreach (var drive in SystemIO.DriveInfo.GetDrives()) + { + var recyclePath = SystemIO.Path.Combine(drive.Name, "$RECYCLE.BIN", sid); + + if (drive.DriveType is SystemIO.DriveType.Network || + !SystemIO.Directory.Exists(recyclePath)) + continue; + + SystemIO.FileSystemWatcher watcher = new() + { + Path = recyclePath, + Filter = "*.*", + NotifyFilter = SystemIO.NotifyFilters.LastWrite | SystemIO.NotifyFilters.FileName | SystemIO.NotifyFilters.DirectoryName + }; + + watcher.Created += Watcher_Changed; + watcher.Deleted += Watcher_Changed; + watcher.EnableRaisingEvents = true; + + _watchers.Add(watcher); + } + } + + /// + public void StopWatcher() + { + foreach (var watcher in _watchers) + watcher.Dispose(); + } + + private void Watcher_Changed(object sender, SystemIO.FileSystemEventArgs e) + { + // Don't listen changes on files starting with '$I' + if (string.IsNullOrEmpty(e.Name) || + e.Name.StartsWith("$I", StringComparison.Ordinal)) + return; + + switch (e.ChangeType) + { + case SystemIO.WatcherChangeTypes.Created: + ItemAdded?.Invoke(this, e); + break; + case SystemIO.WatcherChangeTypes.Deleted: + ItemDeleted?.Invoke(this, e); + break; + case SystemIO.WatcherChangeTypes.Renamed: + ItemRenamed?.Invoke(this, e); + break; + default: + RefreshRequested?.Invoke(this, e); + break; + } + } + + /// + public void Dispose() + { + StopWatcher(); + } + } +} diff --git a/src/Files.App/Actions/FileSystem/EmptyRecycleBinAction.cs b/src/Files.App/Actions/FileSystem/EmptyRecycleBinAction.cs index de778a7d21fc..8bae356d6af2 100644 --- a/src/Files.App/Actions/FileSystem/EmptyRecycleBinAction.cs +++ b/src/Files.App/Actions/FileSystem/EmptyRecycleBinAction.cs @@ -1,10 +1,16 @@ // Copyright (c) 2024 Files Community // Licensed under the MIT License. See the LICENSE. +using Microsoft.UI.Xaml.Controls; +using Windows.Foundation.Metadata; + namespace Files.App.Actions { internal sealed class EmptyRecycleBinAction : BaseUIAction, IAction { + private readonly IWindowsRecycleBinService WindowsRecycleBinService = Ioc.Default.GetRequiredService(); + private readonly StatusCenterViewModel StatusCenterViewModel = Ioc.Default.GetRequiredService(); + private readonly IUserSettingsService UserSettingsService = Ioc.Default.GetRequiredService(); private readonly IContentPageContext context; public string Label @@ -19,7 +25,7 @@ public RichGlyph Glyph public override bool IsExecutable => UIHelpers.CanShowDialog && ((context.PageType == ContentPageTypes.RecycleBin && context.HasItem) || - RecycleBinHelpers.RecycleBinHasItems()); + WindowsRecycleBinService.HasItems()); public EmptyRecycleBinAction() { @@ -30,7 +36,31 @@ public EmptyRecycleBinAction() public async Task ExecuteAsync(object? parameter = null) { - await RecycleBinHelpers.EmptyRecycleBinAsync(); + // TODO: Use AppDialogService + var confirmationDialog = new ContentDialog() + { + Title = "ConfirmEmptyBinDialogTitle".GetLocalizedResource(), + Content = "ConfirmEmptyBinDialogContent".GetLocalizedResource(), + PrimaryButtonText = "Yes".GetLocalizedResource(), + SecondaryButtonText = "Cancel".GetLocalizedResource(), + DefaultButton = ContentDialogButton.Primary + }; + + if (ApiInformation.IsApiContractPresent("Windows.Foundation.UniversalApiContract", 8)) + confirmationDialog.XamlRoot = MainWindow.Instance.Content.XamlRoot; + + if (UserSettingsService.FoldersSettingsService.DeleteConfirmationPolicy is DeleteConfirmationPolicies.Never || + await confirmationDialog.TryShowAsync() is ContentDialogResult.Primary) + { + var banner = StatusCenterHelper.AddCard_EmptyRecycleBin(ReturnResult.InProgress); + + bool result = await Task.Run(WindowsRecycleBinService.DeleteAllAsync); + + StatusCenterViewModel.RemoveItem(banner); + + // Post a status based on the result + StatusCenterHelper.AddCard_EmptyRecycleBin(result ? ReturnResult.Success : ReturnResult.Failed); + } } private void Context_PropertyChanged(object? sender, PropertyChangedEventArgs e) diff --git a/src/Files.App/Actions/FileSystem/RestoreAllRecycleBinAction.cs b/src/Files.App/Actions/FileSystem/RestoreAllRecycleBinAction.cs index 9b32f5eea146..6fd4c98e7e49 100644 --- a/src/Files.App/Actions/FileSystem/RestoreAllRecycleBinAction.cs +++ b/src/Files.App/Actions/FileSystem/RestoreAllRecycleBinAction.cs @@ -1,10 +1,15 @@ // Copyright (c) 2024 Files Community // Licensed under the MIT License. See the LICENSE. +using Microsoft.UI.Xaml.Controls; +using Windows.Foundation.Metadata; + namespace Files.App.Actions { internal sealed class RestoreAllRecycleBinAction : BaseUIAction, IAction { + private readonly IWindowsRecycleBinService WindowsRecycleBinService = Ioc.Default.GetRequiredService(); + public string Label => "RestoreAllItems".GetLocalizedResource(); @@ -16,11 +21,42 @@ public RichGlyph Glyph public override bool IsExecutable => UIHelpers.CanShowDialog && - RecycleBinHelpers.RecycleBinHasItems(); + WindowsRecycleBinService.HasItems(); public async Task ExecuteAsync(object? parameter = null) { - await RecycleBinHelpers.RestoreRecycleBinAsync(); + // TODO: Use AppDialogService + var confirmationDialog = new ContentDialog() + { + Title = "ConfirmRestoreBinDialogTitle".GetLocalizedResource(), + Content = "ConfirmRestoreBinDialogContent".GetLocalizedResource(), + PrimaryButtonText = "Yes".GetLocalizedResource(), + SecondaryButtonText = "Cancel".GetLocalizedResource(), + DefaultButton = ContentDialogButton.Primary + }; + + if (ApiInformation.IsApiContractPresent("Windows.Foundation.UniversalApiContract", 8)) + confirmationDialog.XamlRoot = MainWindow.Instance.Content.XamlRoot; + + if (await confirmationDialog.TryShowAsync() is not ContentDialogResult.Primary) + return; + + bool result = await Task.Run(WindowsRecycleBinService.RestoreAllAsync); + + // Show error dialog when failed + if (!result) + { + var errorDialog = new ContentDialog() + { + Title = "FailedToRestore".GetLocalizedResource(), + PrimaryButtonText = "OK".GetLocalizedResource(), + }; + + if (ApiInformation.IsApiContractPresent("Windows.Foundation.UniversalApiContract", 8)) + errorDialog.XamlRoot = MainWindow.Instance.Content.XamlRoot; + + await errorDialog.TryShowAsync(); + } } } } diff --git a/src/Files.App/Actions/FileSystem/RestoreRecycleBinAction.cs b/src/Files.App/Actions/FileSystem/RestoreRecycleBinAction.cs index 408b35c9b636..ae8a89d37574 100644 --- a/src/Files.App/Actions/FileSystem/RestoreRecycleBinAction.cs +++ b/src/Files.App/Actions/FileSystem/RestoreRecycleBinAction.cs @@ -1,6 +1,10 @@ // Copyright (c) 2024 Files Community // Licensed under the MIT License. See the LICENSE. +using Microsoft.UI.Xaml.Controls; +using Windows.Foundation.Metadata; +using Windows.Storage; + namespace Files.App.Actions { internal sealed class RestoreRecycleBinAction : BaseUIAction, IAction @@ -30,8 +34,32 @@ public RestoreRecycleBinAction() public async Task ExecuteAsync(object? parameter = null) { - if (context.ShellPage is not null) - await RecycleBinHelpers.RestoreSelectionRecycleBinAsync(context.ShellPage); + var confirmationDialog = new ContentDialog() + { + Title = "ConfirmRestoreSelectionBinDialogTitle".GetLocalizedResource(), + Content = string.Format("ConfirmRestoreSelectionBinDialogContent".GetLocalizedResource(), context.SelectedItems.Count), + PrimaryButtonText = "Yes".GetLocalizedResource(), + SecondaryButtonText = "Cancel".GetLocalizedResource(), + DefaultButton = ContentDialogButton.Primary + }; + + if (ApiInformation.IsApiContractPresent("Windows.Foundation.UniversalApiContract", 8)) + confirmationDialog.XamlRoot = MainWindow.Instance.Content.XamlRoot; + + ContentDialogResult result = await confirmationDialog.TryShowAsync(); + + if (result is not ContentDialogResult.Primary) + return; + + var items = context.SelectedItems.ToList().Where(x => x is RecycleBinItem).Select((item) => new + { + Source = StorageHelpers.FromPathAndType( + item.ItemPath, + item.PrimaryItemAttribute == StorageItemTypes.File ? FilesystemItemType.File : FilesystemItemType.Directory), + Dest = ((RecycleBinItem)item).ItemOriginalPath + }); + + await context.ShellPage!.FilesystemHelpers.RestoreItemsFromTrashAsync(items.Select(x => x.Source), items.Select(x => x.Dest), true); } private void Context_PropertyChanged(object? sender, PropertyChangedEventArgs e) diff --git a/src/Files.App/Data/Contracts/IWindowsRecycleBinService.cs b/src/Files.App/Data/Contracts/IWindowsRecycleBinService.cs new file mode 100644 index 000000000000..a97f82df1fc1 --- /dev/null +++ b/src/Files.App/Data/Contracts/IWindowsRecycleBinService.cs @@ -0,0 +1,67 @@ +// Copyright (c) 2024 Files Community +// Licensed under the MIT License. See the LICENSE. + +namespace Files.App.Data.Contracts +{ + /// + /// Provides service for Recycle Bin on Windows. + /// + public interface IWindowsRecycleBinService + { + /// + /// Gets the watcher of Recycle Bin folder. + /// + RecycleBinWatcher Watcher { get; } + + /// + /// Gets all Recycle Bin shell folders. + /// + /// A collection of Recycle Bin shell folders. + Task> GetAllRecycleBinFoldersAsync(); + + /// + /// Gets the info of Recycle Bin. + /// + /// The drive letter, Recycle Bin of which you want. + /// + (bool HasRecycleBin, long NumItems, long BinSize) QueryRecycleBin(string drive = ""); + + /// + /// Gets the used size of Recycle Bin. + /// + /// + ulong GetSize(); + + /// + /// Gets the value that indicates whether Recycle Bin folder has item(s). + /// + /// + bool HasItems(); + + /// + /// Gets the file or folder specified is already moved to Recycle Bin. + /// + /// The path that indicates to a file or folder. + /// True if the file or path is recycled; otherwise, false. + bool IsRecycled(string? path); + + /// + /// Gets the file or folder specified can be moved to Recycle Bin. + /// + /// + /// + Task IsRecyclableAsync(string? path); + + /// + /// Deletes files and folders in Recycle Bin permanently. + /// + /// True if succeeded; otherwise, false + bool DeleteAllAsync(); + + /// + /// Restores files and folders in Recycle Bin to original paths. + /// + /// True if succeeded; otherwise, false + bool RestoreAllAsync(); + } +} diff --git a/src/Files.App/Data/Items/ListedItem.cs b/src/Files.App/Data/Items/ListedItem.cs index 30f4c7d77317..d70958459d48 100644 --- a/src/Files.App/Data/Items/ListedItem.cs +++ b/src/Files.App/Data/Items/ListedItem.cs @@ -501,8 +501,8 @@ public FtpItem(FtpListItem item, string folder) : base(null) public async Task ToStorageItem() => PrimaryItemAttribute switch { - StorageItemTypes.File => await new FtpStorageFile(ItemPath, ItemNameRaw, ItemDateCreatedReal).ToStorageFileAsync(), - StorageItemTypes.Folder => new FtpStorageFolder(ItemPath, ItemNameRaw, ItemDateCreatedReal), + StorageItemTypes.File => await new Utils.Storage.FtpStorageFile(ItemPath, ItemNameRaw, ItemDateCreatedReal).ToStorageFileAsync(), + StorageItemTypes.Folder => new Utils.Storage.FtpStorageFolder(ItemPath, ItemNameRaw, ItemDateCreatedReal), _ => throw new InvalidDataException(), }; } diff --git a/src/Files.App/Data/Items/LocationItem.cs b/src/Files.App/Data/Items/LocationItem.cs index 1c77435872ec..7b2aae4984a8 100644 --- a/src/Files.App/Data/Items/LocationItem.cs +++ b/src/Files.App/Data/Items/LocationItem.cs @@ -124,11 +124,13 @@ public int CompareTo(INavigationControlItem other) public sealed class RecycleBinLocationItem : LocationItem { - public async void RefreshSpaceUsed(object sender, FileSystemEventArgs e) + private readonly IWindowsRecycleBinService WindowsRecycleBinService = Ioc.Default.GetRequiredService(); + + public async void RefreshSpaceUsed(object? sender, FileSystemEventArgs e) { await MainWindow.Instance.DispatcherQueue.EnqueueOrInvokeAsync(() => { - SpaceUsed = RecycleBinHelpers.GetSize(); + SpaceUsed = WindowsRecycleBinService.GetSize(); }); } @@ -150,10 +152,10 @@ public override object ToolTip public RecycleBinLocationItem() { - SpaceUsed = RecycleBinHelpers.GetSize(); + SpaceUsed = WindowsRecycleBinService.GetSize(); - RecycleBinManager.Default.RecycleBinItemCreated += RefreshSpaceUsed; - RecycleBinManager.Default.RecycleBinItemDeleted += RefreshSpaceUsed; + WindowsRecycleBinService.Watcher.ItemAdded += RefreshSpaceUsed; + WindowsRecycleBinService.Watcher.ItemDeleted += RefreshSpaceUsed; } } } diff --git a/src/Files.App/GlobalUsings.cs b/src/Files.App/GlobalUsings.cs index 475f545b00cf..3c77575cce44 100644 --- a/src/Files.App/GlobalUsings.cs +++ b/src/Files.App/GlobalUsings.cs @@ -30,7 +30,6 @@ global using global::Files.App.Utils.Git; global using global::Files.App.Utils.Library; global using global::Files.App.Utils.RecentItem; -global using global::Files.App.Utils.RecycleBin; global using global::Files.App.Utils.Serialization; global using global::Files.App.Utils.Shell; global using global::Files.App.Utils.StatusCenter; @@ -79,6 +78,11 @@ global using global::Files.Core.Storage.Extensions; global using global::Files.Core.Storage.StorageEnumeration; +// Files.App.Storage + +global using global::Files.App.Storage.Storables; +global using global::Files.App.Storage.Watchers; + // Files.Shared global using global::Files.Shared; global using global::Files.Shared.Extensions; diff --git a/src/Files.App/Helpers/Application/AppLifecycleHelper.cs b/src/Files.App/Helpers/Application/AppLifecycleHelper.cs index 3b200b317dee..82ca83a99f3f 100644 --- a/src/Files.App/Helpers/Application/AppLifecycleHelper.cs +++ b/src/Files.App/Helpers/Application/AppLifecycleHelper.cs @@ -196,6 +196,7 @@ public static IHost ConfigureHost() .AddSingleton() .AddSingleton() .AddSingleton() + .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() diff --git a/src/Files.App/Helpers/Win32/Win32Helper.Shell.cs b/src/Files.App/Helpers/Win32/Win32Helper.Shell.cs index 2e9202ffbc15..a307f6041ec8 100644 --- a/src/Files.App/Helpers/Win32/Win32Helper.Shell.cs +++ b/src/Files.App/Helpers/Win32/Win32Helper.Shell.cs @@ -79,24 +79,5 @@ public static partial class Win32Helper return (folder, flc); }); } - - public static (bool HasRecycleBin, long NumItems, long BinSize) QueryRecycleBin(string drive = "") - { - Win32PInvoke.SHQUERYRBINFO queryBinInfo = new Win32PInvoke.SHQUERYRBINFO(); - queryBinInfo.cbSize = Marshal.SizeOf(queryBinInfo); - - var res = Win32PInvoke.SHQueryRecycleBin(drive, ref queryBinInfo); - if (res == HRESULT.S_OK) - { - var numItems = queryBinInfo.i64NumItems; - var binSize = queryBinInfo.i64Size; - - return (true, numItems, binSize); - } - else - { - return (false, 0, 0); - } - } } } diff --git a/src/Files.App/Helpers/Win32/Win32PInvoke.Methods.cs b/src/Files.App/Helpers/Win32/Win32PInvoke.Methods.cs index 49624896c45c..092e07a69af4 100644 --- a/src/Files.App/Helpers/Win32/Win32PInvoke.Methods.cs +++ b/src/Files.App/Helpers/Win32/Win32PInvoke.Methods.cs @@ -497,12 +497,6 @@ public static extern uint RegisterApplicationRestart( int dwFlags ); - [DllImport(Lib.Shell32, SetLastError = false, CharSet = CharSet.Unicode)] - public static extern int SHQueryRecycleBin( - string pszRootPath, - ref SHQUERYRBINFO pSHQueryRBInfo - ); - [DllImport("shell32.dll")] static extern int SHGetKnownFolderPath( [MarshalAs(UnmanagedType.LPStruct)] Guid rfid, @@ -511,6 +505,9 @@ static extern int SHGetKnownFolderPath( out IntPtr pszPath ); + [DllImport("shell32.dll", EntryPoint = "SHUpdateRecycleBinIcon", CharSet = CharSet.Unicode, SetLastError = true)] + public static extern void SHUpdateRecycleBinIcon(); + public static string GetFolderFromKnownFolderGUID(Guid guid) { IntPtr pPath; diff --git a/src/Files.App/Helpers/Win32/Win32PInvoke.Structs.cs b/src/Files.App/Helpers/Win32/Win32PInvoke.Structs.cs index e49ec32eb862..39114386bc74 100644 --- a/src/Files.App/Helpers/Win32/Win32PInvoke.Structs.cs +++ b/src/Files.App/Helpers/Win32/Win32PInvoke.Structs.cs @@ -212,18 +212,6 @@ public struct WIN32_FIND_DATA public string cAlternateFileName; } - // There is usually no need to define Win32 COM interfaces/P-Invoke methods here. - // The Vanara library contains the definitions for all members of Shell32.dll, User32.dll and more - // The ones below are due to bugs in the current version of the library and can be removed once fixed - // Structure used by SHQueryRecycleBin. - [StructLayout(LayoutKind.Sequential, Pack = 0)] - public struct SHQUERYRBINFO - { - public int cbSize; - public long i64Size; - public long i64NumItems; - } - [StructLayout(LayoutKind.Sequential)] public struct MSG { diff --git a/src/Files.App/Services/App/FileTagsService.cs b/src/Files.App/Services/App/FileTagsService.cs index 8ee2fec86328..ba4242ed1c1d 100644 --- a/src/Files.App/Services/App/FileTagsService.cs +++ b/src/Files.App/Services/App/FileTagsService.cs @@ -11,9 +11,9 @@ namespace Files.App.Services /// internal sealed class FileTagsService : IFileTagsService { - private IStorageService StorageService { get; } = Ioc.Default.GetRequiredService(); - - private IFileTagsSettingsService FileTagsSettingsService { get; } = Ioc.Default.GetRequiredService(); + private readonly IWindowsRecycleBinService WindowsRecycleBinService = Ioc.Default.GetRequiredService(); + private readonly IFileTagsSettingsService FileTagsSettingsService = Ioc.Default.GetRequiredService(); + private readonly IStorageService StorageService = Ioc.Default.GetRequiredService(); /// public Task IsSupportedAsync() @@ -42,7 +42,7 @@ public async IAsyncEnumerable GetItemsForTagAsync(string tagUid { foreach (var item in FileTagsHelper.GetDbInstance().GetAll()) { - if (!item.Tags.Contains(tagUid) || RecycleBinHelpers.IsPathUnderRecycleBin(item.FilePath)) + if (!item.Tags.Contains(tagUid) || WindowsRecycleBinService.IsRecycled(item.FilePath)) continue; var storable = await StorageService.TryGetStorableAsync(item.FilePath, cancellationToken); diff --git a/src/Files.App/Services/Windows/WindowsRecycleBinService.cs b/src/Files.App/Services/Windows/WindowsRecycleBinService.cs new file mode 100644 index 000000000000..7a9e2bfc9cd6 --- /dev/null +++ b/src/Files.App/Services/Windows/WindowsRecycleBinService.cs @@ -0,0 +1,145 @@ +// Copyright (c) 2024 Files Community +// Licensed under the MIT License. See the LICENSE. + +using System.Runtime.InteropServices; +using Windows.Win32; +using Windows.Win32.Foundation; +using Windows.Win32.System.Com; +using Windows.Win32.UI.Shell; + +namespace Files.App.Services +{ + /// + public class WindowsRecycleBinService : IWindowsRecycleBinService + { + /// + public RecycleBinWatcher Watcher { get; private set; } = new(); + + /// + public async Task> GetAllRecycleBinFoldersAsync() + { + return (await Win32Helper.GetShellFolderAsync(Constants.UserEnvironmentPaths.RecycleBinPath, false, true, 0, int.MaxValue)).Enumerate; + } + + /// + public (bool HasRecycleBin, long NumItems, long BinSize) QueryRecycleBin(string drive = "") + { + SHQUERYRBINFO queryBinInfo = default; + queryBinInfo.cbSize = (uint)Marshal.SizeOf(queryBinInfo); + + var hRes = PInvoke.SHQueryRecycleBin(drive, ref queryBinInfo); + return hRes == HRESULT.S_OK + ? (true, queryBinInfo.i64NumItems, queryBinInfo.i64Size) + : (false, 0, 0); + } + + /// + public ulong GetSize() + { + return (ulong)QueryRecycleBin().BinSize; + } + + /// + public bool HasItems() + { + return QueryRecycleBin().NumItems > 0; + } + + /// + public bool IsRecycled(string? path) + { + return + !string.IsNullOrWhiteSpace(path) && + RegexHelpers.RecycleBinPath().IsMatch(path); + } + + /// + public async Task IsRecyclableAsync(string? path) + { + if (string.IsNullOrEmpty(path) || + path.StartsWith(@"\\?\", StringComparison.Ordinal)) + return false; + + var result = await FileOperationsHelpers.TestRecycleAsync(path.Split('|')); + + return + result.Item1 &= result.Item2 is not null && + result.Item2.Items.All(x => x.Succeeded); + } + + /// + public bool DeleteAllAsync() + { + var fRes = PInvoke.SHEmptyRecycleBin( + new(), + string.Empty, + 0x00000001 | 0x00000002 /* SHERB_NOCONFIRMATION | SHERB_NOPROGRESSUI */) + .Succeeded; + + return fRes; + } + + /// + public unsafe bool RestoreAllAsync() + { + IShellItem* recycleBinFolderShellItem = default; + IEnumShellItems* enumShellItems = default; + IFileOperation* pFileOperation = default; + IShellItem* pShellItem = default; + + try + { + // Get IShellItem for Recycle Bin + var recycleBinFolderId = FOLDERID.FOLDERID_RecycleBinFolder; + var shellItemGuid = typeof(IShellItem).GUID; + PInvoke.SHGetKnownFolderItem(&recycleBinFolderId, KNOWN_FOLDER_FLAG.KF_FLAG_DEFAULT, HANDLE.Null, &shellItemGuid, (void**)&recycleBinFolderShellItem); + + // Get IEnumShellItems for Recycle Bin + Guid enumShellItemGuid = typeof(IEnumShellItems).GUID; + var enumItemsBHID = BHID.BHID_EnumItems; + recycleBinFolderShellItem->BindToHandler(null, &enumItemsBHID, &enumShellItemGuid, (void**)&enumShellItems); + + // Initialize how to perform the operation + PInvoke.CoCreateInstance(typeof(FileOperation).GUID, null, CLSCTX.CLSCTX_LOCAL_SERVER, out pFileOperation); + pFileOperation->SetOperationFlags(FILEOPERATION_FLAGS.FOF_NO_UI); + pFileOperation->SetOwnerWindow(new(MainWindow.Instance.WindowHandle)); + + while (enumShellItems->Next(1, &pShellItem) == HRESULT.S_OK) + { + // Get original path + pShellItem->QueryInterface(typeof(IShellItem2).GUID, out var pShellItem2Ptr); + var pShellItem2 = (IShellItem2*)pShellItem2Ptr; + PInvoke.PSGetPropertyKeyFromName("System.Recycle.DeletedFrom", out var originalPathPropertyKey); + pShellItem2->GetString(originalPathPropertyKey, out var szOriginalPath); + pShellItem2->Release(); + + // Get IShellItem of the original path + PInvoke.SHCreateItemFromParsingName(szOriginalPath.ToString(), null, typeof(IShellItem).GUID, out var pOriginalPathShellItemPtr); + var pOriginalPathShellItem = (IShellItem*)pOriginalPathShellItemPtr; + + // Define to move the shell item + pFileOperation->MoveItem(pShellItem, pOriginalPathShellItem, new PCWSTR(), null); + } + + // Perform + pFileOperation->PerformOperations(); + + // Reset the icon + Win32PInvoke.SHUpdateRecycleBinIcon(); + + return true; + } + catch + { + return false; + } + finally + { + recycleBinFolderShellItem->Release(); + enumShellItems->Release(); + pFileOperation->Release(); + pShellItem->Release(); + } + } + } +} diff --git a/src/Files.App/Utils/RecycleBin/RecycleBinHelpers.cs b/src/Files.App/Utils/RecycleBin/RecycleBinHelpers.cs deleted file mode 100644 index 4f6df171c2c6..000000000000 --- a/src/Files.App/Utils/RecycleBin/RecycleBinHelpers.cs +++ /dev/null @@ -1,180 +0,0 @@ -// Copyright (c) 2024 Files Community -// Licensed under the MIT License. See the LICENSE. - -using Microsoft.UI.Xaml.Controls; -using Vanara.PInvoke; -using Windows.Foundation.Metadata; -using Windows.Storage; - -namespace Files.App.Utils.RecycleBin -{ - public static class RecycleBinHelpers - { - private static readonly StatusCenterViewModel _statusCenterViewModel = Ioc.Default.GetRequiredService(); - - private static readonly IUserSettingsService userSettingsService = Ioc.Default.GetRequiredService(); - - public static async Task> EnumerateRecycleBin() - { - return (await Win32Helper.GetShellFolderAsync(Constants.UserEnvironmentPaths.RecycleBinPath, false, true, 0, int.MaxValue)).Enumerate; - } - - public static ulong GetSize() - { - return (ulong)Win32Helper.QueryRecycleBin().BinSize; - } - - public static async Task IsRecycleBinItem(IStorageItem item) - { - List recycleBinItems = await EnumerateRecycleBin(); - return recycleBinItems.Any((shellItem) => shellItem.RecyclePath == item.Path); - } - - public static async Task IsRecycleBinItem(string path) - { - List recycleBinItems = await EnumerateRecycleBin(); - return recycleBinItems.Any((shellItem) => shellItem.RecyclePath == path); - } - - public static bool IsPathUnderRecycleBin(string path) - { - return !string.IsNullOrWhiteSpace(path) && RegexHelpers.RecycleBinPath().IsMatch(path); - } - - public static async Task EmptyRecycleBinAsync() - { - // Display confirmation dialog - var ConfirmEmptyBinDialog = new ContentDialog() - { - Title = "ConfirmEmptyBinDialogTitle".GetLocalizedResource(), - Content = "ConfirmEmptyBinDialogContent".GetLocalizedResource(), - PrimaryButtonText = "Yes".GetLocalizedResource(), - SecondaryButtonText = "Cancel".GetLocalizedResource(), - DefaultButton = ContentDialogButton.Primary - }; - - if (ApiInformation.IsApiContractPresent("Windows.Foundation.UniversalApiContract", 8)) - ConfirmEmptyBinDialog.XamlRoot = MainWindow.Instance.Content.XamlRoot; - - // If the operation is approved by the user - if (userSettingsService.FoldersSettingsService.DeleteConfirmationPolicy is DeleteConfirmationPolicies.Never || - await ConfirmEmptyBinDialog.TryShowAsync() == ContentDialogResult.Primary) - { - - var banner = StatusCenterHelper.AddCard_EmptyRecycleBin(ReturnResult.InProgress); - - bool bResult = await Task.Run(() => Shell32.SHEmptyRecycleBin(IntPtr.Zero, null, Shell32.SHERB.SHERB_NOCONFIRMATION | Shell32.SHERB.SHERB_NOPROGRESSUI).Succeeded); - - _statusCenterViewModel.RemoveItem(banner); - - if (bResult) - StatusCenterHelper.AddCard_EmptyRecycleBin(ReturnResult.Success); - else - StatusCenterHelper.AddCard_EmptyRecycleBin(ReturnResult.Failed); - } - } - - public static async Task RestoreRecycleBinAsync() - { - var confirmEmptyBinDialog = new ContentDialog() - { - Title = "ConfirmRestoreBinDialogTitle".GetLocalizedResource(), - Content = "ConfirmRestoreBinDialogContent".GetLocalizedResource(), - PrimaryButtonText = "Yes".GetLocalizedResource(), - SecondaryButtonText = "Cancel".GetLocalizedResource(), - DefaultButton = ContentDialogButton.Primary - }; - - if (ApiInformation.IsApiContractPresent("Windows.Foundation.UniversalApiContract", 8)) - confirmEmptyBinDialog.XamlRoot = MainWindow.Instance.Content.XamlRoot; - - ContentDialogResult result = await confirmEmptyBinDialog.TryShowAsync(); - - if (result == ContentDialogResult.Primary) - { - try - { - Vanara.Windows.Shell.RecycleBin.RestoreAll(); - } - catch (Exception) - { - var errorDialog = new ContentDialog() - { - Title = "FailedToRestore".GetLocalizedResource(), - PrimaryButtonText = "OK".GetLocalizedResource(), - }; - - if (ApiInformation.IsApiContractPresent("Windows.Foundation.UniversalApiContract", 8)) - errorDialog.XamlRoot = MainWindow.Instance.Content.XamlRoot; - - await errorDialog.TryShowAsync(); - } - } - } - - public static async Task RestoreSelectionRecycleBinAsync(IShellPage associatedInstance) - { - var items = associatedInstance.SlimContentPage.SelectedItems; - if (items == null) - return; - var ConfirmEmptyBinDialog = new ContentDialog() - { - Title = "ConfirmRestoreSelectionBinDialogTitle".GetLocalizedResource(), - - Content = string.Format("ConfirmRestoreSelectionBinDialogContent".GetLocalizedResource(), items.Count), - PrimaryButtonText = "Yes".GetLocalizedResource(), - SecondaryButtonText = "Cancel".GetLocalizedResource(), - DefaultButton = ContentDialogButton.Primary - }; - - if (ApiInformation.IsApiContractPresent("Windows.Foundation.UniversalApiContract", 8)) - ConfirmEmptyBinDialog.XamlRoot = MainWindow.Instance.Content.XamlRoot; - - ContentDialogResult result = await ConfirmEmptyBinDialog.TryShowAsync(); - - if (result == ContentDialogResult.Primary) - await RestoreItemAsync(associatedInstance); - } - - public static async Task HasRecycleBin(string? path) - { - if (string.IsNullOrEmpty(path) || path.StartsWith(@"\\?\", StringComparison.Ordinal)) - return false; - - var result = await FileOperationsHelpers.TestRecycleAsync(path.Split('|')); - - return result.Item1 &= result.Item2 is not null && result.Item2.Items.All(x => x.Succeeded); - } - - public static bool RecycleBinHasItems() - { - return Win32Helper.QueryRecycleBin().NumItems > 0; - } - - public static async Task RestoreItemAsync(IShellPage associatedInstance) - { - var selected = associatedInstance.SlimContentPage.SelectedItems; - if (selected == null) - return; - var items = selected.ToList().Where(x => x is RecycleBinItem).Select((item) => new - { - Source = StorageHelpers.FromPathAndType( - item.ItemPath, - item.PrimaryItemAttribute == StorageItemTypes.File ? FilesystemItemType.File : FilesystemItemType.Directory), - Dest = ((RecycleBinItem)item).ItemOriginalPath - }); - await associatedInstance.FilesystemHelpers.RestoreItemsFromTrashAsync(items.Select(x => x.Source), items.Select(x => x.Dest), true); - } - - public static async Task DeleteItemAsync(IShellPage associatedInstance) - { - var selected = associatedInstance.SlimContentPage.SelectedItems; - if (selected == null) - return; - var items = selected.ToList().Select((item) => StorageHelpers.FromPathAndType( - item.ItemPath, - item.PrimaryItemAttribute == StorageItemTypes.File ? FilesystemItemType.File : FilesystemItemType.Directory)); - await associatedInstance.FilesystemHelpers.DeleteItemsAsync(items, userSettingsService.FoldersSettingsService.DeleteConfirmationPolicy, false, true); - } - } -} diff --git a/src/Files.App/Utils/RecycleBin/RecycleBinManager.cs b/src/Files.App/Utils/RecycleBin/RecycleBinManager.cs deleted file mode 100644 index 8b834a19747a..000000000000 --- a/src/Files.App/Utils/RecycleBin/RecycleBinManager.cs +++ /dev/null @@ -1,113 +0,0 @@ -// Copyright (c) 2024 Files Community -// Licensed under the MIT License. See the LICENSE. - -using System.Security.Principal; - -namespace Files.App.Utils.RecycleBin -{ - /// - /// Provides a utility to handle Windows Recycle Bin. - /// - public sealed class RecycleBinManager - { - private static readonly Lazy lazy = new(() => new RecycleBinManager()); - - private List? binWatchers; - - public event SystemIO.FileSystemEventHandler? RecycleBinItemCreated; - - public event SystemIO.FileSystemEventHandler? RecycleBinItemDeleted; - - public event SystemIO.FileSystemEventHandler? RecycleBinItemRenamed; - - public event SystemIO.FileSystemEventHandler? RecycleBinRefreshRequested; - - public static RecycleBinManager Default - => lazy.Value; - - private RecycleBinManager() - { - Initialize(); - } - - private void Initialize() - { - // Create shell COM object and get recycle bin folder - StartRecycleBinWatcher(); - } - - private void StartRecycleBinWatcher() - { - // NOTE: SHChangeNotifyRegister only works if recycle bin is open in explorer - // Create file system watcher to monitor recycle bin folder(s) - binWatchers = []; - - var sid = WindowsIdentity.GetCurrent().User.ToString(); - - foreach (var drive in SystemIO.DriveInfo.GetDrives()) - { - var recyclePath = SystemIO.Path.Combine(drive.Name, "$RECYCLE.BIN", sid); - - if (drive.DriveType == SystemIO.DriveType.Network || !SystemIO.Directory.Exists(recyclePath)) - continue; - - SafetyExtensions.IgnoreExceptions(() => - { - SystemIO.FileSystemWatcher watcher = new() - { - Path = recyclePath, - Filter = "*.*", - NotifyFilter = SystemIO.NotifyFilters.LastWrite | SystemIO.NotifyFilters.FileName | SystemIO.NotifyFilters.DirectoryName - }; - - watcher.Created += RecycleBinWatcher_Changed; - watcher.Deleted += RecycleBinWatcher_Changed; - watcher.EnableRaisingEvents = true; - - binWatchers.Add(watcher); - }); - } - } - - private void RecycleBinWatcher_Changed(object sender, SystemIO.FileSystemEventArgs e) - { - Debug.WriteLine($"Recycle bin event: {e.ChangeType}, {e.FullPath}"); - - if (e.Name.StartsWith("$I", StringComparison.Ordinal)) - { - // Recycle bin also stores a file starting with $I for each item - return; - } - - switch (e.ChangeType) - { - case SystemIO.WatcherChangeTypes.Created: - RecycleBinItemCreated?.Invoke(this, e); - break; - case SystemIO.WatcherChangeTypes.Deleted: - RecycleBinItemDeleted?.Invoke(this, e); - break; - case SystemIO.WatcherChangeTypes.Renamed: - RecycleBinItemRenamed?.Invoke(this, e); - break; - default: - RecycleBinRefreshRequested?.Invoke(this, e); - break; - } - } - - private void Unregister() - { - if (binWatchers is not null) - { - foreach (var watcher in binWatchers) - watcher.Dispose(); - } - } - - ~RecycleBinManager() - { - Unregister(); - } - } -} diff --git a/src/Files.App/Utils/Storage/Operations/FilesystemHelpers.cs b/src/Files.App/Utils/Storage/Operations/FilesystemHelpers.cs index d7df940943ed..4dc32e70d4bb 100644 --- a/src/Files.App/Utils/Storage/Operations/FilesystemHelpers.cs +++ b/src/Files.App/Utils/Storage/Operations/FilesystemHelpers.cs @@ -20,6 +20,7 @@ namespace Files.App.Utils.Storage { public sealed class FilesystemHelpers : IFilesystemHelpers { + private readonly IWindowsRecycleBinService WindowsRecycleBinService = Ioc.Default.GetRequiredService(); private readonly static StatusCenterViewModel _statusCenterViewModel = Ioc.Default.GetRequiredService(); private IShellPage associatedInstance; @@ -90,8 +91,8 @@ public async Task DeleteItemsAsync(IEnumerable item.Path).Any(path => RecycleBinHelpers.IsPathUnderRecycleBin(path)); - var canBeSentToBin = !deleteFromRecycleBin && await RecycleBinHelpers.HasRecycleBin(source.FirstOrDefault()?.Path); + var deleteFromRecycleBin = source.Select(item => item.Path).Any(WindowsRecycleBinService.IsRecycled); + var canBeSentToBin = !deleteFromRecycleBin && await WindowsRecycleBinService.IsRecyclableAsync(source.FirstOrDefault()?.Path); if (showDialog is DeleteConfirmationPolicies.Always || showDialog is DeleteConfirmationPolicies.PermanentOnly && @@ -102,9 +103,9 @@ showDialog is DeleteConfirmationPolicies.PermanentOnly && foreach (var src in source) { - if (RecycleBinHelpers.IsPathUnderRecycleBin(src.Path)) + if (WindowsRecycleBinService.IsRecycled(src.Path)) { - binItems ??= await RecycleBinHelpers.EnumerateRecycleBin(); + binItems ??= await WindowsRecycleBinService.GetAllRecycleBinFoldersAsync(); // Might still be null because we're deserializing the list from Json if (!binItems.IsEmpty()) @@ -363,9 +364,9 @@ public async Task CopyItemsFromClipboard(DataPackageView packageVi List? binItems = null; foreach (var item in source) { - if (RecycleBinHelpers.IsPathUnderRecycleBin(item.Path)) + if (WindowsRecycleBinService.IsRecycled(item.Path)) { - binItems ??= await RecycleBinHelpers.EnumerateRecycleBin(); + binItems ??= await WindowsRecycleBinService.GetAllRecycleBinFoldersAsync(); if (!binItems.IsEmpty()) // Might still be null because we're deserializing the list from Json { var matchingItem = binItems.FirstOrDefault(x => x.RecyclePath == item.Path); // Get original file name @@ -511,9 +512,9 @@ public async Task MoveItemsFromClipboard(DataPackageView packageVi List? binItems = null; foreach (var item in source) { - if (RecycleBinHelpers.IsPathUnderRecycleBin(item.Path)) + if (WindowsRecycleBinService.IsRecycled(item.Path)) { - binItems ??= await RecycleBinHelpers.EnumerateRecycleBin(); + binItems ??= await WindowsRecycleBinService.GetAllRecycleBinFoldersAsync(); if (!binItems.IsEmpty()) // Might still be null because we're deserializing the list from Json { var matchingItem = binItems.FirstOrDefault(x => x.RecyclePath == item.Path); // Get original file name @@ -636,7 +637,7 @@ public async Task RecycleItemsFromClipboard(DataPackageView packag var source = await GetDraggedStorageItems(packageView); ReturnResult returnStatus = ReturnResult.InProgress; - source = source.Where(x => !RecycleBinHelpers.IsPathUnderRecycleBin(x.Path)); // Can't recycle items already in recyclebin + source = source.Where(x => !WindowsRecycleBinService.IsRecycled(x.Path)); // Can't recycle items already in recyclebin returnStatus = await DeleteItemsAsync(source, showDialog, false, registerHistory); return returnStatus; diff --git a/src/Files.App/Utils/Storage/Operations/FilesystemOperations.cs b/src/Files.App/Utils/Storage/Operations/FilesystemOperations.cs index 1d5f420e8372..ee3087198cb5 100644 --- a/src/Files.App/Utils/Storage/Operations/FilesystemOperations.cs +++ b/src/Files.App/Utils/Storage/Operations/FilesystemOperations.cs @@ -16,6 +16,8 @@ namespace Files.App.Utils.Storage /// public sealed class FilesystemOperations : IFilesystemOperations { + private readonly IWindowsRecycleBinService WindowsRecycleBinService = Ioc.Default.GetRequiredService(); + private IShellPage _associatedInstance; public FilesystemOperations(IShellPage associatedInstance) @@ -498,7 +500,7 @@ public async Task DeleteAsync(IStorageItemWithPath source, IPro fsProgress.Report(); - bool deleteFromRecycleBin = RecycleBinHelpers.IsPathUnderRecycleBin(source.Path); + bool deleteFromRecycleBin = WindowsRecycleBinService.IsRecycled(source.Path); FilesystemResult fsResult = FileSystemStatusCode.InProgress; @@ -549,7 +551,7 @@ await _associatedInstance.ShellViewModel.GetFileFromPathAsync(iFilePath) if (!permanently) { // Enumerate Recycle Bin - IEnumerable nameMatchItems, items = await RecycleBinHelpers.EnumerateRecycleBin(); + IEnumerable nameMatchItems, items = await WindowsRecycleBinService.GetAllRecycleBinFoldersAsync(); // Get name matching files if (FileExtensionHelpers.IsShortcutOrUrlFile(source.Path)) // We need to check if it is a shortcut file @@ -934,7 +936,7 @@ public async Task DeleteItemsAsync(IList if (token.IsCancellationRequested) break; - permanently = RecycleBinHelpers.IsPathUnderRecycleBin(source[i].Path) || originalPermanently; + permanently = WindowsRecycleBinService.IsRecycled(source[i].Path) || originalPermanently; rawStorageHistory.Add(await DeleteAsync(source[i], null, permanently, token)); fsProgress.AddProcessedItemsCount(1); diff --git a/src/Files.App/Utils/Storage/Operations/ShellFilesystemOperations.cs b/src/Files.App/Utils/Storage/Operations/ShellFilesystemOperations.cs index bcab988dc692..ca9a6254b303 100644 --- a/src/Files.App/Utils/Storage/Operations/ShellFilesystemOperations.cs +++ b/src/Files.App/Utils/Storage/Operations/ShellFilesystemOperations.cs @@ -11,6 +11,8 @@ namespace Files.App.Utils.Storage /// public sealed class ShellFilesystemOperations : IFilesystemOperations { + private readonly IWindowsRecycleBinService WindowsRecycleBinService = Ioc.Default.GetRequiredService(); + private IShellPage _associatedInstance; private FilesystemOperations _filesystemOperations; @@ -358,7 +360,7 @@ public async Task DeleteItemsAsync(IList fsProgress.Report(); var deleteFilePaths = source.Select(s => s.Path).Distinct(); - var deleteFromRecycleBin = source.Any() && RecycleBinHelpers.IsPathUnderRecycleBin(source.ElementAt(0).Path); + var deleteFromRecycleBin = source.Any() && WindowsRecycleBinService.IsRecycled(source.ElementAt(0).Path); permanently |= deleteFromRecycleBin; @@ -845,9 +847,9 @@ private async Task GetFileListDialog(IEnumerable source, s List binItems = null; foreach (var src in source) { - if (RecycleBinHelpers.IsPathUnderRecycleBin(src)) + if (WindowsRecycleBinService.IsRecycled(src)) { - binItems ??= await RecycleBinHelpers.EnumerateRecycleBin(); + binItems ??= await WindowsRecycleBinService.GetAllRecycleBinFoldersAsync(); // Might still be null because we're deserializing the list from Json if (!binItems.IsEmpty()) diff --git a/src/Files.App/Utils/Storage/Search/FolderSearch.cs b/src/Files.App/Utils/Storage/Search/FolderSearch.cs index 23432318648a..5bd100fe19e4 100644 --- a/src/Files.App/Utils/Storage/Search/FolderSearch.cs +++ b/src/Files.App/Utils/Storage/Search/FolderSearch.cs @@ -16,6 +16,7 @@ public sealed class FolderSearch { private IUserSettingsService UserSettingsService { get; } = Ioc.Default.GetRequiredService(); private DrivesViewModel drivesViewModel = Ioc.Default.GetRequiredService(); + private readonly IWindowsRecycleBinService WindowsRecycleBinService = Ioc.Default.GetRequiredService(); private readonly IFileTagsSettingsService fileTagsSettingsService = Ioc.Default.GetRequiredService(); @@ -198,7 +199,7 @@ private async Task SearchTagsAsync(string folder, IList results, Can var matches = dbInstance.GetAllUnderPath(folder) .Where(x => tags.All(x.Tags.Contains)); if (string.IsNullOrEmpty(folder)) - matches = matches.Where(x => !RecycleBinHelpers.IsPathUnderRecycleBin(x.FilePath)); + matches = matches.Where(x => !WindowsRecycleBinService.IsRecycled(x.FilePath)); foreach (var match in matches) { diff --git a/src/Files.App/ViewModels/Properties/Items/FolderProperties.cs b/src/Files.App/ViewModels/Properties/Items/FolderProperties.cs index 4f50516fe32a..b42f3a7ec7dc 100644 --- a/src/Files.App/ViewModels/Properties/Items/FolderProperties.cs +++ b/src/Files.App/ViewModels/Properties/Items/FolderProperties.cs @@ -10,6 +10,8 @@ namespace Files.App.ViewModels.Properties { internal sealed class FolderProperties : BaseProperties { + private readonly IWindowsRecycleBinService WindowsRecycleBinService = Ioc.Default.GetRequiredService(); + public ListedItem Item { get; } public FolderProperties( @@ -123,7 +125,7 @@ CloudDriveSyncStatus.FolderOnline and not } else if (Item.ItemPath.Equals(Constants.UserEnvironmentPaths.RecycleBinPath, StringComparison.OrdinalIgnoreCase)) { - var recycleBinQuery = Win32Helper.QueryRecycleBin(); + var recycleBinQuery = WindowsRecycleBinService.QueryRecycleBin(); if (recycleBinQuery.BinSize is long binSize) { ViewModel.ItemSizeBytes = binSize; diff --git a/src/Files.App/ViewModels/ShellViewModel.cs b/src/Files.App/ViewModels/ShellViewModel.cs index ea51b1edd52f..71c07f5c871b 100644 --- a/src/Files.App/ViewModels/ShellViewModel.cs +++ b/src/Files.App/ViewModels/ShellViewModel.cs @@ -56,6 +56,7 @@ public sealed class ShellViewModel : ObservableObject, IDisposable private readonly ISizeProvider folderSizeProvider = Ioc.Default.GetRequiredService(); private readonly IStorageCacheService fileListCache = Ioc.Default.GetRequiredService(); private readonly IWindowsSecurityService WindowsSecurityService = Ioc.Default.GetRequiredService(); + private readonly IWindowsRecycleBinService WindowsRecycleBinService = Ioc.Default.GetRequiredService(); // Only used for Binding and ApplyFilesAndFoldersChangesAsync, don't manipulate on this! public BulkConcurrentObservableCollection FilesAndFolders { get; } @@ -536,9 +537,9 @@ public ShellViewModel(LayoutPreferencesManager folderSettingsViewModel) fileTagsSettingsService.OnTagsUpdated += FileTagsSettingsService_OnSettingUpdated; folderSizeProvider.SizeChanged += FolderSizeProvider_SizeChanged; folderSettings.LayoutModeChangeRequested += LayoutModeChangeRequested; - RecycleBinManager.Default.RecycleBinItemCreated += RecycleBinItemCreatedAsync; - RecycleBinManager.Default.RecycleBinItemDeleted += RecycleBinItemDeletedAsync; - RecycleBinManager.Default.RecycleBinRefreshRequested += RecycleBinRefreshRequestedAsync; + WindowsRecycleBinService.Watcher.ItemAdded += RecycleBinItemCreatedAsync; + WindowsRecycleBinService.Watcher.ItemDeleted += RecycleBinItemDeletedAsync; + WindowsRecycleBinService.Watcher.RefreshRequested += RecycleBinRefreshRequestedAsync; } private async void LayoutModeChangeRequested(object? sender, LayoutModeEventArgs e) @@ -2590,9 +2591,9 @@ public void UpdateDateDisplay(bool isFormatChange) public void Dispose() { CancelLoadAndClearFiles(); - RecycleBinManager.Default.RecycleBinItemCreated -= RecycleBinItemCreatedAsync; - RecycleBinManager.Default.RecycleBinItemDeleted -= RecycleBinItemDeletedAsync; - RecycleBinManager.Default.RecycleBinRefreshRequested -= RecycleBinRefreshRequestedAsync; + WindowsRecycleBinService.Watcher.ItemAdded -= RecycleBinItemCreatedAsync; + WindowsRecycleBinService.Watcher.ItemDeleted -= RecycleBinItemDeletedAsync; + WindowsRecycleBinService.Watcher.RefreshRequested -= RecycleBinRefreshRequestedAsync; UserSettingsService.OnSettingChangedEvent -= UserSettingsService_OnSettingChangedEvent; fileTagsSettingsService.OnSettingImportedEvent -= FileTagsSettingsService_OnSettingUpdated; fileTagsSettingsService.OnTagsUpdated -= FileTagsSettingsService_OnSettingUpdated; diff --git a/src/Files.App/Views/Layouts/BaseLayoutPage.cs b/src/Files.App/Views/Layouts/BaseLayoutPage.cs index b33f10283e89..5fa1002cc52a 100644 --- a/src/Files.App/Views/Layouts/BaseLayoutPage.cs +++ b/src/Files.App/Views/Layouts/BaseLayoutPage.cs @@ -41,6 +41,7 @@ public abstract class BaseLayoutPage : Page, IBaseLayoutPage, INotifyPropertyCha protected ICommandManager Commands { get; } = Ioc.Default.GetRequiredService(); public InfoPaneViewModel InfoPaneViewModel { get; } = Ioc.Default.GetRequiredService(); protected readonly IWindowContext WindowContext = Ioc.Default.GetRequiredService(); + protected readonly IWindowsRecycleBinService WindowsRecycleBinService = Ioc.Default.GetRequiredService(); // ViewModels @@ -1291,7 +1292,7 @@ protected void InitializeDrag(UIElement container, ListedItem item) return; UninitializeDrag(container); - if ((item.PrimaryItemAttribute == StorageItemTypes.Folder && !RecycleBinHelpers.IsPathUnderRecycleBin(item.ItemPath)) + if ((item.PrimaryItemAttribute == StorageItemTypes.Folder && !WindowsRecycleBinService.IsRecycled(item.ItemPath)) || item.IsExecutable || item.IsScriptFile) { diff --git a/src/Files.Core.Storage/Contracts/ITrashWatcher.cs b/src/Files.Core.Storage/Contracts/ITrashWatcher.cs index 0bf261fe07a6..6ec106c24f61 100644 --- a/src/Files.Core.Storage/Contracts/ITrashWatcher.cs +++ b/src/Files.Core.Storage/Contracts/ITrashWatcher.cs @@ -1,9 +1,9 @@ -// Copyright (c) 2023 Files Community +// Copyright (c) 2023 Files Community // Licensed under the MIT License. See the LICENSE. namespace Files.Core.Storage.Contracts { - public interface ITrashWatcher + public interface ITrashWatcher : IWatcher { /// /// Gets invoked when an item addition is detected by the watcher diff --git a/src/Files.Core.Storage/Contracts/IWatcher.cs b/src/Files.Core.Storage/Contracts/IWatcher.cs index f62ad0ab065e..bbda993b6985 100644 --- a/src/Files.Core.Storage/Contracts/IWatcher.cs +++ b/src/Files.Core.Storage/Contracts/IWatcher.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2023 Files Community +// Copyright (c) 2023 Files Community // Licensed under the MIT License. See the LICENSE. namespace Files.Core.Storage.Contracts @@ -6,7 +6,7 @@ namespace Files.Core.Storage.Contracts /// /// A disposable object which can notify of changes to the folder. /// - public interface IWatcher : IDisposable, IAsyncDisposable + public interface IWatcher : IDisposable { /// /// Starts the watcher From 77a5b8a4f1682ffd1cf8a6c9e643b05dc21a307d Mon Sep 17 00:00:00 2001 From: 0x5BFA Date: Fri, 23 Aug 2024 18:00:23 +0900 Subject: [PATCH 2/4] Req --- .../Watchers/RecycleBinWatcher.cs | 35 +++++++++---------- .../FileSystem/EmptyRecycleBinAction.cs | 6 ++-- .../FileSystem/RestoreAllRecycleBinAction.cs | 6 ++-- ...nService.cs => IStorageTrashBinService.cs} | 10 +++--- src/Files.App/Data/Items/LocationItem.cs | 10 +++--- .../Helpers/Application/AppLifecycleHelper.cs | 2 +- src/Files.App/Services/App/FileTagsService.cs | 4 +-- .../StorageTrashBinService.cs} | 12 +++---- .../Storage/Operations/FilesystemHelpers.cs | 20 +++++------ .../Operations/FilesystemOperations.cs | 8 ++--- .../Operations/ShellFilesystemOperations.cs | 8 ++--- .../Utils/Storage/Search/FolderSearch.cs | 6 ++-- .../Properties/Items/FolderProperties.cs | 4 +-- src/Files.App/ViewModels/ShellViewModel.cs | 14 ++++---- src/Files.App/Views/Layouts/BaseLayoutPage.cs | 4 +-- .../Contracts/ITrashWatcher.cs | 5 --- 16 files changed, 73 insertions(+), 81 deletions(-) rename src/Files.App/Data/Contracts/{IWindowsRecycleBinService.cs => IStorageTrashBinService.cs} (91%) rename src/Files.App/Services/{Windows/WindowsRecycleBinService.cs => Storage/StorageTrashBinService.cs} (93%) diff --git a/src/Files.App.Storage/Watchers/RecycleBinWatcher.cs b/src/Files.App.Storage/Watchers/RecycleBinWatcher.cs index 1583e35e8b92..7ce564557268 100644 --- a/src/Files.App.Storage/Watchers/RecycleBinWatcher.cs +++ b/src/Files.App.Storage/Watchers/RecycleBinWatcher.cs @@ -1,6 +1,7 @@ // Copyright (c) 2024 Files Community // Licensed under the MIT License. See the LICENSE. +using Files.Shared.Extensions; using System.Security.Principal; namespace Files.App.Storage.Watchers @@ -15,9 +16,6 @@ public class RecycleBinWatcher : ITrashWatcher /// public event EventHandler? ItemDeleted; - /// - public event EventHandler? ItemChanged; - /// public event EventHandler? ItemRenamed; @@ -35,16 +33,13 @@ public RecycleBinWatcher() /// public void StartWatcher() { - // NOTE: - // SHChangeNotifyRegister only works if recycle bin is open in File Explorer. - // Create file system watcher to monitor recycle bin folder(s) instead. + // NOTE: SHChangeNotifyRegister only works if recycle bin is open in File Explorer. // Listen changes only on the Recycle Bin that the current logon user has var sid = WindowsIdentity.GetCurrent().User?.ToString() ?? string.Empty; if (string.IsNullOrEmpty(sid)) return; - // TODO: Use IStorageDevicesService to enumerate drives foreach (var drive in SystemIO.DriveInfo.GetDrives()) { var recyclePath = SystemIO.Path.Combine(drive.Name, "$RECYCLE.BIN", sid); @@ -53,18 +48,22 @@ public void StartWatcher() !SystemIO.Directory.Exists(recyclePath)) continue; - SystemIO.FileSystemWatcher watcher = new() + // NOTE: Suppressed NullReferenceException caused by EnableRaisingEvents in #15808 + SafetyExtensions.IgnoreExceptions(() => { - Path = recyclePath, - Filter = "*.*", - NotifyFilter = SystemIO.NotifyFilters.LastWrite | SystemIO.NotifyFilters.FileName | SystemIO.NotifyFilters.DirectoryName - }; - - watcher.Created += Watcher_Changed; - watcher.Deleted += Watcher_Changed; - watcher.EnableRaisingEvents = true; - - _watchers.Add(watcher); + SystemIO.FileSystemWatcher watcher = new() + { + Path = recyclePath, + Filter = "*.*", + NotifyFilter = SystemIO.NotifyFilters.LastWrite | SystemIO.NotifyFilters.FileName | SystemIO.NotifyFilters.DirectoryName + }; + + watcher.Created += Watcher_Changed; + watcher.Deleted += Watcher_Changed; + watcher.EnableRaisingEvents = true; + + _watchers.Add(watcher); + }); } } diff --git a/src/Files.App/Actions/FileSystem/EmptyRecycleBinAction.cs b/src/Files.App/Actions/FileSystem/EmptyRecycleBinAction.cs index 8bae356d6af2..44dd376df750 100644 --- a/src/Files.App/Actions/FileSystem/EmptyRecycleBinAction.cs +++ b/src/Files.App/Actions/FileSystem/EmptyRecycleBinAction.cs @@ -8,7 +8,7 @@ namespace Files.App.Actions { internal sealed class EmptyRecycleBinAction : BaseUIAction, IAction { - private readonly IWindowsRecycleBinService WindowsRecycleBinService = Ioc.Default.GetRequiredService(); + private readonly IStorageTrashBinService StorageTrashBinService = Ioc.Default.GetRequiredService(); private readonly StatusCenterViewModel StatusCenterViewModel = Ioc.Default.GetRequiredService(); private readonly IUserSettingsService UserSettingsService = Ioc.Default.GetRequiredService(); private readonly IContentPageContext context; @@ -25,7 +25,7 @@ public RichGlyph Glyph public override bool IsExecutable => UIHelpers.CanShowDialog && ((context.PageType == ContentPageTypes.RecycleBin && context.HasItem) || - WindowsRecycleBinService.HasItems()); + StorageTrashBinService.HasItems()); public EmptyRecycleBinAction() { @@ -54,7 +54,7 @@ await confirmationDialog.TryShowAsync() is ContentDialogResult.Primary) { var banner = StatusCenterHelper.AddCard_EmptyRecycleBin(ReturnResult.InProgress); - bool result = await Task.Run(WindowsRecycleBinService.DeleteAllAsync); + bool result = await Task.Run(StorageTrashBinService.EmptyTrashBin); StatusCenterViewModel.RemoveItem(banner); diff --git a/src/Files.App/Actions/FileSystem/RestoreAllRecycleBinAction.cs b/src/Files.App/Actions/FileSystem/RestoreAllRecycleBinAction.cs index 6fd4c98e7e49..8d0db701076d 100644 --- a/src/Files.App/Actions/FileSystem/RestoreAllRecycleBinAction.cs +++ b/src/Files.App/Actions/FileSystem/RestoreAllRecycleBinAction.cs @@ -8,7 +8,7 @@ namespace Files.App.Actions { internal sealed class RestoreAllRecycleBinAction : BaseUIAction, IAction { - private readonly IWindowsRecycleBinService WindowsRecycleBinService = Ioc.Default.GetRequiredService(); + private readonly IStorageTrashBinService StorageTrashBinService = Ioc.Default.GetRequiredService(); public string Label => "RestoreAllItems".GetLocalizedResource(); @@ -21,7 +21,7 @@ public RichGlyph Glyph public override bool IsExecutable => UIHelpers.CanShowDialog && - WindowsRecycleBinService.HasItems(); + StorageTrashBinService.HasItems(); public async Task ExecuteAsync(object? parameter = null) { @@ -41,7 +41,7 @@ public async Task ExecuteAsync(object? parameter = null) if (await confirmationDialog.TryShowAsync() is not ContentDialogResult.Primary) return; - bool result = await Task.Run(WindowsRecycleBinService.RestoreAllAsync); + bool result = await Task.Run(StorageTrashBinService.RestoreAllTrashes); // Show error dialog when failed if (!result) diff --git a/src/Files.App/Data/Contracts/IWindowsRecycleBinService.cs b/src/Files.App/Data/Contracts/IStorageTrashBinService.cs similarity index 91% rename from src/Files.App/Data/Contracts/IWindowsRecycleBinService.cs rename to src/Files.App/Data/Contracts/IStorageTrashBinService.cs index a97f82df1fc1..98b34e0e21dc 100644 --- a/src/Files.App/Data/Contracts/IWindowsRecycleBinService.cs +++ b/src/Files.App/Data/Contracts/IStorageTrashBinService.cs @@ -6,7 +6,7 @@ namespace Files.App.Data.Contracts /// /// Provides service for Recycle Bin on Windows. /// - public interface IWindowsRecycleBinService + public interface IStorageTrashBinService { /// /// Gets the watcher of Recycle Bin folder. @@ -43,25 +43,25 @@ public interface IWindowsRecycleBinService /// /// The path that indicates to a file or folder. /// True if the file or path is recycled; otherwise, false. - bool IsRecycled(string? path); + bool IsUnderTrashBin(string? path); /// /// Gets the file or folder specified can be moved to Recycle Bin. /// /// /// - Task IsRecyclableAsync(string? path); + Task CanGoTrashBin(string? path); /// /// Deletes files and folders in Recycle Bin permanently. /// /// True if succeeded; otherwise, false - bool DeleteAllAsync(); + bool EmptyTrashBin(); /// /// Restores files and folders in Recycle Bin to original paths. /// /// True if succeeded; otherwise, false - bool RestoreAllAsync(); + bool RestoreAllTrashes(); } } diff --git a/src/Files.App/Data/Items/LocationItem.cs b/src/Files.App/Data/Items/LocationItem.cs index 7b2aae4984a8..5e0933c15561 100644 --- a/src/Files.App/Data/Items/LocationItem.cs +++ b/src/Files.App/Data/Items/LocationItem.cs @@ -124,13 +124,13 @@ public int CompareTo(INavigationControlItem other) public sealed class RecycleBinLocationItem : LocationItem { - private readonly IWindowsRecycleBinService WindowsRecycleBinService = Ioc.Default.GetRequiredService(); + private readonly IStorageTrashBinService StorageTrashBinService = Ioc.Default.GetRequiredService(); public async void RefreshSpaceUsed(object? sender, FileSystemEventArgs e) { await MainWindow.Instance.DispatcherQueue.EnqueueOrInvokeAsync(() => { - SpaceUsed = WindowsRecycleBinService.GetSize(); + SpaceUsed = StorageTrashBinService.GetSize(); }); } @@ -152,10 +152,10 @@ public override object ToolTip public RecycleBinLocationItem() { - SpaceUsed = WindowsRecycleBinService.GetSize(); + SpaceUsed = StorageTrashBinService.GetSize(); - WindowsRecycleBinService.Watcher.ItemAdded += RefreshSpaceUsed; - WindowsRecycleBinService.Watcher.ItemDeleted += RefreshSpaceUsed; + StorageTrashBinService.Watcher.ItemAdded += RefreshSpaceUsed; + StorageTrashBinService.Watcher.ItemDeleted += RefreshSpaceUsed; } } } diff --git a/src/Files.App/Helpers/Application/AppLifecycleHelper.cs b/src/Files.App/Helpers/Application/AppLifecycleHelper.cs index 82ca83a99f3f..926adc206dc3 100644 --- a/src/Files.App/Helpers/Application/AppLifecycleHelper.cs +++ b/src/Files.App/Helpers/Application/AppLifecycleHelper.cs @@ -196,7 +196,7 @@ public static IHost ConfigureHost() .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton() + .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() diff --git a/src/Files.App/Services/App/FileTagsService.cs b/src/Files.App/Services/App/FileTagsService.cs index ba4242ed1c1d..e2bbe26bcbe7 100644 --- a/src/Files.App/Services/App/FileTagsService.cs +++ b/src/Files.App/Services/App/FileTagsService.cs @@ -11,7 +11,7 @@ namespace Files.App.Services /// internal sealed class FileTagsService : IFileTagsService { - private readonly IWindowsRecycleBinService WindowsRecycleBinService = Ioc.Default.GetRequiredService(); + private readonly IStorageTrashBinService StorageTrashBinService = Ioc.Default.GetRequiredService(); private readonly IFileTagsSettingsService FileTagsSettingsService = Ioc.Default.GetRequiredService(); private readonly IStorageService StorageService = Ioc.Default.GetRequiredService(); @@ -42,7 +42,7 @@ public async IAsyncEnumerable GetItemsForTagAsync(string tagUid { foreach (var item in FileTagsHelper.GetDbInstance().GetAll()) { - if (!item.Tags.Contains(tagUid) || WindowsRecycleBinService.IsRecycled(item.FilePath)) + if (!item.Tags.Contains(tagUid) || StorageTrashBinService.IsUnderTrashBin(item.FilePath)) continue; var storable = await StorageService.TryGetStorableAsync(item.FilePath, cancellationToken); diff --git a/src/Files.App/Services/Windows/WindowsRecycleBinService.cs b/src/Files.App/Services/Storage/StorageTrashBinService.cs similarity index 93% rename from src/Files.App/Services/Windows/WindowsRecycleBinService.cs rename to src/Files.App/Services/Storage/StorageTrashBinService.cs index 7a9e2bfc9cd6..fcca2d532977 100644 --- a/src/Files.App/Services/Windows/WindowsRecycleBinService.cs +++ b/src/Files.App/Services/Storage/StorageTrashBinService.cs @@ -9,8 +9,8 @@ namespace Files.App.Services { - /// - public class WindowsRecycleBinService : IWindowsRecycleBinService + /// + public class StorageTrashBinService : IStorageTrashBinService { /// public RecycleBinWatcher Watcher { get; private set; } = new(); @@ -46,7 +46,7 @@ public bool HasItems() } /// - public bool IsRecycled(string? path) + public bool IsUnderTrashBin(string? path) { return !string.IsNullOrWhiteSpace(path) && @@ -54,7 +54,7 @@ public bool IsRecycled(string? path) } /// - public async Task IsRecyclableAsync(string? path) + public async Task CanGoTrashBin(string? path) { if (string.IsNullOrEmpty(path) || path.StartsWith(@"\\?\", StringComparison.Ordinal)) @@ -68,7 +68,7 @@ public async Task IsRecyclableAsync(string? path) } /// - public bool DeleteAllAsync() + public bool EmptyTrashBin() { var fRes = PInvoke.SHEmptyRecycleBin( new(), @@ -80,7 +80,7 @@ public bool DeleteAllAsync() } /// - public unsafe bool RestoreAllAsync() + public unsafe bool RestoreAllTrashes() { IShellItem* recycleBinFolderShellItem = default; IEnumShellItems* enumShellItems = default; diff --git a/src/Files.App/Utils/Storage/Operations/FilesystemHelpers.cs b/src/Files.App/Utils/Storage/Operations/FilesystemHelpers.cs index 4dc32e70d4bb..62225a2fcab2 100644 --- a/src/Files.App/Utils/Storage/Operations/FilesystemHelpers.cs +++ b/src/Files.App/Utils/Storage/Operations/FilesystemHelpers.cs @@ -20,7 +20,7 @@ namespace Files.App.Utils.Storage { public sealed class FilesystemHelpers : IFilesystemHelpers { - private readonly IWindowsRecycleBinService WindowsRecycleBinService = Ioc.Default.GetRequiredService(); + private readonly IStorageTrashBinService StorageTrashBinService = Ioc.Default.GetRequiredService(); private readonly static StatusCenterViewModel _statusCenterViewModel = Ioc.Default.GetRequiredService(); private IShellPage associatedInstance; @@ -91,8 +91,8 @@ public async Task DeleteItemsAsync(IEnumerable item.Path).Any(WindowsRecycleBinService.IsRecycled); - var canBeSentToBin = !deleteFromRecycleBin && await WindowsRecycleBinService.IsRecyclableAsync(source.FirstOrDefault()?.Path); + var deleteFromRecycleBin = source.Select(item => item.Path).Any(StorageTrashBinService.IsUnderTrashBin); + var canBeSentToBin = !deleteFromRecycleBin && await StorageTrashBinService.CanGoTrashBin(source.FirstOrDefault()?.Path); if (showDialog is DeleteConfirmationPolicies.Always || showDialog is DeleteConfirmationPolicies.PermanentOnly && @@ -103,9 +103,9 @@ showDialog is DeleteConfirmationPolicies.PermanentOnly && foreach (var src in source) { - if (WindowsRecycleBinService.IsRecycled(src.Path)) + if (StorageTrashBinService.IsUnderTrashBin(src.Path)) { - binItems ??= await WindowsRecycleBinService.GetAllRecycleBinFoldersAsync(); + binItems ??= await StorageTrashBinService.GetAllRecycleBinFoldersAsync(); // Might still be null because we're deserializing the list from Json if (!binItems.IsEmpty()) @@ -364,9 +364,9 @@ public async Task CopyItemsFromClipboard(DataPackageView packageVi List? binItems = null; foreach (var item in source) { - if (WindowsRecycleBinService.IsRecycled(item.Path)) + if (StorageTrashBinService.IsUnderTrashBin(item.Path)) { - binItems ??= await WindowsRecycleBinService.GetAllRecycleBinFoldersAsync(); + binItems ??= await StorageTrashBinService.GetAllRecycleBinFoldersAsync(); if (!binItems.IsEmpty()) // Might still be null because we're deserializing the list from Json { var matchingItem = binItems.FirstOrDefault(x => x.RecyclePath == item.Path); // Get original file name @@ -512,9 +512,9 @@ public async Task MoveItemsFromClipboard(DataPackageView packageVi List? binItems = null; foreach (var item in source) { - if (WindowsRecycleBinService.IsRecycled(item.Path)) + if (StorageTrashBinService.IsUnderTrashBin(item.Path)) { - binItems ??= await WindowsRecycleBinService.GetAllRecycleBinFoldersAsync(); + binItems ??= await StorageTrashBinService.GetAllRecycleBinFoldersAsync(); if (!binItems.IsEmpty()) // Might still be null because we're deserializing the list from Json { var matchingItem = binItems.FirstOrDefault(x => x.RecyclePath == item.Path); // Get original file name @@ -637,7 +637,7 @@ public async Task RecycleItemsFromClipboard(DataPackageView packag var source = await GetDraggedStorageItems(packageView); ReturnResult returnStatus = ReturnResult.InProgress; - source = source.Where(x => !WindowsRecycleBinService.IsRecycled(x.Path)); // Can't recycle items already in recyclebin + source = source.Where(x => !StorageTrashBinService.IsUnderTrashBin(x.Path)); // Can't recycle items already in recyclebin returnStatus = await DeleteItemsAsync(source, showDialog, false, registerHistory); return returnStatus; diff --git a/src/Files.App/Utils/Storage/Operations/FilesystemOperations.cs b/src/Files.App/Utils/Storage/Operations/FilesystemOperations.cs index ee3087198cb5..dbf8dbeb0fae 100644 --- a/src/Files.App/Utils/Storage/Operations/FilesystemOperations.cs +++ b/src/Files.App/Utils/Storage/Operations/FilesystemOperations.cs @@ -16,7 +16,7 @@ namespace Files.App.Utils.Storage /// public sealed class FilesystemOperations : IFilesystemOperations { - private readonly IWindowsRecycleBinService WindowsRecycleBinService = Ioc.Default.GetRequiredService(); + private readonly IStorageTrashBinService StorageTrashBinService = Ioc.Default.GetRequiredService(); private IShellPage _associatedInstance; @@ -500,7 +500,7 @@ public async Task DeleteAsync(IStorageItemWithPath source, IPro fsProgress.Report(); - bool deleteFromRecycleBin = WindowsRecycleBinService.IsRecycled(source.Path); + bool deleteFromRecycleBin = StorageTrashBinService.IsUnderTrashBin(source.Path); FilesystemResult fsResult = FileSystemStatusCode.InProgress; @@ -551,7 +551,7 @@ await _associatedInstance.ShellViewModel.GetFileFromPathAsync(iFilePath) if (!permanently) { // Enumerate Recycle Bin - IEnumerable nameMatchItems, items = await WindowsRecycleBinService.GetAllRecycleBinFoldersAsync(); + IEnumerable nameMatchItems, items = await StorageTrashBinService.GetAllRecycleBinFoldersAsync(); // Get name matching files if (FileExtensionHelpers.IsShortcutOrUrlFile(source.Path)) // We need to check if it is a shortcut file @@ -936,7 +936,7 @@ public async Task DeleteItemsAsync(IList if (token.IsCancellationRequested) break; - permanently = WindowsRecycleBinService.IsRecycled(source[i].Path) || originalPermanently; + permanently = StorageTrashBinService.IsUnderTrashBin(source[i].Path) || originalPermanently; rawStorageHistory.Add(await DeleteAsync(source[i], null, permanently, token)); fsProgress.AddProcessedItemsCount(1); diff --git a/src/Files.App/Utils/Storage/Operations/ShellFilesystemOperations.cs b/src/Files.App/Utils/Storage/Operations/ShellFilesystemOperations.cs index ca9a6254b303..e88aaaf888ce 100644 --- a/src/Files.App/Utils/Storage/Operations/ShellFilesystemOperations.cs +++ b/src/Files.App/Utils/Storage/Operations/ShellFilesystemOperations.cs @@ -11,7 +11,7 @@ namespace Files.App.Utils.Storage /// public sealed class ShellFilesystemOperations : IFilesystemOperations { - private readonly IWindowsRecycleBinService WindowsRecycleBinService = Ioc.Default.GetRequiredService(); + private readonly IStorageTrashBinService StorageTrashBinService = Ioc.Default.GetRequiredService(); private IShellPage _associatedInstance; @@ -360,7 +360,7 @@ public async Task DeleteItemsAsync(IList fsProgress.Report(); var deleteFilePaths = source.Select(s => s.Path).Distinct(); - var deleteFromRecycleBin = source.Any() && WindowsRecycleBinService.IsRecycled(source.ElementAt(0).Path); + var deleteFromRecycleBin = source.Any() && StorageTrashBinService.IsUnderTrashBin(source.ElementAt(0).Path); permanently |= deleteFromRecycleBin; @@ -847,9 +847,9 @@ private async Task GetFileListDialog(IEnumerable source, s List binItems = null; foreach (var src in source) { - if (WindowsRecycleBinService.IsRecycled(src)) + if (StorageTrashBinService.IsUnderTrashBin(src)) { - binItems ??= await WindowsRecycleBinService.GetAllRecycleBinFoldersAsync(); + binItems ??= await StorageTrashBinService.GetAllRecycleBinFoldersAsync(); // Might still be null because we're deserializing the list from Json if (!binItems.IsEmpty()) diff --git a/src/Files.App/Utils/Storage/Search/FolderSearch.cs b/src/Files.App/Utils/Storage/Search/FolderSearch.cs index 5bd100fe19e4..487352966adc 100644 --- a/src/Files.App/Utils/Storage/Search/FolderSearch.cs +++ b/src/Files.App/Utils/Storage/Search/FolderSearch.cs @@ -6,7 +6,6 @@ using Windows.Storage; using Windows.Storage.FileProperties; using Windows.Storage.Search; -using static Files.App.Helpers.Win32Helper; using FileAttributes = System.IO.FileAttributes; using WIN32_FIND_DATA = Files.App.Helpers.Win32PInvoke.WIN32_FIND_DATA; @@ -16,8 +15,7 @@ public sealed class FolderSearch { private IUserSettingsService UserSettingsService { get; } = Ioc.Default.GetRequiredService(); private DrivesViewModel drivesViewModel = Ioc.Default.GetRequiredService(); - private readonly IWindowsRecycleBinService WindowsRecycleBinService = Ioc.Default.GetRequiredService(); - + private readonly IStorageTrashBinService StorageTrashBinService = Ioc.Default.GetRequiredService(); private readonly IFileTagsSettingsService fileTagsSettingsService = Ioc.Default.GetRequiredService(); private const uint defaultStepSize = 500; @@ -199,7 +197,7 @@ private async Task SearchTagsAsync(string folder, IList results, Can var matches = dbInstance.GetAllUnderPath(folder) .Where(x => tags.All(x.Tags.Contains)); if (string.IsNullOrEmpty(folder)) - matches = matches.Where(x => !WindowsRecycleBinService.IsRecycled(x.FilePath)); + matches = matches.Where(x => !StorageTrashBinService.IsUnderTrashBin(x.FilePath)); foreach (var match in matches) { diff --git a/src/Files.App/ViewModels/Properties/Items/FolderProperties.cs b/src/Files.App/ViewModels/Properties/Items/FolderProperties.cs index b42f3a7ec7dc..a24275c367e0 100644 --- a/src/Files.App/ViewModels/Properties/Items/FolderProperties.cs +++ b/src/Files.App/ViewModels/Properties/Items/FolderProperties.cs @@ -10,7 +10,7 @@ namespace Files.App.ViewModels.Properties { internal sealed class FolderProperties : BaseProperties { - private readonly IWindowsRecycleBinService WindowsRecycleBinService = Ioc.Default.GetRequiredService(); + private readonly IStorageTrashBinService StorageTrashBinService = Ioc.Default.GetRequiredService(); public ListedItem Item { get; } @@ -125,7 +125,7 @@ CloudDriveSyncStatus.FolderOnline and not } else if (Item.ItemPath.Equals(Constants.UserEnvironmentPaths.RecycleBinPath, StringComparison.OrdinalIgnoreCase)) { - var recycleBinQuery = WindowsRecycleBinService.QueryRecycleBin(); + var recycleBinQuery = StorageTrashBinService.QueryRecycleBin(); if (recycleBinQuery.BinSize is long binSize) { ViewModel.ItemSizeBytes = binSize; diff --git a/src/Files.App/ViewModels/ShellViewModel.cs b/src/Files.App/ViewModels/ShellViewModel.cs index 71c07f5c871b..9d0857a46157 100644 --- a/src/Files.App/ViewModels/ShellViewModel.cs +++ b/src/Files.App/ViewModels/ShellViewModel.cs @@ -56,7 +56,7 @@ public sealed class ShellViewModel : ObservableObject, IDisposable private readonly ISizeProvider folderSizeProvider = Ioc.Default.GetRequiredService(); private readonly IStorageCacheService fileListCache = Ioc.Default.GetRequiredService(); private readonly IWindowsSecurityService WindowsSecurityService = Ioc.Default.GetRequiredService(); - private readonly IWindowsRecycleBinService WindowsRecycleBinService = Ioc.Default.GetRequiredService(); + private readonly IStorageTrashBinService StorageTrashBinService = Ioc.Default.GetRequiredService(); // Only used for Binding and ApplyFilesAndFoldersChangesAsync, don't manipulate on this! public BulkConcurrentObservableCollection FilesAndFolders { get; } @@ -537,9 +537,9 @@ public ShellViewModel(LayoutPreferencesManager folderSettingsViewModel) fileTagsSettingsService.OnTagsUpdated += FileTagsSettingsService_OnSettingUpdated; folderSizeProvider.SizeChanged += FolderSizeProvider_SizeChanged; folderSettings.LayoutModeChangeRequested += LayoutModeChangeRequested; - WindowsRecycleBinService.Watcher.ItemAdded += RecycleBinItemCreatedAsync; - WindowsRecycleBinService.Watcher.ItemDeleted += RecycleBinItemDeletedAsync; - WindowsRecycleBinService.Watcher.RefreshRequested += RecycleBinRefreshRequestedAsync; + StorageTrashBinService.Watcher.ItemAdded += RecycleBinItemCreatedAsync; + StorageTrashBinService.Watcher.ItemDeleted += RecycleBinItemDeletedAsync; + StorageTrashBinService.Watcher.RefreshRequested += RecycleBinRefreshRequestedAsync; } private async void LayoutModeChangeRequested(object? sender, LayoutModeEventArgs e) @@ -2591,9 +2591,9 @@ public void UpdateDateDisplay(bool isFormatChange) public void Dispose() { CancelLoadAndClearFiles(); - WindowsRecycleBinService.Watcher.ItemAdded -= RecycleBinItemCreatedAsync; - WindowsRecycleBinService.Watcher.ItemDeleted -= RecycleBinItemDeletedAsync; - WindowsRecycleBinService.Watcher.RefreshRequested -= RecycleBinRefreshRequestedAsync; + StorageTrashBinService.Watcher.ItemAdded -= RecycleBinItemCreatedAsync; + StorageTrashBinService.Watcher.ItemDeleted -= RecycleBinItemDeletedAsync; + StorageTrashBinService.Watcher.RefreshRequested -= RecycleBinRefreshRequestedAsync; UserSettingsService.OnSettingChangedEvent -= UserSettingsService_OnSettingChangedEvent; fileTagsSettingsService.OnSettingImportedEvent -= FileTagsSettingsService_OnSettingUpdated; fileTagsSettingsService.OnTagsUpdated -= FileTagsSettingsService_OnSettingUpdated; diff --git a/src/Files.App/Views/Layouts/BaseLayoutPage.cs b/src/Files.App/Views/Layouts/BaseLayoutPage.cs index 5fa1002cc52a..d32864b3f8bf 100644 --- a/src/Files.App/Views/Layouts/BaseLayoutPage.cs +++ b/src/Files.App/Views/Layouts/BaseLayoutPage.cs @@ -41,7 +41,7 @@ public abstract class BaseLayoutPage : Page, IBaseLayoutPage, INotifyPropertyCha protected ICommandManager Commands { get; } = Ioc.Default.GetRequiredService(); public InfoPaneViewModel InfoPaneViewModel { get; } = Ioc.Default.GetRequiredService(); protected readonly IWindowContext WindowContext = Ioc.Default.GetRequiredService(); - protected readonly IWindowsRecycleBinService WindowsRecycleBinService = Ioc.Default.GetRequiredService(); + protected readonly IStorageTrashBinService StorageTrashBinService = Ioc.Default.GetRequiredService(); // ViewModels @@ -1292,7 +1292,7 @@ protected void InitializeDrag(UIElement container, ListedItem item) return; UninitializeDrag(container); - if ((item.PrimaryItemAttribute == StorageItemTypes.Folder && !WindowsRecycleBinService.IsRecycled(item.ItemPath)) + if ((item.PrimaryItemAttribute == StorageItemTypes.Folder && !StorageTrashBinService.IsUnderTrashBin(item.ItemPath)) || item.IsExecutable || item.IsScriptFile) { diff --git a/src/Files.Core.Storage/Contracts/ITrashWatcher.cs b/src/Files.Core.Storage/Contracts/ITrashWatcher.cs index 6ec106c24f61..ad3d24b5e55c 100644 --- a/src/Files.Core.Storage/Contracts/ITrashWatcher.cs +++ b/src/Files.Core.Storage/Contracts/ITrashWatcher.cs @@ -15,11 +15,6 @@ public interface ITrashWatcher : IWatcher /// event EventHandler? ItemDeleted; - /// - /// Gets invoked when an item changing is detected by the watcher - /// - event EventHandler? ItemChanged; - /// /// Gets invoked when an item renaming is detected by the watcher /// From 867975ca06ea5990e52c92fdb71812779939c428 Mon Sep 17 00:00:00 2001 From: 0x5BFA Date: Tue, 27 Aug 2024 07:14:11 +0900 Subject: [PATCH 3/4] Revert --- src/Files.Core.Storage/Contracts/ITrashWatcher.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Files.Core.Storage/Contracts/ITrashWatcher.cs b/src/Files.Core.Storage/Contracts/ITrashWatcher.cs index ad3d24b5e55c..6ec106c24f61 100644 --- a/src/Files.Core.Storage/Contracts/ITrashWatcher.cs +++ b/src/Files.Core.Storage/Contracts/ITrashWatcher.cs @@ -15,6 +15,11 @@ public interface ITrashWatcher : IWatcher /// event EventHandler? ItemDeleted; + /// + /// Gets invoked when an item changing is detected by the watcher + /// + event EventHandler? ItemChanged; + /// /// Gets invoked when an item renaming is detected by the watcher /// From 6f43e0a0ed85fc332b364c484977695fce3b65c1 Mon Sep 17 00:00:00 2001 From: 0x5BFA Date: Tue, 27 Aug 2024 07:16:00 +0900 Subject: [PATCH 4/4] Revert 2 --- src/Files.App.Storage/Watchers/RecycleBinWatcher.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Files.App.Storage/Watchers/RecycleBinWatcher.cs b/src/Files.App.Storage/Watchers/RecycleBinWatcher.cs index 7ce564557268..8dc8bfda9619 100644 --- a/src/Files.App.Storage/Watchers/RecycleBinWatcher.cs +++ b/src/Files.App.Storage/Watchers/RecycleBinWatcher.cs @@ -16,6 +16,9 @@ public class RecycleBinWatcher : ITrashWatcher /// public event EventHandler? ItemDeleted; + /// + public event EventHandler? ItemChanged; + /// public event EventHandler? ItemRenamed;