Statistics
| Branch: | Revision:

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

History | View | Annotate | Download (79.2 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.Threading;
61
using System.Threading.Tasks;
62
using Newtonsoft.Json;
63
using Pithos.Interfaces;
64
using Pithos.Network;
65
using log4net;
66

    
67
namespace Pithos.Network
68
{
69

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

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

    
80
        private HttpClient _baseHttpClient;
81
        private HttpClient _baseHttpClientNoTimeout;
82
        
83

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

    
96
        private HttpClientHandler _httpClientHandler = new HttpClientHandler
97
                                                           {
98
                                                               AllowAutoRedirect = true,
99
                                                               AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate,
100
                                                               UseCookies = true,                                
101
                                                           };
102

    
103

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

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

    
118

    
119
        public Uri RootAddressUri { get; set; }
120

    
121

    
122
        public double DownloadPercentLimit { get; set; }
123
        public double UploadPercentLimit { get; set; }
124

    
125
        public string AuthenticationUrl { get; set; }
126

    
127
 
128
        public string VersionPath
129
        {
130
            get { return UsePithos ? "v1" : "v1.0"; }
131
        }
132

    
133
        public bool UsePithos { get; set; }
134

    
135

    
136

    
137
        public CloudFilesClient(string userName, string apiKey)
138
        {
139
            UserName = userName;
140
            ApiKey = apiKey;
141
        }
142

    
143
        public CloudFilesClient(AccountInfo accountInfo)
144
        {
145
            if (accountInfo==null)
146
                throw new ArgumentNullException("accountInfo");
147
            Contract.Ensures(!String.IsNullOrWhiteSpace(Token));
148
            Contract.Ensures(StorageUrl != null);
149
            Contract.Ensures(_baseClient != null);
150
            Contract.Ensures(RootAddressUri != null);
151
            Contract.EndContractBlock();          
152

    
153
            _baseClient = new RestClient
154
            {
155
                BaseAddress = accountInfo.StorageUri.ToString(),
156
                Timeout = 30000,
157
                Retries = 3,
158
            };
159
            StorageUrl = accountInfo.StorageUri;
160
            Token = accountInfo.Token;
161
            UserName = accountInfo.UserName;
162

    
163
            //Get the root address (StorageUrl without the account)
164
            var storageUrl = StorageUrl.AbsoluteUri;
165
            var usernameIndex = storageUrl.LastIndexOf(UserName);
166
            var rootUrl = storageUrl.Substring(0, usernameIndex);
167
            RootAddressUri = new Uri(rootUrl);
168

    
169
            var httpClientHandler = new HttpClientHandler
170
            {
171
                AllowAutoRedirect = true,
172
                AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate,
173
                UseCookies = true,
174
            };
175

    
176

    
177
            _baseHttpClient = new HttpClient(httpClientHandler)
178
            {
179
                BaseAddress = StorageUrl,
180
                Timeout = TimeSpan.FromSeconds(30)
181
            };
182
            _baseHttpClient.DefaultRequestHeaders.Add(TOKEN_HEADER, Token);
183

    
184
            _baseHttpClientNoTimeout = new HttpClient(httpClientHandler)
185
            {
186
                BaseAddress = StorageUrl,
187
                Timeout = TimeSpan.FromMilliseconds(-1)
188
            };
189
            _baseHttpClientNoTimeout.DefaultRequestHeaders.Add(TOKEN_HEADER, Token);
190

    
191

    
192
        }
193

    
194

    
195
        private static void AssertStatusOK(HttpResponseMessage response, string message)
196
        {
197
            var statusCode = response.StatusCode;
198
            if (statusCode >= HttpStatusCode.BadRequest)
199
                throw new WebException(String.Format("{0} with code {1} - {2}", message, statusCode, response.ReasonPhrase));
200
        }
201

    
202
        public async Task<AccountInfo> Authenticate()
203
        {
204
            if (String.IsNullOrWhiteSpace(UserName))
205
                throw new InvalidOperationException("UserName is empty");
206
            if (String.IsNullOrWhiteSpace(ApiKey))
207
                throw new InvalidOperationException("ApiKey is empty");
208
            if (String.IsNullOrWhiteSpace(AuthenticationUrl))
209
                throw new InvalidOperationException("AuthenticationUrl is empty");
210
            Contract.Ensures(!String.IsNullOrWhiteSpace(Token));
211
            Contract.Ensures(StorageUrl != null);
212
            Contract.Ensures(_baseClient != null);
213
            Contract.Ensures(RootAddressUri != null);
214
            Contract.EndContractBlock();
215

    
216

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

    
219
            var groups = new List<Group>();
220

    
221
            using (var authClient = new HttpClient(_httpClientHandler,false){ BaseAddress = new Uri(AuthenticationUrl),Timeout=TimeSpan.FromSeconds(30) })
222
            {                
223

    
224
                authClient.DefaultRequestHeaders.Add("X-Auth-User", UserName);
225
                authClient.DefaultRequestHeaders.Add("X-Auth-Key", ApiKey);
226

    
227
                string storageUrl;
228
                string token;
229
                
230
                using (var response = await authClient.GetAsyncWithRetries(new Uri(VersionPath, UriKind.Relative),3).ConfigureAwait(false)) // .DownloadStringWithRetryRelative(new Uri(VersionPath, UriKind.Relative), 3);                    
231
                {
232
                    AssertStatusOK(response,"Authentication failed");
233
                
234
                    storageUrl = response.Headers.GetValues("X-Storage-Url").First();
235
                    if (String.IsNullOrWhiteSpace(storageUrl))
236
                        throw new InvalidOperationException("Failed to obtain storage url");
237

    
238
                    token = response.Headers.GetValues(TOKEN_HEADER).First();
239
                    if (String.IsNullOrWhiteSpace(token))
240
                        throw new InvalidOperationException("Failed to obtain token url");
241

    
242
                }
243

    
244

    
245
                _baseClient = new RestClient
246
                {
247
                    BaseAddress = storageUrl,
248
                    Timeout = 30000,
249
                    Retries = 3,                    
250
                };
251

    
252
                StorageUrl = new Uri(storageUrl);
253
                Token = token;
254

    
255
                
256

    
257
                                                               
258
                //Get the root address (StorageUrl without the account)
259
                var usernameIndex=storageUrl.LastIndexOf(UserName);
260
                var rootUrl = storageUrl.Substring(0, usernameIndex);
261
                RootAddressUri = new Uri(rootUrl);
262
                
263

    
264
                _baseHttpClient = new HttpClient(_httpClientHandler,false)
265
                {
266
                    BaseAddress = StorageUrl,
267
                    Timeout = TimeSpan.FromSeconds(30)
268
                };
269
                _baseHttpClient.DefaultRequestHeaders.Add(TOKEN_HEADER, token);
270

    
271
                _baseHttpClientNoTimeout = new HttpClient(_httpClientHandler,false)
272
                {
273
                    BaseAddress = StorageUrl,
274
                    Timeout = TimeSpan.FromMilliseconds(-1)
275
                };
276
                _baseHttpClientNoTimeout.DefaultRequestHeaders.Add(TOKEN_HEADER, token);
277

    
278
                /* var keys = authClient.ResponseHeaders.AllKeys.AsQueryable();
279
                groups = (from key in keys
280
                            where key.StartsWith("X-Account-Group-")
281
                            let name = key.Substring(16)
282
                            select new Group(name, authClient.ResponseHeaders[key]))
283
                        .ToList();
284
                    
285
*/
286
            }
287

    
288
            Log.InfoFormat("[AUTHENTICATE] End for {0}", UserName);
289
            Debug.Assert(_baseClient!=null);
290

    
291
            return new AccountInfo {StorageUri = StorageUrl, Token = Token, UserName = UserName,Groups=groups};            
292

    
293
        }
294

    
295
        private static void TraceStart(string method, Uri actualAddress)
296
        {
297
            Log.InfoFormat("[{0}] {1} {2}", method, DateTime.Now, actualAddress);
298
        }
299

    
300
        private async Task<string> GetStringAsync(Uri targetUri, string errorMessage,DateTime? since=null)
301
        {
302
            TraceStart("GET",targetUri);
303
            var request = new HttpRequestMessage(HttpMethod.Get, targetUri);            
304
            if (since.HasValue)
305
            {
306
                request.Headers.IfModifiedSince = since.Value;
307
            }
308
            using (var response = await _baseHttpClient.SendAsyncWithRetries(request,3).ConfigureAwait(false))
309
            {
310
                AssertStatusOK(response, errorMessage);
311

    
312
                if (response.StatusCode == HttpStatusCode.NoContent)
313
                    return String.Empty;
314

    
315
                var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
316
                return content;
317
            }
318
        }
319

    
320
        public async Task<IList<ContainerInfo>> ListContainers(string account)
321
        {
322

    
323
            var targetUrl = GetTargetUrl(account);
324
            var targetUri = new Uri(String.Format("{0}?format=json", targetUrl));
325
            var result = await GetStringAsync(targetUri, "List Containers failed").ConfigureAwait(false);
326
            if (String.IsNullOrWhiteSpace(result))
327
                return new List<ContainerInfo>();
328
            var infos = JsonConvert.DeserializeObject<IList<ContainerInfo>>(result);
329
            foreach (var info in infos)
330
            {
331
                info.Account = account;
332
            }
333
            return infos;
334
        }
335

    
336
        
337
        private string GetAccountUrl(string account)
338
        {
339
            return RootAddressUri.Combine(account).AbsoluteUri;
340
        }
341

    
342
        public IList<ShareAccountInfo> ListSharingAccounts(DateTime? since=null)
343
        {
344
            using (ThreadContext.Stacks["Share"].Push("List Accounts"))
345
            {
346
                if (Log.IsDebugEnabled) Log.DebugFormat("START");
347

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

    
351
                //If the result is empty, return an empty list,
352
                var infos = String.IsNullOrWhiteSpace(content)
353
                                ? new List<ShareAccountInfo>()
354
                            //Otherwise deserialize the account list into a list of ShareAccountInfos
355
                                : JsonConvert.DeserializeObject<IList<ShareAccountInfo>>(content);
356

    
357
                Log.DebugFormat("END");
358
                return infos;
359
            }
360
        }
361

    
362

    
363
        /// <summary>
364
        /// Request listing of all objects in a container modified since a specific time.
365
        /// If the *since* value is missing, return all objects
366
        /// </summary>
367
        /// <param name="knownContainers">Use the since variable only for the containers listed in knownContainers. Unknown containers are considered new
368
        /// and should be polled anyway
369
        /// </param>
370
        /// <param name="since"></param>
371
        /// <returns></returns>
372
        public IList<ObjectInfo> ListSharedObjects(HashSet<string> knownContainers,DateTime? since = null )
373
        {
374

    
375
            using (ThreadContext.Stacks["Share"].Push("List Objects"))
376
            {
377
                if (Log.IsDebugEnabled) Log.DebugFormat("START");
378
                //'since' is not used here because we need to have ListObjects return a NoChange result
379
                //for all shared accounts,containers
380

    
381
                Func<ContainerInfo, string> getKey = c => String.Format("{0}\\{1}", c.Account, c.Name);
382

    
383
                var containers = (from account in ListSharingAccounts()
384
                                 let conts = ListContainers(account.name).Result
385
                                 from container in conts
386
                                 select container).ToList();                
387
                var items = from container in containers 
388
                            let actualSince=knownContainers.Contains(getKey(container))?since:null
389
                            select ListObjects(container.Account , container.Name,  actualSince);
390
                var objects=items.SelectMany(r=> r).ToList();
391

    
392
                //For each object
393
                //Check parents recursively up to (but not including) the container.
394
                //If parents are missing, add them to the list
395
                //Need function to calculate all parent URLs
396
                objects = AddMissingParents(objects);
397
                
398
                //Store any new containers
399
                foreach (var container in containers)
400
                {
401
                    knownContainers.Add(getKey(container));
402
                }
403

    
404

    
405

    
406
                if (Log.IsDebugEnabled) Log.DebugFormat("END");
407
                return objects;
408
            }
409
        }
410

    
411
        private List<ObjectInfo> AddMissingParents(List<ObjectInfo> objects)
412
        {
413
            //TODO: Remove short-circuit when we decide to use Missing Parents functionality
414
            //return objects;
415

    
416
            var existingUris = objects.ToDictionary(o => o.Uri, o => o);
417
            foreach (var objectInfo in objects)
418
            {
419
                //Can be null when retrieving objects to show in selective sync
420
                if (objectInfo.Name == null)
421
                    continue;
422

    
423
                //No need to unescape here, the parts will be used to create new ObjectInfos
424
                var parts = objectInfo.Name.ToString().Split(new[]{'/'},StringSplitOptions.RemoveEmptyEntries);
425
                //If there is no parent, skip
426
                if (parts.Length == 1)
427
                    continue;
428
                var baseParts = new[]
429
                                  {
430
                                      objectInfo.Uri.Host, objectInfo.Uri.Segments[1].TrimEnd('/'),objectInfo.Account,objectInfo.Container.ToString()
431
                                  };
432
                for (var partIdx = 0; partIdx < parts.Length - 1; partIdx++)
433
                {
434
                    var nameparts = parts.Range(0, partIdx).ToArray();
435
                    var parentName= String.Join("/", nameparts);
436

    
437
                    var parentParts = baseParts.Concat(nameparts);
438
                    var parentUrl = objectInfo.Uri.Scheme+ "://" + String.Join("/", parentParts);
439
                    
440
                    var parentUri = new Uri(parentUrl, UriKind.Absolute);
441

    
442
                    ObjectInfo existingInfo;
443
                    if (!existingUris.TryGetValue(parentUri,out existingInfo))
444
                    {
445
                        var h = parentUrl.GetHashCode();
446
                        var reverse = new string(parentUrl.Reverse().ToArray());
447
                        var rh = reverse.GetHashCode();
448
                        var b1 = BitConverter.GetBytes(h);
449
                        var b2 = BitConverter.GetBytes(rh);
450
                        var g = new Guid(0,0,0,b1.Concat(b2).ToArray());
451
                        
452

    
453
                        existingUris[parentUri] = new ObjectInfo
454
                                                      {
455
                                                          Account = objectInfo.Account,
456
                                                          Container = objectInfo.Container,
457
                                                          Content_Type = ObjectInfo.CONTENT_TYPE_DIRECTORY,
458
                                                          ETag = Signature.MERKLE_EMPTY,
459
                                                          X_Object_Hash = Signature.MERKLE_EMPTY,
460
                                                          Name=new Uri(parentName,UriKind.Relative),
461
                                                          StorageUri=objectInfo.StorageUri,
462
                                                          Bytes = 0,
463
                                                          UUID=g.ToString(),                                                          
464
                                                      };
465
                    }
466
                }
467
            }
468
            return existingUris.Values.ToList();
469
        }
470

    
471
        public void SetTags(ObjectInfo target,IDictionary<string,string> tags)
472
        {
473
            if (String.IsNullOrWhiteSpace(Token))
474
                throw new InvalidOperationException("The Token is not set");
475
            if (StorageUrl == null)
476
                throw new InvalidOperationException("The StorageUrl is not set");
477
            if (target == null)
478
                throw new ArgumentNullException("target");
479
            Contract.EndContractBlock();
480

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

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

    
488
                    client.BaseAddress = GetAccountUrl(target.Account);
489

    
490
                    client.Parameters.Clear();
491
                    client.Parameters.Add("update", "");
492

    
493
                    foreach (var tag in tags)
494
                    {
495
                        var headerTag = String.Format("X-Object-Meta-{0}", tag.Key);
496
                        client.Headers.Add(headerTag, tag.Value);
497
                    }
498
                    
499
                    client.DownloadStringWithRetryRelative(target.Container, 3);
500

    
501
                    
502
                    client.AssertStatusOK("SetTags failed");
503
                    //If the status is NOT ACCEPTED we have a problem
504
                    if (client.StatusCode != HttpStatusCode.Accepted)
505
                    {
506
                        Log.Error("Failed to set tags");
507
                        throw new Exception("Failed to set tags");
508
                    }
509

    
510
                    if (Log.IsDebugEnabled) Log.DebugFormat("END");
511
                }
512
            }
513

    
514

    
515
        }
