#region Disclaimer / License // Copyright (C) 2016, The Duplicati Team // http://www.duplicati.com, info@duplicati.com // // This library is free software; you can redistribute it and/or // modify it under the terms of the GNU Lesser General Public // License as published by the Free Software Foundation; either // version 2.1 of the License, or (at your option) any later version. // // This library is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // Lesser General Public License for more details. // // You should have received a copy of the GNU Lesser General Public // License along with this library; if not, write to the Free Software // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA // #endregion using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using Duplicati.Library.Utility; using Duplicati.Library.Interface; using SP = Microsoft.SharePoint.Client; using Microsoft.SharePoint.Client; // Plain 'using' for extension methods namespace Duplicati.Library.Backend { /// /// Class implementing Duplicati's backend for SharePoint. /// /// /// 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): /// 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; /// public class SharePointBackend : IBackend, IStreamingBackend { #region [Variables and constants declarations] /// Auth-stripped HTTPS-URI as passed to constructor. private readonly Utility.Uri m_orgUrl; /// Server relative path to backup folder. private readonly string m_serverRelPath; /// User's credentials to create client context private System.Net.ICredentials m_userInfo; /// Flag indicating to move files to recycler on deletion. private readonly bool m_deleteToRecycler = false; /// Flag indicating to use UploadBinaryDirect. private readonly bool m_useBinaryDirectMode = false; /// URL to SharePoint web. Will be determined from m_orgUri on first use. private string m_spWebUrl; /// Current context to SharePoint web. private SP.ClientContext m_spContext; /// The chunk size for uploading files. private readonly int m_fileChunkSize = 4 << 20; // Default: 4MB /// The chunk size for uploading files. private readonly int m_useContextTimeoutMs = -1; // default: do not touch original setting #endregion #region [Public Properties] public virtual string ProtocolKey { get { return "mssp"; } } public virtual string DisplayName { get { return Strings.SharePoint.DisplayName; } } public virtual string Description { get { return Strings.SharePoint.Description; } } public virtual bool SupportsStreaming { get { return true; } } public virtual IList SupportedCommands { get { return new List(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), new CommandLineArgument("delete-to-recycler", CommandLineArgument.ArgumentType.Boolean, Strings.SharePoint.DescriptionUseRecyclerShort, Strings.SharePoint.DescriptionUseRecyclerLong), 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"), }); } } public string[] DNSName { get { return new string[] { m_orgUrl.Host, string.IsNullOrWhiteSpace(m_spWebUrl) ? null : new Utility.Uri(m_spWebUrl).Host }; } } #endregion #region [Constructors] public SharePointBackend() { } public SharePointBackend(string url, Dictionary options) { m_deleteToRecycler = Utility.Utility.ParseBoolOption(options, "delete-to-recycler"); 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 { } var u = new Utility.Uri(url); u.RequireHost(); // 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; if (!m_serverRelPath.StartsWith("/", StringComparison.Ordinal)) m_serverRelPath = "/" + m_serverRelPath; if (!m_serverRelPath.EndsWith("/", StringComparison.Ordinal)) m_serverRelPath += "/"; // remove marker for SP-Web m_serverRelPath = m_serverRelPath.Replace("//", "/"); // Authentication settings processing: // 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"); string useUsername = null; string usePassword = null; if (!useIntegratedAuthentication) { if (!string.IsNullOrEmpty(u.Username)) { useUsername = u.Username; if (!string.IsNullOrEmpty(u.Password)) usePassword = u.Password; 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"]; } } } 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. // 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); } } #endregion #region [Private helper methods] /// /// 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). /// 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; } } /// /// 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). /// private static int testUrlForWeb(string url, System.Net.ICredentials userInfo, bool rethrow, out SP.ClientContext retCtx) { int result = -1; retCtx = null; var ctx = CreateNewContext(url); try { ctx.Credentials = userInfo; result = testContextForWeb(ctx, rethrow); if (result >= 0) { retCtx = ctx; ctx = null; } } finally { if (ctx != null) { ctx.Dispose(); } } return result; } /// /// 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. /// If that won't help, we will try all possible pathes from longest /// to shortest... /// private static string findCorrectWebPath(Utility.Uri orgUrl, System.Net.ICredentials userInfo, out SP.ClientContext retCtx) { retCtx = null; string path = orgUrl.Path; int webIndicatorPos = path.IndexOf("//", StringComparison.Ordinal); // 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 int docLibrary = Array.FindIndex(pathParts, p => StringComparer.OrdinalIgnoreCase.Equals(p, "documents")); 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; } /// Return the preconfigured SP.ClientContext to use. 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); m_spContext = CreateNewContext(m_spWebUrl); m_spContext.Credentials = m_userInfo; } if (m_spContext != null && m_useContextTimeoutMs > 0) m_spContext.RequestTimeout = m_useContextTimeoutMs; } return m_spContext; } /// /// 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... /// 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; } } /// /// Helper method to inject the custom webrequest provider that sets the UserAgent /// /// The new context. /// The url to create the context for. private static SP.ClientContext CreateNewContext(string url) { var ctx = new SP.ClientContext(url); ctx.WebRequestExecutorFactory = new CustomWebRequestExecutorFactory(ctx.WebRequestExecutorFactory); return ctx; } /// /// Simple factory override that creates same executor as the implementation /// but sets the UserAgent header, to work around a problem with OD4B servers /// internal class CustomWebRequestExecutorFactory : WebRequestExecutorFactory { /// /// The default factory /// private readonly WebRequestExecutorFactory m_parent; /// /// Initializes a new instance of the /// class. /// /// The default executor. public CustomWebRequestExecutorFactory(WebRequestExecutorFactory parent) { if (parent == null) throw new ArgumentNullException(nameof(parent)); m_parent = parent; } /// /// Creates the web request executor by calling the parent and setting the UserAgent. /// /// The web request executor. /// The context to use. /// The request URL. public override WebRequestExecutor CreateWebRequestExecutor(ClientRuntimeContext context, string requestUrl) { var req = m_parent.CreateWebRequestExecutor(context, requestUrl); if (string.IsNullOrWhiteSpace(req.WebRequest.UserAgent)) req.WebRequest.UserAgent = "Duplicati OD4B v" + System.Reflection.Assembly.GetExecutingAssembly().GetName().Version; return req; } } #endregion #region [Public backend methods] public void Test() { SP.ClientContext ctx = getSpClientContext(true); testContextForWeb(ctx, true); } public IEnumerable List() { return doList(false); } private IEnumerable doList(bool useNewContext) { SP.ClientContext ctx = getSpClientContext(useNewContext); SP.Folder remoteFolder = null; bool retry = false; try { remoteFolder = ctx.Web.GetFolderByServerRelativeUrl(m_serverRelPath); 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)); } catch (ServerException) { throw; /* rethrow if Server answered */ } catch (Interface.FileMissingException) { throw; } catch (Interface.FolderMissingException) { throw; } catch { if (!useNewContext) /* retry */ retry = true; else throw; } if (retry) { // An exception was caught, and List() should be retried. foreach (IFileEntry file in doList(true)) { yield return file; } } else { 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; } } } public void Get(string remotename, string filename) { using (System.IO.FileStream fs = System.IO.File.Create(filename)) Get(remotename, fs); } public void Get(string remotename, System.IO.Stream stream) { doGet(remotename, stream, false); } private void doGet(string remotename, System.IO.Stream stream, bool useNewContext) { string fileurl = m_serverRelPath + System.Web.HttpUtility.UrlPathEncode(remotename); SP.ClientContext ctx = getSpClientContext(useNewContext); 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)); } 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; } finally { } try { 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); } finally { } } public void Put(string remotename, string filename) { using (System.IO.FileStream fs = System.IO.File.OpenRead(filename)) Put(remotename, fs); } public void Put(string remotename, System.IO.Stream stream) { doPut(remotename, stream, false); } private void doPut(string remotename, System.IO.Stream stream, bool useNewContext) { string fileurl = m_serverRelPath + System.Web.HttpUtility.UrlPathEncode(remotename); SP.ClientContext ctx = getSpClientContext(useNewContext); try { SP.Folder remoteFolder = ctx.Web.GetFolderByServerRelativeUrl(m_serverRelPath); ctx.Load(remoteFolder, f => f.Exists, f => f.ServerRelativeUrl); wrappedExecuteQueryOnConext(ctx, m_serverRelPath, true); if (!remoteFolder.Exists) throw new Interface.FolderMissingException(Strings.SharePoint.MissingElementError(m_serverRelPath, m_spWebUrl)); useNewContext = true; // disable retry if (!m_useBinaryDirectMode) uploadFileSlicePerSlice(ctx, remoteFolder, stream, fileurl); } catch (ServerException) { throw; /* rethrow if Server answered */ } catch (Interface.FileMissingException) { throw; } catch (Interface.FolderMissingException) { throw; } catch { if (!useNewContext) /* retry */ { doPut(remotename, stream, true); return; } else throw; } finally { } if (m_useBinaryDirectMode) { try { SP.File.SaveBinaryDirect(ctx, fileurl, stream, true); } finally { } } } /// /// Upload in chunks to bypass filesize limit. /// https://msdn.microsoft.com/en-us/library/office/dn904536.aspx /// private SP.File uploadFileSlicePerSlice(ClientContext ctx, Folder folder, Stream sourceFileStream, string fileName) { // 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) while (bufCnt < blockSize && (lastreadsize = sourceFileStream.Read(buf, bufCnt, blockSize - bufCnt)) > 0) bufCnt += lastreadsize; using (var contentChunk = new MemoryStream(buf, 0, bufCnt, false)) { ClientResult 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; } public void CreateFolder() { doCreateFolder(false); } private void doCreateFolder(bool useNewContext) { SP.ClientContext ctx = getSpClientContext(useNewContext); 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]; StringBuilder relativePathBuilder = new StringBuilder(); int fi = 0; for (; fi < folderNames.Length; fi++) { relativePathBuilder.Append(System.Web.HttpUtility.UrlPathEncode(folderNames[fi])).Append("/"); if (fi < pathLengthToWeb) continue; string folderRelPath = relativePathBuilder.ToString(); 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)); } catch (ServerException) { throw; /* rethrow if Server answered */ } catch (Interface.FileMissingException) { throw; } catch (Interface.FolderMissingException) { throw; } catch { if (!useNewContext) /* retry */ doCreateFolder(true); else throw; } finally { } } public void Delete(string remotename) { doDelete(remotename, false); } private void doDelete(string remotename, bool useNewContext) { SP.ClientContext ctx = getSpClientContext(useNewContext); try { 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)); if (m_deleteToRecycler) remoteFile.Recycle(); else remoteFile.DeleteObject(); 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; } finally { } } #endregion #region IDisposable Members public void Dispose() { try { if (m_spContext != null) m_spContext.Dispose(); } catch { } m_userInfo = null; } #endregion } }