Statistics
| Branch: | Revision:

root / trunk / Pithos.Network / CloudFilesClient.cs @ 259d316d

History | View | Annotate | Download (86.9 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.Net.Http;
58
using System.Net.Http.Headers;
59
using System.Reflection;
60
using System.ServiceModel.Channels;
61
using System.Text;
62
using System.Threading;
63
using System.Threading.Tasks;
64
using Newtonsoft.Json;
65
using Newtonsoft.Json.Linq;
66
using Pithos.Interfaces;
67
using Pithos.Network;
68
using log4net;
69

    
70
namespace Pithos.Network
71
{
72

    
73
    [Export(typeof(ICloudClient))]
74
    public class CloudFilesClient:ICloudClient,IDisposable
75
    {
76
        private const string TOKEN_HEADER = "X-Auth-Token";
77
        private static readonly ILog Log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
78

    
79
        //CloudFilesClient uses *_baseClient* internally to communicate with the server
80
        //RestClient provides a REST-friendly interface over the standard WebClient.
81
        private RestClient _baseClient;
82

    
83
        private HttpClient _baseHttpClient;
84
        private HttpClient _baseHttpClientNoTimeout;
85
        
86

    
87
        //During authentication the client provides a UserName 
88
        public string UserName { get; set; }
89
        
90
        //and and ApiKey to the server
91
        public string ApiKey { get; set; }
92
        
93
        //And receives an authentication Token. This token must be provided in ALL other operations,
94
        //in the X-Auth-Token header
95
        private string _token;
96
        private readonly string _emptyGuid = Guid.Empty.ToString();
97
        private readonly Uri _emptyUri = new Uri("",UriKind.Relative);
98

    
99
        private HttpClientHandler _httpClientHandler = new HttpClientHandler
100
                                                           {
101
                                                               AllowAutoRedirect = true,
102
                                                               AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate,
103
                                                               UseCookies = true,   
104
                                                           };
105

    
106

    
107
        public string Token
108
        {
109
            get { return _token; }
110
            set
111
            {
112
                _token = value;
113
                _baseClient.Headers[TOKEN_HEADER] = value;
114
            }
115
        }
116

    
117
        //The client also receives a StorageUrl after authentication. All subsequent operations must
118
        //use this url
119
        public Uri StorageUrl { get; set; }
120

    
121

    
122
        public Uri RootAddressUri { get; set; }
123

    
124

    
125
        public double DownloadPercentLimit { get; set; }
126
        public double UploadPercentLimit { get; set; }
127

    
128
        public string AuthenticationUrl { get; set; }
129

    
130
 
131
        public string VersionPath
132
        {
133
            get { return UsePithos ? "v1" : "v1.0"; }
134
        }
135

    
136
        public bool UsePithos { get; set; }
137

    
138

    
139
        BufferManager _bufferManager=BufferManager.CreateBufferManager(TreeHash.DEFAULT_BLOCK_SIZE*4,(int)TreeHash.DEFAULT_BLOCK_SIZE);
140
        private string _userCatalogUrl;
141

    
142
        public CloudFilesClient(string userName, string apiKey)
143
        {
144
            UserName = userName;
145
            ApiKey = apiKey;
146
            _userCatalogUrl = "https://pithos.okeanos.io/user_catalogs";
147
        }
148

    
149
        public CloudFilesClient(AccountInfo accountInfo)
150
        {
151
            Contract.Requires<ArgumentNullException>(accountInfo!=null,"accountInfo is null");
152
            Contract.Ensures(!String.IsNullOrWhiteSpace(Token));
153
            Contract.Ensures(StorageUrl != null);
154
            Contract.Ensures(_baseClient != null);
155
            Contract.Ensures(RootAddressUri != null);
156
            Contract.EndContractBlock();          
157

    
158
            _baseClient = new RestClient
159
            {
160
                BaseAddress = accountInfo.StorageUri.ToString(),
161
                Timeout = 30000,
162
                Retries = 3,
163
            };
164
            StorageUrl = accountInfo.StorageUri;
165
            Token = accountInfo.Token;
166
            UserName = accountInfo.UserName;
167
            _userCatalogUrl = "https://pithos.okeanos.io/user_catalogs";
168
            //Get the root address (StorageUrl without the account)
169
            var storageUrl = StorageUrl.AbsoluteUri;
170
            //var usernameIndex = storageUrl.LastIndexOf(UserName);
171
            //var rootUrl = storageUrl.Substring(0, usernameIndex);
172
//            RootAddressUri = new Uri(rootUrl);
173
            RootAddressUri = new Uri(storageUrl);
174

    
175
            var httpClientHandler = new HttpClientHandler
176
            {
177
                AllowAutoRedirect = true,
178
                AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate,
179
                UseCookies = true,
180
            };
181

    
182

    
183
            _baseHttpClient = new HttpClient(httpClientHandler)
184
            {
185
                BaseAddress = StorageUrl,
186
                Timeout = TimeSpan.FromSeconds(30)
187
            };
188
            _baseHttpClient.DefaultRequestHeaders.Add(TOKEN_HEADER, Token);
189

    
190
            _baseHttpClientNoTimeout = new HttpClient(httpClientHandler)
191
            {
192
                BaseAddress = StorageUrl,
193
                Timeout = TimeSpan.FromMilliseconds(-1)
194
            };
195
            _baseHttpClientNoTimeout.DefaultRequestHeaders.Add(TOKEN_HEADER, Token);
196

    
197

    
198
        }
199

    
200

    
201
        private static void AssertStatusOK(HttpResponseMessage response, string message)
202
        {
203
            var statusCode = response.StatusCode;
204
            if (statusCode >= HttpStatusCode.BadRequest)
205
                throw new WebException(String.Format("{0} with code {1} - {2}", message, statusCode, response.ReasonPhrase));
206
        }
207

    
208
        public async Task<AccountInfo> Authenticate()
209
        {
210
            if (String.IsNullOrWhiteSpace(UserName))
211
                throw new InvalidOperationException("UserName is empty");
212
            if (String.IsNullOrWhiteSpace(ApiKey))
213
                throw new InvalidOperationException("ApiKey is empty");
214
            if (String.IsNullOrWhiteSpace(AuthenticationUrl))
215
                throw new InvalidOperationException("AuthenticationUrl is empty");
216
            //if (String.IsNullOrWhiteSpace(Token))
217
            //    throw new InvalidOperationException("Token is Empty");
218
            
219
            //Contract.Ensures(!String.IsNullOrWhiteSpace(Token));
220
            //Contract.Ensures(StorageUrl != null);
221
            //Contract.Ensures(_baseClient != null);
222
            //Contract.Ensures(RootAddressUri != null);
223
            //Contract.EndContractBlock();
224

    
225

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

    
228
            var groups = new List<Group>();
229

    
230
//            using (var authClient = new HttpClient(_httpClientHandler,false){ BaseAddress = new Uri(AuthenticationUrl),Timeout=TimeSpan.FromSeconds(30) })
231
            using (var authClient = new HttpClient(_httpClientHandler, false) { BaseAddress = new Uri(AuthenticationUrl), Timeout = TimeSpan.FromSeconds(30) })
232
            {                
233

    
234
                authClient.DefaultRequestHeaders.Add("X-Auth-User", UserName);
235
                authClient.DefaultRequestHeaders.Add("X-Auth-Key", ApiKey);
236

    
237
                string storageUrl;
238
                string token;
239
                
240
                //using (var response = await authClient.GetAsyncWithRetries(new Uri(VersionPath, UriKind.Relative),3).ConfigureAwait(false)) // .DownloadStringWithRetryRelative(new Uri(VersionPath, UriKind.Relative), 3);                    
241
                using (var response = await authClient.SendAsyncWithRetries(new HttpRequestMessage(HttpMethod.Post,new Uri("", UriKind.Relative)), 3).ConfigureAwait(false))
242
                {
243
                    AssertStatusOK(response,"Authentication failed");
244

    
245
                    var body=await response.Content.ReadAsStringAsync();
246
                    JObject json = (JObject) JsonConvert.DeserializeObject(body);
247
                    dynamic jsonD = json;
248
                    
249
                    storageUrl = json["access"]["serviceCatalog"][6]["endpoints"][0]["publicURL"].ToString();
250
                    if (String.IsNullOrWhiteSpace(storageUrl))
251
                        throw new InvalidOperationException("Failed to obtain storage url");
252

    
253
                    token = this.ApiKey;// response.Headers.GetFirstValue(TOKEN_HEADER);
254
                    if (String.IsNullOrWhiteSpace(token))
255
                        throw new InvalidOperationException("Failed to obtain token url");
256
                }
257

    
258
                _baseClient = new RestClient
259
                {
260
                    BaseAddress = storageUrl,
261
                    Timeout = 30000,
262
                    Retries = 3,                    
263
                };
264

    
265
                StorageUrl = new Uri(storageUrl);
266
                Token = token;
267

    
268
                //Get the root address (StorageUrl without the account)
269
                //var usernameIndex=storageUrl.LastIndexOf(UserName);
270
                var rootUrl = storageUrl;//.Substring(0, usernameIndex);
271
                RootAddressUri = new Uri(rootUrl);
272
                
273

    
274
                _baseHttpClient = new HttpClient(_httpClientHandler,false)
275
                {
276
                    BaseAddress = StorageUrl,
277
                    Timeout = TimeSpan.FromSeconds(30)
278
                };
279
                _baseHttpClient.DefaultRequestHeaders.Add(TOKEN_HEADER, token);
280

    
281
                _baseHttpClientNoTimeout = new HttpClient(_httpClientHandler,false)
282
                {
283
                    BaseAddress = StorageUrl,
284
                    Timeout = TimeSpan.FromMilliseconds(-1)
285
                };
286
                _baseHttpClientNoTimeout.DefaultRequestHeaders.Add(TOKEN_HEADER, token);
287

    
288
                /* var keys = authClient.ResponseHeaders.AllKeys.AsQueryable();
289
                groups = (from key in keys
290
                            where key.StartsWith("X-Account-Group-")
291
                            let name = key.Substring(16)
292
                            select new Group(name, authClient.ResponseHeaders[key]))
293
                        .ToList();
294
                    
295
*/
296
            }
297

    
298
            Log.InfoFormat("[AUTHENTICATE] End for {0}", UserName);
299
            Debug.Assert(_baseClient!=null);
300
            var displayName = UserName;
301
            Guid uuid;
302
            if (Guid.TryParse(UserName, out uuid))
303
            {
304
                displayName = await ResolveName(uuid);
305
            }
306
            return new AccountInfo {StorageUri = StorageUrl, Token = Token, UserName = UserName, DisplayName=displayName, Groups=groups};            
307

    
308
        }
309

    
310
        private static void TraceStart(string method, Uri actualAddress)
311
        {
312
            Log.InfoFormat("[{0}] {1} {2}", method, DateTime.Now, actualAddress);
313
        }
314

    
315
        private async Task<string> GetStringAsync(Uri targetUri, string errorMessage,DateTimeOffset? since=null)
316
        {
317
            TraceStart("GET",targetUri);
318
            var request = new HttpRequestMessage(HttpMethod.Get, targetUri);
319
            //request.Headers.Add("User-Agent", "Pithos+ Custom Header");
320
            if (since.HasValue)
321
            {
322
                request.Headers.IfModifiedSince = since.Value;
323
            }
324
            using (var response = await _baseHttpClient.SendAsyncWithRetries(request,3).ConfigureAwait(false))
325
            {
326
                AssertStatusOK(response, errorMessage);
327

    
328
                if (response.StatusCode == HttpStatusCode.NoContent)
329
                    return String.Empty;
330

    
331
                var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
332
                return content;
333
            }
334
        }
335

    
336
        public async Task<IList<ContainerInfo>> ListContainers(string account)
337
        {
338

    
339
            var targetUrl = GetTargetUrl(account);
340
            var targetUri = new Uri(String.Format("{0}?format=json", targetUrl));
341
            var result = await GetStringAsync(targetUri, "List Containers failed").ConfigureAwait(false);
342
            if (String.IsNullOrWhiteSpace(result))
343
                return new List<ContainerInfo>();
344
            var infos = JsonConvert.DeserializeObject<IList<ContainerInfo>>(result);
345
            foreach (var info in infos)
346
            {
347
                info.Account = account;
348
            }
349
            return infos;
350
        }
351

    
352
        
353
        private string GetAccountUrl(string account)
354
        {
355
            return RootAddressUri.Combine(account).AbsoluteUri;
356
        }
357

    
358
        public IList<ShareAccountInfo> ListSharingAccounts(DateTime? since=null)
359
        {
360
            using (ThreadContext.Stacks["Share"].Push("List Accounts"))
361
            {
362
                if (Log.IsDebugEnabled) Log.DebugFormat("START");
363

    
364
                var targetUri = new Uri(String.Format("{0}?format=json", RootAddressUri), UriKind.Absolute);
365
                var content=TaskEx.Run(async ()=>await GetStringAsync(targetUri, "ListSharingAccounts failed", since).ConfigureAwait(false)).Result;
366

    
367
                //If the result is empty, return an empty list,
368
                var infos = String.IsNullOrWhiteSpace(content)
369
                                ? new List<ShareAccountInfo>()
370
                            //Otherwise deserialize the account list into a list of ShareAccountInfos
371
                                : JsonConvert.DeserializeObject<IList<ShareAccountInfo>>(content);
372

    
373
                Log.DebugFormat("END");
374
                return infos;
375
            }
376
        }
377

    
378

    
379
        /// <summary>
380
        /// Request listing of all objects in a container modified since a specific time.
381
        /// If the *since* value is missing, return all objects
382
        /// </summary>
383
        /// <param name="knownContainers">Use the since variable only for the containers listed in knownContainers. Unknown containers are considered new
384
        /// and should be polled anyway
385
        /// </param>
386
        /// <param name="since"></param>
387
        /// <returns></returns>
388
        public IList<ObjectInfo> ListSharedObjects(HashSet<string> knownContainers, DateTimeOffset? since)
389
        {
390

    
391
            using (ThreadContext.Stacks["Share"].Push("List Objects"))
392
            {
393
                if (Log.IsDebugEnabled) Log.DebugFormat("START");
394
                //'since' is not used here because we need to have ListObjects return a NoChange result
395
                //for all shared accounts,containers
396

    
397
                Func<ContainerInfo, string> getKey = c => String.Format("{0}\\{1}", c.Account, c.Name);
398
                
399
                var containers = (from account in ListSharingAccounts()
400
                                 let conts = TaskEx.Run(async ()=>await ListContainers(account.name).ConfigureAwait(false)).Result
401
                                 from container in conts
402
                                 select container).ToList();                
403
                var items = from container in containers 
404
                            let actualSince=knownContainers.Contains(getKey(container))?since:null
405
                            select ListObjects(container.Account , container.Name,  actualSince);
406
                var objects=items.SelectMany(r=> r).ToList();
407

    
408
                //For each object
409
                //Check parents recursively up to (but not including) the container.
410
                //If parents are missing, add them to the list
411
                //Need function to calculate all parent URLs
412
                objects = AddMissingParents(objects);
413
                
414
                //Store any new containers
415
                foreach (var container in containers)
416
                {
417
                    knownContainers.Add(getKey(container));
418
                }
419

    
420
                if (Log.IsDebugEnabled) Log.DebugFormat("END");
421
                return objects;
422
            }
423
        }
424

    
425
        private List<ObjectInfo> AddMissingParents(List<ObjectInfo> objects)
426
        {
427
            //TODO: Remove short-circuit when we decide to use Missing Parents functionality
428
            //return objects;
429

    
430
            var existingUris = objects.ToDictionary(o => o.Uri, o => o);
431
            foreach (var objectInfo in objects)
432
            {
433
                //Can be null when retrieving objects to show in selective sync
434
                if (objectInfo.Name == null)
435
                    continue;
436

    
437
                //No need to unescape here, the parts will be used to create new ObjectInfos
438
                var parts = objectInfo.Name.ToString().Split(new[]{'/'},StringSplitOptions.RemoveEmptyEntries);
439
                //If there is no parent, skip
440
                if (parts.Length == 1)
441
                    continue;
442
                var baseParts = new[]
443
                                  {
444
                                      objectInfo.Uri.Host, objectInfo.Uri.Segments[1].TrimEnd('/'),objectInfo.Account,objectInfo.Container.ToString()
445
                                  };
446
                for (var partIdx = 0; partIdx < parts.Length - 1; partIdx++)
447
                {
448
                    var nameparts = parts.Range(0, partIdx).ToArray();
449
                    var parentName= String.Join("/", nameparts);
450

    
451
                    var parentParts = baseParts.Concat(nameparts);
452
                    var parentUrl = objectInfo.Uri.Scheme+ "://" + String.Join("/", parentParts);
453
                    
454
                    var parentUri = new Uri(parentUrl, UriKind.Absolute);
455

    
456
                    ObjectInfo existingInfo;
457
                    if (!existingUris.TryGetValue(parentUri,out existingInfo))
458
                    {
459
                        var h = parentUrl.GetHashCode();
460
                        var reverse = new string(parentUrl.Reverse().ToArray());
461
                        var rh = reverse.GetHashCode();
462
                        var b1 = BitConverter.GetBytes(h);
463
                        var b2 = BitConverter.GetBytes(rh);
464
                        var g = new Guid(0,0,0,b1.Concat(b2).ToArray());
465
                        
466
                        existingUris[parentUri] = new ObjectInfo
467
                                                      {
468
                                                          Account = objectInfo.Account,
469
                                                          Container = objectInfo.Container,
470
                                                          Content_Type = ObjectInfo.CONTENT_TYPE_DIRECTORY,
471
                                                          ETag = Signature.MERKLE_EMPTY,
472
                                                          X_Object_Hash = Signature.MERKLE_EMPTY,
473
                                                          Name=new Uri(parentName,UriKind.Relative),
474
                                                          StorageUri=objectInfo.StorageUri,
475
                                                          Bytes = 0,
476
                                                          UUID=g.ToString(),                                                          
477
                                                      };
478
                    }
479
                }
480
            }
481
            return existingUris.Values.ToList();
482
        }
483

    
484
        public void SetTags(ObjectInfo target,IDictionary<string,string> tags)
485
        {
486
            Contract.Requires<InvalidOperationException>(!String.IsNullOrWhiteSpace(Token),"The Token is not set");
487
            Contract.Requires<InvalidOperationException>(StorageUrl != null,"The StorageUrl is not set");
488
            Contract.Requires<ArgumentNullException>(target != null,"target is null");
489
            Contract.EndContractBlock();
490

    
491
            using (ThreadContext.Stacks["Share"].Push("Share Object"))
492
            {
493
                if (Log.IsDebugEnabled) Log.DebugFormat("START");
494

    
495
                using (var client = new RestClient(_baseClient))
496
                {
497

    
498
                    client.BaseAddress = GetAccountUrl(target.Account);
499

    
500
                    client.Parameters.Clear();
501
                    client.Parameters.Add("update", "");
502

    
503
                    foreach (var tag in tags)
504
                    {
505
                        var headerTag = String.Format("X-Object-Meta-{0}", tag.Key);
506
                        client.Headers.Add(headerTag, tag.Value);
507
                    }
508
                    
509
                    client.DownloadStringWithRetryRelative(target.Container, 3);
510

    
511
                    
512
                    client.AssertStatusOK("SetTags failed");
513
                    //If the status is NOT ACCEPTED we have a problem
514
                    if (client.StatusCode != HttpStatusCode.Accepted)
515
                    {
516
                        Log.Error("Failed to set tags");
517
                        throw new Exception("Failed to set tags");
518
                    }
519

    
520
                    if (Log.IsDebugEnabled) Log.DebugFormat("END");
521
                }
522
            }
523

    
524

    
525
        }
526

    
527
        public void ShareObject(string account, Uri container, Uri objectName, string shareTo, bool read, bool write)
528
        {
529

    
530
            Contract.Requires<InvalidOperationException>(!String.IsNullOrWhiteSpace(Token), "The Token is not set");
531
            Contract.Requires<InvalidOperationException>(StorageUrl != null, "The StorageUrl is not set");
532
            Contract.Requires<ArgumentNullException>(container != null, "container is null");
533
            Contract.Requires<ArgumentException>(!container.IsAbsoluteUri, "container is absolute");
534
            Contract.Requires<ArgumentNullException>(objectName != null, "objectName is null");
535
            Contract.Requires<ArgumentException>(!objectName.IsAbsoluteUri, "objectName  is absolute");
536
            Contract.Requires<ArgumentNullException>(!String.IsNullOrWhiteSpace(account), "account is not set");
537
            Contract.Requires<ArgumentNullException>(!String.IsNullOrWhiteSpace(shareTo), "shareTo is not set");
538
            Contract.EndContractBlock();
539

    
540
            using (ThreadContext.Stacks["Share"].Push("Share Object"))
541
            {
542
                if (Log.IsDebugEnabled) Log.DebugFormat("START");
543
                
544
                using (var client = new RestClient(_baseClient))
545
                {
546

    
547
                    client.BaseAddress = GetAccountUrl(account);
548

    
549
                    client.Parameters.Clear();
550
                    client.Parameters.Add("format", "json");
551

    
552
                    string permission = "";
553
                    if (write)
554
                        permission = String.Format("write={0}", shareTo);
555
                    else if (read)
556
                        permission = String.Format("read={0}", shareTo);
557
                    client.Headers.Add("X-Object-Sharing", permission);
558

    
559
                    var content = client.DownloadStringWithRetryRelative(container, 3);
560

    
561
                    client.AssertStatusOK("ShareObject failed");
562

    
563
                    //If the result is empty, return an empty list,
564
                    var infos = String.IsNullOrWhiteSpace(content)
565
                                    ? new List<ObjectInfo>()
566
                                //Otherwise deserialize the object list into a list of ObjectInfos
567
                                    : JsonConvert.DeserializeObject<IList<ObjectInfo>>(content);
568

    
569
                    if (Log.IsDebugEnabled) Log.DebugFormat("END");
570
                }
571
            }
572

    
573

    
574
        }
575

    
576
        public async Task<AccountInfo> GetAccountPolicies(AccountInfo accountInfo)
577
        {
578
            if (accountInfo==null)
579
                throw new ArgumentNullException("accountInfo");
580
            Contract.EndContractBlock();
581

    
582
            using (ThreadContext.Stacks["Account"].Push("GetPolicies"))
583
            {
584
                if (Log.IsDebugEnabled) Log.DebugFormat("START");
585

    
586
/*
587
                if (_baseClient == null)
588
                {
589
                    _baseClient = new RestClient
590
                    {
591
                        BaseAddress = accountInfo.StorageUri.ToString(),
592
                        Timeout = 30000,
593
                        Retries = 3,
594
                    };
595
                }
596

    
597
*/                
598
                var containerUri = GetTargetUri(accountInfo.UserName);
599
                var targetUri = new Uri(String.Format("{0}?format=json", containerUri), UriKind.Absolute);
600
                using(var response=await _baseHttpClient.HeadAsyncWithRetries(targetUri,3).ConfigureAwait(false))
601
                {
602
                    
603
                    var quotaValue=response.Headers.GetFirstValue("X-Account-Policy-Quota");
604
                    var bytesValue = response.Headers.GetFirstValue("X-Account-Bytes-Used");
605
                    long quota, bytes;
606
                    if (long.TryParse(quotaValue, out quota))
607
                        accountInfo.Quota = quota;
608
                    if (long.TryParse(bytesValue, out bytes))
609
                        accountInfo.BytesUsed = bytes;
610

    
611
                    return accountInfo;   
612
                }
613

    
614

    
615
                //using (var client = new RestClient(_baseClient))
616
                //{
617
                //    if (!String.IsNullOrWhiteSpace(accountInfo.UserName))
618
                //        client.BaseAddress = GetAccountUrl(accountInfo.UserName);
619

    
620
                //    client.Parameters.Clear();
621
                //    client.Parameters.Add("format", "json");                    
622
                //    client.Head(_emptyUri, 3);
623

    
624
                //    var quotaValue=client.ResponseHeaders["X-Account-Policy-Quota"];
625
                //    var bytesValue= client.ResponseHeaders["X-Account-Bytes-Used"];
626

    
627
                //    long quota, bytes;
628
                //    if (long.TryParse(quotaValue, out quota))
629
                //        accountInfo.Quota = quota;
630
                //    if (long.TryParse(bytesValue, out bytes))
631
                //        accountInfo.BytesUsed = bytes;
632
                    
633
                //    return accountInfo;
634

    
635
                //}
636

    
637
            }
638
        }
639

    
640
        public void UpdateMetadata(ObjectInfo objectInfo)
641
        {
642
            Contract.Requires<ArgumentNullException>(objectInfo != null,"objectInfo is null");
643
            Contract.EndContractBlock();
644

    
645
            using (ThreadContext.Stacks["Objects"].Push("UpdateMetadata"))
646
            {
647
                if (Log.IsDebugEnabled) Log.DebugFormat("START");
648

    
649

    
650
                using(var client=new RestClient(_baseClient))
651
                {
652

    
653
                    client.BaseAddress = GetAccountUrl(objectInfo.Account);
654
                    
655
                    client.Parameters.Clear();
656
                    
657

    
658
                    //Set Tags
659
                    foreach (var tag in objectInfo.Tags)
660
                    {
661
                        var headerTag = String.Format("X-Object-Meta-{0}", tag.Key);
662
                        client.Headers.Add(headerTag, tag.Value);
663
                    }
664

    
665
                    //Set Permissions
666

    
667
                    var permissions=objectInfo.GetPermissionString();
668
                    client.SetNonEmptyHeaderValue("X-Object-Sharing",permissions);
669

    
670
                    client.SetNonEmptyHeaderValue("Content-Disposition",objectInfo.ContendDisposition);
671
                    client.SetNonEmptyHeaderValue("Content-Encoding",objectInfo.ContentEncoding);
672
                    client.SetNonEmptyHeaderValue("X-Object-Manifest",objectInfo.Manifest);
673
                    var isPublic = objectInfo.IsPublic.ToString().ToLower();
674
                    client.Headers.Add("X-Object-Public", isPublic);
675

    
676

    
677
                    var address = String.Format("{0}/{1}?update=",objectInfo.Container, objectInfo.Name);
678
                    client.PostWithRetry(new Uri(address,UriKind.Relative),"application/xml");
679
                    
680
                    client.AssertStatusOK("UpdateMetadata failed");
681
                    //If the status is NOT ACCEPTED or OK we have a problem
682
                    if (!(client.StatusCode == HttpStatusCode.Accepted || client.StatusCode == HttpStatusCode.OK))
683
                    {
684
                        Log.Error("Failed to update metadata");
685
                        throw new Exception("Failed to update metadata");
686
                    }
687

    
688
                    if (Log.IsDebugEnabled) Log.DebugFormat("END");
689
                }
690
            }
691

    
692
        }
693

    
694
        public void UpdateMetadata(ContainerInfo containerInfo)
695
        {
696
            if (containerInfo == null)
697
                throw new ArgumentNullException("containerInfo");
698
            Contract.EndContractBlock();
699

    
700
            using (ThreadContext.Stacks["Containers"].Push("UpdateMetadata"))
701
            {
702
                if (Log.IsDebugEnabled) Log.DebugFormat("START");
703

    
704

    
705
                using(var client=new RestClient(_baseClient))
706
                {
707

    
708
                    client.BaseAddress = GetAccountUrl(containerInfo.Account);
709
                    
710
                    client.Parameters.Clear();
711
                    
712

    
713
                    //Set Tags
714
                    foreach (var tag in containerInfo.Tags)
715
                    {
716
                        var headerTag = String.Format("X-Container-Meta-{0}", tag.Key);
717
                        client.Headers.Add(headerTag, tag.Value);
718
                    }
719

    
720
                    
721
                    //Set Policies
722
                    foreach (var policy in containerInfo.Policies)
723
                    {
724
                        var headerPolicy = String.Format("X-Container-Policy-{0}", policy.Key);
725
                        client.Headers.Add(headerPolicy, policy.Value);
726
                    }
727

    
728

    
729
                    var uriBuilder = client.GetAddressBuilder(containerInfo.Name,_emptyUri);
730
                    var uri = uriBuilder.Uri;
731

    
732
                    client.UploadValues(uri,new NameValueCollection());
733

    
734

    
735
                    client.AssertStatusOK("UpdateMetadata failed");
736
                    //If the status is NOT ACCEPTED or OK we have a problem
737
                    if (!(client.StatusCode == HttpStatusCode.Accepted || client.StatusCode == HttpStatusCode.OK))
738
                    {
739
                        Log.Error("Failed to update metadata");
740
                        throw new Exception("Failed to update metadata");
741
                    }
742

    
743
                    if (Log.IsDebugEnabled) Log.DebugFormat("END");
744
                }
745
            }
746

    
747
        }
748

    
749
       
750

    
751

    
752
        public IList<ObjectInfo> ListObjects(string account, Uri container, DateTimeOffset? since = null)
753
        {
754
/*
755
            if (container==null)
756
                throw new ArgumentNullException("container");
757
            if (container.IsAbsoluteUri)
758
                throw new ArgumentException("container");
759
            Contract.EndContractBlock();
760
*/
761

    
762
            using (ThreadContext.Stacks["Objects"].Push("List"))
763
            {
764

    
765
                var containerUri = GetTargetUri(account).Combine(container);
766
                var targetUri = new Uri(String.Format("{0}?format=json", containerUri), UriKind.Absolute);
767

    
768
                var content =TaskEx.Run(async ()=>await GetStringAsync(targetUri, "ListObjects failed", since).ConfigureAwait(false)).Result;
769

    
770
                //304 will result in an empty string. Empty containers return an empty json array
771
                if (String.IsNullOrWhiteSpace(content))
772
                     return new[] {new NoModificationInfo(account, container)};
773

    
774
                 //If the result is empty, return an empty list,
775
                 var infos = String.IsNullOrWhiteSpace(content)
776
                                 ? new List<ObjectInfo>()
777
                             //Otherwise deserialize the object list into a list of ObjectInfos
778
                                 : JsonConvert.DeserializeObject<IList<ObjectInfo>>(content);
779

    
780
                 foreach (var info in infos)
781
                 {
782
                     info.Container = container;
783
                     info.Account = account;
784
                     info.StorageUri = StorageUrl;
785
                 }
786
                 if (Log.IsDebugEnabled) Log.DebugFormat("END");
787
                 return infos;
788
            }
789
        }
790

    
791
        public IList<ObjectInfo> ListObjects(string account, Uri container, Uri folder, DateTimeOffset? since = null)
792
        {
793
/*            if (container==null)
794
                throw new ArgumentNullException("container");
795
            if (container.IsAbsoluteUri)
796
                throw new ArgumentException("container");
797
            Contract.EndContractBlock();*/
798

    
799
            using (ThreadContext.Stacks["Objects"].Push("List"))
800
            {
801
                if (Log.IsDebugEnabled) Log.DebugFormat("START");
802

    
803
                var containerUri = GetTargetUri(account).Combine(container);
804
                var targetUri = new Uri(String.Format("{0}?format=json&path={1}", containerUri,folder), UriKind.Absolute);
805
                var content = TaskEx.Run(async ()=>await GetStringAsync(targetUri, "ListObjects failed", since).ConfigureAwait(false)).Result;                
806

    
807
                //304 will result in an empty string. Empty containers return an empty json array
808
                if (String.IsNullOrWhiteSpace(content))
809
                    return new[] { new NoModificationInfo(account, container) };
810

    
811

    
812
                var infos = JsonConvert.DeserializeObject<IList<ObjectInfo>>(content);
813
                foreach (var info in infos)
814
                {
815
                    info.Account = account;
816
                    if (info.Container == null)
817
                        info.Container = container;
818
                    info.StorageUri = StorageUrl;
819
                }
820
                if (Log.IsDebugEnabled) Log.DebugFormat("END");
821
                return infos;
822
/*
823
                using (var client = new RestClient(_baseClient))
824
                {
825
                    if (!String.IsNullOrWhiteSpace(account))
826
                        client.BaseAddress = GetAccountUrl(account);
827

    
828
                    client.Parameters.Clear();
829
                    client.Parameters.Add("format", "json");
830
                    client.Parameters.Add("path", folder.ToString());
831
                    client.IfModifiedSince = since;
832
                    var content = client.DownloadStringWithRetryRelative(container, 3);
833
                    client.AssertStatusOK("ListObjects failed");
834

    
835
                    if (client.StatusCode==HttpStatusCode.NotModified)
836
                        return new[]{new NoModificationInfo(account,container,folder)};
837

    
838
                    var infos = JsonConvert.DeserializeObject<IList<ObjectInfo>>(content);
839
                    foreach (var info in infos)
840
                    {
841
                        info.Account = account;
842
                        if (info.Container == null)
843
                            info.Container = container;
844
                        info.StorageUri = StorageUrl;
845
                    }
846
                    if (Log.IsDebugEnabled) Log.DebugFormat("END");
847
                    return infos;
848
                }
849
*/
850
            }
851
        }
852

    
853
 
854
        public async Task<bool> ContainerExists(string account, Uri container)
855
        {
856
            if (container==null)
857
                throw new ArgumentNullException("container", "The container property can't be empty");
858
            if (container.IsAbsoluteUri)
859
                throw new ArgumentException( "The container must be relative","container");
860
            Contract.EndContractBlock();
861

    
862
            using (ThreadContext.Stacks["Containters"].Push("Exists"))
863
            {
864
                if (Log.IsDebugEnabled) Log.DebugFormat("START");
865

    
866
                var targetUri = GetTargetUri(account).Combine(container);
867

    
868
                using (var response =await _baseHttpClient.HeadAsyncWithRetries(targetUri, 3))
869
                {
870

    
871
                    bool result;
872
                    switch (response.StatusCode)
873
                    {
874
                        case HttpStatusCode.OK:
875
                        case HttpStatusCode.NoContent:
876
                            result = true;
877
                            break;
878
                        case HttpStatusCode.NotFound:
879
                            result = false;
880
                            break;
881
                        default:
882
                            throw CreateWebException("ContainerExists", response.StatusCode);
883
                    }
884
                    if (Log.IsDebugEnabled) Log.DebugFormat("END");
885

    
886
                    return result;
887
                }
888

    
889
            }
890
        }
891

    
892
        private Uri GetTargetUri(string account)
893
        {
894
            return new Uri(GetTargetUrl(account),UriKind.Absolute);
895
        }
896

    
897
        private string GetTargetUrl(string account)
898
        {
899
            return String.IsNullOrWhiteSpace(account)
900
                       ? _baseHttpClient.BaseAddress.ToString()
901
                       : GetAccountUrl(account);
902
        }
903

    
904
        public async Task<bool> ObjectExists(string account, Uri container, Uri objectName)
905
        {
906
            if (container == null)
907
                throw new ArgumentNullException("container", "The container property can't be empty");
908
            if (container.IsAbsoluteUri)
909
                throw new ArgumentException("The container must be relative","container");
910
            if (objectName == null)
911
                throw new ArgumentNullException("objectName", "The objectName property can't be empty");
912
            if (objectName.IsAbsoluteUri)
913
                throw new ArgumentException("The objectName must be relative","objectName");
914
            Contract.EndContractBlock();
915

    
916
                var targetUri=GetTargetUri(account).Combine(container).Combine(objectName);
917

    
918
            using (var response = await _baseHttpClient.HeadAsyncWithRetries(targetUri, 3).ConfigureAwait(false))
919
            {
920
                switch (response.StatusCode)
921
                {
922
                    case HttpStatusCode.OK:
923
                    case HttpStatusCode.NoContent:
924
                        return true;
925
                    case HttpStatusCode.NotFound:
926
                        return false;
927
                    default:
928
                        throw CreateWebException("ObjectExists", response.StatusCode);
929
                }
930
            }
931
        }
932

    
933
        public async Task<ObjectInfo> GetObjectInfo(string account, Uri container, Uri objectName)
934
        {
935
            if (container == null)
936
                throw new ArgumentNullException("container", "The container property can't be empty");
937
            if (container.IsAbsoluteUri)
938
                throw new ArgumentException("The container must be relative", "container");
939
            if (objectName == null)
940
                throw new ArgumentNullException("objectName", "The objectName property can't be empty");
941
            if (objectName.IsAbsoluteUri)
942
                throw new ArgumentException("The objectName must be relative", "objectName");
943
            Contract.EndContractBlock();
944

    
945
            using (ThreadContext.Stacks["Objects"].Push("GetObjectInfo"))
946
            {
947

    
948
                var targetUri = GetTargetUri(account).Combine(container).Combine(objectName);
949
                try
950
                {
951
                    using (var response = await _baseHttpClient.HeadAsyncWithRetries(targetUri, 3,true))
952
                    {
953
                        switch (response.StatusCode)
954
                        {
955
                            case HttpStatusCode.OK:
956
                            case HttpStatusCode.NoContent:
957
                                var tags = response.Headers.GetMeta("X-Object-Meta-");
958
                                var extensions = (from header in response.Headers
959
                                                  where
960
                                                      header.Key.StartsWith("X-Object-") &&
961
                                                      !header.Key.StartsWith("X-Object-Meta-")
962
                                                  select new {Name = header.Key, Value = header.Value.FirstOrDefault()})
963
                                    .ToDictionary(t => t.Name, t => t.Value);
964

    
965
                                var permissions = response.Headers.GetFirstValue("X-Object-Sharing");
966

    
967

    
968
                                var info = new ObjectInfo
969
                                               {
970
                                                   Account = account,
971
                                                   Container = container,
972
                                                   Name = objectName,
973
                                                   ETag = response.Headers.ETag.NullSafe(e=>e.Tag),
974
                                                   UUID = response.Headers.GetFirstValue("X-Object-UUID"),
975
                                                   X_Object_Hash = response.Headers.GetFirstValue("X-Object-Hash"),
976
                                                   Content_Type = response.Headers.GetFirstValue("Content-Type"),
977
                                                   Bytes = Convert.ToInt64(response.Content.Headers.ContentLength),
978
                                                   Tags = tags,
979
                                                   Last_Modified = response.Content.Headers.LastModified,
980
                                                   Extensions = extensions,
981
                                                   ContentEncoding =
982
                                                       response.Content.Headers.ContentEncoding.FirstOrDefault(),
983
                                                   ContendDisposition =
984
                                                       response.Content.Headers.ContentDisposition.NullSafe(c=>c.ToString()),
985
                                                   Manifest = response.Headers.GetFirstValue("X-Object-Manifest"),
986
                                                   PublicUrl = response.Headers.GetFirstValue("X-Object-Public"),
987
                                                   StorageUri = StorageUrl,
988
                                               };
989
                                info.SetPermissions(permissions);
990
                                return info;
991
                            case HttpStatusCode.NotFound:
992
                                return ObjectInfo.Empty;
993
                            default:
994
                                throw new WebException(
995
                                    String.Format("[FAIL] GetObjectInfo for {0} failed with unexpected status code {1}",
996
                                                  objectName, response.StatusCode));
997
                        }
998
                    }
999
                }
1000
                catch (RetryException)
1001
                {
1002
                    Log.WarnFormat("[RETRY FAIL] GetObjectInfo for {0} failed.", objectName);
1003
                    return ObjectInfo.Empty;
1004
                }
1005
                catch (WebException e)
1006
                {
1007
                    Log.Error(
1008
                        String.Format("[FAIL] GetObjectInfo for {0} failed with unexpected status {1}",
1009
                                      objectName, e.Status), e);
1010
                    throw;
1011
                }
1012
            }
1013
        }
1014

    
1015

    
1016

    
1017
        public async Task CreateFolder(string account, Uri container, Uri folder)
1018
        {
1019
            if (container == null)
1020
                throw new ArgumentNullException("container", "The container property can't be empty");
1021
            if (container.IsAbsoluteUri)
1022
                throw new ArgumentException("The container must be relative","container");
1023
            if (folder == null)
1024
                throw new ArgumentNullException("folder", "The objectName property can't be empty");
1025
            if (folder.IsAbsoluteUri)
1026
                throw new ArgumentException("The objectName must be relative","folder");
1027
            Contract.EndContractBlock();
1028

    
1029
            var folderUri=container.Combine(folder);            
1030
            var targetUri = GetTargetUri(account).Combine(folderUri);
1031
            var message = new HttpRequestMessage(HttpMethod.Put, targetUri);
1032
            message.Content=new StringContent("");
1033
            message.Content.Headers.ContentType = new MediaTypeHeaderValue(ObjectInfo.CONTENT_TYPE_DIRECTORY);
1034
            //message.Headers.Add("Content-Length", "0");
1035
            using (var response = await _baseHttpClient.SendAsyncWithRetries(message, 3).ConfigureAwait(false))
1036
            {
1037
                if (response.StatusCode != HttpStatusCode.Created && response.StatusCode != HttpStatusCode.Accepted)
1038
                    throw CreateWebException("CreateFolder", response.StatusCode);
1039
            }
1040
        }
1041

    
1042
        private Dictionary<string, string> GetMeta(HttpResponseMessage response,string metaPrefix)
1043
        {
1044
            Contract.Requires<ArgumentNullException>(!String.IsNullOrWhiteSpace(metaPrefix), "metaPrefix is empty");
1045
            Contract.EndContractBlock();
1046

    
1047
            var dict = (from header in response.Headers
1048
                        where header.Key.StartsWith(metaPrefix)
1049
                         select new { Name = header.Key, Value = String.Join(",", header.Value) })
1050
                        .ToDictionary(t => t.Name, t => t.Value);
1051

    
1052
          
1053
            return dict;
1054
        }
1055

    
1056

    
1057
        public async Task<ContainerInfo> GetContainerInfo(string account, Uri container)
1058
        {
1059
            if (container == null)
1060
                throw new ArgumentNullException("container", "The container property can't be empty");
1061
            if (container.IsAbsoluteUri)
1062
                throw new ArgumentException("The container must be relative","container");
1063
            Contract.EndContractBlock();
1064

    
1065
            var targetUri = GetTargetUri(account).Combine(container);            
1066
            using (var response = await _baseHttpClient.HeadAsyncWithRetries(targetUri, 3,true).ConfigureAwait(false))
1067
            {
1068
                if (Log.IsDebugEnabled)
1069
                    Log.DebugFormat("ContainerInfo data: {0}\n{1}",response,await response.Content.ReadAsStringAsync().ConfigureAwait(false));
1070
                switch (response.StatusCode)
1071
                {
1072
                    case HttpStatusCode.OK:
1073
                    case HttpStatusCode.NoContent:
1074
                        var tags = GetMeta(response,"X-Container-Meta-");
1075
                        var policies = GetMeta(response,"X-Container-Policy-");
1076

    
1077
                        var containerInfo = new ContainerInfo
1078
                                                {
1079
                                                    Account = account,
1080
                                                    Name = container,
1081
                                                    StorageUrl = StorageUrl.ToString(),
1082
                                                    Count =long.Parse(response.Headers.GetFirstValue("X-Container-Object-Count")),
1083
                                                    Bytes = long.Parse(response.Headers.GetFirstValue("X-Container-Bytes-Used")),
1084
                                                    BlockHash = response.Headers.GetFirstValue("X-Container-Block-Hash"),
1085
                                                    BlockSize =
1086
                                                        int.Parse(response.Headers.GetFirstValue("X-Container-Block-Size")),
1087
                                                    Last_Modified = response.Content.Headers.LastModified,
1088
                                                    Tags = tags,
1089
                                                    Policies = policies
1090
                                                };
1091

    
1092

    
1093
                        return containerInfo;
1094
                    case HttpStatusCode.NotFound:
1095
                        return ContainerInfo.Empty;
1096
                    default:
1097
                        throw CreateWebException("GetContainerInfo", response.StatusCode);
1098
                }
1099
            }            
1100
        }
1101

    
1102
        public async Task CreateContainer(string account, Uri container)
1103
        {
1104
            if (container == null)
1105
                throw new ArgumentNullException("container", "The container property can't be empty");
1106
            if (container.IsAbsoluteUri)
1107
                throw new ArgumentException("The container must be relative","container");
1108
            Contract.EndContractBlock();
1109

    
1110
            var targetUri=GetTargetUri(account).Combine(container);
1111
            var message = new HttpRequestMessage(HttpMethod.Put, targetUri);
1112
            
1113
            //message.Content.Headers.ContentLength = 0;
1114
            using (var response =await _baseHttpClient.SendAsyncWithRetries(message, 3).ConfigureAwait(false))
1115
            {            
1116
                var expectedCodes = new[] {HttpStatusCode.Created, HttpStatusCode.Accepted, HttpStatusCode.OK};
1117
                if (!expectedCodes.Contains(response.StatusCode))
1118
                    throw CreateWebException("CreateContainer", response.StatusCode);
1119
            }
1120
        }
1121

    
1122
        public async Task WipeContainer(string account, Uri container)
1123
        {
1124
            if (container == null)
1125
                throw new ArgumentNullException("container", "The container property can't be empty");
1126
            if (container.IsAbsoluteUri)
1127
                throw new ArgumentException("The container must be relative", "container");
1128
            Contract.EndContractBlock();
1129

    
1130
            await DeleteContainer(account, new Uri(String.Format("{0}?delimiter=/", container), UriKind.Relative)).ConfigureAwait(false);
1131
        }
1132

    
1133

    
1134
        public async Task DeleteContainer(string account, Uri container)
1135
        {
1136
            if (container == null)
1137
                throw new ArgumentNullException("container", "The container property can't be empty");
1138
            if (container.IsAbsoluteUri)
1139
                throw new ArgumentException("The container must be relative","container");
1140
            Contract.EndContractBlock();
1141

    
1142
            var targetUri = GetTargetUri(account).Combine(container);
1143
            var message = new HttpRequestMessage(HttpMethod.Delete, targetUri);
1144
            using (var response = await _baseHttpClient.SendAsyncWithRetries(message, 3).ConfigureAwait(false))
1145
            {
1146
                var expectedCodes = new[] { HttpStatusCode.NotFound, HttpStatusCode.NoContent };
1147
                if (!expectedCodes.Contains(response.StatusCode))
1148
                    throw CreateWebException("DeleteContainer", response.StatusCode);
1149
            }
1150

    
1151
        }
1152

    
1153
        /// <summary>
1154
        /// 
1155
        /// </summary>
1156
        /// <param name="account"></param>
1157
        /// <param name="container"></param>
1158
        /// <param name="objectName"></param>
1159
        /// <param name="fileName"></param>
1160
        /// <param name="cancellationToken"> </param>
1161
        /// <returns></returns>
1162
        /// <remarks>This method should have no timeout or a very long one</remarks>
1163
        //Asynchronously download the object specified by *objectName* in a specific *container* to 
1164
        // a local file
1165
        public async Task GetObject(string account, Uri container, Uri objectName, string fileName,CancellationToken cancellationToken)
1166
        {
1167
            if (container == null)
1168
                throw new ArgumentNullException("container", "The container property can't be empty");
1169
            if (container.IsAbsoluteUri)
1170
                throw new ArgumentException("The container must be relative","container");
1171
            if (objectName == null)
1172
                throw new ArgumentNullException("objectName", "The objectName property can't be empty");
1173
            if (objectName.IsAbsoluteUri)
1174
                throw new ArgumentException("The objectName must be relative","objectName");
1175
            Contract.EndContractBlock();
1176
                        
1177

    
1178
            try
1179
            {
1180
                //WebClient, and by extension RestClient, are not thread-safe. Create a new RestClient
1181
                //object to avoid concurrency errors.
1182
                //
1183
                //Download operations take a long time therefore they have no timeout.
1184
                using(var client = new RestClient(_baseClient) { Timeout = 0 })
1185
                {
1186
                    if (!String.IsNullOrWhiteSpace(account))
1187
                        client.BaseAddress = GetAccountUrl(account);
1188

    
1189
                    //The container and objectName are relative names. They are joined with the client's
1190
                    //BaseAddress to create the object's absolute address
1191
                    var builder = client.GetAddressBuilder(container, objectName);
1192
                    var uri = builder.Uri;
1193

    
1194
                    //Download progress is reported to the Trace log
1195
                    Log.InfoFormat("[GET] START {0}", objectName);
1196
                    /*client.DownloadProgressChanged += (sender, args) =>
1197
                                                      Log.InfoFormat("[GET PROGRESS] {0} {1}% {2} of {3}",
1198
                                                                     fileName, args.ProgressPercentage,
1199
                                                                     args.BytesReceived,
1200
                                                                     args.TotalBytesToReceive);*/
1201
                    var progress = new Progress<DownloadProgressChangedEventArgs>(args =>
1202
                                {
1203
                                    Log.InfoFormat("[GET PROGRESS] {0} {1}% {2} of {3}",
1204
                                                   fileName, args.ProgressPercentage,
1205
                                                   args.BytesReceived,
1206
                                                   args.TotalBytesToReceive);
1207
                                    if (DownloadProgressChanged!=null)
1208
                                        DownloadProgressChanged(this, new DownloadArgs(args));
1209
                                });
1210
                    
1211
                    //Start downloading the object asynchronously                    
1212
                    await client.DownloadFileTaskAsync(uri, fileName, cancellationToken,progress).ConfigureAwait(false);
1213

    
1214
                    //Once the download completes
1215
                    //Delete the local client object
1216
                }
1217
                //And report failure or completion
1218
            }
1219
            catch (Exception exc)
1220
            {
1221
                Log.ErrorFormat("[GET] FAIL {0} with {1}", objectName, exc);
1222
                throw;
1223
            }
1224

    
1225
            Log.InfoFormat("[GET] END {0}", objectName);                                             
1226

    
1227

    
1228
        }
1229

    
1230
        public async Task<IList<string>> PutHashMap(string account, Uri container, Uri objectName, TreeHash hash)
1231
        {
1232
            if (container == null)
1233
                throw new ArgumentNullException("container", "The container property can't be empty");
1234
            if (container.IsAbsoluteUri)
1235
                throw new ArgumentException("The container must be relative","container");
1236
            if (objectName == null)
1237
                throw new ArgumentNullException("objectName", "The objectName property can't be empty");
1238
            if (objectName.IsAbsoluteUri)
1239
                throw new ArgumentException("The objectName must be relative","objectName");
1240
            if (hash == null)
1241
                throw new ArgumentNullException("hash");
1242
            if (String.IsNullOrWhiteSpace(Token))
1243
                throw new InvalidOperationException("Invalid Token");
1244
            if (StorageUrl == null)
1245
                throw new InvalidOperationException("Invalid Storage Url");
1246
            Contract.EndContractBlock();
1247

    
1248
            
1249

    
1250
            //The container and objectName are relative names. They are joined with the client's
1251
            //BaseAddress to create the object's absolute address
1252

    
1253
            var targetUri = GetTargetUri(account).Combine(container).Combine(objectName);
1254
  
1255

    
1256
            var uri = new Uri(String.Format("{0}?format=json&hashmap",targetUri),UriKind.Absolute);
1257

    
1258
            
1259
            //Send the tree hash as Json to the server            
1260
            var jsonHash = hash.ToJson();
1261
            if (Log.IsDebugEnabled)
1262
                Log.DebugFormat("Hashes:\r\n{0}", jsonHash);
1263

    
1264
            var mimeType = objectName.GetMimeType();
1265

    
1266
            var message = new HttpRequestMessage(HttpMethod.Put, uri)
1267
            {
1268
                Content = new StringContent(jsonHash)
1269
            };
1270
            message.Content.Headers.ContentType = mimeType;
1271
            message.Headers.Add("ETag",hash.TopHash.ToHashString());
1272
            
1273
            
1274
            //Don't use a timeout because putting the hashmap may be a long process
1275

    
1276
            using (var response = await _baseHttpClientNoTimeout.SendAsyncWithRetries(message, 3).ConfigureAwait(false))
1277
            {
1278
                var empty = (IList<string>)new List<string>();
1279
                
1280
                switch (response.StatusCode)
1281
                {
1282
                    case HttpStatusCode.Created:
1283
                        //The server will respond either with 201-created if all blocks were already on the server
1284
                        return empty;
1285
                    case HttpStatusCode.Conflict:
1286
                        //or with a 409-conflict and return the list of missing parts
1287
                        using (var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false))
1288
                        using(var reader=stream.GetLoggedReader(Log))
1289
                        {                            
1290
                            var serializer = new JsonSerializer();                            
1291
                            serializer.Error += (sender, args) => Log.ErrorFormat("Deserialization error at [{0}] [{1}]", args.ErrorContext.Error, args.ErrorContext.Member);
1292
                            var hashes = (List<string>)serializer.Deserialize(reader, typeof(List<string>));
1293
                            return hashes;
1294
                        }                        
1295
                    default:
1296
                        //All other cases are unexpected
1297
                        //Ensure that failure codes raise exceptions
1298
                        response.EnsureSuccessStatusCode();
1299
                        //And log any other codes as warngings, but continute processing
1300
                        Log.WarnFormat("Unexcpected status code when putting map: {0} - {1}",response.StatusCode,response.ReasonPhrase);
1301
                        return empty;
1302
                }
1303
            }
1304

    
1305
        }
1306

    
1307

    
1308
        public async Task<byte[]> GetBlock(string account, Uri container, Uri relativeUrl, long start, long? end, CancellationToken cancellationToken)
1309
        {
1310
            if (String.IsNullOrWhiteSpace(Token))
1311
                throw new InvalidOperationException("Invalid Token");
1312
            if (StorageUrl == null)
1313
                throw new InvalidOperationException("Invalid Storage Url");
1314
            if (container == null)
1315
                throw new ArgumentNullException("container", "The container property can't be empty");
1316
            if (container.IsAbsoluteUri)
1317
                throw new ArgumentException("The container must be relative","container");
1318
            if (relativeUrl == null)
1319
                throw new ArgumentNullException("relativeUrl");
1320
            if (end.HasValue && end < 0)
1321
                throw new ArgumentOutOfRangeException("end");
1322
            if (start < 0)
1323
                throw new ArgumentOutOfRangeException("start");
1324
            Contract.EndContractBlock();
1325

    
1326

    
1327
            var targetUri = GetTargetUri(account).Combine(container).Combine(relativeUrl);
1328
            var message = new HttpRequestMessage(HttpMethod.Get, targetUri);
1329
            //Don't add a range if start=0, end=null (empty files)
1330
            if (start!=0 || end!=null)
1331
                message.Headers.Range=new RangeHeaderValue(start,end);
1332

    
1333
            //Don't use a timeout because putting the hashmap may be a long process
1334

    
1335
            IProgress<DownloadArgs> progress = new Progress<DownloadArgs>(args =>
1336
                {
1337
                    Log.DebugFormat("[GET PROGRESS] {0} {1}% {2} of {3}",
1338
                                    targetUri.Segments.Last(), args.ProgressPercentage,
1339
                                    args.BytesReceived,
1340
                                    args.TotalBytesToReceive);
1341

    
1342
                    if (DownloadProgressChanged!=null)
1343
                        DownloadProgressChanged(this,  args);
1344
                });
1345

    
1346

    
1347
            using (var response = await _baseHttpClientNoTimeout.SendAsyncWithRetries(message, 3, false,HttpCompletionOption.ResponseHeadersRead,
1348
                                                          cancellationToken).ConfigureAwait(false))
1349
            using (var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false))
1350
            {
1351
                long totalSize = response.Content.Headers.ContentLength ?? 0;
1352
                    byte[] buffer,streambuf;
1353
                    lock (_bufferManager)
1354
                    {
1355
                        buffer = _bufferManager.TakeBuffer(65536);
1356
                        streambuf = _bufferManager.TakeBuffer((int)totalSize);
1357
                    }
1358

    
1359
                using (var targetStream = new MemoryStream(streambuf))
1360
                {
1361

    
1362
                    long total = 0;
1363
                    try
1364
                    {
1365

    
1366
                        int read;
1367
                        while ((read = await stream.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false)) != 0)
1368
                        {
1369
                            total += read;
1370
                            progress.Report(new DownloadArgs(total, totalSize));
1371
                            await targetStream.WriteAsync(buffer, 0, read).ConfigureAwait(false);
1372
                        }
1373
                    }
1374
                    finally
1375
                    {
1376
                        lock (_bufferManager)
1377
                        {
1378
                            _bufferManager.ReturnBuffer(buffer);
1379
                            _bufferManager.ReturnBuffer(streambuf);
1380
                        }
1381
                    }
1382
                    var result = targetStream.ToArray();
1383
                    return result;
1384
                }
1385
            }
1386

    
1387
        }
