Modified loggers to use their enclosing class
[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;
50 using System.Collections.Generic;
51 using System.Collections.Specialized;
52 using System.ComponentModel.Composition;
53 using System.Diagnostics;
54 using System.Diagnostics.Contracts;
55 using System.IO;
56 using System.Linq;
57 using System.Net;
58 using System.Reflection;
59 using System.Security.Cryptography;
60 using System.Text;
61 using System.Threading.Tasks;
62 using Newtonsoft.Json;
63 using Pithos.Interfaces;
64 using log4net;
65
66 namespace Pithos.Network
67 {
68     [Export(typeof(ICloudClient))]
69     public class CloudFilesClient:ICloudClient
70     {
71         private static readonly ILog Log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
72
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;
76         
77
78         //During authentication the client provides a UserName 
79         public string UserName { get; set; }
80         
81         //and and ApiKey to the server
82         public string ApiKey { get; set; }
83         
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         public string Token
88         {
89             get { return _token; }
90             set
91             {
92                 _token = value;
93                 _baseClient.Headers["X-Auth-Token"] = value;
94             }
95         }
96
97         //The client also receives a StorageUrl after authentication. All subsequent operations must
98         //use this url
99         public Uri StorageUrl { get; set; }
100
101
102         protected Uri RootAddressUri { get; set; }
103
104        /* private WebProxy _proxy;
105         public WebProxy Proxy
106         {
107             get { return _proxy; }
108             set
109             {
110                 _proxy = value;
111                 if (_baseClient != null)
112                     _baseClient.Proxy = value;                
113             }
114         }
115 */
116
117         /* private Uri _proxy;
118         public Uri Proxy
119         {
120             get { return _proxy; }
121             set
122             {
123                 _proxy = value;
124                 if (_baseClient != null)
125                     _baseClient.Proxy = new WebProxy(value);                
126             }
127         }*/
128
129         public double DownloadPercentLimit { get; set; }
130         public double UploadPercentLimit { get; set; }
131
132         public string AuthenticationUrl { get; set; }
133
134  
135         public string VersionPath
136         {
137             get { return UsePithos ? "v1" : "v1.0"; }
138         }
139
140         public bool UsePithos { get; set; }
141
142
143
144         public CloudFilesClient(string userName, string apiKey)
145         {
146             UserName = userName;
147             ApiKey = apiKey;
148         }
149
150         public CloudFilesClient(AccountInfo accountInfo)
151         {
152             if (accountInfo==null)
153                 throw new ArgumentNullException("accountInfo");
154             Contract.Ensures(!String.IsNullOrWhiteSpace(Token));
155             Contract.Ensures(StorageUrl != null);
156             Contract.Ensures(_baseClient != null);
157             Contract.Ensures(RootAddressUri != null);
158             Contract.EndContractBlock();          
159
160             _baseClient = new RestClient
161             {
162                 BaseAddress = accountInfo.StorageUri.ToString(),
163                 Timeout = 10000,
164                 Retries = 3,
165             };
166             StorageUrl = accountInfo.StorageUri;
167             Token = accountInfo.Token;
168             UserName = accountInfo.UserName;
169
170             //Get the root address (StorageUrl without the account)
171             var storageUrl = StorageUrl.AbsoluteUri;
172             var usernameIndex = storageUrl.LastIndexOf(UserName);
173             var rootUrl = storageUrl.Substring(0, usernameIndex);
174             RootAddressUri = new Uri(rootUrl);
175         }
176
177
178         public AccountInfo Authenticate()
179         {
180             if (String.IsNullOrWhiteSpace(UserName))
181                 throw new InvalidOperationException("UserName is empty");
182             if (String.IsNullOrWhiteSpace(ApiKey))
183                 throw new InvalidOperationException("ApiKey is empty");
184             if (String.IsNullOrWhiteSpace(AuthenticationUrl))
185                 throw new InvalidOperationException("AuthenticationUrl is empty");
186             Contract.Ensures(!String.IsNullOrWhiteSpace(Token));
187             Contract.Ensures(StorageUrl != null);
188             Contract.Ensures(_baseClient != null);
189             Contract.Ensures(RootAddressUri != null);
190             Contract.EndContractBlock();
191
192
193             Log.InfoFormat("[AUTHENTICATE] Start for {0}", UserName);
194
195             var groups = new List<Group>();
196
197             using (var authClient = new RestClient{BaseAddress=AuthenticationUrl})
198             {                
199                /* if (Proxy != null)
200                     authClient.Proxy = Proxy;*/
201
202                 Contract.Assume(authClient.Headers!=null);
203
204                 authClient.Headers.Add("X-Auth-User", UserName);
205                 authClient.Headers.Add("X-Auth-Key", ApiKey);
206
207                 authClient.DownloadStringWithRetry(VersionPath, 3);
208
209                 authClient.AssertStatusOK("Authentication failed");
210
211                 var storageUrl = authClient.GetHeaderValue("X-Storage-Url");
212                 if (String.IsNullOrWhiteSpace(storageUrl))
213                     throw new InvalidOperationException("Failed to obtain storage url");
214                 
215                 _baseClient = new RestClient
216                 {
217                     BaseAddress = storageUrl,
218                     Timeout = 10000,
219                     Retries = 3,
220                     //Proxy=Proxy
221                 };
222
223                 StorageUrl = new Uri(storageUrl);
224                 
225                 //Get the root address (StorageUrl without the account)
226                 var usernameIndex=storageUrl.LastIndexOf(UserName);
227                 var rootUrl = storageUrl.Substring(0, usernameIndex);
228                 RootAddressUri = new Uri(rootUrl);
229                 
230                 var token = authClient.GetHeaderValue("X-Auth-Token");
231                 if (String.IsNullOrWhiteSpace(token))
232                     throw new InvalidOperationException("Failed to obtain token url");
233                 Token = token;
234
235                /* var keys = authClient.ResponseHeaders.AllKeys.AsQueryable();
236                 groups = (from key in keys
237                             where key.StartsWith("X-Account-Group-")
238                             let name = key.Substring(16)
239                             select new Group(name, authClient.ResponseHeaders[key]))
240                         .ToList();
241                     
242 */
243             }
244
245             Log.InfoFormat("[AUTHENTICATE] End for {0}", UserName);
246             
247
248             return new AccountInfo {StorageUri = StorageUrl, Token = Token, UserName = UserName,Groups=groups};            
249
250         }
251
252
253
254         public IList<ContainerInfo> ListContainers(string account)
255         {
256             using (var client = new RestClient(_baseClient))
257             {
258                 if (!String.IsNullOrWhiteSpace(account))
259                     client.BaseAddress = GetAccountUrl(account);
260                 
261                 client.Parameters.Clear();
262                 client.Parameters.Add("format", "json");
263                 var content = client.DownloadStringWithRetry("", 3);
264                 client.AssertStatusOK("List Containers failed");
265
266                 if (client.StatusCode == HttpStatusCode.NoContent)
267                     return new List<ContainerInfo>();
268                 var infos = JsonConvert.DeserializeObject<IList<ContainerInfo>>(content);
269                 
270                 foreach (var info in infos)
271                 {
272                     info.Account = account;
273                 }
274                 return infos;
275             }
276
277         }
278
279         private string GetAccountUrl(string account)
280         {
281             return new Uri(RootAddressUri, new Uri(account,UriKind.Relative)).AbsoluteUri;
282         }
283
284         public IList<ShareAccountInfo> ListSharingAccounts(DateTime? since=null)
285         {
286             using (ThreadContext.Stacks["Share"].Push("List Accounts"))
287             {
288                 if (Log.IsDebugEnabled) Log.DebugFormat("START");
289
290                 using (var client = new RestClient(_baseClient))
291                 {
292                     client.Parameters.Clear();
293                     client.Parameters.Add("format", "json");
294                     client.IfModifiedSince = since;
295
296                     //Extract the username from the base address
297                     client.BaseAddress = RootAddressUri.AbsoluteUri;
298
299                     var content = client.DownloadStringWithRetry(@"", 3);
300
301                     client.AssertStatusOK("ListSharingAccounts failed");
302
303                     //If the result is empty, return an empty list,
304                     var infos = String.IsNullOrWhiteSpace(content)
305                                     ? new List<ShareAccountInfo>()
306                                 //Otherwise deserialize the account list into a list of ShareAccountInfos
307                                     : JsonConvert.DeserializeObject<IList<ShareAccountInfo>>(content);
308
309                     Log.DebugFormat("END");
310                     return infos;
311                 }
312             }
313         }
314
315         //Request listing of all objects in a container modified since a specific time.
316         //If the *since* value is missing, return all objects
317         public IList<ObjectInfo> ListSharedObjects(DateTime? since = null)
318         {
319
320             using (ThreadContext.Stacks["Share"].Push("List Objects"))
321             {
322                 if (Log.IsDebugEnabled) Log.DebugFormat("START");
323                 //'since' is not used here because we need to have ListObjects return a NoChange result
324                 //for all shared accounts,containers
325                 var accounts = ListSharingAccounts();
326                 var items = from account in accounts 
327                             let containers = ListContainers(account.name) 
328                             from container in containers 
329                             select ListObjects(account.name, container.Name,since);
330                 var objects=items.SelectMany(r=> r).ToList();
331 /*
332                 var objects = new List<ObjectInfo>();
333                 foreach (var containerObjects in items)
334                 {
335                     objects.AddRange(containerObjects);
336                 }
337 */
338                 if (Log.IsDebugEnabled) Log.DebugFormat("END");
339                 return objects;
340             }
341         }
342
343         public void SetTags(ObjectInfo target,IDictionary<string,string> tags)
344         {
345             if (String.IsNullOrWhiteSpace(Token))
346                 throw new InvalidOperationException("The Token is not set");
347             if (StorageUrl == null)
348                 throw new InvalidOperationException("The StorageUrl is not set");
349             if (target == null)
350                 throw new ArgumentNullException("target");
351             Contract.EndContractBlock();
352
353             using (ThreadContext.Stacks["Share"].Push("Share Object"))
354             {
355                 if (Log.IsDebugEnabled) Log.DebugFormat("START");
356
357                 using (var client = new RestClient(_baseClient))
358                 {
359
360                     client.BaseAddress = GetAccountUrl(target.Account);
361
362                     client.Parameters.Clear();
363                     client.Parameters.Add("update", "");
364
365                     foreach (var tag in tags)
366                     {
367                         var headerTag = String.Format("X-Object-Meta-{0}", tag.Key);
368                         client.Headers.Add(headerTag, tag.Value);
369                     }
370                     
371                     client.DownloadStringWithRetry(target.Container, 3);
372
373                     
374                     client.AssertStatusOK("SetTags failed");
375                     //If the status is NOT ACCEPTED we have a problem
376                     if (client.StatusCode != HttpStatusCode.Accepted)
377                     {
378                         Log.Error("Failed to set tags");
379                         throw new Exception("Failed to set tags");
380                     }
381
382                     if (Log.IsDebugEnabled) Log.DebugFormat("END");
383                 }
384             }
385
386
387         }
388
389         public void ShareObject(string account, string container, string objectName, string shareTo, bool read, bool write)
390         {
391             if (String.IsNullOrWhiteSpace(Token))
392                 throw new InvalidOperationException("The Token is not set");
393             if (StorageUrl==null)
394                 throw new InvalidOperationException("The StorageUrl is not set");
395             if (String.IsNullOrWhiteSpace(container))
396                 throw new ArgumentNullException("container");
397             if (String.IsNullOrWhiteSpace(objectName))
398                 throw new ArgumentNullException("objectName");
399             if (String.IsNullOrWhiteSpace(account))
400                 throw new ArgumentNullException("account");
401             if (String.IsNullOrWhiteSpace(shareTo))
402                 throw new ArgumentNullException("shareTo");
403             Contract.EndContractBlock();
404
405             using (ThreadContext.Stacks["Share"].Push("Share Object"))
406             {
407                 if (Log.IsDebugEnabled) Log.DebugFormat("START");
408                 
409                 using (var client = new RestClient(_baseClient))
410                 {
411
412                     client.BaseAddress = GetAccountUrl(account);
413
414                     client.Parameters.Clear();
415                     client.Parameters.Add("format", "json");
416
417                     string permission = "";
418                     if (write)
419                         permission = String.Format("write={0}", shareTo);
420                     else if (read)
421                         permission = String.Format("read={0}", shareTo);
422                     client.Headers.Add("X-Object-Sharing", permission);
423
424                     var content = client.DownloadStringWithRetry(container, 3);
425
426                     client.AssertStatusOK("ShareObject failed");
427
428                     //If the result is empty, return an empty list,
429                     var infos = String.IsNullOrWhiteSpace(content)
430                                     ? new List<ObjectInfo>()
431                                 //Otherwise deserialize the object list into a list of ObjectInfos
432                                     : JsonConvert.DeserializeObject<IList<ObjectInfo>>(content);
433
434                     if (Log.IsDebugEnabled) Log.DebugFormat("END");
435                 }
436             }
437
438
439         }
440
441         public AccountInfo GetAccountPolicies(AccountInfo accountInfo)
442         {
443             if (accountInfo==null)
444                 throw new ArgumentNullException("accountInfo");
445             Contract.EndContractBlock();
446
447             using (ThreadContext.Stacks["Account"].Push("GetPolicies"))
448             {
449                 if (Log.IsDebugEnabled) Log.DebugFormat("START");
450
451                 using (var client = new RestClient(_baseClient))
452                 {
453                     if (!String.IsNullOrWhiteSpace(accountInfo.UserName))
454                         client.BaseAddress = GetAccountUrl(accountInfo.UserName);
455
456                     client.Parameters.Clear();
457                     client.Parameters.Add("format", "json");                    
458                     client.Head(String.Empty, 3);
459
460                     var quotaValue=client.ResponseHeaders["X-Account-Policy-Quota"];
461                     var bytesValue= client.ResponseHeaders["X-Account-Bytes-Used"];
462
463                     long quota, bytes;
464                     if (long.TryParse(quotaValue, out quota))
465                         accountInfo.Quota = quota;
466                     if (long.TryParse(bytesValue, out bytes))
467                         accountInfo.BytesUsed = bytes;
468                     
469                     return accountInfo;
470
471                 }
472
473             }
474         }
475
476         public void UpdateMetadata(ObjectInfo objectInfo)
477         {
478             if (objectInfo == null)
479                 throw new ArgumentNullException("objectInfo");
480             Contract.EndContractBlock();
481
482             using (ThreadContext.Stacks["Objects"].Push("UpdateMetadata"))
483             {
484                 if (Log.IsDebugEnabled) Log.DebugFormat("START");
485
486
487                 using(var client=new RestClient(_baseClient))
488                 {
489
490                     client.BaseAddress = GetAccountUrl(objectInfo.Account);
491                     
492                     client.Parameters.Clear();
493                     
494
495                     //Set Tags
496                     foreach (var tag in objectInfo.Tags)
497                     {
498                         var headerTag = String.Format("X-Object-Meta-{0}", tag.Key);
499                         client.Headers.Add(headerTag, tag.Value);
500                     }
501
502                     //Set Permissions
503
504                     var permissions=objectInfo.GetPermissionString();
505                     client.SetNonEmptyHeaderValue("X-Object-Sharing",permissions);
506
507                     client.SetNonEmptyHeaderValue("Content-Disposition",objectInfo.ContendDisposition);
508                     client.SetNonEmptyHeaderValue("Content-Encoding",objectInfo.ContentEncoding);
509                     client.SetNonEmptyHeaderValue("X-Object-Manifest",objectInfo.Manifest);
510                     var isPublic = objectInfo.IsPublic.ToString().ToLower();
511                     client.Headers.Add("X-Object-Public", isPublic);
512
513
514                     var uriBuilder = client.GetAddressBuilder(objectInfo.Container, objectInfo.Name);
515                     var uri = uriBuilder.Uri;
516
517                     client.UploadValues(uri,new NameValueCollection());
518
519
520                     client.AssertStatusOK("UpdateMetadata failed");
521                     //If the status is NOT ACCEPTED or OK we have a problem
522                     if (!(client.StatusCode == HttpStatusCode.Accepted || client.StatusCode == HttpStatusCode.OK))
523                     {
524                         Log.Error("Failed to update metadata");
525                         throw new Exception("Failed to update metadata");
526                     }
527
528                     if (Log.IsDebugEnabled) Log.DebugFormat("END");
529                 }
530             }
531
532         }
533
534         public void UpdateMetadata(ContainerInfo containerInfo)
535         {
536             if (containerInfo == null)
537                 throw new ArgumentNullException("containerInfo");
538             Contract.EndContractBlock();
539
540             using (ThreadContext.Stacks["Containers"].Push("UpdateMetadata"))
541             {
542                 if (Log.IsDebugEnabled) Log.DebugFormat("START");
543
544
545                 using(var client=new RestClient(_baseClient))
546                 {
547
548                     client.BaseAddress = GetAccountUrl(containerInfo.Account);
549                     
550                     client.Parameters.Clear();
551                     
552
553                     //Set Tags
554                     foreach (var tag in containerInfo.Tags)
555                     {
556                         var headerTag = String.Format("X-Container-Meta-{0}", tag.Key);
557                         client.Headers.Add(headerTag, tag.Value);
558                     }
559
560                     
561                     //Set Policies
562                     foreach (var policy in containerInfo.Policies)
563                     {
564                         var headerPolicy = String.Format("X-Container-Policy-{0}", policy.Key);
565                         client.Headers.Add(headerPolicy, policy.Value);
566                     }
567
568
569                     var uriBuilder = client.GetAddressBuilder(containerInfo.Name,"");
570                     var uri = uriBuilder.Uri;
571
572                     client.UploadValues(uri,new NameValueCollection());
573
574
575                     client.AssertStatusOK("UpdateMetadata failed");
576                     //If the status is NOT ACCEPTED or OK we have a problem
577                     if (!(client.StatusCode == HttpStatusCode.Accepted || client.StatusCode == HttpStatusCode.OK))
578                     {
579                         Log.Error("Failed to update metadata");
580                         throw new Exception("Failed to update metadata");
581                     }
582
583                     if (Log.IsDebugEnabled) Log.DebugFormat("END");
584                 }
585             }
586
587         }
588
589
590         public IList<ObjectInfo> ListObjects(string account, string container, DateTime? since = null)
591         {
592             if (String.IsNullOrWhiteSpace(container))
593                 throw new ArgumentNullException("container");
594             Contract.EndContractBlock();
595
596             using (ThreadContext.Stacks["Objects"].Push("List"))
597             {
598                 if (Log.IsDebugEnabled) Log.DebugFormat("START");
599
600                 using (var client = new RestClient(_baseClient))
601                 {
602                     if (!String.IsNullOrWhiteSpace(account))
603                         client.BaseAddress = GetAccountUrl(account);
604
605                     client.Parameters.Clear();
606                     client.Parameters.Add("format", "json");
607                     client.IfModifiedSince = since;
608                     var content = client.DownloadStringWithRetry(container, 3);
609
610                     client.AssertStatusOK("ListObjects failed");
611
612                     if (client.StatusCode==HttpStatusCode.NotModified)
613                         return new[]{new NoModificationInfo(account,container)};
614                     //If the result is empty, return an empty list,
615                     var infos = String.IsNullOrWhiteSpace(content)
616                                     ? new List<ObjectInfo>()
617                                 //Otherwise deserialize the object list into a list of ObjectInfos
618                                     : JsonConvert.DeserializeObject<IList<ObjectInfo>>(content);
619
620                     foreach (var info in infos)
621                     {
622                         info.Container = container;
623                         info.Account = account;
624                         info.StorageUri = this.StorageUrl;
625                     }
626                     if (Log.IsDebugEnabled) Log.DebugFormat("END");
627                     return infos;
628                 }
629             }
630         }
631
632         public IList<ObjectInfo> ListObjects(string account, string container, string folder, DateTime? since = null)
633         {
634             if (String.IsNullOrWhiteSpace(container))
635                 throw new ArgumentNullException("container");
636 /*
637             if (String.IsNullOrWhiteSpace(folder))
638                 throw new ArgumentNullException("folder");
639 */
640             Contract.EndContractBlock();
641
642             using (ThreadContext.Stacks["Objects"].Push("List"))
643             {
644                 if (Log.IsDebugEnabled) Log.DebugFormat("START");
645
646                 using (var client = new RestClient(_baseClient))
647                 {
648                     if (!String.IsNullOrWhiteSpace(account))
649                         client.BaseAddress = GetAccountUrl(account);
650
651                     client.Parameters.Clear();
652                     client.Parameters.Add("format", "json");
653                     client.Parameters.Add("path", folder);
654                     client.IfModifiedSince = since;
655                     var content = client.DownloadStringWithRetry(container, 3);
656                     client.AssertStatusOK("ListObjects failed");
657
658                     if (client.StatusCode==HttpStatusCode.NotModified)
659                         return new[]{new NoModificationInfo(account,container,folder)};
660
661                     var infos = JsonConvert.DeserializeObject<IList<ObjectInfo>>(content);
662                     foreach (var info in infos)
663                     {
664                         info.Account = account;
665                         if (info.Container == null)
666                             info.Container = container;
667                         info.StorageUri = this.StorageUrl;
668                     }
669                     if (Log.IsDebugEnabled) Log.DebugFormat("END");
670                     return infos;
671                 }
672             }
673         }
674
675  
676         public bool ContainerExists(string account, string container)
677         {
678             if (String.IsNullOrWhiteSpace(container))
679                 throw new ArgumentNullException("container", "The container property can't be empty");
680             Contract.EndContractBlock();
681
682             using (ThreadContext.Stacks["Containters"].Push("Exists"))
683             {
684                 if (Log.IsDebugEnabled) Log.DebugFormat("START");
685
686                 using (var client = new RestClient(_baseClient))
687                 {
688                     if (!String.IsNullOrWhiteSpace(account))
689                         client.BaseAddress = GetAccountUrl(account);
690
691                     client.Parameters.Clear();
692                     client.Head(container, 3);
693                                         
694                     bool result;
695                     switch (client.StatusCode)
696                     {
697                         case HttpStatusCode.OK:
698                         case HttpStatusCode.NoContent:
699                             result=true;
700                             break;
701                         case HttpStatusCode.NotFound:
702                             result=false;
703                             break;
704                         default:
705                             throw CreateWebException("ContainerExists", client.StatusCode);
706                     }
707                     if (Log.IsDebugEnabled) Log.DebugFormat("END");
708
709                     return result;
710                 }
711                 
712             }
713         }
714
715         public bool ObjectExists(string account, string container, string objectName)
716         {
717             if (String.IsNullOrWhiteSpace(container))
718                 throw new ArgumentNullException("container", "The container property can't be empty");
719             if (String.IsNullOrWhiteSpace(objectName))
720                 throw new ArgumentNullException("objectName", "The objectName property can't be empty");
721             Contract.EndContractBlock();
722
723             using (var client = new RestClient(_baseClient))
724             {
725                 if (!String.IsNullOrWhiteSpace(account))
726                     client.BaseAddress = GetAccountUrl(account);
727
728                 client.Parameters.Clear();
729                 client.Head(container + "/" + objectName, 3);
730
731                 switch (client.StatusCode)
732                 {
733                     case HttpStatusCode.OK:
734                     case HttpStatusCode.NoContent:
735                         return true;
736                     case HttpStatusCode.NotFound:
737                         return false;
738                     default:
739                         throw CreateWebException("ObjectExists", client.StatusCode);
740                 }
741             }
742
743         }
744
745         public ObjectInfo GetObjectInfo(string account, string container, string objectName)
746         {
747             if (String.IsNullOrWhiteSpace(container))
748                 throw new ArgumentNullException("container", "The container property can't be empty");
749             if (String.IsNullOrWhiteSpace(objectName))
750                 throw new ArgumentNullException("objectName", "The objectName property can't be empty");
751             Contract.EndContractBlock();
752
753             using (ThreadContext.Stacks["Objects"].Push("GetObjectInfo"))
754             {                
755
756                 using (var client = new RestClient(_baseClient))
757                 {
758                     if (!String.IsNullOrWhiteSpace(account))
759                         client.BaseAddress = GetAccountUrl(account);
760                     try
761                     {
762                         client.Parameters.Clear();
763
764                         client.Head(container + "/" + objectName, 3);
765
766                         if (client.TimedOut)
767                             return ObjectInfo.Empty;
768
769                         switch (client.StatusCode)
770                         {
771                             case HttpStatusCode.OK:
772                             case HttpStatusCode.NoContent:
773                                 var keys = client.ResponseHeaders.AllKeys.AsQueryable();
774                                 var tags = client.GetMeta("X-Object-Meta-");
775                                 var extensions = (from key in keys
776                                                   where key.StartsWith("X-Object-") && !key.StartsWith("X-Object-Meta-")
777                                                   select new {Name = key, Value = client.ResponseHeaders[key]})
778                                     .ToDictionary(t => t.Name, t => t.Value);
779
780                                 var permissions=client.GetHeaderValue("X-Object-Sharing", true);
781                                 
782                                 
783                                 var info = new ObjectInfo
784                                                {
785                                                    Account = account,
786                                                    Container = container,
787                                                    Name = objectName,
788                                                    Hash = client.GetHeaderValue("ETag"),
789                                                    Content_Type = client.GetHeaderValue("Content-Type"),
790                                                    Bytes = Convert.ToInt64(client.GetHeaderValue("Content-Length",true)),
791                                                    Tags = tags,
792                                                    Last_Modified = client.LastModified,
793                                                    Extensions = extensions,
794                                                    ContentEncoding=client.GetHeaderValue("Content-Encoding",true),
795                                                    ContendDisposition = client.GetHeaderValue("Content-Disposition",true),
796                                                    Manifest=client.GetHeaderValue("X-Object-Manifest",true),
797                                                    PublicUrl=client.GetHeaderValue("X-Object-Public",true),                                                   
798                                                };
799                                 info.SetPermissions(permissions);
800                                 return info;
801                             case HttpStatusCode.NotFound:
802                                 return ObjectInfo.Empty;
803                             default:
804                                 throw new WebException(
805                                     String.Format("[FAIL] GetObjectInfo for {0} failed with unexpected status code {1}",
806                                                   objectName, client.StatusCode));
807                         }
808
809                     }
810                     catch (RetryException)
811                     {
812                         Log.WarnFormat("[RETRY FAIL] GetObjectInfo for {0} failed.",objectName);
813                         return ObjectInfo.Empty;
814                     }
815                     catch (WebException e)
816                     {
817                         Log.Error(
818                             String.Format("[FAIL] GetObjectInfo for {0} failed with unexpected status code {1}",
819                                           objectName, client.StatusCode), e);
820                         throw;
821                     }
822                 }                
823             }
824
825         }
826
827         public void CreateFolder(string account, string container, string folder)
828         {
829             if (String.IsNullOrWhiteSpace(container))
830                 throw new ArgumentNullException("container", "The container property can't be empty");
831             if (String.IsNullOrWhiteSpace(folder))
832                 throw new ArgumentNullException("folder", "The folder property can't be empty");
833             Contract.EndContractBlock();
834
835             var folderUrl=String.Format("{0}/{1}",container,folder);
836             using (var client = new RestClient(_baseClient))
837             {
838                 if (!String.IsNullOrWhiteSpace(account))
839                     client.BaseAddress = GetAccountUrl(account);
840
841                 client.Parameters.Clear();
842                 client.Headers.Add("Content-Type", @"application/directory");
843                 client.Headers.Add("Content-Length", "0");
844                 client.PutWithRetry(folderUrl, 3);
845
846                 if (client.StatusCode != HttpStatusCode.Created && client.StatusCode != HttpStatusCode.Accepted)
847                     throw CreateWebException("CreateFolder", client.StatusCode);
848             }
849         }
850
851      
852
853         public ContainerInfo GetContainerInfo(string account, string container)
854         {
855             if (String.IsNullOrWhiteSpace(container))
856                 throw new ArgumentNullException("container", "The container property can't be empty");
857             Contract.EndContractBlock();
858
859             using (var client = new RestClient(_baseClient))
860             {
861                 if (!String.IsNullOrWhiteSpace(account))
862                     client.BaseAddress = GetAccountUrl(account);                
863
864                 client.Head(container);
865                 switch (client.StatusCode)
866                 {
867                     case HttpStatusCode.OK:
868                     case HttpStatusCode.NoContent:
869                         var tags = client.GetMeta("X-Container-Meta-");
870                         var policies = client.GetMeta("X-Container-Policy-");
871
872                         var containerInfo = new ContainerInfo
873                                                 {
874                                                     Account=account,
875                                                     Name = container,
876                                                     Count =
877                                                         long.Parse(client.GetHeaderValue("X-Container-Object-Count")),
878                                                     Bytes = long.Parse(client.GetHeaderValue("X-Container-Bytes-Used")),
879                                                     BlockHash = client.GetHeaderValue("X-Container-Block-Hash"),
880                                                     BlockSize=int.Parse(client.GetHeaderValue("X-Container-Block-Size")),
881                                                     Last_Modified=client.LastModified,
882                                                     Tags=tags,
883                                                     Policies=policies
884                                                 };
885                         
886
887                         return containerInfo;
888                     case HttpStatusCode.NotFound:
889                         return ContainerInfo.Empty;
890                     default:
891                         throw CreateWebException("GetContainerInfo", client.StatusCode);
892                 }
893             }
894         }
895
896         public void CreateContainer(string account, string container)
897         {
898             if (String.IsNullOrWhiteSpace(account))
899                 throw new ArgumentNullException("account");
900             if (String.IsNullOrWhiteSpace(container))
901                 throw new ArgumentNullException("container");
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.PutWithRetry(container, 3);
910                 var expectedCodes = new[] {HttpStatusCode.Created, HttpStatusCode.Accepted, HttpStatusCode.OK};
911                 if (!expectedCodes.Contains(client.StatusCode))
912                     throw CreateWebException("CreateContainer", client.StatusCode);
913             }
914         }
915
916         public void DeleteContainer(string account, string container)
917         {
918             if (String.IsNullOrWhiteSpace(container))
919                 throw new ArgumentNullException("container", "The container property can't be empty");
920             Contract.EndContractBlock();
921
922             using (var client = new RestClient(_baseClient))
923             {
924                 if (!String.IsNullOrWhiteSpace(account))
925                     client.BaseAddress = GetAccountUrl(account);
926
927                 client.DeleteWithRetry(container, 3);
928                 var expectedCodes = new[] {HttpStatusCode.NotFound, HttpStatusCode.NoContent};
929                 if (!expectedCodes.Contains(client.StatusCode))
930                     throw CreateWebException("DeleteContainer", client.StatusCode);
931             }
932
933         }
934
935         /// <summary>
936         /// 
937         /// </summary>
938         /// <param name="account"></param>
939         /// <param name="container"></param>
940         /// <param name="objectName"></param>
941         /// <param name="fileName"></param>
942         /// <returns></returns>
943         /// <remarks>This method should have no timeout or a very long one</remarks>
944         //Asynchronously download the object specified by *objectName* in a specific *container* to 
945         // a local file
946         public Task GetObject(string account, string container, string objectName, string fileName)
947         {
948             if (String.IsNullOrWhiteSpace(container))
949                 throw new ArgumentNullException("container", "The container property can't be empty");
950             if (String.IsNullOrWhiteSpace(objectName))
951                 throw new ArgumentNullException("objectName", "The objectName property can't be empty");            
952             Contract.EndContractBlock();
953
954             try
955             {
956                 //WebClient, and by extension RestClient, are not thread-safe. Create a new RestClient
957                 //object to avoid concurrency errors.
958                 //
959                 //Download operations take a long time therefore they have no timeout.
960                 var client = new RestClient(_baseClient) { Timeout = 0 };
961                 if (!String.IsNullOrWhiteSpace(account))
962                     client.BaseAddress = GetAccountUrl(account);
963
964                 //The container and objectName are relative names. They are joined with the client's
965                 //BaseAddress to create the object's absolute address
966                 var builder = client.GetAddressBuilder(container, objectName);
967                 var uri = builder.Uri;
968
969                 //Download progress is reported to the Trace log
970                 Log.InfoFormat("[GET] START {0}", objectName);
971                 client.DownloadProgressChanged += (sender, args) => 
972                     Log.InfoFormat("[GET PROGRESS] {0} {1}% {2} of {3}",
973                                     fileName, args.ProgressPercentage,
974                                     args.BytesReceived,
975                                     args.TotalBytesToReceive);                                
976
977
978                 //Start downloading the object asynchronously
979                 var downloadTask = client.DownloadFileTask(uri, fileName);
980                 
981                 //Once the download completes
982                 return downloadTask.ContinueWith(download =>
983                                       {
984                                           //Delete the local client object
985                                           client.Dispose();
986                                           //And report failure or completion
987                                           if (download.IsFaulted)
988                                           {
989                                               Log.ErrorFormat("[GET] FAIL for {0} with \r{1}", objectName,
990                                                                download.Exception);
991                                           }
992                                           else
993                                           {
994                                               Log.InfoFormat("[GET] END {0}", objectName);                                             
995                                           }
996                                       });
997             }
998             catch (Exception exc)
999             {
1000                 Log.ErrorFormat("[GET] END {0} with {1}", objectName, exc);
1001                 throw;
1002             }
1003
1004
1005
1006         }
1007
1008         public Task<IList<string>> PutHashMap(string account, string container, string objectName, TreeHash hash)
1009         {
1010             if (String.IsNullOrWhiteSpace(container))
1011                 throw new ArgumentNullException("container");
1012             if (String.IsNullOrWhiteSpace(objectName))
1013                 throw new ArgumentNullException("objectName");
1014             if (hash==null)
1015                 throw new ArgumentNullException("hash");
1016             if (String.IsNullOrWhiteSpace(Token))
1017                 throw new InvalidOperationException("Invalid Token");
1018             if (StorageUrl == null)
1019                 throw new InvalidOperationException("Invalid Storage Url");
1020             Contract.EndContractBlock();
1021
1022
1023             //Don't use a timeout because putting the hashmap may be a long process
1024             var client = new RestClient(_baseClient) { Timeout = 0 };           
1025             if (!String.IsNullOrWhiteSpace(account))
1026                 client.BaseAddress = GetAccountUrl(account);
1027
1028             //The container and objectName are relative names. They are joined with the client's
1029             //BaseAddress to create the object's absolute address
1030             var builder = client.GetAddressBuilder(container, objectName);
1031             builder.Query = "format=json&hashmap";
1032             var uri = builder.Uri;
1033
1034
1035             //Send the tree hash as Json to the server            
1036             client.Headers[HttpRequestHeader.ContentType] = "application/octet-stream";
1037             var jsonHash = hash.ToJson();
1038             var uploadTask=client.UploadStringTask(uri, "PUT", jsonHash);
1039             
1040             return uploadTask.ContinueWith(t =>
1041             {
1042
1043                 var empty = (IList<string>)new List<string>();
1044                 
1045
1046                 //The server will respond either with 201-created if all blocks were already on the server
1047                 if (client.StatusCode == HttpStatusCode.Created)                    
1048                 {
1049                     //in which case we return an empty hash list
1050                     return empty;
1051                 }
1052                 //or with a 409-conflict and return the list of missing parts
1053                 //A 409 will cause an exception so we need to check t.IsFaulted to avoid propagating the exception                
1054                 if (t.IsFaulted)
1055                 {
1056                     var ex = t.Exception.InnerException;
1057                     var we = ex as WebException;
1058                     var response = we.Response as HttpWebResponse;
1059                     if (response!=null && response.StatusCode==HttpStatusCode.Conflict)
1060                     {
1061                         //In case of 409 the missing parts will be in the response content                        
1062                         using (var stream = response.GetResponseStream())
1063                         using(var reader=new StreamReader(stream))
1064                         {
1065                             Debug.Assert(stream.Position == 0);
1066                             //We used to have to cleanup the content before returning it because it contains
1067                             //error content after the list of hashes
1068                             //
1069                             //As of 30/1/2012, the result is a proper Json array so we don't need to read the content
1070                             //line by line
1071                             
1072                             var serializer = new JsonSerializer();                            
1073                             var hashes=(List<string>)serializer.Deserialize(reader, typeof (List<string>));
1074
1075                             return hashes;
1076                         }                        
1077                     }                    
1078                     //Any other status code is unexpected and the exception should be rethrown
1079                     throw ex;
1080                     
1081                 }
1082                 //Any other status code is unexpected but there was no exception. We can probably continue processing
1083                 Log.WarnFormat("Unexcpected status code when putting map: {0} - {1}",client.StatusCode,client.StatusDescription);                    
1084                 
1085                 return empty;
1086             });
1087
1088         }
1089
1090         public Task<byte[]> GetBlock(string account, string container, Uri relativeUrl, long start, long? end)
1091         {
1092             if (String.IsNullOrWhiteSpace(Token))
1093                 throw new InvalidOperationException("Invalid Token");
1094             if (StorageUrl == null)
1095                 throw new InvalidOperationException("Invalid Storage Url");
1096             if (String.IsNullOrWhiteSpace(container))
1097                 throw new ArgumentNullException("container");
1098             if (relativeUrl== null)
1099                 throw new ArgumentNullException("relativeUrl");
1100             if (end.HasValue && end<0)
1101                 throw new ArgumentOutOfRangeException("end");
1102             if (start<0)
1103                 throw new ArgumentOutOfRangeException("start");
1104             Contract.EndContractBlock();
1105
1106
1107             //Don't use a timeout because putting the hashmap may be a long process
1108             var client = new RestClient(_baseClient) {Timeout = 0, RangeFrom = start, RangeTo = end};
1109             if (!String.IsNullOrWhiteSpace(account))
1110                 client.BaseAddress = GetAccountUrl(account);
1111
1112             var builder = client.GetAddressBuilder(container, relativeUrl.ToString());
1113             var uri = builder.Uri;
1114
1115             return client.DownloadDataTask(uri)
1116                 .ContinueWith(t=>
1117                                   {
1118                                       client.Dispose();
1119                                       return t.Result;
1120                                   });
1121         }
1122
1123
1124         public async Task PostBlock(string account, string container, byte[] block, int offset, int count)
1125         {
1126             if (String.IsNullOrWhiteSpace(container))
1127                 throw new ArgumentNullException("container");
1128             if (block == null)
1129                 throw new ArgumentNullException("block");
1130             if (offset < 0 || offset >= block.Length)
1131                 throw new ArgumentOutOfRangeException("offset");
1132             if (count < 0 || count > block.Length)
1133                 throw new ArgumentOutOfRangeException("count");
1134             if (String.IsNullOrWhiteSpace(Token))
1135                 throw new InvalidOperationException("Invalid Token");
1136             if (StorageUrl == null)
1137                 throw new InvalidOperationException("Invalid Storage Url");                        
1138             Contract.EndContractBlock();
1139
1140
1141             try
1142             {
1143
1144             //Don't use a timeout because putting the hashmap may be a long process
1145                 using (var client = new RestClient(_baseClient) { Timeout = 0 })
1146                 {
1147                     if (!String.IsNullOrWhiteSpace(account))
1148                         client.BaseAddress = GetAccountUrl(account);
1149
1150                     var builder = client.GetAddressBuilder(container, "");
1151                     //We are doing an update
1152                     builder.Query = "update";
1153                     var uri = builder.Uri;
1154
1155                     client.Headers[HttpRequestHeader.ContentType] = "application/octet-stream";
1156
1157                     Log.InfoFormat("[BLOCK POST] START");
1158
1159                     client.UploadProgressChanged += (sender, args) =>
1160                                                     Log.InfoFormat("[BLOCK POST PROGRESS] {0}% {1} of {2}",
1161                                                                    args.ProgressPercentage, args.BytesSent,
1162                                                                    args.TotalBytesToSend);
1163                     client.UploadFileCompleted += (sender, args) =>
1164                                                   Log.InfoFormat("[BLOCK POST PROGRESS] Completed ");
1165
1166                     var buffer = new byte[count];
1167                     Buffer.BlockCopy(block, offset, buffer, 0, count);
1168                     //Send the block
1169                     await client.UploadDataTask(uri, "POST", buffer);
1170                     Log.InfoFormat("[BLOCK POST] END");
1171                 }
1172             }
1173             catch (Exception exc)
1174             {
1175                 Log.ErrorFormat("[BLOCK POST] FAIL with \r{0}", exc);                                        
1176                 throw;
1177             }
1178         }
1179
1180
1181         public async Task<TreeHash> GetHashMap(string account, string container, string objectName)
1182         {
1183             if (String.IsNullOrWhiteSpace(container))
1184                 throw new ArgumentNullException("container");
1185             if (String.IsNullOrWhiteSpace(objectName))
1186                 throw new ArgumentNullException("objectName");
1187             if (String.IsNullOrWhiteSpace(Token))
1188                 throw new InvalidOperationException("Invalid Token");
1189             if (StorageUrl == null)
1190                 throw new InvalidOperationException("Invalid Storage Url");
1191             Contract.EndContractBlock();
1192
1193             try
1194             {
1195                 //WebClient, and by extension RestClient, are not thread-safe. Create a new RestClient
1196                 //object to avoid concurrency errors.
1197                 //
1198                 //Download operations take a long time therefore they have no timeout.
1199                 //TODO: Do they really? this is a hashmap operation, not a download
1200                 
1201                 //Start downloading the object asynchronously
1202                 using (var client = new RestClient(_baseClient) { Timeout = 0 })
1203                 {
1204                     if (!String.IsNullOrWhiteSpace(account))
1205                         client.BaseAddress = GetAccountUrl(account);
1206
1207                     //The container and objectName are relative names. They are joined with the client's
1208                     //BaseAddress to create the object's absolute address
1209                     var builder = client.GetAddressBuilder(container, objectName);
1210                     builder.Query = "format=json&hashmap";
1211                     var uri = builder.Uri;
1212
1213
1214                     var json = await client.DownloadStringTaskAsync(uri);
1215                     var treeHash = TreeHash.Parse(json);
1216                     Log.InfoFormat("[GET HASH] END {0}", objectName);
1217                     return treeHash;
1218                 }
1219             }
1220             catch (Exception exc)
1221             {
1222                 Log.ErrorFormat("[GET HASH] END {0} with {1}", objectName, exc);
1223                 throw;
1224             }
1225
1226         }
1227
1228
1229         /// <summary>
1230         /// 
1231         /// </summary>
1232         /// <param name="account"></param>
1233         /// <param name="container"></param>
1234         /// <param name="objectName"></param>
1235         /// <param name="fileName"></param>
1236         /// <param name="hash">Optional hash value for the file. If no hash is provided, the method calculates a new hash</param>
1237         /// <remarks>>This method should have no timeout or a very long one</remarks>
1238         public async Task PutObject(string account, string container, string objectName, string fileName, string hash = null, string contentType = "application/octet-stream")
1239         {
1240             if (String.IsNullOrWhiteSpace(container))
1241                 throw new ArgumentNullException("container", "The container property can't be empty");
1242             if (String.IsNullOrWhiteSpace(objectName))
1243                 throw new ArgumentNullException("objectName", "The objectName property can't be empty");
1244             if (String.IsNullOrWhiteSpace(fileName))
1245                 throw new ArgumentNullException("fileName", "The fileName property can't be empty");
1246 /*
1247             if (!File.Exists(fileName) && !Directory.Exists(fileName))
1248                 throw new FileNotFoundException("The file or directory does not exist",fileName);
1249 */
1250             Contract.EndContractBlock();
1251             
1252             try
1253             {
1254
1255                 using (var client = new RestClient(_baseClient) { Timeout = 0 })
1256                 {
1257                     if (!String.IsNullOrWhiteSpace(account))
1258                         client.BaseAddress = GetAccountUrl(account);
1259
1260                     var builder = client.GetAddressBuilder(container, objectName);
1261                     var uri = builder.Uri;
1262
1263                     string etag = hash ?? CalculateHash(fileName);
1264
1265                     client.Headers.Add("Content-Type", contentType);
1266                     client.Headers.Add("ETag", etag);
1267
1268
1269                     Log.InfoFormat("[PUT] START {0}", objectName);
1270                     client.UploadProgressChanged += (sender, args) =>
1271                                                         {
1272                                                             using (ThreadContext.Stacks["PUT"].Push("Progress"))
1273                                                             {
1274                                                                 Log.InfoFormat("{0} {1}% {2} of {3}", fileName,
1275                                                                                args.ProgressPercentage,
1276                                                                                args.BytesSent, args.TotalBytesToSend);
1277                                                             }
1278                                                         };
1279
1280                     client.UploadFileCompleted += (sender, args) =>
1281                                                       {
1282                                                           using (ThreadContext.Stacks["PUT"].Push("Progress"))
1283                                                           {
1284                                                               Log.InfoFormat("Completed {0}", fileName);
1285                                                           }
1286                                                       };
1287                     if (contentType=="application/directory")
1288                         await client.UploadDataTaskAsync(uri, "PUT", new byte[0]);
1289                     else
1290                         await client.UploadFileTaskAsync(uri, "PUT", fileName);
1291                 }
1292
1293                 Log.InfoFormat("[PUT] END {0}", objectName);
1294             }
1295             catch (Exception exc)
1296             {
1297                 Log.ErrorFormat("[PUT] END {0} with {1}", objectName, exc);
1298                 throw;
1299             }                
1300
1301         }
1302        
1303         
1304         private static string CalculateHash(string fileName)
1305         {
1306             Contract.Requires(!String.IsNullOrWhiteSpace(fileName));
1307             Contract.EndContractBlock();
1308
1309             string hash;
1310             using (var hasher = MD5.Create())
1311             using(var stream=File.OpenRead(fileName))
1312             {
1313                 var hashBuilder=new StringBuilder();
1314                 foreach (byte b in hasher.ComputeHash(stream))
1315                     hashBuilder.Append(b.ToString("x2").ToLower());
1316                 hash = hashBuilder.ToString();                
1317             }
1318             return hash;
1319         }
1320         
1321         public void MoveObject(string account, string sourceContainer, string oldObjectName, string targetContainer, string newObjectName)
1322         {
1323             if (String.IsNullOrWhiteSpace(sourceContainer))
1324                 throw new ArgumentNullException("sourceContainer", "The container property can't be empty");
1325             if (String.IsNullOrWhiteSpace(oldObjectName))
1326                 throw new ArgumentNullException("oldObjectName", "The oldObjectName property can't be empty");
1327             if (String.IsNullOrWhiteSpace(targetContainer))
1328                 throw new ArgumentNullException("targetContainer", "The container property can't be empty");
1329             if (String.IsNullOrWhiteSpace(newObjectName))
1330                 throw new ArgumentNullException("newObjectName", "The newObjectName property can't be empty");
1331             Contract.EndContractBlock();
1332
1333             var targetUrl = targetContainer + "/" + newObjectName;
1334             var sourceUrl = String.Format("/{0}/{1}", sourceContainer, oldObjectName);
1335
1336             using (var client = new RestClient(_baseClient))
1337             {
1338                 if (!String.IsNullOrWhiteSpace(account))
1339                     client.BaseAddress = GetAccountUrl(account);
1340
1341                 client.Headers.Add("X-Move-From", sourceUrl);
1342                 client.PutWithRetry(targetUrl, 3);
1343
1344                 var expectedCodes = new[] {HttpStatusCode.OK, HttpStatusCode.NoContent, HttpStatusCode.Created};
1345                 if (!expectedCodes.Contains(client.StatusCode))
1346                     throw CreateWebException("MoveObject", client.StatusCode);
1347             }
1348         }
1349
1350         public void DeleteObject(string account, string sourceContainer, string objectName)
1351         {            
1352             if (String.IsNullOrWhiteSpace(sourceContainer))
1353                 throw new ArgumentNullException("sourceContainer", "The container property can't be empty");
1354             if (String.IsNullOrWhiteSpace(objectName))
1355                 throw new ArgumentNullException("objectName", "The oldObjectName property can't be empty");
1356             Contract.EndContractBlock();
1357
1358             var targetUrl = FolderConstants.TrashContainer + "/" + objectName;
1359             var sourceUrl = String.Format("/{0}/{1}", sourceContainer, objectName);
1360
1361             using (var client = new RestClient(_baseClient))
1362             {
1363                 if (!String.IsNullOrWhiteSpace(account))
1364                     client.BaseAddress = GetAccountUrl(account);
1365
1366                 client.Headers.Add("X-Move-From", sourceUrl);
1367                 client.AllowedStatusCodes.Add(HttpStatusCode.NotFound);
1368                 client.PutWithRetry(targetUrl, 3);
1369
1370                 var expectedCodes = new[] {HttpStatusCode.OK, HttpStatusCode.NoContent, HttpStatusCode.Created,HttpStatusCode.NotFound};
1371                 if (!expectedCodes.Contains(client.StatusCode))
1372                     throw CreateWebException("DeleteObject", client.StatusCode);
1373             }
1374         }
1375
1376       
1377         private static WebException CreateWebException(string operation, HttpStatusCode statusCode)
1378         {
1379             return new WebException(String.Format("{0} failed with unexpected status code {1}", operation, statusCode));
1380         }
1381
1382
1383 /*
1384         public IEnumerable<ObjectInfo> ListDirectories(ContainerInfo container)
1385         {
1386             var directories=this.ListObjects(container.Account, container.Name, "/");
1387         }
1388 */
1389     }
1390
1391     public class ShareAccountInfo
1392     {
1393         public DateTime? last_modified { get; set; }
1394         public string name { get; set; }
1395     }
1396 }