#region Disclaimer / License // Copyright (C) 2015, The Duplicati Team // http://www.duplicati.com, info@duplicati.com // // 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.Diagnostics; using System.Text; using Duplicati.Library.Common.IO; 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 sealed class LinuxSnapshot : SnapshotBase { /// /// The tag used for logging messages /// public static readonly string LOGTAG = Logging.Log.LogTagFromType(); /// /// This is a lookup, mapping each source folder to the corresponding snapshot /// private readonly List> m_entries; /// /// This is the list of the snapshots we have created, which must be disposed /// private List m_snapShots; /// /// Constructs a new snapshot module using LVM /// /// The list of folders to create snapshots for public LinuxSnapshot(IEnumerable sources) { try { m_entries = new List>(); // Make sure we do not create more snapshots than we have to var snaps = new Dictionary(); foreach (var path in sources) { var tmp = new SnapShot(path); if (!snaps.TryGetValue(tmp.DeviceName, out var snap)) { snaps.Add(tmp.DeviceName, tmp); snap = tmp; } m_entries.Add(new KeyValuePair(path, snap)); } m_snapShots = new List(snaps.Values); // We have all the snapshots that we need, lets activate them foreach (var snap in m_snapShots) { snap.CreateSnapshotVolume(); } } catch { // If something goes wrong, try to clean up try { Dispose(); } catch (Exception ex) { Logging.Log.WriteVerboseMessage(LOGTAG, "SnapshotCleanupError", ex, "Failed to clean up after error"); } throw; } } /// protected override void Dispose(bool disposing) { if (m_snapShots != null) { if (disposing) { // Attempt to clean out as many as possible foreach(var s in m_snapShots) { try { s.Dispose(); } catch (Exception ex) { Logging.Log.WriteVerboseMessage(LOGTAG, "SnapshotCloseError", ex, "Failed to close a snapshot"); } } } // Don't try this again m_snapShots = null; } base.Dispose(disposing); } /// /// Internal helper class for keeping track of a single snapshot volume /// private sealed class SnapShot : IDisposable { /// /// The unique id of the snapshot /// private readonly string m_name; /// /// Constructs a new snapshot for the given folder /// /// public SnapShot(string path) { m_name = $"duplicati-{Guid.NewGuid().ToString()}"; LocalPath = System.IO.Directory.Exists(path) ? Util.AppendDirSeparator(path) : path; Initialize(LocalPath); } /// /// Gets the path of the folder that this snapshot represents /// public string LocalPath { get; } /// /// Gets a value representing the volume on which the folder resides /// public string DeviceName { get; private set; } /// /// Gets the path where the snapshot is mounted /// public string SnapshotPath { get; private set; } /// /// Gets the path the source disk is originally mounted /// public string MountPoint { get; private set; } #region IDisposable Members /// /// Cleanup any used resources /// public void Dispose() { if (SnapshotPath != null && System.IO.Directory.Exists(SnapshotPath)) { var output = ExecuteCommand("remove-lvm-snapshot.sh", $"\"{m_name}\" \"{DeviceName}\" \"{SnapshotPath}\"", 0); if (System.IO.Directory.Exists(SnapshotPath)) throw new Exception(Strings.LinuxSnapshot.MountFolderNotRemovedError(SnapshotPath, output)); SnapshotPath = null; DeviceName = null; } } #endregion /// /// Converts a local path to a snapshot path /// /// The local path /// The snapshot path public string ConvertToSnapshotPath(string localPath) { if (!localPath.StartsWith(MountPoint, StringComparison.Ordinal)) throw new InvalidOperationException(); return SystemIOLinux.NormalizePath(SnapshotPath + localPath.Substring(MountPoint.Length)); } /// /// Converts a snapshot path to a local path /// /// The snapshot path /// The local path public string ConvertToLocalPath(string snapshotPath) { if (!snapshotPath.StartsWith(SnapshotPath, StringComparison.Ordinal)) throw new InvalidOperationException(); return MountPoint + snapshotPath.Substring(SnapshotPath.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(AutoUpdater.UpdaterManager.InstalledBaseDir, "lvm-scripts"), program); var inf = new ProcessStartInfo(program, commandline) { CreateNoWindow = true, RedirectStandardError = true, RedirectStandardOutput = true, RedirectStandardInput = false, WindowStyle = ProcessWindowStyle.Hidden, UseShellExecute = false }; try { var p = 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 Interface.UserInformationException(Strings.LinuxSnapshot.ExternalProgramTimeoutError(program, commandline), "LvmScriptTimeout"); } //Build the output string. Since the process has exited, these cannot block var 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 Interface.UserInformationException(Strings.LinuxSnapshot.ScriptExitCodeError(p.ExitCode, expectedExitCode, output), "LvmScriptWrongExitCode"); return output; } catch (Exception ex) { throw new Exception(Strings.LinuxSnapshot.ExternalProgramLaunchError(ex.ToString(), program, commandline)); } } /// /// Finds the LVM id of the volume id where the folder is placed /// private void Initialize(string folder) { //Figure out what logical volume the path is located on var output = ExecuteCommand("find-volume.sh", $"\"{folder}\"", 0); var rex = new System.Text.RegularExpressions.Regex("device=\"(?[^\"]+)\""); var m = rex.Match(output); if (!m.Success) throw new Exception(Strings.LinuxSnapshot.ScriptOutputError("device", output)); DeviceName = rex.Match(output).Groups["device"].Value; if (string.IsNullOrEmpty(DeviceName) || DeviceName.Trim().Length == 0) throw new Exception(Strings.LinuxSnapshot.ScriptOutputError("device", output)); rex = new System.Text.RegularExpressions.Regex("mountpoint=\"(?[^\"]+)\""); m = rex.Match(output); if (!m.Success) throw new Exception(Strings.LinuxSnapshot.ScriptOutputError("mountpoint", output)); MountPoint = rex.Match(output).Groups["mountpoint"].Value; if (string.IsNullOrEmpty(MountPoint) || MountPoint.Trim().Length == 0) throw new Exception(Strings.LinuxSnapshot.ScriptOutputError("mountpoint", output)); MountPoint = Util.AppendDirSeparator(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 (DeviceName == null) throw new InvalidOperationException(); if (SnapshotPath != null) throw new InvalidOperationException(); //Create the snapshot volume var output = ExecuteCommand("create-lvm-snapshot.sh", $"\"{m_name}\" \"{DeviceName}\" \"{Util.AppendDirSeparator(Utility.TempFolder.SystemTempPath)}\"", 0); var rex = new System.Text.RegularExpressions.Regex("tmpdir=\"(?[^\"]+)\""); var m = rex.Match(output); if (!m.Success) throw new Exception(Strings.LinuxSnapshot.ScriptOutputError("tmpdir", output)); SnapshotPath = rex.Match(output).Groups["tmpdir"].Value; if (!System.IO.Directory.Exists(SnapshotPath)) throw new Exception(Strings.LinuxSnapshot.MountFolderMissingError(SnapshotPath, output)); SnapshotPath = Util.AppendDirSeparator(SnapshotPath); } } #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 protected override string[] ListFolders(string localFolderPath) { var snap = FindSnapshotByLocalPath(localFolderPath); var tmp = System.IO.Directory.GetDirectories(snap.ConvertToSnapshotPath(localFolderPath)); for (var i = 0; i < tmp.Length; i++) tmp[i] = snap.ConvertToLocalPath(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 protected override string[] ListFiles(string localFolderPath) { var snap = FindSnapshotByLocalPath(localFolderPath); var tmp = System.IO.Directory.GetFiles(snap.ConvertToSnapshotPath(localFolderPath)); for (var i = 0; i < tmp.Length; i++) tmp[i] = snap.ConvertToLocalPath(tmp[i]); return tmp; } /// /// Locates the snapshot instance that maps the path /// /// The file or folder name to match /// The matching snapshot private SnapShot FindSnapshotByLocalPath(string localPath) { KeyValuePair? best = null; foreach (var s in m_entries) { if (localPath.StartsWith(s.Key, StringComparison.Ordinal) && (best == null || s.Key.Length > best.Value.Key.Length)) { best = s; } } if (best != null) return best.Value.Value; var sb = new StringBuilder(); sb.Append(Environment.NewLine); foreach (var s in m_entries) { sb.Append($"{s.Key} ({s.Value.MountPoint} -> {s.Value.SnapshotPath}){Environment.NewLine}"); } throw new InvalidOperationException(Strings.LinuxSnapshot.InvalidFilePathError(localPath, sb.ToString())); } /// /// Locates the snapshot containing the snapshot path /// /// /// Snapshot containing snapshotPath private SnapShot FindSnapshotBySnapshotPath(string snapshotPath) { foreach (var snap in m_snapShots) { if (snapshotPath.StartsWith(snap.SnapshotPath, StringComparison.Ordinal)) return snap; } throw new InvalidOperationException(); } #endregion #region ISnapshotService Members /// /// 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 override DateTime GetLastWriteTimeUtc(string localPath) { return System.IO.File.GetLastWriteTimeUtc(ConvertToSnapshotPath(localPath)); } /// /// Gets the creation 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 override DateTime GetCreationTimeUtc(string localPath) { return System.IO.File.GetLastWriteTimeUtc(ConvertToSnapshotPath(localPath)); } /// /// Opens a file for reading /// /// The full path to the file in non-snapshot format /// An open filestream that can be read public override System.IO.Stream OpenRead(string localPath) { return System.IO.File.OpenRead(ConvertToSnapshotPath(localPath)); } /// /// Returns the size of a file /// /// The full path to the file in non-snapshot format /// The length of the file public override long GetFileSize(string localPath) { return new System.IO.FileInfo(ConvertToSnapshotPath(localPath)).Length; } /// /// Gets the attributes for the given file or folder /// /// The file attributes /// The file or folder to examine public override System.IO.FileAttributes GetAttributes(string localPath) { return System.IO.File.GetAttributes(ConvertToSnapshotPath(localPath)); } /// /// Returns the symlink target if the entry is a symlink, and null otherwise /// /// The file or folder to examine /// The symlink target public override string GetSymlinkTarget(string localPath) { return SystemIO.IO_SYS.GetSymlinkTarget(ConvertToSnapshotPath(localPath)); } /// /// Gets the metadata for the given file or folder /// /// The metadata for the given file or folder /// The file or folder to examine /// A flag indicating if the target is a symlink /// A flag indicating if a symlink should be followed public override Dictionary GetMetadata(string localPath, bool isSymlink, bool followSymlink) { return SystemIO.IO_SYS.GetMetadata(ConvertToSnapshotPath(localPath), isSymlink, followSymlink); } /// /// 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 override bool IsBlockDevice(string localPath) { try { var n = UnixSupport.File.GetFileType(SystemIOLinux.NormalizePath(localPath)); switch (n) { case UnixSupport.File.FileType.Directory: case UnixSupport.File.FileType.Symlink: case UnixSupport.File.FileType.File: return false; default: return true; } } catch { if (!System.IO.File.Exists(SystemIOLinux.NormalizePath(localPath))) return false; throw; } } /// /// Gets a unique hardlink target ID /// /// The hardlink ID /// The file or folder to examine public override string HardlinkTargetID(string localPath) { var snapshotPath = ConvertToSnapshotPath(localPath); if (UnixSupport.File.GetHardlinkCount(snapshotPath) <= 1) return null; return UnixSupport.File.GetInodeTargetID(snapshotPath); } /// public override string ConvertToLocalPath(string snapshotPath) { return FindSnapshotBySnapshotPath(snapshotPath).ConvertToLocalPath(snapshotPath); } /// public override string ConvertToSnapshotPath(string localPath) { return FindSnapshotByLocalPath(localPath).ConvertToSnapshotPath(localPath); } /// public override bool IsSnapshot => true; #endregion } }