Skip to content

Code Quality: Introduced IStorageTrashBinService #15773

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Sep 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion src/Files.App.CsWin32/NativeMethods.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
{
"$schema": "https://aka.ms/CsWin32.schema.json",
"allowMarshaling": false,
"public": true
"public": true,
"comInterop": {
"preserveSigMethods": [
"IEnumShellItems.Next"
]
}
}
12 changes: 12 additions & 0 deletions src/Files.App.CsWin32/NativeMethods.txt
Original file line number Diff line number Diff line change
Expand Up @@ -120,4 +120,16 @@ WindowsCreateString
WindowsDeleteString
IPreviewHandler
AssocQueryString
GetModuleHandle
SHEmptyRecycleBin
SHFileOperation
SHGetFolderPath
SHGFP_TYPE
SHGetKnownFolderItem
SHQUERYRBINFO
SHQueryRecycleBin
FileOperation
IFileOperation
IShellItem2
PSGetPropertyKeyFromName
ShellExecuteEx
34 changes: 34 additions & 0 deletions src/Files.App.CsWin32/Windows.Win32.Extras.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
10 changes: 0 additions & 10 deletions src/Files.App.CsWin32/Windows.Win32.MONITORENUMPROC.cs

This file was deleted.

10 changes: 0 additions & 10 deletions src/Files.App.CsWin32/Windows.Win32.WNDPROC.cs

This file was deleted.

110 changes: 110 additions & 0 deletions src/Files.App.Storage/Watchers/RecycleBinWatcher.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// 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
{
public class RecycleBinWatcher : ITrashWatcher
{
private readonly List<SystemIO.FileSystemWatcher> _watchers = [];

/// <inheritdoc/>
public event EventHandler<SystemIO.FileSystemEventArgs>? ItemAdded;

/// <inheritdoc/>
public event EventHandler<SystemIO.FileSystemEventArgs>? ItemDeleted;

/// <inheritdoc/>
public event EventHandler<SystemIO.FileSystemEventArgs>? ItemChanged;

/// <inheritdoc/>
public event EventHandler<SystemIO.FileSystemEventArgs>? ItemRenamed;

/// <inheritdoc/>
public event EventHandler<SystemIO.FileSystemEventArgs>? RefreshRequested;

/// <summary>
/// Initializes an instance of <see cref="RecycleBinWatcher"/> class.
/// </summary>
public RecycleBinWatcher()
{
StartWatcher();
}

/// <inheritdoc/>
public void StartWatcher()
{
// 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;

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;

// NOTE: Suppressed NullReferenceException caused by EnableRaisingEvents in #15808
SafetyExtensions.IgnoreExceptions(() =>
{
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);
});
}
}

/// <inheritdoc/>
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;
}
}

/// <inheritdoc/>
public void Dispose()
{
StopWatcher();
}
}
}
34 changes: 32 additions & 2 deletions src/Files.App/Actions/FileSystem/EmptyRecycleBinAction.cs
Original file line number Diff line number Diff line change
@@ -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 IStorageTrashBinService StorageTrashBinService = Ioc.Default.GetRequiredService<IStorageTrashBinService>();
private readonly StatusCenterViewModel StatusCenterViewModel = Ioc.Default.GetRequiredService<StatusCenterViewModel>();
private readonly IUserSettingsService UserSettingsService = Ioc.Default.GetRequiredService<IUserSettingsService>();
private readonly IContentPageContext context;

public string Label
Expand All @@ -19,7 +25,7 @@ public RichGlyph Glyph
public override bool IsExecutable =>
UIHelpers.CanShowDialog &&
((context.PageType == ContentPageTypes.RecycleBin && context.HasItem) ||
RecycleBinHelpers.RecycleBinHasItems());
StorageTrashBinService.HasItems());

public EmptyRecycleBinAction()
{
Expand All @@ -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(StorageTrashBinService.EmptyTrashBin);

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)
Expand Down
40 changes: 38 additions & 2 deletions src/Files.App/Actions/FileSystem/RestoreAllRecycleBinAction.cs
Original file line number Diff line number Diff line change
@@ -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 IStorageTrashBinService StorageTrashBinService = Ioc.Default.GetRequiredService<IStorageTrashBinService>();

public string Label
=> "RestoreAllItems".GetLocalizedResource();

Expand All @@ -16,11 +21,42 @@ public RichGlyph Glyph

public override bool IsExecutable =>
UIHelpers.CanShowDialog &&
RecycleBinHelpers.RecycleBinHasItems();
StorageTrashBinService.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(StorageTrashBinService.RestoreAllTrashes);

// 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();
}
}
}
}
32 changes: 30 additions & 2 deletions src/Files.App/Actions/FileSystem/RestoreRecycleBinAction.cs
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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)
Expand Down
Loading