Files
duplicati/Duplicati/Library/Snapshots/USNHelper.cs
T

519 lines
22 KiB
C#

#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
{
/// <summary>
/// Class that encapsulates USN access to a single volume
/// </summary>
public class USNHelper : IDisposable
{
/// <summary>
/// The path this USN points to
/// </summary>
private string m_path;
/// <summary>
/// The safe filehandle
/// </summary>
private SafeFileHandle m_volumeHandle;
/// <summary>
/// The current USN journal from the device
/// </summary>
private Win32USN.USN_JOURNAL_DATA m_journal;
/// <summary>
/// The FileNameReferenceNumber for the root folder
/// </summary>
private ulong m_volumeRootFileNameReferenceNumber;
/// <summary>
/// This is a cache of all the filesystem entries found on the drive
/// </summary>
private List<KeyValuePair<string, Win32USN.USN_RECORD>> m_entryList;
/// <summary>
/// Constructs a new USN helper instance
/// </summary>
/// <param name="path">The path to the folder to perform USN services</param>
public USNHelper(string path)
: this(path, null)
{
}
/// <summary>
/// Constructs a new USN helper instance
/// </summary>
/// <param name="path">The path to the folder to perform USN services</param>
/// <param name="volumeRoot">The root volume where the USN lookup is performed</param>
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);
}
/// <summary>
/// Helper function to support the Duplicati enumeration callback system
/// </summary>
/// <param name="rootpath">The root path to look in and use as filter base</param>
/// <param name="callback">The callback function that collects the output</param>
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<string, Win32USN.USN_RECORD> 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));
}
}
}
/// <summary>
/// Gets the USN JournalID for the current volume
/// </summary>
public long JournalID
{
get
{
return m_journal.UsnJournalID;
}
}
/// <summary>
/// Gets the next USN number for the volume
/// </summary>
public long USN
{
get
{
return m_journal.NextUsn;
}
}
/// <summary>
/// Internal property to access the cached version of the file entry list
/// </summary>
private List<KeyValuePair<string, Win32USN.USN_RECORD>> Records
{
get
{
if (m_entryList == null)
m_entryList = BuildUSNTable(0);
return m_entryList;
}
}
/// <summary>
/// Returns a list of files or folders that have changed since the recorded USN
/// </summary>
/// <param name="sourceFolder">The folder to find entries for</param>
/// <param name="lastUsn">The last known USN</param>
/// <returns>A list of changed files and folders</returns>
public List<string> GetChangedFileSystemEntries(string sourceFolder, long lastUsn)
{
List<string> result = new List<string>();
foreach (KeyValuePair<string, Win32USN.USN_RECORD> 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<string, Win32USN.USN_RECORD> r in this.Records)
if (r.Key.Equals(entry, Utility.Utility.ClientFilenameStringComparision))
return r.Value.Reason.ToString();
return "<not found>";
}
public List<string> GetRenamedFileSystemEntries(string sourceFolder, long lastUsn)
{
List<string> result = new List<string>();
foreach (KeyValuePair<string, Win32USN.USN_RECORD> 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;
}
/// <summary>
/// Returns a list of all files and folders in a given subfolder
/// </summary>
/// <param name="sourceFolder">The folder to enumerat</param>
/// <returns>A list of folders</returns>
public List<string> GetFileSystemEntries(string sourceFolder)
{
List<string> result = new List<string>();
foreach (KeyValuePair<string, Win32USN.USN_RECORD> r in this.Records)
if (r.Key.StartsWith(sourceFolder, Utility.Utility.ClientFilenameStringComparision))
result.Add(r.Key);
return result;
}
/// <summary>
/// Returns a list of files and folders found in the folder
/// </summary>
public List<string> FileSystemEntries
{
get
{
List<string> result = new List<string>();
foreach (KeyValuePair<string, Win32USN.USN_RECORD> r in this.Records)
if (r.Key.StartsWith(m_path, Utility.Utility.ClientFilenameStringComparision))
result.Add(r.Key);
return result;
}
}
/// <summary>
/// Returns a list of files found in the folder
/// </summary>
public List<string> Files
{
get
{
List<string> result = new List<string>();
foreach (KeyValuePair<string, Win32USN.USN_RECORD> 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;
}
}
/// <summary>
/// Returns a list of folders found in the folder
/// </summary>
public List<string> Folders
{
get
{
List<string> result = new List<string>();
foreach (KeyValuePair<string, Win32USN.USN_RECORD> 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;
}
}
/// <summary>
/// Cached copy of the USN_RECORD size
/// </summary>
static readonly int USN_RECORD_SIZE = Marshal.SizeOf(typeof(Win32USN.USN_RECORD));
/// <summary>
/// Helper function to work with unmanaged memory
/// </summary>
/// <param name="bytesRead">The number of bytes in buffer</param>
/// <param name="startOffset">The pointer to the start of the buffer</param>
/// <param name="records">The list of records to update with results</param>
/// <param name="moreEntries">A flag indicating if the end of the list is reached</param>
/// <returns>The next USN or FRN number, depending on what pointer was passed</returns>
private long ExtractUsnEntries(uint bytesRead, IntPtr startOffset, List<KeyValuePair<string, Win32USN.USN_RECORD>> 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<string, Win32USN.USN_RECORD>(filename, r));
}
bytesLeft -= (int)r.RecordLength;
curPtr = new IntPtr(curPtr.ToInt64() + r.RecordLength);
}
return nextUsn;
}
/// <summary>
/// Returns a list of all files and folders changed since the USN
/// </summary>
/// <param name="startUsn">The USN number to start the list from, set to zero to get all</param>
/// <returns>A list of files and folders changed since the USN</returns>
private List<KeyValuePair<string, Win32USN.USN_RECORD>> BuildUSNTable(long startUsn)
{
const int ALLOCATED_MEMORY = 64 * 1024;
IntPtr allocatedMemory = IntPtr.Zero;
List<KeyValuePair<string, Win32USN.USN_RECORD>> records = new List<KeyValuePair<string, Win32USN.USN_RECORD>>();
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;
}
}
}
/// <summary>
/// Calculates the full path of each entry in the USN table
/// </summary>
/// <param name="records">The list of records with local names</param>
/// <returns>A list of USN entries with full path</returns>
private List<KeyValuePair<string, Win32USN.USN_RECORD>> ParseRecordList(List<KeyValuePair<string, Win32USN.USN_RECORD>> records)
{
string rootDir = System.IO.Path.GetPathRoot(m_path);
List<KeyValuePair<string, Win32USN.USN_RECORD>> result = new List<KeyValuePair<string,Win32USN.USN_RECORD>>();
Dictionary<ulong, int> folderLookup = new Dictionary<ulong, int>();
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<string, Win32USN.USN_RECORD> r in records)
{
//Build the path, starting at the file/folder entry
List<string> items = new List<string>();
items.Add(r.Key + (((r.Value.FileAttributes & Win32USN.EFileAttributes.Directory) == 0) ? "" : "\\"));
KeyValuePair<string, Win32USN.USN_RECORD> 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<string, Win32USN.USN_RECORD>(System.IO.Path.Combine(rootDir, string.Join(System.IO.Path.DirectorySeparatorChar.ToString(), items.ToArray())), r.Value));
}
return result;
}
/// <summary>
/// Unused internal function that can be used to read all USN records
/// </summary>
/// <param name="lastUsn">The USN number to start from</param>
private void GetChangedItems(long lastUsn)
{
const int ALLOCATED_MEMORY = 64 * 1024;
IntPtr allocatedMemory = IntPtr.Zero;
List<KeyValuePair<string, Win32USN.USN_RECORD>> records = new List<KeyValuePair<string, Win32USN.USN_RECORD>>();
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
/// <summary>
/// Cleans up any resources held, including the volume handle
/// </summary>
public void Dispose()
{
if (m_volumeHandle != null)
{
m_volumeHandle.Dispose();
m_volumeHandle = null;
}
}
#endregion
}
}