516

    
517
        public void ShareObject(string account, Uri container, Uri objectName, string shareTo, bool read, bool write)
518
        {
519
            if (String.IsNullOrWhiteSpace(Token))
520
                throw new InvalidOperationException("The Token is not set");
521
            if (StorageUrl==null)
522
                throw new InvalidOperationException("The StorageUrl is not set");
523
            if (container==null)
524
                throw new ArgumentNullException("container");
525
            if (container.IsAbsoluteUri)
526
                throw new ArgumentException("container");
527
            if (objectName==null)
528
                throw new ArgumentNullException("objectName");
529
            if (objectName.IsAbsoluteUri)
530
                throw new ArgumentException("objectName");
531
            if (String.IsNullOrWhiteSpace(account))
532
                throw new ArgumentNullException("account");
533
            if (String.IsNullOrWhiteSpace(shareTo))
534
                throw new ArgumentNullException("shareTo");
535
            Contract.EndContractBlock();
536

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

    
544
                    client.BaseAddress = GetAccountUrl(account);
545

    
546
                    client.Parameters.Clear();
547
                    client.Parameters.Add("format", "json");
548

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

    
556
                    var content = client.DownloadStringWithRetryRelative(container, 3);
557

    
558
                    client.AssertStatusOK("ShareObject failed");
559

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

    
566
                    if (Log.IsDebugEnabled) Log.DebugFormat("END");
567
                }
