Related
I want to start the run dialog (Windows+R) from Windows within my C# code.
I assume this can be done using explorer.exe but I'm not sure how.
Use RunFileDlg:
[DllImport("shell32.dll", EntryPoint = "#61", CharSet = CharSet.Unicode)]
public static extern int RunFileDlg(
[In] IntPtr hWnd,
[In] IntPtr icon,
[In] string path,
[In] string title,
[In] string prompt,
[In] uint flags);
private static void Main(string[] args)
{
// You might also want to add title, window handle...etc.
RunFileDlg(IntPtr.Zero, IntPtr.Zero, null, null, null, 0);
}
Possible values for flags:
RFF_NOBROWSE = 1; //Removes the browse button.
RFF_NODEFAULT = 2; // No default item selected.
RFF_CALCDIRECTORY = 4; // Calculates the working directory from the file name.
RFF_NOLABEL = 8; // Removes the edit box label.
RFF_NOSEPARATEMEM = 14; // Removes the Separate Memory Space check box (Windows NT only).
See also How to programmatically open Run c++?
The RunFileDlg API is unsupported and may be removed by Microsoft from future versions of Windows (I'll grant that MS's commitment to backwards compatibility and the fact that this API, though undocumented, appears to be fairly widely known makes this unlikely, but it's still a possibility).
The supported way to launch the run dialog is using the IShellDispatch::FileRun method.
In C#, you can access this method by going to Add Reference, select the COM tab, and select "Microsoft Shell Controls and Automation". After doing this you can launch the dialog as follows:
Shell32.Shell shell = new Shell32.Shell();
shell.FileRun();
Yes, the RunFileDlg API offers more customizability, but this has the advantage of being documented, supported, and therefore unlikely to break in the future.
Note that Shell32 must be run on an STA thread. If you get an exception in your code, add [STAThread] above your method declaration like this, for example:
[STAThread]
private static void OpenRun() {
//Shell32 code here
}
Any method calling a method that uses Shell32 should also be run on an STA thread.
Another method would be to emulate the Windows+R key combination.
using System.Runtime.InteropServices;
using System.Windows.Forms;
static class KeyboardSend
{
[DllImport("user32.dll")]
private static extern void keybd_event(byte bVk, byte bScan, int dwFlags, int dwExtraInfo);
private const int KEYEVENTF_EXTENDEDKEY = 1;
private const int KEYEVENTF_KEYUP = 2;
public static void KeyDown(Keys vKey)
{
keybd_event((byte)vKey, 0, KEYEVENTF_EXTENDEDKEY, 0);
}
public static void KeyUp(Keys vKey)
{
keybd_event((byte)vKey, 0, KEYEVENTF_EXTENDEDKEY | KEYEVENTF_KEYUP, 0);
}
}
and call:
KeyboardSend.KeyDown(Keys.LWin);
KeyboardSend.KeyDown(Keys.R);
KeyboardSend.KeyUp(Keys.R);
KeyboardSend.KeyUp(Keys.LWin);
I want to write a program that shows the files of another drive with hard links.
I want to keep both hardlinks consistent in filename and other things, so I have to get a function/method where I can list all current hard links of a file.
For example:
I have a file C:\file.txt and a second hard link to D:\file.txt.
Then I rename D:\file.txt to D:\file_new.txt.
I now want to be able to also rename the hardlink on the C drive as well.
So I need a function which returns for D:\file_new.txt that there are the following hardlinks:
C:\file.txt
D:\file_new.txt
then I can rename the hard link on C:\ also to get D:\file_new.txt
So I need to get all hard links of a physical file.
Or: All hard links of a file addressed with a hard link.
Hope somebody can help!
Edit:
Oliver noticed that hard links can't be used on different disks. thanks... So I extend the question to: What do I need? Junction Points? Symbolic Links? It should also work with files not only with folders!
the following code should work well (originally postet by Peter provost on PowerShell Code Repository):
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using Microsoft.Win32.SafeHandles;
using FILETIME = System.Runtime.InteropServices.ComTypes.FILETIME;
namespace HardLinkEnumerator
{
public static class Kernel32Api
{
[StructLayout(LayoutKind.Sequential)]
public struct BY_HANDLE_FILE_INFORMATION
{
public uint FileAttributes;
public FILETIME CreationTime;
public FILETIME LastAccessTime;
public FILETIME LastWriteTime;
public uint VolumeSerialNumber;
public uint FileSizeHigh;
public uint FileSizeLow;
public uint NumberOfLinks;
public uint FileIndexHigh;
public uint FileIndexLow;
}
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
static extern SafeFileHandle CreateFile(
string lpFileName,
[MarshalAs(UnmanagedType.U4)] FileAccess dwDesiredAccess,
[MarshalAs(UnmanagedType.U4)] FileShare dwShareMode,
IntPtr lpSecurityAttributes,
[MarshalAs(UnmanagedType.U4)] FileMode dwCreationDisposition,
[MarshalAs(UnmanagedType.U4)] FileAttributes dwFlagsAndAttributes,
IntPtr hTemplateFile);
[DllImport("kernel32.dll", SetLastError = true)]
static extern bool GetFileInformationByHandle(SafeFileHandle handle, out BY_HANDLE_FILE_INFORMATION lpFileInformation);
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
static extern bool CloseHandle(SafeHandle hObject);
[DllImport("kernel32.dll", SetLastError=true, CharSet=CharSet.Unicode)]
static extern IntPtr FindFirstFileNameW(
string lpFileName,
uint dwFlags,
ref uint stringLength,
StringBuilder fileName);
[DllImport("kernel32.dll", SetLastError = true, CharSet=CharSet.Unicode)]
static extern bool FindNextFileNameW(
IntPtr hFindStream,
ref uint stringLength,
StringBuilder fileName);
[DllImport("kernel32.dll", SetLastError = true)]
static extern bool FindClose(IntPtr fFindHandle);
[DllImport("kernel32.dll")]
static extern bool GetVolumePathName(string lpszFileName,
[Out] StringBuilder lpszVolumePathName, uint cchBufferLength);
[DllImport("shlwapi.dll", CharSet = CharSet.Auto)]
static extern bool PathAppend([In, Out] StringBuilder pszPath, string pszMore);
public static int GetFileLinkCount(string filepath)
{
int result = 0;
SafeFileHandle handle = CreateFile(filepath, FileAccess.Read, FileShare.Read, IntPtr.Zero, FileMode.Open, FileAttributes.Archive, IntPtr.Zero);
BY_HANDLE_FILE_INFORMATION fileInfo = new BY_HANDLE_FILE_INFORMATION();
if (GetFileInformationByHandle(handle, out fileInfo))
result = (int)fileInfo.NumberOfLinks;
CloseHandle(handle);
return result;
}
public static string[] GetFileSiblingHardLinks(string filepath)
{
List<string> result = new List<string>();
uint stringLength = 256;
StringBuilder sb = new StringBuilder(256);
GetVolumePathName(filepath, sb, stringLength);
string volume = sb.ToString();
sb.Length = 0; stringLength = 256;
IntPtr findHandle = FindFirstFileNameW(filepath, 0, ref stringLength, sb);
if (findHandle.ToInt32() != -1)
{
do
{
StringBuilder pathSb = new StringBuilder(volume, 256);
PathAppend(pathSb, sb.ToString());
result.Add(pathSb.ToString());
sb.Length = 0; stringLength = 256;
} while (FindNextFileNameW(findHandle, ref stringLength, sb));
FindClose(findHandle);
return result.ToArray();
}
return null;
}
}
}
Maybe i misunderstand your questions, but hardlinks can't go from one drive to another. They can only exist on a single drive.
Within the .Net framwork there is no support to get these informations. But the Win32 API can provide you with these informations.
Take a look at this article. It may help you.
Update
As far as i know it is not possible to do it between different drives. Junction Points are definitely not your friend cause it only works on foldes. But after reading this wikipedia article it seems that you can do it on Vista and Win7 with symbolic links. There is also a link to this shell extension which seems to cover everything you can do with these NTFS special features. Maybe with this you can check if your goal is reachable and maybe afterwards check the MSDN for the desired Win32 API function.
Note:
Hard links can only be files on the same volume, which contradicts the requirements of the question, which led to a follow-up question in the question body that the OP himself answered.
Given the title of the question, however, users who find this post by googling are most likely interest in a solution to the problem as stated in the title: given a file, how can I find all hard links to it (which are by definition all on the same volume).
The solution below is a streamlined and modernized adaptation of Marcel Nolte's helpful answer.
Its behavior and constraints are:
For a given input file, its array of hard links is returned, as full file paths, which includes the input file's path itself.
If the file has only one hard link (itself), or you specify a directory, only that file's / directory's full path is returned.
If the path refers to a volume that doesn't support hard links, or the path doesn't exist, null is returned.
NiKiZe notes that you cannot query hardlinks via a CIFS/SMB connection (network drive).
The following is a self-contained Windows console application that you should be able to compile and run as-is; the method of interest is HardLinkHelper.GetHardLinks():
using System;
using System.Text;
using System.Collections.Generic;
using System.Runtime.InteropServices;
namespace demo
{
public static class Program
{
public static void Main()
{
// Sample file that is known to have (one) hard link.
var file = Environment.ExpandEnvironmentVariables(#"%SYSTEMROOT%\explorer.exe");
foreach (var link in HardLinkHelper.GetHardLinks(file) ?? new string[] { "n/a" })
{
Console.WriteLine(link);
}
}
}
public static class HardLinkHelper
{
#region WinAPI P/Invoke declarations
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
static extern IntPtr FindFirstFileNameW(string lpFileName, uint dwFlags, ref uint StringLength, StringBuilder LinkName);
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
static extern bool FindNextFileNameW(IntPtr hFindStream, ref uint StringLength, StringBuilder LinkName);
[DllImport("kernel32.dll", SetLastError = true)]
static extern bool FindClose(IntPtr hFindFile);
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
static extern bool GetVolumePathName(string lpszFileName, [Out] StringBuilder lpszVolumePathName, uint cchBufferLength);
public static readonly IntPtr INVALID_HANDLE_VALUE = (IntPtr)(-1); // 0xffffffff;
public const int MAX_PATH = 65535; // Max. NTFS path length.
#endregion
/// <summary>
//// Returns the enumeration of hardlinks for the given *file* as full file paths, which includes
/// the input path itself.
/// </summary>
/// <remarks>
/// If the file has only one hardlink (itself), or you specify a directory, only that
/// file's / directory's full path is returned.
/// If the path refers to a volume that doesn't support hardlinks, or the path
/// doesn't exist, null is returned.
/// </remarks>
public static string[] GetHardLinks(string filepath)
{
StringBuilder sbPath = new StringBuilder(MAX_PATH);
uint charCount = (uint)sbPath.Capacity; // in/out character-count variable for the WinAPI calls.
// Get the volume (drive) part of the target file's full path (e.g., #"C:\")
GetVolumePathName(filepath, sbPath, (uint)sbPath.Capacity);
string volume = sbPath.ToString();
// Trim the trailing "\" from the volume path, to enable simple concatenation
// with the volume-relative paths returned by the FindFirstFileNameW() and FindFirstFileNameW() functions,
// which have a leading "\"
volume = volume.Substring(0, volume.Length - 1);
// Loop over and collect all hard links as their full paths.
IntPtr findHandle;
if (INVALID_HANDLE_VALUE == (findHandle = FindFirstFileNameW(filepath, 0, ref charCount, sbPath))) return null;
List<string> links = new List<string>();
do
{
links.Add(volume + sbPath.ToString()); // Add the full path to the result list.
charCount = (uint)sbPath.Capacity; // Prepare for the next FindNextFileNameW() call.
} while (FindNextFileNameW(findHandle, ref charCount, sbPath));
FindClose(findHandle);
return links.ToArray();
}
}
}
I found a solution:
First I don't have to use hard links (since they can't point to an other disk). I have to use symbolic links instead. So I have one hard linked file on the original disk and symbolic links on other disks to this file. The limitation is OS must be Vista or newer.
Second I have to be able to find out where the symbolic link is pointing to.
Here I found a good example how to find out the information I need:
http://www.codeproject.com/KB/vista/ReparsePointID.aspx
The only thing I don't managed is to find all symbolic links from a specific file (hard link). I guess there is no out of the box solution and I have to recurse all symbolic links and test the target. But in my case that's no problem.
I hope that can help others!
I used the HardLinkHelper class in a project, and it works great for finding hard links on Windows NTFS drives.
Here's my version of HardLinkHelper class with the following changes:
Does not use StringBuilder, because Microsoft recommends to avoid using StringBuilder on pinvokes.
It has the member variables (INVALID_HANDLE_VALUE & MAX_PATH) private.
Null is never returned, and instead empty list is returned for non-existing path or unsupported path. This allows safe use of foreach on return value.
Added ReturnEmptyListIfOnlyOne input variable which when set to true, allows calling function to use it in a foreach, where the foreach loop will only be entered if the file has multiple shared hard links.
Example usage:
foreach (var link in HardLinkHelper.GetHardLinks(entry.Path, true))
public static class HardLinkHelper {
#region WinAPI P/Invoke declarations
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
static extern IntPtr FindFirstFileNameW(string lpFileName, uint dwFlags, ref uint StringLength, char[] LinkName);
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
static extern bool FindNextFileNameW(IntPtr hFindStream, ref uint StringLength, char[] LinkName);
[DllImport("kernel32.dll", SetLastError = true)]
static extern bool FindClose(IntPtr hFindFile);
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
static extern bool GetVolumePathName(string lpszFileName, [Out] char[] lpszVolumePathName, uint cchBufferLength);
private static readonly IntPtr INVALID_HANDLE_VALUE = (IntPtr)(-1); // 0xffffffff;
private const int MAX_PATH = 65535; // Max. NTFS path length.
#endregion
/// <summary>
/// Checks for hard links on a Windows NTFS drive associated with the given path.
/// </summary>
/// <param name="filepath">Fully qualified path of the file to check for shared hard links</param>
/// <param name="ReturnEmptyListIfOnlyOne">Set true, to return populated list only for files having multiple hard links</param>
/// <returns>
/// Empty list is returned for non-existing path or unsupported path.
/// Single hard link paths returns empty list if ReturnEmptyListIfOnlyOne is true. If false, returns single item list.
/// For multiple shared hard links, returns list of all the shared hard links.
/// </returns>
public static List<string> GetHardLinks(string filepath, bool ReturnEmptyListIfOnlyOne = false) {
List<string> links = new List<string>();
try {
Char[] sbPath = new Char[MAX_PATH + 1];
uint charCount = (uint)MAX_PATH;
GetVolumePathName(filepath, sbPath, (uint)MAX_PATH); // Must use GetVolumePathName, because Path.GetPathRoot fails on a mounted drive on an empty folder.
string volume = new string(sbPath).Trim('\0');
volume = volume.Substring(0, volume.Length - 1);
Array.Clear(sbPath, 0, MAX_PATH); // Reset the array because these API's can leave garbage at the end of the buffer.
IntPtr findHandle;
if (INVALID_HANDLE_VALUE != (findHandle = FindFirstFileNameW(filepath, 0, ref charCount, sbPath))) {
do {
links.Add((volume + new string(sbPath)).Trim('\0')); // Add the full path to the result list.
charCount = (uint)MAX_PATH;
Array.Clear(sbPath, 0, MAX_PATH);
} while (FindNextFileNameW(findHandle, ref charCount, sbPath));
FindClose(findHandle);
}
}
catch (Exception ex) {
//Logger.Instance.Info($"GetHardLinks: Exception, file: {filepath}, reason: {ex.Message}, stacktrace {ex.StackTrace}");
}
if (ReturnEmptyListIfOnlyOne && links.Count < 2)
links.Clear();
return links;
}
}
try:
using System.IO;
string[] filePathsC = Directory.GetFiles(#"c:\");
string[] filePathsD = Directory.GetFiles(#"d:\");
and loop through the arrays, find the files and change the name
EDIT:
By reading the comments I know that I answered before I knew what a hardlink is. I realise now that this answer is not helping.
Using c#, how do I copy a symbolic link in Windows Vista,7,2008.
When I use File.Copy to copy a symlink, its target gets copied.
I wish to mimic the behavior one gets when you use xcopy with the /B option.
Is this possible using .NET 3.5?
You can use the Win32 CopyFileEx function to do this. It took a bit of effort, but here is the whole CopyFileEx helper class (C# 3.0 and .NET 3.5 Client Profile compatible and tested!). You can also reuse it for any other CopyFileEx tasks that you have:
using System;
using System.ComponentModel;
using System.Runtime.InteropServices;
public static class CopyHelper
{
[Flags]
public enum CopyFileFlags : uint
{
COPY_FILE_FAIL_IF_EXISTS = 0x00000001,
COPY_FILE_RESTARTABLE = 0x00000002,
COPY_FILE_OPEN_SOURCE_FOR_WRITE = 0x00000004,
COPY_FILE_ALLOW_DECRYPTED_DESTINATION = 0x00000008,
COPY_FILE_COPY_SYMLINK = 0x00000800 //NT 6.0+
}
public enum CopyProgressResult : uint
{
PROGRESS_CONTINUE = 0,
PROGRESS_CANCEL = 1,
PROGRESS_STOP = 2,
PROGRESS_QUIET = 3
}
public enum CopyProgressCallbackReason : uint
{
CALLBACK_CHUNK_FINISHED = 0x00000000,
CALLBACK_STREAM_SWITCH = 0x00000001
}
public delegate CopyProgressResult CopyProgressRoutine(
long TotalFileSize,
long TotalBytesTransferred,
long StreamSize,
long StreamBytesTransferred,
uint dwStreamNumber,
CopyProgressCallbackReason dwCallbackReason,
IntPtr hSourceFile,
IntPtr hDestinationFile,
IntPtr lpData);
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool CopyFileEx(string lpExistingFileName,
string lpNewFileName, CopyProgressRoutine lpProgressRoutine,
IntPtr lpData, ref bool pbCancel, CopyFileFlags dwCopyFlags);
}
Here is some sample code that shows how to use it to copy a symbolic link (and not the file it refers to):
string srcLink = #"c:\l.txt"; // Sample source soft link
string destFile = #"d:\l.txt"; // Sample destination soft link
bool bCancel = false;
bool bSuccess = CopyHelper.CopyFileEx(srcLink, destFile,
null, IntPtr.Zero, ref bCancel,
CopyHelper.CopyFileFlags.COPY_FILE_COPY_SYMLINK);
if (!bSuccess)
{
int error = Marshal.GetLastWin32Error();
throw new Win32Exception(error);
}
There is no API for links (neither hardlinks, softlinks or symbolic links) in the .NET framework.
You must either call mklink.exe with Process.Start and create the link you want,
or you have a look for a third party library which is able to do such things.
You could make use of pinvoke and call CopyFileEx. Note the COPY_FILE_COPY_SYMLINK which is what you are looking for.
I recognize this is a fairly old thread, but I've just finished some work in C code (yes, really) that supports this. Posting because I searched all over for this information, and it was not easy to find.
Sadly CopyFileEx with COPY_FILE_SYMLINK does not do the trick. That simply creates a 0-size file in the target directory with the correct name, but no symbolic link. Another old article contains a solution you could try to implement: How to copy directory symbolic link as a link to the target?.
I do think the problem with CopyFileEx is a bug, one that still exists in Windows 10.
I'm not very good with P/Invoke. Can anyone tell me how to declare and use the following shell32.dll function in .NET?
From http://msdn.microsoft.com/en-us/library/bb762230%28VS.85%29.aspx:
HRESULT SHMultiFileProperties(
IDataObject *pdtobj,
DWORD dwFlags
);
It is for displaying the Windows Shell Properties dialog for multiple file system objects.
I already figured out how to use SHObjectProperties for one file or folder:
[DllImport("shell32.dll", SetLastError = true)]
static extern bool SHObjectProperties(uint hwnd, uint shopObjectType, [MarshalAs(UnmanagedType.LPWStr)] string pszObjectName, [MarshalAs(UnmanagedType.LPWStr)] string pszPropertyPage);
public static void ShowDialog(Form parent, FileSystemInfo selected)
{
SHObjectProperties((uint)parent.Handle, (uint)ObjectType.File, selected.FullName, null));
}
enum ObjectType
{
Printer = 0x01,
File = 0x02,
VoumeGuid = 0x04,
}
Can anyone help?
There's an IDataObject interface and a DataObject class in the .NET Framework.
[DllImport("shell32.dll", SetLastError = true)]
static extern int SHMultiFileProperties(IDataObject pdtobj, int flags);
public static void Foo()
{
var pdtobj = new DataObject();
pdtobj.SetFileDropList(new StringCollection { #"C:\Users", #"C:\Windows" });
if (SHMultiFileProperties(pdtobj, 0) != 0 /*S_OK*/)
{
throw new Win32Exception();
}
}
EDIT: I've just compiled and tested this and it works (pops up some dialog with folder appearance settings).
I maybe reading you question incorrectly, but I think you are looking for the extended file properties for files. i.e. opening windows explorer and viewing columns like attributes, owner, copyright, size, date created etc?
There is an API in Shell32 called GetDetailsOf that will provide this information. A starting article on codeproject
Cheers,
John
I need to be able to move an entire directory in a single atomic operation, guaranteeing that nothing else on the system will be able to subvert the operation by creating new files after I start, having a lock on a file, etc.
Presumably, I would use System.IO.Directory.Move() if the directories were on the same volume (if Directory.GetDirectoryRoot() is the same), otherwise I'd have to create a new target directory on the other volume and recursively copy/move all the directories and files underneath.
Nothing I've read shows how to gain an exclusive lock to an entire directory leaf in .NET so this can be done safely. Is there a recommended/supported way to do this?
Vista does support transactions in NTFS volumes:
http://msdn.microsoft.com/en-us/magazine/cc163388.aspx
Could you work around this by renaming the "root" directory temporarily (creating a directory with the same name immediately thereafter so that anyone accessing that directory doesn't encounter an error), then work on the files in the renamed directory?
I remember being able to do this at the DOS level by simply renaming the directory. There was a move command, which also seemed to work. But it makes sense. You're not really moving all of the files in the directory, you're just changing the meta data in the directory structure itself. I also remember this from hacking directory structures directly using a disk editor on my fathers Zenith Data Systems 8088. I could make directories invisible by changing the attribute bits on disk, even hiding ".." and "." and making subdirectories appear to be root (the parent directories were invisible). Hope this works for you. I haven't revisited this in hmmm too many years to count ;-). May it work for you.
By the way, you should not have to lock anything because if you're just renaming, it happens really fast, and it's just a single operation.
You can use the Transactional NTFS via PInvoke. Note that it's unclear if it works properly across different volumes, please see the documentation. You may need to use Distributed Transactions, which is significantly more complicated. It will only work on NTFS volumes, not FAT.
Caveat: this code is entirely untested.
[DllImport("Kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode, BestFitMapping = false)]
bool GetVolumeInformationW(
[In, MarshalAs(UnmanagedType.LPWStr)]
string lpRootPathName,
IntPtr lpVolumeNameBuffer,
int nVolumeNameSize,
out int lpVolumeSerialNumber,
out int lpMaximumComponentLength,
out int lpFileSystemFlags,
IntPtr lpFileSystemNameBuffer,
int nFileSystemNameSize
);
[DllImport("KtmW32.dll", SetLastError = true, CharSet = CharSet.Unicode, BestFitMapping = false)]
private static extern SafeFileHandle CreateTransaction(
IntPtr lpTransactionAttributes,
IntPtr UOW,
int CreateOptions,
int IsolationLevel,
int IsolationFlags,
int Timeout,
[In, MarshalAs(UnmanagedType.LPWStr)]
string Description
);
[DllImport("KtmW32.dll", SetLastError = true, BestFitMapping = false)]
private static extern bool CommitTransaction(SafeFileHandle hTransaction);
public enum ProgressResponse
{
PROGRESS_CONTINUE, // Continue the copy operation.
PROGRESS_CANCEL, // Cancel the copy operation and delete the destination file.
PROGRESS_STOP, // Stop the copy operation. It can be restarted at a later time.
PROGRESS_QUIET, // Continue the copy operation, but stop invoking CopyProgressRoutine to report progress.
}
public delegate ProgressResponse ProgressRoutine(
long TotalFileSize,
long TotalBytesTransferred,
long StreamSize,
long StreamBytesTransferred,
int dwStreamNumber,
int dwCallbackReason,
IntPtr hSourceFile,
IntPtr hDestinationFile,
IntPtr lpData
);
[DllImport("Kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode, BestFitMapping = false)]
private static extern bool MoveFileTransactedW(
[In, MarshalAs(UnmanagedType.LPWStr)]
string lpExistingFileName,
[In, MarshalAs(UnmanagedType.LPWStr)]
string lpNewFileName,
ProgressRoutine lpProgressRoutine,
IntPtr lpData,
int dwFlags,
SafeFileHandle hTransaction
);
private static bool CheckSupportsTransactions(string filePath)
{
const int FILE_SUPPORTS_TRANSACTIONS = 0x00200000;
if(!GetVolumeInformationW(
Path.GetPathRoot(sourceFullPath),
IntPtr.Zero, 0,
out var _,
out var _,
out var flags,
IntPtr.Zero, 0)
throw new Win32Exception(Marshal.GetLastWin32Error());
return flags & FILE_SUPPORTS_TRANSACTIONS != 0;
}
public static void MoveDirectoryTransacted(string sourceFullPath, string destFullPath, ProgressRoutine progress = null)
{
const int MOVEFILE_COPY_ALLOWED = 0x2;
const int ERROR_REQUEST_ABORTED = 0x4D3;
sourceFullPath = Path.GetFullPath(sourceFullPath);
destFullPath = Path.GetFullPath(destFullPath);
if(!CheckSupportsTransactions(sourceFullPath) ||
!CheckSupportsTransactions(destFullPath))
{
throw new InvalidOperationException("Volume does not support transactions");
}
using (var tran = CreateTransaction(IntPtr.Zero, IntPtr.Zero, 0, 0, 0, 0, null))
{
if (tran.IsInvalid)
{
throw new Win32Exception(Marshal.GetLastWin32Error());
}
if (!MoveFileTransactedW(
sourceFullPath,
destFullPath,
progress,
IntPtr.Zero,
MOVEFILE_COPY_ALLOWED,
tran))
{
var error = Marshal.GetLastWin32Error();
if (error == ERROR_REQUEST_ABORTED)
throw new OperationCanceledException();
throw new Win32Exception(error);
}
if (!CommitTransaction(tran))
{
throw new Win32Exception(Marshal.GetLastWin32Error());
}
}
}
If you have a CancellationToken, you could invoke it like this
MoveDirectoryTransacted("sourcePath", "destPath",
() => cancelToken.IsCancellationRequested ? ProgressResponse.PROGRESS_CANCEL : ProgressResponse.PROGRESS_CONTINUE);
If you have the ability to run your copy process as "service account" that is only used by the copy process, you could set the permissions of the folder to only allow that account to work with it. Then reset the permissions back to what they were after the copy process finished.
For example, something like the following:
using System;
using System.IO;
using System.Security.AccessControl;
using System.Security.Principal;
namespace ExclusiveLockFileCopy
{
public class ExclusiveLockMover
{
public DirectorySecurity LockFolder(DirectoryInfo di)
{
var originalSecurity = di.GetAccessControl(System.Security.AccessControl.AccessControlSections.All);
//make sure inherted permissions will come back when UnlockFolder is called
originalSecurity.SetAccessRuleProtection(true, true);
var tmpSecurity = di.GetAccessControl(System.Security.AccessControl.AccessControlSections.All);
// remove all rules
var currentRules = tmpSecurity.GetAccessRules(true, true, typeof(System.Security.Principal.NTAccount));
foreach (AccessRule rule in currentRules)
{
tmpSecurity.PurgeAccessRules(rule.IdentityReference);
tmpSecurity.ModifyAccessRule(AccessControlModification.RemoveAll, rule, out var tmpModified);
Console.WriteLine($"Removed access for {rule.IdentityReference.Value}");
}
//add back the current process' identity after the for loop - don't assume the account will show up in the current rule list (i.e. inherited access)
var _me = WindowsIdentity.GetCurrent();
var _meNT = new NTAccount(_me.Name);
tmpSecurity.AddAccessRule(new FileSystemAccessRule(_meNT, FileSystemRights.FullControl, AccessControlType.Allow));
Console.WriteLine($"Ensuring {_meNT.Value} maintains full access");
//strip out inherited permissions
tmpSecurity.SetAccessRuleProtection(true, false);
di.SetAccessControl(tmpSecurity);
//send back the original security incase it is needed later for "unlocking"
return originalSecurity;
}
public void UnlockFolder(DirectoryInfo di, DirectorySecurity originalSecurity)
=> di.SetAccessControl(originalSecurity);
public void CopyFolderExclusive(string srcFolder, string dstFolder)
{
DirectorySecurity diSourceOriginalSecurity = null;
DirectorySecurity diDestinationOriginalSecurity = null;
var diSource = new DirectoryInfo(srcFolder);
var diDestination = new DirectoryInfo(dstFolder);
try
{
diSourceOriginalSecurity = LockFolder(diSource);
if (!diDestination.Exists)
diDestination.Create();
diDestinationOriginalSecurity = LockFolder(diDestination);
// perform your folder/file copy here //
}
catch (Exception ex)
{
Console.WriteLine(ex.ToString());
}
finally
{
if (diSourceOriginalSecurity != null)
UnlockFolder(diSource, diSourceOriginalSecurity);
if (diDestinationOriginalSecurity != null)
UnlockFolder(diDestination, diDestinationOriginalSecurity);
}
}
}
}
I'd say what you really need is a transactional file system... which NTFS ain't, and while there have been MS plans for such, it was cut from Longhorn before it became Vista (and from Cairo before that).
You could try to gain exclusive locks on every file in the directory before the move, and do the moving with explicit file reading/writing, but recursively? I'm not so sure that's a good idea... and besides, that won't protect against new files being added.
What are you really trying to do? Why are you worried about concurrent activity?