1388

    
1389
        public event EventHandler<UploadArgs> UploadProgressChanged;
1390
        public event EventHandler<DownloadArgs> DownloadProgressChanged;
1391
        
1392

    
1393
        public async Task PostBlock(string account, Uri container, byte[] block, int offset, int count,string blockHash,CancellationToken token)
1394
        {
1395
            if (container == null)
1396
                throw new ArgumentNullException("container", "The container property can't be empty");
1397
            if (container.IsAbsoluteUri)
1398
                throw new ArgumentException("The container must be relative","container");
1399
            if (block == null)
1400
                throw new ArgumentNullException("block");
1401
            if (offset < 0 || offset >= block.Length)
1402
                throw new ArgumentOutOfRangeException("offset");
1403
            if (count < 0 || count > block.Length)
1404
                throw new ArgumentOutOfRangeException("count");
1405
            if (String.IsNullOrWhiteSpace(Token))
1406
                throw new InvalidOperationException("Invalid Token");
1407
            if (StorageUrl == null)
1408
                throw new InvalidOperationException("Invalid Storage Url");                        
1409
            Contract.EndContractBlock();
1410

    
1411

    
1412
            try
1413
            {
1414
                var containerUri = GetTargetUri(account).Combine(container);
1415
                var targetUri = new Uri(String.Format("{0}?update", containerUri));
1416

    
1417

    
1418
                //Don't use a timeout because putting the hashmap may be a long process
1419

    
1420

    
1421
                Log.InfoFormat("[BLOCK POST] START");
1422

    
1423

    
1424
                var progress = new Progress<UploadArgs>(args =>
1425
                {
1426
                    Log.InfoFormat("[BLOCK POST PROGRESS] {0}% {1} of {2}",
1427
                        args.ProgressPercentage,
1428
                        args.BytesSent,
1429
                        args.TotalBytesToSend);
1430
                    if (UploadProgressChanged != null)
1431
                        UploadProgressChanged(this,args);
1432
                });
1433

    
1434
                var message = new HttpRequestMessage(HttpMethod.Post, targetUri)
1435
                                  {
1436
                                      Content = new ByteArrayContentWithProgress(block, offset, count,progress)
1437
                                  };
1438
                message.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(@"application/octet-stream");
1439

    
1440
                //Send the block
1441
                using (var response = await _baseHttpClientNoTimeout.SendAsyncWithRetries(message, 3,false,HttpCompletionOption.ResponseContentRead,token).ConfigureAwait(false))
1442
                {                    
1443
                    Log.InfoFormat("[BLOCK POST PROGRESS] Completed ");
1444
                    response.EnsureSuccessStatusCode();
1445
                    var responseHash = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
1446
                    var cleanHash = responseHash.TrimEnd();
1447
                    Debug.Assert(blockHash==cleanHash);
1448
                    if (!blockHash.Equals(cleanHash, StringComparison.OrdinalIgnoreCase))
1449
                        Log.ErrorFormat("Block hash mismatch posting to [{0}]:[{1}], expected [{2}] but was [{3}]", account, container, blockHash, cleanHash);
1450
                }
1451
                Log.InfoFormat("[BLOCK POST] END");               
1452
            }
1453
            catch (TaskCanceledException )
1454
            {
1455
                Log.Info("Aborting block");
1456
                throw;
1457
            }
1458
            catch (Exception exc)
1459
            {
1460
                Log.ErrorFormat("[BLOCK POST] FAIL with \r{0}", exc);
1461
                throw;
1462
            }
1463
        }
