Statistics
| Branch: | Revision:

root / trunk / Pithos.Network / CloudFilesClient.cs @ 3c43ec9b

History | View | Annotate | Download (26.5 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 = "https://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
        private bool _authenticated = false;
55

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

    
64
            if (_authenticated)
65
                return;
66

    
67
            UserName = userName;
68
            ApiKey = apiKey;
69
            
70
            var proxy = Proxy != null ? Proxy.ToString() : null;
71
            if (UsePithos)
72
            {
73
                Token = ApiKey;
74
                string storageUrl = String.Format("{0}/{1}/{2}", AuthUrl, VersionPath, UserName);
75
                StorageUrl = new Uri(storageUrl);
76
            }
77
            else
78
            {
79

    
80
                string authUrl = String.Format("{0}/{1}", AuthUrl, VersionPath);
81
                var authClient = new RestClient {Path = authUrl, Proxy = proxy};                
82

    
83
                authClient.AddHeader("X-Auth-User", UserName);
84
                authClient.AddHeader("X-Auth-Key", ApiKey);
85

    
86
                var response = authClient.Request();
87

    
88
                ThrowIfNotStatusOK(response, "Authentication failed");
89

    
90
                var keys = response.Headers.AllKeys.AsQueryable();
91

    
92
                string storageUrl = GetHeaderValue("X-Storage-Url", response, keys);
93
                if (String.IsNullOrWhiteSpace(storageUrl))
94
                    throw new InvalidOperationException("Failed to obtain storage url");
95
                StorageUrl = new Uri(storageUrl);
96

    
97
                var token = GetHeaderValue("X-Auth-Token", response, keys);
98
                if (String.IsNullOrWhiteSpace(token))
99
                    throw new InvalidOperationException("Failed to obtain token url");
100
                Token = token;
101
            }
102

    
103
            _retryPolicy = new RetryPolicy { RetryCount = _retries };
104
            _retryPolicy.RetryConditions.Add(new TimeoutRetryCondition());
105

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

    
116
            Trace.TraceInformation("[AUTHENTICATE] End for {0}", userName);
117
        }
118

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

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

    
139
            var response = client.Request(request);
140

    
141
            if (response.StatusCode == HttpStatusCode.NoContent)
142
                return new List<ContainerInfo>();
143

    
144
            ThrowIfNotStatusOK(response, "List Containers failed");
145

    
146

    
147
            var infos=JsonConvert.DeserializeObject<IList<ContainerInfo>>(response.Content);
148
            
149
            return infos;
150
        }
151

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

    
157
            Trace.TraceInformation("[START] ListObjects");
158

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

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

    
169

    
170

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

    
176
            Trace.TraceInformation("[START] ListObjects");
177

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

    
185
            Trace.TraceInformation("[END] ListObjects");
186
            return infos;
187
        }
188

    
189
        private static IList<ObjectInfo> InfosFromContent(RestResponse response)
190
        {
191
            if (response.TimedOut)
192
                return new List<ObjectInfo>();
193

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

    
197
            if (response.StatusCode == HttpStatusCode.NoContent)
198
                return new List<ObjectInfo>();
199

    
200

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

    
208
            var infos = JsonConvert.DeserializeObject<IList<ObjectInfo>>(response.Content);
209
            return infos;
210
        }
211

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

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

    
220
            switch(response.StatusCode)
221
            {
222
                case HttpStatusCode.OK:
223
                case HttpStatusCode.NoContent:
224
                    return true;
225
                case HttpStatusCode.NotFound:
226
                    return false;          
227
                default:
228
                    throw CreateWebException("ContainerExists",response.StatusCode);
229
            }
230
        }
231

    
232
        public bool ObjectExists(string container,string objectName)
