From cf5b4bb24dc5617179df78fea57532ff862fcb95 Mon Sep 17 00:00:00 2001 From: Kenneth Skovhede Date: Wed, 19 Feb 2025 14:00:14 +0100 Subject: [PATCH] Added support for Wmic based Windows VSS snapshots. Refactored code a bit to be easier to understand. --- .../Operation/Backup/CountFilesHandler.cs | 2 +- .../Backup/FileEnumerationProcess.cs | 3 +- .../Library/Main/Operation/BackupHandler.cs | 1 + Duplicati/Library/Main/Options.cs | 19 ++ Duplicati/Library/Main/Strings.cs | 2 + .../Library/Modules/Builtin/HyperVOptions.cs | 43 ++-- .../Library/Modules/Builtin/MSSQLOptions.cs | 45 ++-- Duplicati/Library/Snapshots/Program.cs | 2 - .../Library/Snapshots/SnapshotProvider.cs | 30 +++ .../Library/Snapshots/{ => USN}/USNJournal.cs | 2 +- .../Snapshots/{ => USN}/UsnJournalService.cs | 2 +- .../Library/Snapshots/{ => USN}/Win32USN.cs | 42 ++-- .../Snapshots/Windows/AlphaVssBackup.cs | 200 ++++++++++++++++ .../Snapshots/{ => Windows}/HyperVUtility.cs | 11 +- .../Snapshots/Windows/ISnapshotProvider.cs | 118 ++++++++++ .../Snapshots/{ => Windows}/MSSQLUtility.cs | 49 ++-- .../Windows/SnapshotManager.cs} | 180 ++++++--------- .../Windows/WmicShadowCopyManager.cs | 215 ++++++++++++++++++ .../Snapshots/Windows/WmicVssBackup.cs | 139 +++++++++++ .../Library/Snapshots/WindowsSnapshot.cs | 21 +- .../WebserverCore/Endpoints/V1/HyperV.cs | 5 +- Duplicati/WebserverCore/Endpoints/V1/MSSql.cs | 3 +- 22 files changed, 914 insertions(+), 220 deletions(-) create mode 100644 Duplicati/Library/Snapshots/SnapshotProvider.cs rename Duplicati/Library/Snapshots/{ => USN}/USNJournal.cs (97%) rename Duplicati/Library/Snapshots/{ => USN}/UsnJournalService.cs (97%) rename Duplicati/Library/Snapshots/{ => USN}/Win32USN.cs (97%) create mode 100644 Duplicati/Library/Snapshots/Windows/AlphaVssBackup.cs rename Duplicati/Library/Snapshots/{ => Windows}/HyperVUtility.cs (95%) create mode 100644 Duplicati/Library/Snapshots/Windows/ISnapshotProvider.cs rename Duplicati/Library/Snapshots/{ => Windows}/MSSQLUtility.cs (94%) rename Duplicati/Library/{Common/IO/VssBackupComponents.cs => Snapshots/Windows/SnapshotManager.cs} (61%) create mode 100644 Duplicati/Library/Snapshots/Windows/WmicShadowCopyManager.cs create mode 100644 Duplicati/Library/Snapshots/Windows/WmicVssBackup.cs diff --git a/Duplicati/Library/Main/Operation/Backup/CountFilesHandler.cs b/Duplicati/Library/Main/Operation/Backup/CountFilesHandler.cs index ffefa0abc..1d16f29d9 100644 --- a/Duplicati/Library/Main/Operation/Backup/CountFilesHandler.cs +++ b/Duplicati/Library/Main/Operation/Backup/CountFilesHandler.cs @@ -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 { diff --git a/Duplicati/Library/Main/Operation/Backup/FileEnumerationProcess.cs b/Duplicati/Library/Main/Operation/Backup/FileEnumerationProcess.cs index a7db2b2ce..a181907ff 100644 --- a/Duplicati/Library/Main/Operation/Backup/FileEnumerationProcess.cs +++ b/Duplicati/Library/Main/Operation/Backup/FileEnumerationProcess.cs @@ -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 { diff --git a/Duplicati/Library/Main/Operation/BackupHandler.cs b/Duplicati/Library/Main/Operation/BackupHandler.cs index 84fc89b69..059bbc8df 100644 --- a/Duplicati/Library/Main/Operation/BackupHandler.cs +++ b/Duplicati/Library/Main/Operation/BackupHandler.cs @@ -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 { diff --git a/Duplicati/Library/Main/Options.cs b/Duplicati/Library/Main/Options.cs index c0d681aaa..4d32c8ae3 100644 --- a/Duplicati/Library/Main/Options.cs +++ b/Duplicati/Library/Main/Options.cs @@ -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 } } + /// + /// Gets the snapshot strategy to use + /// + 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; + } + } + /// /// Gets a flag indicating if advisory locking should be ignored /// diff --git a/Duplicati/Library/Main/Strings.cs b/Duplicati/Library/Main/Strings.cs index 6ee298520..862d064c2 100644 --- a/Duplicati/Library/Main/Strings.cs +++ b/Duplicati/Library/Main/Strings.cs @@ -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."); } } diff --git a/Duplicati/Library/Modules/Builtin/HyperVOptions.cs b/Duplicati/Library/Modules/Builtin/HyperVOptions.cs index 2df703c2e..67bda3d9e 100644 --- a/Duplicati/Library/Modules/Builtin/HyperVOptions.cs +++ b/Duplicati/Library/Modules/Builtin/HyperVOptions.cs @@ -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) diff --git a/Duplicati/Library/Modules/Builtin/MSSQLOptions.cs b/Duplicati/Library/Modules/Builtin/MSSQLOptions.cs index d41f84009..058b119c5 100644 --- a/Duplicati/Library/Modules/Builtin/MSSQLOptions.cs +++ b/Duplicati/Library/Modules/Builtin/MSSQLOptions.cs @@ -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) diff --git a/Duplicati/Library/Snapshots/Program.cs b/Duplicati/Library/Snapshots/Program.cs index 6afd2647d..8e48df343 100644 --- a/Duplicati/Library/Snapshots/Program.cs +++ b/Duplicati/Library/Snapshots/Program.cs @@ -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 diff --git a/Duplicati/Library/Snapshots/SnapshotProvider.cs b/Duplicati/Library/Snapshots/SnapshotProvider.cs new file mode 100644 index 000000000..c1e8984fa --- /dev/null +++ b/Duplicati/Library/Snapshots/SnapshotProvider.cs @@ -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 + } +} diff --git a/Duplicati/Library/Snapshots/USNJournal.cs b/Duplicati/Library/Snapshots/USN/USNJournal.cs similarity index 97% rename from Duplicati/Library/Snapshots/USNJournal.cs rename to Duplicati/Library/Snapshots/USN/USNJournal.cs index 2608395d5..73346ae59 100644 --- a/Duplicati/Library/Snapshots/USNJournal.cs +++ b/Duplicati/Library/Snapshots/USN/USNJournal.cs @@ -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 { /// /// Class that encapsulates USN journal access to a single volume diff --git a/Duplicati/Library/Snapshots/UsnJournalService.cs b/Duplicati/Library/Snapshots/USN/UsnJournalService.cs similarity index 97% rename from Duplicati/Library/Snapshots/UsnJournalService.cs rename to Duplicati/Library/Snapshots/USN/UsnJournalService.cs index 8f1298472..d44bad907 100644 --- a/Duplicati/Library/Snapshots/UsnJournalService.cs +++ b/Duplicati/Library/Snapshots/USN/UsnJournalService.cs @@ -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 diff --git a/Duplicati/Library/Snapshots/Win32USN.cs b/Duplicati/Library/Snapshots/USN/Win32USN.cs similarity index 97% rename from Duplicati/Library/Snapshots/Win32USN.cs rename to Duplicati/Library/Snapshots/USN/Win32USN.cs index ed40e8c3f..3a9ebf7c2 100644 --- a/Duplicati/Library/Snapshots/Win32USN.cs +++ b/Duplicati/Library/Snapshots/USN/Win32USN.cs @@ -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 diff --git a/Duplicati/Library/Snapshots/Windows/AlphaVssBackup.cs b/Duplicati/Library/Snapshots/Windows/AlphaVssBackup.cs new file mode 100644 index 000000000..85d65f49b --- /dev/null +++ b/Duplicati/Library/Snapshots/Windows/AlphaVssBackup.cs @@ -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; + +/// +/// Implementation of a snapshot, using AlphaVSS +/// +internal class AlphaVssBackup : ISnapshotProvider +{ + /// + /// The implementation that is being wrapped + /// + private IVssBackupComponents _components; + /// + /// Creates a new snapshot implementation + /// + /// The components to use + private AlphaVssBackup(IVssBackupComponents components) + { + _components = components; + } + + /// + public void EnableWriterClasses(Guid[] guids) + => _components.EnableWriterClasses(guids); + + /// + public void DisableWriterClasses(Guid[] guids) + => _components.DisableWriterClasses(guids); + + /// + public void GatherWriterMetadata() + => _components.GatherWriterMetadata(); + /// + public void FreeWriterMetadata() + => _components.FreeWriterMetadata(); + + /// + 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())); + } + } + } + + /// + /// Stub for returning the snapshot info + /// + /// The path where the snapshot is located + private sealed record SnapshotInfo(string SnapshotDeviceObject) : ISnapshotInfo; + + /// + public ISnapshotInfo GetSnapshotProperties(Guid shadowId) + => new SnapshotInfo(_components.GetSnapshotProperties(shadowId).SnapshotDeviceObject); + + /// + public void StartSnapshotSet() + => _components.StartSnapshotSet(); + + /// + public void PrepareForBackup() + => _components.PrepareForBackup(); + + /// + public void DoSnapshotSet() + => _components.DoSnapshotSet(); + + /// + public bool IsVolumeSupported(string drive) + => _components.IsVolumeSupported(drive); + + /// + public Guid AddToSnapshotSet(string drive) + => _components.AddToSnapshotSet(drive); + + /// + public void BackupComplete() + => _components.BackupComplete(); + + /// + public void DeleteSnapshot(Guid shadowId, bool forceDelete) + => _components.DeleteSnapshot(shadowId, forceDelete); + + /// + public static ISnapshotProvider Create() + => new AlphaVssBackup(GetVssBackupComponents()); + + /// + /// Creates a new AlphaVssBackupComponents instance and adds the context + /// + /// The configured instance + 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; + } + + /// + /// Loads the AlphaVSS implementation for the current OS + /// + /// The implementation + private static IVssBackupComponents CreateVssBackupComponents() + { + var vss = VssFactoryProvider.Default.GetVssFactory(); + if (vss == null) + throw new InvalidOperationException(); + + return vss.CreateVssBackupComponents(); + } + + /// + public IEnumerable 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) + }; + } + } + } + + /// + /// Returns the paths from the writer metadata + /// + /// The component to get the paths from + /// The list of paths + private List GetPathsFromComponent(IVssWMComponent component) + { + var paths = new List(); + + 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; + + } + + /// + public void Dispose() + { + _components.Dispose(); + } +} \ No newline at end of file diff --git a/Duplicati/Library/Snapshots/HyperVUtility.cs b/Duplicati/Library/Snapshots/Windows/HyperVUtility.cs similarity index 95% rename from Duplicati/Library/Snapshots/HyperVUtility.cs rename to Duplicati/Library/Snapshots/Windows/HyperVUtility.cs index 4e7b268a6..9e0f16524 100644 --- a/Duplicati/Library/Snapshots/HyperVUtility.cs +++ b/Duplicati/Library/Snapshots/Windows/HyperVUtility.cs @@ -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 { @@ -160,8 +160,9 @@ namespace Duplicati.Library.Snapshots /// Query Hyper-V for all Virtual Machines info /// /// Specify if returned data should contain VM paths + /// The provider to use for VSS /// List of Hyper-V Machines - 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 /// /// A collection of VMs and paths - private static IEnumerable GetAllVMsPathsVSS() + private static IEnumerable GetAllVMsPathsVSS(SnapshotProvider provider) { - using (var vssBackupComponents = new VssBackupComponents()) + using (var vssBackupComponents = new SnapshotManager(provider)) { var writerGUIDS = new[] { HyperVWriterGuid }; diff --git a/Duplicati/Library/Snapshots/Windows/ISnapshotProvider.cs b/Duplicati/Library/Snapshots/Windows/ISnapshotProvider.cs new file mode 100644 index 000000000..db6a0965a --- /dev/null +++ b/Duplicati/Library/Snapshots/Windows/ISnapshotProvider.cs @@ -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; + +/// +/// Interface for the snapshot information +/// +internal interface ISnapshotInfo +{ + /// + /// The path where the snapshot is located + /// + string SnapshotDeviceObject { get; } +} + +/// +/// Interface for a snapshot provider +/// +internal interface ISnapshotProvider : IDisposable +{ + /// + /// Explicitly enables the selected writer GUIDs + /// + /// The GUIDs to enable + void EnableWriterClasses(Guid[] guids); + /// + /// Explicitly disable the selected writer GUIDs + /// + /// The GUIDs to disable + void DisableWriterClasses(Guid[] guids); + /// + /// Gather writer metadata prior to the snapshot + /// + void GatherWriterMetadata(); + /// + /// Release any allocated metadata + /// + void FreeWriterMetadata(); + /// + /// Verify that the writes are enabled on the snapshot + /// + /// The writer GUIDs to verify as enabled + void VerifyWriters(Guid[] guids); + + /// + /// Gets information of a snapshot + /// + /// The shadow copy id + /// The snapshot information + ISnapshotInfo GetSnapshotProperties(Guid shadowId); + + /// + /// Creates a snapshot + /// + void StartSnapshotSet(); + /// + /// Starts the snapshot as a backup snapshot + /// + void PrepareForBackup(); + /// + /// Finalizes the snapshot creation + /// + void DoSnapshotSet(); + /// + /// Checks if the volume is supported by the snapshot + /// + /// The drive to check + /// true if the volume is supported; false otherwise + bool IsVolumeSupported(string drive); + /// + /// Adds a drive to the snapshot set + /// + /// The drive to add + /// The shadow id + Guid AddToSnapshotSet(string drive); + + /// + /// Completes the snapshot after the backup has completed + /// + void BackupComplete(); + /// + /// Releases all resources held by the snapshot + /// + /// The snapshot shadow id + /// Flag to choose force deletion + void DeleteSnapshot(Guid shadowId, bool forceDelete); + + /// + /// Parses write metadata + /// + /// The writers to return information from + /// The writer metadata + IEnumerable ParseWriterMetaData(Guid[] writers); +} \ No newline at end of file diff --git a/Duplicati/Library/Snapshots/MSSQLUtility.cs b/Duplicati/Library/Snapshots/Windows/MSSQLUtility.cs similarity index 94% rename from Duplicati/Library/Snapshots/MSSQLUtility.cs rename to Duplicati/Library/Snapshots/Windows/MSSQLUtility.cs index 896c24686..7cbf8444e 100644 --- a/Duplicati/Library/Snapshots/MSSQLUtility.cs +++ b/Duplicati/Library/Snapshots/Windows/MSSQLUtility.cs @@ -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 { @@ -152,14 +151,14 @@ namespace Duplicati.Library.Snapshots /// For all MS SQL databases it enumerate all associated paths using VSS data /// /// A collection of DBs and paths - 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 diff --git a/Duplicati/Library/Common/IO/VssBackupComponents.cs b/Duplicati/Library/Snapshots/Windows/SnapshotManager.cs similarity index 61% rename from Duplicati/Library/Common/IO/VssBackupComponents.cs rename to Duplicati/Library/Snapshots/Windows/SnapshotManager.cs index 8d8619a2a..78d21fa91 100644 --- a/Duplicati/Library/Common/IO/VssBackupComponents.cs +++ b/Duplicati/Library/Snapshots/Windows/SnapshotManager.cs @@ -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 { + /// + /// Metadata from a writer instance + /// public class WriterMetaData { public string Name { get; set; } @@ -36,16 +39,21 @@ namespace Duplicati.Library.Common.IO public List Paths { get; set; } } + /// + /// The manager for the Windows snapshot + /// [SupportedOSPlatform("windows")] - public class VssBackupComponents : IDisposable + public class SnapshotManager : IDisposable { /// /// The tag used for logging messages /// - public static readonly string LOGTAG = Logging.Log.LogTagFromType(); + private static readonly string LOGTAG = Logging.Log.LogTagFromType(); - - private IVssBackupComponents _vssBackupComponents; + /// + /// The snapshot implementation + /// + private ISnapshotProvider _snapshotProvider; /// /// 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 /// private List _mappedDrives; - public VssBackupComponents() + /// + /// Creates a new snapshot manager + /// + /// The provider to use + public SnapshotManager(SnapshotProvider provider) { - _vssBackupComponents = VssBackupComponentsHelper.GetVssBackupComponents(); + _snapshotProvider = provider switch { + SnapshotProvider.AlphaVSS => AlphaVssBackup.Create(), + SnapshotProvider.Wmic => new WmicVssBackup(), + _ => throw new ArgumentException($"Invalid provider: {provider}") + }; } + /// + /// Setup writers on the snapshot + /// + /// The explicitly included writers + /// The explicitly excluded writers 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); } + /// + /// Maps drives from snapshot paths + /// public void MapDrives() { _mappedDrives = new List(); @@ -124,6 +142,9 @@ namespace Duplicati.Library.Common.IO } } + /// + /// Makes a map of the snapshots for easy lookup + /// public void MapVolumesToSnapShots() { _volumeMap = new Dictionary(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 SnapshotDeviceAndVolumes - { - get - { - return _volumeReverseMap; - } - } - - public IEnumerable SnapshotLocalPaths => _volumeMap.Keys; - - private List GetPathsFromComponent(IVssWMComponent component) - { - var paths = new List(); - - 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; - - } + /// + /// Returns the map of snapshot path and logical path + /// + public Dictionary SnapshotDeviceAndVolumes + => _volumeReverseMap; /// /// Enumerable to iterate over components of the given writers. @@ -178,38 +173,30 @@ namespace Duplicati.Library.Common.IO /// Anonymous object containing GUID, component name, logical path and paths of the component. /// Writers. public IEnumerable 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); + /// + /// Prepares the snapshot and notifies all writers + /// + /// The soruces to include public void InitShadowVolumes(IEnumerable 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(); } + /// + /// Gets the snapshot path from the local path + /// + /// The local path + /// The snapshot path 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 sources) + /// + /// Checks if the volumes can be added and creates snapshots as needed + /// + /// The sources to create snapshots for + private void CheckAndAddSupportedVolumes(IEnumerable sources) { //Figure out which volumes are in the set _volumes = new Dictionary(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)); } } } + /// 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(); - } - } } diff --git a/Duplicati/Library/Snapshots/Windows/WmicShadowCopyManager.cs b/Duplicati/Library/Snapshots/Windows/WmicShadowCopyManager.cs new file mode 100644 index 000000000..45a1ddd6b --- /dev/null +++ b/Duplicati/Library/Snapshots/Windows/WmicShadowCopyManager.cs @@ -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; + +/// +/// A shadow copy manager using the wmic commandline tool +/// +[SupportedOSPlatform("windows")] +internal class WmicShadowCopyManager : IDisposable +{ + /// + /// The tag used for logging messages + /// + public static readonly string LOGTAG = Logging.Log.LogTagFromType(); + + /// + /// A single shadow copy + /// + /// The shadow ID as a string + /// The shadow ID as a GUID + /// The drive that the snapshot is for + /// The path that contains the snapshot + public class WmicShadowCopy(string shadowId, Guid parsedId, string originalDrive, string mappedPath) : IDisposable + { + /// + /// Gets the shadow ID + /// + public string ShadowID { get; } = shadowId; + /// + /// Gets the shadow ID + /// + public Guid ParsedId { get; } = parsedId; + /// + /// Gets the drive that was originally mapped + /// + public string OriginalDrive { get; } = originalDrive; + /// + /// Gets the path where the snapshot is found + /// + public string MappedPath { get; } = mappedPath; + + /// + /// Flag keeping track of the snapshot deletion state + /// + private bool _snapshotDeleted; + + /// + public void Dispose() + { + DeleteShadowCopy(); + } + + /// + /// Deletes the shadow copy + /// + private void DeleteShadowCopy() + { + if (_snapshotDeleted) + return; + if (!string.IsNullOrEmpty(ShadowID)) + { + _snapshotDeleted = true; + Logging.Log.WriteVerboseMessage(LOGTAG, "DeleteShadowCopy", $"Deleting Shadow Copy: {ShadowID}"); + DeleteShadow(ShadowID); + } + } + } + + /// + /// The list of the currently registered shadow copies + /// + private List _shadowCopies = new List(); + + /// + /// Gets the list of the currently registered shadow copies + /// + public IEnumerable ShadowCopies => _shadowCopies; + + /// + /// Creates a new snapshot for the given drive + /// + /// The drive to create the snapshot for + /// The created snapshot + 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; + } + + /// + public void Dispose() + { + foreach (var shadow in ShadowCopies) + { + shadow.Dispose(); + } + _shadowCopies.Clear(); + } + + /// + /// Creates a shadow copy + /// + /// The drive to create the snapshot for + /// The shadow id + 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; + } + + /// + /// Gets the path where the shadow copy is mounted + /// + /// The shadow copy id + /// The path where the copy is mounted + 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; + } + + /// + /// Delete a shadow copy + /// + /// The shadow id + private static void DeleteShadow(string shadowId) + => ExecuteCommand("wmic", $"shadowcopy where ID=\"{shadowId}\" delete", 5000); + + /// + /// Executes a command + /// + /// The binary to execute + /// The arguments to use + /// The timeout + /// The output of the command + 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; + } + } +} \ No newline at end of file diff --git a/Duplicati/Library/Snapshots/Windows/WmicVssBackup.cs b/Duplicati/Library/Snapshots/Windows/WmicVssBackup.cs new file mode 100644 index 000000000..37d901dd8 --- /dev/null +++ b/Duplicati/Library/Snapshots/Windows/WmicVssBackup.cs @@ -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; + +/// +/// Snapshot provider based on WMIC +/// +[SupportedOSPlatform("windows")] +internal class WmicVssBackup : ISnapshotProvider +{ + /// + /// The manager that contains the actual interface to WMIC + /// + private WmicShadowCopyManager _manager = new WmicShadowCopyManager(); + + /// + public Guid AddToSnapshotSet(string drive) + => _manager.Add(drive).ParsedId; + + /// + public void BackupComplete() + { + } + + /// + public void DeleteSnapshot(Guid shadowId, bool forceDelete) + { + foreach (var x in _manager.ShadowCopies.Where(x => x.ParsedId == shadowId)) + x.Dispose(); + } + + /// + public void DisableWriterClasses(Guid[] guids) + { + } + + /// + public void Dispose() + { + _manager.Dispose(); + } + + /// + public void DoSnapshotSet() + { + } + + /// + public void EnableWriterClasses(Guid[] guids) + { + } + + /// + public void FreeWriterMetadata() + { + } + + /// + public void GatherWriterMetadata() + { + } + + /// + /// Stub for returning the snapshot info + /// + /// The path where the snapshot is located + private sealed record SnapshotInfo (string SnapshotDeviceObject) : ISnapshotInfo; + + /// + public ISnapshotInfo GetSnapshotProperties(Guid shadowId) + => new SnapshotInfo(_manager.ShadowCopies.First(x => x.ParsedId == shadowId).MappedPath); + + /// + 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; + } + } + + /// + public IEnumerable ParseWriterMetaData(Guid[] writers) + => Enumerable.Empty(); + + /// + public void PrepareForBackup() + { + } + + /// + public void StartSnapshotSet() + { + } + + /// + public void VerifyWriters(Guid[] guids) + { + } +} \ No newline at end of file diff --git a/Duplicati/Library/Snapshots/WindowsSnapshot.cs b/Duplicati/Library/Snapshots/WindowsSnapshot.cs index 08bd5e1ae..1c1e70416 100644 --- a/Duplicati/Library/Snapshots/WindowsSnapshot.cs +++ b/Duplicati/Library/Snapshots/WindowsSnapshot.cs @@ -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 /// /// The main reference to the backup controller /// - private readonly VssBackupComponents _vssBackupComponents; + private readonly SnapshotManager _snapshotManager; /// /// 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); diff --git a/Duplicati/WebserverCore/Endpoints/V1/HyperV.cs b/Duplicati/WebserverCore/Endpoints/V1/HyperV.cs index cde404bae..98f8fed9e 100644 --- a/Duplicati/WebserverCore/Endpoints/V1/HyperV.cs +++ b/Duplicati/WebserverCore/Endpoints/V1/HyperV.cs @@ -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) diff --git a/Duplicati/WebserverCore/Endpoints/V1/MSSql.cs b/Duplicati/WebserverCore/Endpoints/V1/MSSql.cs index 0a2b98679..b4f4e6607 100644 --- a/Duplicati/WebserverCore/Endpoints/V1/MSSql.cs +++ b/Duplicati/WebserverCore/Endpoints/V1/MSSql.cs @@ -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)) {