1464

    
1465
        public async Task PostBlock(string account, Uri container, string filePath, long offset, int count, string blockHash, CancellationToken token)
1466
        {
1467
            if (container == null)
1468
                throw new ArgumentNullException("container", "The container property can't be empty");
1469
            if (container.IsAbsoluteUri)
1470
                throw new ArgumentException("The container must be relative", "container");
1471
            if (String.IsNullOrWhiteSpace(filePath))
1472
                throw new ArgumentNullException("filePath");
1473
            if (!File.Exists(filePath))
1474
                throw new FileNotFoundException("Missing file","filePath");
1475
            if (String.IsNullOrWhiteSpace(Token))
1476
                throw new InvalidOperationException("Invalid Token");
1477
            if (StorageUrl == null)
1478
                throw new InvalidOperationException("Invalid Storage Url");
1479
            Contract.EndContractBlock();
1480

    
1481

    
1482
            try
1483
            {
1484
                var containerUri = GetTargetUri(account).Combine(container);
1485
                var targetUri = new Uri(String.Format("{0}?update", containerUri));
1486

    
1487

    
1488
                //Don't use a timeout because putting the hashmap may be a long process
1489

    
1490

    
1491
                Log.InfoFormat("[BLOCK POST] START");
1492

    
1493

    
1494
                var progress = new Progress<UploadArgs>(args =>
1495
                {
1496
                    Log.InfoFormat("[BLOCK POST PROGRESS] {0}% {1} of {2} at {3:###,} Kbps ",
1497
                        args.ProgressPercentage,
1498
                        args.BytesSent,
1499
                        args.TotalBytesToSend,args.Speed);
1500
                    if (UploadProgressChanged != null)
1501
                        UploadProgressChanged(this, args);
1502
                });
1503

    
1504
                var message = new HttpRequestMessage(HttpMethod.Post, targetUri)
1505
                {
1506
                    Content = new FileBlockContent(filePath, offset, count, progress)
1507
                };
1508
                message.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(@"application/octet-stream");
1509

    
1510
                //Send the block
1511
                using (var response = await _baseHttpClientNoTimeout.SendAsyncWithRetries(message, 3, false, HttpCompletionOption.ResponseContentRead, token).ConfigureAwait(false))
1512
                {
1513
                    Log.InfoFormat("[BLOCK POST PROGRESS] Completed ");
1514
                    response.EnsureSuccessStatusCode();
1515
                    var responseHash = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
1516
                    var cleanHash = responseHash.TrimEnd();
1517
                    Debug.Assert(blockHash == cleanHash);
1518
                    if (!blockHash.Equals(cleanHash, StringComparison.OrdinalIgnoreCase))
1519
                        Log.ErrorFormat("Block hash mismatch posting to [{0}]:[{1}], expected [{2}] but was [{3}]", account, container, blockHash, cleanHash);
1520
                }
1521
                Log.InfoFormat("[BLOCK POST] END");
1522
            }
1523
            catch (TaskCanceledException)
1524
            {
1525
                Log.Info("Aborting block");
1526
                throw;
1527
            }                
1528
            catch (Exception exc)
1529
            {
1530
                Log.ErrorFormat("[BLOCK POST] FAIL with \r{0}", exc);
1531
                throw;
1532
            }
1533
        }
