From 2beebb60860bc41cfdda7cd7530c4b18aeea20e0 Mon Sep 17 00:00:00 2001 From: Kenneth Skovhede Date: Tue, 3 Mar 2026 10:06:53 +0100 Subject: [PATCH] Improve link detection on Windows This PR adds a check that prevents performing link lookups on drive roots as this is reported to fail on some Windows versions. The PR also makes a link lookup failure a warning instead of failing the restore process. --- Duplicati/Library/Common/IO/Util.cs | 47 ++++++++++++++++++++++++++--- 1 file changed, 43 insertions(+), 4 deletions(-) diff --git a/Duplicati/Library/Common/IO/Util.cs b/Duplicati/Library/Common/IO/Util.cs index 5f828123c..ec9d817b7 100644 --- a/Duplicati/Library/Common/IO/Util.cs +++ b/Duplicati/Library/Common/IO/Util.cs @@ -22,6 +22,7 @@ using System; using System.Collections.Generic; using System.IO; +using Duplicati.Library.Logging; #nullable enable @@ -29,6 +30,11 @@ namespace Duplicati.Library.Common.IO { public static class Util { + /// + /// The log tag for messages + /// + private static readonly string LOGTAG = Log.LogTagFromType(typeof(Util)); + /// /// A cached instance of the directory separator as a string /// @@ -130,6 +136,26 @@ namespace Duplicati.Library.Common.IO return true; } + /// + /// Checks if the path is a Windows drive root (e.g., "C:\" or "D:\"). + /// + /// The path to check. + /// true if the path is a drive root, false otherwise. + private static bool IsDriveRoot(string path) + { + if (!OperatingSystem.IsWindows()) + return false; + + // GetPathRoot returns "C:\" for "C:\" or "C:\Users" + var root = Path.GetPathRoot(path); + if (string.IsNullOrEmpty(root)) + return false; + + // Check if the input is exactly the root + return string.Equals(path.TrimEnd('\\'), root.TrimEnd('\\'), StringComparison.OrdinalIgnoreCase) + && root.EndsWith(Path.DirectorySeparatorChar); + } + /// /// Get the final resolved path, accounting for symlinks in existing segments. /// @@ -158,11 +184,24 @@ namespace Duplicati.Library.Common.IO } // 2. Resolve symlinks for the part of the path that actually exists - FileSystemInfo info = Directory.Exists(current) - ? new DirectoryInfo(current) - : new FileInfo(current); + // Skip symlink resolution for Windows drive roots (e.g., "C:\") as + // ResolveLinkTarget can throw DirectoryNotFoundException on some systems + var resolvedPath = current; + if (!IsDriveRoot(current)) + { + FileSystemInfo info = Directory.Exists(current) + ? new DirectoryInfo(current) + : new FileInfo(current); - var resolvedPath = info.ResolveLinkTarget(returnFinalTarget: true)?.FullName ?? info.FullName; + try + { + resolvedPath = info.ResolveLinkTarget(returnFinalTarget: true)?.FullName ?? info.FullName; + } + catch (Exception ex) + { + Log.WriteWarningMessage(LOGTAG, "ResolveLinkTargetFailed", ex, "Failed to resolve link target for {0}, assuming it is not a link", current); + } + } // 3. Re-attach the non-existent segments to the resolved base path while (ghostSegments.Count > 0)