2 /* -----------------------------------------------------------------------
3 * <copyright file="CloudFilesClient.cs" company="GRNet">
5 * Copyright 2011-2012 GRNET S.A. All rights reserved.
7 * Redistribution and use in source and binary forms, with or
8 * without modification, are permitted provided that the following
11 * 1. Redistributions of source code must retain the above
12 * copyright notice, this list of conditions and the following
15 * 2. Redistributions in binary form must reproduce the above
16 * copyright notice, this list of conditions and the following
17 * disclaimer in the documentation and/or other materials
18 * provided with the distribution.
21 * THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
22 * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
23 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
24 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
25 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
26 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
27 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
28 * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
29 * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
30 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
31 * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
32 * POSSIBILITY OF SUCH DAMAGE.
34 * The views and conclusions contained in the software and
35 * documentation are those of the authors and should not be
36 * interpreted as representing official policies, either expressed
37 * or implied, of GRNET S.A.
39 * -----------------------------------------------------------------------
43 // **CloudFilesClient** provides a simple client interface to CloudFiles and Pithos
45 // The class provides methods to upload/download files, delete files, manage containers
49 using System.Collections.Generic;
50 using System.Collections.Specialized;
51 using System.ComponentModel.Composition;
52 using System.Diagnostics;
53 using System.Diagnostics.Contracts;
57 using System.Security.Cryptography;
59 using System.Threading.Tasks;
60 using Newtonsoft.Json;
61 using Pithos.Interfaces;
64 namespace Pithos.Network
66 [Export(typeof(ICloudClient))]
67 public class CloudFilesClient:ICloudClient
69 //CloudFilesClient uses *_baseClient* internally to communicate with the server
70 //RestClient provides a REST-friendly interface over the standard WebClient.
71 private RestClient _baseClient;
74 //During authentication the client provides a UserName
75 public string UserName { get; set; }
77 //and and ApiKey to the server
78 public string ApiKey { get; set; }
80 //And receives an authentication Token. This token must be provided in ALL other operations,
81 //in the X-Auth-Token header
82 private string _token;
85 get { return _token; }
89 _baseClient.Headers["X-Auth-Token"] = value;
93 //The client also receives a StorageUrl after authentication. All subsequent operations must
95 public Uri StorageUrl { get; set; }
98 protected Uri RootAddressUri { get; set; }
100 /* private WebProxy _proxy;
101 public WebProxy Proxy
103 get { return _proxy; }
107 if (_baseClient != null)
108 _baseClient.Proxy = value;
113 /* private Uri _proxy;
116 get { return _proxy; }
120 if (_baseClient != null)
121 _baseClient.Proxy = new WebProxy(value);
125 public double DownloadPercentLimit { get; set; }
126 public double UploadPercentLimit { get; set; }
128 public string AuthenticationUrl { get; set; }
131 public string VersionPath
133 get { return UsePithos ? "v1" : "v1.0"; }
136 public bool UsePithos { get; set; }
139 private static readonly ILog Log = LogManager.GetLogger("CloudFilesClient");
141 public CloudFilesClient(string userName, string apiKey)
147 public CloudFilesClient(AccountInfo accountInfo)
149 if (accountInfo==null)
150 throw new ArgumentNullException("accountInfo");
151 Contract.Ensures(!String.IsNullOrWhiteSpace(Token));
152 Contract.Ensures(StorageUrl != null);
153 Contract.Ensures(_baseClient != null);
154 Contract.Ensures(RootAddressUri != null);
155 Contract.EndContractBlock();
157 _baseClient = new RestClient
159 BaseAddress = accountInfo.StorageUri.ToString(),
163 StorageUrl = accountInfo.StorageUri;
164 Token = accountInfo.Token;
165 UserName = accountInfo.UserName;
167 //Get the root address (StorageUrl without the account)
168 var storageUrl = StorageUrl.AbsoluteUri;
169 var usernameIndex = storageUrl.LastIndexOf(UserName);
170 var rootUrl = storageUrl.Substring(0, usernameIndex);
171 RootAddressUri = new Uri(rootUrl);
175 public AccountInfo Authenticate()
177 if (String.IsNullOrWhiteSpace(UserName))
178 throw new InvalidOperationException("UserName is empty");
179 if (String.IsNullOrWhiteSpace(ApiKey))
180 throw new InvalidOperationException("ApiKey is empty");
181 if (String.IsNullOrWhiteSpace(AuthenticationUrl))
182 throw new InvalidOperationException("AuthenticationUrl is empty");
183 Contract.Ensures(!String.IsNullOrWhiteSpace(Token));
184 Contract.Ensures(StorageUrl != null);
185 Contract.Ensures(_baseClient != null);
186 Contract.Ensures(RootAddressUri != null);
187 Contract.EndContractBlock();
190 Log.InfoFormat("[AUTHENTICATE] Start for {0}", UserName);
192 var groups = new List<Group>();
194 using (var authClient = new RestClient{BaseAddress=AuthenticationUrl})
196 /* if (Proxy != null)
197 authClient.Proxy = Proxy;*/
199 Contract.Assume(authClient.Headers!=null);
201 authClient.Headers.Add("X-Auth-User", UserName);
202 authClient.Headers.Add("X-Auth-Key", ApiKey);
204 authClient.DownloadStringWithRetry(VersionPath, 3);
206 authClient.AssertStatusOK("Authentication failed");
208 var storageUrl = authClient.GetHeaderValue("X-Storage-Url");
209 if (String.IsNullOrWhiteSpace(storageUrl))
210 throw new InvalidOperationException("Failed to obtain storage url");
212 _baseClient = new RestClient
214 BaseAddress = storageUrl,
220 StorageUrl = new Uri(storageUrl);
222 //Get the root address (StorageUrl without the account)
223 var usernameIndex=storageUrl.LastIndexOf(UserName);
224 var rootUrl = storageUrl.Substring(0, usernameIndex);
225 RootAddressUri = new Uri(rootUrl);
227 var token = authClient.GetHeaderValue("X-Auth-Token");
228 if (String.IsNullOrWhiteSpace(token))
229 throw new InvalidOperationException("Failed to obtain token url");
232 /* var keys = authClient.ResponseHeaders.AllKeys.AsQueryable();
233 groups = (from key in keys
234 where key.StartsWith("X-Account-Group-")
235 let name = key.Substring(16)
236 select new Group(name, authClient.ResponseHeaders[key]))
242 Log.InfoFormat("[AUTHENTICATE] End for {0}", UserName);
245 return new AccountInfo {StorageUri = StorageUrl, Token = Token, UserName = UserName,Groups=groups};
251 public IList<ContainerInfo> ListContainers(string account)
253 using (var client = new RestClient(_baseClient))
255 if (!String.IsNullOrWhiteSpace(account))
256 client.BaseAddress = GetAccountUrl(account);
258 client.Parameters.Clear();
259 client.Parameters.Add("format", "json");
260 var content = client.DownloadStringWithRetry("", 3);
261 client.AssertStatusOK("List Containers failed");
263 if (client.StatusCode == HttpStatusCode.NoContent)
264 return new List<ContainerInfo>();
265 var infos = JsonConvert.DeserializeObject<IList<ContainerInfo>>(content);
267 foreach (var info in infos)
269 info.Account = account;
276 private string GetAccountUrl(string account)
278 return new Uri(RootAddressUri, new Uri(account,UriKind.Relative)).AbsoluteUri;
281 public IList<ShareAccountInfo> ListSharingAccounts(DateTime? since=null)
283 using (ThreadContext.Stacks["Share"].Push("List Accounts"))
285 if (Log.IsDebugEnabled) Log.DebugFormat("START");
287 using (var client = new RestClient(_baseClient))
289 client.Parameters.Clear();
290 client.Parameters.Add("format", "json");
291 client.IfModifiedSince = since;
293 //Extract the username from the base address
294 client.BaseAddress = RootAddressUri.AbsoluteUri;
296 var content = client.DownloadStringWithRetry(@"", 3);
298 client.AssertStatusOK("ListSharingAccounts failed");
300 //If the result is empty, return an empty list,
301 var infos = String.IsNullOrWhiteSpace(content)
302 ? new List<ShareAccountInfo>()
303 //Otherwise deserialize the account list into a list of ShareAccountInfos
304 : JsonConvert.DeserializeObject<IList<ShareAccountInfo>>(content);
306 Log.DebugFormat("END");
312 //Request listing of all objects in a container modified since a specific time.
313 //If the *since* value is missing, return all objects
314 public IList<ObjectInfo> ListSharedObjects(DateTime? since = null)
317 using (ThreadContext.Stacks["Share"].Push("List Objects"))
319 if (Log.IsDebugEnabled) Log.DebugFormat("START");
321 var objects = new List<ObjectInfo>();
322 var accounts = ListSharingAccounts(since);
323 foreach (var account in accounts)
325 var containers = ListContainers(account.name);
326 foreach (var container in containers)
328 var containerObjects = ListObjects(account.name, container.Name);
329 objects.AddRange(containerObjects);
332 if (Log.IsDebugEnabled) Log.DebugFormat("END");
337 public void SetTags(ObjectInfo target,IDictionary<string,string> tags)
339 if (String.IsNullOrWhiteSpace(Token))
340 throw new InvalidOperationException("The Token is not set");
341 if (StorageUrl == null)
342 throw new InvalidOperationException("The StorageUrl is not set");
344 throw new ArgumentNullException("target");
345 Contract.EndContractBlock();
347 using (ThreadContext.Stacks["Share"].Push("Share Object"))
349 if (Log.IsDebugEnabled) Log.DebugFormat("START");
351 using (var client = new RestClient(_baseClient))
354 client.BaseAddress = GetAccountUrl(target.Account);
356 client.Parameters.Clear();
357 client.Parameters.Add("update", "");
359 foreach (var tag in tags)
361 var headerTag = String.Format("X-Object-Meta-{0}", tag.Key);
362 client.Headers.Add(headerTag, tag.Value);
365 client.DownloadStringWithRetry(target.Container, 3);
368 client.AssertStatusOK("SetTags failed");
369 //If the status is NOT ACCEPTED we have a problem
370 if (client.StatusCode != HttpStatusCode.Accepted)
372 Log.Error("Failed to set tags");
373 throw new Exception("Failed to set tags");
376 if (Log.IsDebugEnabled) Log.DebugFormat("END");
383 public void ShareObject(string account, string container, string objectName, string shareTo, bool read, bool write)
385 if (String.IsNullOrWhiteSpace(Token))
386 throw new InvalidOperationException("The Token is not set");
387 if (StorageUrl==null)
388 throw new InvalidOperationException("The StorageUrl is not set");
389 if (String.IsNullOrWhiteSpace(container))
390 throw new ArgumentNullException("container");
391 if (String.IsNullOrWhiteSpace(objectName))
392 throw new ArgumentNullException("objectName");
393 if (String.IsNullOrWhiteSpace(account))
394 throw new ArgumentNullException("account");
395 if (String.IsNullOrWhiteSpace(shareTo))
396 throw new ArgumentNullException("shareTo");
397 Contract.EndContractBlock();
399 using (ThreadContext.Stacks["Share"].Push("Share Object"))
401 if (Log.IsDebugEnabled) Log.DebugFormat("START");
403 using (var client = new RestClient(_baseClient))
406 client.BaseAddress = GetAccountUrl(account);
408 client.Parameters.Clear();
409 client.Parameters.Add("format", "json");
411 string permission = "";
413 permission = String.Format("write={0}", shareTo);
415 permission = String.Format("read={0}", shareTo);
416 client.Headers.Add("X-Object-Sharing", permission);
418 var content = client.DownloadStringWithRetry(container, 3);
420 client.AssertStatusOK("ShareObject failed");
422 //If the result is empty, return an empty list,
423 var infos = String.IsNullOrWhiteSpace(content)
424 ? new List<ObjectInfo>()
425 //Otherwise deserialize the object list into a list of ObjectInfos
426 : JsonConvert.DeserializeObject<IList<ObjectInfo>>(content);
428 if (Log.IsDebugEnabled) Log.DebugFormat("END");
435 public AccountInfo GetAccountPolicies(AccountInfo accountInfo)
437 if (accountInfo==null)
438 throw new ArgumentNullException("accountInfo");
439 Contract.EndContractBlock();
441 using (ThreadContext.Stacks["Account"].Push("GetPolicies"))
443 if (Log.IsDebugEnabled) Log.DebugFormat("START");
445 using (var client = new RestClient(_baseClient))
447 if (!String.IsNullOrWhiteSpace(accountInfo.UserName))
448 client.BaseAddress = GetAccountUrl(accountInfo.UserName);
450 client.Parameters.Clear();
451 client.Parameters.Add("format", "json");
452 client.Head(String.Empty, 3);
454 var quotaValue=client.ResponseHeaders["X-Account-Policy-Quota"];
455 var bytesValue= client.ResponseHeaders["X-Account-Bytes-Used"];
458 if (long.TryParse(quotaValue, out quota))
459 accountInfo.Quota = quota;
460 if (long.TryParse(bytesValue, out bytes))
461 accountInfo.BytesUsed = bytes;
470 public void UpdateMetadata(ObjectInfo objectInfo)
472 if (objectInfo == null)
473 throw new ArgumentNullException("objectInfo");
474 Contract.EndContractBlock();
476 using (ThreadContext.Stacks["Objects"].Push("UpdateMetadata"))
478 if (Log.IsDebugEnabled) Log.DebugFormat("START");
481 using(var client=new RestClient(_baseClient))
484 client.BaseAddress = GetAccountUrl(objectInfo.Account);
486 client.Parameters.Clear();
490 foreach (var tag in objectInfo.Tags)
492 var headerTag = String.Format("X-Object-Meta-{0}", tag.Key);
493 client.Headers.Add(headerTag, tag.Value);
498 var permissions=objectInfo.GetPermissionString();
499 client.SetNonEmptyHeaderValue("X-Object-Sharing",permissions);
501 client.SetNonEmptyHeaderValue("Content-Disposition",objectInfo.ContendDisposition);
502 client.SetNonEmptyHeaderValue("Content-Encoding",objectInfo.ContentEncoding);
503 client.SetNonEmptyHeaderValue("X-Object-Manifest",objectInfo.Manifest);
504 var isPublic = objectInfo.IsPublic.ToString().ToLower();
505 client.Headers.Add("X-Object-Public", isPublic);
508 var uriBuilder = client.GetAddressBuilder(objectInfo.Container, objectInfo.Name);
509 var uri = uriBuilder.Uri;
511 client.UploadValues(uri,new NameValueCollection());
514 client.AssertStatusOK("UpdateMetadata failed");
515 //If the status is NOT ACCEPTED or OK we have a problem
516 if (!(client.StatusCode == HttpStatusCode.Accepted || client.StatusCode == HttpStatusCode.OK))
518 Log.Error("Failed to update metadata");
519 throw new Exception("Failed to update metadata");
522 if (Log.IsDebugEnabled) Log.DebugFormat("END");
528 public void UpdateMetadata(ContainerInfo containerInfo)
530 if (containerInfo == null)
531 throw new ArgumentNullException("containerInfo");
532 Contract.EndContractBlock();
534 using (ThreadContext.Stacks["Containers"].Push("UpdateMetadata"))
536 if (Log.IsDebugEnabled) Log.DebugFormat("START");
539 using(var client=new RestClient(_baseClient))
542 client.BaseAddress = GetAccountUrl(containerInfo.Account);
544 client.Parameters.Clear();
548 foreach (var tag in containerInfo.Tags)
550 var headerTag = String.Format("X-Container-Meta-{0}", tag.Key);
551 client.Headers.Add(headerTag, tag.Value);
556 foreach (var policy in containerInfo.Policies)
558 var headerPolicy = String.Format("X-Container-Policy-{0}", policy.Key);
559 client.Headers.Add(headerPolicy, policy.Value);
563 var uriBuilder = client.GetAddressBuilder(containerInfo.Name,"");
564 var uri = uriBuilder.Uri;
566 client.UploadValues(uri,new NameValueCollection());
569 client.AssertStatusOK("UpdateMetadata failed");
570 //If the status is NOT ACCEPTED or OK we have a problem
571 if (!(client.StatusCode == HttpStatusCode.Accepted || client.StatusCode == HttpStatusCode.OK))
573 Log.Error("Failed to update metadata");
574 throw new Exception("Failed to update metadata");
577 if (Log.IsDebugEnabled) Log.DebugFormat("END");
584 public IList<ObjectInfo> ListObjects(string account, string container, DateTime? since = null)
586 if (String.IsNullOrWhiteSpace(container))
587 throw new ArgumentNullException("container");
588 Contract.EndContractBlock();
590 using (ThreadContext.Stacks["Objects"].Push("List"))
592 if (Log.IsDebugEnabled) Log.DebugFormat("START");
594 using (var client = new RestClient(_baseClient))
596 if (!String.IsNullOrWhiteSpace(account))
597 client.BaseAddress = GetAccountUrl(account);
599 client.Parameters.Clear();
600 client.Parameters.Add("format", "json");
601 client.IfModifiedSince = since;
602 var content = client.DownloadStringWithRetry(container, 3);
604 client.AssertStatusOK("ListObjects failed");
606 //If the result is empty, return an empty list,
607 var infos = String.IsNullOrWhiteSpace(content)
608 ? new List<ObjectInfo>()
609 //Otherwise deserialize the object list into a list of ObjectInfos
610 : JsonConvert.DeserializeObject<IList<ObjectInfo>>(content);
612 foreach (var info in infos)
614 info.Container = container;
615 info.Account = account;
617 if (Log.IsDebugEnabled) Log.DebugFormat("START");
625 public IList<ObjectInfo> ListObjects(string account, string container, string folder, DateTime? since = null)
627 if (String.IsNullOrWhiteSpace(container))
628 throw new ArgumentNullException("container");
629 if (String.IsNullOrWhiteSpace(folder))
630 throw new ArgumentNullException("folder");
631 Contract.EndContractBlock();
633 using (ThreadContext.Stacks["Objects"].Push("List"))
635 if (Log.IsDebugEnabled) Log.DebugFormat("START");
637 using (var client = new RestClient(_baseClient))
639 if (!String.IsNullOrWhiteSpace(account))
640 client.BaseAddress = GetAccountUrl(account);
642 client.Parameters.Clear();
643 client.Parameters.Add("format", "json");
644 client.Parameters.Add("path", folder);
645 client.IfModifiedSince = since;
646 var content = client.DownloadStringWithRetry(container, 3);
647 client.AssertStatusOK("ListObjects failed");
649 var infos = JsonConvert.DeserializeObject<IList<ObjectInfo>>(content);
651 if (Log.IsDebugEnabled) Log.DebugFormat("END");
658 public bool ContainerExists(string account, string container)
660 if (String.IsNullOrWhiteSpace(container))
661 throw new ArgumentNullException("container", "The container property can't be empty");
662 Contract.EndContractBlock();
664 using (ThreadContext.Stacks["Containters"].Push("Exists"))
666 if (Log.IsDebugEnabled) Log.DebugFormat("START");
668 using (var client = new RestClient(_baseClient))
670 if (!String.IsNullOrWhiteSpace(account))
671 client.BaseAddress = GetAccountUrl(account);
673 client.Parameters.Clear();
674 client.Head(container, 3);
677 switch (client.StatusCode)
679 case HttpStatusCode.OK:
680 case HttpStatusCode.NoContent:
683 case HttpStatusCode.NotFound:
687 throw CreateWebException("ContainerExists", client.StatusCode);
689 if (Log.IsDebugEnabled) Log.DebugFormat("END");
697 public bool ObjectExists(string account, string container, string objectName)
699 if (String.IsNullOrWhiteSpace(container))
700 throw new ArgumentNullException("container", "The container property can't be empty");
701 if (String.IsNullOrWhiteSpace(objectName))
702 throw new ArgumentNullException("objectName", "The objectName property can't be empty");
703 Contract.EndContractBlock();
705 using (var client = new RestClient(_baseClient))
707 if (!String.IsNullOrWhiteSpace(account))
708 client.BaseAddress = GetAccountUrl(account);
710 client.Parameters.Clear();
711 client.Head(container + "/" + objectName, 3);
713 switch (client.StatusCode)
715 case HttpStatusCode.OK:
716 case HttpStatusCode.NoContent:
718 case HttpStatusCode.NotFound:
721 throw CreateWebException("ObjectExists", client.StatusCode);
727 public ObjectInfo GetObjectInfo(string account, string container, string objectName)
729 if (String.IsNullOrWhiteSpace(container))
730 throw new ArgumentNullException("container", "The container property can't be empty");
731 if (String.IsNullOrWhiteSpace(objectName))
732 throw new ArgumentNullException("objectName", "The objectName property can't be empty");
733 Contract.EndContractBlock();
735 using (ThreadContext.Stacks["Objects"].Push("GetObjectInfo"))
738 using (var client = new RestClient(_baseClient))
740 if (!String.IsNullOrWhiteSpace(account))
741 client.BaseAddress = GetAccountUrl(account);
744 client.Parameters.Clear();
746 client.Head(container + "/" + objectName, 3);
749 return ObjectInfo.Empty;
751 switch (client.StatusCode)
753 case HttpStatusCode.OK:
754 case HttpStatusCode.NoContent:
755 var keys = client.ResponseHeaders.AllKeys.AsQueryable();
756 var tags = client.GetMeta("X-Object-Meta-");
757 var extensions = (from key in keys
758 where key.StartsWith("X-Object-") && !key.StartsWith("X-Object-Meta-")
759 select new {Name = key, Value = client.ResponseHeaders[key]})
760 .ToDictionary(t => t.Name, t => t.Value);
762 var permissions=client.GetHeaderValue("X-Object-Sharing", true);
765 var info = new ObjectInfo
768 Container = container,
770 Hash = client.GetHeaderValue("ETag"),
771 Content_Type = client.GetHeaderValue("Content-Type"),
772 Bytes = Convert.ToInt64(client.GetHeaderValue("Content-Length",true)),
774 Last_Modified = client.LastModified,
775 Extensions = extensions,
776 ContentEncoding=client.GetHeaderValue("Content-Encoding",true),
777 ContendDisposition = client.GetHeaderValue("Content-Disposition",true),
778 Manifest=client.GetHeaderValue("X-Object-Manifest",true),
779 PublicUrl=client.GetHeaderValue("X-Object-Public",true),
781 info.SetPermissions(permissions);
783 case HttpStatusCode.NotFound:
784 return ObjectInfo.Empty;
786 throw new WebException(
787 String.Format("[FAIL] GetObjectInfo for {0} failed with unexpected status code {1}",
788 objectName, client.StatusCode));
792 catch (RetryException)
794 Log.WarnFormat("[RETRY FAIL] GetObjectInfo for {0} failed.",objectName);
795 return ObjectInfo.Empty;
797 catch (WebException e)
800 String.Format("[FAIL] GetObjectInfo for {0} failed with unexpected status code {1}",
801 objectName, client.StatusCode), e);
809 public void CreateFolder(string account, string container, string folder)
811 if (String.IsNullOrWhiteSpace(container))
812 throw new ArgumentNullException("container", "The container property can't be empty");
813 if (String.IsNullOrWhiteSpace(folder))
814 throw new ArgumentNullException("folder", "The folder property can't be empty");
815 Contract.EndContractBlock();
817 var folderUrl=String.Format("{0}/{1}",container,folder);
818 using (var client = new RestClient(_baseClient))
820 if (!String.IsNullOrWhiteSpace(account))
821 client.BaseAddress = GetAccountUrl(account);
823 client.Parameters.Clear();
824 client.Headers.Add("Content-Type", @"application/directory");
825 client.Headers.Add("Content-Length", "0");
826 client.PutWithRetry(folderUrl, 3);
828 if (client.StatusCode != HttpStatusCode.Created && client.StatusCode != HttpStatusCode.Accepted)
829 throw CreateWebException("CreateFolder", client.StatusCode);
835 public ContainerInfo GetContainerInfo(string account, string container)
837 if (String.IsNullOrWhiteSpace(container))
838 throw new ArgumentNullException("container", "The container property can't be empty");
839 Contract.EndContractBlock();
841 using (var client = new RestClient(_baseClient))
843 if (!String.IsNullOrWhiteSpace(account))
844 client.BaseAddress = GetAccountUrl(account);
846 client.Head(container);
847 switch (client.StatusCode)
849 case HttpStatusCode.OK:
850 case HttpStatusCode.NoContent:
851 var tags = client.GetMeta("X-Container-Meta-");
852 var policies = client.GetMeta("X-Container-Policy-");
854 var containerInfo = new ContainerInfo
859 long.Parse(client.GetHeaderValue("X-Container-Object-Count")),
860 Bytes = long.Parse(client.GetHeaderValue("X-Container-Bytes-Used")),
861 BlockHash = client.GetHeaderValue("X-Container-Block-Hash"),
862 BlockSize=int.Parse(client.GetHeaderValue("X-Container-Block-Size")),
863 Last_Modified=client.LastModified,
869 return containerInfo;
870 case HttpStatusCode.NotFound:
871 return ContainerInfo.Empty;
873 throw CreateWebException("GetContainerInfo", client.StatusCode);
878 public void CreateContainer(string account, string container)
880 if (String.IsNullOrWhiteSpace(account))
881 throw new ArgumentNullException("account");
882 if (String.IsNullOrWhiteSpace(container))
883 throw new ArgumentNullException("container");
884 Contract.EndContractBlock();
886 using (var client = new RestClient(_baseClient))
888 if (!String.IsNullOrWhiteSpace(account))
889 client.BaseAddress = GetAccountUrl(account);
891 client.PutWithRetry(container, 3);
892 var expectedCodes = new[] {HttpStatusCode.Created, HttpStatusCode.Accepted, HttpStatusCode.OK};
893 if (!expectedCodes.Contains(client.StatusCode))
894 throw CreateWebException("CreateContainer", client.StatusCode);
898 public void DeleteContainer(string account, string container)
900 if (String.IsNullOrWhiteSpace(container))
901 throw new ArgumentNullException("container", "The container property can't be empty");
902 Contract.EndContractBlock();
904 using (var client = new RestClient(_baseClient))
906 if (!String.IsNullOrWhiteSpace(account))
907 client.BaseAddress = GetAccountUrl(account);
909 client.DeleteWithRetry(container, 3);
910 var expectedCodes = new[] {HttpStatusCode.NotFound, HttpStatusCode.NoContent};
911 if (!expectedCodes.Contains(client.StatusCode))
912 throw CreateWebException("DeleteContainer", client.StatusCode);
920 /// <param name="account"></param>
921 /// <param name="container"></param>
922 /// <param name="objectName"></param>
923 /// <param name="fileName"></param>
924 /// <returns></returns>
925 /// <remarks>This method should have no timeout or a very long one</remarks>
926 //Asynchronously download the object specified by *objectName* in a specific *container* to
928 public Task GetObject(string account, string container, string objectName, string fileName)
930 if (String.IsNullOrWhiteSpace(container))
931 throw new ArgumentNullException("container", "The container property can't be empty");
932 if (String.IsNullOrWhiteSpace(objectName))
933 throw new ArgumentNullException("objectName", "The objectName property can't be empty");
934 Contract.EndContractBlock();
938 //WebClient, and by extension RestClient, are not thread-safe. Create a new RestClient
939 //object to avoid concurrency errors.
941 //Download operations take a long time therefore they have no timeout.
942 var client = new RestClient(_baseClient) { Timeout = 0 };
943 if (!String.IsNullOrWhiteSpace(account))
944 client.BaseAddress = GetAccountUrl(account);
946 //The container and objectName are relative names. They are joined with the client's
947 //BaseAddress to create the object's absolute address
948 var builder = client.GetAddressBuilder(container, objectName);
949 var uri = builder.Uri;
951 //Download progress is reported to the Trace log
952 Log.InfoFormat("[GET] START {0}", objectName);
953 client.DownloadProgressChanged += (sender, args) =>
954 Log.InfoFormat("[GET PROGRESS] {0} {1}% {2} of {3}",
955 fileName, args.ProgressPercentage,
957 args.TotalBytesToReceive);
960 //Start downloading the object asynchronously
961 var downloadTask = client.DownloadFileTask(uri, fileName);
963 //Once the download completes
964 return downloadTask.ContinueWith(download =>
966 //Delete the local client object
968 //And report failure or completion
969 if (download.IsFaulted)
971 Log.ErrorFormat("[GET] FAIL for {0} with \r{1}", objectName,
976 Log.InfoFormat("[GET] END {0}", objectName);
980 catch (Exception exc)
982 Log.ErrorFormat("[GET] END {0} with {1}", objectName, exc);
990 public Task<IList<string>> PutHashMap(string account, string container, string objectName, TreeHash hash)
992 if (String.IsNullOrWhiteSpace(container))
993 throw new ArgumentNullException("container");
994 if (String.IsNullOrWhiteSpace(objectName))
995 throw new ArgumentNullException("objectName");
997 throw new ArgumentNullException("hash");
998 if (String.IsNullOrWhiteSpace(Token))
999 throw new InvalidOperationException("Invalid Token");
1000 if (StorageUrl == null)
1001 throw new InvalidOperationException("Invalid Storage Url");
1002 Contract.EndContractBlock();
1005 //Don't use a timeout because putting the hashmap may be a long process
1006 var client = new RestClient(_baseClient) { Timeout = 0 };
1007 if (!String.IsNullOrWhiteSpace(account))
1008 client.BaseAddress = GetAccountUrl(account);
1010 //The container and objectName are relative names. They are joined with the client's
1011 //BaseAddress to create the object's absolute address
1012 var builder = client.GetAddressBuilder(container, objectName);
1013 builder.Query = "format=json&hashmap";
1014 var uri = builder.Uri;
1017 //Send the tree hash as Json to the server
1018 client.Headers[HttpRequestHeader.ContentType] = "application/octet-stream";
1019 var jsonHash = hash.ToJson();
1020 var uploadTask=client.UploadStringTask(uri, "PUT", jsonHash);
1022 return uploadTask.ContinueWith(t =>
1025 var empty = (IList<string>)new List<string>();
1028 //The server will respond either with 201-created if all blocks were already on the server
1029 if (client.StatusCode == HttpStatusCode.Created)
1031 //in which case we return an empty hash list
1034 //or with a 409-conflict and return the list of missing parts
1035 //A 409 will cause an exception so we need to check t.IsFaulted to avoid propagating the exception
1038 var ex = t.Exception.InnerException;
1039 var we = ex as WebException;
1040 var response = we.Response as HttpWebResponse;
1041 if (response!=null && response.StatusCode==HttpStatusCode.Conflict)
1043 //In case of 409 the missing parts will be in the response content
1044 using (var stream = response.GetResponseStream())
1045 using(var reader=new StreamReader(stream))
1047 Debug.Assert(stream.Position == 0);
1048 //We used to have to cleanup the content before returning it because it contains
1049 //error content after the list of hashes
1051 //As of 30/1/2012, the result is a proper Json array so we don't need to read the content
1054 var serializer = new JsonSerializer();
1055 var hashes=(List<string>)serializer.Deserialize(reader, typeof (List<string>));
1060 //Any other status code is unexpected and the exception should be rethrown
1064 //Any other status code is unexpected but there was no exception. We can probably continue processing
1065 Log.WarnFormat("Unexcpected status code when putting map: {0} - {1}",client.StatusCode,client.StatusDescription);
1072 public Task<byte[]> GetBlock(string account, string container, Uri relativeUrl, long start, long? end)
1074 if (String.IsNullOrWhiteSpace(Token))
1075 throw new InvalidOperationException("Invalid Token");
1076 if (StorageUrl == null)
1077 throw new InvalidOperationException("Invalid Storage Url");
1078 if (String.IsNullOrWhiteSpace(container))
1079 throw new ArgumentNullException("container");
1080 if (relativeUrl== null)
1081 throw new ArgumentNullException("relativeUrl");
1082 if (end.HasValue && end<0)
1083 throw new ArgumentOutOfRangeException("end");
1085 throw new ArgumentOutOfRangeException("start");
1086 Contract.EndContractBlock();
1089 //Don't use a timeout because putting the hashmap may be a long process
1090 var client = new RestClient(_baseClient) {Timeout = 0, RangeFrom = start, RangeTo = end};
1091 if (!String.IsNullOrWhiteSpace(account))
1092 client.BaseAddress = GetAccountUrl(account);
1094 var builder = client.GetAddressBuilder(container, relativeUrl.ToString());
1095 var uri = builder.Uri;
1097 return client.DownloadDataTask(uri)
1106 public async Task PostBlock(string account, string container, byte[] block, int offset, int count)
1108 if (String.IsNullOrWhiteSpace(container))
1109 throw new ArgumentNullException("container");
1111 throw new ArgumentNullException("block");
1112 if (offset < 0 || offset >= block.Length)
1113 throw new ArgumentOutOfRangeException("offset");
1114 if (count < 0 || count > block.Length)
1115 throw new ArgumentOutOfRangeException("count");
1116 if (String.IsNullOrWhiteSpace(Token))
1117 throw new InvalidOperationException("Invalid Token");
1118 if (StorageUrl == null)
1119 throw new InvalidOperationException("Invalid Storage Url");
1120 Contract.EndContractBlock();
1126 //Don't use a timeout because putting the hashmap may be a long process
1127 using (var client = new RestClient(_baseClient) { Timeout = 0 })
1129 if (!String.IsNullOrWhiteSpace(account))
1130 client.BaseAddress = GetAccountUrl(account);
1132 var builder = client.GetAddressBuilder(container, "");
1133 //We are doing an update
1134 builder.Query = "update";
1135 var uri = builder.Uri;
1137 client.Headers[HttpRequestHeader.ContentType] = "application/octet-stream";
1139 Log.InfoFormat("[BLOCK POST] START");
1141 client.UploadProgressChanged += (sender, args) =>
1142 Log.InfoFormat("[BLOCK POST PROGRESS] {0}% {1} of {2}",
1143 args.ProgressPercentage, args.BytesSent,
1144 args.TotalBytesToSend);
1145 client.UploadFileCompleted += (sender, args) =>
1146 Log.InfoFormat("[BLOCK POST PROGRESS] Completed ");
1148 var buffer = new byte[count];
1149 Buffer.BlockCopy(block, offset, buffer, 0, count);
1151 await client.UploadDataTask(uri, "POST", buffer);
1152 Log.InfoFormat("[BLOCK POST] END");
1155 catch (Exception exc)
1157 Log.ErrorFormat("[BLOCK POST] FAIL with \r{0}", exc);
1163 public async Task<TreeHash> GetHashMap(string account, string container, string objectName)
1165 if (String.IsNullOrWhiteSpace(container))
1166 throw new ArgumentNullException("container");
1167 if (String.IsNullOrWhiteSpace(objectName))
1168 throw new ArgumentNullException("objectName");
1169 if (String.IsNullOrWhiteSpace(Token))
1170 throw new InvalidOperationException("Invalid Token");
1171 if (StorageUrl == null)
1172 throw new InvalidOperationException("Invalid Storage Url");
1173 Contract.EndContractBlock();
1177 //WebClient, and by extension RestClient, are not thread-safe. Create a new RestClient
1178 //object to avoid concurrency errors.
1180 //Download operations take a long time therefore they have no timeout.
1181 //TODO: Do they really? this is a hashmap operation, not a download
1183 //Start downloading the object asynchronously
1184 using (var client = new RestClient(_baseClient) { Timeout = 0 })
1186 if (!String.IsNullOrWhiteSpace(account))
1187 client.BaseAddress = GetAccountUrl(account);
1189 //The container and objectName are relative names. They are joined with the client's
1190 //BaseAddress to create the object's absolute address
1191 var builder = client.GetAddressBuilder(container, objectName);
1192 builder.Query = "format=json&hashmap";
1193 var uri = builder.Uri;
1196 var json = await client.DownloadStringTaskAsync(uri);
1197 var treeHash = TreeHash.Parse(json);
1198 Log.InfoFormat("[GET HASH] END {0}", objectName);
1202 catch (Exception exc)
1204 Log.ErrorFormat("[GET HASH] END {0} with {1}", objectName, exc);
1214 /// <param name="account"></param>
1215 /// <param name="container"></param>
1216 /// <param name="objectName"></param>
1217 /// <param name="fileName"></param>
1218 /// <param name="hash">Optional hash value for the file. If no hash is provided, the method calculates a new hash</param>
1219 /// <remarks>>This method should have no timeout or a very long one</remarks>
1220 public async Task PutObject(string account, string container, string objectName, string fileName, string hash = null, string contentType = "application/octet-stream")
1222 if (String.IsNullOrWhiteSpace(container))
1223 throw new ArgumentNullException("container", "The container property can't be empty");
1224 if (String.IsNullOrWhiteSpace(objectName))
1225 throw new ArgumentNullException("objectName", "The objectName property can't be empty");
1226 if (String.IsNullOrWhiteSpace(fileName))
1227 throw new ArgumentNullException("fileName", "The fileName property can't be empty");
1229 if (!File.Exists(fileName) && !Directory.Exists(fileName))
1230 throw new FileNotFoundException("The file or directory does not exist",fileName);
1232 Contract.EndContractBlock();
1237 using (var client = new RestClient(_baseClient) { Timeout = 0 })
1239 if (!String.IsNullOrWhiteSpace(account))
1240 client.BaseAddress = GetAccountUrl(account);
1242 var builder = client.GetAddressBuilder(container, objectName);
1243 var uri = builder.Uri;
1245 string etag = hash ?? CalculateHash(fileName);
1247 client.Headers.Add("Content-Type", contentType);
1248 client.Headers.Add("ETag", etag);
1251 Log.InfoFormat("[PUT] START {0}", objectName);
1252 client.UploadProgressChanged += (sender, args) =>
1254 using (ThreadContext.Stacks["PUT"].Push("Progress"))
1256 Log.InfoFormat("{0} {1}% {2} of {3}", fileName,
1257 args.ProgressPercentage,
1258 args.BytesSent, args.TotalBytesToSend);
1262 client.UploadFileCompleted += (sender, args) =>
1264 using (ThreadContext.Stacks["PUT"].Push("Progress"))
1266 Log.InfoFormat("Completed {0}", fileName);
1269 if (contentType=="application/directory")
1270 await client.UploadDataTaskAsync(uri, "PUT", new byte[0]);
1272 await client.UploadFileTaskAsync(uri, "PUT", fileName);
1275 Log.InfoFormat("[PUT] END {0}", objectName);
1277 catch (Exception exc)
1279 Log.ErrorFormat("[PUT] END {0} with {1}", objectName, exc);
1286 private static string CalculateHash(string fileName)
1288 Contract.Requires(!String.IsNullOrWhiteSpace(fileName));
1289 Contract.EndContractBlock();
1292 using (var hasher = MD5.Create())
1293 using(var stream=File.OpenRead(fileName))
1295 var hashBuilder=new StringBuilder();
1296 foreach (byte b in hasher.ComputeHash(stream))
1297 hashBuilder.Append(b.ToString("x2").ToLower());
1298 hash = hashBuilder.ToString();
1303 public void MoveObject(string account, string sourceContainer, string oldObjectName, string targetContainer, string newObjectName)
1305 if (String.IsNullOrWhiteSpace(sourceContainer))
1306 throw new ArgumentNullException("sourceContainer", "The container property can't be empty");
1307 if (String.IsNullOrWhiteSpace(oldObjectName))
1308 throw new ArgumentNullException("oldObjectName", "The oldObjectName property can't be empty");
1309 if (String.IsNullOrWhiteSpace(targetContainer))
1310 throw new ArgumentNullException("targetContainer", "The container property can't be empty");
1311 if (String.IsNullOrWhiteSpace(newObjectName))
1312 throw new ArgumentNullException("newObjectName", "The newObjectName property can't be empty");
1313 Contract.EndContractBlock();
1315 var targetUrl = targetContainer + "/" + newObjectName;
1316 var sourceUrl = String.Format("/{0}/{1}", sourceContainer, oldObjectName);
1318 using (var client = new RestClient(_baseClient))
1320 if (!String.IsNullOrWhiteSpace(account))
1321 client.BaseAddress = GetAccountUrl(account);
1323 client.Headers.Add("X-Move-From", sourceUrl);
1324 client.PutWithRetry(targetUrl, 3);
1326 var expectedCodes = new[] {HttpStatusCode.OK, HttpStatusCode.NoContent, HttpStatusCode.Created};
1327 if (!expectedCodes.Contains(client.StatusCode))
1328 throw CreateWebException("MoveObject", client.StatusCode);
1332 public void DeleteObject(string account, string sourceContainer, string objectName)
1334 if (String.IsNullOrWhiteSpace(sourceContainer))
1335 throw new ArgumentNullException("sourceContainer", "The container property can't be empty");
1336 if (String.IsNullOrWhiteSpace(objectName))
1337 throw new ArgumentNullException("objectName", "The oldObjectName property can't be empty");
1338 Contract.EndContractBlock();
1340 var targetUrl = FolderConstants.TrashContainer + "/" + objectName;
1341 var sourceUrl = String.Format("/{0}/{1}", sourceContainer, objectName);
1343 using (var client = new RestClient(_baseClient))
1345 if (!String.IsNullOrWhiteSpace(account))
1346 client.BaseAddress = GetAccountUrl(account);
1348 client.Headers.Add("X-Move-From", sourceUrl);
1349 client.AllowedStatusCodes.Add(HttpStatusCode.NotFound);
1350 client.PutWithRetry(targetUrl, 3);
1352 var expectedCodes = new[] {HttpStatusCode.OK, HttpStatusCode.NoContent, HttpStatusCode.Created,HttpStatusCode.NotFound};
1353 if (!expectedCodes.Contains(client.StatusCode))
1354 throw CreateWebException("DeleteObject", client.StatusCode);
1359 private static WebException CreateWebException(string operation, HttpStatusCode statusCode)
1361 return new WebException(String.Format("{0} failed with unexpected status code {1}", operation, statusCode));
1367 public class ShareAccountInfo
1369 public DateTime? last_modified { get; set; }
1370 public string name { get; set; }