wasDAVClient – Blame information for rev 5

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  
5 office 73 _client = new HttpClient(handler) {Timeout = TimeSpan.FromSeconds(Timeout)};
3 office 74 _client.DefaultRequestHeaders.ExpectContinue = false;
75  
5 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  
1 office 127 #endregion
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>
137 public async Task<IEnumerable<Item>> List(string path = "/", int? depth = 1)
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>();
143 if (depth != null)
144 {
145 headers.Add("Depth", depth.ToString());
146 }
147  
148 HttpResponseMessage response = null;
149  
150 try
151 {
3 office 152 response =
153 await
154 HttpRequest(listUri.Uri, PropFind, headers, Encoding.UTF8.GetBytes(PropFindRequestContent))
155 .ConfigureAwait(false);
1 office 156  
157 if (response.StatusCode != HttpStatusCode.OK &&
3 office 158 (int) response.StatusCode != HttpStatusCode_MultiStatus)
1 office 159 {
3 office 160 throw new wasDAVException((int) response.StatusCode, "Failed retrieving items in folder.");
1 office 161 }
162  
163 using (var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false))
164 {
165 var items = ResponseParser.ParseItems(stream);
166  
167 if (items == null)
168 {
169 throw new wasDAVException("Failed deserializing data returned from server.");
170 }
171  
172 var listUrl = listUri.ToString();
173  
5 office 174 return items.AsParallel().Select(async item =>
1 office 175 {
5 office 176 switch (!item.IsCollection)
1 office 177 {
5 office 178 case true:
179 return item;
180 default:
181 // If it's not the requested parent folder, add it to the result
182 if (!string.Equals((await GetServerUrl(item.Href, true).ConfigureAwait(false)).ToString(),
183 listUrl, StringComparison.CurrentCultureIgnoreCase))
184 {
185 return item;
186 }
187 break;
1 office 188 }
5 office 189  
190 return null;
191 }).Select(o => o.Result).OfType<Item>();
1 office 192 }
193 }
194 finally
195 {
5 office 196 response?.Dispose();
1 office 197 }
198 }
199  
200 /// <summary>
3 office 201 /// List all files present on the server.
1 office 202 /// </summary>
203 /// <returns>A list of files (entries without a trailing slash) and directories (entries with a trailing slash)</returns>
204 public async Task<Item> GetFolder(string path = "/")
205 {
5 office 206 return await Get((await GetServerUrl(path, true).ConfigureAwait(false)).Uri).ConfigureAwait(false);
1 office 207 }
208  
209 /// <summary>
3 office 210 /// List all files present on the server.
1 office 211 /// </summary>
212 /// <returns>A list of files (entries without a trailing slash) and directories (entries with a trailing slash)</returns>
213 public async Task<Item> GetFile(string path = "/")
214 {
5 office 215 return await Get((await GetServerUrl(path, false).ConfigureAwait(false)).Uri).ConfigureAwait(false);
1 office 216 }
217  
218  
219 /// <summary>
3 office 220 /// List all files present on the server.
1 office 221 /// </summary>
222 /// <returns>A list of files (entries without a trailing slash) and directories (entries with a trailing slash)</returns>
5 office 223 private async Task<Item> Get(Uri listUri)
1 office 224 {
225 // Depth header: http://webdav.org/specs/rfc4918.html#rfc.section.9.1.4
226 IDictionary<string, string> headers = new Dictionary<string, string>();
227 headers.Add("Depth", "0");
228  
229 HttpResponseMessage response = null;
230  
231 try
232 {
3 office 233 response =
234 await
235 HttpRequest(listUri, PropFind, headers, Encoding.UTF8.GetBytes(PropFindRequestContent))
236 .ConfigureAwait(false);
1 office 237  
238 if (response.StatusCode != HttpStatusCode.OK &&
3 office 239 (int) response.StatusCode != HttpStatusCode_MultiStatus)
1 office 240 {
3 office 241 throw new wasDAVException((int) response.StatusCode,
5 office 242 $"Failed retrieving item/folder (Status Code: {response.StatusCode})");
1 office 243 }
244  
245 using (var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false))
246 {
247 var result = ResponseParser.ParseItem(stream);
248  
249 if (result == null)
250 {
251 throw new wasDAVException("Failed deserializing data returned from server.");
252 }
253  
254 return result;
255 }
256 }
257 finally
258 {
5 office 259 response?.Dispose();
1 office 260 }
261 }
262  
263 /// <summary>
3 office 264 /// Download a file from the server
1 office 265 /// </summary>
266 /// <param name="remoteFilePath">Source path and filename of the file on the server</param>
267 public async Task<Stream> Download(string remoteFilePath)
268 {
269 // Should not have a trailing slash.
270 var downloadUri = await GetServerUrl(remoteFilePath, false).ConfigureAwait(false);
271  
3 office 272 var dictionary = new Dictionary<string, string> {{"translate", "f"}};
1 office 273 var response = await HttpRequest(downloadUri.Uri, HttpMethod.Get, dictionary).ConfigureAwait(false);
274 if (response.StatusCode != HttpStatusCode.OK)
275 {
3 office 276 throw new wasDAVException((int) response.StatusCode, "Failed retrieving file.");
1 office 277 }
278 return await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
279 }
280  
281 /// <summary>
3 office 282 /// Download a file from the server
1 office 283 /// </summary>
284 /// <param name="remoteFilePath">Source path and filename of the file on the server</param>
285 /// <param name="content"></param>
286 /// <param name="name"></param>
287 public async Task<bool> Upload(string remoteFilePath, Stream content, string name)
288 {
289 // Should not have a trailing slash.
3 office 290 var uploadUri =
291 await GetServerUrl(remoteFilePath.TrimEnd('/') + "/" + name.TrimStart('/'), false).ConfigureAwait(false);
1 office 292  
293 HttpResponseMessage response = null;
294  
295 try
296 {
297 response = await HttpUploadRequest(uploadUri.Uri, HttpMethod.Put, content).ConfigureAwait(false);
298  
299 if (response.StatusCode != HttpStatusCode.OK &&
300 response.StatusCode != HttpStatusCode.NoContent &&
301 response.StatusCode != HttpStatusCode.Created)
302 {
3 office 303 throw new wasDAVException((int) response.StatusCode, "Failed uploading file.");
1 office 304 }
305  
306 return response.IsSuccessStatusCode;
307 }
308 finally
309 {
5 office 310 response?.Dispose();
1 office 311 }
312 }
313  
314  
315 /// <summary>
3 office 316 /// Create a directory on the server
1 office 317 /// </summary>
5 office 318 /// <param name="remotePath">Destination path of the directory on the server.</param>
319 /// <param name="name">The name of the folder to create.</param>
1 office 320 public async Task<bool> CreateDir(string remotePath, string name)
321 {
322 // Should not have a trailing slash.
3 office 323 var dirUri =
324 await GetServerUrl(remotePath.TrimEnd('/') + "/" + name.TrimStart('/'), false).ConfigureAwait(false);
1 office 325  
326 HttpResponseMessage response = null;
327  
328 try
329 {
330 response = await HttpRequest(dirUri.Uri, MkCol).ConfigureAwait(false);
331  
332 if (response.StatusCode == HttpStatusCode.Conflict)
3 office 333 throw new wasDAVConflictException((int) response.StatusCode, "Failed creating folder.");
1 office 334  
335 if (response.StatusCode != HttpStatusCode.OK &&
336 response.StatusCode != HttpStatusCode.NoContent &&
337 response.StatusCode != HttpStatusCode.Created)
338 {
3 office 339 throw new wasDAVException((int) response.StatusCode, "Failed creating folder.");
1 office 340 }
341  
342 return response.IsSuccessStatusCode;
343 }
344 finally
345 {
5 office 346 response?.Dispose();
1 office 347 }
348 }
349  
350 public async Task DeleteFolder(string href)
351 {
5 office 352 await Delete((await GetServerUrl(href, true).ConfigureAwait(false)).Uri).ConfigureAwait(false);
1 office 353 }
354  
355 public async Task DeleteFile(string href)
356 {
5 office 357 await Delete((await GetServerUrl(href, false).ConfigureAwait(false)).Uri).ConfigureAwait(false);
1 office 358 }
359  
360  
361 private async Task Delete(Uri listUri)
362 {
363 var response = await HttpRequest(listUri, HttpMethod.Delete).ConfigureAwait(false);
364  
365 if (response.StatusCode != HttpStatusCode.OK &&
366 response.StatusCode != HttpStatusCode.NoContent)
367 {
3 office 368 throw new wasDAVException((int) response.StatusCode, "Failed deleting item.");
1 office 369 }
370 }
371  
372 public async Task<bool> MoveFolder(string srcFolderPath, string dstFolderPath)
373 {
374 // Should have a trailing slash.
5 office 375 return
376 await
377 Move((await GetServerUrl(srcFolderPath, true).ConfigureAwait(false)).Uri,
378 (await GetServerUrl(dstFolderPath, true).ConfigureAwait(false)).Uri).ConfigureAwait(false);
1 office 379 }
380  
381 public async Task<bool> MoveFile(string srcFilePath, string dstFilePath)
382 {
383 // Should not have a trailing slash.
5 office 384 return
385 await
386 Move((await GetServerUrl(srcFilePath, false).ConfigureAwait(false)).Uri,
387 (await GetServerUrl(dstFilePath, false).ConfigureAwait(false)).Uri).ConfigureAwait(false);
1 office 388 }
389  
390  
391 private async Task<bool> Move(Uri srcUri, Uri dstUri)
392 {
393 const string requestContent = "MOVE";
394  
5 office 395 IDictionary<string, string> headers = new Dictionary<string, string>
396 {
397 {"Destination", dstUri.ToString()}
398 };
1 office 399  
3 office 400 var response =
401 await
402 HttpRequest(srcUri, MoveMethod, headers, Encoding.UTF8.GetBytes(requestContent))
403 .ConfigureAwait(false);
1 office 404  
405 if (response.StatusCode != HttpStatusCode.OK &&
406 response.StatusCode != HttpStatusCode.Created)
407 {
3 office 408 throw new wasDAVException((int) response.StatusCode, "Failed moving file.");
1 office 409 }
410  
411 return response.IsSuccessStatusCode;
412 }
413  
414 #endregion
415  
416 #region Server communication
417  
418 /// <summary>
3 office 419 /// Perform the WebDAV call and fire the callback when finished.
1 office 420 /// </summary>
421 /// <param name="uri"></param>
422 /// <param name="method"></param>
423 /// <param name="headers"></param>
424 /// <param name="content"></param>
3 office 425 private async Task<HttpResponseMessage> HttpRequest(Uri uri, HttpMethod method,
426 IDictionary<string, string> headers = null, byte[] content = null)
1 office 427 {
428 using (var request = new HttpRequestMessage(method, uri))
429 {
430 request.Headers.Connection.Add("Keep-Alive");
5 office 431 request.Headers.UserAgent.Add(!string.IsNullOrWhiteSpace(UserAgent)
432 ? new ProductInfoHeaderValue(UserAgent, UserAgentVersion)
433 : new ProductInfoHeaderValue("WebDAVClient", AssemblyVersion));
1 office 434  
435 if (headers != null)
436 {
3 office 437 foreach (var key in headers.Keys)
1 office 438 {
439 request.Headers.Add(key, headers[key]);
440 }
441 }
442  
443 // Need to send along content?
444 if (content != null)
445 {
446 request.Content = new ByteArrayContent(content);
447 request.Content.Headers.ContentType = new MediaTypeHeaderValue("text/xml");
448 }
449  
450 return await _client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
451 }
452 }
453  
454 /// <summary>
3 office 455 /// Perform the WebDAV call and fire the callback when finished.
1 office 456 /// </summary>
457 /// <param name="uri"></param>
458 /// <param name="headers"></param>
459 /// <param name="method"></param>
460 /// <param name="content"></param>
3 office 461 private async Task<HttpResponseMessage> HttpUploadRequest(Uri uri, HttpMethod method, Stream content,
462 IDictionary<string, string> headers = null)
1 office 463 {
464 using (var request = new HttpRequestMessage(method, uri))
465 {
466 request.Headers.Connection.Add("Keep-Alive");
5 office 467 request.Headers.UserAgent.Add(!string.IsNullOrWhiteSpace(UserAgent)
468 ? new ProductInfoHeaderValue(UserAgent, UserAgentVersion)
469 : new ProductInfoHeaderValue("WebDAVClient", AssemblyVersion));
1 office 470  
471 if (headers != null)
472 {
3 office 473 foreach (var key in headers.Keys)
1 office 474 {
475 request.Headers.Add(key, headers[key]);
476 }
477 }
478  
479 // Need to send along content?
480 if (content != null)
481 {
482 request.Content = new StreamContent(content);
483 }
484  
5 office 485 return await (_uploadClient ?? _client).SendAsync(request).ConfigureAwait(false);
1 office 486 }
487 }
488  
489 /// <summary>
3 office 490 /// Try to create an Uri with kind UriKind.Absolute
491 /// This particular implementation also works on Mono/Linux
492 /// It seems that on Mono it is expected behaviour that uris
493 /// of kind /a/b are indeed absolute uris since it referes to a file in /a/b.
494 /// https://bugzilla.xamarin.com/show_bug.cgi?id=30854
1 office 495 /// </summary>
496 /// <param name="uriString"></param>
497 /// <param name="uriResult"></param>
498 /// <returns></returns>
499 private static bool TryCreateAbsolute(string uriString, out Uri uriResult)
500 {
501 return Uri.TryCreate(uriString, UriKind.Absolute, out uriResult) && uriResult.Scheme != Uri.UriSchemeFile;
502 }
503  
504 private async Task<UriBuilder> GetServerUrl(string path, bool appendTrailingSlash)
505 {
506 // Resolve the base path on the server
507 if (_encodedBasePath == null)
508 {
3 office 509 var baseUri = new UriBuilder(_server) {Path = _basePath};
5 office 510 var root = await Get(baseUri.Uri).ConfigureAwait(false);
1 office 511  
512 _encodedBasePath = root.Href;
513 }
514  
515  
516 // If we've been asked for the "root" folder
517 if (string.IsNullOrEmpty(path))
518 {
519 // If the resolved base path is an absolute URI, use it
520 Uri absoluteBaseUri;
521 if (TryCreateAbsolute(_encodedBasePath, out absoluteBaseUri))
522 {
523 return new UriBuilder(absoluteBaseUri);
524 }
525  
526 // Otherwise, use the resolved base path relatively to the server
3 office 527 var baseUri = new UriBuilder(_server) {Path = _encodedBasePath};
1 office 528 return baseUri;
529 }
530  
531 // If the requested path is absolute, use it
532 Uri absoluteUri;
533 if (TryCreateAbsolute(path, out absoluteUri))
534 {
535 var baseUri = new UriBuilder(absoluteUri);
536 return baseUri;
537 }
538 else
539 {
540 // Otherwise, create a URI relative to the server
541 UriBuilder baseUri;
542 if (TryCreateAbsolute(_encodedBasePath, out absoluteUri))
543 {
544 baseUri = new UriBuilder(absoluteUri);
545  
546 baseUri.Path = baseUri.Path.TrimEnd('/') + "/" + path.TrimStart('/');
547  
548 if (appendTrailingSlash && !baseUri.Path.EndsWith("/"))
549 baseUri.Path += "/";
550 }
551 else
552 {
553 baseUri = new UriBuilder(_server);
554  
555 // Ensure we don't add the base path twice
556 var finalPath = path;
557 if (!finalPath.StartsWith(_encodedBasePath, StringComparison.InvariantCultureIgnoreCase))
558 {
559 finalPath = _encodedBasePath.TrimEnd('/') + "/" + path;
560 }
561 if (appendTrailingSlash)
562 finalPath = finalPath.TrimEnd('/') + "/";
563  
564 baseUri.Path = finalPath;
565 }
566  
567  
568 return baseUri;
569 }
570 }
571  
4 office 572 public void Dispose()
573 {
5 office 574 _client?.Dispose();
575 _uploadClient?.Dispose();
4 office 576 }
577  
1 office 578 #endregion
579 }
3 office 580 }