This adds a Windows Shell Extension that shows overlay icons on folders that are included in Duplicati backups, similar to how cloud storage solutions like OneDrive and Google Drive indicate sync status. The feature includes: - New API endpoint /api/v1/folderstatus to query backup status of folders - Windows Shell Icon Overlay handlers for backed up, warning, error and syncing states - Client methods in DuplicatiServerClient for the new endpoint - Registration script for the COM shell extension Closes #6450
291 lines
9.4 KiB
C#
291 lines
9.4 KiB
C#
// 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;
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
[ComVisible(true)]
|
|
[Guid("E4B5F8A3-9C1D-4F2E-B6A7-8D3C5E6F7A9B")]
|
|
[ClassInterface(ClassInterfaceType.None)]
|
|
public class DuplicatiBackedUpOverlay : IconOverlayHandlerBase
|
|
{
|
|
/// <summary>
|
|
/// The icon file name for successfully backed up folders
|
|
/// </summary>
|
|
protected override string IconFileName => "overlay_backed_up.ico";
|
|
|
|
/// <summary>
|
|
/// Priority determines the order of overlay handlers (lower = higher priority)
|
|
/// </summary>
|
|
protected override int Priority => 10;
|
|
|
|
/// <summary>
|
|
/// Determines if this overlay should be shown for the given path
|
|
/// </summary>
|
|
protected override bool ShouldShowOverlay(string path, FolderBackupStatus status)
|
|
{
|
|
return status == FolderBackupStatus.BackedUp;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overlay handler for folders with backup warnings
|
|
/// </summary>
|
|
[ComVisible(true)]
|
|
[Guid("E4B5F8A3-9C1D-4F2E-B6A7-8D3C5E6F7A9C")]
|
|
[ClassInterface(ClassInterfaceType.None)]
|
|
public class DuplicatiWarningOverlay : IconOverlayHandlerBase
|
|
{
|
|
/// <summary>
|
|
/// The icon file name for folders with backup warnings
|
|
/// </summary>
|
|
protected override string IconFileName => "overlay_warning.ico";
|
|
|
|
/// <summary>
|
|
/// Priority for warning overlay
|
|
/// </summary>
|
|
protected override int Priority => 11;
|
|
|
|
/// <summary>
|
|
/// Determines if this overlay should be shown for the given path
|
|
/// </summary>
|
|
protected override bool ShouldShowOverlay(string path, FolderBackupStatus status)
|
|
{
|
|
return status == FolderBackupStatus.BackedUpWithWarning;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overlay handler for folders with backup errors
|
|
/// </summary>
|
|
[ComVisible(true)]
|
|
[Guid("E4B5F8A3-9C1D-4F2E-B6A7-8D3C5E6F7A9D")]
|
|
[ClassInterface(ClassInterfaceType.None)]
|
|
public class DuplicatiErrorOverlay : IconOverlayHandlerBase
|
|
{
|
|
/// <summary>
|
|
/// The icon file name for folders with backup errors
|
|
/// </summary>
|
|
protected override string IconFileName => "overlay_error.ico";
|
|
|
|
/// <summary>
|
|
/// Priority for error overlay
|
|
/// </summary>
|
|
protected override int Priority => 12;
|
|
|
|
/// <summary>
|
|
/// Determines if this overlay should be shown for the given path
|
|
/// </summary>
|
|
protected override bool ShouldShowOverlay(string path, FolderBackupStatus status)
|
|
{
|
|
return status == FolderBackupStatus.BackupFailed;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overlay handler for folders with backup in progress
|
|
/// </summary>
|
|
[ComVisible(true)]
|
|
[Guid("E4B5F8A3-9C1D-4F2E-B6A7-8D3C5E6F7A9E")]
|
|
[ClassInterface(ClassInterfaceType.None)]
|
|
public class DuplicatiSyncingOverlay : IconOverlayHandlerBase
|
|
{
|
|
/// <summary>
|
|
/// The icon file name for folders with backup in progress
|
|
/// </summary>
|
|
protected override string IconFileName => "overlay_syncing.ico";
|
|
|
|
/// <summary>
|
|
/// Priority for syncing overlay
|
|
/// </summary>
|
|
protected override int Priority => 9;
|
|
|
|
/// <summary>
|
|
/// Determines if this overlay should be shown for the given path
|
|
/// </summary>
|
|
protected override bool ShouldShowOverlay(string path, FolderBackupStatus status)
|
|
{
|
|
return status == FolderBackupStatus.BackupInProgress;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Base class for Duplicati icon overlay handlers
|
|
/// </summary>
|
|
public abstract class IconOverlayHandlerBase : IShellIconOverlayIdentifier
|
|
{
|
|
private static readonly Lazy<DuplicatiClient> Client = new(() => new DuplicatiClient());
|
|
|
|
/// <summary>
|
|
/// The icon file name to use for this overlay
|
|
/// </summary>
|
|
protected abstract string IconFileName { get; }
|
|
|
|
/// <summary>
|
|
/// Priority of this overlay handler
|
|
/// </summary>
|
|
protected abstract int Priority { get; }
|
|
|
|
/// <summary>
|
|
/// Determines if this overlay should be shown for the given path and status
|
|
/// </summary>
|
|
protected abstract bool ShouldShowOverlay(string path, FolderBackupStatus status);
|
|
|
|
/// <summary>
|
|
/// Gets the overlay icon information
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the priority of this overlay handler
|
|
/// </summary>
|
|
public int GetPriority(out int pPriority)
|
|
{
|
|
pPriority = Priority;
|
|
return S_OK;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Determines if the overlay should be shown for the specified path
|
|
/// </summary>
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the full path to the overlay icon
|
|
/// </summary>
|
|
private string GetIconPath()
|
|
{
|
|
var assemblyPath = Path.GetDirectoryName(typeof(IconOverlayHandlerBase).Assembly.Location);
|
|
return Path.Combine(assemblyPath ?? "", "Icons", IconFileName);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks if the path is a system folder that shouldn't show overlays
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// COM interface for Windows Shell Icon Overlay Identifiers
|
|
/// </summary>
|
|
[ComImport]
|
|
[Guid("0C6C4200-C589-11D0-999A-00C04FD655E1")]
|
|
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
|
public interface IShellIconOverlayIdentifier
|
|
{
|
|
/// <summary>
|
|
/// Determines whether the overlay should be displayed for the specified file
|
|
/// </summary>
|
|
[PreserveSig]
|
|
int IsMemberOf([MarshalAs(UnmanagedType.LPWStr)] string pwszPath, uint dwAttrib);
|
|
|
|
/// <summary>
|
|
/// Provides the path to the overlay icon
|
|
/// </summary>
|
|
[PreserveSig]
|
|
int GetOverlayInfo(IntPtr pwszIconFile, int cchMax, out int pIndex, out uint pdwFlags);
|
|
|
|
/// <summary>
|
|
/// Specifies the priority of the overlay
|
|
/// </summary>
|
|
[PreserveSig]
|
|
int GetPriority(out int pPriority);
|
|
}
|