Skip to content

Commit 5b5541d

Browse files
authored
Feature: Added 'compress' attribute option to the properties window (#16319)
1 parent 2a343ee commit 5b5541d

File tree

14 files changed

+187
-30
lines changed

14 files changed

+187
-30
lines changed

src/Files.App.CsWin32/NativeMethods.txt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,12 @@ PSGetPropertyKeyFromName
135135
ShellExecuteEx
136136
CoTaskMemFree
137137
QueryDosDevice
138+
DeviceIoControl
139+
GetLastError
140+
CreateFile
141+
GetVolumeInformation
142+
COMPRESSION_FORMAT
143+
FILE_ACCESS_RIGHTS
138144
FindFirstFileEx
139145
FindNextFile
140146
CreateFile
@@ -160,5 +166,10 @@ IApplicationDocumentLists
160166
ApplicationDocumentLists
161167
IApplicationActivationManager
162168
MENU_ITEM_TYPE
169+
COMPRESSION_FORMAT
170+
FSCTL_SET_COMPRESSION
171+
FSCTL_DISMOUNT_VOLUME
172+
FSCTL_LOCK_VOLUME
173+
FILE_FILE_COMPRESSION
163174
WM_WINDOWPOSCHANGING
164175
WINDOWPOS

src/Files.App/Data/Models/RemovableDevice.cs

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
// Copyright (c) 2024 Files Community
22
// Licensed under the MIT License. See the LICENSE.
33

4-
using System;
5-
using System.Diagnostics;
6-
using System.Threading.Tasks;
4+
using Windows.Win32;
5+
using Windows.Win32.Storage.FileSystem;
76
using static Files.App.Helpers.Win32PInvoke;
87

98
namespace Files.App.Data.Models
@@ -20,7 +19,7 @@ public RemovableDevice(string letter)
2019
string filename = @"\\.\" + driveLetter + ":";
2120

2221
handle = CreateFileFromAppW(filename,
23-
GENERIC_READ | GENERIC_WRITE,
22+
(uint)(FILE_ACCESS_RIGHTS.FILE_GENERIC_READ | FILE_ACCESS_RIGHTS.FILE_GENERIC_WRITE),
2423
FILE_SHARE_READ | FILE_SHARE_WRITE,
2524
nint.Zero, OPEN_EXISTING, 0, nint.Zero);
2625
}
@@ -52,7 +51,7 @@ private async Task<bool> LockVolumeAsync()
5251

