Statistics
| Branch: | Revision:

root / trunk / Pithos.Network / CloudFilesClient.cs @ 3a62612d

History | View | Annotate | Download (87.1 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
123
        {
124
            get { return _rootAddressUri; }
125
            set { _rootAddressUri = value; }
126
        }
127

    
128

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

    
132
        public string AuthenticationUrl { get; set; }
133

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

    
140
        public bool UsePithos { get; set; }
141

    
142

    
143
        BufferManager _bufferManager=BufferManager.CreateBufferManager(TreeHash.DEFAULT_BLOCK_SIZE*4,(int)TreeHash.DEFAULT_BLOCK_SIZE);
144
        private string _userCatalogUrl;
145
        private Uri _rootAddressUri;
146

    
147
        public CloudFilesClient(string userName, string apiKey)
148
        {
149
            UserName = userName;
150
            ApiKey = apiKey;
151
            _userCatalogUrl = "https://pithos.okeanos.io/user_catalogs";
152
        }
153

    
154
        public CloudFilesClient(AccountInfo accountInfo)
155
        {
156
            Contract.Requires<ArgumentNullException>(accountInfo!=null,"accountInfo is null");
157
            Contract.Ensures(!String.IsNullOrWhiteSpace(Token));
158
            Contract.Ensures(StorageUrl != null);
159
            Contract.Ensures(_baseClient != null);
160
            Contract.Ensures(RootAddressUri != null);
161
            Contract.EndContractBlock();          
162

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

    
180
            var httpClientHandler = new HttpClientHandler
181
            {
182
                AllowAutoRedirect = true,
183
                AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate,
184
                UseCookies = true,
185
            };
186

    
187

    
188
            _baseHttpClient = new HttpClient(httpClientHandler)
189
            {
190
                BaseAddress = StorageUrl,
191
                Timeout = TimeSpan.FromSeconds(30)
192
            };
193
            _baseHttpClient.DefaultRequestHeaders.Add(TOKEN_HEADER, Token);
194

    
195
            _baseHttpClientNoTimeout = new HttpClient(httpClientHandler)
196
            {
197
                BaseAddress = StorageUrl,
198
                Timeout = TimeSpan.FromMilliseconds(-1)
199
            };
200
            _baseHttpClientNoTimeout.DefaultRequestHeaders.Add(TOKEN_HEADER, Token);
201

    
202

    
203
        }
204

    
205

    
206
        private static void AssertStatusOK(HttpResponseMessage response, string message)
207
        {
208
            var statusCode = response.StatusCode;
209
            if (statusCode >= HttpStatusCode.BadRequest)
210
                throw new WebException(String.Format("{0} with code {1} - {2}", message, statusCode, response.ReasonPhrase));
211
        }
212

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

    
230

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

    
233
            var groups = new List<Group>();
234

    
235
//            using (var authClient = new HttpClient(_httpClientHandler,false){ BaseAddress = new Uri(AuthenticationUrl),Timeout=TimeSpan.FromSeconds(30) })
236
            using (var authClient = new HttpClient(_httpClientHandler, false) { BaseAddress = new Uri(AuthenticationUrl), Timeout = TimeSpan.FromSeconds(30) })
237
            {                
238

    
239
                authClient.DefaultRequestHeaders.Add("X-Auth-User", UserName);
240
                authClient.DefaultRequestHeaders.Add("X-Auth-Key", ApiKey);
241

    
242
                
243
                string token;
244
                string objectStorageService;
245
                
246
                //using (var response = await authClient.GetAsyncWithRetries(new Uri(VersionPath, UriKind.Relative),3).ConfigureAwait(false)) // .DownloadStringWithRetryRelative(new Uri(VersionPath, UriKind.Relative), 3);                    
247
                using (var response = await authClient.SendAsyncWithRetries(new HttpRequestMessage(HttpMethod.Post,new Uri("", UriKind.Relative)), 3).ConfigureAwait(false))
248
                {
249
                    AssertStatusOK(response,"Authentication failed");
250

    
251
                    var body=await response.Content.ReadAsStringAsync();
252
                    JObject json = (JObject) JsonConvert.DeserializeObject(body);
253
                    dynamic jsonD = json;
254

    
255
                    objectStorageService = json["access"]["serviceCatalog"][6]["endpoints"][0]["publicURL"].ToString();
256
                    if (String.IsNullOrWhiteSpace(objectStorageService))
257
                        throw new InvalidOperationException("Failed to obtain storage url");
258

    
259
                    token = this.ApiKey;// response.Headers.GetFirstValue(TOKEN_HEADER);
260
                    if (String.IsNullOrWhiteSpace(token))
261
                        throw new InvalidOperationException("Failed to obtain token url");
262
                }
263

    
264
                string storageUrl = new Uri(objectStorageService).Combine(UserName).ToString();
265
                _baseClient = new RestClient
266
                {
267
                    BaseAddress = storageUrl,
268
                    Timeout = 30000,
269
                    Retries = 3,                    
270
                };
271

    
272
                StorageUrl = new Uri(storageUrl);
273
                Token = token;
274

    
275
                //Get the root address (StorageUrl without the account)
276
                var rootUrl = objectStorageService;//.Substring(0, usernameIndex);
277
                RootAddressUri = new Uri(rootUrl);
278
                
279

    
280
                _baseHttpClient = new HttpClient(_httpClientHandler,false)
281
                {
282
                    BaseAddress = StorageUrl,
283
                    Timeout = TimeSpan.FromSeconds(30)
284
                };
285
                _baseHttpClient.DefaultRequestHeaders.Add(TOKEN_HEADER, token);
286

    
287
                _baseHttpClientNoTimeout = new HttpClient(_httpClientHandler,false)
288
                {
289
                    BaseAddress = StorageUrl,
290
                    Timeout = TimeSpan.FromMilliseconds(-1)
291
                };
292
                _baseHttpClientNoTimeout.DefaultRequestHeaders.Add(TOKEN_HEADER, token);
293

    
294
                /* var keys = authClient.ResponseHeaders.AllKeys.AsQueryable();
295
                groups = (from key in keys
296
                            where key.StartsWith("X-Account-Group-")
297
                            let name = key.Substring(16)
298
                            select new Group(name, authClient.ResponseHeaders[key]))
299
                        .ToList();
300
                    
301
*/
302
            }
303

    
304
            Log.InfoFormat("[AUTHENTICATE] End for {0}", UserName);
305
            Debug.Assert(_baseClient!=null);
306
            var displayName = UserName;
307
            Guid uuid;
308
            if (Guid.TryParse(UserName, out uuid))
309
            {
310
                displayName = await ResolveName(uuid);
311
            }
312
            return new AccountInfo {StorageUri = StorageUrl, Token = Token, UserName = UserName, DisplayName=displayName, Groups=groups};            
313

    
314
        }
