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.Reflection;
58 using System.Security.Cryptography;
60 using System.Threading;
61 using System.Threading.Tasks;
62 using Newtonsoft.Json;
63 using Pithos.Interfaces;
66 namespace Pithos.Network
68 [Export(typeof(ICloudClient))]
69 public class CloudFilesClient:ICloudClient
71 private static readonly ILog Log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
73 //CloudFilesClient uses *_baseClient* internally to communicate with the server
74 //RestClient provides a REST-friendly interface over the standard WebClient.
75 private RestClient _baseClient;
78 //During authentication the client provides a UserName
79 public string UserName { get; set; }
81 //and and ApiKey to the server
82 public string ApiKey { get; set; }
84 //And receives an authentication Token. This token must be provided in ALL other operations,
85 //in the X-Auth-Token header
86 private string _token;
87 private readonly string _emptyGuid = Guid.Empty.ToString();
92 get { return _token; }
96 _baseClient.Headers["X-Auth-Token"] = value;
100 //The client also receives a StorageUrl after authentication. All subsequent operations must
102 public Uri StorageUrl { get; set; }
105 public Uri RootAddressUri { get; set; }
107 /* private WebProxy _proxy;
108 public WebProxy Proxy
110 get { return _proxy; }
114 if (_baseClient != null)
115 _baseClient.Proxy = value;
120 /* private Uri _proxy;
123 get { return _proxy; }
127 if (_baseClient != null)
128 _baseClient.Proxy = new WebProxy(value);
132 public double DownloadPercentLimit { get; set; }
133 public double UploadPercentLimit { get; set; }
135 public string AuthenticationUrl { get; set; }
138 public string VersionPath
140 get { return UsePithos ? "v1" : "v1.0"; }
143 public bool UsePithos { get; set; }
147 public CloudFilesClient(string userName, string apiKey)
153 public CloudFilesClient(AccountInfo accountInfo)
155 if (accountInfo==null)
156 throw new ArgumentNullException("accountInfo");
157 Contract.Ensures(!String.IsNullOrWhiteSpace(Token));
158 Contract.Ensures(StorageUrl != null);
159 Contract.Ensures(_baseClient != null);
160 Contract.Ensures(RootAddressUri != null);
161 Contract.EndContractBlock();
163 _baseClient = new RestClient
165 BaseAddress = accountInfo.StorageUri.ToString(),
169 StorageUrl = accountInfo.StorageUri;
170 Token = accountInfo.Token;
171 UserName = accountInfo.UserName;
173 //Get the root address (StorageUrl without the account)
174 var storageUrl = StorageUrl.AbsoluteUri;
175 var usernameIndex = storageUrl.LastIndexOf(UserName);
176 var rootUrl = storageUrl.Substring(0, usernameIndex);
177 RootAddressUri = new Uri(rootUrl);
181 public AccountInfo Authenticate()
183 if (String.IsNullOrWhiteSpace(UserName))
184 throw new InvalidOperationException("UserName is empty");
185 if (String.IsNullOrWhiteSpace(ApiKey))
186 throw new InvalidOperationException("ApiKey is empty");
187 if (String.IsNullOrWhiteSpace(AuthenticationUrl))
188 throw new InvalidOperationException("AuthenticationUrl is empty");
189 Contract.Ensures(!String.IsNullOrWhiteSpace(Token));
190 Contract.Ensures(StorageUrl != null);
191 Contract.Ensures(_baseClient != null);
192 Contract.Ensures(RootAddressUri != null);
193 Contract.EndContractBlock();
196 Log.InfoFormat("[AUTHENTICATE] Start for {0}", UserName);
198 var groups = new List<Group>();
200 using (var authClient = new RestClient{BaseAddress=AuthenticationUrl})
202 /* if (Proxy != null)
203 authClient.Proxy = Proxy;*/
205 Contract.Assume(authClient.Headers!=null);
207 authClient.Headers.Add("X-Auth-User", UserName);
208 authClient.Headers.Add("X-Auth-Key", ApiKey);
209 //TODO: Remove after testing. Added to overcome server auth bug
210 //authClient.Headers.Add("X-Auth-Token", ApiKey);
212 authClient.DownloadStringWithRetry(VersionPath, 3);
214 authClient.AssertStatusOK("Authentication failed");
216 var storageUrl = authClient.GetHeaderValue("X-Storage-Url");
217 if (String.IsNullOrWhiteSpace(storageUrl))
218 throw new InvalidOperationException("Failed to obtain storage url");
220 _baseClient = new RestClient
222 BaseAddress = storageUrl,
228 StorageUrl = new Uri(storageUrl);
230 //Get the root address (StorageUrl without the account)
231 var usernameIndex=storageUrl.LastIndexOf(UserName);
232 var rootUrl = storageUrl.Substring(0, usernameIndex);
233 RootAddressUri = new Uri(rootUrl);
235 var token = authClient.GetHeaderValue("X-Auth-Token");
236 if (String.IsNullOrWhiteSpace(token))
237 throw new InvalidOperationException("Failed to obtain token url");
240 /* var keys = authClient.ResponseHeaders.AllKeys.AsQueryable();
241 groups = (from key in keys
242 where key.StartsWith("X-Account-Group-")
243 let name = key.Substring(16)
244 select new Group(name, authClient.ResponseHeaders[key]))
250 Log.InfoFormat("[AUTHENTICATE] End for {0}", UserName);
251 Debug.Assert(_baseClient!=null);
253 return new AccountInfo {StorageUri = StorageUrl, Token = Token, UserName = UserName,Groups=groups};
259 public IList<ContainerInfo> ListContainers(string account)
261 using (var client = new RestClient(_baseClient))
263 if (!String.IsNullOrWhiteSpace(account))
264 client.BaseAddress = GetAccountUrl(account);
266 client.Parameters.Clear();
267 client.Parameters.Add("format", "json");
268 var content = client.DownloadStringWithRetry("", 3);
269 client.AssertStatusOK("List Containers failed");
271 if (client.StatusCode == HttpStatusCode.NoContent)
272 return new List<ContainerInfo>();
273 var infos = JsonConvert.DeserializeObject<IList<ContainerInfo>>(content);
275 foreach (var info in infos)
277 info.Account = account;
284 private string GetAccountUrl(string account)
286 return new Uri(RootAddressUri, new Uri(account,UriKind.Relative)).AbsoluteUri;
289 public IList<ShareAccountInfo> ListSharingAccounts(DateTime? since=null)
291 using (ThreadContext.Stacks["Share"].Push("List Accounts"))
293 if (Log.IsDebugEnabled) Log.DebugFormat("START");
295 using (var client = new RestClient(_baseClient))
297 client.Parameters.Clear();
298 client.Parameters.Add("format", "json");
299 client.IfModifiedSince = since;
301 //Extract the username from the base address
302 client.BaseAddress = RootAddressUri.AbsoluteUri;
304 var content = client.DownloadStringWithRetry(@"", 3);
306 client.AssertStatusOK("ListSharingAccounts failed");
308 //If the result is empty, return an empty list,
309 var infos = String.IsNullOrWhiteSpace(content)
310 ? new List<ShareAccountInfo>()
311 //Otherwise deserialize the account list into a list of ShareAccountInfos
312 : JsonConvert.DeserializeObject<IList<ShareAccountInfo>>(content);
314 Log.DebugFormat("END");
322 /// Request listing of all objects in a container modified since a specific time.
323 /// If the *since* value is missing, return all objects
325 /// <param name="knownContainers">Use the since variable only for the containers listed in knownContainers. Unknown containers are considered new
326 /// and should be polled anyway
328 /// <param name="since"></param>
329 /// <returns></returns>
330 public IList<ObjectInfo> ListSharedObjects(HashSet<string> knownContainers,DateTime? since = null )
333 using (ThreadContext.Stacks["Share"].Push("List Objects"))
335 if (Log.IsDebugEnabled) Log.DebugFormat("START");
336 //'since' is not used here because we need to have ListObjects return a NoChange result
337 //for all shared accounts,containers
339 Func<ContainerInfo, string> GetKey = c => String.Format("{0}\\{1}", c.Account, c.Name);
341 var accounts = ListSharingAccounts();
342 var containers = (from account in accounts
343 let conts = ListContainers(account.name)
344 from container in conts
345 select container).ToList();
346 var items = from container in containers
347 let actualSince=knownContainers.Contains(GetKey(container))?since:null
348 select ListObjects(container.Account , container.Name, actualSince);
349 var objects=items.SelectMany(r=> r).ToList();
352 //Check parents recursively up to (but not including) the container.
353 //If parents are missing, add them to the list
354 //Need function to calculate all parent URLs
355 objects = AddMissingParents(objects);
357 //Store any new containers
358 foreach (var container in containers)
360 knownContainers.Add(GetKey(container));
365 if (Log.IsDebugEnabled) Log.DebugFormat("END");
370 private List<ObjectInfo> AddMissingParents(List<ObjectInfo> objects)
372 //TODO: Remove short-circuit when we decide to use Missing Parents functionality
375 var existingUris = objects.ToDictionary(o => o.Uri, o => o);
376 foreach (var objectInfo in objects)
378 //Can be null when retrieving objects to show in selective sync
379 if (objectInfo.Name == null)
382 var parts = objectInfo.Name.Split(new[]{'/'},StringSplitOptions.RemoveEmptyEntries);
383 //If there is no parent, skip
384 if (parts.Length == 1)
386 var baseParts = new[]
388 objectInfo.Uri.Host, objectInfo.Uri.Segments[1].TrimEnd('/'),objectInfo.Account,objectInfo.Container
390 for (var partIdx = 0; partIdx < parts.Length - 1; partIdx++)
392 var nameparts = parts.Range(0, partIdx).ToArray();
393 var parentName= String.Join("/", nameparts);
395 var parentParts = baseParts.Concat(nameparts);
396 var parentUrl = objectInfo.Uri.Scheme+ "://" + String.Join("/", parentParts);
398 var parentUri = new Uri(parentUrl, UriKind.Absolute);
400 ObjectInfo existingInfo;
401 if (!existingUris.TryGetValue(parentUri,out existingInfo))
403 var h = parentUrl.GetHashCode();
404 var reverse = new string(parentUrl.Reverse().ToArray());
405 var rh = reverse.GetHashCode();
406 var b1 = BitConverter.GetBytes(h);
407 var b2 = BitConverter.GetBytes(rh);
408 var g = new Guid(0,0,0,b1.Concat(b2).ToArray());
411 existingUris[parentUri] = new ObjectInfo
413 Account = objectInfo.Account,
414 Container = objectInfo.Container,
415 Content_Type = @"application/directory",
416 ETag = Signature.MD5_EMPTY,
417 X_Object_Hash = Signature.MERKLE_EMPTY,
419 StorageUri=objectInfo.StorageUri,
426 return existingUris.Values.ToList();
429 public void SetTags(ObjectInfo target,IDictionary<string,string> tags)
431 if (String.IsNullOrWhiteSpace(Token))
432 throw new InvalidOperationException("The Token is not set");
433 if (StorageUrl == null)
434 throw new InvalidOperationException("The StorageUrl is not set");
436 throw new ArgumentNullException("target");
437 Contract.EndContractBlock();
439 using (ThreadContext.Stacks["Share"].Push("Share Object"))
441 if (Log.IsDebugEnabled) Log.DebugFormat("START");
443 using (var client = new RestClient(_baseClient))
446 client.BaseAddress = GetAccountUrl(target.Account);
448 client.Parameters.Clear();
449 client.Parameters.Add("update", "");
451 foreach (var tag in tags)
453 var headerTag = String.Format("X-Object-Meta-{0}", tag.Key);
454 client.Headers.Add(headerTag, tag.Value);
457 client.DownloadStringWithRetry(target.Container, 3);
460 client.AssertStatusOK("SetTags failed");
461 //If the status is NOT ACCEPTED we have a problem
462 if (client.StatusCode != HttpStatusCode.Accepted)
464 Log.Error("Failed to set tags");
465 throw new Exception("Failed to set tags");
468 if (Log.IsDebugEnabled) Log.DebugFormat("END");
475 public void ShareObject(string account, string container, string objectName, string shareTo, bool read, bool write)
477 if (String.IsNullOrWhiteSpace(Token))
478 throw new InvalidOperationException("The Token is not set");
479 if (StorageUrl==null)
480 throw new InvalidOperationException("The StorageUrl is not set");
481 if (String.IsNullOrWhiteSpace(container))
482 throw new ArgumentNullException("container");
483 if (String.IsNullOrWhiteSpace(objectName))
484 throw new ArgumentNullException("objectName");
485 if (String.IsNullOrWhiteSpace(account))
486 throw new ArgumentNullException("account");
487 if (String.IsNullOrWhiteSpace(shareTo))
488 throw new ArgumentNullException("shareTo");
489 Contract.EndContractBlock();
491 using (ThreadContext.Stacks["Share"].Push("Share Object"))
493 if (Log.IsDebugEnabled) Log.DebugFormat("START");
495 using (var client = new RestClient(_baseClient))
498 client.BaseAddress = GetAccountUrl(account);
500 client.Parameters.Clear();
501 client.Parameters.Add("format", "json");
503 string permission = "";
505 permission = String.Format("write={0}", shareTo);
507 permission = String.Format("read={0}", shareTo);
508 client.Headers.Add("X-Object-Sharing", permission);
510 var content = client.DownloadStringWithRetry(container, 3);
512 client.AssertStatusOK("ShareObject failed");
514 //If the result is empty, return an empty list,
515 var infos = String.IsNullOrWhiteSpace(content)
516 ? new List<ObjectInfo>()
517 //Otherwise deserialize the object list into a list of ObjectInfos
518 : JsonConvert.DeserializeObject<IList<ObjectInfo>>(content);
520 if (Log.IsDebugEnabled) Log.DebugFormat("END");
527 public AccountInfo GetAccountPolicies(AccountInfo accountInfo)
529 if (accountInfo==null)
530 throw new ArgumentNullException("accountInfo");
531 Contract.EndContractBlock();
533 using (ThreadContext.Stacks["Account"].Push("GetPolicies"))
535 if (Log.IsDebugEnabled) Log.DebugFormat("START");
537 using (var client = new RestClient(_baseClient))
539 if (!String.IsNullOrWhiteSpace(accountInfo.UserName))
540 client.BaseAddress = GetAccountUrl(accountInfo.UserName);
542 client.Parameters.Clear();
543 client.Parameters.Add("format", "json");
544 client.Head(String.Empty, 3);
546 var quotaValue=client.ResponseHeaders["X-Account-Policy-Quota"];
547 var bytesValue= client.ResponseHeaders["X-Account-Bytes-Used"];
550 if (long.TryParse(quotaValue, out quota))
551 accountInfo.Quota = quota;
552 if (long.TryParse(bytesValue, out bytes))
553 accountInfo.BytesUsed = bytes;
562 public void UpdateMetadata(ObjectInfo objectInfo)
564 if (objectInfo == null)
565 throw new ArgumentNullException("objectInfo");
566 Contract.EndContractBlock();
568 using (ThreadContext.Stacks["Objects"].Push("UpdateMetadata"))
570 if (Log.IsDebugEnabled) Log.DebugFormat("START");
573 using(var client=new RestClient(_baseClient))
576 client.BaseAddress = GetAccountUrl(objectInfo.Account);
578 client.Parameters.Clear();
582 foreach (var tag in objectInfo.Tags)
584 var headerTag = String.Format("X-Object-Meta-{0}", tag.Key);
585 client.Headers.Add(headerTag, tag.Value);
590 var permissions=objectInfo.GetPermissionString();
591 client.SetNonEmptyHeaderValue("X-Object-Sharing",permissions);
593 client.SetNonEmptyHeaderValue("Content-Disposition",objectInfo.ContendDisposition);
594 client.SetNonEmptyHeaderValue("Content-Encoding",objectInfo.ContentEncoding);
595 client.SetNonEmptyHeaderValue("X-Object-Manifest",objectInfo.Manifest);
596 var isPublic = objectInfo.IsPublic.ToString().ToLower();
597 client.Headers.Add("X-Object-Public", isPublic);
600 /*var uriBuilder = client.GetAddressBuilder(objectInfo.Container, objectInfo.Name);
601 uriBuilder.Query = "update=";
602 var uri = uriBuilder.Uri.MakeRelativeUri(this.RootAddressUri);*/
603 var address = String.Format("{0}/{1}?update=",objectInfo.Container, objectInfo.Name);
604 client.PostWithRetry(address,"application/xml");
606 //client.UploadValues(uri,new NameValueCollection());
609 client.AssertStatusOK("UpdateMetadata failed");
610 //If the status is NOT ACCEPTED or OK we have a problem
611 if (!(client.StatusCode == HttpStatusCode.Accepted || client.StatusCode == HttpStatusCode.OK))
613 Log.Error("Failed to update metadata");
614 throw new Exception("Failed to update metadata");
617 if (Log.IsDebugEnabled) Log.DebugFormat("END");
623 public void UpdateMetadata(ContainerInfo containerInfo)
625 if (containerInfo == null)
626 throw new ArgumentNullException("containerInfo");
627 Contract.EndContractBlock();
629 using (ThreadContext.Stacks["Containers"].Push("UpdateMetadata"))
631 if (Log.IsDebugEnabled) Log.DebugFormat("START");
634 using(var client=new RestClient(_baseClient))
637 client.BaseAddress = GetAccountUrl(containerInfo.Account);
639 client.Parameters.Clear();
643 foreach (var tag in containerInfo.Tags)
645 var headerTag = String.Format("X-Container-Meta-{0}", tag.Key);
646 client.Headers.Add(headerTag, tag.Value);
651 foreach (var policy in containerInfo.Policies)
653 var headerPolicy = String.Format("X-Container-Policy-{0}", policy.Key);
654 client.Headers.Add(headerPolicy, policy.Value);
658 var uriBuilder = client.GetAddressBuilder(containerInfo.Name,"");
659 var uri = uriBuilder.Uri;
661 client.UploadValues(uri,new NameValueCollection());
664 client.AssertStatusOK("UpdateMetadata failed");
665 //If the status is NOT ACCEPTED or OK we have a problem
666 if (!(client.StatusCode == HttpStatusCode.Accepted || client.StatusCode == HttpStatusCode.OK))
668 Log.Error("Failed to update metadata");
669 throw new Exception("Failed to update metadata");
672 if (Log.IsDebugEnabled) Log.DebugFormat("END");
679 public IList<ObjectInfo> ListObjects(string account, string container, DateTime? since = null)
681 if (String.IsNullOrWhiteSpace(container))
682 throw new ArgumentNullException("container");
683 Contract.EndContractBlock();
685 using (ThreadContext.Stacks["Objects"].Push("List"))
687 if (Log.IsDebugEnabled) Log.DebugFormat("START");
689 using (var client = new RestClient(_baseClient))
691 if (!String.IsNullOrWhiteSpace(account))
692 client.BaseAddress = GetAccountUrl(account);
694 client.Parameters.Clear();
695 client.Parameters.Add("format", "json");
696 client.IfModifiedSince = since;
697 var content = client.DownloadStringWithRetry(container, 3);
699 client.AssertStatusOK("ListObjects failed");
701 if (client.StatusCode==HttpStatusCode.NotModified)
702 return new[]{new NoModificationInfo(account,container)};
703 //If the result is empty, return an empty list,
704 var infos = String.IsNullOrWhiteSpace(content)
705 ? new List<ObjectInfo>()
706 //Otherwise deserialize the object list into a list of ObjectInfos
707 : JsonConvert.DeserializeObject<IList<ObjectInfo>>(content);
709 foreach (var info in infos)
711 info.Container = container;
712 info.Account = account;
713 info.StorageUri = this.StorageUrl;
715 if (Log.IsDebugEnabled) Log.DebugFormat("END");
721 public IList<ObjectInfo> ListObjects(string account, string container, string folder, DateTime? since = null)
723 if (String.IsNullOrWhiteSpace(container))
724 throw new ArgumentNullException("container");
726 if (String.IsNullOrWhiteSpace(folder))
727 throw new ArgumentNullException("folder");
729 Contract.EndContractBlock();
731 using (ThreadContext.Stacks["Objects"].Push("List"))
733 if (Log.IsDebugEnabled) Log.DebugFormat("START");
735 using (var client = new RestClient(_baseClient))
737 if (!String.IsNullOrWhiteSpace(account))
738 client.BaseAddress = GetAccountUrl(account);
740 client.Parameters.Clear();
741 client.Parameters.Add("format", "json");
742 client.Parameters.Add("path", folder);
743 client.IfModifiedSince = since;
744 var content = client.DownloadStringWithRetry(container, 3);
745 client.AssertStatusOK("ListObjects failed");
747 if (client.StatusCode==HttpStatusCode.NotModified)
748 return new[]{new NoModificationInfo(account,container,folder)};
750 var infos = JsonConvert.DeserializeObject<IList<ObjectInfo>>(content);
751 foreach (var info in infos)
753 info.Account = account;
754 if (info.Container == null)
755 info.Container = container;
756 info.StorageUri = this.StorageUrl;
758 if (Log.IsDebugEnabled) Log.DebugFormat("END");
765 public bool ContainerExists(string account, string container)
767 if (String.IsNullOrWhiteSpace(container))
768 throw new ArgumentNullException("container", "The container property can't be empty");
769 Contract.EndContractBlock();
771 using (ThreadContext.Stacks["Containters"].Push("Exists"))
773 if (Log.IsDebugEnabled) Log.DebugFormat("START");
775 using (var client = new RestClient(_baseClient))
777 if (!String.IsNullOrWhiteSpace(account))
778 client.BaseAddress = GetAccountUrl(account);
780 client.Parameters.Clear();
781 client.Head(container, 3);
784 switch (client.StatusCode)
786 case HttpStatusCode.OK:
787 case HttpStatusCode.NoContent:
790 case HttpStatusCode.NotFound:
794 throw CreateWebException("ContainerExists", client.StatusCode);
796 if (Log.IsDebugEnabled) Log.DebugFormat("END");
804 public bool ObjectExists(string account, string container, string objectName)
806 if (String.IsNullOrWhiteSpace(container))
807 throw new ArgumentNullException("container", "The container property can't be empty");
808 if (String.IsNullOrWhiteSpace(objectName))
809 throw new ArgumentNullException("objectName", "The objectName property can't be empty");
810 Contract.EndContractBlock();
812 using (var client = new RestClient(_baseClient))
814 if (!String.IsNullOrWhiteSpace(account))
815 client.BaseAddress = GetAccountUrl(account);
817 client.Parameters.Clear();
818 client.Head(container + "/" + objectName, 3);
820 switch (client.StatusCode)
822 case HttpStatusCode.OK:
823 case HttpStatusCode.NoContent:
825 case HttpStatusCode.NotFound:
828 throw CreateWebException("ObjectExists", client.StatusCode);
834 public ObjectInfo GetObjectInfo(string account, string container, string objectName)
836 if (String.IsNullOrWhiteSpace(container))
837 throw new ArgumentNullException("container", "The container property can't be empty");
838 if (String.IsNullOrWhiteSpace(objectName))
839 throw new ArgumentNullException("objectName", "The objectName property can't be empty");
840 Contract.EndContractBlock();
842 using (ThreadContext.Stacks["Objects"].Push("GetObjectInfo"))
845 using (var client = new RestClient(_baseClient))
847 if (!String.IsNullOrWhiteSpace(account))
848 client.BaseAddress = GetAccountUrl(account);
851 client.Parameters.Clear();
853 client.Head(container + "/" + objectName, 3);
856 return ObjectInfo.Empty;
858 switch (client.StatusCode)
860 case HttpStatusCode.OK:
861 case HttpStatusCode.NoContent:
862 var keys = client.ResponseHeaders.AllKeys.AsQueryable();
863 var tags = client.GetMeta("X-Object-Meta-");
864 var extensions = (from key in keys
865 where key.StartsWith("X-Object-") && !key.StartsWith("X-Object-Meta-")
866 select new {Name = key, Value = client.ResponseHeaders[key]})
867 .ToDictionary(t => t.Name, t => t.Value);
869 var permissions=client.GetHeaderValue("X-Object-Sharing", true);
872 var info = new ObjectInfo
875 Container = container,
877 ETag = client.GetHeaderValue("ETag"),
878 UUID=client.GetHeaderValue("X-Object-UUID"),
879 X_Object_Hash = client.GetHeaderValue("X-Object-Hash"),
880 Content_Type = client.GetHeaderValue("Content-Type"),
881 Bytes = Convert.ToInt64(client.GetHeaderValue("Content-Length",true)),
883 Last_Modified = client.LastModified,
884 Extensions = extensions,
885 ContentEncoding=client.GetHeaderValue("Content-Encoding",true),
886 ContendDisposition = client.GetHeaderValue("Content-Disposition",true),
887 Manifest=client.GetHeaderValue("X-Object-Manifest",true),
888 PublicUrl=client.GetHeaderValue("X-Object-Public",true),
889 StorageUri=this.StorageUrl,
891 info.SetPermissions(permissions);
893 case HttpStatusCode.NotFound:
894 return ObjectInfo.Empty;
896 throw new WebException(
897 String.Format("[FAIL] GetObjectInfo for {0} failed with unexpected status code {1}",
898 objectName, client.StatusCode));
902 catch (RetryException)
904 Log.WarnFormat("[RETRY FAIL] GetObjectInfo for {0} failed.",objectName);
905 return ObjectInfo.Empty;
907 catch (WebException e)
910 String.Format("[FAIL] GetObjectInfo for {0} failed with unexpected status code {1}",
911 objectName, client.StatusCode), e);
919 public void CreateFolder(string account, string container, string folder)
921 if (String.IsNullOrWhiteSpace(container))
922 throw new ArgumentNullException("container", "The container property can't be empty");
923 if (String.IsNullOrWhiteSpace(folder))
924 throw new ArgumentNullException("folder", "The folder property can't be empty");
925 Contract.EndContractBlock();
927 var folderUrl=String.Format("{0}/{1}",container,folder);
928 using (var client = new RestClient(_baseClient))
930 if (!String.IsNullOrWhiteSpace(account))
931 client.BaseAddress = GetAccountUrl(account);
933 client.Parameters.Clear();
934 client.Headers.Add("Content-Type", @"application/directory");
935 client.Headers.Add("Content-Length", "0");
936 client.PutWithRetry(folderUrl, 3);
938 if (client.StatusCode != HttpStatusCode.Created && client.StatusCode != HttpStatusCode.Accepted)
939 throw CreateWebException("CreateFolder", client.StatusCode);
945 public ContainerInfo GetContainerInfo(string account, string container)
947 if (String.IsNullOrWhiteSpace(container))
948 throw new ArgumentNullException("container", "The container property can't be empty");
949 Contract.EndContractBlock();
951 using (var client = new RestClient(_baseClient))
953 if (!String.IsNullOrWhiteSpace(account))
954 client.BaseAddress = GetAccountUrl(account);
956 client.Head(container);
957 switch (client.StatusCode)
959 case HttpStatusCode.OK:
960 case HttpStatusCode.NoContent:
961 var tags = client.GetMeta("X-Container-Meta-");
962 var policies = client.GetMeta("X-Container-Policy-");
964 var containerInfo = new ContainerInfo
968 StorageUrl=this.StorageUrl.ToString(),
970 long.Parse(client.GetHeaderValue("X-Container-Object-Count")),
971 Bytes = long.Parse(client.GetHeaderValue("X-Container-Bytes-Used")),
972 BlockHash = client.GetHeaderValue("X-Container-Block-Hash"),
973 BlockSize=int.Parse(client.GetHeaderValue("X-Container-Block-Size")),
974 Last_Modified=client.LastModified,
980 return containerInfo;
981 case HttpStatusCode.NotFound:
982 return ContainerInfo.Empty;
984 throw CreateWebException("GetContainerInfo", client.StatusCode);
989 public void CreateContainer(string account, string container)
991 if (String.IsNullOrWhiteSpace(account))
992 throw new ArgumentNullException("account");
993 if (String.IsNullOrWhiteSpace(container))
994 throw new ArgumentNullException("container");
995 Contract.EndContractBlock();
997 using (var client = new RestClient(_baseClient))
999 if (!String.IsNullOrWhiteSpace(account))
1000 client.BaseAddress = GetAccountUrl(account);
1002 client.PutWithRetry(container, 3);
1003 var expectedCodes = new[] {HttpStatusCode.Created, HttpStatusCode.Accepted, HttpStatusCode.OK};
1004 if (!expectedCodes.Contains(client.StatusCode))
1005 throw CreateWebException("CreateContainer", client.StatusCode);
1009 public void DeleteContainer(string account, string container)
1011 if (String.IsNullOrWhiteSpace(container))
1012 throw new ArgumentNullException("container", "The container property can't be empty");
1013 Contract.EndContractBlock();
1015 using (var client = new RestClient(_baseClient))
1017 if (!String.IsNullOrWhiteSpace(account))
1018 client.BaseAddress = GetAccountUrl(account);
1020 client.DeleteWithRetry(container, 3);
1021 var expectedCodes = new[] {HttpStatusCode.NotFound, HttpStatusCode.NoContent};
1022 if (!expectedCodes.Contains(client.StatusCode))
1023 throw CreateWebException("DeleteContainer", client.StatusCode);
1031 /// <param name="account"></param>
1032 /// <param name="container"></param>
1033 /// <param name="objectName"></param>
1034 /// <param name="fileName"></param>
1035 /// <returns></returns>
1036 /// <remarks>This method should have no timeout or a very long one</remarks>
1037 //Asynchronously download the object specified by *objectName* in a specific *container* to
1039 public async Task GetObject(string account, string container, string objectName, string fileName,CancellationToken cancellationToken)
1041 if (String.IsNullOrWhiteSpace(container))
1042 throw new ArgumentNullException("container", "The container property can't be empty");
1043 if (String.IsNullOrWhiteSpace(objectName))
1044 throw new ArgumentNullException("objectName", "The objectName property can't be empty");
1045 Contract.EndContractBlock();
1049 //WebClient, and by extension RestClient, are not thread-safe. Create a new RestClient
1050 //object to avoid concurrency errors.
1052 //Download operations take a long time therefore they have no timeout.
1053 using(var client = new RestClient(_baseClient) { Timeout = 0 })
1055 if (!String.IsNullOrWhiteSpace(account))
1056 client.BaseAddress = GetAccountUrl(account);
1058 //The container and objectName are relative names. They are joined with the client's
1059 //BaseAddress to create the object's absolute address
1060 var builder = client.GetAddressBuilder(container, objectName);
1061 var uri = builder.Uri;
1063 //Download progress is reported to the Trace log
1064 Log.InfoFormat("[GET] START {0}", objectName);
1065 /*client.DownloadProgressChanged += (sender, args) =>
1066 Log.InfoFormat("[GET PROGRESS] {0} {1}% {2} of {3}",
1067 fileName, args.ProgressPercentage,
1069 args.TotalBytesToReceive);*/
1070 var progress = new Progress<DownloadProgressChangedEventArgs>(args =>
1072 Log.InfoFormat("[GET PROGRESS] {0} {1}% {2} of {3}",
1073 fileName, args.ProgressPercentage,
1075 args.TotalBytesToReceive);
1076 if (DownloadProgressChanged!=null)
1077 DownloadProgressChanged(this, args);
1080 //Start downloading the object asynchronously
1081 await client.DownloadFileTaskAsync(uri, fileName, cancellationToken,progress).ConfigureAwait(false);
1083 //Once the download completes
1084 //Delete the local client object
1086 //And report failure or completion
1088 catch (Exception exc)
1090 Log.ErrorFormat("[GET] FAIL {0} with {1}", objectName, exc);
1094 Log.InfoFormat("[GET] END {0}", objectName);
1099 public Task<IList<string>> PutHashMap(string account, string container, string objectName, TreeHash hash)
1101 if (String.IsNullOrWhiteSpace(container))
1102 throw new ArgumentNullException("container");
1103 if (String.IsNullOrWhiteSpace(objectName))
1104 throw new ArgumentNullException("objectName");
1106 throw new ArgumentNullException("hash");
1107 if (String.IsNullOrWhiteSpace(Token))
1108 throw new InvalidOperationException("Invalid Token");
1109 if (StorageUrl == null)
1110 throw new InvalidOperationException("Invalid Storage Url");
1111 Contract.EndContractBlock();
1114 //Don't use a timeout because putting the hashmap may be a long process
1115 var client = new RestClient(_baseClient) { Timeout = 0 };
1116 if (!String.IsNullOrWhiteSpace(account))
1117 client.BaseAddress = GetAccountUrl(account);
1119 //The container and objectName are relative names. They are joined with the client's
1120 //BaseAddress to create the object's absolute address
1121 var builder = client.GetAddressBuilder(container, objectName);
1122 builder.Query = "format=json&hashmap";
1123 var uri = builder.Uri;
1126 //Send the tree hash as Json to the server
1127 client.Headers[HttpRequestHeader.ContentType] = "application/octet-stream";
1128 var jsonHash = hash.ToJson();
1130 client.Headers.Add("ETag",hash.MD5);
1131 var uploadTask=client.UploadStringTask(uri, "PUT", jsonHash);
1132 if (Log.IsDebugEnabled)
1133 Log.DebugFormat("Hashes:\r\n{0}", jsonHash);
1134 return uploadTask.ContinueWith(t =>
1137 var empty = (IList<string>)new List<string>();
1140 //The server will respond either with 201-created if all blocks were already on the server
1141 if (client.StatusCode == HttpStatusCode.Created)
1143 //in which case we return an empty hash list
1146 //or with a 409-conflict and return the list of missing parts
1147 //A 409 will cause an exception so we need to check t.IsFaulted to avoid propagating the exception
1150 var ex = t.Exception.InnerException;
1151 var we = ex as WebException;
1152 var response = we.Response as HttpWebResponse;
1153 if (response!=null && response.StatusCode==HttpStatusCode.Conflict)
1155 //In case of 409 the missing parts will be in the response content
1156 using (var stream = response.GetResponseStream())
1157 using(var reader=stream.GetLoggedReader(Log))
1159 //We used to have to cleanup the content before returning it because it contains
1160 //error content after the list of hashes
1162 //As of 30/1/2012, the result is a proper Json array so we don't need to read the content
1165 var serializer = new JsonSerializer();
1166 serializer.Error += (sender, args) => Log.ErrorFormat("Deserialization error at [{0}] [{1}]", args.ErrorContext.Error, args.ErrorContext.Member);
1167 var hashes = (List<string>)serializer.Deserialize(reader, typeof(List<string>));
1171 //Any other status code is unexpected and the exception should be rethrown
1172 Log.LogError(response);
1177 //Any other status code is unexpected but there was no exception. We can probably continue processing
1178 Log.WarnFormat("Unexcpected status code when putting map: {0} - {1}",client.StatusCode,client.StatusDescription);
1186 public async Task<byte[]> GetBlock(string account, string container, Uri relativeUrl, long start, long? end, CancellationToken cancellationToken)
1188 if (String.IsNullOrWhiteSpace(Token))
1189 throw new InvalidOperationException("Invalid Token");
1190 if (StorageUrl == null)
1191 throw new InvalidOperationException("Invalid Storage Url");
1192 if (String.IsNullOrWhiteSpace(container))
1193 throw new ArgumentNullException("container");
1194 if (relativeUrl == null)
1195 throw new ArgumentNullException("relativeUrl");
1196 if (end.HasValue && end < 0)
1197 throw new ArgumentOutOfRangeException("end");
1199 throw new ArgumentOutOfRangeException("start");
1200 Contract.EndContractBlock();
1202 //Don't use a timeout because putting the hashmap may be a long process
1203 using (var client = new RestClient(_baseClient) {Timeout = 0, RangeFrom = start, RangeTo = end})
1205 if (!String.IsNullOrWhiteSpace(account))
1206 client.BaseAddress = GetAccountUrl(account);
1208 var builder = client.GetAddressBuilder(container, relativeUrl.ToString());
1209 var uri = builder.Uri;
1211 /* client.DownloadProgressChanged += (sender, args) =>
1213 Log.DebugFormat("[GET PROGRESS] {0} {1}% {2} of {3}",
1214 uri.Segments.Last(), args.ProgressPercentage,
1216 args.TotalBytesToReceive);
1217 DownloadProgressChanged(sender, args);
1219 var progress = new Progress<DownloadProgressChangedEventArgs>(args =>
1221 Log.DebugFormat("[GET PROGRESS] {0} {1}% {2} of {3}",
1222 uri.Segments.Last(), args.ProgressPercentage,
1224 args.TotalBytesToReceive);
1225 if (DownloadProgressChanged!=null)
1226 DownloadProgressChanged(this, args);
1230 var result = await client.DownloadDataTaskAsync(uri, cancellationToken,progress).ConfigureAwait(false);
1235 public event UploadProgressChangedEventHandler UploadProgressChanged;
1236 public event DownloadProgressChangedEventHandler DownloadProgressChanged;
1238 public async Task PostBlock(string account, string container, byte[] block, int offset, int count,CancellationToken token)
1240 if (String.IsNullOrWhiteSpace(container))
1241 throw new ArgumentNullException("container");
1243 throw new ArgumentNullException("block");
1244 if (offset < 0 || offset >= block.Length)
1245 throw new ArgumentOutOfRangeException("offset");
1246 if (count < 0 || count > block.Length)
1247 throw new ArgumentOutOfRangeException("count");
1248 if (String.IsNullOrWhiteSpace(Token))
1249 throw new InvalidOperationException("Invalid Token");
1250 if (StorageUrl == null)
1251 throw new InvalidOperationException("Invalid Storage Url");
1252 Contract.EndContractBlock();
1258 //Don't use a timeout because putting the hashmap may be a long process
1259 using (var client = new RestClient(_baseClient) { Timeout = 0 })
1261 if (!String.IsNullOrWhiteSpace(account))
1262 client.BaseAddress = GetAccountUrl(account);
1264 token.Register(client.CancelAsync);
1266 var builder = client.GetAddressBuilder(container, "");
1267 //We are doing an update
1268 builder.Query = "update";
1269 var uri = builder.Uri;
1271 client.Headers[HttpRequestHeader.ContentType] = "application/octet-stream";
1273 Log.InfoFormat("[BLOCK POST] START");
1276 client.UploadProgressChanged += (sender, args) =>
1278 Log.InfoFormat("[BLOCK POST PROGRESS] {0}% {1} of {2}",
1279 args.ProgressPercentage, args.BytesSent,
1280 args.TotalBytesToSend);
1281 UploadProgressChanged(sender, args);
1284 client.UploadFileCompleted += (sender, args) =>
1285 Log.InfoFormat("[BLOCK POST PROGRESS] Completed ");
1287 var progress=new Progress<UploadProgressChangedEventArgs>(args=>
1289 Log.InfoFormat("[BLOCK POST PROGRESS] {0}% {1} of {2}",
1290 args.ProgressPercentage, args.BytesSent,
1291 args.TotalBytesToSend);
1292 if (UploadProgressChanged!=null)
1293 UploadProgressChanged(this, args);
1295 var buffer = new byte[count];
1296 Buffer.BlockCopy(block, offset, buffer, 0, count);
1298 await client.UploadDataTaskAsync(uri, "POST", buffer,token,progress).ConfigureAwait(false);
1299 Log.InfoFormat("[BLOCK POST] END");
1302 catch (TaskCanceledException )
1304 Log.Info("Aborting block");
1307 catch (Exception exc)
1309 Log.ErrorFormat("[BLOCK POST] FAIL with \r{0}", exc);
1315 public async Task<TreeHash> GetHashMap(string account, string container, string objectName)
1317 if (String.IsNullOrWhiteSpace(container))
1318 throw new ArgumentNullException("container");
1319 if (String.IsNullOrWhiteSpace(objectName))
1320 throw new ArgumentNullException("objectName");
1321 if (String.IsNullOrWhiteSpace(Token))
1322 throw new InvalidOperationException("Invalid Token");
1323 if (StorageUrl == null)
1324 throw new InvalidOperationException("Invalid Storage Url");
1325 Contract.EndContractBlock();
1329 //WebClient, and by extension RestClient, are not thread-safe. Create a new RestClient
1330 //object to avoid concurrency errors.
1332 //Download operations take a long time therefore they have no timeout.
1333 //TODO: Do they really? this is a hashmap operation, not a download
1335 //Start downloading the object asynchronously
1336 using (var client = new RestClient(_baseClient) { Timeout = 0 })
1338 if (!String.IsNullOrWhiteSpace(account))
1339 client.BaseAddress = GetAccountUrl(account);
1341 //The container and objectName are relative names. They are joined with the client's
1342 //BaseAddress to create the object's absolute address
1343 var builder = client.GetAddressBuilder(container, objectName);
1344 builder.Query = "format=json&hashmap";
1345 var uri = builder.Uri;
1348 var json = await client.DownloadStringTaskAsync(uri).ConfigureAwait(false);
1349 var treeHash = TreeHash.Parse(json);
1350 Log.InfoFormat("[GET HASH] END {0}", objectName);
1354 catch (Exception exc)
1356 Log.ErrorFormat("[GET HASH] END {0} with {1}", objectName, exc);
1366 /// <param name="account"></param>
1367 /// <param name="container"></param>
1368 /// <param name="objectName"></param>
1369 /// <param name="fileName"></param>
1370 /// <param name="hash">Optional hash value for the file. If no hash is provided, the method calculates a new hash</param>
1371 /// <remarks>>This method should have no timeout or a very long one</remarks>
1372 public async Task PutObject(string account, string container, string objectName, string fileName, string hash = null, string contentType = "application/octet-stream")
1374 if (String.IsNullOrWhiteSpace(container))
1375 throw new ArgumentNullException("container", "The container property can't be empty");
1376 if (String.IsNullOrWhiteSpace(objectName))
1377 throw new ArgumentNullException("objectName", "The objectName property can't be empty");
1378 if (String.IsNullOrWhiteSpace(fileName))
1379 throw new ArgumentNullException("fileName", "The fileName property can't be empty");
1381 if (!File.Exists(fileName) && !Directory.Exists(fileName))
1382 throw new FileNotFoundException("The file or directory does not exist",fileName);
1388 using (var client = new RestClient(_baseClient) { Timeout = 0 })
1390 if (!String.IsNullOrWhiteSpace(account))
1391 client.BaseAddress = GetAccountUrl(account);
1393 var builder = client.GetAddressBuilder(container, objectName);
1394 var uri = builder.Uri;
1396 string etag = hash ?? CalculateHash(fileName);
1398 client.Headers.Add("Content-Type", contentType);
1399 client.Headers.Add("ETag", etag);
1402 Log.InfoFormat("[PUT] START {0}", objectName);
1403 client.UploadProgressChanged += (sender, args) =>
1405 using (ThreadContext.Stacks["PUT"].Push("Progress"))
1407 Log.InfoFormat("{0} {1}% {2} of {3}", fileName,
1408 args.ProgressPercentage,
1409 args.BytesSent, args.TotalBytesToSend);
1413 client.UploadFileCompleted += (sender, args) =>
1415 using (ThreadContext.Stacks["PUT"].Push("Progress"))
1417 Log.InfoFormat("Completed {0}", fileName);
1420 if (contentType=="application/directory")
1421 await client.UploadDataTaskAsync(uri, "PUT", new byte[0]).ConfigureAwait(false);
1423 await client.UploadFileTaskAsync(uri, "PUT", fileName).ConfigureAwait(false);
1426 Log.InfoFormat("[PUT] END {0}", objectName);
1428 catch (Exception exc)
1430 Log.ErrorFormat("[PUT] END {0} with {1}", objectName, exc);
1437 private static string CalculateHash(string fileName)
1439 Contract.Requires(!String.IsNullOrWhiteSpace(fileName));
1440 Contract.EndContractBlock();
1443 using (var hasher = MD5.Create())
1444 using(var stream=File.OpenRead(fileName))
1446 var hashBuilder=new StringBuilder();
1447 foreach (byte b in hasher.ComputeHash(stream))
1448 hashBuilder.Append(b.ToString("x2").ToLower());
1449 hash = hashBuilder.ToString();
1454 public void MoveObject(string account, string sourceContainer, string oldObjectName, string targetContainer, string newObjectName)
1456 if (String.IsNullOrWhiteSpace(sourceContainer))
1457 throw new ArgumentNullException("sourceContainer", "The container property can't be empty");
1458 if (String.IsNullOrWhiteSpace(oldObjectName))
1459 throw new ArgumentNullException("oldObjectName", "The oldObjectName property can't be empty");
1460 if (String.IsNullOrWhiteSpace(targetContainer))
1461 throw new ArgumentNullException("targetContainer", "The container property can't be empty");
1462 if (String.IsNullOrWhiteSpace(newObjectName))
1463 throw new ArgumentNullException("newObjectName", "The newObjectName property can't be empty");
1464 Contract.EndContractBlock();
1466 var targetUrl = targetContainer + "/" + newObjectName;
1467 var sourceUrl = String.Format("/{0}/{1}", sourceContainer, oldObjectName);
1469 using (var client = new RestClient(_baseClient))
1471 if (!String.IsNullOrWhiteSpace(account))
1472 client.BaseAddress = GetAccountUrl(account);
1474 client.Headers.Add("X-Move-From", sourceUrl);
1475 client.PutWithRetry(targetUrl, 3);
1477 var expectedCodes = new[] {HttpStatusCode.OK, HttpStatusCode.NoContent, HttpStatusCode.Created};
1478 if (!expectedCodes.Contains(client.StatusCode))
1479 throw CreateWebException("MoveObject", client.StatusCode);
1483 public void DeleteObject(string account, string sourceContainer, string objectName, bool isDirectory)
1485 if (String.IsNullOrWhiteSpace(sourceContainer))
1486 throw new ArgumentNullException("sourceContainer", "The container property can't be empty");
1487 if (String.IsNullOrWhiteSpace(objectName))
1488 throw new ArgumentNullException("objectName", "The oldObjectName property can't be empty");
1489 Contract.EndContractBlock();
1491 var targetUrl = FolderConstants.TrashContainer + "/" + objectName;
1494 targetUrl = targetUrl + "?delimiter=/";
1497 var sourceUrl = String.Format("/{0}/{1}", sourceContainer, objectName);
1499 using (var client = new RestClient(_baseClient))
1501 if (!String.IsNullOrWhiteSpace(account))
1502 client.BaseAddress = GetAccountUrl(account);
1504 client.Headers.Add("X-Move-From", sourceUrl);
1505 client.AllowedStatusCodes.Add(HttpStatusCode.NotFound);
1506 Log.InfoFormat("[TRASH] [{0}] to [{1}]",sourceUrl,targetUrl);
1507 client.PutWithRetry(targetUrl, 3);
1509 var expectedCodes = new[] {HttpStatusCode.OK, HttpStatusCode.NoContent, HttpStatusCode.Created,HttpStatusCode.NotFound};
1510 if (!expectedCodes.Contains(client.StatusCode))
1511 throw CreateWebException("DeleteObject", client.StatusCode);
1516 private static WebException CreateWebException(string operation, HttpStatusCode statusCode)
1518 return new WebException(String.Format("{0} failed with unexpected status code {1}", operation, statusCode));
1523 public IEnumerable<ObjectInfo> ListDirectories(ContainerInfo container)
1525 var directories=this.ListObjects(container.Account, container.Name, "/");
1529 public bool CanUpload(string account, ObjectInfo cloudFile)
1531 Contract.Requires(!String.IsNullOrWhiteSpace(account));
1532 Contract.Requires(cloudFile!=null);
1534 using (var client = new RestClient(_baseClient))
1536 if (!String.IsNullOrWhiteSpace(account))
1537 client.BaseAddress = GetAccountUrl(account);
1540 var parts = cloudFile.Name.Split('/');
1541 var folder = String.Join("/", parts,0,parts.Length-1);
1543 var fileUrl=String.Format("{0}/{1}/{2}.pithos.ignore",cloudFile.Container,folder,Guid.NewGuid());
1545 client.Parameters.Clear();
1548 client.PutWithRetry(fileUrl, 3, @"application/octet-stream");
1550 var expectedCodes = new[] { HttpStatusCode.OK, HttpStatusCode.NoContent, HttpStatusCode.Created};
1551 var result=(expectedCodes.Contains(client.StatusCode));
1552 DeleteObject(account, cloudFile.Container, fileUrl, cloudFile.IsDirectory);
1563 public class ShareAccountInfo
1565 public DateTime? last_modified { get; set; }
1566 public string name { get; set; }