Statistics
| Branch: | Revision:

root / trunk / Pithos.Network / CloudFilesClient.cs @ 5ce54458

History | View | Annotate | Download (31.4 kB)

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 WebHeaderCollection = System.Net.WebHeaderCollection;
22

    
23
namespace Pithos.Network
24
{
25
    [Export(typeof(ICloudClient))]
26
    public class CloudFilesClient:ICloudClient
27
    {
28
        //CloudFilesClient uses *_baseClient* internally to communicate with the server
29
        //RestClient provides a REST-friendly interface over the standard WebClient.
30
        private RestClient _baseClient;
31
        
32
        //Some operations can specify a Timeout. The default value of all timeouts is 10 seconds
33
        private readonly TimeSpan _shortTimeout = TimeSpan.FromSeconds(10);
34
        
35
        //Some operations can be retried before failing. The default number of retries is 5
36
        private readonly int _retries = 5;        
37
        
38
        //During authentication the client provides a UserName 
39
        public string UserName { 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
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
        
52
        public Uri Proxy { get; set; }
53

    
54
        public double DownloadPercentLimit { get; set; }
55
        public double UploadPercentLimit { get; set; }
56

    
57
        public string AuthenticationUrl { get; set; }
58

    
59
 
60
        public string VersionPath
61
        {
62
            get { return UsePithos ? "v1" : "v1.0"; }
63
        }
64

    
65
        public bool UsePithos { get; set; }
66

    
67
        private bool _authenticated = false;
68

    
69
        //
70
        public void Authenticate(string userName,string apiKey)
71
        {
72
            Trace.TraceInformation("[AUTHENTICATE] Start for {0}", userName);
73
            if (String.IsNullOrWhiteSpace(userName))
74
                throw new ArgumentNullException("userName", "The userName property can't be empty");
75
            if (String.IsNullOrWhiteSpace(apiKey))
76
                throw new ArgumentNullException("apiKey", "The apiKey property can't be empty");
77

    
78
            if (_authenticated)
79
                return;
80

    
81
            UserName = userName;
82
            ApiKey = apiKey;
83
            
84

    
85
            using (var authClient = new RestClient{BaseAddress=AuthenticationUrl})
86
            {
87
                if (Proxy != null)
88
                    authClient.Proxy = new WebProxy(Proxy);
89

    
90
                authClient.Headers.Add("X-Auth-User", UserName);
91
                authClient.Headers.Add("X-Auth-Key", ApiKey);
92

    
93
                authClient.DownloadStringWithRetry(VersionPath, 3);
94

    
95
                authClient.AssertStatusOK("Authentication failed");
96

    
97
                var storageUrl = authClient.GetHeaderValue("X-Storage-Url");
98
                if (String.IsNullOrWhiteSpace(storageUrl))
99
                    throw new InvalidOperationException("Failed to obtain storage url");
100
                StorageUrl = new Uri(storageUrl);
101
                
102
                var token = authClient.GetHeaderValue("X-Auth-Token");
103
                if (String.IsNullOrWhiteSpace(token))
104
                    throw new InvalidOperationException("Failed to obtain token url");
105
                Token = token;
106
            }
107

    
108
            _baseClient = new RestClient{
109
                BaseAddress  = StorageUrl.AbsoluteUri,                
110
                Timeout=10000,
111
                Retries=3};
112
            if (Proxy!=null)
113
                _baseClient.Proxy = new WebProxy(Proxy);
114

    
115
            _baseClient.Headers.Add("X-Auth-Token", Token);
116

    
117
            Trace.TraceInformation("[AUTHENTICATE] End for {0}", userName);
118
        }
119

    
120

    
121
        public IList<ContainerInfo> ListContainers()
122
        {
123
            using (var client = new RestClient(_baseClient))
124
            {
125
                client.Parameters.Clear();
126
                client.Parameters.Add("format", "json");
127
                var content = client.DownloadStringWithRetry("", 3);
128
                client.AssertStatusOK("List Containers failed");
129

    
130
                if (client.StatusCode == HttpStatusCode.NoContent)
131
                    return new List<ContainerInfo>();
132
                var infos = JsonConvert.DeserializeObject<IList<ContainerInfo>>(content);
133
                return infos;
134
            }
135

    
136
        }
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
140
        public IList<ObjectInfo> ListObjects(string container, DateTime? since = null)
141
        {
142
            if (String.IsNullOrWhiteSpace(container))
143
                throw new ArgumentNullException("container");
144
            Contract.EndContractBlock();
145

    
146
            Trace.TraceInformation("[START] ListObjects");
147

    
148
            using (var client = new RestClient(_baseClient))
149
            {
150
                client.Parameters.Clear();
151
                client.Parameters.Add("format", "json");
152
                client.IfModifiedSince = since;
153
                var content = client.DownloadStringWithRetry(container, 3);
154

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

    
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);
162

    
163
                Trace.TraceInformation("[END] ListObjects");
164
                return infos;
165
            }
166
        }
167

    
168

    
169

    
170
        public IList<ObjectInfo> ListObjects(string container, string folder, DateTime? since = null)
171
        {
172
            if (String.IsNullOrWhiteSpace(container))
173
                throw new ArgumentNullException("container");
174
            if (String.IsNullOrWhiteSpace(folder))
175
                throw new ArgumentNullException("folder");
176
            Contract.EndContractBlock();
177

    
178
            Trace.TraceInformation("[START] ListObjects");
179

    
180
            using (var client = new RestClient(_baseClient))
181
            {
182
                client.Parameters.Clear();
183
                client.Parameters.Add("format", "json");
184
                client.Parameters.Add("path", folder);
185
                client.IfModifiedSince = since;
186
                var content = client.DownloadStringWithRetry(container, 3);
187
                client.AssertStatusOK("ListObjects failed");
188

    
189
                var infos = JsonConvert.DeserializeObject<IList<ObjectInfo>>(content);
190

    
191
                Trace.TraceInformation("[END] ListObjects");
192
                return infos;
193
            }
194
        }
195

    
196
 
197
        public bool ContainerExists(string container)
198
        {
199
            if (String.IsNullOrWhiteSpace(container))
200
                throw new ArgumentNullException("container", "The container property can't be empty");
201
            using (var client = new RestClient(_baseClient))
202
            {
203
                client.Parameters.Clear();
204
                client.Head(container, 3);
205

    
206
                switch (client.StatusCode)
207
                {
208
                    case HttpStatusCode.OK:
209
                    case HttpStatusCode.NoContent:
210
                        return true;
211
                    case HttpStatusCode.NotFound:
212
                        return false;
213
                    default:
214
                        throw CreateWebException("ContainerExists", client.StatusCode);
215
                }
216
            }
217
        }
218

    
219
        public bool ObjectExists(string container,string objectName)
220
        {
221
            if (String.IsNullOrWhiteSpace(container))
222
                throw new ArgumentNullException("container", "The container property can't be empty");
223
            if (String.IsNullOrWhiteSpace(objectName))
224
                throw new ArgumentNullException("objectName", "The objectName property can't be empty");
225
            using (var client = new RestClient(_baseClient))
226
            {
227
                client.Parameters.Clear();
228
                client.Head(container + "/" + objectName, 3);
229

    
230
                switch (client.StatusCode)
231
                {
232
                    case HttpStatusCode.OK:
233
                    case HttpStatusCode.NoContent:
234
                        return true;
235
                    case HttpStatusCode.NotFound:
236
                        return false;
237
                    default:
238
                        throw CreateWebException("ObjectExists", client.StatusCode);
239
                }
240
            }
241

    
242
        }
243

    
244
        public ObjectInfo GetObjectInfo(string container, string objectName)
245
        {
246
            if (String.IsNullOrWhiteSpace(container))
247
                throw new ArgumentNullException("container", "The container property can't be empty");
248
            if (String.IsNullOrWhiteSpace(objectName))
249
                throw new ArgumentNullException("objectName", "The objectName property can't be empty");
250

    
251
            using (var client = new RestClient(_baseClient))
252
            {
253
                try
254
                {
255
                    client.Parameters.Clear();
256

    
257
                    client.Head(container + "/" + objectName, 3);
258

    
259
                    if (client.TimedOut)
260
                        return ObjectInfo.Empty;
261

    
262
                    switch (client.StatusCode)
263
                    {
264
                        case HttpStatusCode.OK:
265
                        case HttpStatusCode.NoContent:
266
                            var keys = client.ResponseHeaders.AllKeys.AsQueryable();
267
                            var tags = (from key in keys
268
                                        where key.StartsWith("X-Object-Meta-")
269
                                        let name = key.Substring(14)
270
                                        select new {Name = name, Value = client.ResponseHeaders[name]})
271
                                .ToDictionary(t => t.Name, t => t.Value);
272
                            var extensions = (from key in keys
273
                                              where key.StartsWith("X-Object-") && !key.StartsWith("X-Object-Meta-")
274
                                              let name = key.Substring(9)
275
                                              select new {Name = name, Value = client.ResponseHeaders[name]})
276
                                .ToDictionary(t => t.Name, t => t.Value);
277
                            return new ObjectInfo
278
                                       {
279
                                           Name = objectName,
280
                                           /*Bytes =
281
                                               long.Parse(client.GetHeaderValue("Content-Length")),*/
282
                                           Hash = client.GetHeaderValue("ETag"),
283
                                           Content_Type = client.GetHeaderValue("Content-Type"),
284
                                           Tags = tags,
285
                                           Extensions = extensions
286
                                       };
287
                        case HttpStatusCode.NotFound:
288
                            return ObjectInfo.Empty;
289
                        default:
290
                            throw new WebException(
291
                                String.Format("[FAIL] GetObjectInfo for {0} failed with unexpected status code {1}",
292
                                              objectName, client.StatusCode));
293
                    }
294

    
295
                }
296
                catch(RetryException e)
297
                {
298
                    Trace.TraceWarning("[RETRY FAIL] GetObjectInfo for {0} failed.");
299
                    return ObjectInfo.Empty;
300
                }
301
                catch(WebException e)
302
                {
303
                    Trace.TraceError(
304
                        String.Format("[FAIL] GetObjectInfo for {0} failed with unexpected status code {1}",
305
                                      objectName, client.StatusCode), e);
306
                    throw;
307
                }
308
            }
309

    
310
        }
311

    
312
        public void CreateFolder(string container, string folder)
313
        {
314
            if (String.IsNullOrWhiteSpace(container))
315
                throw new ArgumentNullException("container", "The container property can't be empty");
316
            if (String.IsNullOrWhiteSpace(folder))
317
                throw new ArgumentNullException("folder", "The folder property can't be empty");
318

    
319
            var folderUrl=String.Format("{0}/{1}",container,folder);
320
            using (var client = new RestClient(_baseClient))
321
            {
322
                client.Parameters.Clear();
323
                client.Headers.Add("Content-Type", @"application/directory");
324
                client.Headers.Add("Content-Length", "0");
325
                client.PutWithRetry(folderUrl, 3);
326

    
327
                if (client.StatusCode != HttpStatusCode.Created && client.StatusCode != HttpStatusCode.Accepted)
328
                    throw CreateWebException("CreateFolder", client.StatusCode);
329
            }
330
        }
331

    
332
        public ContainerInfo GetContainerInfo(string container)
333
        {
334
            if (String.IsNullOrWhiteSpace(container))
335
                throw new ArgumentNullException("container", "The container property can't be empty");
336
            using (var client = new RestClient(_baseClient))
337
            {
338
                client.Head(container);
339
                switch (client.StatusCode)
340
                {
341
                    case HttpStatusCode.OK:
342
                    case HttpStatusCode.NoContent:
343
                        var containerInfo = new ContainerInfo
344
                                                {
345
                                                    Name = container,
346
                                                    Count =
347
                                                        long.Parse(client.GetHeaderValue("X-Container-Object-Count")),
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"))
351
                                                };
352
                        return containerInfo;
353
                    case HttpStatusCode.NotFound:
354
                        return ContainerInfo.Empty;
355
                    default:
356
                        throw CreateWebException("GetContainerInfo", client.StatusCode);
357
                }
358
            }
359
        }
360

    
361
        public void CreateContainer(string container)
362
        {
363
            if (String.IsNullOrWhiteSpace(container))
364
                throw new ArgumentNullException("container", "The container property can't be empty");
365
            using (var client = new RestClient(_baseClient))
366
            {
367
                client.PutWithRetry(container, 3);
368
                var expectedCodes = new[] {HttpStatusCode.Created, HttpStatusCode.Accepted, HttpStatusCode.OK};
369
                if (!expectedCodes.Contains(client.StatusCode))
370
                    throw CreateWebException("CreateContainer", client.StatusCode);
371
            }
372
        }
373

    
374
        public void DeleteContainer(string container)
375
        {
376
            if (String.IsNullOrWhiteSpace(container))
377
                throw new ArgumentNullException("container", "The container property can't be empty");
378
            using (var client = new RestClient(_baseClient))
379
            {
380
                client.DeleteWithRetry(container, 3);
381
                var expectedCodes = new[] {HttpStatusCode.NotFound, HttpStatusCode.NoContent};
382
                if (!expectedCodes.Contains(client.StatusCode))
383
                    throw CreateWebException("DeleteContainer", client.StatusCode);
384
            }
385

    
386
        }
387

    
388
        /// <summary>
389
        /// 
390
        /// </summary>
391
        /// <param name="container"></param>
392
        /// <param name="objectName"></param>
393
        /// <param name="fileName"></param>
394
        /// <returns></returns>
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
398
        public Task GetObject(string container, string objectName, string fileName)
399
        {
400
            if (String.IsNullOrWhiteSpace(container))
401
                throw new ArgumentNullException("container", "The container property can't be empty");
402
            if (String.IsNullOrWhiteSpace(objectName))
403
                throw new ArgumentNullException("objectName", "The objectName property can't be empty");            
404
            Contract.EndContractBlock();
405

    
406
            try
407
            {
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.
416
                var client = new RestClient(_baseClient) { Timeout = 0 };
417
               
418
                //Download progress is reported to the Trace log
419
                Trace.TraceInformation("[GET] START {0}", objectName);
420
                client.DownloadProgressChanged += (sender, args) => 
421
                    Trace.TraceInformation("[GET PROGRESS] {0} {1}% {2} of {3}",
422
                                    fileName, args.ProgressPercentage,
423
                                    args.BytesReceived,
424
                                    args.TotalBytesToReceive);                                
425

    
426

    
427
                //Start downloading the object asynchronously
428
                var downloadTask = client.DownloadFileTask(uri, fileName);
429
                
430
                //Once the download completes
431
                return downloadTask.ContinueWith(download =>
432
                                      {
433
                                          //Delete the local client object
434
                                          client.Dispose();
435
                                          //And report failure or completion
436
                                          if (download.IsFaulted)
437
                                          {
438
                                              Trace.TraceError("[GET] FAIL for {0} with \r{1}", objectName,
439
                                                               download.Exception);
440
                                          }
441
                                          else
442
                                          {
443
                                              Trace.TraceInformation("[GET] END {0}", objectName);                                             
444
                                          }
445
                                      });
446
            }
447
            catch (Exception exc)
448
            {
449
                Trace.TraceError("[GET] END {0} with {1}", objectName, exc);
450
                throw;
451
            }
452

    
453

    
454

    
455
        }
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

    
593
        /// <summary>
594
        /// 
595
        /// </summary>
596
        /// <param name="container"></param>
597
        /// <param name="objectName"></param>
598
        /// <param name="fileName"></param>
599
        /// <param name="hash">Optional hash value for the file. If no hash is provided, the method calculates a new hash</param>
600
        /// <remarks>>This method should have no timeout or a very long one</remarks>
601
        public Task PutObject(string container, string objectName, string fileName, string hash = null)
602
        {
603
            if (String.IsNullOrWhiteSpace(container))
604
                throw new ArgumentNullException("container", "The container property can't be empty");
605
            if (String.IsNullOrWhiteSpace(objectName))
606
                throw new ArgumentNullException("objectName", "The objectName property can't be empty");
607
            if (String.IsNullOrWhiteSpace(fileName))
608
                throw new ArgumentNullException("fileName", "The fileName property can't be empty");
609
            if (!File.Exists(fileName))
610
                throw new FileNotFoundException("The file does not exist",fileName);
611

    
612
            
613
            try
614
            {
615
                var builder= GetAddressBuilder(container,objectName);
616
                var uri = builder.Uri;
617

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

    
621
                client.Headers.Add("Content-Type", "application/octet-stream");
622
                client.Headers.Add("ETag", etag);
623

    
624

    
625
                Trace.TraceInformation("[PUT] START {0}", objectName);
626
                client.UploadProgressChanged += (sender, args) =>
627
                {
628
                    Trace.TraceInformation("[PUT PROGRESS] {0} {1}% {2} of {3}", fileName, args.ProgressPercentage, args.BytesSent, args.TotalBytesToSend);
629
                };
630
               
631
                return client.UploadFileTask(uri, "PUT", fileName)
632
                    .ContinueWith(upload=>
633
                                      {
634
                                          client.Dispose();
635

    
636
                                          if (upload.IsFaulted)
637
                                          {                                              
638
                                              Trace.TraceError("[PUT] FAIL for {0} with \r{1}",objectName,upload.Exception);
639
                                          }
640
                                          else
641
                                            Trace.TraceInformation("[PUT] END {0}", objectName);
642
                                      });
643
            }
644
            catch (Exception exc)
645
            {
646
                Trace.TraceError("[PUT] END {0} with {1}", objectName, exc);
647
                throw;
648
            }                
649

    
650
        }
651
       
652
        
653
        private static string CalculateHash(string fileName)
654
        {
655
            string hash;
656
            using (var hasher = MD5.Create())
657
            using(var stream=File.OpenRead(fileName))
658
            {
659
                var hashBuilder=new StringBuilder();
660
                foreach (byte b in hasher.ComputeHash(stream))
661
                    hashBuilder.Append(b.ToString("x2").ToLower());
662
                hash = hashBuilder.ToString();                
663
            }
664
            return hash;
665
        }
666

    
667
        public void DeleteObject(string container, string objectName)
668
        {
669
            if (String.IsNullOrWhiteSpace(container))
670
                throw new ArgumentNullException("container", "The container property can't be empty");
671
            if (String.IsNullOrWhiteSpace(objectName))
672
                throw new ArgumentNullException("objectName", "The objectName property can't be empty");
673
            using (var client = new RestClient(_baseClient))
674
            {
675

    
676
                client.DeleteWithRetry(container + "/" + objectName, 3);
677

    
678
                var expectedCodes = new[] {HttpStatusCode.NotFound, HttpStatusCode.NoContent};
679
                if (!expectedCodes.Contains(client.StatusCode))
680
                    throw CreateWebException("DeleteObject", client.StatusCode);
681
            }
682

    
683
        }
684

    
685
        public void MoveObject(string sourceContainer, string oldObjectName, string targetContainer,string newObjectName)
686
        {
687
            if (String.IsNullOrWhiteSpace(sourceContainer))
688
                throw new ArgumentNullException("sourceContainer", "The container property can't be empty");
689
            if (String.IsNullOrWhiteSpace(oldObjectName))
690
                throw new ArgumentNullException("oldObjectName", "The oldObjectName property can't be empty");
691
            if (String.IsNullOrWhiteSpace(targetContainer))
692
                throw new ArgumentNullException("targetContainer", "The container property can't be empty");
693
            if (String.IsNullOrWhiteSpace(newObjectName))
694
                throw new ArgumentNullException("newObjectName", "The newObjectName property can't be empty");
695

    
696
            var targetUrl = targetContainer + "/" + newObjectName;
697
            var sourceUrl = String.Format("/{0}/{1}", sourceContainer, oldObjectName);
698

    
699
            using (var client = new RestClient(_baseClient))
700
            {
701
                client.Headers.Add("X-Copy-From", sourceUrl);
702
                client.PutWithRetry(targetUrl, 3);
703

    
704
                var expectedCodes = new[] {HttpStatusCode.OK, HttpStatusCode.NoContent, HttpStatusCode.Created};
705
                if (expectedCodes.Contains(client.StatusCode))
706
                {
707
                    this.DeleteObject(sourceContainer, oldObjectName);
708
                }
709
                else
710
                    throw CreateWebException("MoveObject", client.StatusCode);
711
            }
712
        }
713

    
714
      
715
        private static WebException CreateWebException(string operation, HttpStatusCode statusCode)
716
        {
717
            return new WebException(String.Format("{0} failed with unexpected status code {1}", operation, statusCode));
718
        }
719

    
720
        
721
    }
722
}