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.Contracts;
13 using System.Security.Cryptography;
15 using System.Threading.Tasks;
16 using Newtonsoft.Json;
17 using Pithos.Interfaces;
20 namespace Pithos.Network
22 [Export(typeof(ICloudClient))]
23 public class CloudFilesClient:ICloudClient
25 //CloudFilesClient uses *_baseClient* internally to communicate with the server
26 //RestClient provides a REST-friendly interface over the standard WebClient.
27 private RestClient _baseClient;
30 //During authentication the client provides a UserName
31 public string UserName { get; set; }
33 //and and ApiKey to the server
34 public string ApiKey { get; set; }
36 //And receives an authentication Token. This token must be provided in ALL other operations,
37 //in the X-Auth-Token header
38 private string _token;
41 get { return _token; }
45 _baseClient.Headers["X-Auth-Token"] = value;
49 //The client also receives a StorageUrl after authentication. All subsequent operations must
51 public Uri StorageUrl { get; set; }
54 protected Uri RootAddressUri { get; set; }
59 get { return _proxy; }
63 if (_baseClient != null)
64 _baseClient.Proxy = new WebProxy(value);
68 public double DownloadPercentLimit { get; set; }
69 public double UploadPercentLimit { get; set; }
71 public string AuthenticationUrl { get; set; }
74 public string VersionPath
76 get { return UsePithos ? "v1" : "v1.0"; }
79 public bool UsePithos { get; set; }
82 private static readonly ILog Log = LogManager.GetLogger("CloudFilesClient");
84 public CloudFilesClient(string userName, string apiKey)
90 public CloudFilesClient(AccountInfo accountInfo)
92 if (accountInfo==null)
93 throw new ArgumentNullException("accountInfo");
94 Contract.Ensures(!String.IsNullOrWhiteSpace(Token));
95 Contract.Ensures(StorageUrl != null);
96 Contract.Ensures(_baseClient != null);
97 Contract.Ensures(RootAddressUri != null);
98 Contract.EndContractBlock();
100 _baseClient = new RestClient
102 BaseAddress = accountInfo.StorageUri.ToString(),
106 StorageUrl = accountInfo.StorageUri;
107 Token = accountInfo.Token;
108 UserName = accountInfo.UserName;
110 //Get the root address (StorageUrl without the account)
111 var storageUrl = StorageUrl.AbsoluteUri;
112 var usernameIndex = storageUrl.LastIndexOf(UserName);
113 var rootUrl = storageUrl.Substring(0, usernameIndex);
114 RootAddressUri = new Uri(rootUrl);
118 public AccountInfo Authenticate()
120 if (String.IsNullOrWhiteSpace(UserName))
121 throw new InvalidOperationException("UserName is empty");
122 if (String.IsNullOrWhiteSpace(ApiKey))
123 throw new InvalidOperationException("ApiKey is empty");
124 if (String.IsNullOrWhiteSpace(AuthenticationUrl))
125 throw new InvalidOperationException("AuthenticationUrl is empty");
126 Contract.Ensures(!String.IsNullOrWhiteSpace(Token));
127 Contract.Ensures(StorageUrl != null);
128 Contract.Ensures(_baseClient != null);
129 Contract.Ensures(RootAddressUri != null);
130 Contract.EndContractBlock();
133 Log.InfoFormat("[AUTHENTICATE] Start for {0}", UserName);
135 using (var authClient = new RestClient{BaseAddress=AuthenticationUrl})
138 authClient.Proxy = new WebProxy(Proxy);
140 Contract.Assume(authClient.Headers!=null);
142 authClient.Headers.Add("X-Auth-User", UserName);
143 authClient.Headers.Add("X-Auth-Key", ApiKey);
145 authClient.DownloadStringWithRetry(VersionPath, 3);
147 authClient.AssertStatusOK("Authentication failed");
149 var storageUrl = authClient.GetHeaderValue("X-Storage-Url");
150 if (String.IsNullOrWhiteSpace(storageUrl))
151 throw new InvalidOperationException("Failed to obtain storage url");
153 _baseClient = new RestClient
155 BaseAddress = storageUrl,
160 StorageUrl = new Uri(storageUrl);
162 //Get the root address (StorageUrl without the account)
163 var usernameIndex=storageUrl.LastIndexOf(UserName);
164 var rootUrl = storageUrl.Substring(0, usernameIndex);
165 RootAddressUri = new Uri(rootUrl);
167 var token = authClient.GetHeaderValue("X-Auth-Token");
168 if (String.IsNullOrWhiteSpace(token))
169 throw new InvalidOperationException("Failed to obtain token url");
174 Log.InfoFormat("[AUTHENTICATE] End for {0}", UserName);
177 return new AccountInfo {StorageUri = StorageUrl, Token = Token, UserName = UserName};
183 public IList<ContainerInfo> ListContainers(string account)
185 using (var client = new RestClient(_baseClient))
187 if (!String.IsNullOrWhiteSpace(account))
188 client.BaseAddress = GetAccountUrl(account);
190 client.Parameters.Clear();
191 client.Parameters.Add("format", "json");
192 var content = client.DownloadStringWithRetry("", 3);
193 client.AssertStatusOK("List Containers failed");
195 if (client.StatusCode == HttpStatusCode.NoContent)
196 return new List<ContainerInfo>();
197 var infos = JsonConvert.DeserializeObject<IList<ContainerInfo>>(content);
203 private string GetAccountUrl(string account)
205 return new Uri(this.RootAddressUri, new Uri(account,UriKind.Relative)).AbsoluteUri;
208 public IList<ShareAccountInfo> ListSharingAccounts(DateTime? since=null)
210 using (log4net.ThreadContext.Stacks["Share"].Push("List Accounts"))
212 if (Log.IsDebugEnabled) Log.DebugFormat("START");
214 using (var client = new RestClient(_baseClient))
216 client.Parameters.Clear();
217 client.Parameters.Add("format", "json");
218 client.IfModifiedSince = since;
220 //Extract the username from the base address
221 client.BaseAddress = RootAddressUri.AbsoluteUri;
223 var content = client.DownloadStringWithRetry(@"", 3);
225 client.AssertStatusOK("ListSharingAccounts failed");
227 //If the result is empty, return an empty list,
228 var infos = String.IsNullOrWhiteSpace(content)
229 ? new List<ShareAccountInfo>()
230 //Otherwise deserialize the account list into a list of ShareAccountInfos
231 : JsonConvert.DeserializeObject<IList<ShareAccountInfo>>(content);
233 Log.DebugFormat("END");
239 //Request listing of all objects in a container modified since a specific time.
240 //If the *since* value is missing, return all objects
241 public IList<ObjectInfo> ListSharedObjects(DateTime? since = null)
244 using (log4net.ThreadContext.Stacks["Share"].Push("List Objects"))
246 if (Log.IsDebugEnabled) Log.DebugFormat("START");
248 var objects = new List<ObjectInfo>();
249 var accounts = ListSharingAccounts(since);
250 foreach (var account in accounts)
252 var containers = ListContainers(account.name);
253 foreach (var container in containers)
255 var containerObjects = ListObjects(account.name, container.Name, null);
256 objects.AddRange(containerObjects);
259 if (Log.IsDebugEnabled) Log.DebugFormat("END");
264 public void ShareObject(string account, string container, string objectName, string shareTo, bool read, bool write)
266 if (String.IsNullOrWhiteSpace(Token))
267 throw new InvalidOperationException("The Token is not set");
268 if (StorageUrl==null)
269 throw new InvalidOperationException("The StorageUrl is not set");
270 if (String.IsNullOrWhiteSpace(container))
271 throw new ArgumentNullException("container");
272 if (String.IsNullOrWhiteSpace(objectName))
273 throw new ArgumentNullException("objectName");
274 if (String.IsNullOrWhiteSpace(account))
275 throw new ArgumentNullException("account");
276 if (String.IsNullOrWhiteSpace(shareTo))
277 throw new ArgumentNullException("shareTo");
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(account);
289 client.Parameters.Clear();
290 client.Parameters.Add("format", "json");
292 string permission = "";
294 permission = String.Format("write={0}", shareTo);
296 permission = String.Format("read={0}", shareTo);
297 client.Headers.Add("X-Object-Sharing", permission);
299 var content = client.DownloadStringWithRetry(container, 3);
301 client.AssertStatusOK("ShareObject failed");
303 //If the result is empty, return an empty list,
304 var infos = String.IsNullOrWhiteSpace(content)
305 ? new List<ObjectInfo>()
306 //Otherwise deserialize the object list into a list of ObjectInfos
307 : JsonConvert.DeserializeObject<IList<ObjectInfo>>(content);
309 if (Log.IsDebugEnabled) Log.DebugFormat("END");
316 public AccountInfo GetAccountPolicies(AccountInfo accountInfo)
318 if (accountInfo==null)
319 throw new ArgumentNullException("accountInfo");
320 Contract.EndContractBlock();
322 using (log4net.ThreadContext.Stacks["Account"].Push("GetPolicies"))
324 if (Log.IsDebugEnabled) Log.DebugFormat("START");
326 using (var client = new RestClient(_baseClient))
328 if (!String.IsNullOrWhiteSpace(accountInfo.UserName))
329 client.BaseAddress = GetAccountUrl(accountInfo.UserName);
331 client.Parameters.Clear();
332 client.Parameters.Add("format", "json");
333 client.Head(String.Empty, 3);
335 var quotaValue=client.ResponseHeaders["X-Account-Policy-Quota"];
336 var bytesValue= client.ResponseHeaders["X-Account-Bytes-Used"];
339 if (long.TryParse(quotaValue, out quota))
340 accountInfo.Quota = quota;
341 if (long.TryParse(bytesValue, out bytes))
342 accountInfo.BytesUsed = bytes;
352 public IList<ObjectInfo> ListObjects(string account, string container, DateTime? since = null)
354 if (String.IsNullOrWhiteSpace(container))
355 throw new ArgumentNullException("container");
356 Contract.EndContractBlock();
358 using (log4net.ThreadContext.Stacks["Objects"].Push("List"))
360 if (Log.IsDebugEnabled) Log.DebugFormat("START");
362 using (var client = new RestClient(_baseClient))
364 if (!String.IsNullOrWhiteSpace(account))
365 client.BaseAddress = GetAccountUrl(account);
367 client.Parameters.Clear();
368 client.Parameters.Add("format", "json");
369 client.IfModifiedSince = since;
370 var content = client.DownloadStringWithRetry(container, 3);
372 client.AssertStatusOK("ListObjects failed");
374 //If the result is empty, return an empty list,
375 var infos = String.IsNullOrWhiteSpace(content)
376 ? new List<ObjectInfo>()
377 //Otherwise deserialize the object list into a list of ObjectInfos
378 : JsonConvert.DeserializeObject<IList<ObjectInfo>>(content);
380 foreach (var info in infos)
382 info.Container = container;
383 info.Account = account;
385 if (Log.IsDebugEnabled) Log.DebugFormat("START");
393 public IList<ObjectInfo> ListObjects(string account, string container, string folder, DateTime? since = null)
395 if (String.IsNullOrWhiteSpace(container))
396 throw new ArgumentNullException("container");
397 if (String.IsNullOrWhiteSpace(folder))
398 throw new ArgumentNullException("folder");
399 Contract.EndContractBlock();
401 using (log4net.ThreadContext.Stacks["Objects"].Push("List"))
403 if (Log.IsDebugEnabled) Log.DebugFormat("START");
405 using (var client = new RestClient(_baseClient))
407 if (!String.IsNullOrWhiteSpace(account))
408 client.BaseAddress = GetAccountUrl(account);
410 client.Parameters.Clear();
411 client.Parameters.Add("format", "json");
412 client.Parameters.Add("path", folder);
413 client.IfModifiedSince = since;
414 var content = client.DownloadStringWithRetry(container, 3);
415 client.AssertStatusOK("ListObjects failed");
417 var infos = JsonConvert.DeserializeObject<IList<ObjectInfo>>(content);
419 if (Log.IsDebugEnabled) Log.DebugFormat("END");
426 public bool ContainerExists(string account, string container)
428 if (String.IsNullOrWhiteSpace(container))
429 throw new ArgumentNullException("container", "The container property can't be empty");
430 Contract.EndContractBlock();
432 using (log4net.ThreadContext.Stacks["Containters"].Push("Exists"))
434 if (Log.IsDebugEnabled) Log.DebugFormat("START");
436 using (var client = new RestClient(_baseClient))
438 if (!String.IsNullOrWhiteSpace(account))
439 client.BaseAddress = GetAccountUrl(account);
441 client.Parameters.Clear();
442 client.Head(container, 3);
445 switch (client.StatusCode)
447 case HttpStatusCode.OK:
448 case HttpStatusCode.NoContent:
451 case HttpStatusCode.NotFound:
455 throw CreateWebException("ContainerExists", client.StatusCode);
457 if (Log.IsDebugEnabled) Log.DebugFormat("END");
465 public bool ObjectExists(string account, string container, string objectName)
467 if (String.IsNullOrWhiteSpace(container))
468 throw new ArgumentNullException("container", "The container property can't be empty");
469 if (String.IsNullOrWhiteSpace(objectName))
470 throw new ArgumentNullException("objectName", "The objectName property can't be empty");
471 Contract.EndContractBlock();
473 using (var client = new RestClient(_baseClient))
475 if (!String.IsNullOrWhiteSpace(account))
476 client.BaseAddress = GetAccountUrl(account);
478 client.Parameters.Clear();
479 client.Head(container + "/" + objectName, 3);
481 switch (client.StatusCode)
483 case HttpStatusCode.OK:
484 case HttpStatusCode.NoContent:
486 case HttpStatusCode.NotFound:
489 throw CreateWebException("ObjectExists", client.StatusCode);
495 public ObjectInfo GetObjectInfo(string account, string container, string objectName)
497 if (String.IsNullOrWhiteSpace(container))
498 throw new ArgumentNullException("container", "The container property can't be empty");
499 if (String.IsNullOrWhiteSpace(objectName))
500 throw new ArgumentNullException("objectName", "The objectName property can't be empty");
501 Contract.EndContractBlock();
503 using (log4net.ThreadContext.Stacks["Objects"].Push("GetObjectInfo"))
506 using (var client = new RestClient(_baseClient))
508 if (!String.IsNullOrWhiteSpace(account))
509 client.BaseAddress = GetAccountUrl(account);
512 client.Parameters.Clear();
514 client.Head(container + "/" + objectName, 3);
517 return ObjectInfo.Empty;
519 switch (client.StatusCode)
521 case HttpStatusCode.OK:
522 case HttpStatusCode.NoContent:
523 var keys = client.ResponseHeaders.AllKeys.AsQueryable();
524 var tags = (from key in keys
525 where key.StartsWith("X-Object-Meta-")
526 let name = key.Substring(14)
527 select new {Name = name, Value = client.ResponseHeaders[name]})
528 .ToDictionary(t => t.Name, t => t.Value);
529 var extensions = (from key in keys
530 where key.StartsWith("X-Object-") && !key.StartsWith("X-Object-Meta-")
531 select new {Name = key, Value = client.ResponseHeaders[key]})
532 .ToDictionary(t => t.Name, t => t.Value);
533 var info = new ObjectInfo
536 Hash = client.GetHeaderValue("ETag"),
537 Content_Type = client.GetHeaderValue("Content-Type"),
539 Last_Modified = client.LastModified,
540 Extensions = extensions
543 case HttpStatusCode.NotFound:
544 return ObjectInfo.Empty;
546 throw new WebException(
547 String.Format("[FAIL] GetObjectInfo for {0} failed with unexpected status code {1}",
548 objectName, client.StatusCode));
552 catch (RetryException)
554 Log.WarnFormat("[RETRY FAIL] GetObjectInfo for {0} failed.");
555 return ObjectInfo.Empty;
557 catch (WebException e)
560 String.Format("[FAIL] GetObjectInfo for {0} failed with unexpected status code {1}",
561 objectName, client.StatusCode), e);
569 public void CreateFolder(string account, string container, string folder)
571 if (String.IsNullOrWhiteSpace(container))
572 throw new ArgumentNullException("container", "The container property can't be empty");
573 if (String.IsNullOrWhiteSpace(folder))
574 throw new ArgumentNullException("folder", "The folder property can't be empty");
575 Contract.EndContractBlock();
577 var folderUrl=String.Format("{0}/{1}",container,folder);
578 using (var client = new RestClient(_baseClient))
580 if (!String.IsNullOrWhiteSpace(account))
581 client.BaseAddress = GetAccountUrl(account);
583 client.Parameters.Clear();
584 client.Headers.Add("Content-Type", @"application/directory");
585 client.Headers.Add("Content-Length", "0");
586 client.PutWithRetry(folderUrl, 3);
588 if (client.StatusCode != HttpStatusCode.Created && client.StatusCode != HttpStatusCode.Accepted)
589 throw CreateWebException("CreateFolder", client.StatusCode);
593 public ContainerInfo GetContainerInfo(string account, string container)
595 if (String.IsNullOrWhiteSpace(container))
596 throw new ArgumentNullException("container", "The container property can't be empty");
597 Contract.EndContractBlock();
599 using (var client = new RestClient(_baseClient))
601 if (!String.IsNullOrWhiteSpace(account))
602 client.BaseAddress = GetAccountUrl(account);
604 client.Head(container);
605 switch (client.StatusCode)
607 case HttpStatusCode.OK:
608 case HttpStatusCode.NoContent:
609 var containerInfo = new ContainerInfo
613 long.Parse(client.GetHeaderValue("X-Container-Object-Count")),
614 Bytes = long.Parse(client.GetHeaderValue("X-Container-Bytes-Used")),
615 BlockHash = client.GetHeaderValue("X-Container-Block-Hash"),
616 BlockSize=int.Parse(client.GetHeaderValue("X-Container-Block-Size"))
618 return containerInfo;
619 case HttpStatusCode.NotFound:
620 return ContainerInfo.Empty;
622 throw CreateWebException("GetContainerInfo", client.StatusCode);
627 public void CreateContainer(string account, string container)
629 if (String.IsNullOrWhiteSpace(container))
630 throw new ArgumentNullException("container", "The container property can't be empty");
631 Contract.EndContractBlock();
633 using (var client = new RestClient(_baseClient))
635 if (!String.IsNullOrWhiteSpace(account))
636 client.BaseAddress = GetAccountUrl(account);
638 client.PutWithRetry(container, 3);
639 var expectedCodes = new[] {HttpStatusCode.Created, HttpStatusCode.Accepted, HttpStatusCode.OK};
640 if (!expectedCodes.Contains(client.StatusCode))
641 throw CreateWebException("CreateContainer", client.StatusCode);
645 public void DeleteContainer(string account, string container)
647 if (String.IsNullOrWhiteSpace(container))
648 throw new ArgumentNullException("container", "The container property can't be empty");
649 Contract.EndContractBlock();
651 using (var client = new RestClient(_baseClient))
653 if (!String.IsNullOrWhiteSpace(account))
654 client.BaseAddress = GetAccountUrl(account);
656 client.DeleteWithRetry(container, 3);
657 var expectedCodes = new[] {HttpStatusCode.NotFound, HttpStatusCode.NoContent};
658 if (!expectedCodes.Contains(client.StatusCode))
659 throw CreateWebException("DeleteContainer", client.StatusCode);
667 /// <param name="account"></param>
668 /// <param name="container"></param>
669 /// <param name="objectName"></param>
670 /// <param name="fileName"></param>
671 /// <returns></returns>
672 /// <remarks>This method should have no timeout or a very long one</remarks>
673 //Asynchronously download the object specified by *objectName* in a specific *container* to
675 public Task GetObject(string account, string container, string objectName, string fileName)
677 if (String.IsNullOrWhiteSpace(container))
678 throw new ArgumentNullException("container", "The container property can't be empty");
679 if (String.IsNullOrWhiteSpace(objectName))
680 throw new ArgumentNullException("objectName", "The objectName property can't be empty");
681 Contract.EndContractBlock();
685 //WebClient, and by extension RestClient, are not thread-safe. Create a new RestClient
686 //object to avoid concurrency errors.
688 //Download operations take a long time therefore they have no timeout.
689 var client = new RestClient(_baseClient) { Timeout = 0 };
690 if (!String.IsNullOrWhiteSpace(account))
691 client.BaseAddress = GetAccountUrl(account);
693 //The container and objectName are relative names. They are joined with the client's
694 //BaseAddress to create the object's absolute address
695 var builder = client.GetAddressBuilder(container, objectName);
696 var uri = builder.Uri;
698 //Download progress is reported to the Trace log
699 Log.InfoFormat("[GET] START {0}", objectName);
700 client.DownloadProgressChanged += (sender, args) =>
701 Log.InfoFormat("[GET PROGRESS] {0} {1}% {2} of {3}",
702 fileName, args.ProgressPercentage,
704 args.TotalBytesToReceive);
707 //Start downloading the object asynchronously
708 var downloadTask = client.DownloadFileTask(uri, fileName);
710 //Once the download completes
711 return downloadTask.ContinueWith(download =>
713 //Delete the local client object
715 //And report failure or completion
716 if (download.IsFaulted)
718 Log.ErrorFormat("[GET] FAIL for {0} with \r{1}", objectName,
723 Log.InfoFormat("[GET] END {0}", objectName);
727 catch (Exception exc)
729 Log.ErrorFormat("[GET] END {0} with {1}", objectName, exc);
737 public Task<IList<string>> PutHashMap(string account, string container, string objectName, TreeHash hash)
739 if (String.IsNullOrWhiteSpace(container))
740 throw new ArgumentNullException("container");
741 if (String.IsNullOrWhiteSpace(objectName))
742 throw new ArgumentNullException("objectName");
744 throw new ArgumentNullException("hash");
745 if (String.IsNullOrWhiteSpace(Token))
746 throw new InvalidOperationException("Invalid Token");
747 if (StorageUrl == null)
748 throw new InvalidOperationException("Invalid Storage Url");
749 Contract.EndContractBlock();
752 //Don't use a timeout because putting the hashmap may be a long process
753 var client = new RestClient(_baseClient) { Timeout = 0 };
754 if (!String.IsNullOrWhiteSpace(account))
755 client.BaseAddress = GetAccountUrl(account);
757 //The container and objectName are relative names. They are joined with the client's
758 //BaseAddress to create the object's absolute address
759 var builder = client.GetAddressBuilder(container, objectName);
760 builder.Query = "format=json&hashmap";
761 var uri = builder.Uri;
764 //Send the tree hash as Json to the server
765 client.Headers[HttpRequestHeader.ContentType] = "application/octet-stream";
766 var uploadTask=client.UploadStringTask(uri, "PUT", hash.ToJson());
769 return uploadTask.ContinueWith(t =>
772 var empty = (IList<string>)new List<string>();
775 //The server will respond either with 201-created if all blocks were already on the server
776 if (client.StatusCode == HttpStatusCode.Created)
778 //in which case we return an empty hash list
781 //or with a 409-conflict and return the list of missing parts
782 //A 409 will cause an exception so we need to check t.IsFaulted to avoid propagating the exception
785 var ex = t.Exception.InnerException;
786 var we = ex as WebException;
787 var response = we.Response as HttpWebResponse;
788 if (response!=null && response.StatusCode==HttpStatusCode.Conflict)
790 //In case of 409 the missing parts will be in the response content
791 using (var stream = response.GetResponseStream())
792 using(var reader=new StreamReader(stream))
794 //We need to cleanup the content before returning it because it contains
795 //error content after the list of hashes
796 var hashes = new List<string>();
798 //All lines up to the first empty line are hashes
799 while(!String.IsNullOrWhiteSpace(line=reader.ReadLine()))
808 //Any other status code is unexpected and the exception should be rethrown
812 //Any other status code is unexpected but there was no exception. We can probably continue processing
815 Log.WarnFormat("Unexcpected status code when putting map: {0} - {1}",client.StatusCode,client.StatusDescription);
822 public Task<byte[]> GetBlock(string account, string container, Uri relativeUrl, long start, long? end)
824 if (String.IsNullOrWhiteSpace(Token))
825 throw new InvalidOperationException("Invalid Token");
826 if (StorageUrl == null)
827 throw new InvalidOperationException("Invalid Storage Url");
828 if (String.IsNullOrWhiteSpace(container))
829 throw new ArgumentNullException("container");
830 if (relativeUrl== null)
831 throw new ArgumentNullException("relativeUrl");
832 if (end.HasValue && end<0)
833 throw new ArgumentOutOfRangeException("end");
835 throw new ArgumentOutOfRangeException("start");
836 Contract.EndContractBlock();
839 //Don't use a timeout because putting the hashmap may be a long process
840 var client = new RestClient(_baseClient) {Timeout = 0, RangeFrom = start, RangeTo = end};
841 if (!String.IsNullOrWhiteSpace(account))
842 client.BaseAddress = GetAccountUrl(account);
844 var builder = client.GetAddressBuilder(container, relativeUrl.ToString());
845 var uri = builder.Uri;
847 return client.DownloadDataTask(uri)
856 public Task PostBlock(string account, string container, byte[] block, int offset, int count)
858 if (String.IsNullOrWhiteSpace(container))
859 throw new ArgumentNullException("container");
861 throw new ArgumentNullException("block");
862 if (offset < 0 || offset >= block.Length)
863 throw new ArgumentOutOfRangeException("offset");
864 if (count < 0 || count > block.Length)
865 throw new ArgumentOutOfRangeException("count");
866 if (String.IsNullOrWhiteSpace(Token))
867 throw new InvalidOperationException("Invalid Token");
868 if (StorageUrl == null)
869 throw new InvalidOperationException("Invalid Storage Url");
870 Contract.EndContractBlock();
873 //Don't use a timeout because putting the hashmap may be a long process
874 var client = new RestClient(_baseClient) { Timeout = 0 };
875 if (!String.IsNullOrWhiteSpace(account))
876 client.BaseAddress = GetAccountUrl(account);
878 var builder = client.GetAddressBuilder(container, "");
879 //We are doing an update
880 builder.Query = "update";
881 var uri = builder.Uri;
883 client.Headers[HttpRequestHeader.ContentType] = "application/octet-stream";
885 Log.InfoFormat("[BLOCK POST] START");
887 client.UploadProgressChanged += (sender, args) =>
888 Log.InfoFormat("[BLOCK POST PROGRESS] {0}% {1} of {2}",
889 args.ProgressPercentage, args.BytesSent,
890 args.TotalBytesToSend);
891 client.UploadFileCompleted += (sender, args) =>
892 Log.InfoFormat("[BLOCK POST PROGRESS] Completed ");
896 var uploadTask = client.UploadDataTask(uri, "POST", block)
897 .ContinueWith(upload =>
901 if (upload.IsFaulted)
903 var exception = upload.Exception.InnerException;
904 Log.ErrorFormat("[BLOCK POST] FAIL with \r{0}", exception);
908 Log.InfoFormat("[BLOCK POST] END");
914 public Task<TreeHash> GetHashMap(string account, string container, string objectName)
916 if (String.IsNullOrWhiteSpace(container))
917 throw new ArgumentNullException("container");
918 if (String.IsNullOrWhiteSpace(objectName))
919 throw new ArgumentNullException("objectName");
920 if (String.IsNullOrWhiteSpace(Token))
921 throw new InvalidOperationException("Invalid Token");
922 if (StorageUrl == null)
923 throw new InvalidOperationException("Invalid Storage Url");
924 Contract.EndContractBlock();
928 //WebClient, and by extension RestClient, are not thread-safe. Create a new RestClient
929 //object to avoid concurrency errors.
931 //Download operations take a long time therefore they have no timeout.
932 //TODO: Do we really? this is a hashmap operation, not a download
933 var client = new RestClient(_baseClient) { Timeout = 0 };
934 if (!String.IsNullOrWhiteSpace(account))
935 client.BaseAddress = GetAccountUrl(account);
938 //The container and objectName are relative names. They are joined with the client's
939 //BaseAddress to create the object's absolute address
940 var builder = client.GetAddressBuilder(container, objectName);
941 builder.Query = "format=json&hashmap";
942 var uri = builder.Uri;
944 //Start downloading the object asynchronously
945 var downloadTask = client.DownloadStringTask(uri);
947 //Once the download completes
948 return downloadTask.ContinueWith(download =>
950 //Delete the local client object
952 //And report failure or completion
953 if (download.IsFaulted)
955 Log.ErrorFormat("[GET HASH] FAIL for {0} with \r{1}", objectName,
957 throw download.Exception;
960 //The server will return an empty string if the file is empty
961 var json = download.Result;
962 var treeHash = TreeHash.Parse(json);
963 Log.InfoFormat("[GET HASH] END {0}", objectName);
967 catch (Exception exc)
969 Log.ErrorFormat("[GET HASH] END {0} with {1}", objectName, exc);
981 /// <param name="account"></param>
982 /// <param name="container"></param>
983 /// <param name="objectName"></param>
984 /// <param name="fileName"></param>
985 /// <param name="hash">Optional hash value for the file. If no hash is provided, the method calculates a new hash</param>
986 /// <remarks>>This method should have no timeout or a very long one</remarks>
987 public Task PutObject(string account, string container, string objectName, string fileName, string hash = null)
989 if (String.IsNullOrWhiteSpace(container))
990 throw new ArgumentNullException("container", "The container property can't be empty");
991 if (String.IsNullOrWhiteSpace(objectName))
992 throw new ArgumentNullException("objectName", "The objectName property can't be empty");
993 if (String.IsNullOrWhiteSpace(fileName))
994 throw new ArgumentNullException("fileName", "The fileName property can't be empty");
995 if (!File.Exists(fileName))
996 throw new FileNotFoundException("The file does not exist",fileName);
997 Contract.EndContractBlock();
1002 var client = new RestClient(_baseClient){Timeout=0};
1003 if (!String.IsNullOrWhiteSpace(account))
1004 client.BaseAddress = GetAccountUrl(account);
1006 var builder = client.GetAddressBuilder(container, objectName);
1007 var uri = builder.Uri;
1009 string etag = hash ?? CalculateHash(fileName);
1011 client.Headers.Add("Content-Type", "application/octet-stream");
1012 client.Headers.Add("ETag", etag);
1015 Log.InfoFormat("[PUT] START {0}", objectName);
1016 client.UploadProgressChanged += (sender, args) =>
1018 using (log4net.ThreadContext.Stacks["PUT"].Push("Progress"))
1020 Log.InfoFormat("{0} {1}% {2} of {3}", fileName, args.ProgressPercentage,
1021 args.BytesSent, args.TotalBytesToSend);
1025 client.UploadFileCompleted += (sender, args) =>
1027 using (log4net.ThreadContext.Stacks["PUT"].Push("Progress"))
1029 Log.InfoFormat("Completed {0}", fileName);
1032 return client.UploadFileTask(uri, "PUT", fileName)
1033 .ContinueWith(upload=>
1037 if (upload.IsFaulted)
1039 var exc = upload.Exception.InnerException;
1040 Log.ErrorFormat("[PUT] FAIL for {0} with \r{1}",objectName,exc);
1044 Log.InfoFormat("[PUT] END {0}", objectName);
1047 catch (Exception exc)
1049 Log.ErrorFormat("[PUT] END {0} with {1}", objectName, exc);
1056 private static string CalculateHash(string fileName)
1058 Contract.Requires(!String.IsNullOrWhiteSpace(fileName));
1059 Contract.EndContractBlock();
1062 using (var hasher = MD5.Create())
1063 using(var stream=File.OpenRead(fileName))
1065 var hashBuilder=new StringBuilder();
1066 foreach (byte b in hasher.ComputeHash(stream))
1067 hashBuilder.Append(b.ToString("x2").ToLower());
1068 hash = hashBuilder.ToString();
1073 public void MoveObject(string account, string sourceContainer, string oldObjectName, string targetContainer, string newObjectName)
1075 if (String.IsNullOrWhiteSpace(sourceContainer))
1076 throw new ArgumentNullException("sourceContainer", "The container property can't be empty");
1077 if (String.IsNullOrWhiteSpace(oldObjectName))
1078 throw new ArgumentNullException("oldObjectName", "The oldObjectName property can't be empty");
1079 if (String.IsNullOrWhiteSpace(targetContainer))
1080 throw new ArgumentNullException("targetContainer", "The container property can't be empty");
1081 if (String.IsNullOrWhiteSpace(newObjectName))
1082 throw new ArgumentNullException("newObjectName", "The newObjectName property can't be empty");
1083 Contract.EndContractBlock();
1085 var targetUrl = targetContainer + "/" + newObjectName;
1086 var sourceUrl = String.Format("/{0}/{1}", sourceContainer, oldObjectName);
1088 using (var client = new RestClient(_baseClient))
1090 if (!String.IsNullOrWhiteSpace(account))
1091 client.BaseAddress = GetAccountUrl(account);
1093 client.Headers.Add("X-Move-From", sourceUrl);
1094 client.PutWithRetry(targetUrl, 3);
1096 var expectedCodes = new[] {HttpStatusCode.OK, HttpStatusCode.NoContent, HttpStatusCode.Created};
1097 if (!expectedCodes.Contains(client.StatusCode))
1098 throw CreateWebException("MoveObject", client.StatusCode);
1102 public void DeleteObject(string account, string sourceContainer, string objectName)
1104 if (String.IsNullOrWhiteSpace(sourceContainer))
1105 throw new ArgumentNullException("sourceContainer", "The container property can't be empty");
1106 if (String.IsNullOrWhiteSpace(objectName))
1107 throw new ArgumentNullException("objectName", "The oldObjectName property can't be empty");
1108 Contract.EndContractBlock();
1110 var targetUrl = FolderConstants.TrashContainer + "/" + objectName;
1111 var sourceUrl = String.Format("/{0}/{1}", sourceContainer, objectName);
1113 using (var client = new RestClient(_baseClient))
1115 if (!String.IsNullOrWhiteSpace(account))
1116 client.BaseAddress = GetAccountUrl(account);
1118 client.Headers.Add("X-Move-From", sourceUrl);
1119 client.AllowedStatusCodes.Add(HttpStatusCode.NotFound);
1120 client.PutWithRetry(targetUrl, 3);
1122 var expectedCodes = new[] {HttpStatusCode.OK, HttpStatusCode.NoContent, HttpStatusCode.Created,HttpStatusCode.NotFound};
1123 if (!expectedCodes.Contains(client.StatusCode))
1124 throw CreateWebException("DeleteObject", client.StatusCode);
1129 private static WebException CreateWebException(string operation, HttpStatusCode statusCode)
1131 return new WebException(String.Format("{0} failed with unexpected status code {1}", operation, statusCode));
1137 public class ShareAccountInfo
1139 public DateTime? last_modified { get; set; }
1140 public string name { get; set; }