Statistics
| Branch: | Revision:

root / trunk / Pithos.Network / CloudFilesClient.cs @ 6f03d6e1

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.Generic;
50
using System.Collections.Specialized;
51
using System.ComponentModel.Composition;
52
using System.Diagnostics;
53
using System.Diagnostics.Contracts;
54
using System.IO;
55
using System.Linq;
56
using System.Net;
57
using System.Reflection;
58
using System.Security.Cryptography;
59
using System.Text;
60
using System.Threading.Tasks;
61
using Newtonsoft.Json;
62
using Pithos.Interfaces;
63
using log4net;
64

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

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

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

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

    
100

    
101
        protected Uri RootAddressUri { get; set; }
102

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

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

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

    
131
        public string AuthenticationUrl { get; set; }
132

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

    
139
        public bool UsePithos { get; set; }
140

    
141

    
142

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

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

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

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

    
176

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

    
191

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

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

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

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

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

    
206
                authClient.DownloadStringWithRetry(VersionPath, 3);
207

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

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

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

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

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

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

    
249
        }
250

    
251

    
252

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

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

    
276
        }
277

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
385

    
386
        }
387

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

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

    
411
                    client.BaseAddress = GetAccountUrl(account);
412

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

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

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

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

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

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

    
437

    
438
        }
439

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

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

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

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

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

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

    
470
                }
471

    
472
            }
473
        }
474

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

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

    
485

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

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

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

    
501
                    //Set Permissions
502

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

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

    
512

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

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

    
518

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

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

    
531
        }
532

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

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

    
543

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

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

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

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

    
567

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

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

    
573

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

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

    
586
        }
587

    
588

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
742
        }
743

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

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

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

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

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

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

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

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

    
824
        }
825

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

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

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

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

    
850
     
851

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

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

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

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

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

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

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

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

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

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

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

    
932
        }
933

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

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

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

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

    
976

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

    
1003

    
1004

    
1005
        }
1006

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

    
1021

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

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

    
1033

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

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

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

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

    
1087
        }
1088

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

    
1105

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

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

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

    
1122

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

    
1139

    
1140
            try
1141
            {
1142

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

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

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

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

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

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

    
1179

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

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

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

    
1212

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

    
1225
        }
1226

    
1227

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

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

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

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

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

    
1267

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

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

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

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

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

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

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

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

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

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

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

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

    
1365
                client.Headers.Add("X-Move-From", sourceUrl);
1366
                client.AllowedStatusCodes.Add(HttpStatusCode.NotFound);
1367
                Log.InfoFormat("[TRASH] [{0}] to [{1}]",sourceUrl,targetUrl);
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
}