Statistics
| Branch: | Revision:

root / trunk / Pithos.Network / CloudFilesClient.cs @ 5d4e820b

History | View | Annotate | Download (22.7 kB)

1
using System;
2
using System.Collections.Generic;
3
using System.ComponentModel.Composition;
4
using System.Diagnostics;
5
using System.Diagnostics.Contracts;
6
using System.Globalization;
7
using System.IO;
8
using System.Linq;
9
using System.Net;
10
using System.Security.Cryptography;
11
using System.Text;
12
using System.Threading.Algorithms;
13
using System.Threading.Tasks;
14
using Newtonsoft.Json;
15
using Pithos.Interfaces;
16
using WebHeaderCollection = System.Net.WebHeaderCollection;
17

    
18
namespace Pithos.Network
19
{
20
    [Export(typeof(ICloudClient))]
21
    public class CloudFilesClient:ICloudClient
22
    {
23

    
24
        private RestClient _baseClient;
25
        private readonly TimeSpan _shortTimeout = TimeSpan.FromSeconds(10);
26
        private readonly int _retries = 5;        
27
        public string ApiKey { get; set; }
28
        public string UserName { get; set; }
29
        public Uri StorageUrl { get; set; }
30
        public string Token { get; set; }
31
        public Uri Proxy { get; set; }
32

    
33
        public double DownloadPercentLimit { get; set; }
34
        public double UploadPercentLimit { get; set; }
35

    
36
        public string AuthenticationUrl { get; set; }
37

    
38
 
39
        public string VersionPath
40
        {
41
            get { return UsePithos ? "v1" : "v1.0"; }
42
        }
43

    
44
        public bool UsePithos { get; set; }
45

    
46
        private bool _authenticated = false;
47

    
48
        public void Authenticate(string userName,string apiKey)
49
        {
50
            Trace.TraceInformation("[AUTHENTICATE] Start for {0}", userName);
51
            if (String.IsNullOrWhiteSpace(userName))
52
                throw new ArgumentNullException("userName", "The userName property can't be empty");
53
            if (String.IsNullOrWhiteSpace(apiKey))
54
                throw new ArgumentNullException("apiKey", "The apiKey property can't be empty");
55

    
56
            if (_authenticated)
57
                return;
58

    
59
            UserName = userName;
60
            ApiKey = apiKey;
61
            
62

    
63
            using (var authClient = new RestClient{BaseAddress=AuthenticationUrl})
64
            {
65
                if (Proxy != null)
66
                    authClient.Proxy = new WebProxy(Proxy);
67

    
68
                authClient.Headers.Add("X-Auth-User", UserName);
69
                authClient.Headers.Add("X-Auth-Key", ApiKey);
70

    
71
                authClient.DownloadStringWithRetry(VersionPath, 3);
72

    
73
                authClient.AssertStatusOK("Authentication failed");
74

    
75
                var storageUrl = authClient.GetHeaderValue("X-Storage-Url");
76
                if (String.IsNullOrWhiteSpace(storageUrl))
77
                    throw new InvalidOperationException("Failed to obtain storage url");
78
                StorageUrl = new Uri(storageUrl);
79
                
80
                var token = authClient.GetHeaderValue("X-Auth-Token");
81
                if (String.IsNullOrWhiteSpace(token))
82
                    throw new InvalidOperationException("Failed to obtain token url");
83
                Token = token;
84
            }
85

    
86
            _baseClient = new RestClient{
87
                BaseAddress  = StorageUrl.AbsoluteUri,                
88
                Timeout=10000,
89
                Retries=3};
90
            if (Proxy!=null)
91
                _baseClient.Proxy = new WebProxy(Proxy);
92

    
93
            _baseClient.Headers.Add("X-Auth-Token", Token);
94

    
95
            Trace.TraceInformation("[AUTHENTICATE] End for {0}", userName);
96
        }
97

    
98

    
99
        public IList<ContainerInfo> ListContainers()
100
        {
101
            using (var client = new RestClient(_baseClient))
102
            {
103
                var content = client.DownloadStringWithRetry("", 3);
104
                client.Parameters.Clear();
105
                client.Parameters.Add("format", "json");
106
                client.AssertStatusOK("List Containers failed");
107

    
108
                if (client.StatusCode == HttpStatusCode.NoContent)
109
                    return new List<ContainerInfo>();
110
                var infos = JsonConvert.DeserializeObject<IList<ContainerInfo>>(content);
111
                return infos;
112
            }
113

    
114
        }
115

    
116
        public IList<ObjectInfo> ListObjects(string container, DateTime? since = null)
117
        {
118
            if (String.IsNullOrWhiteSpace(container))
119
                throw new ArgumentNullException("container");
120
            Contract.EndContractBlock();
121

    
122
            Trace.TraceInformation("[START] ListObjects");
123

    
124
            using (var client = new RestClient(_baseClient))
125
            {
126
                client.Parameters.Clear();
127
                client.Parameters.Add("format", "json");
128
                client.IfModifiedSince = since;
129
                var content = client.DownloadStringWithRetry(container, 3);
130

    
131
                client.AssertStatusOK("ListObjects failed");
132

    
133
                var infos = JsonConvert.DeserializeObject<IList<ObjectInfo>>(content);
134

    
135
                Trace.TraceInformation("[END] ListObjects");
136
                return infos;
137
            }
138
        }
139

    
140

    
141

    
142
        public IList<ObjectInfo> ListObjects(string container, string folder, DateTime? since = null)
143
        {
144
            if (String.IsNullOrWhiteSpace(container))
145
                throw new ArgumentNullException("container");
146
            if (String.IsNullOrWhiteSpace(folder))
147
                throw new ArgumentNullException("folder");
148
            Contract.EndContractBlock();
149

    
150
            Trace.TraceInformation("[START] ListObjects");
151

    
152
            using (var client = new RestClient(_baseClient))
153
            {
154
                client.Parameters.Clear();
155
                client.Parameters.Add("format", "json");
156
                client.Parameters.Add("path", folder);
157
                client.IfModifiedSince = since;
158
                var content = client.DownloadStringWithRetry(container, 3);
159
                client.AssertStatusOK("ListObjects failed");
160

    
161
                var infos = JsonConvert.DeserializeObject<IList<ObjectInfo>>(content);
162

    
163
                Trace.TraceInformation("[END] ListObjects");
164
                return infos;
165
            }
166
        }
167

    
168
 
169
        public bool ContainerExists(string container)
170
        {
171
            if (String.IsNullOrWhiteSpace(container))
172
                throw new ArgumentNullException("container", "The container property can't be empty");
173
            using (var client = new RestClient(_baseClient))
174
            {
175
                client.Parameters.Clear();
176
                client.Head(container, 3);
177

    
178
                switch (client.StatusCode)
179
                {
180
                    case HttpStatusCode.OK:
181
                    case HttpStatusCode.NoContent:
182
                        return true;
183
                    case HttpStatusCode.NotFound:
184
                        return false;
185
                    default:
186
                        throw CreateWebException("ContainerExists", client.StatusCode);
187
                }
188
            }
189
        }
190

    
191
        public bool ObjectExists(string container,string objectName)
192
        {
193
            if (String.IsNullOrWhiteSpace(container))
194
                throw new ArgumentNullException("container", "The container property can't be empty");
195
            if (String.IsNullOrWhiteSpace(objectName))
196
                throw new ArgumentNullException("objectName", "The objectName property can't be empty");
197
            using (var client = new RestClient(_baseClient))
198
            {
199
                client.Parameters.Clear();
200
                client.Head(container + "/" + objectName, 3);
201

    
202
                switch (client.StatusCode)
203
                {
204
                    case HttpStatusCode.OK:
205
                    case HttpStatusCode.NoContent:
206
                        return true;
207
                    case HttpStatusCode.NotFound:
208
                        return false;
209
                    default:
210
                        throw CreateWebException("ObjectExists", client.StatusCode);
211
                }
212
            }
213

    
214
        }
215

    
216
        public ObjectInfo GetObjectInfo(string container, string objectName)
217
        {
218
            if (String.IsNullOrWhiteSpace(container))
219
                throw new ArgumentNullException("container", "The container property can't be empty");
220
            if (String.IsNullOrWhiteSpace(objectName))
221
                throw new ArgumentNullException("objectName", "The objectName property can't be empty");
222

    
223
            using (var client = new RestClient(_baseClient))
224
            {
225
                try
226
                {
227
                    client.Parameters.Clear();
228

    
229
                    client.Head(container + "/" + objectName, 3);
230

    
231
                    if (client.TimedOut)
232
                        return ObjectInfo.Empty;
233

    
234
                    switch (client.StatusCode)
235
                    {
236
                        case HttpStatusCode.OK:
237
                        case HttpStatusCode.NoContent:
238
                            var keys = client.ResponseHeaders.AllKeys.AsQueryable();
239
                            var tags = (from key in keys
240
                                        where key.StartsWith("X-Object-Meta-")
241
                                        let name = key.Substring(14)
242
                                        select new {Name = name, Value = client.ResponseHeaders[name]})
243
                                .ToDictionary(t => t.Name, t => t.Value);
244
                            var extensions = (from key in keys
245
                                              where key.StartsWith("X-Object-") && !key.StartsWith("X-Object-Meta-")
246
                                              let name = key.Substring(9)
247
                                              select new {Name = name, Value = client.ResponseHeaders[name]})
248
                                .ToDictionary(t => t.Name, t => t.Value);
249
                            return new ObjectInfo
250
                                       {
251
                                           Name = objectName,
252
                                           /*Bytes =
253
                                               long.Parse(client.GetHeaderValue("Content-Length")),*/
254
                                           Hash = client.GetHeaderValue("ETag"),
255
                                           Content_Type = client.GetHeaderValue("Content-Type"),
256
                                           Tags = tags,
257
                                           Extensions = extensions
258
                                       };
259
                        case HttpStatusCode.NotFound:
260
                            return ObjectInfo.Empty;
261
                        default:
262
                            throw new WebException(
263
                                String.Format("[FAIL] GetObjectInfo for {0} failed with unexpected status code {1}",
264
                                              objectName, client.StatusCode));
265
                    }
266

    
267
                }
268
                catch(RetryException e)
269
                {
270
                    Trace.TraceWarning("[RETRY FAIL] GetObjectInfo for {0} failed.");
271
                    return ObjectInfo.Empty;
272
                }
273
                catch(WebException e)
274
                {
275
                    Trace.TraceError(
276
                        String.Format("[FAIL] GetObjectInfo for {0} failed with unexpected status code {1}",
277
                                      objectName, client.StatusCode), e);
278
                    throw;
279
                }
280
            }
281

    
282
        }
283

    
284
        public void CreateFolder(string container, string folder)
285
        {
286
            if (String.IsNullOrWhiteSpace(container))
287
                throw new ArgumentNullException("container", "The container property can't be empty");
288
            if (String.IsNullOrWhiteSpace(folder))
289
                throw new ArgumentNullException("folder", "The folder property can't be empty");
290

    
291
            var folderUrl=String.Format("{0}/{1}",container,folder);
292
            using (var client = new RestClient(_baseClient))
293
            {
294
                client.Parameters.Clear();
295
                client.Headers.Add("Content-Type", @"application/directory");
296
                client.Headers.Add("Content-Length", "0");
297
                client.PutWithRetry(folderUrl, 3);
298

    
299
                if (client.StatusCode != HttpStatusCode.Created && client.StatusCode != HttpStatusCode.Accepted)
300
                    throw CreateWebException("CreateFolder", client.StatusCode);
301
            }
302
        }
303

    
304
        public ContainerInfo GetContainerInfo(string container)
305
        {
306
            if (String.IsNullOrWhiteSpace(container))
307
                throw new ArgumentNullException("container", "The container property can't be empty");
308
            using (var client = new RestClient(_baseClient))
309
            {
310
                client.Head(container);
311
                switch (client.StatusCode)
312
                {
313
                    case HttpStatusCode.NoContent:
314
                        var containerInfo = new ContainerInfo
315
                                                {
316
                                                    Name = container,
317
                                                    Count =
318
                                                        long.Parse(client.GetHeaderValue("X-Container-Object-Count")),
319
                                                    Bytes = long.Parse(client.GetHeaderValue("X-Container-Bytes-Used"))
320
                                                };
321
                        return containerInfo;
322
                    case HttpStatusCode.NotFound:
323
                        return ContainerInfo.Empty;
324
                    default:
325
                        throw CreateWebException("GetContainerInfo", client.StatusCode);
326
                }
327
            }
328
        }
329

    
330
        public void CreateContainer(string container)
331
        {
332
            if (String.IsNullOrWhiteSpace(container))
333
                throw new ArgumentNullException("container", "The container property can't be empty");
334
            using (var client = new RestClient(_baseClient))
335
            {
336
                client.PutWithRetry(container, 3);
337
                var expectedCodes = new[] {HttpStatusCode.Created, HttpStatusCode.Accepted, HttpStatusCode.OK};
338
                if (!expectedCodes.Contains(client.StatusCode))
339
                    throw CreateWebException("CreateContainer", client.StatusCode);
340
            }
341
        }
342

    
343
        public void DeleteContainer(string container)
344
        {
345
            if (String.IsNullOrWhiteSpace(container))
346
                throw new ArgumentNullException("container", "The container property can't be empty");
347
            using (var client = new RestClient(_baseClient))
348
            {
349
                client.DeleteWithRetry(container, 3);
350
                var expectedCodes = new[] {HttpStatusCode.NotFound, HttpStatusCode.NoContent};
351
                if (!expectedCodes.Contains(client.StatusCode))
352
                    throw CreateWebException("DeleteContainer", client.StatusCode);
353
            }
354

    
355
        }
356

    
357
        /// <summary>
358
        /// 
359
        /// </summary>
360
        /// <param name="container"></param>
361
        /// <param name="objectName"></param>
362
        /// <param name="fileName"></param>
363
        /// <returns></returns>
364
        /// <remarks>>This method should have no timeout or a very long one</remarks>
365
        public Task GetObject(string container, string objectName, string fileName)
366
        {
367
            if (String.IsNullOrWhiteSpace(container))
368
                throw new ArgumentNullException("container", "The container property can't be empty");
369
            if (String.IsNullOrWhiteSpace(objectName))
370
                throw new ArgumentNullException("objectName", "The objectName property can't be empty");
371
            
372
            try
373
            {
374

    
375
                var url = String.Join("/", _baseClient.BaseAddress, container, objectName);
376
                var uri = new Uri(url);
377

    
378
                var client = new RestClient(_baseClient) { Timeout = 0 };
379
               
380

    
381
                Trace.TraceInformation("[GET] START {0}", objectName);
382
                client.DownloadProgressChanged += (sender, args) => 
383
                    Trace.TraceInformation("[GET PROGRESS] {0} {1}% {2} of {3}",
384
                                    fileName, args.ProgressPercentage,
385
                                    args.BytesReceived,
386
                                    args.TotalBytesToReceive);
387
                
388
                return client.DownloadFileTask(uri, fileName)
389
                    .ContinueWith(download =>
390
                                      {
391
                                          client.Dispose();
392

    
393
                                          if (download.IsFaulted)
394
                                          {
395
                                              Trace.TraceError("[GET] FAIL for {0} with \r{1}", objectName,
396
                                                               download.Exception);
397
                                          }
398
                                          else
399
                                          {
400
                                              Trace.TraceInformation("[GET] END {0}", objectName);                                             
401
                                          }
402
                                      });
403
            }
404
            catch (Exception exc)
405
            {
406
                Trace.TraceError("[GET] END {0} with {1}", objectName, exc);
407
                throw;
408
            }
409

    
410

    
411

    
412
        }
413

    
414
        /// <summary>
415
        /// 
416
        /// </summary>
417
        /// <param name="container"></param>
418
        /// <param name="objectName"></param>
419
        /// <param name="fileName"></param>
420
        /// <param name="hash">Optional hash value for the file. If no hash is provided, the method calculates a new hash</param>
421
        /// <remarks>>This method should have no timeout or a very long one</remarks>
422
        public Task PutObject(string container, string objectName, string fileName, string hash = null)
423
        {
424
            if (String.IsNullOrWhiteSpace(container))
425
                throw new ArgumentNullException("container", "The container property can't be empty");
426
            if (String.IsNullOrWhiteSpace(objectName))
427
                throw new ArgumentNullException("objectName", "The objectName property can't be empty");
428
            if (String.IsNullOrWhiteSpace(fileName))
429
                throw new ArgumentNullException("fileName", "The fileName property can't be empty");
430
            if (!File.Exists(fileName))
431
                throw new FileNotFoundException("The file does not exist",fileName);
432

    
433
            
434
            try
435
            {
436
                var url = String.Join("/",_baseClient.BaseAddress,container,objectName);
437
                var uri = new Uri(url);
438

    
439
                var client = new RestClient(_baseClient){Timeout=0};           
440
                string etag = hash ?? CalculateHash(fileName);
441

    
442
                client.Headers.Add("Content-Type", "application/octet-stream");
443
                client.Headers.Add("ETag", etag);
444

    
445

    
446
                Trace.TraceInformation("[PUT] START {0}", objectName);
447
                client.UploadProgressChanged += (sender, args) =>
448
                {
449
                    Trace.TraceInformation("[PUT PROGRESS] {0} {1}% {2} of {3}", fileName, args.ProgressPercentage, args.BytesSent, args.TotalBytesToSend);
450
                };
451
               
452
                return client.UploadFileTask(uri, "PUT", fileName)
453
                    .ContinueWith(upload=>
454
                                      {
455
                                          client.Dispose();
456

    
457
                                          if (upload.IsFaulted)
458
                                          {                                              
459
                                              Trace.TraceError("[PUT] FAIL for {0} with \r{1}",objectName,upload.Exception);
460
                                          }
461
                                          else
462
                                            Trace.TraceInformation("[PUT] END {0}", objectName);
463
                                      });
464
            }
465
            catch (Exception exc)
466
            {
467
                Trace.TraceError("[PUT] END {0} with {1}", objectName, exc);
468
                throw;
469
            }                
470

    
471
        }
472
       
473
        
474
        private static string CalculateHash(string fileName)
475
        {
476
            string hash;
477
            using (var hasher = MD5.Create())
478
            using(var stream=File.OpenRead(fileName))
479
            {
480
                var hashBuilder=new StringBuilder();
481
                foreach (byte b in hasher.ComputeHash(stream))
482
                    hashBuilder.Append(b.ToString("x2").ToLower());
483
                hash = hashBuilder.ToString();                
484
            }
485
            return hash;
486
        }
487

    
488
        public void DeleteObject(string container, string objectName)
489
        {
490
            if (String.IsNullOrWhiteSpace(container))
491
                throw new ArgumentNullException("container", "The container property can't be empty");
492
            if (String.IsNullOrWhiteSpace(objectName))
493
                throw new ArgumentNullException("objectName", "The objectName property can't be empty");
494
            using (var client = new RestClient(_baseClient))
495
            {
496

    
497
                client.DeleteWithRetry(container + "/" + objectName, 3);
498

    
499
                var expectedCodes = new[] {HttpStatusCode.NotFound, HttpStatusCode.NoContent};
500
                if (!expectedCodes.Contains(client.StatusCode))
501
                    throw CreateWebException("DeleteObject", client.StatusCode);
502
            }
503

    
504
        }
505

    
506
        public void MoveObject(string sourceContainer, string oldObjectName, string targetContainer,string newObjectName)
507
        {
508
            if (String.IsNullOrWhiteSpace(sourceContainer))
509
                throw new ArgumentNullException("sourceContainer", "The container property can't be empty");
510
            if (String.IsNullOrWhiteSpace(oldObjectName))
511
                throw new ArgumentNullException("oldObjectName", "The oldObjectName property can't be empty");
512
            if (String.IsNullOrWhiteSpace(targetContainer))
513
                throw new ArgumentNullException("targetContainer", "The container property can't be empty");
514
            if (String.IsNullOrWhiteSpace(newObjectName))
515
                throw new ArgumentNullException("newObjectName", "The newObjectName property can't be empty");
516

    
517
            var targetUrl = targetContainer + "/" + newObjectName;
518
            var sourceUrl = String.Format("/{0}/{1}", sourceContainer, oldObjectName);
519

    
520
            using (var client = new RestClient(_baseClient))
521
            {
522
                client.Headers.Add("X-Copy-From", sourceUrl);
523
                client.PutWithRetry(targetUrl, 3);
524

    
525
                var expectedCodes = new[] {HttpStatusCode.OK, HttpStatusCode.NoContent, HttpStatusCode.Created};
526
                if (expectedCodes.Contains(client.StatusCode))
527
                {
528
                    this.DeleteObject(sourceContainer, oldObjectName);
529
                }
530
                else
531
                    throw CreateWebException("MoveObject", client.StatusCode);
532
            }
533
        }
534

    
535
      
536
        private static WebException CreateWebException(string operation, HttpStatusCode statusCode)
537
        {
538
            return new WebException(String.Format("{0} failed with unexpected status code {1}", operation, statusCode));
539
        }
540

    
541
        
542
    }
543
}