1534

    
1535

    
1536
        public async Task<TreeHash> GetHashMap(string account, Uri container, Uri objectName)
1537
        {
1538
            if (container == null)
1539
                throw new ArgumentNullException("container", "The container property can't be empty");
1540
            if (container.IsAbsoluteUri)
1541
                throw new ArgumentException("The container must be relative","container");
1542
            if (objectName == null)
1543
                throw new ArgumentNullException("objectName", "The objectName property can't be empty");
1544
            if (objectName.IsAbsoluteUri)
1545
                throw new ArgumentException("The objectName must be relative","objectName");
1546
            if (String.IsNullOrWhiteSpace(Token))
1547
                throw new InvalidOperationException("Invalid Token");
1548
            if (StorageUrl == null)
1549
                throw new InvalidOperationException("Invalid Storage Url");
1550
            Contract.EndContractBlock();
1551

    
1552
            try
1553
            {
1554

    
1555
                var objectUri = GetTargetUri(account).Combine(container).Combine(objectName);
1556
                var targetUri = new Uri(String.Format("{0}?format=json&hashmap", objectUri));
1557

    
1558
                //Start downloading the object asynchronously
1559
                var json = await GetStringAsync(targetUri, "").ConfigureAwait(false);
1560
                var treeHash = TreeHash.Parse(json);
1561
                Log.InfoFormat("[GET HASH] END {0}", objectName);
1562
                return treeHash;
1563

    
1564
            }
1565
            catch (Exception exc)
1566
            {
1567
                Log.ErrorFormat("[GET HASH] END {0} with {1}", objectName, exc);
1568
                throw;
1569
            }
1570

    
1571
        }
