Skip to content

Commit 9897dba

Browse files
authored
Added the option to eject removable devices (#2328)
1 parent 45314b4 commit 9897dba

31 files changed

+644
-12
lines changed

Files.Launcher/Program.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,7 @@ private static Func<string, bool> FilterMenuItems(bool showOpenMenu)
374374
"runas", "runasuser", "pintohome", "PinToStartScreen",
375375
"cut", "copy", "paste", "delete", "properties", "link",
376376
"Windows.ModernShare", "Windows.Share", "setdesktopwallpaper",
377+
"eject",
377378
Win32API.ExtractStringFromDLL("shell32.dll", 30312), // SendTo menu
378379
Win32API.ExtractStringFromDLL("shell32.dll", 34593), // Add to collection
379380
};

Files/App.xaml.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,17 +81,17 @@ private void OnLeavingBackground(object sender, LeavingBackgroundEventArgs e)
8181
AppSettings?.DrivesManager?.ResumeDeviceWatcher();
8282
}
8383

84-
public static INavigationControlItem rightClickedItem;
84+
public static INavigationControlItem RightClickedItem;
8585

8686
public static void UnpinItem_Click(object sender, RoutedEventArgs e)
8787
{
88-
if (rightClickedItem.Path.Equals(AppSettings.RecycleBinPath, StringComparison.OrdinalIgnoreCase))
88+
if (RightClickedItem.Path.Equals(AppSettings.RecycleBinPath, StringComparison.OrdinalIgnoreCase))
8989
{
9090
AppSettings.PinRecycleBinToSideBar = false;
9191
}
9292
else
9393
{
94-
SidebarPinnedController.Model.RemoveItem(rightClickedItem.Path.ToString());
94+
SidebarPinnedController.Model.RemoveItem(RightClickedItem.Path.ToString());
9595
}
9696
}
9797

136 Bytes
Binary file not shown.

Files/Files.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,7 @@
204204
<Compile Include="Helpers\NativeDirectoryChangesHelper.cs" />
205205
<Compile Include="Helpers\NativeFileOperationsHelper.cs" />
206206
<Compile Include="Helpers\NativeFindStorageItemHelper.cs" />
207+
<Compile Include="Helpers\NativeIoDeviceControlHelper.cs" />
207208
<Compile Include="Helpers\NativeWinApiHelper.cs" />
208209
<Compile Include="Helpers\NaturalStringComparer.cs" />
209210
<Compile Include="Helpers\PackageHelper.cs" />
@@ -212,6 +213,7 @@
212213
<Compile Include="Helpers\BulkObservableCollection.cs" />
213214
<Compile Include="Helpers\ThemeHelper.cs" />
214215
<Compile Include="Helpers\Win32FindDataExtensions.cs" />
216+
<Compile Include="Interacts\RemovableDevice.cs" />
215217
<Compile Include="Program.cs" />
216218
<Compile Include="INavigationToolbar.cs" />
217219
<Compile Include="UserControls\FileIcon.xaml.cs">
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
using System;
2+
using System.Runtime.InteropServices;
3+
4+
namespace Files.Helpers
5+
{
6+
public class NativeIoDeviceControlHelper
7+
{
8+
[DllImport("api-ms-win-core-file-fromapp-l1-1-0.dll", SetLastError = true, CharSet = CharSet.Auto)]
9+
public static extern IntPtr CreateFileFromAppW(
10+
string lpFileName,
11+
uint dwDesiredAccess,
12+
uint dwShareMode,
13+
IntPtr SecurityAttributes,
14+
uint dwCreationDisposition,
15+
uint dwFlagsAndAttributes,
16+
IntPtr hTemplateFile
17+
);
18+
19+
[DllImport("api-ms-win-core-io-l1-1-0.dll", ExactSpelling = true, SetLastError = true, CharSet = CharSet.Auto)]
20+
public static extern bool DeviceIoControl(
21+
IntPtr hDevice,
22+
uint dwIoControlCode,
23+
IntPtr lpInBuffer,
24+
uint nInBufferSize,
25+
IntPtr lpOutBuffer,
26+
uint nOutBufferSize,
27+
out uint lpBytesReturned,
28+
IntPtr lpOverlapped
29+
);
30+
31+
[DllImport("api-ms-win-core-io-l1-1-0.dll", ExactSpelling = true, SetLastError = true, CharSet = CharSet.Auto)]
32+
public static extern bool DeviceIoControl(
33+
IntPtr hDevice,
34+
uint dwIoControlCode,
35+
byte[] lpInBuffer,
36+
uint nInBufferSize,
37+
IntPtr lpOutBuffer,
38+
uint nOutBufferSize,
39+
out uint lpBytesReturned,
40+
IntPtr lpOverlapped
41+
);
42+
43+
[DllImport("api-ms-win-core-io-l1-1-0.dll", SetLastError = true)]
44+
[return: MarshalAs(UnmanagedType.Bool)]
45+
public static extern bool CloseHandle(IntPtr hObject);
46+
47+
public const int INVALID_HANDLE_VALUE = -1;
48+
public const uint GENERIC_READ = 0x80000000;
49+
public const uint GENERIC_WRITE = 0x40000000;
50+
public const int FILE_SHARE_READ = 0x00000001;
51+
public const int FILE_SHARE_WRITE = 0x00000002;
52+
public const int OPEN_EXISTING = 3;
53+
public const int FSCTL_LOCK_VOLUME = 0x00090018;
54+
public const int FSCTL_DISMOUNT_VOLUME = 0x00090020;
55+
public const int IOCTL_STORAGE_EJECT_MEDIA = 0x2D4808;
56+
public const int IOCTL_STORAGE_MEDIA_REMOVAL = 0x002D4804;
57+
}
58+
}

