Added support for Wmic based Windows VSS snapshots.

Refactored code a bit to be easier to understand.
This commit is contained in:
Kenneth Skovhede
2025-02-19 14:00:14 +01:00
parent ac603a4d8f
commit cf5b4bb24d
22 changed files with 914 additions and 220 deletions
@@ -24,7 +24,7 @@ using System.Threading.Tasks;
using Duplicati.Library.Utility;
using CoCoL;
using Duplicati.Library.Interface;
using Duplicati.Library.Snapshots;
using Duplicati.Library.Snapshots.USN;
namespace Duplicati.Library.Main.Operation.Backup
{
@@ -31,9 +31,8 @@ using System.Threading;
using Duplicati.Library.Main.Operation.Common;
using Duplicati.Library.Interface;
using System.Runtime.CompilerServices;
using Duplicati.Library.Snapshots;
using Duplicati.Library.SourceProvider;
using Duplicati.Library.Common.IO;
using Duplicati.Library.Snapshots.USN;
namespace Duplicati.Library.Main.Operation.Backup
{
@@ -36,6 +36,7 @@ using Duplicati.Library.Main.Operation.Common;
using System.Data;
using Duplicati.Library.DynamicLoader;
using Duplicati.Library.SourceProvider;
using Duplicati.Library.Snapshots.USN;
namespace Duplicati.Library.Main.Operation
{
+19
View File
@@ -345,6 +345,7 @@ namespace Duplicati.Library.Main
new CommandLineArgument("upload-unchanged-backups", CommandLineArgument.ArgumentType.Boolean, Strings.Options.UploadUnchangedBackupsShort, Strings.Options.UploadUnchangedBackupsLong, "false"),
new CommandLineArgument("snapshot-policy", CommandLineArgument.ArgumentType.Enumeration, Strings.Options.SnapshotpolicyShort, Strings.Options.SnapshotpolicyLong, "off", null, Enum.GetNames(typeof(OptimizationStrategy))),
new CommandLineArgument("snapshot-provider", CommandLineArgument.ArgumentType.Enumeration, Strings.Options.SnapshotproviderShort, Strings.Options.SnapshotproviderLong, OperatingSystem.IsWindows() ? Snapshots.SnapshotProvider.AlphaVSS.ToString() : Snapshots.SnapshotProvider.LVM.ToString(), null, (OperatingSystem.IsWindows() ? [Snapshots.SnapshotProvider.AlphaVSS, Snapshots.SnapshotProvider.Wmic] : new [] { Snapshots.SnapshotProvider.LVM }).Select(x => x.ToString()).ToArray()),
new CommandLineArgument("vss-exclude-writers", CommandLineArgument.ArgumentType.String, Strings.Options.VssexcludewritersShort, Strings.Options.VssexcludewritersLong, "{e8132975-6f93-4464-a53e-1050253ae220}"),
new CommandLineArgument("vss-use-mapping", CommandLineArgument.ArgumentType.Boolean, Strings.Options.VssusemappingShort, Strings.Options.VssusemappingLong, "false"),
new CommandLineArgument("usn-policy", CommandLineArgument.ArgumentType.Enumeration, Strings.Options.UsnpolicyShort, Strings.Options.UsnpolicyLong, "off", null, Enum.GetNames(typeof(OptimizationStrategy))),
@@ -1025,6 +1026,24 @@ namespace Duplicati.Library.Main
}
}
/// <summary>
/// Gets the snapshot strategy to use
/// </summary>
public Snapshots.SnapshotProvider SnapShotProvider
{
get
{
if (!m_options.TryGetValue("snapshot-provider", out var provider))
provider = "";
Snapshots.SnapshotProvider r;
if (!Enum.TryParse(provider, true, out r))
r = OperatingSystem.IsWindows() ? Snapshots.SnapshotProvider.AlphaVSS : Snapshots.SnapshotProvider.LVM;
return r;
}
}
/// <summary>
/// Gets a flag indicating if advisory locking should be ignored
/// </summary>
+2
View File
@@ -116,6 +116,8 @@ namespace Duplicati.Library.Main.Strings
public static string EnablemoduleShort { get { return LC.L(@"Enable one or more modules"); } }
public static string SnapshotpolicyLong { get { return LC.L(@"This setting controls the usage of snapshots, which allows Duplicati to backup files that are locked by other programs. If this is set to ""off"", Duplicati will not attempt to create a disk snapshot. Setting this to ""auto"" makes Duplicati attempt to create a snapshot, and fail silently if that was not allowed or supported (note that the OS may still log system warnings). A setting of ""on"" will also make Duplicati attempt to create a snapshot, but will produce a warning message in the log if it fails. Setting it to ""required"" will make Duplicati abort the backup if the snapshot creation fails. On windows this uses the Volume Shadow Copy Services (VSS) and requires administrative privileges. On Linux this uses Logical Volume Management (LVM) and requires root privileges."); } }
public static string SnapshotpolicyShort { get { return LC.L(@"Control the use of disk snapshots"); } }
public static string SnapshotproviderLong { get { return LC.L(@"The snapshot provider implementation for Windows. The AlphaVSS is the most feature complete, but is not supported on Arm64. The WMIC based snapshot has less features, but is more portable. On Linux, only LVM is supported"); } }
public static string SnapshotproviderShort { get { return LC.L(@"The snapshot provider implementation to use"); } }
public static string AsynchronousuploadfolderLong { get { return LC.L(@"The pre-generated volumes will be placed into the temporary folder by default. This option can set a different folder for placing the temporary volumes. Despite the name, this also works for synchronous runs."); } }
public static string AsynchronousuploadfolderShort { get { return LC.L(@"The path where ready volumes are placed until uploaded"); } }
public static string AsynchronousuploadlimitLong { get { return LC.L(@"When performing asynchronous uploads, Duplicati will create volumes that can be uploaded. To prevent Duplicati from generating too many volumes, this option limits the number of pending uploads. Set to zero to disable the limit. The volume(s) that are being created are not counted in this limit. Use the option --concurrency-compressors=1 to limit the number of volumes being created."); } }
@@ -1,22 +1,22 @@
// Copyright (C) 2025, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// Copyright (C) 2025, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System;
@@ -24,8 +24,8 @@ using System.Collections.Generic;
using System.Linq;
using System.Runtime.Versioning;
using System.Text.RegularExpressions;
using Duplicati.Library.Common;
using Duplicati.Library.Snapshots;
using Duplicati.Library.Snapshots.Windows;
namespace Duplicati.Library.Modules.Builtin
{
@@ -149,7 +149,8 @@ namespace Duplicati.Library.Modules.Builtin
Logging.Log.WriteWarningMessage(LOGTAG, "HyperVOnServerOnly", null, "This is client version of Windows. Hyper-V VSS writer is present only on Server version. Backup will continue, but will be crash consistent only in opposite to application consistent in Server version");
Logging.Log.WriteInformationMessage(LOGTAG, "StartingHyperVQuery", "Starting to gather Hyper-V information");
hypervUtility.QueryHyperVGuestsInfo(true);
var provider = Utility.Utility.ParseEnumOption(changedOptions.AsReadOnly(), "snapshot-provider", SnapshotProvider.AlphaVSS);
hypervUtility.QueryHyperVGuestsInfo(provider, true);
Logging.Log.WriteInformationMessage(LOGTAG, "HyperVMachineCount", "Found {0} virtual machines on Hyper-V", hypervUtility.Guests.Count);
foreach (var guest in hypervUtility.Guests)
@@ -1,31 +1,31 @@
// Copyright (C) 2025, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
// Copyright (C) 2025, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Versioning;
using System.Text.RegularExpressions;
using Duplicati.Library.Common;
using Duplicati.Library.Snapshots;
using Duplicati.Library.Snapshots.Windows;
namespace Duplicati.Library.Modules.Builtin
{
@@ -144,7 +144,8 @@ namespace Duplicati.Library.Modules.Builtin
}
Logging.Log.WriteInformationMessage(LOGTAG, "StartingMsSqlQuery", "Starting to gather Microsoft SQL Server information", Logging.LogMessageType.Information);
mssqlUtility.QueryDBsInfo();
var provider = Utility.Utility.ParseEnumOption(changedOptions.AsReadOnly(), "snapshot-provider", SnapshotProvider.AlphaVSS);
mssqlUtility.QueryDBsInfo(provider);
Logging.Log.WriteInformationMessage(LOGTAG, "MsSqlDatabaseCount", "Found {0} databases on Microsoft SQL Server", mssqlUtility.DBs.Count);
foreach(var db in mssqlUtility.DBs)
-2
View File
@@ -21,9 +21,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Duplicati.Library.AutoUpdater;
using Duplicati.Library.Common;
using Duplicati.Library.Utility;
namespace Duplicati.Library.Snapshots
@@ -0,0 +1,30 @@
// Copyright (C) 2025, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
namespace Duplicati.Library.Snapshots
{
public enum SnapshotProvider
{
AlphaVSS,
Wmic,
LVM
}
}
@@ -31,7 +31,7 @@ using System.Runtime.Versioning;
using Duplicati.Library.Common.IO;
using Microsoft.Win32.SafeHandles;
namespace Duplicati.Library.Snapshots
namespace Duplicati.Library.Snapshots.USN
{
/// <summary>
/// Class that encapsulates USN journal access to a single volume
@@ -33,7 +33,7 @@ using Duplicati.Library.Utility;
using System.Threading.Tasks;
using System.Runtime.CompilerServices;
namespace Duplicati.Library.Snapshots
namespace Duplicati.Library.Snapshots.USN
{
[SupportedOSPlatform("windows")]
public class UsnJournalService
@@ -1,30 +1,30 @@
// Copyright (C) 2025, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
// Copyright (C) 2025, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using Microsoft.Win32.SafeHandles;
namespace Duplicati.Library.Snapshots
namespace Duplicati.Library.Snapshots.USN
{
//The signatures in this file are from http://pinvoke.net
@@ -0,0 +1,200 @@
// Copyright (C) 2025, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
#nullable enable
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Alphaleonis.Win32.Vss;
using Duplicati.Library.Common.IO;
namespace Duplicati.Library.Snapshots.Windows;
/// <summary>
/// Implementation of a snapshot, using AlphaVSS
/// </summary>
internal class AlphaVssBackup : ISnapshotProvider
{
/// <summary>
/// The implementation that is being wrapped
/// </summary>
private IVssBackupComponents _components;
/// <summary>
/// Creates a new snapshot implementation
/// </summary>
/// <param name="components">The components to use</param>
private AlphaVssBackup(IVssBackupComponents components)
{
_components = components;
}
/// <inheritdoc/>
public void EnableWriterClasses(Guid[] guids)
=> _components.EnableWriterClasses(guids);
/// <inheritdoc/>
public void DisableWriterClasses(Guid[] guids)
=> _components.DisableWriterClasses(guids);
/// <inheritdoc/>
public void GatherWriterMetadata()
=> _components.GatherWriterMetadata();
/// <inheritdoc/>
public void FreeWriterMetadata()
=> _components.FreeWriterMetadata();
/// <inheritdoc/>
public void VerifyWriters(Guid[] guids)
{
foreach (var writerGUID in guids)
{
if (!_components.WriterMetadata.Any(o => o.WriterId.Equals(writerGUID)))
{
throw new Exception(string.Format("Writer with GUID {0} was not added to VSS writer set.", writerGUID.ToString()));
}
}
}
/// <summary>
/// Stub for returning the snapshot info
/// </summary>
/// <param name="SnapshotDeviceObject">The path where the snapshot is located</param>
private sealed record SnapshotInfo(string SnapshotDeviceObject) : ISnapshotInfo;
/// <inheritdoc/>
public ISnapshotInfo GetSnapshotProperties(Guid shadowId)
=> new SnapshotInfo(_components.GetSnapshotProperties(shadowId).SnapshotDeviceObject);
/// <inheritdoc/>
public void StartSnapshotSet()
=> _components.StartSnapshotSet();
/// <inheritdoc/>
public void PrepareForBackup()
=> _components.PrepareForBackup();
/// <inheritdoc/>
public void DoSnapshotSet()
=> _components.DoSnapshotSet();
/// <inheritdoc/>
public bool IsVolumeSupported(string drive)
=> _components.IsVolumeSupported(drive);
/// <inheritdoc/>
public Guid AddToSnapshotSet(string drive)
=> _components.AddToSnapshotSet(drive);
/// <inheritdoc/>
public void BackupComplete()
=> _components.BackupComplete();
/// <inheritdoc/>
public void DeleteSnapshot(Guid shadowId, bool forceDelete)
=> _components.DeleteSnapshot(shadowId, forceDelete);
/// <inheritdoc/>
public static ISnapshotProvider Create()
=> new AlphaVssBackup(GetVssBackupComponents());
/// <summary>
/// Creates a new AlphaVssBackupComponents instance and adds the context
/// </summary>
/// <returns>The configured instance</returns>
private static IVssBackupComponents GetVssBackupComponents()
{
//Prepare the backup
IVssBackupComponents vssBackupComponents = CreateVssBackupComponents();
vssBackupComponents.InitializeForBackup(null);
vssBackupComponents.SetContext(VssSnapshotContext.Backup);
vssBackupComponents.SetBackupState(false, true, VssBackupType.Full, false);
return vssBackupComponents;
}
/// <summary>
/// Loads the AlphaVSS implementation for the current OS
/// </summary>
/// <returns>The implementation</returns>
private static IVssBackupComponents CreateVssBackupComponents()
{
var vss = VssFactoryProvider.Default.GetVssFactory();
if (vss == null)
throw new InvalidOperationException();
return vss.CreateVssBackupComponents();
}
/// <inheritdoc/>
public IEnumerable<WriterMetaData> ParseWriterMetaData(Guid[] writers)
{
// check if writers got enabled
foreach (var writerGUID in writers)
{
var writer = _components.WriterMetadata.First(o => o.WriterId.Equals(writerGUID));
foreach (var component in writer.Components)
{
yield return new WriterMetaData
{
Guid = writerGUID,
Name = component.ComponentName,
LogicalPath = component.LogicalPath,
Paths = GetPathsFromComponent(component)
};
}
}
}
/// <summary>
/// Returns the paths from the writer metadata
/// </summary>
/// <param name="component">The component to get the paths from</param>
/// <returns>The list of paths</returns>
private List<string> GetPathsFromComponent(IVssWMComponent component)
{
var paths = new List<string>();
foreach (var file in component.Files)
{
if (file.FileSpecification.Contains("*"))
{
if (Directory.Exists(Util.AppendDirSeparator(file.Path)))
paths.Add(Util.AppendDirSeparator(file.Path));
}
else
{
var fileWithSpec = SystemIO.IO_OS.PathCombine(file.Path, file.FileSpecification);
if (File.Exists(fileWithSpec))
paths.Add(fileWithSpec);
}
}
return paths;
}
/// <inheritdoc/>
public void Dispose()
{
_components.Dispose();
}
}
@@ -26,7 +26,7 @@ using System.Management;
using System.Runtime.Versioning;
using Duplicati.Library.Common.IO;
namespace Duplicati.Library.Snapshots
namespace Duplicati.Library.Snapshots.Windows
{
public class HyperVGuest : IEquatable<HyperVGuest>
{
@@ -160,8 +160,9 @@ namespace Duplicati.Library.Snapshots
/// Query Hyper-V for all Virtual Machines info
/// </summary>
/// <param name="bIncludePaths">Specify if returned data should contain VM paths</param>
/// <param name="provider">The provider to use for VSS</param>
/// <returns>List of Hyper-V Machines</returns>
public void QueryHyperVGuestsInfo(bool bIncludePaths = false)
public void QueryHyperVGuestsInfo(SnapshotProvider provider, bool bIncludePaths = false)
{
if (!IsHyperVInstalled)
return;
@@ -178,7 +179,7 @@ namespace Duplicati.Library.Snapshots
if (bIncludePaths)
{
foreach (var o in GetAllVMsPathsVSS())
foreach (var o in GetAllVMsPathsVSS(provider))
{
foreach (var mObject in moCollection)
{
@@ -218,9 +219,9 @@ namespace Duplicati.Library.Snapshots
/// For all Hyper-V guests it enumerate all associated paths using VSS data
/// </summary>
/// <returns>A collection of VMs and paths</returns>
private static IEnumerable<WriterMetaData> GetAllVMsPathsVSS()
private static IEnumerable<WriterMetaData> GetAllVMsPathsVSS(SnapshotProvider provider)
{
using (var vssBackupComponents = new VssBackupComponents())
using (var vssBackupComponents = new SnapshotManager(provider))
{
var writerGUIDS = new[] { HyperVWriterGuid };
@@ -0,0 +1,118 @@
// Copyright (C) 2025, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
#nullable enable
using System;
using System.Collections.Generic;
namespace Duplicati.Library.Snapshots.Windows;
/// <summary>
/// Interface for the snapshot information
/// </summary>
internal interface ISnapshotInfo
{
/// <summary>
/// The path where the snapshot is located
/// </summary>
string SnapshotDeviceObject { get; }
}
/// <summary>
/// Interface for a snapshot provider
/// </summary>
internal interface ISnapshotProvider : IDisposable
{
/// <summary>
/// Explicitly enables the selected writer GUIDs
/// </summary>
/// <param name="guids">The GUIDs to enable</param>
void EnableWriterClasses(Guid[] guids);
/// <summary>
/// Explicitly disable the selected writer GUIDs
/// </summary>
/// <param name="guids">The GUIDs to disable</param>
void DisableWriterClasses(Guid[] guids);
/// <summary>
/// Gather writer metadata prior to the snapshot
/// </summary>
void GatherWriterMetadata();
/// <summary>
/// Release any allocated metadata
/// </summary>
void FreeWriterMetadata();
/// <summary>
/// Verify that the writes are enabled on the snapshot
/// </summary>
/// <param name="guids">The writer GUIDs to verify as enabled</param>
void VerifyWriters(Guid[] guids);
/// <summary>
/// Gets information of a snapshot
/// </summary>
/// <param name="shadowId">The shadow copy id</param>
/// <returns>The snapshot information</returns>
ISnapshotInfo GetSnapshotProperties(Guid shadowId);
/// <summary>
/// Creates a snapshot
/// </summary>
void StartSnapshotSet();
/// <summary>
/// Starts the snapshot as a backup snapshot
/// </summary>
void PrepareForBackup();
/// <summary>
/// Finalizes the snapshot creation
/// </summary>
void DoSnapshotSet();
/// <summary>
/// Checks if the volume is supported by the snapshot
/// </summary>
/// <param name="drive">The drive to check</param>
/// <returns><c>true</c> if the volume is supported; <c>false<c/> otherwise</returns>
bool IsVolumeSupported(string drive);
/// <summary>
/// Adds a drive to the snapshot set
/// </summary>
/// <param name="drive">The drive to add</param>
/// <returns>The shadow id</returns>
Guid AddToSnapshotSet(string drive);
/// <summary>
/// Completes the snapshot after the backup has completed
/// </summary>
void BackupComplete();
/// <summary>
/// Releases all resources held by the snapshot
/// </summary>
/// <param name="shadowId">The snapshot shadow id</param>
/// <param name="forceDelete">Flag to choose force deletion</param>
void DeleteSnapshot(Guid shadowId, bool forceDelete);
/// <summary>
/// Parses write metadata
/// </summary>
/// <param name="writers">The writers to return information from</param>
/// <returns>The writer metadata</returns>
IEnumerable<WriterMetaData> ParseWriterMetaData(Guid[] writers);
}
@@ -1,31 +1,30 @@
// Copyright (C) 2025, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using Duplicati.Library.Common;
using Duplicati.Library.Common.IO;
// Copyright (C) 2025, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Versioning;
namespace Duplicati.Library.Snapshots
namespace Duplicati.Library.Snapshots.Windows
{
public class MSSQLDB : IEquatable<MSSQLDB>
{
@@ -152,14 +151,14 @@ namespace Duplicati.Library.Snapshots
/// For all MS SQL databases it enumerate all associated paths using VSS data
/// </summary>
/// <returns>A collection of DBs and paths</returns>
public void QueryDBsInfo()
public void QueryDBsInfo(SnapshotProvider provider)
{
if (!IsMSSQLInstalled)
return;
m_DBs.Clear();
using (var vssBackupComponents = new VssBackupComponents())
using (var vssBackupComponents = new SnapshotManager(provider))
{
var writerGUIDS = new [] { MSSQLWriterGuid };
try
@@ -21,13 +21,16 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.Versioning;
using Alphaleonis.Win32.Vss;
using Duplicati.Library.Common.IO;
namespace Duplicati.Library.Common.IO
namespace Duplicati.Library.Snapshots.Windows
{
/// <summary>
/// Metadata from a writer instance
/// </summary>
public class WriterMetaData
{
public string Name { get; set; }
@@ -36,16 +39,21 @@ namespace Duplicati.Library.Common.IO
public List<string> Paths { get; set; }
}
/// <summary>
/// The manager for the Windows snapshot
/// </summary>
[SupportedOSPlatform("windows")]
public class VssBackupComponents : IDisposable
public class SnapshotManager : IDisposable
{
/// <summary>
/// The tag used for logging messages
/// </summary>
public static readonly string LOGTAG = Logging.Log.LogTagFromType<VssBackupComponents>();
private static readonly string LOGTAG = Logging.Log.LogTagFromType<SnapshotManager>();
private IVssBackupComponents _vssBackupComponents;
/// <summary>
/// The snapshot implementation
/// </summary>
private ISnapshotProvider _snapshotProvider;
/// <summary>
/// The list of snapshot ids for each volume, key is the path root, e.g. C:\.
@@ -69,26 +77,39 @@ namespace Duplicati.Library.Common.IO
/// </summary>
private List<DefineDosDevice> _mappedDrives;
public VssBackupComponents()
/// <summary>
/// Creates a new snapshot manager
/// </summary>
/// <param name="provider">The provider to use</param>
public SnapshotManager(SnapshotProvider provider)
{
_vssBackupComponents = VssBackupComponentsHelper.GetVssBackupComponents();
_snapshotProvider = provider switch {
SnapshotProvider.AlphaVSS => AlphaVssBackup.Create(),
SnapshotProvider.Wmic => new WmicVssBackup(),
_ => throw new ArgumentException($"Invalid provider: {provider}")
};
}
/// <summary>
/// Setup writers on the snapshot
/// </summary>
/// <param name="includedWriters">The explicitly included writers</param>
/// <param name="excludedWriters">The explicitly excluded writers</param>
public void SetupWriters(Guid[] includedWriters, Guid[] excludedWriters)
{
if (includedWriters != null && includedWriters.Length > 0)
_vssBackupComponents.EnableWriterClasses(includedWriters);
_snapshotProvider.EnableWriterClasses(includedWriters);
if (excludedWriters != null && excludedWriters.Length > 0)
_vssBackupComponents.DisableWriterClasses(excludedWriters);
_snapshotProvider.DisableWriterClasses(excludedWriters);
try
{
_vssBackupComponents.GatherWriterMetadata();
_snapshotProvider.GatherWriterMetadata();
}
finally
{
_vssBackupComponents.FreeWriterMetadata();
_snapshotProvider.FreeWriterMetadata();
}
if (includedWriters == null)
@@ -97,15 +118,12 @@ namespace Duplicati.Library.Common.IO
}
// check if writers got enabled
foreach (var writerGUID in includedWriters)
{
if (!_vssBackupComponents.WriterMetadata.Any(o => o.WriterId.Equals(writerGUID)))
{
throw new Exception(string.Format("Writer with GUID {0} was not added to VSS writer set.", writerGUID.ToString()));
}
}
_snapshotProvider.VerifyWriters(includedWriters);
}
/// <summary>
/// Maps drives from snapshot paths
/// </summary>
public void MapDrives()
{
_mappedDrives = new List<DefineDosDevice>();
@@ -124,6 +142,9 @@ namespace Duplicati.Library.Common.IO
}
}
/// <summary>
/// Makes a map of the snapshots for easy lookup
/// </summary>
public void MapVolumesToSnapShots()
{
_volumeMap = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
@@ -133,43 +154,17 @@ namespace Duplicati.Library.Common.IO
// Attempting to get attributes on this path will fail, so we need to append a double backslash
// Strangely, the double backslash is not needed when acessing a subfolder, here a single backslash is enough,
// but double backslash works for both cases due to path normalization.
_volumeMap.Add(kvp.Key, Util.AppendDirSeparator(_vssBackupComponents.GetSnapshotProperties(kvp.Value).SnapshotDeviceObject) + "\\");
_volumeMap.Add(kvp.Key, Util.AppendDirSeparator(_snapshotProvider.GetSnapshotProperties(kvp.Value).SnapshotDeviceObject) + "\\");
}
_volumeReverseMap = _volumeMap.ToDictionary(x => x.Value, x => x.Key);
}
public Dictionary<string, string> SnapshotDeviceAndVolumes
{
get
{
return _volumeReverseMap;
}
}
public IEnumerable<string> SnapshotLocalPaths => _volumeMap.Keys;
private List<string> GetPathsFromComponent(IVssWMComponent component)
{
var paths = new List<string>();
foreach (var file in component.Files)
{
if (file.FileSpecification.Contains("*"))
{
if (Directory.Exists(Util.AppendDirSeparator(file.Path)))
paths.Add(Util.AppendDirSeparator(file.Path));
}
else
{
var fileWithSpec = SystemIO.IO_OS.PathCombine(file.Path, file.FileSpecification);
if (File.Exists(fileWithSpec))
paths.Add(fileWithSpec);
}
}
return paths;
}
/// <summary>
/// Returns the map of snapshot path and logical path
/// </summary>
public Dictionary<string, string> SnapshotDeviceAndVolumes
=> _volumeReverseMap;
/// <summary>
/// Enumerable to iterate over components of the given writers.
@@ -178,38 +173,30 @@ namespace Duplicati.Library.Common.IO
/// <returns>Anonymous object containing GUID, component name, logical path and paths of the component.</returns>
/// <param name="writers">Writers.</param>
public IEnumerable<WriterMetaData> ParseWriterMetaData(Guid[] writers)
{
// check if writers got enabled
foreach (var writerGUID in writers)
{
var writer = _vssBackupComponents.WriterMetadata.First(o => o.WriterId.Equals(writerGUID));
foreach (var component in writer.Components)
{
yield return new WriterMetaData
{
Guid = writerGUID,
Name = component.ComponentName,
LogicalPath = component.LogicalPath,
Paths = GetPathsFromComponent(component)
};
}
}
}
=> _snapshotProvider.ParseWriterMetaData(writers);
/// <summary>
/// Prepares the snapshot and notifies all writers
/// </summary>
/// <param name="sources">The soruces to include</param>
public void InitShadowVolumes(IEnumerable<string> sources)
{
_vssBackupComponents.StartSnapshotSet();
_snapshotProvider.StartSnapshotSet();
CheckSupportedVolumes(sources);
CheckAndAddSupportedVolumes(sources);
//Make all writers aware that we are going to do the backup
_vssBackupComponents.PrepareForBackup();
_snapshotProvider.PrepareForBackup();
//Create the shadow volumes
_vssBackupComponents.DoSnapshotSet();
_snapshotProvider.DoSnapshotSet();
}
/// <summary>
/// Gets the snapshot path from the local path
/// </summary>
/// <param name="path">The local path</param>
/// <returns>The snapshot path</returns>
public string GetVolumeFromCache(string path)
{
if (!_volumeMap.TryGetValue(path, out var volumePath))
@@ -218,7 +205,11 @@ namespace Duplicati.Library.Common.IO
return volumePath;
}
public void CheckSupportedVolumes(IEnumerable<string> sources)
/// <summary>
/// Checks if the volumes can be added and creates snapshots as needed
/// </summary>
/// <param name="sources">The sources to create snapshots for</param>
private void CheckAndAddSupportedVolumes(IEnumerable<string> sources)
{
//Figure out which volumes are in the set
_volumes = new Dictionary<string, Guid>(StringComparer.OrdinalIgnoreCase);
@@ -227,16 +218,17 @@ namespace Duplicati.Library.Common.IO
var drive = SystemIO.IO_OS.GetPathRoot(s);
if (!_volumes.ContainsKey(drive))
{
if (!_vssBackupComponents.IsVolumeSupported(drive))
if (!_snapshotProvider.IsVolumeSupported(drive))
{
throw new VssVolumeNotSupportedException(drive);
}
_volumes.Add(drive, _vssBackupComponents.AddToSnapshotSet(drive));
_volumes.Add(drive, _snapshotProvider.AddToSnapshotSet(drive));
}
}
}
/// <inheritdoc/>
public void Dispose()
{
try
@@ -258,7 +250,7 @@ namespace Duplicati.Library.Common.IO
try
{
_vssBackupComponents?.BackupComplete();
_snapshotProvider?.BackupComplete();
}
catch (Exception ex)
{
@@ -267,13 +259,13 @@ namespace Duplicati.Library.Common.IO
try
{
if (_vssBackupComponents != null)
if (_snapshotProvider != null)
{
foreach (var g in _volumes.Values)
{
try
{
_vssBackupComponents.DeleteSnapshot(g, false);
_snapshotProvider.DeleteSnapshot(g, false);
}
catch (Exception ex)
{
@@ -287,35 +279,11 @@ namespace Duplicati.Library.Common.IO
Logging.Log.WriteVerboseMessage(LOGTAG, "VSSSnapShotDeleteCleanError", ex, "Failed during VSS esnapshot closing");
}
if (_vssBackupComponents != null)
if (_snapshotProvider != null)
{
_vssBackupComponents.Dispose();
_vssBackupComponents = null;
_snapshotProvider.Dispose();
_snapshotProvider = null;
}
}
}
public static class VssBackupComponentsHelper
{
public static IVssBackupComponents GetVssBackupComponents()
{
//Prepare the backup
IVssBackupComponents vssBackupComponents = CreateVssBackupComponents();
vssBackupComponents.InitializeForBackup(null);
vssBackupComponents.SetContext(VssSnapshotContext.Backup);
vssBackupComponents.SetBackupState(false, true, VssBackupType.Full, false);
return vssBackupComponents;
}
public static IVssBackupComponents CreateVssBackupComponents()
{
var vss = VssFactoryProvider.Default.GetVssFactory();
if (vss == null)
throw new InvalidOperationException();
return vss.CreateVssBackupComponents();
}
}
}
@@ -0,0 +1,215 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.Versioning;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
#nullable enable
namespace Duplicati.Library.Snapshots.Windows;
/// <summary>
/// A shadow copy manager using the wmic commandline tool
/// </summary>
[SupportedOSPlatform("windows")]
internal class WmicShadowCopyManager : IDisposable
{
/// <summary>
/// The tag used for logging messages
/// </summary>
public static readonly string LOGTAG = Logging.Log.LogTagFromType<SnapshotManager>();
/// <summary>
/// A single shadow copy
/// </summary>
/// <param name="shadowId">The shadow ID as a string</param>
/// <param name="parsedId">The shadow ID as a GUID</param>
/// <param name="originalDrive">The drive that the snapshot is for</param>
/// <param name="mappedPath">The path that contains the snapshot</param>
public class WmicShadowCopy(string shadowId, Guid parsedId, string originalDrive, string mappedPath) : IDisposable
{
/// <summary>
/// Gets the shadow ID
/// </summary>
public string ShadowID { get; } = shadowId;
/// <summary>
/// Gets the shadow ID
/// </summary>
public Guid ParsedId { get; } = parsedId;
/// <summary>
/// Gets the drive that was originally mapped
/// </summary>
public string OriginalDrive { get; } = originalDrive;
/// <summary>
/// Gets the path where the snapshot is found
/// </summary>
public string MappedPath { get; } = mappedPath;
/// <summary>
/// Flag keeping track of the snapshot deletion state
/// </summary>
private bool _snapshotDeleted;
/// <inheritdoc/>
public void Dispose()
{
DeleteShadowCopy();
}
/// <summary>
/// Deletes the shadow copy
/// </summary>
private void DeleteShadowCopy()
{
if (_snapshotDeleted)
return;
if (!string.IsNullOrEmpty(ShadowID))
{
_snapshotDeleted = true;
Logging.Log.WriteVerboseMessage(LOGTAG, "DeleteShadowCopy", $"Deleting Shadow Copy: {ShadowID}");
DeleteShadow(ShadowID);
}
}
}
/// <summary>
/// The list of the currently registered shadow copies
/// </summary>
private List<WmicShadowCopy> _shadowCopies = new List<WmicShadowCopy>();
/// <summary>
/// Gets the list of the currently registered shadow copies
/// </summary>
public IEnumerable<WmicShadowCopy> ShadowCopies => _shadowCopies;
/// <summary>
/// Creates a new snapshot for the given drive
/// </summary>
/// <param name="drive">The drive to create the snapshot for</param>
/// <returns>The created snapshot</returns>
public WmicShadowCopy Add(string drive)
{
var shadowId = CreateShadowCopy(drive);
if (string.IsNullOrEmpty(shadowId))
throw new InvalidOperationException("Failed to create shadow copy");
var shadowPath = GetShadowPath(shadowId);
if (string.IsNullOrEmpty(shadowPath))
{
DeleteShadow(shadowId);
throw new InvalidOperationException("Failed to get shadow copy path");
}
var snapshot = new WmicShadowCopy(shadowId, Guid.Parse(shadowId), drive, shadowPath);
_shadowCopies.Add(snapshot);
return snapshot;
}
/// <inheritdoc />
public void Dispose()
{
foreach (var shadow in ShadowCopies)
{
shadow.Dispose();
}
_shadowCopies.Clear();
}
/// <summary>
/// Creates a shadow copy
/// </summary>
/// <param name="drive">The drive to create the snapshot for</param>
/// <returns>The shadow id</returns>
private static string? CreateShadowCopy(string drive)
{
string output = ExecuteCommand("wmic", $"shadowcopy call create Volume='{drive}'", 10000);
// Extract ShadowID using regex
Match match = Regex.Match(output, @"ShadowID\s*=\s*""({[0-9A-F\-]+})""");
if (match.Success)
{
return match.Groups[1].Value;
}
Logging.Log.WriteErrorMessage(LOGTAG, "ShadowCopyFailed", null, "Failed to create shadow copy for {0}: {1}", drive, output);
return null;
}
/// <summary>
/// Gets the path where the shadow copy is mounted
/// </summary>
/// <param name="shadowId">The shadow copy id</param>
/// <returns>The path where the copy is mounted</returns>
private static string? GetShadowPath(string shadowId)
{
string output = ExecuteCommand("wmic", "shadowcopy get ID, DeviceObject", 5000);
// Extract DeviceObject corresponding to the ShadowID
string pattern = $@"(\\\\\?\\GLOBALROOT\\Device\\HarddiskVolumeShadowCopy\d+)\s+{shadowId}";
Match match = Regex.Match(output, pattern);
if (match.Success)
{
return match.Groups[1].Value;
}
Logging.Log.WriteErrorMessage(LOGTAG, "ShadowCopyFailed", null, "Failed to get shadow copy path for {0}: {1}", shadowId, output);
return null;
}
/// <summary>
/// Delete a shadow copy
/// </summary>
/// <param name="shadowId">The shadow id</param>
private static void DeleteShadow(string shadowId)
=> ExecuteCommand("wmic", $"shadowcopy where ID=\"{shadowId}\" delete", 5000);
/// <summary>
/// Executes a command
/// </summary>
/// <param name="fileName">The binary to execute</param>
/// <param name="arguments">The arguments to use</param>
/// <param name="timeoutMs">The timeout</param>
/// <returns>The output of the command</returns>
private static string ExecuteCommand(string fileName, string arguments, int timeoutMs)
{
try
{
using (var process = new Process())
{
process.StartInfo.FileName = fileName;
process.StartInfo.Arguments = arguments;
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.RedirectStandardError = true;
process.StartInfo.UseShellExecute = false;
process.StartInfo.CreateNoWindow = true;
process.Start();
var outputTask = process.StandardOutput.ReadToEndAsync();
var errorTask = process.StandardError.ReadToEndAsync();
if (!process.WaitForExit(timeoutMs))
{
process.Kill();
throw new TimeoutException($"Command '{fileName} {arguments}' timed out after {timeoutMs}ms.");
}
var output = outputTask.Result;
var error = errorTask.Result;
if (!string.IsNullOrWhiteSpace(error))
Logging.Log.WriteWarningMessage(LOGTAG, "ShadowCopyFailed", null, "Failed to execute command: {0} {1}: {2}", fileName, arguments, error);
return output;
}
}
catch (Exception ex)
{
Logging.Log.WriteErrorMessage(LOGTAG, "ShadowCopyFailed", ex, "Failed to execute command: {0} {1}", fileName, arguments);
return string.Empty;
}
}
}
@@ -0,0 +1,139 @@
// Copyright (C) 2025, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Management;
using System.Runtime.Versioning;
namespace Duplicati.Library.Snapshots.Windows;
/// <summary>
/// Snapshot provider based on WMIC
/// </summary>
[SupportedOSPlatform("windows")]
internal class WmicVssBackup : ISnapshotProvider
{
/// <summary>
/// The manager that contains the actual interface to WMIC
/// </summary>
private WmicShadowCopyManager _manager = new WmicShadowCopyManager();
/// <inheritdoc/>
public Guid AddToSnapshotSet(string drive)
=> _manager.Add(drive).ParsedId;
/// <inheritdoc/>
public void BackupComplete()
{
}
/// <inheritdoc/>
public void DeleteSnapshot(Guid shadowId, bool forceDelete)
{
foreach (var x in _manager.ShadowCopies.Where(x => x.ParsedId == shadowId))
x.Dispose();
}
/// <inheritdoc/>
public void DisableWriterClasses(Guid[] guids)
{
}
/// <inheritdoc/>
public void Dispose()
{
_manager.Dispose();
}
/// <inheritdoc/>
public void DoSnapshotSet()
{
}
/// <inheritdoc/>
public void EnableWriterClasses(Guid[] guids)
{
}
/// <inheritdoc/>
public void FreeWriterMetadata()
{
}
/// <inheritdoc/>
public void GatherWriterMetadata()
{
}
/// <summary>
/// Stub for returning the snapshot info
/// </summary>
/// <param name="SnapshotDeviceObject">The path where the snapshot is located</param>
private sealed record SnapshotInfo (string SnapshotDeviceObject) : ISnapshotInfo;
/// <inheritdoc/>
public ISnapshotInfo GetSnapshotProperties(Guid shadowId)
=> new SnapshotInfo(_manager.ShadowCopies.First(x => x.ParsedId == shadowId).MappedPath);
/// <inheritdoc/>
public bool IsVolumeSupported(string drive)
{
if (string.IsNullOrWhiteSpace(drive))
return false;
try
{
var query = $"SELECT * FROM Win32_ShadowCopy WHERE VolumeName LIKE '%{drive[0]}%'";
var searcher = new ManagementObjectSearcher(query);
var results = searcher.Get();
return results.Count > 0;
}
catch (Exception ex)
{
Console.WriteLine($"Error checking VSS support: {ex.Message}");
return false;
}
}
/// <inheritdoc/>
public IEnumerable<WriterMetaData> ParseWriterMetaData(Guid[] writers)
=> Enumerable.Empty<WriterMetaData>();
/// <inheritdoc/>
public void PrepareForBackup()
{
}
/// <inheritdoc/>
public void StartSnapshotSet()
{
}
/// <inheritdoc/>
public void VerifyWriters(Guid[] guids)
{
}
}
+11 -10
View File
@@ -19,7 +19,6 @@
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System;
using System.Collections.Generic;
using System.IO;
@@ -27,6 +26,7 @@ using System.Linq;
using System.Runtime.Versioning;
using Duplicati.Library.Common.IO;
using Duplicati.Library.Interface;
using Duplicati.Library.Snapshots.Windows;
namespace Duplicati.Library.Snapshots
{
@@ -53,7 +53,7 @@ namespace Duplicati.Library.Snapshots
/// <summary>
/// The main reference to the backup controller
/// </summary>
private readonly VssBackupComponents _vssBackupComponents;
private readonly SnapshotManager _snapshotManager;
/// <summary>
/// The source folders included in the snapshot
@@ -73,7 +73,8 @@ namespace Duplicati.Library.Snapshots
_sourceEntries = sources.Select(SystemIOWindows.RemoveExtendedDevicePathPrefix).ToList();
try
{
_vssBackupComponents = new VssBackupComponents();
var provider = Utility.Utility.ParseEnumOption(options.AsReadOnly(), "snapshot-provider", SnapshotProvider.AlphaVSS);
_snapshotManager = new SnapshotManager(provider);
// Default to exclude the System State writer
var excludedWriters = new Guid[] { new Guid("{e8132975-6f93-4464-a53e-1050253ae220}") };
@@ -85,16 +86,16 @@ namespace Duplicati.Library.Snapshots
.Select(x => new Guid(x))
.ToArray();
}
_vssBackupComponents.SetupWriters(null, excludedWriters);
_snapshotManager.SetupWriters(null, excludedWriters);
_vssBackupComponents.InitShadowVolumes(_sourceEntries);
_snapshotManager.InitShadowVolumes(_sourceEntries);
_vssBackupComponents.MapVolumesToSnapShots();
_snapshotManager.MapVolumesToSnapShots();
//If we should map the drives, we do that now and update the volumeMap
if (Utility.Utility.ParseBoolOption(options.AsReadOnly(), "vss-use-mapping"))
{
_vssBackupComponents.MapDrives();
_snapshotManager.MapDrives();
}
}
catch (Exception ex1)
@@ -279,7 +280,7 @@ namespace Duplicati.Library.Snapshots
if (!Path.IsPathRooted(snapshotPath))
throw new InvalidOperationException();
foreach (var kvp in _vssBackupComponents.SnapshotDeviceAndVolumes)
foreach (var kvp in _snapshotManager.SnapshotDeviceAndVolumes)
{
if (snapshotPath.StartsWith(kvp.Key, Utility.Utility.ClientFilenameStringComparison))
return IO_WIN.PathCombine(kvp.Value, snapshotPath.Substring(kvp.Key.Length));
@@ -298,7 +299,7 @@ namespace Duplicati.Library.Snapshots
throw new InvalidOperationException();
var root = IO_WIN.GetPathRoot(localPath);
var volumePath = _vssBackupComponents.GetVolumeFromCache(root);
var volumePath = _snapshotManager.GetVolumeFromCache(root);
// Note that using a simple Path.Combine() for the following code
// can result in invalid snapshot paths; e.g., if localPath is
@@ -333,7 +334,7 @@ namespace Duplicati.Library.Snapshots
{
if (disposing)
{
_vssBackupComponents?.Dispose();
_snapshotManager?.Dispose();
}
base.Dispose(disposing);
@@ -21,6 +21,7 @@
using System.Runtime.Versioning;
using System.Security.Principal;
using Duplicati.Library.Snapshots;
using Duplicati.Library.Snapshots.Windows;
using Duplicati.WebserverCore.Abstractions;
using Duplicati.WebserverCore.Exceptions;
using Microsoft.AspNetCore.Mvc;
@@ -58,7 +59,7 @@ public class HyperV : IEndpointV1
{
if (string.IsNullOrEmpty(key))
{
hypervUtility.QueryHyperVGuestsInfo();
hypervUtility.QueryHyperVGuestsInfo(SnapshotProvider.AlphaVSS);
return hypervUtility.Guests.Select(x => new Dto.TreeNodeDto()
{
id = x.ID.ToString(),
@@ -77,7 +78,7 @@ public class HyperV : IEndpointV1
}
else
{
hypervUtility.QueryHyperVGuestsInfo(true);
hypervUtility.QueryHyperVGuestsInfo(SnapshotProvider.AlphaVSS, true);
var foundVMs = hypervUtility.Guests.FindAll(x => x.ID.Equals(new Guid(key)));
if (foundVMs.Count != 1)
@@ -21,6 +21,7 @@
using System.Runtime.Versioning;
using System.Security.Principal;
using Duplicati.Library.Snapshots;
using Duplicati.Library.Snapshots.Windows;
using Duplicati.WebserverCore.Abstractions;
using Duplicati.WebserverCore.Exceptions;
using Microsoft.AspNetCore.Mvc;
@@ -56,7 +57,7 @@ public class MSSql : IEndpointV1
try
{
mssqlUtility.QueryDBsInfo();
mssqlUtility.QueryDBsInfo(SnapshotProvider.AlphaVSS);
if (string.IsNullOrEmpty(key))
{