Added support for defining the scope, if not using the default.

Added internal overwrite option to not rely on the general restore overwrite option.
This commit is contained in:
Kenneth Skovhede
2026-01-20 12:13:19 +01:00
parent a4b33e5d16
commit 59b5480991
6 changed files with 51 additions and 19 deletions
+12 -4
View File
@@ -56,6 +56,11 @@ internal class APIHelper : IDisposable
/// </summary>
private readonly string? _certificatePassword;
/// <summary>
/// The scope for the API requests
/// </summary>
private readonly string _scope;
/// <summary>
/// The timeout options for the backend
/// </summary>
@@ -71,7 +76,8 @@ internal class APIHelper : IDisposable
/// <param name="timeouts">The timeout options</param>
/// <param name="certificatePath">The path to the certificate file</param>
/// <param name="certificatePassword">The password for the certificate file</param>
private APIHelper(HttpClient httpClient, AuthOptionsHelper.AuthOptions authOptions, string tenantId, string graphBaseUrl, TimeoutOptionsHelper.Timeouts timeouts, string? certificatePath, string? certificatePassword)
/// <param name="scope">The scope for the API requests</param>
private APIHelper(HttpClient httpClient, AuthOptionsHelper.AuthOptions authOptions, string tenantId, string graphBaseUrl, TimeoutOptionsHelper.Timeouts timeouts, string? certificatePath, string? certificatePassword, string scope)
{
_httpClient = httpClient;
_authOptions = authOptions;
@@ -80,6 +86,7 @@ internal class APIHelper : IDisposable
_timeouts = timeouts;
_certificatePath = certificatePath;
_certificatePassword = certificatePassword;
_scope = scope;
}
/// <summary>
@@ -91,8 +98,9 @@ internal class APIHelper : IDisposable
/// <param name="timeouts">The timeout options</param>
/// <param name="certificatePath">The path to the certificate file</param>
/// <param name="certificatePassword">The password for the certificate file</param>
/// <param name="scope">The scope for the API requests</param>
/// <returns>A new instance of the <see cref="APIHelper"/> class</returns>
public static APIHelper Create(AuthOptionsHelper.AuthOptions authOptions, string tenantId, string graphBaseUrl, TimeoutOptionsHelper.Timeouts timeouts, string? certificatePath = null, string? certificatePassword = null)
public static APIHelper Create(AuthOptionsHelper.AuthOptions authOptions, string tenantId, string graphBaseUrl, TimeoutOptionsHelper.Timeouts timeouts, string? certificatePath = null, string? certificatePassword = null, string scope = "https://graph.microsoft.com/.default")
{
var handler = new HttpClientHandler
{
@@ -103,7 +111,7 @@ internal class APIHelper : IDisposable
httpClient.Timeout = Timeout.InfiniteTimeSpan;
httpClient.DefaultRequestHeaders.Add("User-Agent", $"Duplicati/{System.Reflection.Assembly.GetExecutingAssembly().GetName().Version}");
return new APIHelper(httpClient, authOptions, tenantId, graphBaseUrl, timeouts, certificatePath, certificatePassword);
return new APIHelper(httpClient, authOptions, tenantId, graphBaseUrl, timeouts, certificatePath, certificatePassword, scope);
}
/// <summary>
@@ -134,7 +142,7 @@ internal class APIHelper : IDisposable
{
["grant_type"] = "client_credentials",
["client_id"] = _authOptions.Username!,
["scope"] = $"{_graphBaseUrl.TrimEnd('/')}/.default"
["scope"] = _scope
};
if (!string.IsNullOrWhiteSpace(_certificatePath))
+13 -3
View File
@@ -12,6 +12,9 @@ internal static class OptionsHelper
internal const string OFFICE_CERTIFICATE_PATH_OPTION = "office-certificate-path";
internal const string OFFICE_CERTIFICATE_PASSWORD_OPTION = "office-certificate-password";
internal const string OFFICE_GRAPH_BASE_OPTION = "office-graph-base-url";
internal const string OFFICE_SCOPE_OPTION = "office-scope";
internal const string OFFICE_SCOPE_OPTION_DEFAULT = "https://graph.microsoft.com/.default";
internal const string OFFICE_IGNORE_EXISTING_OPTION = "office-ignore-existing";
internal const string DEFAULT_GRAPH_BASE_URL = "https://graph.microsoft.com";
@@ -23,7 +26,8 @@ internal static class OptionsHelper
AuthOptionsHelper.AuthOptions AuthOptions,
string GraphBaseUrl,
string? CertificatePath,
string? CertificatePassword
string? CertificatePassword,
string Scope
);
internal static ParsedOptions ParseAndValidateOptions(string url, Dictionary<string, string?> options)
@@ -41,12 +45,17 @@ internal static class OptionsHelper
var _certificatePath = options.GetValueOrDefault(OFFICE_CERTIFICATE_PATH_OPTION);
var _certificatePassword = options.GetValueOrDefault(OFFICE_CERTIFICATE_PASSWORD_OPTION);
var _scope = options.GetValueOrDefault(OFFICE_SCOPE_OPTION);
if (string.IsNullOrWhiteSpace(_scope))
_scope = OFFICE_SCOPE_OPTION_DEFAULT;
return new ParsedOptions(
TenantId: _tenantId,
AuthOptions: _authOptions,
GraphBaseUrl: _graphBaseUrl,
CertificatePath: _certificatePath,
CertificatePassword: _certificatePassword
CertificatePassword: _certificatePassword,
Scope: _scope
);
}
@@ -57,6 +66,7 @@ internal static class OptionsHelper
new CommandLineArgument(OFFICE_SECRET_OPTION, CommandLineArgument.ArgumentType.Password, Strings.OfficeSecretOptionShort, Strings.OfficeSecretOptionLong, null, [AuthOptionsHelper.AuthPasswordOption], null),
new CommandLineArgument(OFFICE_CERTIFICATE_PATH_OPTION, CommandLineArgument.ArgumentType.Path, Strings.OfficeCertificatePathOptionShort, Strings.OfficeCertificatePathOptionLong),
new CommandLineArgument(OFFICE_CERTIFICATE_PASSWORD_OPTION, CommandLineArgument.ArgumentType.Password, Strings.OfficeCertificatePasswordOptionShort, Strings.OfficeCertificatePasswordOptionLong),
new CommandLineArgument(OFFICE_GRAPH_BASE_OPTION, CommandLineArgument.ArgumentType.String, Strings.OfficeGraphBaseOptionShort, Strings.OfficeGraphBaseOptionLong, DEFAULT_GRAPH_BASE_URL)
new CommandLineArgument(OFFICE_GRAPH_BASE_OPTION, CommandLineArgument.ArgumentType.String, Strings.OfficeGraphBaseOptionShort, Strings.OfficeGraphBaseOptionLong, DEFAULT_GRAPH_BASE_URL),
new CommandLineArgument(OFFICE_SCOPE_OPTION, CommandLineArgument.ArgumentType.String, Strings.OfficeScopeOptionShort, Strings.OfficeScopeOptionLong, OFFICE_SCOPE_OPTION_DEFAULT)
];
}
@@ -497,7 +497,7 @@ partial class RestoreProvider
{
try
{
if (!Provider._overwrite && !string.IsNullOrWhiteSpace(eventItem.Subject))
if (!Provider._ignoreExisting && !string.IsNullOrWhiteSpace(eventItem.Subject))
{
DateTimeOffset? start = null;
if (eventItem.Start is JsonElement startElem && startElem.ValueKind == JsonValueKind.Object)
@@ -542,7 +542,7 @@ partial class RestoreProvider
{
try
{
if (!Provider._overwrite && !string.IsNullOrWhiteSpace(eventItem.Subject))
if (!Provider._ignoreExisting && !string.IsNullOrWhiteSpace(eventItem.Subject))
{
DateTimeOffset? start = null;
if (eventItem.Start is JsonElement startElem && startElem.ValueKind == JsonValueKind.Object)
@@ -110,7 +110,7 @@ public partial class RestoreProvider : IRestoreDestinationProviderModule
/// <summary>
/// Whether to overwrite existing items
/// </summary>
internal readonly bool _overwrite;
internal readonly bool _ignoreExisting;
/// <summary>
/// Default constructor for the restore provider.
@@ -140,10 +140,14 @@ public partial class RestoreProvider : IRestoreDestinationProviderModule
graphBaseUrl: parsedOptions.GraphBaseUrl,
certificatePath: parsedOptions.CertificatePath,
certificatePassword: parsedOptions.CertificatePassword,
timeouts: TimeoutOptionsHelper.Parse(options)
timeouts: TimeoutOptionsHelper.Parse(options),
scope: parsedOptions.Scope
);
_overwrite = Utility.ParseBoolOption(options, "overwrite");
_ignoreExisting = Utility.ParseBoolOption(options, OptionsHelper.OFFICE_IGNORE_EXISTING_OPTION);
var overwrite = Utility.ParseBoolOption(options, "overwrite");
if (!overwrite)
throw new UserInformationException(Strings.RestoreTargetMissingOverwriteOption(OptionsHelper.OFFICE_IGNORE_EXISTING_OPTION), "OverwriteOptionNotSet");
var sourceOpts = new Dictionary<string, string?>(options)
{
@@ -167,7 +171,7 @@ public partial class RestoreProvider : IRestoreDestinationProviderModule
public string Description => Strings.ProviderDescription;
/// <inheritdoc />
public IList<ICommandLineArgument> SupportedCommands => OptionsHelper.SupportedCommands;
public IList<ICommandLineArgument> SupportedCommands => OptionsHelper.SupportedCommands.Concat([new CommandLineArgument(OptionsHelper.OFFICE_IGNORE_EXISTING_OPTION, CommandLineArgument.ArgumentType.Boolean, Strings.OfficeIgnoreExistingOptionShort, Strings.OfficeIgnoreExistingOptionLong)]).ToList();
/// <inheritdoc />
public string TargetDestination => _restorePath;
@@ -847,7 +851,7 @@ public partial class RestoreProvider : IRestoreDestinationProviderModule
targetFolderId = mappedFolderId;
}
if (!string.IsNullOrWhiteSpace(internetMessageId) && await EmailApi.EmailExistsInFolderByInternetMessageIdAsync(userId, targetFolderId, internetMessageId, cancel))
if (!_ignoreExisting && !string.IsNullOrWhiteSpace(internetMessageId) && await EmailApi.EmailExistsInFolderByInternetMessageIdAsync(userId, targetFolderId, internetMessageId, cancel))
{
Log.WriteInformationMessage(LOGTAG, "RestoreUserEmailsSkipExisting", null, $"Email with InternetMessageId {internetMessageId} already exists in target mailbox, skipping restore for {emailSource.Key}.");
}
@@ -1099,7 +1103,7 @@ public partial class RestoreProvider : IRestoreDestinationProviderModule
var displayName = file.Value.GetValueOrDefault("o365:Name") ?? Path.GetFileName(originalPath);
if (!_overwrite)
if (!_ignoreExisting)
{
var existingItem = await DriveApi.GetDriveItemAsync(driveId, parentId, displayName, cancel);
if (existingItem != null)
@@ -1316,7 +1320,7 @@ public partial class RestoreProvider : IRestoreDestinationProviderModule
targetFolderId = mappedFolderId;
}
if (!_overwrite)
if (!_ignoreExisting)
{
using (var checkStream = SystemIO.IO_OS.FileOpenRead(contentEntry))
{
@@ -1496,7 +1500,7 @@ public partial class RestoreProvider : IRestoreDestinationProviderModule
continue;
}
if (!_overwrite)
if (!_ignoreExisting)
{
if (!existingTasksByPlan.TryGetValue(planId, out var existingTasks))
{
@@ -1576,7 +1580,7 @@ public partial class RestoreProvider : IRestoreDestinationProviderModule
taskData?.AppliedCategories,
cancel);
if (!_overwrite && existingTasksByPlan.TryGetValue(planId, out var currentTasks))
if (!_ignoreExisting && existingTasksByPlan.TryGetValue(planId, out var currentTasks))
{
currentTasks.Add(newTask);
}
@@ -129,7 +129,8 @@ public sealed partial class SourceProvider : ISourceProviderModule, IDisposable
graphBaseUrl: parsedOptions.GraphBaseUrl,
timeouts: _timeouts,
certificatePath: parsedOptions.CertificatePath,
certificatePassword: parsedOptions.CertificatePassword
certificatePassword: parsedOptions.CertificatePassword,
scope: parsedOptions.Scope
);
}
+9
View File
@@ -20,6 +20,9 @@ internal static class Strings
public static string MetadataStorageNotEnabled(string optionname) => LC.L(
$"Storing metadata content in the database must be enabled to use the Microsoft 365 source provider. Use the option: --{optionname}");
public static string RestoreTargetMissingOverwriteOption(string optionname) => LC.L($"The Office365 restore target must have the overwrite option set to true. Use the provider specific option --{optionname} to avoid overwriting existing items.");
public static string MissingTenantId => LC.L("A tenant ID must be supplied to query the Office 365 Management API.");
public static string OfficeTenantOptionShort => LC.L("Azure AD tenant identifier.");
@@ -44,6 +47,12 @@ internal static class Strings
public static string OfficeGraphBaseOptionLong => LC.L("Base URL for Microsoft Graph if targeting a sovereign cloud.");
public static string OfficeIgnoreExistingOptionShort => LC.L("Ignore existing items.");
public static string OfficeIgnoreExistingOptionLong => LC.L("If set, existing items in the destination will not be overwritten.");
public static string OfficeScopeOptionShort => LC.L("Microsoft Graph API scope.");
public static string OfficeScopeOptionLong => LC.L("The scope to use when requesting an access token for the Microsoft Graph API.");
public static string WebModuleOperationShort => LC.L("The operation to perform.");
public static string WebModuleOperationLong => LC.L("The operation that the web module should perform.");