/////////////////////////////////////////////////////////////////////////// // Copyright (C) Wizardry and Steamworks 2016 - License: GNU GPLv3 // // Please see: http://www.gnu.org/licenses/gpl.html for legal details, // // rights of fair usage, the disclaimer and warranty conditions. // /////////////////////////////////////////////////////////////////////////// // Originally based on: WebDAV .NET client by Sergey Kazantsev using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Text; using System.Threading.Tasks; using wasDAVClient.Helpers; using wasDAVClient.Model; namespace wasDAVClient { public class Client : IClient, IDisposable { private const int HttpStatusCode_MultiStatus = 207; // http://webdav.org/specs/rfc4918.html#METHOD_PROPFIND private const string PropFindRequestContent = "" + "" + "" + //" " + //" " + //" " + //" " + //" " + //" " + //" " + //" " + //" " + //" " + ""; private static readonly HttpMethod PropFind = new HttpMethod("PROPFIND"); private static readonly HttpMethod MoveMethod = new HttpMethod("MOVE"); private static readonly HttpMethod MkCol = new HttpMethod(WebRequestMethods.Http.MkCol); private static readonly string AssemblyVersion = typeof(IClient).Assembly.GetName().Version.ToString(); private readonly HttpClient _client; private readonly HttpClient _uploadClient; private string _basePath = "/"; private string _encodedBasePath; private string _server; public Client(ICredentials credential = null) { var handler = new HttpClientHandler(); if (handler.SupportsProxy) handler.Proxy = WebRequest.DefaultWebProxy; if (handler.SupportsAutomaticDecompression) handler.AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip; if (credential != null) { handler.Credentials = credential; handler.PreAuthenticate = true; } _client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(Timeout) }; _client.DefaultRequestHeaders.ExpectContinue = false; _uploadClient = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(Timeout) }; _uploadClient.DefaultRequestHeaders.ExpectContinue = false; } #region WebDAV connection parameters /// /// Specify the WebDAV hostname (required). /// public string Server { get { return _server; } set { _server = value.TrimEnd('/'); } } /// /// Specify the path of a WebDAV directory to use as 'root' (default: /) /// public string BasePath { get { return _basePath; } set { value = value.Trim('/'); if (string.IsNullOrEmpty(value)) _basePath = "/"; else _basePath = "/" + value + "/"; } } /// /// Specify an port (default: null = auto-detect) /// public int? Port { get; set; } /// /// Specify the UserAgent (and UserAgent version) string to use in requests /// public string UserAgent { get; set; } /// /// Specify the UserAgent (and UserAgent version) string to use in requests /// public string UserAgentVersion { get; set; } /// /// The HTTP request timeout in seconds. /// public int Timeout { get; set; } = 60; #endregion WebDAV connection parameters #region WebDAV operations /// /// List all files present on the server. /// /// List only files in this path /// Recursion depth /// A list of files (entries without a trailing slash) and directories (entries with a trailing slash) public async Task> List(string path = "/", string depth = Constants.DavDepth.MEMBERS) { var listUri = await GetServerUrl(path, true).ConfigureAwait(false); // Depth header: http://webdav.org/specs/rfc4918.html#rfc.section.9.1.4 IDictionary headers = new Dictionary(); headers.Add("Depth", depth); HttpResponseMessage response = null; try { response = await HttpRequest(listUri.Uri, PropFind, headers, Encoding.UTF8.GetBytes(PropFindRequestContent)) .ConfigureAwait(false); if (response.StatusCode != HttpStatusCode.OK && (int)response.StatusCode != HttpStatusCode_MultiStatus) { throw new wasDAVException((int)response.StatusCode, "Failed retrieving items in folder."); } using (var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false)) { var items = ResponseParser.ParseItems(stream); if (items == null) { throw new wasDAVException("Failed deserializing data returned from server."); } var listUrl = listUri.ToString(); return items.AsParallel().Select(async item => { switch (!item.IsCollection) { case true: return item; default: // If it's not the requested parent folder, add it to the result if (!string.Equals((await GetServerUrl(item.Href, true).ConfigureAwait(false)).ToString(), listUrl, StringComparison.CurrentCultureIgnoreCase)) { return item; } break; } return null; }).Select(o => o.Result).OfType(); } } finally { response?.Dispose(); } } /// /// List all files present on the server. /// /// A list of files (entries without a trailing slash) and directories (entries with a trailing slash) public async Task GetFolder(string path = "/") { return await Get((await GetServerUrl(path, true).ConfigureAwait(false)).Uri).ConfigureAwait(false); } /// /// List all files present on the server. /// /// A list of files (entries without a trailing slash) and directories (entries with a trailing slash) public async Task GetFile(string path = "/") { return await Get((await GetServerUrl(path, false).ConfigureAwait(false)).Uri).ConfigureAwait(false); } /// /// List all files present on the server. /// /// A list of files (entries without a trailing slash) and directories (entries with a trailing slash) private async Task Get(Uri listUri) { // Depth header: http://webdav.org/specs/rfc4918.html#rfc.section.9.1.4 IDictionary headers = new Dictionary(); headers.Add("Depth", "0"); HttpResponseMessage response = null; try { response = await HttpRequest(listUri, PropFind, headers, Encoding.UTF8.GetBytes(PropFindRequestContent)) .ConfigureAwait(false); if (response.StatusCode != HttpStatusCode.OK && (int)response.StatusCode != HttpStatusCode_MultiStatus) { throw new wasDAVException((int)response.StatusCode, $"Failed retrieving item/folder (Status Code: {response.StatusCode})"); } using (var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false)) { var result = ResponseParser.ParseItem(stream); if (result == null) { throw new wasDAVException("Failed deserializing data returned from server."); } return result; } } finally { response?.Dispose(); } } /// /// Download a file from the server /// /// Source path and filename of the file on the server public async Task Download(string remoteFilePath) { // Should not have a trailing slash. var downloadUri = await GetServerUrl(remoteFilePath, false).ConfigureAwait(false); var dictionary = new Dictionary { { "translate", "f" } }; var response = await HttpRequest(downloadUri.Uri, HttpMethod.Get, dictionary).ConfigureAwait(false); if (response.StatusCode != HttpStatusCode.OK) { throw new wasDAVException((int)response.StatusCode, "Failed retrieving file."); } return await response.Content.ReadAsStreamAsync().ConfigureAwait(false); } /// /// Download a file from the server /// /// Source path and filename of the file on the server /// /// public async Task Upload(string remoteFilePath, Stream content, string name) { // Should not have a trailing slash. var uploadUri = await GetServerUrl(remoteFilePath.TrimEnd('/') + "/" + name.TrimStart('/'), false).ConfigureAwait(false); HttpResponseMessage response = null; try { response = await HttpUploadRequest(uploadUri.Uri, HttpMethod.Put, content).ConfigureAwait(false); if (response.StatusCode != HttpStatusCode.OK && response.StatusCode != HttpStatusCode.NoContent && response.StatusCode != HttpStatusCode.Created) { throw new wasDAVException((int)response.StatusCode, "Failed uploading file."); } return response.IsSuccessStatusCode; } finally { response?.Dispose(); } } /// /// Create a directory on the server /// /// Destination path of the directory on the server. /// The name of the folder to create. public async Task CreateDir(string remotePath, string name) { // Should not have a trailing slash. var dirUri = await GetServerUrl(remotePath.TrimEnd('/') + "/" + name.TrimStart('/'), false).ConfigureAwait(false); HttpResponseMessage response = null; try { response = await HttpRequest(dirUri.Uri, MkCol).ConfigureAwait(false); if (response.StatusCode == HttpStatusCode.Conflict) throw new wasDAVConflictException((int)response.StatusCode, "Failed creating folder."); if (response.StatusCode != HttpStatusCode.OK && response.StatusCode != HttpStatusCode.NoContent && response.StatusCode != HttpStatusCode.Created) { throw new wasDAVException((int)response.StatusCode, "Failed creating folder."); } return response.IsSuccessStatusCode; } finally { response?.Dispose(); } } public async Task DeleteFolder(string href) { await Delete((await GetServerUrl(href, true).ConfigureAwait(false)).Uri).ConfigureAwait(false); } public async Task DeleteFile(string href) { await Delete((await GetServerUrl(href, false).ConfigureAwait(false)).Uri).ConfigureAwait(false); } private async Task Delete(Uri listUri) { var response = await HttpRequest(listUri, HttpMethod.Delete).ConfigureAwait(false); if (response.StatusCode != HttpStatusCode.OK && response.StatusCode != HttpStatusCode.NoContent) { throw new wasDAVException((int)response.StatusCode, "Failed deleting item."); } } public async Task MoveFolder(string srcFolderPath, string dstFolderPath) { // Should have a trailing slash. return await Move((await GetServerUrl(srcFolderPath, true).ConfigureAwait(false)).Uri, (await GetServerUrl(dstFolderPath, true).ConfigureAwait(false)).Uri).ConfigureAwait(false); } public async Task MoveFile(string srcFilePath, string dstFilePath) { // Should not have a trailing slash. return await Move((await GetServerUrl(srcFilePath, false).ConfigureAwait(false)).Uri, (await GetServerUrl(dstFilePath, false).ConfigureAwait(false)).Uri).ConfigureAwait(false); } private async Task Move(Uri srcUri, Uri dstUri) { const string requestContent = "MOVE"; IDictionary headers = new Dictionary { {"Destination", dstUri.ToString()} }; var response = await HttpRequest(srcUri, MoveMethod, headers, Encoding.UTF8.GetBytes(requestContent)) .ConfigureAwait(false); if (response.StatusCode != HttpStatusCode.OK && response.StatusCode != HttpStatusCode.Created) { throw new wasDAVException((int)response.StatusCode, "Failed moving file."); } return response.IsSuccessStatusCode; } #endregion WebDAV operations #region Server communication /// /// Perform the WebDAV call and fire the callback when finished. /// /// /// /// /// private async Task HttpRequest(Uri uri, HttpMethod method, IDictionary headers = null, byte[] content = null) { using (var request = new HttpRequestMessage(method, uri)) { request.Headers.Connection.Add("Keep-Alive"); request.Headers.UserAgent.Add(!string.IsNullOrWhiteSpace(UserAgent) ? new ProductInfoHeaderValue(UserAgent, UserAgentVersion) : new ProductInfoHeaderValue("WebDAVClient", AssemblyVersion)); request.Headers.Add("Accept", @"*/*"); request.Headers.Add("Accept-Encoding", "gzip,deflate"); if (headers != null) { foreach (var key in headers.Keys) { request.Headers.Add(key, headers[key]); } } // Need to send along content? if (content != null) { request.Content = new ByteArrayContent(content); request.Content.Headers.ContentType = new MediaTypeHeaderValue("text/xml"); } return await _client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false); } } /// /// Perform the WebDAV call and fire the callback when finished. /// /// /// /// /// private async Task HttpUploadRequest(Uri uri, HttpMethod method, Stream content, IDictionary headers = null) { using (var request = new HttpRequestMessage(method, uri)) { request.Headers.Connection.Add("Keep-Alive"); request.Headers.UserAgent.Add(!string.IsNullOrWhiteSpace(UserAgent) ? new ProductInfoHeaderValue(UserAgent, UserAgentVersion) : new ProductInfoHeaderValue("WebDAVClient", AssemblyVersion)); if (headers != null) { foreach (var key in headers.Keys) { request.Headers.Add(key, headers[key]); } } // Need to send along content? if (content != null) { request.Content = new StreamContent(content); } return await (_uploadClient ?? _client).SendAsync(request).ConfigureAwait(false); } } /// /// Try to create an Uri with kind UriKind.Absolute /// This particular implementation also works on Mono/Linux /// It seems that on Mono it is expected behaviour that uris /// of kind /a/b are indeed absolute uris since it referes to a file in /a/b. /// https://bugzilla.xamarin.com/show_bug.cgi?id=30854 /// /// /// /// private static bool TryCreateAbsolute(string uriString, out Uri uriResult) { return Uri.TryCreate(uriString, UriKind.Absolute, out uriResult) && uriResult.Scheme != Uri.UriSchemeFile; } private async Task GetServerUrl(string path, bool appendTrailingSlash) { // Resolve the base path on the server if (_encodedBasePath == null) { var baseUri = new UriBuilder(_server) { Path = _basePath }; var root = await Get(baseUri.Uri).ConfigureAwait(false); _encodedBasePath = root.Href; } // If we've been asked for the "root" folder if (string.IsNullOrEmpty(path)) { // If the resolved base path is an absolute URI, use it Uri absoluteBaseUri; if (TryCreateAbsolute(_encodedBasePath, out absoluteBaseUri)) { return new UriBuilder(absoluteBaseUri); } // Otherwise, use the resolved base path relatively to the server var baseUri = new UriBuilder(_server) { Path = _encodedBasePath }; return baseUri; } // If the requested path is absolute, use it Uri absoluteUri; if (TryCreateAbsolute(path, out absoluteUri)) { var baseUri = new UriBuilder(absoluteUri); return baseUri; } else { // Otherwise, create a URI relative to the server UriBuilder baseUri; if (TryCreateAbsolute(_encodedBasePath, out absoluteUri)) { baseUri = new UriBuilder(absoluteUri); baseUri.Path = baseUri.Path.TrimEnd('/') + "/" + path.TrimStart('/'); if (appendTrailingSlash && !baseUri.Path.EndsWith("/")) baseUri.Path += "/"; } else { baseUri = new UriBuilder(_server); // Ensure we don't add the base path twice var finalPath = path; if (!finalPath.StartsWith(_encodedBasePath, StringComparison.InvariantCultureIgnoreCase)) { finalPath = _encodedBasePath.TrimEnd('/') + "/" + path; } if (appendTrailingSlash) finalPath = finalPath.TrimEnd('/') + "/"; baseUri.Path = finalPath; } return baseUri; } } public void Dispose() { _client?.Dispose(); _uploadClient?.Dispose(); } #endregion Server communication } }