/* * Minimal Object Storage Library, (C) 2015 Minio, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ using System; using System.Collections.Generic; using System.Net; using System.Linq; using RestSharp; using System.IO; using Minio.Client.Xml; using System.Xml.Serialization; using System.Xml.Linq; using Minio.Client.Errors; namespace Minio.Client { public class ObjectStorageClient { private static int PART_SIZE = 5 * 1024 * 1024; private RestClient client; private string region; private string SystemUserAgent = "minio-cs/0.0.1 (Windows 8.1; x86_64)"; private string CustomUserAgent = ""; private string FullUserAgent { get { return SystemUserAgent + " " + CustomUserAgent; } } internal ObjectStorageClient(Uri uri, string accessKey, string secretKey) { this.client = new RestClient(uri); this.region = Regions.GetRegion(uri.Host); this.client.UserAgent = this.FullUserAgent; if (accessKey != null && secretKey != null) { this.client.Authenticator = new V4Authenticator(accessKey, secretKey); } } /// /// Creates and returns an object storage client. /// /// Location of the server, supports HTTP and HTTPS. /// Object Storage Client with the uri set as the server location. public static ObjectStorageClient GetClient(Uri uri) { return GetClient(uri, null, null); } /// /// Creates and returns an object storage client /// /// Location of the server, supports HTTP and HTTPS /// Access Key for authenticated requests /// Secret Key for authenticated requests /// Object Storage Client with the uri set as the server location and authentication parameters set. public static ObjectStorageClient GetClient(Uri uri, string accessKey, string secretKey) { if (uri == null) { throw new NullReferenceException(); } if (!(uri.Scheme == "http" || uri.Scheme == "https")) { throw new UriFormatException("Expecting http or https"); } if (uri.Query.Length != 0) { throw new UriFormatException("Expecting no query"); } if (uri.AbsolutePath.Length == 0 || (uri.AbsolutePath.Length == 1 && uri.AbsolutePath[0] == '/')) { String path = uri.Scheme + "://" + uri.Host + ":" + uri.Port + "/"; return new ObjectStorageClient(new Uri(path), accessKey, secretKey); } throw new UriFormatException("Expecting AbsolutePath to be empty"); } /// /// Creates and returns an object storage client /// /// Location of the server, supports HTTP and HTTPS /// Object Storage Client with the uri set as the server location and authentication parameters set. public static ObjectStorageClient GetClient(string url) { return GetClient(url, null, null); } /// /// Creates and returns an object storage client /// /// Location of the server, supports HTTP and HTTPS /// Access Key for authenticated requests /// Secret Key for authenticated requests /// Object Storage Client with the uri set as the server location and authentication parameters set. public static ObjectStorageClient GetClient(string url, string accessKey, string secretKey) { Uri uri = new Uri(url); return GetClient(uri, accessKey, secretKey); } public void SetUserAgent(string product, string version, IEnumerable attributes) { if (string.IsNullOrEmpty(product)) { throw new ArgumentException("product cannot be null or empty"); } if (string.IsNullOrEmpty(version)) { throw new ArgumentException("version cannot be null or empty"); } string customAgent = product + "/" + version; string[] attributesArray = attributes.ToArray(); if (attributes.Count() > 0) { customAgent += "("; customAgent += string.Join("; ", attributesArray); customAgent += ")"; } this.CustomUserAgent = customAgent; this.client.UserAgent = this.FullUserAgent; this.client.FollowRedirects = false; } /// /// Returns true if the specified bucket exists, otherwise returns false. /// /// Bucket to test existence of /// true if exists and user has access public bool BucketExists(string bucket) { var request = new RestRequest(bucket, Method.HEAD); var response = client.Execute(request); if (response.StatusCode == HttpStatusCode.OK) { return true; } return false; } /// /// Create a bucket with a given name and canned ACL /// /// Name of the new bucket /// Canned ACL to set public void MakeBucket(string bucket, Acl acl) { var request = new RestRequest("/" + bucket, Method.PUT); CreateBucketConfiguration config = new CreateBucketConfiguration() { LocationConstraint = this.region }; request.AddHeader("x-amz-acl", acl.ToString()); request.AddBody(config); var response = client.Execute(request); if (response.StatusCode == HttpStatusCode.OK) { return; } throw ParseError(response); } /// /// Create a private bucket with a give name. /// /// Name of the new bucket public void MakeBucket(string bucket) { this.MakeBucket(bucket, Acl.Private); } /// /// Remove a bucket /// /// Name of bucket to remove public void RemoveBucket(string bucket) { var request = new RestRequest(bucket, Method.DELETE); var response = client.Execute(request); if (!response.StatusCode.Equals(HttpStatusCode.NoContent)) { throw ParseError(response); } } /// /// Get bucket ACL /// /// NAme of bucket to retrieve canned ACL /// Canned ACL public Acl GetBucketAcl(string bucket) { var request = new RestRequest(bucket + "?acl", Method.GET); var response = client.Execute(request); if (response.StatusCode == HttpStatusCode.OK) { var content = StripXmlnsXsi(response.Content); var contentBytes = System.Text.Encoding.UTF8.GetBytes(content); var stream = new MemoryStream(contentBytes); AccessControlPolicy bucketList = (AccessControlPolicy)(new XmlSerializer(typeof(AccessControlPolicy)).Deserialize(stream)); bool publicRead = false; bool publicWrite = false; bool authenticatedRead = false; foreach (var x in bucketList.Grants) { if ("http://acs.amazonaws.com/groups/global/AllUsers".Equals(x.Grantee.URI) && x.Permission.Equals("READ")) { publicRead = true; } if ("http://acs.amazonaws.com/groups/global/AllUsers".Equals(x.Grantee.URI) && x.Permission.Equals("WRITE")) { publicWrite = true; } if ("http://acs.amazonaws.com/groups/global/AuthenticatedUsers".Equals(x.Grantee.URI) && x.Permission.Equals("READ")) { authenticatedRead = true; } } if (publicRead && publicWrite && !authenticatedRead) { return Acl.PublicReadWrite; } if (publicRead && !publicWrite && !authenticatedRead) { return Acl.PublicRead; } if (!publicRead && !publicWrite && authenticatedRead) { return Acl.AuthenticatedRead; } return Acl.Private; } throw ParseError(response); } /// /// Set a bucket's canned ACL /// /// Name of bucket to set canned ACL /// Canned ACL to set public void SetBucketAcl(string bucket, Acl acl) { var request = new RestRequest(bucket + "?acl", Method.PUT); // TODO add acl header request.AddHeader("x-amz-acl", acl.ToString()); var response = client.Execute(request); if (!HttpStatusCode.OK.Equals(response.StatusCode)) { throw ParseError(response); } } /// /// Lists all buckets owned by the user /// /// A list of all buckets owned by the user. public List ListBuckets() { var request = new RestRequest("/", Method.GET); var response = client.Execute(request); if (response.StatusCode == HttpStatusCode.OK) { try { var contentBytes = System.Text.Encoding.UTF8.GetBytes(response.Content); var stream = new MemoryStream(contentBytes); ListAllMyBucketsResult bucketList = (ListAllMyBucketsResult)(new XmlSerializer(typeof(ListAllMyBucketsResult)).Deserialize(stream)); return bucketList.Buckets; } catch (InvalidOperationException ex) { // unauthed returns html, so we catch xml parse exception and return access denied throw new AccessDeniedException(); } } throw ParseError(response); } /// /// Get an object. The object will be streamed to the callback given by the user. /// /// Bucket to retrieve object from /// Key of object to retrieve /// A stream will be passed to the callback public void GetObject(string bucket, string key, Action callback) { RestRequest request = new RestRequest(bucket + "/" + UrlEncode(key), Method.GET); request.ResponseWriter = callback; var response = client.Execute(request); if (response.StatusCode == HttpStatusCode.OK) { return; } throw ParseError(response); } /// /// Get an object starting with the byte specified in offset. The object will be streamed to the callback given by the user /// /// Bucket to retrieve object from /// Key of object to retrieve /// Number of bytes to skip /// A stream will be passed to the callback public void GetObject(string bucket, string key, ulong offset, Action callback) { var stat = this.StatObject(bucket, key); RestRequest request = new RestRequest(bucket + "/" + UrlEncode(key), Method.GET); request.AddHeader("Range", "bytes=" + offset + "-" + (stat.Size - 1)); request.ResponseWriter = callback; client.Execute(request); // response status code is 0, bug in upstream library, cannot rely on it for errors with PartialContent } /// /// 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 /// /// Bucket to retrieve object from /// Key of object to retrieve /// Number of bytes to skip /// Length of requested byte range /// A stream will be passed to the callback public void GetObject(string bucket, string key, ulong offset, ulong length, Action callback) { RestRequest request = new RestRequest(bucket + "/" + UrlEncode(key), Method.GET); request.AddHeader("Range", "bytes=" + offset + "-" + (offset + length - 1)); request.ResponseWriter = callback; client.Execute(request); // response status code is 0, bug in upstream library, cannot rely on it for errors with PartialContent } /// /// Tests the object's existence and returns metadata about existing objects. /// /// Bucket to test object in /// Key of object to stat /// Facts about the object public ObjectStat StatObject(string bucket, string key) { var request = new RestRequest(bucket + "/" + UrlEncode(key), Method.HEAD); var response = client.Execute(request); if (response.StatusCode == HttpStatusCode.OK) { long size = 0; DateTime lastModified = new DateTime(); string etag = ""; foreach (Parameter parameter in response.Headers) { if (parameter.Name == "Content-Length") { size = long.Parse(parameter.Value.ToString()); } if (parameter.Name == "Last-Modified") { DateTime.Parse(parameter.Value.ToString()); } if (parameter.Name == "ETag") { etag = parameter.Value.ToString().Replace("\"", ""); } } return new ObjectStat(key, size, lastModified, etag); } throw ParseError(response); } /// /// Creates an object /// /// Bucket to create object in /// Key of the new object /// Total size of bytes to be written, must match with data's length /// Content type of the new object, null defaults to "application/octet-stream" /// Stream of bytes to send public void PutObject(string bucket, string key, long size, string contentType, Stream data) { if (size <= ObjectStorageClient.PART_SIZE) { var bytes = ReadFull(data, (int)size); if (data.ReadByte() > 0) { throw new DataSizeMismatchException(); } if (bytes.Length != (int)size) { throw new DataSizeMismatchException() { Bucket = bucket, Key = key, UserSpecifiedSize = size, ActualReadSize = bytes.Length }; } this.DoPutObject(bucket, key, null, 0, contentType, bytes); } else { var partSize = CalculatePartSize(size); var uploads = this.ListAllIncompleteUploads(bucket, key); string uploadId = null; Dictionary etags = new Dictionary(); if (uploads.Count() > 0) { uploadId = uploads.Last().UploadId; var parts = this.ListParts(bucket, key, uploadId); foreach (Part part in parts) { etags[part.PartNumber] = part.ETag; } } if (uploadId == null) { uploadId = this.NewMultipartUpload(bucket, key); } int partNumber = 0; long totalWritten = 0; while (totalWritten < size) { partNumber++; var currentPartSize = (int)Math.Min((long)partSize, (size - totalWritten)); byte[] dataToCopy = ReadFull(data, currentPartSize); if (dataToCopy == null) { break; } System.Security.Cryptography.MD5 md5 = System.Security.Cryptography.MD5.Create(); byte[] hash = md5.ComputeHash(dataToCopy); string etag = BitConverter.ToString(hash).Replace("-", string.Empty).ToLower(); if (!etags.ContainsKey(partNumber) || !etags[partNumber].Equals(etag)) { etag = DoPutObject(bucket, key, uploadId, partNumber, contentType, dataToCopy); } etags[partNumber] = etag; totalWritten += dataToCopy.Length; } // test if any more data is on the stream if (data.ReadByte() != -1) { throw new DataSizeMismatchException() { Bucket = bucket, Key = key, UserSpecifiedSize = size, ActualReadSize = totalWritten + 1 }; } if (totalWritten != size) { throw new DataSizeMismatchException() { Bucket = bucket, Key = key, UserSpecifiedSize = size, ActualReadSize = totalWritten }; } foreach (int curPartNumber in etags.Keys) { if (curPartNumber > partNumber) { etags.Remove(curPartNumber); } } this.CompleteMultipartUpload(bucket, key, uploadId, etags); } } private byte[] ReadFull(Stream data, int currentPartSize) { byte[] result = new byte[currentPartSize]; int totalRead = 0; while (totalRead < currentPartSize) { byte[] curData = new byte[currentPartSize - totalRead]; int curRead = data.Read(curData, 0, currentPartSize - totalRead); if (curRead == 0) { break; } for (int i = 0; i < curRead; i++) { result[totalRead + i] = curData[i]; } totalRead += curRead; } if (totalRead == 0) return null; if (totalRead == currentPartSize) return result; byte[] truncatedResult = new byte[totalRead]; for (int i = 0; i < totalRead; i++) { truncatedResult[i] = result[i]; } return truncatedResult; } private void CompleteMultipartUpload(string bucket, string key, string uploadId, Dictionary etags) { var path = bucket + "/" + UrlEncode(key) + "?uploadId=" + uploadId; var request = new RestRequest(path, Method.POST); List parts = new List(); for (int i = 1; i <= etags.Count; i++) { parts.Add(new XElement("Part", new XElement("PartNumber", i), new XElement("ETag", etags[i]))); } var completeMultipartUploadXml = new XElement("CompleteMultipartUpload", parts); var bodyString = completeMultipartUploadXml.ToString(); var body = System.Text.Encoding.UTF8.GetBytes(bodyString); request.AddParameter("application/xml", body, RestSharp.ParameterType.RequestBody); var response = client.Execute(request); if (response.StatusCode.Equals(HttpStatusCode.OK)) { return; } throw ParseError(response); } private int CalculatePartSize(long size) { int minimumPartSize = PART_SIZE; // 5MB int partSize = (int)(size / 9999); // using 10000 may cause part size to become too small, and not fit the entire object in return Math.Max(minimumPartSize, partSize); } private IEnumerable ListParts(string bucket, string key, string uploadId) { int nextPartNumberMarker = 0; bool isRunning = true; while (isRunning) { var uploads = GetListParts(bucket, key, uploadId); foreach (Part part in uploads.Item2) { yield return part; } nextPartNumberMarker = uploads.Item1.NextPartNumberMarker; isRunning = uploads.Item1.IsTruncated; } } private Tuple> GetListParts(string bucket, string key, string uploadId) { var path = bucket + "/" + UrlEncode(key) + "?uploadId=" + uploadId; var request = new RestRequest(path, Method.GET); var response = client.Execute(request); if (response.StatusCode.Equals(HttpStatusCode.OK)) { var contentBytes = System.Text.Encoding.UTF8.GetBytes(response.Content); var stream = new MemoryStream(contentBytes); ListPartsResult listPartsResult = (ListPartsResult)(new XmlSerializer(typeof(ListPartsResult)).Deserialize(stream)); XDocument root = XDocument.Parse(response.Content); var uploads = (from c in root.Root.Descendants("{http://s3.amazonaws.com/doc/2006-03-01/}Part") select new Part() { PartNumber = int.Parse(c.Element("{http://s3.amazonaws.com/doc/2006-03-01/}PartNumber").Value), ETag = c.Element("{http://s3.amazonaws.com/doc/2006-03-01/}ETag").Value.Replace("\"", "") }); return new Tuple>(listPartsResult, new List()); } throw ParseError(response); } private string NewMultipartUpload(string bucket, string key) { var path = bucket + "/" + UrlEncode(key) + "?uploads"; var request = new RestRequest(path, Method.POST); var response = client.Execute(request); if (response.StatusCode.Equals(HttpStatusCode.OK)) { var contentBytes = System.Text.Encoding.UTF8.GetBytes(response.Content); var stream = new MemoryStream(contentBytes); InitiateMultipartUploadResult newUpload = (InitiateMultipartUploadResult)(new XmlSerializer(typeof(InitiateMultipartUploadResult)).Deserialize(stream)); return newUpload.UploadId; } throw ParseError(response); } private string DoPutObject(string bucket, string key, string uploadId, int partNumber, string contentType, byte[] data) { var path = bucket + "/" + UrlEncode(key); var queries = new List(); if (uploadId != null) { path += "?uploadId=" + uploadId + "&partNumber=" + partNumber; } var request = new RestRequest(path, Method.PUT); if (contentType == null) { contentType = "application/octet-stream"; } request.AddHeader("Content-Type", contentType); request.AddParameter(contentType, data, RestSharp.ParameterType.RequestBody); var response = client.Execute(request); if (response.StatusCode.Equals(HttpStatusCode.OK)) { string etag = null; foreach (Parameter parameter in response.Headers) { if (parameter.Name == "ETag") { etag = parameter.Value.ToString(); } } return etag; } throw ParseError(response); } private ClientException ParseError(IRestResponse response) { if (response == null) { return new ConnectionException(); } if (HttpStatusCode.Redirect.Equals(response.StatusCode) || HttpStatusCode.TemporaryRedirect.Equals(response.StatusCode)) { return new RedirectionException(); } if (HttpStatusCode.Forbidden.Equals(response.StatusCode) || HttpStatusCode.NotFound.Equals(response.StatusCode)) { ClientException e = null; ErrorResponse errorResponse = new ErrorResponse(); foreach (Parameter parameter in response.Headers) { if (parameter.Name.Equals("x-amz-id-2", StringComparison.CurrentCultureIgnoreCase)) { errorResponse.XAmzID2 = parameter.Value.ToString(); } if (parameter.Name.Equals("x-amz-request-id", StringComparison.CurrentCultureIgnoreCase)) { errorResponse.RequestID = parameter.Value.ToString(); } } errorResponse.Resource = response.Request.Resource; if (HttpStatusCode.NotFound.Equals(response.StatusCode)) { int pathLength = response.Request.Resource.Split('/').Count(); if (pathLength > 1) { errorResponse.Code = "NoSuchKey"; e = new ObjectNotFoundException(); } else if (pathLength == 1) { errorResponse.Code = "NoSuchBucket"; e = new BucketNotFoundException(); } else { e = new InternalClientException("404 without body resulted in path with less than two components"); } } else { errorResponse.Code = "Forbidden"; e = new AccessDeniedException(); } e.Response = errorResponse; return e; } if (string.IsNullOrWhiteSpace(response.Content)) { throw new InternalClientException("Unsuccessful response from server without XML error: " + response.StatusCode); } var contentBytes = System.Text.Encoding.UTF8.GetBytes(response.Content); var stream = new MemoryStream(contentBytes); ErrorResponse errResponse = (ErrorResponse)(new XmlSerializer(typeof(ErrorResponse)).Deserialize(stream)); string code = errResponse.Code; ClientException clientException; if ("NoSuchBucket".Equals(code)) clientException = new BucketNotFoundException(); else if ("NoSuchKey".Equals(code)) clientException = new ObjectNotFoundException(); else if ("InvalidBucketName".Equals(code)) clientException = new InvalidKeyNameException(); else if ("InvalidObjectName".Equals(code)) clientException = new InvalidKeyNameException(); else if ("AccessDenied".Equals(code)) clientException = new AccessDeniedException(); else if ("BucketAlreadyExists".Equals(code)) clientException = new BucketExistsException(); else if ("ObjectAlreadyExists".Equals(code)) clientException = new ObjectExistsException(); else if ("InternalError".Equals(code)) clientException = new InternalServerException(); else if ("KeyTooLong".Equals(code)) clientException = new InvalidKeyNameException(); else if ("TooManyBuckets".Equals(code)) clientException = new MaxBucketsReachedException(); else if ("PermanentRedirect".Equals(code)) clientException = new RedirectionException(); else if ("MethodNotAllowed".Equals(code)) clientException = new ObjectExistsException(); else if ("BucketAlreadyOwnedByYou".Equals(code)) clientException = new BucketExistsException(); else clientException = new InternalClientException(errResponse.ToString()); clientException.Response = errResponse; clientException.XmlError = response.Content; return clientException; } private string StripXmlnsXsi(string input) { string result = input.Replace("xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:type=\"CanonicalUser\"", ""); result = result.Replace("xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:type=\"Group\"", ""); return result; } /// /// List all objects in a bucket /// /// Bucket to list objects from /// An iterator lazily populated with objects public IEnumerable ListObjects(string bucket) { return this.ListObjects(bucket, null, true); } /// /// List all objects in a bucket with a given prefix /// /// BUcket to list objects from /// Filters all objects not beginning with a given prefix /// An iterator lazily populated with objects public IEnumerable ListObjects(string bucket, string prefix) { return this.ListObjects(bucket, prefix, true); } /// /// List all objects non-recursively in a bucket with a given prefix, optionally emulating a directory /// /// Bucket to list objects from /// Filters all objects not beginning with a given prefix /// Set to false to emulate a directory /// A iterator lazily populated with objects public IEnumerable ListObjects(string bucket, string prefix, bool recursive) { bool isRunning = true; string marker = null; while (isRunning) { Tuple> result = GetObjectList(bucket, prefix, recursive, marker); Item lastItem = null; foreach (Item item in result.Item2) { lastItem = item; yield return item; } if (result.Item1.NextMarker != null) { marker = result.Item1.NextMarker; } else { marker = lastItem.Key; } isRunning = result.Item1.IsTruncated; } } private Tuple> GetObjectList(string bucket, string prefix, bool recursive, string marker) { var queries = new List(); if (!recursive) { queries.Add("delimiter=%2F"); } if (prefix != null) { queries.Add("prefix=" + Uri.EscapeDataString(prefix)); } if (marker != null) { queries.Add("marker=" + marker); } string query = string.Join("&", queries); string path = bucket; if (query.Length > 0) { path += "?" + query; } var request = new RestRequest(path, Method.GET); var response = client.Execute(request); if (response.StatusCode == HttpStatusCode.OK) { var contentBytes = System.Text.Encoding.UTF8.GetBytes(response.Content); var stream = new MemoryStream(contentBytes); ListBucketResult listBucketResult = (ListBucketResult)(new XmlSerializer(typeof(ListBucketResult)).Deserialize(stream)); XDocument root = XDocument.Parse(response.Content); var items = (from c in root.Root.Descendants("{http://s3.amazonaws.com/doc/2006-03-01/}Contents") select new Item() { Key = c.Element("{http://s3.amazonaws.com/doc/2006-03-01/}Key").Value, LastModified = c.Element("{http://s3.amazonaws.com/doc/2006-03-01/}LastModified").Value, ETag = c.Element("{http://s3.amazonaws.com/doc/2006-03-01/}ETag").Value, Size = UInt64.Parse(c.Element("{http://s3.amazonaws.com/doc/2006-03-01/}Size").Value), IsDir = false }); var prefixes = (from c in root.Root.Descendants("{http://s3.amazonaws.com/doc/2006-03-01/}CommonPrefixes") select new Item() { Key = c.Element("{http://s3.amazonaws.com/doc/2006-03-01/}Prefix").Value, IsDir = true }); items = items.Concat(prefixes); return new Tuple>(listBucketResult, items.ToList()); } throw ParseError(response); } private Tuple> GetMultipartUploadsList(string bucket, string prefix, string keyMarker, string uploadIdMarker) { var queries = new List(); queries.Add("uploads"); if (prefix != null) { queries.Add("prefix=" + Uri.EscapeDataString(prefix)); } if (keyMarker != null) { queries.Add("key-marker=" + Uri.EscapeDataString(keyMarker)); } if (uploadIdMarker != null) { queries.Add("upload-id-marker=" + uploadIdMarker); } string query = string.Join("&", queries); string path = bucket; path += "?" + query; var request = new RestRequest(path, Method.GET); var response = client.Execute(request); if (response.StatusCode == HttpStatusCode.OK) { var contentBytes = System.Text.Encoding.UTF8.GetBytes(response.Content); var stream = new MemoryStream(contentBytes); ListMultipartUploadsResult listBucketResult = (ListMultipartUploadsResult)(new XmlSerializer(typeof(ListMultipartUploadsResult)).Deserialize(stream)); XDocument root = XDocument.Parse(response.Content); var uploads = (from c in root.Root.Descendants("{http://s3.amazonaws.com/doc/2006-03-01/}Upload") select new Upload() { Key = c.Element("{http://s3.amazonaws.com/doc/2006-03-01/}Key").Value, UploadId = c.Element("{http://s3.amazonaws.com/doc/2006-03-01/}UploadId").Value, Initiated = c.Element("{http://s3.amazonaws.com/doc/2006-03-01/}Initiated").Value }); return new Tuple>(listBucketResult, uploads.ToList()); } throw ParseError(response); } /// /// Lists all incomplete uploads in a given bucket /// /// Bucket to list all incomplepte uploads from /// A lazily populated list of incomplete uploads public IEnumerable ListAllIncompleteUploads(string bucket) { return this.ListAllIncompleteUploads(bucket, null); } /// /// Lists all incomplete uploads in a given bucket with a given key /// /// Bucket to list incomplete uploads from /// Key of object to list incomplete uploads from /// public IEnumerable ListAllIncompleteUploads(string bucket, string key) { string nextKeyMarker = null; string nextUploadIdMarker = null; bool isRunning = true; while (isRunning) { var uploads = GetMultipartUploadsList(bucket, key, nextKeyMarker, nextUploadIdMarker); foreach (Upload upload in uploads.Item2) { if (key != null && !key.Equals(upload.Key)) { continue; } yield return upload; } nextKeyMarker = uploads.Item1.NextKeyMarker; nextUploadIdMarker = uploads.Item1.NextUploadIdMarker; isRunning = uploads.Item1.IsTruncated; } } /// /// Drop incomplete uploads from a given bucket and key /// /// Bucket to drop incomplete uploads from /// Key to drop incomplete uploads from public void DropIncompleteUpload(string bucket, string key) { var uploads = this.ListAllIncompleteUploads(bucket, key); foreach (Upload upload in uploads) { this.DropUpload(bucket, key, upload.UploadId); } } /// /// Drops all incomplete uploads from a given bucket /// /// Bucket to drop all incomplete uploads from public void DropAllIncompleteUploads(string bucket) { var uploads = this.ListAllIncompleteUploads(bucket); foreach (Upload upload in uploads) { this.DropUpload(bucket, upload.Key, upload.UploadId); } } private void DropUpload(string bucket, string key, string uploadId) { var path = bucket + "/" + UrlEncode(key) + "?uploadId=" + uploadId; var request = new RestRequest(path, Method.DELETE); var response = client.Execute(request); if (response.StatusCode == HttpStatusCode.NoContent) { return; } throw ParseError(response); } private string UrlEncode(string input) { return Uri.EscapeDataString(input).Replace("%2F", "/"); } } }