#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
}
}