233
        {
234
            if (String.IsNullOrWhiteSpace(container))
235
                throw new ArgumentNullException("container", "The container property can't be empty");
236
            if (String.IsNullOrWhiteSpace(objectName))
237
                throw new ArgumentNullException("objectName", "The objectName property can't be empty");
238

    
239

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

    
243
            switch (response.StatusCode)
244
            {
245
                case HttpStatusCode.OK:
246
                case HttpStatusCode.NoContent:
247
                    return true;
248
                case HttpStatusCode.NotFound:
249
                    return false;
250
                default:
251
                    throw CreateWebException("ObjectExists",response.StatusCode);
252
            }
253
            
254
        }
255

    
256
        public ObjectInfo GetObjectInfo(string container, string objectName)
257
        {
258
            if (String.IsNullOrWhiteSpace(container))
259
                throw new ArgumentNullException("container", "The container property can't be empty");
260
            if (String.IsNullOrWhiteSpace(objectName))
261
                throw new ArgumentNullException("objectName", "The objectName property can't be empty");
262

    
263

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

    
267
            if (response.TimedOut)
268
                return ObjectInfo.Empty;
269

    
270
            switch (response.StatusCode)
271
            {
272
                case HttpStatusCode.OK:
273
                case HttpStatusCode.NoContent:
274
                    var keys = response.Headers.AllKeys.AsQueryable();
275
                    var tags=(from key in keys
276
                             where key.StartsWith("X-Object-Meta-")
277
                             let name=key.Substring(14)
278
                             select new {Name=name,Value=response.Headers[name]})
279
                             .ToDictionary(t=>t.Name,t=>t.Value);
280
                    var extensions = (from key in keys
281
                                      where key.StartsWith("X-Object-") && !key.StartsWith("X-Object-Meta-")
282
                                      let name = key.Substring(9)
283
                                      select new { Name = name, Value = response.Headers[name] })
284
                             .ToDictionary(t => t.Name, t => t.Value);
285
                    return new ObjectInfo
286
                               {
287
                                   Name = objectName,
288
                                   Bytes = long.Parse(GetHeaderValue("Content-Length", response, keys)),
289
                                   Hash = GetHeaderValue("ETag", response, keys),
290
                                   Content_Type = GetHeaderValue("Content-Type", response, keys),
291
                                   Tags=tags,
292
                                   Extensions=extensions
293
                               };
294
                case HttpStatusCode.NotFound:
295
                    return ObjectInfo.Empty;
296
                default:
297
                    if (request.RetryState.RepeatCount > 0)
298
                    {
299
                        Trace.TraceWarning("[RETRY FAIL] GetObjectInfo for {0} failed after {1} retries",
300
                                                      objectName, request.RetryState.RepeatCount);
301
                        return ObjectInfo.Empty;
302
                    }
303
                    if (response.InnerException != null)
304
                        throw new WebException(String.Format("[FAIL] GetObjectInfo for {0} failed with unexpected status code {1}", objectName, response.StatusCode), response.InnerException);
305
                    throw new WebException(String.Format("[FAIL] GetObjectInfo for {0} failed with unexpected status code {1}", objectName, response.StatusCode));
306
            }
307
        }
308

    
309
        public void CreateFolder(string container, string folder)
310
        {
311
            if (String.IsNullOrWhiteSpace(container))
312
                throw new ArgumentNullException("container", "The container property can't be empty");
313
            if (String.IsNullOrWhiteSpace(folder))
314
                throw new ArgumentNullException("folder", "The folder property can't be empty");
315

    
316
            var folderUrl=String.Format("{0}/{1}",container,folder);
317
            var request = new RestRequest { Path = folderUrl, Method = WebMethod.Put, RetryPolicy = _retryPolicy,Timeout = _shortTimeout };
318
            request.AddHeader("Content-Type", @"application/directory");
319
            request.AddHeader("Content-Length", "0");
320

    
321
            var response = _client.Request(request);
322

    
323
            if (response.StatusCode != HttpStatusCode.Created && response.StatusCode != HttpStatusCode.Accepted)
324
                throw CreateWebException("CreateFolder", response.StatusCode);
325

    
326
        }
