5 <title>CloudFilesClient.cs</title>
6 <meta http-equiv="content-type" content="text/html; charset=UTF-8" />
7 <link href="focco.css" rel="stylesheet" media="all" type="text/css" />
8 <script src="prettify.js" type="text/javascript"></script>
10 <body onload="prettyPrint()">
12 <div id="background"></div>
13 <table cellpadding="0" cellspacing="0">
17 <h1>CloudFilesClient.cs</h1>
19 <th class="code"></th>
26 <a class="pilcrow" href="#section_1">¶</a>
28 <p><strong>CloudFilesClient</strong> provides a simple client interface to CloudFiles and Pithos</p>
30 <p>The class provides methods to upload/download files, delete files, manage containers</p>
34 <pre><code class='prettyprint'>
37 using System.Collections.Generic;
38 using System.ComponentModel.Composition;
39 using System.Diagnostics;
40 using System.Diagnostics.Contracts;
41 using System.Globalization;
45 using System.Security.Cryptography;
47 using System.Threading.Algorithms;
48 using System.Threading.Tasks;
49 using Newtonsoft.Json;
50 using Pithos.Interfaces;
51 using WebHeaderCollection = System.Net.WebHeaderCollection;
53 namespace Pithos.Network
55 [Export(typeof(ICloudClient))]
56 public class CloudFilesClient:ICloudClient
64 <a class="pilcrow" href="#section_2">¶</a>
66 <p>CloudFilesClient uses <em>_baseClient</em> internally to communicate with the server
67 RestClient provides a REST-friendly interface over the standard WebClient.</p>
71 <pre><code class='prettyprint'> private RestClient _baseClient;
79 <a class="pilcrow" href="#section_3">¶</a>
81 <p>Some operations can specify a Timeout. The default value of all timeouts is 10 seconds</p>
85 <pre><code class='prettyprint'> private readonly TimeSpan _shortTimeout = TimeSpan.FromSeconds(10);
93 <a class="pilcrow" href="#section_4">¶</a>
95 <p>Some operations can be retried before failing. The default number of retries is 5</p>
99 <pre><code class='prettyprint'> private readonly int _retries = 5;
106 <div class="pilwrap">
107 <a class="pilcrow" href="#section_5">¶</a>
109 <p>During authentication the client provides a UserName </p>
113 <pre><code class='prettyprint'> public string UserName { get; set; }
120 <div class="pilwrap">
121 <a class="pilcrow" href="#section_6">¶</a>
123 <p>and and ApiKey to the server</p>
127 <pre><code class='prettyprint'> public string ApiKey { get; set; }
134 <div class="pilwrap">
135 <a class="pilcrow" href="#section_7">¶</a>
137 <p>And receives an authentication Token. This token must be provided in ALL other operations,
138 in the X-Auth-Token header</p>
142 <pre><code class='prettyprint'> public string Token { get; set; }
149 <div class="pilwrap">
150 <a class="pilcrow" href="#section_8">¶</a>
152 <p>The client also receives a StorageUrl after authentication. All subsequent operations must
157 <pre><code class='prettyprint'> public Uri StorageUrl { get; set; }
159 public Uri Proxy { get; set; }
161 public double DownloadPercentLimit { get; set; }
162 public double UploadPercentLimit { get; set; }
164 public string AuthenticationUrl { get; set; }
167 public string VersionPath
169 get { return UsePithos ? "v1" : "v1.0"; }
172 public bool UsePithos { get; set; }
174 private bool _authenticated = false;
181 <div class="pilwrap">
182 <a class="pilcrow" href="#section_9">¶</a>
188 <pre><code class='prettyprint'> public void Authenticate(string userName,string apiKey)
190 Trace.TraceInformation("[AUTHENTICATE] Start for {0}", userName);
191 if (String.IsNullOrWhiteSpace(userName))
192 throw new ArgumentNullException("userName", "The userName property can't be empty");
193 if (String.IsNullOrWhiteSpace(apiKey))
194 throw new ArgumentNullException("apiKey", "The apiKey property can't be empty");
203 using (var authClient = new RestClient{BaseAddress=AuthenticationUrl})
206 authClient.Proxy = new WebProxy(Proxy);
208 authClient.Headers.Add("X-Auth-User", UserName);
209 authClient.Headers.Add("X-Auth-Key", ApiKey);
211 authClient.DownloadStringWithRetry(VersionPath, 3);
213 authClient.AssertStatusOK("Authentication failed");
215 var storageUrl = authClient.GetHeaderValue("X-Storage-Url");
216 if (String.IsNullOrWhiteSpace(storageUrl))
217 throw new InvalidOperationException("Failed to obtain storage url");
218 StorageUrl = new Uri(storageUrl);
220 var token = authClient.GetHeaderValue("X-Auth-Token");
221 if (String.IsNullOrWhiteSpace(token))
222 throw new InvalidOperationException("Failed to obtain token url");
226 _baseClient = new RestClient{
227 BaseAddress = StorageUrl.AbsoluteUri,
231 _baseClient.Proxy = new WebProxy(Proxy);
233 _baseClient.Headers.Add("X-Auth-Token", Token);
235 Trace.TraceInformation("[AUTHENTICATE] End for {0}", userName);
239 public IList<ContainerInfo> ListContainers()
241 using (var client = new RestClient(_baseClient))
243 var content = client.DownloadStringWithRetry("", 3);
244 client.Parameters.Clear();
245 client.Parameters.Add("format", "json");
246 client.AssertStatusOK("List Containers failed");
248 if (client.StatusCode == HttpStatusCode.NoContent)
249 return new List<ContainerInfo>();
250 var infos = JsonConvert.DeserializeObject<IList<ContainerInfo>>(content);
256 public IList<ObjectInfo> ListObjects(string container, DateTime? since = null)
258 if (String.IsNullOrWhiteSpace(container))
259 throw new ArgumentNullException("container");
260 Contract.EndContractBlock();
262 Trace.TraceInformation("[START] ListObjects");
264 using (var client = new RestClient(_baseClient))
266 client.Parameters.Clear();
267 client.Parameters.Add("format", "json");
268 client.IfModifiedSince = since;
269 var content = client.DownloadStringWithRetry(container, 3);
271 client.AssertStatusOK("ListObjects failed");
273 var infos = JsonConvert.DeserializeObject<IList<ObjectInfo>>(content);
275 Trace.TraceInformation("[END] ListObjects");
282 public IList<ObjectInfo> ListObjects(string container, string folder, DateTime? since = null)
284 if (String.IsNullOrWhiteSpace(container))
285 throw new ArgumentNullException("container");
286 if (String.IsNullOrWhiteSpace(folder))
287 throw new ArgumentNullException("folder");
288 Contract.EndContractBlock();
290 Trace.TraceInformation("[START] ListObjects");
292 using (var client = new RestClient(_baseClient))
294 client.Parameters.Clear();
295 client.Parameters.Add("format", "json");
296 client.Parameters.Add("path", folder);
297 client.IfModifiedSince = since;
298 var content = client.DownloadStringWithRetry(container, 3);
299 client.AssertStatusOK("ListObjects failed");
301 var infos = JsonConvert.DeserializeObject<IList<ObjectInfo>>(content);
303 Trace.TraceInformation("[END] ListObjects");
309 public bool ContainerExists(string container)
311 if (String.IsNullOrWhiteSpace(container))
312 throw new ArgumentNullException("container", "The container property can't be empty");
313 using (var client = new RestClient(_baseClient))
315 client.Parameters.Clear();
316 client.Head(container, 3);
318 switch (client.StatusCode)
320 case HttpStatusCode.OK:
321 case HttpStatusCode.NoContent:
323 case HttpStatusCode.NotFound:
326 throw CreateWebException("ContainerExists", client.StatusCode);
331 public bool ObjectExists(string container,string objectName)
333 if (String.IsNullOrWhiteSpace(container))
334 throw new ArgumentNullException("container", "The container property can't be empty");
335 if (String.IsNullOrWhiteSpace(objectName))
336 throw new ArgumentNullException("objectName", "The objectName property can't be empty");
337 using (var client = new RestClient(_baseClient))
339 client.Parameters.Clear();
340 client.Head(container + "/" + objectName, 3);
342 switch (client.StatusCode)
344 case HttpStatusCode.OK:
345 case HttpStatusCode.NoContent:
347 case HttpStatusCode.NotFound:
350 throw CreateWebException("ObjectExists", client.StatusCode);
356 public ObjectInfo GetObjectInfo(string container, string objectName)
358 if (String.IsNullOrWhiteSpace(container))
359 throw new ArgumentNullException("container", "The container property can't be empty");
360 if (String.IsNullOrWhiteSpace(objectName))
361 throw new ArgumentNullException("objectName", "The objectName property can't be empty");
363 using (var client = new RestClient(_baseClient))
367 client.Parameters.Clear();
369 client.Head(container + "/" + objectName, 3);
372 return ObjectInfo.Empty;
374 switch (client.StatusCode)
376 case HttpStatusCode.OK:
377 case HttpStatusCode.NoContent:
378 var keys = client.ResponseHeaders.AllKeys.AsQueryable();
379 var tags = (from key in keys
380 where key.StartsWith("X-Object-Meta-")
381 let name = key.Substring(14)
382 select new {Name = name, Value = client.ResponseHeaders[name]})
383 .ToDictionary(t => t.Name, t => t.Value);
384 var extensions = (from key in keys
385 where key.StartsWith("X-Object-") && !key.StartsWith("X-Object-Meta-")
386 let name = key.Substring(9)
387 select new {Name = name, Value = client.ResponseHeaders[name]})
388 .ToDictionary(t => t.Name, t => t.Value);
389 return new ObjectInfo
393 long.Parse(client.GetHeaderValue("Content-Length")),*/
394 Hash = client.GetHeaderValue("ETag"),
395 Content_Type = client.GetHeaderValue("Content-Type"),
397 Extensions = extensions
399 case HttpStatusCode.NotFound:
400 return ObjectInfo.Empty;
402 throw new WebException(
403 String.Format("[FAIL] GetObjectInfo for {0} failed with unexpected status code {1}",
404 objectName, client.StatusCode));
408 catch(RetryException e)
410 Trace.TraceWarning("[RETRY FAIL] GetObjectInfo for {0} failed.");
411 return ObjectInfo.Empty;
413 catch(WebException e)
416 String.Format("[FAIL] GetObjectInfo for {0} failed with unexpected status code {1}",
417 objectName, client.StatusCode), e);
424 public void CreateFolder(string container, string folder)
426 if (String.IsNullOrWhiteSpace(container))
427 throw new ArgumentNullException("container", "The container property can't be empty");
428 if (String.IsNullOrWhiteSpace(folder))
429 throw new ArgumentNullException("folder", "The folder property can't be empty");
431 var folderUrl=String.Format("{0}/{1}",container,folder);
432 using (var client = new RestClient(_baseClient))
434 client.Parameters.Clear();
435 client.Headers.Add("Content-Type", @"application/directory");
436 client.Headers.Add("Content-Length", "0");
437 client.PutWithRetry(folderUrl, 3);
439 if (client.StatusCode != HttpStatusCode.Created && client.StatusCode != HttpStatusCode.Accepted)
440 throw CreateWebException("CreateFolder", client.StatusCode);
444 public ContainerInfo GetContainerInfo(string container)
446 if (String.IsNullOrWhiteSpace(container))
447 throw new ArgumentNullException("container", "The container property can't be empty");
448 using (var client = new RestClient(_baseClient))
450 client.Head(container);
451 switch (client.StatusCode)
453 case HttpStatusCode.NoContent:
454 var containerInfo = new ContainerInfo
458 long.Parse(client.GetHeaderValue("X-Container-Object-Count")),
459 Bytes = long.Parse(client.GetHeaderValue("X-Container-Bytes-Used"))
461 return containerInfo;
462 case HttpStatusCode.NotFound:
463 return ContainerInfo.Empty;
465 throw CreateWebException("GetContainerInfo", client.StatusCode);
470 public void CreateContainer(string container)
472 if (String.IsNullOrWhiteSpace(container))
473 throw new ArgumentNullException("container", "The container property can't be empty");
474 using (var client = new RestClient(_baseClient))
476 client.PutWithRetry(container, 3);
477 var expectedCodes = new[] {HttpStatusCode.Created, HttpStatusCode.Accepted, HttpStatusCode.OK};
478 if (!expectedCodes.Contains(client.StatusCode))
479 throw CreateWebException("CreateContainer", client.StatusCode);
483 public void DeleteContainer(string container)
485 if (String.IsNullOrWhiteSpace(container))
486 throw new ArgumentNullException("container", "The container property can't be empty");
487 using (var client = new RestClient(_baseClient))
489 client.DeleteWithRetry(container, 3);
490 var expectedCodes = new[] {HttpStatusCode.NotFound, HttpStatusCode.NoContent};
491 if (!expectedCodes.Contains(client.StatusCode))
492 throw CreateWebException("DeleteContainer", client.StatusCode);
502 <div class="pilwrap">
503 <a class="pilcrow" href="#section_10">¶</a>
508 / <param name="container"></param>
509 / <param name="objectName"></param>
510 / <param name="fileName"></param>
511 / <returns></returns>
512 / <remarks>This method should have no timeout or a very long one</remarks>
513 Asynchronously download the object specified by <em>objectName</em> in a specific <em>container</em> to
518 <pre><code class='prettyprint'> public Task GetObject(string container, string objectName, string fileName)
520 if (String.IsNullOrWhiteSpace(container))
521 throw new ArgumentNullException("container", "The container property can't be empty");
522 if (String.IsNullOrWhiteSpace(objectName))
523 throw new ArgumentNullException("objectName", "The objectName property can't be empty");
524 Contract.EndContractBlock();
533 <div class="pilwrap">
534 <a class="pilcrow" href="#section_11">¶</a>
536 <p>The container and objectName are relative names. They are joined with the client's
537 BaseAddress to create the object's absolute address</p>
541 <pre><code class='prettyprint'> var url = String.Join("/", _baseClient.BaseAddress, container, objectName);
542 var uri = new Uri(url);
548 <div class="pilwrap">
549 <a class="pilcrow" href="#section_12">¶</a>
551 <p>WebClient, and by extension RestClient, are not thread-safe. Create a new RestClient
552 object to avoid concurrency errors.</p>
554 <p>Download operations take a long time therefore they have no timeout.</p>
558 <pre><code class='prettyprint'> var client = new RestClient(_baseClient) { Timeout = 0 };
565 <div class="pilwrap">
566 <a class="pilcrow" href="#section_13">¶</a>
568 <p>Download progress is reported to the Trace log</p>
572 <pre><code class='prettyprint'> Trace.TraceInformation("[GET] START {0}", objectName);
573 client.DownloadProgressChanged += (sender, args) =>
574 Trace.TraceInformation("[GET PROGRESS] {0} {1}% {2} of {3}",
575 fileName, args.ProgressPercentage,
577 args.TotalBytesToReceive);
584 <div class="pilwrap">
585 <a class="pilcrow" href="#section_14">¶</a>
587 <p>Start downloading the object asynchronously</p>
591 <pre><code class='prettyprint'> var downloadTask = client.DownloadFileTask(uri, fileName);
598 <div class="pilwrap">
599 <a class="pilcrow" href="#section_15">¶</a>
601 <p>Once the download completes</p>
605 <pre><code class='prettyprint'> return downloadTask.ContinueWith(download =>
612 <div class="pilwrap">
613 <a class="pilcrow" href="#section_16">¶</a>
615 <p>Delete the local client object</p>
619 <pre><code class='prettyprint'> client.Dispose();
625 <div class="pilwrap">
626 <a class="pilcrow" href="#section_17">¶</a>
628 <p>And report failure or completion</p>
632 <pre><code class='prettyprint'> if (download.IsFaulted)
634 Trace.TraceError("[GET] FAIL for {0} with \r{1}", objectName,
639 Trace.TraceInformation("[GET] END {0}", objectName);
643 catch (Exception exc)
645 Trace.TraceError("[GET] END {0} with {1}", objectName, exc);
658 <div class="pilwrap">
659 <a class="pilcrow" href="#section_18">¶</a>
664 / <param name="container"></param>
665 / <param name="objectName"></param>
666 / <param name="fileName"></param>
667 / <param name="hash">Optional hash value for the file. If no hash is provided, the method calculates a new hash</param>
668 / <remarks>>This method should have no timeout or a very long one</remarks></p>
672 <pre><code class='prettyprint'> public Task PutObject(string container, string objectName, string fileName, string hash = null)
674 if (String.IsNullOrWhiteSpace(container))
675 throw new ArgumentNullException("container", "The container property can't be empty");
676 if (String.IsNullOrWhiteSpace(objectName))
677 throw new ArgumentNullException("objectName", "The objectName property can't be empty");
678 if (String.IsNullOrWhiteSpace(fileName))
679 throw new ArgumentNullException("fileName", "The fileName property can't be empty");
680 if (!File.Exists(fileName))
681 throw new FileNotFoundException("The file does not exist",fileName);
686 var url = String.Join("/",_baseClient.BaseAddress,container,objectName);
687 var uri = new Uri(url);
689 var client = new RestClient(_baseClient){Timeout=0};
690 string etag = hash ?? CalculateHash(fileName);
692 client.Headers.Add("Content-Type", "application/octet-stream");
693 client.Headers.Add("ETag", etag);
696 Trace.TraceInformation("[PUT] START {0}", objectName);
697 client.UploadProgressChanged += (sender, args) =>
699 Trace.TraceInformation("[PUT PROGRESS] {0} {1}% {2} of {3}", fileName, args.ProgressPercentage, args.BytesSent, args.TotalBytesToSend);
702 return client.UploadFileTask(uri, "PUT", fileName)
703 .ContinueWith(upload=>
707 if (upload.IsFaulted)
709 Trace.TraceError("[PUT] FAIL for {0} with \r{1}",objectName,upload.Exception);
712 Trace.TraceInformation("[PUT] END {0}", objectName);
715 catch (Exception exc)
717 Trace.TraceError("[PUT] END {0} with {1}", objectName, exc);
724 private static string CalculateHash(string fileName)
727 using (var hasher = MD5.Create())
728 using(var stream=File.OpenRead(fileName))
730 var hashBuilder=new StringBuilder();
731 foreach (byte b in hasher.ComputeHash(stream))
732 hashBuilder.Append(b.ToString("x2").ToLower());
733 hash = hashBuilder.ToString();
738 public void DeleteObject(string container, string objectName)
740 if (String.IsNullOrWhiteSpace(container))
741 throw new ArgumentNullException("container", "The container property can't be empty");
742 if (String.IsNullOrWhiteSpace(objectName))
743 throw new ArgumentNullException("objectName", "The objectName property can't be empty");
744 using (var client = new RestClient(_baseClient))
747 client.DeleteWithRetry(container + "/" + objectName, 3);
749 var expectedCodes = new[] {HttpStatusCode.NotFound, HttpStatusCode.NoContent};
750 if (!expectedCodes.Contains(client.StatusCode))
751 throw CreateWebException("DeleteObject", client.StatusCode);
756 public void MoveObject(string sourceContainer, string oldObjectName, string targetContainer,string newObjectName)
758 if (String.IsNullOrWhiteSpace(sourceContainer))
759 throw new ArgumentNullException("sourceContainer", "The container property can't be empty");
760 if (String.IsNullOrWhiteSpace(oldObjectName))
761 throw new ArgumentNullException("oldObjectName", "The oldObjectName property can't be empty");
762 if (String.IsNullOrWhiteSpace(targetContainer))
763 throw new ArgumentNullException("targetContainer", "The container property can't be empty");
764 if (String.IsNullOrWhiteSpace(newObjectName))
765 throw new ArgumentNullException("newObjectName", "The newObjectName property can't be empty");
767 var targetUrl = targetContainer + "/" + newObjectName;
768 var sourceUrl = String.Format("/{0}/{1}", sourceContainer, oldObjectName);
770 using (var client = new RestClient(_baseClient))
772 client.Headers.Add("X-Copy-From", sourceUrl);
773 client.PutWithRetry(targetUrl, 3);
775 var expectedCodes = new[] {HttpStatusCode.OK, HttpStatusCode.NoContent, HttpStatusCode.Created};
776 if (expectedCodes.Contains(client.StatusCode))
778 this.DeleteObject(sourceContainer, oldObjectName);
781 throw CreateWebException("MoveObject", client.StatusCode);
786 private static WebException CreateWebException(string operation, HttpStatusCode statusCode)
788 return new WebException(String.Format("{0} failed with unexpected status code {1}", operation, statusCode));