Statistics
| Branch: | Revision:

root / trunk / Pithos.Network / CloudFilesClient.cs @ db8a9589

History | View | Annotate | Download (59 kB)

1
#region
2
/* -----------------------------------------------------------------------
3
 * <copyright file="CloudFilesClient.cs" company="GRNet">
4
 * 
5
 * Copyright 2011-2012 GRNET S.A. All rights reserved.
6
 *
7
 * Redistribution and use in source and binary forms, with or
8
 * without modification, are permitted provided that the following
9
 * conditions are met:
10
 *
11
 *   1. Redistributions of source code must retain the above
12
 *      copyright notice, this list of conditions and the following
13
 *      disclaimer.
14
 *
15
 *   2. Redistributions in binary form must reproduce the above
16
 *      copyright notice, this list of conditions and the following
17
 *      disclaimer in the documentation and/or other materials
18
 *      provided with the distribution.
19
 *
20
 *
21
 * THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
22
 * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
23
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
24
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
25
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
26
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
27
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
28
 * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
29
 * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
30
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
31
 * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
32
 * POSSIBILITY OF SUCH DAMAGE.
33
 *
34
 * The views and conclusions contained in the software and
35
 * documentation are those of the authors and should not be
36
 * interpreted as representing official policies, either expressed
37
 * or implied, of GRNET S.A.
38
 * </copyright>
39
 * -----------------------------------------------------------------------
40
 */
