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.

463 lines
22 KiB

  1. /*
  2. * MinIO .NET Library for Amazon S3 Compatible Cloud Storage,
  3. * (C) 2017, 2018, 2019 MinIO, Inc.
  4. *
  5. * Licensed under the Apache License, Version 2.0 (the "License");
  6. * you may not use this file except in compliance with the License.
  7. * You may obtain a copy of the License at
  8. *
  9. * http://www.apache.org/licenses/LICENSE-2.0
  10. *
  11. * Unless required by applicable law or agreed to in writing, software
  12. * distributed under the License is distributed on an "AS IS" BASIS,
  13. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. * See the License for the specific language governing permissions and
  15. * limitations under the License.
  16. */
  17. using Minio.DataModel;
  18. using Minio.Exceptions;
  19. using RestSharp;
  20. using System;
  21. using System.Collections.Generic;
  22. using System.Globalization;
  23. using System.IO;
  24. using System.Linq;
  25. using System.Net;
  26. using System.Reactive.Linq;
  27. using System.Threading;
  28. using System.Threading.Tasks;
  29. using System.Xml.Linq;
  30. using System.Xml.Serialization;
  31. using System.Web;
  32. namespace Minio
  33. {
  34. public partial class MinioClient : IBucketOperations
  35. {
  36. /// <summary>
  37. /// List all objects in a bucket
  38. /// </summary>
  39. /// <param name="cancellationToken">Optional cancellation token to cancel the operation</param>
  40. /// <returns>Task with an iterator lazily populated with objects</returns>
  41. public async Task<ListAllMyBucketsResult> ListBucketsAsync(CancellationToken cancellationToken = default(CancellationToken))
  42. {
  43. var request = await this.CreateRequest(Method.GET, resourcePath: "/").ConfigureAwait(false);
  44. var response = await this.ExecuteTaskAsync(this.NoErrorHandlers, request, cancellationToken).ConfigureAwait(false);
  45. ListAllMyBucketsResult bucketList = new ListAllMyBucketsResult();
  46. if (HttpStatusCode.OK.Equals(response.StatusCode))
  47. {
  48. var contentBytes = System.Text.Encoding.UTF8.GetBytes(response.Content);
  49. using (var stream = new MemoryStream(contentBytes))
  50. bucketList = (ListAllMyBucketsResult)new XmlSerializer(typeof(ListAllMyBucketsResult)).Deserialize(stream);
  51. return bucketList;
  52. }
  53. return bucketList;
  54. }
  55. /// <summary>
  56. /// Create a private bucket with the given name.
  57. /// </summary>
  58. /// <param name="bucketName">Name of the new bucket</param>
  59. /// <param name="location">Region</param>
  60. /// <param name="cancellationToken">Optional cancellation token to cancel the operation</param>
  61. /// <returns> Task </returns>
  62. /// <exception cref="InvalidBucketNameException">When bucketName is null</exception>
  63. public async Task MakeBucketAsync(string bucketName, string location = "us-east-1", CancellationToken cancellationToken = default(CancellationToken))
  64. {
  65. if (bucketName == null)
  66. {
  67. throw new InvalidBucketNameException(bucketName, "bucketName cannot be null");
  68. }
  69. if (location == "us-east-1")
  70. {
  71. if (this.Region != string.Empty)
  72. {
  73. location = this.Region;
  74. }
  75. }
  76. // Set Target URL
  77. Uri requestUrl = RequestUtil.MakeTargetURL(this.BaseUrl, this.Secure, location);
  78. SetTargetURL(requestUrl);
  79. var request = new RestRequest("/" + bucketName, Method.PUT)
  80. {
  81. XmlSerializer = new RestSharp.Serializers.DotNetXmlSerializer(),
  82. RequestFormat = DataFormat.Xml
  83. };
  84. // ``us-east-1`` is not a valid location constraint according to amazon, so we skip it.
  85. if (location != "us-east-1")
  86. {
  87. CreateBucketConfiguration config = new CreateBucketConfiguration(location);
  88. request.AddBody(config);
  89. }
  90. var response = await this.ExecuteTaskAsync(this.NoErrorHandlers, request, cancellationToken).ConfigureAwait(false);
  91. }
  92. /// <summary>
  93. /// Returns true if the specified bucketName exists, otherwise returns false.
  94. /// </summary>
  95. /// <param name="bucketName">Bucket to test existence of</param>
  96. /// <param name="cancellationToken">Optional cancellation token to cancel the operation</param>
  97. /// <returns>Task that returns true if exists and user has access</returns>
  98. public async Task<bool> BucketExistsAsync(string bucketName, CancellationToken cancellationToken = default(CancellationToken))
  99. {
  100. try
  101. {
  102. var request = await this.CreateRequest(Method.HEAD, bucketName).ConfigureAwait(false);
  103. var response = await this.ExecuteTaskAsync(this.NoErrorHandlers, request, cancellationToken).ConfigureAwait(false);
  104. }
  105. catch (Exception ex)
  106. {
  107. if (ex.GetType() == typeof(BucketNotFoundException))
  108. {
  109. return false;
  110. }
  111. throw;
  112. }
  113. return true;
  114. }
  115. /// <summary>
  116. /// Remove a bucket
  117. /// </summary>
  118. /// <param name="bucketName">Name of bucket to remove</param>
  119. /// <param name="cancellationToken">Optional cancellation token to cancel the operation</param>
  120. /// <returns>Task</returns>
  121. public async Task RemoveBucketAsync(string bucketName, CancellationToken cancellationToken = default(CancellationToken))
  122. {
  123. var request = await this.CreateRequest(Method.DELETE, bucketName, resourcePath: null).ConfigureAwait(false);
  124. var response = await this.ExecuteTaskAsync(this.NoErrorHandlers, request, cancellationToken).ConfigureAwait(false);
  125. }
  126. /// <summary>
  127. /// List all objects non-recursively in a bucket with a given prefix, optionally emulating a directory
  128. /// </summary>
  129. /// <param name="bucketName">Bucket to list objects from</param>
  130. /// <param name="prefix">Filters all objects beginning with a given prefix</param>
  131. /// <param name="recursive">Set to true to recursively list all objects</param>
  132. /// <param name="cancellationToken">Optional cancellation token to cancel the operation</param>
  133. /// <returns>An observable of items that client can subscribe to</returns>
  134. public IObservable<Item> ListObjectsAsync(string bucketName, string prefix = null, bool recursive = false, CancellationToken cancellationToken = default(CancellationToken))
  135. {
  136. return Observable.Create<Item>(
  137. async (obs, ct) =>
  138. {
  139. bool isRunning = true;
  140. string marker = null;
  141. var delimiter = "/";
  142. if (recursive)
  143. {
  144. delimiter = string.Empty;
  145. }
  146. using(var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, ct)) {
  147. while (isRunning)
  148. {
  149. Tuple<ListBucketResult, List<Item>> result = await GetObjectListAsync(bucketName, prefix, delimiter, marker, cts.Token).ConfigureAwait(false);
  150. Item lastItem = null;
  151. foreach (Item item in result.Item2)
  152. {
  153. lastItem = item;
  154. if (result.Item1.EncodingType == "url")
  155. {
  156. item.Key = HttpUtility.UrlDecode(item.Key);
  157. }
  158. obs.OnNext(item);
  159. }
  160. if (result.Item1.NextMarker != null)
  161. {
  162. if (result.Item1.EncodingType == "url")
  163. {
  164. marker = HttpUtility.UrlDecode(result.Item1.NextMarker);
  165. }
  166. else
  167. {
  168. marker = result.Item1.NextMarker;
  169. }
  170. }
  171. else if (lastItem != null)
  172. {
  173. if (result.Item1.EncodingType == "url")
  174. {
  175. marker = HttpUtility.UrlDecode(lastItem.Key);
  176. }
  177. else
  178. {
  179. marker = lastItem.Key;
  180. }
  181. }
  182. isRunning = result.Item1.IsTruncated;
  183. cts.Token.ThrowIfCancellationRequested();
  184. }
  185. }
  186. });
  187. }
  188. /// <summary>
  189. /// Gets the list of objects in the bucket filtered by prefix
  190. /// </summary>
  191. /// <param name="bucketName">Bucket to list objects from</param>
  192. /// <param name="prefix">Filters all objects starting with a given prefix</param>
  193. /// <param name="delimiter">Delimit the output upto this character</param>
  194. /// <param name="marker">marks location in the iterator sequence</param>
  195. /// <returns>Task with a tuple populated with objects</returns>
  196. /// <param name="cancellationToken">Optional cancellation token to cancel the operation</param>
  197. private async Task<Tuple<ListBucketResult, List<Item>>> GetObjectListAsync(string bucketName, string prefix, string delimiter, string marker, CancellationToken cancellationToken = default(CancellationToken))
  198. {
  199. var queries = new List<string>();
  200. // null values are treated as empty strings.
  201. if (delimiter == null)
  202. {
  203. delimiter = string.Empty;
  204. }
  205. if (prefix == null)
  206. {
  207. prefix = string.Empty;
  208. }
  209. if (marker == null)
  210. {
  211. marker = string.Empty;
  212. }
  213. queries.Add("delimiter=" + Uri.EscapeDataString(delimiter));
  214. queries.Add("prefix=" + Uri.EscapeDataString(prefix));
  215. queries.Add("max-keys=1000");
  216. queries.Add("marker=" + Uri.EscapeDataString(marker));
  217. queries.Add("encoding-type=url");
  218. string query = string.Join("&", queries);
  219. var request = await this.CreateRequest(Method.GET,
  220. bucketName,
  221. resourcePath: "?" + query)
  222. .ConfigureAwait(false);
  223. var response = await this.ExecuteTaskAsync(this.NoErrorHandlers, request, cancellationToken).ConfigureAwait(false);
  224. var contentBytes = System.Text.Encoding.UTF8.GetBytes(response.Content);
  225. ListBucketResult listBucketResult = null;
  226. using (var stream = new MemoryStream(contentBytes))
  227. {
  228. listBucketResult = (ListBucketResult)new XmlSerializer(typeof(ListBucketResult)).Deserialize(stream);
  229. }
  230. XDocument root = XDocument.Parse(response.Content);
  231. var items = from c in root.Root.Descendants("{http://s3.amazonaws.com/doc/2006-03-01/}Contents")
  232. select new Item
  233. {
  234. Key = c.Element("{http://s3.amazonaws.com/doc/2006-03-01/}Key").Value,
  235. LastModified = c.Element("{http://s3.amazonaws.com/doc/2006-03-01/}LastModified").Value,
  236. ETag = c.Element("{http://s3.amazonaws.com/doc/2006-03-01/}ETag").Value,
  237. Size = ulong.Parse(c.Element("{http://s3.amazonaws.com/doc/2006-03-01/}Size").Value, CultureInfo.CurrentCulture),
  238. IsDir = false
  239. };
  240. var prefixes = from c in root.Root.Descendants("{http://s3.amazonaws.com/doc/2006-03-01/}CommonPrefixes")
  241. select new Item
  242. {
  243. Key = c.Element("{http://s3.amazonaws.com/doc/2006-03-01/}Prefix").Value,
  244. IsDir = true
  245. };
  246. items = items.Concat(prefixes);
  247. return Tuple.Create(listBucketResult, items.ToList());
  248. }
  249. /// <summary>
  250. /// Returns current policy stored on the server for this bucket
  251. /// </summary>
  252. /// <param name="bucketName">Bucket name.</param>
  253. /// <param name="cancellationToken">Optional cancellation token to cancel the operation</param>
  254. /// <returns>Task that returns the Bucket policy as a json string</returns>
  255. public async Task<string> GetPolicyAsync(string bucketName, CancellationToken cancellationToken = default(CancellationToken))
  256. {
  257. IRestResponse response = null;
  258. var path = $"{bucketName}?policy";
  259. var request = await this.CreateRequest(Method.GET, bucketName,
  260. contentType: "application/json",
  261. resourcePath: "?policy")
  262. .ConfigureAwait(false);
  263. string policyString = null;
  264. response = await this.ExecuteTaskAsync(this.NoErrorHandlers, request, cancellationToken).ConfigureAwait(false);
  265. var contentBytes = System.Text.Encoding.UTF8.GetBytes(response.Content);
  266. using (var stream = new MemoryStream(contentBytes))
  267. using (var streamReader = new StreamReader(stream))
  268. {
  269. policyString = await streamReader.ReadToEndAsync().ConfigureAwait(false);
  270. }
  271. return policyString;
  272. }
  273. /// <summary>
  274. /// Sets the current bucket policy
  275. /// </summary>
  276. /// <param name="bucketName">Bucket Name</param>
  277. /// <param name="policyJson">Policy json as string </param>
  278. /// <param name="cancellationToken">Optional cancellation token to cancel the operation</param>
  279. /// <returns>Task to set a policy</returns>
  280. public async Task SetPolicyAsync(string bucketName, string policyJson, CancellationToken cancellationToken = default(CancellationToken))
  281. {
  282. var request = await this.CreateRequest(Method.PUT, bucketName,
  283. resourcePath: "?policy",
  284. contentType: "application/json",
  285. body: policyJson)
  286. .ConfigureAwait(false);
  287. IRestResponse response = await this.ExecuteTaskAsync(this.NoErrorHandlers, request, cancellationToken).ConfigureAwait(false);
  288. }
  289. /// <summary>
  290. /// Gets notification configuration for this bucket
  291. /// </summary>
  292. /// <param name="bucketName">Bucket name</param>
  293. /// <param name="cancellationToken">Optional cancellation token to cancel the operation</param>
  294. /// <returns></returns>
  295. public async Task<BucketNotification> GetBucketNotificationsAsync(string bucketName, CancellationToken cancellationToken = default(CancellationToken))
  296. {
  297. utils.ValidateBucketName(bucketName);
  298. var request = await this.CreateRequest(Method.GET,
  299. bucketName,
  300. resourcePath: "?notification")
  301. .ConfigureAwait(false);
  302. var response = await this.ExecuteTaskAsync(this.NoErrorHandlers, request, cancellationToken).ConfigureAwait(false);
  303. var contentBytes = System.Text.Encoding.UTF8.GetBytes(response.Content);
  304. using (var stream = new MemoryStream(contentBytes))
  305. {
  306. return (BucketNotification)new XmlSerializer(typeof(BucketNotification)).Deserialize(stream);
  307. }
  308. }
  309. /// <summary>
  310. /// Sets the notification configuration for this bucket
  311. /// </summary>
  312. /// <param name="bucketName">Bucket name</param>
  313. /// <param name="notification">Notification object with configuration to be set on the server</param>
  314. /// <param name="cancellationToken">Optional cancellation token to cancel the operation</param>
  315. /// <returns></returns>
  316. public async Task SetBucketNotificationsAsync(string bucketName, BucketNotification notification, CancellationToken cancellationToken = default(CancellationToken))
  317. {
  318. utils.ValidateBucketName(bucketName);
  319. var request = await this.CreateRequest(Method.PUT, bucketName,
  320. resourcePath: "?notification")
  321. .ConfigureAwait(false);
  322. request.XmlSerializer = new RestSharp.Serializers.DotNetXmlSerializer();
  323. request.RequestFormat = DataFormat.Xml;
  324. request.AddBody(notification);
  325. IRestResponse response = await this.ExecuteTaskAsync(this.NoErrorHandlers, request, cancellationToken).ConfigureAwait(false);
  326. }
  327. /// <summary>
  328. /// Removes all bucket notification configurations stored on the server.
  329. /// </summary>
  330. /// <param name="bucketName">Bucket name</param>
  331. /// <param name="cancellationToken">Optional cancellation token to cancel the operation</param>
  332. /// <returns></returns>
  333. public Task RemoveAllBucketNotificationsAsync(string bucketName, CancellationToken cancellationToken = default(CancellationToken))
  334. {
  335. utils.ValidateBucketName(bucketName);
  336. BucketNotification notification = new BucketNotification();
  337. return SetBucketNotificationsAsync(bucketName, notification, cancellationToken);
  338. }
  339. /// <summary>
  340. /// Subscribes to bucket change notifications (a Minio-only extension)
  341. /// </summary>
  342. /// <param name="bucketName">Bucket to get notifications from</param>
  343. /// <param name="events">Events to listen for</param>
  344. /// <param name="prefix">Filter keys starting with this prefix</param>
  345. /// <param name="suffix">Filter keys ending with this suffix</param>
  346. /// <param name="cancellationToken">Optional cancellation token to cancel the operation</param>
  347. /// <returns>An observable of JSON-based notification events</returns>
  348. public IObservable<MinioNotificationRaw> ListenBucketNotificationsAsync(string bucketName, IList<EventType> events, string prefix = "", string suffix = "", CancellationToken cancellationToken = default(CancellationToken))
  349. {
  350. return Observable.Create<MinioNotificationRaw>(
  351. async (obs, ct) =>
  352. {
  353. bool isRunning = true;
  354. using (var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, ct))
  355. {
  356. while (isRunning)
  357. {
  358. var queries = new List<string>();
  359. queries.Add("prefix=" + Uri.EscapeDataString(prefix));
  360. queries.Add("suffix=" + Uri.EscapeDataString(suffix));
  361. foreach (var eventType in events)
  362. {
  363. queries.Add("events=" + Uri.EscapeDataString(eventType.value));
  364. }
  365. string query = string.Join("&", queries);
  366. var request = await this.CreateRequest(Method.GET,
  367. bucketName,
  368. resourcePath: "?" + query)
  369. .ConfigureAwait(false);
  370. var startTime = DateTime.Now;
  371. // Logs full url when HTTPtracing is enabled (as in MinioClient.ExecuteTaskAsync)
  372. if (this.trace)
  373. {
  374. var fullUrl = this.restClient.BuildUri(request);
  375. Console.WriteLine($"Full URL of Request {fullUrl}");
  376. }
  377. request.ResponseWriter = responseStream =>
  378. {
  379. using (responseStream)
  380. {
  381. var sr = new StreamReader(responseStream);
  382. while (true)
  383. {
  384. string line = sr.ReadLine();
  385. if (this.trace)
  386. {
  387. Console.WriteLine("== ListenBucketNotificationsAsync read line ==");
  388. Console.WriteLine(line);
  389. Console.WriteLine("==============================================");
  390. }
  391. if (line == null)
  392. {
  393. break;
  394. }
  395. string trimmed = line.Trim();
  396. if (trimmed.Length > 2)
  397. {
  398. obs.OnNext(new MinioNotificationRaw(trimmed));
  399. }
  400. }
  401. }
  402. };
  403. IRestResponse response = await this.restClient.ExecuteTaskAsync(request, cancellationToken).ConfigureAwait(false);
  404. this.HandleIfErrorResponse(response, this.NoErrorHandlers, startTime);
  405. cts.Token.ThrowIfCancellationRequested();
  406. }
  407. }
  408. });
  409. }
  410. }
  411. }