1 // **CloudFilesClient** provides a simple client interface to CloudFiles and Pithos
3 // The class provides methods to upload/download files, delete files, manage containers
7 using System.Collections.Generic;
8 using System.ComponentModel.Composition;
9 using System.Diagnostics;
10 using System.Diagnostics.Contracts;
11 using System.Globalization;
15 using System.Security.Cryptography;
17 using System.Threading.Algorithms;
18 using System.Threading.Tasks;
19 using Newtonsoft.Json;
20 using Pithos.Interfaces;
21 using WebHeaderCollection = System.Net.WebHeaderCollection;
23 namespace Pithos.Network
25 [Export(typeof(ICloudClient))]
26 public class CloudFilesClient:ICloudClient
28 //CloudFilesClient uses *_baseClient* internally to communicate with the server
29 //RestClient provides a REST-friendly interface over the standard WebClient.
30 private RestClient _baseClient;
32 //During authentication the client provides a UserName
33 public string UserName { get; set; }
35 //and and ApiKey to the server
36 public string ApiKey { get; set; }
38 //And receives an authentication Token. This token must be provided in ALL other operations,
39 //in the X-Auth-Token header
40 public string Token { get; set; }
42 //The client also receives a StorageUrl after authentication. All subsequent operations must
44 public Uri StorageUrl { get; set; }
46 protected Uri RootAddressUri { get; set; }
48 public Uri Proxy { get; set; }
50 public double DownloadPercentLimit { get; set; }
51 public double UploadPercentLimit { get; set; }
53 public string AuthenticationUrl { get; set; }
56 public string VersionPath
58 get { return UsePithos ? "v1" : "v1.0"; }
61 public bool UsePithos { get; set; }
63 private bool _authenticated = false;
66 public void Authenticate(string userName,string apiKey)
68 if (String.IsNullOrWhiteSpace(userName))
69 throw new ArgumentNullException("userName", "The userName property can't be empty");
70 if (String.IsNullOrWhiteSpace(apiKey))
71 throw new ArgumentNullException("apiKey", "The apiKey property can't be empty");
72 Contract.Ensures(_baseClient != null);
73 Contract.EndContractBlock();
75 Trace.TraceInformation("[AUTHENTICATE] Start for {0}", userName);
84 using (var authClient = new RestClient{BaseAddress=AuthenticationUrl})
87 authClient.Proxy = new WebProxy(Proxy);
89 Contract.Assume(authClient.Headers!=null);
91 authClient.Headers.Add("X-Auth-User", UserName);
92 authClient.Headers.Add("X-Auth-Key", ApiKey);
94 authClient.DownloadStringWithRetry(VersionPath, 3);
96 authClient.AssertStatusOK("Authentication failed");
98 var storageUrl = authClient.GetHeaderValue("X-Storage-Url");
99 if (String.IsNullOrWhiteSpace(storageUrl))
100 throw new InvalidOperationException("Failed to obtain storage url");
101 StorageUrl = new Uri(storageUrl);
103 //Get the root address (StorageUrl without the account)
104 var usernameIndex=storageUrl.LastIndexOf(UserName);
105 var rootUrl = storageUrl.Substring(0, usernameIndex);
106 RootAddressUri = new Uri(rootUrl);
108 var token = authClient.GetHeaderValue("X-Auth-Token");
109 if (String.IsNullOrWhiteSpace(token))
110 throw new InvalidOperationException("Failed to obtain token url");
114 _baseClient = new RestClient{
115 BaseAddress = StorageUrl.AbsoluteUri,
119 _baseClient.Proxy = new WebProxy(Proxy);
123 Contract.Assume(_baseClient.Headers!=null);
124 _baseClient.Headers.Add("X-Auth-Token", Token);
126 Trace.TraceInformation("[AUTHENTICATE] End for {0}", userName);
131 public IList<ContainerInfo> ListContainers(string account)
134 using (var client = new RestClient(_baseClient))
136 if (!String.IsNullOrWhiteSpace(account))
137 client.BaseAddress = GetAccountUrl(account);
139 client.Parameters.Clear();
140 client.Parameters.Add("format", "json");
141 var content = client.DownloadStringWithRetry("", 3);
142 client.AssertStatusOK("List Containers failed");
144 if (client.StatusCode == HttpStatusCode.NoContent)
145 return new List<ContainerInfo>();
146 var infos = JsonConvert.DeserializeObject<IList<ContainerInfo>>(content);
152 private string GetAccountUrl(string account)
154 return new Uri(this.RootAddressUri, new Uri(account,UriKind.Relative)).AbsoluteUri;
157 public IList<ShareAccountInfo> ListSharingAccounts(DateTime? since=null)
159 Trace.TraceInformation("[START] ListSharingAccounts");
161 using (var client = new RestClient(_baseClient))
163 client.Parameters.Clear();
164 client.Parameters.Add("format", "json");
165 client.IfModifiedSince = since;
167 //Extract the username from the base address
168 client.BaseAddress = RootAddressUri.AbsoluteUri;
170 var content = client.DownloadStringWithRetry(@"", 3);
172 client.AssertStatusOK("ListSharingAccounts failed");
174 //If the result is empty, return an empty list,
175 var infos = String.IsNullOrWhiteSpace(content)
176 ? new List<ShareAccountInfo>()
177 //Otherwise deserialize the account list into a list of ShareAccountInfos
178 : JsonConvert.DeserializeObject<IList<ShareAccountInfo>>(content);
180 Trace.TraceInformation("[END] ListSharingAccounts");
185 //Request listing of all objects in a container modified since a specific time.
186 //If the *since* value is missing, return all objects
187 public IList<ObjectInfo> ListSharedObjects(DateTime? since = null)
190 Trace.TraceInformation("[START] ListSharedObjects");
192 var objects=new List<ObjectInfo>();
193 var accounts=ListSharingAccounts(since);
194 foreach (var account in accounts)
196 var containers=ListContainers(account.name);
197 foreach (var container in containers)
199 var containerObjects=ListObjects(account.name, container.Name, account.last_modified);
200 objects.AddRange(containerObjects);
206 public void ShareObject(string account, string container, string objectName, string shareTo, bool read, bool write)
208 if (String.IsNullOrWhiteSpace(Token))
209 throw new InvalidOperationException("The Token is not set");
210 if (StorageUrl==null)
211 throw new InvalidOperationException("The StorageUrl is not set");
212 if (String.IsNullOrWhiteSpace(container))
213 throw new ArgumentNullException("container");
214 if (String.IsNullOrWhiteSpace(objectName))
215 throw new ArgumentNullException("objectName");
216 if (String.IsNullOrWhiteSpace(account))
217 throw new ArgumentNullException("account");
218 if (String.IsNullOrWhiteSpace(shareTo))
219 throw new ArgumentNullException("shareTo");
220 Contract.EndContractBlock();
222 using (var client = new RestClient(_baseClient))
225 client.BaseAddress = GetAccountUrl(account);
227 client.Parameters.Clear();
228 client.Parameters.Add("format", "json");
230 string permission = "";
232 permission = String.Format("write={0}", shareTo);
234 permission=String.Format("read={0}", shareTo);
235 client.Headers.Add("X-Object-Sharing",permission);
237 var content = client.DownloadStringWithRetry(container, 3);
239 client.AssertStatusOK("ShareObject failed");
241 //If the result is empty, return an empty list,
242 var infos = String.IsNullOrWhiteSpace(content)
243 ? new List<ObjectInfo>()
244 //Otherwise deserialize the object list into a list of ObjectInfos
245 : JsonConvert.DeserializeObject<IList<ObjectInfo>>(content);
247 Trace.TraceInformation("[END] ListObjects");
254 public IList<ObjectInfo> ListObjects(string account, string container, DateTime? since = null)
256 if (String.IsNullOrWhiteSpace(container))
257 throw new ArgumentNullException("container");
258 Contract.EndContractBlock();
260 Trace.TraceInformation("[START] ListObjects");
262 using (var client = new RestClient(_baseClient))
264 if (!String.IsNullOrWhiteSpace(account))
265 client.BaseAddress = GetAccountUrl(account);
267 client.Parameters.Clear();
268 client.Parameters.Add("format", "json");
269 client.IfModifiedSince = since;
270 var content = client.DownloadStringWithRetry(container, 3);
272 client.AssertStatusOK("ListObjects failed");
274 //If the result is empty, return an empty list,
275 var infos=String.IsNullOrWhiteSpace(content)
276 ? new List<ObjectInfo>()
277 //Otherwise deserialize the object list into a list of ObjectInfos
278 : JsonConvert.DeserializeObject<IList<ObjectInfo>>(content);
280 foreach (var info in infos)
282 info.Container = container;
283 info.Account = account;
285 Trace.TraceInformation("[END] ListObjects");
292 public IList<ObjectInfo> ListObjects(string account, string container, string folder, DateTime? since = null)
294 if (String.IsNullOrWhiteSpace(container))
295 throw new ArgumentNullException("container");
296 if (String.IsNullOrWhiteSpace(folder))
297 throw new ArgumentNullException("folder");
298 Contract.EndContractBlock();
300 Trace.TraceInformation("[START] ListObjects");
302 using (var client = new RestClient(_baseClient))
304 if (!String.IsNullOrWhiteSpace(account))
305 client.BaseAddress = GetAccountUrl(account);
307 client.Parameters.Clear();
308 client.Parameters.Add("format", "json");
309 client.Parameters.Add("path", folder);
310 client.IfModifiedSince = since;
311 var content = client.DownloadStringWithRetry(container, 3);
312 client.AssertStatusOK("ListObjects failed");
314 var infos = JsonConvert.DeserializeObject<IList<ObjectInfo>>(content);
316 Trace.TraceInformation("[END] ListObjects");
322 public bool ContainerExists(string account, string container)
324 if (String.IsNullOrWhiteSpace(container))
325 throw new ArgumentNullException("container", "The container property can't be empty");
326 Contract.EndContractBlock();
328 using (var client = new RestClient(_baseClient))
330 if (!String.IsNullOrWhiteSpace(account))
331 client.BaseAddress = GetAccountUrl(account);
333 client.Parameters.Clear();
334 client.Head(container, 3);
336 switch (client.StatusCode)
338 case HttpStatusCode.OK:
339 case HttpStatusCode.NoContent:
341 case HttpStatusCode.NotFound:
344 throw CreateWebException("ContainerExists", client.StatusCode);
349 public bool ObjectExists(string account, string container, string objectName)
351 if (String.IsNullOrWhiteSpace(container))
352 throw new ArgumentNullException("container", "The container property can't be empty");
353 if (String.IsNullOrWhiteSpace(objectName))
354 throw new ArgumentNullException("objectName", "The objectName property can't be empty");
355 Contract.EndContractBlock();
357 using (var client = new RestClient(_baseClient))
359 if (!String.IsNullOrWhiteSpace(account))
360 client.BaseAddress = GetAccountUrl(account);
362 client.Parameters.Clear();
363 client.Head(container + "/" + objectName, 3);
365 switch (client.StatusCode)
367 case HttpStatusCode.OK:
368 case HttpStatusCode.NoContent:
370 case HttpStatusCode.NotFound:
373 throw CreateWebException("ObjectExists", client.StatusCode);
379 public ObjectInfo GetObjectInfo(string account, string container, string objectName)
381 if (String.IsNullOrWhiteSpace(container))
382 throw new ArgumentNullException("container", "The container property can't be empty");
383 if (String.IsNullOrWhiteSpace(objectName))
384 throw new ArgumentNullException("objectName", "The objectName property can't be empty");
385 Contract.EndContractBlock();
387 using (var client = new RestClient(_baseClient))
389 if (!String.IsNullOrWhiteSpace(account))
390 client.BaseAddress = GetAccountUrl(account);
393 client.Parameters.Clear();
395 client.Head(container + "/" + objectName, 3);
398 return ObjectInfo.Empty;
400 switch (client.StatusCode)
402 case HttpStatusCode.OK:
403 case HttpStatusCode.NoContent:
404 var keys = client.ResponseHeaders.AllKeys.AsQueryable();
405 var tags = (from key in keys
406 where key.StartsWith("X-Object-Meta-")
407 let name = key.Substring(14)
408 select new {Name = name, Value = client.ResponseHeaders[name]})
409 .ToDictionary(t => t.Name, t => t.Value);
410 var extensions = (from key in keys
411 where key.StartsWith("X-Object-") && !key.StartsWith("X-Object-Meta-")
412 select new {Name = key, Value = client.ResponseHeaders[key]})
413 .ToDictionary(t => t.Name, t => t.Value);
414 var info = new ObjectInfo
417 Hash = client.GetHeaderValue("ETag"),
418 Content_Type = client.GetHeaderValue("Content-Type"),
420 Last_Modified = client.LastModified,
421 Extensions = extensions
424 case HttpStatusCode.NotFound:
425 return ObjectInfo.Empty;
427 throw new WebException(
428 String.Format("[FAIL] GetObjectInfo for {0} failed with unexpected status code {1}",
429 objectName, client.StatusCode));
433 catch(RetryException)
435 Trace.TraceWarning("[RETRY FAIL] GetObjectInfo for {0} failed.");
436 return ObjectInfo.Empty;
438 catch(WebException e)
441 String.Format("[FAIL] GetObjectInfo for {0} failed with unexpected status code {1}",
442 objectName, client.StatusCode), e);
449 public void CreateFolder(string account, string container, string folder)
451 if (String.IsNullOrWhiteSpace(container))
452 throw new ArgumentNullException("container", "The container property can't be empty");
453 if (String.IsNullOrWhiteSpace(folder))
454 throw new ArgumentNullException("folder", "The folder property can't be empty");
455 Contract.EndContractBlock();
457 var folderUrl=String.Format("{0}/{1}",container,folder);
458 using (var client = new RestClient(_baseClient))
460 if (!String.IsNullOrWhiteSpace(account))
461 client.BaseAddress = GetAccountUrl(account);
463 client.Parameters.Clear();
464 client.Headers.Add("Content-Type", @"application/directory");
465 client.Headers.Add("Content-Length", "0");
466 client.PutWithRetry(folderUrl, 3);
468 if (client.StatusCode != HttpStatusCode.Created && client.StatusCode != HttpStatusCode.Accepted)
469 throw CreateWebException("CreateFolder", client.StatusCode);
473 public ContainerInfo GetContainerInfo(string account, string container)
475 if (String.IsNullOrWhiteSpace(container))
476 throw new ArgumentNullException("container", "The container property can't be empty");
477 Contract.EndContractBlock();
479 using (var client = new RestClient(_baseClient))
481 if (!String.IsNullOrWhiteSpace(account))
482 client.BaseAddress = GetAccountUrl(account);
484 client.Head(container);
485 switch (client.StatusCode)
487 case HttpStatusCode.OK:
488 case HttpStatusCode.NoContent:
489 var containerInfo = new ContainerInfo
493 long.Parse(client.GetHeaderValue("X-Container-Object-Count")),
494 Bytes = long.Parse(client.GetHeaderValue("X-Container-Bytes-Used")),
495 BlockHash = client.GetHeaderValue("X-Container-Block-Hash"),
496 BlockSize=int.Parse(client.GetHeaderValue("X-Container-Block-Size"))
498 return containerInfo;
499 case HttpStatusCode.NotFound:
500 return ContainerInfo.Empty;
502 throw CreateWebException("GetContainerInfo", client.StatusCode);
507 public void CreateContainer(string account, string container)
509 if (String.IsNullOrWhiteSpace(container))
510 throw new ArgumentNullException("container", "The container property can't be empty");
511 Contract.EndContractBlock();
513 using (var client = new RestClient(_baseClient))
515 if (!String.IsNullOrWhiteSpace(account))
516 client.BaseAddress = GetAccountUrl(account);
518 client.PutWithRetry(container, 3);
519 var expectedCodes = new[] {HttpStatusCode.Created, HttpStatusCode.Accepted, HttpStatusCode.OK};
520 if (!expectedCodes.Contains(client.StatusCode))
521 throw CreateWebException("CreateContainer", client.StatusCode);
525 public void DeleteContainer(string account, string container)
527 if (String.IsNullOrWhiteSpace(container))
528 throw new ArgumentNullException("container", "The container property can't be empty");
529 Contract.EndContractBlock();
531 using (var client = new RestClient(_baseClient))
533 if (!String.IsNullOrWhiteSpace(account))
534 client.BaseAddress = GetAccountUrl(account);
536 client.DeleteWithRetry(container, 3);
537 var expectedCodes = new[] {HttpStatusCode.NotFound, HttpStatusCode.NoContent};
538 if (!expectedCodes.Contains(client.StatusCode))
539 throw CreateWebException("DeleteContainer", client.StatusCode);
547 /// <param name="account"></param>
548 /// <param name="container"></param>
549 /// <param name="objectName"></param>
550 /// <param name="fileName"></param>
551 /// <returns></returns>
552 /// <remarks>This method should have no timeout or a very long one</remarks>
553 //Asynchronously download the object specified by *objectName* in a specific *container* to
555 public Task GetObject(string account, string container, string objectName, string fileName)
557 if (String.IsNullOrWhiteSpace(container))
558 throw new ArgumentNullException("container", "The container property can't be empty");
559 if (String.IsNullOrWhiteSpace(objectName))
560 throw new ArgumentNullException("objectName", "The objectName property can't be empty");
561 Contract.EndContractBlock();
565 //WebClient, and by extension RestClient, are not thread-safe. Create a new RestClient
566 //object to avoid concurrency errors.
568 //Download operations take a long time therefore they have no timeout.
569 var client = new RestClient(_baseClient) { Timeout = 0 };
570 if (!String.IsNullOrWhiteSpace(account))
571 client.BaseAddress = GetAccountUrl(account);
573 //The container and objectName are relative names. They are joined with the client's
574 //BaseAddress to create the object's absolute address
575 var builder = client.GetAddressBuilder(container, objectName);
576 var uri = builder.Uri;
578 //Download progress is reported to the Trace log
579 Trace.TraceInformation("[GET] START {0}", objectName);
580 client.DownloadProgressChanged += (sender, args) =>
581 Trace.TraceInformation("[GET PROGRESS] {0} {1}% {2} of {3}",
582 fileName, args.ProgressPercentage,
584 args.TotalBytesToReceive);
587 //Start downloading the object asynchronously
588 var downloadTask = client.DownloadFileTask(uri, fileName);
590 //Once the download completes
591 return downloadTask.ContinueWith(download =>
593 //Delete the local client object
595 //And report failure or completion
596 if (download.IsFaulted)
598 Trace.TraceError("[GET] FAIL for {0} with \r{1}", objectName,
603 Trace.TraceInformation("[GET] END {0}", objectName);
607 catch (Exception exc)
609 Trace.TraceError("[GET] END {0} with {1}", objectName, exc);
617 public Task<IList<string>> PutHashMap(string account, string container, string objectName, TreeHash hash)
619 if (String.IsNullOrWhiteSpace(container))
620 throw new ArgumentNullException("container");
621 if (String.IsNullOrWhiteSpace(objectName))
622 throw new ArgumentNullException("objectName");
624 throw new ArgumentNullException("hash");
625 if (String.IsNullOrWhiteSpace(Token))
626 throw new InvalidOperationException("Invalid Token");
627 if (StorageUrl == null)
628 throw new InvalidOperationException("Invalid Storage Url");
629 Contract.EndContractBlock();
632 //Don't use a timeout because putting the hashmap may be a long process
633 var client = new RestClient(_baseClient) { Timeout = 0 };
634 if (!String.IsNullOrWhiteSpace(account))
635 client.BaseAddress = GetAccountUrl(account);
637 //The container and objectName are relative names. They are joined with the client's
638 //BaseAddress to create the object's absolute address
639 var builder = client.GetAddressBuilder(container, objectName);
640 builder.Query = "format=json&hashmap";
641 var uri = builder.Uri;
644 //Send the tree hash as Json to the server
645 client.Headers[HttpRequestHeader.ContentType] = "application/octet-stream";
646 var uploadTask=client.UploadStringTask(uri, "PUT", hash.ToJson());
649 return uploadTask.ContinueWith(t =>
652 var empty = (IList<string>)new List<string>();
655 //The server will respond either with 201-created if all blocks were already on the server
656 if (client.StatusCode == HttpStatusCode.Created)
658 //in which case we return an empty hash list
661 //or with a 409-conflict and return the list of missing parts
662 //A 409 will cause an exception so we need to check t.IsFaulted to avoid propagating the exception
665 var ex = t.Exception.InnerException;
666 var we = ex as WebException;
667 var response = we.Response as HttpWebResponse;
668 if (response!=null && response.StatusCode==HttpStatusCode.Conflict)
670 //In case of 409 the missing parts will be in the response content
671 using (var stream = response.GetResponseStream())
672 using(var reader=new StreamReader(stream))
674 //We need to cleanup the content before returning it because it contains
675 //error content after the list of hashes
676 var hashes = new List<string>();
678 //All lines up to the first empty line are hashes
679 while(!String.IsNullOrWhiteSpace(line=reader.ReadLine()))
688 //Any other status code is unexpected and the exception should be rethrown
692 //Any other status code is unexpected but there was no exception. We can probably continue processing
695 Trace.TraceWarning("Unexcpected status code when putting map: {0} - {1}",client.StatusCode,client.StatusDescription);
702 public Task<byte[]> GetBlock(string account, string container, Uri relativeUrl, long start, long? end)
704 if (String.IsNullOrWhiteSpace(Token))
705 throw new InvalidOperationException("Invalid Token");
706 if (StorageUrl == null)
707 throw new InvalidOperationException("Invalid Storage Url");
708 if (String.IsNullOrWhiteSpace(container))
709 throw new ArgumentNullException("container");
710 if (relativeUrl== null)
711 throw new ArgumentNullException("relativeUrl");
712 if (end.HasValue && end<0)
713 throw new ArgumentOutOfRangeException("end");
715 throw new ArgumentOutOfRangeException("start");
716 Contract.EndContractBlock();
719 //Don't use a timeout because putting the hashmap may be a long process
720 var client = new RestClient(_baseClient) {Timeout = 0, RangeFrom = start, RangeTo = end};
721 if (!String.IsNullOrWhiteSpace(account))
722 client.BaseAddress = GetAccountUrl(account);
724 var builder = client.GetAddressBuilder(container, relativeUrl.ToString());
725 var uri = builder.Uri;
727 return client.DownloadDataTask(uri)
736 public Task PostBlock(string account, string container, byte[] block, int offset, int count)
738 if (String.IsNullOrWhiteSpace(container))
739 throw new ArgumentNullException("container");
741 throw new ArgumentNullException("block");
742 if (offset < 0 || offset >= block.Length)
743 throw new ArgumentOutOfRangeException("offset");
744 if (count < 0 || count > block.Length)
745 throw new ArgumentOutOfRangeException("count");
746 if (String.IsNullOrWhiteSpace(Token))
747 throw new InvalidOperationException("Invalid Token");
748 if (StorageUrl == null)
749 throw new InvalidOperationException("Invalid Storage Url");
750 Contract.EndContractBlock();
753 //Don't use a timeout because putting the hashmap may be a long process
754 var client = new RestClient(_baseClient) { Timeout = 0 };
755 if (!String.IsNullOrWhiteSpace(account))
756 client.BaseAddress = GetAccountUrl(account);
758 var builder = client.GetAddressBuilder(container, "");
759 //We are doing an update
760 builder.Query = "update";
761 var uri = builder.Uri;
763 client.Headers[HttpRequestHeader.ContentType] = "application/octet-stream";
765 Trace.TraceInformation("[BLOCK POST] START");
767 client.UploadProgressChanged += (sender, args) =>
768 Trace.TraceInformation("[BLOCK POST PROGRESS] {0}% {1} of {2}",
769 args.ProgressPercentage, args.BytesSent,
770 args.TotalBytesToSend);
771 client.UploadFileCompleted += (sender, args) =>
772 Trace.TraceInformation("[BLOCK POST PROGRESS] Completed ");
776 var uploadTask = client.UploadDataTask(uri, "POST", block)
777 .ContinueWith(upload =>
781 if (upload.IsFaulted)
783 var exception = upload.Exception.InnerException;
784 Trace.TraceError("[BLOCK POST] FAIL with \r{0}", exception);
788 Trace.TraceInformation("[BLOCK POST] END");
794 public Task<TreeHash> GetHashMap(string account, string container, string objectName)
796 if (String.IsNullOrWhiteSpace(container))
797 throw new ArgumentNullException("container");
798 if (String.IsNullOrWhiteSpace(objectName))
799 throw new ArgumentNullException("objectName");
800 if (String.IsNullOrWhiteSpace(Token))
801 throw new InvalidOperationException("Invalid Token");
802 if (StorageUrl == null)
803 throw new InvalidOperationException("Invalid Storage Url");
804 Contract.EndContractBlock();
808 //WebClient, and by extension RestClient, are not thread-safe. Create a new RestClient
809 //object to avoid concurrency errors.
811 //Download operations take a long time therefore they have no timeout.
812 //TODO: Do we really? this is a hashmap operation, not a download
813 var client = new RestClient(_baseClient) { Timeout = 0 };
814 if (!String.IsNullOrWhiteSpace(account))
815 client.BaseAddress = GetAccountUrl(account);
818 //The container and objectName are relative names. They are joined with the client's
819 //BaseAddress to create the object's absolute address
820 var builder = client.GetAddressBuilder(container, objectName);
821 builder.Query = "format=json&hashmap";
822 var uri = builder.Uri;
824 //Start downloading the object asynchronously
825 var downloadTask = client.DownloadStringTask(uri);
827 //Once the download completes
828 return downloadTask.ContinueWith(download =>
830 //Delete the local client object
832 //And report failure or completion
833 if (download.IsFaulted)
835 Trace.TraceError("[GET HASH] FAIL for {0} with \r{1}", objectName,
837 throw download.Exception;
840 //The server will return an empty string if the file is empty
841 var json = download.Result;
842 var treeHash = TreeHash.Parse(json);
843 Trace.TraceInformation("[GET HASH] END {0}", objectName);
847 catch (Exception exc)
849 Trace.TraceError("[GET HASH] END {0} with {1}", objectName, exc);
861 /// <param name="account"></param>
862 /// <param name="container"></param>
863 /// <param name="objectName"></param>
864 /// <param name="fileName"></param>
865 /// <param name="hash">Optional hash value for the file. If no hash is provided, the method calculates a new hash</param>
866 /// <remarks>>This method should have no timeout or a very long one</remarks>
867 public Task PutObject(string account, string container, string objectName, string fileName, string hash = null)
869 if (String.IsNullOrWhiteSpace(container))
870 throw new ArgumentNullException("container", "The container property can't be empty");
871 if (String.IsNullOrWhiteSpace(objectName))
872 throw new ArgumentNullException("objectName", "The objectName property can't be empty");
873 if (String.IsNullOrWhiteSpace(fileName))
874 throw new ArgumentNullException("fileName", "The fileName property can't be empty");
875 if (!File.Exists(fileName))
876 throw new FileNotFoundException("The file does not exist",fileName);
877 Contract.EndContractBlock();
882 var client = new RestClient(_baseClient){Timeout=0};
883 if (!String.IsNullOrWhiteSpace(account))
884 client.BaseAddress = GetAccountUrl(account);
886 var builder = client.GetAddressBuilder(container, objectName);
887 var uri = builder.Uri;
889 string etag = hash ?? CalculateHash(fileName);
891 client.Headers.Add("Content-Type", "application/octet-stream");
892 client.Headers.Add("ETag", etag);
895 Trace.TraceInformation("[PUT] START {0}", objectName);
896 client.UploadProgressChanged += (sender, args) =>
898 Trace.TraceInformation("[PUT PROGRESS] {0} {1}% {2} of {3}", fileName, args.ProgressPercentage, args.BytesSent, args.TotalBytesToSend);
901 client.UploadFileCompleted += (sender, args) =>
903 Trace.TraceInformation("[PUT PROGRESS] Completed {0}", fileName);
905 return client.UploadFileTask(uri, "PUT", fileName)
906 .ContinueWith(upload=>
910 if (upload.IsFaulted)
912 var exc = upload.Exception.InnerException;
913 Trace.TraceError("[PUT] FAIL for {0} with \r{1}",objectName,exc);
917 Trace.TraceInformation("[PUT] END {0}", objectName);
920 catch (Exception exc)
922 Trace.TraceError("[PUT] END {0} with {1}", objectName, exc);
929 private static string CalculateHash(string fileName)
932 using (var hasher = MD5.Create())
933 using(var stream=File.OpenRead(fileName))
935 var hashBuilder=new StringBuilder();
936 foreach (byte b in hasher.ComputeHash(stream))
937 hashBuilder.Append(b.ToString("x2").ToLower());
938 hash = hashBuilder.ToString();
943 /* public void DeleteObject(string container, string objectName,string account)
945 if (String.IsNullOrWhiteSpace(container))
946 throw new ArgumentNullException("container", "The container property can't be empty");
947 if (String.IsNullOrWhiteSpace(objectName))
948 throw new ArgumentNullException("objectName", "The objectName property can't be empty");
949 Contract.EndContractBlock();
951 using (var client = new RestClient(_baseClient))
953 if (!String.IsNullOrWhiteSpace(account))
954 client.BaseAddress = GetAccountUrl(account);
956 client.DeleteWithRetry(container + "/" + objectName, 3);
958 var expectedCodes = new[] {HttpStatusCode.NotFound, HttpStatusCode.NoContent};
959 if (!expectedCodes.Contains(client.StatusCode))
960 throw CreateWebException("DeleteObject", client.StatusCode);
965 public void MoveObject(string account, string sourceContainer, string oldObjectName, string targetContainer, string newObjectName)
967 if (String.IsNullOrWhiteSpace(sourceContainer))
968 throw new ArgumentNullException("sourceContainer", "The container property can't be empty");
969 if (String.IsNullOrWhiteSpace(oldObjectName))
970 throw new ArgumentNullException("oldObjectName", "The oldObjectName property can't be empty");
971 if (String.IsNullOrWhiteSpace(targetContainer))
972 throw new ArgumentNullException("targetContainer", "The container property can't be empty");
973 if (String.IsNullOrWhiteSpace(newObjectName))
974 throw new ArgumentNullException("newObjectName", "The newObjectName property can't be empty");
975 Contract.EndContractBlock();
977 var targetUrl = targetContainer + "/" + newObjectName;
978 var sourceUrl = String.Format("/{0}/{1}", sourceContainer, oldObjectName);
980 using (var client = new RestClient(_baseClient))
982 if (!String.IsNullOrWhiteSpace(account))
983 client.BaseAddress = GetAccountUrl(account);
985 client.Headers.Add("X-Move-From", sourceUrl);
986 client.PutWithRetry(targetUrl, 3);
988 var expectedCodes = new[] {HttpStatusCode.OK, HttpStatusCode.NoContent, HttpStatusCode.Created};
989 if (!expectedCodes.Contains(client.StatusCode))
990 throw CreateWebException("MoveObject", client.StatusCode);
994 public void DeleteObject(string account, string sourceContainer, string objectName, string targetContainer)
996 if (String.IsNullOrWhiteSpace(sourceContainer))
997 throw new ArgumentNullException("sourceContainer", "The container property can't be empty");
998 if (String.IsNullOrWhiteSpace(objectName))
999 throw new ArgumentNullException("objectName", "The oldObjectName property can't be empty");
1000 if (String.IsNullOrWhiteSpace(targetContainer))
1001 throw new ArgumentNullException("targetContainer", "The container property can't be empty");
1002 Contract.EndContractBlock();
1004 var targetUrl = targetContainer + "/" + objectName;
1005 var sourceUrl = String.Format("/{0}/{1}", sourceContainer, objectName);
1007 using (var client = new RestClient(_baseClient))
1009 if (!String.IsNullOrWhiteSpace(account))
1010 client.BaseAddress = GetAccountUrl(account);
1012 client.Headers.Add("X-Move-From", sourceUrl);
1013 client.PutWithRetry(targetUrl, 3);
1015 var expectedCodes = new[] {HttpStatusCode.OK, HttpStatusCode.NoContent, HttpStatusCode.Created,HttpStatusCode.NotFound};
1016 if (!expectedCodes.Contains(client.StatusCode))
1017 throw CreateWebException("DeleteObject", client.StatusCode);
1022 private static WebException CreateWebException(string operation, HttpStatusCode statusCode)
1024 return new WebException(String.Format("{0} failed with unexpected status code {1}", operation, statusCode));
1030 public class ShareAccountInfo
1032 public DateTime? last_modified { get; set; }
1033 public string name { get; set; }