// Copyright (c) 2026 Duplicati Inc. All rights reserved. using Duplicati.Library.Interface; using Duplicati.Library.Logging; using Duplicati.Proprietary.GoogleWorkspace.SourceItems; using System.Collections.Concurrent; namespace Duplicati.Proprietary.GoogleWorkspace; public sealed class SourceProvider : ISourceProviderModule, IDisposable { /// /// The log tag for this class. /// private static readonly string LOGTAG = Log.LogTagFromType(); private readonly APIHelper _apiHelper; private readonly string _mountPoint; private readonly OptionsHelper.GoogleWorkspaceOptions _options; 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 shared drives that has been enumerated. /// private int _sharedDriveCount = 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 shared drives. /// private int _sharedDriveLicenseWarningIssued = 0; /// /// Whether a license warning has been issued for sites. /// private int _siteLicenseWarningIssued = 0; /// /// Whether this provider is being used for a restore operation. /// internal bool UsedForRestoreOperation { get; private init; } /// /// Whether to avoid reading calendar ACLs. /// internal bool AvoidCalendarAcl { get; private init; } /// /// Indicates whether the metadata storage option has been set. /// private readonly bool _hasSetMetadataStorageOption; public APIHelper ApiHelper => _apiHelper; public OptionsHelper.GoogleWorkspaceOptions Options => _options; public SourceProvider() { _apiHelper = null!; _mountPoint = null!; _options = null!; } public SourceProvider(string url, string mountPoint, Dictionary options) : this(url, mountPoint, options, false) { } public SourceProvider(string url, string mountPoint, Dictionary options, bool usedForRestoreOperation) { _hasSetMetadataStorageOption = Library.Utility.Utility.ParseBoolOption(options, "store-metadata-content-in-database"); _mountPoint = mountPoint; _options = OptionsHelper.ParseOptions(options); _apiHelper = new APIHelper(_options, usedForRestoreOperation); UsedForRestoreOperation = usedForRestoreOperation; AvoidCalendarAcl = Library.Utility.Utility.ParseBoolOption(options, OptionsHelper.GOOGLE_AVOID_CALENDAR_ACL_OPTION); } public string Key => OptionsHelper.ModuleKey; public string DisplayName => Strings.Common.DisplayName; public string Description => Strings.Common.Description; public IList SupportedCommands => OptionsHelper.SourceProviderSupportedCommands; public string MountedPath => _mountPoint; public void Dispose() { } public IAsyncEnumerable EnumerateAsync(CancellationToken cancellationToken) { return new RootSourceEntry(this).Enumerate(cancellationToken); } public async Task GetEntryAsync(string path, bool isFolder, CancellationToken cancellationToken) { if (_entryCache.TryGetValue(path, out var cachedEntry)) return cachedEntry; ISourceProviderEntry resultEntry = new RootSourceEntry(this); 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; } public Task InitializeAsync(CancellationToken cancellationToken) { if (!_hasSetMetadataStorageOption) throw new UserInformationException(Strings.MetadataStorageNotEnabled("store-metadata-content-in-database"), "DatabaseMetadataStorageNotEnabled"); return Task.CompletedTask; } public Task TestAsync(CancellationToken cancellationToken) { _apiHelper.TestConnection(); return Task.CompletedTask; } /// /// Determines whether the license is approved for the given entry. /// /// The path to verify. /// The type of entry. /// The ID of the entry. /// Whether to increment the count if the license is approved. /// true if the license is approved; otherwise, false. internal bool LicenseApprovedForEntry(string path, GoogleRootType type, string id, bool increment) { // 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; if (type == GoogleRootType.Users) { var approved = LicenseChecker.LicenseHelper.AvailableGoogleWorkspaceUserSeats; var current = _userCount; if (current >= approved) { if (increment && Interlocked.Exchange(ref _userLicenseWarningIssued, 1) == 0) Log.WriteWarningMessage(LOGTAG, "LicenseWarning", null, Strings.LicenseWarning(type, approved)); return false; } } else if (type == GoogleRootType.Groups) { var approved = LicenseChecker.LicenseHelper.AvailableGoogleWorkspaceGroupSeats; var current = _groupCount; if (current >= approved) { if (increment && Interlocked.Exchange(ref _groupLicenseWarningIssued, 1) == 0) Log.WriteWarningMessage(LOGTAG, "LicenseWarning", null, Strings.LicenseWarning(type, approved)); return false; } } else if (type == GoogleRootType.SharedDrives) { var approved = LicenseChecker.LicenseHelper.AvailableGoogleWorkspaceSharedDriveSeats; var current = _sharedDriveCount; if (current >= approved) { if (increment && Interlocked.Exchange(ref _sharedDriveLicenseWarningIssued, 1) == 0) Log.WriteWarningMessage(LOGTAG, "LicenseWarning", null, Strings.LicenseWarning(type, approved)); return false; } } else if (type == GoogleRootType.Sites) { var approved = LicenseChecker.LicenseHelper.AvailableGoogleWorkspaceSiteSeats; var current = _siteCount; if (current >= approved) { if (increment && Interlocked.Exchange(ref _siteLicenseWarningIssued, 1) == 0) Log.WriteWarningMessage(LOGTAG, "LicenseWarning", null, Strings.LicenseWarning(type, approved)); return false; } } else { // Unknown type, allow it return true; } if (increment && _enumerationCounter.TryAdd(targetpath, true)) { if (type == GoogleRootType.Users) Interlocked.Increment(ref _userCount); else if (type == GoogleRootType.Groups) Interlocked.Increment(ref _groupCount); else if (type == GoogleRootType.SharedDrives) Interlocked.Increment(ref _sharedDriveCount); else if (type == GoogleRootType.Sites) Interlocked.Increment(ref _siteCount); } return true; } }