Skip to content

Commit 57e34b7

Browse files
authored
Add uploadSnowballObjects() method. (#1277)
Signed-off-by: Bala.FA <bala@minio.io>
1 parent 6765c54 commit 57e34b7

File tree

7 files changed

+464
-1
lines changed

7 files changed

+464
-1
lines changed

api/src/main/java/io/minio/MinioClient.java

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@
5151
import io.minio.messages.SseConfiguration;
5252
import io.minio.messages.Tags;
5353
import io.minio.messages.VersioningConfiguration;
54+
import java.io.BufferedOutputStream;
55+
import java.io.ByteArrayOutputStream;
56+
import java.io.FileOutputStream;
5457
import java.io.IOException;
5558
import java.io.InputStream;
5659
import java.io.OutputStream;
@@ -64,6 +67,7 @@
6467
import java.nio.file.StandardOpenOption;
6568
import java.security.InvalidKeyException;
6669
import java.security.NoSuchAlgorithmException;
70+
import java.util.Date;
6771
import java.util.Iterator;
6872
import java.util.LinkedList;
6973
import java.util.List;
@@ -73,6 +77,9 @@
7377
import okhttp3.OkHttpClient;
7478
import okhttp3.Request;
7579
import okhttp3.Response;
80+
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
81+
import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream;
82+
import org.xerial.snappy.SnappyFramedOutputStream;
7683

7784
/**
7885
* Simple Storage Service (aka S3) client to perform bucket and object operations.
@@ -2591,6 +2598,131 @@ public void deleteObjectTags(DeleteObjectTagsArgs args)
25912598
executeDelete(args, null, queryParams);
25922599
}
25932600

2601+
/**
2602+
* Uploads multiple objects in a single put call. It is done by creating intermediate TAR file
2603+
* optionally compressed which is uploaded to S3 service.
2604+
*
2605+
* <pre>Example:{@code
2606+
* // Upload snowball objects.
2607+
* List<SnowballObject> objects = new ArrayList<SnowballObject>();
2608+
* objects.add(
2609+
* new SnowballObject(
2610+
* "my-object-one",
2611+
* new ByteArrayInputStream("hello".getBytes(StandardCharsets.UTF_8)),
2612+
* 5,
2613+
* null));
2614+
* objects.add(
2615+
* new SnowballObject(
2616+
* "my-object-two",
2617+
* new ByteArrayInputStream("java".getBytes(StandardCharsets.UTF_8)),
2618+
* 4,
2619+
* null));
2620+
* minioClient.uploadSnowballObjects(
2621+
* UploadSnowballObjectsArgs.builder().bucket("my-bucketname").objects(objects).build());
2622+
* }</pre>
2623+
*
2624+
* @param args {@link UploadSnowballObjectsArgs} object.
2625+
* @throws ErrorResponseException thrown to indicate S3 service returned an error response.
2626+
* @throws InsufficientDataException thrown to indicate not enough data available in InputStream.
2627+
* @throws InternalException thrown to indicate internal library error.
2628+
* @throws InvalidKeyException thrown to indicate missing of HMAC SHA-256 library.
2629+
* @throws InvalidResponseException thrown to indicate S3 service returned invalid or no error
2630+
* response.
2631+
* @throws IOException thrown to indicate I/O error on S3 operation.
2632+
* @throws NoSuchAlgorithmException thrown to indicate missing of MD5 or SHA-256 digest library.
2633+
* @throws XmlParserException thrown to indicate XML parsing error.
2634+
*/
2635+
public ObjectWriteResponse uploadSnowballObjects(UploadSnowballObjectsArgs args)
2636+
throws ErrorResponseException, InsufficientDataException, InternalException,
2637+
InvalidKeyException, InvalidResponseException, IOException, NoSuchAlgorithmException,
2638+
ServerException, XmlParserException {
2639+
checkArgs(args);
2640+
2641+
FileOutputStream fos = null;
2642+
BufferedOutputStream bos = null;
2643+
SnappyFramedOutputStream sos = null;
2644+
ByteArrayOutputStream baos = null;
2645+
TarArchiveOutputStream tarOutputStream = null;
2646+
2647+
try {
2648+
OutputStream os = null;
2649+
if (args.stagingFilename() != null) {
2650+
fos = new FileOutputStream(args.stagingFilename());
2651+
bos = new BufferedOutputStream(fos);
2652+
os = bos;
2653+
} else {
2654+
baos = new ByteArrayOutputStream();
2655+
os = baos;
2656+
}
2657+
2658+
if (args.compression()) {
2659+
sos = new SnappyFramedOutputStream(os);
2660+
os = sos;
2661+
}
2662+
2663+
tarOutputStream = new TarArchiveOutputStream(os);
2664+
for (SnowballObject object : args.objects()) {
2665+
if (object.filename() != null) {
2666+
Path filePath = Paths.get(object.filename());
2667+
TarArchiveEntry entry = new TarArchiveEntry(filePath.toFile(), object.name());
2668+
tarOutputStream.putArchiveEntry(entry);
2669+
Files.copy(filePath, tarOutputStream);
2670+
} else {
2671+
TarArchiveEntry entry = new TarArchiveEntry(object.name());
2672+
if (object.modificationTime() != null) {
2673+
entry.setModTime(Date.from(object.modificationTime().toInstant()));
2674+
}
2675+
entry.setSize(object.size());
2676+
tarOutputStream.putArchiveEntry(entry);
2677+
ByteStreams.copy(object.stream(), tarOutputStream);
2678+
}
2679+
tarOutputStream.closeArchiveEntry();
2680+
}
2681+
tarOutputStream.finish();
2682+
} finally {
2683+
if (tarOutputStream != null) tarOutputStream.flush();
2684+
if (sos != null) sos.flush();
2685+
if (bos != null) bos.flush();
2686+
if (fos != null) fos.flush();
2687+
if (tarOutputStream != null) tarOutputStream.close();
2688+
if (sos != null) sos.close();
2689+
if (bos != null) bos.close();
2690+
if (fos != null) fos.close();
2691+
}
2692+
2693+
Multimap<String, String> headers = newMultimap(args.extraHeaders());
2694+
headers.putAll(args.genHeaders());
2695+
headers.put("X-Amz-Meta-Snowball-Auto-Extract", "true");
2696+
2697+
if (args.stagingFilename() == null) {
2698+
byte[] data = baos.toByteArray();
2699+
return putObject(
2700+
args.bucket(),
2701+
args.region(),
2702+
args.object(),
2703+
data,
2704+
data.length,
2705+
headers,
2706+
args.extraQueryParams());
2707+
}
2708+
2709+
long length = Paths.get(args.stagingFilename()).toFile().length();
2710+
if (length > ObjectWriteArgs.MAX_OBJECT_SIZE) {
2711+
throw new IllegalArgumentException(
2712+
"tarball size " + length + " is more than maximum allowed 5TiB");
2713+
}
2714+
try (RandomAccessFile file = new RandomAccessFile(args.stagingFilename(), "r")) {
2715+
return putObject(
2716+
args.bucket(),
2717+
args.region(),
2718+
args.object(),
2719+
file,
2720+
length,
2721+
headers,
2722+
args.extraQueryParams());
2723+
}
2724+
}
2725+
25942726
public static Builder builder() {
25952727
return new Builder();
25962728
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/*
2+
* MinIO Java SDK for Amazon S3 Compatible Cloud Storage, (C) 2021 MinIO, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.minio;
18+
19+
import java.io.InputStream;
20+
import java.time.ZonedDateTime;
21+
import java.util.Objects;
22+
import javax.annotation.Nonnull;
23+
import javax.annotation.Nullable;
24+
25+
/** Single object entry for {@link UploadSnowballObjectsArgs#objects}. */
26+
public class SnowballObject {
27+
private String name;
28+
private InputStream stream;
29+
private long size;
30+
private ZonedDateTime modificationTime;
31+
private String filename;
32+
33+
public SnowballObject(
34+
@Nonnull String name,
35+
@Nonnull InputStream stream,
36+
long size,
37+
@Nullable ZonedDateTime modificationTime) {
38+
if (name == null || name.isEmpty()) throw new IllegalArgumentException("name must be provided");
39+
this.name = name.startsWith("/") ? name.substring(1) : name;
40+
this.stream = Objects.requireNonNull(stream, "stream must not be null");
41+
if (size < 0) throw new IllegalArgumentException("size cannot be negative value");
42+
this.size = size;
43+
this.modificationTime = modificationTime;
44+
}
45+
46+
public SnowballObject(@Nonnull String name, @Nonnull String filename) {
47+
if (name == null || name.isEmpty()) throw new IllegalArgumentException("name must be provided");
48+
this.name = name.startsWith("/") ? name.substring(1) : name;
49+
if (filename == null || filename.isEmpty()) {
50+
throw new IllegalArgumentException("filename must be provided");
51+
}
52+
this.filename = filename;
53+
}
54+
55+
public String name() {
56+
return this.name;
57+
}
58+
59+
public InputStream stream() {
60+
return this.stream;
61+
}
62+
63+
public long size() {
64+
return this.size;
65+
}
66+
67+
public String filename() {
68+
return this.filename;
69+
}
70+
71+
public ZonedDateTime modificationTime() {
72+
return this.modificationTime;
73+
}
74+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/*
2+
* MinIO Java SDK for Amazon S3 Compatible Cloud Storage, (C) 2021 MinIO, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.minio;
18+
19+
import java.security.SecureRandom;
20+
import java.util.Objects;
21+
import java.util.Random;
22+
23+
/** Argument class of {@link MinioClient#uploadSnowballObjects}. */
24+
public class UploadSnowballObjectsArgs extends ObjectWriteArgs {
25+
private static final Random random = new Random(new SecureRandom().nextLong());
26+
27+
private Iterable<SnowballObject> objects;
28+
private String stagingFilename;
29+
private boolean compression;
30+
31+
public Iterable<SnowballObject> objects() {
32+
return this.objects;
33+
}
34+
35+
public String stagingFilename() {
36+
return stagingFilename;
37+
}
38+
39+
public boolean compression() {
40+
return compression;
41+
}
42+
43+
public static Builder builder() {
44+
return new Builder();
45+
}
46+
47+
/** Argument builder of {@link UploadSnowballObjectsArgs}. */
48+
public static final class Builder
49+
extends ObjectWriteArgs.Builder<Builder, UploadSnowballObjectsArgs> {
50+
private void validateObjects(Iterable<SnowballObject> objects) {
51+
validateNotNull(objects, "objects");
52+
}
53+
54+
@Override
55+
protected void validate(UploadSnowballObjectsArgs args) {
56+
args.objectName = "snowball." + random.nextLong() + ".tar";
57+
validateObjects(args.objects);
58+
super.validate(args);
59+
}
60+
61+
public Builder objects(Iterable<SnowballObject> objects) {
62+
validateObjects(objects);
63+
operations.add(args -> args.objects = objects);
64+
return this;
65+
}
66+
67+
public Builder stagingFilename(String stagingFilename) {
68+
if (stagingFilename != null && stagingFilename.isEmpty()) {
69+
throw new IllegalArgumentException("staging filename must not be empty");
70+
}
71+
operations.add(args -> args.stagingFilename = stagingFilename);
72+
return this;
73+
}
74+
75+
public Builder compression(boolean compression) {
76+
operations.add(args -> args.compression = compression);
77+
return this;
78+
}
79+
}
80+
81+
@Override
82+
public boolean equals(Object o) {
83+
if (this == o) return true;
84+
if (!(o instanceof UploadSnowballObjectsArgs)) return false;
85+
if (!super.equals(o)) return false;
86+
UploadSnowballObjectsArgs that = (UploadSnowballObjectsArgs) o;
87+
return Objects.equals(objects, that.objects)
88+
&& Objects.equals(stagingFilename, that.stagingFilename)
89+
&& compression == that.compression;
90+
}
91+
92+
@Override
93+
public int hashCode() {
94+
return Objects.hash(super.hashCode(), objects, stagingFilename, compression);
95+
}
96+
}

build.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ subprojects {
5858
compile "com.fasterxml.jackson.core:jackson-core:2.11.2"
5959
compile "com.fasterxml.jackson.core:jackson-databind:2.11.2"
6060
compile "org.bouncycastle:bcprov-jdk15on:1.69"
61+
compile 'org.apache.commons:commons-compress:1.21'
62+
compile "org.xerial.snappy:snappy-java:1.1.8.4"
6163
compileOnly "com.github.spotbugs:spotbugs-annotations:4.1.2"
6264

6365
testImplementation "com.squareup.okhttp3:mockwebserver:4.8.1"

docs/API.md

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ MinioClient minioClient =
4545
| [`makeBucket`](#makeBucket) | [`setObjectTags`](#setObjectTags) |
4646
| [`removeBucket`](#removeBucket) | [`statObject`](#statObject) |
4747
| [`setBucketEncryption`](#setBucketEncryption) | [`uploadObject`](#uploadObject) |
48-
| [`setBucketLifecycle`](#setBucketLifecycle) | |
48+
| [`setBucketLifecycle`](#setBucketLifecycle) | [`uploadSnowballObjects`](#uploadSnowballObjects) |
4949
| [`setBucketNotification`](#setBucketNotification) | |
5050
| [`setBucketPolicy`](#setBucketPolicy) | |
5151
| [`setBucketReplication`](#setBucketReplication) | |
@@ -1543,6 +1543,36 @@ minioClient.uploadObject(
15431543
.build());
15441544
```
15451545

1546+
<a name="uploadSnowballObjects"></a>
1547+
### uploadSnowballObjects(UploadSnowballObjectsArgs args)
1548+
`public void uploadSnowballObjects(UploadSnowballObjectsArgs args)` _[[Javadoc]](http://minio.github.io/minio-java/io/minio/MinioClient.html#uploadSnowballObjects-io.minio.UploadSnowballObjectsArgs-)_
1549+
1550+
Uploads multiple objects in a single put call. It is done by creating intermediate TAR file optionally compressed which is uploaded to S3 service.
1551+
1552+
__Parameters__
1553+
| Parameter | Type | Description |
1554+
|:----------|:------------------------------|:------------|
1555+
| ``args`` | _[UploadSnowballObjectsArgs]_ | Arguments. |
1556+
1557+
__Example__
1558+
```java
1559+
List<SnowballObject> objects = new ArrayList<SnowballObject>();
1560+
objects.add(
1561+
new SnowballObject(
1562+
"my-object-one",
1563+
new ByteArrayInputStream("hello".getBytes(StandardCharsets.UTF_8)),
1564+
5,
1565+
null));
1566+
objects.add(
1567+
new SnowballObject(
1568+
"my-object-two",
1569+
new ByteArrayInputStream("java".getBytes(StandardCharsets.UTF_8)),
1570+
4,
1571+
null));
1572+
minioClient.uploadSnowballObjects(
1573+
UploadSnowballObjectsArgs.builder().bucket("my-bucketname").objects(objects).build());
1574+
```
1575+
15461576
<a name="removeObject"></a>
15471577
### removeObject(RemoveObjectArgs args)
15481578
`public void removeObject(RemoveObjectArgs args)` _[[Javadoc]](http://minio.github.io/minio-java/io/minio/MinioClient.html#removeObject-io.minio.RemoveObjectArgs-)_
@@ -1830,6 +1860,7 @@ ObjectStat objectStat =
18301860
[CopyObjectArgs]: http://minio.github.io/minio-java/io/minio/CopyObjectArgs.html
18311861
[PutObjectArgs]: http://minio.github.io/minio-java/io/minio/PutObjectArgs.html
18321862
[UploadObjectArgs]: http://minio.github.io/minio-java/io/minio/UploadObjectArgs.html
1863+
[UploadSnowballObjectsArgs]: http://minio.github.io/minio-java/io/minio/UploadSnowballObjectsArgs.html
18331864
[ComposeObjectArgs]: http://minio.github.io/minio-java/io/minio/ComposeObjectArgs.html
18341865
[ObjectWriteResponse]: http://minio.github.io/minio-java/io/minio/ObjectWriteResponse.html
18351866
[ListBucketsArgs]: http://minio.github.io/minio-java/io/minio/ListBucketsArgs.html

0 commit comments

Comments
 (0)