5352
for (int i = 0; i < 5; i++)
5453
{
55-
if (DeviceIoControl(handle, FSCTL_LOCK_VOLUME, nint.Zero, 0, nint.Zero, 0, out _, nint.Zero))
54+
if (DeviceIoControl(handle, PInvoke.FSCTL_LOCK_VOLUME, nint.Zero, 0, nint.Zero, 0, out _, nint.Zero))
5655
{
5756
Debug.WriteLine("Lock successful!");
5857
result = true;
@@ -72,7 +71,7 @@ private async Task<bool> LockVolumeAsync()
7271

7372
private bool DismountVolume()
7473
{
75-
return DeviceIoControl(handle, FSCTL_DISMOUNT_VOLUME, nint.Zero, 0, nint.Zero, 0, out _, nint.Zero);
74+
return DeviceIoControl(handle, PInvoke.FSCTL_DISMOUNT_VOLUME, nint.Zero, 0, nint.Zero, 0, out _, nint.Zero);
7675
}
7776

7877
private bool PreventRemovalOfVolume(bool prevent)

src/Files.App/Data/Models/SelectedItemsPropertiesViewModel.cs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -718,6 +718,40 @@ public bool? IsHiddenEditedValue
718718
set => SetProperty(ref isHiddenEditedValue, value);
719719
}
720720

721+
private bool? isContentCompressed;
722+
/// <remarks>
723+
/// Applies to NTFS item compression.
724+
/// </remarks>
725+
public bool? IsContentCompressed
726+
{
727+
get => isContentCompressed;
728+
set
729+
{
730+
SetProperty(ref isContentCompressed, value);
731+
IsContentCompressedEditedValue = value;
732+
}
733+
}
734+
735+
private bool? isContentCompressedEditedValue;
736+
/// <remarks>
737+
/// Applies to NTFS item compression.
738+
/// </remarks>
739+
public bool? IsContentCompressedEditedValue
740+
{
741+
get => isContentCompressedEditedValue;
742+
set => SetProperty(ref isContentCompressedEditedValue, value);
743+
}
744+
745+
private bool canCompressContent;
746+
/// <remarks>
747+
/// Applies to NTFS item compression.
748+
/// </remarks>
749+
public bool CanCompressContent
750+
{
751+
get => canCompressContent;
752+
set => SetProperty(ref canCompressContent, value);
753+
}
754+
721755
private bool runAsAdmin;
722756
public bool RunAsAdmin
723757
{

src/Files.App/Helpers/Win32/Win32Helper.Storage.cs

Lines changed: 69 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,13 @@
1313
using System.Windows.Forms;
1414
using Vanara.PInvoke;
1515
using Windows.System;
16+
using Windows.Win32;
17+
using Windows.Win32.Foundation;
18+
using Windows.Win32.Storage.FileSystem;
19+
using static Vanara.PInvoke.Kernel32;
20+
using COMPRESSION_FORMAT = Windows.Win32.Storage.FileSystem.COMPRESSION_FORMAT;
21+
using HRESULT = Vanara.PInvoke.HRESULT;
22+
using HWND = Vanara.PInvoke.HWND;
1623

1724
namespace Files.App.Helpers
1825
{
@@ -324,7 +331,7 @@ public static string ExtractStringFromDLL(string file, int number)
324331
}
325332

326333
if (iconData is not null || iconOptions.HasFlag(IconOptions.ReturnThumbnailOnly))
327-
return iconData;
334+
return iconData;
328335
else
329336
{
330337
var shfi = new Shell32.SHFILEINFO();
@@ -333,7 +340,7 @@ public static string ExtractStringFromDLL(string file, int number)
333340
// Cannot access file, use file attributes
334341
var useFileAttibutes = iconData is null;
335342

336-
var ret = Shell32.SHGetFileInfo(path, isFolder ? FileAttributes.Directory : 0, ref shfi, Shell32.SHFILEINFO.Size, flags);
343+
var ret = Shell32.SHGetFileInfo(path, isFolder ? FileAttributes.Directory : 0, ref shfi, Shell32.SHFILEINFO.Size, flags);
337344
if (ret == IntPtr.Zero)
338345
return iconData;
339346

@@ -884,24 +891,24 @@ private static Process CreatePowershellProcess(string command, PowerShellExecuti
884891
public static SafeFileHandle CreateFileForWrite(string filePath, bool overwrite = true)
885892
{
886893
return new SafeFileHandle(Win32PInvoke.CreateFileFromApp(filePath,
887-
Win32PInvoke.GENERIC_WRITE, 0, IntPtr.Zero, overwrite ? Win32PInvoke.CREATE_ALWAYS : Win32PInvoke.OPEN_ALWAYS, (uint)Win32PInvoke.File_Attributes.BackupSemantics, IntPtr.Zero), true);
894+
(uint)FILE_ACCESS_RIGHTS.FILE_GENERIC_WRITE, 0, IntPtr.Zero, overwrite ? Win32PInvoke.CREATE_ALWAYS : Win32PInvoke.OPEN_ALWAYS, (uint)Win32PInvoke.File_Attributes.BackupSemantics, IntPtr.Zero), true);
888895
}
889896

890897
public static SafeFileHandle OpenFileForRead(string filePath, bool readWrite = false, uint flags = 0)
891898
{
892899
return new SafeFileHandle(Win32PInvoke.CreateFileFromApp(filePath,
893-
Win32PInvoke.GENERIC_READ | (readWrite ? Win32PInvoke.GENERIC_WRITE : 0), (uint)(Win32PInvoke.FILE_SHARE_READ | (readWrite ? 0 : Win32PInvoke.FILE_SHARE_WRITE)), IntPtr.Zero, Win32PInvoke.OPEN_EXISTING, (uint)Win32PInvoke.File_Attributes.BackupSemantics | flags, IntPtr.Zero), true);
900+
(uint)FILE_ACCESS_RIGHTS.FILE_GENERIC_READ | (uint)(readWrite ? FILE_ACCESS_RIGHTS.FILE_GENERIC_WRITE : 0u), (uint)(Win32PInvoke.FILE_SHARE_READ | (readWrite ? 0 : Win32PInvoke.FILE_SHARE_WRITE)), IntPtr.Zero, Win32PInvoke.OPEN_EXISTING, (uint)Win32PInvoke.File_Attributes.BackupSemantics | flags, IntPtr.Zero), true);
894901
}
895902

