2 using System.Collections.Generic;
3 using System.ComponentModel.Composition;
4 using System.Diagnostics;
5 using System.Diagnostics.Contracts;
9 using System.Security.Cryptography;
11 using System.Threading.Tasks;
12 using Newtonsoft.Json;
13 using Pithos.Interfaces;
14 using WebHeaderCollection = System.Net.WebHeaderCollection;
16 namespace Pithos.Network
18 [Export(typeof(ICloudClient))]
19 public class CloudFilesClient:ICloudClient
22 private PithosClient _client;
23 private readonly TimeSpan _shortTimeout = TimeSpan.FromSeconds(10);
24 private readonly int _retries = 5;
25 public string ApiKey { get; set; }
26 public string UserName { get; set; }
27 public Uri StorageUrl { get; set; }
28 public string Token { get; set; }
29 public Uri Proxy { get; set; }
31 public double DownloadPercentLimit { get; set; }
32 public double UploadPercentLimit { get; set; }
34 public string AuthenticationUrl { get; set; }
37 public string VersionPath
39 get { return UsePithos ? "v1" : "v1.0"; }
42 public bool UsePithos { get; set; }
44 private bool _authenticated = false;
46 public void Authenticate(string userName,string apiKey)
48 Trace.TraceInformation("[AUTHENTICATE] Start for {0}", userName);
49 if (String.IsNullOrWhiteSpace(userName))
50 throw new ArgumentNullException("userName", "The userName property can't be empty");
51 if (String.IsNullOrWhiteSpace(apiKey))
52 throw new ArgumentNullException("apiKey", "The apiKey property can't be empty");
63 string storageUrl = String.Format("{0}/{1}/{2}", AuthenticationUrl, VersionPath, UserName);
64 StorageUrl = new Uri(storageUrl);
69 string authUrl = String.Format("{0}/{1}", AuthenticationUrl, VersionPath);
70 var authClient = new PithosClient{BaseAddress= authUrl};
72 authClient.Proxy = new WebProxy(Proxy);
74 authClient.Headers.Add("X-Auth-User", UserName);
75 authClient.Headers.Add("X-Auth-Key", ApiKey);
77 var response = authClient.DownloadStringWithRetry("",3);
79 authClient.AssertStatusOK("Authentication failed");
81 //var keys = authClient.ResponseHeaders.AllKeys.AsQueryable();
83 string storageUrl = authClient.GetHeaderValue("X-Storage-Url");
84 if (String.IsNullOrWhiteSpace(storageUrl))
85 throw new InvalidOperationException("Failed to obtain storage url");
86 StorageUrl = new Uri(storageUrl);
88 var token = authClient.GetHeaderValue("X-Auth-Token");
89 if (String.IsNullOrWhiteSpace(token))
90 throw new InvalidOperationException("Failed to obtain token url");
94 /*_retryPolicy = new RetryPolicy { RetryCount = _retries };
95 _retryPolicy.RetryConditions.Add(new TimeoutRetryCondition());*/
97 _client = new PithosClient{
98 BaseAddress = StorageUrl.AbsoluteUri,
102 _client.Proxy = new WebProxy(Proxy);
103 //_client.FileProgress += OnFileProgress;
105 _client.Headers.Add("X-Auth-Token", Token);
108 _client.AddHeader("X-Auth-User", UserName);
109 _client.AddHeader("X-Auth-Key",ApiKey);
112 Trace.TraceInformation("[AUTHENTICATE] End for {0}", userName);
115 /* private void OnFileProgress(object sender, FileProgressEventArgs e)
117 Trace.TraceInformation("[PROGRESS] {0} {1:p} {2} of {3}",e.FileName,(double)e.BytesWritten/e.TotalBytes, e.BytesWritten,e.TotalBytes);
120 public IList<ContainerInfo> ListContainers()
122 //Workaround for Hammock quirk: Hammock always
123 //appends a / unless a Path is specified.
125 //Create a request with a complete path
126 //var request = new RestRequest { Path = StorageUrl.ToString(), RetryPolicy = _retryPolicy,Timeout = _shortTimeout };
127 //request.AddParameter("format","json");
128 //Create a client clone
130 /*var url = String.Join("/", new[] { _client.Authority, StorageUrl.ToString() });
131 var builder=new UriBuilder(url);
132 builder.Query = "format=json";
134 var client= new PithosClient(_client){Timeout=10}; */
135 var content=_client.DownloadStringWithRetry("",3);
136 _client.Parameters.Clear();
137 _client.Parameters.Add("format", "json");
138 _client.AssertStatusOK("List Containers failed");
140 if (_client.StatusCode==HttpStatusCode.NoContent)
141 return new List<ContainerInfo>();
142 var infos = JsonConvert.DeserializeObject<IList<ContainerInfo>>(content);
147 var client = new RestClient{Proxy=Proxy.ToString()};
148 foreach (var header in _client.GetAllHeaders())
150 client.AddHeader(header.Name,header.Value);
157 var response = client.Request(request);
159 if (response.StatusCode == HttpStatusCode.NoContent)
160 return new List<ContainerInfo>();
162 ThrowIfNotStatusOK(response, "List Containers failed");
165 var infos=JsonConvert.DeserializeObject<IList<ContainerInfo>>(response.Content);
170 public IList<ObjectInfo> ListObjects(string container)
172 if (String.IsNullOrWhiteSpace(container))
173 throw new ArgumentNullException("container", "The container property can't be empty");
175 Trace.TraceInformation("[START] ListObjects");
177 //var request = new RestRequest { Path = container, RetryPolicy = _retryPolicy, Timeout = TimeSpan.FromMinutes(1) };
178 //request.AddParameter("format", "json");
179 //var response = _client.Request(request);
183 var url = String.Join("/", new[] { _client.Authority, container });
184 var builder = new UriBuilder(url) {Query = "format=json"};
186 var client = new PithosClient(_client) { Timeout = 60000 };
188 _client.Parameters.Clear();
189 _client.Parameters.Add("format", "json");
190 var content = _client.DownloadStringWithRetry(container, 3);
192 _client.AssertStatusOK("ListObjects failed");
194 var infos = JsonConvert.DeserializeObject<IList<ObjectInfo>>(content);
196 Trace.TraceInformation("[END] ListObjects");
202 public IList<ObjectInfo> ListObjects(string container,string folder)
204 if (String.IsNullOrWhiteSpace(container))
205 throw new ArgumentNullException("container", "The container property can't be empty");
207 Trace.TraceInformation("[START] ListObjects");
209 /* var request = new RestRequest { Path = container,RetryPolicy = _retryPolicy, Timeout = TimeSpan.FromMinutes(1) };
210 request.AddParameter("format", "json");
211 request.AddParameter("path", folder);*/
213 _client.Parameters.Clear();
214 _client.Parameters.Add("format", "json");
215 _client.Parameters.Add("path", folder);
216 var content = _client.DownloadStringWithRetry(container, 3);
217 _client.AssertStatusOK("ListObjects failed");
219 var infos = JsonConvert.DeserializeObject<IList<ObjectInfo>>(content);
221 /* var response = _client.Request(request);
223 var infos = InfosFromContent(response);*/
225 Trace.TraceInformation("[END] ListObjects");
229 /* private static IList<ObjectInfo> InfosFromContent(RestResponse response)
231 if (response.TimedOut)
232 return new List<ObjectInfo>();
234 if (response.StatusCode == 0)
235 return new List<ObjectInfo>();
237 if (response.StatusCode == HttpStatusCode.NoContent)
238 return new List<ObjectInfo>();
241 var statusCode = (int)response.StatusCode;
242 if (statusCode < 200 || statusCode >= 300)
244 Trace.TraceWarning("ListObjects failed with code {0} - {1}", response.StatusCode, response.StatusDescription);
245 return new List<ObjectInfo>();
248 var infos = JsonConvert.DeserializeObject<IList<ObjectInfo>>(response.Content);
252 public bool ContainerExists(string container)
254 if (String.IsNullOrWhiteSpace(container))
255 throw new ArgumentNullException("container", "The container property can't be empty");
257 _client.Parameters.Clear();
258 _client.Head(container,3);
259 //var request = new RestRequest { Path = container, Method = WebMethod.Head, RetryPolicy = _retryPolicy,Timeout = _shortTimeout };
260 //var response = _client.Request(request);
262 switch (_client.StatusCode)
264 case HttpStatusCode.OK:
265 case HttpStatusCode.NoContent:
267 case HttpStatusCode.NotFound:
270 throw CreateWebException("ContainerExists", _client.StatusCode);
274 public bool ObjectExists(string container,string objectName)
276 if (String.IsNullOrWhiteSpace(container))
277 throw new ArgumentNullException("container", "The container property can't be empty");
278 if (String.IsNullOrWhiteSpace(objectName))
279 throw new ArgumentNullException("objectName", "The objectName property can't be empty");
283 var request = new RestRequest { Path = container + "/" + objectName, Method = WebMethod.Head,RetryPolicy = _retryPolicy, Timeout = _shortTimeout };
284 var response = _client.Request(request);
286 _client.Parameters.Clear();
287 _client.Head(container + "/" + objectName, 3);
289 switch (_client.StatusCode)
291 case HttpStatusCode.OK:
292 case HttpStatusCode.NoContent:
294 case HttpStatusCode.NotFound:
297 throw CreateWebException("ObjectExists", _client.StatusCode);
302 public ObjectInfo GetObjectInfo(string container, string objectName)
304 if (String.IsNullOrWhiteSpace(container))
305 throw new ArgumentNullException("container", "The container property can't be empty");
306 if (String.IsNullOrWhiteSpace(objectName))
307 throw new ArgumentNullException("objectName", "The objectName property can't be empty");
311 var request = new RestRequest { Path = container + "/" + objectName, Method = WebMethod.Head, RetryPolicy = _retryPolicy,Timeout = _shortTimeout };
312 var response = _client.Request(request);
316 _client.Parameters.Clear();
318 _client.Head(container + "/" + objectName, 3);
320 if (_client.TimedOut)
321 return ObjectInfo.Empty;
323 switch (_client.StatusCode)
325 case HttpStatusCode.OK:
326 case HttpStatusCode.NoContent:
327 var keys = _client.ResponseHeaders.AllKeys.AsQueryable();
328 var tags = (from key in keys
329 where key.StartsWith("X-Object-Meta-")
330 let name = key.Substring(14)
331 select new { Name = name, Value = _client.ResponseHeaders[name] })
332 .ToDictionary(t => t.Name, t => t.Value);
333 var extensions = (from key in keys
334 where key.StartsWith("X-Object-") && !key.StartsWith("X-Object-Meta-")
335 let name = key.Substring(9)
336 select new { Name = name, Value = _client.ResponseHeaders[name] })
337 .ToDictionary(t => t.Name, t => t.Value);
338 return new ObjectInfo
342 long.Parse(_client.GetHeaderValue("Content-Length")),
343 Hash = _client.GetHeaderValue("ETag"),
344 Content_Type = _client.GetHeaderValue("Content-Type"),
346 Extensions = extensions
348 case HttpStatusCode.NotFound:
349 return ObjectInfo.Empty;
351 throw new WebException(
352 String.Format("[FAIL] GetObjectInfo for {0} failed with unexpected status code {1}",
353 objectName, _client.StatusCode));
356 catch (RetryException e)
358 Trace.TraceWarning("[RETRY FAIL] GetObjectInfo for {0} failed.");
359 return ObjectInfo.Empty;
361 catch (WebException e)
363 Trace.TraceError(String.Format("[FAIL] GetObjectInfo for {0} failed with unexpected status code {1}",
364 objectName, _client.StatusCode), e);
370 public void CreateFolder(string container, string folder)
372 if (String.IsNullOrWhiteSpace(container))
373 throw new ArgumentNullException("container", "The container property can't be empty");
374 if (String.IsNullOrWhiteSpace(folder))
375 throw new ArgumentNullException("folder", "The folder property can't be empty");
377 var folderUrl=String.Format("{0}/{1}",container,folder);
379 var request = new RestRequest { Path = folderUrl, Method = WebMethod.Put, RetryPolicy = _retryPolicy,Timeout = _shortTimeout };
380 request.AddHeader("Content-Type", @"application/directory");
381 request.AddHeader("Content-Length", "0");
384 _client.Parameters.Clear();
385 _client.Headers.Add("Content-Type", @"application/directory");
386 _client.Headers.Add("Content-Length", "0");
387 _client.PutWithRetry(folderUrl,3);
389 if (_client.StatusCode != HttpStatusCode.Created && _client.StatusCode != HttpStatusCode.Accepted)
390 throw CreateWebException("CreateFolder", _client.StatusCode);
394 public ContainerInfo GetContainerInfo(string container)
396 if (String.IsNullOrWhiteSpace(container))
397 throw new ArgumentNullException("container", "The container property can't be empty");
400 var request = new RestRequest { Path = container, Method = WebMethod.Head, RetryPolicy = _retryPolicy,Timeout = _shortTimeout };
401 var response = _client.Request(request);
403 _client.Head(container);
404 switch (_client.StatusCode)
406 case HttpStatusCode.NoContent:
407 var containerInfo = new ContainerInfo
410 Count =long.Parse(_client.GetHeaderValue("X-Container-Object-Count")),
411 Bytes = long.Parse(_client.GetHeaderValue("X-Container-Bytes-Used"))
413 return containerInfo;
414 case HttpStatusCode.NotFound:
415 return ContainerInfo.Empty;
417 throw CreateWebException("GetContainerInfo", _client.StatusCode);
421 public void CreateContainer(string container)
423 if (String.IsNullOrWhiteSpace(container))
424 throw new ArgumentNullException("container", "The container property can't be empty");
427 var request = new RestRequest { Path = container, Method = WebMethod.Put, RetryPolicy = _retryPolicy,Timeout = _shortTimeout };
428 var response = _client.Request(request);
430 _client.PutWithRetry(container,3);
431 var expectedCodes = new[]{HttpStatusCode.Created ,HttpStatusCode.Accepted , HttpStatusCode.OK};
432 if (!expectedCodes.Contains(_client.StatusCode))
433 throw CreateWebException("CreateContainer", _client.StatusCode);
436 public void DeleteContainer(string container)
438 if (String.IsNullOrWhiteSpace(container))
439 throw new ArgumentNullException("container", "The container property can't be empty");
442 var request = new RestRequest { Path = container, Method = WebMethod.Delete, RetryPolicy = _retryPolicy,Timeout = _shortTimeout };
443 var response = _client.Request(request);
445 _client.DeleteWithRetry(container,3);
446 var expectedCodes = new[] { HttpStatusCode.NotFound, HttpStatusCode.NoContent};
447 if (!expectedCodes.Contains(_client.StatusCode))
448 throw CreateWebException("DeleteContainer", _client.StatusCode);
455 /// <param name="container"></param>
456 /// <param name="objectName"></param>
457 /// <param name="fileName"></param>
458 /// <returns></returns>
459 /// <remarks>>This method should have no timeout or a very long one</remarks>
460 public Task GetObject(string container, string objectName, string fileName)
462 if (String.IsNullOrWhiteSpace(container))
463 throw new ArgumentNullException("container", "The container property can't be empty");
464 if (String.IsNullOrWhiteSpace(objectName))
465 throw new ArgumentNullException("objectName", "The objectName property can't be empty");
467 var request = new RestRequest {Path = container + "/" + objectName, Method = WebMethod.Get};
469 if (DownloadPercentLimit > 0)
470 request.TaskOptions = new TaskOptions<int> { RateLimitPercent = DownloadPercentLimit };
474 var url = String.Join("/", _client.BaseAddress, container, objectName);
475 var uri = new Uri(url);
477 var client = new PithosClient(_client){Timeout=0};
478 /* if (!String.IsNullOrWhiteSpace(_client.Proxy))
479 client.Proxy = new WebProxy(_client.Proxy);
481 CopyHeaders(_client, client);*/
483 Trace.TraceInformation("[GET] START {0}", objectName);
484 client.DownloadProgressChanged += (sender, args) =>
485 Trace.TraceInformation("[GET PROGRESS] {0} {1}% {2} of {3}",
486 fileName, args.ProgressPercentage,
488 args.TotalBytesToReceive);
490 return _client.DownloadFileTask(uri, fileName)
491 .ContinueWith(download =>
495 if (download.IsFaulted)
497 Trace.TraceError("[GET] FAIL for {0} with \r{1}", objectName,
502 Trace.TraceInformation("[GET] END {0}", objectName);
506 catch (Exception exc)
508 Trace.TraceError("[GET] END {0} with {1}", objectName, exc);
519 /// <param name="container"></param>
520 /// <param name="objectName"></param>
521 /// <param name="fileName"></param>
522 /// <param name="hash">Optional hash value for the file. If no hash is provided, the method calculates a new hash</param>
523 /// <remarks>>This method should have no timeout or a very long one</remarks>
524 public Task PutObject(string container, string objectName, string fileName, string hash = null)
526 if (String.IsNullOrWhiteSpace(container))
527 throw new ArgumentNullException("container", "The container property can't be empty");
528 if (String.IsNullOrWhiteSpace(objectName))
529 throw new ArgumentNullException("objectName", "The objectName property can't be empty");
530 if (String.IsNullOrWhiteSpace(fileName))
531 throw new ArgumentNullException("fileName", "The fileName property can't be empty");
532 if (!File.Exists(fileName))
533 throw new FileNotFoundException("The file does not exist",fileName);
538 var url = String.Join("/",_client.BaseAddress,container,objectName);
539 var uri = new Uri(url);
541 var client = new PithosClient(_client){Timeout=0};
542 string etag = hash ?? CalculateHash(fileName);
544 client.Headers.Add("Content-Type", "application/octet-stream");
545 client.Headers.Add("ETag", etag);
548 if(!String.IsNullOrWhiteSpace(_client.Proxy))
549 client.Proxy = new WebProxy(_client.Proxy);
551 CopyHeaders(_client, client);
554 Trace.TraceInformation("[PUT] START {0}", objectName);
555 client.UploadProgressChanged += (sender, args) =>
557 Trace.TraceInformation("[PUT PROGRESS] {0} {1}% {2} of {3}", fileName, args.ProgressPercentage, args.BytesSent, args.TotalBytesToSend);
560 return client.UploadFileTask(uri, "PUT", fileName)
561 .ContinueWith(upload=>
565 if (upload.IsFaulted)
567 Trace.TraceError("[PUT] FAIL for {0} with \r{1}",objectName,upload.Exception);
570 Trace.TraceInformation("[PUT] END {0}", objectName);
573 catch (Exception exc)
575 Trace.TraceError("[PUT] END {0} with {1}", objectName, exc);
583 /// Copies headers from a Hammock RestClient to a WebClient
585 /// <param name="source">The RestClient from which the headers are copied</param>
586 /// <param name="target">The WebClient to which the headers are copied</param>
587 private static void CopyHeaders(RestClient source, WebClient target)
589 Contract.Requires(source!=null,"source can't be null");
590 Contract.Requires(target != null, "target can't be null");
592 throw new ArgumentNullException("source", "source can't be null");
594 throw new ArgumentNullException("target", "target can't be null");
596 foreach (var header in source.GetAllHeaders())
598 target.Headers.Add(header.Name, header.Value);
603 /// Copies headers from a Hammock RestClient to a WebClient
605 /// <param name="source">The RestClient from which the headers are copied</param>
606 /// <param name="target">The WebClient to which the headers are copied</param>
607 private static void CopyHeaders(RestClient source, WebRequest target)
609 Contract.Requires(source!=null,"source can't be null");
610 Contract.Requires(target != null, "target can't be null");
612 throw new ArgumentNullException("source", "source can't be null");
614 throw new ArgumentNullException("target", "target can't be null");
616 foreach (var header in source.GetAllHeaders())
618 target.Headers.Add(header.Name, header.Value);
623 private static string CalculateHash(string fileName)
626 using (var hasher = MD5.Create())
627 using(var stream=File.OpenRead(fileName))
629 var hashBuilder=new StringBuilder();
630 foreach (byte b in hasher.ComputeHash(stream))
631 hashBuilder.Append(b.ToString("x2").ToLower());
632 hash = hashBuilder.ToString();
637 public void DeleteObject(string container, string objectName)
639 if (String.IsNullOrWhiteSpace(container))
640 throw new ArgumentNullException("container", "The container property can't be empty");
641 if (String.IsNullOrWhiteSpace(objectName))
642 throw new ArgumentNullException("objectName", "The objectName property can't be empty");
645 var request = new RestRequest { Path = container + "/" + objectName, Method = WebMethod.Delete, RetryPolicy = _retryPolicy,Timeout = _shortTimeout };
646 var response = _client.Request(request);
648 _client.DeleteWithRetry(container + "/" + objectName,3);
650 var expectedCodes = new[] { HttpStatusCode.NotFound, HttpStatusCode.NoContent };
651 if (!expectedCodes.Contains(_client.StatusCode))
652 throw CreateWebException("DeleteObject", _client.StatusCode);
656 public void MoveObject(string sourceContainer, string oldObjectName, string targetContainer,string newObjectName)
658 if (String.IsNullOrWhiteSpace(sourceContainer))
659 throw new ArgumentNullException("sourceContainer", "The container property can't be empty");
660 if (String.IsNullOrWhiteSpace(oldObjectName))
661 throw new ArgumentNullException("oldObjectName", "The oldObjectName property can't be empty");
662 if (String.IsNullOrWhiteSpace(targetContainer))
663 throw new ArgumentNullException("targetContainer", "The container property can't be empty");
664 if (String.IsNullOrWhiteSpace(newObjectName))
665 throw new ArgumentNullException("newObjectName", "The newObjectName property can't be empty");
667 var targetUrl = targetContainer + "/" + newObjectName;
668 var sourceUrl = String.Format("/{0}/{1}", sourceContainer, oldObjectName);
671 var request = new RestRequest { Path = targetUrl, Method = WebMethod.Put };
672 request.AddHeader("X-Copy-From",sourceUrl);
673 request.AddPostContent(new byte[]{});
674 var response = _client.Request(request);
677 var client = new PithosClient(_client);
678 client.Headers.Add("X-Copy-From", sourceUrl);
679 client.PutWithRetry(targetUrl,3);
681 var expectedCodes = new[] { HttpStatusCode.OK ,HttpStatusCode.NoContent ,HttpStatusCode.Created };
682 if (expectedCodes.Contains(client.StatusCode))
684 this.DeleteObject(sourceContainer,oldObjectName);
687 throw CreateWebException("MoveObject", client.StatusCode);
692 /*private string GetHeaderValue(string headerName, WebHeaderCollection headers, IQueryable<string> keys)
694 if (keys.Any(key => key == headerName))
695 return headers[headerName];
697 throw new WebException(String.Format("The {0} header is missing", headerName));
700 /* private static void ThrowIfNotStatusOK(RestResponse response, string message)
702 int status = (int)response.StatusCode;
703 ThrowIfNotStatusOK(status, message);
706 private static void ThrowIfNotStatusOK(HttpWebResponse response, string message)
708 int status = (int)response.StatusCode;
709 ThrowIfNotStatusOK(status, message);
712 private static void ThrowIfNotStatusOK(int status, string message)
714 if (status < 200 || status >= 300)
715 throw new WebException(String.Format("{0} with code {1}", message, status));
718 private static WebException CreateWebException(string operation, HttpStatusCode statusCode)
720 return new WebException(String.Format("{0} failed with unexpected status code {1}", operation, statusCode));
723 /* public static Func<T> Retry<T>(Func<int,T> original, int retryCount,int timeout)
731 return original(timeout);
733 catch (WebException e)
735 if (e.Status == WebExceptionStatus.Timeout)