2024-02-28 15:45:30 +01:00
// Copyright (C) 2024, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
2016-03-22 00:49:16 +01:00
//
2024-02-28 15:45:30 +01:00
// 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:
2016-03-22 00:49:16 +01:00
//
2024-02-28 15:45:30 +01:00
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
2016-03-22 00:49:16 +01:00
//
2024-02-28 15:45:30 +01:00
// 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
// DEALINGS IN THE SOFTWARE.
2016-03-22 00:49:16 +01:00
2019-02-22 21:58:40 -06:00
using Duplicati.Library.Common.IO ;
using Duplicati.Library.Interface ;
using Duplicati.Library.Utility ;
using Microsoft.SharePoint.Client ; // Plain 'using' for extension methods
2016-03-22 00:49:16 +01:00
using System ;
using System.Collections.Generic ;
2016-04-23 18:59:59 +02:00
using System.IO ;
2016-03-22 00:49:16 +01:00
using System.Linq ;
using System.Text ;
2019-06-11 19:44:25 -07:00
using System.Threading ;
using System.Threading.Tasks ;
2016-03-22 00:49:16 +01:00
using SP = Microsoft . SharePoint . Client ;
2019-06-11 19:44:25 -07:00
2016-03-22 00:49:16 +01:00
namespace Duplicati.Library.Backend
{
2016-03-22 20:05:36 +01:00
2016-03-22 00:49:16 +01:00
/// <summary>
/// Class implementing Duplicati's backend for SharePoint.
/// </summary>
/// <remarks>
2016-03-22 19:28:05 +01:00
/// Find SharePoint Server 2013 Client Components SDK (15.xxx): https://www.microsoft.com/en-us/download/details.aspx?id=35585
/// Currently used is MS-package from nuget (for SharePoint Server 2016, 16.xxx): https://www.nuget.org/packages/Microsoft.SharePointOnline.CSOM
/// Infos for further development (pursue on demand):
2016-03-22 00:49:16 +01:00
/// Using ADAL-Tokens (Azure Active directory): https://samlman.wordpress.com/2015/02/27/using-adal-access-tokens-with-o365-rest-apis-and-csom/
/// Outline:
/// - On AzureAD --> AddNewApp and configure access to O_365 (SharePoint/OneDrive4Busi)
/// --> Get App's CLIENT ID and REDIRECT URIS.
/// - Get SAML token (WebRequest).
/// - Add auth code with access token in event ctx.ExecutingWebRequest
/// --> e.WebRequestExecutor.RequestHeaders[“Authorization”] = “Bearer ” + ar.AccessToken;
/// </remarks>
public class SharePointBackend : IBackend , IStreamingBackend
{
2016-03-22 19:28:05 +01:00
#region [ Variables and constants declarations ]
2016-03-22 00:49:16 +01:00
2016-03-22 19:28:05 +01:00
/// <summary> Auth-stripped HTTPS-URI as passed to constructor. </summary>
2018-05-23 21:18:01 -07:00
private readonly Utility . Uri m_orgUrl ;
2016-03-22 19:28:05 +01:00
/// <summary> Server relative path to backup folder. </summary>
2018-05-23 21:18:01 -07:00
private readonly string m_serverRelPath ;
2016-03-22 19:28:05 +01:00
/// <summary> User's credentials to create client context </summary>
2016-03-22 00:49:16 +01:00
private System . Net . ICredentials m_userInfo ;
2016-03-22 21:10:04 +01:00
/// <summary> Flag indicating to move files to recycler on deletion. </summary>
2018-05-23 21:18:01 -07:00
private readonly bool m_deleteToRecycler = false ;
2016-05-16 17:46:29 +02:00
/// <summary> Flag indicating to use UploadBinaryDirect. </summary>
2018-05-23 21:18:01 -07:00
private readonly bool m_useBinaryDirectMode = false ;
2016-03-22 00:49:16 +01:00
2016-03-22 19:28:05 +01:00
/// <summary> URL to SharePoint web. Will be determined from m_orgUri on first use. </summary>
private string m_spWebUrl ;
/// <summary> Current context to SharePoint web. </summary>
private SP . ClientContext m_spContext ;
2016-03-22 00:49:16 +01:00
2016-05-16 17:46:29 +02:00
/// <summary> The chunk size for uploading files. </summary>
2018-05-23 21:18:01 -07:00
private readonly int m_fileChunkSize = 4 << 20 ; // Default: 4MB
2016-05-16 17:46:29 +02:00
/// <summary> The chunk size for uploading files. </summary>
2018-05-23 21:18:01 -07:00
private readonly int m_useContextTimeoutMs = - 1 ; // default: do not touch original setting
2016-05-16 17:46:29 +02:00
2016-03-22 19:28:05 +01:00
#endregion
2016-03-22 00:49:16 +01:00
2016-03-22 19:28:05 +01:00
#region [ Public Properties ]
2016-03-22 00:49:16 +01:00
2016-03-22 20:05:36 +01:00
public virtual string ProtocolKey
{
get { return "mssp" ; }
}
public virtual string DisplayName
2016-03-22 00:49:16 +01:00
{
get { return Strings . SharePoint . DisplayName ; }
}
2016-03-22 20:05:36 +01:00
public virtual string Description
{
get { return Strings . SharePoint . Description ; }
}
public virtual IList < ICommandLineArgument > SupportedCommands
2016-03-22 00:49:16 +01:00
{
get
{
return new List < ICommandLineArgument >( new ICommandLineArgument [] {
new CommandLineArgument ( "auth-password" , CommandLineArgument . ArgumentType . Password , Strings . SharePoint . DescriptionAuthPasswordShort , Strings . SharePoint . DescriptionAuthPasswordLong ),
new CommandLineArgument ( "auth-username" , CommandLineArgument . ArgumentType . String , Strings . SharePoint . DescriptionAuthUsernameShort , Strings . SharePoint . DescriptionAuthUsernameLong ),
new CommandLineArgument ( "integrated-authentication" , CommandLineArgument . ArgumentType . Boolean , Strings . SharePoint . DescriptionIntegratedAuthenticationShort , Strings . SharePoint . DescriptionIntegratedAuthenticationLong ),
2016-03-22 21:10:04 +01:00
new CommandLineArgument ( "delete-to-recycler" , CommandLineArgument . ArgumentType . Boolean , Strings . SharePoint . DescriptionUseRecyclerShort , Strings . SharePoint . DescriptionUseRecyclerLong ),
2016-05-16 17:46:29 +02:00
new CommandLineArgument ( "binary-direct-mode" , CommandLineArgument . ArgumentType . Boolean , Strings . SharePoint . DescriptionBinaryDirectModeShort , Strings . SharePoint . DescriptionBinaryDirectModeLong , "false" ),
new CommandLineArgument ( "web-timeout" , CommandLineArgument . ArgumentType . Timespan , Strings . SharePoint . DescriptionWebTimeoutShort , Strings . SharePoint . DescriptionWebTimeoutLong ),
new CommandLineArgument ( "chunk-size" , CommandLineArgument . ArgumentType . Size , Strings . SharePoint . DescriptionChunkSizeShort , Strings . SharePoint . DescriptionChunkSizeLong , "4mb" ),
2016-03-22 00:49:16 +01:00
});
}
2019-06-11 19:44:25 -07:00
}
2018-02-26 11:37:10 +01:00
public string [] DNSName
2018-02-21 22:38:01 +01:00
{
2018-02-26 11:37:10 +01:00
get { return new string [] { m_orgUrl . Host , string . IsNullOrWhiteSpace ( m_spWebUrl ) ? null : new Utility . Uri ( m_spWebUrl ). Host }; }
2019-06-11 19:44:25 -07:00
}
2016-03-22 19:28:05 +01:00
#endregion
2019-06-11 19:44:25 -07:00
2016-03-22 19:28:05 +01:00
#region [ Constructors ]
2019-06-11 19:44:25 -07:00
2016-03-22 00:49:16 +01:00
public SharePointBackend ()
{ }
public SharePointBackend ( string url , Dictionary < string , string > options )
{
2016-03-22 21:10:04 +01:00
m_deleteToRecycler = Utility . Utility . ParseBoolOption ( options , "delete-to-recycler" );
2016-05-16 17:46:29 +02:00
m_useBinaryDirectMode = Utility . Utility . ParseBoolOption ( options , "binary-direct-mode" );
try
{
string strSpan ;
if ( options . TryGetValue ( "web-timeout" , out strSpan ))
{
TimeSpan ts = Timeparser . ParseTimeSpan ( strSpan );
if ( ts . TotalMilliseconds > 30000 && ts . TotalMilliseconds < int . MaxValue )
this . m_useContextTimeoutMs = ( int ) ts . TotalMilliseconds ;
}
}
catch { }
try
{
string strChunkSize ;
if ( options . TryGetValue ( "chunk-size" , out strChunkSize ))
{
long pSize = Utility . Sizeparser . ParseSize ( strChunkSize , "MB" );
if ( pSize >= ( 1 << 14 ) && pSize <= ( 1 << 30 )) // [16kb .. 1GB]
this . m_fileChunkSize = ( int ) pSize ;
}
}
catch { }
2016-03-22 21:10:04 +01:00
2016-03-22 00:49:16 +01:00
var u = new Utility . Uri ( url );
u . RequireHost ();
2016-03-22 19:28:05 +01:00
2016-03-22 00:49:16 +01:00
// Create sanitized plain https-URI (note: still has double slashes for processing web)
m_orgUrl = new Utility . Uri ( "https" , u . Host , u . Path , null , null , null , u . Port );
// Actual path to Web will be searched for on first use. Ctor should not throw.
m_spWebUrl = null ;
m_serverRelPath = u . Path ;
2017-11-26 10:53:14 -08:00
if (! m_serverRelPath . StartsWith ( "/" , StringComparison . Ordinal ))
2016-03-22 00:49:16 +01:00
m_serverRelPath = "/" + m_serverRelPath ;
2018-10-27 12:17:07 +02:00
m_serverRelPath = Util . AppendDirSeparator ( m_serverRelPath , "/" );
2016-03-22 00:49:16 +01:00
// remove marker for SP-Web
m_serverRelPath = m_serverRelPath . Replace ( "//" , "/" );
// Authentication settings processing:
2016-03-22 19:28:05 +01:00
// Default: try integrated auth (will normally not work for Office365, but maybe with on-prem SharePoint...).
// Otherwise: Use settings from URL(precedence) or from command line options.
bool useIntegratedAuthentication = Utility . Utility . ParseBoolOption ( options , "integrated-authentication" );
2016-03-22 00:49:16 +01:00
string useUsername = null ;
string usePassword = null ;
2016-03-22 19:28:05 +01:00
2016-03-22 00:49:16 +01:00
if (! useIntegratedAuthentication )
{
2016-03-22 19:28:05 +01:00
if (! string . IsNullOrEmpty ( u . Username ))
2016-03-22 00:49:16 +01:00
{
useUsername = u . Username ;
2016-03-22 19:28:05 +01:00
if (! string . IsNullOrEmpty ( u . Password ))
2016-03-22 00:49:16 +01:00
usePassword = u . Password ;
2016-03-22 19:28:05 +01:00
else if ( options . ContainsKey ( "auth-password" ))
usePassword = options [ "auth-password" ];
}
else
{
if ( options . ContainsKey ( "auth-username" ))
{
useUsername = options [ "auth-username" ];
if ( options . ContainsKey ( "auth-password" ))
usePassword = options [ "auth-password" ];
}
}
2016-03-22 00:49:16 +01:00
}
if ( useIntegratedAuthentication || ( useUsername == null || usePassword == null ))
{
// This might or might not work for on-premises SP. Maybe support if someone complains...
m_userInfo = System . Net . CredentialCache . DefaultNetworkCredentials ;
}
else
{
System . Security . SecureString securePwd = new System . Security . SecureString ();
usePassword . ToList (). ForEach ( c => securePwd . AppendChar ( c ));
m_userInfo = new Microsoft . SharePoint . Client . SharePointOnlineCredentials ( useUsername , securePwd );
// Other options (also ADAL, see class remarks) might be supported on request.
2016-03-22 19:28:05 +01:00
// Maybe go in deep then and also look at:
// - Microsoft.SharePoint.Client.AppPrincipalCredential.CreateFromKeyGroup()
// - ctx.AuthenticationMode = SP.ClientAuthenticationMode.FormsAuthentication;
// - ctx.FormsAuthenticationLoginInfo = new SP.FormsAuthenticationLoginInfo(user, pwd);
2016-03-22 00:49:16 +01:00
}
}
2016-03-22 19:28:05 +01:00
#endregion
2016-03-22 00:49:16 +01:00
#region [ Private helper methods ]
/// <summary>
/// Tries a simple query to test the passed context.
/// Returns 0 on success, negative if completely invalid, positive if SharePoint error (wrong creds are negative).
/// </summary>
private static int testContextForWeb ( SP . ClientContext ctx , bool rethrow = true )
{
try
{
ctx . Load ( ctx . Web , w => w . Title );
ctx . ExecuteQuery (); // should fail and throw if anything wrong.
string webTitle = ctx . Web . Title ;
if ( webTitle == null )
throw new UnauthorizedAccessException ( Strings . SharePoint . WebTitleReadFailedError );
return 0 ;
}
catch ( Microsoft . SharePoint . Client . ServerException )
{
if ( rethrow ) throw ;
else return 1 ;
}
catch ( Exception )
{
if ( rethrow ) throw ;
else return - 1 ;
}
}
/// <summary>
/// Builds a client context and tries a simple query to test if there's a web.
/// Returns 0 on success, negative if completely invalid, positive if SharePoint error (likely wrong creds).
/// </summary>
private static int testUrlForWeb ( string url , System . Net . ICredentials userInfo , bool rethrow , out SP . ClientContext retCtx )
{
int result = - 1 ;
retCtx = null ;
2018-01-18 11:44:04 +01:00
var ctx = CreateNewContext ( url );
2016-03-22 00:49:16 +01:00
try
{
ctx . Credentials = userInfo ;
result = testContextForWeb ( ctx , rethrow );
if ( result >= 0 )
{
retCtx = ctx ;
ctx = null ;
}
}
finally { if ( ctx != null ) { ctx . Dispose (); } }
return result ;
}
/// <summary>
/// SharePoint has nested subwebs but sometimes different webs
/// are hooked into a sub path.
/// For finding files SharePoint is picky to use the correct
/// path to the web, so we will trial and error here.
/// The user can give us a hint by supplying an URI with a double
/// slash to separate web.
/// Otherwise it's a good guess to look for "/documents", as we expect
/// that the default document library is used in the path.
2019-11-30 11:35:43 -08:00
/// If that won't help, we will try all possible paths from longest
2016-03-22 00:49:16 +01:00
/// to shortest...
/// </summary>
private static string findCorrectWebPath ( Utility . Uri orgUrl , System . Net . ICredentials userInfo , out SP . ClientContext retCtx )
{
retCtx = null ;
string path = orgUrl . Path ;
2017-11-26 10:53:14 -08:00
int webIndicatorPos = path . IndexOf ( "//" , StringComparison . Ordinal );
2016-03-22 00:49:16 +01:00
// if a hint is supplied, we will of course use this first.
if ( webIndicatorPos >= 0 )
{
string testUrl = new Utility . Uri ( orgUrl . Scheme , orgUrl . Host , path . Substring ( 0 , webIndicatorPos ), null , null , null , orgUrl . Port ). ToString ();
if ( testUrlForWeb ( testUrl , userInfo , false , out retCtx ) >= 0 )
return testUrl ;
}
// Now go through path and see where we land a success.
string [] pathParts = path . Split ( new char [] { '/' }, StringSplitOptions . RemoveEmptyEntries );
// first we look for the doc library
2017-09-18 23:23:45 -06:00
int docLibrary = Array . FindIndex ( pathParts , p => StringComparer . OrdinalIgnoreCase . Equals ( p , "documents" ));
2016-03-22 00:49:16 +01:00
if ( docLibrary >= 0 )
{
string testUrl = new Utility . Uri ( orgUrl . Scheme , orgUrl . Host ,
string . Join ( "/" , pathParts , 0 , docLibrary ),
null , null , null , orgUrl . Port ). ToString ();
if ( testUrlForWeb ( testUrl , userInfo , false , out retCtx ) >= 0 )
return testUrl ;
}
// last but not least: try one after the other.
for ( int pi = pathParts . Length - 1 ; pi >= 0 ; pi --)
{
if ( pi == docLibrary ) continue ; // already tested
string testUrl = new Utility . Uri ( orgUrl . Scheme , orgUrl . Host ,
string . Join ( "/" , pathParts , 0 , pi ),
null , null , null , orgUrl . Port ). ToString ();
if ( testUrlForWeb ( testUrl , userInfo , false , out retCtx ) >= 0 )
return testUrl ;
}
// nothing worked :(
return null ;
}
/// <summary> Return the preconfigured SP.ClientContext to use. </summary>
private SP . ClientContext getSpClientContext ( bool forceNewContext = false )
{
if ( forceNewContext )
{
if ( m_spContext != null ) m_spContext . Dispose ();
m_spContext = null ;
}
if ( m_spContext == null )
{
if ( m_spWebUrl == null )
{
m_spWebUrl = findCorrectWebPath ( m_orgUrl , m_userInfo , out m_spContext );
if ( m_spWebUrl == null )
throw new System . Net . WebException ( Strings . SharePoint . NoSharePointWebFoundError ( m_orgUrl . ToString ()));
}
else
{
// would query: testUrlForWeb(m_spWebUrl, userInfo, true, out m_spContext);
2018-01-18 11:44:04 +01:00
m_spContext = CreateNewContext ( m_spWebUrl );
2016-03-22 00:49:16 +01:00
m_spContext . Credentials = m_userInfo ;
}
2016-05-16 17:46:29 +02:00
if ( m_spContext != null && m_useContextTimeoutMs > 0 )
m_spContext . RequestTimeout = m_useContextTimeoutMs ;
2016-03-22 00:49:16 +01:00
}
return m_spContext ;
}
/// <summary>
/// Dedicated code to wrap ExecuteQuery on file ops and check for errors.
/// We have to check for the exceptions thrown to know about file /folder existence.
/// Why the funny guys at MS provided an .Exists field stays a mystery...
/// </summary>
private void wrappedExecuteQueryOnConext ( SP . ClientContext ctx , string serverRelPathInfo , bool isFolder )
{
try { ctx . ExecuteQuery (); }
catch ( ServerException ex )
{
// funny: If a folder is not found, we still get a FileNotFoundException from Server...?!?
// Thus, we help ourselves by just passing the info if we wanted to query a folder.
if ( ex . ServerErrorTypeName == "System.IO.DirectoryNotFoundException"
|| ( ex . ServerErrorTypeName == "System.IO.FileNotFoundException" && isFolder ))
throw new Interface . FolderMissingException ( Strings . SharePoint . MissingElementError ( serverRelPathInfo , m_spWebUrl ));
if ( ex . ServerErrorTypeName == "System.IO.FileNotFoundException" )
throw new Interface . FileMissingException ( Strings . SharePoint . MissingElementError ( serverRelPathInfo , m_spWebUrl ));
else
throw ;
}
}
2018-01-18 11:44:04 +01:00
/// <summary>
/// Helper method to inject the custom webrequest provider that sets the UserAgent
/// </summary>
/// <returns>The new context.</returns>
/// <param name="url">The url to create the context for.</param>
private static SP . ClientContext CreateNewContext ( string url )
{
var ctx = new SP . ClientContext ( url );
ctx . WebRequestExecutorFactory = new CustomWebRequestExecutorFactory ( ctx . WebRequestExecutorFactory );
return ctx ;
}
/// <summary>
/// Simple factory override that creates same executor as the implementation
/// but sets the UserAgent header, to work around a problem with OD4B servers
/// </summary>
internal class CustomWebRequestExecutorFactory : WebRequestExecutorFactory
{
/// <summary>
/// The default factory
/// </summary>
private readonly WebRequestExecutorFactory m_parent ;
/// <summary>
/// Initializes a new instance of the
/// <see cref="T:Duplicati.Library.Backend.SharePointBackend.CustomWebRequestExecutorFactory"/> class.
/// </summary>
/// <param name="parent">The default executor.</param>
public CustomWebRequestExecutorFactory ( WebRequestExecutorFactory parent )
{
if ( parent == null )
2018-03-07 17:53:08 -08:00
throw new ArgumentNullException ( nameof ( parent ));
2018-01-18 11:44:04 +01:00
m_parent = parent ;
}
/// <summary>
/// Creates the web request executor by calling the parent and setting the UserAgent.
/// </summary>
/// <returns>The web request executor.</returns>
/// <param name="context">The context to use.</param>
/// <param name="requestUrl">The request URL.</param>
public override WebRequestExecutor CreateWebRequestExecutor ( ClientRuntimeContext context , string requestUrl )
{
var req = m_parent . CreateWebRequestExecutor ( context , requestUrl );
2024-03-01 14:45:31 +01:00
if ( string . IsNullOrWhiteSpace ( req . WebRequest . Headers [ "User-Agent" ]))
req . WebRequest . Headers [ "User-Agent" ] = "Duplicati OD4B v" + System . Reflection . Assembly . GetExecutingAssembly (). GetName (). Version ;
2018-01-18 11:44:04 +01:00
return req ;
}
}
2016-03-22 00:49:16 +01:00
#endregion
#region [ Public backend methods ]
public void Test ()
{
2016-03-22 19:28:05 +01:00
SP . ClientContext ctx = getSpClientContext ( true );
2016-03-22 00:49:16 +01:00
testContextForWeb ( ctx , true );
}
2017-09-25 23:19:31 -06:00
2017-09-25 23:17:45 -06:00
public IEnumerable < IFileEntry > List () { return doList ( false ); }
private IEnumerable < IFileEntry > doList ( bool useNewContext )
2016-03-22 00:49:16 +01:00
{
2016-03-22 19:28:05 +01:00
SP . ClientContext ctx = getSpClientContext ( useNewContext );
2017-09-25 23:19:31 -06:00
SP . Folder remoteFolder = null ;
bool retry = false ;
2016-03-22 00:49:16 +01:00
try
{
2017-09-25 23:19:31 -06:00
remoteFolder = ctx . Web . GetFolderByServerRelativeUrl ( m_serverRelPath );
2016-03-22 00:49:16 +01:00
ctx . Load ( remoteFolder , f => f . Exists );
ctx . Load ( remoteFolder , f => f . Files , f => f . Folders );
wrappedExecuteQueryOnConext ( ctx , m_serverRelPath , true );
if (! remoteFolder . Exists )
throw new Interface . FolderMissingException ( Strings . SharePoint . MissingElementError ( m_serverRelPath , m_spWebUrl ));
}
2016-03-22 19:28:05 +01:00
catch ( ServerException ) { throw ; /* rethrow if Server answered */ }
catch ( Interface . FileMissingException ) { throw ; }
catch ( Interface . FolderMissingException ) { throw ; }
2017-09-25 23:19:31 -06:00
catch { if (! useNewContext ) /* retry */ retry = true ; else throw ; }
2016-03-22 00:49:16 +01:00
2017-09-25 23:19:31 -06:00
if ( retry )
2017-09-25 23:17:45 -06:00
{
2017-09-25 23:19:31 -06:00
// An exception was caught, and List() should be retried.
foreach ( IFileEntry file in doList ( true ))
{
yield return file ;
}
2017-09-25 23:17:45 -06:00
}
2017-09-25 23:19:31 -06:00
else
2017-09-25 23:17:45 -06:00
{
2017-09-25 23:19:31 -06:00
foreach ( var f in remoteFolder . Folders . Where ( ff => ff . Exists ))
{
FileEntry fe = new FileEntry ( f . Name , - 1 , f . TimeLastModified , f . TimeLastModified ); // f.TimeCreated
fe . IsFolder = true ;
yield return fe ;
}
foreach ( var f in remoteFolder . Files . Where ( ff => ff . Exists ))
{
FileEntry fe = new FileEntry ( f . Name , f . Length , f . TimeLastModified , f . TimeLastModified ); // f.TimeCreated
fe . IsFolder = false ;
yield return fe ;
}
2017-09-25 23:17:45 -06:00
}
}
2016-03-22 00:49:16 +01:00
public void Get ( string remotename , string filename )
{
using ( System . IO . FileStream fs = System . IO . File . Create ( filename ))
Get ( remotename , fs );
}
2016-03-22 19:28:05 +01:00
public void Get ( string remotename , System . IO . Stream stream ) { doGet ( remotename , stream , false ); }
private void doGet ( string remotename , System . IO . Stream stream , bool useNewContext )
2016-03-22 00:49:16 +01:00
{
2016-03-22 19:28:05 +01:00
string fileurl = m_serverRelPath + System . Web . HttpUtility . UrlPathEncode ( remotename );
SP . ClientContext ctx = getSpClientContext ( useNewContext );
2016-03-22 00:49:16 +01:00
try
{
SP . File remoteFile = ctx . Web . GetFileByServerRelativeUrl ( fileurl );
ctx . Load ( remoteFile , f => f . Exists );
wrappedExecuteQueryOnConext ( ctx , fileurl , false );
if (! remoteFile . Exists )
throw new Interface . FileMissingException ( Strings . SharePoint . MissingElementError ( fileurl , m_spWebUrl ));
2016-03-22 19:28:05 +01:00
}
catch ( ServerException ) { throw ; /* rethrow if Server answered */ }
catch ( Interface . FileMissingException ) { throw ; }
catch ( Interface . FolderMissingException ) { throw ; }
catch { if (! useNewContext ) /* retry */ doGet ( remotename , stream , true ); else throw ; }
2018-12-31 13:45:41 -08:00
byte [] copybuffer = new byte [ Duplicati . Library . Utility . Utility . DEFAULT_BUFFER_SIZE ];
using ( var fileInfo = SP . File . OpenBinaryDirect ( ctx , fileurl ))
using ( var s = fileInfo . Stream )
Utility . Utility . CopyStream ( s , stream , true , copybuffer );
2016-03-22 00:49:16 +01:00
}
2021-06-12 11:30:08 -07:00
public async Task PutAsync ( string remotename , string filename , CancellationToken cancelToken )
2016-03-22 00:49:16 +01:00
{
2019-02-22 21:58:40 -06:00
using ( FileStream fs = System . IO . File . OpenRead ( filename ))
2021-06-12 11:30:08 -07:00
await PutAsync ( remotename , fs , cancelToken );
2016-03-22 00:49:16 +01:00
}
2019-03-17 18:20:14 -05:00
public Task PutAsync ( string remotename , Stream stream , CancellationToken cancelToken ) { return doPut ( remotename , stream , false , cancelToken ); }
2019-02-23 09:21:16 -06:00
private async Task doPut ( string remotename , Stream stream , bool useNewContext , CancellationToken cancelToken )
2016-03-22 00:49:16 +01:00
{
2016-03-22 19:28:05 +01:00
string fileurl = m_serverRelPath + System . Web . HttpUtility . UrlPathEncode ( remotename );
SP . ClientContext ctx = getSpClientContext ( useNewContext );
2016-03-22 00:49:16 +01:00
try
{
SP . Folder remoteFolder = ctx . Web . GetFolderByServerRelativeUrl ( m_serverRelPath );
2016-05-16 17:46:29 +02:00
ctx . Load ( remoteFolder , f => f . Exists , f => f . ServerRelativeUrl );
2016-03-22 00:49:16 +01:00
wrappedExecuteQueryOnConext ( ctx , m_serverRelPath , true );
if (! remoteFolder . Exists )
throw new Interface . FolderMissingException ( Strings . SharePoint . MissingElementError ( m_serverRelPath , m_spWebUrl ));
2016-05-16 17:46:29 +02:00
useNewContext = true ; // disable retry
if (! m_useBinaryDirectMode )
2019-03-07 18:16:39 -06:00
await uploadFileSlicePerSlice ( ctx , remoteFolder , stream , fileurl , cancelToken ). ConfigureAwait ( false );
2016-03-22 19:28:05 +01:00
}
catch ( ServerException ) { throw ; /* rethrow if Server answered */ }
catch ( Interface . FileMissingException ) { throw ; }
catch ( Interface . FolderMissingException ) { throw ; }
2019-03-07 18:16:39 -06:00
catch { if (! useNewContext ) /* retry */ { await doPut ( remotename , stream , true , cancelToken ). ConfigureAwait ( false ); } else throw ; }
2016-05-16 17:46:29 +02:00
if ( m_useBinaryDirectMode )
{
2018-12-31 13:45:41 -08:00
SP . File . SaveBinaryDirect ( ctx , fileurl , stream , true );
2016-05-16 17:46:29 +02:00
}
2016-03-22 00:49:16 +01:00
}
2016-09-15 11:39:27 +02:00
/// <summary>
/// Upload in chunks to bypass filesize limit.
/// https://msdn.microsoft.com/en-us/library/office/dn904536.aspx
/// </summary>
2019-02-23 09:21:16 -06:00
private async Task < SP . File > uploadFileSlicePerSlice ( ClientContext ctx , Folder folder , Stream sourceFileStream , string fileName , CancellationToken cancelToken )
2016-05-16 17:46:29 +02:00
{
// Each sliced upload requires a unique ID.
Guid uploadId = Guid . NewGuid ();
// Get the name of the file.
string uniqueFileName = Path . GetFileName ( fileName );
// File object.
SP . File uploadFile = null ;
// Calculate block size in bytes.
int blockSize = m_fileChunkSize ;
byte [] buf = new byte [ blockSize ];
bool first = true ;
bool needsFinalize = true ;
long fileoffset = 0 ;
int lastreadsize = - 1 ;
while ( lastreadsize != 0 )
{
int bufCnt = 0 ;
// read chunk to array (necessary because chunk uploads fail if size unknown)
2019-03-07 18:16:39 -06:00
while ( bufCnt < blockSize && ( lastreadsize = await sourceFileStream . ReadAsync ( buf , bufCnt , blockSize - bufCnt , cancelToken ). ConfigureAwait ( false )) > 0 )
2016-05-16 17:46:29 +02:00
bufCnt += lastreadsize ;
using ( var contentChunk = new MemoryStream ( buf , 0 , bufCnt , false ))
{
ClientResult < long > bytesUploaded = null ;
if ( first )
{
// Add an empty / single chunk file.
FileCreationInformation fileInfo = new FileCreationInformation ();
fileInfo . Url = uniqueFileName ;
fileInfo . Overwrite = true ;
fileInfo . ContentStream = ( bufCnt < blockSize ) ? contentChunk : new MemoryStream ( 0 );
uploadFile = folder . Files . Add ( fileInfo );
if ( bufCnt < blockSize ) needsFinalize = false ;
else bytesUploaded = uploadFile . StartUpload ( uploadId , contentChunk ); // new OverrideableStream(chunkStream));
first = false ;
}
else
{
// Get a reference to your file.
//uploadFile = ctx.Web.GetFileByServerRelativeUrl(folder.ServerRelativeUrl + System.IO.Path.AltDirectorySeparatorChar + uniqueFileName);
if ( bufCnt < blockSize ) // Last block: end sliced upload by calling FinishUpload.
{
uploadFile = uploadFile . FinishUpload ( uploadId , fileoffset , contentChunk );
needsFinalize = false ; // signal no final call necessary.
}
else // Continue sliced upload.
{
bytesUploaded = uploadFile . ContinueUpload ( uploadId , fileoffset , contentChunk );
}
}
if ( bytesUploaded == null )
ctx . Load ( uploadFile , f => f . Length );
ctx . ExecuteQuery ();
// Check consistency and update fileoffset for the next slice.
if ( bytesUploaded != null )
{
if ( bytesUploaded . Value != fileoffset + bufCnt )
throw new InvalidDataException ( string . Format ( "Reported uploaded file size ({0:N0}) does not match internal recording ({1:N0}) for '{2}'." , bytesUploaded . Value , fileoffset + bufCnt , uniqueFileName ));
fileoffset = bytesUploaded . Value ; // Update fileoffset for the next slice.
}
else fileoffset += bufCnt ;
}
}
if ( needsFinalize ) // finalize file (should only occur if filesize is exactly a multiple of chunksize)
{
// End sliced upload by calling FinishUpload.
uploadFile = uploadFile . FinishUpload ( uploadId , fileoffset , new MemoryStream ( 0 ));
ctx . Load ( uploadFile , f => f . Length );
ctx . ExecuteQuery ();
}
if ( uploadFile . Length != fileoffset )
throw new InvalidDataException ( string . Format ( "Reported final file size ({0:N0}) does not match internal recording ({1:N0}) for '{2}'." , uploadFile . Length , fileoffset , uniqueFileName ));
return uploadFile ;
}
2016-04-23 18:59:59 +02:00
2016-09-15 11:39:27 +02:00
public void CreateFolder () { doCreateFolder ( false ); }
2016-03-22 19:28:05 +01:00
private void doCreateFolder ( bool useNewContext )
2016-03-22 00:49:16 +01:00
{
2016-03-22 19:28:05 +01:00
SP . ClientContext ctx = getSpClientContext ( useNewContext );
2016-03-22 00:49:16 +01:00
try
{
int pathLengthToWeb = new Utility . Uri ( m_spWebUrl ). Path . Split ( new char [] { '/' }, StringSplitOptions . RemoveEmptyEntries ). Length ;
string [] folderNames = m_serverRelPath . Substring ( 0 , m_serverRelPath . Length - 1 ). Split ( '/' );
folderNames = Array . ConvertAll ( folderNames , fold => System . Net . WebUtility . UrlDecode ( fold ));
var spfolders = new SP . Folder [ folderNames . Length ];
2018-05-13 15:29:55 -07:00
StringBuilder relativePathBuilder = new StringBuilder ();
2016-03-22 00:49:16 +01:00
int fi = 0 ;
for (; fi < folderNames . Length ; fi ++)
{
2018-05-13 15:29:55 -07:00
relativePathBuilder . Append ( System . Web . HttpUtility . UrlPathEncode ( folderNames [ fi ])). Append ( "/" );
2019-06-11 19:44:25 -07:00
if ( fi < pathLengthToWeb ) continue ;
2018-05-13 15:29:55 -07:00
string folderRelPath = relativePathBuilder . ToString ();
2016-03-22 00:49:16 +01:00
var folder = ctx . Web . GetFolderByServerRelativeUrl ( folderRelPath );
spfolders [ fi ] = folder ;
ctx . Load ( folder , f => f . Exists );
try { wrappedExecuteQueryOnConext ( ctx , folderRelPath , true ); }
catch ( FolderMissingException )
{ break ; }
if (! folder . Exists ) break ;
}
for (; fi < folderNames . Length ; fi ++)
spfolders [ fi ] = spfolders [ fi - 1 ]. Folders . Add ( folderNames [ fi ]);
ctx . Load ( spfolders [ folderNames . Length - 1 ], f => f . Exists );
wrappedExecuteQueryOnConext ( ctx , m_serverRelPath , true );
if (! spfolders [ folderNames . Length - 1 ]. Exists )
throw new Interface . FolderMissingException ( Strings . SharePoint . MissingElementError ( m_serverRelPath , m_spWebUrl ));
}
2016-03-22 19:28:05 +01:00
catch ( ServerException ) { throw ; /* rethrow if Server answered */ }
catch ( Interface . FileMissingException ) { throw ; }
catch ( Interface . FolderMissingException ) { throw ; }
catch { if (! useNewContext ) /* retry */ doCreateFolder ( true ); else throw ; }
2016-03-22 00:49:16 +01:00
}
2016-03-22 19:28:05 +01:00
public void Delete ( string remotename ) { doDelete ( remotename , false ); }
private void doDelete ( string remotename , bool useNewContext )
2016-03-22 00:49:16 +01:00
{
2016-03-22 19:28:05 +01:00
SP . ClientContext ctx = getSpClientContext ( useNewContext );
2016-03-22 00:49:16 +01:00
try
2016-03-22 19:28:05 +01:00
{
string fileurl = m_serverRelPath + System . Web . HttpUtility . UrlPathEncode ( remotename );
SP . File remoteFile = ctx . Web . GetFileByServerRelativeUrl ( fileurl );
ctx . Load ( remoteFile );
wrappedExecuteQueryOnConext ( ctx , fileurl , false );
if (! remoteFile . Exists )
throw new Interface . FileMissingException ( Strings . SharePoint . MissingElementError ( fileurl , m_spWebUrl ));
2016-03-22 00:49:16 +01:00
2016-03-22 21:10:04 +01:00
if ( m_deleteToRecycler ) remoteFile . Recycle ();
else remoteFile . DeleteObject ();
2016-03-22 19:28:05 +01:00
ctx . ExecuteQuery ();
}
catch ( ServerException ) { throw ; /* rethrow if Server answered */ }
catch ( Interface . FileMissingException ) { throw ; }
catch ( Interface . FolderMissingException ) { throw ; }
catch { if (! useNewContext ) /* retry */ doDelete ( remotename , true ); else throw ; }
2016-03-22 00:49:16 +01:00
}
#endregion
#region IDisposable Members
public void Dispose ()
{
try
{
if ( m_spContext != null )
m_spContext . Dispose ();
}
catch { }
m_userInfo = null ;
}
#endregion
}
}