Revision 5ce54458 trunk/Pithos.Network/CloudFilesClient.cs
b/trunk/Pithos.Network/CloudFilesClient.cs | ||
---|---|---|
1 |
using System; |
|
1 |
// **CloudFilesClient** provides a simple client interface to CloudFiles and Pithos |
|
2 |
// |
|
3 |
// The class provides methods to upload/download files, delete files, manage containers |
|
4 |
|
|
5 |
|
|
6 |
using System; |
|
2 | 7 |
using System.Collections.Generic; |
3 | 8 |
using System.ComponentModel.Composition; |
4 | 9 |
using System.Diagnostics; |
... | ... | |
20 | 25 |
[Export(typeof(ICloudClient))] |
21 | 26 |
public class CloudFilesClient:ICloudClient |
22 | 27 |
{ |
23 |
|
|
28 |
//CloudFilesClient uses *_baseClient* internally to communicate with the server |
|
29 |
//RestClient provides a REST-friendly interface over the standard WebClient. |
|
24 | 30 |
private RestClient _baseClient; |
31 |
|
|
32 |
//Some operations can specify a Timeout. The default value of all timeouts is 10 seconds |
|
25 | 33 |
private readonly TimeSpan _shortTimeout = TimeSpan.FromSeconds(10); |
34 |
|
|
35 |
//Some operations can be retried before failing. The default number of retries is 5 |
|
26 | 36 |
private readonly int _retries = 5; |
27 |
public string ApiKey { get; set; } |
|
37 |
|
|
38 |
//During authentication the client provides a UserName |
|
28 | 39 |
public string UserName { get; set; } |
29 |
public Uri StorageUrl { get; set; } |
|
40 |
|
|
41 |
//and and ApiKey to the server |
|
42 |
public string ApiKey { get; set; } |
|
43 |
|
|
44 |
//And receives an authentication Token. This token must be provided in ALL other operations, |
|
45 |
//in the X-Auth-Token header |
|
30 | 46 |
public string Token { get; set; } |
47 |
|
|
48 |
//The client also receives a StorageUrl after authentication. All subsequent operations must |
|
49 |
//use this url |
|
50 |
public Uri StorageUrl { get; set; } |
|
51 |
|
|
31 | 52 |
public Uri Proxy { get; set; } |
32 | 53 |
|
33 | 54 |
public double DownloadPercentLimit { get; set; } |
... | ... | |
45 | 66 |
|
46 | 67 |
private bool _authenticated = false; |
47 | 68 |
|
69 |
// |
|
48 | 70 |
public void Authenticate(string userName,string apiKey) |
49 | 71 |
{ |
50 | 72 |
Trace.TraceInformation("[AUTHENTICATE] Start for {0}", userName); |
... | ... | |
100 | 122 |
{ |
101 | 123 |
using (var client = new RestClient(_baseClient)) |
102 | 124 |
{ |
103 |
var content = client.DownloadStringWithRetry("", 3); |
|
104 | 125 |
client.Parameters.Clear(); |
105 | 126 |
client.Parameters.Add("format", "json"); |
127 |
var content = client.DownloadStringWithRetry("", 3); |
|
106 | 128 |
client.AssertStatusOK("List Containers failed"); |
107 | 129 |
|
108 | 130 |
if (client.StatusCode == HttpStatusCode.NoContent) |
... | ... | |
113 | 135 |
|
114 | 136 |
} |
115 | 137 |
|
138 |
//Request listing of all objects in a container modified since a specific time. |
|
139 |
//If the *since* value is missing, return all objects |
|
116 | 140 |
public IList<ObjectInfo> ListObjects(string container, DateTime? since = null) |
117 | 141 |
{ |
118 | 142 |
if (String.IsNullOrWhiteSpace(container)) |
... | ... | |
130 | 154 |
|
131 | 155 |
client.AssertStatusOK("ListObjects failed"); |
132 | 156 |
|
133 |
var infos = JsonConvert.DeserializeObject<IList<ObjectInfo>>(content); |
|
157 |
//If the result is empty, return an empty list, |
|
158 |
var infos=String.IsNullOrWhiteSpace(content) |
|
159 |
? new List<ObjectInfo>() |
|
160 |
//Otherwise deserialize the object list into a list of ObjectInfos |
|
161 |
: JsonConvert.DeserializeObject<IList<ObjectInfo>>(content); |
|
134 | 162 |
|
135 | 163 |
Trace.TraceInformation("[END] ListObjects"); |
136 | 164 |
return infos; |
... | ... | |
310 | 338 |
client.Head(container); |
311 | 339 |
switch (client.StatusCode) |
312 | 340 |
{ |
341 |
case HttpStatusCode.OK: |
|
313 | 342 |
case HttpStatusCode.NoContent: |
314 | 343 |
var containerInfo = new ContainerInfo |
315 | 344 |
{ |
316 | 345 |
Name = container, |
317 | 346 |
Count = |
318 | 347 |
long.Parse(client.GetHeaderValue("X-Container-Object-Count")), |
319 |
Bytes = long.Parse(client.GetHeaderValue("X-Container-Bytes-Used")) |
|
348 |
Bytes = long.Parse(client.GetHeaderValue("X-Container-Bytes-Used")), |
|
349 |
BlockHash = client.GetHeaderValue("X-Container-Block-Hash"), |
|
350 |
BlockSize=int.Parse(client.GetHeaderValue("X-Container-Block-Size")) |
|
320 | 351 |
}; |
321 | 352 |
return containerInfo; |
322 | 353 |
case HttpStatusCode.NotFound: |
... | ... | |
361 | 392 |
/// <param name="objectName"></param> |
362 | 393 |
/// <param name="fileName"></param> |
363 | 394 |
/// <returns></returns> |
364 |
/// <remarks>>This method should have no timeout or a very long one</remarks> |
|
395 |
/// <remarks>This method should have no timeout or a very long one</remarks> |
|
396 |
//Asynchronously download the object specified by *objectName* in a specific *container* to |
|
397 |
// a local file |
|
365 | 398 |
public Task GetObject(string container, string objectName, string fileName) |
366 | 399 |
{ |
367 | 400 |
if (String.IsNullOrWhiteSpace(container)) |
368 | 401 |
throw new ArgumentNullException("container", "The container property can't be empty"); |
369 | 402 |
if (String.IsNullOrWhiteSpace(objectName)) |
370 |
throw new ArgumentNullException("objectName", "The objectName property can't be empty"); |
|
371 |
|
|
403 |
throw new ArgumentNullException("objectName", "The objectName property can't be empty"); |
|
404 |
Contract.EndContractBlock(); |
|
405 |
|
|
372 | 406 |
try |
373 | 407 |
{ |
374 |
|
|
375 |
var url = String.Join("/", _baseClient.BaseAddress, container, objectName); |
|
376 |
var uri = new Uri(url); |
|
377 |
|
|
408 |
//The container and objectName are relative names. They are joined with the client's |
|
409 |
//BaseAddress to create the object's absolute address |
|
410 |
var builder = GetAddressBuilder(container, objectName); |
|
411 |
var uri = builder.Uri; |
|
412 |
//WebClient, and by extension RestClient, are not thread-safe. Create a new RestClient |
|
413 |
//object to avoid concurrency errors. |
|
414 |
// |
|
415 |
//Download operations take a long time therefore they have no timeout. |
|
378 | 416 |
var client = new RestClient(_baseClient) { Timeout = 0 }; |
379 | 417 |
|
380 |
|
|
418 |
//Download progress is reported to the Trace log |
|
381 | 419 |
Trace.TraceInformation("[GET] START {0}", objectName); |
382 | 420 |
client.DownloadProgressChanged += (sender, args) => |
383 | 421 |
Trace.TraceInformation("[GET PROGRESS] {0} {1}% {2} of {3}", |
384 | 422 |
fileName, args.ProgressPercentage, |
385 | 423 |
args.BytesReceived, |
386 |
args.TotalBytesToReceive); |
|
424 |
args.TotalBytesToReceive); |
|
425 |
|
|
426 |
|
|
427 |
//Start downloading the object asynchronously |
|
428 |
var downloadTask = client.DownloadFileTask(uri, fileName); |
|
387 | 429 |
|
388 |
return client.DownloadFileTask(uri, fileName)
|
|
389 |
.ContinueWith(download =>
|
|
430 |
//Once the download completes
|
|
431 |
return downloadTask.ContinueWith(download =>
|
|
390 | 432 |
{ |
433 |
//Delete the local client object |
|
391 | 434 |
client.Dispose(); |
392 |
|
|
435 |
//And report failure or completion |
|
393 | 436 |
if (download.IsFaulted) |
394 | 437 |
{ |
395 | 438 |
Trace.TraceError("[GET] FAIL for {0} with \r{1}", objectName, |
... | ... | |
411 | 454 |
|
412 | 455 |
} |
413 | 456 |
|
457 |
public Task<string> PutHashMap(string container, string objectName, TreeHash hash) |
|
458 |
{ |
|
459 |
if (String.IsNullOrWhiteSpace(container)) |
|
460 |
throw new ArgumentNullException("container"); |
|
461 |
if (String.IsNullOrWhiteSpace(objectName)) |
|
462 |
throw new ArgumentNullException("objectName"); |
|
463 |
if (hash==null) |
|
464 |
throw new ArgumentNullException("hash"); |
|
465 |
if (String.IsNullOrWhiteSpace(Token)) |
|
466 |
throw new InvalidOperationException("Invalid Token"); |
|
467 |
if (StorageUrl == null) |
|
468 |
throw new InvalidOperationException("Invalid Storage Url"); |
|
469 |
Contract.EndContractBlock(); |
|
470 |
//The container and objectName are relative names. They are joined with the client's |
|
471 |
//BaseAddress to create the object's absolute address |
|
472 |
var builder = GetAddressBuilder(container, objectName); |
|
473 |
builder.Query = "format=json&hashmap"; |
|
474 |
var uri = builder.Uri; |
|
475 |
|
|
476 |
//Don't use a timeout because putting the hashmap may be a long process |
|
477 |
var client = new RestClient(_baseClient) { Timeout = 0 }; |
|
478 |
|
|
479 |
//Send the tree hash as Json to the server |
|
480 |
client.Headers[HttpRequestHeader.ContentType] = "application/octet-stream"; |
|
481 |
var uploadTask=client.UploadStringTask(uri, "PUT", hash.ToJson()); |
|
482 |
|
|
483 |
|
|
484 |
return uploadTask.ContinueWith(t => |
|
485 |
{ |
|
486 |
|
|
487 |
|
|
488 |
|
|
489 |
//The server will respond either with 201-created if all blocks were already on the server |
|
490 |
if (client.StatusCode == HttpStatusCode.Created) |
|
491 |
//in which case we return an empty hash list |
|
492 |
return t.Result; |
|
493 |
//or with a 409-conflict and return the list of missing parts |
|
494 |
//A 409 will cause an exception so we need to check t.IsFaulted to avoid propagating the exception |
|
495 |
if (t.IsFaulted) |
|
496 |
{ |
|
497 |
var ex = t.Exception.InnerException; |
|
498 |
var we = ex as WebException; |
|
499 |
var response = we.Response as HttpWebResponse; |
|
500 |
if (response!=null && response.StatusCode==HttpStatusCode.Conflict) |
|
501 |
{ |
|
502 |
//In case of 409 the missing parts will be in the response content |
|
503 |
using (var stream = response.GetResponseStream()) |
|
504 |
using(var reader=new StreamReader(stream)) |
|
505 |
{ |
|
506 |
var content=reader.ReadToEnd(); |
|
507 |
return content; |
|
508 |
} |
|
509 |
} |
|
510 |
else |
|
511 |
//Any other status code is unexpected and the exception should be rethrown |
|
512 |
throw ex; |
|
513 |
|
|
514 |
} |
|
515 |
//Any other status code is unexpected but there was no exception. We can probably continue processing |
|
516 |
else |
|
517 |
{ |
|
518 |
Trace.TraceWarning("Unexcpected status code when putting map: {0} - {1}",client.StatusCode,client.StatusDescription); |
|
519 |
} |
|
520 |
return t.Result; |
|
521 |
}); |
|
522 |
|
|
523 |
} |
|
524 |
|
|
525 |
|
|
526 |
public Task<TreeHash> GetHashMap(string container, string objectName) |
|
527 |
{ |
|
528 |
if (String.IsNullOrWhiteSpace(container)) |
|
529 |
throw new ArgumentNullException("container"); |
|
530 |
if (String.IsNullOrWhiteSpace(objectName)) |
|
531 |
throw new ArgumentNullException("objectName"); |
|
532 |
if (String.IsNullOrWhiteSpace(Token)) |
|
533 |
throw new InvalidOperationException("Invalid Token"); |
|
534 |
if (StorageUrl == null) |
|
535 |
throw new InvalidOperationException("Invalid Storage Url"); |
|
536 |
Contract.EndContractBlock(); |
|
537 |
|
|
538 |
try |
|
539 |
{ |
|
540 |
//The container and objectName are relative names. They are joined with the client's |
|
541 |
//BaseAddress to create the object's absolute address |
|
542 |
var builder = GetAddressBuilder(container, objectName); |
|
543 |
builder.Query="format=json&hashmap"; |
|
544 |
var uri = builder.Uri; |
|
545 |
//WebClient, and by extension RestClient, are not thread-safe. Create a new RestClient |
|
546 |
//object to avoid concurrency errors. |
|
547 |
// |
|
548 |
//Download operations take a long time therefore they have no timeout. |
|
549 |
//TODO: Do we really? this is a hashmap operation, not a download |
|
550 |
var client = new RestClient(_baseClient) { Timeout = 0 }; |
|
551 |
|
|
552 |
|
|
553 |
//Start downloading the object asynchronously |
|
554 |
var downloadTask = client.DownloadStringTask(uri); |
|
555 |
|
|
556 |
//Once the download completes |
|
557 |
return downloadTask.ContinueWith(download => |
|
558 |
{ |
|
559 |
//Delete the local client object |
|
560 |
client.Dispose(); |
|
561 |
//And report failure or completion |
|
562 |
if (download.IsFaulted) |
|
563 |
{ |
|
564 |
Trace.TraceError("[GET HASH] FAIL for {0} with \r{1}", objectName, |
|
565 |
download.Exception); |
|
566 |
throw download.Exception; |
|
567 |
} |
|
568 |
|
|
569 |
//The server will return an empty string if the file is empty |
|
570 |
var json = download.Result; |
|
571 |
var treeHash = TreeHash.Parse(json); |
|
572 |
Trace.TraceInformation("[GET HASH] END {0}", objectName); |
|
573 |
return treeHash; |
|
574 |
}); |
|
575 |
} |
|
576 |
catch (Exception exc) |
|
577 |
{ |
|
578 |
Trace.TraceError("[GET HASH] END {0} with {1}", objectName, exc); |
|
579 |
throw; |
|
580 |
} |
|
581 |
|
|
582 |
|
|
583 |
|
|
584 |
} |
|
585 |
|
|
586 |
private UriBuilder GetAddressBuilder(string container, string objectName) |
|
587 |
{ |
|
588 |
var builder = new UriBuilder(String.Join("/", _baseClient.BaseAddress, container, objectName)); |
|
589 |
return builder; |
|
590 |
} |
|
591 |
|
|
592 |
|
|
414 | 593 |
/// <summary> |
415 | 594 |
/// |
416 | 595 |
/// </summary> |
... | ... | |
433 | 612 |
|
434 | 613 |
try |
435 | 614 |
{ |
436 |
var url = String.Join("/",_baseClient.BaseAddress,container,objectName);
|
|
437 |
var uri = new Uri(url);
|
|
615 |
var builder= GetAddressBuilder(container,objectName);
|
|
616 |
var uri = builder.Uri;
|
|
438 | 617 |
|
439 | 618 |
var client = new RestClient(_baseClient){Timeout=0}; |
440 | 619 |
string etag = hash ?? CalculateHash(fileName); |
Also available in: Unified diff