568
            }
569

    
570

    
571
        }
572

    
573
        public AccountInfo GetAccountPolicies(AccountInfo accountInfo)
574
        {
575
            if (accountInfo==null)
576
                throw new ArgumentNullException("accountInfo");
577
            Contract.EndContractBlock();
578

    
579
            using (ThreadContext.Stacks["Account"].Push("GetPolicies"))
580
            {
581
                if (Log.IsDebugEnabled) Log.DebugFormat("START");
582

    
583
                if (_baseClient == null)
584
                {
585
                    _baseClient = new RestClient
586
                    {
587
                        BaseAddress = accountInfo.StorageUri.ToString(),
588
                        Timeout = 30000,
589
                        Retries = 3,
590
                    };
591
                }
592

    
593
                using (var client = new RestClient(_baseClient))
594
                {
595
                    if (!String.IsNullOrWhiteSpace(accountInfo.UserName))
596
                        client.BaseAddress = GetAccountUrl(accountInfo.UserName);
597

    
598
                    client.Parameters.Clear();
599
                    client.Parameters.Add("format", "json");                    
600
                    client.Head(_emptyUri, 3);
601

    
602
                    var quotaValue=client.ResponseHeaders["X-Account-Policy-Quota"];
603
                    var bytesValue= client.ResponseHeaders["X-Account-Bytes-Used"];
604

    
605
                    long quota, bytes;
606
                    if (long.TryParse(quotaValue, out quota))
607
                        accountInfo.Quota = quota;
608
                    if (long.TryParse(bytesValue, out bytes))
609
                        accountInfo.BytesUsed = bytes;
610
                    
611
                    return accountInfo;
612

    
613
                }
614

    
615
            }
616
        }
617

    
618
        public void UpdateMetadata(ObjectInfo objectInfo)
619
        {
620
            if (objectInfo == null)
621
                throw new ArgumentNullException("objectInfo");
622
            Contract.EndContractBlock();
623

    
624
            using (ThreadContext.Stacks["Objects"].Push("UpdateMetadata"))
625
            {
626
                if (Log.IsDebugEnabled) Log.DebugFormat("START");
627

    
628

    
629
                using(var client=new RestClient(_baseClient))
630
                {
631

    
632
                    client.BaseAddress = GetAccountUrl(objectInfo.Account);
633
                    
634
                    client.Parameters.Clear();
635
                    
636

    
637
                    //Set Tags
638
                    foreach (var tag in objectInfo.Tags)
639
                    {
640
                        var headerTag = String.Format("X-Object-Meta-{0}", tag.Key);
641
                        client.Headers.Add(headerTag, tag.Value);
642
                    }
643

    
644
                    //Set Permissions
645

    
646
                    var permissions=objectInfo.GetPermissionString();
647
                    client.SetNonEmptyHeaderValue("X-Object-Sharing",permissions);
648

    
649
                    client.SetNonEmptyHeaderValue("Content-Disposition",objectInfo.ContendDisposition);
650
                    client.SetNonEmptyHeaderValue("Content-Encoding",objectInfo.ContentEncoding);
651
                    client.SetNonEmptyHeaderValue("X-Object-Manifest",objectInfo.Manifest);
652
                    var isPublic = objectInfo.IsPublic.ToString().ToLower();
653
                    client.Headers.Add("X-Object-Public", isPublic);
654

    
655

    
656
                    var address = String.Format("{0}/{1}?update=",objectInfo.Container, objectInfo.Name);
657
                    client.PostWithRetry(new Uri(address,UriKind.Relative),"application/xml");
658
                    
659
                    client.AssertStatusOK("UpdateMetadata failed");
660
                    //If the status is NOT ACCEPTED or OK we have a problem
661
                    if (!(client.StatusCode == HttpStatusCode.Accepted || client.StatusCode == HttpStatusCode.OK))
662
                    {
663
                        Log.Error("Failed to update metadata");
664
                        throw new Exception("Failed to update metadata");
665
                    }
666

    
667
                    if (Log.IsDebugEnabled) Log.DebugFormat("END");
668
                }
669
            }
670

    
671
        }
672

    
673
        public void UpdateMetadata(ContainerInfo containerInfo)
674
        {
675
            if (containerInfo == null)
676
                throw new ArgumentNullException("containerInfo");
677
            Contract.EndContractBlock();
678

    
679
            using (ThreadContext.Stacks["Containers"].Push("UpdateMetadata"))
680
            {
681
                if (Log.IsDebugEnabled) Log.DebugFormat("START");
682

    
683

    
684
                using(var client=new RestClient(_baseClient))
685
                {
686

    
687
                    client.BaseAddress = GetAccountUrl(containerInfo.Account);
688
                    
689
                    client.Parameters.Clear();
690
                    
691

    
692
                    //Set Tags
693
                    foreach (var tag in containerInfo.Tags)
694
                    {
695
                        var headerTag = String.Format("X-Container-Meta-{0}", tag.Key);
696
                        client.Headers.Add(headerTag, tag.Value);
697
                    }
698

    
699
                    
700
                    //Set Policies
701
                    foreach (var policy in containerInfo.Policies)
702
                    {
703
                        var headerPolicy = String.Format("X-Container-Policy-{0}", policy.Key);
704
                        client.Headers.Add(headerPolicy, policy.Value);
705
                    }
706

    
707

    
708
                    var uriBuilder = client.GetAddressBuilder(containerInfo.Name,_emptyUri);
709
                    var uri = uriBuilder.Uri;
710

    
711
                    client.UploadValues(uri,new NameValueCollection());
712

    
713

    
714
                    client.AssertStatusOK("UpdateMetadata failed");
715
                    //If the status is NOT ACCEPTED or OK we have a problem
716
                    if (!(client.StatusCode == HttpStatusCode.Accepted || client.StatusCode == HttpStatusCode.OK))
717
                    {
718
                        Log.Error("Failed to update metadata");
719
                        throw new Exception("Failed to update metadata");
720
                    }
721

    
722
                    if (Log.IsDebugEnabled) Log.DebugFormat("END");
723
                }
724
            }
725

    
726
        }
727

    
728
       
729

    
730

    
731
        public IList<ObjectInfo> ListObjects(string account, Uri container, DateTime? since = null)
732
        {
733
            if (container==null)
734
                throw new ArgumentNullException("container");
735
            if (container.IsAbsoluteUri)
736
                throw new ArgumentException("container");
737
            Contract.EndContractBlock();
738

    
739
            using (ThreadContext.Stacks["Objects"].Push("List"))
740
            {
741

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

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

    
747
                //304 will result in an empty string. Empty containers return an empty json array
748
                if (String.IsNullOrWhiteSpace(content))
749
                     return new[] {new NoModificationInfo(account, container)};
750

    
751
                 //If the result is empty, return an empty list,
752
                 var infos = String.IsNullOrWhiteSpace(content)
753
                                 ? new List<ObjectInfo>()
754
                             //Otherwise deserialize the object list into a list of ObjectInfos
755
                                 : JsonConvert.DeserializeObject<IList<ObjectInfo>>(content);
756

    
757
                 foreach (var info in infos)
758
                 {
759
                     info.Container = container;
760
                     info.Account = account;
761
                     info.StorageUri = StorageUrl;
762
                 }
763
                 if (Log.IsDebugEnabled) Log.DebugFormat("END");
764
                 return infos;
765
            }
766
        }
