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