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;
22 using WebHeaderCollection = System.Net.WebHeaderCollection;
24 namespace Pithos.Network
26 [Export(typeof(ICloudClient))]
27 public class CloudFilesClient:ICloudClient
29 //CloudFilesClient uses *_baseClient* internally to communicate with the server
30 //RestClient provides a REST-friendly interface over the standard WebClient.
31 private RestClient _baseClient;
33 //During authentication the client provides a UserName
34 public string UserName { get; set; }
36 //and and ApiKey to the server
37 public string ApiKey { get; set; }
39 //And receives an authentication Token. This token must be provided in ALL other operations,
40 //in the X-Auth-Token header
41 public string Token { get; set; }
43 //The client also receives a StorageUrl after authentication. All subsequent operations must
45 public Uri StorageUrl { get; set; }
47 protected Uri RootAddressUri { get; set; }
49 public Uri Proxy { get; set; }
51 public double DownloadPercentLimit { get; set; }
52 public double UploadPercentLimit { get; set; }
54 public string AuthenticationUrl { get; set; }
57 public string VersionPath
59 get { return UsePithos ? "v1" : "v1.0"; }
62 public bool UsePithos { get; set; }
64 private bool _authenticated = false;
66 private static readonly ILog Log = LogManager.GetLogger("CloudFilesClient");
69 public void Authenticate(string userName,string apiKey)
71 if (String.IsNullOrWhiteSpace(userName))
72 throw new ArgumentNullException("userName", "The userName property can't be empty");
73 if (String.IsNullOrWhiteSpace(apiKey))
74 throw new ArgumentNullException("apiKey", "The apiKey property can't be empty");
75 Contract.Ensures(_baseClient != null);
76 Contract.EndContractBlock();
78 Log.InfoFormat("[AUTHENTICATE] Start for {0}", userName);
87 using (var authClient = new RestClient{BaseAddress=AuthenticationUrl})
90 authClient.Proxy = new WebProxy(Proxy);
92 Contract.Assume(authClient.Headers!=null);
94 authClient.Headers.Add("X-Auth-User", UserName);
95 authClient.Headers.Add("X-Auth-Key", ApiKey);
97 authClient.DownloadStringWithRetry(VersionPath, 3);
99 authClient.AssertStatusOK("Authentication failed");
101 var storageUrl = authClient.GetHeaderValue("X-Storage-Url");
102 if (String.IsNullOrWhiteSpace(storageUrl))
103 throw new InvalidOperationException("Failed to obtain storage url");
104 StorageUrl = new Uri(storageUrl);
106 //Get the root address (StorageUrl without the account)
107 var usernameIndex=storageUrl.LastIndexOf(UserName);
108 var rootUrl = storageUrl.Substring(0, usernameIndex);
109 RootAddressUri = new Uri(rootUrl);
111 var token = authClient.GetHeaderValue("X-Auth-Token");
112 if (String.IsNullOrWhiteSpace(token))
113 throw new InvalidOperationException("Failed to obtain token url");
117 _baseClient = new RestClient{
118 BaseAddress = StorageUrl.AbsoluteUri,
122 _baseClient.Proxy = new WebProxy(Proxy);
126 Contract.Assume(_baseClient.Headers!=null);
127 _baseClient.Headers.Add("X-Auth-Token", Token);
129 Log.InfoFormat("[AUTHENTICATE] End for {0}", userName);
134 public IList<ContainerInfo> ListContainers(string account)
137 using (var client = new RestClient(_baseClient))
139 if (!String.IsNullOrWhiteSpace(account))
140 client.BaseAddress = GetAccountUrl(account);
142 client.Parameters.Clear();
143 client.Parameters.Add("format", "json");
144 var content = client.DownloadStringWithRetry("", 3);
145 client.AssertStatusOK("List Containers failed");
147 if (client.StatusCode == HttpStatusCode.NoContent)
148 return new List<ContainerInfo>();
149 var infos = JsonConvert.DeserializeObject<IList<ContainerInfo>>(content);
155 private string GetAccountUrl(string account)
157 return new Uri(this.RootAddressUri, new Uri(account,UriKind.Relative)).AbsoluteUri;
160 public IList<ShareAccountInfo> ListSharingAccounts(DateTime? since=null)
162 using (log4net.ThreadContext.Stacks["Share"].Push("List Accounts"))
164 if (Log.IsDebugEnabled) Log.DebugFormat("START");
166 using (var client = new RestClient(_baseClient))
168 client.Parameters.Clear();
169 client.Parameters.Add("format", "json");
170 client.IfModifiedSince = since;
172 //Extract the username from the base address
173 client.BaseAddress = RootAddressUri.AbsoluteUri;
175 var content = client.DownloadStringWithRetry(@"", 3);
177 client.AssertStatusOK("ListSharingAccounts failed");
179 //If the result is empty, return an empty list,
180 var infos = String.IsNullOrWhiteSpace(content)
181 ? new List<ShareAccountInfo>()
182 //Otherwise deserialize the account list into a list of ShareAccountInfos
183 : JsonConvert.DeserializeObject<IList<ShareAccountInfo>>(content);
185 Log.DebugFormat("END");
191 //Request listing of all objects in a container modified since a specific time.
192 //If the *since* value is missing, return all objects
193 public IList<ObjectInfo> ListSharedObjects(DateTime? since = null)
196 using (log4net.ThreadContext.Stacks["Share"].Push("List Objects"))
198 if (Log.IsDebugEnabled) Log.DebugFormat("START");
200 var objects = new List<ObjectInfo>();
201 var accounts = ListSharingAccounts(since);
202 foreach (var account in accounts)
204 var containers = ListContainers(account.name);
205 foreach (var container in containers)
207 var containerObjects = ListObjects(account.name, container.Name, account.last_modified);
208 objects.AddRange(containerObjects);
211 if (Log.IsDebugEnabled) Log.DebugFormat("END");
216 public void ShareObject(string account, string container, string objectName, string shareTo, bool read, bool write)
218 if (String.IsNullOrWhiteSpace(Token))
219 throw new InvalidOperationException("The Token is not set");
220 if (StorageUrl==null)
221 throw new InvalidOperationException("The StorageUrl is not set");
222 if (String.IsNullOrWhiteSpace(container))
223 throw new ArgumentNullException("container");
224 if (String.IsNullOrWhiteSpace(objectName))
225 throw new ArgumentNullException("objectName");
226 if (String.IsNullOrWhiteSpace(account))
227 throw new ArgumentNullException("account");
228 if (String.IsNullOrWhiteSpace(shareTo))
229 throw new ArgumentNullException("shareTo");
230 Contract.EndContractBlock();
232 using (log4net.ThreadContext.Stacks["Share"].Push("Share Object"))
234 if (Log.IsDebugEnabled) Log.DebugFormat("START");
236 using (var client = new RestClient(_baseClient))
239 client.BaseAddress = GetAccountUrl(account);
241 client.Parameters.Clear();
242 client.Parameters.Add("format", "json");
244 string permission = "";
246 permission = String.Format("write={0}", shareTo);
248 permission = String.Format("read={0}", shareTo);
249 client.Headers.Add("X-Object-Sharing", permission);
251 var content = client.DownloadStringWithRetry(container, 3);
253 client.AssertStatusOK("ShareObject failed");
255 //If the result is empty, return an empty list,
256 var infos = String.IsNullOrWhiteSpace(content)
257 ? new List<ObjectInfo>()
258 //Otherwise deserialize the object list into a list of ObjectInfos
259 : JsonConvert.DeserializeObject<IList<ObjectInfo>>(content);
261 if (Log.IsDebugEnabled) Log.DebugFormat("END");
269 public IList<ObjectInfo> ListObjects(string account, string container, DateTime? since = null)
271 if (String.IsNullOrWhiteSpace(container))
272 throw new ArgumentNullException("container");
273 Contract.EndContractBlock();
275 using (log4net.ThreadContext.Stacks["Objects"].Push("List"))
277 if (Log.IsDebugEnabled) Log.DebugFormat("START");
279 using (var client = new RestClient(_baseClient))
281 if (!String.IsNullOrWhiteSpace(account))
282 client.BaseAddress = GetAccountUrl(account);
284 client.Parameters.Clear();
285 client.Parameters.Add("format", "json");
286 client.IfModifiedSince = since;
287 var content = client.DownloadStringWithRetry(container, 3);
289 client.AssertStatusOK("ListObjects failed");
291 //If the result is empty, return an empty list,
292 var infos = String.IsNullOrWhiteSpace(content)
293 ? new List<ObjectInfo>()
294 //Otherwise deserialize the object list into a list of ObjectInfos
295 : JsonConvert.DeserializeObject<IList<ObjectInfo>>(content);
297 foreach (var info in infos)
299 info.Container = container;
300 info.Account = account;
302 if (Log.IsDebugEnabled) Log.DebugFormat("START");
310 public IList<ObjectInfo> ListObjects(string account, string container, string folder, DateTime? since = null)
312 if (String.IsNullOrWhiteSpace(container))
313 throw new ArgumentNullException("container");
314 if (String.IsNullOrWhiteSpace(folder))
315 throw new ArgumentNullException("folder");
316 Contract.EndContractBlock();
318 using (log4net.ThreadContext.Stacks["Objects"].Push("List"))
320 if (Log.IsDebugEnabled) Log.DebugFormat("START");
322 using (var client = new RestClient(_baseClient))
324 if (!String.IsNullOrWhiteSpace(account))
325 client.BaseAddress = GetAccountUrl(account);
327 client.Parameters.Clear();
328 client.Parameters.Add("format", "json");
329 client.Parameters.Add("path", folder);
330 client.IfModifiedSince = since;
331 var content = client.DownloadStringWithRetry(container, 3);
332 client.AssertStatusOK("ListObjects failed");
334 var infos = JsonConvert.DeserializeObject<IList<ObjectInfo>>(content);
336 if (Log.IsDebugEnabled) Log.DebugFormat("END");
343 public bool ContainerExists(string account, string container)
345 if (String.IsNullOrWhiteSpace(container))
346 throw new ArgumentNullException("container", "The container property can't be empty");
347 Contract.EndContractBlock();
349 using (log4net.ThreadContext.Stacks["Containters"].Push("Exists"))
351 if (Log.IsDebugEnabled) Log.DebugFormat("START");
353 using (var client = new RestClient(_baseClient))
355 if (!String.IsNullOrWhiteSpace(account))
356 client.BaseAddress = GetAccountUrl(account);
358 client.Parameters.Clear();
359 client.Head(container, 3);
362 switch (client.StatusCode)
364 case HttpStatusCode.OK:
365 case HttpStatusCode.NoContent:
368 case HttpStatusCode.NotFound:
372 throw CreateWebException("ContainerExists", client.StatusCode);
374 if (Log.IsDebugEnabled) Log.DebugFormat("END");
382 public bool ObjectExists(string account, string container, string objectName)
384 if (String.IsNullOrWhiteSpace(container))
385 throw new ArgumentNullException("container", "The container property can't be empty");
386 if (String.IsNullOrWhiteSpace(objectName))
387 throw new ArgumentNullException("objectName", "The objectName property can't be empty");
388 Contract.EndContractBlock();
390 using (var client = new RestClient(_baseClient))
392 if (!String.IsNullOrWhiteSpace(account))
393 client.BaseAddress = GetAccountUrl(account);
395 client.Parameters.Clear();
396 client.Head(container + "/" + objectName, 3);
398 switch (client.StatusCode)
400 case HttpStatusCode.OK:
401 case HttpStatusCode.NoContent:
403 case HttpStatusCode.NotFound:
406 throw CreateWebException("ObjectExists", client.StatusCode);
412 public ObjectInfo GetObjectInfo(string account, string container, string objectName)
414 if (String.IsNullOrWhiteSpace(container))
415 throw new ArgumentNullException("container", "The container property can't be empty");
416 if (String.IsNullOrWhiteSpace(objectName))
417 throw new ArgumentNullException("objectName", "The objectName property can't be empty");
418 Contract.EndContractBlock();
420 using (log4net.ThreadContext.Stacks["Objects"].Push("GetObjectInfo"))
423 using (var client = new RestClient(_baseClient))
425 if (!String.IsNullOrWhiteSpace(account))
426 client.BaseAddress = GetAccountUrl(account);
429 client.Parameters.Clear();
431 client.Head(container + "/" + objectName, 3);
434 return ObjectInfo.Empty;
436 switch (client.StatusCode)
438 case HttpStatusCode.OK:
439 case HttpStatusCode.NoContent:
440 var keys = client.ResponseHeaders.AllKeys.AsQueryable();
441 var tags = (from key in keys
442 where key.StartsWith("X-Object-Meta-")
443 let name = key.Substring(14)
444 select new {Name = name, Value = client.ResponseHeaders[name]})
445 .ToDictionary(t => t.Name, t => t.Value);
446 var extensions = (from key in keys
447 where key.StartsWith("X-Object-") && !key.StartsWith("X-Object-Meta-")
448 select new {Name = key, Value = client.ResponseHeaders[key]})
449 .ToDictionary(t => t.Name, t => t.Value);
450 var info = new ObjectInfo
453 Hash = client.GetHeaderValue("ETag"),
454 Content_Type = client.GetHeaderValue("Content-Type"),
456 Last_Modified = client.LastModified,
457 Extensions = extensions
460 case HttpStatusCode.NotFound:
461 return ObjectInfo.Empty;
463 throw new WebException(
464 String.Format("[FAIL] GetObjectInfo for {0} failed with unexpected status code {1}",
465 objectName, client.StatusCode));
469 catch (RetryException)
471 Log.WarnFormat("[RETRY FAIL] GetObjectInfo for {0} failed.");
472 return ObjectInfo.Empty;
474 catch (WebException e)
477 String.Format("[FAIL] GetObjectInfo for {0} failed with unexpected status code {1}",
478 objectName, client.StatusCode), e);
486 public void CreateFolder(string account, string container, string folder)
488 if (String.IsNullOrWhiteSpace(container))
489 throw new ArgumentNullException("container", "The container property can't be empty");
490 if (String.IsNullOrWhiteSpace(folder))
491 throw new ArgumentNullException("folder", "The folder property can't be empty");
492 Contract.EndContractBlock();
494 var folderUrl=String.Format("{0}/{1}",container,folder);
495 using (var client = new RestClient(_baseClient))
497 if (!String.IsNullOrWhiteSpace(account))
498 client.BaseAddress = GetAccountUrl(account);
500 client.Parameters.Clear();
501 client.Headers.Add("Content-Type", @"application/directory");
502 client.Headers.Add("Content-Length", "0");
503 client.PutWithRetry(folderUrl, 3);
505 if (client.StatusCode != HttpStatusCode.Created && client.StatusCode != HttpStatusCode.Accepted)
506 throw CreateWebException("CreateFolder", client.StatusCode);
510 public ContainerInfo GetContainerInfo(string account, string container)
512 if (String.IsNullOrWhiteSpace(container))
513 throw new ArgumentNullException("container", "The container property can't be empty");
514 Contract.EndContractBlock();
516 using (var client = new RestClient(_baseClient))
518 if (!String.IsNullOrWhiteSpace(account))
519 client.BaseAddress = GetAccountUrl(account);
521 client.Head(container);
522 switch (client.StatusCode)
524 case HttpStatusCode.OK:
525 case HttpStatusCode.NoContent:
526 var containerInfo = new ContainerInfo
530 long.Parse(client.GetHeaderValue("X-Container-Object-Count")),
531 Bytes = long.Parse(client.GetHeaderValue("X-Container-Bytes-Used")),
532 BlockHash = client.GetHeaderValue("X-Container-Block-Hash"),
533 BlockSize=int.Parse(client.GetHeaderValue("X-Container-Block-Size"))
535 return containerInfo;
536 case HttpStatusCode.NotFound:
537 return ContainerInfo.Empty;
539 throw CreateWebException("GetContainerInfo", client.StatusCode);
544 public void CreateContainer(string account, string container)
546 if (String.IsNullOrWhiteSpace(container))
547 throw new ArgumentNullException("container", "The container property can't be empty");
548 Contract.EndContractBlock();
550 using (var client = new RestClient(_baseClient))
552 if (!String.IsNullOrWhiteSpace(account))
553 client.BaseAddress = GetAccountUrl(account);
555 client.PutWithRetry(container, 3);
556 var expectedCodes = new[] {HttpStatusCode.Created, HttpStatusCode.Accepted, HttpStatusCode.OK};
557 if (!expectedCodes.Contains(client.StatusCode))
558 throw CreateWebException("CreateContainer", client.StatusCode);
562 public void DeleteContainer(string account, string container)
564 if (String.IsNullOrWhiteSpace(container))
565 throw new ArgumentNullException("container", "The container property can't be empty");
566 Contract.EndContractBlock();
568 using (var client = new RestClient(_baseClient))
570 if (!String.IsNullOrWhiteSpace(account))
571 client.BaseAddress = GetAccountUrl(account);
573 client.DeleteWithRetry(container, 3);
574 var expectedCodes = new[] {HttpStatusCode.NotFound, HttpStatusCode.NoContent};
575 if (!expectedCodes.Contains(client.StatusCode))
576 throw CreateWebException("DeleteContainer", client.StatusCode);
584 /// <param name="account"></param>
585 /// <param name="container"></param>
586 /// <param name="objectName"></param>
587 /// <param name="fileName"></param>
588 /// <returns></returns>
589 /// <remarks>This method should have no timeout or a very long one</remarks>
590 //Asynchronously download the object specified by *objectName* in a specific *container* to
592 public Task GetObject(string account, string container, string objectName, string fileName)
594 if (String.IsNullOrWhiteSpace(container))
595 throw new ArgumentNullException("container", "The container property can't be empty");
596 if (String.IsNullOrWhiteSpace(objectName))
597 throw new ArgumentNullException("objectName", "The objectName property can't be empty");
598 Contract.EndContractBlock();
602 //WebClient, and by extension RestClient, are not thread-safe. Create a new RestClient
603 //object to avoid concurrency errors.
605 //Download operations take a long time therefore they have no timeout.
606 var client = new RestClient(_baseClient) { Timeout = 0 };
607 if (!String.IsNullOrWhiteSpace(account))
608 client.BaseAddress = GetAccountUrl(account);
610 //The container and objectName are relative names. They are joined with the client's
611 //BaseAddress to create the object's absolute address
612 var builder = client.GetAddressBuilder(container, objectName);
613 var uri = builder.Uri;
615 //Download progress is reported to the Trace log
616 Log.InfoFormat("[GET] START {0}", objectName);
617 client.DownloadProgressChanged += (sender, args) =>
618 Log.InfoFormat("[GET PROGRESS] {0} {1}% {2} of {3}",
619 fileName, args.ProgressPercentage,
621 args.TotalBytesToReceive);
624 //Start downloading the object asynchronously
625 var downloadTask = client.DownloadFileTask(uri, fileName);
627 //Once the download completes
628 return downloadTask.ContinueWith(download =>
630 //Delete the local client object
632 //And report failure or completion
633 if (download.IsFaulted)
635 Log.ErrorFormat("[GET] FAIL for {0} with \r{1}", objectName,
640 Log.InfoFormat("[GET] END {0}", objectName);
644 catch (Exception exc)
646 Log.ErrorFormat("[GET] END {0} with {1}", objectName, exc);
654 public Task<IList<string>> PutHashMap(string account, string container, string objectName, TreeHash hash)
656 if (String.IsNullOrWhiteSpace(container))
657 throw new ArgumentNullException("container");
658 if (String.IsNullOrWhiteSpace(objectName))
659 throw new ArgumentNullException("objectName");
661 throw new ArgumentNullException("hash");
662 if (String.IsNullOrWhiteSpace(Token))
663 throw new InvalidOperationException("Invalid Token");
664 if (StorageUrl == null)
665 throw new InvalidOperationException("Invalid Storage Url");
666 Contract.EndContractBlock();
669 //Don't use a timeout because putting the hashmap may be a long process
670 var client = new RestClient(_baseClient) { Timeout = 0 };
671 if (!String.IsNullOrWhiteSpace(account))
672 client.BaseAddress = GetAccountUrl(account);
674 //The container and objectName are relative names. They are joined with the client's
675 //BaseAddress to create the object's absolute address
676 var builder = client.GetAddressBuilder(container, objectName);
677 builder.Query = "format=json&hashmap";
678 var uri = builder.Uri;
681 //Send the tree hash as Json to the server
682 client.Headers[HttpRequestHeader.ContentType] = "application/octet-stream";
683 var uploadTask=client.UploadStringTask(uri, "PUT", hash.ToJson());
686 return uploadTask.ContinueWith(t =>
689 var empty = (IList<string>)new List<string>();
692 //The server will respond either with 201-created if all blocks were already on the server
693 if (client.StatusCode == HttpStatusCode.Created)
695 //in which case we return an empty hash list
698 //or with a 409-conflict and return the list of missing parts
699 //A 409 will cause an exception so we need to check t.IsFaulted to avoid propagating the exception
702 var ex = t.Exception.InnerException;
703 var we = ex as WebException;
704 var response = we.Response as HttpWebResponse;
705 if (response!=null && response.StatusCode==HttpStatusCode.Conflict)
707 //In case of 409 the missing parts will be in the response content
708 using (var stream = response.GetResponseStream())
709 using(var reader=new StreamReader(stream))
711 //We need to cleanup the content before returning it because it contains
712 //error content after the list of hashes
713 var hashes = new List<string>();
715 //All lines up to the first empty line are hashes
716 while(!String.IsNullOrWhiteSpace(line=reader.ReadLine()))
725 //Any other status code is unexpected and the exception should be rethrown
729 //Any other status code is unexpected but there was no exception. We can probably continue processing
732 Log.WarnFormat("Unexcpected status code when putting map: {0} - {1}",client.StatusCode,client.StatusDescription);
739 public Task<byte[]> GetBlock(string account, string container, Uri relativeUrl, long start, long? end)
741 if (String.IsNullOrWhiteSpace(Token))
742 throw new InvalidOperationException("Invalid Token");
743 if (StorageUrl == null)
744 throw new InvalidOperationException("Invalid Storage Url");
745 if (String.IsNullOrWhiteSpace(container))
746 throw new ArgumentNullException("container");
747 if (relativeUrl== null)
748 throw new ArgumentNullException("relativeUrl");
749 if (end.HasValue && end<0)
750 throw new ArgumentOutOfRangeException("end");
752 throw new ArgumentOutOfRangeException("start");
753 Contract.EndContractBlock();
756 //Don't use a timeout because putting the hashmap may be a long process
757 var client = new RestClient(_baseClient) {Timeout = 0, RangeFrom = start, RangeTo = end};
758 if (!String.IsNullOrWhiteSpace(account))
759 client.BaseAddress = GetAccountUrl(account);
761 var builder = client.GetAddressBuilder(container, relativeUrl.ToString());
762 var uri = builder.Uri;
764 return client.DownloadDataTask(uri)
773 public Task PostBlock(string account, string container, byte[] block, int offset, int count)
775 if (String.IsNullOrWhiteSpace(container))
776 throw new ArgumentNullException("container");
778 throw new ArgumentNullException("block");
779 if (offset < 0 || offset >= block.Length)
780 throw new ArgumentOutOfRangeException("offset");
781 if (count < 0 || count > block.Length)
782 throw new ArgumentOutOfRangeException("count");
783 if (String.IsNullOrWhiteSpace(Token))
784 throw new InvalidOperationException("Invalid Token");
785 if (StorageUrl == null)
786 throw new InvalidOperationException("Invalid Storage Url");
787 Contract.EndContractBlock();
790 //Don't use a timeout because putting the hashmap may be a long process
791 var client = new RestClient(_baseClient) { Timeout = 0 };
792 if (!String.IsNullOrWhiteSpace(account))
793 client.BaseAddress = GetAccountUrl(account);
795 var builder = client.GetAddressBuilder(container, "");
796 //We are doing an update
797 builder.Query = "update";
798 var uri = builder.Uri;
800 client.Headers[HttpRequestHeader.ContentType] = "application/octet-stream";
802 Log.InfoFormat("[BLOCK POST] START");
804 client.UploadProgressChanged += (sender, args) =>
805 Log.InfoFormat("[BLOCK POST PROGRESS] {0}% {1} of {2}",
806 args.ProgressPercentage, args.BytesSent,
807 args.TotalBytesToSend);
808 client.UploadFileCompleted += (sender, args) =>
809 Log.InfoFormat("[BLOCK POST PROGRESS] Completed ");
813 var uploadTask = client.UploadDataTask(uri, "POST", block)
814 .ContinueWith(upload =>
818 if (upload.IsFaulted)
820 var exception = upload.Exception.InnerException;
821 Log.ErrorFormat("[BLOCK POST] FAIL with \r{0}", exception);
825 Log.InfoFormat("[BLOCK POST] END");
831 public Task<TreeHash> GetHashMap(string account, string container, string objectName)
833 if (String.IsNullOrWhiteSpace(container))
834 throw new ArgumentNullException("container");
835 if (String.IsNullOrWhiteSpace(objectName))
836 throw new ArgumentNullException("objectName");
837 if (String.IsNullOrWhiteSpace(Token))
838 throw new InvalidOperationException("Invalid Token");
839 if (StorageUrl == null)
840 throw new InvalidOperationException("Invalid Storage Url");
841 Contract.EndContractBlock();
845 //WebClient, and by extension RestClient, are not thread-safe. Create a new RestClient
846 //object to avoid concurrency errors.
848 //Download operations take a long time therefore they have no timeout.
849 //TODO: Do we really? this is a hashmap operation, not a download
850 var client = new RestClient(_baseClient) { Timeout = 0 };
851 if (!String.IsNullOrWhiteSpace(account))
852 client.BaseAddress = GetAccountUrl(account);
855 //The container and objectName are relative names. They are joined with the client's
856 //BaseAddress to create the object's absolute address
857 var builder = client.GetAddressBuilder(container, objectName);
858 builder.Query = "format=json&hashmap";
859 var uri = builder.Uri;
861 //Start downloading the object asynchronously
862 var downloadTask = client.DownloadStringTask(uri);
864 //Once the download completes
865 return downloadTask.ContinueWith(download =>
867 //Delete the local client object
869 //And report failure or completion
870 if (download.IsFaulted)
872 Log.ErrorFormat("[GET HASH] FAIL for {0} with \r{1}", objectName,
874 throw download.Exception;
877 //The server will return an empty string if the file is empty
878 var json = download.Result;
879 var treeHash = TreeHash.Parse(json);
880 Log.InfoFormat("[GET HASH] END {0}", objectName);
884 catch (Exception exc)
886 Log.ErrorFormat("[GET HASH] END {0} with {1}", objectName, exc);
898 /// <param name="account"></param>
899 /// <param name="container"></param>
900 /// <param name="objectName"></param>
901 /// <param name="fileName"></param>
902 /// <param name="hash">Optional hash value for the file. If no hash is provided, the method calculates a new hash</param>
903 /// <remarks>>This method should have no timeout or a very long one</remarks>
904 public Task PutObject(string account, string container, string objectName, string fileName, string hash = null)
906 if (String.IsNullOrWhiteSpace(container))
907 throw new ArgumentNullException("container", "The container property can't be empty");
908 if (String.IsNullOrWhiteSpace(objectName))
909 throw new ArgumentNullException("objectName", "The objectName property can't be empty");
910 if (String.IsNullOrWhiteSpace(fileName))
911 throw new ArgumentNullException("fileName", "The fileName property can't be empty");
912 if (!File.Exists(fileName))
913 throw new FileNotFoundException("The file does not exist",fileName);
914 Contract.EndContractBlock();
919 var client = new RestClient(_baseClient){Timeout=0};
920 if (!String.IsNullOrWhiteSpace(account))
921 client.BaseAddress = GetAccountUrl(account);
923 var builder = client.GetAddressBuilder(container, objectName);
924 var uri = builder.Uri;
926 string etag = hash ?? CalculateHash(fileName);
928 client.Headers.Add("Content-Type", "application/octet-stream");
929 client.Headers.Add("ETag", etag);
932 Log.InfoFormat("[PUT] START {0}", objectName);
933 client.UploadProgressChanged += (sender, args) =>
935 using (log4net.ThreadContext.Stacks["PUT"].Push("Progress"))
937 Log.InfoFormat("{0} {1}% {2} of {3}", fileName, args.ProgressPercentage,
938 args.BytesSent, args.TotalBytesToSend);
942 client.UploadFileCompleted += (sender, args) =>
944 using (log4net.ThreadContext.Stacks["PUT"].Push("Progress"))
946 Log.InfoFormat("Completed {0}", fileName);
949 return client.UploadFileTask(uri, "PUT", fileName)
950 .ContinueWith(upload=>
954 if (upload.IsFaulted)
956 var exc = upload.Exception.InnerException;
957 Log.ErrorFormat("[PUT] FAIL for {0} with \r{1}",objectName,exc);
961 Log.InfoFormat("[PUT] END {0}", objectName);
964 catch (Exception exc)
966 Log.ErrorFormat("[PUT] END {0} with {1}", objectName, exc);
973 private static string CalculateHash(string fileName)
976 using (var hasher = MD5.Create())
977 using(var stream=File.OpenRead(fileName))
979 var hashBuilder=new StringBuilder();
980 foreach (byte b in hasher.ComputeHash(stream))
981 hashBuilder.Append(b.ToString("x2").ToLower());
982 hash = hashBuilder.ToString();
987 /* public void DeleteObject(string container, string objectName,string account)
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 Contract.EndContractBlock();
995 using (var client = new RestClient(_baseClient))
997 if (!String.IsNullOrWhiteSpace(account))
998 client.BaseAddress = GetAccountUrl(account);
1000 client.DeleteWithRetry(container + "/" + objectName, 3);
1002 var expectedCodes = new[] {HttpStatusCode.NotFound, HttpStatusCode.NoContent};
1003 if (!expectedCodes.Contains(client.StatusCode))
1004 throw CreateWebException("DeleteObject", client.StatusCode);
1009 public void MoveObject(string account, string sourceContainer, string oldObjectName, string targetContainer, string newObjectName)
1011 if (String.IsNullOrWhiteSpace(sourceContainer))
1012 throw new ArgumentNullException("sourceContainer", "The container property can't be empty");
1013 if (String.IsNullOrWhiteSpace(oldObjectName))
1014 throw new ArgumentNullException("oldObjectName", "The oldObjectName property can't be empty");
1015 if (String.IsNullOrWhiteSpace(targetContainer))
1016 throw new ArgumentNullException("targetContainer", "The container property can't be empty");
1017 if (String.IsNullOrWhiteSpace(newObjectName))
1018 throw new ArgumentNullException("newObjectName", "The newObjectName property can't be empty");
1019 Contract.EndContractBlock();
1021 var targetUrl = targetContainer + "/" + newObjectName;
1022 var sourceUrl = String.Format("/{0}/{1}", sourceContainer, oldObjectName);
1024 using (var client = new RestClient(_baseClient))
1026 if (!String.IsNullOrWhiteSpace(account))
1027 client.BaseAddress = GetAccountUrl(account);
1029 client.Headers.Add("X-Move-From", sourceUrl);
1030 client.PutWithRetry(targetUrl, 3);
1032 var expectedCodes = new[] {HttpStatusCode.OK, HttpStatusCode.NoContent, HttpStatusCode.Created};
1033 if (!expectedCodes.Contains(client.StatusCode))
1034 throw CreateWebException("MoveObject", client.StatusCode);
1038 public void DeleteObject(string account, string sourceContainer, string objectName, string targetContainer)
1040 if (String.IsNullOrWhiteSpace(sourceContainer))
1041 throw new ArgumentNullException("sourceContainer", "The container property can't be empty");
1042 if (String.IsNullOrWhiteSpace(objectName))
1043 throw new ArgumentNullException("objectName", "The oldObjectName property can't be empty");
1044 if (String.IsNullOrWhiteSpace(targetContainer))
1045 throw new ArgumentNullException("targetContainer", "The container property can't be empty");
1046 Contract.EndContractBlock();
1048 var targetUrl = targetContainer + "/" + objectName;
1049 var sourceUrl = String.Format("/{0}/{1}", sourceContainer, objectName);
1051 using (var client = new RestClient(_baseClient))
1053 if (!String.IsNullOrWhiteSpace(account))
1054 client.BaseAddress = GetAccountUrl(account);
1056 client.Headers.Add("X-Move-From", sourceUrl);
1057 client.PutWithRetry(targetUrl, 3);
1059 var expectedCodes = new[] {HttpStatusCode.OK, HttpStatusCode.NoContent, HttpStatusCode.Created,HttpStatusCode.NotFound};
1060 if (!expectedCodes.Contains(client.StatusCode))
1061 throw CreateWebException("DeleteObject", client.StatusCode);
1066 private static WebException CreateWebException(string operation, HttpStatusCode statusCode)
1068 return new WebException(String.Format("{0} failed with unexpected status code {1}", operation, statusCode));
1074 public class ShareAccountInfo
1076 public DateTime? last_modified { get; set; }
1077 public string name { get; set; }