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.Collections.Specialized;
9 using System.ComponentModel.Composition;
10 using System.Diagnostics.Contracts;
14 using System.Security.Cryptography;
16 using System.Threading.Tasks;
17 using Newtonsoft.Json;
18 using Pithos.Interfaces;
21 namespace Pithos.Network
23 [Export(typeof(ICloudClient))]
24 public class CloudFilesClient:ICloudClient
26 //CloudFilesClient uses *_baseClient* internally to communicate with the server
27 //RestClient provides a REST-friendly interface over the standard WebClient.
28 private RestClient _baseClient;
31 //During authentication the client provides a UserName
32 public string UserName { get; set; }
34 //and and ApiKey to the server
35 public string ApiKey { get; set; }
37 //And receives an authentication Token. This token must be provided in ALL other operations,
38 //in the X-Auth-Token header
39 private string _token;
42 get { return _token; }
46 _baseClient.Headers["X-Auth-Token"] = value;
50 //The client also receives a StorageUrl after authentication. All subsequent operations must
52 public Uri StorageUrl { get; set; }
55 protected Uri RootAddressUri { get; set; }
60 get { return _proxy; }
64 if (_baseClient != null)
65 _baseClient.Proxy = new WebProxy(value);
69 public double DownloadPercentLimit { get; set; }
70 public double UploadPercentLimit { get; set; }
72 public string AuthenticationUrl { get; set; }
75 public string VersionPath
77 get { return UsePithos ? "v1" : "v1.0"; }
80 public bool UsePithos { get; set; }
83 private static readonly ILog Log = LogManager.GetLogger("CloudFilesClient");
85 public CloudFilesClient(string userName, string apiKey)
91 public CloudFilesClient(AccountInfo accountInfo)
93 if (accountInfo==null)
94 throw new ArgumentNullException("accountInfo");
95 Contract.Ensures(!String.IsNullOrWhiteSpace(Token));
96 Contract.Ensures(StorageUrl != null);
97 Contract.Ensures(_baseClient != null);
98 Contract.Ensures(RootAddressUri != null);
99 Contract.EndContractBlock();
101 _baseClient = new RestClient
103 BaseAddress = accountInfo.StorageUri.ToString(),
107 StorageUrl = accountInfo.StorageUri;
108 Token = accountInfo.Token;
109 UserName = accountInfo.UserName;
111 //Get the root address (StorageUrl without the account)
112 var storageUrl = StorageUrl.AbsoluteUri;
113 var usernameIndex = storageUrl.LastIndexOf(UserName);
114 var rootUrl = storageUrl.Substring(0, usernameIndex);
115 RootAddressUri = new Uri(rootUrl);
119 public AccountInfo Authenticate()
121 if (String.IsNullOrWhiteSpace(UserName))
122 throw new InvalidOperationException("UserName is empty");
123 if (String.IsNullOrWhiteSpace(ApiKey))
124 throw new InvalidOperationException("ApiKey is empty");
125 if (String.IsNullOrWhiteSpace(AuthenticationUrl))
126 throw new InvalidOperationException("AuthenticationUrl is empty");
127 Contract.Ensures(!String.IsNullOrWhiteSpace(Token));
128 Contract.Ensures(StorageUrl != null);
129 Contract.Ensures(_baseClient != null);
130 Contract.Ensures(RootAddressUri != null);
131 Contract.EndContractBlock();
134 Log.InfoFormat("[AUTHENTICATE] Start for {0}", UserName);
136 using (var authClient = new RestClient{BaseAddress=AuthenticationUrl})
139 authClient.Proxy = new WebProxy(Proxy);
141 Contract.Assume(authClient.Headers!=null);
143 authClient.Headers.Add("X-Auth-User", UserName);
144 authClient.Headers.Add("X-Auth-Key", ApiKey);
146 authClient.DownloadStringWithRetry(VersionPath, 3);
148 authClient.AssertStatusOK("Authentication failed");
150 var storageUrl = authClient.GetHeaderValue("X-Storage-Url");
151 if (String.IsNullOrWhiteSpace(storageUrl))
152 throw new InvalidOperationException("Failed to obtain storage url");
154 _baseClient = new RestClient
156 BaseAddress = storageUrl,
161 StorageUrl = new Uri(storageUrl);
163 //Get the root address (StorageUrl without the account)
164 var usernameIndex=storageUrl.LastIndexOf(UserName);
165 var rootUrl = storageUrl.Substring(0, usernameIndex);
166 RootAddressUri = new Uri(rootUrl);
168 var token = authClient.GetHeaderValue("X-Auth-Token");
169 if (String.IsNullOrWhiteSpace(token))
170 throw new InvalidOperationException("Failed to obtain token url");
175 Log.InfoFormat("[AUTHENTICATE] End for {0}", UserName);
178 return new AccountInfo {StorageUri = StorageUrl, Token = Token, UserName = UserName};
184 public IList<ContainerInfo> ListContainers(string account)
186 using (var client = new RestClient(_baseClient))
188 if (!String.IsNullOrWhiteSpace(account))
189 client.BaseAddress = GetAccountUrl(account);
191 client.Parameters.Clear();
192 client.Parameters.Add("format", "json");
193 var content = client.DownloadStringWithRetry("", 3);
194 client.AssertStatusOK("List Containers failed");
196 if (client.StatusCode == HttpStatusCode.NoContent)
197 return new List<ContainerInfo>();
198 var infos = JsonConvert.DeserializeObject<IList<ContainerInfo>>(content);
200 foreach (var info in infos)
202 info.Account = account;
209 private string GetAccountUrl(string account)
211 return new Uri(this.RootAddressUri, new Uri(account,UriKind.Relative)).AbsoluteUri;
214 public IList<ShareAccountInfo> ListSharingAccounts(DateTime? since=null)
216 using (log4net.ThreadContext.Stacks["Share"].Push("List Accounts"))
218 if (Log.IsDebugEnabled) Log.DebugFormat("START");
220 using (var client = new RestClient(_baseClient))
222 client.Parameters.Clear();
223 client.Parameters.Add("format", "json");
224 client.IfModifiedSince = since;
226 //Extract the username from the base address
227 client.BaseAddress = RootAddressUri.AbsoluteUri;
229 var content = client.DownloadStringWithRetry(@"", 3);
231 client.AssertStatusOK("ListSharingAccounts failed");
233 //If the result is empty, return an empty list,
234 var infos = String.IsNullOrWhiteSpace(content)
235 ? new List<ShareAccountInfo>()
236 //Otherwise deserialize the account list into a list of ShareAccountInfos
237 : JsonConvert.DeserializeObject<IList<ShareAccountInfo>>(content);
239 Log.DebugFormat("END");
245 //Request listing of all objects in a container modified since a specific time.
246 //If the *since* value is missing, return all objects
247 public IList<ObjectInfo> ListSharedObjects(DateTime? since = null)
250 using (log4net.ThreadContext.Stacks["Share"].Push("List Objects"))
252 if (Log.IsDebugEnabled) Log.DebugFormat("START");
254 var objects = new List<ObjectInfo>();
255 var accounts = ListSharingAccounts(since);
256 foreach (var account in accounts)
258 var containers = ListContainers(account.name);
259 foreach (var container in containers)
261 var containerObjects = ListObjects(account.name, container.Name, null);
262 objects.AddRange(containerObjects);
265 if (Log.IsDebugEnabled) Log.DebugFormat("END");
270 public void SetTags(ObjectInfo target,IDictionary<string,string> tags)
272 if (String.IsNullOrWhiteSpace(Token))
273 throw new InvalidOperationException("The Token is not set");
274 if (StorageUrl == null)
275 throw new InvalidOperationException("The StorageUrl is not set");
277 throw new ArgumentNullException("target");
278 Contract.EndContractBlock();
280 using (log4net.ThreadContext.Stacks["Share"].Push("Share Object"))
282 if (Log.IsDebugEnabled) Log.DebugFormat("START");
284 using (var client = new RestClient(_baseClient))
287 client.BaseAddress = GetAccountUrl(target.Account);
289 client.Parameters.Clear();
290 client.Parameters.Add("update", "");
292 foreach (var tag in tags)
294 var headerTag = String.Format("X-Object-Meta-{0}", tag.Key);
295 client.Headers.Add(headerTag, tag.Value);
298 var content = client.DownloadStringWithRetry(target.Container, 3);
301 client.AssertStatusOK("SetTags failed");
302 //If the status is NOT ACCEPTED we have a problem
303 if (client.StatusCode != HttpStatusCode.Accepted)
305 Log.Error("Failed to set tags");
306 throw new Exception("Failed to set tags");
309 if (Log.IsDebugEnabled) Log.DebugFormat("END");
316 public void ShareObject(string account, string container, string objectName, string shareTo, bool read, bool write)
318 if (String.IsNullOrWhiteSpace(Token))
319 throw new InvalidOperationException("The Token is not set");
320 if (StorageUrl==null)
321 throw new InvalidOperationException("The StorageUrl is not set");
322 if (String.IsNullOrWhiteSpace(container))
323 throw new ArgumentNullException("container");
324 if (String.IsNullOrWhiteSpace(objectName))
325 throw new ArgumentNullException("objectName");
326 if (String.IsNullOrWhiteSpace(account))
327 throw new ArgumentNullException("account");
328 if (String.IsNullOrWhiteSpace(shareTo))
329 throw new ArgumentNullException("shareTo");
330 Contract.EndContractBlock();
332 using (log4net.ThreadContext.Stacks["Share"].Push("Share Object"))
334 if (Log.IsDebugEnabled) Log.DebugFormat("START");
336 using (var client = new RestClient(_baseClient))
339 client.BaseAddress = GetAccountUrl(account);
341 client.Parameters.Clear();
342 client.Parameters.Add("format", "json");
344 string permission = "";
346 permission = String.Format("write={0}", shareTo);
348 permission = String.Format("read={0}", shareTo);
349 client.Headers.Add("X-Object-Sharing", permission);
351 var content = client.DownloadStringWithRetry(container, 3);
353 client.AssertStatusOK("ShareObject failed");
355 //If the result is empty, return an empty list,
356 var infos = String.IsNullOrWhiteSpace(content)
357 ? new List<ObjectInfo>()
358 //Otherwise deserialize the object list into a list of ObjectInfos
359 : JsonConvert.DeserializeObject<IList<ObjectInfo>>(content);
361 if (Log.IsDebugEnabled) Log.DebugFormat("END");
368 public AccountInfo GetAccountPolicies(AccountInfo accountInfo)
370 if (accountInfo==null)
371 throw new ArgumentNullException("accountInfo");
372 Contract.EndContractBlock();
374 using (log4net.ThreadContext.Stacks["Account"].Push("GetPolicies"))
376 if (Log.IsDebugEnabled) Log.DebugFormat("START");
378 using (var client = new RestClient(_baseClient))
380 if (!String.IsNullOrWhiteSpace(accountInfo.UserName))
381 client.BaseAddress = GetAccountUrl(accountInfo.UserName);
383 client.Parameters.Clear();
384 client.Parameters.Add("format", "json");
385 client.Head(String.Empty, 3);
387 var quotaValue=client.ResponseHeaders["X-Account-Policy-Quota"];
388 var bytesValue= client.ResponseHeaders["X-Account-Bytes-Used"];
391 if (long.TryParse(quotaValue, out quota))
392 accountInfo.Quota = quota;
393 if (long.TryParse(bytesValue, out bytes))
394 accountInfo.BytesUsed = bytes;
403 public void UpdateMetadata(ObjectInfo objectInfo)
405 if (objectInfo == null)
406 throw new ArgumentNullException("objectInfo");
407 Contract.EndContractBlock();
409 using (log4net.ThreadContext.Stacks["Objects"].Push("UpdateMetadata"))
411 if (Log.IsDebugEnabled) Log.DebugFormat("START");
414 using(var client=new RestClient(_baseClient))
417 client.BaseAddress = GetAccountUrl(objectInfo.Account);
419 client.Parameters.Clear();
423 foreach (var tag in objectInfo.Tags)
425 var headerTag = String.Format("X-Object-Meta-{0}", tag.Key);
426 client.Headers.Add(headerTag, tag.Value);
431 var permissions=objectInfo.GetPermissionString();
432 client.SetNonEmptyHeaderValue("X-Object-Sharing",permissions);
434 client.SetNonEmptyHeaderValue("Content-Disposition",objectInfo.ContendDisposition);
435 client.SetNonEmptyHeaderValue("Content-Encoding",objectInfo.ContentEncoding);
436 client.SetNonEmptyHeaderValue("X-Object-Manifest",objectInfo.Manifest);
437 var isPublic = objectInfo.IsPublic.ToString().ToLower();
438 client.Headers.Add("X-Object-Public", isPublic);
441 var uriBuilder = client.GetAddressBuilder(objectInfo.Container, objectInfo.Name);
442 var uri = uriBuilder.Uri;
444 var content = client.UploadValues(uri,new NameValueCollection());
447 client.AssertStatusOK("UpdateMetadata failed");
448 //If the status is NOT ACCEPTED or OK we have a problem
449 if (!(client.StatusCode == HttpStatusCode.Accepted || client.StatusCode == HttpStatusCode.OK))
451 Log.Error("Failed to update metadata");
452 throw new Exception("Failed to update metadata");
455 if (Log.IsDebugEnabled) Log.DebugFormat("END");
462 public IList<ObjectInfo> ListObjects(string account, string container, DateTime? since = null)
464 if (String.IsNullOrWhiteSpace(container))
465 throw new ArgumentNullException("container");
466 Contract.EndContractBlock();
468 using (log4net.ThreadContext.Stacks["Objects"].Push("List"))
470 if (Log.IsDebugEnabled) Log.DebugFormat("START");
472 using (var client = new RestClient(_baseClient))
474 if (!String.IsNullOrWhiteSpace(account))
475 client.BaseAddress = GetAccountUrl(account);
477 client.Parameters.Clear();
478 client.Parameters.Add("format", "json");
479 client.IfModifiedSince = since;
480 var content = client.DownloadStringWithRetry(container, 3);
482 client.AssertStatusOK("ListObjects failed");
484 //If the result is empty, return an empty list,
485 var infos = String.IsNullOrWhiteSpace(content)
486 ? new List<ObjectInfo>()
487 //Otherwise deserialize the object list into a list of ObjectInfos
488 : JsonConvert.DeserializeObject<IList<ObjectInfo>>(content);
490 foreach (var info in infos)
492 info.Container = container;
493 info.Account = account;
495 if (Log.IsDebugEnabled) Log.DebugFormat("START");
503 public IList<ObjectInfo> ListObjects(string account, string container, string folder, DateTime? since = null)
505 if (String.IsNullOrWhiteSpace(container))
506 throw new ArgumentNullException("container");
507 if (String.IsNullOrWhiteSpace(folder))
508 throw new ArgumentNullException("folder");
509 Contract.EndContractBlock();
511 using (log4net.ThreadContext.Stacks["Objects"].Push("List"))
513 if (Log.IsDebugEnabled) Log.DebugFormat("START");
515 using (var client = new RestClient(_baseClient))
517 if (!String.IsNullOrWhiteSpace(account))
518 client.BaseAddress = GetAccountUrl(account);
520 client.Parameters.Clear();
521 client.Parameters.Add("format", "json");
522 client.Parameters.Add("path", folder);
523 client.IfModifiedSince = since;
524 var content = client.DownloadStringWithRetry(container, 3);
525 client.AssertStatusOK("ListObjects failed");
527 var infos = JsonConvert.DeserializeObject<IList<ObjectInfo>>(content);
529 if (Log.IsDebugEnabled) Log.DebugFormat("END");
536 public bool ContainerExists(string account, string container)
538 if (String.IsNullOrWhiteSpace(container))
539 throw new ArgumentNullException("container", "The container property can't be empty");
540 Contract.EndContractBlock();
542 using (log4net.ThreadContext.Stacks["Containters"].Push("Exists"))
544 if (Log.IsDebugEnabled) Log.DebugFormat("START");
546 using (var client = new RestClient(_baseClient))
548 if (!String.IsNullOrWhiteSpace(account))
549 client.BaseAddress = GetAccountUrl(account);
551 client.Parameters.Clear();
552 client.Head(container, 3);
555 switch (client.StatusCode)
557 case HttpStatusCode.OK:
558 case HttpStatusCode.NoContent:
561 case HttpStatusCode.NotFound:
565 throw CreateWebException("ContainerExists", client.StatusCode);
567 if (Log.IsDebugEnabled) Log.DebugFormat("END");
575 public bool ObjectExists(string account, string container, string objectName)
577 if (String.IsNullOrWhiteSpace(container))
578 throw new ArgumentNullException("container", "The container property can't be empty");
579 if (String.IsNullOrWhiteSpace(objectName))
580 throw new ArgumentNullException("objectName", "The objectName property can't be empty");
581 Contract.EndContractBlock();
583 using (var client = new RestClient(_baseClient))
585 if (!String.IsNullOrWhiteSpace(account))
586 client.BaseAddress = GetAccountUrl(account);
588 client.Parameters.Clear();
589 client.Head(container + "/" + objectName, 3);
591 switch (client.StatusCode)
593 case HttpStatusCode.OK:
594 case HttpStatusCode.NoContent:
596 case HttpStatusCode.NotFound:
599 throw CreateWebException("ObjectExists", client.StatusCode);
605 public ObjectInfo GetObjectInfo(string account, string container, string objectName)
607 if (String.IsNullOrWhiteSpace(container))
608 throw new ArgumentNullException("container", "The container property can't be empty");
609 if (String.IsNullOrWhiteSpace(objectName))
610 throw new ArgumentNullException("objectName", "The objectName property can't be empty");
611 Contract.EndContractBlock();
613 using (log4net.ThreadContext.Stacks["Objects"].Push("GetObjectInfo"))
616 using (var client = new RestClient(_baseClient))
618 if (!String.IsNullOrWhiteSpace(account))
619 client.BaseAddress = GetAccountUrl(account);
622 client.Parameters.Clear();
624 client.Head(container + "/" + objectName, 3);
627 return ObjectInfo.Empty;
629 switch (client.StatusCode)
631 case HttpStatusCode.OK:
632 case HttpStatusCode.NoContent:
633 var keys = client.ResponseHeaders.AllKeys.AsQueryable();
634 var tags = (from key in keys
635 where key.StartsWith("X-Object-Meta-")
636 let name = key.Substring(14)
637 select new {Name = name, Value = client.ResponseHeaders[key]})
638 .ToDictionary(t => t.Name, t => t.Value);
639 var extensions = (from key in keys
640 where key.StartsWith("X-Object-") && !key.StartsWith("X-Object-Meta-")
641 select new {Name = key, Value = client.ResponseHeaders[key]})
642 .ToDictionary(t => t.Name, t => t.Value);
644 var permissions=client.GetHeaderValue("X-Object-Sharing", false);
647 var info = new ObjectInfo
650 Container = container,
652 Hash = client.GetHeaderValue("ETag"),
653 Content_Type = client.GetHeaderValue("Content-Type"),
654 Bytes = Convert.ToInt64(client.GetHeaderValue("Content-Length")),
656 Last_Modified = client.LastModified,
657 Extensions = extensions,
658 ContentEncoding=client.GetHeaderValue("Content-Encoding",true),
659 ContendDisposition = client.GetHeaderValue("Content-Disposition",true),
660 Manifest=client.GetHeaderValue("X-Object-Manifest",true),
661 PublicUrl=client.GetHeaderValue("X-Object-Public",true),
663 info.SetPermissions(permissions);
665 case HttpStatusCode.NotFound:
666 return ObjectInfo.Empty;
668 throw new WebException(
669 String.Format("[FAIL] GetObjectInfo for {0} failed with unexpected status code {1}",
670 objectName, client.StatusCode));
674 catch (RetryException)
676 Log.WarnFormat("[RETRY FAIL] GetObjectInfo for {0} failed.");
677 return ObjectInfo.Empty;
679 catch (WebException e)
682 String.Format("[FAIL] GetObjectInfo for {0} failed with unexpected status code {1}",
683 objectName, client.StatusCode), e);
691 public void CreateFolder(string account, string container, string folder)
693 if (String.IsNullOrWhiteSpace(container))
694 throw new ArgumentNullException("container", "The container property can't be empty");
695 if (String.IsNullOrWhiteSpace(folder))
696 throw new ArgumentNullException("folder", "The folder property can't be empty");
697 Contract.EndContractBlock();
699 var folderUrl=String.Format("{0}/{1}",container,folder);
700 using (var client = new RestClient(_baseClient))
702 if (!String.IsNullOrWhiteSpace(account))
703 client.BaseAddress = GetAccountUrl(account);
705 client.Parameters.Clear();
706 client.Headers.Add("Content-Type", @"application/directory");
707 client.Headers.Add("Content-Length", "0");
708 client.PutWithRetry(folderUrl, 3);
710 if (client.StatusCode != HttpStatusCode.Created && client.StatusCode != HttpStatusCode.Accepted)
711 throw CreateWebException("CreateFolder", client.StatusCode);
715 public ContainerInfo GetContainerInfo(string account, string container)
717 if (String.IsNullOrWhiteSpace(container))
718 throw new ArgumentNullException("container", "The container property can't be empty");
719 Contract.EndContractBlock();
721 using (var client = new RestClient(_baseClient))
723 if (!String.IsNullOrWhiteSpace(account))
724 client.BaseAddress = GetAccountUrl(account);
726 client.Head(container);
727 switch (client.StatusCode)
729 case HttpStatusCode.OK:
730 case HttpStatusCode.NoContent:
731 var keys = client.ResponseHeaders.AllKeys.AsQueryable();
732 var tags = (from key in keys
733 where key.StartsWith("X-Container-Meta-")
734 let name = key.Substring(14)
735 select new { Name = name, Value = client.ResponseHeaders[name] })
736 .ToDictionary(t => t.Name, t => t.Value);
737 var policies= (from key in keys
738 where key.StartsWith("X-Container-Policy-")
739 let name = key.Substring(14)
740 select new { Name = name, Value = client.ResponseHeaders[name] })
741 .ToDictionary(t => t.Name, t => t.Value);
743 var containerInfo = new ContainerInfo
748 long.Parse(client.GetHeaderValue("X-Container-Object-Count")),
749 Bytes = long.Parse(client.GetHeaderValue("X-Container-Bytes-Used")),
750 BlockHash = client.GetHeaderValue("X-Container-Block-Hash"),
751 BlockSize=int.Parse(client.GetHeaderValue("X-Container-Block-Size")),
752 Last_Modified=client.LastModified,
758 return containerInfo;
759 case HttpStatusCode.NotFound:
760 return ContainerInfo.Empty;
762 throw CreateWebException("GetContainerInfo", client.StatusCode);
767 public void CreateContainer(string account, string container)
769 if (String.IsNullOrWhiteSpace(container))
770 throw new ArgumentNullException("container", "The container property can't be empty");
771 Contract.EndContractBlock();
773 using (var client = new RestClient(_baseClient))
775 if (!String.IsNullOrWhiteSpace(account))
776 client.BaseAddress = GetAccountUrl(account);
778 client.PutWithRetry(container, 3);
779 var expectedCodes = new[] {HttpStatusCode.Created, HttpStatusCode.Accepted, HttpStatusCode.OK};
780 if (!expectedCodes.Contains(client.StatusCode))
781 throw CreateWebException("CreateContainer", client.StatusCode);
785 public void DeleteContainer(string account, string container)
787 if (String.IsNullOrWhiteSpace(container))
788 throw new ArgumentNullException("container", "The container property can't be empty");
789 Contract.EndContractBlock();
791 using (var client = new RestClient(_baseClient))
793 if (!String.IsNullOrWhiteSpace(account))
794 client.BaseAddress = GetAccountUrl(account);
796 client.DeleteWithRetry(container, 3);
797 var expectedCodes = new[] {HttpStatusCode.NotFound, HttpStatusCode.NoContent};
798 if (!expectedCodes.Contains(client.StatusCode))
799 throw CreateWebException("DeleteContainer", client.StatusCode);
807 /// <param name="account"></param>
808 /// <param name="container"></param>
809 /// <param name="objectName"></param>
810 /// <param name="fileName"></param>
811 /// <returns></returns>
812 /// <remarks>This method should have no timeout or a very long one</remarks>
813 //Asynchronously download the object specified by *objectName* in a specific *container* to
815 public Task GetObject(string account, string container, string objectName, string fileName)
817 if (String.IsNullOrWhiteSpace(container))
818 throw new ArgumentNullException("container", "The container property can't be empty");
819 if (String.IsNullOrWhiteSpace(objectName))
820 throw new ArgumentNullException("objectName", "The objectName property can't be empty");
821 Contract.EndContractBlock();
825 //WebClient, and by extension RestClient, are not thread-safe. Create a new RestClient
826 //object to avoid concurrency errors.
828 //Download operations take a long time therefore they have no timeout.
829 var client = new RestClient(_baseClient) { Timeout = 0 };
830 if (!String.IsNullOrWhiteSpace(account))
831 client.BaseAddress = GetAccountUrl(account);
833 //The container and objectName are relative names. They are joined with the client's
834 //BaseAddress to create the object's absolute address
835 var builder = client.GetAddressBuilder(container, objectName);
836 var uri = builder.Uri;
838 //Download progress is reported to the Trace log
839 Log.InfoFormat("[GET] START {0}", objectName);
840 client.DownloadProgressChanged += (sender, args) =>
841 Log.InfoFormat("[GET PROGRESS] {0} {1}% {2} of {3}",
842 fileName, args.ProgressPercentage,
844 args.TotalBytesToReceive);
847 //Start downloading the object asynchronously
848 var downloadTask = client.DownloadFileTask(uri, fileName);
850 //Once the download completes
851 return downloadTask.ContinueWith(download =>
853 //Delete the local client object
855 //And report failure or completion
856 if (download.IsFaulted)
858 Log.ErrorFormat("[GET] FAIL for {0} with \r{1}", objectName,
863 Log.InfoFormat("[GET] END {0}", objectName);
867 catch (Exception exc)
869 Log.ErrorFormat("[GET] END {0} with {1}", objectName, exc);
877 public Task<IList<string>> PutHashMap(string account, string container, string objectName, TreeHash hash)
879 if (String.IsNullOrWhiteSpace(container))
880 throw new ArgumentNullException("container");
881 if (String.IsNullOrWhiteSpace(objectName))
882 throw new ArgumentNullException("objectName");
884 throw new ArgumentNullException("hash");
885 if (String.IsNullOrWhiteSpace(Token))
886 throw new InvalidOperationException("Invalid Token");
887 if (StorageUrl == null)
888 throw new InvalidOperationException("Invalid Storage Url");
889 Contract.EndContractBlock();
892 //Don't use a timeout because putting the hashmap may be a long process
893 var client = new RestClient(_baseClient) { Timeout = 0 };
894 if (!String.IsNullOrWhiteSpace(account))
895 client.BaseAddress = GetAccountUrl(account);
897 //The container and objectName are relative names. They are joined with the client's
898 //BaseAddress to create the object's absolute address
899 var builder = client.GetAddressBuilder(container, objectName);
900 builder.Query = "format=json&hashmap";
901 var uri = builder.Uri;
904 //Send the tree hash as Json to the server
905 client.Headers[HttpRequestHeader.ContentType] = "application/octet-stream";
906 var uploadTask=client.UploadStringTask(uri, "PUT", hash.ToJson());
909 return uploadTask.ContinueWith(t =>
912 var empty = (IList<string>)new List<string>();
915 //The server will respond either with 201-created if all blocks were already on the server
916 if (client.StatusCode == HttpStatusCode.Created)
918 //in which case we return an empty hash list
921 //or with a 409-conflict and return the list of missing parts
922 //A 409 will cause an exception so we need to check t.IsFaulted to avoid propagating the exception
925 var ex = t.Exception.InnerException;
926 var we = ex as WebException;
927 var response = we.Response as HttpWebResponse;
928 if (response!=null && response.StatusCode==HttpStatusCode.Conflict)
930 //In case of 409 the missing parts will be in the response content
931 using (var stream = response.GetResponseStream())
932 using(var reader=new StreamReader(stream))
934 //We need to cleanup the content before returning it because it contains
935 //error content after the list of hashes
936 var hashes = new List<string>();
938 //All lines up to the first empty line are hashes
939 while(!String.IsNullOrWhiteSpace(line=reader.ReadLine()))
948 //Any other status code is unexpected and the exception should be rethrown
952 //Any other status code is unexpected but there was no exception. We can probably continue processing
955 Log.WarnFormat("Unexcpected status code when putting map: {0} - {1}",client.StatusCode,client.StatusDescription);
962 public Task<byte[]> GetBlock(string account, string container, Uri relativeUrl, long start, long? end)
964 if (String.IsNullOrWhiteSpace(Token))
965 throw new InvalidOperationException("Invalid Token");
966 if (StorageUrl == null)
967 throw new InvalidOperationException("Invalid Storage Url");
968 if (String.IsNullOrWhiteSpace(container))
969 throw new ArgumentNullException("container");
970 if (relativeUrl== null)
971 throw new ArgumentNullException("relativeUrl");
972 if (end.HasValue && end<0)
973 throw new ArgumentOutOfRangeException("end");
975 throw new ArgumentOutOfRangeException("start");
976 Contract.EndContractBlock();
979 //Don't use a timeout because putting the hashmap may be a long process
980 var client = new RestClient(_baseClient) {Timeout = 0, RangeFrom = start, RangeTo = end};
981 if (!String.IsNullOrWhiteSpace(account))
982 client.BaseAddress = GetAccountUrl(account);
984 var builder = client.GetAddressBuilder(container, relativeUrl.ToString());
985 var uri = builder.Uri;
987 return client.DownloadDataTask(uri)
996 public Task PostBlock(string account, string container, byte[] block, int offset, int count)
998 if (String.IsNullOrWhiteSpace(container))
999 throw new ArgumentNullException("container");
1001 throw new ArgumentNullException("block");
1002 if (offset < 0 || offset >= block.Length)
1003 throw new ArgumentOutOfRangeException("offset");
1004 if (count < 0 || count > block.Length)
1005 throw new ArgumentOutOfRangeException("count");
1006 if (String.IsNullOrWhiteSpace(Token))
1007 throw new InvalidOperationException("Invalid Token");
1008 if (StorageUrl == null)
1009 throw new InvalidOperationException("Invalid Storage Url");
1010 Contract.EndContractBlock();
1013 //Don't use a timeout because putting the hashmap may be a long process
1014 var client = new RestClient(_baseClient) { Timeout = 0 };
1015 if (!String.IsNullOrWhiteSpace(account))
1016 client.BaseAddress = GetAccountUrl(account);
1018 var builder = client.GetAddressBuilder(container, "");
1019 //We are doing an update
1020 builder.Query = "update";
1021 var uri = builder.Uri;
1023 client.Headers[HttpRequestHeader.ContentType] = "application/octet-stream";
1025 Log.InfoFormat("[BLOCK POST] START");
1027 client.UploadProgressChanged += (sender, args) =>
1028 Log.InfoFormat("[BLOCK POST PROGRESS] {0}% {1} of {2}",
1029 args.ProgressPercentage, args.BytesSent,
1030 args.TotalBytesToSend);
1031 client.UploadFileCompleted += (sender, args) =>
1032 Log.InfoFormat("[BLOCK POST PROGRESS] Completed ");
1036 var uploadTask = client.UploadDataTask(uri, "POST", block)
1037 .ContinueWith(upload =>
1041 if (upload.IsFaulted)
1043 var exception = upload.Exception.InnerException;
1044 Log.ErrorFormat("[BLOCK POST] FAIL with \r{0}", exception);
1048 Log.InfoFormat("[BLOCK POST] END");
1054 public Task<TreeHash> GetHashMap(string account, string container, string objectName)
1056 if (String.IsNullOrWhiteSpace(container))
1057 throw new ArgumentNullException("container");
1058 if (String.IsNullOrWhiteSpace(objectName))
1059 throw new ArgumentNullException("objectName");
1060 if (String.IsNullOrWhiteSpace(Token))
1061 throw new InvalidOperationException("Invalid Token");
1062 if (StorageUrl == null)
1063 throw new InvalidOperationException("Invalid Storage Url");
1064 Contract.EndContractBlock();
1068 //WebClient, and by extension RestClient, are not thread-safe. Create a new RestClient
1069 //object to avoid concurrency errors.
1071 //Download operations take a long time therefore they have no timeout.
1072 //TODO: Do we really? this is a hashmap operation, not a download
1073 var client = new RestClient(_baseClient) { Timeout = 0 };
1074 if (!String.IsNullOrWhiteSpace(account))
1075 client.BaseAddress = GetAccountUrl(account);
1078 //The container and objectName are relative names. They are joined with the client's
1079 //BaseAddress to create the object's absolute address
1080 var builder = client.GetAddressBuilder(container, objectName);
1081 builder.Query = "format=json&hashmap";
1082 var uri = builder.Uri;
1084 //Start downloading the object asynchronously
1085 var downloadTask = client.DownloadStringTask(uri);
1087 //Once the download completes
1088 return downloadTask.ContinueWith(download =>
1090 //Delete the local client object
1092 //And report failure or completion
1093 if (download.IsFaulted)
1095 Log.ErrorFormat("[GET HASH] FAIL for {0} with \r{1}", objectName,
1096 download.Exception);
1097 throw download.Exception;
1100 //The server will return an empty string if the file is empty
1101 var json = download.Result;
1102 var treeHash = TreeHash.Parse(json);
1103 Log.InfoFormat("[GET HASH] END {0}", objectName);
1107 catch (Exception exc)
1109 Log.ErrorFormat("[GET HASH] END {0} with {1}", objectName, exc);
1121 /// <param name="account"></param>
1122 /// <param name="container"></param>
1123 /// <param name="objectName"></param>
1124 /// <param name="fileName"></param>
1125 /// <param name="hash">Optional hash value for the file. If no hash is provided, the method calculates a new hash</param>
1126 /// <remarks>>This method should have no timeout or a very long one</remarks>
1127 public Task PutObject(string account, string container, string objectName, string fileName, string hash = null)
1129 if (String.IsNullOrWhiteSpace(container))
1130 throw new ArgumentNullException("container", "The container property can't be empty");
1131 if (String.IsNullOrWhiteSpace(objectName))
1132 throw new ArgumentNullException("objectName", "The objectName property can't be empty");
1133 if (String.IsNullOrWhiteSpace(fileName))
1134 throw new ArgumentNullException("fileName", "The fileName property can't be empty");
1135 if (!File.Exists(fileName))
1136 throw new FileNotFoundException("The file does not exist",fileName);
1137 Contract.EndContractBlock();
1142 var client = new RestClient(_baseClient){Timeout=0};
1143 if (!String.IsNullOrWhiteSpace(account))
1144 client.BaseAddress = GetAccountUrl(account);
1146 var builder = client.GetAddressBuilder(container, objectName);
1147 var uri = builder.Uri;
1149 string etag = hash ?? CalculateHash(fileName);
1151 client.Headers.Add("Content-Type", "application/octet-stream");
1152 client.Headers.Add("ETag", etag);
1155 Log.InfoFormat("[PUT] START {0}", objectName);
1156 client.UploadProgressChanged += (sender, args) =>
1158 using (log4net.ThreadContext.Stacks["PUT"].Push("Progress"))
1160 Log.InfoFormat("{0} {1}% {2} of {3}", fileName, args.ProgressPercentage,
1161 args.BytesSent, args.TotalBytesToSend);
1165 client.UploadFileCompleted += (sender, args) =>
1167 using (log4net.ThreadContext.Stacks["PUT"].Push("Progress"))
1169 Log.InfoFormat("Completed {0}", fileName);
1172 return client.UploadFileTask(uri, "PUT", fileName)
1173 .ContinueWith(upload=>
1177 if (upload.IsFaulted)
1179 var exc = upload.Exception.InnerException;
1180 Log.ErrorFormat("[PUT] FAIL for {0} with \r{1}",objectName,exc);
1184 Log.InfoFormat("[PUT] END {0}", objectName);
1187 catch (Exception exc)
1189 Log.ErrorFormat("[PUT] END {0} with {1}", objectName, exc);
1196 private static string CalculateHash(string fileName)
1198 Contract.Requires(!String.IsNullOrWhiteSpace(fileName));
1199 Contract.EndContractBlock();
1202 using (var hasher = MD5.Create())
1203 using(var stream=File.OpenRead(fileName))
1205 var hashBuilder=new StringBuilder();
1206 foreach (byte b in hasher.ComputeHash(stream))
1207 hashBuilder.Append(b.ToString("x2").ToLower());
1208 hash = hashBuilder.ToString();
1213 public void MoveObject(string account, string sourceContainer, string oldObjectName, string targetContainer, string newObjectName)
1215 if (String.IsNullOrWhiteSpace(sourceContainer))
1216 throw new ArgumentNullException("sourceContainer", "The container property can't be empty");
1217 if (String.IsNullOrWhiteSpace(oldObjectName))
1218 throw new ArgumentNullException("oldObjectName", "The oldObjectName property can't be empty");
1219 if (String.IsNullOrWhiteSpace(targetContainer))
1220 throw new ArgumentNullException("targetContainer", "The container property can't be empty");
1221 if (String.IsNullOrWhiteSpace(newObjectName))
1222 throw new ArgumentNullException("newObjectName", "The newObjectName property can't be empty");
1223 Contract.EndContractBlock();
1225 var targetUrl = targetContainer + "/" + newObjectName;
1226 var sourceUrl = String.Format("/{0}/{1}", sourceContainer, oldObjectName);
1228 using (var client = new RestClient(_baseClient))
1230 if (!String.IsNullOrWhiteSpace(account))
1231 client.BaseAddress = GetAccountUrl(account);
1233 client.Headers.Add("X-Move-From", sourceUrl);
1234 client.PutWithRetry(targetUrl, 3);
1236 var expectedCodes = new[] {HttpStatusCode.OK, HttpStatusCode.NoContent, HttpStatusCode.Created};
1237 if (!expectedCodes.Contains(client.StatusCode))
1238 throw CreateWebException("MoveObject", client.StatusCode);
1242 public void DeleteObject(string account, string sourceContainer, string objectName)
1244 if (String.IsNullOrWhiteSpace(sourceContainer))
1245 throw new ArgumentNullException("sourceContainer", "The container property can't be empty");
1246 if (String.IsNullOrWhiteSpace(objectName))
1247 throw new ArgumentNullException("objectName", "The oldObjectName property can't be empty");
1248 Contract.EndContractBlock();
1250 var targetUrl = FolderConstants.TrashContainer + "/" + objectName;
1251 var sourceUrl = String.Format("/{0}/{1}", sourceContainer, objectName);
1253 using (var client = new RestClient(_baseClient))
1255 if (!String.IsNullOrWhiteSpace(account))
1256 client.BaseAddress = GetAccountUrl(account);
1258 client.Headers.Add("X-Move-From", sourceUrl);
1259 client.AllowedStatusCodes.Add(HttpStatusCode.NotFound);
1260 client.PutWithRetry(targetUrl, 3);
1262 var expectedCodes = new[] {HttpStatusCode.OK, HttpStatusCode.NoContent, HttpStatusCode.Created,HttpStatusCode.NotFound};
1263 if (!expectedCodes.Contains(client.StatusCode))
1264 throw CreateWebException("DeleteObject", client.StatusCode);
1269 private static WebException CreateWebException(string operation, HttpStatusCode statusCode)
1271 return new WebException(String.Format("{0} failed with unexpected status code {1}", operation, statusCode));
1277 public class ShareAccountInfo
1279 public DateTime? last_modified { get; set; }
1280 public string name { get; set; }