You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

999 lines
41 KiB

  1. /*
  2. * Minimal Object Storage Library, (C) 2015 Minio, Inc.
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. using System;
  17. using System.Collections.Generic;
  18. using System.Net;
  19. using System.Linq;
  20. using RestSharp;
  21. using System.IO;
  22. using Minio.Client.Xml;
  23. using System.Xml.Serialization;
  24. using System.Xml.Linq;
  25. using Minio.Client.Errors;
  26. namespace Minio.Client
  27. {
  28. public class ObjectStorageClient
  29. {
  30. private static int PART_SIZE = 5 * 1024 * 1024;
  31. private RestClient client;
  32. private string region;
  33. private string SystemUserAgent = "minio-cs/0.0.1 (Windows 8.1; x86_64)";
  34. private string CustomUserAgent = "";
  35. private string FullUserAgent
  36. {
  37. get
  38. {
  39. return SystemUserAgent + " " + CustomUserAgent;
  40. }
  41. }
  42. internal ObjectStorageClient(Uri uri, string accessKey, string secretKey)
  43. {
  44. this.client = new RestClient(uri);
  45. this.region = Regions.GetRegion(uri.Host);
  46. this.client.UserAgent = this.FullUserAgent;
  47. if (accessKey != null && secretKey != null)
  48. {
  49. this.client.Authenticator = new V4Authenticator(accessKey, secretKey);
  50. }
  51. }
  52. /// <summary>
  53. /// Creates and returns an object storage client.
  54. /// </summary>
  55. /// <param name="uri">Location of the server, supports HTTP and HTTPS.</param>
  56. /// <returns>Object Storage Client with the uri set as the server location.</returns>
  57. public static ObjectStorageClient GetClient(Uri uri)
  58. {
  59. return GetClient(uri, null, null);
  60. }
  61. /// <summary>
  62. /// Creates and returns an object storage client
  63. /// </summary>
  64. /// <param name="uri">Location of the server, supports HTTP and HTTPS</param>
  65. /// <param name="accessKey">Access Key for authenticated requests</param>
  66. /// <param name="secretKey">Secret Key for authenticated requests</param>
  67. /// <returns>Object Storage Client with the uri set as the server location and authentication parameters set.</returns>
  68. public static ObjectStorageClient GetClient(Uri uri, string accessKey, string secretKey)
  69. {
  70. if (uri == null)
  71. {
  72. throw new NullReferenceException();
  73. }
  74. if (!(uri.Scheme == "http" || uri.Scheme == "https"))
  75. {
  76. throw new UriFormatException("Expecting http or https");
  77. }
  78. if (uri.Query.Length != 0)
  79. {
  80. throw new UriFormatException("Expecting no query");
  81. }
  82. if (uri.AbsolutePath.Length == 0 || (uri.AbsolutePath.Length == 1 && uri.AbsolutePath[0] == '/'))
  83. {
  84. String path = uri.Scheme + "://" + uri.Host + ":" + uri.Port + "/";
  85. return new ObjectStorageClient(new Uri(path), accessKey, secretKey);
  86. }
  87. throw new UriFormatException("Expecting AbsolutePath to be empty");
  88. }
  89. /// <summary>
  90. /// Creates and returns an object storage client
  91. /// </summary>
  92. /// <param name="uri">Location of the server, supports HTTP and HTTPS</param>
  93. /// <returns>Object Storage Client with the uri set as the server location and authentication parameters set.</returns>
  94. public static ObjectStorageClient GetClient(string url)
  95. {
  96. return GetClient(url, null, null);
  97. }
  98. /// <summary>
  99. /// Creates and returns an object storage client
  100. /// </summary>
  101. /// <param name="uri">Location of the server, supports HTTP and HTTPS</param>
  102. /// <param name="accessKey">Access Key for authenticated requests</param>
  103. /// <param name="secretKey">Secret Key for authenticated requests</param>
  104. /// <returns>Object Storage Client with the uri set as the server location and authentication parameters set.</returns>
  105. public static ObjectStorageClient GetClient(string url, string accessKey, string secretKey)
  106. {
  107. Uri uri = new Uri(url);
  108. return GetClient(uri, accessKey, secretKey);
  109. }
  110. public void SetUserAgent(string product, string version, IEnumerable<string> attributes)
  111. {
  112. if (string.IsNullOrEmpty(product))
  113. {
  114. throw new ArgumentException("product cannot be null or empty");
  115. }
  116. if (string.IsNullOrEmpty(version))
  117. {
  118. throw new ArgumentException("version cannot be null or empty");
  119. }
  120. string customAgent = product + "/" + version;
  121. string[] attributesArray = attributes.ToArray();
  122. if (attributes.Count() > 0)
  123. {
  124. customAgent += "(";
  125. customAgent += string.Join("; ", attributesArray);
  126. customAgent += ")";
  127. }
  128. this.CustomUserAgent = customAgent;
  129. this.client.UserAgent = this.FullUserAgent;
  130. this.client.FollowRedirects = false;
  131. }
  132. /// <summary>
  133. /// Returns true if the specified bucket exists, otherwise returns false.
  134. /// </summary>
  135. /// <param name="bucket">Bucket to test existence of</param>
  136. /// <returns>true if exists and user has access</returns>
  137. public bool BucketExists(string bucket)
  138. {
  139. var request = new RestRequest(bucket, Method.HEAD);
  140. var response = client.Execute(request);
  141. if (response.StatusCode == HttpStatusCode.OK)
  142. {
  143. return true;
  144. }
  145. return false;
  146. }
  147. /// <summary>
  148. /// Create a bucket with a given name and canned ACL
  149. /// </summary>
  150. /// <param name="bucket">Name of the new bucket</param>
  151. /// <param name="acl">Canned ACL to set</param>
  152. public void MakeBucket(string bucket, Acl acl)
  153. {
  154. var request = new RestRequest("/" + bucket, Method.PUT);
  155. CreateBucketConfiguration config = new CreateBucketConfiguration()
  156. {
  157. LocationConstraint = this.region
  158. };
  159. request.AddHeader("x-amz-acl", acl.ToString());
  160. request.AddBody(config);
  161. var response = client.Execute(request);
  162. if (response.StatusCode == HttpStatusCode.OK)
  163. {
  164. return;
  165. }
  166. throw ParseError(response);
  167. }
  168. /// <summary>
  169. /// Create a private bucket with a give name.
  170. /// </summary>
  171. /// <param name="bucket">Name of the new bucket</param>
  172. public void MakeBucket(string bucket)
  173. {
  174. this.MakeBucket(bucket, Acl.Private);
  175. }
  176. /// <summary>
  177. /// Remove a bucket
  178. /// </summary>
  179. /// <param name="bucket">Name of bucket to remove</param>
  180. public void RemoveBucket(string bucket)
  181. {
  182. var request = new RestRequest(bucket, Method.DELETE);
  183. var response = client.Execute(request);
  184. if (!response.StatusCode.Equals(HttpStatusCode.NoContent))
  185. {
  186. throw ParseError(response);
  187. }
  188. }
  189. /// <summary>
  190. /// Get bucket ACL
  191. /// </summary>
  192. /// <param name="bucket">NAme of bucket to retrieve canned ACL</param>
  193. /// <returns>Canned ACL</returns>
  194. public Acl GetBucketAcl(string bucket)
  195. {
  196. var request = new RestRequest(bucket + "?acl", Method.GET);
  197. var response = client.Execute(request);
  198. if (response.StatusCode == HttpStatusCode.OK)
  199. {
  200. var content = StripXmlnsXsi(response.Content);
  201. var contentBytes = System.Text.Encoding.UTF8.GetBytes(content);
  202. var stream = new MemoryStream(contentBytes);
  203. AccessControlPolicy bucketList = (AccessControlPolicy)(new XmlSerializer(typeof(AccessControlPolicy)).Deserialize(stream));
  204. bool publicRead = false;
  205. bool publicWrite = false;
  206. bool authenticatedRead = false;
  207. foreach (var x in bucketList.Grants)
  208. {
  209. if ("http://acs.amazonaws.com/groups/global/AllUsers".Equals(x.Grantee.URI) && x.Permission.Equals("READ"))
  210. {
  211. publicRead = true;
  212. }
  213. if ("http://acs.amazonaws.com/groups/global/AllUsers".Equals(x.Grantee.URI) && x.Permission.Equals("WRITE"))
  214. {
  215. publicWrite = true;
  216. }
  217. if ("http://acs.amazonaws.com/groups/global/AuthenticatedUsers".Equals(x.Grantee.URI) && x.Permission.Equals("READ"))
  218. {
  219. authenticatedRead = true;
  220. }
  221. }
  222. if (publicRead && publicWrite && !authenticatedRead)
  223. {
  224. return Acl.PublicReadWrite;
  225. }
  226. if (publicRead && !publicWrite && !authenticatedRead)
  227. {
  228. return Acl.PublicRead;
  229. }
  230. if (!publicRead && !publicWrite && authenticatedRead)
  231. {
  232. return Acl.AuthenticatedRead;
  233. }
  234. return Acl.Private;
  235. }
  236. throw ParseError(response);
  237. }
  238. /// <summary>
  239. /// Set a bucket's canned ACL
  240. /// </summary>
  241. /// <param name="bucket">Name of bucket to set canned ACL</param>
  242. /// <param name="acl">Canned ACL to set</param>
  243. public void SetBucketAcl(string bucket, Acl acl)
  244. {
  245. var request = new RestRequest(bucket + "?acl", Method.PUT);
  246. // TODO add acl header
  247. request.AddHeader("x-amz-acl", acl.ToString());
  248. var response = client.Execute(request);
  249. if (!HttpStatusCode.OK.Equals(response.StatusCode))
  250. {
  251. throw ParseError(response);
  252. }
  253. }
  254. /// <summary>
  255. /// Lists all buckets owned by the user
  256. /// </summary>
  257. /// <returns>A list of all buckets owned by the user.</returns>
  258. public List<Bucket> ListBuckets()
  259. {
  260. var request = new RestRequest("/", Method.GET);
  261. var response = client.Execute(request);
  262. if (response.StatusCode == HttpStatusCode.OK)
  263. {
  264. try
  265. {
  266. var contentBytes = System.Text.Encoding.UTF8.GetBytes(response.Content);
  267. var stream = new MemoryStream(contentBytes);
  268. ListAllMyBucketsResult bucketList = (ListAllMyBucketsResult)(new XmlSerializer(typeof(ListAllMyBucketsResult)).Deserialize(stream));
  269. return bucketList.Buckets;
  270. }
  271. catch (InvalidOperationException ex)
  272. {
  273. // unauthed returns html, so we catch xml parse exception and return access denied
  274. throw new AccessDeniedException();
  275. }
  276. }
  277. throw ParseError(response);
  278. }
  279. /// <summary>
  280. /// Get an object. The object will be streamed to the callback given by the user.
  281. /// </summary>
  282. /// <param name="bucket">Bucket to retrieve object from</param>
  283. /// <param name="key">Key of object to retrieve</param>
  284. /// <param name="callback">A stream will be passed to the callback</param>
  285. public void GetObject(string bucket, string key, Action<Stream> callback)
  286. {
  287. RestRequest request = new RestRequest(bucket + "/" + UrlEncode(key), Method.GET);
  288. request.ResponseWriter = callback;
  289. var response = client.Execute(request);
  290. if (response.StatusCode == HttpStatusCode.OK)
  291. {
  292. return;
  293. }
  294. throw ParseError(response);
  295. }
  296. /// <summary>
  297. /// Get an object starting with the byte specified in offset. The object will be streamed to the callback given by the user
  298. /// </summary>
  299. /// <param name="bucket">Bucket to retrieve object from</param>
  300. /// <param name="key">Key of object to retrieve</param>
  301. /// <param name="offset">Number of bytes to skip</param>
  302. /// <param name="callback">A stream will be passed to the callback</param>
  303. public void GetObject(string bucket, string key, ulong offset, Action<Stream> callback)
  304. {
  305. var stat = this.StatObject(bucket, key);
  306. RestRequest request = new RestRequest(bucket + "/" + UrlEncode(key), Method.GET);
  307. request.AddHeader("Range", "bytes=" + offset + "-" + (stat.Size - 1));
  308. request.ResponseWriter = callback;
  309. client.Execute(request);
  310. // response status code is 0, bug in upstream library, cannot rely on it for errors with PartialContent
  311. }
  312. /// <summary>
  313. /// Get a byte range of an object given by the offset and length. The object will be streamed to the callback given by the user
  314. /// </summary>
  315. /// <param name="bucket">Bucket to retrieve object from</param>
  316. /// <param name="key">Key of object to retrieve</param>
  317. /// <param name="offset">Number of bytes to skip</param>
  318. /// <param name="length">Length of requested byte range</param>
  319. /// <param name="callback">A stream will be passed to the callback</param>
  320. public void GetObject(string bucket, string key, ulong offset, ulong length, Action<Stream> callback)
  321. {
  322. RestRequest request = new RestRequest(bucket + "/" + UrlEncode(key), Method.GET);
  323. request.AddHeader("Range", "bytes=" + offset + "-" + (offset + length - 1));
  324. request.ResponseWriter = callback;
  325. client.Execute(request);
  326. // response status code is 0, bug in upstream library, cannot rely on it for errors with PartialContent
  327. }
  328. /// <summary>
  329. /// Tests the object's existence and returns metadata about existing objects.
  330. /// </summary>
  331. /// <param name="bucket">Bucket to test object in</param>
  332. /// <param name="key">Key of object to stat</param>
  333. /// <returns>Facts about the object</returns>
  334. public ObjectStat StatObject(string bucket, string key)
  335. {
  336. var request = new RestRequest(bucket + "/" + UrlEncode(key), Method.HEAD);
  337. var response = client.Execute(request);
  338. if (response.StatusCode == HttpStatusCode.OK)
  339. {
  340. long size = 0;
  341. DateTime lastModified = new DateTime();
  342. string etag = "";
  343. foreach (Parameter parameter in response.Headers)
  344. {
  345. if (parameter.Name == "Content-Length")
  346. {
  347. size = long.Parse(parameter.Value.ToString());
  348. }
  349. if (parameter.Name == "Last-Modified")
  350. {
  351. DateTime.Parse(parameter.Value.ToString());
  352. }
  353. if (parameter.Name == "ETag")
  354. {
  355. etag = parameter.Value.ToString().Replace("\"", "");
  356. }
  357. }
  358. return new ObjectStat(key, size, lastModified, etag);
  359. }
  360. throw ParseError(response);
  361. }
  362. /// <summary>
  363. /// Creates an object
  364. /// </summary>
  365. /// <param name="bucket">Bucket to create object in</param>
  366. /// <param name="key">Key of the new object</param>
  367. /// <param name="size">Total size of bytes to be written, must match with data's length</param>
  368. /// <param name="contentType">Content type of the new object, null defaults to "application/octet-stream"</param>
  369. /// <param name="data">Stream of bytes to send</param>
  370. public void PutObject(string bucket, string key, long size, string contentType, Stream data)
  371. {
  372. if (size <= ObjectStorageClient.PART_SIZE)
  373. {
  374. var bytes = ReadFull(data, (int)size);
  375. if (data.ReadByte() > 0)
  376. {
  377. throw new DataSizeMismatchException();
  378. }
  379. if (bytes.Length != (int)size)
  380. {
  381. throw new DataSizeMismatchException()
  382. {
  383. Bucket = bucket,
  384. Key = key,
  385. UserSpecifiedSize = size,
  386. ActualReadSize = bytes.Length
  387. };
  388. }
  389. this.DoPutObject(bucket, key, null, 0, contentType, bytes);
  390. }
  391. else
  392. {
  393. var partSize = CalculatePartSize(size);
  394. var uploads = this.ListAllIncompleteUploads(bucket, key);
  395. string uploadId = null;
  396. Dictionary<int, string> etags = new Dictionary<int, string>();
  397. if (uploads.Count() > 0)
  398. {
  399. uploadId = uploads.Last().UploadId;
  400. var parts = this.ListParts(bucket, key, uploadId);
  401. foreach (Part part in parts)
  402. {
  403. etags[part.PartNumber] = part.ETag;
  404. }
  405. }
  406. if (uploadId == null)
  407. {
  408. uploadId = this.NewMultipartUpload(bucket, key);
  409. }
  410. int partNumber = 0;
  411. long totalWritten = 0;
  412. while (totalWritten < size)
  413. {
  414. partNumber++;
  415. var currentPartSize = (int)Math.Min((long)partSize, (size - totalWritten));
  416. byte[] dataToCopy = ReadFull(data, currentPartSize);
  417. if (dataToCopy == null)
  418. {
  419. break;
  420. }
  421. System.Security.Cryptography.MD5 md5 = System.Security.Cryptography.MD5.Create();
  422. byte[] hash = md5.ComputeHash(dataToCopy);
  423. string etag = BitConverter.ToString(hash).Replace("-", string.Empty).ToLower();
  424. if (!etags.ContainsKey(partNumber) || !etags[partNumber].Equals(etag))
  425. {
  426. etag = DoPutObject(bucket, key, uploadId, partNumber, contentType, dataToCopy);
  427. }
  428. etags[partNumber] = etag;
  429. totalWritten += dataToCopy.Length;
  430. }
  431. // test if any more data is on the stream
  432. if (data.ReadByte() != -1)
  433. {
  434. throw new DataSizeMismatchException()
  435. {
  436. Bucket = bucket,
  437. Key = key,
  438. UserSpecifiedSize = size,
  439. ActualReadSize = totalWritten + 1
  440. };
  441. }
  442. if (totalWritten != size)
  443. {
  444. throw new DataSizeMismatchException()
  445. {
  446. Bucket = bucket,
  447. Key = key,
  448. UserSpecifiedSize = size,
  449. ActualReadSize = totalWritten
  450. };
  451. }
  452. foreach (int curPartNumber in etags.Keys)
  453. {
  454. if (curPartNumber > partNumber)
  455. {
  456. etags.Remove(curPartNumber);
  457. }
  458. }
  459. this.CompleteMultipartUpload(bucket, key, uploadId, etags);
  460. }
  461. }
  462. private byte[] ReadFull(Stream data, int currentPartSize)
  463. {
  464. byte[] result = new byte[currentPartSize];
  465. int totalRead = 0;
  466. while (totalRead < currentPartSize)
  467. {
  468. byte[] curData = new byte[currentPartSize - totalRead];
  469. int curRead = data.Read(curData, 0, currentPartSize - totalRead);
  470. if (curRead == 0)
  471. {
  472. break;
  473. }
  474. for (int i = 0; i < curRead; i++)
  475. {
  476. result[totalRead + i] = curData[i];
  477. }
  478. totalRead += curRead;
  479. }
  480. if (totalRead == 0) return null;
  481. if (totalRead == currentPartSize) return result;
  482. byte[] truncatedResult = new byte[totalRead];
  483. for (int i = 0; i < totalRead; i++)
  484. {
  485. truncatedResult[i] = result[i];
  486. }
  487. return truncatedResult;
  488. }
  489. private void CompleteMultipartUpload(string bucket, string key, string uploadId, Dictionary<int, string> etags)
  490. {
  491. var path = bucket + "/" + UrlEncode(key) + "?uploadId=" + uploadId;
  492. var request = new RestRequest(path, Method.POST);
  493. List<XElement> parts = new List<XElement>();
  494. for (int i = 1; i <= etags.Count; i++)
  495. {
  496. parts.Add(new XElement("Part",
  497. new XElement("PartNumber", i),
  498. new XElement("ETag", etags[i])));
  499. }
  500. var completeMultipartUploadXml = new XElement("CompleteMultipartUpload", parts);
  501. var bodyString = completeMultipartUploadXml.ToString();
  502. var body = System.Text.Encoding.UTF8.GetBytes(bodyString);
  503. request.AddParameter("application/xml", body, RestSharp.ParameterType.RequestBody);
  504. var response = client.Execute(request);
  505. if (response.StatusCode.Equals(HttpStatusCode.OK))
  506. {
  507. return;
  508. }
  509. throw ParseError(response);
  510. }
  511. private int CalculatePartSize(long size)
  512. {
  513. int minimumPartSize = PART_SIZE; // 5MB
  514. int partSize = (int)(size / 9999); // using 10000 may cause part size to become too small, and not fit the entire object in
  515. return Math.Max(minimumPartSize, partSize);
  516. }
  517. private IEnumerable<Part> ListParts(string bucket, string key, string uploadId)
  518. {
  519. int nextPartNumberMarker = 0;
  520. bool isRunning = true;
  521. while (isRunning)
  522. {
  523. var uploads = GetListParts(bucket, key, uploadId);
  524. foreach (Part part in uploads.Item2)
  525. {
  526. yield return part;
  527. }
  528. nextPartNumberMarker = uploads.Item1.NextPartNumberMarker;
  529. isRunning = uploads.Item1.IsTruncated;
  530. }
  531. }
  532. private Tuple<ListPartsResult, List<Part>> GetListParts(string bucket, string key, string uploadId)
  533. {
  534. var path = bucket + "/" + UrlEncode(key) + "?uploadId=" + uploadId;
  535. var request = new RestRequest(path, Method.GET);
  536. var response = client.Execute(request);
  537. if (response.StatusCode.Equals(HttpStatusCode.OK))
  538. {
  539. var contentBytes = System.Text.Encoding.UTF8.GetBytes(response.Content);
  540. var stream = new MemoryStream(contentBytes);
  541. ListPartsResult listPartsResult = (ListPartsResult)(new XmlSerializer(typeof(ListPartsResult)).Deserialize(stream));
  542. XDocument root = XDocument.Parse(response.Content);
  543. var uploads = (from c in root.Root.Descendants("{http://s3.amazonaws.com/doc/2006-03-01/}Part")
  544. select new Part()
  545. {
  546. PartNumber = int.Parse(c.Element("{http://s3.amazonaws.com/doc/2006-03-01/}PartNumber").Value),
  547. ETag = c.Element("{http://s3.amazonaws.com/doc/2006-03-01/}ETag").Value.Replace("\"", "")
  548. });
  549. return new Tuple<ListPartsResult, List<Part>>(listPartsResult, new List<Part>());
  550. }
  551. throw ParseError(response);
  552. }
  553. private string NewMultipartUpload(string bucket, string key)
  554. {
  555. var path = bucket + "/" + UrlEncode(key) + "?uploads";
  556. var request = new RestRequest(path, Method.POST);
  557. var response = client.Execute(request);
  558. if (response.StatusCode.Equals(HttpStatusCode.OK))
  559. {
  560. var contentBytes = System.Text.Encoding.UTF8.GetBytes(response.Content);
  561. var stream = new MemoryStream(contentBytes);
  562. InitiateMultipartUploadResult newUpload = (InitiateMultipartUploadResult)(new XmlSerializer(typeof(InitiateMultipartUploadResult)).Deserialize(stream));
  563. return newUpload.UploadId;
  564. }
  565. throw ParseError(response);
  566. }
  567. private string DoPutObject(string bucket, string key, string uploadId, int partNumber, string contentType, byte[] data)
  568. {
  569. var path = bucket + "/" + UrlEncode(key);
  570. var queries = new List<string>();
  571. if (uploadId != null)
  572. {
  573. path += "?uploadId=" + uploadId + "&partNumber=" + partNumber;
  574. }
  575. var request = new RestRequest(path, Method.PUT);
  576. if (contentType == null)
  577. {
  578. contentType = "application/octet-stream";
  579. }
  580. request.AddHeader("Content-Type", contentType);
  581. request.AddParameter(contentType, data, RestSharp.ParameterType.RequestBody);
  582. var response = client.Execute(request);
  583. if (response.StatusCode.Equals(HttpStatusCode.OK))
  584. {
  585. string etag = null;
  586. foreach (Parameter parameter in response.Headers)
  587. {
  588. if (parameter.Name == "ETag")
  589. {
  590. etag = parameter.Value.ToString();
  591. }
  592. }
  593. return etag;
  594. }
  595. throw ParseError(response);
  596. }
  597. private ClientException ParseError(IRestResponse response)
  598. {
  599. if (response == null)
  600. {
  601. return new ConnectionException();
  602. }
  603. if (HttpStatusCode.Redirect.Equals(response.StatusCode) || HttpStatusCode.TemporaryRedirect.Equals(response.StatusCode))
  604. {
  605. return new RedirectionException();
  606. }
  607. if (HttpStatusCode.Forbidden.Equals(response.StatusCode) || HttpStatusCode.NotFound.Equals(response.StatusCode))
  608. {
  609. ClientException e = null;
  610. ErrorResponse errorResponse = new ErrorResponse();
  611. foreach (Parameter parameter in response.Headers)
  612. {
  613. if (parameter.Name.Equals("x-amz-id-2", StringComparison.CurrentCultureIgnoreCase))
  614. {
  615. errorResponse.XAmzID2 = parameter.Value.ToString();
  616. }
  617. if (parameter.Name.Equals("x-amz-request-id", StringComparison.CurrentCultureIgnoreCase))
  618. {
  619. errorResponse.RequestID = parameter.Value.ToString();
  620. }
  621. }
  622. errorResponse.Resource = response.Request.Resource;
  623. if (HttpStatusCode.NotFound.Equals(response.StatusCode))
  624. {
  625. int pathLength = response.Request.Resource.Split('/').Count();
  626. if (pathLength > 1)
  627. {
  628. errorResponse.Code = "NoSuchKey";
  629. e = new ObjectNotFoundException();
  630. }
  631. else if (pathLength == 1)
  632. {
  633. errorResponse.Code = "NoSuchBucket";
  634. e = new BucketNotFoundException();
  635. }
  636. else
  637. {
  638. e = new InternalClientException("404 without body resulted in path with less than two components");
  639. }
  640. }
  641. else
  642. {
  643. errorResponse.Code = "Forbidden";
  644. e = new AccessDeniedException();
  645. }
  646. e.Response = errorResponse;
  647. return e;
  648. }
  649. if (string.IsNullOrWhiteSpace(response.Content))
  650. {
  651. throw new InternalClientException("Unsuccessful response from server without XML error: " + response.StatusCode);
  652. }
  653. var contentBytes = System.Text.Encoding.UTF8.GetBytes(response.Content);
  654. var stream = new MemoryStream(contentBytes);
  655. ErrorResponse errResponse = (ErrorResponse)(new XmlSerializer(typeof(ErrorResponse)).Deserialize(stream));
  656. string code = errResponse.Code;
  657. ClientException clientException;
  658. if ("NoSuchBucket".Equals(code)) clientException = new BucketNotFoundException();
  659. else if ("NoSuchKey".Equals(code)) clientException = new ObjectNotFoundException();
  660. else if ("InvalidBucketName".Equals(code)) clientException = new InvalidKeyNameException();
  661. else if ("InvalidObjectName".Equals(code)) clientException = new InvalidKeyNameException();
  662. else if ("AccessDenied".Equals(code)) clientException = new AccessDeniedException();
  663. else if ("BucketAlreadyExists".Equals(code)) clientException = new BucketExistsException();
  664. else if ("ObjectAlreadyExists".Equals(code)) clientException = new ObjectExistsException();
  665. else if ("InternalError".Equals(code)) clientException = new InternalServerException();
  666. else if ("KeyTooLong".Equals(code)) clientException = new InvalidKeyNameException();
  667. else if ("TooManyBuckets".Equals(code)) clientException = new MaxBucketsReachedException();
  668. else if ("PermanentRedirect".Equals(code)) clientException = new RedirectionException();
  669. else if ("MethodNotAllowed".Equals(code)) clientException = new ObjectExistsException();
  670. else if ("BucketAlreadyOwnedByYou".Equals(code)) clientException = new BucketExistsException();
  671. else clientException = new InternalClientException(errResponse.ToString());
  672. clientException.Response = errResponse;
  673. clientException.XmlError = response.Content;
  674. return clientException;
  675. }
  676. private string StripXmlnsXsi(string input)
  677. {
  678. string result = input.Replace("xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:type=\"CanonicalUser\"", "");
  679. result = result.Replace("xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:type=\"Group\"", "");
  680. return result;
  681. }
  682. /// <summary>
  683. /// List all objects in a bucket
  684. /// </summary>
  685. /// <param name="bucket">Bucket to list objects from</param>
  686. /// <returns>An iterator lazily populated with objects</returns>
  687. public IEnumerable<Item> ListObjects(string bucket)
  688. {
  689. return this.ListObjects(bucket, null, true);
  690. }
  691. /// <summary>
  692. /// List all objects in a bucket with a given prefix
  693. /// </summary>
  694. /// <param name="bucket">BUcket to list objects from</param>
  695. /// <param name="prefix">Filters all objects not beginning with a given prefix</param>
  696. /// <returns>An iterator lazily populated with objects</returns>
  697. public IEnumerable<Item> ListObjects(string bucket, string prefix)
  698. {
  699. return this.ListObjects(bucket, prefix, true);
  700. }
  701. /// <summary>
  702. /// List all objects non-recursively in a bucket with a given prefix, optionally emulating a directory
  703. /// </summary>
  704. /// <param name="bucket">Bucket to list objects from</param>
  705. /// <param name="prefix">Filters all objects not beginning with a given prefix</param>
  706. /// <param name="recursive">Set to false to emulate a directory</param>
  707. /// <returns>A iterator lazily populated with objects</returns>
  708. public IEnumerable<Item> ListObjects(string bucket, string prefix, bool recursive)
  709. {
  710. bool isRunning = true;
  711. string marker = null;
  712. while (isRunning)
  713. {
  714. Tuple<ListBucketResult, List<Item>> result = GetObjectList(bucket, prefix, recursive, marker);
  715. Item lastItem = null;
  716. foreach (Item item in result.Item2)
  717. {
  718. lastItem = item;
  719. yield return item;
  720. }
  721. if (result.Item1.NextMarker != null)
  722. {
  723. marker = result.Item1.NextMarker;
  724. }
  725. else
  726. {
  727. marker = lastItem.Key;
  728. }
  729. isRunning = result.Item1.IsTruncated;
  730. }
  731. }
  732. private Tuple<ListBucketResult, List<Item>> GetObjectList(string bucket, string prefix, bool recursive, string marker)
  733. {
  734. var queries = new List<string>();
  735. if (!recursive)
  736. {
  737. queries.Add("delimiter=%2F");
  738. }
  739. if (prefix != null)
  740. {
  741. queries.Add("prefix=" + Uri.EscapeDataString(prefix));
  742. }
  743. if (marker != null)
  744. {
  745. queries.Add("marker=" + marker);
  746. }
  747. string query = string.Join("&", queries);
  748. string path = bucket;
  749. if (query.Length > 0)
  750. {
  751. path += "?" + query;
  752. }
  753. var request = new RestRequest(path, Method.GET);
  754. var response = client.Execute(request);
  755. if (response.StatusCode == HttpStatusCode.OK)
  756. {
  757. var contentBytes = System.Text.Encoding.UTF8.GetBytes(response.Content);
  758. var stream = new MemoryStream(contentBytes);
  759. ListBucketResult listBucketResult = (ListBucketResult)(new XmlSerializer(typeof(ListBucketResult)).Deserialize(stream));
  760. XDocument root = XDocument.Parse(response.Content);
  761. var items = (from c in root.Root.Descendants("{http://s3.amazonaws.com/doc/2006-03-01/}Contents")
  762. select new Item()
  763. {
  764. Key = c.Element("{http://s3.amazonaws.com/doc/2006-03-01/}Key").Value,
  765. LastModified = c.Element("{http://s3.amazonaws.com/doc/2006-03-01/}LastModified").Value,
  766. ETag = c.Element("{http://s3.amazonaws.com/doc/2006-03-01/}ETag").Value,
  767. Size = UInt64.Parse(c.Element("{http://s3.amazonaws.com/doc/2006-03-01/}Size").Value),
  768. IsDir = false
  769. });
  770. var prefixes = (from c in root.Root.Descendants("{http://s3.amazonaws.com/doc/2006-03-01/}CommonPrefixes")
  771. select new Item()
  772. {
  773. Key = c.Element("{http://s3.amazonaws.com/doc/2006-03-01/}Prefix").Value,
  774. IsDir = true
  775. });
  776. items = items.Concat(prefixes);
  777. return new Tuple<ListBucketResult, List<Item>>(listBucketResult, items.ToList());
  778. }
  779. throw ParseError(response);
  780. }
  781. private Tuple<ListMultipartUploadsResult, List<Upload>> GetMultipartUploadsList(string bucket, string prefix, string keyMarker, string uploadIdMarker)
  782. {
  783. var queries = new List<string>();
  784. queries.Add("uploads");
  785. if (prefix != null)
  786. {
  787. queries.Add("prefix=" + Uri.EscapeDataString(prefix));
  788. }
  789. if (keyMarker != null)
  790. {
  791. queries.Add("key-marker=" + Uri.EscapeDataString(keyMarker));
  792. }
  793. if (uploadIdMarker != null)
  794. {
  795. queries.Add("upload-id-marker=" + uploadIdMarker);
  796. }
  797. string query = string.Join("&", queries);
  798. string path = bucket;
  799. path += "?" + query;
  800. var request = new RestRequest(path, Method.GET);
  801. var response = client.Execute(request);
  802. if (response.StatusCode == HttpStatusCode.OK)
  803. {
  804. var contentBytes = System.Text.Encoding.UTF8.GetBytes(response.Content);
  805. var stream = new MemoryStream(contentBytes);
  806. ListMultipartUploadsResult listBucketResult = (ListMultipartUploadsResult)(new XmlSerializer(typeof(ListMultipartUploadsResult)).Deserialize(stream));
  807. XDocument root = XDocument.Parse(response.Content);
  808. var uploads = (from c in root.Root.Descendants("{http://s3.amazonaws.com/doc/2006-03-01/}Upload")
  809. select new Upload()
  810. {
  811. Key = c.Element("{http://s3.amazonaws.com/doc/2006-03-01/}Key").Value,
  812. UploadId = c.Element("{http://s3.amazonaws.com/doc/2006-03-01/}UploadId").Value,
  813. Initiated = c.Element("{http://s3.amazonaws.com/doc/2006-03-01/}Initiated").Value
  814. });
  815. return new Tuple<ListMultipartUploadsResult, List<Upload>>(listBucketResult, uploads.ToList());
  816. }
  817. throw ParseError(response);
  818. }
  819. /// <summary>
  820. /// Lists all incomplete uploads in a given bucket
  821. /// </summary>
  822. /// <param name="bucket">Bucket to list all incomplepte uploads from</param>
  823. /// <returns>A lazily populated list of incomplete uploads</returns>
  824. public IEnumerable<Upload> ListAllIncompleteUploads(string bucket)
  825. {
  826. return this.ListAllIncompleteUploads(bucket, null);
  827. }
  828. /// <summary>
  829. /// Lists all incomplete uploads in a given bucket with a given key
  830. /// </summary>
  831. /// <param name="bucket">Bucket to list incomplete uploads from</param>
  832. /// <param name="key">Key of object to list incomplete uploads from</param>
  833. /// <returns></returns>
  834. public IEnumerable<Upload> ListAllIncompleteUploads(string bucket, string key)
  835. {
  836. string nextKeyMarker = null;
  837. string nextUploadIdMarker = null;
  838. bool isRunning = true;
  839. while (isRunning)
  840. {
  841. var uploads = GetMultipartUploadsList(bucket, key, nextKeyMarker, nextUploadIdMarker);
  842. foreach (Upload upload in uploads.Item2)
  843. {
  844. if (key != null && !key.Equals(upload.Key))
  845. {
  846. continue;
  847. }
  848. yield return upload;
  849. }
  850. nextKeyMarker = uploads.Item1.NextKeyMarker;
  851. nextUploadIdMarker = uploads.Item1.NextUploadIdMarker;
  852. isRunning = uploads.Item1.IsTruncated;
  853. }
  854. }
  855. /// <summary>
  856. /// Drop incomplete uploads from a given bucket and key
  857. /// </summary>
  858. /// <param name="bucket">Bucket to drop incomplete uploads from</param>
  859. /// <param name="key">Key to drop incomplete uploads from</param>
  860. public void DropIncompleteUpload(string bucket, string key)
  861. {
  862. var uploads = this.ListAllIncompleteUploads(bucket, key);
  863. foreach (Upload upload in uploads)
  864. {
  865. this.DropUpload(bucket, key, upload.UploadId);
  866. }
  867. }
  868. /// <summary>
  869. /// Drops all incomplete uploads from a given bucket
  870. /// </summary>
  871. /// <param name="bucket">Bucket to drop all incomplete uploads from</param>
  872. public void DropAllIncompleteUploads(string bucket)
  873. {
  874. var uploads = this.ListAllIncompleteUploads(bucket);
  875. foreach (Upload upload in uploads)
  876. {
  877. this.DropUpload(bucket, upload.Key, upload.UploadId);
  878. }
  879. }
  880. private void DropUpload(string bucket, string key, string uploadId)
  881. {
  882. var path = bucket + "/" + UrlEncode(key) + "?uploadId=" + uploadId;
  883. var request = new RestRequest(path, Method.DELETE);
  884. var response = client.Execute(request);
  885. if (response.StatusCode == HttpStatusCode.NoContent)
  886. {
  887. return;
  888. }
  889. throw ParseError(response);
  890. }
  891. private string UrlEncode(string input)
  892. {
  893. return Uri.EscapeDataString(input).Replace("%2F", "/");
  894. }
  895. }
  896. }