Question

I want to compress a folder using NTFS compression in .NET. I found this post, but it does not work. It throws an exception ("Invalid Parameter").

DirectoryInfo directoryInfo = new DirectoryInfo( destinationDir );
if( ( directoryInfo.Attributes & FileAttributes.Compressed ) != FileAttributes.Compressed )
{
   string objPath = "Win32_Directory.Name=" + "\"" + destinationDir + "\"";
   using( ManagementObject dir = new ManagementObject( objPath ) )
   {
      ManagementBaseObject outParams = dir.InvokeMethod( "Compress", null, null );
      uint ret = (uint)( outParams.Properties["ReturnValue"].Value );
   }
}

Anybody knows how to enable NTFS compression on a folder?

Was it helpful?

Solution

Using P/Invoke is, in my experience, usually easier than WMI. I believe the following should work:

private const int FSCTL_SET_COMPRESSION = 0x9C040;
private const short COMPRESSION_FORMAT_DEFAULT = 1;

[DllImport("kernel32.dll", SetLastError = true)]
private static extern int DeviceIoControl(
    SafeFileHandle hDevice,
    int dwIoControlCode,
    ref short lpInBuffer,
    int nInBufferSize,
    IntPtr lpOutBuffer,
    int nOutBufferSize,
    ref int lpBytesReturned,
    IntPtr lpOverlapped);

public static bool EnableCompression(SafeFileHandle handle)
{
    int lpBytesReturned = 0;
    short lpInBuffer = COMPRESSION_FORMAT_DEFAULT;

    return DeviceIoControl(handle, FSCTL_SET_COMPRESSION,
        ref lpInBuffer, sizeof(short), IntPtr.Zero, 0,
        ref lpBytesReturned, IntPtr.Zero) != 0;
}

Since you're trying to set this on a directory, you will probably need to use P/Invoke to call CreateFile using FILE_FLAG_BACKUP_SEMANTICS to get the SafeFileHandle on the directory.

Also, note that setting compression on a directory in NTFS does not compress all the contents, it only makes new files show up as compressed (the same is true for encryption). If you want to compress the entire directory, you'll need to walk the entire directory and call DeviceIoControl on each file/folder.

OTHER TIPS

I have tested the code and it alt text!

  • Make sure it works for you with the gui. Maybe the allocation unit size is too big for compression. Or you don't have sufficient permissions.
  • For your destination use format like so: "c:/temp/testcomp" with forward slashes.

Full code:

using System.IO;
using System.Management;

class Program
{
    static void Main(string[] args)
    {
        string destinationDir = "c:/temp/testcomp";
        DirectoryInfo directoryInfo = new DirectoryInfo(destinationDir);
        if ((directoryInfo.Attributes & FileAttributes.Compressed) != FileAttributes.Compressed)
        {
            string objPath = "Win32_Directory.Name=" + "\"" + destinationDir + "\"";
            using (ManagementObject dir = new ManagementObject(objPath))
            {
                ManagementBaseObject outParams = dir.InvokeMethod("Compress", null, null);
                uint ret = (uint)(outParams.Properties["ReturnValue"].Value);
            }
        }
     }
}

When creating the Win32_Directory.Name=... string you need to double the backslashes, so for example the path C:\Foo\Bar would be built up as:

Win32_Directory.Name="C:\\Foo\\Bar",

or using your example code:

string objPath = "Win32_Directory.Name=\"C:\\\\Foo\\\\Bar\"";

Apparently the string is fed to some process that expects an escaped form of the path string.

I don't believe there is a way to set folder compression in the .NET framework as the docs (remarks section) claim it cannot be done through File.SetAttributes. This seems to be only available in the Win32 API using the DeviceIoControl function. One can still do this through .NET by using PInvoke.

Once familiar with PInvoke in general, check out the reference at pinvoke.net that discusses what the signature needs to look like in order to make this happen.

There is a much simpler way, which I am using in Windows 8 64-bit, rewritten for VB.NET. Enjoy.

    Dim Path as string = "c:\test"
    Dim strComputer As String = "."
    Dim objWMIService = GetObject("winmgmts:{impersonationLevel=impersonate}!\\.\root\cimv2")
    Dim colFolders = objWMIService.ExecQuery("Select * from Win32_Directory where name = '" & Replace(path, "\", "\\") & "'")
    For Each objFolder In colFolders
        objFolder.Compress()
    Next

works great for me. Chagne .\root to \pcname\root if you need to do it on another computer. Use with care.

This is a slight adaption of Igal Serban answer. I ran into a subtle issue with the Name having to be in a very specific format. So I added some Replace("\\", @"\\").TrimEnd('\\') magic to normalize the path first, I also cleaned up the code a bit.

var dir = new DirectoryInfo(_outputFolder);

if (!dir.Exists)
{
    dir.Create();
}

if ((dir.Attributes & FileAttributes.Compressed) == 0)
{
    try
    {
        // Enable compression for the output folder
        // (this will save a ton of disk space)

        string objPath = "Win32_Directory.Name=" + "'" + dir.FullName.Replace("\\", @"\\").TrimEnd('\\') + "'";

        using (ManagementObject obj = new ManagementObject(objPath))
        {
            using (obj.InvokeMethod("Compress", null, null))
            {
                // I don't really care about the return value, 
                // if we enabled it great but it can also be done manually
                // if really needed
            }
        }
    }
    catch (Exception ex)
    {
        System.Diagnostics.Trace.WriteLine("Cannot enable compression for folder '" + dir.FullName + "': " + ex.Message, "WMI");
    }
}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top