wasDAVClient – Rev 5
?pathlinks?
///////////////////////////////////////////////////////////////////////////
// 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 =
"<?xml version=\"1.0\" encoding=\"utf-8\" ?>" +
"<propfind xmlns=\"DAV:\">" +
"<allprop/>" +
//" <propname/>" +
//" <prop>" +
//" <creationdate/>" +
//" <getlastmodified/>" +
//" <displayname/>" +
//" <getcontentlength/>" +
//" <getcontenttype/>" +
//" <getetag/>" +
//" <resourcetype/>" +
//" </prop> " +
"</propfind>";
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
/// <summary>
/// Specify the WebDAV hostname (required).
/// </summary>
public string Server
{
get { return _server; }
set { _server = value.TrimEnd('/'); }
}
/// <summary>
/// Specify the path of a WebDAV directory to use as 'root' (default: /)
/// </summary>
public string BasePath
{
get { return _basePath; }
set
{
value = value.Trim('/');
if (string.IsNullOrEmpty(value))
_basePath = "/";
else
_basePath = "/" + value + "/";
}
}
/// <summary>
/// Specify an port (default: null = auto-detect)
/// </summary>
public int? Port { get; set; }
/// <summary>
/// Specify the UserAgent (and UserAgent version) string to use in requests
/// </summary>
public string UserAgent { get; set; }
/// <summary>
/// Specify the UserAgent (and UserAgent version) string to use in requests
/// </summary>
public string UserAgentVersion { get; set; }
/// <summary>
/// The HTTP request timeout in seconds.
/// </summary>
public int Timeout { get; set; } = 60;
#endregion
#region WebDAV operations
/// <summary>
/// List all files present on the server.
/// </summary>
/// <param name="path">List only files in this path</param>
/// <param name="depth">Recursion depth</param>
/// <returns>A list of files (entries without a trailing slash) and directories (entries with a trailing slash)</returns>
public async Task<IEnumerable<Item>> List(string path = "/", int? depth = 1)
{
var listUri = await GetServerUrl(path, true).ConfigureAwait(false);
// Depth header: http://webdav.org/specs/rfc4918.html#rfc.section.9.1.4
IDictionary<string, string> headers = new Dictionary<string, string>();
if (depth != null)
{
headers.Add("Depth", depth.ToString());
}
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<Item>();
}
}
finally
{
response?.Dispose();
}
}
/// <summary>
/// List all files present on the server.
/// </summary>
/// <returns>A list of files (entries without a trailing slash) and directories (entries with a trailing slash)</returns>
public async Task<Item> GetFolder(string path = "/")
{
return await Get((await GetServerUrl(path, true).ConfigureAwait(false)).Uri).ConfigureAwait(false);
}
/// <summary>
/// List all files present on the server.
/// </summary>
/// <returns>A list of files (entries without a trailing slash) and directories (entries with a trailing slash)</returns>
public async Task<Item> GetFile(string path = "/")
{
return await Get((await GetServerUrl(path, false).ConfigureAwait(false)).Uri).ConfigureAwait(false);
}
/// <summary>
/// List all files present on the server.
/// </summary>
/// <returns>A list of files (entries without a trailing slash) and directories (entries with a trailing slash)</returns>
private async Task<Item> Get(Uri listUri)
{
// Depth header: http://webdav.org/specs/rfc4918.html#rfc.section.9.1.4
IDictionary<string, string> headers = new Dictionary<string, string>();
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();
}
}
/// <summary>
/// Download a file from the server
/// </summary>
/// <param name="remoteFilePath">Source path and filename of the file on the server</param>
public async Task<Stream> Download(string remoteFilePath)
{
// Should not have a trailing slash.
var downloadUri = await GetServerUrl(remoteFilePath, false).ConfigureAwait(false);
var dictionary = new Dictionary<string, string> {{"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);
}
/// <summary>
/// Download a file from the server
/// </summary>
/// <param name="remoteFilePath">Source path and filename of the file on the server</param>
/// <param name="content"></param>
/// <param name="name"></param>
public async Task<bool> 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();
}
}
/// <summary>
/// Create a directory on the server
/// </summary>
/// <param name="remotePath">Destination path of the directory on the server.</param>
/// <param name="name">The name of the folder to create.</param>
public async Task<bool> 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<bool> 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<bool> 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<bool> Move(Uri srcUri, Uri dstUri)
{
const string requestContent = "MOVE";
IDictionary<string, string> headers = new Dictionary<string, string>
{
{"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
#region Server communication
/// <summary>
/// Perform the WebDAV call and fire the callback when finished.
/// </summary>
/// <param name="uri"></param>
/// <param name="method"></param>
/// <param name="headers"></param>
/// <param name="content"></param>
private async Task<HttpResponseMessage> HttpRequest(Uri uri, HttpMethod method,
IDictionary<string, string> 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));
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);
}
}
/// <summary>
/// Perform the WebDAV call and fire the callback when finished.
/// </summary>
/// <param name="uri"></param>
/// <param name="headers"></param>
/// <param name="method"></param>
/// <param name="content"></param>
private async Task<HttpResponseMessage> HttpUploadRequest(Uri uri, HttpMethod method, Stream content,
IDictionary<string, string> 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);
}
}
/// <summary>
/// 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
/// </summary>
/// <param name="uriString"></param>
/// <param name="uriResult"></param>
/// <returns></returns>
private static bool TryCreateAbsolute(string uriString, out Uri uriResult)
{
return Uri.TryCreate(uriString, UriKind.Absolute, out uriResult) && uriResult.Scheme != Uri.UriSchemeFile;
}
private async Task<UriBuilder> 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
}
}
Generated by GNU Enscript 1.6.5.90.