896903
public static bool GetFileDateModified(string filePath, out FILETIME dateModified)
897904
{
898-
using var hFile = new SafeFileHandle(Win32PInvoke.CreateFileFromApp(filePath, Win32PInvoke.GENERIC_READ, Win32PInvoke.FILE_SHARE_READ, IntPtr.Zero, Win32PInvoke.OPEN_EXISTING, (uint)Win32PInvoke.File_Attributes.BackupSemantics, IntPtr.Zero), true);
905+
using var hFile = new SafeFileHandle(Win32PInvoke.CreateFileFromApp(filePath, (uint)FILE_ACCESS_RIGHTS.FILE_GENERIC_READ, Win32PInvoke.FILE_SHARE_READ, IntPtr.Zero, Win32PInvoke.OPEN_EXISTING, (uint)Win32PInvoke.File_Attributes.BackupSemantics, IntPtr.Zero), true);
899906
return Win32PInvoke.GetFileTime(hFile.DangerousGetHandle(), out _, out _, out dateModified);
900907
}
901908

902909
public static bool SetFileDateModified(string filePath, FILETIME dateModified)
903910
{
904-
using var hFile = new SafeFileHandle(Win32PInvoke.CreateFileFromApp(filePath, Win32PInvoke.FILE_WRITE_ATTRIBUTES, 0, IntPtr.Zero, Win32PInvoke.OPEN_EXISTING, (uint)Win32PInvoke.File_Attributes.BackupSemantics, IntPtr.Zero), true);
911+
using var hFile = new SafeFileHandle(Win32PInvoke.CreateFileFromApp(filePath, (uint)FILE_ACCESS_RIGHTS.FILE_WRITE_ATTRIBUTES, 0, IntPtr.Zero, Win32PInvoke.OPEN_EXISTING, (uint)Win32PInvoke.File_Attributes.BackupSemantics, IntPtr.Zero), true);
905912
return Win32PInvoke.SetFileTime(hFile.DangerousGetHandle(), new(), new(), dateModified);
906913
}
907914

@@ -935,10 +942,64 @@ public static bool UnsetFileAttribute(string lpFileName, FileAttributes dwAttrs)
935942
return Win32PInvoke.SetFileAttributesFromApp(lpFileName, lpFileInfo.dwFileAttributes & ~dwAttrs);
936943
}
937944