327

    
328
        public ContainerInfo GetContainerInfo(string container)
329
        {
330
            if (String.IsNullOrWhiteSpace(container))
331
                throw new ArgumentNullException("container", "The container property can't be empty");
332

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

    
336
            switch(response.StatusCode)
337
            {
338
                case HttpStatusCode.NoContent:
339
                    var keys = response.Headers.AllKeys.AsQueryable();
340
                    var containerInfo = new ContainerInfo
341
                                            {
342
                                                Name = container,
343
                                                Count =long.Parse(GetHeaderValue("X-Container-Object-Count", response, keys)),
344
                                                Bytes =long.Parse(GetHeaderValue("X-Container-Bytes-Used", response, keys))
345
                                            };
346
                    return containerInfo;
347
                case HttpStatusCode.NotFound:
348
                    return ContainerInfo.Empty;                    
349
                default:
350
                    throw CreateWebException("GetContainerInfo", response.StatusCode);
351
            }
352
        }
353

    
354
        public void CreateContainer(string container)
355
        {
356
            if (String.IsNullOrWhiteSpace(container))
357
                throw new ArgumentNullException("container", "The container property can't be empty");
358

    
359
            var request = new RestRequest { Path = container, Method = WebMethod.Put, RetryPolicy = _retryPolicy,Timeout = _shortTimeout };
360
            
361
            var response = _client.Request(request);
362

    
363
            var expectedCodes = new[]{HttpStatusCode.Created ,HttpStatusCode.Accepted , HttpStatusCode.OK};
364
            if (!expectedCodes.Contains(response.StatusCode))
365
                throw CreateWebException("CreateContainer", response.StatusCode);
366
        }
367

    
368
        public void DeleteContainer(string container)
369
        {
370
            if (String.IsNullOrWhiteSpace(container))
371
                throw new ArgumentNullException("container", "The container property can't be empty");
372

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

    
376
            var expectedCodes = new[] { HttpStatusCode.NotFound, HttpStatusCode.NoContent};
377
            if (!expectedCodes.Contains(response.StatusCode))
378
                throw CreateWebException("DeleteContainer", response.StatusCode);
379

    
380
        }
381

    
382
        /// <summary>
383
        /// 
384
        /// </summary>
385
        /// <param name="container"></param>
386
        /// <param name="objectName"></param>
387
        /// <param name="fileName"></param>
388
        /// <returns></returns>
389
        /// <remarks>>This method should have no timeout or a very long one</remarks>
390
        public Task GetObject(string container, string objectName, string fileName)
391
        {
392
            if (String.IsNullOrWhiteSpace(container))
393
                throw new ArgumentNullException("container", "The container property can't be empty");
394
            if (String.IsNullOrWhiteSpace(objectName))
395
                throw new ArgumentNullException("objectName", "The objectName property can't be empty");
396

    
397
            var request = new RestRequest {Path = container + "/" + objectName, Method = WebMethod.Get};
398
            /*
399
                        if (DownloadPercentLimit > 0)
400
                            request.TaskOptions = new TaskOptions<int> { RateLimitPercent = DownloadPercentLimit };
401
            */
402
            try
403
            {
404
                var url = String.Join("/", new[] {_client.Authority, container, objectName});
405
                var uri = new Uri(url);
406

    
407
                var client = new WebClient();
408
                if (!String.IsNullOrWhiteSpace(_client.Proxy))
409
                    client.Proxy = new WebProxy(_client.Proxy);
410

    
411
                CopyHeaders(_client, client);
412

    
413
                Trace.TraceInformation("[GET] START {0}", objectName);
414
                client.DownloadProgressChanged += (sender, args) => 
415
                    Trace.TraceInformation("[GET PROGRESS] {0} {1}% {2} of {3}",
416
                                    fileName, args.ProgressPercentage,
417
                                    args.BytesReceived,
418
                                    args.TotalBytesToReceive);
419
                
420
                return client.DownloadFileTask(uri, fileName)
421
                    .ContinueWith(download =>
422
                                      {
423
                                          client.Dispose();
424

    
425
                                          if (download.IsFaulted)
426
                                          {
427
                                              Trace.TraceError("[GET] FAIL for {0} with \r{1}", objectName,
428
                                                               download.Exception);
429
                                          }
430
                                          else
431
                                          {
432
                                              Trace.TraceInformation("[GET] END {0}", objectName);                                             
433
                                          }
434
                                      });
435
            }
436
            catch (Exception exc)
437
            {
438
                Trace.TraceError("[GET] END {0} with {1}", objectName, exc);
439
                throw;
440
            }
441

    
442

    
443

    
444
        }