41
#endregion
42

    
43
// **CloudFilesClient** provides a simple client interface to CloudFiles and Pithos
44
//
45
// The class provides methods to upload/download files, delete files, manage containers
46

    
47

    
48
using System;
49
using System.Collections;
50
using System.Collections.Generic;
51
using System.Collections.Specialized;
52
using System.ComponentModel.Composition;
53
using System.Diagnostics;
54
using System.Diagnostics.Contracts;
55
using System.IO;
56
using System.Linq;
57
using System.Net;
58
using System.Reflection;
59
using System.Security.Cryptography;
60
using System.Text;
61
using System.Threading.Tasks;
62
using Newtonsoft.Json;
63
using Pithos.Interfaces;
64
using log4net;
65

    
66
namespace Pithos.Network
67
{
68
    [Export(typeof(ICloudClient))]
69
    public class CloudFilesClient:ICloudClient
70
    {
71
        private static readonly ILog Log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
72

    
73
        //CloudFilesClient uses *_baseClient* internally to communicate with the server
74
        //RestClient provides a REST-friendly interface over the standard WebClient.
75
        private RestClient _baseClient;
76
        
77

    
78
        //During authentication the client provides a UserName 
79
        public string UserName { get; set; }
80
        
81
        //and and ApiKey to the server
82
        public string ApiKey { get; set; }
83
        
84
        //And receives an authentication Token. This token must be provided in ALL other operations,
85
        //in the X-Auth-Token header
86
        private string _token;
87
        public string Token
88
        {
89
            get { return _token; }
90
            set
91
            {
92
                _token = value;
93
                _baseClient.Headers["X-Auth-Token"] = value;
94
            }
95
        }
96

    
97
        //The client also receives a StorageUrl after authentication. All subsequent operations must
98
        //use this url
99
        public Uri StorageUrl { get; set; }
100

    
101

    
102
        protected Uri RootAddressUri { get; set; }
103

    
104
       /* private WebProxy _proxy;
105
        public WebProxy Proxy
106
        {
107
            get { return _proxy; }
108
            set
109
            {
110
                _proxy = value;
111
                if (_baseClient != null)
112
                    _baseClient.Proxy = value;                
113
            }
114
        }
115
*/
116

    
117
        /* private Uri _proxy;
118
        public Uri Proxy
119
        {
120
            get { return _proxy; }
121
            set
122
            {
123
                _proxy = value;
124
                if (_baseClient != null)
125
                    _baseClient.Proxy = new WebProxy(value);                
126
            }
127
        }*/
128

    
129
        public double DownloadPercentLimit { get; set; }
130
        public double UploadPercentLimit { get; set; }
131

    
132
        public string AuthenticationUrl { get; set; }
133

    
134
 
135
        public string VersionPath
136
        {
137
            get { return UsePithos ? "v1" : "v1.0"; }
138
        }
139

    
140
        public bool UsePithos { get; set; }
141

    
142

    
143

    
144
        public CloudFilesClient(string userName, string apiKey)
145
        {
146
            UserName = userName;
147
            ApiKey = apiKey;
148
        }
149

    
150
        public CloudFilesClient(AccountInfo accountInfo)
151
        {
152
            if (accountInfo==null)
153
                throw new ArgumentNullException("accountInfo");
154
            Contract.Ensures(!String.IsNullOrWhiteSpace(Token));
155
            Contract.Ensures(StorageUrl != null);
156
            Contract.Ensures(_baseClient != null);
157
            Contract.Ensures(RootAddressUri != null);
158
            Contract.EndContractBlock();          
159

    
160
            _baseClient = new RestClient
161
            {
162
                BaseAddress = accountInfo.StorageUri.ToString(),
163
                Timeout = 10000,
164
                Retries = 3,
165
            };
166
            StorageUrl = accountInfo.StorageUri;
167
            Token = accountInfo.Token;
168
            UserName = accountInfo.UserName;
169

    
170
            //Get the root address (StorageUrl without the account)
171
            var storageUrl = StorageUrl.AbsoluteUri;
172
            var usernameIndex = storageUrl.LastIndexOf(UserName);
173
            var rootUrl = storageUrl.Substring(0, usernameIndex);
174
            RootAddressUri = new Uri(rootUrl);
175
        }
176

    
177

    
178
        public AccountInfo Authenticate()
179
        {
180
            if (String.IsNullOrWhiteSpace(UserName))
181
                throw new InvalidOperationException("UserName is empty");
182
            if (String.IsNullOrWhiteSpace(ApiKey))
183
                throw new InvalidOperationException("ApiKey is empty");
184
            if (String.IsNullOrWhiteSpace(AuthenticationUrl))
185
                throw new InvalidOperationException("AuthenticationUrl is empty");
186
            Contract.Ensures(!String.IsNullOrWhiteSpace(Token));
187
            Contract.Ensures(StorageUrl != null);
188
            Contract.Ensures(_baseClient != null);
189
            Contract.Ensures(RootAddressUri != null);
190
            Contract.EndContractBlock();
191

    
192

    
193
            Log.InfoFormat("[AUTHENTICATE] Start for {0}", UserName);
194

    
195
            var groups = new List<Group>();
196

    
197
            using (var authClient = new RestClient{BaseAddress=AuthenticationUrl})
198
            {                
199
               /* if (Proxy != null)
200
                    authClient.Proxy = Proxy;*/
201

    
202
                Contract.Assume(authClient.Headers!=null);
203

    
204
                authClient.Headers.Add("X-Auth-User", UserName);
205
                authClient.Headers.Add("X-Auth-Key", ApiKey);
206

    
207
                authClient.DownloadStringWithRetry(VersionPath, 3);
208

    
209
                authClient.AssertStatusOK("Authentication failed");
210

    
211
                var storageUrl = authClient.GetHeaderValue("X-Storage-Url");
212
                if (String.IsNullOrWhiteSpace(storageUrl))
213
                    throw new InvalidOperationException("Failed to obtain storage url");
214
                
215
                _baseClient = new RestClient
216
                {
217
                    BaseAddress = storageUrl,
218
                    Timeout = 10000,
219
                    Retries = 3,
220
                    //Proxy=Proxy
221
                };
222

    
223
                StorageUrl = new Uri(storageUrl);
224
                
225
                //Get the root address (StorageUrl without the account)
226
                var usernameIndex=storageUrl.LastIndexOf(UserName);
227
                var rootUrl = storageUrl.Substring(0, usernameIndex);
228
                RootAddressUri = new Uri(rootUrl);
229
                
230
                var token = authClient.GetHeaderValue("X-Auth-Token");
231
                if (String.IsNullOrWhiteSpace(token))
232
                    throw new InvalidOperationException("Failed to obtain token url");
233
                Token = token;
234

    
235
               /* var keys = authClient.ResponseHeaders.AllKeys.AsQueryable();
236
                groups = (from key in keys
237
                            where key.StartsWith("X-Account-Group-")
238
                            let name = key.Substring(16)
239
                            select new Group(name, authClient.ResponseHeaders[key]))
240
                        .ToList();
241
                    
242
*/
243
            }
244

    
245
            Log.InfoFormat("[AUTHENTICATE] End for {0}", UserName);
246
            
247

    
248
            return new AccountInfo {StorageUri = StorageUrl, Token = Token, UserName = UserName,Groups=groups};            
249

    
250
        }
251

    
252

    
253

    
254
        public IList<ContainerInfo> ListContainers(string account)
255
        {
256
            using (var client = new RestClient(_baseClient))
257
            {
258
                if (!String.IsNullOrWhiteSpace(account))
259
                    client.BaseAddress = GetAccountUrl(account);
260
                
261
                client.Parameters.Clear();
262
                client.Parameters.Add("format", "json");
263
                var content = client.DownloadStringWithRetry("", 3);
264
                client.AssertStatusOK("List Containers failed");
265

    
266
                if (client.StatusCode == HttpStatusCode.NoContent)
267
                    return new List<ContainerInfo>();
268
                var infos = JsonConvert.DeserializeObject<IList<ContainerInfo>>(content);
269
                
270
                foreach (var info in infos)
271
                {
272
                    info.Account = account;
273
                }
274
                return infos;
275
            }
276

    
277
        }
278

    
279
        private string GetAccountUrl(string account)
280
        {
281
            return new Uri(RootAddressUri, new Uri(account,UriKind.Relative)).AbsoluteUri;
282
        }
283

    
284
        public IList<ShareAccountInfo> ListSharingAccounts(DateTime? since=null)
285
        {
286
            using (ThreadContext.Stacks["Share"].Push("List Accounts"))
287
            {
288
                if (Log.IsDebugEnabled) Log.DebugFormat("START");
289

    
290
                using (var client = new RestClient(_baseClient))
291
                {
292
                    client.Parameters.Clear();
293
                    client.Parameters.Add("format", "json");
294
                    client.IfModifiedSince = since;
295

    
296
                    //Extract the username from the base address
297
                    client.BaseAddress = RootAddressUri.AbsoluteUri;
298

    
299
                    var content = client.DownloadStringWithRetry(@"", 3);
300

    
301
                    client.AssertStatusOK("ListSharingAccounts failed");
302

    
303
                    //If the result is empty, return an empty list,
304
                    var infos = String.IsNullOrWhiteSpace(content)
305
                                    ? new List<ShareAccountInfo>()
306
                                //Otherwise deserialize the account list into a list of ShareAccountInfos
307
                                    : JsonConvert.DeserializeObject<IList<ShareAccountInfo>>(content);
308

    
309
                    Log.DebugFormat("END");
310
                    return infos;
311
                }
312
            }
313
        }
314

    
315
        //Request listing of all objects in a container modified since a specific time.
316
        //If the *since* value is missing, return all objects
317
        public IList<ObjectInfo> ListSharedObjects(DateTime? since = null)
318
        {
319

    
320
            using (ThreadContext.Stacks["Share"].Push("List Objects"))
321
            {
322
                if (Log.IsDebugEnabled) Log.DebugFormat("START");
323
                //'since' is not used here because we need to have ListObjects return a NoChange result
324
                //for all shared accounts,containers
325
                var accounts = ListSharingAccounts();
326
                var items = from account in accounts 
327
                            let containers = ListContainers(account.name) 
328
                            from container in containers 
329
                            select ListObjects(account.name, container.Name,since);
330
                var objects=items.SelectMany(r=> r).ToList();
331
/*
332
                var objects = new List<ObjectInfo>();
333
                foreach (var containerObjects in items)
334
                {
335
                    objects.AddRange(containerObjects);
336
                }
337
*/
338
                if (Log.IsDebugEnabled) Log.DebugFormat("END");
339
                return objects;
340
            }
341
        }
342

    
343
        public void SetTags(ObjectInfo target,IDictionary<string,string> tags)
344
        {
345
            if (String.IsNullOrWhiteSpace(Token))
346
                throw new InvalidOperationException("The Token is not set");
347
            if (StorageUrl == null)
348
                throw new InvalidOperationException("The StorageUrl is not set");
349
            if (target == null)
350
                throw new ArgumentNullException("target");
351
            Contract.EndContractBlock();
352

    
353
            using (ThreadContext.Stacks["Share"].Push("Share Object"))
354
            {
355
                if (Log.IsDebugEnabled) Log.DebugFormat("START");
356

    
357
                using (var client = new RestClient(_baseClient))
358
                {
359

    
360
                    client.BaseAddress = GetAccountUrl(target.Account);
361

    
362
                    client.Parameters.Clear();
363
                    client.Parameters.Add("update", "");
364

    
365
                    foreach (var tag in tags)
366
                    {
367
                        var headerTag = String.Format("X-Object-Meta-{0}", tag.Key);
368
                        client.Headers.Add(headerTag, tag.Value);
369
                    }
370
                    
371
                    client.DownloadStringWithRetry(target.Container, 3);
372

    
373
                    
374
                    client.AssertStatusOK("SetTags failed");
375
                    //If the status is NOT ACCEPTED we have a problem
376
                    if (client.StatusCode != HttpStatusCode.Accepted)
377
                    {
378
                        Log.Error("Failed to set tags");
379
                        throw new Exception("Failed to set tags");
380
                    }
381

    
382
                    if (Log.IsDebugEnabled) Log.DebugFormat("END");
383
                }
384
            }
385

    
386

    
387
        }
388

    
389
        public void ShareObject(string account, string container, string objectName, string shareTo, bool read, bool write)
390
        {
391
            if (String.IsNullOrWhiteSpace(Token))
392
                throw new InvalidOperationException("The Token is not set");
393
            if (StorageUrl==null)
394
                throw new InvalidOperationException("The StorageUrl is not set");
395
            if (String.IsNullOrWhiteSpace(container))
396
                throw new ArgumentNullException("container");
397
            if (String.IsNullOrWhiteSpace(objectName))
398
                throw new ArgumentNullException("objectName");
399
            if (String.IsNullOrWhiteSpace(account))
400
                throw new ArgumentNullException("account");
401
            if (String.IsNullOrWhiteSpace(shareTo))
402
                throw new ArgumentNullException("shareTo");
403
            Contract.EndContractBlock();
404

    
405
            using (ThreadContext.Stacks["Share"].Push("Share Object"))
406
            {
407
                if (Log.IsDebugEnabled) Log.DebugFormat("START");
408
                
409
                using (var client = new RestClient(_baseClient))
410
                {
411

    
412
                    client.BaseAddress = GetAccountUrl(account);
413

    
414
                    client.Parameters.Clear();
415
                    client.Parameters.Add("format", "json");
416

    
417
                    string permission = "";
418
                    if (write)
419
                        permission = String.Format("write={0}", shareTo);
420
                    else if (read)
421
                        permission = String.Format("read={0}", shareTo);
422
                    client.Headers.Add("X-Object-Sharing", permission);
423

    
424
                    var content = client.DownloadStringWithRetry(container, 3);
425

    
426
                    client.AssertStatusOK("ShareObject failed");
427

    
428
                    //If the result is empty, return an empty list,
429
                    var infos = String.IsNullOrWhiteSpace(content)
430
                                    ? new List<ObjectInfo>()
431
                                //Otherwise deserialize the object list into a list of ObjectInfos
432
                                    : JsonConvert.DeserializeObject<IList<ObjectInfo>>(content);
433

    
434
                    if (Log.IsDebugEnabled) Log.DebugFormat("END");
435
                }
436
            }
437

    
438

    
439
        }
440

    
441
        public AccountInfo GetAccountPolicies(AccountInfo accountInfo)
442
        {
443
            if (accountInfo==null)
444
                throw new ArgumentNullException("accountInfo");
445
            Contract.EndContractBlock();
446

    
447
            using (ThreadContext.Stacks["Account"].Push("GetPolicies"))
448
            {
449
                if (Log.IsDebugEnabled) Log.DebugFormat("START");
450

    
451
                using (var client = new RestClient(_baseClient))
452
                {
453
                    if (!String.IsNullOrWhiteSpace(accountInfo.UserName))
454
                        client.BaseAddress = GetAccountUrl(accountInfo.UserName);
455

    
456
                    client.Parameters.Clear();
457
                    client.Parameters.Add("format", "json");                    
458
                    client.Head(String.Empty, 3);
459

    
460
                    var quotaValue=client.ResponseHeaders["X-Account-Policy-Quota"];
461
                    var bytesValue= client.ResponseHeaders["X-Account-Bytes-Used"];
462

    
463
                    long quota, bytes;
464
                    if (long.TryParse(quotaValue, out quota))
465
                        accountInfo.Quota = quota;
466
                    if (long.TryParse(bytesValue, out bytes))
467
                        accountInfo.BytesUsed = bytes;
468
                    
469
                    return accountInfo;
470

    
471
                }
472

    
473
            }
474
        }
475

    
476
        public void UpdateMetadata(ObjectInfo objectInfo)
477
        {
478
            if (objectInfo == null)
479
                throw new ArgumentNullException("objectInfo");
480
            Contract.EndContractBlock();
481

    
482
            using (ThreadContext.Stacks["Objects"].Push("UpdateMetadata"))
483
            {
484
                if (Log.IsDebugEnabled) Log.DebugFormat("START");
485

    
486

    
487
                using(var client=new RestClient(_baseClient))
488
                {
489

    
490
                    client.BaseAddress = GetAccountUrl(objectInfo.Account);
491
                    
492
                    client.Parameters.Clear();
493
                    
494

    
495
                    //Set Tags
496
                    foreach (var tag in objectInfo.Tags)
497
                    {
498
                        var headerTag = String.Format("X-Object-Meta-{0}", tag.Key);
499
                        client.Headers.Add(headerTag, tag.Value);
500
                    }
501

    
502
                    //Set Permissions
503

    
504
                    var permissions=objectInfo.GetPermissionString();
505
                    client.SetNonEmptyHeaderValue("X-Object-Sharing",permissions);
506

    
507
                    client.SetNonEmptyHeaderValue("Content-Disposition",objectInfo.ContendDisposition);
508
                    client.SetNonEmptyHeaderValue("Content-Encoding",objectInfo.ContentEncoding);
509
                    client.SetNonEmptyHeaderValue("X-Object-Manifest",objectInfo.Manifest);
510
                    var isPublic = objectInfo.IsPublic.ToString().ToLower();
511
                    client.Headers.Add("X-Object-Public", isPublic);
512

    
513

    
514
                    var uriBuilder = client.GetAddressBuilder(objectInfo.Container, objectInfo.Name);
515
                    var uri = uriBuilder.Uri;
516

    
517
                    client.UploadValues(uri,new NameValueCollection());
518

    
519

    
520
                    client.AssertStatusOK("UpdateMetadata failed");
521
                    //If the status is NOT ACCEPTED or OK we have a problem
522
                    if (!(client.StatusCode == HttpStatusCode.Accepted || client.StatusCode == HttpStatusCode.OK))
523
                    {
524
                        Log.Error("Failed to update metadata");
525
                        throw new Exception("Failed to update metadata");
526
                    }
527

    
528
                    if (Log.IsDebugEnabled) Log.DebugFormat("END");
529
                }
530
            }
531

    
532
        }
533

    
534
        public void UpdateMetadata(ContainerInfo containerInfo)
535
        {
536
            if (containerInfo == null)
537
                throw new ArgumentNullException("containerInfo");
538
            Contract.EndContractBlock();
539

    
540
            using (ThreadContext.Stacks["Containers"].Push("UpdateMetadata"))
541
            {
542
                if (Log.IsDebugEnabled) Log.DebugFormat("START");
543

    
544

    
545
                using(var client=new RestClient(_baseClient))
546
                {
547

    
548
                    client.BaseAddress = GetAccountUrl(containerInfo.Account);
549
                    
550
                    client.Parameters.Clear();
551
                    
552

    
553
                    //Set Tags
554
                    foreach (var tag in containerInfo.Tags)
555
                    {
556
                        var headerTag = String.Format("X-Container-Meta-{0}", tag.Key);
557
                        client.Headers.Add(headerTag, tag.Value);
558
                    }
559

    
560
                    
561
                    //Set Policies
562
                    foreach (var policy in containerInfo.Policies)
563
                    {
564
                        var headerPolicy = String.Format("X-Container-Policy-{0}", policy.Key);
565
                        client.Headers.Add(headerPolicy, policy.Value);
566
                    }
567

    
568

    
569
                    var uriBuilder = client.GetAddressBuilder(containerInfo.Name,"");
570
                    var uri = uriBuilder.Uri;
571

    
572
                    client.UploadValues(uri,new NameValueCollection());
573

    
574

    
575
                    client.AssertStatusOK("UpdateMetadata failed");
576
                    //If the status is NOT ACCEPTED or OK we have a problem
577
                    if (!(client.StatusCode == HttpStatusCode.Accepted || client.StatusCode == HttpStatusCode.OK))
578
                    {
579
                        Log.Error("Failed to update metadata");
580
                        throw new Exception("Failed to update metadata");
581
                    }
582

    
583
                    if (Log.IsDebugEnabled) Log.DebugFormat("END");
584
                }
585
            }
586

    
587
        }
588

    
589

    
590
        public IList<ObjectInfo> ListObjects(string account, string container, DateTime? since = null)
591
        {
592
            if (String.IsNullOrWhiteSpace(container))
593
                throw new ArgumentNullException("container");
594
            Contract.EndContractBlock();
595

    
596
            using (ThreadContext.Stacks["Objects"].Push("List"))
597
            {
598
                if (Log.IsDebugEnabled) Log.DebugFormat("START");
599

    
600
                using (var client = new RestClient(_baseClient))
601
                {
602
                    if (!String.IsNullOrWhiteSpace(account))
603
                        client.BaseAddress = GetAccountUrl(account);
604

    
605
                    client.Parameters.Clear();
606
                    client.Parameters.Add("format", "json");
607
                    client.IfModifiedSince = since;
608
                    var content = client.DownloadStringWithRetry(container, 3);
609

    
610
                    client.AssertStatusOK("ListObjects failed");
611

    
612
                    if (client.StatusCode==HttpStatusCode.NotModified)
613
                        return new[]{new NoModificationInfo(account,container)};
614
                    //If the result is empty, return an empty list,
615
                    var infos = String.IsNullOrWhiteSpace(content)
616
                                    ? new List<ObjectInfo>()
617
                                //Otherwise deserialize the object list into a list of ObjectInfos
618
                                    : JsonConvert.DeserializeObject<IList<ObjectInfo>>(content);
619

    
620
                    foreach (var info in infos)
621
                    {
622
                        info.Container = container;
623
                        info.Account = account;
624
                        info.StorageUri = this.StorageUrl;
625
                    }
626
                    if (Log.IsDebugEnabled) Log.DebugFormat("END");
627
                    return infos;
628
                }
629
            }
630
        }
631

    
632
        public IList<ObjectInfo> ListObjects(string account, string container, string folder, DateTime? since = null)
633
        {
634
            if (String.IsNullOrWhiteSpace(container))
635
                throw new ArgumentNullException("container");
636
/*
637
            if (String.IsNullOrWhiteSpace(folder))
638
                throw new ArgumentNullException("folder");
639
*/
640
            Contract.EndContractBlock();
641

    
642
            using (ThreadContext.Stacks["Objects"].Push("List"))
643
            {
644
                if (Log.IsDebugEnabled) Log.DebugFormat("START");
645

    
646
                using (var client = new RestClient(_baseClient))
647
                {
648
                    if (!String.IsNullOrWhiteSpace(account))
649
                        client.BaseAddress = GetAccountUrl(account);
650

    
651
                    client.Parameters.Clear();
652
                    client.Parameters.Add("format", "json");
653
                    client.Parameters.Add("path", folder);
654
                    client.IfModifiedSince = since;
655
                    var content = client.DownloadStringWithRetry(container, 3);
656
                    client.AssertStatusOK("ListObjects failed");
657

    
658
                    if (client.StatusCode==HttpStatusCode.NotModified)
659
                        return new[]{new NoModificationInfo(account,container,folder)};
660

    
661
                    var infos = JsonConvert.DeserializeObject<IList<ObjectInfo>>(content);
662
                    foreach (var info in infos)
663
                    {
664
                        info.Account = account;
665
                        if (info.Container == null)
666
                            info.Container = container;
667
                        info.StorageUri = this.StorageUrl;
668
                    }
669
                    if (Log.IsDebugEnabled) Log.DebugFormat("END");
670
                    return infos;
671
                }
672
            }
673
        }
674

    
675
 
676
        public bool ContainerExists(string account, string container)
677
        {
678
            if (String.IsNullOrWhiteSpace(container))
679
                throw new ArgumentNullException("container", "The container property can't be empty");
680
            Contract.EndContractBlock();
681

    
682
            using (ThreadContext.Stacks["Containters"].Push("Exists"))
683
            {
684
                if (Log.IsDebugEnabled) Log.DebugFormat("START");
685

    
686
                using (var client = new RestClient(_baseClient))
687
                {
688
                    if (!String.IsNullOrWhiteSpace(account))
689
                        client.BaseAddress = GetAccountUrl(account);
690

    
691
                    client.Parameters.Clear();
692
                    client.Head(container, 3);
693
                                        
694
                    bool result;
695
                    switch (client.StatusCode)
696
                    {
697
                        case HttpStatusCode.OK:
698
                        case HttpStatusCode.NoContent:
699
                            result=true;
700
                            break;
701
                        case HttpStatusCode.NotFound:
702
                            result=false;
703
                            break;
704
                        default:
705
                            throw CreateWebException("ContainerExists", client.StatusCode);
706
                    }
707
                    if (Log.IsDebugEnabled) Log.DebugFormat("END");
708

    
709
                    return result;
710
                }
711
                
712
            }
713
        }
714

    
715
        public bool ObjectExists(string account, string container, string objectName)
716
        {
717
            if (String.IsNullOrWhiteSpace(container))
718
                throw new ArgumentNullException("container", "The container property can't be empty");
719
            if (String.IsNullOrWhiteSpace(objectName))
720
                throw new ArgumentNullException("objectName", "The objectName property can't be empty");
721
            Contract.EndContractBlock();
722

    
723
            using (var client = new RestClient(_baseClient))
724
            {
725
                if (!String.IsNullOrWhiteSpace(account))
726
                    client.BaseAddress = GetAccountUrl(account);
727

    
728
                client.Parameters.Clear();
729
                client.Head(container + "/" + objectName, 3);
730

    
731
                switch (client.StatusCode)
732
                {
733
                    case HttpStatusCode.OK:
734
                    case HttpStatusCode.NoContent:
735
                        return true;
736
                    case HttpStatusCode.NotFound:
737
                        return false;
738
                    default:
739
                        throw CreateWebException("ObjectExists", client.StatusCode);
740
                }
741
            }
742

    
743
        }
744

    
745
        public ObjectInfo GetObjectInfo(string account, string container, string objectName)
746
        {
747
            if (String.IsNullOrWhiteSpace(container))
748
                throw new ArgumentNullException("container", "The container property can't be empty");
749
            if (String.IsNullOrWhiteSpace(objectName))
750
                throw new ArgumentNullException("objectName", "The objectName property can't be empty");
751
            Contract.EndContractBlock();
752

    
753
            using (ThreadContext.Stacks["Objects"].Push("GetObjectInfo"))
754
            {                
755

    
756
                using (var client = new RestClient(_baseClient))
757
                {
758
                    if (!String.IsNullOrWhiteSpace(account))
759
                        client.BaseAddress = GetAccountUrl(account);
760
                    try
761
                    {
762
                        client.Parameters.Clear();
763

    
764
                        client.Head(container + "/" + objectName, 3);
765

    
766
                        if (client.TimedOut)
767
                            return ObjectInfo.Empty;
768

    
769
                        switch (client.StatusCode)
770
                        {
771
                            case HttpStatusCode.OK:
772
                            case HttpStatusCode.NoContent:
773
                                var keys = client.ResponseHeaders.AllKeys.AsQueryable();
774
                                var tags = client.GetMeta("X-Object-Meta-");
775
                                var extensions = (from key in keys
776
                                                  where key.StartsWith("X-Object-") && !key.StartsWith("X-Object-Meta-")
777
                                                  select new {Name = key, Value = client.ResponseHeaders[key]})
778
                                    .ToDictionary(t => t.Name, t => t.Value);
779

    
780
                                var permissions=client.GetHeaderValue("X-Object-Sharing", true);
781
                                
782
                                
783
                                var info = new ObjectInfo
784
                                               {
785
                                                   Account = account,
786
                                                   Container = container,
787
                                                   Name = objectName,
788
                                                   Hash = client.GetHeaderValue("ETag"),
789
                                                   Content_Type = client.GetHeaderValue("Content-Type"),
790
                                                   Bytes = Convert.ToInt64(client.GetHeaderValue("Content-Length",true)),
791
                                                   Tags = tags,
792
                                                   Last_Modified = client.LastModified,
793
                                                   Extensions = extensions,
794
                                                   ContentEncoding=client.GetHeaderValue("Content-Encoding",true),
795
                                                   ContendDisposition = client.GetHeaderValue("Content-Disposition",true),
796
                                                   Manifest=client.GetHeaderValue("X-Object-Manifest",true),
797
                                                   PublicUrl=client.GetHeaderValue("X-Object-Public",true),                                                   
798
                                               };
799
                                info.SetPermissions(permissions);
800
                                return info;
801
                            case HttpStatusCode.NotFound:
802
                                return ObjectInfo.Empty;
803
                            default:
804
                                throw new WebException(
805
                                    String.Format("[FAIL] GetObjectInfo for {0} failed with unexpected status code {1}",
806
                                                  objectName, client.StatusCode));
807
                        }
808

    
809
                    }
810
                    catch (RetryException)
811
                    {
812
                        Log.WarnFormat("[RETRY FAIL] GetObjectInfo for {0} failed.",objectName);
813
                        return ObjectInfo.Empty;
814
                    }
815
                    catch (WebException e)
816
                    {
817
                        Log.Error(
818
                            String.Format("[FAIL] GetObjectInfo for {0} failed with unexpected status code {1}",
819
                                          objectName, client.StatusCode), e);
820
                        throw;
821
                    }
822
                }                
823
            }
824

    
825
        }
826

    
827
        public void CreateFolder(string account, string container, string folder)
828
        {
829
            if (String.IsNullOrWhiteSpace(container))
830
                throw new ArgumentNullException("container", "The container property can't be empty");
831
            if (String.IsNullOrWhiteSpace(folder))
832
                throw new ArgumentNullException("folder", "The folder property can't be empty");
833
            Contract.EndContractBlock();
834

    
835
            var folderUrl=String.Format("{0}/{1}",container,folder);
836
            using (var client = new RestClient(_baseClient))
837
            {
838
                if (!String.IsNullOrWhiteSpace(account))
839
                    client.BaseAddress = GetAccountUrl(account);
840

    
841
                client.Parameters.Clear();
842
                client.Headers.Add("Content-Type", @"application/directory");
843
                client.Headers.Add("Content-Length", "0");
844
                client.PutWithRetry(folderUrl, 3);
845

    
846
                if (client.StatusCode != HttpStatusCode.Created && client.StatusCode != HttpStatusCode.Accepted)
847
                    throw CreateWebException("CreateFolder", client.StatusCode);
848
            }
849
        }
850

    
851
     
852

    
853
        public ContainerInfo GetContainerInfo(string account, string container)
854
        {
855
            if (String.IsNullOrWhiteSpace(container))
856
                throw new ArgumentNullException("container", "The container property can't be empty");
857
            Contract.EndContractBlock();
858

    
859
            using (var client = new RestClient(_baseClient))
860
            {
861
                if (!String.IsNullOrWhiteSpace(account))
862
                    client.BaseAddress = GetAccountUrl(account);                
863

    
864
                client.Head(container);
865
                switch (client.StatusCode)
866
                {
867
                    case HttpStatusCode.OK:
868
                    case HttpStatusCode.NoContent:
869
                        var tags = client.GetMeta("X-Container-Meta-");
870
                        var policies = client.GetMeta("X-Container-Policy-");
871

    
872
                        var containerInfo = new ContainerInfo
873
                                                {
874
                                                    Account=account,
875
                                                    Name = container,
876
                                                    Count =
877
                                                        long.Parse(client.GetHeaderValue("X-Container-Object-Count")),
878
                                                    Bytes = long.Parse(client.GetHeaderValue("X-Container-Bytes-Used")),
879
                                                    BlockHash = client.GetHeaderValue("X-Container-Block-Hash"),
880
                                                    BlockSize=int.Parse(client.GetHeaderValue("X-Container-Block-Size")),
881
                                                    Last_Modified=client.LastModified,
882
                                                    Tags=tags,
883
                                                    Policies=policies
884
                                                };
885
                        
886

    
887
                        return containerInfo;
888
                    case HttpStatusCode.NotFound:
889
                        return ContainerInfo.Empty;
890
                    default:
891
                        throw CreateWebException("GetContainerInfo", client.StatusCode);
892
                }
893
            }
894
        }
895

    
896
        public void CreateContainer(string account, string container)
897
        {
898
            if (String.IsNullOrWhiteSpace(account))
899
                throw new ArgumentNullException("account");
900
            if (String.IsNullOrWhiteSpace(container))
901
                throw new ArgumentNullException("container");
902
            Contract.EndContractBlock();
903

    
904
            using (var client = new RestClient(_baseClient))
905
            {
906
                if (!String.IsNullOrWhiteSpace(account))
907
                    client.BaseAddress = GetAccountUrl(account);
908

    
909
                client.PutWithRetry(container, 3);
910
                var expectedCodes = new[] {HttpStatusCode.Created, HttpStatusCode.Accepted, HttpStatusCode.OK};
911
                if (!expectedCodes.Contains(client.StatusCode))
912
                    throw CreateWebException("CreateContainer", client.StatusCode);
913
            }
914
        }
915

    
916
        public void DeleteContainer(string account, string container)
917
        {
918
            if (String.IsNullOrWhiteSpace(container))
919
                throw new ArgumentNullException("container", "The container property can't be empty");
920
            Contract.EndContractBlock();
921

    
922
            using (var client = new RestClient(_baseClient))
923
            {
924
                if (!String.IsNullOrWhiteSpace(account))
925
                    client.BaseAddress = GetAccountUrl(account);
926

    
927
                client.DeleteWithRetry(container, 3);
928
                var expectedCodes = new[] {HttpStatusCode.NotFound, HttpStatusCode.NoContent};
929
                if (!expectedCodes.Contains(client.StatusCode))
930
                    throw CreateWebException("DeleteContainer", client.StatusCode);
931
            }
932

    
933
        }
934

    
935
        /// <summary>
936
        /// 
937
        /// </summary>
938
        /// <param name="account"></param>
939
        /// <param name="container"></param>
940
        /// <param name="objectName"></param>
941
        /// <param name="fileName"></param>
942
        /// <returns></returns>
943
        /// <remarks>This method should have no timeout or a very long one</remarks>
944
        //Asynchronously download the object specified by *objectName* in a specific *container* to 
945
        // a local file
946
        public Task GetObject(string account, string container, string objectName, string fileName)
947
        {
948
            if (String.IsNullOrWhiteSpace(container))
949
                throw new ArgumentNullException("container", "The container property can't be empty");
950
            if (String.IsNullOrWhiteSpace(objectName))
951
                throw new ArgumentNullException("objectName", "The objectName property can't be empty");            
952
            Contract.EndContractBlock();
953

    
954
            try
955
            {
956
                //WebClient, and by extension RestClient, are not thread-safe. Create a new RestClient
957
                //object to avoid concurrency errors.
958
                //
959
                //Download operations take a long time therefore they have no timeout.
960
                var client = new RestClient(_baseClient) { Timeout = 0 };
961
                if (!String.IsNullOrWhiteSpace(account))
962
                    client.BaseAddress = GetAccountUrl(account);
963

    
964
                //The container and objectName are relative names. They are joined with the client's
965
                //BaseAddress to create the object's absolute address
966
                var builder = client.GetAddressBuilder(container, objectName);
967
                var uri = builder.Uri;
968

    
969
                //Download progress is reported to the Trace log
970
                Log.InfoFormat("[GET] START {0}", objectName);
971
                client.DownloadProgressChanged += (sender, args) => 
972
                    Log.InfoFormat("[GET PROGRESS] {0} {1}% {2} of {3}",
973
                                    fileName, args.ProgressPercentage,
974
                                    args.BytesReceived,
975
                                    args.TotalBytesToReceive);                                
976

    
977

    
978
                //Start downloading the object asynchronously
979
                var downloadTask = client.DownloadFileTask(uri, fileName);
980
                
981
                //Once the download completes
982
                return downloadTask.ContinueWith(download =>
983
                                      {
984
                                          //Delete the local client object
985
                                          client.Dispose();
986
                                          //And report failure or completion
987
                                          if (download.IsFaulted)
988
                                          {
989
                                              Log.ErrorFormat("[GET] FAIL for {0} with \r{1}", objectName,
990
                                                               download.Exception);
991
                                          }
992
                                          else
993
                                          {
994
                                              Log.InfoFormat("[GET] END {0}", objectName);                                             
995
                                          }
996
                                      });
997
            }
998
            catch (Exception exc)
999
            {
1000
                Log.ErrorFormat("[GET] END {0} with {1}", objectName, exc);
1001
                throw;
1002
            }
1003

    
1004

    
1005

    
1006
        }
1007

    
1008
        public Task<IList<string>> PutHashMap(string account, string container, string objectName, TreeHash hash)
1009
        {
1010
            if (String.IsNullOrWhiteSpace(container))
1011
                throw new ArgumentNullException("container");
1012
            if (String.IsNullOrWhiteSpace(objectName))
1013
                throw new ArgumentNullException("objectName");
1014
            if (hash==null)
1015
                throw new ArgumentNullException("hash");
1016
            if (String.IsNullOrWhiteSpace(Token))
1017
                throw new InvalidOperationException("Invalid Token");
1018
            if (StorageUrl == null)
1019
                throw new InvalidOperationException("Invalid Storage Url");
1020
            Contract.EndContractBlock();
1021

    
1022

    
1023
            //Don't use a timeout because putting the hashmap may be a long process
1024
            var client = new RestClient(_baseClient) { Timeout = 0 };           
1025
            if (!String.IsNullOrWhiteSpace(account))
1026
                client.BaseAddress = GetAccountUrl(account);
1027

    
1028
            //The container and objectName are relative names. They are joined with the client's
1029
            //BaseAddress to create the object's absolute address
1030
            var builder = client.GetAddressBuilder(container, objectName);
1031
            builder.Query = "format=json&hashmap";
1032
            var uri = builder.Uri;
1033

    
1034

    
1035
            //Send the tree hash as Json to the server            
1036
            client.Headers[HttpRequestHeader.ContentType] = "application/octet-stream";
1037
            var jsonHash = hash.ToJson();
1038
            var uploadTask=client.UploadStringTask(uri, "PUT", jsonHash);
1039
            
1040
            return uploadTask.ContinueWith(t =>
1041
            {
1042

    
1043
                var empty = (IList<string>)new List<string>();
1044
                
1045

    
1046
                //The server will respond either with 201-created if all blocks were already on the server
1047
                if (client.StatusCode == HttpStatusCode.Created)                    
1048
                {
1049
                    //in which case we return an empty hash list
1050
                    return empty;
1051
                }
1052
                //or with a 409-conflict and return the list of missing parts
1053
                //A 409 will cause an exception so we need to check t.IsFaulted to avoid propagating the exception                
1054
                if (t.IsFaulted)
1055
                {
1056
                    var ex = t.Exception.InnerException;
1057
                    var we = ex as WebException;
1058
                    var response = we.Response as HttpWebResponse;
1059
                    if (response!=null && response.StatusCode==HttpStatusCode.Conflict)
1060
                    {
1061
                        //In case of 409 the missing parts will be in the response content                        
1062
                        using (var stream = response.GetResponseStream())
1063
                        using(var reader=new StreamReader(stream))
1064
                        {
1065
                            Debug.Assert(stream.Position == 0);
1066
                            //We used to have to cleanup the content before returning it because it contains
1067
                            //error content after the list of hashes
1068
                            //
1069
                            //As of 30/1/2012, the result is a proper Json array so we don't need to read the content
1070
                            //line by line
1071
                            
1072
                            var serializer = new JsonSerializer();                            
1073
                            var hashes=(List<string>)serializer.Deserialize(reader, typeof (List<string>));
1074

    
1075
                            return hashes;
1076
                        }                        
1077
                    }                    
1078
                    //Any other status code is unexpected and the exception should be rethrown
1079
                    throw ex;
1080
                    
1081
                }
1082
                //Any other status code is unexpected but there was no exception. We can probably continue processing
1083
                Log.WarnFormat("Unexcpected status code when putting map: {0} - {1}",client.StatusCode,client.StatusDescription);                    
1084
                
1085
                return empty;
1086
            });
1087

    
1088
        }
1089

    
1090
        public Task<byte[]> GetBlock(string account, string container, Uri relativeUrl, long start, long? end)
1091
        {
1092
            if (String.IsNullOrWhiteSpace(Token))
1093
                throw new InvalidOperationException("Invalid Token");
1094
            if (StorageUrl == null)
1095
                throw new InvalidOperationException("Invalid Storage Url");
1096
            if (String.IsNullOrWhiteSpace(container))
1097
                throw new ArgumentNullException("container");
1098
            if (relativeUrl== null)
1099
                throw new ArgumentNullException("relativeUrl");
1100
            if (end.HasValue && end<0)
1101
                throw new ArgumentOutOfRangeException("end");
1102
            if (start<0)
1103
                throw new ArgumentOutOfRangeException("start");
1104
            Contract.EndContractBlock();
1105

    
1106

    
1107
            //Don't use a timeout because putting the hashmap may be a long process
1108
            var client = new RestClient(_baseClient) {Timeout = 0, RangeFrom = start, RangeTo = end};
1109
            if (!String.IsNullOrWhiteSpace(account))
1110
                client.BaseAddress = GetAccountUrl(account);
1111

    
1112
            var builder = client.GetAddressBuilder(container, relativeUrl.ToString());
1113
            var uri = builder.Uri;
1114

    
1115
            return client.DownloadDataTask(uri)
1116
                .ContinueWith(t=>
1117
                                  {
1118
                                      client.Dispose();
1119
                                      return t.Result;
1120
                                  });
1121
        }
1122

    
1123

    
1124
        public async Task PostBlock(string account, string container, byte[] block, int offset, int count)
1125
        {
1126
            if (String.IsNullOrWhiteSpace(container))
1127
                throw new ArgumentNullException("container");
1128
            if (block == null)
1129
                throw new ArgumentNullException("block");
1130
            if (offset < 0 || offset >= block.Length)
1131
                throw new ArgumentOutOfRangeException("offset");
1132
            if (count < 0 || count > block.Length)
1133
                throw new ArgumentOutOfRangeException("count");
1134
            if (String.IsNullOrWhiteSpace(Token))
1135
                throw new InvalidOperationException("Invalid Token");
1136
            if (StorageUrl == null)
1137
                throw new InvalidOperationException("Invalid Storage Url");                        
1138
            Contract.EndContractBlock();
1139

    
1140

    
1141
            try
1142
            {
1143

    
1144
            //Don't use a timeout because putting the hashmap may be a long process
1145
                using (var client = new RestClient(_baseClient) { Timeout = 0 })
1146
                {
1147
                    if (!String.IsNullOrWhiteSpace(account))
1148
                        client.BaseAddress = GetAccountUrl(account);
1149

    
1150
                    var builder = client.GetAddressBuilder(container, "");
1151
                    //We are doing an update
1152
                    builder.Query = "update";
1153
                    var uri = builder.Uri;
1154

    
1155
                    client.Headers[HttpRequestHeader.ContentType] = "application/octet-stream";
1156

    
1157
                    Log.InfoFormat("[BLOCK POST] START");
1158

    
1159
                    client.UploadProgressChanged += (sender, args) =>
1160
                                                    Log.InfoFormat("[BLOCK POST PROGRESS] {0}% {1} of {2}",
1161
                                                                   args.ProgressPercentage, args.BytesSent,
1162
                                                                   args.TotalBytesToSend);
1163
                    client.UploadFileCompleted += (sender, args) =>
1164
                                                  Log.InfoFormat("[BLOCK POST PROGRESS] Completed ");
1165

    
1166
                    var buffer = new byte[count];
1167
                    Buffer.BlockCopy(block, offset, buffer, 0, count);
1168
                    //Send the block
1169
                    await client.UploadDataTask(uri, "POST", buffer);
1170
                    Log.InfoFormat("[BLOCK POST] END");
1171
                }
1172
            }
1173
            catch (Exception exc)
1174
            {
1175
                Log.ErrorFormat("[BLOCK POST] FAIL with \r{0}", exc);                                        
1176
                throw;
1177
            }
1178
        }
1179

    
1180

    
1181
        public async Task<TreeHash> GetHashMap(string account, string container, string objectName)
1182
        {
1183
            if (String.IsNullOrWhiteSpace(container))
1184
                throw new ArgumentNullException("container");
1185
            if (String.IsNullOrWhiteSpace(objectName))
1186
                throw new ArgumentNullException("objectName");
1187
            if (String.IsNullOrWhiteSpace(Token))
1188
                throw new InvalidOperationException("Invalid Token");
1189
            if (StorageUrl == null)
1190
                throw new InvalidOperationException("Invalid Storage Url");
1191
            Contract.EndContractBlock();
1192

    
1193
            try
1194
            {
1195
                //WebClient, and by extension RestClient, are not thread-safe. Create a new RestClient
1196
                //object to avoid concurrency errors.
1197
                //
1198
                //Download operations take a long time therefore they have no timeout.
1199
                //TODO: Do they really? this is a hashmap operation, not a download
1200
                
1201
                //Start downloading the object asynchronously
1202
                using (var client = new RestClient(_baseClient) { Timeout = 0 })
1203
                {
1204
                    if (!String.IsNullOrWhiteSpace(account))
1205
                        client.BaseAddress = GetAccountUrl(account);
1206

    
1207
                    //The container and objectName are relative names. They are joined with the client's
1208
                    //BaseAddress to create the object's absolute address
1209
                    var builder = client.GetAddressBuilder(container, objectName);
1210
                    builder.Query = "format=json&hashmap";
1211
                    var uri = builder.Uri;
1212

    
1213

    
1214
                    var json = await client.DownloadStringTaskAsync(uri);
1215
                    var treeHash = TreeHash.Parse(json);
1216
                    Log.InfoFormat("[GET HASH] END {0}", objectName);
1217
                    return treeHash;
1218
                }
1219
            }
1220
            catch (Exception exc)
1221
            {
1222
                Log.ErrorFormat("[GET HASH] END {0} with {1}", objectName, exc);
1223
                throw;
1224
            }
1225

    
1226
        }
1227

    
1228

    
1229
        /// <summary>
1230
        /// 
1231
        /// </summary>
1232
        /// <param name="account"></param>
1233
        /// <param name="container"></param>
1234
        /// <param name="objectName"></param>
1235
        /// <param name="fileName"></param>
1236
        /// <param name="hash">Optional hash value for the file. If no hash is provided, the method calculates a new hash</param>
1237
        /// <remarks>>This method should have no timeout or a very long one</remarks>
1238
        public async Task PutObject(string account, string container, string objectName, string fileName, string hash = null, string contentType = "application/octet-stream")
1239
        {
1240
            if (String.IsNullOrWhiteSpace(container))
1241
                throw new ArgumentNullException("container", "The container property can't be empty");
1242
            if (String.IsNullOrWhiteSpace(objectName))
1243
                throw new ArgumentNullException("objectName", "The objectName property can't be empty");
1244
            if (String.IsNullOrWhiteSpace(fileName))
1245
                throw new ArgumentNullException("fileName", "The fileName property can't be empty");
1246
/*
1247
            if (!File.Exists(fileName) && !Directory.Exists(fileName))
1248
                throw new FileNotFoundException("The file or directory does not exist",fileName);
1249
*/
1250
            Contract.EndContractBlock();
1251
            
1252
            try
1253
            {
1254

    
1255
                using (var client = new RestClient(_baseClient) { Timeout = 0 })
1256
                {
1257
                    if (!String.IsNullOrWhiteSpace(account))
1258
                        client.BaseAddress = GetAccountUrl(account);
1259

    
1260
                    var builder = client.GetAddressBuilder(container, objectName);
1261
                    var uri = builder.Uri;
1262

    
1263
                    string etag = hash ?? CalculateHash(fileName);
1264

    
1265
                    client.Headers.Add("Content-Type", contentType);
1266
                    client.Headers.Add("ETag", etag);
1267

    
1268

    
1269
                    Log.InfoFormat("[PUT] START {0}", objectName);
1270
                    client.UploadProgressChanged += (sender, args) =>
1271
                                                        {
1272
                                                            using (ThreadContext.Stacks["PUT"].Push("Progress"))
1273
                                                            {
1274
                                                                Log.InfoFormat("{0} {1}% {2} of {3}", fileName,
1275
                                                                               args.ProgressPercentage,
1276
                                                                               args.BytesSent, args.TotalBytesToSend);
1277
                                                            }
1278
                                                        };
1279

    
1280
                    client.UploadFileCompleted += (sender, args) =>
1281
                                                      {
1282
                                                          using (ThreadContext.Stacks["PUT"].Push("Progress"))
1283
                                                          {
1284
                                                              Log.InfoFormat("Completed {0}", fileName);
1285
                                                          }
1286
                                                      };
1287
                    if (contentType=="application/directory")
1288
                        await client.UploadDataTaskAsync(uri, "PUT", new byte[0]);
1289
                    else
1290
                        await client.UploadFileTaskAsync(uri, "PUT", fileName);
1291
                }
1292

    
1293
                Log.InfoFormat("[PUT] END {0}", objectName);
1294
            }
1295
            catch (Exception exc)
1296
            {
1297
                Log.ErrorFormat("[PUT] END {0} with {1}", objectName, exc);
1298
                throw;
1299
            }                
1300

    
1301
        }
1302
       
1303
        
1304
        private static string CalculateHash(string fileName)
1305
        {
1306
            Contract.Requires(!String.IsNullOrWhiteSpace(fileName));
1307
            Contract.EndContractBlock();
1308

    
1309
            string hash;
1310
            using (var hasher = MD5.Create())
1311
            using(var stream=File.OpenRead(fileName))
1312
            {
1313
                var hashBuilder=new StringBuilder();
1314
                foreach (byte b in hasher.ComputeHash(stream))
1315
                    hashBuilder.Append(b.ToString("x2").ToLower());
1316
                hash = hashBuilder.ToString();                
1317
            }
1318
            return hash;
1319
        }
1320
        
1321
        public void MoveObject(string account, string sourceContainer, string oldObjectName, string targetContainer, string newObjectName)
1322
        {
1323
            if (String.IsNullOrWhiteSpace(sourceContainer))
1324
                throw new ArgumentNullException("sourceContainer", "The container property can't be empty");
1325
            if (String.IsNullOrWhiteSpace(oldObjectName))
1326
                throw new ArgumentNullException("oldObjectName", "The oldObjectName property can't be empty");
1327
            if (String.IsNullOrWhiteSpace(targetContainer))
1328
                throw new ArgumentNullException("targetContainer", "The container property can't be empty");
1329
            if (String.IsNullOrWhiteSpace(newObjectName))
1330
                throw new ArgumentNullException("newObjectName", "The newObjectName property can't be empty");
1331
            Contract.EndContractBlock();
1332

    
1333
            var targetUrl = targetContainer + "/" + newObjectName;
1334
            var sourceUrl = String.Format("/{0}/{1}", sourceContainer, oldObjectName);
1335

    
1336
            using (var client = new RestClient(_baseClient))
1337
            {
1338
                if (!String.IsNullOrWhiteSpace(account))
1339
                    client.BaseAddress = GetAccountUrl(account);
1340

    
1341
                client.Headers.Add("X-Move-From", sourceUrl);
1342
                client.PutWithRetry(targetUrl, 3);
1343

    
1344
                var expectedCodes = new[] {HttpStatusCode.OK, HttpStatusCode.NoContent, HttpStatusCode.Created};
1345
                if (!expectedCodes.Contains(client.StatusCode))
1346
                    throw CreateWebException("MoveObject", client.StatusCode);
1347
            }
1348
        }
1349

    
1350
        public void DeleteObject(string account, string sourceContainer, string objectName)
1351
        {            
1352
            if (String.IsNullOrWhiteSpace(sourceContainer))
1353
                throw new ArgumentNullException("sourceContainer", "The container property can't be empty");
1354
            if (String.IsNullOrWhiteSpace(objectName))
1355
                throw new ArgumentNullException("objectName", "The oldObjectName property can't be empty");
1356
            Contract.EndContractBlock();
1357

    
1358
            var targetUrl = FolderConstants.TrashContainer + "/" + objectName;
1359
            var sourceUrl = String.Format("/{0}/{1}", sourceContainer, objectName);
1360

    
1361
            using (var client = new RestClient(_baseClient))
1362
            {
1363
                if (!String.IsNullOrWhiteSpace(account))
1364
                    client.BaseAddress = GetAccountUrl(account);
1365

    
1366
                client.Headers.Add("X-Move-From", sourceUrl);
1367
                client.AllowedStatusCodes.Add(HttpStatusCode.NotFound);
1368
                client.PutWithRetry(targetUrl, 3);
1369

    
1370
                var expectedCodes = new[] {HttpStatusCode.OK, HttpStatusCode.NoContent, HttpStatusCode.Created,HttpStatusCode.NotFound};
1371
                if (!expectedCodes.Contains(client.StatusCode))
1372
                    throw CreateWebException("DeleteObject", client.StatusCode);
1373
            }
1374
        }
1375

    
1376
      
1377
        private static WebException CreateWebException(string operation, HttpStatusCode statusCode)
1378
        {
1379
            return new WebException(String.Format("{0} failed with unexpected status code {1}", operation, statusCode));
1380
        }
1381

    
1382

    
1383
/*
1384
        public IEnumerable<ObjectInfo> ListDirectories(ContainerInfo container)
1385
        {
1386
            var directories=this.ListObjects(container.Account, container.Name, "/");
1387
        }
1388
*/
1389
    }
1390

    
1391
    public class ShareAccountInfo
1392
    {
1393
        public DateTime? last_modified { get; set; }
1394
        public string name { get; set; }
1395
    }
1396
}