Statistics
| Branch: | Revision:

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

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

    
70
namespace Pithos.Network
71
{
72

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

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

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

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

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

    
106

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

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

    
121

    
122
        public Uri RootAddressUri { get; set; }
123

    
124

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

    
128
        public string AuthenticationUrl { get; set; }
129

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

    
136
        public bool UsePithos { get; set; }
137

    
138

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

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

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

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

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

    
181

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

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

    
196

    
197
        }
198

    
199

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

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

    
224

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

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

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

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

    
235
                string storageUrl;
236
                string token;
237
                
238
                using (var response = await authClient.GetAsyncWithRetries(new Uri(VersionPath, UriKind.Relative),3).ConfigureAwait(false)) // .DownloadStringWithRetryRelative(new Uri(VersionPath, UriKind.Relative), 3);                    
239
                {
240
                    AssertStatusOK(response,"Authentication failed");
241
                
242
                    storageUrl = response.Headers.GetFirstValue("X-Storage-Url");
243
                    if (String.IsNullOrWhiteSpace(storageUrl))
244
                        throw new InvalidOperationException("Failed to obtain storage url");
245

    
246
                    token = response.Headers.GetFirstValue(TOKEN_HEADER);
247
                    if (String.IsNullOrWhiteSpace(token))
248
                        throw new InvalidOperationException("Failed to obtain token url");
249
                }
250

    
251
                _baseClient = new RestClient
252
                {
253
                    BaseAddress = storageUrl,
254
                    Timeout = 30000,
255
                    Retries = 3,                    
256
                };
257

    
258
                StorageUrl = new Uri(storageUrl);
259
                Token = token;
260

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

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

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

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

    
291
            Log.InfoFormat("[AUTHENTICATE] End for {0}", UserName);
292
            Debug.Assert(_baseClient!=null);
293
            var displayName = UserName;
294
            Guid uuid;
295
            if (Guid.TryParse(UserName, out uuid))
296
            {
297
                displayName = await ResolveName(uuid);
298
            }
299
            return new AccountInfo {StorageUri = StorageUrl, Token = Token, UserName = UserName,DisplayName=displayName,Groups=groups};            
300

    
301
        }
302

    
303
        private static void TraceStart(string method, Uri actualAddress)
304
        {
305
            Log.InfoFormat("[{0}] {1} {2}", method, DateTime.Now, actualAddress);
306
        }
307

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

    
321
                if (response.StatusCode == HttpStatusCode.NoContent)
322
                    return String.Empty;
323

    
324
                var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
325
                return content;
326
            }
327
        }
328

    
329
        public async Task<IList<ContainerInfo>> ListContainers(string account)
330
        {
331

    
332
            var targetUrl = GetTargetUrl(account);
333
            var targetUri = new Uri(String.Format("{0}?format=json", targetUrl));
334
            var result = await GetStringAsync(targetUri, "List Containers failed").ConfigureAwait(false);
335
            if (String.IsNullOrWhiteSpace(result))
336
                return new List<ContainerInfo>();
337
            var infos = JsonConvert.DeserializeObject<IList<ContainerInfo>>(result);
338
            foreach (var info in infos)
339
            {
340
                info.Account = account;
341
            }
342
            return infos;
343
        }
344

    
345
        
346
        private string GetAccountUrl(string account)
347
        {
348
            return RootAddressUri.Combine(account).AbsoluteUri;
349
        }
350

    
351
        public IList<ShareAccountInfo> ListSharingAccounts(DateTime? since=null)
352
        {
353
            using (ThreadContext.Stacks["Share"].Push("List Accounts"))
354
            {
355
                if (Log.IsDebugEnabled) Log.DebugFormat("START");
356

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

    
360
                //If the result is empty, return an empty list,
361
                var infos = String.IsNullOrWhiteSpace(content)
362
                                ? new List<ShareAccountInfo>()
363
                            //Otherwise deserialize the account list into a list of ShareAccountInfos
364
                                : JsonConvert.DeserializeObject<IList<ShareAccountInfo>>(content);
365

    
366
                Log.DebugFormat("END");
367
                return infos;
368
            }
369
        }
370

    
371

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

    
384
            using (ThreadContext.Stacks["Share"].Push("List Objects"))
385
            {
386
                if (Log.IsDebugEnabled) Log.DebugFormat("START");
387
                //'since' is not used here because we need to have ListObjects return a NoChange result
388
                //for all shared accounts,containers
389

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

    
401
                //For each object
402
                //Check parents recursively up to (but not including) the container.
403
                //If parents are missing, add them to the list
404
                //Need function to calculate all parent URLs
405
                objects = AddMissingParents(objects);
406
                
407
                //Store any new containers
408
                foreach (var container in containers)
409
                {
410
                    knownContainers.Add(getKey(container));
411
                }
412

    
413
                if (Log.IsDebugEnabled) Log.DebugFormat("END");
414
                return objects;
415
            }
416
        }
417

    
418
        private List<ObjectInfo> AddMissingParents(List<ObjectInfo> objects)
