Kyle Delaney
Kyle Delaney

Reputation: 12284

How do I modify directory timestamps when File Explorer is open?

My application creates files and directories throughout the year and needs to access the timestamps of those directories to determine if it's time to create another one. So it's vital that when I move a directory I preserve its timestamps. I can do it like this when Directory.Move() isn't an option (e.g. when moving to a different drive).

    FileSystem.CopyDirectory(sourcePath, targetPath, overwrite);

    Directory.SetCreationTimeUtc  (targetPath, Directory.GetCreationTimeUtc  (sourcePath));
    Directory.SetLastAccessTimeUtc(targetPath, Directory.GetLastAccessTimeUtc(sourcePath));
    Directory.SetLastWriteTimeUtc (targetPath, Directory.GetLastWriteTimeUtc (sourcePath));

    Directory.Delete(sourcePath, true);

However, all three of these "Directory.Set" methods fail if File Explorer is open, and it seems that it doesn't even matter whether the directory in question is currently visible in File Explorer or not (EDIT: I suspect this has something to do with Quick Access, but the reason isn't particularly important). It throws an IOException that says "The process cannot access the file 'C:\MyFolder' because it is being used by another process."

How should I handle this? Is there an alternative way to modify a timestamp that doesn't throw an error when File Explorer is open? Should I automatically close File Explorer? Or if my application simply needs to fail, then I'd like to fail before any file operations take place. Is there a way to determine ahead of time if Directory.SetCreationTimeUtc() for example will encounter an IOException?

Thanks in advance.

EDIT: I've made a discovery. Here's some sample code you can use to try recreating the problem:

using System;
using System.IO;

namespace CreationTimeTest
{
    class Program
    {
        static void Main( string[] args )
        {
            try
            {
                DirectoryInfo di = new DirectoryInfo( @"C:\Test" );

                di.CreationTimeUtc = DateTime.UtcNow;

                Console.WriteLine( di.FullName + " creation time set to " + di.CreationTimeUtc );
            }
            catch ( Exception ex )
            {
                Console.WriteLine( ex );
                //throw;
            }

            finally
            {
                Console.ReadKey( true );
            }

        }
    }
}

Create C:\Test, build CreationTimeTest.exe, and run it.

I've found that the "used by another process" error doesn't always occur just because File Explorer is open. It occurs if the folder C:\Test had been visible because C:\ was expanded. This means the time stamp can be set just fine if File Explorer is open and C:\ was never expanded. However, once C:\Test becomes visible in File Explorer, it seems to remember that folder and not allow any time stamp modification even after C:\ is collapsed. Can anyone recreate this?

EDIT: I'm now thinking that this is a File Explorer bug.

I have recreated this behavior using CreationTimeTest on multiple Windows 10 devices. There are two ways an attempt to set the creation time can throw the "used by another process" exception. The first is to have C:\Test open in the main pane, but in that case you can navigate away from C:\Test and then the program will run successfully again. But the second way is to have C:\Test visible in the navigation pane, i.e. to have C:\ expanded. And once you've done that, it seems File Explorer keeps a handle open because the program continues to fail even once you collapse C:\ until you close File Explorer.

I was mistaken earlier. Having C:\Test be visible doesn't cause the problem. C:\Test can be visible in the main pane without issue. Its visibility in the navigation pane is what matters.

Upvotes: 2

Views: 647

Answers (3)

Andrew Dennison
Andrew Dennison

Reputation: 1099

As others have reported I found that:
Directory.SetLastWriteTime(directoryPathName, dateTime); fails consistently when File Explorer has opened the directory. I used Handle v5.0 to confirm that Explorer sometimes keeps a directory open after you have navigated to another folder.

After Explorer was closed, the handle was closed and SetLastWriteTime succeeded. Since I wanted to invoke my tool from an Explorer Context Menu, this was not an acceptable workaround. But it did confirm the root cause was Explorer, not bad coding (failure to dispose of a stream for example) on my part or an anti-virus tool holding a handle.

Since other tools such as NirSoft's FolderTimeUpdate and Beyond Compare succeed when SetLastWriteTime fails, I kept looking.

With the help of other articles, I found that BackupSemantics is required on the call to CreateFile() to open the directory.

When re-testing I was puzzled to find that Directory.SetLastWriteTime worked. I double-checked that Explorer had the folder open. The additional factor was that my recent test inadvertently was built with .NET 8.0.

So here is what I use when:

  • Using NET472
  • Explorer has opened the directory.

Note: This code is not well-tested yet. It works for my use case, "Set directory lastModifedDateTime when invoked from a File Explorer Context Menu." The error handling is low grade. It should probably throw errors that mimic the .NET method.

It has not been tested for all versions of.NET, net-standard, and Net Core.