445

    
446
        /// <summary>
447
        /// 
448
        /// </summary>
449
        /// <param name="container"></param>
450
        /// <param name="objectName"></param>
451
        /// <param name="fileName"></param>
452
        /// <param name="hash">Optional hash value for the file. If no hash is provided, the method calculates a new hash</param>
453
        /// <remarks>>This method should have no timeout or a very long one</remarks>
454
        public Task PutObject(string container, string objectName, string fileName, string hash = null)
455
        {
456
            if (String.IsNullOrWhiteSpace(container))
457
                throw new ArgumentNullException("container", "The container property can't be empty");
458
            if (String.IsNullOrWhiteSpace(objectName))
459
                throw new ArgumentNullException("objectName", "The objectName property can't be empty");
460
            if (String.IsNullOrWhiteSpace(fileName))
461
                throw new ArgumentNullException("fileName", "The fileName property can't be empty");
462
            if (!File.Exists(fileName))
463
                throw new FileNotFoundException("The file does not exist",fileName);
464

    
465
            
466
            try
467
            {
468
                var url = String.Join("/",new[]{_client.Authority,container,objectName});
469
                var uri = new Uri(url);
470

    
471
                var client = new WebClient();                
472
                string etag = hash ?? CalculateHash(fileName);
473

    
474
                client.Headers.Add("Content-Type", "application/octet-stream");
475
                client.Headers.Add("ETag", etag);
476

    
477
                if(!String.IsNullOrWhiteSpace(_client.Proxy))
478
                    client.Proxy = new WebProxy(_client.Proxy);
479

    
480
                CopyHeaders(_client, client);
481

    
482
                Trace.TraceInformation("[PUT] START {0}", objectName);
483
                client.UploadProgressChanged += (sender, args) =>
484
                {
485
                    Trace.TraceInformation("[PUT PROGRESS] {0} {1}% {2} of {3}", fileName, args.ProgressPercentage, args.BytesSent, args.TotalBytesToSend);
486
                };
487
               
488
                return client.UploadFileTask(uri, "PUT", fileName)
489
                    .ContinueWith(upload=>
490
                                      {
491
                                          client.Dispose();
492

    
493
                                          if (upload.IsFaulted)
494
                                          {                                              
495
                                              Trace.TraceError("[PUT] FAIL for {0} with \r{1}",objectName,upload.Exception);
496
                                          }
497
                                          else
498
                                            Trace.TraceInformation("[PUT] END {0}", objectName);
499
                                      });
500
            }
501
            catch (Exception exc)
502
            {
503
                Trace.TraceError("[PUT] END {0} with {1}", objectName, exc);
504
                throw;
505
            }                
506

    
507
        }
508
       
509

    
510
        /// <summary>
511
        /// Copies headers from a Hammock RestClient to a WebClient
512
        /// </summary>
513
        /// <param name="source">The RestClient from which the headers are copied</param>
514
        /// <param name="target">The WebClient to which the headers are copied</param>
515
        private static void CopyHeaders(RestClient source, WebClient target)