419
        {
420
            //TODO: Remove short-circuit when we decide to use Missing Parents functionality
421
            //return objects;
422

    
423
            var existingUris = objects.ToDictionary(o => o.Uri, o => o);
424
            foreach (var objectInfo in objects)
425
            {
426
                //Can be null when retrieving objects to show in selective sync
427
                if (objectInfo.Name == null)
428
                    continue;
429

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

    
444
                    var parentParts = baseParts.Concat(nameparts);
445
                    var parentUrl = objectInfo.Uri.Scheme+ "://" + String.Join("/", parentParts);
446
                    
447
                    var parentUri = new Uri(parentUrl, UriKind.Absolute);
448

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

    
477
        public void SetTags(ObjectInfo target,IDictionary<string,string> tags)
478
        {
479
            Contract.Requires<InvalidOperationException>(!String.IsNullOrWhiteSpace(Token),"The Token is not set");
480
            Contract.Requires<InvalidOperationException>(StorageUrl != null,"The StorageUrl is not set");
481
            Contract.Requires<ArgumentNullException>(target != null,"target is null");
482
            Contract.EndContractBlock();
483

    
484
            using (ThreadContext.Stacks["Share"].Push("Share Object"))
485
            {
486
                if (Log.IsDebugEnabled) Log.DebugFormat("START");
487

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

    
491
                    client.BaseAddress = GetAccountUrl(target.Account);
492

    
493
                    client.Parameters.Clear();
494
                    client.Parameters.Add("update", "");
495

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

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

    
513
                    if (Log.IsDebugEnabled) Log.DebugFormat("END");
514
                }
515
            }
516

    
517

    
518
        }
519

    
520
        public void ShareObject(string account, Uri container, Uri objectName, string shareTo, bool read, bool write)
521
        {
522

    
523
            Contract.Requires<InvalidOperationException>(!String.IsNullOrWhiteSpace(Token), "The Token is not set");
524
            Contract.Requires<InvalidOperationException>(StorageUrl != null, "The StorageUrl is not set");
525
            Contract.Requires<ArgumentNullException>(container != null, "container is null");
526
            Contract.Requires<ArgumentException>(!container.IsAbsoluteUri, "container is absolute");
527
            Contract.Requires<ArgumentNullException>(objectName != null, "objectName is null");
528
            Contract.Requires<ArgumentException>(!objectName.IsAbsoluteUri, "objectName  is absolute");
529
            Contract.Requires<ArgumentNullException>(!String.IsNullOrWhiteSpace(account), "account is not set");
530
            Contract.Requires<ArgumentNullException>(!String.IsNullOrWhiteSpace(shareTo), "shareTo is not set");
531
            Contract.EndContractBlock();
532

    
533
            using (ThreadContext.Stacks["Share"].Push("Share Object"))
534
            {
535
                if (Log.IsDebugEnabled) Log.DebugFormat("START");
536
                
537
                using (var client = new RestClient(_baseClient))
538
                {
539

    
540
                    client.BaseAddress = GetAccountUrl(account);
541

    
542
                    client.Parameters.Clear();
543
                    client.Parameters.Add("format", "json");
544

    
545
                    string permission = "";
546
                    if (write)
547
                        permission = String.Format("write={0}", shareTo);
548
                    else if (read)
549
                        permission = String.Format("read={0}", shareTo);
550
                    client.Headers.Add("X-Object-Sharing", permission);
551

    
552
                    var content = client.DownloadStringWithRetryRelative(container, 3);
553

    
554
                    client.AssertStatusOK("ShareObject failed");
555

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

    
562
                    if (Log.IsDebugEnabled) Log.DebugFormat("END");
563
                }
564
            }
565

    
566

    
567
        }
568

    
569
        public async Task<AccountInfo> GetAccountPolicies(AccountInfo accountInfo)
570
        {
571
            if (accountInfo==null)
572
                throw new ArgumentNullException("accountInfo");
573
            Contract.EndContractBlock();
574

    
575
            using (ThreadContext.Stacks["Account"].Push("GetPolicies"))
576
            {
577
                if (Log.IsDebugEnabled) Log.DebugFormat("START");
578

    
579
/*
580
                if (_baseClient == null)
581
                {
582
                    _baseClient = new RestClient
583
                    {
584
                        BaseAddress = accountInfo.StorageUri.ToString(),
585
                        Timeout = 30000,
586
                        Retries = 3,
587
                    };
588
                }
589

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

    
604
                    return accountInfo;   
605
                }
606

    
607

    
608
                //using (var client = new RestClient(_baseClient))
609
                //{
610
                //    if (!String.IsNullOrWhiteSpace(accountInfo.UserName))
611
                //        client.BaseAddress = GetAccountUrl(accountInfo.UserName);
612

    
613
                //    client.Parameters.Clear();
614
                //    client.Parameters.Add("format", "json");                    
615
                //    client.Head(_emptyUri, 3);
616

    
617
                //    var quotaValue=client.ResponseHeaders["X-Account-Policy-Quota"];
618
                //    var bytesValue= client.ResponseHeaders["X-Account-Bytes-Used"];
619

    
620
                //    long quota, bytes;
621
                //    if (long.TryParse(quotaValue, out quota))
622
                //        accountInfo.Quota = quota;
623
                //    if (long.TryParse(bytesValue, out bytes))
624
                //        accountInfo.BytesUsed = bytes;
625
                    
626
                //    return accountInfo;
627

    
628
                //}
629

    
630
            }
631
        }
632

    
633
        public void UpdateMetadata(ObjectInfo objectInfo)
634
        {
635
            Contract.Requires<ArgumentNullException>(objectInfo != null,"objectInfo is null");
636
            Contract.EndContractBlock();
637

    
638
            using (ThreadContext.Stacks["Objects"].Push("UpdateMetadata"))
639
            {
640
                if (Log.IsDebugEnabled) Log.DebugFormat("START");
641

    
642

    
643
                using(var client=new RestClient(_baseClient))
644
                {
645

    
646
                    client.BaseAddress = GetAccountUrl(objectInfo.Account);
647
                    
648
                    client.Parameters.Clear();
649
                    
650

    
651
                    //Set Tags
652
                    foreach (var tag in objectInfo.Tags)
653
                    {
654
                        var headerTag = String.Format("X-Object-Meta-{0}", tag.Key);
655
                        client.Headers.Add(headerTag, tag.Value);
656
                    }
657

    
658
                    //Set Permissions
659

    
660
                    var permissions=objectInfo.GetPermissionString();
661
                    client.SetNonEmptyHeaderValue("X-Object-Sharing",permissions);
662

    
663
                    client.SetNonEmptyHeaderValue("Content-Disposition",objectInfo.ContendDisposition);
664
                    client.SetNonEmptyHeaderValue("Content-Encoding",objectInfo.ContentEncoding);
665
                    client.SetNonEmptyHeaderValue("X-Object-Manifest",objectInfo.Manifest);
666
                    var isPublic = objectInfo.IsPublic.ToString().ToLower();
667
                    client.Headers.Add("X-Object-Public", isPublic);
668

    
669

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

    
681
                    if (Log.IsDebugEnabled) Log.DebugFormat("END");
682
                }
683
            }
684

    
685
        }
686

    
687
        public void UpdateMetadata(ContainerInfo containerInfo)
688
        {
689
            if (containerInfo == null)
690
                throw new ArgumentNullException("containerInfo");
691
            Contract.EndContractBlock();
692

    
693
            using (ThreadContext.Stacks["Containers"].Push("UpdateMetadata"))
694
            {
695
                if (Log.IsDebugEnabled) Log.DebugFormat("START");
696

    
697

    
698
                using(var client=new RestClient(_baseClient))
699
                {
700

    
701
                    client.BaseAddress = GetAccountUrl(containerInfo.Account);
702
                    
703
                    client.Parameters.Clear();
704
                    
705

    
706
                    //Set Tags
707
                    foreach (var tag in containerInfo.Tags)
708
                    {
709
                        var headerTag = String.Format("X-Container-Meta-{0}", tag.Key);
710
                        client.Headers.Add(headerTag, tag.Value);
711
                    }
712

    
713
                    
714
                    //Set Policies
715
                    foreach (var policy in containerInfo.Policies)
716
                    {
717
                        var headerPolicy = String.Format("X-Container-Policy-{0}", policy.Key);
718
                        client.Headers.Add(headerPolicy, policy.Value);
719
                    }
720

    
721

    
722
                    var uriBuilder = client.GetAddressBuilder(containerInfo.Name,_emptyUri);
723
                    var uri = uriBuilder.Uri;
724

    
725
                    client.UploadValues(uri,new NameValueCollection());
726

    
727

    
728
                    client.AssertStatusOK("UpdateMetadata failed");
729
                    //If the status is NOT ACCEPTED or OK we have a problem
730
                    if (!(client.StatusCode == HttpStatusCode.Accepted || client.StatusCode == HttpStatusCode.OK))
731
                    {
732
                        Log.Error("Failed to update metadata");
733
                        throw new Exception("Failed to update metadata");
734
                    }
735

    
736
                    if (Log.IsDebugEnabled) Log.DebugFormat("END");
737
                }
738
            }
739

    
740
        }
741

    
742
       
743

    
744

    
745
        public IList<ObjectInfo> ListObjects(string account, Uri container, DateTimeOffset? since = null)
746
        {
747
/*
748
            if (container==null)
749
                throw new ArgumentNullException("container");
750
            if (container.IsAbsoluteUri)
751
                throw new ArgumentException("container");
752
            Contract.EndContractBlock();
753
*/
754

    
755
            using (ThreadContext.Stacks["Objects"].Push("List"))
756
            {
757

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

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

    
763
                //304 will result in an empty string. Empty containers return an empty json array
764
                if (String.IsNullOrWhiteSpace(content))
765
                     return new[] {new NoModificationInfo(account, container)};
766

    
767
                 //If the result is empty, return an empty list,
768
                 var infos = String.IsNullOrWhiteSpace(content)
769
                                 ? new List<ObjectInfo>()
770
                             //Otherwise deserialize the object list into a list of ObjectInfos
771
                                 : JsonConvert.DeserializeObject<IList<ObjectInfo>>(content);
772

    
773
                 foreach (var info in infos)
774
                 {
775
                     info.Container = container;
776
                     info.Account = account;
777
                     info.StorageUri = StorageUrl;
778
                 }
779
                 if (Log.IsDebugEnabled) Log.DebugFormat("END");
780
                 return infos;
781
            }
782
        }
783

    
784
        public IList<ObjectInfo> ListObjects(string account, Uri container, Uri folder, DateTimeOffset? since = null)
785
        {
786
/*            if (container==null)
787
                throw new ArgumentNullException("container");
788
            if (container.IsAbsoluteUri)
789
                throw new ArgumentException("container");
790
            Contract.EndContractBlock();*/
791

    
792
            using (ThreadContext.Stacks["Objects"].Push("List"))
793
            {
794
                if (Log.IsDebugEnabled) Log.DebugFormat("START");
795

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

    
800
                //304 will result in an empty string. Empty containers return an empty json array
801
                if (String.IsNullOrWhiteSpace(content))
802
                    return new[] { new NoModificationInfo(account, container) };
803

    
804

    
805
                var infos = JsonConvert.DeserializeObject<IList<ObjectInfo>>(content);
806
                foreach (var info in infos)
807
                {
808
                    info.Account = account;
809
                    if (info.Container == null)
810
                        info.Container = container;
811
                    info.StorageUri = StorageUrl;
812
                }
813
                if (Log.IsDebugEnabled) Log.DebugFormat("END");
814
                return infos;
815
/*
816
                using (var client = new RestClient(_baseClient))
817
                {
818
                    if (!String.IsNullOrWhiteSpace(account))
819
                        client.BaseAddress = GetAccountUrl(account);
820

    
821
                    client.Parameters.Clear();
822
                    client.Parameters.Add("format", "json");
823
                    client.Parameters.Add("path", folder.ToString());
824
                    client.IfModifiedSince = since;
825
                    var content = client.DownloadStringWithRetryRelative(container, 3);
826
                    client.AssertStatusOK("ListObjects failed");
827

    
828
                    if (client.StatusCode==HttpStatusCode.NotModified)
829
                        return new[]{new NoModificationInfo(account,container,folder)};
830

    
831
                    var infos = JsonConvert.DeserializeObject<IList<ObjectInfo>>(content);
832
                    foreach (var info in infos)
833
                    {
834
                        info.Account = account;
835
                        if (info.Container == null)
836
                            info.Container = container;
837
                        info.StorageUri = StorageUrl;
838
                    }
839
                    if (Log.IsDebugEnabled) Log.DebugFormat("END");
840
                    return infos;
841
                }
842
*/
843
            }
844
        }
845

    
846
 
847
        public async Task<bool> ContainerExists(string account, Uri container)
848
        {
849
            if (container==null)
850
                throw new ArgumentNullException("container", "The container property can't be empty");
851
            if (container.IsAbsoluteUri)
852
                throw new ArgumentException( "The container must be relative","container");
853
            Contract.EndContractBlock();
854

    
855
            using (ThreadContext.Stacks["Containters"].Push("Exists"))
856
            {
857
                if (Log.IsDebugEnabled) Log.DebugFormat("START");
858

    
859
                var targetUri = GetTargetUri(account).Combine(container);
860

    
861
                using (var response =await _baseHttpClient.HeadAsyncWithRetries(targetUri, 3))
862
                {
863

    
864
                    bool result;
865
                    switch (response.StatusCode)
866
                    {
867
                        case HttpStatusCode.OK:
868
                        case HttpStatusCode.NoContent:
869
                            result = true;
870
                            break;
871
                        case HttpStatusCode.NotFound:
872
                            result = false;
873
                            break;
874
                        default:
875
                            throw CreateWebException("ContainerExists", response.StatusCode);
876
                    }
877
                    if (Log.IsDebugEnabled) Log.DebugFormat("END");
878

    
879
                    return result;
880
                }
881

    
882
            }
883
        }
884

    
885
        private Uri GetTargetUri(string account)
886
        {
887
            return new Uri(GetTargetUrl(account),UriKind.Absolute);
888
        }
889

    
890
        private string GetTargetUrl(string account)
891
        {
892
            return String.IsNullOrWhiteSpace(account)
893
                       ? _baseHttpClient.BaseAddress.ToString()
894
                       : GetAccountUrl(account);
895
        }
896

    
897
        public async Task<bool> ObjectExists(string account, Uri container, Uri objectName)
898
        {
899
            if (container == null)
900
                throw new ArgumentNullException("container", "The container property can't be empty");
901
            if (container.IsAbsoluteUri)
902
                throw new ArgumentException("The container must be relative","container");
903
            if (objectName == null)
904
                throw new ArgumentNullException("objectName", "The objectName property can't be empty");
905
            if (objectName.IsAbsoluteUri)
906
                throw new ArgumentException("The objectName must be relative","objectName");
907
            Contract.EndContractBlock();
908

    
909
                var targetUri=GetTargetUri(account).Combine(container).Combine(objectName);
910

    
911
            using (var response = await _baseHttpClient.HeadAsyncWithRetries(targetUri, 3).ConfigureAwait(false))
912
            {
913
                switch (response.StatusCode)
914
                {
915
                    case HttpStatusCode.OK:
916
                    case HttpStatusCode.NoContent:
917
                        return true;
918
                    case HttpStatusCode.NotFound:
919
                        return false;
920
                    default:
921
                        throw CreateWebException("ObjectExists", response.StatusCode);
922
                }
923
            }
924
        }
925

    
926
        public async Task<ObjectInfo> GetObjectInfo(string account, Uri container, Uri objectName)
927
        {
928
            if (container == null)
929
                throw new ArgumentNullException("container", "The container property can't be empty");
930
            if (container.IsAbsoluteUri)
931
                throw new ArgumentException("The container must be relative", "container");
932
            if (objectName == null)
933
                throw new ArgumentNullException("objectName", "The objectName property can't be empty");
934
            if (objectName.IsAbsoluteUri)
935
                throw new ArgumentException("The objectName must be relative", "objectName");
936
            Contract.EndContractBlock();
937

    
938
            using (ThreadContext.Stacks["Objects"].Push("GetObjectInfo"))
939
            {
940

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

    
958
                                var permissions = response.Headers.GetFirstValue("X-Object-Sharing");
959

    
960

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

    
1008

    
1009

    
1010
        public async Task CreateFolder(string account, Uri container, Uri folder)
1011
        {
1012
            if (container == null)
1013
                throw new ArgumentNullException("container", "The container property can't be empty");
1014
            if (container.IsAbsoluteUri)
1015
                throw new ArgumentException("The container must be relative","container");
1016
            if (folder == null)
1017
                throw new ArgumentNullException("folder", "The objectName property can't be empty");
1018
            if (folder.IsAbsoluteUri)
1019
                throw new ArgumentException("The objectName must be relative","folder");
1020
            Contract.EndContractBlock();
1021

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

    
1035
        private Dictionary<string, string> GetMeta(HttpResponseMessage response,string metaPrefix)
1036
        {
1037
            Contract.Requires<ArgumentNullException>(!String.IsNullOrWhiteSpace(metaPrefix), "metaPrefix is empty");
1038
            Contract.EndContractBlock();
1039

    
1040
            var dict = (from header in response.Headers
1041
                        where header.Key.StartsWith(metaPrefix)
1042
                         select new { Name = header.Key, Value = String.Join(",", header.Value) })
1043
                        .ToDictionary(t => t.Name, t => t.Value);
1044

    
1045
          
1046
            return dict;
1047
        }
1048

    
1049

    
1050
        public async Task<ContainerInfo> GetContainerInfo(string account, Uri container)
1051
        {
1052
            if (container == null)
1053
                throw new ArgumentNullException("container", "The container property can't be empty");
1054
            if (container.IsAbsoluteUri)
1055
                throw new ArgumentException("The container must be relative","container");
1056
            Contract.EndContractBlock();
1057

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

    
1070
                        var containerInfo = new ContainerInfo
1071
                                                {
1072
                                                    Account = account,
1073
                                                    Name = container,
1074
                                                    StorageUrl = StorageUrl.ToString(),
1075
                                                    Count =long.Parse(response.Headers.GetFirstValue("X-Container-Object-Count")),
1076
                                                    Bytes = long.Parse(response.Headers.GetFirstValue("X-Container-Bytes-Used")),
1077
                                                    BlockHash = response.Headers.GetFirstValue("X-Container-Block-Hash"),
1078
                                                    BlockSize =
1079
                                                        int.Parse(response.Headers.GetFirstValue("X-Container-Block-Size")),
1080
                                                    Last_Modified = response.Content.Headers.LastModified,
1081
                                                    Tags = tags,
1082
                                                    Policies = policies
1083
                                                };
1084

    
1085

    
1086
                        return containerInfo;
1087
                    case HttpStatusCode.NotFound:
1088
                        return ContainerInfo.Empty;
1089
                    default:
1090
                        throw CreateWebException("GetContainerInfo", response.StatusCode);
1091
                }
1092
            }            
1093
        }
1094

    
1095
        public async Task CreateContainer(string account, Uri container)
1096
        {
1097
            if (container == null)
1098
                throw new ArgumentNullException("container", "The container property can't be empty");
1099
            if (container.IsAbsoluteUri)
1100
                throw new ArgumentException("The container must be relative","container");
1101
            Contract.EndContractBlock();
1102

    
1103
            var targetUri=GetTargetUri(account).Combine(container);
1104
            var message = new HttpRequestMessage(HttpMethod.Put, targetUri);
1105
            
1106
            //message.Content.Headers.ContentLength = 0;
1107
            using (var response =await _baseHttpClient.SendAsyncWithRetries(message, 3).ConfigureAwait(false))
1108
            {            
1109
                var expectedCodes = new[] {HttpStatusCode.Created, HttpStatusCode.Accepted, HttpStatusCode.OK};
1110
                if (!expectedCodes.Contains(response.StatusCode))
1111
                    throw CreateWebException("CreateContainer", response.StatusCode);
1112
            }
1113
        }
1114

    
1115
        public async Task WipeContainer(string account, Uri container)
1116
        {
1117
            if (container == null)
1118
                throw new ArgumentNullException("container", "The container property can't be empty");
1119
            if (container.IsAbsoluteUri)
1120
                throw new ArgumentException("The container must be relative", "container");
1121
            Contract.EndContractBlock();
1122

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

    
1126

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

    
1135
            var targetUri = GetTargetUri(account).Combine(container);
1136
            var message = new HttpRequestMessage(HttpMethod.Delete, targetUri);
1137
            using (var response = await _baseHttpClient.SendAsyncWithRetries(message, 3).ConfigureAwait(false))
1138
            {
1139
                var expectedCodes = new[] { HttpStatusCode.NotFound, HttpStatusCode.NoContent };
1140
                if (!expectedCodes.Contains(response.StatusCode))
1141
                    throw CreateWebException("DeleteContainer", response.StatusCode);
1142
            }
1143

    
1144
        }
1145

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

    
1171
            try
1172
            {
1173
                //WebClient, and by extension RestClient, are not thread-safe. Create a new RestClient
1174
                //object to avoid concurrency errors.
1175
                //
1176
                //Download operations take a long time therefore they have no timeout.
1177
                using(var client = new RestClient(_baseClient) { Timeout = 0 })
1178
                {
1179
                    if (!String.IsNullOrWhiteSpace(account))
1180
                        client.BaseAddress = GetAccountUrl(account);
1181

    
1182
                    //The container and objectName are relative names. They are joined with the client's
1183
                    //BaseAddress to create the object's absolute address
1184
                    var builder = client.GetAddressBuilder(container, objectName);
1185
                    var uri = builder.Uri;
1186

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

    
1207
                    //Once the download completes
1208
                    //Delete the local client object
1209
                }
1210
                //And report failure or completion
1211
            }
1212
            catch (Exception exc)
1213
            {
1214
                Log.ErrorFormat("[GET] FAIL {0} with {1}", objectName, exc);
1215
                throw;
1216
            }
1217

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

    
1220

    
1221
        }
1222

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

    
1241
            
1242

    
1243
            //The container and objectName are relative names. They are joined with the client's
1244
            //BaseAddress to create the object's absolute address
1245

    
1246
            var targetUri = GetTargetUri(account).Combine(container).Combine(objectName);
1247
  
1248

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

    
1251
            
1252
            //Send the tree hash as Json to the server            
1253
            var jsonHash = hash.ToJson();
1254
            if (Log.IsDebugEnabled)
1255
                Log.DebugFormat("Hashes:\r\n{0}", jsonHash);
1256

    
1257
            var mimeType = objectName.GetMimeType();
1258

    
1259
            var message = new HttpRequestMessage(HttpMethod.Put, uri)
1260
            {
1261
                Content = new StringContent(jsonHash)
1262
            };
1263
            message.Content.Headers.ContentType = mimeType;
1264
            message.Headers.Add("ETag",hash.TopHash.ToHashString());
1265
            
1266
            
1267
            //Don't use a timeout because putting the hashmap may be a long process
1268

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

    
1298
        }
1299

    
1300

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

    
1319

    
1320
            var targetUri = GetTargetUri(account).Combine(container).Combine(relativeUrl);
1321
            var message = new HttpRequestMessage(HttpMethod.Get, targetUri);
1322
            //Don't add a range if start=0, end=null (empty files)
1323
            if (start!=0 || end!=null)
1324
                message.Headers.Range=new RangeHeaderValue(start,end);
1325

    
1326
            //Don't use a timeout because putting the hashmap may be a long process
1327

    
1328
            IProgress<DownloadArgs> progress = new Progress<DownloadArgs>(args =>
1329
                {
1330
                    Log.DebugFormat("[GET PROGRESS] {0} {1}% {2} of {3}",
1331
                                    targetUri.Segments.Last(), args.ProgressPercentage,
1332
                                    args.BytesReceived,
1333
                                    args.TotalBytesToReceive);
1334

    
1335
                    if (DownloadProgressChanged!=null)
1336
                        DownloadProgressChanged(this,  args);
1337
                });
1338

    
1339

    
1340
            using (var response = await _baseHttpClientNoTimeout.SendAsyncWithRetries(message, 3, false,HttpCompletionOption.ResponseHeadersRead,
1341
                                                          cancellationToken).ConfigureAwait(false))
1342
            using (var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false))
