Fixes to hashing
[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;
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             Debug.Assert(_baseClient!=null);
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                     uriBuilder.Query = "update=";
516                     var uri = uriBuilder.Uri.MakeRelativeUri(this.RootAddressUri);*/
517                     var address = String.Format("{0}/{1}?update=",objectInfo.Container, objectInfo.Name);
518                     client.PostWithRetry(address,"application/xml");
519                     
520                     //client.UploadValues(uri,new NameValueCollection());
521
522
523                     client.AssertStatusOK("UpdateMetadata failed");
524                     //If the status is NOT ACCEPTED or OK we have a problem
525                     if (!(client.StatusCode == HttpStatusCode.Accepted || client.StatusCode == HttpStatusCode.OK))
526                     {
527                         Log.Error("Failed to update metadata");
528                         throw new Exception("Failed to update metadata");
529                     }
530
531                     if (Log.IsDebugEnabled) Log.DebugFormat("END");
532                 }
533             }
534
535         }
536
537         public void UpdateMetadata(ContainerInfo containerInfo)
538         {
539             if (containerInfo == null)
540                 throw new ArgumentNullException("containerInfo");
541             Contract.EndContractBlock();
542
543             using (ThreadContext.Stacks["Containers"].Push("UpdateMetadata"))
544             {
545                 if (Log.IsDebugEnabled) Log.DebugFormat("START");
546
547
548                 using(var client=new RestClient(_baseClient))
549                 {
550
551                     client.BaseAddress = GetAccountUrl(containerInfo.Account);
552                     
553                     client.Parameters.Clear();
554                     
555
556                     //Set Tags
557                     foreach (var tag in containerInfo.Tags)
558                     {
559                         var headerTag = String.Format("X-Container-Meta-{0}", tag.Key);
560                         client.Headers.Add(headerTag, tag.Value);
561                     }
562
563                     
564                     //Set Policies
565                     foreach (var policy in containerInfo.Policies)
566                     {
567                         var headerPolicy = String.Format("X-Container-Policy-{0}", policy.Key);
568                         client.Headers.Add(headerPolicy, policy.Value);
569                     }
570
571
572                     var uriBuilder = client.GetAddressBuilder(containerInfo.Name,"");
573                     var uri = uriBuilder.Uri;
574
575                     client.UploadValues(uri,new NameValueCollection());
576
577
578                     client.AssertStatusOK("UpdateMetadata failed");
579                     //If the status is NOT ACCEPTED or OK we have a problem
580                     if (!(client.StatusCode == HttpStatusCode.Accepted || client.StatusCode == HttpStatusCode.OK))
581                     {
582                         Log.Error("Failed to update metadata");
583                         throw new Exception("Failed to update metadata");
584                     }
585
586                     if (Log.IsDebugEnabled) Log.DebugFormat("END");
587                 }
588             }
589
590         }
591
592
593         public IList<ObjectInfo> ListObjects(string account, string container, DateTime? since = null)
594         {
595             if (String.IsNullOrWhiteSpace(container))
596                 throw new ArgumentNullException("container");
597             Contract.EndContractBlock();
598
599             using (ThreadContext.Stacks["Objects"].Push("List"))
600             {
601                 if (Log.IsDebugEnabled) Log.DebugFormat("START");
602
603                 using (var client = new RestClient(_baseClient))
604                 {
605                     if (!String.IsNullOrWhiteSpace(account))
606                         client.BaseAddress = GetAccountUrl(account);
607
608                     client.Parameters.Clear();
609                     client.Parameters.Add("format", "json");
610                     client.IfModifiedSince = since;
611                     var content = client.DownloadStringWithRetry(container, 3);
612
613                     client.AssertStatusOK("ListObjects failed");
614
615                     if (client.StatusCode==HttpStatusCode.NotModified)
616                         return new[]{new NoModificationInfo(account,container)};
617                     //If the result is empty, return an empty list,
618                     var infos = String.IsNullOrWhiteSpace(content)
619                                     ? new List<ObjectInfo>()
620                                 //Otherwise deserialize the object list into a list of ObjectInfos
621                                     : JsonConvert.DeserializeObject<IList<ObjectInfo>>(content);
622
623                     foreach (var info in infos)
624                     {
625                         info.Container = container;
626                         info.Account = account;
627                         info.StorageUri = this.StorageUrl;
628                     }
629                     if (Log.IsDebugEnabled) Log.DebugFormat("END");
630                     return infos;
631                 }
632             }
633         }
634
635         public IList<ObjectInfo> ListObjects(string account, string container, string folder, DateTime? since = null)
636         {
637             if (String.IsNullOrWhiteSpace(container))
638                 throw new ArgumentNullException("container");
639 /*
640             if (String.IsNullOrWhiteSpace(folder))
641                 throw new ArgumentNullException("folder");
642 */
643             Contract.EndContractBlock();
644
645             using (ThreadContext.Stacks["Objects"].Push("List"))
646             {
647                 if (Log.IsDebugEnabled) Log.DebugFormat("START");
648
649                 using (var client = new RestClient(_baseClient))
650                 {
651                     if (!String.IsNullOrWhiteSpace(account))
652                         client.BaseAddress = GetAccountUrl(account);
653
654                     client.Parameters.Clear();
655                     client.Parameters.Add("format", "json");
656                     client.Parameters.Add("path", folder);
657                     client.IfModifiedSince = since;
658                     var content = client.DownloadStringWithRetry(container, 3);
659                     client.AssertStatusOK("ListObjects failed");
660
661                     if (client.StatusCode==HttpStatusCode.NotModified)
662                         return new[]{new NoModificationInfo(account,container,folder)};
663
664                     var infos = JsonConvert.DeserializeObject<IList<ObjectInfo>>(content);
665                     foreach (var info in infos)
666                     {
667                         info.Account = account;
668                         if (info.Container == null)
669                             info.Container = container;
670                         info.StorageUri = this.StorageUrl;
671                     }
672                     if (Log.IsDebugEnabled) Log.DebugFormat("END");
673                     return infos;
674                 }
675             }
676         }
677
678  
679         public bool ContainerExists(string account, string container)
680         {
681             if (String.IsNullOrWhiteSpace(container))
682                 throw new ArgumentNullException("container", "The container property can't be empty");
683             Contract.EndContractBlock();
684
685             using (ThreadContext.Stacks["Containters"].Push("Exists"))
686             {
687                 if (Log.IsDebugEnabled) Log.DebugFormat("START");
688
689                 using (var client = new RestClient(_baseClient))
690                 {
691                     if (!String.IsNullOrWhiteSpace(account))
692                         client.BaseAddress = GetAccountUrl(account);
693
694                     client.Parameters.Clear();
695                     client.Head(container, 3);
696                                         
697                     bool result;
698                     switch (client.StatusCode)
699                     {
700                         case HttpStatusCode.OK:
701                         case HttpStatusCode.NoContent:
702                             result=true;
703                             break;
704                         case HttpStatusCode.NotFound:
705                             result=false;
706                             break;
707                         default:
708                             throw CreateWebException("ContainerExists", client.StatusCode);
709                     }
710                     if (Log.IsDebugEnabled) Log.DebugFormat("END");
711
712                     return result;
713                 }
714                 
715             }
716         }
717
718         public bool ObjectExists(string account, string container, string objectName)
719         {
720             if (String.IsNullOrWhiteSpace(container))
721                 throw new ArgumentNullException("container", "The container property can't be empty");
722             if (String.IsNullOrWhiteSpace(objectName))
723                 throw new ArgumentNullException("objectName", "The objectName property can't be empty");
724             Contract.EndContractBlock();
725
726             using (var client = new RestClient(_baseClient))
727             {
728                 if (!String.IsNullOrWhiteSpace(account))
729                     client.BaseAddress = GetAccountUrl(account);
730
731                 client.Parameters.Clear();
732                 client.Head(container + "/" + objectName, 3);
733
734                 switch (client.StatusCode)
735                 {
736                     case HttpStatusCode.OK:
737                     case HttpStatusCode.NoContent:
738                         return true;
739                     case HttpStatusCode.NotFound:
740                         return false;
741                     default:
742                         throw CreateWebException("ObjectExists", client.StatusCode);
743                 }
744             }
745
746         }
747
748         public ObjectInfo GetObjectInfo(string account, string container, string objectName)
749         {
750             if (String.IsNullOrWhiteSpace(container))
751                 throw new ArgumentNullException("container", "The container property can't be empty");
752             if (String.IsNullOrWhiteSpace(objectName))
753                 throw new ArgumentNullException("objectName", "The objectName property can't be empty");
754             Contract.EndContractBlock();
755
756             using (ThreadContext.Stacks["Objects"].Push("GetObjectInfo"))
757             {                
758
759                 using (var client = new RestClient(_baseClient))
760                 {
761                     if (!String.IsNullOrWhiteSpace(account))
762                         client.BaseAddress = GetAccountUrl(account);
763                     try
764                     {
765                         client.Parameters.Clear();
766
767                         client.Head(container + "/" + objectName, 3);
768
769                         if (client.TimedOut)
770                             return ObjectInfo.Empty;
771
772                         switch (client.StatusCode)
773                         {
774                             case HttpStatusCode.OK:
775                             case HttpStatusCode.NoContent:
776                                 var keys = client.ResponseHeaders.AllKeys.AsQueryable();
777                                 var tags = client.GetMeta("X-Object-Meta-");
778                                 var extensions = (from key in keys
779                                                   where key.StartsWith("X-Object-") && !key.StartsWith("X-Object-Meta-")
780                                                   select new {Name = key, Value = client.ResponseHeaders[key]})
781                                     .ToDictionary(t => t.Name, t => t.Value);
782
783                                 var permissions=client.GetHeaderValue("X-Object-Sharing", true);
784                                 
785                                 
786                                 var info = new ObjectInfo
787                                                {
788                                                    Account = account,
789                                                    Container = container,
790                                                    Name = objectName,
791                                                    ETag = client.GetHeaderValue("ETag"),
792                                                    X_Object_Hash = client.GetHeaderValue("X-Object-Hash"),
793                                                    Content_Type = client.GetHeaderValue("Content-Type"),
794                                                    Bytes = Convert.ToInt64(client.GetHeaderValue("Content-Length",true)),
795                                                    Tags = tags,
796                                                    Last_Modified = client.LastModified,
797                                                    Extensions = extensions,
798                                                    ContentEncoding=client.GetHeaderValue("Content-Encoding",true),
799                                                    ContendDisposition = client.GetHeaderValue("Content-Disposition",true),
800                                                    Manifest=client.GetHeaderValue("X-Object-Manifest",true),
801                                                    PublicUrl=client.GetHeaderValue("X-Object-Public",true),  
802                                                    StorageUri=this.StorageUrl,
803                                                };
804                                 info.SetPermissions(permissions);
805                                 return info;
806                             case HttpStatusCode.NotFound:
807                                 return ObjectInfo.Empty;
808                             default:
809                                 throw new WebException(
810                                     String.Format("[FAIL] GetObjectInfo for {0} failed with unexpected status code {1}",
811                                                   objectName, client.StatusCode));
812                         }
813
814                     }
815                     catch (RetryException)
816                     {
817                         Log.WarnFormat("[RETRY FAIL] GetObjectInfo for {0} failed.",objectName);
818                         return ObjectInfo.Empty;
819                     }
820                     catch (WebException e)
821                     {
822                         Log.Error(
823                             String.Format("[FAIL] GetObjectInfo for {0} failed with unexpected status code {1}",
824                                           objectName, client.StatusCode), e);
825                         throw;
826                     }
827                 }                
828             }
829
830         }
831
832         public void CreateFolder(string account, string container, string folder)
833         {
834             if (String.IsNullOrWhiteSpace(container))
835                 throw new ArgumentNullException("container", "The container property can't be empty");
836             if (String.IsNullOrWhiteSpace(folder))
837                 throw new ArgumentNullException("folder", "The folder property can't be empty");
838             Contract.EndContractBlock();
839
840             var folderUrl=String.Format("{0}/{1}",container,folder);
841             using (var client = new RestClient(_baseClient))
842             {
843                 if (!String.IsNullOrWhiteSpace(account))
844                     client.BaseAddress = GetAccountUrl(account);
845
846                 client.Parameters.Clear();
847                 client.Headers.Add("Content-Type", @"application/directory");
848                 client.Headers.Add("Content-Length", "0");
849                 client.PutWithRetry(folderUrl, 3);
850
851                 if (client.StatusCode != HttpStatusCode.Created && client.StatusCode != HttpStatusCode.Accepted)
852                     throw CreateWebException("CreateFolder", client.StatusCode);
853             }
854         }
855
856      
857
858         public ContainerInfo GetContainerInfo(string account, string container)
859         {
860             if (String.IsNullOrWhiteSpace(container))
861                 throw new ArgumentNullException("container", "The container property can't be empty");
862             Contract.EndContractBlock();
863
864             using (var client = new RestClient(_baseClient))
865             {
866                 if (!String.IsNullOrWhiteSpace(account))
867                     client.BaseAddress = GetAccountUrl(account);                
868
869                 client.Head(container);
870                 switch (client.StatusCode)
871                 {
872                     case HttpStatusCode.OK:
873                     case HttpStatusCode.NoContent:
874                         var tags = client.GetMeta("X-Container-Meta-");
875                         var policies = client.GetMeta("X-Container-Policy-");
876
877                         var containerInfo = new ContainerInfo
878                                                 {
879                                                     Account=account,
880                                                     Name = container,
881                                                     StorageUrl=this.StorageUrl.ToString(),
882                                                     Count =
883                                                         long.Parse(client.GetHeaderValue("X-Container-Object-Count")),
884                                                     Bytes = long.Parse(client.GetHeaderValue("X-Container-Bytes-Used")),
885                                                     BlockHash = client.GetHeaderValue("X-Container-Block-Hash"),
886                                                     BlockSize=int.Parse(client.GetHeaderValue("X-Container-Block-Size")),
887                                                     Last_Modified=client.LastModified,
888                                                     Tags=tags,
889                                                     Policies=policies
890                                                 };
891                         
892
893                         return containerInfo;
894                     case HttpStatusCode.NotFound:
895                         return ContainerInfo.Empty;
896                     default:
897                         throw CreateWebException("GetContainerInfo", client.StatusCode);
898                 }
899             }
900         }
901
902         public void CreateContainer(string account, string container)
903         {
904             if (String.IsNullOrWhiteSpace(account))
905                 throw new ArgumentNullException("account");
906             if (String.IsNullOrWhiteSpace(container))
907                 throw new ArgumentNullException("container");
908             Contract.EndContractBlock();
909
910             using (var client = new RestClient(_baseClient))
911             {
912                 if (!String.IsNullOrWhiteSpace(account))
913                     client.BaseAddress = GetAccountUrl(account);
914
915                 client.PutWithRetry(container, 3);
916                 var expectedCodes = new[] {HttpStatusCode.Created, HttpStatusCode.Accepted, HttpStatusCode.OK};
917                 if (!expectedCodes.Contains(client.StatusCode))
918                     throw CreateWebException("CreateContainer", client.StatusCode);
919             }
920         }
921
922         public void DeleteContainer(string account, string container)
923         {
924             if (String.IsNullOrWhiteSpace(container))
925                 throw new ArgumentNullException("container", "The container property can't be empty");
926             Contract.EndContractBlock();
927
928             using (var client = new RestClient(_baseClient))
929             {
930                 if (!String.IsNullOrWhiteSpace(account))
931                     client.BaseAddress = GetAccountUrl(account);
932
933                 client.DeleteWithRetry(container, 3);
934                 var expectedCodes = new[] {HttpStatusCode.NotFound, HttpStatusCode.NoContent};
935                 if (!expectedCodes.Contains(client.StatusCode))
936                     throw CreateWebException("DeleteContainer", client.StatusCode);
937             }
938
939         }
940
941         /// <summary>
942         /// 
943         /// </summary>
944         /// <param name="account"></param>
945         /// <param name="container"></param>
946         /// <param name="objectName"></param>
947         /// <param name="fileName"></param>
948         /// <returns></returns>
949         /// <remarks>This method should have no timeout or a very long one</remarks>
950         //Asynchronously download the object specified by *objectName* in a specific *container* to 
951         // a local file
952         public async Task GetObject(string account, string container, string objectName, string fileName,CancellationToken cancellationToken)
953         {
954             if (String.IsNullOrWhiteSpace(container))
955                 throw new ArgumentNullException("container", "The container property can't be empty");
956             if (String.IsNullOrWhiteSpace(objectName))
957                 throw new ArgumentNullException("objectName", "The objectName property can't be empty");            
958             Contract.EndContractBlock();
959
960             try
961             {
962                 //WebClient, and by extension RestClient, are not thread-safe. Create a new RestClient
963                 //object to avoid concurrency errors.
964                 //
965                 //Download operations take a long time therefore they have no timeout.
966                 using(var client = new RestClient(_baseClient) { Timeout = 0 })
967                 {
968                     if (!String.IsNullOrWhiteSpace(account))
969                         client.BaseAddress = GetAccountUrl(account);
970
971                     //The container and objectName are relative names. They are joined with the client's
972                     //BaseAddress to create the object's absolute address
973                     var builder = client.GetAddressBuilder(container, objectName);
974                     var uri = builder.Uri;
975
976                     //Download progress is reported to the Trace log
977                     Log.InfoFormat("[GET] START {0}", objectName);
978                     client.DownloadProgressChanged += (sender, args) =>
979                                                       Log.InfoFormat("[GET PROGRESS] {0} {1}% {2} of {3}",
980                                                                      fileName, args.ProgressPercentage,
981                                                                      args.BytesReceived,
982                                                                      args.TotalBytesToReceive);
983
984                     
985                     //Start downloading the object asynchronously
986                     await client.DownloadFileTaskAsync(uri, fileName,cancellationToken);
987
988                     //Once the download completes
989                     //Delete the local client object
990                 }
991                 //And report failure or completion
992             }
993             catch (Exception exc)
994             {
995                 Log.ErrorFormat("[GET] FAIL {0} with {1}", objectName, exc);
996                 throw;
997             }
998
999             Log.InfoFormat("[GET] END {0}", objectName);                                             
1000
1001
1002         }
1003
1004         public Task<IList<string>> PutHashMap(string account, string container, string objectName, TreeHash hash)
1005         {
1006             if (String.IsNullOrWhiteSpace(container))
1007                 throw new ArgumentNullException("container");
1008             if (String.IsNullOrWhiteSpace(objectName))
1009                 throw new ArgumentNullException("objectName");
1010             if (hash==null)
1011                 throw new ArgumentNullException("hash");
1012             if (String.IsNullOrWhiteSpace(Token))
1013                 throw new InvalidOperationException("Invalid Token");
1014             if (StorageUrl == null)
1015                 throw new InvalidOperationException("Invalid Storage Url");
1016             Contract.EndContractBlock();
1017
1018
1019             //Don't use a timeout because putting the hashmap may be a long process
1020             var client = new RestClient(_baseClient) { Timeout = 0 };           
1021             if (!String.IsNullOrWhiteSpace(account))
1022                 client.BaseAddress = GetAccountUrl(account);
1023
1024             //The container and objectName are relative names. They are joined with the client's
1025             //BaseAddress to create the object's absolute address
1026             var builder = client.GetAddressBuilder(container, objectName);
1027             builder.Query = "format=json&hashmap";
1028             var uri = builder.Uri;
1029
1030
1031             //Send the tree hash as Json to the server            
1032             client.Headers[HttpRequestHeader.ContentType] = "application/octet-stream";
1033             var jsonHash = hash.ToJson();
1034             var uploadTask=client.UploadStringTask(uri, "PUT", jsonHash);
1035             if (Log.IsDebugEnabled)
1036                 Log.DebugFormat("Hashes:\r\n{0}", jsonHash);
1037             return uploadTask.ContinueWith(t =>
1038             {
1039
1040                 var empty = (IList<string>)new List<string>();
1041                 
1042
1043                 //The server will respond either with 201-created if all blocks were already on the server
1044                 if (client.StatusCode == HttpStatusCode.Created)                    
1045                 {
1046                     //in which case we return an empty hash list
1047                     return empty;
1048                 }
1049                 //or with a 409-conflict and return the list of missing parts
1050                 //A 409 will cause an exception so we need to check t.IsFaulted to avoid propagating the exception                
1051                 if (t.IsFaulted)
1052                 {
1053                     var ex = t.Exception.InnerException;
1054                     var we = ex as WebException;
1055                     var response = we.Response as HttpWebResponse;
1056                     if (response!=null && response.StatusCode==HttpStatusCode.Conflict)
1057                     {
1058                         //In case of 409 the missing parts will be in the response content                        
1059                         using (var stream = response.GetResponseStream())
1060                         using(var reader=stream.GetLoggedReader(Log))
1061                         {
1062                             //We used to have to cleanup the content before returning it because it contains
1063                             //error content after the list of hashes
1064                             //
1065                             //As of 30/1/2012, the result is a proper Json array so we don't need to read the content
1066                             //line by line
1067                             
1068                             var serializer = new JsonSerializer();                            
1069                             serializer.Error += (sender, args) => Log.ErrorFormat("Deserialization error at [{0}] [{1}]", args.ErrorContext.Error, args.ErrorContext.Member);
1070                             var hashes = (List<string>)serializer.Deserialize(reader, typeof(List<string>));
1071                             return hashes;
1072                         }                        
1073                     }                    
1074                     //Any other status code is unexpected and the exception should be rethrown
1075                     Log.LogError(response);
1076                     throw ex;
1077                     
1078                 }
1079
1080                 //Any other status code is unexpected but there was no exception. We can probably continue processing
1081                 Log.WarnFormat("Unexcpected status code when putting map: {0} - {1}",client.StatusCode,client.StatusDescription);                    
1082                 
1083                 return empty;
1084             });
1085
1086         }
1087
1088
1089         public async Task<byte[]> GetBlock(string account, string container, Uri relativeUrl, long start, long? end, CancellationToken cancellationToken)
1090         {
1091             if (String.IsNullOrWhiteSpace(Token))
1092                 throw new InvalidOperationException("Invalid Token");
1093             if (StorageUrl == null)
1094                 throw new InvalidOperationException("Invalid Storage Url");
1095             if (String.IsNullOrWhiteSpace(container))
1096                 throw new ArgumentNullException("container");
1097             if (relativeUrl == null)
1098                 throw new ArgumentNullException("relativeUrl");
1099             if (end.HasValue && end < 0)
1100                 throw new ArgumentOutOfRangeException("end");
1101             if (start < 0)
1102                 throw new ArgumentOutOfRangeException("start");
1103             Contract.EndContractBlock();
1104
1105             //Don't use a timeout because putting the hashmap may be a long process
1106             using (var client = new RestClient(_baseClient) {Timeout = 0, RangeFrom = start, RangeTo = end})
1107             {
1108                 if (!String.IsNullOrWhiteSpace(account))
1109                     client.BaseAddress = GetAccountUrl(account);
1110
1111                 var builder = client.GetAddressBuilder(container, relativeUrl.ToString());
1112                 var uri = builder.Uri;
1113
1114                 var result = await client.DownloadDataTaskAsync(uri, cancellationToken);
1115                 return result;
1116             }
1117         }
1118
1119
1120         public async Task PostBlock(string account, string container, byte[] block, int offset, int count)
1121         {
1122             if (String.IsNullOrWhiteSpace(container))
1123                 throw new ArgumentNullException("container");
1124             if (block == null)
1125                 throw new ArgumentNullException("block");
1126             if (offset < 0 || offset >= block.Length)
1127                 throw new ArgumentOutOfRangeException("offset");
1128             if (count < 0 || count > block.Length)
1129                 throw new ArgumentOutOfRangeException("count");
1130             if (String.IsNullOrWhiteSpace(Token))
1131                 throw new InvalidOperationException("Invalid Token");
1132             if (StorageUrl == null)
1133                 throw new InvalidOperationException("Invalid Storage Url");                        
1134             Contract.EndContractBlock();
1135
1136
1137             try
1138             {
1139
1140             //Don't use a timeout because putting the hashmap may be a long process
1141                 using (var client = new RestClient(_baseClient) { Timeout = 0 })
1142                 {                    
1143                     if (!String.IsNullOrWhiteSpace(account))
1144                         client.BaseAddress = GetAccountUrl(account);
1145
1146                     var builder = client.GetAddressBuilder(container, "");
1147                     //We are doing an update
1148                     builder.Query = "update";
1149                     var uri = builder.Uri;
1150
1151                     client.Headers[HttpRequestHeader.ContentType] = "application/octet-stream";
1152
1153                     Log.InfoFormat("[BLOCK POST] START");
1154
1155                     client.UploadProgressChanged += (sender, args) =>
1156                                                     Log.InfoFormat("[BLOCK POST PROGRESS] {0}% {1} of {2}",
1157                                                                    args.ProgressPercentage, args.BytesSent,
1158                                                                    args.TotalBytesToSend);
1159                     client.UploadFileCompleted += (sender, args) =>
1160                                                   Log.InfoFormat("[BLOCK POST PROGRESS] Completed ");
1161
1162                     var buffer = new byte[count];
1163                     Buffer.BlockCopy(block, offset, buffer, 0, count);
1164                     //Send the block
1165                     await client.UploadDataTaskAsync(uri, "POST", buffer);                    
1166                     Log.InfoFormat("[BLOCK POST] END");
1167                 }
1168             }
1169             catch (Exception exc)
1170             {
1171                 Log.ErrorFormat("[BLOCK POST] FAIL with \r{0}", exc);                                        
1172                 throw;
1173             }
1174         }
1175
1176
1177         public async Task<TreeHash> GetHashMap(string account, string container, string objectName)
1178         {
1179             if (String.IsNullOrWhiteSpace(container))
1180                 throw new ArgumentNullException("container");
1181             if (String.IsNullOrWhiteSpace(objectName))
1182                 throw new ArgumentNullException("objectName");
1183             if (String.IsNullOrWhiteSpace(Token))
1184                 throw new InvalidOperationException("Invalid Token");
1185             if (StorageUrl == null)
1186                 throw new InvalidOperationException("Invalid Storage Url");
1187             Contract.EndContractBlock();
1188
1189             try
1190             {
1191                 //WebClient, and by extension RestClient, are not thread-safe. Create a new RestClient
1192                 //object to avoid concurrency errors.
1193                 //
1194                 //Download operations take a long time therefore they have no timeout.
1195                 //TODO: Do they really? this is a hashmap operation, not a download
1196                 
1197                 //Start downloading the object asynchronously
1198                 using (var client = new RestClient(_baseClient) { Timeout = 0 })
1199                 {
1200                     if (!String.IsNullOrWhiteSpace(account))
1201                         client.BaseAddress = GetAccountUrl(account);
1202
1203                     //The container and objectName are relative names. They are joined with the client's
1204                     //BaseAddress to create the object's absolute address
1205                     var builder = client.GetAddressBuilder(container, objectName);
1206                     builder.Query = "format=json&hashmap";
1207                     var uri = builder.Uri;
1208
1209
1210                     var json = await client.DownloadStringTaskAsync(uri);
1211                     var treeHash = TreeHash.Parse(json);
1212                     Log.InfoFormat("[GET HASH] END {0}", objectName);
1213                     return treeHash;
1214                 }
1215             }
1216             catch (Exception exc)
1217             {
1218                 Log.ErrorFormat("[GET HASH] END {0} with {1}", objectName, exc);
1219                 throw;
1220             }
1221
1222         }
1223
1224
1225         /// <summary>
1226         /// 
1227         /// </summary>
1228         /// <param name="account"></param>
1229         /// <param name="container"></param>
1230         /// <param name="objectName"></param>
1231         /// <param name="fileName"></param>
1232         /// <param name="hash">Optional hash value for the file. If no hash is provided, the method calculates a new hash</param>
1233         /// <remarks>>This method should have no timeout or a very long one</remarks>
1234         public async Task PutObject(string account, string container, string objectName, string fileName, string hash = null, string contentType = "application/octet-stream")
1235         {
1236             if (String.IsNullOrWhiteSpace(container))
1237                 throw new ArgumentNullException("container", "The container property can't be empty");
1238             if (String.IsNullOrWhiteSpace(objectName))
1239                 throw new ArgumentNullException("objectName", "The objectName property can't be empty");
1240             if (String.IsNullOrWhiteSpace(fileName))
1241                 throw new ArgumentNullException("fileName", "The fileName property can't be empty");
1242 /*
1243             if (!File.Exists(fileName) && !Directory.Exists(fileName))
1244                 throw new FileNotFoundException("The file or directory does not exist",fileName);
1245 */
1246             
1247             try
1248             {
1249
1250                 using (var client = new RestClient(_baseClient) { Timeout = 0 })
1251                 {
1252                     if (!String.IsNullOrWhiteSpace(account))
1253                         client.BaseAddress = GetAccountUrl(account);
1254
1255                     var builder = client.GetAddressBuilder(container, objectName);
1256                     var uri = builder.Uri;
1257
1258                     string etag = hash ?? CalculateHash(fileName);
1259
1260                     client.Headers.Add("Content-Type", contentType);
1261                     client.Headers.Add("ETag", etag);
1262
1263
1264                     Log.InfoFormat("[PUT] START {0}", objectName);
1265                     client.UploadProgressChanged += (sender, args) =>
1266                                                         {
1267                                                             using (ThreadContext.Stacks["PUT"].Push("Progress"))
1268                                                             {
1269                                                                 Log.InfoFormat("{0} {1}% {2} of {3}", fileName,
1270                                                                                args.ProgressPercentage,
1271                                                                                args.BytesSent, args.TotalBytesToSend);
1272                                                             }
1273                                                         };
1274
1275                     client.UploadFileCompleted += (sender, args) =>
1276                                                       {
1277                                                           using (ThreadContext.Stacks["PUT"].Push("Progress"))
1278                                                           {
1279                                                               Log.InfoFormat("Completed {0}", fileName);
1280                                                           }
1281                                                       };                    
1282                     if (contentType=="application/directory")
1283                         await client.UploadDataTaskAsync(uri, "PUT", new byte[0]);
1284                     else
1285                         await client.UploadFileTaskAsync(uri, "PUT", fileName);
1286                 }
1287
1288                 Log.InfoFormat("[PUT] END {0}", objectName);
1289             }
1290             catch (Exception exc)
1291             {
1292                 Log.ErrorFormat("[PUT] END {0} with {1}", objectName, exc);
1293                 throw;
1294             }                
1295
1296         }
1297        
1298         
1299         private static string CalculateHash(string fileName)
1300         {
1301             Contract.Requires(!String.IsNullOrWhiteSpace(fileName));
1302             Contract.EndContractBlock();
1303
1304             string hash;
1305             using (var hasher = MD5.Create())
1306             using(var stream=File.OpenRead(fileName))
1307             {
1308                 var hashBuilder=new StringBuilder();
1309                 foreach (byte b in hasher.ComputeHash(stream))
1310                     hashBuilder.Append(b.ToString("x2").ToLower());
1311                 hash = hashBuilder.ToString();                
1312             }
1313             return hash;
1314         }
1315         
1316         public void MoveObject(string account, string sourceContainer, string oldObjectName, string targetContainer, string newObjectName)
1317         {
1318             if (String.IsNullOrWhiteSpace(sourceContainer))
1319                 throw new ArgumentNullException("sourceContainer", "The container property can't be empty");
1320             if (String.IsNullOrWhiteSpace(oldObjectName))
1321                 throw new ArgumentNullException("oldObjectName", "The oldObjectName property can't be empty");
1322             if (String.IsNullOrWhiteSpace(targetContainer))
1323                 throw new ArgumentNullException("targetContainer", "The container property can't be empty");
1324             if (String.IsNullOrWhiteSpace(newObjectName))
1325                 throw new ArgumentNullException("newObjectName", "The newObjectName property can't be empty");
1326             Contract.EndContractBlock();
1327
1328             var targetUrl = targetContainer + "/" + newObjectName;
1329             var sourceUrl = String.Format("/{0}/{1}", sourceContainer, oldObjectName);
1330
1331             using (var client = new RestClient(_baseClient))
1332             {
1333                 if (!String.IsNullOrWhiteSpace(account))
1334                     client.BaseAddress = GetAccountUrl(account);
1335
1336                 client.Headers.Add("X-Move-From", sourceUrl);
1337                 client.PutWithRetry(targetUrl, 3);
1338
1339                 var expectedCodes = new[] {HttpStatusCode.OK, HttpStatusCode.NoContent, HttpStatusCode.Created};
1340                 if (!expectedCodes.Contains(client.StatusCode))
1341                     throw CreateWebException("MoveObject", client.StatusCode);
1342             }
1343         }
1344
1345         public void DeleteObject(string account, string sourceContainer, string objectName)
1346         {            
1347             if (String.IsNullOrWhiteSpace(sourceContainer))
1348                 throw new ArgumentNullException("sourceContainer", "The container property can't be empty");
1349             if (String.IsNullOrWhiteSpace(objectName))
1350                 throw new ArgumentNullException("objectName", "The oldObjectName property can't be empty");
1351             Contract.EndContractBlock();
1352
1353             var targetUrl = FolderConstants.TrashContainer + "/" + objectName;
1354             var sourceUrl = String.Format("/{0}/{1}", sourceContainer, objectName);
1355
1356             using (var client = new RestClient(_baseClient))
1357             {
1358                 if (!String.IsNullOrWhiteSpace(account))
1359                     client.BaseAddress = GetAccountUrl(account);
1360
1361                 client.Headers.Add("X-Move-From", sourceUrl);
1362                 client.AllowedStatusCodes.Add(HttpStatusCode.NotFound);
1363                 Log.InfoFormat("[TRASH] [{0}] to [{1}]",sourceUrl,targetUrl);
1364                 client.PutWithRetry(targetUrl, 3);
1365
1366                 var expectedCodes = new[] {HttpStatusCode.OK, HttpStatusCode.NoContent, HttpStatusCode.Created,HttpStatusCode.NotFound};
1367                 if (!expectedCodes.Contains(client.StatusCode))
1368                     throw CreateWebException("DeleteObject", client.StatusCode);
1369             }
1370         }
1371
1372       
1373         private static WebException CreateWebException(string operation, HttpStatusCode statusCode)
1374         {
1375             return new WebException(String.Format("{0} failed with unexpected status code {1}", operation, statusCode));
1376         }
1377
1378
1379 /*
1380         public IEnumerable<ObjectInfo> ListDirectories(ContainerInfo container)
1381         {
1382             var directories=this.ListObjects(container.Account, container.Name, "/");
1383         }
1384 */
1385
1386         public bool CanUpload(string account, ObjectInfo cloudFile)
1387         {
1388             Contract.Requires(!String.IsNullOrWhiteSpace(account));
1389             Contract.Requires(cloudFile!=null);
1390
1391             using (var client = new RestClient(_baseClient))
1392             {
1393                 if (!String.IsNullOrWhiteSpace(account))
1394                     client.BaseAddress = GetAccountUrl(account);
1395
1396
1397                 var parts = cloudFile.Name.Split('/');
1398                 var folder = String.Join("/", parts,0,parts.Length-1);
1399
1400                 var fileUrl=String.Format("{0}/{1}/{2}.pithos.ignore",cloudFile.Container,folder,Guid.NewGuid());
1401
1402                 client.Parameters.Clear();
1403                 try
1404                 {
1405                     client.PutWithRetry(fileUrl, 3, @"application/octet-stream");
1406
1407                     var expectedCodes = new[] { HttpStatusCode.OK, HttpStatusCode.NoContent, HttpStatusCode.Created};
1408                     var result=(expectedCodes.Contains(client.StatusCode));
1409                     DeleteObject(account, cloudFile.Container, fileUrl);
1410                     return result;
1411                 }
1412                 catch
1413                 {
1414                     return false;
1415                 }
1416             }
1417         }
1418     }
1419
1420     public class ShareAccountInfo
1421     {
1422         public DateTime? last_modified { get; set; }
1423         public string name { get; set; }
1424     }
1425 }