// 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);
}