View Javadoc
1   /*
2    * Copyright 2023 Adrian Herscu
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 dev.aherscu.qa.s3.publisher.maven.plugin.util;
18  
19  import java.io.*;
20  import java.text.*;
21  import java.util.*;
22  
23  import org.apache.commons.codec.binary.*;
24  import org.apache.commons.codec.digest.*;
25  import org.apache.maven.plugin.logging.*;
26  
27  import com.amazonaws.*;
28  import com.amazonaws.services.s3.*;
29  import com.amazonaws.services.s3.model.*;
30  
31  import dev.aherscu.qa.s3.publisher.maven.plugin.config.*;
32  
33  public class S3Uploader {
34  
35      private final AmazonS3Client                  client;
36      private final Log                             log;
37      private final String                          bucketName;
38      private final File                            inputDirectory;
39      private final List<ManagedFileContentEncoder> contentEncoders;
40      private final boolean                         refreshExpiredObjects;
41      private final SimpleDateFormat                httpDateFormat;
42  
43      public S3Uploader(final AmazonS3Client client, final Log log,
44          final List<ManagedFileContentEncoder> contentEncoders,
45          final String bucketName,
46          final File inputDirectory, final File tmpDirectory,
47          final boolean refreshExpiredObjects) {
48          this.client = client;
49          this.log = log;
50          this.bucketName = bucketName;
51          this.inputDirectory = inputDirectory;
52          this.contentEncoders = contentEncoders;
53          this.refreshExpiredObjects = refreshExpiredObjects;
54          httpDateFormat =
55              new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US);
56          httpDateFormat.setTimeZone(TimeZone.getTimeZone("GMT"));
57      }
58  
59      private static String calculateETag(final File file) throws Exception {
60          return Hex.encodeHexString(DigestUtils.md5(new FileInputStream(file)));
61      }
62  
63      private static boolean isLocalFileSameAsRemote(final File localFile,
64          final ObjectMetadata remoteFileMetadata) throws Exception {
65          return remoteFileMetadata != null &&
66              remoteFileMetadata.getETag().equals(calculateETag(localFile));
67      }
68  
69      private static boolean isMetadataExpired() {
70          // NOTE: quick and dirty fix
71          return true;
72      }
73  
74      private static String transformFileNameSlashesToS3(final String fileName) {
75          return fileName.replace('\\', '/');
76      }
77  
78      public void uploadManagedFile(final ManagedFile managedFile)
79          throws Exception {
80          final File encodedFile = encodeManagedFile(managedFile);
81          final String remoteFileName = getRemoteFileName(managedFile);
82          final ObjectMetadata remoteMetadata =
83              retrieveObjectMetadata(remoteFileName);
84          final ObjectMetadataBuilder objectMetadataBuilder =
85              new ObjectMetadataBuilder(managedFile, encodedFile);
86          final ObjectMetadata objectMetadata =
87              objectMetadataBuilder.buildMetadata();
88  
89          if (!isLocalFileSameAsRemote(encodedFile, remoteMetadata)) {
90              log.debug("uploading file " + managedFile.getFilename() + " to "
91                  + bucketName);
92              client.putObject(bucketName, remoteFileName,
93                  new FileInputStream(encodedFile), objectMetadata);
94              setObjectAcl(managedFile, remoteFileName);
95          } else {
96              if (refreshExpiredObjects && isMetadataExpired()) {
97                  log.debug("refreshing metadata for file "
98                      + managedFile.getFilename());
99                  client.copyObject(
100                     buildCopyObjectRequest(remoteFileName, objectMetadata));
101                 setObjectAcl(managedFile, remoteFileName);
102             } else {
103                 log.debug("the object " + remoteFileName + " stored at "
104                     + bucketName + " does not require update");
105             }
106         }
107     }
108 
109     private CopyObjectRequest buildCopyObjectRequest(
110         final String remoteFileName,
111         final ObjectMetadata objectMetadata) {
112         return new CopyObjectRequest(bucketName, remoteFileName, bucketName,
113             remoteFileName)
114             .withNewObjectMetadata(objectMetadata);
115     }
116 
117     private File encodeManagedFile(final ManagedFile managedFile)
118         throws Exception {
119         File encodedFile = null;
120         for (final ManagedFileContentEncoder contentEncoder : contentEncoders) {
121             if (contentEncoder.isContentEncodingSupported(
122                 managedFile.getMetadata().getContentEncoding())) {
123                 log.debug("contentEncoding file " + managedFile.getFilename());
124                 encodedFile = contentEncoder.encode(managedFile);
125             }
126         }
127         return encodedFile;
128     }
129 
130     private String getRemoteFileName(final ManagedFile managedFile) {
131         final File file = new File(managedFile.getFilename());
132         return transformFileNameSlashesToS3(removeBasePath(file));
133     }
134 
135     private void logObjectMetadata(final ObjectMetadata objectMetadata) {
136         log.debug("  ETag: " + objectMetadata.getETag());
137         log.debug("  ContentType: " + objectMetadata.getContentType());
138         log.debug("  CacheControl: " + objectMetadata.getCacheControl());
139         log.debug("  ContentEncoding: " + objectMetadata.getContentEncoding());
140         log.debug("  ContentLength: " + objectMetadata.getContentLength());
141         log.debug("  Expires: "
142             + (objectMetadata.getHttpExpiresDate() == null ? "unknown"
143                 : httpDateFormat.format(objectMetadata.getHttpExpiresDate())));
144         log.debug("  LastModified: " + objectMetadata.getLastModified());
145     }
146 
147     private String removeBasePath(final File file) {
148         return file.getPath().substring(inputDirectory.getPath().length() + 1);
149     }
150 
151     private ObjectMetadata retrieveObjectMetadata(final String remoteFileName) {
152         log.debug("retrieving metadata for " + remoteFileName);
153         ObjectMetadata objectMetadata = null;
154         try {
155             objectMetadata =
156                 client.getObjectMetadata(bucketName, remoteFileName);
157             logObjectMetadata(objectMetadata);
158         } catch (final AmazonServiceException e) {
159             log.debug(e);
160         }
161         return objectMetadata;
162     }
163 
164     private void setObjectAcl(final ManagedFile managedFile,
165         final String remoteFileName) {
166         final CannedAccessControlList acl = CannedAccessControlList
167             .valueOf(managedFile.getMetadata().getCannedAcl());
168         client.setObjectAcl(bucketName, remoteFileName, acl);
169     }
170 }