315

    
316
        private static void TraceStart(string method, Uri actualAddress)
317
        {
318
            Log.InfoFormat("[{0}] {1} {2}", method, DateTime.Now, actualAddress);
319
        }
320

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

    
334
                if (response.StatusCode == HttpStatusCode.NoContent)
335
                    return String.Empty;
336

    
337
                var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
338
                return content;
339
            }
340
        }
341

    
342
        public async Task<IList<ContainerInfo>> ListContainers(string account)
343
        {
344

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

    
358
        
359
        private string GetAccountUrl(string account)
360
        {
361
            return RootAddressUri.Combine(account).AbsoluteUri;
362
        }
363

    
364
        public IList<ShareAccountInfo> ListSharingAccounts(DateTime? since=null)
365
        {
366
            using (ThreadContext.Stacks["Share"].Push("List Accounts"))
367
            {
368
                if (Log.IsDebugEnabled) Log.DebugFormat("START");
369

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

    
373
                //If the result is empty, return an empty list,
374
                var infos = String.IsNullOrWhiteSpace(content)
375
                                ? new List<ShareAccountInfo>()
376
                            //Otherwise deserialize the account list into a list of ShareAccountInfos
377
                                : JsonConvert.DeserializeObject<IList<ShareAccountInfo>>(content);
378

    
379
                Log.DebugFormat("END");
380
                return infos;
381
            }
382
        }
383

    
384

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

    
397
            using (ThreadContext.Stacks["Share"].Push("List Objects"))
398
            {
399
                if (Log.IsDebugEnabled) Log.DebugFormat("START");
400
                //'since' is not used here because we need to have ListObjects return a NoChange result
401
                //for all shared accounts,containers
402

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

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

    
426
                if (Log.IsDebugEnabled) Log.DebugFormat("END");
427
                return objects;
428
            }
429
        }
430

    
431
        private List<ObjectInfo> AddMissingParents(List<ObjectInfo> objects)
