Statistics
| Branch: | Revision:

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

History | View | Annotate | Download (21.3 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.IO;
7
using System.Linq;
8
using System.Net;
9
using System.Security.Cryptography;
10
using System.Text;
11
using System.Threading.Algorithms;
12
using System.Threading.Tasks;
13
using Newtonsoft.Json;
14
using Pithos.Interfaces;
15
using WebHeaderCollection = System.Net.WebHeaderCollection;
16

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

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

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

    
35
        public string AuthenticationUrl { get; set; }
36

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

    
43
        public bool UsePithos { get; set; }
44

    
45
        private bool _authenticated = false;
46

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

    
55
            if (_authenticated)
56
                return;
57

    
58
            UserName = userName;
59
            ApiKey = apiKey;
60
            
61
            if (UsePithos)
62
            {
63
                Token = ApiKey;
64
                string storageUrl = String.Format("{0}/{1}/{2}", AuthenticationUrl, VersionPath, UserName);
65
                StorageUrl = new Uri(storageUrl);
66
            }
67
            else
68
            {
69

    
70
                string authUrl = String.Format("{0}/{1}", AuthenticationUrl, VersionPath);
71
                var authClient = new PithosClient{BaseAddress= authUrl};
72
                if (Proxy != null)
73
                    authClient.Proxy = new WebProxy(Proxy);
74

    
75
                authClient.Headers.Add("X-Auth-User", UserName);
76
                authClient.Headers.Add("X-Auth-Key", ApiKey);
77

    
78
                var response = authClient.DownloadStringWithRetry("",3);
79

    
80
                authClient.AssertStatusOK("Authentication failed");
81

    
82
                string storageUrl = authClient.GetHeaderValue("X-Storage-Url");
83
                if (String.IsNullOrWhiteSpace(storageUrl))
84
                    throw new InvalidOperationException("Failed to obtain storage url");
85
                StorageUrl = new Uri(storageUrl);
86

    
87
                var token = authClient.GetHeaderValue("X-Auth-Token");
88
                if (String.IsNullOrWhiteSpace(token))
89
                    throw new InvalidOperationException("Failed to obtain token url");
90
                Token = token;
91
            }
92

    
93
            _client = new PithosClient{
94
                BaseAddress  = StorageUrl.AbsoluteUri,                
95
                Timeout=10000,
96
                Retries=3};
97
            if (Proxy!=null)
98
                _client.Proxy = new WebProxy(Proxy);
99
            
100
            _client.Headers.Add("X-Auth-Token", Token);
101

    
102
            Trace.TraceInformation("[AUTHENTICATE] End for {0}", userName);
103
        }
104

    
105

    
106
        public IList<ContainerInfo> ListContainers()
107
        {                        
108
                  
109
            var content=_client.DownloadStringWithRetry("",3);
110
            _client.Parameters.Clear();
111
            _client.Parameters.Add("format", "json");
112
            _client.AssertStatusOK("List Containers failed");
113

    
114
            if (_client.StatusCode==HttpStatusCode.NoContent)
115
                return new List<ContainerInfo>();
116
            var infos = JsonConvert.DeserializeObject<IList<ContainerInfo>>(content);
117
            return infos;
118

    
119
        }
120

    
121
        public IList<ObjectInfo> ListObjects(string container)
