1 // **CloudFilesClient** provides a simple client interface to CloudFiles and Pithos
3 // The class provides methods to upload/download files, delete files, manage containers
7 using System.Collections.Generic;
8 using System.ComponentModel.Composition;
9 using System.Diagnostics;
10 using System.Diagnostics.Contracts;
11 using System.Globalization;
15 using System.Security.Cryptography;
17 using System.Threading.Algorithms;
18 using System.Threading.Tasks;
19 using Newtonsoft.Json;
20 using Pithos.Interfaces;
21 using WebHeaderCollection = System.Net.WebHeaderCollection;
23 namespace Pithos.Network
25 [Export(typeof(ICloudClient))]
26 public class CloudFilesClient:ICloudClient
28 //CloudFilesClient uses *_baseClient* internally to communicate with the server
29 //RestClient provides a REST-friendly interface over the standard WebClient.
30 private RestClient _baseClient;
32 //Some operations can specify a Timeout. The default value of all timeouts is 10 seconds
33 private readonly TimeSpan _shortTimeout = TimeSpan.FromSeconds(10);
35 //Some operations can be retried before failing. The default number of retries is 5
36 private readonly int _retries = 5;
38 //During authentication the client provides a UserName
39 public string UserName { get; set; }
41 //and and ApiKey to the server
42 public string ApiKey { get; set; }
44 //And receives an authentication Token. This token must be provided in ALL other operations,
45 //in the X-Auth-Token header
46 public string Token { get; set; }
48 //The client also receives a StorageUrl after authentication. All subsequent operations must
50 public Uri StorageUrl { get; set; }
52 public Uri Proxy { get; set; }
54 public double DownloadPercentLimit { get; set; }
55 public double UploadPercentLimit { get; set; }
57 public string AuthenticationUrl { get; set; }
60 public string VersionPath
62 get { return UsePithos ? "v1" : "v1.0"; }
65 public bool UsePithos { get; set; }
67 private bool _authenticated = false;
70 public void Authenticate(string userName,string apiKey)
72 Trace.TraceInformation("[AUTHENTICATE] Start for {0}", userName);
73 if (String.IsNullOrWhiteSpace(userName))
74 throw new ArgumentNullException("userName", "The userName property can't be empty");
75 if (String.IsNullOrWhiteSpace(apiKey))
76 throw new ArgumentNullException("apiKey", "The apiKey property can't be empty");
85 using (var authClient = new RestClient{BaseAddress=AuthenticationUrl})
88 authClient.Proxy = new WebProxy(Proxy);
90 authClient.Headers.Add("X-Auth-User", UserName);
91 authClient.Headers.Add("X-Auth-Key", ApiKey);
93 authClient.DownloadStringWithRetry(VersionPath, 3);
95 authClient.AssertStatusOK("Authentication failed");
97 var storageUrl = authClient.GetHeaderValue("X-Storage-Url");
98 if (String.IsNullOrWhiteSpace(storageUrl))
99 throw new InvalidOperationException("Failed to obtain storage url");
100 StorageUrl = new Uri(storageUrl);
102 var token = authClient.GetHeaderValue("X-Auth-Token");
103 if (String.IsNullOrWhiteSpace(token))
104 throw new InvalidOperationException("Failed to obtain token url");
108 _baseClient = new RestClient{
109 BaseAddress = StorageUrl.AbsoluteUri,
113 _baseClient.Proxy = new WebProxy(Proxy);
115 _baseClient.Headers.Add("X-Auth-Token", Token);
117 Trace.TraceInformation("[AUTHENTICATE] End for {0}", userName);
121 public IList<ContainerInfo> ListContainers()
123 using (var client = new RestClient(_baseClient))
125 client.Parameters.Clear();
126 client.Parameters.Add("format", "json");
127 var content = client.DownloadStringWithRetry("", 3);
128 client.AssertStatusOK("List Containers failed");
130 if (client.StatusCode == HttpStatusCode.NoContent)
131 return new List<ContainerInfo>();
132 var infos = JsonConvert.DeserializeObject<IList<ContainerInfo>>(content);
138 //Request listing of all objects in a container modified since a specific time.
139 //If the *since* value is missing, return all objects
140 public IList<ObjectInfo> ListObjects(string container, DateTime? since = null)
142 if (String.IsNullOrWhiteSpace(container))
143 throw new ArgumentNullException("container");
144 Contract.EndContractBlock();
146 Trace.TraceInformation("[START] ListObjects");
148 using (var client = new RestClient(_baseClient))
150 client.Parameters.Clear();
151 client.Parameters.Add("format", "json");
152 client.IfModifiedSince = since;
153 var content = client.DownloadStringWithRetry(container, 3);
155 client.AssertStatusOK("ListObjects failed");
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);
163 Trace.TraceInformation("[END] ListObjects");
170 public IList<ObjectInfo> ListObjects(string container, string folder, DateTime? since = null)
172 if (String.IsNullOrWhiteSpace(container))
173 throw new ArgumentNullException("container");
174 if (String.IsNullOrWhiteSpace(folder))
175 throw new ArgumentNullException("folder");
176 Contract.EndContractBlock();
178 Trace.TraceInformation("[START] ListObjects");
180 using (var client = new RestClient(_baseClient))
182 client.Parameters.Clear();
183 client.Parameters.Add("format", "json");
184 client.Parameters.Add("path", folder);
185 client.IfModifiedSince = since;
186 var content = client.DownloadStringWithRetry(container, 3);
187 client.AssertStatusOK("ListObjects failed");
189 var infos = JsonConvert.DeserializeObject<IList<ObjectInfo>>(content);
191 Trace.TraceInformation("[END] ListObjects");
197 public bool ContainerExists(string container)
199 if (String.IsNullOrWhiteSpace(container))
200 throw new ArgumentNullException("container", "The container property can't be empty");
201 using (var client = new RestClient(_baseClient))
203 client.Parameters.Clear();
204 client.Head(container, 3);
206 switch (client.StatusCode)
208 case HttpStatusCode.OK:
209 case HttpStatusCode.NoContent:
211 case HttpStatusCode.NotFound:
214 throw CreateWebException("ContainerExists", client.StatusCode);
219 public bool ObjectExists(string container,string objectName)
221 if (String.IsNullOrWhiteSpace(container))
222 throw new ArgumentNullException("container", "The container property can't be empty");
223 if (String.IsNullOrWhiteSpace(objectName))
224 throw new ArgumentNullException("objectName", "The objectName property can't be empty");
225 using (var client = new RestClient(_baseClient))
227 client.Parameters.Clear();
228 client.Head(container + "/" + objectName, 3);
230 switch (client.StatusCode)
232 case HttpStatusCode.OK:
233 case HttpStatusCode.NoContent:
235 case HttpStatusCode.NotFound:
238 throw CreateWebException("ObjectExists", client.StatusCode);
244 public ObjectInfo GetObjectInfo(string container, string objectName)
246 if (String.IsNullOrWhiteSpace(container))
247 throw new ArgumentNullException("container", "The container property can't be empty");
248 if (String.IsNullOrWhiteSpace(objectName))
249 throw new ArgumentNullException("objectName", "The objectName property can't be empty");
251 using (var client = new RestClient(_baseClient))
255 client.Parameters.Clear();
257 client.Head(container + "/" + objectName, 3);
260 return ObjectInfo.Empty;
262 switch (client.StatusCode)
264 case HttpStatusCode.OK:
265 case HttpStatusCode.NoContent:
266 var keys = client.ResponseHeaders.AllKeys.AsQueryable();
267 var tags = (from key in keys
268 where key.StartsWith("X-Object-Meta-")
269 let name = key.Substring(14)
270 select new {Name = name, Value = client.ResponseHeaders[name]})
271 .ToDictionary(t => t.Name, t => t.Value);
272 var extensions = (from key in keys
273 where key.StartsWith("X-Object-") && !key.StartsWith("X-Object-Meta-")
274 select new {Name = key, Value = client.ResponseHeaders[key]})
275 .ToDictionary(t => t.Name, t => t.Value);
276 var info = new ObjectInfo
279 Hash = client.GetHeaderValue("ETag"),
280 Content_Type = client.GetHeaderValue("Content-Type"),
282 Last_Modified = client.LastModified,
283 Extensions = extensions
286 case HttpStatusCode.NotFound:
287 return ObjectInfo.Empty;
289 throw new WebException(
290 String.Format("[FAIL] GetObjectInfo for {0} failed with unexpected status code {1}",
291 objectName, client.StatusCode));
295 catch(RetryException)
297 Trace.TraceWarning("[RETRY FAIL] GetObjectInfo for {0} failed.");
298 return ObjectInfo.Empty;
300 catch(WebException e)
303 String.Format("[FAIL] GetObjectInfo for {0} failed with unexpected status code {1}",
304 objectName, client.StatusCode), e);
311 public void CreateFolder(string container, string folder)
313 if (String.IsNullOrWhiteSpace(container))
314 throw new ArgumentNullException("container", "The container property can't be empty");
315 if (String.IsNullOrWhiteSpace(folder))
316 throw new ArgumentNullException("folder", "The folder property can't be empty");
318 var folderUrl=String.Format("{0}/{1}",container,folder);
319 using (var client = new RestClient(_baseClient))
321 client.Parameters.Clear();
322 client.Headers.Add("Content-Type", @"application/directory");
323 client.Headers.Add("Content-Length", "0");
324 client.PutWithRetry(folderUrl, 3);
326 if (client.StatusCode != HttpStatusCode.Created && client.StatusCode != HttpStatusCode.Accepted)
327 throw CreateWebException("CreateFolder", client.StatusCode);
331 public ContainerInfo GetContainerInfo(string container)
333 if (String.IsNullOrWhiteSpace(container))
334 throw new ArgumentNullException("container", "The container property can't be empty");
335 using (var client = new RestClient(_baseClient))
337 client.Head(container);
338 switch (client.StatusCode)
340 case HttpStatusCode.OK:
341 case HttpStatusCode.NoContent:
342 var containerInfo = new ContainerInfo
346 long.Parse(client.GetHeaderValue("X-Container-Object-Count")),
347 Bytes = long.Parse(client.GetHeaderValue("X-Container-Bytes-Used")),
348 BlockHash = client.GetHeaderValue("X-Container-Block-Hash"),
349 BlockSize=int.Parse(client.GetHeaderValue("X-Container-Block-Size"))
351 return containerInfo;
352 case HttpStatusCode.NotFound:
353 return ContainerInfo.Empty;
355 throw CreateWebException("GetContainerInfo", client.StatusCode);
360 public void CreateContainer(string container)
362 if (String.IsNullOrWhiteSpace(container))
363 throw new ArgumentNullException("container", "The container property can't be empty");
364 using (var client = new RestClient(_baseClient))
366 client.PutWithRetry(container, 3);
367 var expectedCodes = new[] {HttpStatusCode.Created, HttpStatusCode.Accepted, HttpStatusCode.OK};
368 if (!expectedCodes.Contains(client.StatusCode))
369 throw CreateWebException("CreateContainer", client.StatusCode);
373 public void DeleteContainer(string container)
375 if (String.IsNullOrWhiteSpace(container))
376 throw new ArgumentNullException("container", "The container property can't be empty");
377 using (var client = new RestClient(_baseClient))
379 client.DeleteWithRetry(container, 3);
380 var expectedCodes = new[] {HttpStatusCode.NotFound, HttpStatusCode.NoContent};
381 if (!expectedCodes.Contains(client.StatusCode))
382 throw CreateWebException("DeleteContainer", client.StatusCode);
390 /// <param name="container"></param>
391 /// <param name="objectName"></param>
392 /// <param name="fileName"></param>
393 /// <returns></returns>
394 /// <remarks>This method should have no timeout or a very long one</remarks>
395 //Asynchronously download the object specified by *objectName* in a specific *container* to
397 public Task GetObject(string container, string objectName, string fileName)
399 if (String.IsNullOrWhiteSpace(container))
400 throw new ArgumentNullException("container", "The container property can't be empty");
401 if (String.IsNullOrWhiteSpace(objectName))
402 throw new ArgumentNullException("objectName", "The objectName property can't be empty");
403 Contract.EndContractBlock();
407 //The container and objectName are relative names. They are joined with the client's
408 //BaseAddress to create the object's absolute address
409 var builder = GetAddressBuilder(container, objectName);
410 var uri = builder.Uri;
411 //WebClient, and by extension RestClient, are not thread-safe. Create a new RestClient
412 //object to avoid concurrency errors.
414 //Download operations take a long time therefore they have no timeout.
415 var client = new RestClient(_baseClient) { Timeout = 0 };
417 //Download progress is reported to the Trace log
418 Trace.TraceInformation("[GET] START {0}", objectName);
419 client.DownloadProgressChanged += (sender, args) =>
420 Trace.TraceInformation("[GET PROGRESS] {0} {1}% {2} of {3}",
421 fileName, args.ProgressPercentage,
423 args.TotalBytesToReceive);
426 //Start downloading the object asynchronously
427 var downloadTask = client.DownloadFileTask(uri, fileName);
429 //Once the download completes
430 return downloadTask.ContinueWith(download =>
432 //Delete the local client object
434 //And report failure or completion
435 if (download.IsFaulted)
437 Trace.TraceError("[GET] FAIL for {0} with \r{1}", objectName,
442 Trace.TraceInformation("[GET] END {0}", objectName);
446 catch (Exception exc)
448 Trace.TraceError("[GET] END {0} with {1}", objectName, exc);
456 public Task<IList<string>> PutHashMap(string container, string objectName, TreeHash hash)
458 if (String.IsNullOrWhiteSpace(container))
459 throw new ArgumentNullException("container");
460 if (String.IsNullOrWhiteSpace(objectName))
461 throw new ArgumentNullException("objectName");
463 throw new ArgumentNullException("hash");
464 if (String.IsNullOrWhiteSpace(Token))
465 throw new InvalidOperationException("Invalid Token");
466 if (StorageUrl == null)
467 throw new InvalidOperationException("Invalid Storage Url");
468 Contract.EndContractBlock();
469 //The container and objectName are relative names. They are joined with the client's
470 //BaseAddress to create the object's absolute address
471 var builder = GetAddressBuilder(container, objectName);
472 builder.Query = "format=json&hashmap";
473 var uri = builder.Uri;
475 //Don't use a timeout because putting the hashmap may be a long process
476 var client = new RestClient(_baseClient) { Timeout = 0 };
478 //Send the tree hash as Json to the server
479 client.Headers[HttpRequestHeader.ContentType] = "application/octet-stream";
480 var uploadTask=client.UploadStringTask(uri, "PUT", hash.ToJson());
483 return uploadTask.ContinueWith(t =>
486 var empty = (IList<string>)new List<string>();
489 //The server will respond either with 201-created if all blocks were already on the server
490 if (client.StatusCode == HttpStatusCode.Created)
492 //in which case we return an empty hash list
495 //or with a 409-conflict and return the list of missing parts
496 //A 409 will cause an exception so we need to check t.IsFaulted to avoid propagating the exception
499 var ex = t.Exception.InnerException;
500 var we = ex as WebException;
501 var response = we.Response as HttpWebResponse;
502 if (response!=null && response.StatusCode==HttpStatusCode.Conflict)
504 //In case of 409 the missing parts will be in the response content
505 using (var stream = response.GetResponseStream())
506 using(var reader=new StreamReader(stream))
508 //We need to cleanup the content before returning it because it contains
509 //error content after the list of hashes
510 var hashes = new List<string>();
512 //All lines up to the first empty line are hashes
513 while(!String.IsNullOrWhiteSpace(line=reader.ReadLine()))
522 //Any other status code is unexpected and the exception should be rethrown
526 //Any other status code is unexpected but there was no exception. We can probably continue processing
529 Trace.TraceWarning("Unexcpected status code when putting map: {0} - {1}",client.StatusCode,client.StatusDescription);
536 public Task<byte[]> GetBlock(string container, Uri relativeUrl, long start, long? end=null)
538 if (String.IsNullOrWhiteSpace(Token))
539 throw new InvalidOperationException("Invalid Token");
540 if (StorageUrl == null)
541 throw new InvalidOperationException("Invalid Storage Url");
542 if (String.IsNullOrWhiteSpace(container))
543 throw new ArgumentNullException("container");
544 if (relativeUrl== null)
545 throw new ArgumentNullException("relativeUrl");
546 if (end.HasValue && end<0)
547 throw new ArgumentOutOfRangeException("end");
549 throw new ArgumentOutOfRangeException("start");
550 Contract.EndContractBlock();
552 var builder = GetAddressBuilder(container, relativeUrl.ToString());
554 var uri = builder.Uri;
556 //Don't use a timeout because putting the hashmap may be a long process
557 var client = new RestClient(_baseClient) {Timeout = 0, RangeFrom = start, RangeTo = end};
558 return client.DownloadDataTask(uri)
567 public Task PostBlock(string container,byte[] block,int offset,int count)
569 if (String.IsNullOrWhiteSpace(container))
570 throw new ArgumentNullException("container");
572 throw new ArgumentNullException("block");
573 if (offset < 0 || offset >= block.Length)
574 throw new ArgumentOutOfRangeException("offset");
575 if (count < 0 || count > block.Length)
576 throw new ArgumentOutOfRangeException("count");
577 if (String.IsNullOrWhiteSpace(Token))
578 throw new InvalidOperationException("Invalid Token");
579 if (StorageUrl == null)
580 throw new InvalidOperationException("Invalid Storage Url");
581 Contract.EndContractBlock();
583 var builder = GetAddressBuilder(container, "");
584 //We are doing an update
585 builder.Query = "update";
586 var uri = builder.Uri;
588 //Don't use a timeout because putting the hashmap may be a long process
589 var client = new RestClient(_baseClient) { Timeout = 0 };
590 client.Headers[HttpRequestHeader.ContentType] = "application/octet-stream";
592 Trace.TraceInformation("[BLOCK POST] START");
594 client.UploadProgressChanged += (sender, args) =>
595 Trace.TraceInformation("[BLOCK POST PROGRESS] {0}% {1} of {2}",
596 args.ProgressPercentage, args.BytesSent,
597 args.TotalBytesToSend);
598 client.UploadFileCompleted += (sender, args) =>
599 Trace.TraceInformation("[BLOCK POST PROGRESS] Completed ");
603 var uploadTask = client.UploadDataTask(uri, "POST", block)
604 .ContinueWith(upload =>
608 if (upload.IsFaulted)
610 var exception = upload.Exception.InnerException;
611 Trace.TraceError("[BLOCK POST] FAIL with \r{0}", exception);
615 Trace.TraceInformation("[BLOCK POST] END");
621 public Task<TreeHash> GetHashMap(string container, string objectName)
623 if (String.IsNullOrWhiteSpace(container))
624 throw new ArgumentNullException("container");
625 if (String.IsNullOrWhiteSpace(objectName))
626 throw new ArgumentNullException("objectName");
627 if (String.IsNullOrWhiteSpace(Token))
628 throw new InvalidOperationException("Invalid Token");
629 if (StorageUrl == null)
630 throw new InvalidOperationException("Invalid Storage Url");
631 Contract.EndContractBlock();
635 //The container and objectName are relative names. They are joined with the client's
636 //BaseAddress to create the object's absolute address
637 var builder = GetAddressBuilder(container, objectName);
638 builder.Query="format=json&hashmap";
639 var uri = builder.Uri;
640 //WebClient, and by extension RestClient, are not thread-safe. Create a new RestClient
641 //object to avoid concurrency errors.
643 //Download operations take a long time therefore they have no timeout.
644 //TODO: Do we really? this is a hashmap operation, not a download
645 var client = new RestClient(_baseClient) { Timeout = 0 };
648 //Start downloading the object asynchronously
649 var downloadTask = client.DownloadStringTask(uri);
651 //Once the download completes
652 return downloadTask.ContinueWith(download =>
654 //Delete the local client object
656 //And report failure or completion
657 if (download.IsFaulted)
659 Trace.TraceError("[GET HASH] FAIL for {0} with \r{1}", objectName,
661 throw download.Exception;
664 //The server will return an empty string if the file is empty
665 var json = download.Result;
666 var treeHash = TreeHash.Parse(json);
667 Trace.TraceInformation("[GET HASH] END {0}", objectName);
671 catch (Exception exc)
673 Trace.TraceError("[GET HASH] END {0} with {1}", objectName, exc);
681 private UriBuilder GetAddressBuilder(string container, string objectName)
683 var builder = new UriBuilder(String.Join("/", _baseClient.BaseAddress, container, objectName));
691 /// <param name="container"></param>
692 /// <param name="objectName"></param>
693 /// <param name="fileName"></param>
694 /// <param name="hash">Optional hash value for the file. If no hash is provided, the method calculates a new hash</param>
695 /// <remarks>>This method should have no timeout or a very long one</remarks>
696 public Task PutObject(string container, string objectName, string fileName, string hash = null)
698 if (String.IsNullOrWhiteSpace(container))
699 throw new ArgumentNullException("container", "The container property can't be empty");
700 if (String.IsNullOrWhiteSpace(objectName))
701 throw new ArgumentNullException("objectName", "The objectName property can't be empty");
702 if (String.IsNullOrWhiteSpace(fileName))
703 throw new ArgumentNullException("fileName", "The fileName property can't be empty");
704 if (!File.Exists(fileName))
705 throw new FileNotFoundException("The file does not exist",fileName);
710 var builder= GetAddressBuilder(container,objectName);
711 var uri = builder.Uri;
713 var client = new RestClient(_baseClient){Timeout=0};
714 string etag = hash ?? CalculateHash(fileName);
716 client.Headers.Add("Content-Type", "application/octet-stream");
717 client.Headers.Add("ETag", etag);
720 Trace.TraceInformation("[PUT] START {0}", objectName);
721 client.UploadProgressChanged += (sender, args) =>
723 Trace.TraceInformation("[PUT PROGRESS] {0} {1}% {2} of {3}", fileName, args.ProgressPercentage, args.BytesSent, args.TotalBytesToSend);
726 client.UploadFileCompleted += (sender, args) =>
728 Trace.TraceInformation("[PUT PROGRESS] Completed {0}", fileName);
730 return client.UploadFileTask(uri, "PUT", fileName)
731 .ContinueWith(upload=>
735 if (upload.IsFaulted)
737 var exc = upload.Exception.InnerException;
738 Trace.TraceError("[PUT] FAIL for {0} with \r{1}",objectName,exc);
742 Trace.TraceInformation("[PUT] END {0}", objectName);
745 catch (Exception exc)
747 Trace.TraceError("[PUT] END {0} with {1}", objectName, exc);
754 private static string CalculateHash(string fileName)
757 using (var hasher = MD5.Create())
758 using(var stream=File.OpenRead(fileName))
760 var hashBuilder=new StringBuilder();
761 foreach (byte b in hasher.ComputeHash(stream))
762 hashBuilder.Append(b.ToString("x2").ToLower());
763 hash = hashBuilder.ToString();
768 public void DeleteObject(string container, string objectName)
770 if (String.IsNullOrWhiteSpace(container))
771 throw new ArgumentNullException("container", "The container property can't be empty");
772 if (String.IsNullOrWhiteSpace(objectName))
773 throw new ArgumentNullException("objectName", "The objectName property can't be empty");
774 using (var client = new RestClient(_baseClient))
777 client.DeleteWithRetry(container + "/" + objectName, 3);
779 var expectedCodes = new[] {HttpStatusCode.NotFound, HttpStatusCode.NoContent};
780 if (!expectedCodes.Contains(client.StatusCode))
781 throw CreateWebException("DeleteObject", client.StatusCode);
786 public void MoveObject(string sourceContainer, string oldObjectName, string targetContainer,string newObjectName)
788 if (String.IsNullOrWhiteSpace(sourceContainer))
789 throw new ArgumentNullException("sourceContainer", "The container property can't be empty");
790 if (String.IsNullOrWhiteSpace(oldObjectName))
791 throw new ArgumentNullException("oldObjectName", "The oldObjectName property can't be empty");
792 if (String.IsNullOrWhiteSpace(targetContainer))
793 throw new ArgumentNullException("targetContainer", "The container property can't be empty");
794 if (String.IsNullOrWhiteSpace(newObjectName))
795 throw new ArgumentNullException("newObjectName", "The newObjectName property can't be empty");
797 var targetUrl = targetContainer + "/" + newObjectName;
798 var sourceUrl = String.Format("/{0}/{1}", sourceContainer, oldObjectName);
800 using (var client = new RestClient(_baseClient))
802 client.Headers.Add("X-Move-From", sourceUrl);
803 client.PutWithRetry(targetUrl, 3);
805 var expectedCodes = new[] {HttpStatusCode.OK, HttpStatusCode.NoContent, HttpStatusCode.Created};
806 if (!expectedCodes.Contains(client.StatusCode))
807 throw CreateWebException("MoveObject", client.StatusCode);
811 public void DeleteObject(string sourceContainer, string objectName, string targetContainer)
813 if (String.IsNullOrWhiteSpace(sourceContainer))
814 throw new ArgumentNullException("sourceContainer", "The container property can't be empty");
815 if (String.IsNullOrWhiteSpace(objectName))
816 throw new ArgumentNullException("objectName", "The oldObjectName property can't be empty");
817 if (String.IsNullOrWhiteSpace(targetContainer))
818 throw new ArgumentNullException("targetContainer", "The container property can't be empty");
820 var targetUrl = targetContainer + "/" + objectName;
821 var sourceUrl = String.Format("/{0}/{1}", sourceContainer, objectName);
823 using (var client = new RestClient(_baseClient))
825 client.Headers.Add("X-Move-From", sourceUrl);
826 client.PutWithRetry(targetUrl, 3);
828 var expectedCodes = new[] {HttpStatusCode.OK, HttpStatusCode.NoContent, HttpStatusCode.Created,HttpStatusCode.NotFound};
829 if (!expectedCodes.Contains(client.StatusCode))
830 throw CreateWebException("DeleteObject", client.StatusCode);
835 private static WebException CreateWebException(string operation, HttpStatusCode statusCode)
837 return new WebException(String.Format("{0} failed with unexpected status code {1}", operation, statusCode));