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;
13 using Hammock.Caching;
14 using Hammock.Retries;
15 using Hammock.Serialization;
18 using Newtonsoft.Json;
19 using Pithos.Interfaces;
21 namespace Pithos.Network
23 [Export(typeof(ICloudClient))]
24 public class CloudFilesClient:ICloudClient
26 string _rackSpaceAuthUrl = "https://auth.api.rackspacecloud.com";
27 private string _pithosAuthUrl = "https://pithos.grnet.gr";
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; }
39 public double DownloadPercentLimit { get; set; }
40 public double UploadPercentLimit { get; set; }
44 get { return UsePithos ? _pithosAuthUrl : _rackSpaceAuthUrl; }
47 public string VersionPath
49 get { return UsePithos ? "v1" : "v1.0"; }
52 public bool UsePithos { get; set; }
54 public void Authenticate(string userName,string apiKey)
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");
65 var proxy = Proxy != null ? Proxy.ToString() : null;
69 string storageUrl = String.Format("{0}/{1}/{2}", AuthUrl, VersionPath, UserName);
70 StorageUrl = new Uri(storageUrl);
75 string authUrl = String.Format("{0}/{1}", AuthUrl, VersionPath);
76 var authClient = new RestClient {Path = authUrl, Proxy = proxy};
78 authClient.AddHeader("X-Auth-User", UserName);
79 authClient.AddHeader("X-Auth-Key", ApiKey);
81 var response = authClient.Request();
83 ThrowIfNotStatusOK(response, "Authentication failed");
85 var keys = response.Headers.AllKeys.AsQueryable();
87 string storageUrl = GetHeaderValue("X-Storage-Url", response, keys);
88 if (String.IsNullOrWhiteSpace(storageUrl))
89 throw new InvalidOperationException("Failed to obtain storage url");
90 StorageUrl = new Uri(storageUrl);
92 var token = GetHeaderValue("X-Auth-Token", response, keys);
93 if (String.IsNullOrWhiteSpace(token))
94 throw new InvalidOperationException("Failed to obtain token url");
98 _retryPolicy = new RetryPolicy { RetryCount = _retries };
99 _retryPolicy.RetryConditions.Add(new TimeoutRetryCondition());
101 _client = new RestClient { Authority = StorageUrl.AbsoluteUri, Path = UserName, Proxy = proxy };
102 _client.FileProgress += OnFileProgress;
104 _client.AddHeader("X-Auth-Token", Token);
107 _client.AddHeader("X-Auth-User", UserName);
108 _client.AddHeader("X-Auth-Key",ApiKey);
111 Trace.TraceInformation("[AUTHENTICATE] End for {0}", userName);
114 private void OnFileProgress(object sender, FileProgressEventArgs e)
116 Trace.TraceInformation("[PROGRESS] {0} {1:p} {2} of {3}",e.FileName,(double)e.BytesWritten/e.TotalBytes, e.BytesWritten,e.TotalBytes);
119 public IList<ContainerInfo> ListContainers()
121 //Workaround for Hammock quirk: Hammock always
122 //appends a / unless a Path is specified.
124 //Create a request with a complete path
125 var request = new RestRequest { Path = StorageUrl.ToString(), RetryPolicy = _retryPolicy,Timeout = _shortTimeout };
126 request.AddParameter("format","json");
127 //Create a client clone
128 var client = new RestClient{Proxy=Proxy.ToString()};
129 foreach (var header in _client.GetAllHeaders())
131 client.AddHeader(header.Name,header.Value);
134 var response = client.Request(request);
136 if (response.StatusCode == HttpStatusCode.NoContent)
137 return new List<ContainerInfo>();
139 ThrowIfNotStatusOK(response, "List Containers failed");
142 var infos=JsonConvert.DeserializeObject<IList<ContainerInfo>>(response.Content);
147 public IList<ObjectInfo> ListObjects(string container)
149 if (String.IsNullOrWhiteSpace(container))
150 throw new ArgumentNullException("container", "The container property can't be empty");
152 Trace.TraceInformation("[START] ListObjects");
154 var request = new RestRequest { Path = container, RetryPolicy = _retryPolicy, Timeout = TimeSpan.FromMinutes(1) };
155 request.AddParameter("format", "json");
156 var response = _client.Request(request);
158 var infos = InfosFromContent(response);
160 Trace.TraceInformation("[END] ListObjects");
166 public IList<ObjectInfo> ListObjects(string container,string folder)
168 if (String.IsNullOrWhiteSpace(container))
169 throw new ArgumentNullException("container", "The container property can't be empty");
171 Trace.TraceInformation("[START] ListObjects");
173 var request = new RestRequest { Path = container,RetryPolicy = _retryPolicy, Timeout = TimeSpan.FromMinutes(1) };
174 request.AddParameter("format", "json");
175 request.AddParameter("path", folder);
176 var response = _client.Request(request);
178 var infos = InfosFromContent(response);
180 Trace.TraceInformation("[END] ListObjects");
184 private static IList<ObjectInfo> InfosFromContent(RestResponse response)
186 if (response.TimedOut)
187 return new List<ObjectInfo>();
189 if (response.StatusCode == 0)
190 return new List<ObjectInfo>();
192 if (response.StatusCode == HttpStatusCode.NoContent)
193 return new List<ObjectInfo>();
196 var statusCode = (int)response.StatusCode;
197 if (statusCode < 200 || statusCode >= 300)
199 Trace.TraceWarning("ListObjects failed with code {0} - {1}", response.StatusCode, response.StatusDescription);
200 return new List<ObjectInfo>();
203 var infos = JsonConvert.DeserializeObject<IList<ObjectInfo>>(response.Content);
207 public bool ContainerExists(string container)
209 if (String.IsNullOrWhiteSpace(container))
210 throw new ArgumentNullException("container", "The container property can't be empty");
212 var request = new RestRequest { Path = container, Method = WebMethod.Head, RetryPolicy = _retryPolicy,Timeout = _shortTimeout };
213 var response = _client.Request(request);
215 switch(response.StatusCode)
217 case HttpStatusCode.NoContent:
219 case HttpStatusCode.NotFound:
222 throw new WebException(String.Format("ContainerExists failed with unexpected status code {0}",response.StatusCode));
226 public bool ObjectExists(string container,string objectName)
228 if (String.IsNullOrWhiteSpace(container))
229 throw new ArgumentNullException("container", "The container property can't be empty");
230 if (String.IsNullOrWhiteSpace(objectName))
231 throw new ArgumentNullException("objectName", "The objectName property can't be empty");
234 var request = new RestRequest { Path = container + "/" + objectName, Method = WebMethod.Head,RetryPolicy = _retryPolicy, Timeout = _shortTimeout };
235 var response = _client.Request(request);
237 switch (response.StatusCode)
239 case HttpStatusCode.OK:
240 case HttpStatusCode.NoContent:
242 case HttpStatusCode.NotFound:
245 throw new WebException(String.Format("ObjectExists failed with unexpected status code {0}", response.StatusCode));
250 public ObjectInfo GetObjectInfo(string container, string objectName)
252 if (String.IsNullOrWhiteSpace(container))
253 throw new ArgumentNullException("container", "The container property can't be empty");
254 if (String.IsNullOrWhiteSpace(objectName))
255 throw new ArgumentNullException("objectName", "The objectName property can't be empty");
258 var request = new RestRequest { Path = container + "/" + objectName, Method = WebMethod.Head, RetryPolicy = _retryPolicy,Timeout = _shortTimeout };
259 var response = _client.Request(request);
261 if (response.TimedOut)
262 return ObjectInfo.Empty;
264 switch (response.StatusCode)
266 case HttpStatusCode.OK:
267 case HttpStatusCode.NoContent:
268 var keys = response.Headers.AllKeys.AsQueryable();
269 var tags=(from key in keys
270 where key.StartsWith("X-Object-Meta-")
271 let name=key.Substring(14)
272 select new {Name=name,Value=response.Headers[name]})
273 .ToDictionary(t=>t.Name,t=>t.Value);
274 return new ObjectInfo
277 Bytes = long.Parse(GetHeaderValue("Content-Length", response, keys)),
278 Hash = GetHeaderValue("ETag", response, keys),
279 Content_Type = GetHeaderValue("Content-Type", response, keys),
282 case HttpStatusCode.NotFound:
283 return ObjectInfo.Empty;
285 if (request.RetryState.RepeatCount > 0)
287 Trace.TraceWarning("[RETRY FAIL] GetObjectInfo for {0} failed after {1} retries",
288 objectName, request.RetryState.RepeatCount);
289 return ObjectInfo.Empty;
291 if (response.InnerException != null)
292 throw new WebException(String.Format("[FAIL] GetObjectInfo for {0} failed with unexpected status code {1}", objectName, response.StatusCode), response.InnerException);
293 throw new WebException(String.Format("[FAIL] GetObjectInfo for {0} failed with unexpected status code {1}", objectName, response.StatusCode));
297 public void CreateFolder(string container, string folder)
299 if (String.IsNullOrWhiteSpace(container))
300 throw new ArgumentNullException("container", "The container property can't be empty");
301 if (String.IsNullOrWhiteSpace(folder))
302 throw new ArgumentNullException("folder", "The folder property can't be empty");
304 var folderUrl=String.Format("{0}/{1}",container,folder);
305 var request = new RestRequest { Path = folderUrl, Method = WebMethod.Put, RetryPolicy = _retryPolicy,Timeout = _shortTimeout };
306 request.AddHeader("Content-Type", @"application/directory");
307 request.AddHeader("Content-Length", "0");
309 var response = _client.Request(request);
311 if (response.StatusCode != HttpStatusCode.Created && response.StatusCode != HttpStatusCode.Accepted)
312 throw new WebException(String.Format("CreateFolder failed with unexpected status code {0}", response.StatusCode));
316 public ContainerInfo GetContainerInfo(string container)
318 if (String.IsNullOrWhiteSpace(container))
319 throw new ArgumentNullException("container", "The container property can't be empty");
321 var request = new RestRequest { Path = container, Method = WebMethod.Head, RetryPolicy = _retryPolicy,Timeout = _shortTimeout };
322 var response = _client.Request(request);
324 switch(response.StatusCode)
326 case HttpStatusCode.NoContent:
327 var keys = response.Headers.AllKeys.AsQueryable();
328 var containerInfo = new ContainerInfo
331 Count =long.Parse(GetHeaderValue("X-Container-Object-Count", response, keys)),
332 Bytes =long.Parse(GetHeaderValue("X-Container-Bytes-Used", response, keys))
334 return containerInfo;
335 case HttpStatusCode.NotFound:
336 return ContainerInfo.Empty;
338 throw new WebException(String.Format("ContainerExists failed with unexpected status code {0}",response.StatusCode));
342 public void CreateContainer(string container)
344 if (String.IsNullOrWhiteSpace(container))
345 throw new ArgumentNullException("container", "The container property can't be empty");
347 var request = new RestRequest { Path = container, Method = WebMethod.Put, RetryPolicy = _retryPolicy,Timeout = _shortTimeout };
349 var response = _client.Request(request);
351 if (response.StatusCode!=HttpStatusCode.Created && response.StatusCode!=HttpStatusCode.Accepted )
352 throw new WebException(String.Format("ContainerExists failed with unexpected status code {0}", response.StatusCode));
355 public void DeleteContainer(string container)
357 if (String.IsNullOrWhiteSpace(container))
358 throw new ArgumentNullException("container", "The container property can't be empty");
360 var request = new RestRequest { Path = container, Method = WebMethod.Delete, RetryPolicy = _retryPolicy,Timeout = _shortTimeout };
361 var response = _client.Request(request);
363 if (response.StatusCode == HttpStatusCode.NotFound || response.StatusCode == HttpStatusCode.NoContent)
366 throw new WebException(String.Format("DeleteContainer failed with unexpected status code {0}", response.StatusCode));
373 /// <param name="container"></param>
374 /// <param name="objectName"></param>
375 /// <returns></returns>
376 /// <remarks>>This method should have no timeout or a very long one</remarks>
377 public Stream GetObject(string container, string objectName)
379 if (String.IsNullOrWhiteSpace(container))
380 throw new ArgumentNullException("container", "The container property can't be empty");
381 if (String.IsNullOrWhiteSpace(objectName))
382 throw new ArgumentNullException("objectName", "The objectName property can't be empty");
384 var request = new RestRequest { Path = container + "/" + objectName, Method = WebMethod.Get };
386 if (DownloadPercentLimit > 0)
387 request.TaskOptions = new TaskOptions<int> { RateLimitPercent = DownloadPercentLimit };
390 var response = _client.Request(request);
392 if (response.StatusCode == HttpStatusCode.NotFound)
393 throw new FileNotFoundException();
394 if (response.StatusCode == HttpStatusCode.OK)
396 return response.ContentStream;
399 throw new WebException(String.Format("GetObject failed with unexpected status code {0}", response.StatusCode));
405 /// <param name="container"></param>
406 /// <param name="objectName"></param>
407 /// <param name="fileName"></param>
408 /// <param name="hash">Optional hash value for the file. If no hash is provided, the method calculates a new hash</param>
409 /// <remarks>>This method should have no timeout or a very long one</remarks>
410 public Task PutObject(string container, string objectName, string fileName, string hash = null)
412 if (String.IsNullOrWhiteSpace(container))
413 throw new ArgumentNullException("container", "The container property can't be empty");
414 if (String.IsNullOrWhiteSpace(objectName))
415 throw new ArgumentNullException("objectName", "The objectName property can't be empty");
416 if (String.IsNullOrWhiteSpace(fileName))
417 throw new ArgumentNullException("fileName", "The fileName property can't be empty");
418 if (!File.Exists(fileName))
419 throw new FileNotFoundException("The file does not exist",fileName);
424 var url = String.Join("/",new[]{_client.Authority,container,objectName});
425 var uri = new Uri(url);
427 var client = new WebClient();
428 string etag = hash ?? CalculateHash(fileName);
430 client.Headers.Add("Content-Type", "application/octet-stream");
431 client.Headers.Add("ETag", etag);
433 if(!String.IsNullOrWhiteSpace(_client.Proxy))
434 client.Proxy = new WebProxy(_client.Proxy);
436 CopyHeaders(_client, client);
438 Trace.TraceInformation("[PUT] START {0}", objectName);
439 client.UploadProgressChanged += (sender, args) =>
441 Trace.TraceInformation("[PROGRESS] {0} {1}% {2} of {3}", fileName, args.ProgressPercentage, args.BytesSent, args.TotalBytesToSend);
444 return client.UploadFileTask(uri, "PUT", fileName)
445 .ContinueWith(upload=>
449 if (upload.IsFaulted)
451 Trace.TraceError("[PUT] FAIL for {0} with \r{1}",objectName,upload.Exception);
454 Trace.TraceInformation("[PUT] END {0}", objectName);
457 catch (Exception exc)
459 Trace.TraceError("[PUT] END {0} with {1}", objectName, exc);
467 /// Copies headers from a Hammock RestClient to a WebClient
469 /// <param name="source">The RestClient from which the headers are copied</param>
470 /// <param name="target">The WebClient to which the headers are copied</param>
471 private static void CopyHeaders(RestClient source, WebClient target)
473 Contract.Requires(source!=null,"source can't be null");
474 Contract.Requires(target != null, "target can't be null");
476 throw new ArgumentNullException("source", "source can't be null");
478 throw new ArgumentNullException("target", "target can't be null");
480 foreach (var header in source.GetAllHeaders())
482 target.Headers.Add(header.Name, header.Value);
486 private static string CalculateHash(string fileName)
489 using (var hasher = MD5.Create())
490 using(var stream=File.OpenRead(fileName))
492 var hashBuilder=new StringBuilder();
493 foreach (byte b in hasher.ComputeHash(stream))
494 hashBuilder.Append(b.ToString("x2").ToLower());
495 hash = hashBuilder.ToString();
500 public void DeleteObject(string container, string objectName)
502 if (String.IsNullOrWhiteSpace(container))
503 throw new ArgumentNullException("container", "The container property can't be empty");
504 if (String.IsNullOrWhiteSpace(objectName))
505 throw new ArgumentNullException("objectName", "The objectName property can't be empty");
507 var request = new RestRequest { Path = container + "/" + objectName, Method = WebMethod.Delete, RetryPolicy = _retryPolicy,Timeout = _shortTimeout };
508 var response = _client.Request(request);
510 if (response.StatusCode == HttpStatusCode.NotFound || response.StatusCode == HttpStatusCode.NoContent)
513 throw new WebException(String.Format("DeleteObject failed with unexpected status code {0}", response.StatusCode));
517 public void MoveObject(string container, string oldObjectName, string newObjectName)
519 if (String.IsNullOrWhiteSpace(container))
520 throw new ArgumentNullException("container", "The container property can't be empty");
521 if (String.IsNullOrWhiteSpace(oldObjectName))
522 throw new ArgumentNullException("oldObjectName", "The oldObjectName property can't be empty");
523 if (String.IsNullOrWhiteSpace(newObjectName))
524 throw new ArgumentNullException("newObjectName", "The newObjectName property can't be empty");
526 var request = new RestRequest { Path = container + "/" + newObjectName, Method = WebMethod.Put };
527 request.AddHeader("X-Copy-From",String.Format("/{0}/{1}",container,oldObjectName));
528 request.AddPostContent(new byte[]{});
529 var response = _client.Request(request);
531 if (response.StatusCode == HttpStatusCode.OK || response.StatusCode == HttpStatusCode.NoContent || response.StatusCode==HttpStatusCode.Created)
533 this.DeleteObject(container,oldObjectName);
536 throw new WebException(String.Format("MoveObject failed with unexpected status code {0}", response.StatusCode));
539 private string GetHeaderValue(string headerName, RestResponse response, IQueryable<string> keys)
541 if (keys.Any(key => key == headerName))
542 return response.Headers[headerName];
544 throw new WebException(String.Format("The {0} header is missing",headerName));
547 private static void ThrowIfNotStatusOK(RestResponse response, string message)
549 int status = (int)response.StatusCode;
550 if (status < 200 || status >= 300)
551 throw new WebException(String.Format("{0} with code {1}",message, status));