122
        {
123
            if (String.IsNullOrWhiteSpace(container))
124
                throw new ArgumentNullException("container", "The container property can't be empty");
125

    
126
            Trace.TraceInformation("[START] ListObjects");
127

    
128
            
129
            _client.Parameters.Clear();
130
            _client.Parameters.Add("format", "json");
131
            var content = _client.DownloadStringWithRetry(container, 3);
132

    
133
            _client.AssertStatusOK("ListObjects failed");
134

    
135
            var infos = JsonConvert.DeserializeObject<IList<ObjectInfo>>(content);
136

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

    
141

    
142

    
143
        public IList<ObjectInfo> ListObjects(string container,string folder)
144
        {
145
            if (String.IsNullOrWhiteSpace(container))
146
                throw new ArgumentNullException("container", "The container property can't be empty");
147

    
148
            Trace.TraceInformation("[START] ListObjects");
149

    
150
           
151
            
152
            _client.Parameters.Clear();
153
            _client.Parameters.Add("format", "json");
154
            _client.Parameters.Add("path", folder);
155
            var content = _client.DownloadStringWithRetry(container, 3);
156
            _client.AssertStatusOK("ListObjects failed");
157

    
158
            var infos = JsonConvert.DeserializeObject<IList<ObjectInfo>>(content);
159

    
160
         
161

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

    
166
 
167
        public bool ContainerExists(string container)
168
        {
169
            if (String.IsNullOrWhiteSpace(container))
170
                throw new ArgumentNullException("container", "The container property can't be empty");
171

    
172
            _client.Parameters.Clear();
173
            _client.Head(container,3);
174
           
175
            switch (_client.StatusCode)
176
            {
177
                case HttpStatusCode.OK:
178
                case HttpStatusCode.NoContent:
179
                    return true;
180
                case HttpStatusCode.NotFound:
181
                    return false;          
182
                default:
183
                    throw CreateWebException("ContainerExists", _client.StatusCode);
184
            }
185
        }
186

    
187
        public bool ObjectExists(string container,string objectName)
188
        {
189
            if (String.IsNullOrWhiteSpace(container))
190
                throw new ArgumentNullException("container", "The container property can't be empty");
191
            if (String.IsNullOrWhiteSpace(objectName))
192
                throw new ArgumentNullException("objectName", "The objectName property can't be empty");
193

    
194
            _client.Parameters.Clear();
195
            _client.Head(container + "/" + objectName, 3);
196

    
197
            switch (_client.StatusCode)
198
            {
199
                case HttpStatusCode.OK:
200
                case HttpStatusCode.NoContent:
201
                    return true;
202
                case HttpStatusCode.NotFound:
203
                    return false;
204
                default:
205
                    throw CreateWebException("ObjectExists", _client.StatusCode);
206
            }
207
            
208
        }
209

    
210
        public ObjectInfo GetObjectInfo(string container, string objectName)
211
        {
212
            if (String.IsNullOrWhiteSpace(container))
213
                throw new ArgumentNullException("container", "The container property can't be empty");
214
            if (String.IsNullOrWhiteSpace(objectName))
215
                throw new ArgumentNullException("objectName", "The objectName property can't be empty");
216

    
217
            try
218
            {
219
                _client.Parameters.Clear();
220

    
221
                _client.Head(container + "/" + objectName, 3);
222

    
223
                if (_client.TimedOut)
224
                    return ObjectInfo.Empty;
225

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

    
273
        public void CreateFolder(string container, string folder)
274
        {
275
            if (String.IsNullOrWhiteSpace(container))
276
                throw new ArgumentNullException("container", "The container property can't be empty");
277
            if (String.IsNullOrWhiteSpace(folder))
278
                throw new ArgumentNullException("folder", "The folder property can't be empty");
279

    
280
            var folderUrl=String.Format("{0}/{1}",container,folder);
281
   
282
            _client.Parameters.Clear();
283
            _client.Headers.Add("Content-Type", @"application/directory");
284
            _client.Headers.Add("Content-Length", "0");
285
            _client.PutWithRetry(folderUrl,3);
286

    
287
            if (_client.StatusCode != HttpStatusCode.Created && _client.StatusCode != HttpStatusCode.Accepted)
288
                throw CreateWebException("CreateFolder", _client.StatusCode);
289

    
290
        }
291

    
292
        public ContainerInfo GetContainerInfo(string container)
293
        {
294
            if (String.IsNullOrWhiteSpace(container))
295
                throw new ArgumentNullException("container", "The container property can't be empty");
296

    
297
            _client.Head(container);
298
            switch (_client.StatusCode)
299
            {
300
                case HttpStatusCode.NoContent:
301
                    var containerInfo = new ContainerInfo
302
                                            {
303
                                                Name = container,
304
                                                Count =long.Parse(_client.GetHeaderValue("X-Container-Object-Count")),
305
                                                Bytes = long.Parse(_client.GetHeaderValue("X-Container-Bytes-Used"))
306
                                            };
307
                    return containerInfo;
308
                case HttpStatusCode.NotFound:
309
                    return ContainerInfo.Empty;                    
310
                default:
311
                    throw CreateWebException("GetContainerInfo", _client.StatusCode);
312
            }
313
        }
314

    
315
        public void CreateContainer(string container)
316
        {
317
            if (String.IsNullOrWhiteSpace(container))
318
                throw new ArgumentNullException("container", "The container property can't be empty");
319

    
320
            _client.PutWithRetry(container,3);
321
            var expectedCodes = new[]{HttpStatusCode.Created ,HttpStatusCode.Accepted , HttpStatusCode.OK};
322
            if (!expectedCodes.Contains(_client.StatusCode))
323
                throw CreateWebException("CreateContainer", _client.StatusCode);
324
        }
325

    
326
        public void DeleteContainer(string container)
327
        {
328
            if (String.IsNullOrWhiteSpace(container))
329
                throw new ArgumentNullException("container", "The container property can't be empty");
330

    
331
            _client.DeleteWithRetry(container,3);
332
            var expectedCodes = new[] { HttpStatusCode.NotFound, HttpStatusCode.NoContent};
333
            if (!expectedCodes.Contains(_client.StatusCode))
334
                throw CreateWebException("DeleteContainer", _client.StatusCode);
335

    
336
        }
337

    
338
        /// <summary>
339
        /// 
340
        /// </summary>
341
        /// <param name="container"></param>
342
        /// <param name="objectName"></param>
343
        /// <param name="fileName"></param>
344
        /// <returns></returns>
345
        /// <remarks>>This method should have no timeout or a very long one</remarks>
346
        public Task GetObject(string container, string objectName, string fileName)
347
        {
348
            if (String.IsNullOrWhiteSpace(container))
349
                throw new ArgumentNullException("container", "The container property can't be empty");
350
            if (String.IsNullOrWhiteSpace(objectName))
351
                throw new ArgumentNullException("objectName", "The objectName property can't be empty");
352
            
353
            try
354
            {
355
                var url = String.Join("/", _client.BaseAddress, container, objectName);
356
                var uri = new Uri(url);
357

    
358
                var client = new PithosClient(_client){Timeout=0};
359
               
360

    
361
                Trace.TraceInformation("[GET] START {0}", objectName);
362
                client.DownloadProgressChanged += (sender, args) => 
363
                    Trace.TraceInformation("[GET PROGRESS] {0} {1}% {2} of {3}",
364
                                    fileName, args.ProgressPercentage,
365
                                    args.BytesReceived,
366
                                    args.TotalBytesToReceive);
367
                
368
                return _client.DownloadFileTask(uri, fileName)
369
                    .ContinueWith(download =>
370
                                      {
371
                                          client.Dispose();
372

    
373
                                          if (download.IsFaulted)
374
                                          {
375
                                              Trace.TraceError("[GET] FAIL for {0} with \r{1}", objectName,
376
                                                               download.Exception);
377
                                          }
378
                                          else
379
                                          {
380
                                              Trace.TraceInformation("[GET] END {0}", objectName);                                             
381
                                          }
382
                                      });
383
            }
384
            catch (Exception exc)
385
            {
386
                Trace.TraceError("[GET] END {0} with {1}", objectName, exc);
387
                throw;
388
            }
389

    
390

    
391

    
392
        }
393

    
394
        /// <summary>
395
        /// 
396
        /// </summary>
397
        /// <param name="container"></param>
398
        /// <param name="objectName"></param>
399
        /// <param name="fileName"></param>
400
        /// <param name="hash">Optional hash value for the file. If no hash is provided, the method calculates a new hash</param>
401
        /// <remarks>>This method should have no timeout or a very long one</remarks>
402
        public Task PutObject(string container, string objectName, string fileName, string hash = null)
403
        {
404
            if (String.IsNullOrWhiteSpace(container))
405
                throw new ArgumentNullException("container", "The container property can't be empty");
406
            if (String.IsNullOrWhiteSpace(objectName))
407
                throw new ArgumentNullException("objectName", "The objectName property can't be empty");
408
            if (String.IsNullOrWhiteSpace(fileName))
409
                throw new ArgumentNullException("fileName", "The fileName property can't be empty");
410
            if (!File.Exists(fileName))
411
                throw new FileNotFoundException("The file does not exist",fileName);
412

    
413
            
414
            try
415
            {
416
                var url = String.Join("/",_client.BaseAddress,container,objectName);
417
                var uri = new Uri(url);
418

    
419
                var client = new PithosClient(_client){Timeout=0};           
420
                string etag = hash ?? CalculateHash(fileName);
421

    
422
                client.Headers.Add("Content-Type", "application/octet-stream");
423
                client.Headers.Add("ETag", etag);
424

    
425

    
426
                Trace.TraceInformation("[PUT] START {0}", objectName);
427
                client.UploadProgressChanged += (sender, args) =>
428
                {
429
                    Trace.TraceInformation("[PUT PROGRESS] {0} {1}% {2} of {3}", fileName, args.ProgressPercentage, args.BytesSent, args.TotalBytesToSend);
430
                };
431
               
432
                return client.UploadFileTask(uri, "PUT", fileName)
433
                    .ContinueWith(upload=>
434
                                      {
435
                                          client.Dispose();
436

    
437
                                          if (upload.IsFaulted)
438
                                          {                                              
439
                                              Trace.TraceError("[PUT] FAIL for {0} with \r{1}",objectName,upload.Exception);
440
                                          }
441
                                          else
442
                                            Trace.TraceInformation("[PUT] END {0}", objectName);
443
                                      });
444
            }
445
            catch (Exception exc)
446
            {
447
                Trace.TraceError("[PUT] END {0} with {1}", objectName, exc);
448
                throw;
449
            }                
450

    
451
        }
452
       
453
        
454
        private static string CalculateHash(string fileName)
455
        {
456
            string hash;
457
            using (var hasher = MD5.Create())
458
            using(var stream=File.OpenRead(fileName))
459
            {
460
                var hashBuilder=new StringBuilder();
461
                foreach (byte b in hasher.ComputeHash(stream))
462
                    hashBuilder.Append(b.ToString("x2").ToLower());
463
                hash = hashBuilder.ToString();                
464
            }
465
            return hash;
466
        }
467

    
468
        public void DeleteObject(string container, string objectName)
469
        {
470
            if (String.IsNullOrWhiteSpace(container))
471
                throw new ArgumentNullException("container", "The container property can't be empty");
472
            if (String.IsNullOrWhiteSpace(objectName))
473
                throw new ArgumentNullException("objectName", "The objectName property can't be empty");
474

    
475
            _client.DeleteWithRetry(container + "/" + objectName,3);
476

    
477
            var expectedCodes = new[] { HttpStatusCode.NotFound, HttpStatusCode.NoContent };
478
            if (!expectedCodes.Contains(_client.StatusCode))
479
                throw CreateWebException("DeleteObject", _client.StatusCode);
480
   
481
        }
482

    
483
        public void MoveObject(string sourceContainer, string oldObjectName, string targetContainer,string newObjectName)
484
        {
485
            if (String.IsNullOrWhiteSpace(sourceContainer))
486
                throw new ArgumentNullException("sourceContainer", "The container property can't be empty");
487
            if (String.IsNullOrWhiteSpace(oldObjectName))
488
                throw new ArgumentNullException("oldObjectName", "The oldObjectName property can't be empty");
489
            if (String.IsNullOrWhiteSpace(targetContainer))
490
                throw new ArgumentNullException("targetContainer", "The container property can't be empty");
491
            if (String.IsNullOrWhiteSpace(newObjectName))
492
                throw new ArgumentNullException("newObjectName", "The newObjectName property can't be empty");
493

    
494
            var targetUrl = targetContainer + "/" + newObjectName;
495
            var sourceUrl = String.Format("/{0}/{1}", sourceContainer, oldObjectName);
496

    
497
            var client = new PithosClient(_client);
498
            client.Headers.Add("X-Copy-From", sourceUrl);
499
            client.PutWithRetry(targetUrl,3);
500

    
501
            var expectedCodes = new[] { HttpStatusCode.OK ,HttpStatusCode.NoContent ,HttpStatusCode.Created };
502
            if (expectedCodes.Contains(client.StatusCode))
503
            {
504
                this.DeleteObject(sourceContainer,oldObjectName);
505
            }                
506
            else
507
                throw CreateWebException("MoveObject", client.StatusCode);
508
        }
509

    
510
      
511
        private static WebException CreateWebException(string operation, HttpStatusCode statusCode)
512
        {
513
            return new WebException(String.Format("{0} failed with unexpected status code {1}", operation, statusCode));
514
        }
515

    
516
        
517
    }
518
}