Added header to all files. Closes #2064
[pithos-ms-client] / trunk / Pithos.Network / CloudFilesClient.cs
1 #region
2 /* -----------------------------------------------------------------------
3  * <copyright file="CloudFilesClient.cs" company="GRNet">
4  * 
5  * Copyright 2011-2012 GRNET S.A. All rights reserved.
6  *
7  * Redistribution and use in source and binary forms, with or
8  * without modification, are permitted provided that the following
9  * conditions are met:
10  *
11  *   1. Redistributions of source code must retain the above
12  *      copyright notice, this list of conditions and the following
13  *      disclaimer.
14  *
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.
19  *
20  *
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.
33  *
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.
38  * </copyright>
39  * -----------------------------------------------------------------------
40  */
41 #endregion
42
43 // **CloudFilesClient** provides a simple client interface to CloudFiles and Pithos
44 //
45 // The class provides methods to upload/download files, delete files, manage containers
46
47
48 using System;
49 using System.Collections.Generic;
50 using System.Collections.Specialized;
51 using System.ComponentModel.Composition;
52 using System.Diagnostics;
53 using System.Diagnostics.Contracts;
54 using System.IO;
55 using System.Linq;
56 using System.Net;
57 using System.Security.Cryptography;
58 using System.Text;
59 using System.Threading.Tasks;
60 using Newtonsoft.Json;
61 using Pithos.Interfaces;
62 using log4net;
63
64 namespace Pithos.Network
65 {
66     [Export(typeof(ICloudClient))]
67     public class CloudFilesClient:ICloudClient
68     {
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;
72         
73
74         //During authentication the client provides a UserName 
75         public string UserName { get; set; }
76         
77         //and and ApiKey to the server
78         public string ApiKey { get; set; }
79         
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;
83         public string Token
84         {
85             get { return _token; }
86             set
87             {
88                 _token = value;
89                 _baseClient.Headers["X-Auth-Token"] = value;
90             }
91         }
92
93         //The client also receives a StorageUrl after authentication. All subsequent operations must
94         //use this url
95         public Uri StorageUrl { get; set; }
96
97
98         protected Uri RootAddressUri { get; set; }
99
100        /* private WebProxy _proxy;
101         public WebProxy Proxy
102         {
103             get { return _proxy; }
104             set
105             {
106                 _proxy = value;
107                 if (_baseClient != null)
108                     _baseClient.Proxy = value;                
109             }
110         }
111 */
112
113         /* private Uri _proxy;
114         public Uri Proxy
115         {
116             get { return _proxy; }
117             set
118             {
119                 _proxy = value;
120                 if (_baseClient != null)
121                     _baseClient.Proxy = new WebProxy(value);                
122             }
123         }*/
124
125         public double DownloadPercentLimit { get; set; }
126         public double UploadPercentLimit { get; set; }
127
128         public string AuthenticationUrl { get; set; }
129
130  
131         public string VersionPath
132         {
133             get { return UsePithos ? "v1" : "v1.0"; }
134         }
135
136         public bool UsePithos { get; set; }
137
138
139         private static readonly ILog Log = LogManager.GetLogger("CloudFilesClient");
140
141         public CloudFilesClient(string userName, string apiKey)
142         {
143             UserName = userName;
144             ApiKey = apiKey;
145         }
146
147         public CloudFilesClient(AccountInfo accountInfo)
148         {
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();          
156
157             _baseClient = new RestClient
158             {
159                 BaseAddress = accountInfo.StorageUri.ToString(),
160                 Timeout = 10000,
161                 Retries = 3,
162             };
163             StorageUrl = accountInfo.StorageUri;
164             Token = accountInfo.Token;
165             UserName = accountInfo.UserName;
166
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);
172         }
173
174
175         public AccountInfo Authenticate()
176         {
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();
188
189
190             Log.InfoFormat("[AUTHENTICATE] Start for {0}", UserName);
191
192             var groups = new List<Group>();
193
194             using (var authClient = new RestClient{BaseAddress=AuthenticationUrl})
195             {                
196                /* if (Proxy != null)
197                     authClient.Proxy = Proxy;*/
198
199                 Contract.Assume(authClient.Headers!=null);
200
201                 authClient.Headers.Add("X-Auth-User", UserName);
202                 authClient.Headers.Add("X-Auth-Key", ApiKey);
203
204                 authClient.DownloadStringWithRetry(VersionPath, 3);
205
206                 authClient.AssertStatusOK("Authentication failed");
207
208                 var storageUrl = authClient.GetHeaderValue("X-Storage-Url");
209                 if (String.IsNullOrWhiteSpace(storageUrl))
210                     throw new InvalidOperationException("Failed to obtain storage url");
211                 
212                 _baseClient = new RestClient
213                 {
214                     BaseAddress = storageUrl,
215                     Timeout = 10000,
216                     Retries = 3,
217                     //Proxy=Proxy
218                 };
219
220                 StorageUrl = new Uri(storageUrl);
221                 
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);
226                 
227                 var token = authClient.GetHeaderValue("X-Auth-Token");
228                 if (String.IsNullOrWhiteSpace(token))
229                     throw new InvalidOperationException("Failed to obtain token url");
230                 Token = token;
231
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]))
237                         .ToList();
238                     
239 */
240             }
241
242             Log.InfoFormat("[AUTHENTICATE] End for {0}", UserName);
243             
244
245             return new AccountInfo {StorageUri = StorageUrl, Token = Token, UserName = UserName,Groups=groups};            
246
247         }
248
249
250
251         public IList<ContainerInfo> ListContainers(string account)
252         {
253             using (var client = new RestClient(_baseClient))
254             {
255                 if (!String.IsNullOrWhiteSpace(account))
256                     client.BaseAddress = GetAccountUrl(account);
257                 
258                 client.Parameters.Clear();
259                 client.Parameters.Add("format", "json");
260                 var content = client.DownloadStringWithRetry("", 3);
261                 client.AssertStatusOK("List Containers failed");
262
263                 if (client.StatusCode == HttpStatusCode.NoContent)
264                     return new List<ContainerInfo>();
265                 var infos = JsonConvert.DeserializeObject<IList<ContainerInfo>>(content);
266                 
267                 foreach (var info in infos)
268                 {
269                     info.Account = account;
270                 }
271                 return infos;
272             }
273
274         }
275
276         private string GetAccountUrl(string account)
277         {
278             return new Uri(RootAddressUri, new Uri(account,UriKind.Relative)).AbsoluteUri;
279         }
280
281         public IList<ShareAccountInfo> ListSharingAccounts(DateTime? since=null)
282         {
283             using (ThreadContext.Stacks["Share"].Push("List Accounts"))
284             {
285                 if (Log.IsDebugEnabled) Log.DebugFormat("START");
286
287                 using (var client = new RestClient(_baseClient))
288                 {
289                     client.Parameters.Clear();
290                     client.Parameters.Add("format", "json");
291                     client.IfModifiedSince = since;
292
293                     //Extract the username from the base address
294                     client.BaseAddress = RootAddressUri.AbsoluteUri;
295
296                     var content = client.DownloadStringWithRetry(@"", 3);
297
298                     client.AssertStatusOK("ListSharingAccounts failed");
299
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);
305
306                     Log.DebugFormat("END");
307                     return infos;
308                 }
309             }
310         }
311
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)
315         {
316
317             using (ThreadContext.Stacks["Share"].Push("List Objects"))
318             {
319                 if (Log.IsDebugEnabled) Log.DebugFormat("START");
320
321                 var objects = new List<ObjectInfo>();
322                 var accounts = ListSharingAccounts(since);
323                 foreach (var account in accounts)
324                 {
325                     var containers = ListContainers(account.name);
326                     foreach (var container in containers)
327                     {
328                         var containerObjects = ListObjects(account.name, container.Name);
329                         objects.AddRange(containerObjects);
330                     }
331                 }
332                 if (Log.IsDebugEnabled) Log.DebugFormat("END");
333                 return objects;
334             }
335         }
336
337         public void SetTags(ObjectInfo target,IDictionary<string,string> tags)
338         {
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");
343             if (target == null)
344                 throw new ArgumentNullException("target");
345             Contract.EndContractBlock();
346
347             using (ThreadContext.Stacks["Share"].Push("Share Object"))
348             {
349                 if (Log.IsDebugEnabled) Log.DebugFormat("START");
350
351                 using (var client = new RestClient(_baseClient))
352                 {
353
354                     client.BaseAddress = GetAccountUrl(target.Account);
355
356                     client.Parameters.Clear();
357                     client.Parameters.Add("update", "");
358
359                     foreach (var tag in tags)
360                     {
361                         var headerTag = String.Format("X-Object-Meta-{0}", tag.Key);
362                         client.Headers.Add(headerTag, tag.Value);
363                     }
364                     
365                     client.DownloadStringWithRetry(target.Container, 3);
366
367                     
368                     client.AssertStatusOK("SetTags failed");
369                     //If the status is NOT ACCEPTED we have a problem
370                     if (client.StatusCode != HttpStatusCode.Accepted)
371                     {
372                         Log.Error("Failed to set tags");
373                         throw new Exception("Failed to set tags");
374                     }
375
376                     if (Log.IsDebugEnabled) Log.DebugFormat("END");
377                 }
378             }
379
380
381         }
382
383         public void ShareObject(string account, string container, string objectName, string shareTo, bool read, bool write)
384         {
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();
398
399             using (ThreadContext.Stacks["Share"].Push("Share Object"))
400             {
401                 if (Log.IsDebugEnabled) Log.DebugFormat("START");
402                 
403                 using (var client = new RestClient(_baseClient))
404                 {
405
406                     client.BaseAddress = GetAccountUrl(account);
407
408                     client.Parameters.Clear();
409                     client.Parameters.Add("format", "json");
410
411                     string permission = "";
412                     if (write)
413                         permission = String.Format("write={0}", shareTo);
414                     else if (read)
415                         permission = String.Format("read={0}", shareTo);
416                     client.Headers.Add("X-Object-Sharing", permission);
417
418                     var content = client.DownloadStringWithRetry(container, 3);
419
420                     client.AssertStatusOK("ShareObject failed");
421
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);
427
428                     if (Log.IsDebugEnabled) Log.DebugFormat("END");
429                 }
430             }
431
432
433         }
434
435         public AccountInfo GetAccountPolicies(AccountInfo accountInfo)
436         {
437             if (accountInfo==null)
438                 throw new ArgumentNullException("accountInfo");
439             Contract.EndContractBlock();
440
441             using (ThreadContext.Stacks["Account"].Push("GetPolicies"))
442             {
443                 if (Log.IsDebugEnabled) Log.DebugFormat("START");
444
445                 using (var client = new RestClient(_baseClient))
446                 {
447                     if (!String.IsNullOrWhiteSpace(accountInfo.UserName))
448                         client.BaseAddress = GetAccountUrl(accountInfo.UserName);
449
450                     client.Parameters.Clear();
451                     client.Parameters.Add("format", "json");                    
452                     client.Head(String.Empty, 3);
453
454                     var quotaValue=client.ResponseHeaders["X-Account-Policy-Quota"];
455                     var bytesValue= client.ResponseHeaders["X-Account-Bytes-Used"];
456
457                     long quota, bytes;
458                     if (long.TryParse(quotaValue, out quota))
459                         accountInfo.Quota = quota;
460                     if (long.TryParse(bytesValue, out bytes))
461                         accountInfo.BytesUsed = bytes;
462                     
463                     return accountInfo;
464
465                 }
466
467             }
468         }
469
470         public void UpdateMetadata(ObjectInfo objectInfo)
471         {
472             if (objectInfo == null)
473                 throw new ArgumentNullException("objectInfo");
474             Contract.EndContractBlock();
475
476             using (ThreadContext.Stacks["Objects"].Push("UpdateMetadata"))
477             {
478                 if (Log.IsDebugEnabled) Log.DebugFormat("START");
479
480
481                 using(var client=new RestClient(_baseClient))
482                 {
483
484                     client.BaseAddress = GetAccountUrl(objectInfo.Account);
485                     
486                     client.Parameters.Clear();
487                     
488
489                     //Set Tags
490                     foreach (var tag in objectInfo.Tags)
491                     {
492                         var headerTag = String.Format("X-Object-Meta-{0}", tag.Key);
493                         client.Headers.Add(headerTag, tag.Value);
494                     }
495
496                     //Set Permissions
497
498                     var permissions=objectInfo.GetPermissionString();
499                     client.SetNonEmptyHeaderValue("X-Object-Sharing",permissions);
500
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);
506
507
508                     var uriBuilder = client.GetAddressBuilder(objectInfo.Container, objectInfo.Name);
509                     var uri = uriBuilder.Uri;
510
511                     client.UploadValues(uri,new NameValueCollection());
512
513
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))
517                     {
518                         Log.Error("Failed to update metadata");
519                         throw new Exception("Failed to update metadata");
520                     }
521
522                     if (Log.IsDebugEnabled) Log.DebugFormat("END");
523                 }
524             }
525
526         }
527
528         public void UpdateMetadata(ContainerInfo containerInfo)
529         {
530             if (containerInfo == null)
531                 throw new ArgumentNullException("containerInfo");
532             Contract.EndContractBlock();
533
534             using (ThreadContext.Stacks["Containers"].Push("UpdateMetadata"))
535             {
536                 if (Log.IsDebugEnabled) Log.DebugFormat("START");
537
538
539                 using(var client=new RestClient(_baseClient))
540                 {
541
542                     client.BaseAddress = GetAccountUrl(containerInfo.Account);
543                     
544                     client.Parameters.Clear();
545                     
546
547                     //Set Tags
548                     foreach (var tag in containerInfo.Tags)
549                     {
550                         var headerTag = String.Format("X-Container-Meta-{0}", tag.Key);
551                         client.Headers.Add(headerTag, tag.Value);
552                     }
553
554                     
555                     //Set Policies
556                     foreach (var policy in containerInfo.Policies)
557                     {
558                         var headerPolicy = String.Format("X-Container-Policy-{0}", policy.Key);
559                         client.Headers.Add(headerPolicy, policy.Value);
560                     }
561
562
563                     var uriBuilder = client.GetAddressBuilder(containerInfo.Name,"");
564                     var uri = uriBuilder.Uri;
565
566                     client.UploadValues(uri,new NameValueCollection());
567
568
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))
572                     {
573                         Log.Error("Failed to update metadata");
574                         throw new Exception("Failed to update metadata");
575                     }
576
577                     if (Log.IsDebugEnabled) Log.DebugFormat("END");
578                 }
579             }
580
581         }
582
583
584         public IList<ObjectInfo> ListObjects(string account, string container, DateTime? since = null)
585         {
586             if (String.IsNullOrWhiteSpace(container))
587                 throw new ArgumentNullException("container");
588             Contract.EndContractBlock();
589
590             using (ThreadContext.Stacks["Objects"].Push("List"))
591             {
592                 if (Log.IsDebugEnabled) Log.DebugFormat("START");
593
594                 using (var client = new RestClient(_baseClient))
595                 {
596                     if (!String.IsNullOrWhiteSpace(account))
597                         client.BaseAddress = GetAccountUrl(account);
598
599                     client.Parameters.Clear();
600                     client.Parameters.Add("format", "json");
601                     client.IfModifiedSince = since;
602                     var content = client.DownloadStringWithRetry(container, 3);
603
604                     client.AssertStatusOK("ListObjects failed");
605
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);
611
612                     foreach (var info in infos)
613                     {
614                         info.Container = container;
615                         info.Account = account;
616                     }
617                     if (Log.IsDebugEnabled) Log.DebugFormat("START");
618                     return infos;
619                 }
620             }
621         }
622
623
624
625         public IList<ObjectInfo> ListObjects(string account, string container, string folder, DateTime? since = null)
626         {
627             if (String.IsNullOrWhiteSpace(container))
628                 throw new ArgumentNullException("container");
629             if (String.IsNullOrWhiteSpace(folder))
630                 throw new ArgumentNullException("folder");
631             Contract.EndContractBlock();
632
633             using (ThreadContext.Stacks["Objects"].Push("List"))
634             {
635                 if (Log.IsDebugEnabled) Log.DebugFormat("START");
636
637                 using (var client = new RestClient(_baseClient))
638                 {
639                     if (!String.IsNullOrWhiteSpace(account))
640                         client.BaseAddress = GetAccountUrl(account);
641
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");
648
649                     var infos = JsonConvert.DeserializeObject<IList<ObjectInfo>>(content);
650
651                     if (Log.IsDebugEnabled) Log.DebugFormat("END");
652                     return infos;
653                 }
654             }
655         }
656
657  
658         public bool ContainerExists(string account, string container)
659         {
660             if (String.IsNullOrWhiteSpace(container))
661                 throw new ArgumentNullException("container", "The container property can't be empty");
662             Contract.EndContractBlock();
663
664             using (ThreadContext.Stacks["Containters"].Push("Exists"))
665             {
666                 if (Log.IsDebugEnabled) Log.DebugFormat("START");
667
668                 using (var client = new RestClient(_baseClient))
669                 {
670                     if (!String.IsNullOrWhiteSpace(account))
671                         client.BaseAddress = GetAccountUrl(account);
672
673                     client.Parameters.Clear();
674                     client.Head(container, 3);
675                                         
676                     bool result;
677                     switch (client.StatusCode)
678                     {
679                         case HttpStatusCode.OK:
680                         case HttpStatusCode.NoContent:
681                             result=true;
682                             break;
683                         case HttpStatusCode.NotFound:
684                             result=false;
685                             break;
686                         default:
687                             throw CreateWebException("ContainerExists", client.StatusCode);
688                     }
689                     if (Log.IsDebugEnabled) Log.DebugFormat("END");
690
691                     return result;
692                 }
693                 
694             }
695         }
696
697         public bool ObjectExists(string account, string container, string objectName)
698         {
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();
704
705             using (var client = new RestClient(_baseClient))
706             {
707                 if (!String.IsNullOrWhiteSpace(account))
708                     client.BaseAddress = GetAccountUrl(account);
709
710                 client.Parameters.Clear();
711                 client.Head(container + "/" + objectName, 3);
712
713                 switch (client.StatusCode)
714                 {
715                     case HttpStatusCode.OK:
716                     case HttpStatusCode.NoContent:
717                         return true;
718                     case HttpStatusCode.NotFound:
719                         return false;
720                     default:
721                         throw CreateWebException("ObjectExists", client.StatusCode);
722                 }
723             }
724
725         }
726
727         public ObjectInfo GetObjectInfo(string account, string container, string objectName)
728         {
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();
734
735             using (ThreadContext.Stacks["Objects"].Push("GetObjectInfo"))
736             {                
737
738                 using (var client = new RestClient(_baseClient))
739                 {
740                     if (!String.IsNullOrWhiteSpace(account))
741                         client.BaseAddress = GetAccountUrl(account);
742                     try
743                     {
744                         client.Parameters.Clear();
745
746                         client.Head(container + "/" + objectName, 3);
747
748                         if (client.TimedOut)
749                             return ObjectInfo.Empty;
750
751                         switch (client.StatusCode)
752                         {
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);
761
762                                 var permissions=client.GetHeaderValue("X-Object-Sharing", true);
763                                 
764                                 
765                                 var info = new ObjectInfo
766                                                {
767                                                    Account = account,
768                                                    Container = container,
769                                                    Name = objectName,
770                                                    Hash = client.GetHeaderValue("ETag"),
771                                                    Content_Type = client.GetHeaderValue("Content-Type"),
772                                                    Bytes = Convert.ToInt64(client.GetHeaderValue("Content-Length",true)),
773                                                    Tags = tags,
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),                                                   
780                                                };
781                                 info.SetPermissions(permissions);
782                                 return info;
783                             case HttpStatusCode.NotFound:
784                                 return ObjectInfo.Empty;
785                             default:
786                                 throw new WebException(
787                                     String.Format("[FAIL] GetObjectInfo for {0} failed with unexpected status code {1}",
788                                                   objectName, client.StatusCode));
789                         }
790
791                     }
792                     catch (RetryException)
793                     {
794                         Log.WarnFormat("[RETRY FAIL] GetObjectInfo for {0} failed.",objectName);
795                         return ObjectInfo.Empty;
796                     }
797                     catch (WebException e)
798                     {
799                         Log.Error(
800                             String.Format("[FAIL] GetObjectInfo for {0} failed with unexpected status code {1}",
801                                           objectName, client.StatusCode), e);
802                         throw;
803                     }
804                 }                
805             }
806
807         }
808
809         public void CreateFolder(string account, string container, string folder)
810         {
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();
816
817             var folderUrl=String.Format("{0}/{1}",container,folder);
818             using (var client = new RestClient(_baseClient))
819             {
820                 if (!String.IsNullOrWhiteSpace(account))
821                     client.BaseAddress = GetAccountUrl(account);
822
823                 client.Parameters.Clear();
824                 client.Headers.Add("Content-Type", @"application/directory");
825                 client.Headers.Add("Content-Length", "0");
826                 client.PutWithRetry(folderUrl, 3);
827
828                 if (client.StatusCode != HttpStatusCode.Created && client.StatusCode != HttpStatusCode.Accepted)
829                     throw CreateWebException("CreateFolder", client.StatusCode);
830             }
831         }
832
833      
834
835         public ContainerInfo GetContainerInfo(string account, string container)
836         {
837             if (String.IsNullOrWhiteSpace(container))
838                 throw new ArgumentNullException("container", "The container property can't be empty");
839             Contract.EndContractBlock();
840
841             using (var client = new RestClient(_baseClient))
842             {
843                 if (!String.IsNullOrWhiteSpace(account))
844                     client.BaseAddress = GetAccountUrl(account);                
845
846                 client.Head(container);
847                 switch (client.StatusCode)
848                 {
849                     case HttpStatusCode.OK:
850                     case HttpStatusCode.NoContent:
851                         var tags = client.GetMeta("X-Container-Meta-");
852                         var policies = client.GetMeta("X-Container-Policy-");
853
854                         var containerInfo = new ContainerInfo
855                                                 {
856                                                     Account=account,
857                                                     Name = container,
858                                                     Count =
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,
864                                                     Tags=tags,
865                                                     Policies=policies
866                                                 };
867                         
868
869                         return containerInfo;
870                     case HttpStatusCode.NotFound:
871                         return ContainerInfo.Empty;
872                     default:
873                         throw CreateWebException("GetContainerInfo", client.StatusCode);
874                 }
875             }
876         }
877
878         public void CreateContainer(string account, string container)
879         {
880             if (String.IsNullOrWhiteSpace(account))
881                 throw new ArgumentNullException("account");
882             if (String.IsNullOrWhiteSpace(container))
883                 throw new ArgumentNullException("container");
884             Contract.EndContractBlock();
885
886             using (var client = new RestClient(_baseClient))
887             {
888                 if (!String.IsNullOrWhiteSpace(account))
889                     client.BaseAddress = GetAccountUrl(account);
890
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);
895             }
896         }
897
898         public void DeleteContainer(string account, string container)
899         {
900             if (String.IsNullOrWhiteSpace(container))
901                 throw new ArgumentNullException("container", "The container property can't be empty");
902             Contract.EndContractBlock();
903
904             using (var client = new RestClient(_baseClient))
905             {
906                 if (!String.IsNullOrWhiteSpace(account))
907                     client.BaseAddress = GetAccountUrl(account);
908
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);
913             }
914
915         }
916
917         /// <summary>
918         /// 
919         /// </summary>
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 
927         // a local file
928         public Task GetObject(string account, string container, string objectName, string fileName)
929         {
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();
935
936             try
937             {
938                 //WebClient, and by extension RestClient, are not thread-safe. Create a new RestClient
939                 //object to avoid concurrency errors.
940                 //
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);
945
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;
950
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,
956                                     args.BytesReceived,
957                                     args.TotalBytesToReceive);                                
958
959
960                 //Start downloading the object asynchronously
961                 var downloadTask = client.DownloadFileTask(uri, fileName);
962                 
963                 //Once the download completes
964                 return downloadTask.ContinueWith(download =>
965                                       {
966                                           //Delete the local client object
967                                           client.Dispose();
968                                           //And report failure or completion
969                                           if (download.IsFaulted)
970                                           {
971                                               Log.ErrorFormat("[GET] FAIL for {0} with \r{1}", objectName,
972                                                                download.Exception);
973                                           }
974                                           else
975                                           {
976                                               Log.InfoFormat("[GET] END {0}", objectName);                                             
977                                           }
978                                       });
979             }
980             catch (Exception exc)
981             {
982                 Log.ErrorFormat("[GET] END {0} with {1}", objectName, exc);
983                 throw;
984             }
985
986
987
988         }
989
990         public Task<IList<string>> PutHashMap(string account, string container, string objectName, TreeHash hash)
991         {
992             if (String.IsNullOrWhiteSpace(container))
993                 throw new ArgumentNullException("container");
994             if (String.IsNullOrWhiteSpace(objectName))
995                 throw new ArgumentNullException("objectName");
996             if (hash==null)
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();
1003
1004
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);
1009
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;
1015
1016
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);
1021             
1022             return uploadTask.ContinueWith(t =>
1023             {
1024
1025                 var empty = (IList<string>)new List<string>();
1026                 
1027
1028                 //The server will respond either with 201-created if all blocks were already on the server
1029                 if (client.StatusCode == HttpStatusCode.Created)                    
1030                 {
1031                     //in which case we return an empty hash list
1032                     return empty;
1033                 }
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                
1036                 if (t.IsFaulted)
1037                 {
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)
1042                     {
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))
1046                         {
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
1050                             //
1051                             //As of 30/1/2012, the result is a proper Json array so we don't need to read the content
1052                             //line by line
1053                             
1054                             var serializer = new JsonSerializer();                            
1055                             var hashes=(List<string>)serializer.Deserialize(reader, typeof (List<string>));
1056
1057                             return hashes;
1058                         }                        
1059                     }                    
1060                     //Any other status code is unexpected and the exception should be rethrown
1061                     throw ex;
1062                     
1063                 }
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);                    
1066                 
1067                 return empty;
1068             });
1069
1070         }
1071
1072         public Task<byte[]> GetBlock(string account, string container, Uri relativeUrl, long start, long? end)
1073         {
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");
1084             if (start<0)
1085                 throw new ArgumentOutOfRangeException("start");
1086             Contract.EndContractBlock();
1087
1088
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);
1093
1094             var builder = client.GetAddressBuilder(container, relativeUrl.ToString());
1095             var uri = builder.Uri;
1096
1097             return client.DownloadDataTask(uri)
1098                 .ContinueWith(t=>
1099                                   {
1100                                       client.Dispose();
1101                                       return t.Result;
1102                                   });
1103         }
1104
1105
1106         public async Task PostBlock(string account, string container, byte[] block, int offset, int count)
1107         {
1108             if (String.IsNullOrWhiteSpace(container))
1109                 throw new ArgumentNullException("container");
1110             if (block == null)
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();
1121
1122
1123             try
1124             {
1125
1126             //Don't use a timeout because putting the hashmap may be a long process
1127                 using (var client = new RestClient(_baseClient) { Timeout = 0 })
1128                 {
1129                     if (!String.IsNullOrWhiteSpace(account))
1130                         client.BaseAddress = GetAccountUrl(account);
1131
1132                     var builder = client.GetAddressBuilder(container, "");
1133                     //We are doing an update
1134                     builder.Query = "update";
1135                     var uri = builder.Uri;
1136
1137                     client.Headers[HttpRequestHeader.ContentType] = "application/octet-stream";
1138
1139                     Log.InfoFormat("[BLOCK POST] START");
1140
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 ");
1147
1148                     var buffer = new byte[count];
1149                     Buffer.BlockCopy(block, offset, buffer, 0, count);
1150                     //Send the block
1151                     await client.UploadDataTask(uri, "POST", buffer);
1152                     Log.InfoFormat("[BLOCK POST] END");
1153                 }
1154             }
1155             catch (Exception exc)
1156             {
1157                 Log.ErrorFormat("[BLOCK POST] FAIL with \r{0}", exc);                                        
1158                 throw;
1159             }
1160         }
1161
1162
1163         public async Task<TreeHash> GetHashMap(string account, string container, string objectName)
1164         {
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();
1174
1175             try
1176             {
1177                 //WebClient, and by extension RestClient, are not thread-safe. Create a new RestClient
1178                 //object to avoid concurrency errors.
1179                 //
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
1182                 
1183                 //Start downloading the object asynchronously
1184                 using (var client = new RestClient(_baseClient) { Timeout = 0 })
1185                 {
1186                     if (!String.IsNullOrWhiteSpace(account))
1187                         client.BaseAddress = GetAccountUrl(account);
1188
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;
1194
1195
1196                     var json = await client.DownloadStringTaskAsync(uri);
1197                     var treeHash = TreeHash.Parse(json);
1198                     Log.InfoFormat("[GET HASH] END {0}", objectName);
1199                     return treeHash;
1200                 }
1201             }
1202             catch (Exception exc)
1203             {
1204                 Log.ErrorFormat("[GET HASH] END {0} with {1}", objectName, exc);
1205                 throw;
1206             }
1207
1208         }
1209
1210
1211         /// <summary>
1212         /// 
1213         /// </summary>
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")
1221         {
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");
1228 /*
1229             if (!File.Exists(fileName) && !Directory.Exists(fileName))
1230                 throw new FileNotFoundException("The file or directory does not exist",fileName);
1231 */
1232             Contract.EndContractBlock();
1233             
1234             try
1235             {
1236
1237                 using (var client = new RestClient(_baseClient) { Timeout = 0 })
1238                 {
1239                     if (!String.IsNullOrWhiteSpace(account))
1240                         client.BaseAddress = GetAccountUrl(account);
1241
1242                     var builder = client.GetAddressBuilder(container, objectName);
1243                     var uri = builder.Uri;
1244
1245                     string etag = hash ?? CalculateHash(fileName);
1246
1247                     client.Headers.Add("Content-Type", contentType);
1248                     client.Headers.Add("ETag", etag);
1249
1250
1251                     Log.InfoFormat("[PUT] START {0}", objectName);
1252                     client.UploadProgressChanged += (sender, args) =>
1253                                                         {
1254                                                             using (ThreadContext.Stacks["PUT"].Push("Progress"))
1255                                                             {
1256                                                                 Log.InfoFormat("{0} {1}% {2} of {3}", fileName,
1257                                                                                args.ProgressPercentage,
1258                                                                                args.BytesSent, args.TotalBytesToSend);
1259                                                             }
1260                                                         };
1261
1262                     client.UploadFileCompleted += (sender, args) =>
1263                                                       {
1264                                                           using (ThreadContext.Stacks["PUT"].Push("Progress"))
1265                                                           {
1266                                                               Log.InfoFormat("Completed {0}", fileName);
1267                                                           }
1268                                                       };
1269                     if (contentType=="application/directory")
1270                         await client.UploadDataTaskAsync(uri, "PUT", new byte[0]);
1271                     else
1272                         await client.UploadFileTaskAsync(uri, "PUT", fileName);
1273                 }
1274
1275                 Log.InfoFormat("[PUT] END {0}", objectName);
1276             }
1277             catch (Exception exc)
1278             {
1279                 Log.ErrorFormat("[PUT] END {0} with {1}", objectName, exc);
1280                 throw;
1281             }                
1282
1283         }
1284        
1285         
1286         private static string CalculateHash(string fileName)
1287         {
1288             Contract.Requires(!String.IsNullOrWhiteSpace(fileName));
1289             Contract.EndContractBlock();
1290
1291             string hash;
1292             using (var hasher = MD5.Create())
1293             using(var stream=File.OpenRead(fileName))
1294             {
1295                 var hashBuilder=new StringBuilder();
1296                 foreach (byte b in hasher.ComputeHash(stream))
1297                     hashBuilder.Append(b.ToString("x2").ToLower());
1298                 hash = hashBuilder.ToString();                
1299             }
1300             return hash;
1301         }
1302         
1303         public void MoveObject(string account, string sourceContainer, string oldObjectName, string targetContainer, string newObjectName)
1304         {
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();
1314
1315             var targetUrl = targetContainer + "/" + newObjectName;
1316             var sourceUrl = String.Format("/{0}/{1}", sourceContainer, oldObjectName);
1317
1318             using (var client = new RestClient(_baseClient))
1319             {
1320                 if (!String.IsNullOrWhiteSpace(account))
1321                     client.BaseAddress = GetAccountUrl(account);
1322
1323                 client.Headers.Add("X-Move-From", sourceUrl);
1324                 client.PutWithRetry(targetUrl, 3);
1325
1326                 var expectedCodes = new[] {HttpStatusCode.OK, HttpStatusCode.NoContent, HttpStatusCode.Created};
1327                 if (!expectedCodes.Contains(client.StatusCode))
1328                     throw CreateWebException("MoveObject", client.StatusCode);
1329             }
1330         }
1331
1332         public void DeleteObject(string account, string sourceContainer, string objectName)
1333         {            
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();
1339
1340             var targetUrl = FolderConstants.TrashContainer + "/" + objectName;
1341             var sourceUrl = String.Format("/{0}/{1}", sourceContainer, objectName);
1342
1343             using (var client = new RestClient(_baseClient))
1344             {
1345                 if (!String.IsNullOrWhiteSpace(account))
1346                     client.BaseAddress = GetAccountUrl(account);
1347
1348                 client.Headers.Add("X-Move-From", sourceUrl);
1349                 client.AllowedStatusCodes.Add(HttpStatusCode.NotFound);
1350                 client.PutWithRetry(targetUrl, 3);
1351
1352                 var expectedCodes = new[] {HttpStatusCode.OK, HttpStatusCode.NoContent, HttpStatusCode.Created,HttpStatusCode.NotFound};
1353                 if (!expectedCodes.Contains(client.StatusCode))
1354                     throw CreateWebException("DeleteObject", client.StatusCode);
1355             }
1356         }
1357
1358       
1359         private static WebException CreateWebException(string operation, HttpStatusCode statusCode)
1360         {
1361             return new WebException(String.Format("{0} failed with unexpected status code {1}", operation, statusCode));
1362         }
1363
1364         
1365     }
1366
1367     public class ShareAccountInfo
1368     {
1369         public DateTime? last_modified { get; set; }
1370         public string name { get; set; }
1371     }
1372 }