// Copyright (c) 2026 Duplicati Inc. All rights reserved.
using System.Collections.Concurrent;
using System.Runtime.CompilerServices;
using Duplicati.Library.Interface;
using Duplicati.Library.Utility.Options;
using Duplicati.Proprietary.Office365.SourceItems;
namespace Duplicati.Proprietary.Office365;
///
/// The source provider for Office365.
///
public sealed partial class SourceProvider : ISourceProviderModule, IDisposable
{
///
/// The log tag for this class.
///
private static readonly string LOGTAG = Library.Logging.Log.LogTagFromType();
///
/// API helper for making requests to the Graph API.
///
private readonly APIHelper _apiHelper;
///
/// The mount point for this source provider instance.
///
private readonly string _mountPoint;
///
/// Indicates whether the metadata storage option has been set.
///
private readonly bool _hasSetMetadataStorageOption;
///
/// The timeout options for the backend
///
private readonly TimeoutOptionsHelper.Timeouts _timeouts;
///
/// Cache of already resolved source entries by their path.
///
private readonly ConcurrentDictionary _entryCache = new();
///
/// Counter for tracking which licensed paths are being enumerated.
///
private readonly ConcurrentDictionary _enumerationCounter = new();
///
/// The current number of users that has been enumerated.
///
private int _userCount = 0;
///
/// The current number of groups that has been enumerated.
///
private int _groupCount = 0;
///
/// The current number of sites that has been enumerated.
///
private int _siteCount = 0;
///
/// Whether a license warning has been issued for users.
///
private int _userLicenseWarningIssued = 0;
///
/// Whether a license warning has been issued for groups.
///
private int _groupLicenseWarningIssued = 0;
///
/// Whether a license warning has been issued for sites.
///
private int _siteLicenseWarningIssued = 0;
///
/// The included root types.
///
private readonly Office365MetaType[] _includedRootTypes;
///
/// The included user types.
///
private readonly Office365UserType[] _includedUserTypes;
///
/// The included group types.
///
private readonly Office365GroupType[] _includedGroupTypes;
///
/// Whether this provider is being used for a restore operation.
///
internal bool UsedForRestoreOperation { get; set; } = false;
///
/// Initializes a new instance of the class.
/// Only used for loading metadata properties.
///
public SourceProvider()
{
_apiHelper = null!;
_mountPoint = null!;
_timeouts = null!;
_includedRootTypes = null!;
_includedUserTypes = null!;
_includedGroupTypes = null!;
_hasSetMetadataStorageOption = false;
}
///
/// Initializes a new instance of the class.
///
/// The source provider URL.
/// The mount point.
/// The source provider options.
public SourceProvider(string url, string mountPoint, Dictionary options)
{
_hasSetMetadataStorageOption = Library.Utility.Utility.ParseBoolOption(options, "store-metadata-content-in-database");
_mountPoint = mountPoint;
var parsedOptions = OptionsHelper.ParseAndValidateOptions(url, options);
_timeouts = TimeoutOptionsHelper.Parse(options);
_includedRootTypes = parsedOptions.IncludedRootTypes;
_includedUserTypes = parsedOptions.IncludedUserTypes;
_includedGroupTypes = parsedOptions.IncludedGroupTypes;
_apiHelper = APIHelper.Create(
tenantId: parsedOptions.TenantId,
authOptions: parsedOptions.AuthOptions,
graphBaseUrl: parsedOptions.GraphBaseUrl,
timeouts: _timeouts,
certificatePath: parsedOptions.CertificatePath,
certificatePassword: parsedOptions.CertificatePassword,
scope: parsedOptions.Scope
);
}
///
public string Key => OptionsHelper.ModuleKey;
///
public string DisplayName => Strings.ProviderDisplayName;
///
public string Description => Strings.ProviderDescription;
///
public IList SupportedCommands => OptionsHelper.SupportedCommands.Concat(TimeoutOptionsHelper.GetOptions()).ToList();
///
public string MountedPath => _mountPoint;
///
public void Dispose()
{
_apiHelper?.Dispose();
}
///
public Task Initialize(CancellationToken cancellationToken)
{
if (!_hasSetMetadataStorageOption)
throw new UserInformationException(Strings.MetadataStorageNotEnabled("store-metadata-content-in-database"), "DatabaseMetadataStorageNotEnabled");
return _apiHelper.AcquireAccessTokenAsync(true, cancellationToken);
}
///
public Task Test(CancellationToken cancellationToken)
=> _apiHelper.AcquireAccessTokenAsync(false, cancellationToken);
///
public async IAsyncEnumerable Enumerate([EnumeratorCancellation] CancellationToken cancellationToken)
{
yield return new RootSourceEntry(this, _mountPoint);
}
///
public async Task GetEntry(string path, bool isFolder, CancellationToken cancellationToken)
{
if (_entryCache.TryGetValue(path, out var cachedEntry))
return cachedEntry;
ISourceProviderEntry resultEntry = new RootSourceEntry(this, _mountPoint);
var targetPath = path.TrimEnd(Path.DirectorySeparatorChar);
var currentPath = targetPath;
while (!string.IsNullOrEmpty(currentPath))
{
var parentPath = Path.GetDirectoryName(currentPath);
if (parentPath != null && _entryCache.TryGetValue(parentPath, out var cachedParent))
{
resultEntry = cachedParent;
break;
}
currentPath = parentPath;
}
var relativePath = string.IsNullOrEmpty(currentPath)
? targetPath
: Path.GetRelativePath(currentPath, targetPath);
var pathsegments = relativePath?.Split(Path.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries)
?? Array.Empty();
var segmentIndex = 0;
foreach (var item in pathsegments)
{
var found = false;
var isLastSegment = segmentIndex == pathsegments.Length - 1;
await foreach (var entry in resultEntry.Enumerate(cancellationToken))
{
_entryCache.TryAdd(entry.Path, entry);
var name = entry.Path.TrimEnd(Path.DirectorySeparatorChar)
.Split(Path.DirectorySeparatorChar)
.Last();
if (name.Equals(item, StringComparison.OrdinalIgnoreCase))
{
if (isLastSegment && entry.IsFolder != isFolder)
throw new UserInformationException($"Path segment '{item}' is not a {(isFolder ? "folder" : "file")}", "PathSegmentNotFolder");
else if (!isLastSegment && !entry.IsFolder)
throw new UserInformationException($"Path segment '{item}' is not a folder", "PathSegmentNotFolder");
resultEntry = entry;
found = true;
break;
}
}
segmentIndex++;
if (!found)
return null;
}
return resultEntry;
}
///
/// Determines whether the license is approved for the given entry.
///
/// The path to verify.
/// The type of entry.
/// The ID of the entry.
/// true if the license is approved; otherwise, false.
internal bool LicenseApprovedForEntry(string path, Office365MetaType type, string id)
{
// We do not limit restores
if (UsedForRestoreOperation)
return true;
// Make a unique target path for the type and id, just for counting purposes, not matching actual paths
var targetpath = $"{path.TrimEnd(Path.DirectorySeparatorChar)}{Path.DirectorySeparatorChar}{type}{Path.DirectorySeparatorChar}{id}";
if (_enumerationCounter.ContainsKey(targetpath))
return true;
int approved = 0;
int current = 0;
if (type.HasFlag(Office365MetaType.Users))
{
approved = LicenseChecker.LicenseHelper.AvailableOffice365UserSeats;
current = _userCount;
if (current >= approved)
{
if (Interlocked.Exchange(ref _userLicenseWarningIssued, 1) == 0)
Library.Logging.Log.WriteWarningMessage(LOGTAG, "LicenseWarning", null, $"Licensed Office 365 feature seats exceeded for {type} ({approved}). Some items will not be backed up.");
return false;
}
}
else if (type.HasFlag(Office365MetaType.Groups))
{
approved = LicenseChecker.LicenseHelper.AvailableOffice365GroupSeats;
current = _groupCount;
if (current >= approved)
{
if (Interlocked.Exchange(ref _groupLicenseWarningIssued, 1) == 0)
Library.Logging.Log.WriteWarningMessage(LOGTAG, "LicenseWarning", null, $"Licensed Office 365 feature seats exceeded for {type} ({approved}). Some items will not be backed up.");
return false;
}
}
else if (type.HasFlag(Office365MetaType.Sites))
{
approved = LicenseChecker.LicenseHelper.AvailableOffice365SiteSeats;
current = _siteCount;
if (current >= approved)
{
if (Interlocked.Exchange(ref _siteLicenseWarningIssued, 1) == 0)
Library.Logging.Log.WriteWarningMessage(LOGTAG, "LicenseWarning", null, $"Licensed Office 365 feature seats exceeded for {type} ({approved}). Some items will not be backed up.");
return false;
}
}
else
{
// Unknown type, allow it
return true;
}
if (_enumerationCounter.TryAdd(targetpath, true))
{
if (type.HasFlag(Office365MetaType.Users))
Interlocked.Increment(ref _userCount);
else if (type.HasFlag(Office365MetaType.Groups))
Interlocked.Increment(ref _groupCount);
else if (type.HasFlag(Office365MetaType.Sites))
Interlocked.Increment(ref _siteCount);
}
return true;
}
///
/// Gets the included root types.
///
internal IEnumerable IncludedRootTypes =>
_includedRootTypes;
///
/// Gets the included user types.
///
internal IEnumerable IncludedUserTypes =>
_includedUserTypes;
///
/// Gets the included group types.
///
internal IEnumerable IncludedGroupTypes =>
_includedGroupTypes;
}