1572

    
1573

    
1574
        /// <summary>
1575
        /// 
1576
        /// </summary>
1577
        /// <param name="account"></param>
1578
        /// <param name="container"></param>
1579
        /// <param name="objectName"></param>
1580
        /// <param name="fileName"></param>
1581
        /// <param name="hash">Optional hash value for the file. If no hash is provided, the method calculates a new hash</param>
1582
        /// <param name="contentType"> </param>
1583
        /// <remarks>>This method should have no timeout or a very long one</remarks>
1584
        public async Task PutObject(string account, Uri container, Uri objectName, string fileName, string hash = Signature.MERKLE_EMPTY, string contentType = "application/octet-stream")
1585
        {
1586
            if (container == null)
1587
                throw new ArgumentNullException("container", "The container property can't be empty");
1588
            if (container.IsAbsoluteUri)
1589
                throw new ArgumentException("The container must be relative","container");
1590
            if (objectName == null)
1591
                throw new ArgumentNullException("objectName", "The objectName property can't be empty");
1592
            if (objectName.IsAbsoluteUri)
1593
                throw new ArgumentException("The objectName must be relative","objectName");
1594
            if (String.IsNullOrWhiteSpace(fileName))
1595
                throw new ArgumentNullException("fileName", "The fileName property can't be empty");
1596
            try
1597
            {
1598

    
1599
                using (var client = new RestClient(_baseClient) { Timeout = 0 })
1600
                {
1601
                    if (!String.IsNullOrWhiteSpace(account))
1602
                        client.BaseAddress = GetAccountUrl(account);
1603

    
1604
                    var builder = client.GetAddressBuilder(container, objectName);
1605
                    var uri = builder.Uri;
1606

    
1607
                    string etag = hash ;
1608

    
1609
                    client.Headers.Add("Content-Type", contentType);
1610
                    if (contentType!=ObjectInfo.CONTENT_TYPE_DIRECTORY)
1611
                        client.Headers.Add("ETag", etag);
1612

    
1613

    
1614
                    Log.InfoFormat("[PUT] START {0}", objectName);
1615
                    client.UploadProgressChanged += (sender, args) =>
1616
                                                        {
1617
                                                            using (ThreadContext.Stacks["PUT"].Push("Progress"))
1618
                                                            {
1619
                                                                Log.InfoFormat("{0} {1}% {2} of {3}", fileName,
1620
                                                                               args.ProgressPercentage,
1621
                                                                               args.BytesSent, args.TotalBytesToSend);
1622
                                                            }
1623
                                                        };
1624

    
1625
                    client.UploadFileCompleted += (sender, args) =>
1626
                                                      {
1627
                                                          using (ThreadContext.Stacks["PUT"].Push("Progress"))
1628
                                                          {
1629
                                                              Log.InfoFormat("Completed {0}", fileName);
1630
                                                          }
1631
                                                      }; 
1632
                    
1633
                    if (contentType==ObjectInfo.CONTENT_TYPE_DIRECTORY)
1634
                        await client.UploadDataTaskAsync(uri, "PUT", new byte[0]).ConfigureAwait(false);
1635
                    else
1636
                        await client.UploadFileTaskAsync(uri, "PUT", fileName).ConfigureAwait(false);
1637
                }
1638

    
1639
                Log.InfoFormat("[PUT] END {0}", objectName);
1640
            }
1641
            catch (Exception exc)
1642
            {
1643
                Log.ErrorFormat("[PUT] END {0} with {1}", objectName, exc);
1644
                throw;
1645
            }                
1646

    
1647
        }
