Revision 5ce54458 trunk/Pithos.Network/CloudFilesClient.cs

b/trunk/Pithos.Network/CloudFilesClient.cs
1
using System;
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;
2 7
using System.Collections.Generic;
3 8
using System.ComponentModel.Composition;
4 9
using System.Diagnostics;
......
20 25
    [Export(typeof(ICloudClient))]
21 26
    public class CloudFilesClient:ICloudClient
22 27
    {
23

  
28
        //CloudFilesClient uses *_baseClient* internally to communicate with the server
29
        //RestClient provides a REST-friendly interface over the standard WebClient.
24 30
        private RestClient _baseClient;
31
        
32
        //Some operations can specify a Timeout. The default value of all timeouts is 10 seconds
25 33
        private readonly TimeSpan _shortTimeout = TimeSpan.FromSeconds(10);
34
        
35
        //Some operations can be retried before failing. The default number of retries is 5
26 36
        private readonly int _retries = 5;        
27
        public string ApiKey { get; set; }
37
        
38
        //During authentication the client provides a UserName 
28 39
        public string UserName { get; set; }
29
        public Uri StorageUrl { get; set; }
40
        
41
        //and and ApiKey to the server
42
        public string ApiKey { get; set; }
43
        
44
        //And receives an authentication Token. This token must be provided in ALL other operations,
45
        //in the X-Auth-Token header
30 46
        public string Token { get; set; }
47
        
48
        //The client also receives a StorageUrl after authentication. All subsequent operations must
49
        //use this url
50
        public Uri StorageUrl { get; set; }
51
        
31 52
        public Uri Proxy { get; set; }
32 53

  
33 54
        public double DownloadPercentLimit { get; set; }
......
45 66

  
46 67
        private bool _authenticated = false;
47 68

  
69
        //
48 70
        public void Authenticate(string userName,string apiKey)
49 71
        {
50 72
            Trace.TraceInformation("[AUTHENTICATE] Start for {0}", userName);
......
100 122
        {
101 123
            using (var client = new RestClient(_baseClient))
102 124
            {
103
                var content = client.DownloadStringWithRetry("", 3);
104 125
                client.Parameters.Clear();
105 126
                client.Parameters.Add("format", "json");
127
                var content = client.DownloadStringWithRetry("", 3);
106 128
                client.AssertStatusOK("List Containers failed");
107 129

  
108 130
                if (client.StatusCode == HttpStatusCode.NoContent)
......
113 135

  
114 136
        }
115 137

  
138
        //Request listing of all objects in a container modified since a specific time.
139
        //If the *since* value is missing, return all objects
116 140
        public IList<ObjectInfo> ListObjects(string container, DateTime? since = null)
117 141
        {
118 142
            if (String.IsNullOrWhiteSpace(container))
......
130 154

  
131 155
                client.AssertStatusOK("ListObjects failed");
132 156

  
133
                var infos = JsonConvert.DeserializeObject<IList<ObjectInfo>>(content);
157
                //If the result is empty, return an empty list,
158
                var infos=String.IsNullOrWhiteSpace(content) 
159
                    ? new List<ObjectInfo>() 
160
                    //Otherwise deserialize the object list into a list of ObjectInfos
161
                    : JsonConvert.DeserializeObject<IList<ObjectInfo>>(content);
134 162

  
135 163
                Trace.TraceInformation("[END] ListObjects");
136 164
                return infos;
......
310 338
                client.Head(container);
311 339
                switch (client.StatusCode)
312 340
                {
341
                    case HttpStatusCode.OK:
313 342
                    case HttpStatusCode.NoContent:
314 343
                        var containerInfo = new ContainerInfo
315 344
                                                {
316 345
                                                    Name = container,
317 346
                                                    Count =
318 347
                                                        long.Parse(client.GetHeaderValue("X-Container-Object-Count")),
319
                                                    Bytes = long.Parse(client.GetHeaderValue("X-Container-Bytes-Used"))
348
                                                    Bytes = long.Parse(client.GetHeaderValue("X-Container-Bytes-Used")),
349
                                                    BlockHash = client.GetHeaderValue("X-Container-Block-Hash"),
350
                                                    BlockSize=int.Parse(client.GetHeaderValue("X-Container-Block-Size"))
320 351
                                                };
321 352
                        return containerInfo;
322 353
                    case HttpStatusCode.NotFound:
......
361 392
        /// <param name="objectName"></param>
362 393
        /// <param name="fileName"></param>
363 394
        /// <returns></returns>
364
        /// <remarks>>This method should have no timeout or a very long one</remarks>
395
        /// <remarks>This method should have no timeout or a very long one</remarks>
396
        //Asynchronously download the object specified by *objectName* in a specific *container* to 
397
        // a local file
365 398
        public Task GetObject(string container, string objectName, string fileName)
366 399
        {
367 400
            if (String.IsNullOrWhiteSpace(container))
368 401
                throw new ArgumentNullException("container", "The container property can't be empty");
369 402
            if (String.IsNullOrWhiteSpace(objectName))
370
                throw new ArgumentNullException("objectName", "The objectName property can't be empty");
371
            
403
                throw new ArgumentNullException("objectName", "The objectName property can't be empty");            
404
            Contract.EndContractBlock();
405

  
372 406
            try
373 407
            {
374

  
375
                var url = String.Join("/", _baseClient.BaseAddress, container, objectName);
376
                var uri = new Uri(url);
377

  
408
                //The container and objectName are relative names. They are joined with the client's
409
                //BaseAddress to create the object's absolute address
410
                var builder = GetAddressBuilder(container, objectName);
411
                var uri = builder.Uri;
412
                //WebClient, and by extension RestClient, are not thread-safe. Create a new RestClient
413
                //object to avoid concurrency errors.
414
                //
415
                //Download operations take a long time therefore they have no timeout.
378 416
                var client = new RestClient(_baseClient) { Timeout = 0 };
379 417
               
380

  
418
                //Download progress is reported to the Trace log
381 419
                Trace.TraceInformation("[GET] START {0}", objectName);
382 420
                client.DownloadProgressChanged += (sender, args) => 
383 421
                    Trace.TraceInformation("[GET PROGRESS] {0} {1}% {2} of {3}",
384 422
                                    fileName, args.ProgressPercentage,
385 423
                                    args.BytesReceived,
386
                                    args.TotalBytesToReceive);
424
                                    args.TotalBytesToReceive);                                
425

  
426

  
427
                //Start downloading the object asynchronously
428
                var downloadTask = client.DownloadFileTask(uri, fileName);
387 429
                
388
                return client.DownloadFileTask(uri, fileName)
389
                    .ContinueWith(download =>
430
                //Once the download completes
431
                return downloadTask.ContinueWith(download =>
390 432
                                      {
433
                                          //Delete the local client object
391 434
                                          client.Dispose();
392

  
435
                                          //And report failure or completion
393 436
                                          if (download.IsFaulted)
394 437
                                          {
395 438
                                              Trace.TraceError("[GET] FAIL for {0} with \r{1}", objectName,
......
411 454

  
412 455
        }
413 456

  
457
        public Task<string> PutHashMap(string container, string objectName, TreeHash hash)
458
        {
459
            if (String.IsNullOrWhiteSpace(container))
460
                throw new ArgumentNullException("container");
461
            if (String.IsNullOrWhiteSpace(objectName))
462
                throw new ArgumentNullException("objectName");
463
            if (hash==null)
464
                throw new ArgumentNullException("hash");
465
            if (String.IsNullOrWhiteSpace(Token))
466
                throw new InvalidOperationException("Invalid Token");
467
            if (StorageUrl == null)
468
                throw new InvalidOperationException("Invalid Storage Url");
469
            Contract.EndContractBlock();
470
            //The container and objectName are relative names. They are joined with the client's
471
            //BaseAddress to create the object's absolute address
472
            var builder = GetAddressBuilder(container, objectName);
473
            builder.Query = "format=json&hashmap";
474
            var uri = builder.Uri;
475

  
476
            //Don't use a timeout because putting the hashmap may be a long process
477
            var client = new RestClient(_baseClient) { Timeout = 0 };
478

  
479
            //Send the tree hash as Json to the server            
480
            client.Headers[HttpRequestHeader.ContentType] = "application/octet-stream";
481
            var uploadTask=client.UploadStringTask(uri, "PUT", hash.ToJson());
482

  
483
            
484
            return uploadTask.ContinueWith(t =>
485
            {
486
                
487
                
488

  
489
                //The server will respond either with 201-created if all blocks were already on the server
490
                if (client.StatusCode == HttpStatusCode.Created)
491
                    //in which case we return an empty hash list
492
                    return t.Result;
493
                //or with a 409-conflict and return the list of missing parts
494
                //A 409 will cause an exception so we need to check t.IsFaulted to avoid propagating the exception                
495
                if (t.IsFaulted)
496
                {
497
                    var ex = t.Exception.InnerException;
498
                    var we = ex as WebException;
499
                    var response = we.Response as HttpWebResponse;
500
                    if (response!=null && response.StatusCode==HttpStatusCode.Conflict)
501
                    {
502
                        //In case of 409 the missing parts will be in the response content                        
503
                        using (var stream = response.GetResponseStream())
504
                        using(var reader=new StreamReader(stream))
505
                        {
506
                            var content=reader.ReadToEnd();
507
                            return content;
508
                        }                        
509
                    }
510
                    else
511
                        //Any other status code is unexpected and the exception should be rethrown
512
                        throw ex;
513
                    
514
                }
515
                //Any other status code is unexpected but there was no exception. We can probably continue processing
516
                else
517
                {
518
                    Trace.TraceWarning("Unexcpected status code when putting map: {0} - {1}",client.StatusCode,client.StatusDescription);                    
519
                }
520
                return t.Result;
521
            });
522

  
523
        }
524

  
525

  
526
        public Task<TreeHash> GetHashMap(string container, string objectName)
527
        {
528
            if (String.IsNullOrWhiteSpace(container))
529
                throw new ArgumentNullException("container");
530
            if (String.IsNullOrWhiteSpace(objectName))
531
                throw new ArgumentNullException("objectName");
532
            if (String.IsNullOrWhiteSpace(Token))
533
                throw new InvalidOperationException("Invalid Token");
534
            if (StorageUrl == null)
535
                throw new InvalidOperationException("Invalid Storage Url");
536
            Contract.EndContractBlock();
537

  
538
            try
539
            {
540
                //The container and objectName are relative names. They are joined with the client's
541
                //BaseAddress to create the object's absolute address
542
                var builder = GetAddressBuilder(container, objectName);
543
                builder.Query="format=json&hashmap";
544
                var uri = builder.Uri;                
545
                //WebClient, and by extension RestClient, are not thread-safe. Create a new RestClient
546
                //object to avoid concurrency errors.
547
                //
548
                //Download operations take a long time therefore they have no timeout.
549
                //TODO: Do we really? this is a hashmap operation, not a download
550
                var client = new RestClient(_baseClient) { Timeout = 0 };
551
               
552

  
553
                //Start downloading the object asynchronously
554
                var downloadTask = client.DownloadStringTask(uri);
555
                
556
                //Once the download completes
557
                return downloadTask.ContinueWith(download =>
558
                {
559
                    //Delete the local client object
560
                    client.Dispose();
561
                    //And report failure or completion
562
                    if (download.IsFaulted)
563
                    {
564
                        Trace.TraceError("[GET HASH] FAIL for {0} with \r{1}", objectName,
565
                                        download.Exception);
566
                        throw download.Exception;
567
                    }
568
                                          
569
                    //The server will return an empty string if the file is empty
570
                    var json = download.Result;
571
                    var treeHash = TreeHash.Parse(json);
572
                    Trace.TraceInformation("[GET HASH] END {0}", objectName);                                             
573
                    return treeHash;
574
                });
575
            }
576
            catch (Exception exc)
577
            {
578
                Trace.TraceError("[GET HASH] END {0} with {1}", objectName, exc);
579
                throw;
580
            }
581

  
582

  
583

  
584
        }
585

  
586
        private UriBuilder GetAddressBuilder(string container, string objectName)
587
        {
588
            var builder = new UriBuilder(String.Join("/", _baseClient.BaseAddress, container, objectName));
589
            return builder;
590
        }
591

  
592

  
414 593
        /// <summary>
415 594
        /// 
416 595
        /// </summary>
......
433 612
            
434 613
            try
435 614
            {
436
                var url = String.Join("/",_baseClient.BaseAddress,container,objectName);
437
                var uri = new Uri(url);
615
                var builder= GetAddressBuilder(container,objectName);
616
                var uri = builder.Uri;
438 617

  
439 618
                var client = new RestClient(_baseClient){Timeout=0};           
440 619
                string etag = hash ?? CalculateHash(fileName);

Also available in: Unified diff