767

    
768
        public IList<ObjectInfo> ListObjects(string account, Uri container, Uri folder, DateTime? since = null)
769
        {
770
            if (container==null)
771
                throw new ArgumentNullException("container");
772
            if (container.IsAbsoluteUri)
773
                throw new ArgumentException("container");
774
            Contract.EndContractBlock();
775

    
776
            using (ThreadContext.Stacks["Objects"].Push("List"))
777
            {
778
                if (Log.IsDebugEnabled) Log.DebugFormat("START");
779

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

    
784
                //304 will result in an empty string. Empty containers return an empty json array
785
                if (String.IsNullOrWhiteSpace(content))
786
                    return new[] { new NoModificationInfo(account, container) };
787

    
788

    
789
                var infos = JsonConvert.DeserializeObject<IList<ObjectInfo>>(content);
790
                foreach (var info in infos)
791
                {
792
                    info.Account = account;
793
                    if (info.Container == null)
794
                        info.Container = container;
795
                    info.StorageUri = StorageUrl;
796
                }
797
                if (Log.IsDebugEnabled) Log.DebugFormat("END");
798
                return infos;
799
/*
800
                using (var client = new RestClient(_baseClient))
801
                {
802
                    if (!String.IsNullOrWhiteSpace(account))
803
                        client.BaseAddress = GetAccountUrl(account);
804

    
805
                    client.Parameters.Clear();
806
                    client.Parameters.Add("format", "json");
807
                    client.Parameters.Add("path", folder.ToString());
808
                    client.IfModifiedSince = since;
809
                    var content = client.DownloadStringWithRetryRelative(container, 3);
810
                    client.AssertStatusOK("ListObjects failed");
811

    
812
                    if (client.StatusCode==HttpStatusCode.NotModified)
813
                        return new[]{new NoModificationInfo(account,container,folder)};
814

    
815
                    var infos = JsonConvert.DeserializeObject<IList<ObjectInfo>>(content);
816
                    foreach (var info in infos)
817
                    {
818
                        info.Account = account;
819
                        if (info.Container == null)
820
                            info.Container = container;
821
                        info.StorageUri = StorageUrl;
822
                    }
823
                    if (Log.IsDebugEnabled) Log.DebugFormat("END");
824
                    return infos;
825
                }
826
*/
827
            }
828
        }
829

    
830
 
831
        public bool ContainerExists(string account, Uri container)
832
        {
833
            if (container==null)
834
                throw new ArgumentNullException("container", "The container property can't be empty");
835
            if (container.IsAbsoluteUri)
836
                throw new ArgumentException( "The container must be relative","container");
837
            Contract.EndContractBlock();
838

    
839
            using (ThreadContext.Stacks["Containters"].Push("Exists"))
840
            {
841
                if (Log.IsDebugEnabled) Log.DebugFormat("START");
842

    
843
                var targetUri = GetTargetUri(account).Combine(container);
844

    
845
                using (var response = _baseHttpClient.HeadAsyncWithRetries(targetUri, 3).Result)
846
                {
847

    
848
                    bool result;
849
                    switch (response.StatusCode)
850
                    {
851
                        case HttpStatusCode.OK:
852
                        case HttpStatusCode.NoContent:
853
                            result = true;
854
                            break;
855
                        case HttpStatusCode.NotFound:
856
                            result = false;
857
                            break;
858
                        default:
859
                            throw CreateWebException("ContainerExists", response.StatusCode);
860
                    }
861
                    if (Log.IsDebugEnabled) Log.DebugFormat("END");
862

    
863
                    return result;
864
                }
865
/*
866
                using (var client = new RestClient(_baseClient))
867
                {
868
                    if (!String.IsNullOrWhiteSpace(account))
869
                        client.BaseAddress = GetAccountUrl(account);
870

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

    
889
                    return result;
890
                }
891
*/
892
                
893
            }
894
        }
895

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

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

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

    
920
                var targetUri=GetTargetUri(account).Combine(container).Combine(objectName);
921

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

    
936
/*
937
            using (var client = new RestClient(_baseClient))
938
            {
939
                if (!String.IsNullOrWhiteSpace(account))
940
                    client.BaseAddress = GetAccountUrl(account);
941

    
942
                client.Parameters.Clear();
943
                client.Head(container.Combine(objectName), 3);
944

    
945
                switch (client.StatusCode)
946
                {
947
                    case HttpStatusCode.OK:
948
                    case HttpStatusCode.NoContent:
949
                        return true;
950
                    case HttpStatusCode.NotFound:
951
                        return false;
952
                    default:
953
                        throw CreateWebException("ObjectExists", client.StatusCode);
954
                }
955
            }
956
*/
957

    
958
        }
959

    
960
        public ObjectInfo GetObjectInfo(string account, Uri container, Uri objectName)
961
        {
962
            if (container == null)
963
                throw new ArgumentNullException("container", "The container property can't be empty");
964
            if (container.IsAbsoluteUri)
965
                throw new ArgumentException("The container must be relative","container");
966
            if (objectName == null)
967
                throw new ArgumentNullException("objectName", "The objectName property can't be empty");
968
            if (objectName.IsAbsoluteUri)
969
                throw new ArgumentException("The objectName must be relative","objectName");
970
            Contract.EndContractBlock();
971

    
972
            using (ThreadContext.Stacks["Objects"].Push("GetObjectInfo"))
973
            {                
974

    
975
                using (var client = new RestClient(_baseClient))
976
                {
977
                    if (!String.IsNullOrWhiteSpace(account))
978
                        client.BaseAddress = GetAccountUrl(account);
979
                    try
980
                    {
981
                        client.Parameters.Clear();
982

    
983
                        client.Head(container.Combine(objectName), 3);
984

    
985
                        if (client.TimedOut)
986
                            return ObjectInfo.Empty;
987

    
988
                        switch (client.StatusCode)
989
                        {
990
                            case HttpStatusCode.OK:
991
                            case HttpStatusCode.NoContent:
992
                                var keys = client.ResponseHeaders.AllKeys.AsQueryable();
993
                                var tags = client.GetMeta("X-Object-Meta-");
994
                                var extensions = (from key in keys
995
                                                  where key.StartsWith("X-Object-") && !key.StartsWith("X-Object-Meta-")
996
                                                  select new {Name = key, Value = client.ResponseHeaders[key]})
997
                                    .ToDictionary(t => t.Name, t => t.Value);
998

    
999
                                var permissions=client.GetHeaderValue("X-Object-Sharing", true);
1000
                                
1001
                                
1002
                                var info = new ObjectInfo
1003
                                               {
1004
                                                   Account = account,
1005
                                                   Container = container,
1006
                                                   Name = objectName,
1007
                                                   ETag = client.GetHeaderValue("ETag"),
1008
                                                   UUID=client.GetHeaderValue("X-Object-UUID"),
1009
                                                   X_Object_Hash = client.GetHeaderValue("X-Object-Hash"),
1010
                                                   Content_Type = client.GetHeaderValue("Content-Type"),
1011
                                                   Bytes = Convert.ToInt64(client.GetHeaderValue("Content-Length",true)),
1012
                                                   Tags = tags,
1013
                                                   Last_Modified = client.LastModified,
1014
                                                   Extensions = extensions,
1015
                                                   ContentEncoding=client.GetHeaderValue("Content-Encoding",true),
1016
                                                   ContendDisposition = client.GetHeaderValue("Content-Disposition",true),
1017
                                                   Manifest=client.GetHeaderValue("X-Object-Manifest",true),
1018
                                                   PublicUrl=client.GetHeaderValue("X-Object-Public",true),  
1019
                                                   StorageUri=StorageUrl,
1020
                                               };
1021
                                info.SetPermissions(permissions);
1022
                                return info;
1023
                            case HttpStatusCode.NotFound:
1024
                                return ObjectInfo.Empty;
1025
                            default:
1026
                                throw new WebException(
1027
                                    String.Format("[FAIL] GetObjectInfo for {0} failed with unexpected status code {1}",
1028
                                                  objectName, client.StatusCode));
1029
                        }
1030

    
1031
                    }
1032
                    catch (RetryException)
1033
                    {
1034
                        Log.WarnFormat("[RETRY FAIL] GetObjectInfo for {0} failed.",objectName);
1035
                        return ObjectInfo.Empty;
1036
                    }
1037
                    catch (WebException e)
1038
                    {
1039
                        Log.Error(
1040
                            String.Format("[FAIL] GetObjectInfo for {0} failed with unexpected status code {1}",
1041
                                          objectName, client.StatusCode), e);
1042
                        throw;
1043
                    }
1044
                }                
1045
            }
1046

    
1047
        }