1648
        
1649
        public async Task MoveObject(string account, Uri sourceContainer, Uri oldObjectName, Uri targetContainer, Uri newObjectName)
1650
        {
1651
            if (sourceContainer == null)
1652
                throw new ArgumentNullException("sourceContainer", "The sourceContainer property can't be empty");
1653
            if (sourceContainer.IsAbsoluteUri)
1654
                throw new ArgumentException("The sourceContainer must be relative","sourceContainer");
1655
            if (oldObjectName == null)
1656
                throw new ArgumentNullException("oldObjectName", "The oldObjectName property can't be empty");
1657
            if (oldObjectName.IsAbsoluteUri)
1658
                throw new ArgumentException("The oldObjectName must be relative","oldObjectName");
1659
            if (targetContainer == null)
1660
                throw new ArgumentNullException("targetContainer", "The targetContainer property can't be empty");
1661
            if (targetContainer.IsAbsoluteUri)
1662
                throw new ArgumentException("The targetContainer must be relative","targetContainer");
1663
            if (newObjectName == null)
1664
                throw new ArgumentNullException("newObjectName", "The newObjectName property can't be empty");
1665
            if (newObjectName.IsAbsoluteUri)
1666
                throw new ArgumentException("The newObjectName must be relative","newObjectName");
1667
            Contract.EndContractBlock();
1668

    
1669
            var baseUri = GetTargetUri(account);
1670
            var targetUri = baseUri.Combine(targetContainer).Combine(newObjectName);
1671
            var sourceUri = new Uri(String.Format("/{0}/{1}", sourceContainer, oldObjectName),UriKind.Relative);
1672

    
1673
            var message = new HttpRequestMessage(HttpMethod.Put, targetUri);
1674
            message.Headers.Add("X-Move-From", sourceUri.ToString());
1675
            using (var response = await _baseHttpClient.SendAsyncWithRetries(message, 3).ConfigureAwait(false))
1676
            {
1677
                var expectedCodes = new[] {HttpStatusCode.OK, HttpStatusCode.NoContent, HttpStatusCode.Created};
1678
                if (!expectedCodes.Contains(response.StatusCode))
1679
                    throw CreateWebException("MoveObject", response.StatusCode);
1680
            }
1681
        }
