Reputation: 3353
I have a Windows Forms application which uses File.Move
to move files in response to user request. This is not an asynchronous method, but when the destination is on the same drive it happens almost instantly (I assume because it can just rewrite the header and not move anything). However, it blocks the application if the destination is not on the same drive. Not desirable.
So naturally I thought about switching to async. It looks like the way to do this is to use file streams and use Stream.CopyToAsync
before removing the original. That's fine, except if it is on the same drive, this is a lot of wasteful copying.
Is there a reliable way to determine if a proposed destination for a file is on a different drive than that drive is currently stored on before deciding which operation to perform? I could obviously look at the drive letter, but that seems kind of hacky and I think there are some cases like junction points where it will not work correctly.
Another possible choice is to use something like Task.Run(() => File.Move(x))
, although I have a vaguely bad feeling about that.
Upvotes: 2
Views: 135
Reputation: 15435
I think the best effort for this is going to be comparing volume serial numbers. GetFileInformationByHandle()
is able to get the volume serial number of a file handle. This is definitely a YMMV for RAID / striped volumes, so if those apply, I would test this solution for the performance. It does work for junction points.
The DllImport for it is
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool GetFileInformationByHandle(
IntPtr hFile,
out BY_HANDLE_FILE_INFORMATION lpFileInformation);
We also need a way to get suitable file handles to use. Since we're working with potential destinations, if we assume the destination directory already exists, we can use CreateFile()
to get a directory handle.
For CreateFile()
, the DllImport is
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern IntPtr CreateFile(
[MarshalAs(UnmanagedType.LPTStr)] string filename,
[MarshalAs(UnmanagedType.U4)] FileAccess access,
[MarshalAs(UnmanagedType.U4)] FileShare share,
IntPtr securityAttributes,
[MarshalAs(UnmanagedType.U4)] FileMode creationDisposition,
[MarshalAs(UnmanagedType.U4)] FileAttributes flagsAndAttributes,
IntPtr templateFile);
We'll also need CloseHandle()
,
[DllImport("kernel32.dll", SetLastError = true)]
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
[SuppressUnmanagedCodeSecurity]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool CloseHandle(IntPtr hObject);
With all those pieces in place, create a wrapper method that returns true if the volume serial number of the supplied paths match. It obviously makes sense throw more specific exceptions, but for the sake of brevity:
static bool SameVolume(string src, string dest)
{
IntPtr srcHandle = IntPtr.Zero, destHandle = IntPtr.Zero;
try
{
srcHandle = CreateFile(src, FileAccess.Read, FileShare.ReadWrite, IntPtr.Zero,
FileMode.Open, (FileAttributes) 0x02000000, IntPtr.Zero);
destHandle = CreateFile(dest, FileAccess.Read, FileShare.ReadWrite, IntPtr.Zero,
FileMode.Open, (FileAttributes) 0x02000000, IntPtr.Zero);
var srcInfo = new BY_HANDLE_FILE_INFORMATION();
var destInfo = new BY_HANDLE_FILE_INFORMATION();
if (!GetFileInformationByHandle(srcHandle, out srcInfo))
{
throw new Exception(Marshal.GetLastWin32Error().ToString());
}
if (!GetFileInformationByHandle(destHandle, out destInfo))
{
throw new Exception(Marshal.GetLastWin32Error().ToString());
}
return srcInfo.VolumeSerialNumber == destInfo.VolumeSerialNumber;
}
finally
{
if (srcHandle != IntPtr.Zero)
{
CloseHandle(srcHandle);
}
if (destHandle != IntPtr.Zero)
{
CloseHandle(destHandle);
}
}
}
Alternatively, you could just use
Task.Run(() => File.Move(x))
or another asynchronous construct to get this done without locking up the application's interface. I can't see anything wrong with that.
Upvotes: 1