#region Disclaimer / License // Copyright (C) 2011, Kenneth Skovhede // http://www.hexad.dk, opensource@hexad.dk // // This library is free software; you can redistribute it and/or // modify it under the terms of the GNU Lesser General Public // License as published by the Free Software Foundation; either // version 2.1 of the License, or (at your option) any later version. // // This library is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // Lesser General Public License for more details. // // You should have received a copy of the GNU Lesser General Public // License along with this library; if not, write to the Free Software // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA // #endregion using System; using System.Collections.Generic; using System.Text; using Microsoft.Win32.SafeHandles; using System.Runtime.InteropServices; using System.ComponentModel; namespace Duplicati.Library.Snapshots { /// /// Class that encapsulates USN access to a single volume /// public class USNHelper : IDisposable { /// /// The path this USN points to /// private string m_path; /// /// The safe filehandle /// private SafeFileHandle m_volumeHandle; /// /// The current USN journal from the device /// private Win32USN.USN_JOURNAL_DATA m_journal; /// /// The FileNameReferenceNumber for the root folder /// private ulong m_volumeRootFileNameReferenceNumber; /// /// This is a cache of all the filesystem entries found on the drive /// private List> m_entryList; /// /// Constructs a new USN helper instance /// /// The path to the folder to perform USN services public USNHelper(string path) : this(path, null) { } /// /// Constructs a new USN helper instance /// /// The path to the folder to perform USN services /// The root volume where the USN lookup is performed internal USNHelper(string path, string volumeRoot) { if (Utility.Utility.IsClientLinux) throw new Exception(Strings.USNHelper.LinuxNotSupportedError); if (!System.IO.Path.IsPathRooted(path)) throw new Exception(string.Format("Path {0} is not rooted", path)); m_path = Utility.Utility.AppendDirSeparator(path); try { string devicename = @"\\.\" + System.IO.Path.GetPathRoot(path).TrimEnd('\\'); if (volumeRoot != null) volumeRoot = volumeRoot.TrimEnd('\\'); m_volumeHandle = Win32USN.CreateFile(volumeRoot == null ? devicename : volumeRoot, Win32USN.EFileAccess.GenericRead, Win32USN.EFileShare.ReadWrite, IntPtr.Zero, Win32USN.ECreationDisposition.OpenExisting, Win32USN.EFileAttributes.BackupSemantics, IntPtr.Zero); if (m_volumeHandle == null || m_volumeHandle.IsInvalid) throw new Win32Exception(Marshal.GetLastWin32Error()); uint bytesReturned = 0; if (!Win32USN.DeviceIoControl(m_volumeHandle, Win32USN.EIOControlCode.FsctlQueryUsnJournal, null, 0, out m_journal, (uint)Marshal.SizeOf(typeof(Win32USN.USN_JOURNAL_DATA)), ref bytesReturned, IntPtr.Zero)) throw new Win32Exception(Marshal.GetLastWin32Error()); Win32USN.BY_HANDLE_FILE_INFORMATION fileInfo; using (SafeFileHandle driveHandle = Win32USN.CreateFile(System.IO.Path.GetPathRoot(path), Win32USN.EFileAccess.GenericRead, Win32USN.EFileShare.ReadWrite, IntPtr.Zero, Win32USN.ECreationDisposition.OpenExisting, Win32USN.EFileAttributes.BackupSemantics, IntPtr.Zero)) if (!Win32USN.GetFileInformationByHandle(driveHandle, out fileInfo)) throw new Win32Exception(Marshal.GetLastWin32Error()); m_volumeRootFileNameReferenceNumber = ((ulong)fileInfo.FileIndexHigh << 32) | ((ulong)fileInfo.FileIndexLow); } catch { if (m_volumeHandle != null) { m_volumeHandle.Dispose(); m_volumeHandle = null; } throw; } if (this.FileSystemEntries.Count == 0) throw new Exception(Strings.USNHelper.SafeGuardError); } /// /// Helper function to support the Duplicati enumeration callback system /// /// The root path to look in and use as filter base /// The callback function that collects the output public void EnumerateFilesAndFolders(string rootpath, Duplicati.Library.Utility.Utility.EnumerationFilterDelegate callback) { //Under normal enumeration, the filter will prevent visiting subfolders for excluded folders //But when using USN all files/folders are present in the list, so we have to maintain // a list of subfolders that are excluded from the set System.Text.StringBuilder local_filter = new StringBuilder(); System.Text.RegularExpressions.Regex excludedFolders = null; foreach (KeyValuePair r in this.Records) if (r.Key.StartsWith(rootpath, Utility.Utility.ClientFilenameStringComparision)) { bool isFolder = (r.Value.FileAttributes & Win32USN.EFileAttributes.Directory) != 0; if (excludedFolders != null && excludedFolders.Match(r.Key).Success) continue; if (isFolder) { if (!callback(rootpath, r.Key, (System.IO.FileAttributes)((int)r.Value.FileAttributes & 0xff))) { if (local_filter.Length != 0) local_filter.Append("|"); local_filter.Append("("); local_filter.Append(Utility.Utility.ConvertGlobbingToRegExp(r.Key + "*")); local_filter.Append(")"); if (Utility.Utility.IsFSCaseSensitive) excludedFolders = new System.Text.RegularExpressions.Regex(local_filter.ToString()); else excludedFolders = new System.Text.RegularExpressions.Regex(local_filter.ToString(), System.Text.RegularExpressions.RegexOptions.IgnoreCase); } } else { if (local_filter.Length == 0 || !excludedFolders.IsMatch(r.Key)) callback(rootpath, r.Key, (System.IO.FileAttributes)((int)r.Value.FileAttributes & 0xff)); } } } /// /// Gets the USN JournalID for the current volume /// public long JournalID { get { return m_journal.UsnJournalID; } } /// /// Gets the next USN number for the volume /// public long USN { get { return m_journal.NextUsn; } } /// /// Internal property to access the cached version of the file entry list /// private List> Records { get { if (m_entryList == null) m_entryList = BuildUSNTable(0); return m_entryList; } } /// /// Returns a list of files or folders that have changed since the recorded USN /// /// The folder to find entries for /// The last known USN /// A list of changed files and folders public List GetChangedFileSystemEntries(string sourceFolder, long lastUsn) { List result = new List(); foreach (KeyValuePair r in this.Records) if (r.Value.Usn >= lastUsn && r.Key.StartsWith(sourceFolder, Utility.Utility.ClientFilenameStringComparision)) result.Add(r.Key); return result; } public string GetChangeFlags(string entry) { foreach (KeyValuePair r in this.Records) if (r.Key.Equals(entry, Utility.Utility.ClientFilenameStringComparision)) return r.Value.Reason.ToString(); return ""; } public List GetRenamedFileSystemEntries(string sourceFolder, long lastUsn) { List result = new List(); foreach (KeyValuePair r in this.Records) if (r.Value.Usn >= lastUsn && ((r.Value.Reason & Win32USN.USNReason.USN_REASON_RENAME_NEW_NAME) != 0) && r.Key.StartsWith(sourceFolder, Utility.Utility.ClientFilenameStringComparision)) result.Add(r.Key); return result; } /// /// Returns a list of all files and folders in a given subfolder /// /// The folder to enumerat /// A list of folders public List GetFileSystemEntries(string sourceFolder) { List result = new List(); foreach (KeyValuePair r in this.Records) if (r.Key.StartsWith(sourceFolder, Utility.Utility.ClientFilenameStringComparision)) result.Add(r.Key); return result; } /// /// Returns a list of files and folders found in the folder /// public List FileSystemEntries { get { List result = new List(); foreach (KeyValuePair r in this.Records) if (r.Key.StartsWith(m_path, Utility.Utility.ClientFilenameStringComparision)) result.Add(r.Key); return result; } } /// /// Returns a list of files found in the folder /// public List Files { get { List result = new List(); foreach (KeyValuePair r in this.Records) if (r.Key.StartsWith(m_path, Utility.Utility.ClientFilenameStringComparision) && (r.Value.FileAttributes & Win32USN.EFileAttributes.Directory) == 0) result.Add(r.Key); return result; } } /// /// Returns a list of folders found in the folder /// public List Folders { get { List result = new List(); foreach (KeyValuePair r in this.Records) if (r.Key.StartsWith(m_path, Utility.Utility.ClientFilenameStringComparision) && (r.Value.FileAttributes & Win32USN.EFileAttributes.Directory) != 0) result.Add(r.Key); return result; } } /// /// Cached copy of the USN_RECORD size /// static readonly int USN_RECORD_SIZE = Marshal.SizeOf(typeof(Win32USN.USN_RECORD)); /// /// Helper function to work with unmanaged memory /// /// The number of bytes in buffer /// The pointer to the start of the buffer /// The list of records to update with results /// A flag indicating if the end of the list is reached /// The next USN or FRN number, depending on what pointer was passed private long ExtractUsnEntries(uint bytesRead, IntPtr startOffset, List> records, out bool moreEntries) { int bytesLeft = (int)bytesRead; IntPtr curPtr = startOffset; if (bytesLeft < 8) throw new Exception(Strings.USNHelper.EmptyResponseError); long nextUsn = Marshal.ReadInt64(curPtr); curPtr = new IntPtr(curPtr.ToInt64() + 8); bytesLeft -= 8; moreEntries = (bytesLeft > USN_RECORD_SIZE); while (bytesLeft > USN_RECORD_SIZE) { Win32USN.USN_RECORD r = (Win32USN.USN_RECORD)Marshal.PtrToStructure(curPtr, typeof(Win32USN.USN_RECORD)); //According to MSDN, apps should not handle data less than 2.0, http://msdn.microsoft.com/en-us/library/aa365722.aspx if (r.MajorVersion >= 2) { string filename = Marshal.PtrToStringUni(new IntPtr(curPtr.ToInt64() + r.FileNameOffset), r.FileNameLength / sizeof(char)); if (r.Usn > m_journal.NextUsn) { moreEntries = false; break; } records.Add(new KeyValuePair(filename, r)); } bytesLeft -= (int)r.RecordLength; curPtr = new IntPtr(curPtr.ToInt64() + r.RecordLength); } return nextUsn; } /// /// Returns a list of all files and folders changed since the USN /// /// The USN number to start the list from, set to zero to get all /// A list of files and folders changed since the USN private List> BuildUSNTable(long startUsn) { const int ALLOCATED_MEMORY = 64 * 1024; IntPtr allocatedMemory = IntPtr.Zero; List> records = new List>(); try { uint bytesRead = 0; bool more = true; allocatedMemory = Marshal.AllocHGlobal(ALLOCATED_MEMORY); Win32USN.MFT_ENUM_DATA startParams = new Win32USN.MFT_ENUM_DATA(); startParams.StartFileReferenceNumber = 0; startParams.LowUsn = Math.Max(startUsn, m_journal.LowestValidUsn); startParams.HighUsn = m_journal.NextUsn; while (more) { if (!Win32USN.DeviceIoControl(m_volumeHandle, Win32USN.EIOControlCode.FsctlEnumUsnData, ref startParams, (uint)Marshal.SizeOf(typeof(Win32USN.MFT_ENUM_DATA)), allocatedMemory, ALLOCATED_MEMORY, ref bytesRead, IntPtr.Zero)) { int errorCode = Marshal.GetLastWin32Error(); //If we get no error or EOF the enumeration is completed if (errorCode == Win32USN.ERROR_HANDLE_EOF || errorCode == Win32USN.ERROR_SUCCESS) break; else throw new Win32Exception(errorCode); } startParams.StartFileReferenceNumber = (ulong)ExtractUsnEntries(bytesRead, allocatedMemory, records, out more); } return ParseRecordList(records); } finally { if (allocatedMemory != IntPtr.Zero) { Marshal.FreeHGlobal(allocatedMemory); allocatedMemory = IntPtr.Zero; } } } /// /// Calculates the full path of each entry in the USN table /// /// The list of records with local names /// A list of USN entries with full path private List> ParseRecordList(List> records) { string rootDir = System.IO.Path.GetPathRoot(m_path); List> result = new List>(); Dictionary folderLookup = new Dictionary(); for (int i = 0; i < records.Count; i++) if ((records[i].Value.FileAttributes & Win32USN.EFileAttributes.Directory) != 0) folderLookup.Add(records[i].Value.FileReferenceNumber, i); //Loop through each record foreach (KeyValuePair r in records) { //Build the path, starting at the file/folder entry List items = new List(); items.Add(r.Key + (((r.Value.FileAttributes & Win32USN.EFileAttributes.Directory) == 0) ? "" : "\\")); KeyValuePair cur = r; //Walk back up the chain as far as we can go while (folderLookup.ContainsKey(cur.Value.ParentFileReferenceNumber)) { cur = records[folderLookup[cur.Value.ParentFileReferenceNumber]]; items.Insert(0, cur.Key); } //If the parent entry is indeed the drive, record this entry if (cur.Value.ParentFileReferenceNumber == m_volumeRootFileNameReferenceNumber) result.Add(new KeyValuePair(System.IO.Path.Combine(rootDir, string.Join(System.IO.Path.DirectorySeparatorChar.ToString(), items.ToArray())), r.Value)); } return result; } /// /// Unused internal function that can be used to read all USN records /// /// The USN number to start from private void GetChangedItems(long lastUsn) { const int ALLOCATED_MEMORY = 64 * 1024; IntPtr allocatedMemory = IntPtr.Zero; List> records = new List>(); try { uint bytesRead = 0; bool more = true; allocatedMemory = Marshal.AllocHGlobal(ALLOCATED_MEMORY); Win32USN.READ_USN_JOURNAL_DATA startParams = new Win32USN.READ_USN_JOURNAL_DATA(); startParams.UsnJournalID = m_journal.UsnJournalID; startParams.StartUsn = lastUsn; startParams.ReasonMask = Win32USN.USNReason.USN_REASON_ANY; startParams.ReturnOnlyOnClose = 0; startParams.Timeout = 0; startParams.BytesToWaitFor = 0; while (more) { if (!Win32USN.DeviceIoControl(m_volumeHandle, Win32USN.EIOControlCode.FsctlReadUsnJournal, startParams, (uint)Marshal.SizeOf(typeof(Win32USN.READ_USN_JOURNAL_DATA)), allocatedMemory, ALLOCATED_MEMORY, ref bytesRead, IntPtr.Zero)) { int errorCode = Marshal.GetLastWin32Error(); //If we get no error or EOF the enumeration is completed if (errorCode == Win32USN.ERROR_HANDLE_EOF || errorCode == Win32USN.ERROR_SUCCESS) break; else throw new Win32Exception(errorCode); } startParams.StartUsn = ExtractUsnEntries(bytesRead, allocatedMemory, records, out more); } //Records now contains all Usn entries } finally { if (allocatedMemory != IntPtr.Zero) { Marshal.FreeHGlobal(allocatedMemory); allocatedMemory = IntPtr.Zero; } } } #region IDisposable Members /// /// Cleans up any resources held, including the volume handle /// public void Dispose() { if (m_volumeHandle != null) { m_volumeHandle.Dispose(); m_volumeHandle = null; } } #endregion } }