Files/Interacts/Interaction.cs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using Files.Views.Pages;
99
using Microsoft.Toolkit.Mvvm.Input;
1010
using Microsoft.Toolkit.Uwp.Extensions;
11+
using Microsoft.Toolkit.Uwp.Notifications;
1112
using Newtonsoft.Json;
1213
using NLog;
1314
using System;
@@ -33,6 +34,7 @@
3334
using Windows.System;
3435
using Windows.System.UserProfile;
3536
using Windows.UI.Core;
37+
using Windows.UI.Notifications;
3638
using Windows.UI.Popups;
3739
using Windows.UI.ViewManagement;
3840
using Windows.UI.Xaml;
@@ -1380,5 +1382,55 @@ public async Task<string> GetHashForFileAsync(ListedItem fileItem, string nameOf
13801382
}
13811383
return CryptographicBuffer.EncodeToHexString(hash.GetValueAndReset()).ToLower();
13821384
}
1385+
1386+
public static async Task EjectDeviceAsync(string path)
1387+
{
1388+
var removableDevice = new RemovableDevice(path);
1389+
bool result = await removableDevice.EjectAsync();
1390+
if (result)
1391+
{
1392+
Debug.WriteLine("Device successfully ejected");
1393+
1394+
var toastContent = new ToastContent()
1395+
{
1396+
Visual = new ToastVisual()
1397+
{
1398+
BindingGeneric = new ToastBindingGeneric()
1399+
{
1400+
Children =
1401+
{
1402+
new AdaptiveText()
1403+
{
1404+
Text = "EjectNotificationHeader".GetLocalized()
1405+
},
1406+
new AdaptiveText()
1407+
{
1408+
Text = "EjectNotificationBody".GetLocalized()
1409+
}
1410+
},
1411+
Attribution = new ToastGenericAttributionText()
1412+
{
1413+
Text = "SettingsAboutAppName".GetLocalized()
1414+
}
1415+
}
1416+
},
1417+
ActivationType = ToastActivationType.Protocol
1418+
};
1419+
1420+
// Create the toast notification
1421+
var toastNotif = new ToastNotification(toastContent.GetXml());
1422+
1423+
// And send the notification
1424+
ToastNotificationManager.CreateToastNotifier().Show(toastNotif);
1425+
}
1426+
else
1427+
{
1428+
Debug.WriteLine("Can't eject device");
1429+
1430+
await DialogDisplayHelper.ShowDialogAsync(
1431+
"EjectNotificationErrorDialogHeader".GetLocalized(),
1432+
"EjectNotificationErrorDialogBody".GetLocalized());
1433+
}
1434+
}
13831435
}
13841436
}

