View Javadoc
1   /*
2    * This file is part of Waarp Project (named also Waarp or GG).
3    *
4    *  Copyright (c) 2019, Waarp SAS, and individual contributors by the @author
5    *  tags. See the COPYRIGHT.txt in the distribution for a full listing of
6    * individual contributors.
7    *
8    *  All Waarp Project is free software: you can redistribute it and/or
9    * modify it under the terms of the GNU General Public License as published by
10   * the Free Software Foundation, either version 3 of the License, or (at your
11   * option) any later version.
12   *
13   * Waarp is distributed in the hope that it will be useful, but WITHOUT ANY
14   * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
15   * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
16   *
17   *  You should have received a copy of the GNU General Public License along with
18   * Waarp . If not, see <http://www.gnu.org/licenses/>.
19   */
20  package org.waarp.openr66.s3;
21  
22  import com.google.common.base.Function;
23  import com.google.common.collect.Iterators;
24  import io.minio.BucketExistsArgs;
25  import io.minio.GetObjectArgs;
26  import io.minio.GetObjectRetentionArgs;
27  import io.minio.GetObjectTagsArgs;
28  import io.minio.ListObjectsArgs;
29  import io.minio.MakeBucketArgs;
30  import io.minio.MinioClient;
31  import io.minio.ObjectWriteResponse;
32  import io.minio.RemoveObjectArgs;
33  import io.minio.Result;
34  import io.minio.SetObjectRetentionArgs;
35  import io.minio.SetObjectTagsArgs;
36  import io.minio.UploadObjectArgs;
37  import io.minio.errors.MinioException;
38  import io.minio.messages.Item;
39  import io.minio.messages.Retention;
40  import io.minio.messages.RetentionMode;
41  import io.minio.messages.Tags;
42  import org.checkerframework.checker.nullness.qual.Nullable;
43  import org.waarp.common.file.FileUtils;
44  import org.waarp.common.logging.WaarpLogger;
45  import org.waarp.common.logging.WaarpLoggerFactory;
46  import org.waarp.common.utility.ParametersChecker;
47  import org.waarp.common.utility.SingletonUtils;
48  import org.waarp.openr66.protocol.exception.OpenR66ProtocolNetworkException;
49  
50  import java.io.File;
51  import java.io.FileOutputStream;
52  import java.io.IOException;
53  import java.io.InputStream;
54  import java.net.URL;
55  import java.security.InvalidKeyException;
56  import java.security.NoSuchAlgorithmException;
57  import java.time.ZonedDateTime;
58  import java.util.Iterator;
59  import java.util.Map;
60  
61  import static org.waarp.common.file.FileUtils.*;
62  
63  /**
64   * Waarp R66 S3 Client
65   */
66  public class WaarpR66S3Client {
67    private static final WaarpLogger logger =
68        WaarpLoggerFactory.getLogger(WaarpR66S3Client.class);
69    public static final String BUCKET_OR_TARGET_CANNOT_BE_NULL_OR_EMPTY =
70        "Bucket or Target cannot be null or empty";
71    public static final String S_3_ISSUE = "S3 issue: ";
72    public static final String BUCKET_OR_SOURCE_CANNOT_BE_NULL_OR_EMPTY =
73        "Bucket or Source cannot be null or empty";
74  
75    private static final Function<Result<Item>, String> function =
76        new Function<Result<Item>, String>() {
77          @Override
78          public final @Nullable String apply(
79              @Nullable final Result<Item> itemResult) {
80            try {
81              if (itemResult != null) {
82                final Item item = itemResult.get();
83                if (item != null) {
84                  return item.objectName();
85                }
86              }
87              return null;
88            } catch (final MinioException | InvalidKeyException | IOException |
89                           NoSuchAlgorithmException e) {
90              logger.error(e.getMessage());
91              return null;
92            }
93          }
94        };
95    private final MinioClient minioClient;
96  
97    /**
98     * Initialize context for S3 Client
99     *
100    * @param accessKey
101    * @param secretKey
102    * @param endPointS3
103    */
104   public WaarpR66S3Client(final String accessKey, final String secretKey,
105                           final URL endPointS3) {
106     ParametersChecker.checkParameter("Parameters cannot be null or empty",
107                                      accessKey, secretKey, endPointS3);
108     // Create a minioClient with the MinIO server playground, its access key and secret key.
109     minioClient = MinioClient.builder().endpoint(endPointS3)
110                              .credentials(accessKey, secretKey).build();
111   }
112 
113   /**
114    * Create one file into S3 with optional Tags (null if none)
115    *
116    * @param bucketName
117    * @param targetName
118    * @param file
119    * @param tags
120    *
121    * @return versionId
122    *
123    * @throws OpenR66ProtocolNetworkException
124    */
125   public final String createFile(final String bucketName,
126                                  final String targetName, final File file,
127                                  final Map<String, String> tags)
128       throws OpenR66ProtocolNetworkException {
129     ParametersChecker.checkParameter(BUCKET_OR_TARGET_CANNOT_BE_NULL_OR_EMPTY,
130                                      bucketName, targetName);
131     ParametersChecker.checkParameter("File cannot be null", file);
132     if (!file.canRead()) {
133       throw new IllegalArgumentException(
134           "File cannot be read: " + file.getAbsolutePath());
135     }
136     boolean uploaded = false;
137     boolean error = false;
138     try {
139       // Make bucketName bucket if not exist.
140       final boolean found = minioClient.bucketExists(
141           BucketExistsArgs.builder().bucket(bucketName).build());
142       if (!found) {
143         // Make a new bucket
144         minioClient.makeBucket(
145             MakeBucketArgs.builder().bucket(bucketName).build());
146       } else {
147         logger.info("Bucket {} already exists.", bucketName);
148       }
149       // Upload file as object name targetName to bucket bucketName
150       final ObjectWriteResponse response = minioClient.uploadObject(
151           UploadObjectArgs.builder().bucket(bucketName).object(targetName)
152                           .filename(file.getAbsolutePath()).build());
153       uploaded = true;
154       logger.info("{} is successfully uploaded as object {} to bucket {}.",
155                   file.getAbsolutePath(), targetName, bucketName);
156       if (tags != null && !tags.isEmpty()) {
157         minioClient.setObjectTags(
158             SetObjectTagsArgs.builder().bucket(bucketName).object(targetName)
159                              .tags(tags).build());
160       }
161       logger.debug("Resp: {} {} {} {} {}", response.bucket(), response.object(),
162                    response.versionId(), response.etag(), response.region());
163       return response.versionId();
164     } catch (final MinioException | IOException | NoSuchAlgorithmException |
165                    InvalidKeyException e) {
166       logger.error(e.getMessage());
167       error = true;
168       throw new OpenR66ProtocolNetworkException(S_3_ISSUE + e.getMessage(), e);
169     } finally {
170       if (error && uploaded) {
171         // Clean incompletely object creation
172         try {
173           deleteFile(bucketName, targetName);
174         } catch (final Exception e) {
175           logger.warn(
176               "Error while cleaning S3 file incompletely created" + " : {}",
177               e.getMessage());
178         }
179       }
180     }
181   }
182 
183   /**
184    * Set a Tags to S3
185    *
186    * @param bucketName
187    * @param targetName
188    * @param tags
189    *
190    * @throws OpenR66ProtocolNetworkException
191    */
192   public final void setTags(final String bucketName, final String targetName,
193                             final Map<String, String> tags)
194       throws OpenR66ProtocolNetworkException {
195     ParametersChecker.checkParameter(BUCKET_OR_TARGET_CANNOT_BE_NULL_OR_EMPTY,
196                                      bucketName, targetName);
197     try {
198       if (tags != null && !tags.isEmpty()) {
199         minioClient.setObjectTags(
200             SetObjectTagsArgs.builder().bucket(bucketName).object(targetName)
201                              .tags(tags).build());
202       }
203     } catch (final MinioException | IOException | NoSuchAlgorithmException |
204                    InvalidKeyException e) {
205       logger.error(e.getMessage());
206       throw new OpenR66ProtocolNetworkException(S_3_ISSUE + e.getMessage(), e);
207     }
208   }
209 
210   /**
211    * Get the ZoneDateTime when this Object will be deleted
212    *
213    * @param bucketName
214    * @param sourceName
215    *
216    * @return the ZoneDateTime
217    *
218    * @throws OpenR66ProtocolNetworkException
219    */
220   public final ZonedDateTime getObjectRetention(final String bucketName,
221                                                 final String sourceName)
222       throws OpenR66ProtocolNetworkException {
223     ParametersChecker.checkParameter(BUCKET_OR_TARGET_CANNOT_BE_NULL_OR_EMPTY,
224                                      bucketName, sourceName);
225     try {
226       final Retention retention = minioClient.getObjectRetention(
227           GetObjectRetentionArgs.builder().bucket(bucketName).object(bucketName)
228                                 .build());
229       return retention.retainUntilDate();
230     } catch (final MinioException | InvalidKeyException | IOException |
231                    NoSuchAlgorithmException e) {
232       logger.error(e.getMessage());
233       throw new OpenR66ProtocolNetworkException(S_3_ISSUE + e.getMessage(), e);
234     }
235   }
236 
237   /**
238    * Bypass the governance retention and set a specific validity time in the future
239    *
240    * @param bucketName
241    * @param targetName
242    * @param retentionUntil
243    *
244    * @throws OpenR66ProtocolNetworkException
245    */
246   public final void bypassObjectRetention(final String bucketName,
247                                           final String targetName,
248                                           final ZonedDateTime retentionUntil)
249       throws OpenR66ProtocolNetworkException {
250     ParametersChecker.checkParameter(BUCKET_OR_TARGET_CANNOT_BE_NULL_OR_EMPTY,
251                                      bucketName, targetName);
252     ParametersChecker.checkParameter("Retention cannot be null",
253                                      retentionUntil);
254     if (retentionUntil.isBefore(ZonedDateTime.now())) {
255       logger.warn("Retention Date Time is before now");
256       throw new IllegalArgumentException("Retention Date Time is before now");
257     }
258     final Retention config =
259         new Retention(RetentionMode.COMPLIANCE, retentionUntil);
260     // Set object retention
261     try {
262       minioClient.setObjectRetention(
263           SetObjectRetentionArgs.builder().bucket(bucketName).object(targetName)
264                                 .config(config).bypassGovernanceMode(true)
265                                 .build());
266     } catch (final MinioException | InvalidKeyException | IOException |
267                    NoSuchAlgorithmException e) {
268       logger.error(e.getMessage());
269       throw new OpenR66ProtocolNetworkException(S_3_ISSUE + e.getMessage(), e);
270     }
271   }
272 
273   /**
274    * Get a File from S3
275    *
276    * @param bucketName
277    * @param sourceName
278    * @param file
279    * @param getTags if False, will return an empty Map
280    *
281    * @return the Tag as Map of Strings
282    *
283    * @throws OpenR66ProtocolNetworkException
284    */
285   public final Map<String, String> getFile(final String bucketName,
286                                            final String sourceName,
287                                            final File file,
288                                            final boolean getTags)
289       throws OpenR66ProtocolNetworkException {
290     ParametersChecker.checkParameter(BUCKET_OR_SOURCE_CANNOT_BE_NULL_OR_EMPTY,
291                                      bucketName, sourceName);
292     ParametersChecker.checkParameter("File cannot be null", file);
293     boolean downloaded = false;
294     boolean error = false;
295     // Get input stream
296     try (final InputStream stream = minioClient.getObject(
297         GetObjectArgs.builder().bucket(bucketName).object(sourceName).build());
298          final FileOutputStream outputStream = new FileOutputStream(file)) {
299       final byte[] buf = new byte[ZERO_COPY_CHUNK_SIZE];
300       int bytesRead;
301       while ((bytesRead = stream.read(buf, 0, buf.length)) >= 0) {
302         outputStream.write(buf, 0, bytesRead);
303       }
304       FileUtils.close(outputStream);
305       FileUtils.close(stream);
306       downloaded = true;
307       if (getTags) {
308         final Tags tags = minioClient.getObjectTags(
309             GetObjectTagsArgs.builder().bucket(bucketName).object(sourceName)
310                              .build());
311         return tags.get();
312       } else {
313         return SingletonUtils.singletonMap();
314       }
315     } catch (final MinioException | IOException | NoSuchAlgorithmException |
316                    InvalidKeyException e) {
317       logger.info(e);
318       error = true;
319       throw new OpenR66ProtocolNetworkException(S_3_ISSUE + e.getMessage(), e);
320     } finally {
321       if (error && downloaded) {
322         // Delete wrongly deleted file
323         file.delete();
324       }
325     }
326   }
327 
328   /**
329    * Get a Tags from S3
330    *
331    * @param bucketName
332    * @param sourceName
333    *
334    * @return the Tag as Map of Strings
335    *
336    * @throws OpenR66ProtocolNetworkException
337    */
338   public final Map<String, String> getTags(final String bucketName,
339                                            final String sourceName)
340       throws OpenR66ProtocolNetworkException {
341     ParametersChecker.checkParameter(BUCKET_OR_SOURCE_CANNOT_BE_NULL_OR_EMPTY,
342                                      bucketName, sourceName);
343     try {
344       final Tags tags = minioClient.getObjectTags(
345           GetObjectTagsArgs.builder().bucket(bucketName).object(sourceName)
346                            .build());
347 
348       return tags.get();
349     } catch (final MinioException | IOException | NoSuchAlgorithmException |
350                    InvalidKeyException e) {
351       logger.error(e.getMessage());
352       throw new OpenR66ProtocolNetworkException(S_3_ISSUE + e.getMessage(), e);
353     }
354   }
355 
356   /**
357    * Delete S3 source
358    *
359    * @param bucketName
360    * @param sourceName
361    *
362    * @throws OpenR66ProtocolNetworkException
363    */
364   public final void deleteFile(final String bucketName, final String sourceName)
365       throws OpenR66ProtocolNetworkException {
366     ParametersChecker.checkParameter(BUCKET_OR_SOURCE_CANNOT_BE_NULL_OR_EMPTY,
367                                      bucketName, sourceName);
368     try {
369       // Remove object.
370       minioClient.removeObject(
371           RemoveObjectArgs.builder().bucket(bucketName).object(sourceName)
372                           .build());
373     } catch (final MinioException | IOException | NoSuchAlgorithmException |
374                    InvalidKeyException e) {
375       logger.error(e.getMessage());
376       throw new OpenR66ProtocolNetworkException(S_3_ISSUE + e.getMessage(), e);
377     }
378   }
379 
380   /**
381    * Get sourceNames from S3 from the bucketName specified, names optionally starting with a String, recursively or not,
382    * and possibly unlimited (if limit is <= 0)
383    *
384    * @param bucketName
385    * @param optionalNameStartWith could be null or empty
386    * @param recursively False only from main directory, True scanning also subdirectories
387    * @param limit if <= 0, unlimited
388    *
389    * @return the Iterator on found sourceNames
390    *
391    * @throws OpenR66ProtocolNetworkException
392    */
393   public final Iterator<String> listObjectsFromBucket(final String bucketName,
394                                                       final String optionalNameStartWith,
395                                                       final boolean recursively,
396                                                       final int limit)
397       throws OpenR66ProtocolNetworkException {
398     ParametersChecker.checkParameter("Bucket cannot be null or empty",
399                                      bucketName);
400     try {
401       // List recursively
402       final ListObjectsArgs.Builder builder =
403           ListObjectsArgs.builder().bucket(bucketName).recursive(recursively);
404       if (ParametersChecker.isNotEmpty(optionalNameStartWith)) {
405         builder.prefix(optionalNameStartWith);
406       }
407       if (limit > 0) {
408         builder.maxKeys(limit);
409       }
410       final ListObjectsArgs args = builder.build();
411       final Iterable<Result<Item>> iterable = minioClient.listObjects(args);
412       return Iterators.transform(iterable.iterator(), function);
413     } catch (final Exception e) {
414       logger.error(e.getMessage());
415       throw new OpenR66ProtocolNetworkException(S_3_ISSUE + e.getMessage(), e);
416     }
417   }
418 }