// 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.Runtime.InteropServices; namespace Duplicati.ShellExtension; /// /// Windows Shell Icon Overlay Handler for showing Duplicati backup status on folders. /// This handler displays overlay icons on folders that are included in Duplicati backups, /// similar to how cloud storage solutions show sync status. /// [ComVisible(true)] [Guid("E4B5F8A3-9C1D-4F2E-B6A7-8D3C5E6F7A9B")] [ClassInterface(ClassInterfaceType.None)] public class DuplicatiBackedUpOverlay : IconOverlayHandlerBase { /// /// The icon file name for successfully backed up folders /// protected override string IconFileName => "overlay_backed_up.ico"; /// /// Priority determines the order of overlay handlers (lower = higher priority) /// protected override int Priority => 10; /// /// Determines if this overlay should be shown for the given path /// protected override bool ShouldShowOverlay(string path, FolderBackupStatus status) { return status == FolderBackupStatus.BackedUp; } } /// /// Overlay handler for folders with backup warnings /// [ComVisible(true)] [Guid("E4B5F8A3-9C1D-4F2E-B6A7-8D3C5E6F7A9C")] [ClassInterface(ClassInterfaceType.None)] public class DuplicatiWarningOverlay : IconOverlayHandlerBase { /// /// The icon file name for folders with backup warnings /// protected override string IconFileName => "overlay_warning.ico"; /// /// Priority for warning overlay /// protected override int Priority => 11; /// /// Determines if this overlay should be shown for the given path /// protected override bool ShouldShowOverlay(string path, FolderBackupStatus status) { return status == FolderBackupStatus.BackedUpWithWarning; } } /// /// Overlay handler for folders with backup errors /// [ComVisible(true)] [Guid("E4B5F8A3-9C1D-4F2E-B6A7-8D3C5E6F7A9D")] [ClassInterface(ClassInterfaceType.None)] public class DuplicatiErrorOverlay : IconOverlayHandlerBase { /// /// The icon file name for folders with backup errors /// protected override string IconFileName => "overlay_error.ico"; /// /// Priority for error overlay /// protected override int Priority => 12; /// /// Determines if this overlay should be shown for the given path /// protected override bool ShouldShowOverlay(string path, FolderBackupStatus status) { return status == FolderBackupStatus.BackupFailed; } } /// /// Overlay handler for folders with backup in progress /// [ComVisible(true)] [Guid("E4B5F8A3-9C1D-4F2E-B6A7-8D3C5E6F7A9E")] [ClassInterface(ClassInterfaceType.None)] public class DuplicatiSyncingOverlay : IconOverlayHandlerBase { /// /// The icon file name for folders with backup in progress /// protected override string IconFileName => "overlay_syncing.ico"; /// /// Priority for syncing overlay /// protected override int Priority => 9; /// /// Determines if this overlay should be shown for the given path /// protected override bool ShouldShowOverlay(string path, FolderBackupStatus status) { return status == FolderBackupStatus.BackupInProgress; } } /// /// Base class for Duplicati icon overlay handlers /// public abstract class IconOverlayHandlerBase : IShellIconOverlayIdentifier { private static readonly Lazy Client = new(() => new DuplicatiClient()); /// /// The icon file name to use for this overlay /// protected abstract string IconFileName { get; } /// /// Priority of this overlay handler /// protected abstract int Priority { get; } /// /// Determines if this overlay should be shown for the given path and status /// protected abstract bool ShouldShowOverlay(string path, FolderBackupStatus status); /// /// Gets the overlay icon information /// public int GetOverlayInfo(IntPtr pwszIconFile, int cchMax, out int pIndex, out uint pdwFlags) { pIndex = 0; pdwFlags = ISIOI_ICONFILE; var iconPath = GetIconPath(); if (iconPath.Length < cchMax) { Marshal.Copy(iconPath.ToCharArray(), 0, pwszIconFile, iconPath.Length); Marshal.WriteInt16(pwszIconFile, iconPath.Length * 2, 0); return S_OK; } return S_FALSE; } /// /// Gets the priority of this overlay handler /// public int GetPriority(out int pPriority) { pPriority = Priority; return S_OK; } /// /// Determines if the overlay should be shown for the specified path /// public int IsMemberOf(string pwszPath, uint dwAttrib) { try { // Only show overlay for directories if ((dwAttrib & FILE_ATTRIBUTE_DIRECTORY) == 0) return S_FALSE; // Skip system folders if (IsSystemFolder(pwszPath)) return S_FALSE; // Get the folder status from Duplicati var statusTask = Client.Value.GetFolderStatusAsync(pwszPath); // Use a short timeout to avoid blocking Explorer if (!statusTask.Wait(TimeSpan.FromMilliseconds(100))) return S_FALSE; var statusInfo = statusTask.Result; return ShouldShowOverlay(pwszPath, statusInfo.Status) ? S_OK : S_FALSE; } catch { return S_FALSE; } } /// /// Gets the full path to the overlay icon /// private string GetIconPath() { var assemblyPath = Path.GetDirectoryName(typeof(IconOverlayHandlerBase).Assembly.Location); return Path.Combine(assemblyPath ?? "", "Icons", IconFileName); } /// /// Checks if the path is a system folder that shouldn't show overlays /// private static bool IsSystemFolder(string path) { if (string.IsNullOrEmpty(path)) return true; var normalizedPath = path.ToLowerInvariant(); // Skip Windows and Program Files folders var systemFolders = new[] { Environment.GetFolderPath(Environment.SpecialFolder.Windows).ToLowerInvariant(), Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles).ToLowerInvariant(), Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86).ToLowerInvariant(), Environment.GetFolderPath(Environment.SpecialFolder.CommonProgramFiles).ToLowerInvariant(), Environment.GetFolderPath(Environment.SpecialFolder.CommonProgramFilesX86).ToLowerInvariant() }; foreach (var folder in systemFolders) { if (!string.IsNullOrEmpty(folder) && normalizedPath.StartsWith(folder, StringComparison.OrdinalIgnoreCase)) { return true; } } return false; } // COM interface constants private const int S_OK = 0; private const int S_FALSE = 1; private const uint ISIOI_ICONFILE = 0x00000001; private const uint FILE_ATTRIBUTE_DIRECTORY = 0x10; } /// /// COM interface for Windows Shell Icon Overlay Identifiers /// [ComImport] [Guid("0C6C4200-C589-11D0-999A-00C04FD655E1")] [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] public interface IShellIconOverlayIdentifier { /// /// Determines whether the overlay should be displayed for the specified file /// [PreserveSig] int IsMemberOf([MarshalAs(UnmanagedType.LPWStr)] string pwszPath, uint dwAttrib); /// /// Provides the path to the overlay icon /// [PreserveSig] int GetOverlayInfo(IntPtr pwszIconFile, int cchMax, out int pIndex, out uint pdwFlags); /// /// Specifies the priority of the overlay /// [PreserveSig] int GetPriority(out int pPriority); }