root / trunk / Pithos.Network / CloudFilesClient.cs @ d15e99b4
History | View | Annotate | Download (21.3 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.Algorithms; |
12 |
using System.Threading.Tasks; |
13 |
using Newtonsoft.Json; |
14 |
using Pithos.Interfaces; |
15 |
using WebHeaderCollection = System.Net.WebHeaderCollection; |
16 |
|
17 |
namespace Pithos.Network |
18 |
{ |
19 |
[Export(typeof(ICloudClient))] |
20 |
public class CloudFilesClient:ICloudClient |
21 |
{ |
22 |
|
23 |
private PithosClient _client; |
24 |
private readonly TimeSpan _shortTimeout = TimeSpan.FromSeconds(10); |
25 |
private readonly int _retries = 5; |
26 |
public string ApiKey { get; set; } |
27 |
public string UserName { get; set; } |
28 |
public Uri StorageUrl { get; set; } |
29 |
public string Token { get; set; } |
30 |
public Uri Proxy { get; set; } |
31 |
|
32 |
public double DownloadPercentLimit { get; set; } |
33 |
public double UploadPercentLimit { get; set; } |
34 |
|
35 |
public string AuthenticationUrl { get; set; } |
36 |
|
37 |
|
38 |
public string VersionPath |
39 |
{ |
40 |
get { return UsePithos ? "v1" : "v1.0"; } |
41 |
} |
42 |
|
43 |
public bool UsePithos { get; set; } |
44 |
|
45 |
private bool _authenticated = false; |
46 |
|
47 |
public void Authenticate(string userName,string apiKey) |
48 |
{ |
49 |
Trace.TraceInformation("[AUTHENTICATE] Start for {0}", userName); |
50 |
if (String.IsNullOrWhiteSpace(userName)) |
51 |
throw new ArgumentNullException("userName", "The userName property can't be empty"); |
52 |
if (String.IsNullOrWhiteSpace(apiKey)) |
53 |
throw new ArgumentNullException("apiKey", "The apiKey property can't be empty"); |
54 |
|
55 |
if (_authenticated) |
56 |
return; |
57 |
|
58 |
UserName = userName; |
59 |
ApiKey = apiKey; |
60 |
|
61 |
if (UsePithos) |
62 |
{ |
63 |
Token = ApiKey; |
64 |
string storageUrl = String.Format("{0}/{1}/{2}", AuthenticationUrl, VersionPath, UserName); |
65 |
StorageUrl = new Uri(storageUrl); |
66 |
} |
67 |
else |
68 |
{ |
69 |
|
70 |
string authUrl = String.Format("{0}/{1}", AuthenticationUrl, VersionPath); |
71 |
var authClient = new PithosClient{BaseAddress= authUrl}; |
72 |
if (Proxy != null) |
73 |
authClient.Proxy = new WebProxy(Proxy); |
74 |
|
75 |
authClient.Headers.Add("X-Auth-User", UserName); |
76 |
authClient.Headers.Add("X-Auth-Key", ApiKey); |
77 |
|
78 |
var response = authClient.DownloadStringWithRetry("",3); |
79 |
|
80 |
authClient.AssertStatusOK("Authentication failed"); |
81 |
|
82 |
string storageUrl = authClient.GetHeaderValue("X-Storage-Url"); |
83 |
if (String.IsNullOrWhiteSpace(storageUrl)) |
84 |
throw new InvalidOperationException("Failed to obtain storage url"); |
85 |
StorageUrl = new Uri(storageUrl); |
86 |
|
87 |
var token = authClient.GetHeaderValue("X-Auth-Token"); |
88 |
if (String.IsNullOrWhiteSpace(token)) |
89 |
throw new InvalidOperationException("Failed to obtain token url"); |
90 |
Token = token; |
91 |
} |
92 |
|
93 |
_client = new PithosClient{ |
94 |
BaseAddress = StorageUrl.AbsoluteUri, |
95 |
Timeout=10000, |
96 |
Retries=3}; |
97 |
if (Proxy!=null) |
98 |
_client.Proxy = new WebProxy(Proxy); |
99 |
|
100 |
_client.Headers.Add("X-Auth-Token", Token); |
101 |
|
102 |
Trace.TraceInformation("[AUTHENTICATE] End for {0}", userName); |
103 |
} |
104 |
|
105 |
|
106 |
public IList<ContainerInfo> ListContainers() |
107 |
{ |
108 |
|
109 |
var content=_client.DownloadStringWithRetry("",3); |
110 |
_client.Parameters.Clear(); |
111 |
_client.Parameters.Add("format", "json"); |
112 |
_client.AssertStatusOK("List Containers failed"); |
113 |
|
114 |
if (_client.StatusCode==HttpStatusCode.NoContent) |
115 |
return new List<ContainerInfo>(); |
116 |
var infos = JsonConvert.DeserializeObject<IList<ContainerInfo>>(content); |
117 |
return infos; |
118 |
|
119 |
} |
120 |
|
121 |
public IList<ObjectInfo> ListObjects(string container) |
122 |
{ |
123 |
if (String.IsNullOrWhiteSpace(container)) |
124 |
throw new ArgumentNullException("container", "The container property can't be empty"); |
125 |
|
126 |
Trace.TraceInformation("[START] ListObjects"); |
127 |
|
128 |
|
129 |
_client.Parameters.Clear(); |
130 |
_client.Parameters.Add("format", "json"); |
131 |
var content = _client.DownloadStringWithRetry(container, 3); |
132 |
|
133 |
_client.AssertStatusOK("ListObjects failed"); |
134 |
|
135 |
var infos = JsonConvert.DeserializeObject<IList<ObjectInfo>>(content); |
136 |
|
137 |
Trace.TraceInformation("[END] ListObjects"); |
138 |
return infos; |
139 |
} |
140 |
|
141 |
|
142 |
|
143 |
public IList<ObjectInfo> ListObjects(string container,string folder) |
144 |
{ |
145 |
if (String.IsNullOrWhiteSpace(container)) |
146 |
throw new ArgumentNullException("container", "The container property can't be empty"); |
147 |
|
148 |
Trace.TraceInformation("[START] ListObjects"); |
149 |
|
150 |
|
151 |
|
152 |
_client.Parameters.Clear(); |
153 |
_client.Parameters.Add("format", "json"); |
154 |
_client.Parameters.Add("path", folder); |
155 |
var content = _client.DownloadStringWithRetry(container, 3); |
156 |
_client.AssertStatusOK("ListObjects failed"); |
157 |
|
158 |
var infos = JsonConvert.DeserializeObject<IList<ObjectInfo>>(content); |
159 |
|
160 |
|
161 |
|
162 |
Trace.TraceInformation("[END] ListObjects"); |
163 |
return infos; |
164 |
} |
165 |
|
166 |
|
167 |
public bool ContainerExists(string container) |
168 |
{ |
169 |
if (String.IsNullOrWhiteSpace(container)) |
170 |
throw new ArgumentNullException("container", "The container property can't be empty"); |
171 |
|
172 |
_client.Parameters.Clear(); |
173 |
_client.Head(container,3); |
174 |
|
175 |
switch (_client.StatusCode) |
176 |
{ |
177 |
case HttpStatusCode.OK: |
178 |
case HttpStatusCode.NoContent: |
179 |
return true; |
180 |
case HttpStatusCode.NotFound: |
181 |
return false; |
182 |
default: |
183 |
throw CreateWebException("ContainerExists", _client.StatusCode); |
184 |
} |
185 |
} |
186 |
|
187 |
public bool ObjectExists(string container,string objectName) |
188 |
{ |
189 |
if (String.IsNullOrWhiteSpace(container)) |
190 |
throw new ArgumentNullException("container", "The container property can't be empty"); |
191 |
if (String.IsNullOrWhiteSpace(objectName)) |
192 |
throw new ArgumentNullException("objectName", "The objectName property can't be empty"); |
193 |
|
194 |
_client.Parameters.Clear(); |
195 |
_client.Head(container + "/" + objectName, 3); |
196 |
|
197 |
switch (_client.StatusCode) |
198 |
{ |
199 |
case HttpStatusCode.OK: |
200 |
case HttpStatusCode.NoContent: |
201 |
return true; |
202 |
case HttpStatusCode.NotFound: |
203 |
return false; |
204 |
default: |
205 |
throw CreateWebException("ObjectExists", _client.StatusCode); |
206 |
} |
207 |
|
208 |
} |
209 |
|
210 |
public ObjectInfo GetObjectInfo(string container, string objectName) |
211 |
{ |
212 |
if (String.IsNullOrWhiteSpace(container)) |
213 |
throw new ArgumentNullException("container", "The container property can't be empty"); |
214 |
if (String.IsNullOrWhiteSpace(objectName)) |
215 |
throw new ArgumentNullException("objectName", "The objectName property can't be empty"); |
216 |
|
217 |
try |
218 |
{ |
219 |
_client.Parameters.Clear(); |
220 |
|
221 |
_client.Head(container + "/" + objectName, 3); |
222 |
|
223 |
if (_client.TimedOut) |
224 |
return ObjectInfo.Empty; |
225 |
|
226 |
switch (_client.StatusCode) |
227 |
{ |
228 |
case HttpStatusCode.OK: |
229 |
case HttpStatusCode.NoContent: |
230 |
var keys = _client.ResponseHeaders.AllKeys.AsQueryable(); |
231 |
var tags = (from key in keys |
232 |
where key.StartsWith("X-Object-Meta-") |
233 |
let name = key.Substring(14) |
234 |
select new { Name = name, Value = _client.ResponseHeaders[name] }) |
235 |
.ToDictionary(t => t.Name, t => t.Value); |
236 |
var extensions = (from key in keys |
237 |
where key.StartsWith("X-Object-") && !key.StartsWith("X-Object-Meta-") |
238 |
let name = key.Substring(9) |
239 |
select new { Name = name, Value = _client.ResponseHeaders[name] }) |
240 |
.ToDictionary(t => t.Name, t => t.Value); |
241 |
return new ObjectInfo |
242 |
{ |
243 |
Name = objectName, |
244 |
Bytes = |
245 |
long.Parse(_client.GetHeaderValue("Content-Length")), |
246 |
Hash = _client.GetHeaderValue("ETag"), |
247 |
Content_Type = _client.GetHeaderValue("Content-Type"), |
248 |
Tags = tags, |
249 |
Extensions = extensions |
250 |
}; |
251 |
case HttpStatusCode.NotFound: |
252 |
return ObjectInfo.Empty; |
253 |
default: |
254 |
throw new WebException( |
255 |
String.Format("[FAIL] GetObjectInfo for {0} failed with unexpected status code {1}", |
256 |
objectName, _client.StatusCode)); |
257 |
} |
258 |
} |
259 |
catch (RetryException e) |
260 |
{ |
261 |
Trace.TraceWarning("[RETRY FAIL] GetObjectInfo for {0} failed."); |
262 |
return ObjectInfo.Empty; |
263 |
} |
264 |
catch (WebException e) |
265 |
{ |
266 |
Trace.TraceError(String.Format("[FAIL] GetObjectInfo for {0} failed with unexpected status code {1}", |
267 |
objectName, _client.StatusCode), e); |
268 |
throw; |
269 |
} |
270 |
|
271 |
} |
272 |
|
273 |
public void CreateFolder(string container, string folder) |
274 |
{ |
275 |
if (String.IsNullOrWhiteSpace(container)) |
276 |
throw new ArgumentNullException("container", "The container property can't be empty"); |
277 |
if (String.IsNullOrWhiteSpace(folder)) |
278 |
throw new ArgumentNullException("folder", "The folder property can't be empty"); |
279 |
|
280 |
var folderUrl=String.Format("{0}/{1}",container,folder); |
281 |
|
282 |
_client.Parameters.Clear(); |
283 |
_client.Headers.Add("Content-Type", @"application/directory"); |
284 |
_client.Headers.Add("Content-Length", "0"); |
285 |
_client.PutWithRetry(folderUrl,3); |
286 |
|
287 |
if (_client.StatusCode != HttpStatusCode.Created && _client.StatusCode != HttpStatusCode.Accepted) |
288 |
throw CreateWebException("CreateFolder", _client.StatusCode); |
289 |
|
290 |
} |
291 |
|
292 |
public ContainerInfo GetContainerInfo(string container) |
293 |
{ |
294 |
if (String.IsNullOrWhiteSpace(container)) |
295 |
throw new ArgumentNullException("container", "The container property can't be empty"); |
296 |
|
297 |
_client.Head(container); |
298 |
switch (_client.StatusCode) |
299 |
{ |
300 |
case HttpStatusCode.NoContent: |
301 |
var containerInfo = new ContainerInfo |
302 |
{ |
303 |
Name = container, |
304 |
Count =long.Parse(_client.GetHeaderValue("X-Container-Object-Count")), |
305 |
Bytes = long.Parse(_client.GetHeaderValue("X-Container-Bytes-Used")) |
306 |
}; |
307 |
return containerInfo; |
308 |
case HttpStatusCode.NotFound: |
309 |
return ContainerInfo.Empty; |
310 |
default: |
311 |
throw CreateWebException("GetContainerInfo", _client.StatusCode); |
312 |
} |
313 |
} |
314 |
|
315 |
public void CreateContainer(string container) |
316 |
{ |
317 |
if (String.IsNullOrWhiteSpace(container)) |
318 |
throw new ArgumentNullException("container", "The container property can't be empty"); |
319 |
|
320 |
_client.PutWithRetry(container,3); |
321 |
var expectedCodes = new[]{HttpStatusCode.Created ,HttpStatusCode.Accepted , HttpStatusCode.OK}; |
322 |
if (!expectedCodes.Contains(_client.StatusCode)) |
323 |
throw CreateWebException("CreateContainer", _client.StatusCode); |
324 |
} |
325 |
|
326 |
public void DeleteContainer(string container) |
327 |
{ |
328 |
if (String.IsNullOrWhiteSpace(container)) |
329 |
throw new ArgumentNullException("container", "The container property can't be empty"); |
330 |
|
331 |
_client.DeleteWithRetry(container,3); |
332 |
var expectedCodes = new[] { HttpStatusCode.NotFound, HttpStatusCode.NoContent}; |
333 |
if (!expectedCodes.Contains(_client.StatusCode)) |
334 |
throw CreateWebException("DeleteContainer", _client.StatusCode); |
335 |
|
336 |
} |
337 |
|
338 |
/// <summary> |
339 |
/// |
340 |
/// </summary> |
341 |
/// <param name="container"></param> |
342 |
/// <param name="objectName"></param> |
343 |
/// <param name="fileName"></param> |
344 |
/// <returns></returns> |
345 |
/// <remarks>>This method should have no timeout or a very long one</remarks> |
346 |
public Task GetObject(string container, string objectName, string fileName) |
347 |
{ |
348 |
if (String.IsNullOrWhiteSpace(container)) |
349 |
throw new ArgumentNullException("container", "The container property can't be empty"); |
350 |
if (String.IsNullOrWhiteSpace(objectName)) |
351 |
throw new ArgumentNullException("objectName", "The objectName property can't be empty"); |
352 |
|
353 |
try |
354 |
{ |
355 |
var url = String.Join("/", _client.BaseAddress, container, objectName); |
356 |
var uri = new Uri(url); |
357 |
|
358 |
var client = new PithosClient(_client){Timeout=0}; |
359 |
|
360 |
|
361 |
Trace.TraceInformation("[GET] START {0}", objectName); |
362 |
client.DownloadProgressChanged += (sender, args) => |
363 |
Trace.TraceInformation("[GET PROGRESS] {0} {1}% {2} of {3}", |
364 |
fileName, args.ProgressPercentage, |
365 |
args.BytesReceived, |
366 |
args.TotalBytesToReceive); |
367 |
|
368 |
return _client.DownloadFileTask(uri, fileName) |
369 |
.ContinueWith(download => |
370 |
{ |
371 |
client.Dispose(); |
372 |
|
373 |
if (download.IsFaulted) |
374 |
{ |
375 |
Trace.TraceError("[GET] FAIL for {0} with \r{1}", objectName, |
376 |
download.Exception); |
377 |
} |
378 |
else |
379 |
{ |
380 |
Trace.TraceInformation("[GET] END {0}", objectName); |
381 |
} |
382 |
}); |
383 |
} |
384 |
catch (Exception exc) |
385 |
{ |
386 |
Trace.TraceError("[GET] END {0} with {1}", objectName, exc); |
387 |
throw; |
388 |
} |
389 |
|
390 |
|
391 |
|
392 |
} |
393 |
|
394 |
/// <summary> |
395 |
/// |
396 |
/// </summary> |
397 |
/// <param name="container"></param> |
398 |
/// <param name="objectName"></param> |
399 |
/// <param name="fileName"></param> |
400 |
/// <param name="hash">Optional hash value for the file. If no hash is provided, the method calculates a new hash</param> |
401 |
/// <remarks>>This method should have no timeout or a very long one</remarks> |
402 |
public Task PutObject(string container, string objectName, string fileName, string hash = null) |
403 |
{ |
404 |
if (String.IsNullOrWhiteSpace(container)) |
405 |
throw new ArgumentNullException("container", "The container property can't be empty"); |
406 |
if (String.IsNullOrWhiteSpace(objectName)) |
407 |
throw new ArgumentNullException("objectName", "The objectName property can't be empty"); |
408 |
if (String.IsNullOrWhiteSpace(fileName)) |
409 |
throw new ArgumentNullException("fileName", "The fileName property can't be empty"); |
410 |
if (!File.Exists(fileName)) |
411 |
throw new FileNotFoundException("The file does not exist",fileName); |
412 |
|
413 |
|
414 |
try |
415 |
{ |
416 |
var url = String.Join("/",_client.BaseAddress,container,objectName); |
417 |
var uri = new Uri(url); |
418 |
|
419 |
var client = new PithosClient(_client){Timeout=0}; |
420 |
string etag = hash ?? CalculateHash(fileName); |
421 |
|
422 |
client.Headers.Add("Content-Type", "application/octet-stream"); |
423 |
client.Headers.Add("ETag", etag); |
424 |
|
425 |
|
426 |
Trace.TraceInformation("[PUT] START {0}", objectName); |
427 |
client.UploadProgressChanged += (sender, args) => |
428 |
{ |
429 |
Trace.TraceInformation("[PUT PROGRESS] {0} {1}% {2} of {3}", fileName, args.ProgressPercentage, args.BytesSent, args.TotalBytesToSend); |
430 |
}; |
431 |
|
432 |
return client.UploadFileTask(uri, "PUT", fileName) |
433 |
.ContinueWith(upload=> |
434 |
{ |
435 |
client.Dispose(); |
436 |
|
437 |
if (upload.IsFaulted) |
438 |
{ |
439 |
Trace.TraceError("[PUT] FAIL for {0} with \r{1}",objectName,upload.Exception); |
440 |
} |
441 |
else |
442 |
Trace.TraceInformation("[PUT] END {0}", objectName); |
443 |
}); |
444 |
} |
445 |
catch (Exception exc) |
446 |
{ |
447 |
Trace.TraceError("[PUT] END {0} with {1}", objectName, exc); |
448 |
throw; |
449 |
} |
450 |
|
451 |
} |
452 |
|
453 |
|
454 |
private static string CalculateHash(string fileName) |
455 |
{ |
456 |
string hash; |
457 |
using (var hasher = MD5.Create()) |
458 |
using(var stream=File.OpenRead(fileName)) |
459 |
{ |
460 |
var hashBuilder=new StringBuilder(); |
461 |
foreach (byte b in hasher.ComputeHash(stream)) |
462 |
hashBuilder.Append(b.ToString("x2").ToLower()); |
463 |
hash = hashBuilder.ToString(); |
464 |
} |
465 |
return hash; |
466 |
} |
467 |
|
468 |
public void DeleteObject(string container, string objectName) |
469 |
{ |
470 |
if (String.IsNullOrWhiteSpace(container)) |
471 |
throw new ArgumentNullException("container", "The container property can't be empty"); |
472 |
if (String.IsNullOrWhiteSpace(objectName)) |
473 |
throw new ArgumentNullException("objectName", "The objectName property can't be empty"); |
474 |
|
475 |
_client.DeleteWithRetry(container + "/" + objectName,3); |
476 |
|
477 |
var expectedCodes = new[] { HttpStatusCode.NotFound, HttpStatusCode.NoContent }; |
478 |
if (!expectedCodes.Contains(_client.StatusCode)) |
479 |
throw CreateWebException("DeleteObject", _client.StatusCode); |
480 |
|
481 |
} |
482 |
|
483 |
public void MoveObject(string sourceContainer, string oldObjectName, string targetContainer,string newObjectName) |
484 |
{ |
485 |
if (String.IsNullOrWhiteSpace(sourceContainer)) |
486 |
throw new ArgumentNullException("sourceContainer", "The container property can't be empty"); |
487 |
if (String.IsNullOrWhiteSpace(oldObjectName)) |
488 |
throw new ArgumentNullException("oldObjectName", "The oldObjectName property can't be empty"); |
489 |
if (String.IsNullOrWhiteSpace(targetContainer)) |
490 |
throw new ArgumentNullException("targetContainer", "The container property can't be empty"); |
491 |
if (String.IsNullOrWhiteSpace(newObjectName)) |
492 |
throw new ArgumentNullException("newObjectName", "The newObjectName property can't be empty"); |
493 |
|
494 |
var targetUrl = targetContainer + "/" + newObjectName; |
495 |
var sourceUrl = String.Format("/{0}/{1}", sourceContainer, oldObjectName); |
496 |
|
497 |
var client = new PithosClient(_client); |
498 |
client.Headers.Add("X-Copy-From", sourceUrl); |
499 |
client.PutWithRetry(targetUrl,3); |
500 |
|
501 |
var expectedCodes = new[] { HttpStatusCode.OK ,HttpStatusCode.NoContent ,HttpStatusCode.Created }; |
502 |
if (expectedCodes.Contains(client.StatusCode)) |
503 |
{ |
504 |
this.DeleteObject(sourceContainer,oldObjectName); |
505 |
} |
506 |
else |
507 |
throw CreateWebException("MoveObject", client.StatusCode); |
508 |
} |
509 |
|
510 |
|
511 |
private static WebException CreateWebException(string operation, HttpStatusCode statusCode) |
512 |
{ |
513 |
return new WebException(String.Format("{0} failed with unexpected status code {1}", operation, statusCode)); |
514 |
} |
515 |
|
516 |
|
517 |
} |
518 |
} |