wasDAVClient – Blame information for rev 8

Subversion Repositories:
Rev:
Rev Author Line No. Line
5 office 1 ///////////////////////////////////////////////////////////////////////////
2 // Copyright (C) Wizardry and Steamworks 2016 - License: GNU GPLv3 //
3 // Please see: http://www.gnu.org/licenses/gpl.html for legal details, //
4 // rights of fair usage, the disclaimer and warranty conditions. //
5 ///////////////////////////////////////////////////////////////////////////
6 // Originally based on: WebDAV .NET client by Sergey Kazantsev
7  
8 using System;
1 office 9 using System.Collections.Generic;
10 using System.IO;
11 using System.Linq;
12 using System.Net;
13 using System.Net.Http;
14 using System.Net.Http.Headers;
15 using System.Text;
16 using System.Threading.Tasks;
17 using wasDAVClient.Helpers;
18 using wasDAVClient.Model;
19  
20 namespace wasDAVClient
21 {
4 office 22 public class Client : IClient, IDisposable
1 office 23 {
24 private const int HttpStatusCode_MultiStatus = 207;
25  
26 // http://webdav.org/specs/rfc4918.html#METHOD_PROPFIND
27 private const string PropFindRequestContent =
28 "<?xml version=\"1.0\" encoding=\"utf-8\" ?>" +
29 "<propfind xmlns=\"DAV:\">" +
30 "<allprop/>" +
31 //" <propname/>" +
32 //" <prop>" +
33 //" <creationdate/>" +
34 //" <getlastmodified/>" +
35 //" <displayname/>" +
36 //" <getcontentlength/>" +
37 //" <getcontenttype/>" +
38 //" <getetag/>" +
39 //" <resourcetype/>" +
40 //" </prop> " +
41 "</propfind>";
42  
3 office 43 private static readonly HttpMethod PropFind = new HttpMethod("PROPFIND");
44 private static readonly HttpMethod MoveMethod = new HttpMethod("MOVE");
45  
46 private static readonly HttpMethod MkCol = new HttpMethod(WebRequestMethods.Http.MkCol);
47  
1 office 48 private static readonly string AssemblyVersion = typeof(IClient).Assembly.GetName().Version.ToString();
49  
50 private readonly HttpClient _client;
51 private readonly HttpClient _uploadClient;
52 private string _basePath = "/";
53  
54 private string _encodedBasePath;
3 office 55 private string _server;
1 office 56  
5 office 57 public Client(ICredentials credential = null)
3 office 58 {
59 var handler = new HttpClientHandler();
1 office 60  
3 office 61 if (handler.SupportsProxy)
62 handler.Proxy = WebRequest.DefaultWebProxy;
63  
64 if (handler.SupportsAutomaticDecompression)
65 handler.AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip;
66  
67 if (credential != null)
68 {
69 handler.Credentials = credential;
70 handler.PreAuthenticate = true;
71 }
72  
8 office 73 _client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(Timeout) };
3 office 74 _client.DefaultRequestHeaders.ExpectContinue = false;
75  
8 office 76 _uploadClient = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(Timeout) };
3 office 77 _uploadClient.DefaultRequestHeaders.ExpectContinue = false;
78 }
79  
1 office 80 #region WebDAV connection parameters
81  
82 /// <summary>
3 office 83 /// Specify the WebDAV hostname (required).
1 office 84 /// </summary>
85 public string Server
86 {
3 office 87 get { return _server; }
5 office 88 set { _server = value.TrimEnd('/'); }
1 office 89 }
90  
91 /// <summary>
3 office 92 /// Specify the path of a WebDAV directory to use as 'root' (default: /)
1 office 93 /// </summary>
94 public string BasePath
95 {
3 office 96 get { return _basePath; }
1 office 97 set
98 {
99 value = value.Trim('/');
100 if (string.IsNullOrEmpty(value))
101 _basePath = "/";
102 else
103 _basePath = "/" + value + "/";
104 }
105 }
106  
107 /// <summary>
3 office 108 /// Specify an port (default: null = auto-detect)
1 office 109 /// </summary>
3 office 110 public int? Port { get; set; }
1 office 111  
112 /// <summary>
3 office 113 /// Specify the UserAgent (and UserAgent version) string to use in requests
1 office 114 /// </summary>
3 office 115 public string UserAgent { get; set; }
1 office 116  
117 /// <summary>
3 office 118 /// Specify the UserAgent (and UserAgent version) string to use in requests
1 office 119 /// </summary>
3 office 120 public string UserAgentVersion { get; set; }
1 office 121  
3 office 122 /// <summary>
123 /// The HTTP request timeout in seconds.
124 /// </summary>
125 public int Timeout { get; set; } = 60;
126  
8 office 127 #endregion WebDAV connection parameters
1 office 128  
129 #region WebDAV operations
130  
131 /// <summary>
3 office 132 /// List all files present on the server.
1 office 133 /// </summary>
134 /// <param name="path">List only files in this path</param>
135 /// <param name="depth">Recursion depth</param>
136 /// <returns>A list of files (entries without a trailing slash) and directories (entries with a trailing slash)</returns>
8 office 137 public async Task<IEnumerable<Item>> List(string path = "/", string depth = Constants.DavDepth.MEMBERS)
1 office 138 {
139 var listUri = await GetServerUrl(path, true).ConfigureAwait(false);
140  
141 // Depth header: http://webdav.org/specs/rfc4918.html#rfc.section.9.1.4
142 IDictionary<string, string> headers = new Dictionary<string, string>();
8 office 143 headers.Add("Depth", depth);
1 office 144  
145 HttpResponseMessage response = null;
146  
147 try
148 {
3 office 149 response =
150 await
151 HttpRequest(listUri.Uri, PropFind, headers, Encoding.UTF8.GetBytes(PropFindRequestContent))
152 .ConfigureAwait(false);
1 office 153  
154 if (response.StatusCode != HttpStatusCode.OK &&
8 office 155 (int)response.StatusCode != HttpStatusCode_MultiStatus)
1 office 156 {
8 office 157 throw new wasDAVException((int)response.StatusCode, "Failed retrieving items in folder.");
1 office 158 }
159  
160 using (var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false))
161 {
162 var items = ResponseParser.ParseItems(stream);
163  
164 if (items == null)
165 {
166 throw new wasDAVException("Failed deserializing data returned from server.");
167 }
168  
169 var listUrl = listUri.ToString();
170  
5 office 171 return items.AsParallel().Select(async item =>
1 office 172 {
5 office 173 switch (!item.IsCollection)
1 office 174 {
5 office 175 case true:
176 return item;
8 office 177  
5 office 178 default:
179 // If it's not the requested parent folder, add it to the result
180 if (!string.Equals((await GetServerUrl(item.Href, true).ConfigureAwait(false)).ToString(),
181 listUrl, StringComparison.CurrentCultureIgnoreCase))
182 {
183 return item;
184 }
185 break;
1 office 186 }
5 office 187  
188 return null;
189 }).Select(o => o.Result).OfType<Item>();
1 office 190 }
191 }
192 finally
193 {
5 office 194 response?.Dispose();
1 office 195 }
196 }
197  
198 /// <summary>
3 office 199 /// List all files present on the server.
1 office 200 /// </summary>
201 /// <returns>A list of files (entries without a trailing slash) and directories (entries with a trailing slash)</returns>
202 public async Task<Item> GetFolder(string path = "/")
203 {
5 office 204 return await Get((await GetServerUrl(path, true).ConfigureAwait(false)).Uri).ConfigureAwait(false);
1 office 205 }
206  
207 /// <summary>
3 office 208 /// List all files present on the server.
1 office 209 /// </summary>
210 /// <returns>A list of files (entries without a trailing slash) and directories (entries with a trailing slash)</returns>
211 public async Task<Item> GetFile(string path = "/")
212 {
5 office 213 return await Get((await GetServerUrl(path, false).ConfigureAwait(false)).Uri).ConfigureAwait(false);
1 office 214 }
215  
216 /// <summary>
3 office 217 /// List all files present on the server.
1 office 218 /// </summary>
219 /// <returns>A list of files (entries without a trailing slash) and directories (entries with a trailing slash)</returns>
5 office 220 private async Task<Item> Get(Uri listUri)
1 office 221 {
222 // Depth header: http://webdav.org/specs/rfc4918.html#rfc.section.9.1.4
223 IDictionary<string, string> headers = new Dictionary<string, string>();
224 headers.Add("Depth", "0");
225  
226 HttpResponseMessage response = null;
227  
228 try
229 {
3 office 230 response =
231 await
232 HttpRequest(listUri, PropFind, headers, Encoding.UTF8.GetBytes(PropFindRequestContent))
233 .ConfigureAwait(false);
1 office 234  
235 if (response.StatusCode != HttpStatusCode.OK &&
8 office 236 (int)response.StatusCode != HttpStatusCode_MultiStatus)
1 office 237 {
8 office 238 throw new wasDAVException((int)response.StatusCode,
5 office 239 $"Failed retrieving item/folder (Status Code: {response.StatusCode})");
1 office 240 }
241  
242 using (var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false))
243 {
244 var result = ResponseParser.ParseItem(stream);
245  
246 if (result == null)
247 {
248 throw new wasDAVException("Failed deserializing data returned from server.");
249 }
250  
251 return result;
252 }
253 }
254 finally
255 {
5 office 256 response?.Dispose();
1 office 257 }
258 }
259  
260 /// <summary>
3 office 261 /// Download a file from the server
1 office 262 /// </summary>
263 /// <param name="remoteFilePath">Source path and filename of the file on the server</param>
264 public async Task<Stream> Download(string remoteFilePath)
265 {
266 // Should not have a trailing slash.
267 var downloadUri = await GetServerUrl(remoteFilePath, false).ConfigureAwait(false);
268  
8 office 269 var dictionary = new Dictionary<string, string> { { "translate", "f" } };
1 office 270 var response = await HttpRequest(downloadUri.Uri, HttpMethod.Get, dictionary).ConfigureAwait(false);
271 if (response.StatusCode != HttpStatusCode.OK)
272 {
8 office 273 throw new wasDAVException((int)response.StatusCode, "Failed retrieving file.");
1 office 274 }
275 return await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
276 }
277  
278 /// <summary>
3 office 279 /// Download a file from the server
1 office 280 /// </summary>
281 /// <param name="remoteFilePath">Source path and filename of the file on the server</param>
282 /// <param name="content"></param>
283 /// <param name="name"></param>
284 public async Task<bool> Upload(string remoteFilePath, Stream content, string name)
285 {
286 // Should not have a trailing slash.
3 office 287 var uploadUri =
288 await GetServerUrl(remoteFilePath.TrimEnd('/') + "/" + name.TrimStart('/'), false).ConfigureAwait(false);
1 office 289  
290 HttpResponseMessage response = null;
291  
292 try
293 {
294 response = await HttpUploadRequest(uploadUri.Uri, HttpMethod.Put, content).ConfigureAwait(false);
295  
296 if (response.StatusCode != HttpStatusCode.OK &&
297 response.StatusCode != HttpStatusCode.NoContent &&
298 response.StatusCode != HttpStatusCode.Created)
299 {
8 office 300 throw new wasDAVException((int)response.StatusCode, "Failed uploading file.");
1 office 301 }
302  
303 return response.IsSuccessStatusCode;
304 }
305 finally
306 {
5 office 307 response?.Dispose();
1 office 308 }
309 }
310  
311 /// <summary>
3 office 312 /// Create a directory on the server
1 office 313 /// </summary>
5 office 314 /// <param name="remotePath">Destination path of the directory on the server.</param>
315 /// <param name="name">The name of the folder to create.</param>
1 office 316 public async Task<bool> CreateDir(string remotePath, string name)
317 {
318 // Should not have a trailing slash.
3 office 319 var dirUri =
320 await GetServerUrl(remotePath.TrimEnd('/') + "/" + name.TrimStart('/'), false).ConfigureAwait(false);
1 office 321  
322 HttpResponseMessage response = null;
323  
324 try
325 {
326 response = await HttpRequest(dirUri.Uri, MkCol).ConfigureAwait(false);
327  
328 if (response.StatusCode == HttpStatusCode.Conflict)
8 office 329 throw new wasDAVConflictException((int)response.StatusCode, "Failed creating folder.");
1 office 330  
331 if (response.StatusCode != HttpStatusCode.OK &&
332 response.StatusCode != HttpStatusCode.NoContent &&
333 response.StatusCode != HttpStatusCode.Created)
334 {
8 office 335 throw new wasDAVException((int)response.StatusCode, "Failed creating folder.");
1 office 336 }
337  
338 return response.IsSuccessStatusCode;
339 }
340 finally
341 {
5 office 342 response?.Dispose();
1 office 343 }
344 }
345  
346 public async Task DeleteFolder(string href)
347 {
5 office 348 await Delete((await GetServerUrl(href, true).ConfigureAwait(false)).Uri).ConfigureAwait(false);
1 office 349 }
350  
351 public async Task DeleteFile(string href)
352 {
5 office 353 await Delete((await GetServerUrl(href, false).ConfigureAwait(false)).Uri).ConfigureAwait(false);
1 office 354 }
355  
356 private async Task Delete(Uri listUri)
357 {
358 var response = await HttpRequest(listUri, HttpMethod.Delete).ConfigureAwait(false);
359  
360 if (response.StatusCode != HttpStatusCode.OK &&
361 response.StatusCode != HttpStatusCode.NoContent)
362 {
8 office 363 throw new wasDAVException((int)response.StatusCode, "Failed deleting item.");
1 office 364 }
365 }
366  
367 public async Task<bool> MoveFolder(string srcFolderPath, string dstFolderPath)
368 {
369 // Should have a trailing slash.
5 office 370 return
371 await
372 Move((await GetServerUrl(srcFolderPath, true).ConfigureAwait(false)).Uri,
373 (await GetServerUrl(dstFolderPath, true).ConfigureAwait(false)).Uri).ConfigureAwait(false);
1 office 374 }
375  
376 public async Task<bool> MoveFile(string srcFilePath, string dstFilePath)
377 {
378 // Should not have a trailing slash.
5 office 379 return
380 await
381 Move((await GetServerUrl(srcFilePath, false).ConfigureAwait(false)).Uri,
382 (await GetServerUrl(dstFilePath, false).ConfigureAwait(false)).Uri).ConfigureAwait(false);
1 office 383 }
384  
385 private async Task<bool> Move(Uri srcUri, Uri dstUri)
386 {
387 const string requestContent = "MOVE";
388  
5 office 389 IDictionary<string, string> headers = new Dictionary<string, string>
390 {
391 {"Destination", dstUri.ToString()}
392 };
1 office 393  
3 office 394 var response =
395 await
396 HttpRequest(srcUri, MoveMethod, headers, Encoding.UTF8.GetBytes(requestContent))
397 .ConfigureAwait(false);
1 office 398  
399 if (response.StatusCode != HttpStatusCode.OK &&
400 response.StatusCode != HttpStatusCode.Created)
401 {
8 office 402 throw new wasDAVException((int)response.StatusCode, "Failed moving file.");
1 office 403 }
404  
405 return response.IsSuccessStatusCode;
406 }
407  
8 office 408 #endregion WebDAV operations
1 office 409  
410 #region Server communication
411  
412 /// <summary>
3 office 413 /// Perform the WebDAV call and fire the callback when finished.
1 office 414 /// </summary>
415 /// <param name="uri"></param>
416 /// <param name="method"></param>
417 /// <param name="headers"></param>
418 /// <param name="content"></param>
3 office 419 private async Task<HttpResponseMessage> HttpRequest(Uri uri, HttpMethod method,
420 IDictionary<string, string> headers = null, byte[] content = null)
1 office 421 {
422 using (var request = new HttpRequestMessage(method, uri))
423 {
424 request.Headers.Connection.Add("Keep-Alive");
5 office 425 request.Headers.UserAgent.Add(!string.IsNullOrWhiteSpace(UserAgent)
426 ? new ProductInfoHeaderValue(UserAgent, UserAgentVersion)
427 : new ProductInfoHeaderValue("WebDAVClient", AssemblyVersion));
6 office 428 request.Headers.Add("Accept", @"*/*");
429 request.Headers.Add("Accept-Encoding", "gzip,deflate");
1 office 430  
431 if (headers != null)
432 {
3 office 433 foreach (var key in headers.Keys)
1 office 434 {
435 request.Headers.Add(key, headers[key]);
436 }
437 }
438  
439 // Need to send along content?
440 if (content != null)
441 {
442 request.Content = new ByteArrayContent(content);
443 request.Content.Headers.ContentType = new MediaTypeHeaderValue("text/xml");
444 }
445  
446 return await _client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
447 }
448 }
449  
450 /// <summary>
3 office 451 /// Perform the WebDAV call and fire the callback when finished.
1 office 452 /// </summary>
453 /// <param name="uri"></param>
454 /// <param name="headers"></param>
455 /// <param name="method"></param>
456 /// <param name="content"></param>
3 office 457 private async Task<HttpResponseMessage> HttpUploadRequest(Uri uri, HttpMethod method, Stream content,
458 IDictionary<string, string> headers = null)
1 office 459 {
460 using (var request = new HttpRequestMessage(method, uri))
461 {
462 request.Headers.Connection.Add("Keep-Alive");
5 office 463 request.Headers.UserAgent.Add(!string.IsNullOrWhiteSpace(UserAgent)
464 ? new ProductInfoHeaderValue(UserAgent, UserAgentVersion)
465 : new ProductInfoHeaderValue("WebDAVClient", AssemblyVersion));
1 office 466  
467 if (headers != null)
468 {
3 office 469 foreach (var key in headers.Keys)
1 office 470 {
471 request.Headers.Add(key, headers[key]);
472 }
473 }
474  
475 // Need to send along content?
476 if (content != null)
477 {
478 request.Content = new StreamContent(content);
479 }
480  
5 office 481 return await (_uploadClient ?? _client).SendAsync(request).ConfigureAwait(false);
1 office 482 }
483 }
484  
485 /// <summary>
3 office 486 /// Try to create an Uri with kind UriKind.Absolute
487 /// This particular implementation also works on Mono/Linux
488 /// It seems that on Mono it is expected behaviour that uris
489 /// of kind /a/b are indeed absolute uris since it referes to a file in /a/b.
490 /// https://bugzilla.xamarin.com/show_bug.cgi?id=30854
1 office 491 /// </summary>
492 /// <param name="uriString"></param>
493 /// <param name="uriResult"></param>
494 /// <returns></returns>
495 private static bool TryCreateAbsolute(string uriString, out Uri uriResult)
496 {
497 return Uri.TryCreate(uriString, UriKind.Absolute, out uriResult) && uriResult.Scheme != Uri.UriSchemeFile;
498 }
499  
500 private async Task<UriBuilder> GetServerUrl(string path, bool appendTrailingSlash)
501 {
502 // Resolve the base path on the server
503 if (_encodedBasePath == null)
504 {
8 office 505 var baseUri = new UriBuilder(_server) { Path = _basePath };
5 office 506 var root = await Get(baseUri.Uri).ConfigureAwait(false);
1 office 507  
508 _encodedBasePath = root.Href;
509 }
510  
511 // If we've been asked for the "root" folder
512 if (string.IsNullOrEmpty(path))
513 {
514 // If the resolved base path is an absolute URI, use it
515 Uri absoluteBaseUri;
516 if (TryCreateAbsolute(_encodedBasePath, out absoluteBaseUri))
517 {
518 return new UriBuilder(absoluteBaseUri);
519 }
520  
521 // Otherwise, use the resolved base path relatively to the server
8 office 522 var baseUri = new UriBuilder(_server) { Path = _encodedBasePath };
1 office 523 return baseUri;
524 }
525  
526 // If the requested path is absolute, use it
527 Uri absoluteUri;
528 if (TryCreateAbsolute(path, out absoluteUri))
529 {
530 var baseUri = new UriBuilder(absoluteUri);
531 return baseUri;
532 }
533 else
534 {
535 // Otherwise, create a URI relative to the server
536 UriBuilder baseUri;
537 if (TryCreateAbsolute(_encodedBasePath, out absoluteUri))
538 {
539 baseUri = new UriBuilder(absoluteUri);
540  
541 baseUri.Path = baseUri.Path.TrimEnd('/') + "/" + path.TrimStart('/');
542  
543 if (appendTrailingSlash && !baseUri.Path.EndsWith("/"))
544 baseUri.Path += "/";
545 }
546 else
547 {
548 baseUri = new UriBuilder(_server);
549  
550 // Ensure we don't add the base path twice
551 var finalPath = path;
552 if (!finalPath.StartsWith(_encodedBasePath, StringComparison.InvariantCultureIgnoreCase))
553 {
554 finalPath = _encodedBasePath.TrimEnd('/') + "/" + path;
555 }
556 if (appendTrailingSlash)
557 finalPath = finalPath.TrimEnd('/') + "/";
558  
559 baseUri.Path = finalPath;
560 }
561  
562 return baseUri;
563 }
564 }
565  
4 office 566 public void Dispose()
567 {
5 office 568 _client?.Dispose();
569 _uploadClient?.Dispose();
4 office 570 }
571  
8 office 572 #endregion Server communication
1 office 573 }
8 office 574 }