This code uses the standard definitions from SetFileTime (kernel32)

     /// <summary>
    /// Handles case when the directory is open in Explorer, and we want to change lastWriteTime
    /// This approach can change Directory.LastWriteTime, when the directory is open in Explorer.
    /// Directory.SetLastWriteTime always fails in this case, under .NET 472
    /// This is always the case when invoked from an Explorer context menu.
    /// </summary>
    /// <param name="pathName"> </param>
    /// <param name="creationTime"> </param>
    /// <param name="lastWriteTime"> </param>
    /// <param name="accessTime"> This is probably silly since it is changing all the time esp if a directory </param>
    internal static void SetDatesAndTimes(
        string pathName,
        DateTime? creationTime = null,
        DateTime? lastWriteTime = null,
        DateTime? accessTime = null)
    {
        long lCreationTime = creationTime?.ToFileTimeUtc() ?? 0;

        long lWriteTime = lastWriteTime?.ToFileTimeUtc() ?? 0;

        long lAccessTime = accessTime?.ToFileTimeUtc() ?? 0;

        using var handle = CreateFile(
            lpFileName: pathName,
            dwDesiredAccess: GenericRight.GENERIC_READ | GenericRight.GENERIC_WRITE,
            dwShareMode: FileShare.ReadWrite,
            lpSecurityAttributes: IntPtr.Zero,
            dwCreationDisposition: ECreationDisposition.OPEN_EXISTING,
            dwAttributes: EFileAttributes.BackupSemantics, // << this is the magic sauce
            hTemplateFile: IntPtr.Zero);

        if (handle.IsInvalid) throw new Win32Exception();

        var result = SetFileTime(handle, ref lCreationTime, ref lAccessTime, ref lWriteTime);

        if (result == false) throw new Win32Exception();
    }

Upvotes: 1

Anders Carstensen
Anders Carstensen

Reputation: 4174

I am suspecting FileSystem.CopyDirectory ties into Explorer and somehow blocks the directory. Try copying all the files and directories using standard C# methods, like this:

DirectoryCopy(@"C:\SourceDirectory", @"D:\DestinationDirectory", true);

Using these utility methods:

private static void DirectoryCopy(string sourceDirName, string destDirName, bool copySubDirs)
{
    // Get the subdirectories for the specified directory.
    DirectoryInfo dir = new DirectoryInfo(sourceDirName);

    if (!dir.Exists)
    {
        throw new DirectoryNotFoundException("Source directory does not exist or could not be found: " + sourceDirName);
    }

    if ((dir.Attributes & FileAttributes.ReparsePoint) == FileAttributes.ReparsePoint)
    {
        // Don't copy symbolic links
        return;
    }

    var createdDirectory = false;
    // If the destination directory doesn't exist, create it.
    if (!Directory.Exists(destDirName))
    {
        var newdir = Directory.CreateDirectory(destDirName);
        createdDirectory = true;
    }

    // Get the files in the directory and copy them to the new location.
    DirectoryInfo[] dirs = dir.GetDirectories();
    FileInfo[] files = dir.GetFiles();
    foreach (FileInfo file in files)
    {
        if ((file.Attributes & FileAttributes.ReparsePoint) == FileAttributes.ReparsePoint)
            continue; // Don't copy symbolic links

        string temppath = Path.Combine(destDirName, file.Name);
        file.CopyTo(temppath, false);
        CopyMetaData(file, new FileInfo(temppath));
    }

    // If copying subdirectories, copy them and their contents to new location.
    if (copySubDirs)
    {
        foreach (DirectoryInfo subdir in dirs)
        {
            string temppath = Path.Combine(destDirName, subdir.Name);
            DirectoryCopy(subdir.FullName, temppath, copySubDirs);
        }
    }

    if (createdDirectory)
    {
        // We must set it AFTER copying all files in the directory - otherwise the timestamp gets updated to Now.
        CopyMetaData(dir, new DirectoryInfo(destDirName));
    }
}

private static void CopyMetaData(FileSystemInfo source, FileSystemInfo dest)
{
    dest.Attributes = source.Attributes;
    dest.CreationTimeUtc = source.CreationTimeUtc;
    dest.LastAccessTimeUtc = source.LastAccessTimeUtc;
    dest.LastWriteTimeUtc = source.LastWriteTimeUtc;
}

Upvotes: 1

bic
bic

Reputation: 907

Try this:

        string sourcePath = "";
        string targetPath = "";

        DirectoryInfo sourceDirectoryInfo = new DirectoryInfo(sourcePath);

        FileSystem.CopyDirectory(sourcePath, targetPath, overwrite);

        DirectoryInfo targetDirectory = new DirectoryInfo(targetPath);

        targetDirectory.CreationTimeUtc = sourceDirectoryInfo.CreationTimeUtc;
        targetDirectory.LastAccessTimeUtc = sourceDirectoryInfo.LastAccessTimeUtc;
        targetDirectory.LastWriteTimeUtc = sourceDirectoryInfo.LastWriteTimeUtc;

        Directory.Delete(sourcePath, true);

This will allow you to set the creation/access/write times for the target directory, so long as the directory itself is not open in explorer (I am assuming it won't be, as it has only just been created).

Upvotes: 1

Related Questions