2

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.

Kyle Delaney
  • 11,616
  • 6
  • 39
  • 66

2 Answers2

1

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).

bic
  • 867
  • 1
  • 8
  • 21
  • Good idea, but using `DirectoryInfo` instead of the static `Directory` class throws the same errors. – Kyle Delaney Sep 05 '17 at 20:01
  • Which line of the code is throwing the exception? The only way I could get this code to throw anything with regards to having explorer open was by being inside the `sourcePath ` directory when the `Directory.Delete` is called, which is to be expected. – bic Sep 05 '17 at 21:18
  • The assignment line. The one that's actually meant to change the creation time. That's very strange that it's not throwing an exception for you. Perhaps there's some kind of Windows setting or File Explorer setting that's causing the problem. I've tried running my exe as an administrator and I've tried changing the directory's security settings with no luck. Can you think of anything? Are you using Windows 10? – Kyle Delaney Sep 06 '17 at 14:51
1

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;
}
Anders Carstensen
  • 2,949
  • 23
  • 23
  • Thanks for the code. Is `CopyMetaData()` another one of your methods? – Kyle Delaney Sep 05 '17 at 20:11
  • @KyleDelaney Yeah, it's in the bottom of the code snippet. – Anders Carstensen Sep 05 '17 at 20:12
  • Oh sorry, I didn't scroll down. – Kyle Delaney Sep 05 '17 at 20:14
  • Yeah, I've tested stuff like this. It has nothing to do with `CopyDirectory`. If File Explorer is open, trying to set that metadata fails using either the `Directory` class or `DirectoryInfo`. – Kyle Delaney Sep 05 '17 at 20:17
  • So, have you tried completely leaving out the copying of the directory, and just setting the metadata on some random directory while File Explorer is open? – Anders Carstensen Sep 05 '17 at 20:19
  • Yes, that's what I meant when I said it has nothing to do with `CopyDirectory`. – Kyle Delaney Sep 06 '17 at 14:37
  • 1
    If setting the creation time on **any file** or **any directory** on your drives is blocked while File Explorer is open, then you have a _serious problem_. This is not normal and does certainly not happen on my machine (I use Win10). You have to investigate why this is happening. Perhaps you have some plugin installed - or perhaps some anti-virus? To confirm it is indeed explorer.exe that is blocking the file, you could try to use something like this: https://stackoverflow.com/a/20623311/1492977 - But this is not really a coding issue - the code is correct. You have an issue on your machine. – Anders Carstensen Sep 06 '17 at 14:56
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/153816/discussion-between-kyle-delaney-and-anders-keller-carstensen). – Kyle Delaney Sep 06 '17 at 21:55