Files/Interacts/RemovableDevice.cs

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
using System;
2+
using System.Diagnostics;
3+
using System.Threading.Tasks;
4+
using static Files.Helpers.NativeIoDeviceControlHelper;
5+
6+
namespace Files.Interacts
7+
{
8+
public class RemovableDevice
9+
{
10+
private IntPtr handle;
11+
private char driveLetter;
12+
13+
public RemovableDevice(string letter)
14+
{
15+
driveLetter = letter[0];
16+
string filename = @"\\.\" + driveLetter + ":";
17+
handle = CreateFileFromAppW(filename,
18+
GENERIC_READ | GENERIC_WRITE,
19+
FILE_SHARE_READ | FILE_SHARE_WRITE,
20+
IntPtr.Zero, OPEN_EXISTING, 0, IntPtr.Zero);
21+
}
22+
23+
public async Task<bool> EjectAsync()
24+
{
25+
bool result = false;
26+
27+
if (handle.ToInt32() == INVALID_HANDLE_VALUE)
28+
{
29+
Debug.WriteLine("Unable to open drive " + driveLetter);
30+
return false;
31+
}
32+
33+
if (await LockVolumeAsync() && DismountVolume())
34+
{
35+
PreventRemovalOfVolume(false);
36+
result = AutoEjectVolume();
37+
}
38+
CloseVolume();
39+
return result;
40+
}
41+
42+
private async Task<bool> LockVolumeAsync()
43+
{
44+
bool result = false;
45+
46+
for (int i = 0; i < 5; i++)
47+
{
48+
if (DeviceIoControl(handle, FSCTL_LOCK_VOLUME, IntPtr.Zero, 0, IntPtr.Zero, 0, out _, IntPtr.Zero))
49+
{
50+
Debug.WriteLine("Lock successful!");
51+
result = true;
52+
break;
53+
}
54+
else
55+
{
56+
Debug.WriteLine($"Can't lock device, attempt {i + 1}, trying again... ");
57+
}
58+
await Task.Delay(500);
59+
}
60+
61+
return result;
62+
}
63+
64+
private bool DismountVolume()
65+
{
66+
return DeviceIoControl(handle, FSCTL_DISMOUNT_VOLUME, IntPtr.Zero, 0, IntPtr.Zero, 0, out _, IntPtr.Zero);
67+
}
68+
69+
private bool PreventRemovalOfVolume(bool prevent)
70+
{
71+
byte[] buf = new byte[1];
72+
buf[0] = prevent ? (byte)1 : (byte)0;
73+
return DeviceIoControl(handle, IOCTL_STORAGE_MEDIA_REMOVAL, buf, 1, IntPtr.Zero, 0, out _, IntPtr.Zero);
74+
}
75+
76+
private bool AutoEjectVolume()
77+
{
78+
return DeviceIoControl(handle, IOCTL_STORAGE_EJECT_MEDIA, IntPtr.Zero, 0, IntPtr.Zero, 0, out _, IntPtr.Zero);
79+
}
80+
81+
private bool CloseVolume()
82+
{
83+
return CloseHandle(handle);
84+
}
85+
}
86+
}

