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.Collections;
28  import java.util.HashMap;
29  import java.util.Map;
30  
31  import io.minio.GetObjectArgs;
32  import io.minio.MinioClient;
33  import io.minio.StatObjectArgs;
34  import io.minio.UploadObjectArgs;
35  import io.minio.credentials.Provider;
36  import io.minio.credentials.StaticProvider;
37  import io.minio.errors.ErrorResponseException;
38  import org.eclipse.aether.ConfigurationProperties;
39  import org.eclipse.aether.RepositorySystemSession;
40  import org.eclipse.aether.repository.AuthenticationContext;
41  import org.eclipse.aether.repository.RemoteRepository;
42  import org.eclipse.aether.spi.connector.transport.AbstractTransporter;
43  import org.eclipse.aether.spi.connector.transport.GetTask;
44  import org.eclipse.aether.spi.connector.transport.PeekTask;
45  import org.eclipse.aether.spi.connector.transport.PutTask;
46  import org.eclipse.aether.spi.connector.transport.Transporter;
47  import org.eclipse.aether.spi.io.PathProcessor;
48  import org.eclipse.aether.transfer.NoTransporterException;
49  import org.eclipse.aether.util.ConfigUtils;
50  
51  import static java.util.Objects.requireNonNull;
52  
53  /**
54   * A transporter for S3 backed by MinIO Java.
55   *
56   * @since 2.0.2
57   */
58  final class MinioTransporter extends AbstractTransporter implements Transporter {
59      private final URI baseUri;
60  
61      private final Map<String, String> headers;
62  
63      private final MinioClient client;
64  
65      private final ObjectNameMapper objectNameMapper;
66  
67      private final PathProcessor pathProcessor;
68  
69      MinioTransporter(
70              RepositorySystemSession session,
71              RemoteRepository repository,
72              ObjectNameMapperFactory objectNameMapperFactory,
73              PathProcessor pathProcessor)
74              throws NoTransporterException {
75          try {
76              URI uri = new URI(repository.getUrl()).parseServerAuthority();
77              if (uri.isOpaque()) {
78                  throw new URISyntaxException(repository.getUrl(), "URL must not be opaque");
79              }
80              if (uri.getRawFragment() != null || uri.getRawQuery() != null) {
81                  throw new URISyntaxException(repository.getUrl(), "URL must not have fragment or query");
82              }
83              String path = uri.getPath();
84              if (path == null) {
85                  path = "/";
86              }
87              if (!path.startsWith("/")) {
88                  path = "/" + path;
89              }
90              if (!path.endsWith("/")) {
91                  path = path + "/";
92              }
93              this.baseUri = URI.create(uri.getScheme() + "://" + uri.getRawAuthority() + path);
94          } catch (URISyntaxException e) {
95              throw new NoTransporterException(repository, e.getMessage(), e);
96          }
97  
98          HashMap<String, String> headers = new HashMap<>();
99          @SuppressWarnings("unchecked")
100         Map<Object, Object> configuredHeaders = (Map<Object, Object>) ConfigUtils.getMap(
101                 session,
102                 Collections.emptyMap(),
103                 ConfigurationProperties.HTTP_HEADERS + "." + repository.getId(),
104                 ConfigurationProperties.HTTP_HEADERS);
105         if (configuredHeaders != null) {
106             configuredHeaders.forEach((k, v) -> headers.put(String.valueOf(k), v != null ? String.valueOf(v) : null));
107         }
108         this.headers = headers;
109 
110         String username = null;
111         String password = null;
112         try (AuthenticationContext repoAuthContext = AuthenticationContext.forRepository(session, repository)) {
113             if (repoAuthContext != null) {
114                 username = repoAuthContext.get(AuthenticationContext.USERNAME);
115                 password = repoAuthContext.get(AuthenticationContext.PASSWORD);
116             }
117         }
118         if (username == null || password == null) {
119             throw new IllegalStateException(
120                     "Minio transport: No accessKey and/or secretKey provided for repository " + repository.getId());
121         }
122 
123         Provider credentialsProvider = new StaticProvider(username, password, null);
124         this.client = MinioClient.builder()
125                 .endpoint(repository.getUrl())
126                 .credentialsProvider(credentialsProvider)
127                 .build();
128         this.objectNameMapper = objectNameMapperFactory.create(session, repository, client, headers);
129         this.pathProcessor = requireNonNull(pathProcessor);
130     }
131 
132     @Override
133     public int classify(Throwable error) {
134         if (error instanceof ErrorResponseException) {
135             String errorCode = ((ErrorResponseException) error).errorResponse().code();
136             if ("NoSuchKey".equals(errorCode) || "NoSuchBucket".equals(errorCode)) {
137                 return ERROR_NOT_FOUND;
138             }
139         }
140         return ERROR_OTHER;
141     }
142 
143     @Override
144     protected void implPeek(PeekTask task) throws Exception {
145         ObjectName objectName =
146                 objectNameMapper.name(baseUri.relativize(task.getLocation()).getPath());
147         StatObjectArgs.Builder builder = StatObjectArgs.builder()
148                 .bucket(objectName.getBucket())
149                 .object(objectName.getName())
150                 .extraHeaders(headers);
151         client.statObject(builder.build());
152     }
153 
154     @Override
155     protected void implGet(GetTask task) throws Exception {
156         ObjectName objectName =
157                 objectNameMapper.name(baseUri.relativize(task.getLocation()).getPath());
158         try (InputStream stream = client.getObject(GetObjectArgs.builder()
159                 .bucket(objectName.getBucket())
160                 .object(objectName.getName())
161                 .extraHeaders(headers)
162                 .build())) {
163             final Path dataFile = task.getDataPath();
164             if (dataFile == null) {
165                 utilGet(task, stream, true, -1, false);
166             } else {
167                 try (PathProcessor.CollocatedTempFile tempFile = pathProcessor.newTempFile(dataFile)) {
168                     task.setDataPath(tempFile.getPath(), false);
169                     utilGet(task, stream, true, -1, false);
170                     tempFile.move();
171                 } finally {
172                     task.setDataPath(dataFile);
173                 }
174             }
175         }
176     }
177 
178     @Override
179     protected void implPut(PutTask task) throws Exception {
180         ObjectName objectName =
181                 objectNameMapper.name(baseUri.relativize(task.getLocation()).getPath());
182         task.getListener().transportStarted(0, task.getDataLength());
183         final Path dataFile = task.getDataPath();
184         if (dataFile == null) {
185             try (PathProcessor.TempFile tempFile = pathProcessor.newTempFile()) {
186                 Files.copy(task.newInputStream(), tempFile.getPath(), StandardCopyOption.REPLACE_EXISTING);
187                 client.uploadObject(UploadObjectArgs.builder()
188                         .bucket(objectName.getBucket())
189                         .object(objectName.getName())
190                         .filename(tempFile.getPath().toString())
191                         .build());
192             }
193         } else {
194             client.uploadObject(UploadObjectArgs.builder()
195                     .bucket(objectName.getBucket())
196                     .object(objectName.getName())
197                     .filename(dataFile.toString())
198                     .build());
199         }
200     }
201 
202     @Override
203     protected void implClose() {
204         try {
205             client.close();
206         } catch (Exception e) {
207             throw new RuntimeException(e);
208         }
209     }
210 }