// 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 System.Runtime.Versioning; using Duplicati.Library.Interface; using Duplicati.Library.Logging; using Vanara.PInvoke.VssApi; namespace Duplicati.Library.WindowsModules; /// /// Implementation of a snapshot using the Vanara VSS bindings /// [SupportedOSPlatform("windows")] public class VanaraVssBackup : ISnapshotProvider { private static readonly TimeSpan MaxWaitTime = TimeSpan.FromMinutes(1); private static readonly string LogTag = Log.LogTagFromType(); private IVssBackupComponents _components; private bool _hasAllocatedMetadata; private bool _hasStartedSnapshotSet; private bool _isBackupComplete; public VanaraVssBackup() { _components = GetVssBackupComponents(); } public void EnableWriterClasses(Guid[] guids) => _components.EnableWriterClasses(guids); public void DisableWriterClasses(Guid[] guids) => _components.DisableWriterClasses(guids); public void GatherWriterMetadata() { if (_hasAllocatedMetadata) return; _hasAllocatedMetadata = true; _components.GatherWriterMetadata().Wait((uint)MaxWaitTime.TotalMilliseconds).ThrowIfFailed(); } public void FreeWriterMetadata() { if (!_hasAllocatedMetadata) return; _hasAllocatedMetadata = false; _components.FreeWriterMetadata(); } public void VerifyWriters(Guid[] guids) { if (guids == null || guids.Length == 0) return; // nothing to verify foreach (var wanted in guids) { // Scan all metadata objects and compare their writer GUID bool found = _components .WriterMetadata .Select(GetWriterIdentity) // (WriteId, WriterName) .Any(id => id.WriteId == wanted); if (!found) throw new Exception( $"Writer with GUID {wanted} was not added to the VSS writer set."); } } private sealed record SnapshotInfo(string SnapshotDeviceObject) : ISnapshotInfo; public ISnapshotInfo GetSnapshotProperties(Guid shadowId) { var str = _components.GetSnapshotProperties(shadowId).m_pwszSnapshotDeviceObject; if (string.IsNullOrEmpty(str)) throw new InvalidOperationException($"No snapshot found for shadow ID {shadowId}."); return new SnapshotInfo(str!); } public void StartSnapshotSet() { if (_hasStartedSnapshotSet) throw new InvalidOperationException("Snapshot set has already been started."); _components.StartSnapshotSet(); _hasStartedSnapshotSet = true; } public void PrepareForBackup() => _components.PrepareForBackup().Wait((uint)MaxWaitTime.TotalMilliseconds).ThrowIfFailed(); public void DoSnapshotSet() => _components.DoSnapshotSet().Wait((uint)MaxWaitTime.TotalMilliseconds).ThrowIfFailed(); public bool IsVolumeSupported(string drive) => _components.IsVolumeSupported(Guid.Empty, drive); public Guid AddToSnapshotSet(string drive) => _components.AddToSnapshotSet(drive); public void BackupComplete() { _isBackupComplete = true; _components.BackupComplete(); } public void DeleteSnapshot(Guid shadowId, bool forceDelete) { if (!_hasStartedSnapshotSet) return; _hasStartedSnapshotSet = false; _components.DeleteSnapshots(shadowId, VSS_OBJECT_TYPE.VSS_OBJECT_SNAPSHOT, forceDelete, out _, out _).ThrowIfFailed(); } private static IVssBackupComponents GetVssBackupComponents() { var comp = CreateVssBackupComponents(); comp.InitializeForBackup(null); comp.SetContext(VSS_SNAPSHOT_CONTEXT.VSS_CTX_BACKUP); comp.SetBackupState(false, true, VSS_BACKUP_TYPE.VSS_BT_FULL, false); return comp; } private static IVssBackupComponents CreateVssBackupComponents() { VssFactory.CreateVssBackupComponents(out var comp).ThrowIfFailed(); return comp; } private static (Guid WriteId, string WriterName) GetWriterIdentity(IVssExamineWriterMetadata wm) { // GetIdentity order: // 0 = instanceId (ignore) // 1 = writerId (the GUID we’re after) // 2 = writerName (friendly name) // 3 = instanceName (ignore) // 4 = usage (ignore) // 5 = source (ignore) wm.GetIdentity(out Guid _, out Guid writerId, out string writerName, out _, out _, out _); return (writerId, writerName ?? string.Empty); } public IEnumerable ParseWriterMetaData(Guid[] writers) { // Enumerate all writer metadata objects returned by PrepareForBackup / GatherWriterMetadata foreach (var wm in _components.WriterMetadata) { // Get the writer identity (GUID and name) var (writerId, writerName) = GetWriterIdentity(wm); // Skip writers the caller didn't ask for (if a filter list was supplied) if (writers != null && writers.Length > 0 && !writers.Contains(writerId)) continue; // Emit one WriterMetaData per component the writer exposes foreach (var comp in wm.Components) { var ci = comp.GetComponentInfo(); yield return new WriterMetaData { Guid = writerId, Name = writerName ?? string.Empty, LogicalPath = ci.bstrLogicalPath ?? string.Empty, Paths = GetPathsFromComponent(comp) }; } } } private List GetPathsFromComponent(IVssWMComponent component) { var paths = new List(); foreach (var file in component.Files) { if (file.Path.Contains("*")) { if (Directory.Exists(Util.AppendDirSeparator(file.Path))) paths.Add(Util.AppendDirSeparator(file.Path)); } else { var fileWithSpec = Path.Combine(file.Path, file.FileSpec); if (File.Exists(fileWithSpec)) paths.Add(fileWithSpec); } } return paths; } public void Dispose() { if (_hasAllocatedMetadata) FreeWriterMetadata(); if (_hasStartedSnapshotSet) DeleteSnapshot(Guid.Empty, true); // Delete all snapshots if any were created try { if (!_isBackupComplete) _components?.BackupComplete(); } catch (Exception ex) { Log.WriteVerboseMessage(LogTag, "VssDisposeFailed", ex, "Failed to complete VSS backup"); } } }