1048

    
1049
        
1050

    
1051
        public void CreateFolder(string account, Uri container, Uri folder)
1052
        {
1053
            if (container == null)
1054
                throw new ArgumentNullException("container", "The container property can't be empty");
1055
            if (container.IsAbsoluteUri)
1056
                throw new ArgumentException("The container must be relative","container");
1057
            if (folder == null)
1058
                throw new ArgumentNullException("folder", "The objectName property can't be empty");
1059
            if (folder.IsAbsoluteUri)
1060
                throw new ArgumentException("The objectName must be relative","folder");
1061
            Contract.EndContractBlock();
1062

    
1063
            var folderUri=container.Combine(folder);            
1064
            var targetUri = GetTargetUri(account).Combine(folderUri);
1065
            var message = new HttpRequestMessage(HttpMethod.Put, targetUri);
1066
            
1067
            message.Headers.Add("Content-Type", ObjectInfo.CONTENT_TYPE_DIRECTORY);
1068
            message.Headers.Add("Content-Length", "0");
1069
            using (var response = _baseHttpClient.SendAsyncWithRetries(message, 3).Result)
1070
            {
1071
                if (response.StatusCode != HttpStatusCode.Created && response.StatusCode != HttpStatusCode.Accepted)
1072
                    throw CreateWebException("CreateFolder", response.StatusCode);
1073
            }
1074
/*
1075
            using (var client = new RestClient(_baseClient))
1076
            {
1077
                if (!String.IsNullOrWhiteSpace(account))
1078
                    client.BaseAddress = GetAccountUrl(account);
1079

    
1080
                client.Parameters.Clear();
1081
                client.Headers.Add("Content-Type", ObjectInfo.CONTENT_TYPE_DIRECTORY);
1082
                client.Headers.Add("Content-Length", "0");
1083
                client.PutWithRetry(folderUri, 3);
1084

    
1085
                if (client.StatusCode != HttpStatusCode.Created && client.StatusCode != HttpStatusCode.Accepted)
1086
                    throw CreateWebException("CreateFolder", client.StatusCode);
1087
            }
1088
*/
1089
        }
1090

    
1091
        private Dictionary<string, string> GetMeta(HttpResponseMessage response,string metaPrefix)
1092
        {
1093
            if (String.IsNullOrWhiteSpace(metaPrefix))
1094
                throw new ArgumentNullException("metaPrefix");
1095
            Contract.EndContractBlock();
1096

    
1097
            var dict = (from header in response.Headers
1098
                        where header.Key.StartsWith(metaPrefix)
1099
                         select new { Name = header.Key, Value = String.Join(",", header.Value) })
1100
                        .ToDictionary(t => t.Name, t => t.Value);
1101

    
1102
          
1103
            return dict;
1104
        }
1105

    
1106

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

    
1115
            var targetUri = GetTargetUri(account).Combine(container);
1116
            var message = new HttpRequestMessage(HttpMethod.Head, targetUri);
1117
            using (var response = _baseHttpClient.SendAsyncWithRetries(message, 3).Result)
1118
            {
1119
                if (Log.IsDebugEnabled)
1120
                    Log.DebugFormat("ContainerInfo data: {0}\n{1}",response,response.Content.ReadAsStringAsync().Result);
1121
                switch (response.StatusCode)
1122
                {
1123
                    case HttpStatusCode.OK:
1124
                    case HttpStatusCode.NoContent:
1125
                        var tags = GetMeta(response,"X-Container-Meta-");
1126
                        var policies = GetMeta(response,"X-Container-Policy-");
1127

    
1128
                        var containerInfo = new ContainerInfo
1129
                                                {
1130
                                                    Account = account,
1131
                                                    Name = container,
1132
                                                    StorageUrl = StorageUrl.ToString(),
1133
                                                    Count =long.Parse(response.Headers.GetValues("X-Container-Object-Count").First()),
1134
                                                    Bytes = long.Parse(response.Headers.GetValues("X-Container-Bytes-Used").First()),
1135
                                                    BlockHash = response.Headers.GetValues("X-Container-Block-Hash").First(),
1136
                                                    BlockSize =
1137
                                                        int.Parse(response.Headers.GetValues("X-Container-Block-Size").First()),
1138
                                                    Last_Modified = response.Content.Headers.LastModified,
1139
                                                    Tags = tags,
1140
                                                    Policies = policies
1141
                                                };
1142

    
1143

    
1144
                        return containerInfo;
1145
                    case HttpStatusCode.NotFound:
1146
                        return ContainerInfo.Empty;
1147
                    default:
1148
                        throw CreateWebException("GetContainerInfo", response.StatusCode);
1149
                }
1150
            }            
1151
        }
1152

    
1153
        public void CreateContainer(string account, Uri container)
1154
        {
1155
            if (container == null)
1156
                throw new ArgumentNullException("container", "The container property can't be empty");
1157
            if (container.IsAbsoluteUri)
1158
                throw new ArgumentException("The container must be relative","container");
1159
            Contract.EndContractBlock();
1160

    
1161
            var targetUri=GetTargetUri(account).Combine(container);
1162
            var message = new HttpRequestMessage(HttpMethod.Put, targetUri);
1163
            message.Headers.Add("Content-Length", "0");
1164
            using (var response = _baseHttpClient.SendAsyncWithRetries(message, 3).Result)
1165
            {            
1166
                var expectedCodes = new[] {HttpStatusCode.Created, HttpStatusCode.Accepted, HttpStatusCode.OK};
1167
                if (!expectedCodes.Contains(response.StatusCode))
1168
                    throw CreateWebException("CreateContainer", response.StatusCode);
1169
            }
1170
/*
1171
            using (var client = new RestClient(_baseClient))
1172
            {
1173
                if (!String.IsNullOrWhiteSpace(account))
1174
                    client.BaseAddress = GetAccountUrl(account);
1175

    
1176
                client.PutWithRetry(container, 3);
1177
                var expectedCodes = new[] {HttpStatusCode.Created, HttpStatusCode.Accepted, HttpStatusCode.OK};
1178
                if (!expectedCodes.Contains(client.StatusCode))
1179
                    throw CreateWebException("CreateContainer", client.StatusCode);
1180
            }
1181
*/
1182
        }
1183

    
1184
        public async Task WipeContainer(string account, Uri container)
