#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 // using System.Linq; #endregion using System; using System.Collections.Generic; using System.Text; namespace Duplicati.Library.Snapshots { /// /// This class encapsulates all access to the Linux LVM snapshot feature, /// implementing the disposable patterns to ensure correct release of resources. /// /// The class presents all files and folders with their regular filenames to the caller, /// and internally handles the conversion to the snapshot path. /// public class LinuxSnapshot : ISnapshotService { /// /// A frequently used char-as-string /// protected readonly string DIR_SEP = System.IO.Path.DirectorySeparatorChar.ToString(); /// /// Internal helper class for keeping track of a single snapshot volume /// private class SnapShot : IDisposable { /// /// The unique id of the snapshot /// private string m_name; /// /// The directory that the snapshot represents /// private string m_realDir; /// /// The temporary folder that the snapshot is mounted to /// private string m_tmpDir; /// /// The temporary folder that corresponds to the original folder /// private string m_mountPoint; /// /// The device name for the entry, used to prevent duplicate entries /// private string m_device; /// /// Constructs a new snapshot for the given folder /// /// public SnapShot(string path) { m_name = string.Format("duplicati-{0}", Guid.NewGuid().ToString()); m_realDir = System.IO.Directory.Exists(path) ? Utility.Utility.AppendDirSeparator(path) : path; GetVolumeName(m_realDir); } /// /// Gets the path of the folder that this snapshot represents /// public string LocalPath { get { return m_realDir; } } /// /// Gets a value representing the volume on which the folder resides /// public string DeviceName { get { return m_device; } } /// /// Gets the path where the snapshot is mounted /// public string SnapshotPath { get { return m_tmpDir; } } /// /// Gets the path the source disk is originally mounted /// public string MountPoint { get { return m_mountPoint; } } /// /// Converts a snapshot path to a local path /// /// The snapshot path /// The local path public string ConvertToLocalPath(string path) { if (!path.StartsWith(m_mountPoint)) throw new InvalidOperationException(); return m_tmpDir + path.Substring(m_mountPoint.Length); } /// /// Converts a local path to a snapshot path /// /// The local path /// The snapshot path public string ConvertToSnapshotPath(string path) { if (!path.StartsWith(m_tmpDir)) throw new InvalidOperationException(); return m_mountPoint + path.Substring(m_tmpDir.Length); } /// /// Helper function to execute a script /// /// The name of the lvm-script to execute /// The arguments to pass to the executable /// The exitcode that is expected /// A string with the combined output of the stdout and stderr private static string ExecuteCommand(string program, string commandline, int expectedExitCode) { program = System.IO.Path.Combine(System.IO.Path.Combine(Duplicati.Library.AutoUpdater.UpdaterManager.InstalledBaseDir, "lvm-scripts"), program); System.Diagnostics.ProcessStartInfo inf = new System.Diagnostics.ProcessStartInfo(program, commandline); inf.CreateNoWindow = true; inf.RedirectStandardError = true; inf.RedirectStandardOutput = true; inf.WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden; inf.UseShellExecute = false; try { System.Diagnostics.Process p = System.Diagnostics.Process.Start(inf); //Allow up 20 seconds for the execution if (!p.WaitForExit(30 * 1000)) { //Attempt to close down semi-nicely p.Kill(); p.WaitForExit(5 * 1000); //This should work, and if it does, prevents a race with any cleanup invocations throw new Exception(string.Format(Strings.LinuxSnapshot.ExternalProgramTimeoutError, program, commandline)); } //Build the output string. Since the process has exited, these cannot block string output = string.Format("Exit code: {1}{0}{2}{0}{3}", Environment.NewLine, p.ExitCode, p.StandardOutput.ReadToEnd(), p.StandardError.ReadToEnd()); //Throw an exception if something went wrong if (p.ExitCode != expectedExitCode) throw new Exception(string.Format(Strings.LinuxSnapshot.ScriptExitCodeError, p.ExitCode, expectedExitCode, output)); return output; } catch (Exception ex) { throw new Exception(string.Format(Strings.LinuxSnapshot.ExternalProgramLaunchError, ex.ToString(), program, commandline)); } } /// /// Finds the LVM id of the volume id where the folder is placed /// private void GetVolumeName(string folder) { //Figure out what logical volume the path is located on string output = ExecuteCommand("find-volume.sh", string.Format("\"{0}\"", folder), 0); System.Text.RegularExpressions.Regex rex = new System.Text.RegularExpressions.Regex("device=\"(?[^\"]+)\""); System.Text.RegularExpressions.Match m = rex.Match(output); if (!m.Success) throw new Exception(string.Format(Strings.LinuxSnapshot.ScriptOutputError, "device", output)); m_device = rex.Match(output).Groups["device"].Value; if (string.IsNullOrEmpty(m_device) || m_device.Trim().Length == 0) throw new Exception(string.Format(Strings.LinuxSnapshot.ScriptOutputError, "device", output)); rex = new System.Text.RegularExpressions.Regex("mountpoint=\"(?[^\"]+)\""); m = rex.Match(output); if (!m.Success) throw new Exception(string.Format(Strings.LinuxSnapshot.ScriptOutputError, "mountpoint", output)); m_mountPoint = rex.Match(output).Groups["mountpoint"].Value; if (string.IsNullOrEmpty(m_mountPoint) || m_mountPoint.Trim().Length == 0) throw new Exception(string.Format(Strings.LinuxSnapshot.ScriptOutputError, "mountpoint", output)); m_mountPoint = Utility.Utility.AppendDirSeparator(m_mountPoint); } /// /// Create the snapshot and mount it, this is not done in the constructor, /// because we want to see if some folders are on the same volume /// public void CreateSnapshotVolume() { if (m_device == null) throw new InvalidOperationException(); if (m_tmpDir != null) throw new InvalidOperationException(); //Create the snapshot volume string output = ExecuteCommand("create-lvm-snapshot.sh", string.Format("\"{0}\" \"{1}\" \"{2}\"", m_name, m_device, Utility.Utility.AppendDirSeparator(Utility.TempFolder.SystemTempPath)), 0); System.Text.RegularExpressions.Regex rex = new System.Text.RegularExpressions.Regex("tmpdir=\"(?[^\"]+)\""); System.Text.RegularExpressions.Match m = rex.Match(output); if (!m.Success) throw new Exception(string.Format(Strings.LinuxSnapshot.ScriptOutputError, "tmpdir", output)); m_tmpDir = rex.Match(output).Groups["tmpdir"].Value; if (!System.IO.Directory.Exists(m_tmpDir)) throw new Exception(string.Format(Strings.LinuxSnapshot.MountFolderMissingError, m_tmpDir, output)); m_tmpDir = Utility.Utility.AppendDirSeparator(m_tmpDir); } #region IDisposable Members /// /// Cleanup any used resources /// public void Dispose() { if (m_tmpDir != null && System.IO.Directory.Exists(m_tmpDir)) { string output = ExecuteCommand("remove-lvm-snapshot.sh", string.Format("\"{0}\" \"{1}\" \"{2}\"", m_name, m_device, m_tmpDir), 0); if (System.IO.Directory.Exists(m_tmpDir)) throw new Exception(string.Format(Strings.LinuxSnapshot.MountFolderNotRemovedError, m_tmpDir, output)); m_tmpDir = null; m_device = null; } } #endregion } /// /// This is the list of the snapshots we have created, which must be disposed /// private List m_activeSnapShots; /// /// This is a looup, mapping each source folder to the corresponding snapshot /// private List> m_entries; /// /// Constructs a new snapshot module using LVM /// /// The list of folders to create snapshots for /// A set of commandline options public LinuxSnapshot(string[] sources, Dictionary options) { try { m_entries = new List>(); //Make sure we do not create more snapshots than we have to Dictionary snaps = new Dictionary(); foreach (string s in sources) { SnapShot sn = new SnapShot(s); if (!snaps.ContainsKey(sn.DeviceName)) snaps.Add(sn.DeviceName, sn); m_entries.Add(new KeyValuePair(s, snaps[sn.DeviceName])); } m_activeSnapShots = new List(snaps.Values); //We have all the snapshots that we need, lets activate them foreach (SnapShot s in m_activeSnapShots) s.CreateSnapshotVolume(); } catch { //If something goes wrong, try to clean up try { Dispose(); } catch { } throw; } } #region Private functions /// /// A callback function that takes a non-snapshot path to a folder, /// and returns all folders found in a non-snapshot path format. /// /// The non-snapshot path of the folder to list /// A list of non-snapshot paths private string[] ListFolders(string folder) { KeyValuePair snap = FindSnapShotByLocalPath(folder); string[] tmp = System.IO.Directory.GetDirectories(ConvertToSnapshotPath(snap, folder)); for (int i = 0; i < tmp.Length; i++) tmp[i] = ConvertToLocalPath(snap, tmp[i]); return tmp; } /// /// A callback function that takes a non-snapshot path to a folder, /// and returns all files found in a non-snapshot path format. /// /// The non-snapshot path of the folder to list /// A list of non-snapshot paths private string[] ListFiles(string folder) { KeyValuePair snap = FindSnapShotByLocalPath(folder); string[] tmp = System.IO.Directory.GetFiles(ConvertToSnapshotPath(snap, folder)); for (int i = 0; i < tmp.Length; i++) tmp[i] = ConvertToLocalPath(snap, tmp[i]); return tmp; } /// /// Locates the snapshot instance that maps the path /// /// The file or folder name to match /// The matching snapshot private KeyValuePair FindSnapShotByLocalPath(string name) { KeyValuePair? best = null; foreach (KeyValuePair s in m_entries) if (name.StartsWith(s.Key) && (best == null || s.Key.Length > best.Value.Key.Length)) best = s; if (best == null) { StringBuilder sb = new StringBuilder(); sb.Append(Environment.NewLine); foreach (KeyValuePair s in m_entries) sb.AppendFormat("{0} ({1} -> {2}){3}", s.Key, s.Value.MountPoint, s.Value.SnapshotPath, Environment.NewLine); throw new InvalidOperationException(string.Format(Strings.LinuxSnapshot.InvalidFilePathError, name, sb.ToString())); } return best.Value; } /// /// Converts a local path to a snapshot path /// /// The snapshot that represents the mapping /// The filename to convert /// The converted path private string ConvertToSnapshotPath(KeyValuePair snap, string file) { return snap.Value.SnapshotPath + file.Substring(snap.Value.MountPoint.Length); } /// /// Converts a snapshot path to a local path /// /// The snapshot that represents the mapping /// The filename to convert /// The converted path private string ConvertToLocalPath(KeyValuePair snap, string file) { return snap.Value.MountPoint + file.Substring(snap.Value.SnapshotPath.Length); } #endregion #region ISnapshotService Members /// /// Enumerates all files and folders in the snapshot /// /// The filter to apply when evaluating files and folders /// The callback to invoke with each found path public IEnumerable EnumerateFilesAndFolders(Duplicati.Library.Utility.Utility.EnumerationFilterDelegate callback) { return m_entries.SelectMany( s => Utility.Utility.EnumerateFileSystemEntries(s.Key, callback, this.ListFolders, this.ListFiles, this.GetAttributes) ); } /// /// Gets the last write time of a given file in UTC /// /// The full path to the file in non-snapshot format /// The last write time of the file public DateTime GetLastWriteTimeUtc(string file) { return System.IO.File.GetLastWriteTimeUtc(ConvertToSnapshotPath(FindSnapShotByLocalPath(file), file)); } /// /// Opens a file for reading /// /// The full path to the file in non-snapshot format /// An open filestream that can be read public System.IO.Stream OpenRead(string file) { return System.IO.File.OpenRead(ConvertToSnapshotPath(FindSnapShotByLocalPath(file), file)); } /// /// Returns the size of a file /// /// The full path to the file in non-snapshot format /// The lenth of the file public long GetFileSize(string file) { return new System.IO.FileInfo(ConvertToSnapshotPath(FindSnapShotByLocalPath(file), file)).Length; } /// /// Gets the attributes for the given file or folder /// /// The file attributes /// The file or folder to examine public System.IO.FileAttributes GetAttributes(string file) { return System.IO.File.GetAttributes(ConvertToSnapshotPath(FindSnapShotByLocalPath(file), file)); } /// /// Returns the symlink target if the entry is a symlink, and null otherwise /// /// The file or folder to examine /// The symlink target public string GetSymlinkTarget(string file) { var local = ConvertToSnapshotPath(FindSnapShotByLocalPath(file), file); return UnixSupport.File.GetSymlinkTarget(local.EndsWith(DIR_SEP) ? local.Substring(0, local.Length - 1) : local); } /// /// Gets the metadata for the given file or folder /// /// The metadata for the given file or folder /// The file or folder to examine public Dictionary GetMetadata(string file) { var local = ConvertToSnapshotPath(FindSnapShotByLocalPath(file), file); var n = UnixSupport.File.GetExtendedAttributes(local.EndsWith(DIR_SEP) ? local.Substring(0, local.Length - 1) : local); var dict = new Dictionary(); foreach(var x in n) dict[x.Key] = Convert.ToBase64String(x.Value); return dict; } /// /// Gets a value indicating if the path points to a block device /// /// true if this instance is a block device; otherwise, false. /// The file or folder to examine public bool IsBlockDevice(string file) { var n = UnixSupport.File.GetFileType(file.EndsWith(DIR_SEP) ? file.Substring(0, file.Length - 1) : file); switch (n) { case UnixSupport.File.FileType.Directory: case UnixSupport.File.FileType.Symlink: case UnixSupport.File.FileType.File: return false; default: return true; } } /// /// Gets a unique hardlink target ID /// /// The hardlink ID /// The file or folder to examine public string HardlinkTargetID(string path) { var local = ConvertToSnapshotPath(FindSnapShotByLocalPath(path), path); local = local.EndsWith(DIR_SEP) ? local.Substring(0, local.Length - 1) : local; if (UnixSupport.File.GetHardlinkCount(local) <= 1) return null; return UnixSupport.File.GetInodeTargetID(local); } #endregion #region IDisposable Members /// /// Releases any held resources /// public void Dispose() { if (m_activeSnapShots != null) { Exception exs = null; //Attempt to clean out as many as possible foreach(SnapShot s in m_activeSnapShots) try { s.Dispose(); } catch (Exception ex) { exs = ex; } //Don't try this again m_activeSnapShots = null; //Report errors, if any if (exs != null) throw exs; } } #endregion } }