432
        {
433
            //TODO: Remove short-circuit when we decide to use Missing Parents functionality
434
            //return objects;
435

    
436
            var existingUris = objects.ToDictionary(o => o.Uri, o => o);
437
            foreach (var objectInfo in objects)
438
            {
439
                //Can be null when retrieving objects to show in selective sync
440
                if (objectInfo.Name == null)
441
                    continue;
442

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

    
457
                    var parentParts = baseParts.Concat(nameparts);
458
                    var parentUrl = objectInfo.Uri.Scheme+ "://" + String.Join("/", parentParts);
459
                    
460
                    var parentUri = new Uri(parentUrl, UriKind.Absolute);
461

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

    
490
        public void SetTags(ObjectInfo target,IDictionary<string,string> tags)
491
        {
492
            Contract.Requires<InvalidOperationException>(!String.IsNullOrWhiteSpace(Token),"The Token is not set");
493
            Contract.Requires<InvalidOperationException>(StorageUrl != null,"The StorageUrl is not set");
494
            Contract.Requires<ArgumentNullException>(target != null,"target is null");
495
            Contract.EndContractBlock();
496

    
497
            using (ThreadContext.Stacks["Share"].Push("Share Object"))
498
            {
499
                if (Log.IsDebugEnabled) Log.DebugFormat("START");
500

    
501
                using (var client = new RestClient(_baseClient))
502
                {
503

    
504
                    client.BaseAddress = GetAccountUrl(target.Account);
505

    
506
                    client.Parameters.Clear();
507
                    client.Parameters.Add("update", "");
508

    
509
                    foreach (var tag in tags)
510
                    {
511
                        var headerTag = String.Format("X-Object-Meta-{0}", tag.Key);
512
                        client.Headers.Add(headerTag, tag.Value);
513
                    }
514
                    
515
                    client.DownloadStringWithRetryRelative(target.Container, 3);
516

    
517
                    
518
                    client.AssertStatusOK("SetTags failed");
519
                    //If the status is NOT ACCEPTED we have a problem
520
                    if (client.StatusCode != HttpStatusCode.Accepted)
521
                    {
522
                        Log.Error("Failed to set tags");
523
                        throw new Exception("Failed to set tags");
524
                    }
525

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

    
530

    
531
        }
532

    
533
        public void ShareObject(string account, Uri container, Uri objectName, string shareTo, bool read, bool write)
534
        {
535

    
536
            Contract.Requires<InvalidOperationException>(!String.IsNullOrWhiteSpace(Token), "The Token is not set");
537
            Contract.Requires<InvalidOperationException>(StorageUrl != null, "The StorageUrl is not set");
538
            Contract.Requires<ArgumentNullException>(container != null, "container is null");
539
            Contract.Requires<ArgumentException>(!container.IsAbsoluteUri, "container is absolute");
540
            Contract.Requires<ArgumentNullException>(objectName != null, "objectName is null");
541
            Contract.Requires<ArgumentException>(!objectName.IsAbsoluteUri, "objectName  is absolute");
542
            Contract.Requires<ArgumentNullException>(!String.IsNullOrWhiteSpace(account), "account is not set");
543
            Contract.Requires<ArgumentNullException>(!String.IsNullOrWhiteSpace(shareTo), "shareTo is not set");
544
            Contract.EndContractBlock();
545

    
546
            using (ThreadContext.Stacks["Share"].Push("Share Object"))
547
            {
548
                if (Log.IsDebugEnabled) Log.DebugFormat("START");
549
                
550
                using (var client = new RestClient(_baseClient))
551
                {
552

    
553
                    client.BaseAddress = GetAccountUrl(account);
554

    
555
                    client.Parameters.Clear();
556
                    client.Parameters.Add("format", "json");
557

    
558
                    string permission = "";
559
                    if (write)
560
                        permission = String.Format("write={0}", shareTo);
561
                    else if (read)
562
                        permission = String.Format("read={0}", shareTo);
563
                    client.Headers.Add("X-Object-Sharing", permission);
564

    
565
                    var content = client.DownloadStringWithRetryRelative(container, 3);
566

    
567
                    client.AssertStatusOK("ShareObject failed");
568

    
569
                    //If the result is empty, return an empty list,
570
                    var infos = String.IsNullOrWhiteSpace(content)
571
                                    ? new List<ObjectInfo>()
572
                                //Otherwise deserialize the object list into a list of ObjectInfos
573
                                    : JsonConvert.DeserializeObject<IList<ObjectInfo>>(content);
574

    
575
                    if (Log.IsDebugEnabled) Log.DebugFormat("END");
576
                }
577
            }
578

    
579

    
580
        }
581

    
582
        public async Task<AccountInfo> GetAccountPolicies(AccountInfo accountInfo)
583
        {
584
            if (accountInfo==null)
585
                throw new ArgumentNullException("accountInfo");
586
            Contract.EndContractBlock();
587

    
588
            using (ThreadContext.Stacks["Account"].Push("GetPolicies"))
589
            {
590
                if (Log.IsDebugEnabled) Log.DebugFormat("START");
591

    
592
/*
593
                if (_baseClient == null)
594
                {
595
                    _baseClient = new RestClient
596
                    {
597
                        BaseAddress = accountInfo.StorageUri.ToString(),
598
                        Timeout = 30000,
599
                        Retries = 3,
600
                    };
601
                }
602

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

    
617
                    return accountInfo;   
618
                }
619

    
620

    
621
                //using (var client = new RestClient(_baseClient))
622
                //{
623
                //    if (!String.IsNullOrWhiteSpace(accountInfo.UserName))
624
                //        client.BaseAddress = GetAccountUrl(accountInfo.UserName);
625

    
626
                //    client.Parameters.Clear();
627
                //    client.Parameters.Add("format", "json");                    
628
                //    client.Head(_emptyUri, 3);
629

    
630
                //    var quotaValue=client.ResponseHeaders["X-Account-Policy-Quota"];
631
                //    var bytesValue= client.ResponseHeaders["X-Account-Bytes-Used"];
632

    
633
                //    long quota, bytes;
634
                //    if (long.TryParse(quotaValue, out quota))
635
                //        accountInfo.Quota = quota;
636
                //    if (long.TryParse(bytesValue, out bytes))
637
                //        accountInfo.BytesUsed = bytes;
638
                    
639
                //    return accountInfo;
640

    
641
                //}
642

    
643
            }
644
        }
645

    
646
        public void UpdateMetadata(ObjectInfo objectInfo)
647
        {
648
            Contract.Requires<ArgumentNullException>(objectInfo != null,"objectInfo is null");
649
            Contract.EndContractBlock();
650

    
651
            using (ThreadContext.Stacks["Objects"].Push("UpdateMetadata"))
652
            {
653
                if (Log.IsDebugEnabled) Log.DebugFormat("START");
654

    
655

    
656
                using(var client=new RestClient(_baseClient))
657
                {
658

    
659
                    client.BaseAddress = GetAccountUrl(objectInfo.Account);
660
                    
661
                    client.Parameters.Clear();
662
                    
663

    
664
                    //Set Tags
665
                    foreach (var tag in objectInfo.Tags)
666
                    {
667
                        var headerTag = String.Format("X-Object-Meta-{0}", tag.Key);
668
                        client.Headers.Add(headerTag, tag.Value);
669
                    }
670

    
671
                    //Set Permissions
672

    
673
                    var permissions=objectInfo.GetPermissionString();
674
                    client.SetNonEmptyHeaderValue("X-Object-Sharing",permissions);
675

    
676
                    client.SetNonEmptyHeaderValue("Content-Disposition",objectInfo.ContendDisposition);
677
                    client.SetNonEmptyHeaderValue("Content-Encoding",objectInfo.ContentEncoding);
678
                    client.SetNonEmptyHeaderValue("X-Object-Manifest",objectInfo.Manifest);
679
                    var isPublic = objectInfo.IsPublic.ToString().ToLower();
680
                    client.Headers.Add("X-Object-Public", isPublic);
681

    
682

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

    
694
                    if (Log.IsDebugEnabled) Log.DebugFormat("END");
695
                }
696
            }
697

    
698
        }
699

    
700
        public void UpdateMetadata(ContainerInfo containerInfo)
701
        {
702
            if (containerInfo == null)
703
                throw new ArgumentNullException("containerInfo");
704
            Contract.EndContractBlock();
705

    
706
            using (ThreadContext.Stacks["Containers"].Push("UpdateMetadata"))
707
            {
708
                if (Log.IsDebugEnabled) Log.DebugFormat("START");
709

    
710

    
711
                using(var client=new RestClient(_baseClient))
712
                {
713

    
714
                    client.BaseAddress = GetAccountUrl(containerInfo.Account);
715
                    
716
                    client.Parameters.Clear();
717
                    
718

    
719
                    //Set Tags
720
                    foreach (var tag in containerInfo.Tags)
721
                    {
722
                        var headerTag = String.Format("X-Container-Meta-{0}", tag.Key);
723
                        client.Headers.Add(headerTag, tag.Value);
724
                    }
725

    
726
                    
727
                    //Set Policies
728
                    foreach (var policy in containerInfo.Policies)
729
                    {
730
                        var headerPolicy = String.Format("X-Container-Policy-{0}", policy.Key);
731
                        client.Headers.Add(headerPolicy, policy.Value);
732
                    }
733

    
734

    
735
                    var uriBuilder = client.GetAddressBuilder(containerInfo.Name,_emptyUri);
736
                    var uri = uriBuilder.Uri;
737

    
738
                    client.UploadValues(uri,new NameValueCollection());
739

    
740

    
741
                    client.AssertStatusOK("UpdateMetadata failed");
742
                    //If the status is NOT ACCEPTED or OK we have a problem
743
                    if (!(client.StatusCode == HttpStatusCode.Accepted || client.StatusCode == HttpStatusCode.OK))
744
                    {
745
                        Log.Error("Failed to update metadata");
746
                        throw new Exception("Failed to update metadata");
747
                    }
748

    
749
                    if (Log.IsDebugEnabled) Log.DebugFormat("END");
750
                }
751
            }
752

    
753
        }
754

    
755
       
756

    
757

    
758
        public IList<ObjectInfo> ListObjects(string account, Uri container, DateTimeOffset? since = null)
759
        {
760
/*
761
            if (container==null)
762
                throw new ArgumentNullException("container");
763
            if (container.IsAbsoluteUri)
764
                throw new ArgumentException("container");
765
            Contract.EndContractBlock();
766
*/
767

    
768
            using (ThreadContext.Stacks["Objects"].Push("List"))
769
            {
770

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

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

    
776
                //304 will result in an empty string. Empty containers return an empty json array
777
                if (String.IsNullOrWhiteSpace(content))
778
                     return new[] {new NoModificationInfo(account, container)};
779

    
780
                 //If the result is empty, return an empty list,
781
                 var infos = String.IsNullOrWhiteSpace(content)
782
                                 ? new List<ObjectInfo>()
783
                             //Otherwise deserialize the object list into a list of ObjectInfos
784
                                 : JsonConvert.DeserializeObject<IList<ObjectInfo>>(content);
785

    
786
                 foreach (var info in infos)
787
                 {
788
                     info.Container = container;
789
                     info.Account = account;
790
                     info.StorageUri = StorageUrl;
791
                 }
792
                 if (Log.IsDebugEnabled) Log.DebugFormat("END");
793
                 return infos;
794
            }
795
        }
796

    
797
        public IList<ObjectInfo> ListObjects(string account, Uri container, Uri folder, DateTimeOffset? since = null)
798
        {
799
/*            if (container==null)
800
                throw new ArgumentNullException("container");
801
            if (container.IsAbsoluteUri)
802
                throw new ArgumentException("container");
803
            Contract.EndContractBlock();*/
804

    
805
            using (ThreadContext.Stacks["Objects"].Push("List"))
806
            {
807
                if (Log.IsDebugEnabled) Log.DebugFormat("START");
808

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

    
813
                //304 will result in an empty string. Empty containers return an empty json array
814
                if (String.IsNullOrWhiteSpace(content))
815
                    return new[] { new NoModificationInfo(account, container) };
816

    
817

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

    
834
                    client.Parameters.Clear();
835
                    client.Parameters.Add("format", "json");
836
                    client.Parameters.Add("path", folder.ToString());
837
                    client.IfModifiedSince = since;
838
                    var content = client.DownloadStringWithRetryRelative(container, 3);
839
                    client.AssertStatusOK("ListObjects failed");
840

    
841
                    if (client.StatusCode==HttpStatusCode.NotModified)
842
                        return new[]{new NoModificationInfo(account,container,folder)};
843

    
844
                    var infos = JsonConvert.DeserializeObject<IList<ObjectInfo>>(content);
845
                    foreach (var info in infos)
846
                    {
847
                        info.Account = account;
848
                        if (info.Container == null)
849
                            info.Container = container;
850
                        info.StorageUri = StorageUrl;
851
                    }
852
                    if (Log.IsDebugEnabled) Log.DebugFormat("END");
853
                    return infos;
854
                }
855
*/
856
            }
857
        }
858

    
859
 
860
        public async Task<bool> ContainerExists(string account, Uri container)
861
        {
862
            if (container==null)
863
                throw new ArgumentNullException("container", "The container property can't be empty");
864
            if (container.IsAbsoluteUri)
865
                throw new ArgumentException( "The container must be relative","container");
866
            Contract.EndContractBlock();
867

    
868
            using (ThreadContext.Stacks["Containters"].Push("Exists"))
869
            {
870
                if (Log.IsDebugEnabled) Log.DebugFormat("START");
871

    
872
                var targetUri = GetTargetUri(account).Combine(container);
873

    
874
                using (var response =await _baseHttpClient.HeadAsyncWithRetries(targetUri, 3))
875
                {
876

    
877
                    bool result;
878
                    switch (response.StatusCode)
879
                    {
880
                        case HttpStatusCode.OK:
881
                        case HttpStatusCode.NoContent:
882
                            result = true;
883
                            break;
884
                        case HttpStatusCode.NotFound:
885
                            result = false;
886
                            break;
887
                        default:
888
                            throw CreateWebException("ContainerExists", response.StatusCode);
889
                    }
890
                    if (Log.IsDebugEnabled) Log.DebugFormat("END");
891

    
892
                    return result;
893
                }
894

    
895
            }
896
        }
897

    
898
        private Uri GetTargetUri(string account)
899
        {
900
            return new Uri(GetTargetUrl(account),UriKind.Absolute);
901
        }
902

    
903
        private string GetTargetUrl(string account)
904
        {
905
            return String.IsNullOrWhiteSpace(account)
906
                       ? _baseHttpClient.BaseAddress.ToString()
907
                       : GetAccountUrl(account);
908
        }
909

    
910
        public async Task<bool> ObjectExists(string account, Uri container, Uri objectName)
911
        {
912
            if (container == null)
913
                throw new ArgumentNullException("container", "The container property can't be empty");
914
            if (container.IsAbsoluteUri)
915
                throw new ArgumentException("The container must be relative","container");
916
            if (objectName == null)
917
                throw new ArgumentNullException("objectName", "The objectName property can't be empty");
918
            if (objectName.IsAbsoluteUri)
919
                throw new ArgumentException("The objectName must be relative","objectName");
920
            Contract.EndContractBlock();
921

    
922
                var targetUri=GetTargetUri(account).Combine(container).Combine(objectName);
923

    
924
            using (var response = await _baseHttpClient.HeadAsyncWithRetries(targetUri, 3).ConfigureAwait(false))
925
            {
926
                switch (response.StatusCode)
927
                {
928
                    case HttpStatusCode.OK:
929
                    case HttpStatusCode.NoContent:
930
                        return true;
931
                    case HttpStatusCode.NotFound:
932
                        return false;
933
                    default:
934
                        throw CreateWebException("ObjectExists", response.StatusCode);
935
                }
936
            }
937
        }
938

    
939
        public async Task<ObjectInfo> GetObjectInfo(string account, Uri container, Uri objectName)
940
        {
941
            if (container == null)
942
                throw new ArgumentNullException("container", "The container property can't be empty");
943
            if (container.IsAbsoluteUri)
944
                throw new ArgumentException("The container must be relative", "container");
945
            if (objectName == null)
946
                throw new ArgumentNullException("objectName", "The objectName property can't be empty");
947
            if (objectName.IsAbsoluteUri)
948
                throw new ArgumentException("The objectName must be relative", "objectName");
949
            Contract.EndContractBlock();
950

    
951
            using (ThreadContext.Stacks["Objects"].Push("GetObjectInfo"))
952
            {
953

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

    
971
                                var permissions = response.Headers.GetFirstValue("X-Object-Sharing");
972

    
973

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

    
1021

    
1022

    
1023
        public async Task CreateFolder(string account, Uri container, Uri folder)
1024
        {
1025
            if (container == null)
1026
                throw new ArgumentNullException("container", "The container property can't be empty");
1027
            if (container.IsAbsoluteUri)
1028
                throw new ArgumentException("The container must be relative","container");
1029
            if (folder == null)
1030
                throw new ArgumentNullException("folder", "The objectName property can't be empty");
1031
            if (folder.IsAbsoluteUri)
1032
                throw new ArgumentException("The objectName must be relative","folder");
1033
            Contract.EndContractBlock();
1034

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

    
1048
        private Dictionary<string, string> GetMeta(HttpResponseMessage response,string metaPrefix)
1049
        {
1050
            Contract.Requires<ArgumentNullException>(!String.IsNullOrWhiteSpace(metaPrefix), "metaPrefix is empty");
1051
            Contract.EndContractBlock();
1052

    
1053
            var dict = (from header in response.Headers
1054
                        where header.Key.StartsWith(metaPrefix)
1055
                         select new { Name = header.Key, Value = String.Join(",", header.Value) })
1056
                        .ToDictionary(t => t.Name, t => t.Value);
1057

    
1058
          
1059
            return dict;
1060
        }
1061

    
1062

    
1063
        public async Task<ContainerInfo> GetContainerInfo(string account, Uri container)
1064
        {
1065
            if (container == null)
1066
                throw new ArgumentNullException("container", "The container property can't be empty");
1067
            if (container.IsAbsoluteUri)
1068
                throw new ArgumentException("The container must be relative","container");
1069
            Contract.EndContractBlock();
1070

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

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

    
1098

    
1099
                        return containerInfo;
1100
                    case HttpStatusCode.NotFound:
1101
                        return ContainerInfo.Empty;
1102
                    default:
1103
                        throw CreateWebException("GetContainerInfo", response.StatusCode);
1104
                }
1105
            }            
1106
        }
1107

    
1108
        public async Task CreateContainer(string account, Uri container)
1109
        {
1110
            if (container == null)
1111
                throw new ArgumentNullException("container", "The container property can't be empty");
1112
            if (container.IsAbsoluteUri)
1113
                throw new ArgumentException("The container must be relative","container");
1114
            Contract.EndContractBlock();
1115

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

    
1128
        public async Task WipeContainer(string account, Uri container)
1129
        {
1130
            if (container == null)
1131
                throw new ArgumentNullException("container", "The container property can't be empty");
1132
            if (container.IsAbsoluteUri)
1133
                throw new ArgumentException("The container must be relative", "container");
1134
            Contract.EndContractBlock();
1135

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

    
1139

    
1140
        public async Task DeleteContainer(string account, Uri container)
1141
        {
1142
            if (container == null)
1143
                throw new ArgumentNullException("container", "The container property can't be empty");
1144
            if (container.IsAbsoluteUri)
1145
                throw new ArgumentException("The container must be relative","container");
1146
            Contract.EndContractBlock();
1147

    
1148
            var targetUri = GetTargetUri(account).Combine(container);
1149
            var message = new HttpRequestMessage(HttpMethod.Delete, targetUri);
1150
            using (var response = await _baseHttpClient.SendAsyncWithRetries(message, 3).ConfigureAwait(false))
1151
            {
1152
                var expectedCodes = new[] { HttpStatusCode.NotFound, HttpStatusCode.NoContent };
1153
                if (!expectedCodes.Contains(response.StatusCode))
1154
                    throw CreateWebException("DeleteContainer", response.StatusCode);
1155
            }
1156

    
1157
        }
1158

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

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

    
1195
                    //The container and objectName are relative names. They are joined with the client's
1196
                    //BaseAddress to create the object's absolute address
1197
                    var builder = client.GetAddressBuilder(container, objectName);
1198
                    var uri = builder.Uri;
1199

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

    
1220
                    //Once the download completes
1221
                    //Delete the local client object
1222
                }
1223
                //And report failure or completion
1224
            }
1225
            catch (Exception exc)
1226
            {
1227
                Log.ErrorFormat("[GET] FAIL {0} with {1}", objectName, exc);
1228
                throw;
1229
            }
1230

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

    
1233

    
1234
        }
1235

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

    
1254
            
1255

    
1256
            //The container and objectName are relative names. They are joined with the client's
1257
            //BaseAddress to create the object's absolute address
1258

    
1259
            var targetUri = GetTargetUri(account).Combine(container).Combine(objectName);
1260
  
1261

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

    
1264
            
1265
            //Send the tree hash as Json to the server            
1266
            var jsonHash = hash.ToJson();
1267
            if (Log.IsDebugEnabled)
1268
                Log.DebugFormat("Hashes:\r\n{0}", jsonHash);
1269

    
1270
            var mimeType = objectName.GetMimeType();
1271

    
1272
            var message = new HttpRequestMessage(HttpMethod.Put, uri)
1273
            {
1274
                Content = new StringContent(jsonHash)
1275
            };
1276
            message.Content.Headers.ContentType = mimeType;
1277
            message.Headers.Add("ETag",hash.TopHash.ToHashString());
1278
            
1279
            
1280
            //Don't use a timeout because putting the hashmap may be a long process
1281

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

    
1311
        }
1312

    
1313

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

    
1332

    
1333
            var targetUri = GetTargetUri(account).Combine(container).Combine(relativeUrl);
1334
            var message = new HttpRequestMessage(HttpMethod.Get, targetUri);
1335
            //Don't add a range if start=0, end=null (empty files)
1336
            if (start!=0 || end!=null)
1337
                message.Headers.Range=new RangeHeaderValue(start,end);
1338

    
1339
            //Don't use a timeout because putting the hashmap may be a long process
1340

    
1341
            IProgress<DownloadArgs> progress = new Progress<DownloadArgs>(args =>
1342
                {
1343
                    Log.DebugFormat("[GET PROGRESS] {0} {1}% {2} of {3}",
1344
                                    targetUri.Segments.Last(), args.ProgressPercentage,
1345
                                    args.BytesReceived,
1346
                                    args.TotalBytesToReceive);
1347

    
1348
                    if (DownloadProgressChanged!=null)
1349
                        DownloadProgressChanged(this,  args);
1350
                });
1351

    
1352

    
1353
            using (var response = await _baseHttpClientNoTimeout.SendAsyncWithRetries(message, 3, false,HttpCompletionOption.ResponseHeadersRead,
1354
                                                          cancellationToken).ConfigureAwait(false))
1355
            using (var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false))
1356
            {
1357
                long totalSize = response.Content.Headers.ContentLength ?? 0;
1358
                    byte[] buffer,streambuf;
1359
                    lock (_bufferManager)
1360
                    {
1361
                        buffer = _bufferManager.TakeBuffer(65536);
1362
                        streambuf = _bufferManager.TakeBuffer((int)totalSize);
1363
                    }
1364

    
1365
                using (var targetStream = new MemoryStream(streambuf))
1366
                {
1367

    
1368
                    long total = 0;
1369
                    try
1370
                    {
1371

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

    
1393
        }
1394

    
1395
        public event EventHandler<UploadArgs> UploadProgressChanged;
1396
        public event EventHandler<DownloadArgs> DownloadProgressChanged;
1397
        
1398

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

    
1417

    
1418
            try
1419
            {
1420
                var containerUri = GetTargetUri(account).Combine(container);
1421
                var targetUri = new Uri(String.Format("{0}?update", containerUri));
1422

    
1423

    
1424
                //Don't use a timeout because putting the hashmap may be a long process
1425

    
1426

    
1427
                Log.InfoFormat("[BLOCK POST] START");
1428

    
1429

    
1430
                var progress = new Progress<UploadArgs>(args =>
1431
                {
1432
                    Log.InfoFormat("[BLOCK POST PROGRESS] {0}% {1} of {2}",
1433
                        args.ProgressPercentage,
1434
                        args.BytesSent,
1435
                        args.TotalBytesToSend);
1436
                    if (UploadProgressChanged != null)
1437
                        UploadProgressChanged(this,args);
1438
                });
1439

    
1440
                var message = new HttpRequestMessage(HttpMethod.Post, targetUri)
1441
                                  {
1442
                                      Content = new ByteArrayContentWithProgress(block, offset, count,progress)
1443
                                  };
1444
                message.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(@"application/octet-stream");
1445

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

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

    
1487

    
1488
            try
1489
            {
1490
                var containerUri = GetTargetUri(account).Combine(container);
1491
                var targetUri = new Uri(String.Format("{0}?update", containerUri));
1492

    
1493

    
1494
                //Don't use a timeout because putting the hashmap may be a long process
1495

    
1496

    
1497
                Log.InfoFormat("[BLOCK POST] START");
1498

    
1499

    
1500
                var progress = new Progress<UploadArgs>(args =>
1501
                {
1502
                    Log.InfoFormat("[BLOCK POST PROGRESS] {0}% {1} of {2} at {3:###,} Kbps ",
1503
                        args.ProgressPercentage,
1504
                        args.BytesSent,
1505
                        args.TotalBytesToSend,args.Speed);
1506
                    if (UploadProgressChanged != null)
1507
                        UploadProgressChanged(this, args);
1508
                });
1509

    
1510
                var message = new HttpRequestMessage(HttpMethod.Post, targetUri)
1511
                {
1512
                    Content = new FileBlockContent(filePath, offset, count, progress)
1513
                };
1514
                message.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(@"application/octet-stream");
1515

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

    
1541

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

    
1558
            try
1559
            {
1560

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

    
1564
                //Start downloading the object asynchronously
1565
                var json = await GetStringAsync(targetUri, "").ConfigureAwait(false);
1566
                var treeHash = TreeHash.Parse(json);
1567
                Log.InfoFormat("[GET HASH] END {0}", objectName);
1568
                return treeHash;
1569

    
1570
            }
1571
            catch (Exception exc)
1572
            {
1573
                Log.ErrorFormat("[GET HASH] END {0} with {1}", objectName, exc);
1574
                throw;
1575
            }
1576

    
1577
        }
1578

    
1579

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

    
1605
                using (var client = new RestClient(_baseClient) { Timeout = 0 })
1606
                {
1607
                    if (!String.IsNullOrWhiteSpace(account))
1608
                        client.BaseAddress = GetAccountUrl(account);
1609

    
1610
                    var builder = client.GetAddressBuilder(container, objectName);
1611
                    var uri = builder.Uri;
1612

    
1613
                    string etag = hash ;
1614

    
1615
                    client.Headers.Add("Content-Type", contentType);
1616
                    if (contentType!=ObjectInfo.CONTENT_TYPE_DIRECTORY)
1617
                        client.Headers.Add("ETag", etag);
1618

    
1619

    
1620
                    Log.InfoFormat("[PUT] START {0}", objectName);
1621
                    client.UploadProgressChanged += (sender, args) =>
1622
                                                        {
1623
                                                            using (ThreadContext.Stacks["PUT"].Push("Progress"))
1624
                                                            {
1625
                                                                Log.InfoFormat("{0} {1}% {2} of {3}", fileName,
1626
                                                                               args.ProgressPercentage,
1627
                                                                               args.BytesSent, args.TotalBytesToSend);
1628
                                                            }
1629
                                                        };
1630

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

    
1645
                Log.InfoFormat("[PUT] END {0}", objectName);
1646
            }
1647
            catch (Exception exc)
1648
            {
1649
                Log.ErrorFormat("[PUT] END {0} with {1}", objectName, exc);
1650
                throw;
1651
            }                
1652

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

    
1675
            var baseUri = GetTargetUri(account);
1676
            var targetUri = baseUri.Combine(targetContainer).Combine(newObjectName);
1677
            var sourceUri = new Uri(String.Format("/{0}/{1}", sourceContainer, oldObjectName),UriKind.Relative);
1678

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

    
1689
        public async Task DeleteObject(string account, Uri sourceContainer, Uri objectName, bool isDirectory)
1690
        {
1691
            if (sourceContainer == null)
1692
                throw new ArgumentNullException("sourceContainer", "The sourceContainer property can't be empty");
1693
            if (sourceContainer.IsAbsoluteUri)
1694
                throw new ArgumentException("The sourceContainer must be relative","sourceContainer");
1695
            if (objectName == null)
1696
                throw new ArgumentNullException("objectName", "The objectName property can't be empty");
1697
            if (objectName.IsAbsoluteUri)
1698
                throw new ArgumentException("The objectName must be relative","objectName");
1699
            Contract.EndContractBlock();
1700

    
1701

    
1702

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

    
1705
            
1706
            if (objectName.OriginalString.EndsWith(".ignore"))
1707
                using(var response = await _baseHttpClient.DeleteAsync(sourceUri)){}
1708
            else
1709
            {
1710
                var relativeUri = new Uri(String.Format("{0}/{1}", FolderConstants.TrashContainer, objectName),
1711
                                                UriKind.Relative);
1712

    
1713
/*
1714
                var relativeUri = isDirectory
1715
                                      ? new Uri(
1716
                                            String.Format("{0}/{1}?delimiter=/", FolderConstants.TrashContainer,
1717
                                                          objectName), UriKind.Relative)
1718
                                      : new Uri(String.Format("{0}/{1}", FolderConstants.TrashContainer, objectName),
1719
                                                UriKind.Relative);
1720

    
1721
*/
1722
                var targetUri = GetTargetUri(account).Combine(relativeUri);
1723

    
1724

    
1725
                var message = new HttpRequestMessage(HttpMethod.Put, targetUri);
1726
                message.Headers.Add("X-Move-From", sourceUri.ToString());
1727

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

    
1743
            var targetUrl = FolderConstants.TrashContainer + "/" + objectName;
1744
/*
1745
            if (isDirectory)
1746
                targetUrl = targetUrl + "?delimiter=/";
1747
#1#
1748

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

    
1751
            using (var client = new RestClient(_baseClient))
1752
            {
1753
                if (!String.IsNullOrWhiteSpace(account))
1754
                    client.BaseAddress = GetAccountUrl(account);
1755

    
1756
                client.Headers.Add("X-Move-From", sourceUrl);
1757
                client.AllowedStatusCodes.Add(HttpStatusCode.NotFound);
1758
                Log.InfoFormat("[TRASH] [{0}] to [{1}]",sourceUrl,targetUrl);
1759
                client.PutWithRetry(new Uri(targetUrl,UriKind.Relative), 3);
1760

    
1761
                var expectedCodes = new[] {HttpStatusCode.OK, HttpStatusCode.NoContent, HttpStatusCode.Created,HttpStatusCode.NotFound};
1762
                if (!expectedCodes.Contains(client.StatusCode))
1763
                    throw CreateWebException("DeleteObject", client.StatusCode);
1764
            }
1765
*/
1766
        }
1767

    
1768
      
1769
        private static WebException CreateWebException(string operation, HttpStatusCode statusCode)
1770
        {
1771
            return new WebException(String.Format("{0} failed with unexpected status code {1}", operation, statusCode));
1772
        }
1773

    
1774

    
1775
        public async Task<bool> CanUpload(string account, ObjectInfo cloudFile)
1776
        {
1777
            Contract.Requires(!String.IsNullOrWhiteSpace(account));
1778
            Contract.Requires(cloudFile!=null);
1779

    
1780
                var parts = cloudFile.Name.ToString().Split('/');
1781
                var folder = String.Join("/", parts,0,parts.Length-1);
1782

    
1783
                var fileName = String.Format("{0}/{1}.pithos.ignore", folder, Guid.NewGuid());
1784
                var fileUri=fileName.ToEscapedUri();                                            
1785

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

    
1805
        ~CloudFilesClient()
1806
        {
1807
            Dispose(false);
1808
        }
1809

    
1810
        public void Dispose()
1811
        {
1812
            Dispose(true);
1813
            GC.SuppressFinalize(this);
1814
        }
1815

    
1816
        protected virtual void Dispose(bool disposing)
1817
        {
1818
            if (disposing)
1819
            {
1820
                if (_httpClientHandler!=null)
1821
                    _httpClientHandler.Dispose();
1822
                if (_baseClient!=null)
1823
                    _baseClient.Dispose();
1824
                if(_baseHttpClient!=null)
1825
                    _baseHttpClient.Dispose();
1826
                if (_baseHttpClientNoTimeout!=null)
1827
                    _baseHttpClientNoTimeout.Dispose();
1828
            }
1829
            _httpClientHandler = null;
1830
            _baseClient = null;
1831
            _baseHttpClient = null;
1832
            _baseHttpClientNoTimeout = null;
1833
        }
1834

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

    
1848
            var entry = (JContainer)JsonConvert.DeserializeObject(catalogEntry);
1849
            string key = accountToken.ToString();
1850
            return (string)entry["uuid_catalog"][key];
1851

    
1852
        }
1853

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

    
1867
            var entry = (JContainer)JsonConvert.DeserializeObject(catalogEntry);
1868
            return new Guid((string)entry["displayname_catalog"][displayName]);
1869

    
1870
        }
1871
    }
1872

    
1873
    public class ShareAccountInfo
1874
    {
1875
        public DateTime? last_modified { get; set; }
1876
        public string name { get; set; }
1877
    }
1878
}