1343
            {
1344
                long totalSize = response.Content.Headers.ContentLength ?? 0;
1345
                    byte[] buffer,streambuf;
1346
                    lock (_bufferManager)
1347
                    {
1348
                        buffer = _bufferManager.TakeBuffer(65536);
1349
                        streambuf = _bufferManager.TakeBuffer((int)totalSize);
1350
                    }
1351

    
1352
                using (var targetStream = new MemoryStream(streambuf))
1353
                {
1354

    
1355
                    long total = 0;
1356
                    try
1357
                    {
1358

    
1359
                        int read;
1360
                        while ((read = await stream.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false)) != 0)
1361
                        {
1362
                            total += read;
1363
                            progress.Report(new DownloadArgs(total, totalSize));
1364
                            await targetStream.WriteAsync(buffer, 0, read).ConfigureAwait(false);
1365
                        }
1366
                    }
1367
                    finally
1368
                    {
1369
                        lock (_bufferManager)
1370
                        {
1371
                            _bufferManager.ReturnBuffer(buffer);
1372
                            _bufferManager.ReturnBuffer(streambuf);
1373
                        }
1374
                    }
1375
                    var result = targetStream.ToArray();
1376
                    return result;
1377
                }
1378
            }
1379

    
1380
        }
1381

    
1382
        public event EventHandler<UploadArgs> UploadProgressChanged;