1185
        {
1186
            if (container == null)
1187
                throw new ArgumentNullException("container", "The container property can't be empty");
1188
            if (container.IsAbsoluteUri)
1189
                throw new ArgumentException("The container must be relative", "container");
1190
            Contract.EndContractBlock();
1191

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

    
1195

    
1196
        public async Task DeleteContainer(string account, Uri container)
1197
        {
1198
            if (container == null)
1199
                throw new ArgumentNullException("container", "The container property can't be empty");
1200
            if (container.IsAbsoluteUri)
1201
                throw new ArgumentException("The container must be relative","container");
1202
            Contract.EndContractBlock();
1203

    
1204
            var targetUri = GetTargetUri(account).Combine(container);
1205
            var message = new HttpRequestMessage(HttpMethod.Delete, targetUri);
1206
            using (var response = await _baseHttpClient.SendAsyncWithRetries(message, 3).ConfigureAwait(false))
1207
            {
1208
                var expectedCodes = new[] { HttpStatusCode.NotFound, HttpStatusCode.NoContent };
1209
                if (!expectedCodes.Contains(response.StatusCode))
1210
                    throw CreateWebException("DeleteContainer", response.StatusCode);
1211
            }
1212

    
1213
        }
1214

    
1215
        /// <summary>
1216
        /// 
1217
        /// </summary>
1218
        /// <param name="account"></param>
1219
        /// <param name="container"></param>
1220
        /// <param name="objectName"></param>
1221
        /// <param name="fileName"></param>
1222
        /// <param name="cancellationToken"> </param>
1223
        /// <returns></returns>
1224
        /// <remarks>This method should have no timeout or a very long one</remarks>
1225
        //Asynchronously download the object specified by *objectName* in a specific *container* to 
1226
        // a local file
1227
        public async Task GetObject(string account, Uri container, Uri objectName, string fileName,CancellationToken cancellationToken)
1228
        {
1229
            if (container == null)
1230
                throw new ArgumentNullException("container", "The container property can't be empty");
1231
            if (container.IsAbsoluteUri)
1232
                throw new ArgumentException("The container must be relative","container");
1233
            if (objectName == null)
1234
                throw new ArgumentNullException("objectName", "The objectName property can't be empty");
1235
            if (objectName.IsAbsoluteUri)
1236
                throw new ArgumentException("The objectName must be relative","objectName");
1237
            Contract.EndContractBlock();
1238
                        
1239

    
1240
            try
1241
            {
1242
                //WebClient, and by extension RestClient, are not thread-safe. Create a new RestClient
1243
                //object to avoid concurrency errors.
1244
                //
1245
                //Download operations take a long time therefore they have no timeout.
1246
                using(var client = new RestClient(_baseClient) { Timeout = 0 })
1247
                {
1248
                    if (!String.IsNullOrWhiteSpace(account))
1249
                        client.BaseAddress = GetAccountUrl(account);
1250

    
1251
                    //The container and objectName are relative names. They are joined with the client's
1252
                    //BaseAddress to create the object's absolute address
1253
                    var builder = client.GetAddressBuilder(container, objectName);
1254
                    var uri = builder.Uri;
1255

    
1256
                    //Download progress is reported to the Trace log
1257
                    Log.InfoFormat("[GET] START {0}", objectName);
1258
                    /*client.DownloadProgressChanged += (sender, args) =>
1259
                                                      Log.InfoFormat("[GET PROGRESS] {0} {1}% {2} of {3}",
1260
                                                                     fileName, args.ProgressPercentage,
1261
                                                                     args.BytesReceived,
1262
                                                                     args.TotalBytesToReceive);*/
1263
                    var progress = new Progress<DownloadProgressChangedEventArgs>(args =>
1264
                                {
1265
                                    Log.InfoFormat("[GET PROGRESS] {0} {1}% {2} of {3}",
1266
                                                   fileName, args.ProgressPercentage,
1267
                                                   args.BytesReceived,
1268
                                                   args.TotalBytesToReceive);
1269
                                    if (DownloadProgressChanged!=null)
1270
                                        DownloadProgressChanged(this, new DownloadArgs(args));
1271
                                });
1272
                    
1273
                    //Start downloading the object asynchronously                    
1274
                    await client.DownloadFileTaskAsync(uri, fileName, cancellationToken,progress).ConfigureAwait(false);
1275

    
1276
                    //Once the download completes
1277
                    //Delete the local client object
1278
                }
1279
                //And report failure or completion
1280
            }
1281
            catch (Exception exc)
1282
            {
1283
                Log.ErrorFormat("[GET] FAIL {0} with {1}", objectName, exc);
1284
                throw;
1285
            }
1286

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

    
1289

    
1290
        }
1291

    
1292
        public async Task<IList<string>> PutHashMap(string account, Uri container, Uri objectName, TreeHash hash)
1293
        {
1294
            if (container == null)
1295
                throw new ArgumentNullException("container", "The container property can't be empty");
1296
            if (container.IsAbsoluteUri)
1297
                throw new ArgumentException("The container must be relative","container");
1298
            if (objectName == null)
1299
                throw new ArgumentNullException("objectName", "The objectName property can't be empty");
1300
            if (objectName.IsAbsoluteUri)
1301
                throw new ArgumentException("The objectName must be relative","objectName");
1302
            if (hash == null)
1303
                throw new ArgumentNullException("hash");
1304
            if (String.IsNullOrWhiteSpace(Token))
1305
                throw new InvalidOperationException("Invalid Token");
1306
            if (StorageUrl == null)
1307
                throw new InvalidOperationException("Invalid Storage Url");
1308
            Contract.EndContractBlock();
1309

    
1310
            
1311

    
1312
            //The container and objectName are relative names. They are joined with the client's
1313
            //BaseAddress to create the object's absolute address
1314

    
1315
            var targetUri = GetTargetUri(account).Combine(container).Combine(objectName);
1316
  
1317

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

    
1320
            
1321
            //Send the tree hash as Json to the server            
1322
            var jsonHash = hash.ToJson();
1323
            if (Log.IsDebugEnabled)
1324
                Log.DebugFormat("Hashes:\r\n{0}", jsonHash);
1325

    
1326
            var message = new HttpRequestMessage(HttpMethod.Put, uri)
1327
            {
1328
                Content = new StringContent(jsonHash)
1329
            };
1330
            message.Headers.Add("ETag",hash.TopHash.ToHashString());
1331
            
1332
            //Don't use a timeout because putting the hashmap may be a long process
1333

    
1334
            using (var response = await _baseHttpClientNoTimeout.SendAsyncWithRetries(message, 3).ConfigureAwait(false))
1335
            {
1336
                var empty = (IList<string>)new List<string>();
1337
                
1338
                switch (response.StatusCode)
1339
                {
1340
                    case HttpStatusCode.Created:
1341
                        //The server will respond either with 201-created if all blocks were already on the server
1342
                        return empty;
1343
                    case HttpStatusCode.Conflict:
1344
                        //or with a 409-conflict and return the list of missing parts
1345
                        using (var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false))
1346
                        using(var reader=stream.GetLoggedReader(Log))
1347
                        {                            
1348
                            var serializer = new JsonSerializer();                            
1349
                            serializer.Error += (sender, args) => Log.ErrorFormat("Deserialization error at [{0}] [{1}]", args.ErrorContext.Error, args.ErrorContext.Member);
1350
                            var hashes = (List<string>)serializer.Deserialize(reader, typeof(List<string>));
1351
                            return hashes;
1352
                        }                        
1353
                    default:
1354
                        //All other cases are unexpected
1355
                        //Ensure that failure codes raise exceptions
1356
                        response.EnsureSuccessStatusCode();
1357
                        //And log any other codes as warngings, but continute processing
1358
                        Log.WarnFormat("Unexcpected status code when putting map: {0} - {1}",response.StatusCode,response.ReasonPhrase);
1359
                        return empty;
1360
                }
1361
            }
1362

    
1363
        }
1364

    
1365

    
1366
        public async Task<byte[]> GetBlock(string account, Uri container, Uri relativeUrl, long start, long? end, CancellationToken cancellationToken)
1367
        {
1368
            if (String.IsNullOrWhiteSpace(Token))
1369
                throw new InvalidOperationException("Invalid Token");
1370
            if (StorageUrl == null)
1371
                throw new InvalidOperationException("Invalid Storage Url");
1372
            if (container == null)
1373
                throw new ArgumentNullException("container", "The container property can't be empty");
1374
            if (container.IsAbsoluteUri)
1375
                throw new ArgumentException("The container must be relative","container");
1376
            if (relativeUrl == null)
1377
                throw new ArgumentNullException("relativeUrl");
1378
            if (end.HasValue && end < 0)
1379
                throw new ArgumentOutOfRangeException("end");
1380
            if (start < 0)
1381
                throw new ArgumentOutOfRangeException("start");
1382
            Contract.EndContractBlock();
1383

    
1384

    
1385
            var targetUri = GetTargetUri(account).Combine(container).Combine(relativeUrl);
1386
            var message = new HttpRequestMessage(HttpMethod.Get, targetUri);
1387
            message.Headers.Range=new RangeHeaderValue(start,end);
1388

    
1389
            //Don't use a timeout because putting the hashmap may be a long process
1390

    
1391
            IProgress<DownloadArgs> progress = new Progress<DownloadArgs>(args =>
1392
                {
1393
                    Log.DebugFormat("[GET PROGRESS] {0} {1}% {2} of {3}",
1394
                                    targetUri.Segments.Last(), args.ProgressPercentage,
1395
                                    args.BytesReceived,
1396
                                    args.TotalBytesToReceive);
1397

    
1398
                    if (DownloadProgressChanged!=null)
1399
                        DownloadProgressChanged(this,  args);
1400
                });
1401

    
1402

    
1403
            using (var response = await _baseHttpClientNoTimeout.SendAsyncWithRetries(message, 3, HttpCompletionOption.ResponseHeadersRead,
1404
                                                          cancellationToken).ConfigureAwait(false))
1405
            using (var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false))
