Statistics
| Branch: | Revision:

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

History | View | Annotate | Download (23.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.IO;
7
using System.Linq;
8
using System.Net;
9
using System.Security.Cryptography;
10
using System.Text;
11
using System.Threading.Tasks;
12
using Hammock;
13
using Hammock.Caching;
14
using Hammock.Retries;
15
using Hammock.Serialization;
16
using Hammock.Tasks;
17
using Hammock.Web;
18
using Newtonsoft.Json;
19
using Pithos.Interfaces;
20

    
21
namespace Pithos.Network
22
{
23
    [Export(typeof(ICloudClient))]
24
    public class CloudFilesClient:ICloudClient
25
    {
26
        string _rackSpaceAuthUrl = "https://auth.api.rackspacecloud.com";
27
        private string _pithosAuthUrl = "http://pithos.dev.grnet.gr";
28

    
29
        private RestClient _client;
30
        private readonly TimeSpan _shortTimeout = TimeSpan.FromSeconds(10);
31
        private readonly int _retries = 5;
32
        private RetryPolicy _retryPolicy;
33
        public string ApiKey { get; set; }
34
        public string UserName { get; set; }
35
        public Uri StorageUrl { get; set; }
36
        public string Token { get; set; }
37
        public Uri Proxy { get; set; }
38

    
39
        public double DownloadPercentLimit { get; set; }
40
        public double UploadPercentLimit { get; set; }
41
        
42
        public string AuthUrl
43
        {
44
            get { return UsePithos ? _pithosAuthUrl : _rackSpaceAuthUrl; }
45
        }
46
 
47
        public string VersionPath
48
        {
49
            get { return UsePithos ? "v1" : "v1.0"; }
50
        }
51

    
52
        public bool UsePithos { get; set; }
53

    
54
        public void Authenticate(string userName,string apiKey)
55
        {
56
            Trace.TraceInformation("[AUTHENTICATE] Start for {0}",userName);
57
            if (String.IsNullOrWhiteSpace(userName))
58
                throw new ArgumentNullException("userName","The userName property can't be empty");
59
            if (String.IsNullOrWhiteSpace(apiKey))
60
                throw new ArgumentNullException("apiKey", "The apiKey property can't be empty");
61
            
62

    
63
            UserName = userName;
64
            ApiKey = apiKey;
65

    
66
            string authUrl = UsePithos ? String.Format("{0}/{1}/{2}", AuthUrl, VersionPath,UserName) 
67
                                    : String.Format("{0}/{1}", AuthUrl, VersionPath);
68
            
69
            var proxy = Proxy != null ? Proxy.ToString():null;
70
            
71
            var authClient = new RestClient{Path=authUrl,Proxy=proxy};            
72
            
73
            authClient.AddHeader("X-Auth-User", UserName);
74
            authClient.AddHeader("X-Auth-Key", ApiKey);            
75
            
76
            var response=authClient.Request();
77
            
78
            ThrowIfNotStatusOK(response, "Authentication failed");
79

    
80
            var keys = response.Headers.AllKeys.AsQueryable();
81

    
82
            string storageUrl =UsePithos? 
83
                String.Format("{0}/{1}/{2}",AuthUrl,VersionPath,UserName)
84
                :GetHeaderValue("X-Storage-Url", response, keys);
85
            
86
            if (String.IsNullOrWhiteSpace(storageUrl))
87
                throw new InvalidOperationException("Failed to obtain storage url");
88
            StorageUrl = new Uri(storageUrl);
89

    
90
            if (!UsePithos)
91
            {
92
                var token = GetHeaderValue("X-Auth-Token", response, keys);
93
                if (String.IsNullOrWhiteSpace(token))
94
                    throw new InvalidOperationException("Failed to obtain token url");
95
                Token = token;
96
            }
97
            else
98
                Token = "0000";
99

    
100
            _retryPolicy = new RetryPolicy { RetryCount = _retries };
101
            _retryPolicy.RetryConditions.Add(new TimeoutRetryCondition());
102

    
103
            _client = new RestClient { Authority = StorageUrl.AbsoluteUri, Path = UserName, Proxy = proxy };
104
            _client.FileProgress += OnFileProgress;
105
            
106
            _client.AddHeader("X-Auth-Token", Token);
107
            if (UsePithos)
108
            {
109
                _client.AddHeader("X-Auth-User", UserName);
110
                _client.AddHeader("X-Auth-Key",ApiKey);                
111
            }
112

    
113
            Trace.TraceInformation("[AUTHENTICATE] End for {0}", userName);
114
        }
115

    
116
        private void OnFileProgress(object sender, FileProgressEventArgs e)
117
        {
118
            Trace.TraceInformation("[PROGRESS] {0} {1:p} {2} of {3}",e.FileName,(double)e.BytesWritten/e.TotalBytes, e.BytesWritten,e.TotalBytes);            
119
        }
120

    
121
        public IList<ContainerInfo> ListContainers()
122
        {                        
123
            //Workaround for Hammock quirk: Hammock always
124
            //appends a / unless a Path is specified.
125
            
126
            //Create a request with a complete path
127
            var request = new RestRequest { Path = StorageUrl.ToString(), RetryPolicy = _retryPolicy,Timeout = _shortTimeout };
128
            request.AddParameter("format","json");
129
            //Create a client clone
130
            var client = new RestClient{Proxy=Proxy.ToString()};
131
            foreach (var header in _client.GetAllHeaders())
132
            {
133
                client.AddHeader(header.Name,header.Value);
134
            }            
135

    
136
            var response = client.Request(request);
137

    
138
            if (response.StatusCode == HttpStatusCode.NoContent)
139
                return new List<ContainerInfo>();
140

    
141
            ThrowIfNotStatusOK(response, "List Containers failed");
142

    
143

    
144
            var infos=JsonConvert.DeserializeObject<IList<ContainerInfo>>(response.Content);
145
            
146
            return infos;
147
        }
148

    
149
        public IList<ObjectInfo> ListObjects(string container)
150
        {
151
            if (String.IsNullOrWhiteSpace(container))
152
                throw new ArgumentNullException("container", "The container property can't be empty");
153

    
154
            Trace.TraceInformation("[START] ListObjects");
155

    
156
            var request = new RestRequest { Path = container, RetryPolicy = _retryPolicy, Timeout = TimeSpan.FromMinutes(1) };
157
            request.AddParameter("format", "json");
158
            var response = _client.Request(request);
159
            
160
            var infos = InfosFromContent(response);
161

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

    
166

    
167

    
168
        public IList<ObjectInfo> ListObjects(string container,string folder)
169
        {
170
            if (String.IsNullOrWhiteSpace(container))
171
                throw new ArgumentNullException("container", "The container property can't be empty");
172

    
173
            Trace.TraceInformation("[START] ListObjects");
174

    
175
            var request = new RestRequest { Path = container,RetryPolicy = _retryPolicy, Timeout = TimeSpan.FromMinutes(1) };
176
            request.AddParameter("format", "json");
177
            request.AddParameter("path", folder);
178
            var response = _client.Request(request);
179
            
180
            var infos = InfosFromContent(response);
181

    
182
            Trace.TraceInformation("[END] ListObjects");
183
            return infos;
184
        }
185

    
186
        private static IList<ObjectInfo> InfosFromContent(RestResponse response)
187
        {
188
            if (response.TimedOut)
189
                return new List<ObjectInfo>();
190

    
191
            if (response.StatusCode == 0)
192
                return new List<ObjectInfo>();
193

    
194
            if (response.StatusCode == HttpStatusCode.NoContent)
195
                return new List<ObjectInfo>();
196

    
197

    
198
            var statusCode = (int)response.StatusCode;
199
            if (statusCode < 200 || statusCode >= 300)
200
            {
201
                Trace.TraceWarning("ListObjects failed with code {1} - {2}", response.StatusCode, response.StatusDescription);
202
                return new List<ObjectInfo>();
203
            }
204

    
205
            var infos = JsonConvert.DeserializeObject<IList<ObjectInfo>>(response.Content);
206
            return infos;
207
        }
208

    
209
        public bool ContainerExists(string container)
210
        {
211
            if (String.IsNullOrWhiteSpace(container))
212
                throw new ArgumentNullException("container", "The container property can't be empty");
213

    
214
            var request = new RestRequest { Path = container, Method = WebMethod.Head, RetryPolicy = _retryPolicy,Timeout = _shortTimeout };
215
            var response = _client.Request(request);
216

    
217
            switch(response.StatusCode)
218
            {
219
                case HttpStatusCode.NoContent:
220
                    return true;
221
                case HttpStatusCode.NotFound:
222
                    return false;                    
223
                default:
224
                    throw new WebException(String.Format("ContainerExists failed with unexpected status code {0}",response.StatusCode));
225
            }
226
        }
227

    
228
        public bool ObjectExists(string container,string objectName)
229
        {
230
            if (String.IsNullOrWhiteSpace(container))
231
                throw new ArgumentNullException("container", "The container property can't be empty");
232
            if (String.IsNullOrWhiteSpace(objectName))
233
                throw new ArgumentNullException("objectName", "The objectName property can't be empty");
234

    
235

    
236
            var request = new RestRequest { Path = container + "/" + objectName, Method = WebMethod.Head,RetryPolicy = _retryPolicy, Timeout = _shortTimeout };
237
            var response = _client.Request(request);
238

    
239
            switch (response.StatusCode)
240
            {
241
                case HttpStatusCode.OK:
242
                case HttpStatusCode.NoContent:
243
                    return true;
244
                case HttpStatusCode.NotFound:
245
                    return false;
246
                default:
247
                    throw new WebException(String.Format("ObjectExists failed with unexpected status code {0}", response.StatusCode));
248
            }
249
            
250
        }
251

    
252
        public ObjectInfo GetObjectInfo(string container, string objectName)
253
        {
254
            if (String.IsNullOrWhiteSpace(container))
255
                throw new ArgumentNullException("container", "The container property can't be empty");
256
            if (String.IsNullOrWhiteSpace(objectName))
257
                throw new ArgumentNullException("objectName", "The objectName property can't be empty");
258

    
259

    
260
            var request = new RestRequest { Path = container + "/" + objectName, Method = WebMethod.Head, RetryPolicy = _retryPolicy,Timeout = _shortTimeout };
261
            var response = _client.Request(request);
262

    
263
            if (response.TimedOut)
264
                return ObjectInfo.Empty;
265

    
266
            switch (response.StatusCode)
267
            {
268
                case HttpStatusCode.OK:
269
                case HttpStatusCode.NoContent:
270
                    var keys = response.Headers.AllKeys.AsQueryable();
271
                    return new ObjectInfo
272
                               {
273
                                   Name = objectName,
274
                                   Bytes = long.Parse(GetHeaderValue("Content-Length", response, keys)),
275
                                   Hash = GetHeaderValue("ETag", response, keys),
276
                                   Content_Type = GetHeaderValue("Content-Type", response, keys)
277
                               };
278
                case HttpStatusCode.NotFound:
279
                    return ObjectInfo.Empty;
280
                default:
281
                    if (request.RetryState.RepeatCount > 0)
282
                    {
283
                        Trace.TraceWarning("[RETRY FAIL] GetObjectInfo for {0} failed after {1} retries",
284
                                                      objectName, request.RetryState.RepeatCount);
285
                        return ObjectInfo.Empty;
286
                    }
287
                    if (response.InnerException != null)
288
                        throw new WebException(String.Format("[FAIL] GetObjectInfo for {0} failed with unexpected status code {1}", objectName, response.StatusCode), response.InnerException);
289
                    throw new WebException(String.Format("[FAIL] GetObjectInfo for {0} failed with unexpected status code {1}", objectName, response.StatusCode));
290
            }
291
        }
292

    
293
        public void CreateFolder(string container, string folder)
294
        {
295
            if (String.IsNullOrWhiteSpace(container))
296
                throw new ArgumentNullException("container", "The container property can't be empty");
297
            if (String.IsNullOrWhiteSpace(folder))
298
                throw new ArgumentNullException("folder", "The folder property can't be empty");
299

    
300
            var folderUrl=String.Format("{0}/{1}",container,folder);
301
            var request = new RestRequest { Path = folderUrl, Method = WebMethod.Put, RetryPolicy = _retryPolicy,Timeout = _shortTimeout };
302
            request.AddHeader("Content-Type", @"application/directory");
303
            request.AddHeader("Content-Length", "0");
304

    
305
            var response = _client.Request(request);
306

    
307
            if (response.StatusCode != HttpStatusCode.Created && response.StatusCode != HttpStatusCode.Accepted)
308
                throw new WebException(String.Format("CreateFolder failed with unexpected status code {0}", response.StatusCode));
309

    
310
        }
311

    
312
        public ContainerInfo GetContainerInfo(string container)
313
        {
314
            if (String.IsNullOrWhiteSpace(container))
315
                throw new ArgumentNullException("container", "The container property can't be empty");
316

    
317
            var request = new RestRequest { Path = container, Method = WebMethod.Head, RetryPolicy = _retryPolicy,Timeout = _shortTimeout };
318
            var response = _client.Request(request);
319

    
320
            switch(response.StatusCode)
321
            {
322
                case HttpStatusCode.NoContent:
323
                    var keys = response.Headers.AllKeys.AsQueryable();
324
                    var containerInfo = new ContainerInfo
325
                                            {
326
                                                Name = container,
327
                                                Count =long.Parse(GetHeaderValue("X-Container-Object-Count", response, keys)),
328
                                                Bytes =long.Parse(GetHeaderValue("X-Container-Bytes-Used", response, keys))
329
                                            };
330
                    return containerInfo;
331
                case HttpStatusCode.NotFound:
332
                    return ContainerInfo.Empty;                    
333
                default:
334
                    throw new WebException(String.Format("ContainerExists failed with unexpected status code {0}",response.StatusCode));
335
            }
336
        }
337

    
338
        public void CreateContainer(string container)
339
        {
340
            if (String.IsNullOrWhiteSpace(container))
341
                throw new ArgumentNullException("container", "The container property can't be empty");
342

    
343
            var request = new RestRequest { Path = container, Method = WebMethod.Put, RetryPolicy = _retryPolicy,Timeout = _shortTimeout };
344
            
345
            var response = _client.Request(request);
346
                        
347
            if (response.StatusCode!=HttpStatusCode.Created && response.StatusCode!=HttpStatusCode.Accepted )
348
                    throw new WebException(String.Format("ContainerExists failed with unexpected status code {0}", response.StatusCode));
349
        }
350

    
351
        public void DeleteContainer(string container)
352
        {
353
            if (String.IsNullOrWhiteSpace(container))
354
                throw new ArgumentNullException("container", "The container property can't be empty");
355

    
356
            var request = new RestRequest { Path = container, Method = WebMethod.Delete, RetryPolicy = _retryPolicy,Timeout = _shortTimeout };
357
            var response = _client.Request(request);
358

    
359
            if (response.StatusCode == HttpStatusCode.NotFound || response.StatusCode == HttpStatusCode.NoContent)
360
                return;
361
            else
362
                throw new WebException(String.Format("DeleteContainer failed with unexpected status code {0}", response.StatusCode));
363

    
364
        }
365

    
366
        /// <summary>
367
        /// 
368
        /// </summary>
369
        /// <param name="container"></param>
370
        /// <param name="objectName"></param>
371
        /// <returns></returns>
372
        /// <remarks>>This method should have no timeout or a very long one</remarks>
373
        public Stream GetObject(string container, string objectName)
374
        {
375
            if (String.IsNullOrWhiteSpace(container))
376
                throw new ArgumentNullException("container", "The container property can't be empty");
377
            if (String.IsNullOrWhiteSpace(objectName))
378
                throw new ArgumentNullException("objectName", "The objectName property can't be empty");
379

    
380
            var request = new RestRequest { Path = container + "/" + objectName, Method = WebMethod.Get };
381
/*
382
            if (DownloadPercentLimit > 0)
383
                request.TaskOptions = new TaskOptions<int> { RateLimitPercent = DownloadPercentLimit };
384
*/
385
            
386
            var response = _client.Request(request);
387
            
388
            if (response.StatusCode == HttpStatusCode.NotFound)
389
                throw new FileNotFoundException();
390
            if (response.StatusCode == HttpStatusCode.OK)
391
            {
392
                return response.ContentStream;
393
            }
394
            else
395
                throw new WebException(String.Format("GetObject failed with unexpected status code {0}", response.StatusCode));
396
        }
397

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

    
417

    
418
            string url = container + "/" + objectName;
419

    
420
            var request = new RestRequest {Path=url,Method=WebMethod.Put};           
421
            
422
/*
423
            if(UploadPercentLimit>0)
424
                request.TaskOptions=new TaskOptions<int>{RateLimitPercent=UploadPercentLimit};
425
*/
426
            Trace.TraceInformation("[PUT] START {0}",objectName);
427
            string etag = hash??CalculateHash(fileName);
428
            request.AddFile(fileName, fileName, fileName);            
429
            //request.AddPostContent(File.ReadAllBytes(fileName));
430
            request.AddHeader("Content-Type","application/octet-stream");
431
            request.AddHeader("ETag", etag);
432
            //_client.TaskOptions = new TaskOptions<int> {RateLimitPercent = 0.5};
433
            try
434
            {
435

    
436
                var response=_client.Request(request);
437
                Trace.TraceInformation("[PUT] END {0}", objectName);
438

    
439
                if (response.StatusCode == HttpStatusCode.Created)
440
                    return Task.Factory.StartNew(()=>{});
441
                if (response.StatusCode == HttpStatusCode.LengthRequired)
442
                    throw new InvalidOperationException();
443
                else
444
                    throw new WebException(String.Format("GetObject failed with unexpected status code {0}",
445
                            response.StatusCode));
446
                /*            return Task.Factory.FromAsync(_client.BeginRequest(request),ar=>_client.EndRequest(ar))
447
                                .ContinueWith(t=>
448
                                {*/
449
            }
450
            catch (Exception exc)
451
            {
452
                Trace.TraceError("[PUT] END {0} with {1}", objectName, exc);
453
                throw;
454
            }                
455

    
456
/*
457
            var response = t.Result;
458
                    if (t.IsFaulted)
459
                        Trace.TraceError("[PUT] END {0} with {1}", objectName, t.Exception);
460
                    else
461
                    {
462
                        Trace.TraceInformation("[PUT] END {0}",objectName);
463
                    }
464
*/
465
                  /*  if (response.StatusCode == HttpStatusCode.Created)
466
                        return;
467
                    if (response.StatusCode == HttpStatusCode.LengthRequired)
468
                        throw new InvalidOperationException();
469
                    else
470
                        throw new WebException(String.Format("GetObject failed with unexpected status code {0}",
471
                                response.StatusCode));*/
472
                /*});*/
473
        }
474

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

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

    
496
            var request = new RestRequest { Path = container + "/" + objectName, Method = WebMethod.Delete, RetryPolicy = _retryPolicy,Timeout = _shortTimeout };
497
            var response = _client.Request(request);
498

    
499
            if (response.StatusCode == HttpStatusCode.NotFound || response.StatusCode == HttpStatusCode.NoContent)
500
                return;
501
            else
502
                throw new WebException(String.Format("DeleteObject failed with unexpected status code {0}", response.StatusCode));
503
   
504
        }
505

    
506
        public void MoveObject(string container, string oldObjectName, string newObjectName)
507
        {
508
            if (String.IsNullOrWhiteSpace(container))
509
                throw new ArgumentNullException("container", "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(newObjectName))
513
                throw new ArgumentNullException("newObjectName", "The newObjectName property can't be empty");
514

    
515
            var request = new RestRequest { Path = container + "/" + newObjectName, Method = WebMethod.Put };
516
            request.AddHeader("X-Copy-From",String.Format("/{0}/{1}",container,oldObjectName));
517
            request.AddPostContent(new byte[]{});
518
            var response = _client.Request(request);
519

    
520
            if (response.StatusCode == HttpStatusCode.OK || response.StatusCode == HttpStatusCode.NoContent || response.StatusCode==HttpStatusCode.Created)
521
            {
522
                this.DeleteObject(container,oldObjectName);
523
            }                
524
            else
525
                throw new WebException(String.Format("MoveObject failed with unexpected status code {0}", response.StatusCode));
526
        }
527

    
528
        private string GetHeaderValue(string headerName, RestResponse response, IQueryable<string> keys)
529
        {
530
            if (keys.Any(key => key == headerName))
531
                return response.Headers[headerName];
532
            else
533
                throw new WebException(String.Format("The {0}  header is missing",headerName));
534
        }
535

    
536
        private static void ThrowIfNotStatusOK(RestResponse response, string message)
537
        {
538
            int status = (int)response.StatusCode;
539
            if (status < 200 || status >= 300)
540
                throw new WebException(String.Format("{0} with code {1}",message, status));
541
        }
542
    }
543
}