1383
        public event EventHandler<DownloadArgs> DownloadProgressChanged;
1384
        
1385

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

    
1404

    
1405
            try
1406
            {
1407
                var containerUri = GetTargetUri(account).Combine(container);
1408
                var targetUri = new Uri(String.Format("{0}?update", containerUri));
1409

    
1410

    
1411
                //Don't use a timeout because putting the hashmap may be a long process
1412

    
1413

    
1414
                Log.InfoFormat("[BLOCK POST] START");
1415

    
1416

    
1417
                var progress = new Progress<UploadArgs>(args =>
1418
                {
1419
                    Log.InfoFormat("[BLOCK POST PROGRESS] {0}% {1} of {2}",
1420
                        args.ProgressPercentage,
1421
                        args.BytesSent,
1422
                        args.TotalBytesToSend);
1423
                    if (UploadProgressChanged != null)
1424
                        UploadProgressChanged(this,args);
1425
                });
1426

    
1427
                var message = new HttpRequestMessage(HttpMethod.Post, targetUri)
1428
                                  {
1429
                                      Content = new ByteArrayContentWithProgress(block, offset, count,progress)
1430
                                  };
1431
                message.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(@"application/octet-stream");
1432

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

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

    
1474

    
1475
            try
1476
            {
1477
                var containerUri = GetTargetUri(account).Combine(container);
1478
                var targetUri = new Uri(String.Format("{0}?update", containerUri));
1479

    
1480

    
1481
                //Don't use a timeout because putting the hashmap may be a long process
1482

    
1483

    
1484
                Log.InfoFormat("[BLOCK POST] START");
1485

    
1486

    
1487
                var progress = new Progress<UploadArgs>(args =>
1488
                {
1489
                    Log.InfoFormat("[BLOCK POST PROGRESS] {0}% {1} of {2} at {3:###,} Kbps ",
1490
                        args.ProgressPercentage,
1491
                        args.BytesSent,
1492
                        args.TotalBytesToSend,args.Speed);
1493
                    if (UploadProgressChanged != null)
1494
                        UploadProgressChanged(this, args);
1495
                });
1496

    
1497
                var message = new HttpRequestMessage(HttpMethod.Post, targetUri)
1498
                {
1499
                    Content = new FileBlockContent(filePath, offset, count, progress)
1500
                };
1501
                message.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(@"application/octet-stream");
1502

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

    
1528

    
1529
        public async Task<TreeHash> GetHashMap(string account, Uri container, Uri objectName)
1530
        {
1531
            if (container == null)
1532
                throw new ArgumentNullException("container", "The container property can't be empty");
1533
            if (container.IsAbsoluteUri)
1534
                throw new ArgumentException("The container must be relative","container");
1535
            if (objectName == null)
1536
                throw new ArgumentNullException("objectName", "The objectName property can't be empty");
1537
            if (objectName.IsAbsoluteUri)
1538
                throw new ArgumentException("The objectName must be relative","objectName");
1539
            if (String.IsNullOrWhiteSpace(Token))
1540
                throw new InvalidOperationException("Invalid Token");
1541
            if (StorageUrl == null)
1542
                throw new InvalidOperationException("Invalid Storage Url");
1543
            Contract.EndContractBlock();
1544

    
1545
            try
1546
            {
1547

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

    
1551
                //Start downloading the object asynchronously
1552
                var json = await GetStringAsync(targetUri, "").ConfigureAwait(false);
1553
                var treeHash = TreeHash.Parse(json);
1554
                Log.InfoFormat("[GET HASH] END {0}", objectName);
1555
                return treeHash;
1556

    
1557
            }
1558
            catch (Exception exc)
1559
            {
1560
                Log.ErrorFormat("[GET HASH] END {0} with {1}", objectName, exc);
1561
                throw;
1562
            }
1563

    
1564
        }
1565

    
1566

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

    
1592
                using (var client = new RestClient(_baseClient) { Timeout = 0 })
1593
                {
1594
                    if (!String.IsNullOrWhiteSpace(account))
1595
                        client.BaseAddress = GetAccountUrl(account);
1596

    
1597
                    var builder = client.GetAddressBuilder(container, objectName);
1598
                    var uri = builder.Uri;
1599

    
1600
                    string etag = hash ;
1601

    
1602
                    client.Headers.Add("Content-Type", contentType);
1603
                    if (contentType!=ObjectInfo.CONTENT_TYPE_DIRECTORY)
1604
                        client.Headers.Add("ETag", etag);
1605

    
1606

    
1607
                    Log.InfoFormat("[PUT] START {0}", objectName);
1608
                    client.UploadProgressChanged += (sender, args) =>
1609
                                                        {
1610
                                                            using (ThreadContext.Stacks["PUT"].Push("Progress"))
1611
                                                            {
1612
                                                                Log.InfoFormat("{0} {1}% {2} of {3}", fileName,
1613
                                                                               args.ProgressPercentage,
1614
                                                                               args.BytesSent, args.TotalBytesToSend);
1615
                                                            }
1616
                                                        };
1617

    
1618
                    client.UploadFileCompleted += (sender, args) =>
1619
                                                      {
1620
                                                          using (ThreadContext.Stacks["PUT"].Push("Progress"))
1621
                                                          {
1622
                                                              Log.InfoFormat("Completed {0}", fileName);
1623
                                                          }
1624
                                                      }; 
1625
                    
1626
                    if (contentType==ObjectInfo.CONTENT_TYPE_DIRECTORY)
1627
                        await client.UploadDataTaskAsync(uri, "PUT", new byte[0]).ConfigureAwait(false);
1628
                    else
1629
                        await client.UploadFileTaskAsync(uri, "PUT", fileName).ConfigureAwait(false);
1630
                }
1631

    
1632
                Log.InfoFormat("[PUT] END {0}", objectName);
1633
            }
1634
            catch (Exception exc)
1635
            {
1636
                Log.ErrorFormat("[PUT] END {0} with {1}", objectName, exc);
1637
                throw;
1638
            }                
1639

    
1640
        }