Files/MultilingualResources/Files.de-DE.xlf

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1441,6 +1441,26 @@
14411441
<source>Show hidden files and folders</source>
14421442
<target state="new">Show hidden files and folders</target>
14431443
</trans-unit>
1444+
<trans-unit id="EjectNotificationHeader" translate="yes" xml:space="preserve">
1445+
<source>Safe to remove hardware</source>
1446+
<target state="new">Safe to remove hardware</target>
1447+
</trans-unit>
1448+
<trans-unit id="EjectNotificationBody" translate="yes" xml:space="preserve">
1449+
<source>The device can now be safely removed from the computer.</source>
1450+
<target state="new">The device can now be safely removed from the computer.</target>
1451+
</trans-unit>
1452+
<trans-unit id="EjectNotificationErrorDialogHeader" translate="yes" xml:space="preserve">
1453+
<source>Problem Ejecting Device</source>
1454+
<target state="new">Problem Ejecting Device</target>
1455+
</trans-unit>
1456+
<trans-unit id="EjectNotificationErrorDialogBody" translate="yes" xml:space="preserve">
1457+
<source>This device is currently in use. Close any programs, windows or tabs that might be using the device, and then try again.</source>
1458+
<target state="new">This device is currently in use. Close any programs, windows or tabs that might be using the device, and then try again.</target>
1459+
</trans-unit>
1460+
<trans-unit id="SideBarEjectDevice.Text" translate="yes" xml:space="preserve">
1461+
<source>Eject</source>
1462+
<target state="new">Eject</target>
1463+
</trans-unit>
14441464
</group>
14451465
</body>
14461466
</file>

Files/MultilingualResources/Files.es-ES.xlf

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1438,6 +1438,26 @@
14381438
<source>Show hidden files and folders</source>
14391439
<target state="new">Show hidden files and folders</target>
14401440
</trans-unit>
1441+
<trans-unit id="EjectNotificationHeader" translate="yes" xml:space="preserve">
1442+
<source>Safe to remove hardware</source>
1443+
<target state="new">Safe to remove hardware</target>
1444+
</trans-unit>
1445+
<trans-unit id="EjectNotificationBody" translate="yes" xml:space="preserve">
1446+
<source>The device can now be safely removed from the computer.</source>
1447+
<target state="new">The device can now be safely removed from the computer.</target>
1448+
</trans-unit>
1449+
<trans-unit id="EjectNotificationErrorDialogHeader" translate="yes" xml:space="preserve">
1450+
<source>Problem Ejecting Device</source>
1451+
<target state="new">Problem Ejecting Device</target>
1452+
</trans-unit>
1453+
<trans-unit id="EjectNotificationErrorDialogBody" translate="yes" xml:space="preserve">
1454+
<source>This device is currently in use. Close any programs, windows or tabs that might be using the device, and then try again.</source>
1455+
<target state="new">This device is currently in use. Close any programs, windows or tabs that might be using the device, and then try again.</target>
1456+
</trans-unit>
1457+
<trans-unit id="SideBarEjectDevice.Text" translate="yes" xml:space="preserve">
1458+
<source>Eject</source>
1459+
<target state="new">Eject</target>
1460+
</trans-unit>
14411461
</group>
14421462
</body>
14431463
</file>

Files/MultilingualResources/Files.fr-FR.xlf

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1439,6 +1439,26 @@
14391439
<source>Show hidden files and folders</source>
14401440
<target state="new">Show hidden files and folders</target>
14411441
</trans-unit>
1442+
<trans-unit id="EjectNotificationHeader" translate="yes" xml:space="preserve">
1443+
<source>Safe to remove hardware</source>
1444+
<target state="new">Safe to remove hardware</target>
1445+
</trans-unit>
1446+
<trans-unit id="EjectNotificationBody" translate="yes" xml:space="preserve">
1447+
<source>The device can now be safely removed from the computer.</source>
1448+
<target state="new">The device can now be safely removed from the computer.</target>
1449+
</trans-unit>
1450+
<trans-unit id="EjectNotificationErrorDialogHeader" translate="yes" xml:space="preserve">
1451+
<source>Problem Ejecting Device</source>
1452+
<target state="new">Problem Ejecting Device</target>
1453+
</trans-unit>
1454+
<trans-unit id="EjectNotificationErrorDialogBody" translate="yes" xml:space="preserve">
1455+
<source>This device is currently in use. Close any programs, windows or tabs that might be using the device, and then try again.</source>
1456+
<target state="new">This device is currently in use. Close any programs, windows or tabs that might be using the device, and then try again.</target>
1457+
</trans-unit>
1458+
<trans-unit id="SideBarEjectDevice.Text" translate="yes" xml:space="preserve">
1459+
<source>Eject</source>
1460+
<target state="new">Eject</target>
1461+
</trans-unit>
14421462
</group>
14431463
</body>
14441464
</file>

0 commit comments

Comments
 (0)