2025-02-17 16:45:51 +01:00
// Copyright (C) 2025, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
2024-04-15 13:17:49 +02:00
// DEALINGS IN THE SOFTWARE.
2024-02-28 15:45:30 +01:00
using Duplicati.Library.Backend.MicrosoftGraph ;
2019-05-25 20:09:05 -07:00
using Duplicati.Library.Common.IO ;
2019-02-22 21:58:40 -06:00
using Duplicati.Library.Interface ;
2021-03-23 17:08:58 -06:00
using Duplicati.Library.Logging ;
2019-02-22 21:58:40 -06:00
using Duplicati.Library.Utility ;
2025-03-21 15:58:56 +01:00
using Duplicati.Library.Utility.Options ;
2019-02-22 21:58:40 -06:00
using Newtonsoft.Json ;
2018-05-25 09:18:20 -06:00
using System.Net ;
using System.Net.Http.Headers ;
2025-02-17 16:45:51 +01:00
using System.Runtime.CompilerServices ;
2018-05-25 09:18:20 -06:00
using System.Text ;
namespace Duplicati.Library.Backend
{
/// <summary>
/// Base class for all backends based on the Microsoft Graph API:
/// https://developer.microsoft.com/en-us/graph/
/// </summary>
/// <remarks>
/// HttpClient is used instead of OAuthHelper because OAuthHelper internally converts URLs to System.Uri, which throws UriFormatException when the URL contains ':' characters.
/// https://stackoverflow.com/questions/2143856/why-does-colon-in-uri-passed-to-uri-makerelativeuri-cause-an-exception
/// https://social.msdn.microsoft.com/Forums/vstudio/en-US/bf11fc74-975a-4c4d-8335-8e0579d17fdf/uri-containing-colons-incorrectly-throws-uriformatexception?forum=netfxbcl
///
/// Note that instead of using Task.Result to wait for the results of asynchronous operations,
/// this class uses the Utility.Await() extension method, since it doesn't wrap exceptions in AggregateExceptions.
/// </remarks>
2026-01-28 14:37:51 +01:00
public abstract class MicrosoftGraphBackend : IBackend , IStreamingBackend , IQuotaEnabledBackend , IRenameEnabledBackend , IFolderEnabledBackend
2018-05-25 09:18:20 -06:00
{
2021-03-23 17:08:58 -06:00
private static readonly string LOGTAG = Log . LogTagFromType < MicrosoftGraphBackend >();
2018-05-25 09:18:20 -06:00
private const string SERVICES_AGREEMENT = "https://www.microsoft.com/en-us/servicesagreement" ;
private const string PRIVACY_STATEMENT = "https://privacy.microsoft.com/en-us/privacystatement" ;
private const string BASE_ADDRESS = "https://graph.microsoft.com" ;
private const string UPLOAD_SESSION_FRAGMENT_SIZE_OPTION = "fragment-size" ;
private const string UPLOAD_SESSION_FRAGMENT_RETRY_COUNT_OPTION = "fragment-retry-count" ;
private const string UPLOAD_SESSION_FRAGMENT_RETRY_DELAY_OPTION = "fragment-retry-delay" ;
private const int UPLOAD_SESSION_FRAGMENT_DEFAULT_RETRY_COUNT = 5 ;
private const int UPLOAD_SESSION_FRAGMENT_DEFAULT_RETRY_DELAY = 1000 ;
/// <summary>
/// Max size of file that can be uploaded in a single PUT request is 4 MB:
/// https://developer.microsoft.com/en-us/graph/docs/api-reference/v1.0/api/driveitem_put_content
/// </summary>
private const int PUT_MAX_SIZE = 4 * 1000 * 1000 ;
/// <summary>
/// Max size of each individual upload in an upload session is 60 MiB:
/// https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_createuploadsession
/// </summary>
private const int UPLOAD_SESSION_FRAGMENT_MAX_SIZE = 60 * 1024 * 1024 ;
/// <summary>
/// Default fragment size of 10 MiB, as the documentation recommends something in the range of 5-10 MiB,
/// and it still complies with the 320 KiB multiple requirement.
/// </summary>
private const int UPLOAD_SESSION_FRAGMENT_DEFAULT_SIZE = 10 * 1024 * 1024 ;
/// <summary>
/// Each fragment in an upload session must be a size that is multiple of this size.
/// https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_createuploadsession
/// There is some confusion in the docs as to whether this is actually required, however...
/// </summary>
private const int UPLOAD_SESSION_FRAGMENT_MULTIPLE_SIZE = 320 * 1024 ;
2024-04-15 13:17:49 +02:00
/// <summary>
/// Cached copy of the PATH method
/// </summary>
2018-05-25 09:18:20 -06:00
private static readonly HttpMethod PatchMethod = new HttpMethod ( "PATCH" );
2020-03-23 14:10:40 -06:00
/// <summary>
/// Dummy UploadSession given as an empty body to createUploadSession requests when using the OAuthHelper instead of the OAuthHttpClient.
/// The API expects a ContentLength to be specified, but the body content is optional.
/// Passing an empty object (or specifying the ContentLength explicitly) bypasses this error.
/// </summary>
private static readonly UploadSession dummyUploadSession = new UploadSession ();
2018-05-25 09:18:20 -06:00
protected delegate string DescriptionTemplateDelegate ( string mssadescription , string mssalink , string msopdescription , string msoplink );
private readonly JsonSerializer m_serializer = new JsonSerializer ();
private readonly OAuthHttpClient m_client ;
private readonly int fragmentSize ;
private readonly int fragmentRetryCount ;
private readonly int fragmentRetryDelay ; // In milliseconds
2021-03-23 17:28:39 -06:00
// Whenever a response includes a Retry-After header, we'll update this timestamp with when we can next
// send a request. And before sending any requests, we'll make sure to wait until at least this time.
// Since this may be read and written by multiple threads, it is stored as a long and updated using Interlocked.Exchange.
private readonly RetryAfterHelper m_retryAfter ;
2025-03-21 15:58:56 +01:00
protected readonly TimeoutOptionsHelper . Timeouts m_timeouts ;
2021-03-23 17:28:39 -06:00
2025-03-21 15:58:56 +01:00
private string []? dnsNames = null ;
2018-05-25 09:18:20 -06:00
2019-05-25 20:09:05 -07:00
private readonly Lazy < string > rootPathFromURL ;
private string RootPath => this . rootPathFromURL . Value ;
2025-06-11 09:27:40 +02:00
private string TokenUrl => AuthIdOptionsHelper . GetOAuthLoginUrl ( ProtocolKey , null );
2019-05-25 20:09:05 -07:00
2025-03-21 15:58:56 +01:00
protected MicrosoftGraphBackend ()
{
// Constructor needed for dynamic loading to find it
m_client = null !;
m_retryAfter = null !;
m_timeouts = null !;
rootPathFromURL = null !;
}
2018-05-25 09:18:20 -06:00
2025-03-21 15:58:56 +01:00
protected MicrosoftGraphBackend ( string url , string protocolKey , Dictionary < string , string? > options )
2018-05-25 09:18:20 -06:00
{
2025-03-21 15:58:56 +01:00
var authId = AuthIdOptionsHelper . Parse ( options )
. RequireCredentials ( TokenUrl );
2018-05-25 09:18:20 -06:00
2025-03-21 15:58:56 +01:00
if ( options . TryGetValue ( UPLOAD_SESSION_FRAGMENT_SIZE_OPTION , out var fragmentSizeStr ) && int . TryParse ( fragmentSizeStr , out this . fragmentSize ))
2018-05-25 09:18:20 -06:00
{
// Make sure the fragment size is a multiple of the desired multiple size.
// If it isn't, we round down to the nearest multiple below it.
this . fragmentSize = ( this . fragmentSize / UPLOAD_SESSION_FRAGMENT_MULTIPLE_SIZE ) * UPLOAD_SESSION_FRAGMENT_MULTIPLE_SIZE ;
// Make sure the fragment size isn't larger than the maximum, or smaller than the minimum
this . fragmentSize = Math . Max ( Math . Min ( this . fragmentSize , UPLOAD_SESSION_FRAGMENT_MAX_SIZE ), UPLOAD_SESSION_FRAGMENT_MULTIPLE_SIZE );
}
else
{
this . fragmentSize = UPLOAD_SESSION_FRAGMENT_DEFAULT_SIZE ;
}
2025-03-21 15:58:56 +01:00
if (!( options . TryGetValue ( UPLOAD_SESSION_FRAGMENT_RETRY_COUNT_OPTION , out var fragmentRetryCountStr ) && int . TryParse ( fragmentRetryCountStr , out this . fragmentRetryCount )))
2018-05-25 09:18:20 -06:00
this . fragmentRetryCount = UPLOAD_SESSION_FRAGMENT_DEFAULT_RETRY_COUNT ;
2025-03-21 15:58:56 +01:00
if (!( options . TryGetValue ( UPLOAD_SESSION_FRAGMENT_RETRY_DELAY_OPTION , out var fragmentRetryDelayStr ) && int . TryParse ( fragmentRetryDelayStr , out this . fragmentRetryDelay )))
2018-05-25 09:18:20 -06:00
this . fragmentRetryDelay = UPLOAD_SESSION_FRAGMENT_DEFAULT_RETRY_DELAY ;
2025-06-11 09:27:40 +02:00
this . m_client = new OAuthHttpClient ( authId . AuthId !, protocolKey , authId . OAuthUrl );
2024-12-19 22:40:18 +01:00
this . m_client . BaseAddress = new System . Uri ( BASE_ADDRESS );
2018-05-25 09:18:20 -06:00
2024-11-01 09:20:11 +01:00
this . m_retryAfter = RetryAfterHelper . CreateOrGetRetryAfterHelper ( url );
2025-03-21 15:58:56 +01:00
this . m_timeouts = TimeoutOptionsHelper . Parse ( options );
2021-03-23 17:28:39 -06:00
2024-07-26 00:08:45 -04:00
// Extract out the path to the backup root folder from the given URI. Since this can be an expensive operation,
2018-09-30 15:06:29 -07:00
// we will cache the value using a lazy initializer.
2024-10-01 09:30:02 +02:00
// TODO: Should not call network methods in constructor
this . rootPathFromURL = new Lazy < string >(() => MicrosoftGraphBackend . NormalizeSlashes ( Utility . Utility . Await ( this . GetRootPathFromUrlAsync ( url , CancellationToken . None ))));
2018-05-25 09:18:20 -06:00
}
2025-11-03 12:48:37 +01:00
/// <inheritdoc/>
2018-05-25 09:18:20 -06:00
public abstract string ProtocolKey { get ; }
2025-11-03 12:48:37 +01:00
/// <inheritdoc/>
2018-05-25 09:18:20 -06:00
public abstract string DisplayName { get ; }
2025-11-03 12:48:37 +01:00
/// <inheritdoc/>
2025-03-21 15:58:56 +01:00
public string Description => this . DescriptionTemplate (
2018-05-25 09:18:20 -06:00
"Microsoft Service Agreement" ,
SERVICES_AGREEMENT ,
2024-04-15 13:17:49 +02:00
"Microsoft Online Privacy Statement" ,
PRIVACY_STATEMENT );
2018-05-25 09:18:20 -06:00
2025-11-03 12:48:37 +01:00
/// <inheritdoc/>
public bool SupportsStreaming => true ;
/// <inheritdoc/>
2025-03-21 15:58:56 +01:00
public IList < ICommandLineArgument > SupportedCommands => [
.. AuthIdOptionsHelper . GetOptions ( TokenUrl ),
new CommandLineArgument ( UPLOAD_SESSION_FRAGMENT_SIZE_OPTION , CommandLineArgument . ArgumentType . Integer , Strings . MicrosoftGraph . FragmentSizeShort , Strings . MicrosoftGraph . FragmentSizeLong , Library . Utility . Utility . FormatSizeString ( UPLOAD_SESSION_FRAGMENT_DEFAULT_SIZE )),
new CommandLineArgument ( UPLOAD_SESSION_FRAGMENT_RETRY_COUNT_OPTION , CommandLineArgument . ArgumentType . Integer , Strings . MicrosoftGraph . FragmentRetryCountShort , Strings . MicrosoftGraph . FragmentRetryCountLong , UPLOAD_SESSION_FRAGMENT_DEFAULT_RETRY_COUNT . ToString ()),
new CommandLineArgument ( UPLOAD_SESSION_FRAGMENT_RETRY_DELAY_OPTION , CommandLineArgument . ArgumentType . Integer , Strings . MicrosoftGraph . FragmentRetryDelayShort , Strings . MicrosoftGraph . FragmentRetryDelayLong , UPLOAD_SESSION_FRAGMENT_DEFAULT_RETRY_DELAY . ToString ()),
new CommandLineArgument ( "use-http-client" , CommandLineArgument . ArgumentType . Boolean , Strings . MicrosoftGraph . UseHttpClientShort , Strings . MicrosoftGraph . UseHttpClientLong , "true" , null , null , Strings . MicrosoftGraph . UseHttpClientDeprecated ),
2025-03-31 09:11:59 +02:00
.. AdditionalSupportedCommands ,
2025-03-21 15:58:56 +01:00
.. TimeoutOptionsHelper . GetOptions ()
];
2018-05-25 09:18:20 -06:00
2024-10-02 09:37:52 +02:00
public async Task < string []> GetDNSNamesAsync ( CancellationToken cancelToken )
2018-05-25 09:18:20 -06:00
{
2025-03-21 15:58:56 +01:00
if ( dnsNames == null )
2018-05-25 09:18:20 -06:00
{
2024-10-01 09:30:02 +02:00
// The DNS names that this instance may need to access include:
// - Core graph API endpoint
// - Upload session endpoint (which seems to be different depending on the drive being accessed - not sure if it can vary for a single drive)
// To get the upload session endpoint, we can start an upload session and then immediately cancel it.
// We pick a random file name (using a guid) to make sure we don't conflict with an existing file
2025-03-21 15:58:56 +01:00
var dnsTestFile = string . Format ( "DNSNameTest-{0}" , Guid . NewGuid ());
var drivePrefix = await GetDrivePrefix ( cancelToken ). ConfigureAwait ( false );
var uploadSession = await this . PostAsync < UploadSession >( string . Format ( "{0}/root:{1}{2}:/createUploadSession" , drivePrefix , this . RootPath , NormalizeSlashes ( dnsTestFile )), dummyUploadSession , cancelToken ). ConfigureAwait ( false );
2024-10-01 09:30:02 +02:00
// Canceling an upload session is done by sending a DELETE to the upload URL
await m_retryAfter . WaitForRetryAfterAsync ( cancelToken ). ConfigureAwait ( false );
2024-12-19 22:40:18 +01:00
using ( var request = new HttpRequestMessage ( HttpMethod . Delete , uploadSession . UploadUrl ))
2025-03-21 15:58:56 +01:00
using ( var response = await Utility . Utility . WithTimeout ( m_timeouts . ShortTimeout , cancelToken ,
ct => m_client . SendAsync ( request , false , ct )). ConfigureAwait ( false ))
CheckResponse ( response );
2018-05-25 09:18:20 -06:00
2025-03-21 15:58:56 +01:00
dnsNames = new [] {
new System . Uri ( BASE_ADDRESS ). Host ,
string . IsNullOrWhiteSpace ( uploadSession . UploadUrl ) ? null : new System . Uri ( uploadSession . UploadUrl ). Host ,
}
. WhereNotNullOrWhiteSpace ()
2024-10-01 09:30:02 +02:00
. Distinct ( StringComparer . OrdinalIgnoreCase )
. ToArray ();
2018-05-25 09:18:20 -06:00
}
2024-10-01 09:30:02 +02:00
2025-03-21 15:58:56 +01:00
return dnsNames ;
2018-05-25 09:18:20 -06:00
}
2025-03-21 15:58:56 +01:00
public async Task < IQuotaInfo ?> GetQuotaInfoAsync ( CancellationToken cancelToken )
2018-05-25 09:18:20 -06:00
{
2025-03-21 15:58:56 +01:00
var drivePrefix = await GetDrivePrefix ( cancelToken ). ConfigureAwait ( false );
var driveInfo = await Utility . Utility . WithTimeout ( m_timeouts . ShortTimeout , cancelToken ,
ct => GetAsync < Drive >( drivePrefix , ct )
). ConfigureAwait ( false );
if ( driveInfo ?. Quota != null )
2018-05-25 09:18:20 -06:00
{
2024-10-01 09:30:02 +02:00
// Some sources (SharePoint for example) seem to return 0 for these values even when the quota isn't exceeded..
// As a special test, if all the returned values are 0, we pretend that no quota was reported.
// This way we don't send spurious warnings because the quota looks like it is exceeded.
if ( driveInfo . Quota . Total != 0 || driveInfo . Quota . Remaining != 0 || driveInfo . Quota . Used != 0 )
return new QuotaInfo ( driveInfo . Quota . Total , driveInfo . Quota . Remaining );
2018-05-25 09:18:20 -06:00
}
2024-10-01 09:30:02 +02:00
return null ;
2018-05-25 09:18:20 -06:00
}
/// <summary>
/// Override-able fragment indicating the API version to use each query
/// </summary>
2025-03-21 15:58:56 +01:00
protected virtual string ApiVersion => "/v1.0" ;
2018-05-25 09:18:20 -06:00
/// <summary>
/// Normalized fragment (starting with a slash and ending without one) for the path to the drive to be used.
/// For example: "/me/drive" for the default drive for a user.
/// </summary>
2025-03-21 15:58:56 +01:00
protected abstract Task < string > GetDrivePath ( CancellationToken cancelToken );
2018-05-25 09:18:20 -06:00
protected abstract DescriptionTemplateDelegate DescriptionTemplate { get ; }
2025-03-21 15:58:56 +01:00
protected virtual IList < ICommandLineArgument > AdditionalSupportedCommands => [];
2018-05-25 09:18:20 -06:00
2025-03-21 15:58:56 +01:00
private async Task < string > GetDrivePrefix ( CancellationToken cancelToken )
=> $"{ApiVersion}{await GetDrivePath(cancelToken).ConfigureAwait(false)}" ;
2018-05-25 09:18:20 -06:00
2024-10-01 09:30:02 +02:00
public async Task CreateFolderAsync ( CancellationToken cancelToken )
2018-05-25 09:18:20 -06:00
{
2025-03-21 15:58:56 +01:00
var parentFolder = "root" ;
var parentFolderPath = string . Empty ;
var drivePrefix = await GetDrivePrefix ( cancelToken ). ConfigureAwait ( false );
2018-09-30 14:01:44 -07:00
foreach ( string folder in this . RootPath . Split ( new [] { '/' }, StringSplitOptions . RemoveEmptyEntries ))
2018-05-25 09:18:20 -06:00
{
string nextPath = parentFolderPath + "/" + folder ;
DriveItem folderItem ;
try
{
2025-03-21 15:58:56 +01:00
folderItem = await Utility . Utility . WithTimeout ( m_timeouts . ShortTimeout , cancelToken ,
ct => GetAsync < DriveItem >( string . Format ( "{0}/root:{1}" , drivePrefix , NormalizeSlashes ( nextPath )), ct )
). ConfigureAwait ( false );
2018-05-25 09:18:20 -06:00
}
catch ( DriveItemNotFoundException )
{
2024-10-01 09:30:02 +02:00
var newFolder = new DriveItem ()
2018-05-25 09:18:20 -06:00
{
Name = folder ,
Folder = new FolderFacet (),
};
2025-03-21 15:58:56 +01:00
folderItem = await Utility . Utility . WithTimeout ( m_timeouts . ShortTimeout , cancelToken ,
ct => PostAsync ( string . Format ( "{0}/items/{1}/children" , drivePrefix , parentFolder ), newFolder , ct )
). ConfigureAwait ( false );
2018-05-25 09:18:20 -06:00
}
parentFolder = folderItem . Id ;
parentFolderPath = nextPath ;
}
}
2026-01-28 14:37:51 +01:00
private IFileEntry ParseEntry ( DriveItem item )
{
var fe = new FileEntry (
item . Name ,
item . Size ?? 0 ,
item . FileSystemInfo ?. LastAccessedDateTime ?. UtcDateTime ?? new DateTime (),
item . FileSystemInfo ?. LastModifiedDateTime ?. UtcDateTime ?? item . LastModifiedDateTime ?. UtcDateTime ?? new DateTime ());
fe . IsFolder = item . IsFolder ;
if ( fe . IsFolder && ! fe . Name . EndsWith ( "/" ))
fe . Name += "/" ;
return fe ;
}
2026-01-29 21:46:02 +01:00
private string GetAbsolutePath ( string? path )
{
if ( string . IsNullOrWhiteSpace ( path ))
return this . RootPath ;
var p = path . Replace ( Path . DirectorySeparatorChar , '/' ). Trim ( '/' );
if ( string . IsNullOrWhiteSpace ( this . RootPath ) || this . RootPath == "/" )
p = "/" + p ;
else
p = Util . AppendDirSeparator ( this . RootPath , "/" ) + p ;
if ( p == "/" )
p = "" ;
return p ;
}
2026-01-28 14:37:51 +01:00
public async IAsyncEnumerable < IFileEntry > ListAsync ( string? path , [ EnumeratorCancellation ] CancellationToken cancelToken )
{
var drivePrefix = await GetDrivePrefix ( cancelToken ). ConfigureAwait ( false );
2026-01-29 21:46:02 +01:00
var targetPath = GetAbsolutePath ( path );
2026-01-28 14:37:51 +01:00
string url ;
if ( targetPath == "/" || string . IsNullOrEmpty ( targetPath ))
url = $"{drivePrefix}/root/children" ;
else
url = $"{drivePrefix}/root:{targetPath}:/children" ;
await foreach ( var item in this . Enumerate < DriveItem >( url , cancelToken ). ConfigureAwait ( false ))
{
if (! item . IsDeleted )
yield return ParseEntry ( item );
}
}
public async Task < IFileEntry ?> GetEntryAsync ( string path , CancellationToken cancellationToken )
{
var drivePrefix = await GetDrivePrefix ( cancellationToken ). ConfigureAwait ( false );
2026-01-29 21:46:02 +01:00
var targetPath = GetAbsolutePath ( path );
2026-01-28 14:37:51 +01:00
string url ;
if ( targetPath == "/" || string . IsNullOrEmpty ( targetPath ))
url = $"{drivePrefix}/root" ;
else
url = $"{drivePrefix}/root:{targetPath}" ;
try
{
var item = await Utility . Utility . WithTimeout ( m_timeouts . ShortTimeout , cancellationToken ,
ct => GetAsync < DriveItem >( url , ct )
). ConfigureAwait ( false );
return ParseEntry ( item );
}
catch ( DriveItemNotFoundException )
{
return null ;
}
}
2025-02-17 16:45:51 +01:00
/// <inheritdoc />
public async IAsyncEnumerable < IFileEntry > ListAsync ([ EnumeratorCancellation ] CancellationToken cancelToken )
2018-05-25 09:18:20 -06:00
{
2025-03-21 15:58:56 +01:00
var drivePrefix = await GetDrivePrefix ( cancelToken ). ConfigureAwait ( false );
await foreach ( var item in this . Enumerate < DriveItem >( string . Format ( "{0}/root:{1}:/children" , drivePrefix , this . RootPath ), cancelToken ). ConfigureAwait ( false ))
2018-05-25 09:18:20 -06:00
{
2019-11-13 12:18:19 -07:00
// Exclude non-files and deleted items (not sure if they show up in this listing, but make sure anyway)
if ( item . IsFile && ! item . IsDeleted )
{
2026-01-28 14:37:51 +01:00
yield return ParseEntry ( item );
2019-11-13 12:18:19 -07:00
}
2018-05-25 09:18:20 -06:00
}
}
2024-09-29 22:00:57 +02:00
public async Task GetAsync ( string remotename , string filename , CancellationToken cancelToken )
2018-05-25 09:18:20 -06:00
{
2024-09-29 22:00:57 +02:00
using ( var fileStream = File . OpenWrite ( filename ))
await GetAsync ( remotename , fileStream , cancelToken ). ConfigureAwait ( false );
2018-05-25 09:18:20 -06:00
}
2024-09-29 22:00:57 +02:00
public async Task GetAsync ( string remotename , Stream stream , CancellationToken cancelToken )
2018-05-25 09:18:20 -06:00
{
2018-06-02 15:52:20 -06:00
try
{
2025-03-21 15:58:56 +01:00
var drivePrefix = await GetDrivePrefix ( cancelToken ). ConfigureAwait ( false );
2021-03-23 17:28:39 -06:00
m_retryAfter . WaitForRetryAfter ();
2025-03-21 15:58:56 +01:00
string getUrl = string . Format ( "{0}/root:{1}{2}:/content" , drivePrefix , this . RootPath , NormalizeSlashes ( remotename ));
using ( var response = await Utility . Utility . WithTimeout ( m_timeouts . ShortTimeout , cancelToken ,
ct => m_client . GetAsync ( getUrl , HttpCompletionOption . ResponseHeadersRead , ct )
). ConfigureAwait ( false ))
2018-06-02 15:52:20 -06:00
{
2025-03-21 15:58:56 +01:00
CheckResponse ( response );
using ( var responseStream = await Utility . Utility . WithTimeout ( m_timeouts . ShortTimeout , cancelToken ,
ct => response . Content . ReadAsStreamAsync ( ct )). ConfigureAwait ( false ))
using ( var timeoutSream = responseStream . ObserveReadTimeout ( m_timeouts . ReadWriteTimeout ))
await Utility . Utility . CopyStreamAsync ( responseStream , stream , cancelToken ). ConfigureAwait ( false );
2018-06-02 15:52:20 -06:00
}
}
catch ( DriveItemNotFoundException ex )
2018-05-25 09:18:20 -06:00
{
2018-06-02 15:52:20 -06:00
// If the item wasn't found, wrap the exception so normal handling can occur.
throw new FileMissingException ( ex );
2018-05-25 09:18:20 -06:00
}
}
2024-10-01 09:30:02 +02:00
public async Task RenameAsync ( string oldname , string newname , CancellationToken cancelToken )
2018-05-25 09:18:20 -06:00
{
2018-06-02 15:52:20 -06:00
try
{
2025-03-21 15:58:56 +01:00
var drivePrefix = await GetDrivePrefix ( cancelToken ). ConfigureAwait ( false );
await Utility . Utility . WithTimeout ( m_timeouts . ShortTimeout , cancelToken ,
ct => PatchAsync ( string . Format ( "{0}/root:{1}{2}" , drivePrefix , this . RootPath , NormalizeSlashes ( oldname )), new DriveItem () { Name = newname }, ct )
). ConfigureAwait ( false );
2018-06-02 15:52:20 -06:00
}
catch ( DriveItemNotFoundException ex )
{
// If the item wasn't found, wrap the exception so normal handling can occur.
throw new FileMissingException ( ex );
}
2018-05-25 09:18:20 -06:00
}
2019-11-13 12:18:19 -07:00
public async Task PutAsync ( string remotename , string filename , CancellationToken cancelToken )
2018-05-25 09:18:20 -06:00
{
using ( FileStream fileStream = File . OpenRead ( filename ))
2019-11-14 07:51:49 -07:00
await PutAsync ( remotename , fileStream , cancelToken ). ConfigureAwait ( false );
2018-05-25 09:18:20 -06:00
}
2019-03-17 18:20:14 -05:00
public async Task PutAsync ( string remotename , Stream stream , CancellationToken cancelToken )
2018-05-25 09:18:20 -06:00
{
2025-03-21 15:58:56 +01:00
var drivePrefix = await GetDrivePrefix ( cancelToken ). ConfigureAwait ( false );
2018-05-25 09:18:20 -06:00
// PUT only supports up to 4 MB file uploads. There's a separate process for larger files.
if ( stream . Length < PUT_MAX_SIZE )
{
2021-03-23 17:28:39 -06:00
await m_retryAfter . WaitForRetryAfterAsync ( cancelToken ). ConfigureAwait ( false );
2025-03-21 15:58:56 +01:00
string putUrl = string . Format ( "{0}/root:{1}{2}:/content" , drivePrefix , this . RootPath , NormalizeSlashes ( remotename ));
using ( var timeoutStream = stream . ObserveReadTimeout ( m_timeouts . ReadWriteTimeout , false ))
using ( var streamContent = new StreamContent ( timeoutStream ))
2020-03-23 14:10:40 -06:00
{
2024-12-19 22:40:18 +01:00
streamContent . Headers . ContentType = new MediaTypeHeaderValue ( "application/octet-stream" );
using ( var response = await this . m_client . PutAsync ( putUrl , streamContent , cancelToken ). ConfigureAwait ( false ))
2019-11-13 12:18:19 -07:00
{
// Make sure this response is a valid drive item, though we don't actually use it for anything currently.
2024-10-01 09:30:02 +02:00
await this . ParseResponseAsync < DriveItem >( response , cancelToken ). ConfigureAwait ( false );
2019-11-13 12:18:19 -07:00
}
}
2018-05-25 09:18:20 -06:00
}
else
{
// This file is too large to be sent in a single request, so we need to send it in pieces in an upload session:
// https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_createuploadsession
// The documentation seems somewhat contradictory - it states that uploads must be done sequentially,
// but also states that the nextExpectedRanges value returned may indicate multiple ranges...
// For now, this plays it safe and does a sequential upload.
2025-03-21 15:58:56 +01:00
string createSessionUrl = string . Format ( "{0}/root:{1}{2}:/createUploadSession" , drivePrefix , this . RootPath , NormalizeSlashes ( remotename ));
2024-12-19 22:40:18 +01:00
await m_retryAfter . WaitForRetryAfterAsync ( cancelToken ). ConfigureAwait ( false );
2025-03-21 15:58:56 +01:00
using ( var createSessionRequest = new HttpRequestMessage ( HttpMethod . Post , createSessionUrl ))
using ( var createSessionResponse = await Utility . Utility . WithTimeout ( m_timeouts . ShortTimeout , cancelToken , ct => this . m_client . SendAsync ( createSessionRequest , ct )). ConfigureAwait ( false ))
2018-05-25 09:18:20 -06:00
{
2024-12-19 22:40:18 +01:00
var uploadSession = await this . ParseResponseAsync < UploadSession >( createSessionResponse , cancelToken ). ConfigureAwait ( false );
2018-05-25 09:18:20 -06:00
2024-12-19 22:40:18 +01:00
// If the stream's total length is less than the chosen fragment size, then we should make the buffer only as large as the stream.
int bufferSize = ( int ) Math . Min ( this . fragmentSize , stream . Length );
2018-05-25 09:18:20 -06:00
2024-12-19 22:40:18 +01:00
long read = 0 ;
for ( long offset = 0 ; offset < stream . Length ; offset += read )
{
// If the stream isn't long enough for this to be a full buffer, then limit the length
long currentBufferSize = bufferSize ;
if ( stream . Length < offset + bufferSize )
2018-05-25 09:18:20 -06:00
{
2024-12-19 22:40:18 +01:00
currentBufferSize = stream . Length - offset ;
2021-03-15 18:04:39 -06:00
}
2018-05-25 09:18:20 -06:00
2025-03-21 15:58:56 +01:00
using ( var subStream = new ReadLimitLengthStream ( stream , offset , currentBufferSize ))
2021-03-15 18:04:39 -06:00
{
2024-12-19 22:40:18 +01:00
read = subStream . Length ;
2021-03-17 20:37:46 -06:00
2024-12-19 22:40:18 +01:00
int fragmentCount = ( int ) Math . Ceiling (( double ) stream . Length / bufferSize );
int retryCount = this . fragmentRetryCount ;
for ( int attempt = 0 ; attempt < retryCount ; attempt ++)
2018-05-25 09:18:20 -06:00
{
2024-12-19 22:40:18 +01:00
await m_retryAfter . WaitForRetryAfterAsync ( cancelToken ). ConfigureAwait ( false );
int fragmentNumber = ( int )( offset / bufferSize );
Log . WriteVerboseMessage (
LOGTAG ,
"MicrosoftGraphFragmentUpload" ,
"Uploading fragment {0}/{1} of remote file {2}" ,
2025-10-31 13:41:10 +01:00
fragmentNumber + 1 ,
2024-12-19 22:40:18 +01:00
fragmentCount ,
remotename );
2025-03-21 15:58:56 +01:00
using ( var request = new HttpRequestMessage ( HttpMethod . Put , uploadSession . UploadUrl ))
using ( var timeoutStream = subStream . ObserveReadTimeout ( m_timeouts . ReadWriteTimeout , false ))
using ( var fragmentContent = new StreamContent ( timeoutStream ))
2019-11-13 12:18:19 -07:00
{
2024-12-19 22:40:18 +01:00
fragmentContent . Headers . ContentLength = read ;
fragmentContent . Headers . ContentRange = new ContentRangeHeaderValue ( offset , offset + read - 1 , stream . Length );
request . Content = fragmentContent ;
2019-11-13 12:18:19 -07:00
try
{
2024-12-19 22:40:18 +01:00
// The uploaded put requests will error if they are authenticated
2025-10-31 13:41:10 +01:00
using ( var response = await this . m_client . SendAsync ( request , false , cancelToken ). ConfigureAwait ( false ))
2019-11-13 12:18:19 -07:00
{
// Note: On the last request, the json result includes the default properties of the item that was uploaded
2024-10-01 09:30:02 +02:00
await this . ParseResponseAsync < UploadSession >( response , cancelToken ). ConfigureAwait ( false );
2019-11-13 12:18:19 -07:00
}
}
catch ( MicrosoftGraphException ex )
{
2021-03-15 18:04:39 -06:00
if ( subStream . Position != 0 )
{
if ( subStream . CanSeek )
{
// Make sure to reset the substream to its start in case this is a retry
subStream . Seek ( 0 , SeekOrigin . Begin );
}
else
{
// If any of the source stream was read and the substream can't be seeked back to the beginning,
// then the internal retry mechanism can't be used and the caller will have to retry this whole file.
// Should we consider signaling to the graph API that we're abandoning this upload session?
await this . ThrowUploadSessionException (
uploadSession ,
createSessionResponse ,
2025-10-31 13:41:10 +01:00
fragmentNumber + 1 ,
2021-03-23 17:08:58 -06:00
fragmentCount ,
2021-03-15 18:04:39 -06:00
ex ,
2021-03-16 11:40:38 -06:00
cancelToken ). ConfigureAwait ( false );
2021-03-15 18:04:39 -06:00
}
}
2019-11-13 12:18:19 -07:00
// Error handling based on recommendations here:
// https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_createuploadsession#best-practices
if ( attempt >= retryCount - 1 )
{
// We've used up all our retry attempts
2021-03-15 18:04:39 -06:00
await this . ThrowUploadSessionException (
uploadSession ,
createSessionResponse ,
2025-10-31 13:41:10 +01:00
fragmentNumber + 1 ,
2021-03-23 17:08:58 -06:00
fragmentCount ,
2021-03-15 18:04:39 -06:00
ex ,
2021-03-16 11:40:38 -06:00
cancelToken ). ConfigureAwait ( false );
2019-11-13 12:18:19 -07:00
}
2020-03-23 14:10:40 -06:00
else if (( int ) ex . StatusCode >= 500 && ( int ) ex . StatusCode < 600 )
2019-11-13 12:18:19 -07:00
{
2021-03-16 11:40:38 -06:00
// If a 5xx error code is hit, we should use an exponential backoff strategy before retrying.
// To make things simpler, we just use the current attempt number as the exponential factor.
// If there was a Retry-After header, we'll wait for that right before sending the next request as well.
2021-03-23 17:08:58 -06:00
TimeSpan delay = TimeSpan . FromMilliseconds (( int ) Math . Pow ( 2 , attempt ) * this . fragmentRetryDelay );
Log . WriteRetryMessage (
LOGTAG ,
"MicrosoftGraphFragmentRetryIn" ,
ex ,
"Uploading fragment {0}/{1} of remote file {2} failed and will be retried in {3}" ,
2025-10-31 13:41:10 +01:00
fragmentNumber + 1 ,
2021-03-23 17:08:58 -06:00
fragmentCount ,
remotename ,
delay );
await Task . Delay ( delay ). ConfigureAwait ( false );
2019-11-13 12:18:19 -07:00
continue ;
}
2020-03-23 14:10:40 -06:00
else if ( ex . StatusCode == HttpStatusCode . NotFound )
2019-11-13 12:18:19 -07:00
{
// 404 is a special case indicating the upload session no longer exists, so the fragment shouldn't be retried.
// Instead we'll let the caller re-attempt the whole file.
2021-03-15 18:04:39 -06:00
await this . ThrowUploadSessionException (
uploadSession ,
createSessionResponse ,
2025-10-31 13:41:10 +01:00
fragmentNumber + 1 ,
2021-03-23 17:08:58 -06:00
fragmentCount ,
2021-03-15 18:04:39 -06:00
ex ,
2021-03-16 11:40:38 -06:00
cancelToken ). ConfigureAwait ( false );
2019-11-13 12:18:19 -07:00
}
2020-03-23 14:10:40 -06:00
else if (( int ) ex . StatusCode >= 400 && ( int ) ex . StatusCode < 500 )
2019-11-13 12:18:19 -07:00
{
2021-03-15 18:04:39 -06:00
// If a 4xx error code is hit, we should retry without the exponential backoff attempt.
2021-03-23 17:08:58 -06:00
Log . WriteRetryMessage (
LOGTAG ,
"MicrosoftGraphFragmentRetry" ,
ex ,
"Uploading fragment {0}/{1} of remote file {2} failed and will be retried" ,
2025-10-31 13:41:10 +01:00
fragmentNumber + 1 ,
2021-03-23 17:08:58 -06:00
fragmentCount ,
remotename );
2019-11-13 12:18:19 -07:00
continue ;
}
else
{
// Other errors should be rethrown
2021-03-15 18:04:39 -06:00
await this . ThrowUploadSessionException (
uploadSession ,
createSessionResponse ,
2025-10-31 13:41:10 +01:00
fragmentNumber + 1 ,
2021-03-23 17:08:58 -06:00
fragmentCount ,
2021-03-15 18:04:39 -06:00
ex ,
2021-03-16 11:40:38 -06:00
cancelToken ). ConfigureAwait ( false );
2019-11-13 12:18:19 -07:00
}
}
2021-03-15 18:04:39 -06:00
catch ( Exception ex )
{
// Any other exceptions should also cause the upload session to be canceled.
await this . ThrowUploadSessionException (
uploadSession ,
createSessionResponse ,
2025-10-31 13:41:10 +01:00
fragmentNumber + 1 ,
2021-03-23 17:08:58 -06:00
fragmentCount ,
2021-03-15 18:04:39 -06:00
ex ,
2021-03-16 11:40:38 -06:00
cancelToken ). ConfigureAwait ( false );
2021-03-15 18:04:39 -06:00
}
2019-11-13 12:18:19 -07:00
// If we successfully sent this piece, then we can break out of the retry loop
break ;
}
2018-05-25 09:18:20 -06:00
}
}
}
}
}
}
2024-09-29 23:04:54 +02:00
public async Task DeleteAsync ( string remotename , CancellationToken cancelToken )
2018-05-25 09:18:20 -06:00
{
try
{
2025-03-21 15:58:56 +01:00
var drivePrefix = await GetDrivePrefix ( cancelToken ). ConfigureAwait ( false );
2024-09-29 23:04:54 +02:00
await m_retryAfter . WaitForRetryAfterAsync ( cancelToken ). ConfigureAwait ( false );
2025-03-21 15:58:56 +01:00
string deleteUrl = string . Format ( "{0}/root:{1}{2}" , drivePrefix , this . RootPath , NormalizeSlashes ( remotename ));
using ( var response = await Utility . Utility . WithTimeout ( m_timeouts . ShortTimeout , cancelToken , ct => this . m_client . DeleteAsync ( deleteUrl , ct )). ConfigureAwait ( false ))
2024-12-19 22:40:18 +01:00
this . CheckResponse ( response );
2018-05-25 09:18:20 -06:00
}
2018-06-02 15:52:20 -06:00
catch ( DriveItemNotFoundException ex )
2018-05-25 09:18:20 -06:00
{
2018-06-02 15:52:20 -06:00
// Wrap the existing item not found error in a 'FolderMissingException'
throw new FileMissingException ( ex );
2018-05-25 09:18:20 -06:00
}
}
2024-10-01 09:30:02 +02:00
public async Task TestAsync ( CancellationToken cancelToken )
2018-05-25 09:18:20 -06:00
{
try
{
2025-03-21 15:58:56 +01:00
var drivePrefix = await GetDrivePrefix ( cancelToken ). ConfigureAwait ( false );
string rootPath = string . Format ( "{0}/root:{1}" , drivePrefix , this . RootPath );
2024-10-01 09:30:02 +02:00
await this . GetAsync < DriveItem >( rootPath , cancelToken ). ConfigureAwait ( false );
2018-05-25 09:18:20 -06:00
}
catch ( DriveItemNotFoundException ex )
{
// Wrap the existing item not found error in a 'FolderMissingException'
throw new FolderMissingException ( ex );
}
2025-05-21 17:19:27 +02:00
await this . TestReadWritePermissionsAsync ( cancelToken ). ConfigureAwait ( false );
2018-05-25 09:18:20 -06:00
}
public void Dispose ()
{
if ( this . m_client != null )
{
this . m_client . Dispose ();
}
}
2024-10-01 09:30:02 +02:00
protected virtual Task < string > GetRootPathFromUrlAsync ( string url , CancellationToken cancelToken )
2018-05-25 09:18:20 -06:00
{
// Extract out the path to the backup root folder from the given URI
var uri = new Utility . Uri ( url );
2024-10-01 09:30:02 +02:00
return Task . FromResult ( Utility . Uri . UrlDecode ( uri . HostAndPath ));
2018-05-25 09:18:20 -06:00
}
2024-10-01 09:30:02 +02:00
protected Task < T > GetAsync < T >( string url , CancellationToken cancelToken )
2018-05-25 09:18:20 -06:00
{
2024-10-01 09:30:02 +02:00
return this . SendRequestAsync < T >( HttpMethod . Get , url , cancelToken );
2018-05-25 09:18:20 -06:00
}
2024-10-01 09:30:02 +02:00
protected Task < T > PostAsync < T >( string url , T body , CancellationToken cancelToken ) where T : class
2018-05-25 09:18:20 -06:00
{
2024-10-01 09:30:02 +02:00
return this . SendRequestAsync ( HttpMethod . Post , url , body , cancelToken );
2018-05-25 09:18:20 -06:00
}
2024-10-01 09:30:02 +02:00
protected Task < T > PatchAsync < T >( string url , T body , CancellationToken cancelToken ) where T : class
2018-05-25 09:18:20 -06:00
{
2024-10-01 09:30:02 +02:00
return this . SendRequestAsync ( PatchMethod , url , body , cancelToken );
2018-05-25 09:18:20 -06:00
}
2024-10-01 09:30:02 +02:00
private async Task < T > SendRequestAsync < T >( HttpMethod method , string url , CancellationToken cancelToken )
2018-05-25 09:18:20 -06:00
{
2024-12-19 22:40:18 +01:00
using ( var request = new HttpRequestMessage ( method , url ))
2019-11-13 12:18:19 -07:00
{
2024-12-19 22:40:18 +01:00
return await this . SendRequestAsync < T >( request , cancelToken ). ConfigureAwait ( false );
2019-11-13 12:18:19 -07:00
}
2018-05-25 09:18:20 -06:00
}
2024-10-01 09:30:02 +02:00
private async Task < T > SendRequestAsync < T >( HttpMethod method , string url , T body , CancellationToken cancelToken ) where T : class
2018-05-25 09:18:20 -06:00
{
2024-12-19 22:40:18 +01:00
using ( var request = new HttpRequestMessage ( method , url ))
using ( request . Content = this . PrepareContent ( body ))
2020-03-23 14:10:40 -06:00
{
2024-12-19 22:40:18 +01:00
return await this . SendRequestAsync < T >( request , cancelToken ). ConfigureAwait ( false );
2018-05-25 09:18:20 -06:00
}
}
2024-10-01 09:30:02 +02:00
private async Task < T > SendRequestAsync < T >( HttpRequestMessage request , CancellationToken cancelToken )
2018-05-25 09:18:20 -06:00
{
2024-10-01 09:30:02 +02:00
await m_retryAfter . WaitForRetryAfterAsync ( cancelToken ). ConfigureAwait ( false );
using ( var response = await this . m_client . SendAsync ( request , cancelToken ). ConfigureAwait ( false ))
2019-11-13 12:18:19 -07:00
{
2024-10-01 09:30:02 +02:00
return await this . ParseResponseAsync < T >( response , cancelToken ). ConfigureAwait ( false );
2019-11-13 12:18:19 -07:00
}
2018-05-25 09:18:20 -06:00
}
2024-04-15 13:17:49 +02:00
2025-02-17 16:45:51 +01:00
private async IAsyncEnumerable < T > Enumerate < T >( string url , [ EnumeratorCancellation ] CancellationToken cancelToken )
2018-05-25 09:18:20 -06:00
{
2025-03-21 15:58:56 +01:00
var nextUrl = url ;
2018-05-25 09:18:20 -06:00
while (! string . IsNullOrEmpty ( nextUrl ))
{
2019-11-13 12:18:19 -07:00
GraphCollection < T > results ;
try
{
2025-03-21 15:58:56 +01:00
results = await Utility . Utility . WithTimeout ( m_timeouts . ListTimeout , cancelToken , ct => this . GetAsync < GraphCollection < T >>( nextUrl , ct )). ConfigureAwait ( false );
2019-11-13 12:18:19 -07:00
}
catch ( DriveItemNotFoundException ex )
{
// If there's an 'item not found' exception here, it means the root folder didn't exist.
throw new FolderMissingException ( ex );
}
2025-03-21 15:58:56 +01:00
foreach ( T result in results . Value ?? [])
2018-05-25 09:18:20 -06:00
yield return result ;
nextUrl = results . ODataNextLink ;
}
}
private void CheckResponse ( HttpResponseMessage response )
{
2021-03-23 17:28:39 -06:00
m_retryAfter . SetRetryAfter ( response . Headers . RetryAfter );
2021-03-16 11:40:38 -06:00
2018-05-25 09:18:20 -06:00
if (! response . IsSuccessStatusCode )
{
if ( response . StatusCode == HttpStatusCode . NotFound )
{
// It looks like this is an 'item not found' exception, so wrap it in a new exception class to make it easier to pick things out.
throw new DriveItemNotFoundException ( response );
}
else
{
// Throw a wrapper exception to make it easier for the caller to look at specific status codes, etc.
throw new MicrosoftGraphException ( response );
}
}
}
2024-10-01 09:30:02 +02:00
private async Task < T > ParseResponseAsync < T >( HttpResponseMessage response , CancellationToken cancelToken )
2018-05-25 09:18:20 -06:00
{
2025-03-21 15:58:56 +01:00
CheckResponse ( response );
using ( var responseStream = await Utility . Utility . WithTimeout ( m_timeouts . ShortTimeout , cancelToken , ct => response . Content . ReadAsStreamAsync ( ct )). ConfigureAwait ( false ))
using ( var timeoutStream = responseStream . ObserveReadTimeout ( m_timeouts . ReadWriteTimeout ))
using ( var reader = new StreamReader ( timeoutStream ))
2025-09-19 15:34:40 +02:00
{
var content = await reader . ReadToEndAsync (). ConfigureAwait ( false );
if (! string . IsNullOrWhiteSpace ( content ))
{
try
{
using ( var stringReader = new StringReader ( content ))
using ( var jsonReader = new JsonTextReader ( stringReader ))
{
var data = m_serializer . Deserialize < T >( jsonReader );
if ( data != null )
return data ;
}
}
catch ( JsonException ex )
{
Log . WriteErrorMessage (
LOGTAG ,
"MicrosoftGraphJsonParseError" ,
ex ,
"Error parsing JSON response: {0}" ,
content );
}
}
throw new MicrosoftGraphException ( "Failed to parse response" , response , content );
}
2020-03-23 14:10:40 -06:00
}
2021-03-15 18:04:39 -06:00
private async Task ThrowUploadSessionException (
UploadSession uploadSession ,
HttpResponseMessage createSessionResponse ,
int fragment ,
int fragmentCount ,
Exception ex ,
CancellationToken cancelToken )
{
// Before throwing the exception, cancel the upload session
// The uploaded delete request will error if it is authenticated
2024-10-01 09:30:02 +02:00
using ( var request = new HttpRequestMessage ( HttpMethod . Delete , uploadSession . UploadUrl ))
using ( var response = await this . m_client . SendAsync ( request , false , cancelToken ). ConfigureAwait ( false ))
2021-03-15 18:04:39 -06:00
{
2025-10-26 16:14:34 +01:00
try
{
// Note that the response body should always be empty in this case.
if ( response . StatusCode != HttpStatusCode . NoContent )
await this . ParseResponseAsync < UploadSession >( response , cancelToken ). ConfigureAwait ( false );
}
catch ( Exception parseEx )
{
Log . WriteErrorMessage (
LOGTAG ,
"MicrosoftGraphUploadSessionCancelError" ,
parseEx ,
"Error canceling upload session after fragment upload failure" );
}
2021-03-15 18:04:39 -06:00
}
throw new UploadSessionException ( createSessionResponse , fragment , fragmentCount , ex );
}
2018-05-25 09:18:20 -06:00
/// <summary>
/// Normalizes the slashes in a url fragment. For example:
/// "" => ""
/// "test" => "/test"
/// "test/" => "/test"
/// "a\b" => "/a/b"
/// </summary>
/// <param name="url">Url fragment to normalize</param>
/// <returns>Normalized fragment</returns>
private static string NormalizeSlashes ( string url )
{
url = url . Replace ( '\\' , '/' );
if ( url . Length != 0 && ! url . StartsWith ( "/" , StringComparison . Ordinal ))
url = "/" + url ;
if ( url . EndsWith ( "/" , StringComparison . Ordinal ))
url = url . Substring ( 0 , url . Length - 1 );
return url ;
}
2025-03-21 15:58:56 +01:00
private StringContent ? PrepareContent < T >( T body )
2018-05-25 09:18:20 -06:00
{
2025-03-21 15:58:56 +01:00
if ( body == null )
2019-11-13 12:18:19 -07:00
return null ;
2025-03-21 15:58:56 +01:00
return new StringContent ( JsonConvert . SerializeObject ( body ), Encoding . UTF8 , "application/json" );
2018-05-25 09:18:20 -06:00
}
}
}