1641
        
1642
        public async Task MoveObject(string account, Uri sourceContainer, Uri oldObjectName, Uri targetContainer, Uri newObjectName)
1643
        {
1644
            if (sourceContainer == null)
1645
                throw new ArgumentNullException("sourceContainer", "The sourceContainer property can't be empty");
1646
            if (sourceContainer.IsAbsoluteUri)
1647
                throw new ArgumentException("The sourceContainer must be relative","sourceContainer");
1648
            if (oldObjectName == null)
1649
                throw new ArgumentNullException("oldObjectName", "The oldObjectName property can't be empty");
1650
            if (oldObjectName.IsAbsoluteUri)
1651
                throw new ArgumentException("The oldObjectName must be relative","oldObjectName");
1652
            if (targetContainer == null)
1653
                throw new ArgumentNullException("targetContainer", "The targetContainer property can't be empty");
1654
            if (targetContainer.IsAbsoluteUri)
1655
                throw new ArgumentException("The targetContainer must be relative","targetContainer");
1656
            if (newObjectName == null)
1657
                throw new ArgumentNullException("newObjectName", "The newObjectName property can't be empty");
1658
            if (newObjectName.IsAbsoluteUri)
1659
                throw new ArgumentException("The newObjectName must be relative","newObjectName");
1660
            Contract.EndContractBlock();
1661

    
1662
            var baseUri = GetTargetUri(account);
1663
            var targetUri = baseUri.Combine(targetContainer).Combine(newObjectName);
1664
            var sourceUri = new Uri(String.Format("/{0}/{1}", sourceContainer, oldObjectName),UriKind.Relative);
1665

    
1666
            var message = new HttpRequestMessage(HttpMethod.Put, targetUri);
1667
            message.Headers.Add("X-Move-From", sourceUri.ToString());
1668
            using (var response = await _baseHttpClient.SendAsyncWithRetries(message, 3).ConfigureAwait(false))
1669
            {
1670
                var expectedCodes = new[] {HttpStatusCode.OK, HttpStatusCode.NoContent, HttpStatusCode.Created};
1671
                if (!expectedCodes.Contains(response.StatusCode))
1672
                    throw CreateWebException("MoveObject", response.StatusCode);
1673
            }
1674
        }