1406
            using(var targetStream=new MemoryStream())
1407
            {
1408
                
1409
                long totalSize = response.Content.Headers.ContentLength ?? 0;
1410
                long total = 0;
1411
                var buffer = new byte[65536];
1412
                int read;
1413
                while ((read = await stream.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false)) != 0)
1414
                {
1415
                    total += read;
1416
                    progress.Report(new DownloadArgs(total, totalSize));
1417
                    await targetStream.WriteAsync(buffer, 0, read).ConfigureAwait(false);
1418
                }
1419

    
1420
                var result = targetStream.ToArray();
1421
                return result;
1422
            }
1423
       
1424
        }
1425

    
1426
        public event EventHandler<UploadArgs> UploadProgressChanged;
1427
        public event EventHandler<DownloadArgs> DownloadProgressChanged;
1428
        
1429

    
1430
        public async Task PostBlock(string account, Uri container, byte[] block, int offset, int count,string blockHash,CancellationToken token)
1431
        {
1432
            if (container == null)
1433
                throw new ArgumentNullException("container", "The container property can't be empty");
1434
            if (container.IsAbsoluteUri)
1435
                throw new ArgumentException("The container must be relative","container");
1436
            if (block == null)
1437
                throw new ArgumentNullException("block");
1438
            if (offset < 0 || offset >= block.Length)
1439
                throw new ArgumentOutOfRangeException("offset");
1440
            if (count < 0 || count > block.Length)
1441
                throw new ArgumentOutOfRangeException("count");
1442
            if (String.IsNullOrWhiteSpace(Token))
1443
                throw new InvalidOperationException("Invalid Token");
1444
            if (StorageUrl == null)
1445
                throw new InvalidOperationException("Invalid Storage Url");                        
1446
            Contract.EndContractBlock();
1447

    
1448

    
1449
            try
1450
            {
1451
                var containerUri = GetTargetUri(account).Combine(container);
1452
                var targetUri = new Uri(String.Format("{0}?update", containerUri));
1453

    
1454

    
1455
                //Don't use a timeout because putting the hashmap may be a long process
1456

    
1457

    
1458
                Log.InfoFormat("[BLOCK POST] START");
1459

    
1460

    
1461
                var progress = new Progress<UploadArgs>(args =>
1462
                {
1463
                    Log.InfoFormat("[BLOCK POST PROGRESS] {0}% {1} of {2}",
1464
                        args.ProgressPercentage,
1465
                        args.BytesSent,
1466
                        args.TotalBytesToSend);
1467
                    if (UploadProgressChanged != null)
1468
                        UploadProgressChanged(this,args);
1469
                });
1470

    
1471
                var message = new HttpRequestMessage(HttpMethod.Post, targetUri)
1472
                                  {
1473
                                      Content = new ByteArrayContentWithProgress(block, offset, count,progress)
1474
                                  };
1475
                message.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(@"application/octet-stream");
1476

    
1477
                //Send the block
1478
                using (var response = await _baseHttpClientNoTimeout.SendAsyncWithRetries(message, 3,HttpCompletionOption.ResponseContentRead,token).ConfigureAwait(false))
1479
                {                    
1480
                    Log.InfoFormat("[BLOCK POST PROGRESS] Completed ");
1481
                    response.EnsureSuccessStatusCode();
1482
                    var responseHash = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
1483
                    var cleanHash = responseHash.TrimEnd();
1484
                    Debug.Assert(blockHash==cleanHash);
1485
                    if (responseHash.Equals(cleanHash,StringComparison.OrdinalIgnoreCase))
1486
                        Log.ErrorFormat("Block hash mismatch posting to [{0}]:[{1}], expected [{2}] but was [{3}]",account,container,blockHash,responseHash);
1487
                }
1488
                Log.InfoFormat("[BLOCK POST] END");               
1489
            }
1490
            catch (TaskCanceledException )
1491
            {
1492
                Log.Info("Aborting block");
1493
                throw;
1494
            }
1495
            catch (Exception exc)
1496
            {
1497
                Log.ErrorFormat("[BLOCK POST] FAIL with \r{0}", exc);
1498
                throw;
1499
            }
1500
        }
1501

    
1502

    
1503
        public async Task<TreeHash> GetHashMap(string account, Uri container, Uri objectName)
1504
        {
1505
            if (container == null)
1506
                throw new ArgumentNullException("container", "The container property can't be empty");
1507
            if (container.IsAbsoluteUri)
1508
                throw new ArgumentException("The container must be relative","container");
1509
            if (objectName == null)
1510
                throw new ArgumentNullException("objectName", "The objectName property can't be empty");
1511
            if (objectName.IsAbsoluteUri)
1512
                throw new ArgumentException("The objectName must be relative","objectName");
1513
            if (String.IsNullOrWhiteSpace(Token))
1514
                throw new InvalidOperationException("Invalid Token");
1515
            if (StorageUrl == null)
1516
                throw new InvalidOperationException("Invalid Storage Url");
1517
            Contract.EndContractBlock();
1518

    
1519
            try
1520
            {
1521

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

    
1525
                //Start downloading the object asynchronously
1526
                var json = await GetStringAsync(targetUri, "").ConfigureAwait(false);
1527
                var treeHash = TreeHash.Parse(json);
1528
                Log.InfoFormat("[GET HASH] END {0}", objectName);
1529
                return treeHash;
1530

    
1531
            }
1532
            catch (Exception exc)
1533
            {
1534
                Log.ErrorFormat("[GET HASH] END {0} with {1}", objectName, exc);
1535
                throw;
1536
            }
1537

    
1538
        }
1539

    
1540

    
1541
        /// <summary>
1542
        /// 
1543
        /// </summary>
1544
        /// <param name="account"></param>
1545
        /// <param name="container"></param>
1546
        /// <param name="objectName"></param>
1547
        /// <param name="fileName"></param>
1548
        /// <param name="hash">Optional hash value for the file. If no hash is provided, the method calculates a new hash</param>
1549
        /// <param name="contentType"> </param>
1550
        /// <remarks>>This method should have no timeout or a very long one</remarks>
1551
        public async Task PutObject(string account, Uri container, Uri objectName, string fileName, string hash = Signature.MERKLE_EMPTY, string contentType = "application/octet-stream")
1552
        {
1553
            if (container == null)
1554
                throw new ArgumentNullException("container", "The container property can't be empty");
1555
            if (container.IsAbsoluteUri)
1556
                throw new ArgumentException("The container must be relative","container");
1557
            if (objectName == null)
1558
                throw new ArgumentNullException("objectName", "The objectName property can't be empty");
1559
            if (objectName.IsAbsoluteUri)
1560
                throw new ArgumentException("The objectName must be relative","objectName");
1561
            if (String.IsNullOrWhiteSpace(fileName))
1562
                throw new ArgumentNullException("fileName", "The fileName property can't be empty");
1563
            try
1564
            {
1565

    
1566
                using (var client = new RestClient(_baseClient) { Timeout = 0 })
1567
                {
1568
                    if (!String.IsNullOrWhiteSpace(account))
1569
                        client.BaseAddress = GetAccountUrl(account);
1570

    
1571
                    var builder = client.GetAddressBuilder(container, objectName);
1572
                    var uri = builder.Uri;
1573

    
1574
                    string etag = hash ;
1575

    
1576
                    client.Headers.Add("Content-Type", contentType);
1577
                    if (contentType!=ObjectInfo.CONTENT_TYPE_DIRECTORY)
1578
                        client.Headers.Add("ETag", etag);
1579

    
1580

    
1581
                    Log.InfoFormat("[PUT] START {0}", objectName);
1582
                    client.UploadProgressChanged += (sender, args) =>
1583
                                                        {
1584
                                                            using (ThreadContext.Stacks["PUT"].Push("Progress"))
1585
                                                            {
1586
                                                                Log.InfoFormat("{0} {1}% {2} of {3}", fileName,
1587
                                                                               args.ProgressPercentage,
1588
                                                                               args.BytesSent, args.TotalBytesToSend);
1589
                                                            }
1590
                                                        };
1591

    
1592
                    client.UploadFileCompleted += (sender, args) =>
1593
                                                      {
1594
                                                          using (ThreadContext.Stacks["PUT"].Push("Progress"))
1595
                                                          {
1596
                                                              Log.InfoFormat("Completed {0}", fileName);
1597
                                                          }
1598
                                                      }; 
1599
                    
1600
                    if (contentType==ObjectInfo.CONTENT_TYPE_DIRECTORY)
1601
                        await client.UploadDataTaskAsync(uri, "PUT", new byte[0]).ConfigureAwait(false);
1602
                    else
1603
                        await client.UploadFileTaskAsync(uri, "PUT", fileName).ConfigureAwait(false);
1604
                }
1605

    
1606
                Log.InfoFormat("[PUT] END {0}", objectName);
1607
            }
1608
            catch (Exception exc)
1609
            {
1610
                Log.ErrorFormat("[PUT] END {0} with {1}", objectName, exc);
1611
                throw;
1612
            }                
1613

    
1614
        }
