2026-01-14 10:23:49 +01:00
// Copyright (c) 2026 Duplicati Inc. All rights reserved.
2026-01-16 11:09:13 +01:00
using System.Collections.Concurrent ;
2026-01-14 10:23:49 +01:00
using System.Runtime.CompilerServices ;
using Duplicati.Library.Interface ;
2026-01-20 10:36:37 +01:00
using Duplicati.Library.Utility.Options ;
2026-01-14 10:23:49 +01:00
using Duplicati.Proprietary.Office365.SourceItems ;
namespace Duplicati.Proprietary.Office365 ;
2026-01-20 10:36:37 +01:00
/// <summary>
/// The source provider for Office365.
/// </summary>
2026-01-14 10:23:49 +01:00
public sealed partial class SourceProvider : ISourceProviderModule , IDisposable
{
2026-01-20 10:36:37 +01:00
/// <summary>
/// The log tag for this class.
/// </summary>
private static readonly string LOGTAG = Library . Logging . Log . LogTagFromType < SourceProvider >();
/// <summary>
/// API helper for making requests to the Graph API.
/// </summary>
2026-01-14 10:23:49 +01:00
private readonly APIHelper _apiHelper ;
2026-01-20 10:36:37 +01:00
/// <summary>
/// The mount point for this source provider instance.
/// </summary>
2026-01-14 10:23:49 +01:00
private readonly string _mountPoint ;
2026-01-25 11:56:28 +01:00
/// <summary>
/// Indicates whether the metadata storage option has been set.
/// </summary>
private readonly bool _hasSetMetadataStorageOption ;
2026-01-20 10:36:37 +01:00
/// <summary>
/// The timeout options for the backend
/// </summary>
private readonly TimeoutOptionsHelper . Timeouts _timeouts ;
/// <summary>
/// Cache of already resolved source entries by their path.
/// </summary>
2026-01-16 11:09:13 +01:00
private readonly ConcurrentDictionary < string , ISourceProviderEntry > _entryCache = new ();
2026-01-20 10:36:37 +01:00
/// <summary>
/// Counter for tracking which licensed paths are being enumerated.
/// </summary>
private readonly ConcurrentDictionary < string , bool > _enumerationCounter = new ();
/// <summary>
2026-01-30 17:44:18 +01:00
/// The current number of users that has been enumerated.
2026-01-20 10:36:37 +01:00
/// </summary>
2026-01-30 17:44:18 +01:00
private int _userCount = 0 ;
/// <summary>
/// The current number of groups that has been enumerated.
/// </summary>
private int _groupCount = 0 ;
/// <summary>
/// The current number of sites that has been enumerated.
/// </summary>
private int _siteCount = 0 ;
/// <summary>
/// Whether a license warning has been issued for users.
/// </summary>
private int _userLicenseWarningIssued = 0 ;
/// <summary>
/// Whether a license warning has been issued for groups.
/// </summary>
private int _groupLicenseWarningIssued = 0 ;
/// <summary>
/// Whether a license warning has been issued for sites.
/// </summary>
private int _siteLicenseWarningIssued = 0 ;
2026-01-20 10:36:37 +01:00
/// <summary>
2026-01-22 17:01:13 +01:00
/// The included root types.
2026-01-20 10:36:37 +01:00
/// </summary>
2026-01-22 17:01:13 +01:00
private readonly Office365MetaType [] _includedRootTypes ;
2026-01-14 10:23:49 +01:00
2026-01-20 10:36:37 +01:00
/// <summary>
2026-01-22 17:01:13 +01:00
/// The included user types.
2026-01-20 10:36:37 +01:00
/// </summary>
2026-01-22 17:01:13 +01:00
private readonly Office365UserType [] _includedUserTypes ;
2026-01-14 10:23:49 +01:00
2026-01-20 10:36:37 +01:00
/// <summary>
2026-01-22 17:01:13 +01:00
/// The included group types.
2026-01-20 10:36:37 +01:00
/// </summary>
2026-01-22 17:01:13 +01:00
private readonly Office365GroupType [] _includedGroupTypes ;
2026-01-14 10:23:49 +01:00
2026-01-20 10:36:37 +01:00
/// <summary>
2026-01-22 17:01:13 +01:00
/// Whether this provider is being used for a restore operation.
2026-01-20 10:36:37 +01:00
/// </summary>
2026-01-22 17:01:13 +01:00
internal bool UsedForRestoreOperation { get ; set ; } = false ;
2026-01-14 10:23:49 +01:00
2026-01-20 10:36:37 +01:00
/// <summary>
/// Initializes a new instance of the <see cref="SourceProvider"/> class.
/// Only used for loading metadata properties.
/// </summary>
2026-01-14 10:23:49 +01:00
public SourceProvider ()
{
_apiHelper = null !;
_mountPoint = null !;
2026-01-20 10:36:37 +01:00
_timeouts = null !;
2026-01-22 17:01:13 +01:00
_includedRootTypes = null !;
_includedUserTypes = null !;
_includedGroupTypes = null !;
2026-01-25 11:56:28 +01:00
_hasSetMetadataStorageOption = false ;
2026-01-14 10:23:49 +01:00
}
2026-01-20 10:36:37 +01:00
/// <summary>
/// Initializes a new instance of the <see cref="SourceProvider"/> class.
/// </summary>
/// <param name="url">The source provider URL.</param>
/// <param name="mountPoint">The mount point.</param>
/// <param name="options">The source provider options.</param>
2026-01-14 10:23:49 +01:00
public SourceProvider ( string url , string mountPoint , Dictionary < string , string? > options )
{
2026-01-25 11:56:28 +01:00
_hasSetMetadataStorageOption = Library . Utility . Utility . ParseBoolOption ( options , "store-metadata-content-in-database" );
2026-01-14 10:23:49 +01:00
_mountPoint = mountPoint ;
var parsedOptions = OptionsHelper . ParseAndValidateOptions ( url , options );
2026-01-20 10:36:37 +01:00
_timeouts = TimeoutOptionsHelper . Parse ( options );
2026-01-22 17:01:13 +01:00
_includedRootTypes = parsedOptions . IncludedRootTypes ;
_includedUserTypes = parsedOptions . IncludedUserTypes ;
_includedGroupTypes = parsedOptions . IncludedGroupTypes ;
2026-01-14 10:23:49 +01:00
_apiHelper = APIHelper . Create (
tenantId : parsedOptions . TenantId ,
authOptions : parsedOptions . AuthOptions ,
2026-01-20 10:36:37 +01:00
graphBaseUrl : parsedOptions . GraphBaseUrl ,
timeouts : _timeouts ,
certificatePath : parsedOptions . CertificatePath ,
2026-01-20 12:13:19 +01:00
certificatePassword : parsedOptions . CertificatePassword ,
scope : parsedOptions . Scope
2026-01-14 10:23:49 +01:00
);
}
2026-01-20 10:36:37 +01:00
/// <inheritdoc />
2026-01-14 10:23:49 +01:00
public string Key => OptionsHelper . ModuleKey ;
2026-01-20 10:36:37 +01:00
/// <inheritdoc />
2026-01-14 10:23:49 +01:00
public string DisplayName => Strings . ProviderDisplayName ;
2026-01-20 10:36:37 +01:00
/// <inheritdoc />
2026-01-14 10:23:49 +01:00
public string Description => Strings . ProviderDescription ;
2026-01-20 10:36:37 +01:00
/// <inheritdoc />
public IList < ICommandLineArgument > SupportedCommands => OptionsHelper . SupportedCommands . Concat ( TimeoutOptionsHelper . GetOptions ()). ToList ();
2026-01-14 10:23:49 +01:00
2026-01-20 10:36:37 +01:00
/// <inheritdoc />
2026-01-14 10:23:49 +01:00
public string MountedPath => _mountPoint ;
2026-01-20 10:36:37 +01:00
/// <inheritdoc />
2026-01-14 10:23:49 +01:00
public void Dispose ()
{
_apiHelper ?. Dispose ();
}
2026-01-20 10:36:37 +01:00
/// <inheritdoc />
2026-01-14 10:23:49 +01:00
public Task Initialize ( CancellationToken cancellationToken )
2026-01-25 11:56:28 +01:00
{
if (! _hasSetMetadataStorageOption )
throw new UserInformationException ( Strings . MetadataStorageNotEnabled ( "store-metadata-content-in-database" ), "DatabaseMetadataStorageNotEnabled" );
return _apiHelper . AcquireAccessTokenAsync ( true , cancellationToken );
}
/// <inheritdoc />
public Task Test ( CancellationToken cancellationToken )
=> _apiHelper . AcquireAccessTokenAsync ( false , cancellationToken );
2026-01-14 10:23:49 +01:00
2026-01-20 10:36:37 +01:00
/// <inheritdoc />
2026-01-14 10:23:49 +01:00
public async IAsyncEnumerable < ISourceProviderEntry > Enumerate ([ EnumeratorCancellation ] CancellationToken cancellationToken )
{
yield return new RootSourceEntry ( this , _mountPoint );
}
2026-01-20 10:36:37 +01:00
/// <inheritdoc />
2026-01-16 11:09:13 +01:00
public async Task < ISourceProviderEntry ?> GetEntry ( string path , bool isFolder , CancellationToken cancellationToken )
2026-01-14 10:23:49 +01:00
{
2026-01-16 11:09:13 +01:00
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 );
2026-01-20 10:36:37 +01:00
var pathsegments = relativePath ?. Split ( Path . DirectorySeparatorChar , StringSplitOptions . RemoveEmptyEntries )
?? Array . Empty < string >();
2026-01-16 11:09:13 +01:00
2026-01-20 10:36:37 +01:00
var segmentIndex = 0 ;
2026-01-16 11:09:13 +01:00
foreach ( var item in pathsegments )
{
var found = false ;
2026-01-20 10:36:37 +01:00
var isLastSegment = segmentIndex == pathsegments . Length - 1 ;
2026-01-16 11:09:13 +01:00
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 ))
{
2026-01-20 10:36:37 +01:00
if ( isLastSegment && entry . IsFolder != isFolder )
throw new UserInformationException ( $"Path segment '{item}' is not a {(isFolder ? " folder " : " file ")}" , "PathSegmentNotFolder" );
else if (! isLastSegment && ! entry . IsFolder )
2026-01-16 11:09:13 +01:00
throw new UserInformationException ( $"Path segment '{item}' is not a folder" , "PathSegmentNotFolder" );
resultEntry = entry ;
found = true ;
break ;
}
}
2026-01-20 10:36:37 +01:00
segmentIndex ++;
2026-01-16 11:09:13 +01:00
if (! found )
return null ;
}
return resultEntry ;
2026-01-14 10:23:49 +01:00
}
2026-01-20 10:36:37 +01:00
/// <summary>
/// Determines whether the license is approved for the given entry.
/// </summary>
/// <param name="path">The path to verify.</param>
/// <param name="type">The type of entry.</param>
/// <param name="id">The ID of the entry.</param>
/// <returns><c>true</c> if the license is approved; otherwise, <c>false</c>.</returns>
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 ;
2026-01-30 17:44:18 +01:00
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 ))
2026-01-20 10:36:37 +01:00
{
2026-01-30 17:44:18 +01:00
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 );
2026-01-20 10:36:37 +01:00
}
return true ;
}
/// <summary>
/// Gets the included root types.
/// </summary>
2026-01-14 10:23:49 +01:00
internal IEnumerable < Office365MetaType > IncludedRootTypes =>
2026-01-22 17:01:13 +01:00
_includedRootTypes ;
2026-01-14 10:23:49 +01:00
2026-01-20 10:36:37 +01:00
/// <summary>
/// Gets the included user types.
/// </summary>
2026-01-14 10:23:49 +01:00
internal IEnumerable < Office365UserType > IncludedUserTypes =>
2026-01-22 17:01:13 +01:00
_includedUserTypes ;
2026-01-14 10:23:49 +01:00
2026-01-20 10:36:37 +01:00
/// <summary>
/// Gets the included group types.
/// </summary>
2026-01-14 10:23:49 +01:00
internal IEnumerable < Office365GroupType > IncludedGroupTypes =>
2026-01-22 17:01:13 +01:00
_includedGroupTypes ;
2026-01-14 10:23:49 +01:00
}