Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to synchronously and consistently delete a folder on NTFS with C#

Tags:

this:

Directory.Delete(dir, true);

is not synchronous.

On the immediately proceeding line you can still manipulate/read the directory.

For example, this:

Directory.Delete(destinationDir, true);
Directory.CreateDirectory(destinationDir);
Thread.Sleep(1000);

results in the folder not existing. The delete runs async, CreateDirectory doesn't create because it already exists, then delete actually fires and removes the directory.

Is there an IO API that will give me consistency?

Answer involving Thread.Sleep will invoke Zalgo. I want a real solution please.

like image 940
Andrew Bullock Avatar asked Apr 03 '16 10:04

Andrew Bullock


2 Answers

As mentioned by others it appears that the .net Framework doesn't appear to run this synchronously. Testing it in PowerShell shows that the .Net calls do no wait so something like this would create a similar outcome:

Remove-Item -Recurse -Force "C:\tempfolder"
New-Item -ItemType Directory "C:\tempfolder"

Using a file watcher (also mentioned previously) will ensure that the directory deletion is completed before the creation is done:

var path = @"C:\tempfolder";
var watcher = new FileSystemWatcher(path);
watcher.Deleted += (sender, args) => Directory.CreateDirectory(args.FullPath);
Directory.Delete(path, true);

No real surprises there but at least it's a working solution that doesn't involve calling the C++ API's from Managed Code.

like image 104
Joe Swan Avatar answered Sep 28 '22 03:09

Joe Swan


After doing some testing in C++ it seems that the native Windows functions for removing files/directories does block. It seems the problem is on the .NET side when it comes to the deletion function not being blocked, as Directory.CreateDirectory() appears to be called before Directory.Delete() is finished.

This is what I tried in a Win32 Console Application:

printf("Press enter to begin...");
while(getchar() != '\n');

LPCSTR DeletePath = "C:\\test\\DeleteMe"; //The directory to delete.
_SHFILEOPSTRUCTA* fileopt = new _SHFILEOPSTRUCTA();

fileopt->hwnd = NULL;        //No window handle.
fileopt->wFunc = FO_DELETE;  //Delete mode.
fileopt->pFrom = DeletePath; //The directory to delete.
fileopt->pTo = NULL;         //No target directory (this is only used when moving, copying, etc.).
fileopt->fFlags = FOF_NO_UI; //Display no UI dialogs.

int Success = SHFileOperationA(fileopt); //Remove the entire directory and all it's contents.
bool Success2 = CreateDirectoryA(DeletePath, NULL); //Create a new directory.

LPCSTR ReturnedValue = "False"; //I'm no C++ guru, so please don't hate. :)
LPCSTR ReturnedValue2 = "False";
if(Success == 0) { ReturnedValue = "True"; } //The SHFileOperation() returns 0 if it succeeds.
if(Success2 == true) { ReturnedValue2 = "True"; }

//Print the result of SHFileOperation().
printf("Returned value: ");
printf(ReturnedValue);
printf("\n");

//Print the result of CreateDirectory().
printf("Returned value 2: ");
printf(ReturnedValue2);
printf("\n");

//Continue.
printf("Press enter to exit...");
while(getchar() != '\n');

After pressing ENTER the first time there is a small delay before the result is shown, and when looking at the folder afterwards it's empty with a new creation and last modified date - meaning that it has been deleted and recreated in the correct order.

So in order to achieve what you want I guess you could try to create your own method which invokes SHFileOperation() instead, as the problem seems to be that the Directory.Delete() method performs the iteration itself in .NET code (see the Reference Source).


--- EDIT ---

After testing in C#, this seems to work! The only problem is that the very first time (since the application started) you call the P/Invoked SHFileOperation() function it will return a value of 2, which is equivalent to ERROR_FILE_NOT_FOUND. But if you execute it again it will return 0 (success).

NativeMethods.cs:

Imports required:

using System;
using System.Runtime.InteropServices;

Rest of the code:

[DllImport("shell32.dll", CharSet = CharSet.Unicode)]
public static extern int SHFileOperation([In] ref SHFILEOPSTRUCT lpFileOp);

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct SHFILEOPSTRUCT
{
    public IntPtr hwnd;
    public FileFuncFlags wFunc;

    [MarshalAs(UnmanagedType.LPWStr)]
    public string pFrom;

    [MarshalAs(UnmanagedType.LPWStr)]
    public string pTo;
    public FILEOP_FLAGS fFlags;

    [MarshalAs(UnmanagedType.Bool)]
    public bool fAnyOperationsAborted;
    public IntPtr hNameMappings;

    [MarshalAs(UnmanagedType.LPWStr)]
    public string lpszProgressTitle;
}

public enum FileFuncFlags : uint
{
    FO_MOVE = 0x1,
    FO_COPY = 0x2,
    FO_DELETE = 0x3,
    FO_RENAME = 0x4
}

[Flags]
public enum FILEOP_FLAGS : ushort
{
    FOF_MULTIDESTFILES = 0x1,
    FOF_CONFIRMMOUSE = 0x2,
    /// <summary>
    /// Don't create progress/report
    /// </summary>
    FOF_SILENT = 0x4,
    FOF_RENAMEONCOLLISION = 0x8,
    /// <summary>
    /// Don't prompt the user.
    /// </summary>
    FOF_NOCONFIRMATION = 0x10,
    /// <summary>
    /// Fill in SHFILEOPSTRUCT.hNameMappings.
    /// Must be freed using SHFreeNameMappings
    /// </summary>
    FOF_WANTMAPPINGHANDLE = 0x20,
    FOF_ALLOWUNDO = 0x40,
    /// <summary>
    /// On *.*, do only files
    /// </summary>
    FOF_FILESONLY = 0x80,
    /// <summary>
    /// Don't show names of files
    /// </summary>
    FOF_SIMPLEPROGRESS = 0x100,
    /// <summary>
    /// Don't confirm making any needed dirs
    /// </summary>
    FOF_NOCONFIRMMKDIR = 0x200,
    /// <summary>
    /// Don't put up error UI
    /// </summary>
    FOF_NOERRORUI = 0x400,
    /// <summary>
    /// Dont copy NT file Security Attributes
    /// </summary>
    FOF_NOCOPYSECURITYATTRIBS = 0x800,
    /// <summary>
    /// Don't recurse into directories.
    /// </summary>
    FOF_NORECURSION = 0x1000,
    /// <summary>
    /// Don't operate on connected elements.
    /// </summary>
    FOF_NO_CONNECTED_ELEMENTS = 0x2000,
    /// <summary>
    /// During delete operation, 
    /// warn if nuking instead of recycling (partially overrides FOF_NOCONFIRMATION)
    /// </summary>
    FOF_WANTNUKEWARNING = 0x4000,
    /// <summary>
    /// Treat reparse points as objects, not containers
    /// </summary>
    FOF_NORECURSEREPARSE = 0x8000
}

Some place else:

string DeletePath = "C:\\test\\DeleteMe";
NativeMethods.SHFILEOPSTRUCT fileopt = new NativeMethods.SHFILEOPSTRUCT();

fileopt.hwnd = IntPtr.Zero;
fileopt.wFunc = NativeMethods.FileFuncFlags.FO_DELETE;
fileopt.pFrom = DeletePath;
fileopt.pTo = null;
fileopt.fFlags = NativeMethods.FILEOP_FLAGS.FOF_SILENT | NativeMethods.FILEOP_FLAGS.FOF_NOCONFIRMATION |
                 NativeMethods.FILEOP_FLAGS.FOF_NOERRORUI | NativeMethods.FILEOP_FLAGS.FOF_NOCONFIRMMKDIR; //Equivalent of FOF_NO_UI.

int Success = NativeMethods.SHFileOperation(ref fileopt);
Directory.CreateDirectory(DeletePath);

MessageBox.Show("Operation returned value: " + Success.ToString(), "Test", MessageBoxButtons.OK, MessageBoxIcon.Information);

Hope this helps!

like image 36
Visual Vincent Avatar answered Sep 28 '22 02:09

Visual Vincent