1682

    
1683
        public async Task DeleteObject(string account, Uri sourceContainer, Uri objectName, bool isDirectory)
1684
        {
1685
            if (sourceContainer == null)
1686
                throw new ArgumentNullException("sourceContainer", "The sourceContainer property can't be empty");
1687
            if (sourceContainer.IsAbsoluteUri)
1688
                throw new ArgumentException("The sourceContainer must be relative","sourceContainer");
1689
            if (objectName == null)
1690
                throw new ArgumentNullException("objectName", "The objectName property can't be empty");
1691
            if (objectName.IsAbsoluteUri)
1692
                throw new ArgumentException("The objectName must be relative","objectName");
1693
            Contract.EndContractBlock();
1694

    
1695

    
1696

    
1697
            var sourceUri = new Uri(String.Format("/{0}/{1}", sourceContainer, objectName),UriKind.Relative);
1698

    
1699
            
1700
            if (objectName.OriginalString.EndsWith(".ignore"))
1701
                using(var response = await _baseHttpClient.DeleteAsync(sourceUri)){}
1702
            else
1703
            {
1704
                var relativeUri = new Uri(String.Format("{0}/{1}", FolderConstants.TrashContainer, objectName),
1705
                                                UriKind.Relative);
1706

    
1707
/*
1708
                var relativeUri = isDirectory
1709
                                      ? new Uri(
1710
                                            String.Format("{0}/{1}?delimiter=/", FolderConstants.TrashContainer,
1711
                                                          objectName), UriKind.Relative)
1712
                                      : new Uri(String.Format("{0}/{1}", FolderConstants.TrashContainer, objectName),
1713
                                                UriKind.Relative);
1714

    
1715
*/
1716
                var targetUri = GetTargetUri(account).Combine(relativeUri);
1717

    
1718

    
1719
                var message = new HttpRequestMessage(HttpMethod.Put, targetUri);
1720
                message.Headers.Add("X-Move-From", sourceUri.ToString());
1721

    
1722
                Log.InfoFormat("[TRASH] [{0}] to [{1}]", sourceUri, targetUri);
1723
                using (var response = await _baseHttpClient.SendAsyncWithRetries(message, 3))
1724
                {
1725
                    var expectedCodes = new[]
1726
                                            {
1727
                                                HttpStatusCode.OK, HttpStatusCode.NoContent, HttpStatusCode.Created,
1728
                                                HttpStatusCode.NotFound
1729
                                            };
1730
                    if (!expectedCodes.Contains(response.StatusCode))
1731
                        throw CreateWebException("DeleteObject", response.StatusCode);
1732
                }
1733
            }
1734
/*
1735
            
1736

    
1737
            var targetUrl = FolderConstants.TrashContainer + "/" + objectName;
1738
/*
1739
            if (isDirectory)
1740
                targetUrl = targetUrl + "?delimiter=/";
1741
#1#
1742

    
1743
            var sourceUrl = String.Format("/{0}/{1}", sourceContainer, objectName);
1744

    
1745
            using (var client = new RestClient(_baseClient))
1746
            {
1747
                if (!String.IsNullOrWhiteSpace(account))
1748
                    client.BaseAddress = GetAccountUrl(account);
1749

    
1750
                client.Headers.Add("X-Move-From", sourceUrl);
1751
                client.AllowedStatusCodes.Add(HttpStatusCode.NotFound);
1752
                Log.InfoFormat("[TRASH] [{0}] to [{1}]",sourceUrl,targetUrl);
1753
                client.PutWithRetry(new Uri(targetUrl,UriKind.Relative), 3);
1754

    
1755
                var expectedCodes = new[] {HttpStatusCode.OK, HttpStatusCode.NoContent, HttpStatusCode.Created,HttpStatusCode.NotFound};
1756
                if (!expectedCodes.Contains(client.StatusCode))
1757
                    throw CreateWebException("DeleteObject", client.StatusCode);
1758
            }
1759
*/
1760
        }