945+
public static unsafe bool CanCompressContent(string path)
946+
{
947+
path = Path.GetPathRoot(path) ?? string.Empty;
948+
uint dwFileSystemFlags = 0;
949+
950+
var success = PInvoke.GetVolumeInformation(
951+
path,
952+
null,
953+
0u,
954+
null,
955+
null,
956+
&dwFileSystemFlags,
957+
null,
958+
0u);
959+
960+
if (!success)
961+
return false;
962+
963+
return (dwFileSystemFlags & PInvoke.FILE_FILE_COMPRESSION) != 0;
964+
}
965+
966+
public static unsafe bool SetCompressionAttributeIoctl(string lpFileName, bool isCompressed)
967+
{
968+
// GENERIC_READ | GENERIC_WRITE flags are needed here
969+
// FILE_FLAG_BACKUP_SEMANTICS is used to open directories
970+
using var hFile = PInvoke.CreateFile(
971+
lpFileName,
972+
(uint)(FILE_ACCESS_RIGHTS.FILE_GENERIC_READ | FILE_ACCESS_RIGHTS.FILE_GENERIC_WRITE | FILE_ACCESS_RIGHTS.FILE_WRITE_ATTRIBUTES),
973+
FILE_SHARE_MODE.FILE_SHARE_READ | FILE_SHARE_MODE.FILE_SHARE_WRITE,
974+
lpSecurityAttributes: null,
975+
FILE_CREATION_DISPOSITION.OPEN_EXISTING,
976+
FILE_FLAGS_AND_ATTRIBUTES.FILE_ATTRIBUTE_NORMAL | FILE_FLAGS_AND_ATTRIBUTES.FILE_FLAG_BACKUP_SEMANTICS,
977+
hTemplateFile: null);
978+
979+
if (hFile.IsInvalid)
980+
return false;
981+
982+
var bytesReturned = 0u;
983+
var compressionFormat = isCompressed
984+
? COMPRESSION_FORMAT.COMPRESSION_FORMAT_DEFAULT
985+
: COMPRESSION_FORMAT.COMPRESSION_FORMAT_NONE;
986+
987+
var result = PInvoke.DeviceIoControl(
988+
new(hFile.DangerousGetHandle()),
989+
PInvoke.FSCTL_SET_COMPRESSION,
990+
&compressionFormat,
991+
sizeof(ushort),
992+
null,
993+
0u,
994+
&bytesReturned);
995+
996+
return result;
997+
}
998+
938999
public static string ReadStringFromFile(string filePath)
9391000
{
9401001
IntPtr hFile = Win32PInvoke.CreateFileFromApp(filePath,
941-
Win32PInvoke.GENERIC_READ,
1002+
(uint)FILE_ACCESS_RIGHTS.FILE_GENERIC_READ,
9421003
Win32PInvoke.FILE_SHARE_READ,
9431004
IntPtr.Zero,
9441005
Win32PInvoke.OPEN_EXISTING,
@@ -987,7 +1048,7 @@ public static string ReadStringFromFile(string filePath)
9871048
public static bool WriteStringToFile(string filePath, string str, Win32PInvoke.File_Attributes flags = 0)
9881049
{
9891050
IntPtr hStream = Win32PInvoke.CreateFileFromApp(filePath,
990-
Win32PInvoke.GENERIC_WRITE, 0, IntPtr.Zero, Win32PInvoke.CREATE_ALWAYS, (uint)(Win32PInvoke.File_Attributes.BackupSemantics | flags), IntPtr.Zero);
1051+
(uint)FILE_ACCESS_RIGHTS.FILE_GENERIC_WRITE, 0, IntPtr.Zero, Win32PInvoke.CREATE_ALWAYS, (uint)(Win32PInvoke.File_Attributes.BackupSemantics | flags), IntPtr.Zero);
9911052
if (hStream.ToInt64() == -1)
9921053
{
9931054
return false;

src/Files.App/Helpers/Win32/Win32PInvoke.Consts.cs

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,19 +22,14 @@ public static partial class Win32PInvoke
2222
public const int FILE_NOTIFY_CHANGE_SECURITY = 256;
2323

2424
public const int INVALID_HANDLE_VALUE = -1;
25-
public const uint GENERIC_READ = 0x80000000;
26-
public const uint GENERIC_WRITE = 0x40000000;
2725
public const int FILE_SHARE_READ = 0x00000001;
2826
public const int FILE_SHARE_WRITE = 0x00000002;
2927
public const uint FILE_SHARE_DELETE = 0x00000004;
3028
public const int OPEN_EXISTING = 3;
31-
public const int FSCTL_LOCK_VOLUME = 0x00090018;
32-
public const int FSCTL_DISMOUNT_VOLUME = 0x00090020;
3329
public const int IOCTL_STORAGE_EJECT_MEDIA = 0x2D4808;
3430
public const int IOCTL_STORAGE_MEDIA_REMOVAL = 0x002D4804;
3531

3632
public const uint FILE_APPEND_DATA = 0x0004;
37-
public const uint FILE_WRITE_ATTRIBUTES = 0x100;
3833

3934

4035
public const uint FILE_BEGIN = 0;
@@ -45,8 +40,10 @@ public static partial class Win32PInvoke
4540
public const uint OPEN_ALWAYS = 4;
4641
public const uint TRUNCATE_EXISTING = 5;
4742

48-
public const int MAXIMUM_REPARSE_DATA_BUFFER_SIZE = 16 * 1024;
43+
// FSCTL
4944
public const int FSCTL_GET_REPARSE_POINT = 0x000900A8;
45+
46+
public const int MAXIMUM_REPARSE_DATA_BUFFER_SIZE = 16 * 1024;
5047
public const uint IO_REPARSE_TAG_MOUNT_POINT = 0xA0000003;
5148
public const uint IO_REPARSE_TAG_SYMLINK = 0xA000000C;
5249

src/Files.App/Strings/en-US/Resources.resw

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3977,6 +3977,9 @@
39773977
<data name="BulkRename" xml:space="preserve">
39783978
<value>Bulk rename</value>
39793979
</data>
3980+
<data name="CompressContents" xml:space="preserve">
3981+
<value>Compress contents</value>
3982+
</data>
39803983
<data name="ShowCreateAlternateDataStream" xml:space="preserve">
39813984
<value>Show option to create alternate data stream</value>
39823985
</data>

src/Files.App/Utils/Serialization/Implementation/DefaultSettingsSerializer.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System.IO;
55
using Windows.Win32;
6+
using Windows.Win32.Storage.FileSystem;
67
using static Files.App.Helpers.Win32Helper;
78
using static Files.App.Helpers.Win32PInvoke;
89

@@ -16,7 +17,7 @@ public bool CreateFile(string path)
1617
{
1718
PInvoke.CreateDirectoryFromApp(Path.GetDirectoryName(path), null);
1819

19-
var hFile = CreateFileFromApp(path, GENERIC_READ, FILE_SHARE_READ, IntPtr.Zero, OPEN_ALWAYS, (uint)File_Attributes.BackupSemantics, IntPtr.Zero);
20+
var hFile = CreateFileFromApp(path, (uint)FILE_ACCESS_RIGHTS.FILE_GENERIC_READ, FILE_SHARE_READ, IntPtr.Zero, OPEN_ALWAYS, (uint)File_Attributes.BackupSemantics, IntPtr.Zero);
2021
if (hFile.IsHandleInvalid())
2122
{
2223
return false;

src/Files.App/ViewModels/Properties/Items/CombinedProperties.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,15 @@ private void ViewModel_PropertyChanged(object sender, System.ComponentModel.Prop
170170

171171
}
172172
break;
173+
174+
case "IsContentCompressed":
175+
{
176+
List.ForEach(x =>
177+
{
178+
Win32Helper.SetCompressionAttributeIoctl(x.ItemPath, ViewModel.IsContentCompressed ?? false);
179+
});
180+
}
181+
break;
173182
}
174183
}
175184
}

src/Files.App/ViewModels/Properties/Items/FileProperties.cs

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ public override void GetBaseProperties()
4545
ViewModel.CustomIconSource = Item.CustomIconSource;
4646
ViewModel.LoadFileIcon = Item.LoadFileIcon;
4747
ViewModel.IsDownloadedFile = Win32Helper.ReadStringFromFile($"{Item.ItemPath}:Zone.Identifier") is not null;
48-
ViewModel.IsEditAlbumCoverVisible =
48+
ViewModel.IsEditAlbumCoverVisible =
4949
FileExtensionHelpers.IsVideoFile(Item.FileExtension) ||
5050
FileExtensionHelpers.IsAudioFile(Item.FileExtension);
5151

@@ -93,10 +93,10 @@ await MainWindow.Instance.DispatcherQueue.EnqueueOrInvokeAsync(
9393

9494
public override async Task GetSpecialPropertiesAsync()
9595
{
96-
ViewModel.IsReadOnly = Win32Helper.HasFileAttribute(
97-
Item.ItemPath, System.IO.FileAttributes.ReadOnly);
98-
ViewModel.IsHidden = Win32Helper.HasFileAttribute(
99-
Item.ItemPath, System.IO.FileAttributes.Hidden);
96+
ViewModel.IsReadOnly = Win32Helper.HasFileAttribute(Item.ItemPath, System.IO.FileAttributes.ReadOnly);
97+
ViewModel.IsHidden = Win32Helper.HasFileAttribute(Item.ItemPath, System.IO.FileAttributes.Hidden);
98+
ViewModel.CanCompressContent = Win32Helper.CanCompressContent(Item.ItemPath);
99+
ViewModel.IsContentCompressed = Win32Helper.HasFileAttribute(Item.ItemPath, System.IO.FileAttributes.Compressed);
100100

101101
ViewModel.ItemSizeVisibility = true;
102102
ViewModel.ItemSize = Item.FileSizeBytes.ToLongSizeString();
@@ -279,13 +279,17 @@ private async void ViewModel_PropertyChanged(object sender, System.ComponentMode
279279
if (ViewModel.IsHidden is not null)
280280
{
281281
if ((bool)ViewModel.IsHidden)
282-
Win32Helper.SetFileAttribute(Item.ItemPath, System.IO.FileAttributes.Hidden);
282+
Win32Helper.SetFileAttribute(Item.ItemPath, System.IO.FileAttributes.Hidden);
283283
else
284284
Win32Helper.UnsetFileAttribute(Item.ItemPath, System.IO.FileAttributes.Hidden);
285285
}
286286

287287
break;
288288

289+
case nameof(ViewModel.IsContentCompressed):
290+
Win32Helper.SetCompressionAttributeIoctl(Item.ItemPath, ViewModel.IsContentCompressed ?? false);
291+
break;
292+
289293
case nameof(ViewModel.RunAsAdmin):
290294
case nameof(ViewModel.ShortcutItemPath):
291295
case nameof(ViewModel.ShortcutItemWorkingDir):

src/Files.App/ViewModels/Properties/Items/FolderProperties.cs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,9 @@ await MainWindow.Instance.DispatcherQueue.EnqueueOrInvokeAsync(
7373

7474
public async override Task GetSpecialPropertiesAsync()
7575
{
76-
ViewModel.IsHidden = Win32Helper.HasFileAttribute(
77-
Item.ItemPath, System.IO.FileAttributes.Hidden);
76+
ViewModel.IsHidden = Win32Helper.HasFileAttribute(Item.ItemPath, System.IO.FileAttributes.Hidden);
77+
ViewModel.CanCompressContent = Win32Helper.CanCompressContent(Item.ItemPath);
78+
ViewModel.IsContentCompressed = Win32Helper.HasFileAttribute(Item.ItemPath, System.IO.FileAttributes.Compressed);
7879

7980
var result = await FileThumbnailHelper.GetIconAsync(
8081
Item.ItemPath,
@@ -211,6 +212,10 @@ private async void ViewModel_PropertyChanged(object sender, System.ComponentMode
211212
}
212213
break;
213214

215+
case "IsContentCompressed":
216+
Win32Helper.SetCompressionAttributeIoctl(Item.ItemPath, ViewModel.IsContentCompressed ?? false);
217+
break;
218+
214219
case "ShortcutItemPath":
215220
case "ShortcutItemWorkingDir":
216221
case "ShortcutItemArguments":

0 commit comments

Comments
 (0)