1615
        
1616
        public void MoveObject(string account, Uri sourceContainer, Uri oldObjectName, Uri targetContainer, Uri newObjectName)
1617
        {
1618
            if (sourceContainer == null)
1619
                throw new ArgumentNullException("sourceContainer", "The sourceContainer property can't be empty");
1620
            if (sourceContainer.IsAbsoluteUri)
1621
                throw new ArgumentException("The sourceContainer must be relative","sourceContainer");
1622
            if (oldObjectName == null)
1623
                throw new ArgumentNullException("oldObjectName", "The oldObjectName property can't be empty");
1624
            if (oldObjectName.IsAbsoluteUri)
1625
                throw new ArgumentException("The oldObjectName must be relative","oldObjectName");
1626
            if (targetContainer == null)
1627
                throw new ArgumentNullException("targetContainer", "The targetContainer property can't be empty");
1628
            if (targetContainer.IsAbsoluteUri)
1629
                throw new ArgumentException("The targetContainer must be relative","targetContainer");
1630
            if (newObjectName == null)
1631
                throw new ArgumentNullException("newObjectName", "The newObjectName property can't be empty");
1632
            if (newObjectName.IsAbsoluteUri)
1633
                throw new ArgumentException("The newObjectName must be relative","newObjectName");
1634
            Contract.EndContractBlock();
1635

    
1636
            var baseUri = GetTargetUri(account);
1637
            var targetUri = baseUri.Combine(targetContainer).Combine(newObjectName);
1638
            var sourceUri = new Uri(String.Format("/{0}/{1}", sourceContainer, oldObjectName),UriKind.Relative);
1639

    
1640
            var message = new HttpRequestMessage(HttpMethod.Put, targetUri);
1641
            message.Headers.Add("X-Move-From", sourceUri.ToString());
1642
            using (var response = _baseHttpClient.SendAsyncWithRetries(message, 3).Result)
1643
            {
1644
                var expectedCodes = new[] {HttpStatusCode.OK, HttpStatusCode.NoContent, HttpStatusCode.Created};
1645
                if (!expectedCodes.Contains(response.StatusCode))
1646
                    throw CreateWebException("MoveObject", response.StatusCode);
1647
            }
1648
        }
1649

    
1650
        public void DeleteObject(string account, Uri sourceContainer, Uri objectName, bool isDirectory)
1651
        {
1652
            if (sourceContainer == null)
1653
                throw new ArgumentNullException("sourceContainer", "The sourceContainer property can't be empty");
1654
            if (sourceContainer.IsAbsoluteUri)
1655
                throw new ArgumentException("The sourceContainer must be relative","sourceContainer");
1656
            if (objectName == null)
1657
                throw new ArgumentNullException("objectName", "The objectName property can't be empty");
1658
            if (objectName.IsAbsoluteUri)
1659
                throw new ArgumentException("The objectName must be relative","objectName");
1660
            Contract.EndContractBlock();
1661

    
1662
            var targetUrl = FolderConstants.TrashContainer + "/" + objectName;
1663
/*
1664
            if (isDirectory)
1665
                targetUrl = targetUrl + "?delimiter=/";
1666
*/
1667

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

    
1670
            using (var client = new RestClient(_baseClient))
1671
            {
1672
                if (!String.IsNullOrWhiteSpace(account))
1673
                    client.BaseAddress = GetAccountUrl(account);
1674

    
1675
                client.Headers.Add("X-Move-From", sourceUrl);
1676
                client.AllowedStatusCodes.Add(HttpStatusCode.NotFound);
1677
                Log.InfoFormat("[TRASH] [{0}] to [{1}]",sourceUrl,targetUrl);
1678
                client.PutWithRetry(new Uri(targetUrl,UriKind.Relative), 3);
1679

    
1680
                var expectedCodes = new[] {HttpStatusCode.OK, HttpStatusCode.NoContent, HttpStatusCode.Created,HttpStatusCode.NotFound};
1681
                if (!expectedCodes.Contains(client.StatusCode))
1682
                    throw CreateWebException("DeleteObject", client.StatusCode);
1683
            }
1684
        }
1685

    
1686
      
1687
        private static WebException CreateWebException(string operation, HttpStatusCode statusCode)
1688
        {
1689
            return new WebException(String.Format("{0} failed with unexpected status code {1}", operation, statusCode));
1690
        }
1691

    
1692

    
1693
        public bool CanUpload(string account, ObjectInfo cloudFile)
1694
        {
1695
            Contract.Requires(!String.IsNullOrWhiteSpace(account));
1696
            Contract.Requires(cloudFile!=null);
1697

    
1698
            using (var client = new RestClient(_baseClient))
1699
            {
1700
                if (!String.IsNullOrWhiteSpace(account))
1701
                    client.BaseAddress = GetAccountUrl(account);
1702

    
1703

    
1704
                var parts = cloudFile.Name.ToString().Split('/');
1705
                var folder = String.Join("/", parts,0,parts.Length-1);
1706

    
1707
                var fileName = String.Format("{0}/{1}.pithos.ignore", folder, Guid.NewGuid());
1708
                var fileUri=fileName.ToEscapedUri();                                            
1709

    
1710
                client.Parameters.Clear();
1711
                try
1712
                {
1713
                    var relativeUri = cloudFile.Container.Combine(fileUri);
1714
                    client.PutWithRetry(relativeUri, 3, @"application/octet-stream");
1715

    
1716
                    var expectedCodes = new[] { HttpStatusCode.OK, HttpStatusCode.NoContent, HttpStatusCode.Created};
1717
                    var result=(expectedCodes.Contains(client.StatusCode));
1718
                    DeleteObject(account, cloudFile.Container, fileUri, cloudFile.IsDirectory);
1719
                    return result;
1720
                }
1721
                catch
1722
                {
1723
                    return false;
1724
                }
1725
            }
1726
        }
1727

    
1728
        ~CloudFilesClient()
1729
        {
1730
            Dispose(false);
1731
        }
1732

    
1733
        public void Dispose()
1734
        {
1735
            Dispose(true);
1736
            GC.SuppressFinalize(this);
1737
        }
1738

    
1739
        protected virtual void Dispose(bool disposing)
1740
        {
1741
            if (disposing)
1742
            {
1743
                if (_httpClientHandler!=null)
1744
                    _httpClientHandler.Dispose();
1745
                if (_baseClient!=null)
1746
                    _baseClient.Dispose();
1747
                if(_baseHttpClient!=null)
1748
                    _baseHttpClient.Dispose();
1749
                if (_baseHttpClientNoTimeout!=null)
1750
                    _baseHttpClientNoTimeout.Dispose();
1751
            }
1752
            _httpClientHandler = null;
1753
            _baseClient = null;
1754
            _baseHttpClient = null;
1755
            _baseHttpClientNoTimeout = null;
1756
        }
1757
    }
1758

    
1759
    public class ShareAccountInfo
1760
    {
1761
        public DateTime? last_modified { get; set; }
1762
        public string name { get; set; }
1763
    }
1764
}