516
        {
517
            Contract.Requires(source!=null,"source can't be null");
518
            Contract.Requires(target != null, "target can't be null");
519
            if (source == null)
520
                throw new ArgumentNullException("source", "source can't be null");
521
            if (source == null)
522
                throw new ArgumentNullException("target", "target can't be null");
523

    
524
            foreach (var header in source.GetAllHeaders())
525
            {
526
                target.Headers.Add(header.Name, header.Value);
527
            }
528
        }
529

    
530
        private static string CalculateHash(string fileName)
531
        {
532
            string hash;
533
            using (var hasher = MD5.Create())
534
            using(var stream=File.OpenRead(fileName))
535
            {
536
                var hashBuilder=new StringBuilder();
537
                foreach (byte b in hasher.ComputeHash(stream))
538
                    hashBuilder.Append(b.ToString("x2").ToLower());
539
                hash = hashBuilder.ToString();                
540
            }
541
            return hash;
542
        }
543

    
544
        public void DeleteObject(string container, string objectName)
545
        {
546
            if (String.IsNullOrWhiteSpace(container))
547
                throw new ArgumentNullException("container", "The container property can't be empty");
548
            if (String.IsNullOrWhiteSpace(objectName))
549
                throw new ArgumentNullException("objectName", "The objectName property can't be empty");
550

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

    
554
            var expectedCodes = new[] { HttpStatusCode.NotFound, HttpStatusCode.NoContent };
555
            if (!expectedCodes.Contains(response.StatusCode))
556
                throw CreateWebException("DeleteObject", response.StatusCode);
557
   
558
        }
559

    
560
        public void MoveObject(string sourceContainer, string oldObjectName, string targetContainer,string newObjectName)
561
        {
562
            if (String.IsNullOrWhiteSpace(sourceContainer))
563
                throw new ArgumentNullException("sourceContainer", "The container property can't be empty");
564
            if (String.IsNullOrWhiteSpace(oldObjectName))
565
                throw new ArgumentNullException("oldObjectName", "The oldObjectName property can't be empty");
566
            if (String.IsNullOrWhiteSpace(targetContainer))
567
                throw new ArgumentNullException("targetContainer", "The container property can't be empty");
568
            if (String.IsNullOrWhiteSpace(newObjectName))
569
                throw new ArgumentNullException("newObjectName", "The newObjectName property can't be empty");
570

    
571
            var targetUrl = targetContainer + "/" + newObjectName;
572
            var sourceUrl = String.Format("/{0}/{1}", sourceContainer, oldObjectName);
573

    
574
            var request = new RestRequest { Path = targetUrl, Method = WebMethod.Put };
575
            request.AddHeader("X-Copy-From",sourceUrl);
576
            request.AddPostContent(new byte[]{});
577
            var response = _client.Request(request);
578

    
579

    
580
            var expectedCodes = new[] { HttpStatusCode.OK ,HttpStatusCode.NoContent ,HttpStatusCode.Created };
581
            if (expectedCodes.Contains(response.StatusCode))
582
            {
583
                this.DeleteObject(sourceContainer,oldObjectName);
584
            }                
585
            else
586
                throw CreateWebException("MoveObject", response.StatusCode);
587
        }
588

    
589
        private string GetHeaderValue(string headerName, RestResponse response, IQueryable<string> keys)
590
        {
591
            if (keys.Any(key => key == headerName))
592
                return response.Headers[headerName];
593
            else
594
                throw new WebException(String.Format("The {0}  header is missing",headerName));
595
        }
596

    
597
        private static void ThrowIfNotStatusOK(RestResponse response, string message)
598
        {
599
            int status = (int)response.StatusCode;
600
            if (status < 200 || status >= 300)
601
                throw new WebException(String.Format("{0} with code {1}",message, status));
602
        }
603

    
604
        private static WebException CreateWebException(string operation, HttpStatusCode statusCode)
605
        {
606
            return new WebException(String.Format("{0} failed with unexpected status code {1}", operation, statusCode));
607
        }
608

    
609
    }
610
}