wasDAVClient – Blame information for rev 4

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