View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.eclipse.aether.transport.minio;
20  
21  import java.io.InputStream;
22  import java.net.URI;
23  import java.net.URISyntaxException;
24  import java.nio.file.Files;
25  import java.nio.file.Path;
26  import java.nio.file.StandardCopyOption;
27  import java.util.HashMap;
28  import java.util.Map;
29  
30  import io.minio.GetObjectArgs;
31  import io.minio.MinioClient;
32  import io.minio.StatObjectArgs;
33  import io.minio.UploadObjectArgs;
34  import io.minio.credentials.Provider;
35  import io.minio.credentials.StaticProvider;
36  import io.minio.errors.ErrorResponseException;
37  import org.eclipse.aether.RepositorySystemSession;
38  import org.eclipse.aether.repository.AuthenticationContext;
39  import org.eclipse.aether.repository.RemoteRepository;
40  import org.eclipse.aether.spi.connector.transport.AbstractTransporter;
41  import org.eclipse.aether.spi.connector.transport.GetTask;
42  import org.eclipse.aether.spi.connector.transport.PeekTask;
43  import org.eclipse.aether.spi.connector.transport.PutTask;
44  import org.eclipse.aether.spi.connector.transport.Transporter;
45  import org.eclipse.aether.spi.io.PathProcessor;
46  import org.eclipse.aether.transfer.NoTransporterException;
47  import org.eclipse.aether.util.connector.transport.http.HttpTransporterUtils;
48  
49  import static java.util.Objects.requireNonNull;
50  
51  /**
52   * A transporter for S3 backed by MinIO Java.
53   *
54   * @since 2.0.2
55   */
56  final class MinioTransporter extends AbstractTransporter implements Transporter {
57      private final URI baseUri;
58  
59      private final Map<String, String> headers;
60  
61      private final MinioClient client;
62  
63      private final ObjectNameMapper objectNameMapper;
64  
65      private final PathProcessor pathProcessor;
66  
67      MinioTransporter(
68              RepositorySystemSession session,
69              RemoteRepository repository,
70              ObjectNameMapperFactory objectNameMapperFactory,
71              PathProcessor pathProcessor)
72              throws NoTransporterException {
73          try {
74              URI uri = new URI(repository.getUrl()).parseServerAuthority();
75              if (uri.isOpaque()) {
76                  throw new URISyntaxException(repository.getUrl(), "URL must not be opaque");
77              }
78              if (uri.getRawFragment() != null || uri.getRawQuery() != null) {
79                  throw new URISyntaxException(repository.getUrl(), "URL must not have fragment or query");
80              }
81              String path = uri.getPath();
82              if (path == null) {
83                  path = "/";
84              }
85              if (!path.startsWith("/")) {
86                  path = "/" + path;
87              }
88              if (!path.endsWith("/")) {
89                  path = path + "/";
90              }
91              this.baseUri = URI.create(uri.getScheme() + "://" + uri.getRawAuthority() + path);
92          } catch (URISyntaxException e) {
93              throw new NoTransporterException(repository, e.getMessage(), e);
94          }
95  
96          HashMap<String, String> headers = new HashMap<>();
97          Map<String, String> configuredHeaders = HttpTransporterUtils.getHttpHeaders(session, repository);
98          if (configuredHeaders != null) {
99              headers.putAll(configuredHeaders);
100         }
101         this.headers = headers;
102 
103         String username = null;
104         String password = null;
105         try (AuthenticationContext repoAuthContext = AuthenticationContext.forRepository(session, repository)) {
106             if (repoAuthContext != null) {
107                 username = repoAuthContext.get(AuthenticationContext.USERNAME);
108                 password = repoAuthContext.get(AuthenticationContext.PASSWORD);
109             }
110         }
111         if (username == null || password == null) {
112             throw new IllegalStateException(
113                     "Minio transport: No accessKey and/or secretKey provided for repository " + repository.getId());
114         }
115 
116         Provider credentialsProvider = new StaticProvider(username, password, null);
117         this.client = MinioClient.builder()
118                 .endpoint(repository.getUrl())
119                 .credentialsProvider(credentialsProvider)
120                 .build();
121         this.objectNameMapper = objectNameMapperFactory.create(session, repository, client, headers);
122         this.pathProcessor = requireNonNull(pathProcessor);
123     }
124 
125     @Override
126     public int classify(Throwable error) {
127         if (error instanceof ErrorResponseException) {
128             String errorCode = ((ErrorResponseException) error).errorResponse().code();
129             if ("NoSuchKey".equals(errorCode) || "NoSuchBucket".equals(errorCode)) {
130                 return ERROR_NOT_FOUND;
131             }
132         }
133         return ERROR_OTHER;
134     }
135 
136     @Override
137     protected void implPeek(PeekTask task) throws Exception {
138         ObjectName objectName =
139                 objectNameMapper.name(baseUri.relativize(task.getLocation()).getPath());
140         StatObjectArgs.Builder builder = StatObjectArgs.builder()
141                 .bucket(objectName.getBucket())
142                 .object(objectName.getName())
143                 .extraHeaders(headers);
144         client.statObject(builder.build());
145     }
146 
147     @Override
148     protected void implGet(GetTask task) throws Exception {
149         ObjectName objectName =
150                 objectNameMapper.name(baseUri.relativize(task.getLocation()).getPath());
151         try (InputStream stream = client.getObject(GetObjectArgs.builder()
152                 .bucket(objectName.getBucket())
153                 .object(objectName.getName())
154                 .extraHeaders(headers)
155                 .build())) {
156             final Path dataFile = task.getDataPath();
157             if (dataFile == null) {
158                 utilGet(task, stream, true, -1, false);
159             } else {
160                 try (PathProcessor.CollocatedTempFile tempFile = pathProcessor.newTempFile(dataFile)) {
161                     task.setDataPath(tempFile.getPath(), false);
162                     utilGet(task, stream, true, -1, false);
163                     tempFile.move();
164                 } finally {
165                     task.setDataPath(dataFile);
166                 }
167             }
168         }
169     }
170 
171     @Override
172     protected void implPut(PutTask task) throws Exception {
173         ObjectName objectName =
174                 objectNameMapper.name(baseUri.relativize(task.getLocation()).getPath());
175         task.getListener().transportStarted(0, task.getDataLength());
176         final Path dataFile = task.getDataPath();
177         if (dataFile == null) {
178             try (PathProcessor.TempFile tempFile = pathProcessor.newTempFile()) {
179                 Files.copy(task.newInputStream(), tempFile.getPath(), StandardCopyOption.REPLACE_EXISTING);
180                 client.uploadObject(UploadObjectArgs.builder()
181                         .bucket(objectName.getBucket())
182                         .object(objectName.getName())
183                         .filename(tempFile.getPath().toString())
184                         .build());
185             }
186         } else {
187             client.uploadObject(UploadObjectArgs.builder()
188                     .bucket(objectName.getBucket())
189                     .object(objectName.getName())
190                     .filename(dataFile.toString())
191                     .build());
192         }
193     }
194 
195     @Override
196     protected void implClose() {
197         try {
198             client.close();
199         } catch (Exception e) {
200             throw new RuntimeException(e);
201         }
202     }
203 }