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