Java tutorial
/* * Minio Java Library for Amazon S3 Compatible Cloud Storage, (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. */ package io.minio; import io.minio.errors.*; import io.minio.messages.*; import io.minio.http.*; import com.squareup.okhttp.OkHttpClient; import com.squareup.okhttp.HttpUrl; import com.squareup.okhttp.Request; import com.squareup.okhttp.RequestBody; import com.squareup.okhttp.Response; import com.squareup.okhttp.MediaType; import okio.BufferedSink; import okio.Okio; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserFactory; import org.xmlpull.v1.XmlPullParserException; import org.apache.commons.validator.routines.InetAddressValidator; import org.joda.time.DateTime; import com.google.gson.Gson; import java.io.IOException; import java.io.InputStream; import java.io.RandomAccessFile; import java.io.BufferedInputStream; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.io.StringReader; import java.io.EOFException; import java.net.URL; import java.security.NoSuchAlgorithmException; import java.security.InvalidKeyException; import java.util.*; import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; import java.nio.charset.StandardCharsets; import java.nio.channels.Channels; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.Files; import java.nio.file.StandardOpenOption; import com.google.common.io.ByteStreams; import java.nio.file.StandardCopyOption; import org.apache.commons.collections4.CollectionUtils; /** * <p> * This class implements a simple cloud storage client. This client consists * of a useful subset of S3 compatible functionality. * </p> * <h2>Service</h2> * <ul> * <li>Creating a bucket</li> * <li>Listing buckets</li> * </ul> * <h2>Bucket</h2> * <ul> * <li> Creating an object, including automatic upload resuming for large objects.</li> * <li> Listing objects in a bucket</li> * <li> Listing active multipart uploads</li> * </ul> * <h2>Object</h2> * <ul> * <li>Removing an active multipart upload for a specific object and uploadId</li> * <li>Read object metadata</li> * <li>Reading an object</li> * <li>Reading a range of bytes of an object</li> * <li>Deleting an object</li> * </ul> * <p> * Optionally, users can also provide access/secret keys. If keys are provided, all requests by the * client will be signed using AWS Signature Version 4. * </p> * For examples on using this library, please see * <a href="https://github.com/minio/minio-java/tree/master/src/test/java/io/minio/examples"></a>. */ @SuppressWarnings({ "SameParameterValue", "WeakerAccess" }) public final class MinioClient { private static final Logger LOGGER = Logger.getLogger(MinioClient.class.getName()); // maximum allowed object size is 5TiB private static final long MAX_OBJECT_SIZE = 5L * 1024 * 1024 * 1024 * 1024; private static final int MAX_MULTIPART_COUNT = 10000; // minimum allowed multipart size is 5MiB private static final int MIN_MULTIPART_SIZE = 5 * 1024 * 1024; // default expiration for a presigned URL is 7 days in seconds private static final int DEFAULT_EXPIRY_TIME = 7 * 24 * 3600; private static final String DEFAULT_USER_AGENT = "Minio (" + System.getProperty("os.arch") + "; " + System.getProperty("os.arch") + ") minio-java/" + MinioProperties.INSTANCE.getVersion(); private static final String NULL_STRING = "(null)"; private static final String S3_AMAZONAWS_COM = "s3.amazonaws.com"; private static final String AMAZONAWS_COM = ".amazonaws.com"; private static final String END_HTTP = "----------END-HTTP----------"; private static final String US_EAST_1 = "us-east-1"; private static final String UPLOAD_ID = "uploadId"; private static final Gson gson = new Gson(); private static XmlPullParserFactory xmlPullParserFactory = null; static { try { xmlPullParserFactory = XmlPullParserFactory.newInstance(); xmlPullParserFactory.setNamespaceAware(true); } catch (XmlPullParserException e) { throw new ExceptionInInitializerError(e); } } private PrintWriter traceStream; // the current client instance's base URL. private HttpUrl baseUrl; // access key to sign all requests with private String accessKey; // Secret key to sign all requests with private String secretKey; private String userAgent = DEFAULT_USER_AGENT; private OkHttpClient httpClient = new OkHttpClient(); /** * Creates Minio client object with given endpoint using anonymous access. * * </p><b>Example:</b><br> * <pre>{@code MinioClient minioClient = new MinioClient("https://play.minio.io:9000"); }</pre> * * @param endpoint Request endpoint. Endpoint is an URL, domain name, IPv4 or IPv6 address.<pre> * Valid endpoints: * * https://s3.amazonaws.com * * https://s3.amazonaws.com/ * * https://play.minio.io:9000 * * http://play.minio.io:9010/ * * localhost * * localhost.localdomain * * play.minio.io * * 127.0.0.1 * * 192.168.1.60 * * ::1</pre> * * @see #MinioClient(URL url) * @see #MinioClient(String endpoint, String accessKey, String secretKey) * @see #MinioClient(URL url, String accessKey, String secretKey) * @see #MinioClient(String endpoint, int port, String accessKey, String secretKey) * @see #MinioClient(String endpoint, String accessKey, String secretKey, boolean secure) * @see #MinioClient(String endpoint, int port, String accessKey, String secretKey, boolean secure) */ public MinioClient(String endpoint) throws InvalidEndpointException, InvalidPortException { this(endpoint, 0, null, null); } /** * Creates Minio client object with given URL object using anonymous access. * * </p><b>Example:</b><br> * <pre>{@code MinioClient minioClient = new MinioClient(new URL("https://play.minio.io:9000")); }</pre> * * @param url Endpoint URL object. * * @see #MinioClient(String endpoint) * @see #MinioClient(String endpoint, String accessKey, String secretKey) * @see #MinioClient(URL url, String accessKey, String secretKey) * @see #MinioClient(String endpoint, int port, String accessKey, String secretKey) * @see #MinioClient(String endpoint, String accessKey, String secretKey, boolean secure) * @see #MinioClient(String endpoint, int port, String accessKey, String secretKey, boolean secure) */ public MinioClient(URL url) throws InvalidEndpointException, InvalidPortException { this(url.toString(), 0, null, null); } /** * Creates Minio client object with given HttpUrl object using anonymous access. * * </p><b>Example:</b><br> * <pre>{@code MinioClient minioClient = new MinioClient(new HttpUrl.parse("https://play.minio.io:9000")); }</pre> * * @param url Endpoint HttpUrl object. * * @see #MinioClient(String endpoint) * @see #MinioClient(URL url) * @see #MinioClient(String endpoint, String accessKey, String secretKey) * @see #MinioClient(URL url, String accessKey, String secretKey) * @see #MinioClient(String endpoint, int port, String accessKey, String secretKey) * @see #MinioClient(String endpoint, String accessKey, String secretKey, boolean secure) * @see #MinioClient(String endpoint, int port, String accessKey, String secretKey, boolean secure) */ public MinioClient(HttpUrl url) throws InvalidEndpointException, InvalidPortException { this(url.toString(), 0, null, null); } /** * Creates Minio client object with given endpoint, access key and secret key. * * </p><b>Example:</b><br> * <pre>{@code MinioClient minioClient = new MinioClient("https://play.minio.io:9000", "YOUR-ACCESSKEYID", "YOUR-SECRETACCESSKEY"); }</pre> * * @param endpoint Request endpoint. Endpoint is an URL, domain name, IPv4 or IPv6 address.<pre> * Valid endpoints: * * https://s3.amazonaws.com * * https://s3.amazonaws.com/ * * https://play.minio.io:9000 * * http://play.minio.io:9010/ * * localhost * * localhost.localdomain * * play.minio.io * * 127.0.0.1 * * 192.168.1.60 * * ::1</pre> * @param accessKey Access key to access service in endpoint. * @param secretKey Secret key to access service in endpoint. * * @see #MinioClient(String endpoint) * @see #MinioClient(URL url) * @see #MinioClient(URL url, String accessKey, String secretKey) * @see #MinioClient(String endpoint, int port, String accessKey, String secretKey) * @see #MinioClient(String endpoint, String accessKey, String secretKey, boolean secure) * @see #MinioClient(String endpoint, int port, String accessKey, String secretKey, boolean secure) */ public MinioClient(String endpoint, String accessKey, String secretKey) throws InvalidEndpointException, InvalidPortException { this(endpoint, 0, accessKey, secretKey); } /** * Creates Minio client object with given URL object, access key and secret key. * * </p><b>Example:</b><br> * <pre>{@code MinioClient minioClient = new MinioClient(new URL("https://play.minio.io:9000"), "YOUR-ACCESSKEYID", "YOUR-SECRETACCESSKEY"); }</pre> * * @param url Endpoint URL object. * @param accessKey Access key to access service in endpoint. * @param secretKey Secret key to access service in endpoint. * * @see #MinioClient(String endpoint) * @see #MinioClient(URL url) * @see #MinioClient(String endpoint, String accessKey, String secretKey) * @see #MinioClient(String endpoint, int port, String accessKey, String secretKey) * @see #MinioClient(String endpoint, String accessKey, String secretKey, boolean secure) * @see #MinioClient(String endpoint, int port, String accessKey, String secretKey, boolean secure) */ public MinioClient(URL url, String accessKey, String secretKey) throws InvalidEndpointException, InvalidPortException { this(url.toString(), 0, accessKey, secretKey); } /** * Creates Minio client object with given URL object, access key and secret key. * * </p><b>Example:</b><br> * <pre>{@code MinioClient minioClient = new MinioClient(HttpUrl.parse("https://play.minio.io:9000"), "YOUR-ACCESSKEYID", "YOUR-SECRETACCESSKEY"); }</pre> * * @param url Endpoint HttpUrl object. * @param accessKey Access key to access service in endpoint. * @param secretKey Secret key to access service in endpoint. * * @see #MinioClient(String endpoint) * @see #MinioClient(URL url) * @see #MinioClient(String endpoint, String accessKey, String secretKey) * @see #MinioClient(URL url, String accessKey, String secretKey) * @see #MinioClient(String endpoint, int port, String accessKey, String secretKey) * @see #MinioClient(String endpoint, String accessKey, String secretKey, boolean secure) * @see #MinioClient(String endpoint, int port, String accessKey, String secretKey, boolean secure) */ public MinioClient(HttpUrl url, String accessKey, String secretKey) throws InvalidEndpointException, InvalidPortException { this(url.toString(), 0, accessKey, secretKey); } /** * Creates Minio client object with given endpoint, port, access key and secret key using secure (HTTPS) connection. * * </p><b>Example:</b><br> * <pre>{@code MinioClient minioClient = * new MinioClient("play.minio.io", 9000, "YOUR-ACCESSKEYID", "YOUR-SECRETACCESSKEY"); * }</pre> * * @param endpoint Request endpoint. Endpoint is an URL, domain name, IPv4 or IPv6 address.<pre> * Valid endpoints: * * https://s3.amazonaws.com * * https://s3.amazonaws.com/ * * https://play.minio.io:9000 * * http://play.minio.io:9010/ * * localhost * * localhost.localdomain * * play.minio.io * * 127.0.0.1 * * 192.168.1.60 * * ::1</pre> * * @param port Valid port. It should be in between 1 and 65535. Unused if endpoint is an URL. * @param accessKey Access key to access service in endpoint. * @param secretKey Secret key to access service in endpoint. * * @see #MinioClient(String endpoint) * @see #MinioClient(URL url) * @see #MinioClient(String endpoint, String accessKey, String secretKey) * @see #MinioClient(URL url, String accessKey, String secretKey) * @see #MinioClient(String endpoint, String accessKey, String secretKey, boolean secure) * @see #MinioClient(String endpoint, int port, String accessKey, String secretKey, boolean secure) */ public MinioClient(String endpoint, int port, String accessKey, String secretKey) throws InvalidEndpointException, InvalidPortException { this(endpoint, port, accessKey, secretKey, !(endpoint != null && endpoint.startsWith("http://"))); } /** * Creates Minio client object with given endpoint, access key and secret key using secure (HTTPS) connection. * * </p><b>Example:</b><br> * <pre>{@code MinioClient minioClient = * new MinioClient("play.minio.io:9000", "YOUR-ACCESSKEYID", "YOUR-SECRETACCESSKEY", true); * }</pre> * * @param endpoint Request endpoint. Endpoint is an URL, domain name, IPv4 or IPv6 address.<pre> * Valid endpoints: * * https://s3.amazonaws.com * * https://s3.amazonaws.com/ * * https://play.minio.io:9000 * * http://play.minio.io:9010/ * * localhost * * localhost.localdomain * * play.minio.io * * 127.0.0.1 * * 192.168.1.60 * * ::1</pre> * * @param accessKey Access key to access service in endpoint. * @param secretKey Secret key to access service in endpoint. * @param secure If true, access endpoint using HTTPS else access it using HTTP. * * @see #MinioClient(String endpoint) * @see #MinioClient(URL url) * @see #MinioClient(String endpoint, String accessKey, String secretKey) * @see #MinioClient(URL url, String accessKey, String secretKey) * @see #MinioClient(String endpoint, int port, String accessKey, String secretKey) * @see #MinioClient(String endpoint, int port, String accessKey, String secretKey, boolean secure) */ public MinioClient(String endpoint, String accessKey, String secretKey, boolean secure) throws InvalidEndpointException, InvalidPortException { this(endpoint, 0, accessKey, secretKey, secure); } /** * Creates Minio client object using given endpoint, port, access key, secret key and secure option. * * </p><b>Example:</b><br> * <pre>{@code MinioClient minioClient = * new MinioClient("play.minio.io", 9000, "YOUR-ACCESSKEYID", "YOUR-SECRETACCESSKEY", false); * }</pre> * * @param endpoint Request endpoint. Endpoint is an URL, domain name, IPv4 or IPv6 address.<pre> * Valid endpoints: * * https://s3.amazonaws.com * * https://s3.amazonaws.com/ * * https://play.minio.io:9000 * * http://play.minio.io:9010/ * * localhost * * localhost.localdomain * * play.minio.io * * 127.0.0.1 * * 192.168.1.60 * * ::1</pre> * * @param port Valid port. It should be in between 1 and 65535. Unused if endpoint is an URL. * @param accessKey Access key to access service in endpoint. * @param secretKey Secret key to access service in endpoint. * @param secure If true, access endpoint using HTTPS else access it using HTTP. * * @see #MinioClient(String endpoint) * @see #MinioClient(URL url) * @see #MinioClient(String endpoint, String accessKey, String secretKey) * @see #MinioClient(URL url, String accessKey, String secretKey) * @see #MinioClient(String endpoint, int port, String accessKey, String secretKey) * @see #MinioClient(String endpoint, String accessKey, String secretKey, boolean secure) */ public MinioClient(String endpoint, int port, String accessKey, String secretKey, boolean secure) throws InvalidEndpointException, InvalidPortException { if (endpoint == null) { throw new InvalidEndpointException(NULL_STRING, "null endpoint"); } if (port < 0 || port > 65535) { throw new InvalidPortException(port, "port must be in range of 1 to 65535"); } HttpUrl url = HttpUrl.parse(endpoint); if (url != null) { if (!"/".equals(url.encodedPath())) { throw new InvalidEndpointException(endpoint, "no path allowed in endpoint"); } // treat Amazon S3 host as special case String amzHost = url.host(); if (amzHost.endsWith(AMAZONAWS_COM) && !amzHost.equals(S3_AMAZONAWS_COM)) { throw new InvalidEndpointException(endpoint, "for Amazon S3, host should be 's3.amazonaws.com' in endpoint"); } HttpUrl.Builder urlBuilder = url.newBuilder(); Scheme scheme = Scheme.HTTP; if (secure) { scheme = Scheme.HTTPS; } urlBuilder.scheme(scheme.toString()); if (port > 0) { urlBuilder.port(port); } this.baseUrl = urlBuilder.build(); this.accessKey = accessKey; this.secretKey = secretKey; return; } // endpoint may be a valid hostname, IPv4 or IPv6 address if (!this.isValidEndpoint(endpoint)) { throw new InvalidEndpointException(endpoint, "invalid host"); } // treat Amazon S3 host as special case if (endpoint.endsWith(AMAZONAWS_COM) && !endpoint.equals(S3_AMAZONAWS_COM)) { throw new InvalidEndpointException(endpoint, "for amazon S3, host should be 's3.amazonaws.com'"); } Scheme scheme = Scheme.HTTP; if (secure) { scheme = Scheme.HTTPS; } if (port == 0) { this.baseUrl = new HttpUrl.Builder().scheme(scheme.toString()).host(endpoint).build(); } else { this.baseUrl = new HttpUrl.Builder().scheme(scheme.toString()).host(endpoint).port(port).build(); } this.accessKey = accessKey; this.secretKey = secretKey; } /** * Returns true if given endpoint is valid else false. */ private boolean isValidEndpoint(String endpoint) { if (InetAddressValidator.getInstance().isValid(endpoint)) { return true; } // endpoint may be a hostname // refer https://en.wikipedia.org/wiki/Hostname#Restrictions_on_valid_host_names // why checks are done like below if (endpoint.length() < 1 || endpoint.length() > 253) { return false; } for (String label : endpoint.split("\\.")) { if (label.length() < 1 || label.length() > 63) { return false; } if (!(label.matches("^[a-zA-Z0-9][a-zA-Z0-9-]*") && endpoint.matches(".*[a-zA-Z0-9]$"))) { return false; } } return true; } /** * Validates if given objectPrefix is valid. */ private void checkObjectPrefix(String prefix) throws InvalidObjectPrefixException { // TODO(nl5887): what to do with wildcards in objectPrefix? // if (prefix.length() > 1024) { throw new InvalidObjectPrefixException(prefix, "Object prefix cannot be greater than 1024 characters."); } try { prefix.getBytes("UTF-8"); } catch (java.io.UnsupportedEncodingException exc) { throw new InvalidObjectPrefixException(prefix, "Object prefix cannot be properly encoded to utf-8."); } } /** * Validates if given bucket name is DNS compatible. */ private void checkBucketName(String name) throws InvalidBucketNameException { if (name == null) { throw new InvalidBucketNameException(NULL_STRING, "null bucket name"); } // Bucket names cannot be no less than 3 and no more than 63 characters long. if (name.length() < 3 || name.length() > 63) { String msg = "bucket name must be at least 3 and no more than 63 characters long"; throw new InvalidBucketNameException(name, msg); } // Successive periods in bucket names are not allowed. if (name.matches("\\.\\.")) { String msg = "bucket name cannot contain successive periods. For more information refer " + "http://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html"; throw new InvalidBucketNameException(name, msg); } // Bucket names should be dns compatible. if (!name.matches("^[a-z0-9][a-z0-9\\.\\-]+[a-z0-9]$")) { String msg = "bucket name does not follow Amazon S3 standards. For more information refer " + "http://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html"; throw new InvalidBucketNameException(name, msg); } } /** * Sets HTTP connect, write and read timeouts. A value of 0 means no timeout, otherwise values must be between 1 and * Integer.MAX_VALUE when converted to milliseconds. * * </p><b>Example:</b><br> * <pre>{@code minioClient.setTimeout(TimeUnit.SECONDS.toMillis(10), TimeUnit.SECONDS.toMillis(10), * TimeUnit.SECONDS.toMillis(30)); }</pre> * * @param connectTimeout HTTP connect timeout in milliseconds. * @param writeTimeout HTTP write timeout in milliseconds. * @param readTimeout HTTP read timeout in milliseconds. */ public void setTimeout(long connectTimeout, long writeTimeout, long readTimeout) { httpClient.setConnectTimeout(connectTimeout, TimeUnit.MILLISECONDS); httpClient.setWriteTimeout(writeTimeout, TimeUnit.MILLISECONDS); httpClient.setReadTimeout(readTimeout, TimeUnit.MILLISECONDS); } /** * Creates Request object for given request parameters. * * @param method HTTP method. * @param bucketName Bucket name. * @param objectName Object name in the bucket. * @param region Amazon S3 region of the bucket. * @param headerMap Map of HTTP headers for the request. * @param queryParamMap Map of HTTP query parameters of the request. * @param contentType Content type of the request body. * @param body HTTP request body. * @param length Length of HTTP request body. */ private Request createRequest(Method method, String bucketName, String objectName, String region, Map<String, String> headerMap, Map<String, String> queryParamMap, final String contentType, final Object body, final int length) throws InvalidBucketNameException, NoSuchAlgorithmException, InsufficientDataException, IOException { if (bucketName == null && objectName != null) { throw new InvalidBucketNameException(NULL_STRING, "null bucket name for object '" + objectName + "'"); } HttpUrl.Builder urlBuilder = this.baseUrl.newBuilder(); if (bucketName != null) { checkBucketName(bucketName); String host = this.baseUrl.host(); if (host.equals(S3_AMAZONAWS_COM)) { // special case: handle s3.amazonaws.com separately if (region != null) { host = AwsS3Endpoints.INSTANCE.endpoint(region); } boolean usePathStyle = false; if (method == Method.PUT && objectName == null && queryParamMap == null) { // use path style for make bucket to workaround "AuthorizationHeaderMalformed" error from s3.amazonaws.com usePathStyle = true; } else if (queryParamMap != null && queryParamMap.containsKey("location")) { // use path style for location query usePathStyle = true; } else if (bucketName.contains(".") && this.baseUrl.isHttps()) { // use path style where '.' in bucketName causes SSL certificate validation error usePathStyle = true; } if (usePathStyle) { urlBuilder.host(host); urlBuilder.addPathSegment(bucketName); } else { urlBuilder.host(bucketName + "." + host); } } else { urlBuilder.addPathSegment(bucketName); } } if (objectName != null) { for (String pathSegment : objectName.split("/")) { // Limitation: // 1. OkHttp does not allow to add '.' and '..' as path segment. // 2. Its not allowed to add path segment as '/', '//', '/usr' or 'usr/'. urlBuilder.addPathSegment(pathSegment); } } if (queryParamMap != null) { for (Map.Entry<String, String> entry : queryParamMap.entrySet()) { urlBuilder.addQueryParameter(entry.getKey(), entry.getValue()); } } RequestBody requestBody = null; if (body != null) { requestBody = new RequestBody() { @Override public MediaType contentType() { if (contentType != null) { return MediaType.parse(contentType); } else { return MediaType.parse("application/octet-stream"); } } @Override public long contentLength() { if (body instanceof InputStream || body instanceof RandomAccessFile || body instanceof byte[]) { return length; } if (length == 0) { return -1; } else { return length; } } @Override public void writeTo(BufferedSink sink) throws IOException { byte[] data = null; if (body instanceof InputStream) { InputStream stream = (InputStream) body; sink.write(Okio.source(stream), length); } else if (body instanceof RandomAccessFile) { RandomAccessFile file = (RandomAccessFile) body; sink.write(Okio.source(Channels.newInputStream(file.getChannel())), length); } else if (body instanceof byte[]) { sink.write(data, 0, length); } else { sink.writeUtf8(body.toString()); } } }; } HttpUrl url = urlBuilder.build(); // urlBuilder does not encode some characters properly for Amazon S3. // Encode such characters properly here. List<String> pathSegments = url.encodedPathSegments(); urlBuilder = url.newBuilder(); for (int i = 0; i < pathSegments.size(); i++) { urlBuilder.setEncodedPathSegment(i, pathSegments.get(i).replaceAll("\\!", "%21").replaceAll("\\$", "%24").replaceAll("\\&", "%26") .replaceAll("\\'", "%27").replaceAll("\\(", "%28").replaceAll("\\)", "%29") .replaceAll("\\*", "%2A").replaceAll("\\+", "%2B").replaceAll("\\,", "%2C") .replaceAll("\\:", "%3A").replaceAll("\\;", "%3B").replaceAll("\\=", "%3D") .replaceAll("\\@", "%40").replaceAll("\\[", "%5B").replaceAll("\\]", "%5D")); } url = urlBuilder.build(); Request.Builder requestBuilder = new Request.Builder(); requestBuilder.url(url); requestBuilder.method(method.toString(), requestBody); if (headerMap != null) { for (Map.Entry<String, String> entry : headerMap.entrySet()) { requestBuilder.header(entry.getKey(), entry.getValue()); } } String sha256Hash = null; String md5Hash = null; if (this.accessKey != null && this.secretKey != null) { // No need to compute sha256 if endpoint scheme is HTTPS. Issue #415. if (url.isHttps()) { sha256Hash = "UNSIGNED-PAYLOAD"; if (body instanceof BufferedInputStream) { md5Hash = Digest.md5Hash((BufferedInputStream) body, length); } else if (body instanceof RandomAccessFile) { md5Hash = Digest.md5Hash((RandomAccessFile) body, length); } else if (body instanceof byte[]) { byte[] data = (byte[]) body; md5Hash = Digest.md5Hash(data, length); } } else { if (body == null) { sha256Hash = Digest.sha256Hash(new byte[0]); } else { if (body instanceof BufferedInputStream) { String[] hashes = Digest.sha256md5Hashes((BufferedInputStream) body, length); sha256Hash = hashes[0]; md5Hash = hashes[1]; } else if (body instanceof RandomAccessFile) { String[] hashes = Digest.sha256md5Hashes((RandomAccessFile) body, length); sha256Hash = hashes[0]; md5Hash = hashes[1]; } else if (body instanceof byte[]) { byte[] data = (byte[]) body; sha256Hash = Digest.sha256Hash(data, length); md5Hash = Digest.md5Hash(data, length); } else { sha256Hash = Digest.sha256Hash(body.toString()); } } } } if (md5Hash != null) { requestBuilder.header("Content-MD5", md5Hash); } if (url.port() == 80 || url.port() == 443) { requestBuilder.header("Host", url.host()); } else { requestBuilder.header("Host", url.host() + ":" + url.port()); } requestBuilder.header("User-Agent", this.userAgent); if (sha256Hash != null) { requestBuilder.header("x-amz-content-sha256", sha256Hash); } DateTime date = new DateTime(); requestBuilder.header("x-amz-date", date.toString(DateFormat.AMZ_DATE_FORMAT)); return requestBuilder.build(); } /** * Executes given request parameters. * * @param method HTTP method. * @param region Amazon S3 region of the bucket. * @param bucketName Bucket name. * @param objectName Object name in the bucket. * @param headerMap Map of HTTP headers for the request. * @param queryParamMap Map of HTTP query parameters of the request. * @param contentType Content type of the request body. * @param body HTTP request body. * @param length Length of HTTP request body. */ private HttpResponse execute(Method method, String region, String bucketName, String objectName, Map<String, String> headerMap, Map<String, String> queryParamMap, String contentType, Object body, int length) throws InvalidBucketNameException, NoSuchAlgorithmException, InsufficientDataException, IOException, InvalidKeyException, NoResponseException, XmlPullParserException, ErrorResponseException, InternalException { Request request = createRequest(method, bucketName, objectName, region, headerMap, queryParamMap, contentType, body, length); if (this.accessKey != null && this.secretKey != null) { request = Signer.signV4(request, region, accessKey, secretKey); } if (this.traceStream != null) { this.traceStream.println("---------START-HTTP---------"); String encodedPath = request.httpUrl().encodedPath(); String encodedQuery = request.httpUrl().encodedQuery(); if (encodedQuery != null) { encodedPath += "?" + encodedQuery; } this.traceStream.println(request.method() + " " + encodedPath + " HTTP/1.1"); String headers = request.headers().toString().replaceAll("Signature=([0-9a-f]+)", "Signature=*REDACTED*"); this.traceStream.println(headers); } Response response = this.httpClient.newCall(request).execute(); if (response == null) { if (this.traceStream != null) { this.traceStream.println("<NO RESPONSE>"); this.traceStream.println(END_HTTP); } throw new NoResponseException(); } if (this.traceStream != null) { this.traceStream.println(response.protocol().toString().toUpperCase() + " " + response.code()); this.traceStream.println(response.headers()); } ResponseHeader header = new ResponseHeader(); HeaderParser.set(response.headers(), header); if (response.isSuccessful()) { if (this.traceStream != null) { this.traceStream.println(END_HTTP); } return new HttpResponse(header, response); } ErrorResponse errorResponse = null; // HEAD returns no body, and fails on parseXml if (!method.equals(Method.HEAD)) { try { String errorXml = ""; // read entire body stream to string. Scanner scanner = new java.util.Scanner(response.body().charStream()).useDelimiter("\\A"); if (scanner.hasNext()) { errorXml = scanner.next(); } errorResponse = new ErrorResponse(new StringReader(errorXml)); if (this.traceStream != null) { this.traceStream.println(errorXml); } } finally { response.body().close(); } } if (this.traceStream != null) { this.traceStream.println(END_HTTP); } if (errorResponse == null) { ErrorCode ec; switch (response.code()) { case 400: ec = ErrorCode.INVALID_URI; break; case 404: if (objectName != null) { ec = ErrorCode.NO_SUCH_KEY; } else if (bucketName != null) { ec = ErrorCode.NO_SUCH_BUCKET; } else { ec = ErrorCode.RESOURCE_NOT_FOUND; } break; case 501: case 405: ec = ErrorCode.METHOD_NOT_ALLOWED; break; case 409: if (bucketName != null) { ec = ErrorCode.NO_SUCH_BUCKET; } else { ec = ErrorCode.RESOURCE_CONFLICT; } break; case 403: ec = ErrorCode.ACCESS_DENIED; break; default: throw new InternalException("unhandled HTTP code " + response.code() + ". Please report this issue at " + "https://github.com/minio/minio-java/issues"); } errorResponse = new ErrorResponse(ec, bucketName, objectName, request.httpUrl().encodedPath(), header.xamzRequestId(), header.xamzId2()); } // invalidate region cache if needed if (errorResponse.errorCode() == ErrorCode.NO_SUCH_BUCKET) { BucketRegionCache.INSTANCE.remove(bucketName); // TODO: handle for other cases as well // observation: on HEAD of a bucket with wrong region gives 400 without body } throw new ErrorResponseException(errorResponse, response); } /** * Updates Region cache for given bucket. */ private void updateRegionCache(String bucketName) throws InvalidBucketNameException, NoSuchAlgorithmException, InsufficientDataException, IOException, InvalidKeyException, NoResponseException, XmlPullParserException, ErrorResponseException, InternalException { if (bucketName != null && S3_AMAZONAWS_COM.equals(this.baseUrl.host()) && this.accessKey != null && this.secretKey != null && !BucketRegionCache.INSTANCE.exists(bucketName)) { Map<String, String> queryParamMap = new HashMap<>(); queryParamMap.put("location", null); HttpResponse response = execute(Method.GET, US_EAST_1, bucketName, null, null, queryParamMap, null, null, 0); // existing XmlEntity does not work, so fallback to regular parsing. XmlPullParser xpp = xmlPullParserFactory.newPullParser(); String location = null; xpp.setInput(response.body().charStream()); while (xpp.getEventType() != xpp.END_DOCUMENT) { if (xpp.getEventType() == xpp.START_TAG && xpp.getName() == "LocationConstraint") { xpp.next(); location = getText(xpp, location); break; } xpp.next(); } // close response body. response.body().close(); String region; if (location == null) { region = US_EAST_1; } else { // eu-west-1 can be sometimes 'EU'. if ("EU".equals(location)) { region = "eu-west-1"; } else { region = location; } } // Add the new location. BucketRegionCache.INSTANCE.add(bucketName, region); } } /** * Returns text of given XML element. */ private String getText(XmlPullParser xpp, String location) throws XmlPullParserException { if (xpp.getEventType() == xpp.TEXT) { return xpp.getText(); } return location; } /** * Executes GET method for given request parameters. * * @param bucketName Bucket name. * @param objectName Object name in the bucket. * @param headerMap Map of HTTP headers for the request. * @param queryParamMap Map of HTTP query parameters of the request. */ private HttpResponse executeGet(String bucketName, String objectName, Map<String, String> headerMap, Map<String, String> queryParamMap) throws InvalidBucketNameException, NoSuchAlgorithmException, InsufficientDataException, IOException, InvalidKeyException, NoResponseException, XmlPullParserException, ErrorResponseException, InternalException { updateRegionCache(bucketName); return execute(Method.GET, BucketRegionCache.INSTANCE.region(bucketName), bucketName, objectName, headerMap, queryParamMap, null, null, 0); } /** * Executes HEAD method for given request parameters. * * @param bucketName Bucket name. * @param objectName Object name in the bucket. */ private HttpResponse executeHead(String bucketName, String objectName) throws InvalidBucketNameException, NoSuchAlgorithmException, InsufficientDataException, IOException, InvalidKeyException, NoResponseException, XmlPullParserException, ErrorResponseException, InternalException { updateRegionCache(bucketName); HttpResponse response = execute(Method.HEAD, BucketRegionCache.INSTANCE.region(bucketName), bucketName, objectName, null, null, null, null, 0); response.body().close(); return response; } /** * Executes DELETE method for given request parameters. * * @param bucketName Bucket name. * @param objectName Object name in the bucket. * @param queryParamMap Map of HTTP query parameters of the request. */ private HttpResponse executeDelete(String bucketName, String objectName, Map<String, String> queryParamMap) throws InvalidBucketNameException, NoSuchAlgorithmException, InsufficientDataException, IOException, InvalidKeyException, NoResponseException, XmlPullParserException, ErrorResponseException, InternalException { updateRegionCache(bucketName); HttpResponse response = execute(Method.DELETE, BucketRegionCache.INSTANCE.region(bucketName), bucketName, objectName, null, queryParamMap, null, null, 0); response.body().close(); return response; } /** * Executes POST method for given request parameters. * * @param bucketName Bucket name. * @param objectName Object name in the bucket. * @param headerMap Map of HTTP headers for the request. * @param queryParamMap Map of HTTP query parameters of the request. * @param data HTTP request body data. */ private HttpResponse executePost(String bucketName, String objectName, Map<String, String> headerMap, Map<String, String> queryParamMap, Object data) throws InvalidBucketNameException, NoSuchAlgorithmException, InsufficientDataException, IOException, InvalidKeyException, NoResponseException, XmlPullParserException, ErrorResponseException, InternalException { updateRegionCache(bucketName); return execute(Method.POST, BucketRegionCache.INSTANCE.region(bucketName), bucketName, objectName, headerMap, queryParamMap, null, data, 0); } /** * Executes PUT method for given request parameters. * * @param bucketName Bucket name. * @param objectName Object name in the bucket. * @param headerMap Map of HTTP headers for the request. * @param queryParamMap Map of HTTP query parameters of the request. * @param region Amazon S3 region of the bucket. * @param data HTTP request body data. * @param length Length of HTTP request body data. */ private HttpResponse executePut(String bucketName, String objectName, Map<String, String> headerMap, Map<String, String> queryParamMap, String region, Object data, int length) throws InvalidBucketNameException, NoSuchAlgorithmException, InsufficientDataException, IOException, InvalidKeyException, NoResponseException, XmlPullParserException, ErrorResponseException, InternalException { HttpResponse response = execute(Method.PUT, region, bucketName, objectName, headerMap, queryParamMap, "", data, length); response.body().close(); return response; } /** * Executes PUT method for given request parameters. * * @param bucketName Bucket name. * @param objectName Object name in the bucket. * @param headerMap Map of HTTP headers for the request. * @param queryParamMap Map of HTTP query parameters of the request. * @param data HTTP request body data. * @param length Length of HTTP request body data. */ private HttpResponse executePut(String bucketName, String objectName, Map<String, String> headerMap, Map<String, String> queryParamMap, Object data, int length) throws InvalidBucketNameException, NoSuchAlgorithmException, InsufficientDataException, IOException, InvalidKeyException, NoResponseException, XmlPullParserException, ErrorResponseException, InternalException { updateRegionCache(bucketName); return executePut(bucketName, objectName, headerMap, queryParamMap, BucketRegionCache.INSTANCE.region(bucketName), data, length); } /** * Sets application's name/version to user agent. For more information about user agent * refer <a href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html">#rfc2616</a>. * * @param name Your application name. * @param version Your application version. */ @SuppressWarnings("unused") public void setAppInfo(String name, String version) { if (name == null || version == null) { // nothing to do return; } this.userAgent = DEFAULT_USER_AGENT + " " + name.trim() + "/" + version.trim(); } /** * Returns meta data information of given object in given bucket. * * </p><b>Example:</b><br> * <pre>{@code ObjectStat objectStat = minioClient.statObject("my-bucketname", "my-objectname"); * System.out.println(objectStat); }</pre> * * @param bucketName Bucket name. * @param objectName Object name in the bucket. * * @return Populated object metadata. * * @throws InvalidBucketNameException upon invalid bucket name is given * @throws NoResponseException upon no response from server * @throws IOException upon connection error * @throws XmlPullParserException upon parsing response xml * @throws ErrorResponseException upon unsuccessful execution * @throws InternalException upon internal library error * @see ObjectStat */ public ObjectStat statObject(String bucketName, String objectName) throws InvalidBucketNameException, NoSuchAlgorithmException, InsufficientDataException, IOException, InvalidKeyException, NoResponseException, XmlPullParserException, ErrorResponseException, InternalException { HttpResponse response = executeHead(bucketName, objectName); ResponseHeader header = response.header(); return new ObjectStat(bucketName, objectName, header.lastModified(), header.contentLength(), header.etag(), header.contentType()); } /** * Gets entire object's data as {@link InputStream} in given bucket. The InputStream must be closed * after use else the connection will remain open. * * <p><b>Example:</b> * <pre>{@code InputStream stream = minioClient.getObject("my-bucketname", "my-objectname"); * byte[] buf = new byte[16384]; * int bytesRead; * while ((bytesRead = stream.read(buf, 0, buf.length)) >= 0) { * System.out.println(new String(buf, 0, bytesRead)); * } * stream.close(); }</pre> * * @param bucketName Bucket name. * @param objectName Object name in the bucket. * * @return {@link InputStream} containing the object data. * * @throws InvalidBucketNameException upon invalid bucket name is given * @throws NoResponseException upon no response from server * @throws IOException upon connection error * @throws XmlPullParserException upon parsing response xml * @throws ErrorResponseException upon unsuccessful execution * @throws InternalException upon internal library error */ public InputStream getObject(String bucketName, String objectName) throws InvalidBucketNameException, NoSuchAlgorithmException, InsufficientDataException, IOException, InvalidKeyException, NoResponseException, XmlPullParserException, ErrorResponseException, InternalException, InvalidArgumentException { return getObject(bucketName, objectName, 0, null); } /** * Gets object's data starting from given offset as {@link InputStream} in the given bucket. The InputStream must be * closed after use else the connection will remain open. * * </p><b>Example:</b><br> * <pre>{@code InputStream stream = minioClient.getObject("my-bucketname", "my-objectname", 1024L); * byte[] buf = new byte[16384]; * int bytesRead; * while ((bytesRead = stream.read(buf, 0, buf.length)) >= 0) { * System.out.println(new String(buf, 0, bytesRead)); * } * stream.close(); }</pre> * * @param bucketName Bucket name. * @param objectName Object name in the bucket. * @param offset Offset to read at. * * @return {@link InputStream} containing the object's data. * * @throws InvalidBucketNameException upon invalid bucket name is given * @throws NoResponseException upon no response from server * @throws IOException upon connection error * @throws XmlPullParserException upon parsing response xml * @throws ErrorResponseException upon unsuccessful execution * @throws InternalException upon internal library error */ public InputStream getObject(String bucketName, String objectName, long offset) throws InvalidBucketNameException, NoSuchAlgorithmException, InsufficientDataException, IOException, InvalidKeyException, NoResponseException, XmlPullParserException, ErrorResponseException, InternalException, InvalidArgumentException { return getObject(bucketName, objectName, offset, null); } /** * Gets object's data of given offset and length as {@link InputStream} in the given bucket. The InputStream must be * closed after use else the connection will remain open. * * </p><b>Example:</b><br> * <pre>{@code InputStream stream = minioClient.getObject("my-bucketname", "my-objectname", 1024L, 4096L); * byte[] buf = new byte[16384]; * int bytesRead; * while ((bytesRead = stream.read(buf, 0, buf.length)) >= 0) { * System.out.println(new String(buf, 0, bytesRead)); * } * stream.close(); }</pre> * * @param bucketName Bucket name. * @param objectName Object name in the bucket. * @param offset Offset to read at. * @param length Length to read. * * @return {@link InputStream} containing the object's data. * * @throws InvalidBucketNameException upon invalid bucket name is given * @throws NoResponseException upon no response from server * @throws IOException upon connection error * @throws XmlPullParserException upon parsing response xml * @throws ErrorResponseException upon unsuccessful execution * @throws InternalException upon internal library error */ public InputStream getObject(String bucketName, String objectName, long offset, Long length) throws InvalidBucketNameException, NoSuchAlgorithmException, InsufficientDataException, IOException, InvalidKeyException, NoResponseException, XmlPullParserException, ErrorResponseException, InternalException, InvalidArgumentException { if (offset < 0) { throw new InvalidArgumentException("offset should be zero or greater"); } if (length != null && length <= 0) { throw new InvalidArgumentException("length should be greater than zero"); } Map<String, String> headerMap = new HashMap<>(); if (length != null) { headerMap.put("Range", "bytes=" + offset + "-" + (offset + length - 1)); } else { headerMap.put("Range", "bytes=" + offset + "-"); } HttpResponse response = executeGet(bucketName, objectName, headerMap, null); return response.body().byteStream(); } /** * Gets object's data in the given bucket and stores it to given file name. * * </p><b>Example:</b><br> * <pre>{@code minioClient.getObject("my-bucketname", "my-objectname", "photo.jpg"); }</pre> * * @param bucketName Bucket name. * @param objectName Object name in the bucket. * @param fileName file name. * * @throws InvalidBucketNameException upon invalid bucket name is given * @throws NoResponseException upon no response from server * @throws IOException upon connection error * @throws XmlPullParserException upon parsing response xml * @throws ErrorResponseException upon unsuccessful execution * @throws InternalException upon internal library error */ public void getObject(String bucketName, String objectName, String fileName) throws InvalidBucketNameException, NoSuchAlgorithmException, InsufficientDataException, IOException, InvalidKeyException, NoResponseException, XmlPullParserException, ErrorResponseException, InternalException, InvalidArgumentException { Path filePath = Paths.get(fileName); boolean fileExists = Files.exists(filePath); if (fileExists && !Files.isRegularFile(filePath)) { throw new InvalidArgumentException(fileName + ": not a regular file"); } ObjectStat objectStat = statObject(bucketName, objectName); long length = objectStat.length(); String etag = objectStat.etag(); String tempFileName = fileName + "." + etag + ".part.minio"; Path tempFilePath = Paths.get(tempFileName); boolean tempFileExists = Files.exists(tempFilePath); if (tempFileExists && !Files.isRegularFile(tempFilePath)) { throw new IOException(tempFileName + ": not a regular file"); } long tempFileSize = 0; if (tempFileExists) { tempFileSize = Files.size(tempFilePath); if (tempFileSize > length) { Files.delete(tempFilePath); tempFileExists = false; tempFileSize = 0; } } if (fileExists) { long fileSize = Files.size(filePath); if (fileSize == length) { // already downloaded. nothing to do return; } else if (fileSize > length) { throw new InvalidArgumentException( "'" + fileName + "': object size " + length + " is smaller than file size " + fileSize); } else if (!tempFileExists) { // before resuming the download, copy filename to tempfilename Files.copy(filePath, tempFilePath); tempFileSize = fileSize; tempFileExists = true; } } InputStream is = null; OutputStream os = null; try { is = getObject(bucketName, objectName, tempFileSize); os = Files.newOutputStream(tempFilePath, StandardOpenOption.CREATE, StandardOpenOption.APPEND); long bytesWritten = ByteStreams.copy(is, os); is.close(); os.close(); if (bytesWritten != length - tempFileSize) { throw new IOException(tempFileName + ": unexpected data written. expected = " + (length - tempFileSize) + ", written = " + bytesWritten); } Files.move(tempFilePath, filePath, StandardCopyOption.REPLACE_EXISTING); } finally { if (is != null) { is.close(); } if (os != null) { os.close(); } } } /** * Returns an presigned URL to download the object in the bucket with given expiry time. * * </p><b>Example:</b><br> * <pre>{@code String url = minioClient.presignedGetObject("my-bucketname", "my-objectname", 60 * 60 * 24); * System.out.println(url); }</pre> * * @param bucketName Bucket name. * @param objectName Object name in the bucket. * @param expires Expiration time in seconds of presigned URL. * * @return string contains URL to download the object. * * @throws InvalidBucketNameException upon an invalid bucket name * @throws InvalidKeyException upon an invalid access key or secret key * @throws IOException upon signature calculation failure * @throws NoSuchAlgorithmException upon requested algorithm was not found during signature calculation * @throws InvalidExpiresRangeException upon input expires is out of range */ public String presignedGetObject(String bucketName, String objectName, Integer expires) throws InvalidBucketNameException, NoSuchAlgorithmException, InsufficientDataException, IOException, InvalidKeyException, NoResponseException, XmlPullParserException, ErrorResponseException, InternalException, InvalidExpiresRangeException { // Validate input. if (expires < 1 || expires > DEFAULT_EXPIRY_TIME) { throw new InvalidExpiresRangeException(expires, "expires must be in range of 1 to " + DEFAULT_EXPIRY_TIME); } updateRegionCache(bucketName); String region = BucketRegionCache.INSTANCE.region(bucketName); Request request = createRequest(Method.GET, bucketName, objectName, region, null, null, null, null, 0); HttpUrl url = Signer.presignV4(request, region, accessKey, secretKey, expires); return url.toString(); } /** * Returns an presigned URL to download the object in the bucket with default expiry time. * Default expiry time is 7 days in seconds. * * </p><b>Example:</b><br> * <pre>{@code String url = minioClient.presignedGetObject("my-bucketname", "my-objectname"); * System.out.println(url); }</pre> * * @param bucketName Bucket name. * @param objectName Object name in the bucket. * * @return string contains URL to download the object * * @throws IOException upon connection error * @throws NoSuchAlgorithmException upon requested algorithm was not found during signature calculation * @throws InvalidExpiresRangeException upon input expires is out of range */ public String presignedGetObject(String bucketName, String objectName) throws InvalidBucketNameException, NoSuchAlgorithmException, InsufficientDataException, IOException, InvalidKeyException, NoResponseException, XmlPullParserException, ErrorResponseException, InternalException, InvalidExpiresRangeException { return presignedGetObject(bucketName, objectName, DEFAULT_EXPIRY_TIME); } /** * Returns a presigned URL to upload an object in the bucket with given expiry time. * * </p><b>Example:</b><br> * <pre>{@code String url = minioClient.presignedPutObject("my-bucketname", "my-objectname", 60 * 60 * 24); * System.out.println(url); }</pre> * * @param bucketName Bucket name * @param objectName Object name in the bucket * @param expires Expiration time in seconds to presigned URL. * * @return string contains URL to upload the object. * * @throws InvalidBucketNameException upon an invalid bucket name * @throws InvalidKeyException upon an invalid access key or secret key * @throws IOException upon signature calculation failure * @throws NoSuchAlgorithmException upon requested algorithm was not found during signature calculation * @throws InvalidExpiresRangeException upon input expires is out of range */ public String presignedPutObject(String bucketName, String objectName, Integer expires) throws InvalidBucketNameException, NoSuchAlgorithmException, InsufficientDataException, IOException, InvalidKeyException, NoResponseException, XmlPullParserException, ErrorResponseException, InternalException, InvalidExpiresRangeException { if (expires < 1 || expires > DEFAULT_EXPIRY_TIME) { throw new InvalidExpiresRangeException(expires, "expires must be in range of 1 to " + DEFAULT_EXPIRY_TIME); } updateRegionCache(bucketName); String region = BucketRegionCache.INSTANCE.region(bucketName); Request request = createRequest(Method.PUT, bucketName, objectName, region, null, null, null, "", 0); HttpUrl url = Signer.presignV4(request, region, accessKey, secretKey, expires); return url.toString(); } /** * Returns a presigned URL to upload an object in the bucket with default expiry time. * Default expiry time is 7 days in seconds. * * </p><b>Example:</b><br> * <pre>{@code String url = minioClient.presignedPutObject("my-bucketname", "my-objectname"); * System.out.println(url); }</pre> * * @param bucketName Bucket name. * @param objectName Object name in the bucket. * * @return string contains URL to upload the object. * * @throws IOException upon connection error * @throws NoSuchAlgorithmException upon requested algorithm was not found during signature calculation * @throws InvalidExpiresRangeException upon input expires is out of range */ public String presignedPutObject(String bucketName, String objectName) throws InvalidBucketNameException, NoSuchAlgorithmException, InsufficientDataException, IOException, InvalidKeyException, NoResponseException, XmlPullParserException, ErrorResponseException, InternalException, InvalidExpiresRangeException { return presignedPutObject(bucketName, objectName, DEFAULT_EXPIRY_TIME); } /** * Returns string map for given {@link PostPolicy} to upload object with various post policy conditions. * * </p><b>Example:</b><br> * <pre>{@code // Create new PostPolicy object for 'my-bucketname', 'my-objectname' and 7 days expire time from now. * PostPolicy policy = new PostPolicy("my-bucketname", "my-objectname", DateTime.now().plusDays(7)); * // 'my-objectname' should be 'image/png' content type * policy.setContentType("image/png"); * Map<String,String> formData = minioClient.presignedPostPolicy(policy); * // Print a curl command that can be executable with the file /tmp/userpic.png and the file will be uploaded. * System.out.print("curl -X POST "); * for (Map.Entry<String,String> entry : formData.entrySet()) { * System.out.print(" -F " + entry.getKey() + "=" + entry.getValue()); * } * System.out.println(" -F file=@/tmp/userpic.png https://play.minio.io:9000/my-bucketname"); }</pre> * * @param policy Post policy of an object. * @return Map of strings to construct form-data. * * @see PostPolicy */ public Map<String, String> presignedPostPolicy(PostPolicy policy) throws InvalidBucketNameException, NoSuchAlgorithmException, InsufficientDataException, IOException, InvalidKeyException, NoResponseException, XmlPullParserException, ErrorResponseException, InternalException { updateRegionCache(policy.bucketName()); return policy.formData(this.accessKey, this.secretKey); } /** * Removes an object from a bucket. * * </p><b>Example:</b><br> * <pre>{@code minioClient.removeObject("my-bucketname", "my-objectname"); }</pre> * * @param bucketName Bucket name. * @param objectName Object name in the bucket. * * @throws InvalidBucketNameException upon invalid bucket name is given * @throws NoResponseException upon no response from server * @throws IOException upon connection error * @throws XmlPullParserException upon parsing response xml * @throws ErrorResponseException upon unsuccessful execution * @throws InternalException upon internal library error */ public void removeObject(String bucketName, String objectName) throws InvalidBucketNameException, NoSuchAlgorithmException, InsufficientDataException, IOException, InvalidKeyException, NoResponseException, XmlPullParserException, ErrorResponseException, InternalException { executeDelete(bucketName, objectName, null); } /** * Lists object information in given bucket. * * @param bucketName Bucket name. * * @return an iterator of Result Items. */ public Iterable<Result<Item>> listObjects(final String bucketName) throws XmlPullParserException { return listObjects(bucketName, null); } /** * Lists object information in given bucket and prefix. * * @param bucketName Bucket name. * @param prefix Prefix string. List objects whose name starts with `prefix`. * * @return an iterator of Result Items. */ public Iterable<Result<Item>> listObjects(final String bucketName, final String prefix) throws XmlPullParserException { // list all objects recursively return listObjects(bucketName, prefix, true); } /** * Lists object information as {@code Iterable<Result><Item>>} in given bucket, prefix and recursive flag. * * </p><b>Example:</b><br> * <pre>{@code Iterable<Result<Item>> myObjects = minioClient.listObjects("my-bucketname"); * for (Result<Item> result : myObjects) { * Item item = result.get(); * System.out.println(item.lastModified() + ", " + item.size() + ", " + item.objectName()); * } }</pre> * * @param bucketName Bucket name. * @param prefix Prefix string. List objects whose name starts with `prefix`. * @param recursive when false, emulates a directory structure where each listing returned is either a full object * or part of the object's key up to the first '/'. All objects wit the same prefix up to the first * '/' will be merged into one entry. * * @return an iterator of Result Items. * * @see #listObjects(String bucketName) * @see #listObjects(String bucketName, String prefix) */ public Iterable<Result<Item>> listObjects(final String bucketName, final String prefix, final boolean recursive) { return new Iterable<Result<Item>>() { @Override public Iterator<Result<Item>> iterator() { return new Iterator<Result<Item>>() { private String lastObjectName; private ListBucketResult listBucketResult; private Result<Item> error; private Iterator<Item> itemIterator; private Iterator<Prefix> prefixIterator; private boolean completed = false; private synchronized void populate() { String delimiter = "/"; if (recursive) { delimiter = null; } String marker = null; if (this.listBucketResult != null) { if (delimiter != null) { marker = listBucketResult.nextMarker(); } else { marker = this.lastObjectName; } } this.listBucketResult = null; this.itemIterator = null; this.prefixIterator = null; try { this.listBucketResult = listObjects(bucketName, marker, prefix, delimiter, null); } catch (InvalidBucketNameException | NoSuchAlgorithmException | InsufficientDataException | IOException | InvalidKeyException | NoResponseException | XmlPullParserException | ErrorResponseException | InternalException e) { this.error = new Result<>(null, e); } finally { if (this.listBucketResult != null) { this.itemIterator = this.listBucketResult.contents().iterator(); this.prefixIterator = this.listBucketResult.commonPrefixes().iterator(); } else { this.itemIterator = new LinkedList<Item>().iterator(); this.prefixIterator = new LinkedList<Prefix>().iterator(); } } } @Override public boolean hasNext() { if (this.completed) { return false; } if (this.error == null && this.itemIterator == null && this.prefixIterator == null) { populate(); } if (this.error == null && !this.itemIterator.hasNext() && !this.prefixIterator.hasNext() && this.listBucketResult.isTruncated()) { populate(); } if (this.error != null) { return true; } if (this.itemIterator.hasNext()) { return true; } if (this.prefixIterator.hasNext()) { return true; } this.completed = true; return false; } @Override public Result<Item> next() { if (this.completed) { throw new NoSuchElementException(); } if (this.error == null && this.itemIterator == null && this.prefixIterator == null) { populate(); } if (this.error == null && !this.itemIterator.hasNext() && !this.prefixIterator.hasNext() && this.listBucketResult.isTruncated()) { populate(); } if (this.error != null) { this.completed = true; return this.error; } if (this.itemIterator.hasNext()) { Item item = this.itemIterator.next(); this.lastObjectName = item.objectName(); return new Result<>(item, null); } if (this.prefixIterator.hasNext()) { Prefix prefix = this.prefixIterator.next(); Item item; try { item = new Item(prefix.prefix(), true); } catch (XmlPullParserException e) { // special case: ignore the error as we can't propagate the exception in next() item = null; } return new Result<>(item, null); } this.completed = true; throw new NoSuchElementException(); } @Override public void remove() { throw new UnsupportedOperationException(); } }; } }; } /** * Returns {@link ListBucketResult} of given bucket, marker, prefix, delimiter and maxKeys. * * @param bucketName Bucket name. * @param marker Marker string. List objects whose name is greater than `marker`. * @param prefix Prefix string. List objects whose name starts with `prefix`. * @param delimiter delimiter string. Group objects whose name contains `delimiter`. * @param maxKeys Maximum number of entries to be returned. */ private ListBucketResult listObjects(String bucketName, String marker, String prefix, String delimiter, Integer maxKeys) throws InvalidBucketNameException, NoSuchAlgorithmException, InsufficientDataException, IOException, InvalidKeyException, NoResponseException, XmlPullParserException, ErrorResponseException, InternalException { Map<String, String> queryParamMap = new HashMap<>(); if (marker != null) { queryParamMap.put("marker", marker); } if (prefix != null) { queryParamMap.put("prefix", prefix); } if (delimiter != null) { queryParamMap.put("delimiter", delimiter); } if (maxKeys != null) { queryParamMap.put("max-keys", maxKeys.toString()); } HttpResponse response = executeGet(bucketName, null, null, queryParamMap); ListBucketResult result = new ListBucketResult(); result.parseXml(response.body().charStream()); response.body().close(); return result; } /** * Returns all bucket information owned by the current user. * * </p><b>Example:</b><br> * <pre>{@code List<Bucket> bucketList = minioClient.listBuckets(); * for (Bucket bucket : bucketList) { * System.out.println(bucket.creationDate() + ", " + bucket.name()); * } }</pre> * * @return List of bucket type. * * @throws NoResponseException upon no response from server * @throws IOException upon connection error * @throws XmlPullParserException upon parsing response xml * @throws ErrorResponseException upon unsuccessful execution * @throws InternalException upon internal library error */ public List<Bucket> listBuckets() throws InvalidBucketNameException, NoSuchAlgorithmException, InsufficientDataException, IOException, InvalidKeyException, NoResponseException, XmlPullParserException, ErrorResponseException, InternalException { HttpResponse response = executeGet(null, null, null, null); ListAllMyBucketsResult result = new ListAllMyBucketsResult(); result.parseXml(response.body().charStream()); response.body().close(); return result.buckets(); } /** * Checks if given bucket exist and is having read access. * * </p><b>Example:</b><br> * <pre>{@code boolean found = minioClient.bucketExists("my-bucketname"); * if (found) { * System.out.println("my-bucketname exists"); * } else { * System.out.println("my-bucketname does not exist"); * } }</pre> * * @param bucketName Bucket name. * * @return True if the bucket exists and the user has at least read access. * * @throws InvalidBucketNameException upon invalid bucket name is given * @throws NoResponseException upon no response from server * @throws IOException upon connection error * @throws XmlPullParserException upon parsing response xml * @throws ErrorResponseException upon unsuccessful execution * @throws InternalException upon internal library error */ public boolean bucketExists(String bucketName) throws InvalidBucketNameException, NoSuchAlgorithmException, InsufficientDataException, IOException, InvalidKeyException, NoResponseException, XmlPullParserException, ErrorResponseException, InternalException { try { executeHead(bucketName, null); return true; } catch (ErrorResponseException e) { if (e.errorResponse().errorCode() != ErrorCode.NO_SUCH_BUCKET) { throw e; } } return false; } /** * Creates a bucket with default region. * * @param bucketName Bucket name. * * @throws InvalidBucketNameException upon invalid bucket name is given * @throws NoResponseException upon no response from server * @throws IOException upon connection error * @throws XmlPullParserException upon parsing response xml * @throws ErrorResponseException upon unsuccessful execution * @throws InternalException upon internal library error */ public void makeBucket(String bucketName) throws InvalidBucketNameException, NoSuchAlgorithmException, InsufficientDataException, IOException, InvalidKeyException, NoResponseException, XmlPullParserException, ErrorResponseException, InternalException { this.makeBucket(bucketName, null); } /** * Creates a bucket with given region. * * </p><b>Example:</b><br> * <pre>{@code minioClient.makeBucket("my-bucketname"); * System.out.println("my-bucketname is created successfully"); }</pre> * * @param bucketName Bucket name. * @param region region in which the bucket will be created. * * @throws InvalidBucketNameException upon invalid bucket name is given * @throws NoResponseException upon no response from server * @throws IOException upon connection error * @throws XmlPullParserException upon parsing response xml * @throws ErrorResponseException upon unsuccessful execution * @throws InternalException upon internal library error */ public void makeBucket(String bucketName, String region) throws InvalidBucketNameException, NoSuchAlgorithmException, InsufficientDataException, IOException, InvalidKeyException, NoResponseException, XmlPullParserException, ErrorResponseException, InternalException { Map<String, String> headerMap = new HashMap<>(); String configString; if (region == null || US_EAST_1.equals(region)) { // for 'us-east-1', location constraint is not required. for more info // http://docs.aws.amazon.com/general/latest/gr/rande.html#s3_region configString = ""; } else { CreateBucketConfiguration config = new CreateBucketConfiguration(region); configString = config.toString(); } executePut(bucketName, null, headerMap, null, US_EAST_1, configString, 0); } /** * Removes a bucket. * <p> * NOTE: - * All objects (including all object versions and delete markers) in the bucket * must be deleted prior, this API will not recursively delete objects * </p> * * </p><b>Example:</b><br> * <pre>{@code minioClient.removeBucket("my-bucketname"); * System.out.println("my-bucketname is removed successfully"); }</pre> * * @param bucketName Bucket name. * * @throws InvalidBucketNameException upon invalid bucket name is given * @throws NoResponseException upon no response from server * @throws IOException upon connection error * @throws XmlPullParserException upon parsing response xml * @throws ErrorResponseException upon unsuccessful execution * @throws InternalException upon internal library error */ public void removeBucket(String bucketName) throws InvalidBucketNameException, NoSuchAlgorithmException, InsufficientDataException, IOException, InvalidKeyException, NoResponseException, XmlPullParserException, ErrorResponseException, InternalException { executeDelete(bucketName, null, null); } /** * Uploads given file as object in given bucket. * <p> * If the object is larger than 5MB, the client will automatically use a multipart session. * </p> * <p> * If the session fails, the user may attempt to re-upload the object by attempting to create * the exact same object again. The client will examine all parts of any current upload session * and attempt to reuse the session automatically. If a mismatch is discovered, the upload will fail * before uploading any more data. Otherwise, it will resume uploading where the session left off. * </p> * <p> * If the multipart session fails, the user is responsible for resuming or removing the session. * </p> * * @param bucketName Bucket name. * @param objectName Object name to create in the bucket. * @param fileName File name to upload. * * @throws InvalidBucketNameException upon invalid bucket name is given * @throws NoResponseException upon no response from server * @throws IOException upon connection error * @throws XmlPullParserException upon parsing response xml * @throws ErrorResponseException upon unsuccessful execution * @throws InternalException upon internal library error */ public void putObject(String bucketName, String objectName, String fileName) throws InvalidBucketNameException, NoSuchAlgorithmException, InsufficientDataException, IOException, InvalidKeyException, NoResponseException, XmlPullParserException, ErrorResponseException, InternalException, InvalidArgumentException, InsufficientDataException { if (fileName == null || "".equals(fileName)) { throw new InvalidArgumentException("empty file name is not allowed"); } Path filePath = Paths.get(fileName); if (!Files.isRegularFile(filePath)) { throw new InvalidArgumentException("'" + fileName + "': not a regular file"); } String contentType = Files.probeContentType(filePath); long size = Files.size(filePath); RandomAccessFile file = new RandomAccessFile(filePath.toFile(), "r"); try { putObject(bucketName, objectName, contentType, size, file); } finally { file.close(); } } /** * Uploads data from given stream as object to given bucket. * <p> * If the object is larger than 5MB, the client will automatically use a multipart session. * </p> * <p> * If the session fails, the user may attempt to re-upload the object by attempting to create * the exact same object again. The client will examine all parts of any current upload session * and attempt to reuse the session automatically. If a mismatch is discovered, the upload will fail * before uploading any more data. Otherwise, it will resume uploading where the session left off. * </p> * <p> * If the multipart session fails, the user is responsible for resuming or removing the session. * </p> * * </p><b>Example:</b><br> * <pre>{@code StringBuilder builder = new StringBuilder(); * for (int i = 0; i < 1000; i++) { * builder.append("Sphinx of black quartz, judge my vow: Used by Adobe InDesign to display font samples. "); * builder.append("(29 letters)\n"); * builder.append("Jackdaws love my big sphinx of quartz: Similarly, used by Windows XP for some fonts. "); * builder.append("(31 letters)\n"); * builder.append("Pack my box with five dozen liquor jugs: According to Wikipedia, this one is used on "); * builder.append("NASAs Space Shuttle. (32 letters)\n"); * builder.append("The quick onyx goblin jumps over the lazy dwarf: Flavor text from an Unhinged Magic Card. "); * builder.append("(39 letters)\n"); * builder.append("How razorback-jumping frogs can level six piqued gymnasts!: Not going to win any brevity "); * builder.append("awards at 49 letters long, but old-time Mac users may recognize it.\n"); * builder.append("Cozy lummox gives smart squid who asks for job pen: A 41-letter tester sentence for Mac "); * builder.append("computers after System 7.\n"); * builder.append("A few others we like: Amazingly few discotheques provide jukeboxes; Now fax quiz Jack! my "); * builder.append("brave ghost pled; Watch Jeopardy!, Alex Trebeks fun TV quiz game.\n"); * builder.append("---\n"); * } * ByteArrayInputStream bais = new ByteArrayInputStream(builder.toString().getBytes("UTF-8")); * // create object * minioClient.putObject("my-bucketname", "my-objectname", bais, bais.available(), "application/octet-stream"); * bais.close(); * System.out.println("my-bucketname is uploaded successfully"); }</pre> * * @param bucketName Bucket name. * @param objectName Object name to create in the bucket. * @param stream stream to upload. * @param size Size of all the data that will be uploaded. * @param contentType Content type of the stream. * * @throws InvalidBucketNameException upon invalid bucket name is given * @throws NoResponseException upon no response from server * @throws IOException upon connection error * @throws XmlPullParserException upon parsing response xml * @throws ErrorResponseException upon unsuccessful execution * @throws InternalException upon internal library error * * @see #putObject(String bucketName, String objectName, String fileName) */ public void putObject(String bucketName, String objectName, InputStream stream, long size, String contentType) throws InvalidBucketNameException, NoSuchAlgorithmException, InsufficientDataException, IOException, InvalidKeyException, NoResponseException, XmlPullParserException, ErrorResponseException, InternalException, InvalidArgumentException, InsufficientDataException { putObject(bucketName, objectName, contentType, size, new BufferedInputStream(stream)); } /** * Executes put object and returns ETag of the object. * * @param bucketName Bucket name. * @param objectName Object name in the bucket. * @param length Length of object data. * @param data Object data. * @param uploadId Upload ID of multipart put object. * @param partNumber Part number of multipart put object. */ private String putObject(String bucketName, String objectName, int length, Object data, String uploadId, int partNumber) throws InvalidBucketNameException, NoSuchAlgorithmException, InsufficientDataException, IOException, InvalidKeyException, NoResponseException, XmlPullParserException, ErrorResponseException, InternalException { Map<String, String> queryParamMap = null; if (partNumber > 0 && uploadId != null && !"".equals(uploadId)) { queryParamMap = new HashMap<>(); queryParamMap.put("partNumber", Integer.toString(partNumber)); queryParamMap.put(UPLOAD_ID, uploadId); } HttpResponse response = executePut(bucketName, objectName, null, queryParamMap, data, length); return response.header().etag(); } /** * Executes put object. If size of object data is <= 5MiB, single put object is used else multipart put object is * used. This method also resumes if previous multipart put is found. * * @param bucketName Bucket name. * @param objectName Object name in the bucket. * @param contentType Content type of object data. * @param size Size of object data. * @param data Object data. */ private void putObject(String bucketName, String objectName, String contentType, long size, Object data) throws InvalidBucketNameException, NoSuchAlgorithmException, InsufficientDataException, IOException, InvalidKeyException, NoResponseException, XmlPullParserException, ErrorResponseException, InternalException, InvalidArgumentException, InsufficientDataException { if (size <= MIN_MULTIPART_SIZE) { // single put object putObject(bucketName, objectName, (int) size, data, null, 0); return; } /* Multipart upload */ int[] rv = calculateMultipartSize(size); int partSize = rv[0]; int partCount = rv[1]; int lastPartSize = rv[2]; Part[] totalParts = new Part[partCount]; // check whether there is incomplete multipart upload or not String uploadId = getLatestIncompleteUploadId(bucketName, objectName); Iterator<Result<Part>> existingParts = null; Part part = null; if (uploadId != null) { // resume previous multipart upload existingParts = listObjectParts(bucketName, objectName, uploadId).iterator(); if (existingParts.hasNext()) { part = existingParts.next().get(); } } else { // initiate new multipart upload ie no previous multipart found or no previous valid parts for // multipart found uploadId = initMultipartUpload(bucketName, objectName, contentType); } int expectedReadSize = partSize; for (int partNumber = 1; partNumber <= partCount; partNumber++) { if (partNumber == partCount) { expectedReadSize = lastPartSize; } if (part != null && partNumber == part.partNumber() && expectedReadSize == part.partSize()) { String md5Hash = Digest.md5Hash(data, expectedReadSize); if (md5Hash.equals(part.etag())) { // this part is already uploaded totalParts[partNumber - 1] = new Part(part.partNumber(), part.etag()); skipStream(data, expectedReadSize); part = getPart(existingParts); continue; } } String etag = putObject(bucketName, objectName, expectedReadSize, data, uploadId, partNumber); totalParts[partNumber - 1] = new Part(partNumber, etag); } completeMultipart(bucketName, objectName, uploadId, totalParts); } /** * Get bucket policy at given objectPrefix * * @param bucketName Bucket name. * @param objectPrefix name of the object prefix * * </p><b>Example:</b><br> * <pre>{@code String policy = minioClient.getBucketPolicy("my-bucketname", "my-objectname"); * System.out.println(policy); }</pre> */ public BucketPolicy getBucketPolicy(String bucketName, String objectPrefix) throws InvalidBucketNameException, InvalidObjectPrefixException, NoSuchAlgorithmException, InsufficientDataException, IOException, InvalidKeyException, NoResponseException, XmlPullParserException, ErrorResponseException, InternalException { checkObjectPrefix(objectPrefix); try { BucketAccessPolicy policy = getBucketAccessPolicy(bucketName, objectPrefix); return policy.identifyPolicyType(bucketName, objectPrefix); } catch (ErrorResponseException e) { if (e.errorResponse().errorCode() == ErrorCode.NO_SUCH_BUCKET_POLICY) { return BucketPolicy.None; } throw e; } } /** * Set policy on bucket and object prefix. * * @param bucketName Bucket name. * @param objectPrefix name of the object prefix * @param bucketPolicy policy can be BucketPolicy.None, BucketPolicy.ReadOnly, * BucketPolicy.ReadWrite, BucketPolicy.WriteOnly * * </p><b>Example:</b><br> * <pre>{@code setBucketPolicy("my-bucketname", "my-objectname", BucketPolicy.ReadOnly); * }</pre> */ public void setBucketPolicy(String bucketName, String objectPrefix, BucketPolicy bucketPolicy) throws InvalidBucketNameException, InvalidObjectPrefixException, NoSuchAlgorithmException, InsufficientDataException, IOException, InvalidKeyException, NoResponseException, XmlPullParserException, ErrorResponseException, InternalException, NoSuchBucketPolicyException { checkObjectPrefix(objectPrefix); BucketAccessPolicy policy = BucketAccessPolicy.none(); try { policy = getBucketAccessPolicy(bucketName, objectPrefix); } catch (ErrorResponseException e) { if (e.errorResponse().errorCode() != ErrorCode.NO_SUCH_BUCKET_POLICY) { throw e; } } if (bucketPolicy.equals(BucketPolicy.None) && !policy.hasStatements()) { throw new NoSuchBucketPolicyException(bucketName, objectPrefix, bucketPolicy); } List<Statement> statements = policy.removeBucketPolicyStatement(bucketName, objectPrefix); // add the common bucket statements, if they don't exist if (!policy.hasCommonBucketStatement(bucketName)) { statements.addAll(BucketAccessPolicy.commonBucketStatement(bucketName)); } List<Statement> generatedStatements = BucketAccessPolicy.generatePolicyStatements(bucketPolicy, bucketName, objectPrefix); statements.addAll(generatedStatements); // prevent request if statements are unchanged if (CollectionUtils.isEqualCollection(policy.statements(), statements)) { return; } policy.setStatements(statements); // if no statements, remove bucket access policy. Policy with empty statements is invalid. if (!policy.hasStatements()) { delBucketAccessPolicy(bucketName); } else { putBucketAccessPolicy(bucketName, policy); } } /** * Returns the parsed current bucket access policy. */ private BucketAccessPolicy getBucketAccessPolicy(String bucketName, String objectPrefix) throws InvalidBucketNameException, InvalidObjectPrefixException, NoSuchAlgorithmException, InsufficientDataException, IOException, InvalidKeyException, NoResponseException, XmlPullParserException, ErrorResponseException, InternalException { Map<String, String> queryParamMap = new HashMap<>(); queryParamMap.put("policy", ""); HttpResponse response = executeGet(bucketName, null, null, queryParamMap); BucketAccessPolicy policy = gson.fromJson(response.body().charStream(), BucketAccessPolicy.class); response.body().close(); if (policy == null) { throw new com.google.gson.JsonParseException("policy document could not be decoded."); } return policy; } /** * Deletes the bucket access policy. */ private void delBucketAccessPolicy(String bucketName) throws InvalidBucketNameException, InvalidObjectPrefixException, NoSuchAlgorithmException, InsufficientDataException, IOException, InvalidKeyException, NoResponseException, XmlPullParserException, ErrorResponseException, InternalException { Map<String, String> queryParamMap = new HashMap<>(); queryParamMap.put("policy", ""); HttpResponse response = executeDelete(bucketName, "", queryParamMap); response.body().close(); } /** * Sets the bucket access policy. */ private void putBucketAccessPolicy(String bucketName, BucketAccessPolicy policy) throws InvalidBucketNameException, InvalidObjectPrefixException, NoSuchAlgorithmException, InsufficientDataException, IOException, InvalidKeyException, NoResponseException, XmlPullParserException, ErrorResponseException, InternalException { Map<String, String> queryParamMap = new HashMap<>(); queryParamMap.put("policy", ""); String body = gson.toJson(policy); HttpResponse response = executePut(bucketName, "", null, queryParamMap, body, body.length()); response.body().close(); } /** * Returns next part if exists. */ private Part getPart(Iterator<Result<Part>> existingParts) throws InvalidBucketNameException, NoSuchAlgorithmException, InsufficientDataException, IOException, InvalidKeyException, NoResponseException, XmlPullParserException, ErrorResponseException, InternalException { Part part; part = null; if (existingParts.hasNext()) { part = existingParts.next().get(); } return part; } /** * Returns latest upload ID of incomplete multipart upload of given bucket name and object name. */ private String getLatestIncompleteUploadId(String bucketName, String objectName) throws InvalidBucketNameException, NoSuchAlgorithmException, InsufficientDataException, IOException, InvalidKeyException, NoResponseException, XmlPullParserException, ErrorResponseException, InternalException { Upload latestUpload = null; for (Result<Upload> result : listIncompleteUploads(bucketName, objectName, true, false)) { Upload upload = result.get(); if (upload.objectName().equals(objectName) && (latestUpload == null || latestUpload.initiated().compareTo(upload.initiated()) < 0)) { latestUpload = upload; } } if (latestUpload != null) { return latestUpload.uploadId(); } else { return null; } } /** * Lists incomplete uploads of objects in given bucket. * * @param bucketName Bucket name. * * @return an iterator of Upload. * @see #listIncompleteUploads(String, String, boolean) */ public Iterable<Result<Upload>> listIncompleteUploads(String bucketName) throws XmlPullParserException { return listIncompleteUploads(bucketName, null, true, true); } /** * Lists incomplete uploads of objects in given bucket and prefix. * * @param bucketName Bucket name. * @param prefix filters the list of uploads to include only those that start with prefix. * * @return an iterator of Upload. * @see #listIncompleteUploads(String, String, boolean) */ public Iterable<Result<Upload>> listIncompleteUploads(String bucketName, String prefix) throws XmlPullParserException { return listIncompleteUploads(bucketName, prefix, true, true); } /** * Lists incomplete uploads of objects in given bucket, prefix and recursive flag. * * </p><b>Example:</b><br> * <pre>{@code Iterable<Result<Upload>> myObjects = minioClient.listIncompleteUploads("my-bucketname"); * for (Result<Upload> result : myObjects) { * Upload upload = result.get(); * System.out.println(upload.uploadId() + ", " + upload.objectName()); * } }</pre> * * @param bucketName Bucket name. * @param prefix Prefix string. List objects whose name starts with `prefix`. * @param recursive when false, emulates a directory structure where each listing returned is either a full object * or part of the object's key up to the first '/'. All uploads with the same prefix up to the first * '/' will be merged into one entry. * * @return an iterator of Upload. * * @see #listIncompleteUploads(String bucketName) * @see #listIncompleteUploads(String bucketName, String prefix) */ public Iterable<Result<Upload>> listIncompleteUploads(String bucketName, String prefix, boolean recursive) { return listIncompleteUploads(bucketName, prefix, recursive, true); } /** * Returns {@code Iterable<Result<Upload>>} of given bucket name, prefix and recursive flag. * All parts size are aggregated when aggregatePartSize is true. */ private Iterable<Result<Upload>> listIncompleteUploads(final String bucketName, final String prefix, final boolean recursive, final boolean aggregatePartSize) { return new Iterable<Result<Upload>>() { @Override public Iterator<Result<Upload>> iterator() { return new Iterator<Result<Upload>>() { private String nextKeyMarker; private String nextUploadIdMarker; private ListMultipartUploadsResult listMultipartUploadsResult; private Result<Upload> error; private Iterator<Upload> uploadIterator; private boolean completed = false; private synchronized void populate() { String delimiter = "/"; if (recursive) { delimiter = null; } this.listMultipartUploadsResult = null; this.uploadIterator = null; try { this.listMultipartUploadsResult = listIncompleteUploads(bucketName, nextKeyMarker, nextUploadIdMarker, prefix, delimiter, 1000); } catch (InvalidBucketNameException | NoSuchAlgorithmException | InsufficientDataException | IOException | InvalidKeyException | NoResponseException | XmlPullParserException | ErrorResponseException | InternalException e) { this.error = new Result<>(null, e); } finally { if (this.listMultipartUploadsResult != null) { this.uploadIterator = this.listMultipartUploadsResult.uploads().iterator(); } else { this.uploadIterator = new LinkedList<Upload>().iterator(); } } } private synchronized long getAggregatedPartSize(String objectName, String uploadId) throws InvalidBucketNameException, NoSuchAlgorithmException, InsufficientDataException, IOException, InvalidKeyException, NoResponseException, XmlPullParserException, ErrorResponseException, InternalException { long aggregatedPartSize = 0; for (Result<Part> result : listObjectParts(bucketName, objectName, uploadId)) { aggregatedPartSize += result.get().partSize(); } return aggregatedPartSize; } @Override public boolean hasNext() { if (this.completed) { return false; } if (this.error == null && this.uploadIterator == null) { populate(); } if (this.error == null && !this.uploadIterator.hasNext() && this.listMultipartUploadsResult.isTruncated()) { this.nextKeyMarker = this.listMultipartUploadsResult.nextKeyMarker(); this.nextUploadIdMarker = this.listMultipartUploadsResult.nextUploadIdMarker(); populate(); } if (this.error != null) { return true; } if (this.uploadIterator.hasNext()) { return true; } this.completed = true; return false; } @Override public Result<Upload> next() { if (this.completed) { throw new NoSuchElementException(); } if (this.error == null && this.uploadIterator == null) { populate(); } if (this.error == null && !this.uploadIterator.hasNext() && this.listMultipartUploadsResult.isTruncated()) { this.nextKeyMarker = this.listMultipartUploadsResult.nextKeyMarker(); this.nextUploadIdMarker = this.listMultipartUploadsResult.nextUploadIdMarker(); populate(); } if (this.error != null) { this.completed = true; return this.error; } if (this.uploadIterator.hasNext()) { Upload upload = this.uploadIterator.next(); if (aggregatePartSize) { long aggregatedPartSize; try { aggregatedPartSize = getAggregatedPartSize(upload.objectName(), upload.uploadId()); } catch (InvalidBucketNameException | NoSuchAlgorithmException | InsufficientDataException | IOException | InvalidKeyException | NoResponseException | XmlPullParserException | ErrorResponseException | InternalException e) { // special case: ignore the error as we can't propagate the exception in next() aggregatedPartSize = -1; } upload.setAggregatedPartSize(aggregatedPartSize); } return new Result<>(upload, null); } this.completed = true; throw new NoSuchElementException(); } @Override public void remove() { throw new UnsupportedOperationException(); } }; } }; } /** * Executes List Incomplete uploads S3 call for given bucket name, key marker, upload id marker, prefix, * delimiter and maxUploads and returns {@link ListMultipartUploadsResult}. */ private ListMultipartUploadsResult listIncompleteUploads(String bucketName, String keyMarker, String uploadIdMarker, String prefix, String delimiter, int maxUploads) throws InvalidBucketNameException, NoSuchAlgorithmException, InsufficientDataException, IOException, InvalidKeyException, NoResponseException, XmlPullParserException, ErrorResponseException, InternalException { if (maxUploads < 0 || maxUploads > 1000) { maxUploads = 1000; } Map<String, String> queryParamMap = new HashMap<>(); queryParamMap.put("uploads", ""); queryParamMap.put("max-uploads", Integer.toString(maxUploads)); queryParamMap.put("prefix", prefix); queryParamMap.put("key-marker", keyMarker); queryParamMap.put("upload-id-marker", uploadIdMarker); queryParamMap.put("delimiter", delimiter); HttpResponse response = executeGet(bucketName, null, null, queryParamMap); ListMultipartUploadsResult result = new ListMultipartUploadsResult(); result.parseXml(response.body().charStream()); response.body().close(); return result; } /** * Initializes new multipart upload for given bucket name, object name and content type. */ private String initMultipartUpload(String bucketName, String objectName, String contentType) throws InvalidBucketNameException, NoSuchAlgorithmException, InsufficientDataException, IOException, InvalidKeyException, NoResponseException, XmlPullParserException, ErrorResponseException, InternalException { Map<String, String> headerMap = new HashMap<>(); if (contentType != null) { headerMap.put("Content-Type", contentType); } else { headerMap.put("Content-Type", "application/octet-stream"); } Map<String, String> queryParamMap = new HashMap<>(); queryParamMap.put("uploads", ""); HttpResponse response = executePost(bucketName, objectName, headerMap, queryParamMap, ""); InitiateMultipartUploadResult result = new InitiateMultipartUploadResult(); result.parseXml(response.body().charStream()); response.body().close(); return result.uploadId(); } /** * Executes complete multipart upload of given bucket name, object name, upload ID and parts. */ private void completeMultipart(String bucketName, String objectName, String uploadId, Part[] parts) throws InvalidBucketNameException, NoSuchAlgorithmException, InsufficientDataException, IOException, InvalidKeyException, NoResponseException, XmlPullParserException, ErrorResponseException, InternalException { Map<String, String> queryParamMap = new HashMap<>(); queryParamMap.put(UPLOAD_ID, uploadId); CompleteMultipartUpload completeManifest = new CompleteMultipartUpload(parts); HttpResponse response = executePost(bucketName, objectName, null, queryParamMap, completeManifest); // Fixing issue https://github.com/minio/minio-java/issues/391 String bodyContent = ""; try { // read enitre body stream to string. Scanner scanner = new java.util.Scanner(response.body().charStream()).useDelimiter("\\A"); if (scanner.hasNext()) { bodyContent = scanner.next(); } } catch (EOFException e) { // Getting EOF exception is not an error. // Just log it. LOGGER.log(Level.WARNING, "EOF exception occured: " + e); } finally { response.body().close(); } bodyContent = bodyContent.trim(); if (!bodyContent.isEmpty()) { ErrorResponse errorResponse = new ErrorResponse(new StringReader(bodyContent)); if (errorResponse.code() != null) { throw new ErrorResponseException(errorResponse, response.response()); } } } /** * Executes List object parts of multipart upload for given bucket name, object name and upload ID and * returns {@code Iterable<Result<Part>>}. */ private Iterable<Result<Part>> listObjectParts(final String bucketName, final String objectName, final String uploadId) { return new Iterable<Result<Part>>() { @Override public Iterator<Result<Part>> iterator() { return new Iterator<Result<Part>>() { private int nextPartNumberMarker; private ListPartsResult listPartsResult; private Result<Part> error; private Iterator<Part> partIterator; private boolean completed = false; private synchronized void populate() { this.listPartsResult = null; this.partIterator = null; try { this.listPartsResult = listObjectParts(bucketName, objectName, uploadId, nextPartNumberMarker); } catch (InvalidBucketNameException | NoSuchAlgorithmException | InsufficientDataException | IOException | InvalidKeyException | NoResponseException | XmlPullParserException | ErrorResponseException | InternalException e) { this.error = new Result<>(null, e); } finally { if (this.listPartsResult != null) { this.partIterator = this.listPartsResult.partList().iterator(); } else { this.partIterator = new LinkedList<Part>().iterator(); } } } @Override public boolean hasNext() { if (this.completed) { return false; } if (this.error == null && this.partIterator == null) { populate(); } if (this.error == null && !this.partIterator.hasNext() && this.listPartsResult.isTruncated()) { this.nextPartNumberMarker = this.listPartsResult.nextPartNumberMarker(); populate(); } if (this.error != null) { return true; } if (this.partIterator.hasNext()) { return true; } this.completed = true; return false; } @Override public Result<Part> next() { if (this.completed) { throw new NoSuchElementException(); } if (this.error == null && this.partIterator == null) { populate(); } if (this.error == null && !this.partIterator.hasNext() && this.listPartsResult.isTruncated()) { this.nextPartNumberMarker = this.listPartsResult.nextPartNumberMarker(); populate(); } if (this.error != null) { this.completed = true; return this.error; } if (this.partIterator.hasNext()) { return new Result<>(this.partIterator.next(), null); } this.completed = true; throw new NoSuchElementException(); } @Override public void remove() { throw new UnsupportedOperationException(); } }; } }; } /** * Executes list object parts for given bucket name, object name, upload ID and part number marker and * returns {@link ListPartsResult}. */ private ListPartsResult listObjectParts(String bucketName, String objectName, String uploadId, int partNumberMarker) throws InvalidBucketNameException, NoSuchAlgorithmException, InsufficientDataException, IOException, InvalidKeyException, NoResponseException, XmlPullParserException, ErrorResponseException, InternalException { Map<String, String> queryParamMap = new HashMap<>(); queryParamMap.put(UPLOAD_ID, uploadId); if (partNumberMarker > 0) { queryParamMap.put("part-number-marker", Integer.toString(partNumberMarker)); } HttpResponse response = executeGet(bucketName, objectName, null, queryParamMap); ListPartsResult result = new ListPartsResult(); result.parseXml(response.body().charStream()); response.body().close(); return result; } /** * Aborts multipart upload of given bucket name, object name and upload ID. */ private void abortMultipartUpload(String bucketName, String objectName, String uploadId) throws InvalidBucketNameException, NoSuchAlgorithmException, InsufficientDataException, IOException, InvalidKeyException, NoResponseException, XmlPullParserException, ErrorResponseException, InternalException { Map<String, String> queryParamMap = new HashMap<>(); queryParamMap.put(UPLOAD_ID, uploadId); executeDelete(bucketName, objectName, queryParamMap); } /** * Removes incomplete multipart upload of given object. * * </p><b>Example:</b><br> * <pre>{@code minioClient.removeIncompleteUpload("my-bucketname", "my-objectname"); * System.out.println("successfully removed all incomplete upload session of my-bucketname/my-objectname"); }</pre> * * @param bucketName Bucket name. * @param objectName Object name in the bucket. * * @throws InvalidBucketNameException upon invalid bucket name is given * @throws NoResponseException upon no response from server * @throws IOException upon connection error * @throws XmlPullParserException upon parsing response xml * @throws ErrorResponseException upon unsuccessful execution * @throws InternalException upon internal library error */ public void removeIncompleteUpload(String bucketName, String objectName) throws InvalidBucketNameException, NoSuchAlgorithmException, InsufficientDataException, IOException, InvalidKeyException, NoResponseException, XmlPullParserException, ErrorResponseException, InternalException { for (Result<Upload> r : listIncompleteUploads(bucketName, objectName, true, false)) { Upload upload = r.get(); if (objectName.equals(upload.objectName())) { abortMultipartUpload(bucketName, objectName, upload.uploadId()); return; } } } /** * Skips data of up to given length in given input stream. * * @param inputStream Input stream which is intance of {@link RandomAccessFile} or {@link BufferedInputStream}. * @param n Length of bytes to skip. */ private void skipStream(Object inputStream, long n) throws IOException, InsufficientDataException { RandomAccessFile file = null; BufferedInputStream stream = null; if (inputStream instanceof RandomAccessFile) { file = (RandomAccessFile) inputStream; } else if (inputStream instanceof BufferedInputStream) { stream = (BufferedInputStream) inputStream; } else { throw new IllegalArgumentException("unsupported input stream object"); } if (file != null) { file.seek(file.getFilePointer() + n); return; } long bytesSkipped; long totalBytesSkipped = 0; while ((bytesSkipped = stream.skip(n - totalBytesSkipped)) >= 0) { totalBytesSkipped += bytesSkipped; if (totalBytesSkipped == n) { return; } } throw new InsufficientDataException( "Insufficient data. bytes skipped " + totalBytesSkipped + " expected " + n); } /** * Calculates multipart size of given size and returns three element array contains part size, part count * and last part size. */ private static int[] calculateMultipartSize(long size) throws InvalidArgumentException { if (size > MAX_OBJECT_SIZE) { throw new InvalidArgumentException("size " + size + " is greater than allowed size 5TiB"); } double partSize = Math.ceil((double) size / MAX_MULTIPART_COUNT); partSize = Math.ceil(partSize / MIN_MULTIPART_SIZE) * MIN_MULTIPART_SIZE; double partCount = Math.ceil(size / partSize); double lastPartSize = partSize - (partSize * partCount - size); if (lastPartSize == 0.0) { lastPartSize = partSize; } return new int[] { (int) partSize, (int) partCount, (int) lastPartSize }; } /** * Enables HTTP call tracing and written to traceStream. * * @param traceStream {@link OutputStream} for writing HTTP call tracing. * * @see #traceOff */ public void traceOn(OutputStream traceStream) { if (traceStream == null) { throw new NullPointerException(); } else { this.traceStream = new PrintWriter(new OutputStreamWriter(traceStream, StandardCharsets.UTF_8), true); } } /** * Disables HTTP call tracing previously enabled. * * @see #traceOn */ public void traceOff() throws IOException { this.traceStream = null; } }