1761

    
1762
      
1763
        private static WebException CreateWebException(string operation, HttpStatusCode statusCode)
1764
        {
1765
            return new WebException(String.Format("{0} failed with unexpected status code {1}", operation, statusCode));
1766
        }
1767

    
1768

    
1769
        public async Task<bool> CanUpload(string account, ObjectInfo cloudFile)
1770
        {
1771
            Contract.Requires(!String.IsNullOrWhiteSpace(account));
1772
            Contract.Requires(cloudFile!=null);
1773

    
1774
                var parts = cloudFile.Name.ToString().Split('/');
1775
                var folder = String.Join("/", parts,0,parts.Length-1);
1776

    
1777
                var fileName = String.Format("{0}/{1}.pithos.ignore", folder, Guid.NewGuid());
1778
                var fileUri=fileName.ToEscapedUri();                                            
1779

    
1780
                try
1781
                {
1782
                    var relativeUri = cloudFile.Container.Combine(fileUri);
1783
                    var targetUri = GetTargetUri(account).Combine(relativeUri);
1784
                    var message = new HttpRequestMessage(HttpMethod.Put, targetUri);
1785
                    message.Content.Headers.ContentType =new MediaTypeHeaderValue("application/octet-stream");
1786
                    var response=await _baseHttpClient.SendAsyncWithRetries(message, 3);                    
1787
                    var expectedCodes = new[] { HttpStatusCode.OK, HttpStatusCode.NoContent, HttpStatusCode.Created};
1788
                    var result=(expectedCodes.Contains(response.StatusCode));
1789
                    await DeleteObject(account, cloudFile.Container, fileUri, cloudFile.IsDirectory);
1790
                    return result;
1791
                }
1792
                catch
1793
                {
1794
                    return false;
1795
                }
1796
            
1797
        }
1798

    
1799
        ~CloudFilesClient()
1800
        {
1801
            Dispose(false);
1802
        }
1803

    
1804
        public void Dispose()
1805
        {
1806
            Dispose(true);
1807
            GC.SuppressFinalize(this);
1808
        }
1809

    
1810
        protected virtual void Dispose(bool disposing)
1811
        {
1812
            if (disposing)
1813
            {
1814
                if (_httpClientHandler!=null)
1815
                    _httpClientHandler.Dispose();
1816
                if (_baseClient!=null)
1817
                    _baseClient.Dispose();
1818
                if(_baseHttpClient!=null)
1819
                    _baseHttpClient.Dispose();
1820
                if (_baseHttpClientNoTimeout!=null)
1821
                    _baseHttpClientNoTimeout.Dispose();
1822
            }
1823
            _httpClientHandler = null;
1824
            _baseClient = null;
1825
            _baseHttpClient = null;
1826
            _baseHttpClientNoTimeout = null;
1827
        }
1828

    
1829
        public async Task<string> ResolveName(Guid accountToken)
1830
        {
1831
            string format = string.Format("{{\"uuids\":[\"{0}\"]}}", accountToken);
1832
            var content = new StringContent(format,Encoding.UTF8);
1833
            //content.Headers.ContentType=new MediaTypeHeaderValue("text/html; charset=utf-8");
1834
            string catalogEntry;
1835
            //var catalogUrl = new Uri(_baseHttpClient.BaseAddress.Scheme + "://" +_baseHttpClient.BaseAddress.Host,UriKind.Absolute).Combine("user_catalogs");
1836
            var catalogUrl = new Uri("https://accounts.okeanos.grnet.gr/account/v1.0/user_catalogs");
1837
            using (var response = await _baseHttpClient.PostAsync(catalogUrl, content).ConfigureAwait(false))
1838
            {
1839
                catalogEntry=await response.Content.ReadAsStringAsync().ConfigureAwait(false);
1840
            }
1841

    
1842
            var entry = (JContainer)JsonConvert.DeserializeObject(catalogEntry);
1843
            string key = accountToken.ToString();
1844
            return (string)entry["uuid_catalog"][key];
1845

    
1846
        }
1847

    
1848
        public async Task<Guid> ResolveToken(string displayName)
1849
        {
1850
            string format = string.Format("{{\"displaynames\":[\"{0}\"]}}", displayName);
1851
            var content = new StringContent(format,Encoding.UTF8);
1852
            //content.Headers.ContentType=new MediaTypeHeaderValue("text/html; charset=utf-8");
1853
            string catalogEntry;
1854
            //var catalogUrl = new Uri(_baseHttpClient.BaseAddress.Scheme + "://" +_baseHttpClient.BaseAddress.Host,UriKind.Absolute).Combine("user_catalogs");
1855
            var catalogUrl = new Uri("https://accounts.okeanos.grnet.gr/account/v1.0/user_catalogs");
1856
            using (var response = await _baseHttpClient.PostAsync(catalogUrl, content).ConfigureAwait(false))
1857
            {
1858
                catalogEntry=await response.Content.ReadAsStringAsync().ConfigureAwait(false);
1859
            }
1860

    
1861
            var entry = (JContainer)JsonConvert.DeserializeObject(catalogEntry);
1862
            return new Guid((string)entry["displayname_catalog"][displayName]);
1863

    
1864
        }
1865
    }
1866

    
1867
    public class ShareAccountInfo
1868
    {
1869
        public DateTime? last_modified { get; set; }
1870
        public string name { get; set; }
1871
    }
1872
}