Skip to content

Commit 6dcb360

Browse files
authored
Recycle bin improvements (#1137)
1 parent 9a0ceae commit 6dcb360

25 files changed

+558
-128
lines changed

Common/ShellFileItem.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,16 @@ public class ShellFileItem
1010
public string FilePath;
1111
public DateTime RecycleDate;
1212
public string FileSize;
13-
public long FileSizeBytes;
13+
public ulong FileSizeBytes;
1414
public string FileType;
1515

1616
public ShellFileItem()
1717
{
1818
}
1919

20-
public ShellFileItem(bool isFolder, string recyclePath, string fileName, string filePath, DateTime recycleDate, string fileSize, long fileSizeBytes, string fileType)
20+
public ShellFileItem(
21+
bool isFolder, string recyclePath, string fileName, string filePath,
22+
DateTime recycleDate, string fileSize, ulong fileSizeBytes, string fileType)
2123
{
2224
this.IsFolder = isFolder;
2325
this.RecyclePath = recyclePath;

Files.Launcher/Program.cs

Lines changed: 76 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
using Files.Common;
22
using Newtonsoft.Json;
3-
using NLog;
43
using System;
54
using System.Collections.Generic;
65
using System.ComponentModel;
@@ -15,6 +14,7 @@
1514
using Windows.ApplicationModel.AppService;
1615
using Windows.Foundation.Collections;
1716
using Windows.Storage;
17+
using static Vanara.PInvoke.Shell32;
1818

1919
namespace FilesFullTrust
2020
{
@@ -25,7 +25,7 @@ internal class Program
2525
[STAThread]
2626
private static void Main(string[] args)
2727
{
28-
Windows.Storage.StorageFolder storageFolder = Windows.Storage.ApplicationData.Current.LocalFolder;
28+
StorageFolder storageFolder = ApplicationData.Current.LocalFolder;
2929
NLog.LogManager.Configuration = new NLog.Config.XmlLoggingConfiguration(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "NLog.config"));
3030
NLog.LogManager.Configuration.Variables["LogPath"] = storageFolder.Path;
3131

@@ -45,14 +45,31 @@ private static void Main(string[] args)
4545
try
4646
{
4747
// Create shell COM object and get recycle bin folder
48-
recycler = new ShellFolder(Vanara.PInvoke.Shell32.KNOWNFOLDERID.FOLDERID_RecycleBinFolder);
49-
Windows.Storage.ApplicationData.Current.LocalSettings.Values["RecycleBin_Title"] = recycler.Name;
50-
51-
// Create shell watcher to monitor recycle bin folder
52-
watcher = new ShellItemChangeWatcher(recycler, false);
53-
watcher.NotifyFilter = ChangeFilters.AllDiskEvents;
54-
watcher.Changed += Watcher_Changed;
55-
//watcher.EnableRaisingEvents = true; // TODO: uncomment this when updated library is released
48+
recycler = new ShellFolder(KNOWNFOLDERID.FOLDERID_RecycleBinFolder);
49+
ApplicationData.Current.LocalSettings.Values["RecycleBin_Title"] = recycler.Name;
50+
51+
// Create filesystem watcher to monitor recycle bin folder(s)
52+
// SHChangeNotifyRegister only works if recycle bin is open in explorer :(
53+
watchers = new List<FileSystemWatcher>();
54+
var sid = System.Security.Principal.WindowsIdentity.GetCurrent().User.ToString();
55+
foreach (var drive in DriveInfo.GetDrives())
56+
{
57+
var recycle_path = Path.Combine(drive.Name, "$Recycle.Bin", sid);
58+
if (!Directory.Exists(recycle_path))
59+
{
60+
continue;
61+
}
62+
var watcher = new FileSystemWatcher();
63+
watcher.Path = recycle_path;
64+
watcher.Filter = "*.*";
65+
watcher.NotifyFilter = NotifyFilters.LastWrite
66+
| NotifyFilters.FileName
67+
| NotifyFilters.DirectoryName;
68+
watcher.Created += Watcher_Changed;
69+
watcher.Deleted += Watcher_Changed;
70+
watcher.EnableRaisingEvents = true;
71+
watchers.Add(watcher);
72+
}
5673

5774
// Connect to app service and wait until the connection gets closed
5875
appServiceExit = new AutoResetEvent(false);
@@ -62,7 +79,10 @@ private static void Main(string[] args)
6279
finally
6380
{
6481
connection?.Dispose();
65-
watcher?.Dispose();
82+
foreach (var watcher in watchers)
83+
{
84+
watcher.Dispose();
85+
}
6686
recycler?.Dispose();
6787
appServiceExit?.Dispose();
6888
mutex?.ReleaseMutex();
@@ -75,20 +95,24 @@ private static void UnhandledExceptionTrapper(object sender, UnhandledExceptionE
7595
Logger.Error(exception, exception.Message);
7696
}
7797

78-
private static async void Watcher_Changed(object sender, ShellItemChangeWatcher.ShellItemChangeEventArgs e)
98+
private static async void Watcher_Changed(object sender, FileSystemEventArgs e)
7999
{
80-
Console.WriteLine($"File: {e.ChangedItems.FirstOrDefault()?.FileSystemPath} {e.ChangeType}");
100+
Debug.WriteLine("Reycle bin event: {0}, {1}", e.ChangeType, e.FullPath);
81101
if (connection != null)
82102
{
83103
// Send message to UWP app to refresh items
84-
await connection.SendMessageAsync(new ValueSet() { { "FileSystem", @"Shell:RecycleBinFolder" }, { "Path", e.ChangedItems.FirstOrDefault()?.FileSystemPath }, { "Type", e.ChangeType.ToString() } });
104+
await connection.SendMessageAsync(new ValueSet() {
105+
{ "FileSystem", @"Shell:RecycleBinFolder" },
106+
{ "Path", e.FullPath },
107+
{ "Type", e.ChangeType.ToString() }
108+
});
85109
}
86110
}
87111

88112
private static AppServiceConnection connection;
89113
private static AutoResetEvent appServiceExit;
90114
private static ShellFolder recycler;
91-
private static ShellItemChangeWatcher watcher;
115+
private static IList<FileSystemWatcher> watchers;
92116

93117
private static async void InitializeAppServiceConnection()
94118
{
@@ -126,7 +150,7 @@ private static async void Connection_RequestReceived(AppServiceConnection sender
126150
// Instead a single instance of the process is running
127151
// Requests from UWP app are sent via AppService connection
128152
var arguments = (string)args.Request.Message["Arguments"];
129-
var localSettings = Windows.Storage.ApplicationData.Current.LocalSettings;
153+
var localSettings = ApplicationData.Current.LocalSettings;
130154
Logger.Info($"Argument: {arguments}");
131155

132156
await parseArguments(args, messageDeferral, arguments, localSettings);
@@ -180,8 +204,8 @@ private static async Task parseArguments(AppServiceRequestReceivedEventArgs args
180204
#if DEBUG
181205
// In debug mode this kills this process too??
182206
#else
183-
var pid = (int)args.Request.Message["pid"];
184-
Process.GetProcessById(pid).Kill();
207+
var pid = (int)args.Request.Message["pid"];
208+
Process.GetProcessById(pid).Kill();
185209
#endif
186210

187211
Process process = new Process();
@@ -201,7 +225,7 @@ private static async Task parseArguments(AppServiceRequestReceivedEventArgs args
201225
case "ParseAguments":
202226
var responseArray = new ValueSet();
203227
var resultArgument = Win32API.CommandLineToArgs((string)args.Request.Message["Command"]);
204-
responseArray.Add("ParsedArguments", Newtonsoft.Json.JsonConvert.SerializeObject(resultArgument));
228+
responseArray.Add("ParsedArguments", JsonConvert.SerializeObject(resultArgument));
205229
await args.Request.SendResponseAsync(responseArray);
206230
break;
207231

@@ -236,23 +260,27 @@ private static async Task parseRecycleBinAction(AppServiceRequestReceivedEventAr
236260
{
237261
case "Empty":
238262
// Shell function to empty recyclebin
239-
Vanara.PInvoke.Shell32.SHEmptyRecycleBin(IntPtr.Zero, null, Vanara.PInvoke.Shell32.SHERB.SHERB_NOCONFIRMATION | Vanara.PInvoke.Shell32.SHERB.SHERB_NOPROGRESSUI);
263+
SHEmptyRecycleBin(IntPtr.Zero, null, SHERB.SHERB_NOCONFIRMATION | SHERB.SHERB_NOPROGRESSUI);
240264
break;
241265

242266
case "Query":
243267
var responseQuery = new ValueSet();
244-
Win32API.SHQUERYRBINFO queryBinInfo = new Win32API.SHQUERYRBINFO();
245-
queryBinInfo.cbSize = (uint)Marshal.SizeOf(typeof(Win32API.SHQUERYRBINFO));
246-
var res = Win32API.SHQueryRecycleBin("", ref queryBinInfo);
247-
// TODO: use this when updated library is released
248-
//Vanara.PInvoke.Shell32.SHQUERYRBINFO queryBinInfo = new Vanara.PInvoke.Shell32.SHQUERYRBINFO();
249-
//Vanara.PInvoke.Shell32.SHQueryRecycleBin(null, ref queryBinInfo);
268+
SHQUERYRBINFO queryBinInfo = new SHQUERYRBINFO();
269+
queryBinInfo.cbSize = (uint)Marshal.SizeOf(queryBinInfo);
270+
var res = SHQueryRecycleBin(null, ref queryBinInfo);
250271
if (res == Vanara.PInvoke.HRESULT.S_OK)
251272
{
252273
var numItems = queryBinInfo.i64NumItems;
253274
var binSize = queryBinInfo.i64Size;
254275
responseQuery.Add("NumItems", numItems);
255276
responseQuery.Add("BinSize", binSize);
277+
responseQuery.Add("FileOwner", (string)recycler.Properties[Vanara.PInvoke.Ole32.PROPERTYKEY.System.FileOwner]);
278+
if (watchers.Any())
279+
{
280+
var info = new DirectoryInfo(watchers.First().Path);
281+
responseQuery.Add("DateAccessed", info.LastAccessTime.ToBinary());
282+
responseQuery.Add("DateCreated", info.CreationTime.ToBinary());
283+
}
256284
await args.Request.SendResponseAsync(responseQuery);
257285
}
258286
break;
@@ -265,20 +293,26 @@ private static async Task parseRecycleBinAction(AppServiceRequestReceivedEventAr
265293
{
266294
try
267295
{
268-
folderItem.Properties.ReadOnly = true;
269-
folderItem.Properties.NoInheritedProperties = false;
270296
string recyclePath = folderItem.FileSystemPath; // True path on disk
271297
string fileName = Path.GetFileName(folderItem.Name); // Original file name
272298
string filePath = folderItem.Name; // Original file path + name
273-
var dt = (System.Runtime.InteropServices.ComTypes.FILETIME)folderItem.Properties[Vanara.PInvoke.Ole32.PROPERTYKEY.System.DateCreated];
274-
var recycleDate = dt.ToDateTime().ToLocalTime(); // This is LocalTime
275-
string fileSize = folderItem.Properties.GetPropertyString(Vanara.PInvoke.Ole32.PROPERTYKEY.System.Size);
276-
long fileSizeBytes = (long)folderItem.Properties[Vanara.PInvoke.Ole32.PROPERTYKEY.System.Size];
277-
string fileType = (string)folderItem.Properties[Vanara.PInvoke.Ole32.PROPERTYKEY.System.ItemTypeText];
278299
bool isFolder = folderItem.IsFolder && Path.GetExtension(folderItem.Name) != ".zip";
279-
folderContentsList.Add(new ShellFileItem(isFolder, recyclePath, fileName, filePath, recycleDate, fileSize, fileSizeBytes, fileType));
300+
if (folderItem.Properties == null)
301+
{
302+
folderContentsList.Add(new ShellFileItem(isFolder, recyclePath, fileName, filePath, DateTime.Now, null, 0, null));
303+
continue;
304+
}
305+
folderItem.Properties.TryGetValue<System.Runtime.InteropServices.ComTypes.FILETIME?>(
306+
Vanara.PInvoke.Ole32.PROPERTYKEY.System.DateCreated, out var fileTime);
307+
var recycleDate = fileTime?.ToDateTime().ToLocalTime() ?? DateTime.Now; // This is LocalTime
308+
string fileSize = folderItem.Properties.TryGetValue<ulong?>(
309+
Vanara.PInvoke.Ole32.PROPERTYKEY.System.Size, out var fileSizeBytes) ?
310+
folderItem.Properties.GetPropertyString(Vanara.PInvoke.Ole32.PROPERTYKEY.System.Size) : null;
311+
folderItem.Properties.TryGetValue<string>(
312+
Vanara.PInvoke.Ole32.PROPERTYKEY.System.ItemTypeText, out var fileType);
313+
folderContentsList.Add(new ShellFileItem(isFolder, recyclePath, fileName, filePath, recycleDate, fileSize, fileSizeBytes ?? 0, fileType));
280314
}
281-
catch (System.IO.FileNotFoundException)
315+
catch (FileNotFoundException)
282316
{
283317
// Happens if files are being deleted
284318
}
@@ -287,7 +321,7 @@ private static async Task parseRecycleBinAction(AppServiceRequestReceivedEventAr
287321
folderItem.Dispose();
288322
}
289323
}
290-
responseEnum.Add("Enumerate", Newtonsoft.Json.JsonConvert.SerializeObject(folderContentsList));
324+
responseEnum.Add("Enumerate", JsonConvert.SerializeObject(folderContentsList));
291325
await args.Request.SendResponseAsync(responseEnum);
292326
break;
293327

@@ -377,13 +411,13 @@ private static void HandleApplicationLaunch(string application, AppServiceReques
377411
if (!group.Any()) continue;
378412
var files = group.Select(x => new ShellItem(x));
379413
using var sf = files.First().Parent;
380-
Vanara.PInvoke.Shell32.IContextMenu menu = null;
414+
IContextMenu menu = null;
381415
try
382416
{
383-
menu = sf.GetChildrenUIObjects<Vanara.PInvoke.Shell32.IContextMenu>(null, files.ToArray());
384-
menu.QueryContextMenu(Vanara.PInvoke.HMENU.NULL, 0, 0, 0, Vanara.PInvoke.Shell32.CMF.CMF_DEFAULTONLY);
385-
var pici = new Vanara.PInvoke.Shell32.CMINVOKECOMMANDINFOEX();
386-
pici.lpVerb = Vanara.PInvoke.Shell32.CMDSTR_OPEN;
417+
menu = sf.GetChildrenUIObjects<IContextMenu>(null, files.ToArray());
418+
menu.QueryContextMenu(Vanara.PInvoke.HMENU.NULL, 0, 0, 0, CMF.CMF_DEFAULTONLY);
419+
var pici = new CMINVOKECOMMANDINFOEX();
420+
pici.lpVerb = CMDSTR_OPEN;
387421
pici.nShow = Vanara.PInvoke.ShowWindowCommand.SW_SHOW;
388422
pici.cbSize = (uint)Marshal.SizeOf(pici);
389423
menu.InvokeCommand(pici);
@@ -412,7 +446,7 @@ private static void HandleApplicationLaunch(string application, AppServiceReques
412446

413447
private static bool HandleCommandLineArgs()
414448
{
415-
var localSettings = Windows.Storage.ApplicationData.Current.LocalSettings;
449+
var localSettings = ApplicationData.Current.LocalSettings;
416450
var arguments = (string)localSettings.Values["Arguments"];
417451
if (!string.IsNullOrWhiteSpace(arguments))
418452
{

Files.Launcher/Win32API.cs

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,35 +6,43 @@
66
using System.Linq;
77
using System.Runtime.InteropServices;
88
using System.Text;
9+
using System.Threading;
10+
using System.Threading.Tasks;
911
using Vanara.Windows.Shell;
1012

1113
namespace FilesFullTrust
1214
{
1315
internal class Win32API
1416
{
15-
// TODO: remove this when updated library is released
16-
[DllImport("shell32.dll")]
17-
public static extern Vanara.PInvoke.HRESULT SHQueryRecycleBin(string pszRootPath, ref SHQUERYRBINFO pSHQueryRBInfo);
18-
19-
// TODO: remove this when updated library is released
20-
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto, Pack = 4)]
21-
public struct SHQUERYRBINFO
17+
public static Task<T> StartSTATask<T>(Func<T> func)
2218
{
23-
public uint cbSize;
24-
public long i64Size;
25-
public long i64NumItems;
19+
var tcs = new TaskCompletionSource<T>();
20+
Thread thread = new Thread(() =>
21+
{
22+
try
23+
{
24+
tcs.SetResult(func());
25+
}
26+
catch (Exception e)
27+
{
28+
tcs.SetException(e);
29+
}
30+
});
31+
thread.SetApartmentState(ApartmentState.STA);
32+
thread.Start();
33+
return tcs.Task;
2634
}
2735

2836
[DllImport("shell32.dll", CharSet = CharSet.Ansi)]
29-
public static extern IntPtr FindExecutable(string lpFile, string lpDirectory, [Out] System.Text.StringBuilder lpResult);
37+
public static extern IntPtr FindExecutable(string lpFile, string lpDirectory, [Out] StringBuilder lpResult);
3038

31-
public static async System.Threading.Tasks.Task<string> GetFileAssociation(string filename)
39+
public static async Task<string> GetFileAssociation(string filename)
3240
{
3341
// Find UWP apps
3442
var uwp_apps = await Windows.System.Launcher.FindFileHandlersAsync(System.IO.Path.GetExtension(filename));
3543
if (uwp_apps.Any()) return uwp_apps.First().PackageFamilyName;
3644
// Find desktop apps
37-
var lpResult = new System.Text.StringBuilder();
45+
var lpResult = new StringBuilder();
3846
var hResult = FindExecutable(filename, null, lpResult);
3947
if (hResult.ToInt64() > 32) return lpResult.ToString();
4048
return null;

Files/BaseLayout.cs

Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -320,23 +320,7 @@ private async void MenuLayoutItem_Click(object sender, RoutedEventArgs e)
320320
}
321321
}
322322

323-
public void RightClickContextMenu_Opening(object sender, object e)
324-
{
325-
if (App.CurrentInstance.FilesystemViewModel.WorkingDirectory.StartsWith(AppSettings.RecycleBinPath))
326-
{
327-
(this.FindName("EmptyRecycleBin") as MenuFlyoutItemBase).Visibility = Visibility.Visible;
328-
(this.FindName("OpenTerminal") as MenuFlyoutItemBase).IsEnabled = false;
329-
UnloadMenuFlyoutItemByName("NewEmptySpace");
330-
}
331-
else
332-
{
333-
UnloadMenuFlyoutItemByName("EmptyRecycleBin");
334-
(this.FindName("OpenTerminal") as MenuFlyoutItemBase).IsEnabled = true;
335-
(this.FindName("NewEmptySpace") as MenuFlyoutItemBase).Visibility = Visibility.Visible;
336-
}
337-
}
338-
339-
public async void RightClickItemContextMenu_Opening(object sender, object e)
323+
public void RightClickItemContextMenu_Opening(object sender, object e)
340324
{
341325

342326
SetShellContextmenu();

0 commit comments

Comments
 (0)