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