wasDAVClient – Blame information for rev 1

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