diff --git a/.gitattributes b/.gitattributes index fcadb2cf9..9cf876051 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,14 @@ -* text eol=lf +# Normalize text files to LF, but let Git auto-detect binaries so they are +# stored byte-for-byte (a blanket "* text" corrupts binaries like the Gradle +# wrapper jar by stripping CR bytes during line-ending normalization). +* text=auto eol=lf + +# Always treat these as binary regardless of auto-detection. +*.jar binary +*.zip binary +*.gz binary +*.class binary +*.png binary +*.jpg binary +*.gif binary +*.ico binary diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 5eb62cc47..325316685 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -24,16 +24,16 @@ jobs: - name: Check limited Guava usage if: matrix.os == 'ubuntu-latest' run: | - if grep --with-filename --line-number --no-messages --recursive --exclude-dir=.github "com.google.common.base.Objects" .; then + if grep --with-filename --line-number --no-messages --recursive --exclude-dir=.github --exclude=CLAUDE.md "com.google.common.base.Objects" .; then echo "Error: use java.util.Objects instead of com.google.common.base.Objects" exit 1 fi - - name: Setup java 17 for building + - name: Setup java 25 for building uses: actions/setup-java@v5 with: distribution: temurin - java-version: '17' + java-version: '25' - name: Set environment variables on Ubuntu if: matrix.os == 'ubuntu-latest' @@ -59,7 +59,7 @@ jobs: ./gradlew.bat build - name: Setup java ${{ matrix.java-version }} for testing - if: matrix.java-version != '17' + if: matrix.java-version != '25' uses: actions/setup-java@v5 with: distribution: temurin @@ -69,20 +69,20 @@ jobs: if: matrix.os == 'ubuntu-latest' run: | cd functional - curl -sSfLO https://repo1.maven.org/maven2/org/junit/platform/junit-platform-console-standalone/1.11.4/junit-platform-console-standalone-1.11.4.jar - curl -sSfLO https://repo1.maven.org/maven2/com/github/spotbugs/spotbugs-annotations/4.9.8/spotbugs-annotations-4.9.8.jar - javac -cp spotbugs-annotations-4.9.8.jar:junit-platform-console-standalone-1.11.4.jar:../api/build/libs/minio-${DEV_VERSION}-all.jar:../adminapi/build/libs/minio-admin-${DEV_VERSION}-all.jar:. FunctionalTest.java - java -cp spotbugs-annotations-4.9.8.jar:junit-platform-console-standalone-1.11.4.jar:../api/build/libs/minio-${DEV_VERSION}-all.jar:../adminapi/build/libs/minio-admin-${DEV_VERSION}-all.jar:. FunctionalTest - javac -cp spotbugs-annotations-4.9.8.jar:junit-platform-console-standalone-1.11.4.jar:../api/build/libs/minio-${DEV_VERSION}-all.jar:. ./TestUserAgent.java - java -Dversion=${DEV_VERSION} -cp spotbugs-annotations-4.9.8.jar:junit-platform-console-standalone-1.11.4.jar:../api/build/libs/minio-${DEV_VERSION}-all.jar:. TestUserAgent - javac -cp spotbugs-annotations-4.9.8.jar:junit-platform-console-standalone-1.11.4.jar:../api/build/libs/minio-${RELEASE_VERSION}-all.jar:. ./TestUserAgent.java - java -Dversion=${RELEASE_VERSION} -cp spotbugs-annotations-4.9.8.jar:junit-platform-console-standalone-1.11.4.jar:../api/build/libs/minio-${RELEASE_VERSION}-all.jar:. TestUserAgent + curl -sSfLO https://repo1.maven.org/maven2/org/junit/platform/junit-platform-console-standalone/1.14.4/junit-platform-console-standalone-1.14.4.jar + curl -sSfLO https://repo1.maven.org/maven2/com/github/spotbugs/spotbugs-annotations/4.10.2/spotbugs-annotations-4.10.2.jar + javac -cp spotbugs-annotations-4.10.2.jar:junit-platform-console-standalone-1.14.4.jar:../api/build/libs/minio-${DEV_VERSION}-all.jar:../adminapi/build/libs/minio-admin-${DEV_VERSION}-all.jar:. FunctionalTest.java + java -cp spotbugs-annotations-4.10.2.jar:junit-platform-console-standalone-1.14.4.jar:../api/build/libs/minio-${DEV_VERSION}-all.jar:../adminapi/build/libs/minio-admin-${DEV_VERSION}-all.jar:. FunctionalTest + javac -cp spotbugs-annotations-4.10.2.jar:junit-platform-console-standalone-1.14.4.jar:../api/build/libs/minio-${DEV_VERSION}-all.jar:. ./TestUserAgent.java + java -Dversion=${DEV_VERSION} -cp spotbugs-annotations-4.10.2.jar:junit-platform-console-standalone-1.14.4.jar:../api/build/libs/minio-${DEV_VERSION}-all.jar:. TestUserAgent + javac -cp spotbugs-annotations-4.10.2.jar:junit-platform-console-standalone-1.14.4.jar:../api/build/libs/minio-${RELEASE_VERSION}-all.jar:. ./TestUserAgent.java + java -Dversion=${RELEASE_VERSION} -cp spotbugs-annotations-4.10.2.jar:junit-platform-console-standalone-1.14.4.jar:../api/build/libs/minio-${RELEASE_VERSION}-all.jar:. TestUserAgent - name: Run tests on Windows if: matrix.os == 'windows-latest' run: | cd functional - curl -sSfLO https://repo1.maven.org/maven2/org/junit/platform/junit-platform-console-standalone/1.11.4/junit-platform-console-standalone-1.11.4.jar - curl -sSfLO https://repo1.maven.org/maven2/com/github/spotbugs/spotbugs-annotations/4.9.8/spotbugs-annotations-4.9.8.jar - javac -encoding UTF-8 -cp "spotbugs-annotations-4.9.8.jar;junit-platform-console-standalone-1.11.4.jar;../api/build/libs/minio-$Env:DEV_VERSION-all.jar;../adminapi/build/libs/minio-admin-$Env:DEV_VERSION-all.jar;." FunctionalTest.java - java -cp "spotbugs-annotations-4.9.8.jar;junit-platform-console-standalone-1.11.4.jar;../api/build/libs/minio-$Env:DEV_VERSION-all.jar;../adminapi/build/libs/minio-admin-$Env:DEV_VERSION-all.jar;." FunctionalTest + curl -sSfLO https://repo1.maven.org/maven2/org/junit/platform/junit-platform-console-standalone/1.14.4/junit-platform-console-standalone-1.14.4.jar + curl -sSfLO https://repo1.maven.org/maven2/com/github/spotbugs/spotbugs-annotations/4.10.2/spotbugs-annotations-4.10.2.jar + javac -encoding UTF-8 -cp "spotbugs-annotations-4.10.2.jar;junit-platform-console-standalone-1.14.4.jar;../api/build/libs/minio-$Env:DEV_VERSION-all.jar;../adminapi/build/libs/minio-admin-$Env:DEV_VERSION-all.jar;." FunctionalTest.java + java -cp "spotbugs-annotations-4.10.2.jar;junit-platform-console-standalone-1.14.4.jar;../api/build/libs/minio-$Env:DEV_VERSION-all.jar;../adminapi/build/libs/minio-admin-$Env:DEV_VERSION-all.jar;." FunctionalTest diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..b08df4cb9 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,61 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Overview + +MinIO Java SDK — an S3-compatible client library (`io.minio:minio`) plus a MinIO +admin client (`io.minio:minio-admin`). Multi-module Gradle build. The public API +must remain **Java 8 source/bytecode compatible** even though the build runs on JDK 25. + +## Modules (`settings.gradle`) + +- `api` — the S3 client. Published as artifact `minio`. The core of the project. +- `adminapi` — MinIO server admin client (`io.minio.admin`). Published as `minio-admin`. Depends on `:api`. +- `functional` — integration tests run against a live S3/MinIO server (`FunctionalTest`, `TestMinioAdminClient`, etc.). Not published; sources live in `functional/` (not `src/`). +- `examples` — runnable usage examples. Sources live in `examples/` (not `src/`). + +## Commands + +```sh +./gradlew build # compile, run unit tests, spotless check, spotbugs — run before every PR +./gradlew spotlessApply # auto-format (google-java-format); fixes most lint failures +./gradlew :api:test # unit tests for one module +./gradlew :api:test --tests 'io.minio.MinioClientTest' # single test class +./gradlew :api:test --tests 'io.minio.MakeBucketArgsTest.method' # single test method +./gradlew :api:localeTest # re-runs tests under locale de-DE (part of `check`) +./gradlew runFunctionalTest # integration tests vs play.min.io by default +./gradlew runFunctionalTest -Pendpoint=http://localhost:9000 -PaccessKey=... -PsecretKey=... -Pregion=us-east-1 +``` + +Unit tests are JUnit 5 (Jupiter) in `*/src/test/java`. `functional/` tests hit a real server and are not part of `build`. + +## Build / lint gates (enforced in CI, fail the build) + +- **`--release 8` with `-Werror`** and `-Xlint:unchecked,deprecation`. Any new warning breaks the build. Do not use APIs newer than Java 8 in `api`/`adminapi` main code. +- **Spotless** (`googleJavaFormat`, import order `edu,com,io,java,javax,org`, no unused imports). Run `./gradlew spotlessApply` before committing. +- **SpotBugs** at MAX effort / lowest confidence threshold; suppressions go in `spotbugs-filter.xml`. +- **No `com.google.common.base.Objects`** — CI greps for it; use `java.util.Objects` instead. (Guava is otherwise available.) + +## Architecture + +**Client layering** (`api/src/main/java/io/minio/`): +- `BaseS3Client` (abstract) — HTTP plumbing, request signing, response handling. +- `MinioAsyncClient extends BaseS3Client` — the real implementation; every operation returns `CompletableFuture`. +- `MinioClient` — synchronous facade that **wraps a `MinioAsyncClient`** and blocks on its futures. It does not extend the async client. A new sync operation almost always means adding the async method first, then a blocking wrapper. +- Both are built via a `.builder()...build()` (endpoint, credentials, region, http client). + +**Args pattern** — every public operation takes one immutable `XxxArgs` object built via `XxxArgs.builder()`. The class hierarchy (see `arg-class-structure.txt`) is: +`BaseArgs` → `BucketArgs` (adds bucket/region) → `ObjectArgs` (adds object) → either `ObjectWriteArgs` (uploads: sse, tags, retention, ...) or `ObjectVersionArgs` → `ObjectReadArgs` (ssec) → `ObjectConditionalReadArgs` (offset/length/etag/since). When adding an operation, subclass the right level rather than re-declaring fields. + +**Other key packages:** +- `io.minio.messages` — S3 XML request/response models, (de)serialized with simple-xml-safe. +- `io.minio.credentials` — credential `Provider`s (static, env, IAM, assume-role, LDAP, web-identity, certificate, chained). +- `io.minio.errors` — exception hierarchy rooted at `MinioException`. +- `Http`, `Signer` — OkHttp transport and AWS SigV4 signing. + +## Conventions + +- Public-facing API changes should be reflected in `docs/API.md`. +- Releases are driven by JReleaser (`build.sh` shows the deploy invocation); the `version` lives in `build.gradle` (`-DEV` suffix unless `-Prelease`). +- Target branch for PRs is `master`. diff --git a/adminapi/src/main/java/io/minio/admin/Crypto.java b/adminapi/src/main/java/io/minio/admin/Crypto.java index 8902e6d74..46065e5a6 100644 --- a/adminapi/src/main/java/io/minio/admin/Crypto.java +++ b/adminapi/src/main/java/io/minio/admin/Crypto.java @@ -209,7 +209,9 @@ public static byte[] encrypt(byte[] payload, String password) throws MinioExcept boolean done = false; for (int nonceId = 1; !done; nonceId++) { int to = from + CHUNK_SIZE; - if (to > payload.length) { + // Use >= so a payload that is an exact multiple of CHUNK_SIZE marks its final full chunk as + // the last one, rather than emitting an extra empty trailing chunk (matches madmin-go/sio). + if (to >= payload.length) { additionalData = markAsLast(additionalData); to = payload.length; done = true; diff --git a/adminapi/src/main/java/io/minio/admin/GetDataUsageInfoResponse.java b/adminapi/src/main/java/io/minio/admin/GetDataUsageInfoResponse.java index 0bf3a1b5f..a3aa8dd67 100644 --- a/adminapi/src/main/java/io/minio/admin/GetDataUsageInfoResponse.java +++ b/adminapi/src/main/java/io/minio/admin/GetDataUsageInfoResponse.java @@ -75,7 +75,9 @@ public long objectsTotalSize() { } public Map objectsReplicationInfo() { - return Collections.unmodifiableMap(this.objectsReplicationInfo); + return this.objectsReplicationInfo == null + ? Collections.emptyMap() + : Collections.unmodifiableMap(this.objectsReplicationInfo); } public long bucketsCount() { @@ -83,11 +85,15 @@ public long bucketsCount() { } public Map bucketsUsageInfo() { - return Collections.unmodifiableMap(this.bucketsUsageInfo); + return this.bucketsUsageInfo == null + ? Collections.emptyMap() + : Collections.unmodifiableMap(this.bucketsUsageInfo); } public Map bucketsSizes() { - return Collections.unmodifiableMap(bucketsSizes); + return bucketsSizes == null + ? Collections.emptyMap() + : Collections.unmodifiableMap(bucketsSizes); } public AllTierStats tierStats() { @@ -203,7 +209,9 @@ public long objectsCount() { } public Map objectsSizesHistogram() { - return Collections.unmodifiableMap(this.objectsSizesHistogram); + return this.objectsSizesHistogram == null + ? Collections.emptyMap() + : Collections.unmodifiableMap(this.objectsSizesHistogram); } public long versionsCount() { @@ -215,7 +223,9 @@ public long objectReplicaTotalSize() { } public Map objectsReplicationInfo() { - return Collections.unmodifiableMap(this.objectsReplicationInfo); + return this.objectsReplicationInfo == null + ? Collections.emptyMap() + : Collections.unmodifiableMap(this.objectsReplicationInfo); } } @@ -249,7 +259,7 @@ public static class AllTierStats { private Map tiers; public Map tiers() { - return Collections.unmodifiableMap(this.tiers); + return this.tiers == null ? Collections.emptyMap() : Collections.unmodifiableMap(this.tiers); } } } diff --git a/adminapi/src/main/java/io/minio/admin/GetServerInfoResponse.java b/adminapi/src/main/java/io/minio/admin/GetServerInfoResponse.java index e7ddef460..187f46b9d 100644 --- a/adminapi/src/main/java/io/minio/admin/GetServerInfoResponse.java +++ b/adminapi/src/main/java/io/minio/admin/GetServerInfoResponse.java @@ -289,7 +289,9 @@ public String commitID() { } public Map network() { - return Collections.unmodifiableMap(this.network); + return this.network == null + ? Collections.emptyMap() + : Collections.unmodifiableMap(this.network); } public List disks() { @@ -321,7 +323,9 @@ public GCStats gCStats() { } public Map minioEnvVars() { - return Collections.unmodifiableMap(this.minioEnvVars); + return this.minioEnvVars == null + ? Collections.emptyMap() + : Collections.unmodifiableMap(this.minioEnvVars); } @JsonIgnoreProperties(ignoreUnknown = true) @@ -613,11 +617,13 @@ public Integer totalErrorsTimeout() { } public Map lastMinute() { - return Collections.unmodifiableMap(lastMinute); + return lastMinute == null + ? Collections.emptyMap() + : Collections.unmodifiableMap(lastMinute); } public Map apiCalls() { - return Collections.unmodifiableMap(apiCalls); + return apiCalls == null ? Collections.emptyMap() : Collections.unmodifiableMap(apiCalls); } public Long totalTokens() { diff --git a/adminapi/src/main/java/io/minio/admin/MinioAdminClient.java b/adminapi/src/main/java/io/minio/admin/MinioAdminClient.java index e5588d1bc..e9a82887f 100644 --- a/adminapi/src/main/java/io/minio/admin/MinioAdminClient.java +++ b/adminapi/src/main/java/io/minio/admin/MinioAdminClient.java @@ -167,10 +167,12 @@ private OkHttpClient getHttpClient(PrintWriter traceStream) { } private Response httpExecute( - Http.Method method, Command command, Multimap queryParamMap, byte[] body) + Http.Method method, + Command command, + Multimap queryParamMap, + byte[] body, + Credentials creds) throws IOException, MinioException { - Credentials creds = getCredentials(); - HttpUrl.Builder urlBuilder = this.baseUrl .newBuilder() @@ -224,7 +226,21 @@ private Response execute( Http.Method method, Command command, Multimap queryParamMap, byte[] body) throws MinioException { try { - return httpExecute(method, command, queryParamMap, body); + return httpExecute(method, command, queryParamMap, body, getCredentials()); + } catch (IOException e) { + throw new MinioException(e); + } + } + + private Response execute( + Http.Method method, + Command command, + Multimap queryParamMap, + byte[] body, + Credentials creds) + throws MinioException { + try { + return httpExecute(method, command, queryParamMap, body, creds); } catch (IOException e) { throw new MinioException(e); } @@ -258,7 +274,8 @@ public void addUser( Http.Method.PUT, Command.ADD_USER, ImmutableMultimap.of("accessKey", accessKey), - Crypto.encrypt(OBJECT_MAPPER.writeValueAsBytes(userInfo), creds.secretKey()))) { + Crypto.encrypt(OBJECT_MAPPER.writeValueAsBytes(userInfo), creds.secretKey()), + creds)) { } catch (JsonProcessingException e) { throw new MinioException(e); } @@ -292,8 +309,8 @@ public UserInfo getUserInfo(String accessKey) throws MinioException { * @throws MinioException thrown to indicate SDK exception. */ public Map listUsers() throws MinioException { - try (Response response = execute(Http.Method.GET, Command.LIST_USERS, null, null)) { - Credentials creds = getCredentials(); + Credentials creds = getCredentials(); + try (Response response = execute(Http.Method.GET, Command.LIST_USERS, null, null, creds)) { byte[] jsonData = Crypto.decrypt(response.body().byteStream(), creds.secretKey()); MapType mapType = OBJECT_MAPPER @@ -453,12 +470,14 @@ public long getBucketQuota(String bucketName) throws MinioException { OBJECT_MAPPER .getTypeFactory() .constructMapType(HashMap.class, String.class, JsonNode.class); - return OBJECT_MAPPER.>readValue(response.body().bytes(), mapType) - .entrySet().stream() + return OBJECT_MAPPER + .>readValue(response.body().bytes(), mapType) + .entrySet() + .stream() .filter(entry -> "quota".equals(entry.getKey())) .findFirst() - .map(entry -> Long.valueOf(entry.getValue().toString())) - .orElseThrow(() -> new IllegalArgumentException("found not quota")); + .map(entry -> entry.getValue().asLong()) + .orElseThrow(() -> new IllegalArgumentException("quota not found in response")); } catch (IOException e) { throw new MinioException(e); } @@ -674,7 +693,8 @@ public Credentials addServiceAccount( Http.Method.PUT, Command.ADD_SERVICE_ACCOUNT, null, - Crypto.encrypt(OBJECT_MAPPER.writeValueAsBytes(serviceAccount), creds.secretKey()))) { + Crypto.encrypt(OBJECT_MAPPER.writeValueAsBytes(serviceAccount), creds.secretKey()), + creds)) { byte[] jsonData = Crypto.decrypt(response.body().byteStream(), creds.secretKey()); return OBJECT_MAPPER.readValue(jsonData, AddServiceAccountResponse.class).credentials(); } catch (JsonProcessingException e) { @@ -689,7 +709,7 @@ public Credentials addServiceAccount( * * @param accessKey Access key. * @param newSecretKey New secret key. - * @param newPolicy New policy as JSON string . + * @param newPolicy New policy as JSON string. * @param newStatus New service account status. * @param newName New service account name. * @param newDescription New description. @@ -700,7 +720,7 @@ public void updateServiceAccount( @Nonnull String accessKey, @Nullable String newSecretKey, @Nullable Map newPolicy, - @Nullable boolean newStatus, + @Nullable Boolean newStatus, @Nullable String newName, @Nullable String newDescription, @Nullable ZonedDateTime newExpiration) @@ -722,7 +742,7 @@ public void updateServiceAccount( serviceAccount.put("newSecretKey", newSecretKey); } if (newPolicy != null && !newPolicy.isEmpty()) serviceAccount.put("newPolicy", newPolicy); - serviceAccount.put("newStatus", newStatus ? "on" : "off"); + if (newStatus != null) serviceAccount.put("newStatus", newStatus ? "on" : "off"); if (newName != null && !newName.isEmpty()) serviceAccount.put("newName", newName); if (newDescription != null && !newDescription.isEmpty()) { serviceAccount.put("newDescription", newDescription); @@ -737,7 +757,8 @@ public void updateServiceAccount( Http.Method.POST, Command.UPDATE_SERVICE_ACCOUNT, ImmutableMultimap.of("accessKey", accessKey), - Crypto.encrypt(OBJECT_MAPPER.writeValueAsBytes(serviceAccount), creds.secretKey()))) { + Crypto.encrypt(OBJECT_MAPPER.writeValueAsBytes(serviceAccount), creds.secretKey()), + creds)) { } catch (JsonProcessingException e) { throw new MinioException(e); } @@ -775,13 +796,14 @@ public ListServiceAccountResponse listServiceAccount(@Nonnull String username) throw new IllegalArgumentException("user name must be provided"); } + Credentials creds = getCredentials(); try (Response response = execute( Http.Method.GET, Command.LIST_SERVICE_ACCOUNTS, ImmutableMultimap.of("user", username), - null)) { - Credentials creds = getCredentials(); + null, + creds)) { byte[] jsonData = Crypto.decrypt(response.body().byteStream(), creds.secretKey()); return OBJECT_MAPPER.readValue(jsonData, ListServiceAccountResponse.class); } catch (IOException e) { @@ -802,13 +824,14 @@ public GetServiceAccountInfoResponse getServiceAccountInfo(@Nonnull String acces if (accessKey == null || accessKey.isEmpty()) { throw new IllegalArgumentException("access key must be provided"); } + Credentials creds = getCredentials(); try (Response response = execute( Http.Method.GET, Command.INFO_SERVICE_ACCOUNT, ImmutableMultimap.of("accessKey", accessKey), - null)) { - Credentials creds = getCredentials(); + null, + creds)) { byte[] jsonData = Crypto.decrypt(response.body().byteStream(), creds.secretKey()); return OBJECT_MAPPER.readValue(jsonData, GetServiceAccountInfoResponse.class); } catch (IOException e) { @@ -822,7 +845,7 @@ private PolicyAssociationResponse attachDetachPolicy( @Nullable String user, @Nullable String group) throws MinioException { - if (!(user != null ^ group != null)) { + if (!Utils.xor(user, group)) { throw new IllegalArgumentException("either user or group must be provided"); } @@ -840,7 +863,8 @@ private PolicyAssociationResponse attachDetachPolicy( Http.Method.POST, command, null, - Crypto.encrypt(OBJECT_MAPPER.writeValueAsBytes(map), creds.secretKey()))) { + Crypto.encrypt(OBJECT_MAPPER.writeValueAsBytes(map), creds.secretKey()), + creds)) { return OBJECT_MAPPER.readValue( Crypto.decrypt(response.body().byteStream(), creds.secretKey()), PolicyAssociationResponse.class); diff --git a/adminapi/src/main/java/io/minio/admin/Status.java b/adminapi/src/main/java/io/minio/admin/Status.java index eb1d5026a..a2e403cd5 100644 --- a/adminapi/src/main/java/io/minio/admin/Status.java +++ b/adminapi/src/main/java/io/minio/admin/Status.java @@ -45,7 +45,7 @@ public static Status fromString(String statusString) { return DISABLED; } - if (statusString.isEmpty()) { + if (statusString == null || statusString.isEmpty()) { return null; } diff --git a/api/src/main/java/io/minio/AppendObjectArgs.java b/api/src/main/java/io/minio/AppendObjectArgs.java index 250af4b57..e60a84bcd 100644 --- a/api/src/main/java/io/minio/AppendObjectArgs.java +++ b/api/src/main/java/io/minio/AppendObjectArgs.java @@ -135,6 +135,7 @@ public boolean equals(Object o) { @Override public int hashCode() { - return Objects.hash(super.hashCode(), filename, stream, data, length, chunkSize); + return Objects.hash( + super.hashCode(), filename, stream, Arrays.hashCode(data), length, chunkSize); } } diff --git a/api/src/main/java/io/minio/BaseS3Client.java b/api/src/main/java/io/minio/BaseS3Client.java index 586fe548e..f6103727a 100644 --- a/api/src/main/java/io/minio/BaseS3Client.java +++ b/api/src/main/java/io/minio/BaseS3Client.java @@ -406,12 +406,13 @@ private void onResponse(final Response response) throws IOException { if (!s3request.method().equals(Http.Method.HEAD) && (contentType == null || !Arrays.asList(contentType.split(";")).contains("application/xml"))) { - if (response.code() == 304 && response.body().contentLength() == 0) { + if (response.code() == 304 && errorXml.isEmpty()) { completableFuture.completeExceptionally( new ServerException( "server failed with HTTP status code " + response.code(), response.code(), traceBuilder.toString())); + return; } completableFuture.completeExceptionally( @@ -475,8 +476,8 @@ private void onResponse(final Response response) throws IOException { break; case 409: if (s3request.bucket() != null) { - code = NO_SUCH_BUCKET; - message = NO_SUCH_BUCKET_MESSAGE; + code = "Conflict"; + message = "Bucket not empty"; } else { code = "ResourceConflict"; message = "Request resource conflicts"; diff --git a/api/src/main/java/io/minio/Checksum.java b/api/src/main/java/io/minio/Checksum.java index 75c048b96..a7dd44960 100644 --- a/api/src/main/java/io/minio/Checksum.java +++ b/api/src/main/java/io/minio/Checksum.java @@ -33,6 +33,7 @@ public class Checksum { /** MD5 hash of zero length byte array. */ public static final String ZERO_MD5_HASH = "1B2M2Y8AsgTpgAmY7PhCfg=="; + /** SHA-256 hash of zero length byte array. */ public static final String ZERO_SHA256_HASH = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"; @@ -342,12 +343,13 @@ public CRC64NVME() {} @Override public void update(byte[] p, int off, int len) { + int limit = off + len; java.nio.ByteBuffer byteBuffer = java.nio.ByteBuffer.wrap(p, off, len); byteBuffer.order(ByteOrder.LITTLE_ENDIAN); int offset = byteBuffer.position(); crc = ~crc; - while (p.length >= 64 && (p.length - offset) > 8) { + while (len >= 64 && (limit - offset) > 8) { long value = byteBuffer.getLong(); crc ^= value; crc = @@ -362,7 +364,7 @@ public void update(byte[] p, int off, int len) { offset = byteBuffer.position(); } - for (; offset < len; offset++) { + for (; offset < limit; offset++) { crc = CRC64_TABLE[(int) ((crc ^ (long) p[offset]) & 0xFF)] ^ (crc >>> 8); } diff --git a/api/src/main/java/io/minio/CompleteMultipartUploadArgs.java b/api/src/main/java/io/minio/CompleteMultipartUploadArgs.java index abf6dce31..8c56f9bf5 100644 --- a/api/src/main/java/io/minio/CompleteMultipartUploadArgs.java +++ b/api/src/main/java/io/minio/CompleteMultipartUploadArgs.java @@ -132,6 +132,7 @@ public boolean equals(Object o) { @Override public int hashCode() { - return Objects.hash(super.hashCode(), uploadId, parts, ssec, delayMs, maxRetries); + return Objects.hash( + super.hashCode(), uploadId, Arrays.hashCode(parts), ssec, delayMs, maxRetries); } } diff --git a/api/src/main/java/io/minio/GetObjectAttributesArgs.java b/api/src/main/java/io/minio/GetObjectAttributesArgs.java index b414bc64a..34657e21c 100644 --- a/api/src/main/java/io/minio/GetObjectAttributesArgs.java +++ b/api/src/main/java/io/minio/GetObjectAttributesArgs.java @@ -68,11 +68,17 @@ public Builder objectAttributes(List objectAttributes) { } public Builder maxParts(Integer maxParts) { + if (maxParts != null && maxParts < 1) { + throw new IllegalArgumentException("valid max parts must be provided"); + } operations.add(args -> args.maxParts = maxParts); return this; } public Builder partNumberMarker(Integer partNumberMarker) { + if (partNumberMarker != null && partNumberMarker < 1) { + throw new IllegalArgumentException("valid part number marker must be provided"); + } operations.add(args -> args.partNumberMarker = partNumberMarker); return this; } diff --git a/api/src/main/java/io/minio/GetPresignedObjectUrlArgs.java b/api/src/main/java/io/minio/GetPresignedObjectUrlArgs.java index ea221ea37..82ece9193 100644 --- a/api/src/main/java/io/minio/GetPresignedObjectUrlArgs.java +++ b/api/src/main/java/io/minio/GetPresignedObjectUrlArgs.java @@ -49,7 +49,7 @@ private void validateMethod(Http.Method method) { Utils.validateNotNull(method, "method"); } - private void validateExpiry(int expiry) { + private void validateExpiry(long expiry) { if (expiry < 1 || expiry > DEFAULT_EXPIRY_TIME) { throw new IllegalArgumentException( "expiry must be minimum 1 second to maximum " @@ -73,7 +73,11 @@ public Builder expiry(int expiry) { } public Builder expiry(int duration, TimeUnit unit) { - return expiry((int) unit.toSeconds(duration)); + // Validate the full-precision seconds before narrowing to int, otherwise a large duration + // could overflow into the valid range and pass the check with a truncated value. + long seconds = unit.toSeconds(duration); + validateExpiry(seconds); + return expiry((int) seconds); } @Override diff --git a/api/src/main/java/io/minio/Http.java b/api/src/main/java/io/minio/Http.java index 772ba721d..d4f8f37e4 100644 --- a/api/src/main/java/io/minio/Http.java +++ b/api/src/main/java/io/minio/Http.java @@ -156,7 +156,7 @@ private void setAwsInfo(String host, boolean https) { if (!Utils.HOSTNAME_REGEX.matcher(host).find()) return; if (Utils.AWS_ELB_ENDPOINT_REGEX.matcher(host).find()) { - String[] tokens = host.split("\\.elb\\.amazonaws\\.com", 1)[0].split("\\."); + String[] tokens = host.split("\\.elb\\.amazonaws\\.com")[0].split("\\."); this.region = tokens[tokens.length - 1]; return; } @@ -890,7 +890,7 @@ public static OkHttpClient setTimeout( .build(); } - /** HTTP body of {@link RandomAccessFile}, {@link ByteBuffer} or {@link byte} array. */ + /** HTTP body of {@link RandomAccessFile}, {@link ByteBuffer} or {@code byte} array. */ public static class Body { private okhttp3.RequestBody requestBody; private RandomAccessFile file; diff --git a/api/src/main/java/io/minio/ListObjectVersionsArgs.java b/api/src/main/java/io/minio/ListObjectVersionsArgs.java index 3309a01d0..a6c895f2d 100644 --- a/api/src/main/java/io/minio/ListObjectVersionsArgs.java +++ b/api/src/main/java/io/minio/ListObjectVersionsArgs.java @@ -51,7 +51,7 @@ public String encodingType() { } public int maxKeys() { - return maxKeys; + return maxKeys != null ? maxKeys : 1000; } public String prefix() { diff --git a/api/src/main/java/io/minio/ListObjectsV1Args.java b/api/src/main/java/io/minio/ListObjectsV1Args.java index 9e086a787..9fc56e2c7 100644 --- a/api/src/main/java/io/minio/ListObjectsV1Args.java +++ b/api/src/main/java/io/minio/ListObjectsV1Args.java @@ -49,7 +49,7 @@ public String encodingType() { } public int maxKeys() { - return maxKeys; + return maxKeys != null ? maxKeys : 1000; } public String prefix() { diff --git a/api/src/main/java/io/minio/ListObjectsV2Args.java b/api/src/main/java/io/minio/ListObjectsV2Args.java index 9806ce7ee..abdcc2702 100644 --- a/api/src/main/java/io/minio/ListObjectsV2Args.java +++ b/api/src/main/java/io/minio/ListObjectsV2Args.java @@ -55,7 +55,7 @@ public String encodingType() { } public int maxKeys() { - return maxKeys; + return maxKeys != null ? maxKeys : 1000; } public String prefix() { diff --git a/api/src/main/java/io/minio/ListPartsArgs.java b/api/src/main/java/io/minio/ListPartsArgs.java index dff97394d..48a8fb390 100644 --- a/api/src/main/java/io/minio/ListPartsArgs.java +++ b/api/src/main/java/io/minio/ListPartsArgs.java @@ -41,7 +41,7 @@ public static Builder builder() { } /** Builder of {@link ListPartsArgs}. */ - public static final class Builder extends BucketArgs.Builder { + public static final class Builder extends ObjectArgs.Builder { public Builder uploadId(String uploadId) { Utils.validateNotEmptyString(uploadId, "upload ID"); operations.add(args -> args.uploadId = uploadId); diff --git a/api/src/main/java/io/minio/ListenBucketNotificationArgs.java b/api/src/main/java/io/minio/ListenBucketNotificationArgs.java index a56e86a3f..e5bb55364 100644 --- a/api/src/main/java/io/minio/ListenBucketNotificationArgs.java +++ b/api/src/main/java/io/minio/ListenBucketNotificationArgs.java @@ -52,9 +52,7 @@ private void validateEvents(String[] events) { } protected void validate(ListenBucketNotificationArgs args) { - if (args.bucketName != null) { - super.validate(args); - } + super.validate(args); validateEvents(args.events); } diff --git a/api/src/main/java/io/minio/MinioAsyncClient.java b/api/src/main/java/io/minio/MinioAsyncClient.java index 1e865626f..cb2f6bc4b 100644 --- a/api/src/main/java/io/minio/MinioAsyncClient.java +++ b/api/src/main/java/io/minio/MinioAsyncClient.java @@ -94,7 +94,10 @@ import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Level; +import java.util.logging.Logger; import okhttp3.HttpUrl; import okhttp3.MediaType; import okhttp3.MultipartBody; @@ -343,11 +346,12 @@ private void downloadObject( GetObjectResponse getObjectResponse) throws MinioException { OutputStream os = null; + Path tempFilePath = null; try { Path filePath = Paths.get(filename); String tempFilename = filename + "." + Utils.encode(headObjectResponse.etag()) + ".part.minio"; - Path tempFilePath = Paths.get(tempFilename); + tempFilePath = Paths.get(tempFilename); if (Files.exists(tempFilePath)) Files.delete(tempFilePath); os = Files.newOutputStream(tempFilePath, StandardOpenOption.CREATE, StandardOpenOption.WRITE); long bytesWritten = ByteStreams.copy(getObjectResponse, os); @@ -373,6 +377,17 @@ private void downloadObject( if (os != null) os.close(); } catch (IOException e) { throw new MinioException(e); + } finally { + // Remove the partially-written temp file if it was not moved to its destination. + if (tempFilePath != null) { + try { + Files.deleteIfExists(tempFilePath); + } catch (IOException e) { + // best-effort cleanup; log and continue. + Logger.getLogger(MinioAsyncClient.class.getName()) + .log(Level.WARNING, "failed to delete temporary file " + tempFilePath, e); + } + } } } } @@ -408,7 +423,7 @@ public CompletableFuture downloadObject(DownloadObjectArgs args) { downloadObject(filename, args.overwrite(), headObjectResponse, getObjectResponse); return null; } catch (MinioException e) { - return Utils.failedFuture(e); + throw new CompletionException(e); } }) .thenAccept(nullValue -> {}); @@ -728,7 +743,8 @@ private CompletableFuture uploadParts( final int finalPartNumber = partNumber; future = future.thenCombine( - uploadPartCopy(new UploadPartCopyArgs(args, uploadId, finalPartNumber, headers)), + uploadPartCopy( + new UploadPartCopyArgs(args, uploadId, finalPartNumber, finalHeaders)), (parts, response) -> { parts[response.partNumber() - 1] = response.part(); return parts; @@ -1861,13 +1877,23 @@ private CompletableFuture> uploadPartsParallelly( while (!errorOccurred.get()) { UploadPartArgs.Wrapper part = queue.take(); if (part.args() == null) break; // poison pill - UploadPartResponse response = uploadPart(part.args()).join(); - bufferPool.put(part.args().buffer()); - uploadResults.add(response); + try { + UploadPartResponse response = uploadPart(part.args()).join(); + uploadResults.add(response); + } finally { + // Always return the buffer to the pool, even on upload failure. + bufferPool.put(part.args().buffer()); + } } } catch (InterruptedException e) { + Thread.currentThread().interrupt(); errorOccurred.set(true); // signal to all threads exceptions.add(e); + } catch (RuntimeException e) { + // uploadPart().join() failures surface as CompletionException here; + // signal other threads and the reader so the upload aborts cleanly. + errorOccurred.set(true); + exceptions.add(e); } finally { doneLatch.countDown(); } @@ -1879,7 +1905,9 @@ private CompletableFuture> uploadPartsParallelly( } // Reader: submit initial buffer - queue.put( + offerUntilDoneOrError( + queue, + errorOccurred, new UploadPartArgs.Wrapper( new UploadPartArgs( args, @@ -1893,7 +1921,9 @@ private CompletableFuture> uploadPartsParallelly( while (partReader.partNumber() != partReader.partCount() && !errorOccurred.get()) { ByteBuffer buf = bufferPool.take(); partReader.read(buf); - queue.put( + offerUntilDoneOrError( + queue, + errorOccurred, new UploadPartArgs.Wrapper( new UploadPartArgs( args, @@ -1906,7 +1936,7 @@ private CompletableFuture> uploadPartsParallelly( // Signal all workers to stop with poison pills for (int i = 0; i < parallelUploads; i++) { - queue.put(new UploadPartArgs.Wrapper(null)); + offerUntilDoneOrError(queue, errorOccurred, new UploadPartArgs.Wrapper(null)); } doneLatch.await(); @@ -1930,6 +1960,20 @@ private CompletableFuture> uploadPartsParallelly( }); } + /** + * Offers an item to the queue, retrying until it is accepted or an error has been signalled by a + * worker. Prevents the reader from blocking forever on a full queue once all workers have died. + */ + private static void offerUntilDoneOrError( + BlockingQueue queue, + AtomicBoolean errorOccurred, + UploadPartArgs.Wrapper item) + throws InterruptedException { + while (!queue.offer(item, 200, TimeUnit.MILLISECONDS)) { + if (errorOccurred.get()) return; + } + } + private CompletableFuture putObject( PutObjectBaseArgs args, Object fileStreamData, @@ -3319,11 +3363,32 @@ public CompletableFuture uploadSnowballObjects( throw new IllegalArgumentException( "tarball size " + length + " is more than maximum allowed 5TiB"); } - try (RandomAccessFile file = new RandomAccessFile(args.stagingFilename(), "r")) { - return putObject(new PutObjectAPIArgs(args, file, length, headers)); + final RandomAccessFile file; + try { + file = new RandomAccessFile(args.stagingFilename(), "r"); } catch (IOException e) { throw new CompletionException(new MinioException(e)); } + return putObject(new PutObjectAPIArgs(args, file, length, headers)) + .exceptionally( + e -> { + e = e.getCause(); + try { + file.close(); + } catch (IOException ex) { + e.addSuppressed(new MinioException(ex)); + } + throw new CompletionException(e); + }) + .thenApply( + response -> { + try { + file.close(); + } catch (IOException e) { + throw new CompletionException(new MinioException(e)); + } + return response; + }); }); } @@ -3666,19 +3731,38 @@ public CompletableFuture appendObject(AppendObjectArgs args addSha256Checksum); } - RandomAccessFile file = new RandomAccessFile(args.filename(), "r"); + final RandomAccessFile file = new RandomAccessFile(args.filename(), "r"); return appendObject( - args, - writeOffset, - null, - null, - null, - args.length(), - file, - partSize, - hashers, - addContentSha256, - addSha256Checksum); + args, + writeOffset, + null, + null, + null, + args.length(), + file, + partSize, + hashers, + addContentSha256, + addSha256Checksum) + .exceptionally( + e -> { + e = e.getCause(); + try { + file.close(); + } catch (IOException ex) { + e.addSuppressed(new MinioException(ex)); + } + throw new CompletionException(e); + }) + .thenApply( + resp -> { + try { + file.close(); + } catch (IOException e) { + throw new CompletionException(new MinioException(e)); + } + return resp; + }); } catch (MinioException e) { return Utils.failedFuture(e); } catch (IOException e) { diff --git a/api/src/main/java/io/minio/PartReader.java b/api/src/main/java/io/minio/PartReader.java index 1eb03e208..6715f65ea 100644 --- a/api/src/main/java/io/minio/PartReader.java +++ b/api/src/main/java/io/minio/PartReader.java @@ -96,7 +96,8 @@ private void readOneByte() throws MinioException { int n = 0; try { - while ((n = file != null ? file.read(oneByte) : stream.read(oneByte)) == 0) ; + while ((n = file != null ? file.read(oneByte) : stream.read(oneByte)) == 0) + ; } catch (IOException e) { throw new MinioException(e); } diff --git a/api/src/main/java/io/minio/PromptObjectArgs.java b/api/src/main/java/io/minio/PromptObjectArgs.java index 90dbf9ad9..a1296fb64 100644 --- a/api/src/main/java/io/minio/PromptObjectArgs.java +++ b/api/src/main/java/io/minio/PromptObjectArgs.java @@ -56,7 +56,7 @@ protected void validate(PromptObjectArgs args) { Utils.validateNotNull(args.promptArgs, "prompt argument"); } - public Builder offset(String prompt) { + public Builder prompt(String prompt) { Utils.validateNotEmptyString(prompt, "prompt"); operations.add(args -> args.prompt = prompt); return this; diff --git a/api/src/main/java/io/minio/PutObjectAPIBaseArgs.java b/api/src/main/java/io/minio/PutObjectAPIBaseArgs.java index a1217b493..e873ad7ac 100644 --- a/api/src/main/java/io/minio/PutObjectAPIBaseArgs.java +++ b/api/src/main/java/io/minio/PutObjectAPIBaseArgs.java @@ -120,8 +120,7 @@ public abstract static class Builder, A extends PutObjec extends ObjectArgs.Builder { protected void validate(A args) { super.validate(args); - if (!((args.file != null) != (args.buffer != null) != (args.data != null) - && !(args.file != null && args.buffer != null && args.data != null))) { + if (!Utils.xor(args.file, args.buffer, args.data)) { throw new IllegalArgumentException("only one of file, buffer or data must be provided"); } } @@ -172,6 +171,6 @@ public boolean equals(Object o) { @Override public int hashCode() { - return Objects.hash(super.hashCode(), file, buffer, data, length, headers); + return Objects.hash(super.hashCode(), file, buffer, Arrays.hashCode(data), length, headers); } } diff --git a/api/src/main/java/io/minio/PutObjectArgs.java b/api/src/main/java/io/minio/PutObjectArgs.java index d870c8beb..3c318d65c 100644 --- a/api/src/main/java/io/minio/PutObjectArgs.java +++ b/api/src/main/java/io/minio/PutObjectArgs.java @@ -107,6 +107,6 @@ public boolean equals(Object o) { @Override public int hashCode() { - return Objects.hash(super.hashCode(), stream, data); + return Objects.hash(super.hashCode(), stream, Arrays.hashCode(data)); } } diff --git a/api/src/main/java/io/minio/UploadSnowballObjectsArgs.java b/api/src/main/java/io/minio/UploadSnowballObjectsArgs.java index bf606f2ab..9441df170 100644 --- a/api/src/main/java/io/minio/UploadSnowballObjectsArgs.java +++ b/api/src/main/java/io/minio/UploadSnowballObjectsArgs.java @@ -31,6 +31,12 @@ public class UploadSnowballObjectsArgs extends ObjectWriteArgs { private String stagingFilename; private boolean compression; + public UploadSnowballObjectsArgs() { + // The snowball tarball is uploaded under a transient, auto-generated object key; generate it + // here (once per instance) rather than mutating state inside validate(). + this.objectName = "snowball." + random.nextLong() + ".tar"; + } + public Iterable objects() { return this.objects; } @@ -56,7 +62,6 @@ private void validateObjects(Iterable objects) { @Override protected void validate(UploadSnowballObjectsArgs args) { - args.objectName = "snowball." + random.nextLong() + ".tar"; validateObjects(args.objects); super.validate(args); } diff --git a/api/src/main/java/io/minio/Utils.java b/api/src/main/java/io/minio/Utils.java index 1db11b43a..37fbd4b51 100644 --- a/api/src/main/java/io/minio/Utils.java +++ b/api/src/main/java/io/minio/Utils.java @@ -96,6 +96,15 @@ public static void validateNullOrNotEmptyString(String arg, String argName) { } } + /** Returns true if and only if exactly one of the given objects is non-null. */ + public static boolean xor(Object... objects) { + int count = 0; + for (Object object : objects) { + if (object != null && ++count > 1) return false; + } + return count == 1; + } + public static boolean isValidIPv4OrIPv6(String value) { return InetAddressValidator.getInstance().isValid(value); } diff --git a/api/src/main/java/io/minio/credentials/AwsConfigProvider.java b/api/src/main/java/io/minio/credentials/AwsConfigProvider.java index 0eb95c556..f886fb0d6 100644 --- a/api/src/main/java/io/minio/credentials/AwsConfigProvider.java +++ b/api/src/main/java/io/minio/credentials/AwsConfigProvider.java @@ -110,6 +110,8 @@ public Object put(Object key, Object value) { section = new Properties(); return result.put(header.substring(1, header.length() - 1), section); } + // Ignore key-value entries that appear before any [section] header. + if (section == null) return null; return section.put(key, value); } diff --git a/api/src/main/java/io/minio/credentials/MinioClientConfigProvider.java b/api/src/main/java/io/minio/credentials/MinioClientConfigProvider.java index 1c681d7c1..ba3101608 100644 --- a/api/src/main/java/io/minio/credentials/MinioClientConfigProvider.java +++ b/api/src/main/java/io/minio/credentials/MinioClientConfigProvider.java @@ -113,7 +113,7 @@ public static class Config { private Map> hosts; public Map get(String alias) { - return hosts.get(alias); + return hosts == null ? null : hosts.get(alias); } } } diff --git a/api/src/main/java/io/minio/credentials/MinioEnvironmentProvider.java b/api/src/main/java/io/minio/credentials/MinioEnvironmentProvider.java index 82df00ab4..5c16c4424 100644 --- a/api/src/main/java/io/minio/credentials/MinioEnvironmentProvider.java +++ b/api/src/main/java/io/minio/credentials/MinioEnvironmentProvider.java @@ -16,11 +16,24 @@ package io.minio.credentials; +import java.security.ProviderException; + /** Credential provider using MinIO server specific environment variables. */ public class MinioEnvironmentProvider extends EnvironmentProvider { @Override public Credentials fetch() { - return new Credentials( - getProperty("MINIO_ACCESS_KEY"), getProperty("MINIO_SECRET_KEY"), null, null); + String accessKey = getProperty("MINIO_ACCESS_KEY"); + if (accessKey == null) { + throw new ProviderException( + "Access key does not exist in MINIO_ACCESS_KEY environment variable"); + } + + String secretKey = getProperty("MINIO_SECRET_KEY"); + if (secretKey == null) { + throw new ProviderException( + "Secret key does not exist in MINIO_SECRET_KEY environment variable"); + } + + return new Credentials(accessKey, secretKey, null, null); } } diff --git a/api/src/main/java/io/minio/errors/MinioException.java b/api/src/main/java/io/minio/errors/MinioException.java index 31f3be597..2ec209d04 100644 --- a/api/src/main/java/io/minio/errors/MinioException.java +++ b/api/src/main/java/io/minio/errors/MinioException.java @@ -62,12 +62,26 @@ public String httpTrace() { /** Throws encapsulated exception. */ public void throwEncapsulatedException() - throws BucketPolicyTooLargeException, CertificateException, EOFException, - ErrorResponseException, FileNotFoundException, GeneralSecurityException, - InsufficientDataException, InternalException, InvalidKeyException, - InvalidResponseException, IOException, JsonMappingException, JsonParseException, - JsonProcessingException, KeyManagementException, KeyStoreException, MinioException, - NoSuchAlgorithmException, ServerException, XmlParserException { + throws BucketPolicyTooLargeException, + CertificateException, + EOFException, + ErrorResponseException, + FileNotFoundException, + GeneralSecurityException, + InsufficientDataException, + InternalException, + InvalidKeyException, + InvalidResponseException, + IOException, + JsonMappingException, + JsonParseException, + JsonProcessingException, + KeyManagementException, + KeyStoreException, + MinioException, + NoSuchAlgorithmException, + ServerException, + XmlParserException { Throwable e = getCause(); // Inherited by MinioException diff --git a/api/src/main/java/io/minio/messages/AccessControlPolicy.java b/api/src/main/java/io/minio/messages/AccessControlPolicy.java index 6853bfb1e..aca599bfc 100644 --- a/api/src/main/java/io/minio/messages/AccessControlPolicy.java +++ b/api/src/main/java/io/minio/messages/AccessControlPolicy.java @@ -72,7 +72,8 @@ public String cannedAcl() { return "authenticated-read"; } if ("http://acs.amazonaws.com/groups/global/AllUsers".equals(uri)) return "public-read"; - if (owner.id() != null + if (owner != null + && owner.id() != null && grant.granteeId() != null && owner.id().equals(grant.granteeId())) { return "bucket-owner-read"; diff --git a/api/src/main/java/io/minio/messages/Checksum.java b/api/src/main/java/io/minio/messages/Checksum.java index d8b09c429..97fd1348a 100644 --- a/api/src/main/java/io/minio/messages/Checksum.java +++ b/api/src/main/java/io/minio/messages/Checksum.java @@ -95,7 +95,7 @@ public String checksumType() { private void addHeader(Http.Headers headers, String algorithm, String value) { if (value == null || value.isEmpty()) return; - headers.put("x-amz-checksum-algorithm-" + algorithm, value); + headers.put("x-amz-checksum-" + algorithm, value); headers.put("x-amz-checksum-algorithm", algorithm); } diff --git a/api/src/main/java/io/minio/messages/Filter.java b/api/src/main/java/io/minio/messages/Filter.java index 079ffa83f..510e87dac 100644 --- a/api/src/main/java/io/minio/messages/Filter.java +++ b/api/src/main/java/io/minio/messages/Filter.java @@ -69,7 +69,7 @@ public Filter( @Nullable @Element(name = "ObjectSizeLessThan", required = false) Long objectSizeLessThan, @Nullable @Element(name = "ObjectSizeGreaterThan", required = false) Long objectSizeGreaterThan) { - if (andOperator != null ^ prefix != null ^ tag != null) { + if (Utils.xor(andOperator, prefix, tag)) { this.andOperator = andOperator; this.prefix = prefix; this.tag = tag; @@ -156,9 +156,9 @@ public And( @Nullable Long objectSizeGreaterThan) { List tagList = null; if (tags != null) { - this.tags = new ArrayList<>(); + tagList = new ArrayList<>(); for (Map.Entry entry : tags.entrySet()) { - this.tags.add(new Tag(entry.getKey(), entry.getValue())); + tagList.add(new Tag(entry.getKey(), entry.getValue())); } } set(prefix, tagList, objectSizeLessThan, objectSizeGreaterThan); diff --git a/api/src/main/java/io/minio/messages/LifecycleConfiguration.java b/api/src/main/java/io/minio/messages/LifecycleConfiguration.java index e5694db3e..48de6f6fd 100644 --- a/api/src/main/java/io/minio/messages/LifecycleConfiguration.java +++ b/api/src/main/java/io/minio/messages/LifecycleConfiguration.java @@ -244,7 +244,7 @@ public Expiration( throw new IllegalArgumentException( "ExpiredObjectDeleteMarker must not be provided along with Date and Days"); } - } else if (date != null ^ days != null) { + } else if (Utils.xor(date, days)) { this.date = date; this.days = days; } else { @@ -358,7 +358,7 @@ public Transition( @Nullable @Element(name = "Date", required = false) Time.S3Time date, @Nullable @Element(name = "Days", required = false) Integer days, @Nullable @Element(name = "StorageClass", required = false) String storageClass) { - if (date != null ^ days != null) { + if (Utils.xor(date, days)) { this.date = date; this.days = days; } else { diff --git a/api/src/main/java/io/minio/messages/ObjectLockConfiguration.java b/api/src/main/java/io/minio/messages/ObjectLockConfiguration.java index 2e10682fa..5d9869e57 100644 --- a/api/src/main/java/io/minio/messages/ObjectLockConfiguration.java +++ b/api/src/main/java/io/minio/messages/ObjectLockConfiguration.java @@ -139,7 +139,7 @@ public RetentionDurationUnit unit() { } public int duration() { - return days; + return days == null ? 0 : days; } @Override @@ -165,7 +165,7 @@ public RetentionDurationUnit unit() { } public int duration() { - return years; + return years == null ? 0 : years; } @Override diff --git a/api/src/main/java/io/minio/messages/ReplicationConfiguration.java b/api/src/main/java/io/minio/messages/ReplicationConfiguration.java index 066aa177c..eb17428cd 100644 --- a/api/src/main/java/io/minio/messages/ReplicationConfiguration.java +++ b/api/src/main/java/io/minio/messages/ReplicationConfiguration.java @@ -129,7 +129,7 @@ public Rule( id = id.trim(); if (id.isEmpty()) throw new IllegalArgumentException("ID must be non-empty string"); if (id.length() > 255) - throw new IllegalArgumentException("ID must be exceed 255 characters"); + throw new IllegalArgumentException("ID must not exceed 255 characters"); } this.status = Objects.requireNonNull(status, "Status must not be null"); diff --git a/api/src/main/java/io/minio/messages/VersioningConfiguration.java b/api/src/main/java/io/minio/messages/VersioningConfiguration.java index 8358b9834..52db0068f 100644 --- a/api/src/main/java/io/minio/messages/VersioningConfiguration.java +++ b/api/src/main/java/io/minio/messages/VersioningConfiguration.java @@ -90,8 +90,8 @@ public List excludedPrefixes() { return excludedPrefixes; } - public Boolean excludeFolders() { - return excludeFolders; + public boolean excludeFolders() { + return excludeFolders != null && excludeFolders; } @Override diff --git a/build.gradle b/build.gradle index dedf232cc..28649990d 100644 --- a/build.gradle +++ b/build.gradle @@ -15,14 +15,14 @@ */ /********************************/ -/* gradleVersion = '9.4.1' */ +/* gradleVersion = '9.6.0' */ /********************************/ plugins { - id 'com.gradleup.shadow' version '9.0.0' - id 'com.github.spotbugs' version '6.5.1' - id 'org.jreleaser' version '1.23.0' - id 'com.diffplug.spotless' version '6.13.0' + id 'com.gradleup.shadow' version '9.4.2' + id 'com.github.spotbugs' version '6.5.8' + id 'org.jreleaser' version '1.24.0' + id 'com.diffplug.spotless' version '8.7.0' } /* Root project definitions */ @@ -48,22 +48,26 @@ subprojects { dependencies { api 'com.carrotsearch.thirdparty:simple-xml-safe:2.7.1' api 'com.google.guava:guava:33.6.0-jre' - api 'com.squareup.okhttp3:okhttp:5.3.2' - api 'com.fasterxml.jackson.core:jackson-annotations:2.21' - api 'com.fasterxml.jackson.core:jackson-core:2.21.2' - api 'com.fasterxml.jackson.core:jackson-databind:2.21.2' + api 'com.squareup.okhttp3:okhttp:5.4.0' + api 'com.fasterxml.jackson.core:jackson-annotations:2.22' + api 'com.fasterxml.jackson.core:jackson-core:2.22.0' + api 'com.fasterxml.jackson.core:jackson-databind:2.22.0' api 'org.bouncycastle:bcprov-jdk18on:1.84' api 'org.apache.commons:commons-compress:1.28.0' - api 'commons-codec:commons-codec:1.21.0' + api 'commons-codec:commons-codec:1.22.0' api 'org.xerial.snappy:snappy-java:1.1.10.8' - compileOnly 'com.github.spotbugs:spotbugs-annotations:4.9.8' + compileOnly 'com.github.spotbugs:spotbugs-annotations:4.10.2' - testImplementation 'com.squareup.okhttp3:mockwebserver:5.3.2' - testImplementation 'org.junit.jupiter:junit-jupiter-api:5.14.3' + testImplementation 'com.squareup.okhttp3:mockwebserver:5.4.0' + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.14.4' } [compileJava, compileTestJava].each() { it.options.fork = true + // Build runs on JDK 25 (see java.toolchain) but enforces the Java 8 API surface and + // emits Java 8 bytecode via --release 8. --release also avoids the "source/target 8 is + // obsolete" warning; -Xlint:-options is kept as a belt-and-braces guard for -Werror. + it.options.release = 8 it.options.compilerArgs += ['-Xlint:unchecked', '-Xlint:deprecation', '-Xlint:-options', '-Werror', '-Xdiags:verbose'] it.options.encoding = 'UTF-8' } @@ -103,6 +107,10 @@ subprojects { check.dependsOn localeTest java { + // Compile/test/javadoc run on JDK 25; bytecode/API level pinned to Java 8 via release below. + toolchain { + languageVersion = JavaLanguageVersion.of(25) + } sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 } @@ -112,7 +120,7 @@ subprojects { target '**/*.java' importOrder 'edu', 'com', 'io', 'java', 'javax', 'org', '' removeUnusedImports() - googleJavaFormat('1.7') + googleJavaFormat('1.35.0') } groovyGradle { target '*.gradle' @@ -226,7 +234,7 @@ project(':api') { } signing { - required { + required = { gradle.taskGraph.allTasks.any { it.name.contains('LocalMavenWithChecksums') } } sign publishing.publications.minioJava @@ -346,7 +354,7 @@ project(':adminapi') { } signing { - required { + required = { gradle.taskGraph.allTasks.any { it.name.contains('LocalMavenWithChecksums') } } sign publishing.publications.minioJava @@ -355,7 +363,7 @@ project(':adminapi') { project(':examples') { dependencies { - compileOnly 'me.tongfei:progressbar:0.9.5' + compileOnly 'me.tongfei:progressbar:0.10.2' compileOnly project(':api') } @@ -367,6 +375,9 @@ project(':examples') { main { java { srcDirs = ["$rootDir/examples"] + // srcDir is the module root, which also contains build/; keep generated + // files (e.g. Spotless lint output) out of compilation. + exclude 'build/**' } } } @@ -374,7 +385,7 @@ project(':examples') { project(':functional') { dependencies { - implementation 'org.junit.jupiter:junit-jupiter-api:5.14.3' + implementation 'org.junit.jupiter:junit-jupiter-api:5.14.4' implementation project(':api') implementation project(':adminapi') } @@ -387,6 +398,9 @@ project(':functional') { main { java { srcDirs = ["$rootDir/functional"] + // srcDir is the module root, which also contains build/; keep generated + // files (e.g. Spotless lint output) out of compilation. + exclude 'build/**' } } } diff --git a/examples/GetObject.java b/examples/GetObject.java index 11da0f2fa..f64f8acbc 100644 --- a/examples/GetObject.java +++ b/examples/GetObject.java @@ -38,19 +38,18 @@ public static void main(String[] args) throws IOException, MinioException { // .credentials("YOUR-ACCESSKEY", "YOUR-SECRETACCESSKEY") // .build(); - // Get input stream to have content of 'my-object' from 'my-bucket' - InputStream stream = + // Get input stream to have content of 'my-object' from 'my-bucket'. + // try-with-resources closes the stream (releasing the network connection) even on read error. + try (InputStream stream = minioClient.getObject( - GetObjectArgs.builder().bucket("my-bucket").object("my-object").build()); + GetObjectArgs.builder().bucket("my-bucket").object("my-object").build())) { - // Read the input stream and print to the console till EOF. - byte[] buf = new byte[16384]; - int bytesRead; - while ((bytesRead = stream.read(buf, 0, buf.length)) >= 0) { - System.out.println(new String(buf, 0, bytesRead, StandardCharsets.UTF_8)); + // Read the input stream and print to the console till EOF. + byte[] buf = new byte[16384]; + int bytesRead; + while ((bytesRead = stream.read(buf, 0, buf.length)) >= 0) { + System.out.println(new String(buf, 0, bytesRead, StandardCharsets.UTF_8)); + } } - - // Close the input stream. - stream.close(); } } diff --git a/examples/GetObjectProgressBar.java b/examples/GetObjectProgressBar.java index eeeb8636d..5d9e66f67 100644 --- a/examples/GetObjectProgressBar.java +++ b/examples/GetObjectProgressBar.java @@ -52,30 +52,29 @@ public static void main(String[] args) throws IOException, MinioException { // Get object stat information. StatObjectResponse stat = minioClient.statObject( - StatObjectArgs.builder().bucket("testbucket").object("resumes/4.original.pdf").build()); + StatObjectArgs.builder().bucket("my-bucket").object("my-object").build()); // Get input stream to have content of 'my-object' from 'my-bucket' - InputStream is = - new ProgressStream( - "Downloading .. ", - stat.size(), - minioClient.getObject( - GetObjectArgs.builder().bucket("my-bucket").object("my-object").build())); - Path path = Paths.get("my-filename"); - OutputStream os = Files.newOutputStream(path, StandardOpenOption.CREATE); - - long bytesWritten = ByteStreams.copy(is, os); - is.close(); - os.close(); + try (InputStream is = + new ProgressStream( + "Downloading .. ", + stat.size(), + minioClient.getObject( + GetObjectArgs.builder().bucket("my-bucket").object("my-object").build())); + OutputStream os = + Files.newOutputStream( + path, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) { + long bytesWritten = ByteStreams.copy(is, os); - if (bytesWritten != stat.size()) { - throw new IOException( - path - + ": unexpected data written. expected = " - + stat.size() - + ", written = " - + bytesWritten); + if (bytesWritten != stat.size()) { + throw new IOException( + path + + ": unexpected data written. expected = " + + stat.size() + + ", written = " + + bytesWritten); + } } } } diff --git a/examples/GetPartialObject.java b/examples/GetPartialObject.java index 65555f973..eeea7baa8 100644 --- a/examples/GetPartialObject.java +++ b/examples/GetPartialObject.java @@ -40,23 +40,22 @@ public static void main(String[] args) throws IOException, MinioException { // Get input stream to have content of 'my-object' from 'my-bucket' starts from // byte position 1024 and length 4096. - InputStream stream = + // try-with-resources closes the stream (releasing the network connection) even on read error. + try (InputStream stream = minioClient.getObject( GetObjectArgs.builder() .bucket("my-bucket") .object("my-object") .offset(1024L) .length(4096L) - .build()); + .build())) { - // Read the input stream and print to the console till EOF. - byte[] buf = new byte[16384]; - int bytesRead; - while ((bytesRead = stream.read(buf, 0, buf.length)) >= 0) { - System.out.println(new String(buf, 0, bytesRead, StandardCharsets.UTF_8)); + // Read the input stream and print to the console till EOF. + byte[] buf = new byte[16384]; + int bytesRead; + while ((bytesRead = stream.read(buf, 0, buf.length)) >= 0) { + System.out.println(new String(buf, 0, bytesRead, StandardCharsets.UTF_8)); + } } - - // Close the input stream. - stream.close(); } } diff --git a/examples/GetPresignedPostFormData.java b/examples/GetPresignedPostFormData.java index 2665c1230..b8bd3cd9e 100644 --- a/examples/GetPresignedPostFormData.java +++ b/examples/GetPresignedPostFormData.java @@ -77,11 +77,12 @@ public static void main(String[] args) throws IOException, MinioException { .post(multipartBuilder.build()) .build(); OkHttpClient httpClient = new OkHttpClient().newBuilder().build(); - Response response = httpClient.newCall(request).execute(); - if (response.isSuccessful()) { - System.out.println("Pictures/avatar.png is uploaded successfully using POST object"); - } else { - System.out.println("Failed to upload Pictures/avatar.png"); + try (Response response = httpClient.newCall(request).execute()) { + if (response.isSuccessful()) { + System.out.println("Pictures/avatar.png is uploaded successfully using POST object"); + } else { + System.out.println("Failed to upload Pictures/avatar.png"); + } } // Print curl command usage to upload file /tmp/userpic.jpg. diff --git a/examples/MinioClientWithAssumeRoleProvider.java b/examples/MinioClientWithAssumeRoleProvider.java index 45a1c4aa6..b971b9b5b 100644 --- a/examples/MinioClientWithAssumeRoleProvider.java +++ b/examples/MinioClientWithAssumeRoleProvider.java @@ -25,9 +25,9 @@ public class MinioClientWithAssumeRoleProvider { public static void main(String[] args) throws MinioException { Provider provider = new AssumeRoleProvider( - "https://play.minio.io:9000/", // STS endpoint usually point to MinIO server. - "minio", // Access key. - "minio123", // Secret key. + "https://STS-HOST:STS-PORT/", // STS endpoint usually point to MinIO server. + "YOUR-ACCESSKEY", // Access key. + "YOUR-SECRETACCESSKEY", // Secret key. null, // Duration seconds if available. null, // Policy if available. null, // Region if available. @@ -38,7 +38,7 @@ public static void main(String[] args) throws MinioException { MinioClient minioClient = MinioClient.builder() - .endpoint("https://play.minio.io:9000") + .endpoint("https://MINIO-HOST:MINIO-PORT") .credentialsProvider(provider) .build(); diff --git a/examples/MinioClientWithClientGrantsProvider.java b/examples/MinioClientWithClientGrantsProvider.java index 8636dc3be..543951fb3 100644 --- a/examples/MinioClientWithClientGrantsProvider.java +++ b/examples/MinioClientWithClientGrantsProvider.java @@ -52,6 +52,9 @@ static Jwt getJwt( OkHttpClient client = new OkHttpClient(); try (Response response = client.newCall(request).execute()) { + if (!response.isSuccessful()) { + throw new ProviderException("STS endpoint failed with HTTP status " + response.code()); + } ObjectMapper mapper = new ObjectMapper(); mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); mapper.setVisibility( diff --git a/examples/MinioClientWithWebIdentityProvider.java b/examples/MinioClientWithWebIdentityProvider.java index 10ed3983a..b104dc8d4 100644 --- a/examples/MinioClientWithWebIdentityProvider.java +++ b/examples/MinioClientWithWebIdentityProvider.java @@ -56,6 +56,9 @@ static Jwt getJwt( OkHttpClient client = new OkHttpClient(); try (Response response = client.newCall(request).execute()) { + if (!response.isSuccessful()) { + throw new ProviderException("STS endpoint failed with HTTP status " + response.code()); + } ObjectMapper mapper = new ObjectMapper(); mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); mapper.setVisibility( diff --git a/examples/ProgressStream.java b/examples/ProgressStream.java index 28de24252..d739fba66 100644 --- a/examples/ProgressStream.java +++ b/examples/ProgressStream.java @@ -68,28 +68,30 @@ public void close() throws IOException { @Override public int read() throws IOException { - this.pb.step(); - return this.in.read(); + int b = this.in.read(); + if (b >= 0) this.pb.step(); // Only advance when a byte was actually read (not at EOF). + return b; } @Override public int read(byte[] toStore) throws IOException { int readBytes = this.in.read(toStore); - this.pb.stepBy(readBytes); // Update progress bar. + if (readBytes > 0) this.pb.stepBy(readBytes); // Don't step by the -1 EOF sentinel. return readBytes; } @Override public int read(byte[] toStore, int off, int len) throws IOException { int readBytes = this.in.read(toStore, off, len); - this.pb.stepBy(readBytes); + if (readBytes > 0) this.pb.stepBy(readBytes); return readBytes; } @Override public long skip(long n) throws IOException { - this.pb.stepTo(n); - return this.in.skip(n); + long skipped = this.in.skip(n); + this.pb.stepBy(skipped); // Advance by the bytes actually skipped, not an absolute position. + return skipped; } @Override diff --git a/examples/PutObjectProgressBar.java b/examples/PutObjectProgressBar.java index 4101a7ff7..51f1e473a 100644 --- a/examples/PutObjectProgressBar.java +++ b/examples/PutObjectProgressBar.java @@ -43,14 +43,16 @@ public static void main(String[] args) throws IOException, MinioException { String objectName = "my-object"; String bucketName = "my-bucket"; - InputStream pis = + // Use the actual file length as the object size; available() is only a hint, not a reliable + // size. + long size = Files.size(Paths.get("my-filename")); + try (InputStream pis = new BufferedInputStream( - new ProgressStream("Uploading... ", Files.newInputStream(Paths.get("my-filename")))); - minioClient.putObject( - PutObjectArgs.builder().bucket(bucketName).object(objectName).stream( - pis, (long) pis.available(), null) - .build()); - pis.close(); + new ProgressStream("Uploading... ", Files.newInputStream(Paths.get("my-filename"))))) { + minioClient.putObject( + PutObjectArgs.builder().bucket(bucketName).object(objectName).stream(pis, size, null) + .build()); + } System.out.println("my-object is uploaded successfully"); } } diff --git a/examples/PutObjectUiProgressBar.java b/examples/PutObjectUiProgressBar.java index ecb3bf546..55da17d0a 100644 --- a/examples/PutObjectUiProgressBar.java +++ b/examples/PutObjectUiProgressBar.java @@ -77,9 +77,11 @@ private void uploadFile(String filename) throws MinioException { new ProgressMonitorInputStream(this, "Uploading... " + file.getAbsolutePath(), bis); pmis.getProgressMonitor().setMillisToPopup(10); + // Use the actual file length as the object size; available() is only a hint, not a reliable + // size. minioClient.putObject( PutObjectArgs.builder().bucket("bank").object("my-object").stream( - pmis, (long) pmis.available(), null) + pmis, file.length(), null) .build()); System.out.println("my-object is uploaded successfully"); } catch (FileNotFoundException e) { diff --git a/examples/SelectObjectContent.java b/examples/SelectObjectContent.java index ab4a4ebdb..31fceeb73 100644 --- a/examples/SelectObjectContent.java +++ b/examples/SelectObjectContent.java @@ -65,24 +65,27 @@ public static void main(String[] args) throws IOException, MinioException { OutputSerialization.newCSV( null, null, null, OutputSerialization.QuoteFields.ASNEEDED, null); - SelectResponseStream stream = + try (SelectResponseStream stream = minioClient.selectObjectContent( SelectObjectContentArgs.builder() .bucket("my-bucket") - .object("my-objectName") + .object("my-object") .sqlExpression(sqlExpression) .inputSerialization(is) .outputSerialization(os) .requestProgress(true) - .build()); + .build())) { - byte[] buf = new byte[512]; - int bytesRead = stream.read(buf, 0, buf.length); - System.out.println(new String(buf, 0, bytesRead, StandardCharsets.UTF_8)); - Stats stats = stream.stats(); - System.out.println("bytes scanned: " + stats.bytesScanned()); - System.out.println("bytes processed: " + stats.bytesProcessed()); - System.out.println("bytes returned: " + stats.bytesReturned()); - stream.close(); + byte[] buf = new byte[512]; + int bytesRead; + while ((bytesRead = stream.read(buf, 0, buf.length)) >= 0) { + System.out.print(new String(buf, 0, bytesRead, StandardCharsets.UTF_8)); + } + + Stats stats = stream.stats(); + System.out.println("bytes scanned: " + stats.bytesScanned()); + System.out.println("bytes processed: " + stats.bytesProcessed()); + System.out.println("bytes returned: " + stats.bytesReturned()); + } } } diff --git a/functional/MintLogger.java b/functional/MintLogger.java index ad5fcaa06..37462d311 100644 --- a/functional/MintLogger.java +++ b/functional/MintLogger.java @@ -21,6 +21,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.UncheckedIOException; @JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY) public class MintLogger { @@ -77,9 +78,8 @@ public String toString() { JsonInclude.Value.empty().withValueInclusion(JsonInclude.Include.NON_NULL)) .writeValueAsString(this); } catch (JsonProcessingException e) { - e.printStackTrace(); + throw new UncheckedIOException("unable to serialize MintLogger entry", e); } - return ""; } /** Return Alert. */ diff --git a/functional/PutObjectRunnable.java b/functional/PutObjectRunnable.java index 8e4c82580..e1ccf10b6 100644 --- a/functional/PutObjectRunnable.java +++ b/functional/PutObjectRunnable.java @@ -25,6 +25,7 @@ class PutObjectRunnable implements Runnable { MinioClient client; String bucketName; String filename; + private volatile Exception exception; public PutObjectRunnable(MinioClient client, String bucketName, String filename) { this.client = client; @@ -32,6 +33,11 @@ public PutObjectRunnable(MinioClient client, String bucketName, String filename) this.filename = filename; } + /** Returns the exception that failed this run, or null if it succeeded. */ + public Exception exception() { + return exception; + } + public void run() { StringBuffer traceBuffer = new StringBuffer(); @@ -48,6 +54,8 @@ public void run() { traceBuffer.append("[" + filename + "]: delete object\n"); client.removeObject(RemoveObjectArgs.builder().bucket(bucketName).object(filename).build()); } catch (Exception e) { + // Record the failure so the test thread can detect it after join(); also print for tracing. + this.exception = e; System.err.print(traceBuffer.toString()); e.printStackTrace(); } diff --git a/functional/TestMinioClient.java b/functional/TestMinioClient.java index 28182b3c2..4c8dc06dd 100644 --- a/functional/TestMinioClient.java +++ b/functional/TestMinioClient.java @@ -492,9 +492,11 @@ public void testThreadedPutObject() throws Exception { try { int count = 7; Thread[] threads = new Thread[count]; + PutObjectRunnable[] runnables = new PutObjectRunnable[count]; for (int i = 0; i < count; i++) { - threads[i] = new Thread(new PutObjectRunnable(client, bucketName, createFile6Mb())); + runnables[i] = new PutObjectRunnable(client, bucketName, createFile6Mb()); + threads[i] = new Thread(runnables[i]); } for (int i = 0; i < count; i++) threads[i].start(); @@ -502,6 +504,11 @@ public void testThreadedPutObject() throws Exception { // Waiting for threads to complete. for (int i = 0; i < count; i++) threads[i].join(); + // Fail the test if any thread's upload/cleanup threw. + for (int i = 0; i < count; i++) { + if (runnables[i].exception() != null) throw runnables[i].exception(); + } + // All threads are completed. mintSuccessLog(methodName, testTags, startTime); } catch (Exception e) { @@ -548,7 +555,9 @@ public void putObject() throws Exception { testPutObject( "[object name ends with '/']", - PutObjectArgs.builder().bucket(bucketName).object("path/to/" + getRandomName() + "/") + PutObjectArgs.builder() + .bucket(bucketName) + .object("path/to/" + getRandomName() + "/") .stream(new ContentInputStream(0), 0L, null) .contentType(CUSTOM_CONTENT_TYPE) .build(), @@ -949,11 +958,11 @@ public void testDownloadObject( if (sse != null) builder.sse(sse); client.putObject(builder.build()); client.downloadObject(args); - Files.delete(Paths.get(args.filename())); mintSuccessLog(methodName, testTags, startTime); } catch (Exception e) { handleException(methodName, testTags, startTime, e); } finally { + Files.deleteIfExists(Paths.get(args.filename())); client.removeObject( RemoveObjectArgs.builder().bucket(args.bucket()).object(args.object()).build()); } @@ -1331,10 +1340,11 @@ public void testPutPresignedUrl( String urlString = client.getPresignedObjectUrl(args); try { writeObject(urlString, data); - InputStream is = + try (InputStream is = client.getObject( - GetObjectArgs.builder().bucket(args.bucket()).object(args.object()).build()); - data = readAllBytes(is); + GetObjectArgs.builder().bucket(args.bucket()).object(args.object()).build())) { + data = readAllBytes(is); + } String checksum = getSha256Sum(new ByteArrayInputStream(data), data.length); Assertions.assertEquals( expectedChecksum, @@ -1445,7 +1455,9 @@ public void testCopyObject( client.makeBucket(MakeBucketArgs.builder().bucket(args.source().bucket()).build()); try { PutObjectArgs.Builder builder = - PutObjectArgs.builder().bucket(args.source().bucket()).object(args.source().object()) + PutObjectArgs.builder() + .bucket(args.source().bucket()) + .object(args.source().object()) .stream(new ContentInputStream(1 * KB), 1L * KB, null); if (sse != null) builder.sse(sse); client.putObject(builder.build()); @@ -1939,6 +1951,9 @@ public void composeObject() throws Exception { } } catch (Exception e) { handleException(methodName, null, startTime, e); + // Setup failed; skip the dependent tests (object names may be null). The finally below + // still cleans up any objects that were created. + return; } composeObjectTests(object1Mb, object6Mb, object6MbSsec); @@ -2018,14 +2033,8 @@ public void disableObjectLegalHold() throws Exception { new ContentInputStream(1 * KB), 1L * KB, null) .build()); + checkObjectLegalHold(bucketNameWithLock, objectName, true); checkObjectLegalHold(bucketNameWithLock, objectName, false); - client.enableObjectLegalHold( - EnableObjectLegalHoldArgs.builder() - .bucket(bucketNameWithLock) - .object(objectName) - .build()); - checkObjectLegalHold(bucketNameWithLock, objectName, false); - mintSuccessLog(methodName, null, startTime); } finally { if (objectInfo != null) { client.removeObject( @@ -2067,7 +2076,6 @@ public void isObjectLegalHoldEnabled() throws Exception { Assertions.assertFalse(result, "object legal hold: expected: false, got: " + result); checkObjectLegalHold(bucketNameWithLock, objectName, true); checkObjectLegalHold(bucketNameWithLock, objectName, false); - mintSuccessLog(methodName, null, startTime); } finally { if (objectInfo != null) { client.removeObject( @@ -2646,7 +2654,7 @@ public void getBucketNotification() throws Exception { || !EventType.OBJECT_CREATED_PUT .toString() .equals(config.queueConfigurations().get(0).events().get(0))) { - System.out.println( + throw new Exception( "config: expected: " + Xml.marshal(expectedConfig) + ", got: " + Xml.marshal(config)); } } finally { @@ -2699,7 +2707,7 @@ public void deleteBucketNotification() throws Exception { client.getBucketNotification( GetBucketNotificationArgs.builder().bucket(bucketName).build()); if (config.queueConfigurations().size() != 0) { - System.out.println("config: expected: , got: " + Xml.marshal(config)); + throw new Exception("config: expected: , got: " + Xml.marshal(config)); } } finally { client.removeBucket(RemoveBucketArgs.builder().bucket(bucketName).build()); @@ -2821,10 +2829,10 @@ public void selectObjectContent() throws Exception { Assertions.assertNotNull(stats, "stats is null"); Assertions.assertTrue( stats.bytesScanned() == 256, - "stats.bytesScanned mismatch; expected: 258, got: " + stats.bytesScanned()); + "stats.bytesScanned mismatch; expected: 256, got: " + stats.bytesScanned()); Assertions.assertTrue( stats.bytesProcessed() == 256, - "stats.bytesProcessed mismatch; expected: 258, got: " + stats.bytesProcessed()); + "stats.bytesProcessed mismatch; expected: 256, got: " + stats.bytesProcessed()); Assertions.assertTrue( stats.bytesReturned() == 222, "stats.bytesReturned mismatch; expected: 222, got: " + stats.bytesReturned()); diff --git a/functional/TestUserAgent.java b/functional/TestUserAgent.java index d14bba570..2034e9131 100644 --- a/functional/TestUserAgent.java +++ b/functional/TestUserAgent.java @@ -17,27 +17,38 @@ import io.minio.BucketExistsArgs; import io.minio.MinioClient; +import io.minio.errors.MinioException; import java.io.ByteArrayOutputStream; import java.nio.charset.StandardCharsets; import java.util.Scanner; -@edu.umd.cs.findbugs.annotations.SuppressFBWarnings( - value = "THROWS_METHOD_THROWS_CLAUSE_BASIC_EXCEPTION") public class TestUserAgent { public static void main(String[] args) throws Exception { - MinioClient client = MinioClient.builder().endpoint("http://httpbin.org").build(); ByteArrayOutputStream baos = new ByteArrayOutputStream(); - client.traceOn(baos); - client.bucketExists(BucketExistsArgs.builder().bucket("any-bucket-name-works").build()); - client.traceOff(); + // try-with-resources closes the client so its OkHttp dispatcher/connection-pool threads shut + // down and the JVM exits promptly instead of waiting out the idle keep-alive. + try (MinioClient client = MinioClient.builder().endpoint("http://httpbin.org").build()) { + client.setRetry(null, null, null); + client.traceOn(baos); + try { + client.bucketExists(BucketExistsArgs.builder().bucket("any-bucket-name-works").build()); + } catch (MinioException e) { + // ignore + } + client.traceOff(); + } String expectedVersion = System.getProperty("version"); + if (expectedVersion == null || expectedVersion.isEmpty()) { + throw new Exception("system property 'version' must be set"); + } String version = null; try (Scanner scanner = new Scanner(new String(baos.toByteArray(), StandardCharsets.UTF_8))) { while (scanner.hasNextLine()) { String line = scanner.nextLine(); if (line.startsWith("User-Agent:")) { - version = line.split("/")[1]; + String[] tokens = line.split("/"); + if (tokens.length > 1) version = tokens[1]; break; } } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index a4b76b953..b1b8ef56b 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index c61a118f7..eb84db68d 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,9 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.0-bin.zip networkTimeout=10000 +retries=0 +retryBackOffMs=500 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index f3b75f3b0..249efbb03 100755 --- a/gradlew +++ b/gradlew @@ -1,7 +1,7 @@ #!/bin/sh # -# Copyright © 2015-2021 the original authors. +# Copyright © 2015 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -20,7 +20,7 @@ ############################################################################## # -# Gradle start up script for POSIX generated by Gradle. +# gradlew start up script for POSIX generated by Gradle. # # Important for running: # @@ -29,7 +29,7 @@ # bash, then to run this script, type that shell name before the whole # command line, like: # -# ksh Gradle +# ksh gradlew # # Busybox and similar reduced shells will NOT work, because this script # requires all of these POSIX shell features: @@ -57,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -114,7 +114,6 @@ case "$( uname )" in #( NONSTOP* ) nonstop=true ;; esac -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. @@ -172,7 +171,6 @@ fi # For Cygwin or MSYS, switch paths to Windows format before running java if "$cygwin" || "$msys" ; then APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) - CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) JAVACMD=$( cygpath --unix "$JAVACMD" ) @@ -205,15 +203,14 @@ fi DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Collect all arguments for the java command: -# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, # and any embedded shellness will be escaped. # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be # treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ "$@" # Stop when "xargs" is not available. diff --git a/gradlew.bat b/gradlew.bat index 9d21a2183..a51ec4f58 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -19,12 +19,12 @@ @if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem -@rem Gradle startup script for Windows +@rem gradlew startup script for Windows @rem @rem ########################################################################## -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal +@rem Set local scope for the variables, and ensure extensions are enabled +setlocal EnableExtensions set DIRNAME=%~dp0 if "%DIRNAME%"=="" set DIRNAME=. @@ -51,7 +51,7 @@ echo. 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 -goto fail +"%COMSPEC%" /c exit 1 :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% @@ -65,30 +65,18 @@ echo. 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 -goto fail +"%COMSPEC%" /c exit 1 :execute @rem Setup the command line -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* +@rem Execute gradlew +@rem endlocal doesn't take effect until after the line is parsed and variables are expanded +@rem which allows us to clear the local environment before executing the java command +endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel -:end -@rem End local scope for the variables with windows NT shell -if %ERRORLEVEL% equ 0 goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -set EXIT_CODE=%ERRORLEVEL% -if %EXIT_CODE% equ 0 set EXIT_CODE=1 -if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% -exit /b %EXIT_CODE% - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +:exitWithErrorLevel +@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts +"%COMSPEC%" /c exit %ERRORLEVEL% diff --git a/spotbugs-filter.xml b/spotbugs-filter.xml index bc8accf49..27f43ade6 100644 --- a/spotbugs-filter.xml +++ b/spotbugs-filter.xml @@ -32,4 +32,8 @@ + + + +