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