1675

    
1676
        public async Task DeleteObject(string account, Uri sourceContainer, Uri objectName, bool isDirectory)
1677
        {
1678
            if (sourceContainer == null)
1679
                throw new ArgumentNullException("sourceContainer", "The sourceContainer property can't be empty");
1680
            if (sourceContainer.IsAbsoluteUri)
1681
                throw new ArgumentException("The sourceContainer must be relative","sourceContainer");
1682
            if (objectName == null)
1683
                throw new ArgumentNullException("objectName", "The objectName property can't be empty");
1684
            if (objectName.IsAbsoluteUri)
1685
                throw new ArgumentException("The objectName must be relative","objectName");
1686
            Contract.EndContractBlock();
1687

    
1688

    
1689

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

    
1692
            
1693
            if (objectName.OriginalString.EndsWith(".ignore"))
1694
                using(var response = await _baseHttpClient.DeleteAsync(sourceUri)){}
1695
            else
1696
            {
1697
                var relativeUri = new Uri(String.Format("{0}/{1}", FolderConstants.TrashContainer, objectName),
1698
                                                UriKind.Relative);
1699

    
1700
/*
1701
                var relativeUri = isDirectory
1702
                                      ? new Uri(
1703
                                            String.Format("{0}/{1}?delimiter=/", FolderConstants.TrashContainer,
1704
                                                          objectName), UriKind.Relative)
1705
                                      : new Uri(String.Format("{0}/{1}", FolderConstants.TrashContainer, objectName),
1706
                                                UriKind.Relative);
1707

    
1708
*/
1709
                var targetUri = GetTargetUri(account).Combine(relativeUri);
1710

    
1711

    
1712
                var message = new HttpRequestMessage(HttpMethod.Put, targetUri);
1713
                message.Headers.Add("X-Move-From", sourceUri.ToString());
1714

    
1715
                Log.InfoFormat("[TRASH] [{0}] to [{1}]", sourceUri, targetUri);
1716
                using (var response = await _baseHttpClient.SendAsyncWithRetries(message, 3))
1717
                {
1718
                    var expectedCodes = new[]
1719
                                            {
1720
                                                HttpStatusCode.OK, HttpStatusCode.NoContent, HttpStatusCode.Created,
1721
                                                HttpStatusCode.NotFound
1722
                                            };
1723
                    if (!expectedCodes.Contains(response.StatusCode))
1724
                        throw CreateWebException("DeleteObject", response.StatusCode);
1725
                }
1726
            }
1727
/*
1728
            
1729

    
1730
            var targetUrl = FolderConstants.TrashContainer + "/" + objectName;
1731
/*
1732
            if (isDirectory)
1733
                targetUrl = targetUrl + "?delimiter=/";
1734
#1#
1735

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

    
1738
            using (var client = new RestClient(_baseClient))
1739
            {
1740
                if (!String.IsNullOrWhiteSpace(account))
1741
                    client.BaseAddress = GetAccountUrl(account);
1742

    
1743
                client.Headers.Add("X-Move-From", sourceUrl);
1744
                client.AllowedStatusCodes.Add(HttpStatusCode.NotFound);
1745
                Log.InfoFormat("[TRASH] [{0}] to [{1}]",sourceUrl,targetUrl);
1746
                client.PutWithRetry(new Uri(targetUrl,UriKind.Relative), 3);
1747

    
1748
                var expectedCodes = new[] {HttpStatusCode.OK, HttpStatusCode.NoContent, HttpStatusCode.Created,HttpStatusCode.NotFound};
1749
                if (!expectedCodes.Contains(client.StatusCode))
1750
                    throw CreateWebException("DeleteObject", client.StatusCode);
1751
            }
1752
*/
1753
        }
1754

    
1755
      
1756
        private static WebException CreateWebException(string operation, HttpStatusCode statusCode)
1757
        {
1758
            return new WebException(String.Format("{0} failed with unexpected status code {1}", operation, statusCode));
1759
        }
1760

    
1761

    
1762
        public async Task<bool> CanUpload(string account, ObjectInfo cloudFile)
1763
        {
1764
            Contract.Requires(!String.IsNullOrWhiteSpace(account));
1765
            Contract.Requires(cloudFile!=null);
1766

    
1767
                var parts = cloudFile.Name.ToString().Split('/');
1768
                var folder = String.Join("/", parts,0,parts.Length-1);
1769

    
1770
                var fileName = String.Format("{0}/{1}.pithos.ignore", folder, Guid.NewGuid());
1771
                var fileUri=fileName.ToEscapedUri();                                            
1772

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

    
1792
        ~CloudFilesClient()
1793
        {
1794
            Dispose(false);
1795
        }
1796

    
1797
        public void Dispose()
1798
        {
1799
            Dispose(true);
1800
            GC.SuppressFinalize(this);
1801
        }
1802

    
1803
        protected virtual void Dispose(bool disposing)
1804
        {
1805
            if (disposing)
1806
            {
1807
                if (_httpClientHandler!=null)
1808
                    _httpClientHandler.Dispose();
1809
                if (_baseClient!=null)
1810
                    _baseClient.Dispose();
1811
                if(_baseHttpClient!=null)
1812
                    _baseHttpClient.Dispose();
1813
                if (_baseHttpClientNoTimeout!=null)
1814
                    _baseHttpClientNoTimeout.Dispose();
1815
            }
1816
            _httpClientHandler = null;
1817
            _baseClient = null;
1818
            _baseHttpClient = null;
1819
            _baseHttpClientNoTimeout = null;
1820
        }
1821

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

    
1835
            var entry = (JContainer)JsonConvert.DeserializeObject(catalogEntry);
1836
            string key = accountToken.ToString();
1837
            return (string)entry["uuid_catalog"][key];
1838

    
1839
        }
1840

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

    
1854
            var entry = (JContainer)JsonConvert.DeserializeObject(catalogEntry);
1855
            return new Guid((string)entry["displayname_catalog"][displayName]);
1856

    
1857
        }
1858
    }
1859

    
1860
    public class ShareAccountInfo
1861
    {
1862
        public DateTime? last_modified { get; set; }
1863
        public string name { get; set; }
1864
    }
1865
}