Allow multiple account providers

This allows different backend blobstores to be configured at
runtime, based on the request access key. As a result different
frontend auth can also be used. Currently this can only be done
programmatically and not through properties config file.
pull/51/merge
Ka-Hing Cheung 2015-03-24 11:51:50 -07:00 zatwierdzone przez Andrew Gaul
rodzic 2dc2cd60eb
commit 87bed283cb
2 zmienionych plików z 173 dodań i 123 usunięć

Wyświetl plik

@ -21,6 +21,7 @@ import static java.util.Objects.requireNonNull;
import static com.google.common.base.Preconditions.checkArgument;
import java.net.URI;
import java.util.Map;
import com.google.common.base.Optional;
import com.google.common.base.Strings;
@ -39,6 +40,7 @@ import org.jclouds.blobstore.BlobStore;
*/
public final class S3Proxy {
private final Server server;
private final S3ProxyHandler handler;
static {
// Prevent Jetty from rewriting headers:
@ -81,8 +83,9 @@ public final class S3Proxy {
connector.setHost(endpoint.getHost());
connector.setPort(endpoint.getPort());
server.addConnector(connector);
server.setHandler(new S3ProxyHandler(blobStore, identity, credential,
virtualHost));
handler = new S3ProxyHandler(blobStore, identity, credential,
virtualHost);
server.setHandler(handler);
}
public static final class Builder {
@ -150,4 +153,9 @@ public final class S3Proxy {
public String getState() {
return server.getState();
}
public void setProviders(
Map<String, Map.Entry<String, BlobStore>> providers) {
handler.setProviders(providers);
}
}

Wyświetl plik

@ -34,6 +34,7 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
@ -60,6 +61,7 @@ import com.google.common.base.Strings;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;
import com.google.common.collect.SortedSetMultimap;
import com.google.common.collect.TreeMultimap;
import com.google.common.hash.HashCode;
@ -149,10 +151,9 @@ final class S3ProxyHandler extends AbstractHandler {
"log-delivery-write"
);
private final BlobStore blobStore;
private final String blobStoreType;
private final String identity;
private final String credential;
private Map<String, Map.Entry<String, BlobStore>> providers =
new HashMap<>();
private final BlobStore defaultBlobStore;
private final Optional<String> virtualHost;
private final XMLInputFactory xmlInputFactory =
XMLInputFactory.newInstance();
@ -161,16 +162,22 @@ final class S3ProxyHandler extends AbstractHandler {
S3ProxyHandler(BlobStore blobStore, String identity, String credential,
Optional<String> virtualHost) {
this.blobStore = requireNonNull(blobStore);
this.blobStoreType =
blobStore.getContext().unwrap().getProviderMetadata().getId();
this.identity = identity;
this.credential = credential;
requireNonNull(blobStore);
if (identity != null) {
providers.put(identity, Maps.immutableEntry(credential, blobStore));
defaultBlobStore = null;
} else {
defaultBlobStore = blobStore;
}
this.virtualHost = requireNonNull(virtualHost);
xmlOutputFactory.setProperty("javax.xml.stream.isRepairingNamespaces",
Boolean.FALSE);
}
private static String getBlobStoreType(BlobStore blobStore) {
return blobStore.getContext().unwrap().getProviderMetadata().getId();
}
@Override
public void handle(String target, Request baseRequest,
HttpServletRequest request, HttpServletResponse response)
@ -220,7 +227,7 @@ final class S3ProxyHandler extends AbstractHandler {
}
}
if (identity != null && !hasDateHeader && !hasXAmzDateHeader &&
if (defaultBlobStore == null && !hasDateHeader && !hasXAmzDateHeader &&
request.getParameter("Expires") == null) {
sendSimpleErrorResponse(response, S3ErrorCode.ACCESS_DENIED,
"AWS authentication requires a valid Date or" +
@ -247,54 +254,58 @@ final class S3ProxyHandler extends AbstractHandler {
}
}
if (identity != null) {
String expectedSignature = createAuthorizationSignature(request,
uri, identity, credential);
String headerAuthorization = request.getHeader(
HttpHeaders.AUTHORIZATION);
String headerIdentity = null;
String headerSignature = null;
if (headerAuthorization != null &&
headerAuthorization.startsWith("AWS ")) {
BlobStore blobStore;
String requestIdentity = null;
String requestSignature = null;
String headerAuthorization = request.getHeader(
HttpHeaders.AUTHORIZATION);
if (headerAuthorization != null) {
if (headerAuthorization.startsWith("AWS ")) {
String[] values =
headerAuthorization.substring(4).split(":", 2);
if (values.length != 2) {
throw new S3Exception(S3ErrorCode.INVALID_ARGUMENT);
}
headerIdentity = values[0];
headerSignature = values[1];
} else if (headerAuthorization != null &&
headerAuthorization.startsWith("AWS4-HMAC-SHA256 ")) {
requestIdentity = values[0];
requestSignature = values[1];
} else if (headerAuthorization.startsWith("AWS4-HMAC-SHA256 ")) {
// Fail V4 signature requests to allow clients to retry with V2.
throw new S3Exception(S3ErrorCode.INVALID_ARGUMENT);
}
String parameterIdentity = request.getParameter("AWSAccessKeyId");
String parameterSignature = request.getParameter("Signature");
} else {
requestIdentity = request.getParameter("AWSAccessKeyId");
requestSignature = request.getParameter("Signature");
}
if (headerIdentity != null && headerSignature != null) {
if (!identity.equals(headerIdentity)) {
throw new S3Exception(S3ErrorCode.INVALID_ACCESS_KEY_ID);
} else if (!expectedSignature.equals(headerSignature)) {
throw new S3Exception(S3ErrorCode.SIGNATURE_DOES_NOT_MATCH);
}
} else if (parameterIdentity != null &&
parameterSignature != null) {
if (!identity.equals(parameterIdentity)) {
throw new S3Exception(S3ErrorCode.INVALID_ACCESS_KEY_ID);
} else if (!expectedSignature.equals(parameterSignature)) {
throw new S3Exception(S3ErrorCode.SIGNATURE_DOES_NOT_MATCH);
}
if (requestIdentity != null) {
Map.Entry<String, BlobStore> provider =
providers.get(requestIdentity);
if (provider == null) {
throw new S3Exception(S3ErrorCode.INVALID_ACCESS_KEY_ID);
}
String expiresString = request.getParameter("Expires");
if (expiresString != null) {
long expires = Long.parseLong(expiresString);
long nowSeconds = System.currentTimeMillis() / 1000;
if (nowSeconds > expires) {
throw new S3Exception(S3ErrorCode.ACCESS_DENIED);
}
String expectedSignature = createAuthorizationSignature(request,
uri, requestIdentity, provider.getKey());
if (!expectedSignature.equals(requestSignature)) {
throw new S3Exception(S3ErrorCode.SIGNATURE_DOES_NOT_MATCH);
}
blobStore = provider.getValue();
String expiresString = request.getParameter("Expires");
if (expiresString != null) {
long expires = Long.parseLong(expiresString);
long nowSeconds = System.currentTimeMillis() / 1000;
if (nowSeconds > expires) {
throw new S3Exception(S3ErrorCode.ACCESS_DENIED);
}
} else {
}
} else {
if (defaultBlobStore == null) {
throw new S3Exception(S3ErrorCode.ACCESS_DENIED);
} else {
blobStore = defaultBlobStore;
}
}
@ -316,79 +327,82 @@ final class S3ProxyHandler extends AbstractHandler {
switch (method) {
case "DELETE":
if (path.length <= 2 || path[2].isEmpty()) {
handleContainerDelete(response, path[1]);
handleContainerDelete(response, blobStore, path[1]);
baseRequest.setHandled(true);
return;
} else if (uploadId != null) {
handleAbortMultipartUpload(request, response, path[1], path[2],
uploadId);
handleAbortMultipartUpload(request, response, blobStore,
path[1], path[2], uploadId);
baseRequest.setHandled(true);
return;
} else {
handleBlobRemove(response, path[1], path[2]);
handleBlobRemove(response, blobStore, path[1], path[2]);
baseRequest.setHandled(true);
return;
}
case "GET":
if (uri.equals("/")) {
handleContainerList(response);
handleContainerList(response, blobStore);
baseRequest.setHandled(true);
return;
} else if (path.length <= 2 || path[2].isEmpty()) {
if ("".equals(request.getParameter("acl"))) {
handleGetContainerAcl(response, path[1]);
handleGetContainerAcl(response, blobStore, path[1]);
baseRequest.setHandled(true);
return;
} else if ("".equals(request.getParameter("location"))) {
handleContainerLocation(response, path[1]);
handleContainerLocation(response, blobStore, path[1]);
baseRequest.setHandled(true);
return;
} else if ("".equals(request.getParameter("uploads"))) {
handleListMultipartUploads(response, uploadId);
baseRequest.setHandled(true);
return;
}
handleBlobList(request, response, path[1]);
baseRequest.setHandled(true);
return;
} else {
if ("".equals(request.getParameter("acl"))) {
handleGetBlobAcl(response, path[1], path[2]);
baseRequest.setHandled(true);
return;
} else if (uploadId != null) {
handleListParts(request, response, path[1], path[2],
handleListMultipartUploads(response, blobStore,
uploadId);
baseRequest.setHandled(true);
return;
}
handleGetBlob(request, response, path[1], path[2]);
handleBlobList(request, response, blobStore, path[1]);
baseRequest.setHandled(true);
return;
} else {
if ("".equals(request.getParameter("acl"))) {
handleGetBlobAcl(response, path[1], blobStore,
path[2]);
baseRequest.setHandled(true);
return;
} else if (uploadId != null) {
handleListParts(request, response, blobStore, path[1],
path[2], uploadId);
baseRequest.setHandled(true);
return;
}
handleGetBlob(request, response, blobStore, path[1],
path[2]);
baseRequest.setHandled(true);
return;
}
case "HEAD":
if (path.length <= 2 || path[2].isEmpty()) {
handleContainerExists(response, path[1]);
handleContainerExists(response, blobStore, path[1]);
baseRequest.setHandled(true);
return;
} else {
handleBlobMetadata(response, path[1], path[2]);
handleBlobMetadata(response, blobStore, path[1], path[2]);
baseRequest.setHandled(true);
return;
}
case "POST":
if ("".equals(request.getParameter("delete"))) {
handleMultiBlobRemove(request, response, path[1]);
handleMultiBlobRemove(request, response, blobStore, path[1]);
baseRequest.setHandled(true);
return;
} else if ("".equals(request.getParameter("uploads"))) {
handleInitiateMultipartUpload(request, response, path[1],
path[2]);
handleInitiateMultipartUpload(request, response, blobStore,
path[1], path[2]);
baseRequest.setHandled(true);
return;
} else if (uploadId != null) {
handleCompleteMultipartUpload(request, response, path[1],
path[2], uploadId);
handleCompleteMultipartUpload(request, response, blobStore,
path[1], path[2], uploadId);
baseRequest.setHandled(true);
return;
}
@ -396,29 +410,31 @@ final class S3ProxyHandler extends AbstractHandler {
case "PUT":
if (path.length <= 2 || path[2].isEmpty()) {
if ("".equals(request.getParameter("acl"))) {
handleSetContainerAcl(request, response, path[1]);
handleSetContainerAcl(request, response, blobStore,
path[1]);
baseRequest.setHandled(true);
return;
}
handleContainerCreate(request, response, path[1]);
handleContainerCreate(request, response, blobStore, path[1]);
baseRequest.setHandled(true);
return;
} else if (uploadId != null) {
handleUploadPart(request, response, path[1], path[2],
uploadId);
handleUploadPart(request, response, blobStore, path[1],
path[2], uploadId);
baseRequest.setHandled(true);
return;
} else if (request.getHeader("x-amz-copy-source") != null) {
handleCopyBlob(request, response, path[1], path[2]);
handleCopyBlob(request, response, blobStore, path[1], path[2]);
baseRequest.setHandled(true);
return;
} else {
if ("".equals(request.getParameter("acl"))) {
handleSetBlobAcl(request, response, path[1], path[2]);
handleSetBlobAcl(request, response, blobStore, path[1],
path[2]);
baseRequest.setHandled(true);
return;
}
handlePutBlob(request, response, path[1], path[2]);
handlePutBlob(request, response, blobStore, path[1], path[2]);
baseRequest.setHandled(true);
return;
}
@ -430,8 +446,9 @@ final class S3ProxyHandler extends AbstractHandler {
}
private void handleGetContainerAcl(HttpServletResponse response,
String containerName) throws IOException {
BlobStore blobStore, String containerName) throws IOException {
ContainerAccess access;
String blobStoreType = getBlobStoreType(blobStore);
if (blobStoreType.equals("filesystem") ||
blobStoreType.equals("transient")) {
access = ContainerAccess.PRIVATE;
@ -505,8 +522,8 @@ final class S3ProxyHandler extends AbstractHandler {
}
private void handleSetContainerAcl(HttpServletRequest request,
HttpServletResponse response, String containerName)
throws IOException, S3Exception {
HttpServletResponse response, BlobStore blobStore,
String containerName) throws IOException, S3Exception {
ContainerAccess access;
String cannedAcl = request.getHeader("x-amz-acl");
@ -521,6 +538,7 @@ final class S3ProxyHandler extends AbstractHandler {
return;
}
String blobStoreType = getBlobStoreType(blobStore);
if (!(blobStoreType.equals("filesystem") ||
blobStoreType.equals("transient"))) {
blobStore.setContainerAccess(containerName, access);
@ -528,8 +546,10 @@ final class S3ProxyHandler extends AbstractHandler {
}
private void handleGetBlobAcl(HttpServletResponse response,
String containerName, String blobName) throws IOException {
String containerName, BlobStore blobStore,
String blobName) throws IOException {
BlobAccess access;
String blobStoreType = getBlobStoreType(blobStore);
if (blobStoreType.equals("filesystem") ||
blobStoreType.equals("transient")) {
access = BlobAccess.PRIVATE;
@ -603,8 +623,9 @@ final class S3ProxyHandler extends AbstractHandler {
}
private void handleSetBlobAcl(HttpServletRequest request,
HttpServletResponse response, String containerName,
String blobName) throws IOException, S3Exception {
HttpServletResponse response, BlobStore blobStore,
String containerName, String blobName)
throws IOException, S3Exception {
BlobAccess access;
String cannedAcl = request.getHeader("x-amz-acl");
@ -619,14 +640,15 @@ final class S3ProxyHandler extends AbstractHandler {
return;
}
String blobStoreType = getBlobStoreType(blobStore);
if (!(blobStoreType.equals("filesystem") ||
blobStoreType.equals("transient"))) {
blobStore.setBlobAccess(containerName, blobName, access);
}
}
private void handleContainerList(HttpServletResponse response)
throws IOException {
private void handleContainerList(HttpServletResponse response,
BlobStore blobStore) throws IOException {
try (Writer writer = response.getWriter()) {
XMLStreamWriter xml = xmlOutputFactory.createXMLStreamWriter(
writer);
@ -667,7 +689,7 @@ final class S3ProxyHandler extends AbstractHandler {
}
private void handleContainerLocation(HttpServletResponse response,
String containerName) throws IOException {
BlobStore blobStore, String containerName) throws IOException {
try (Writer writer = response.getWriter()) {
XMLStreamWriter xml = xmlOutputFactory.createXMLStreamWriter(
writer);
@ -683,21 +705,23 @@ final class S3ProxyHandler extends AbstractHandler {
}
private void handleListMultipartUploads(HttpServletResponse response,
String uploadId) throws IOException, S3Exception {
BlobStore blobStore, String uploadId)
throws IOException, S3Exception {
// TODO: list all blobs starting with uploadId
throw new S3Exception(S3ErrorCode.NOT_IMPLEMENTED);
}
private void handleContainerExists(HttpServletResponse response,
String containerName) throws IOException, S3Exception {
BlobStore blobStore, String containerName)
throws IOException, S3Exception {
if (!blobStore.containerExists(containerName)) {
throw new S3Exception(S3ErrorCode.NO_SUCH_BUCKET);
}
}
private void handleContainerCreate(HttpServletRequest request,
HttpServletResponse response, String containerName)
throws IOException, S3Exception {
HttpServletResponse response, BlobStore blobStore,
String containerName) throws IOException, S3Exception {
if (containerName.isEmpty()) {
throw new S3Exception(S3ErrorCode.METHOD_NOT_ALLOWED);
}
@ -771,7 +795,8 @@ final class S3ProxyHandler extends AbstractHandler {
}
private void handleContainerDelete(HttpServletResponse response,
String containerName) throws IOException, S3Exception {
BlobStore blobStore, String containerName)
throws IOException, S3Exception {
if (!blobStore.containerExists(containerName)) {
throw new S3Exception(S3ErrorCode.NO_SUCH_BUCKET);
}
@ -782,8 +807,8 @@ final class S3ProxyHandler extends AbstractHandler {
}
private void handleBlobList(HttpServletRequest request,
HttpServletResponse response, String containerName)
throws IOException, S3Exception {
HttpServletResponse response, BlobStore blobStore,
String containerName) throws IOException, S3Exception {
ListContainerOptions options = new ListContainerOptions();
String delimiter = request.getParameter("delimiter");
if (!(delimiter != null && delimiter.equals("/"))) {
@ -903,6 +928,7 @@ final class S3ProxyHandler extends AbstractHandler {
String eTag = metadata.getETag();
if (eTag != null) {
xml.writeStartElement("ETag");
String blobStoreType = getBlobStoreType(blobStore);
if (blobStoreType.equals("google-cloud-storage")) {
eTag = BaseEncoding.base16().lowerCase().encode(
BaseEncoding.base64().decode(eTag));
@ -942,15 +968,15 @@ final class S3ProxyHandler extends AbstractHandler {
}
private void handleBlobRemove(HttpServletResponse response,
String containerName, String blobName)
throws IOException, S3Exception {
BlobStore blobStore, String containerName,
String blobName) throws IOException, S3Exception {
blobStore.removeBlob(containerName, blobName);
response.sendError(HttpServletResponse.SC_NO_CONTENT);
}
private void handleMultiBlobRemove(HttpServletRequest request,
HttpServletResponse response, String containerName)
throws IOException {
HttpServletResponse response, BlobStore blobStore,
String containerName) throws IOException {
try (InputStream is = request.getInputStream();
Writer writer = new OutputStreamWriter(response.getOutputStream(),
StandardCharsets.UTF_8)) {
@ -978,8 +1004,8 @@ final class S3ProxyHandler extends AbstractHandler {
}
private void handleBlobMetadata(HttpServletResponse response,
String containerName, String blobName)
throws IOException, S3Exception {
BlobStore blobStore, String containerName,
String blobName) throws IOException, S3Exception {
BlobMetadata metadata = blobStore.blobMetadata(containerName, blobName);
if (metadata == null) {
throw new S3Exception(S3ErrorCode.NO_SUCH_KEY);
@ -990,8 +1016,9 @@ final class S3ProxyHandler extends AbstractHandler {
}
private void handleGetBlob(HttpServletRequest request,
HttpServletResponse response, String containerName,
String blobName) throws IOException, S3Exception {
HttpServletResponse response, BlobStore blobStore,
String containerName, String blobName)
throws IOException, S3Exception {
int status = HttpServletResponse.SC_OK;
GetOptions options = new GetOptions();
String range = request.getHeader(HttpHeaders.RANGE);
@ -1026,8 +1053,9 @@ final class S3ProxyHandler extends AbstractHandler {
}
private void handleCopyBlob(HttpServletRequest request,
HttpServletResponse response, String destContainerName,
String destBlobName) throws IOException, S3Exception {
HttpServletResponse response, BlobStore blobStore,
String destContainerName, String destBlobName)
throws IOException, S3Exception {
String copySourceHeader = request.getHeader("x-amz-copy-source");
if (copySourceHeader.startsWith("/")) {
// Some clients like boto do not include the leading slash
@ -1071,6 +1099,7 @@ final class S3ProxyHandler extends AbstractHandler {
}
PutOptions options = new PutOptions();
String blobStoreType = getBlobStoreType(blobStore);
if (blobStoreType.equals("azureblob") &&
contentLength > 64 * 1024 * 1024) {
options.multipart(true);
@ -1103,8 +1132,9 @@ final class S3ProxyHandler extends AbstractHandler {
}
private void handlePutBlob(HttpServletRequest request,
HttpServletResponse response, String containerName,
String blobName) throws IOException, S3Exception {
HttpServletResponse response, BlobStore blobStore,
String containerName, String blobName)
throws IOException, S3Exception {
// Flag headers present since HttpServletResponse.getHeader returns
// null for empty headers values.
String contentLengthString = null;
@ -1156,6 +1186,7 @@ final class S3ProxyHandler extends AbstractHandler {
}
PutOptions options = new PutOptions();
String blobStoreType = getBlobStoreType(blobStore);
if (blobStoreType.equals("azureblob") &&
contentLength > 64 * 1024 * 1024) {
options.multipart(true);
@ -1199,13 +1230,14 @@ final class S3ProxyHandler extends AbstractHandler {
// TODO: jclouds should include this in PutOptions
String cannedAcl = request.getHeader("x-amz-acl");
if (cannedAcl != null && !cannedAcl.equals("private")) {
handleSetBlobAcl(request, response, containerName, blobName);
handleSetBlobAcl(request, response, blobStore, containerName,
blobName);
}
}
private void handleInitiateMultipartUpload(HttpServletRequest request,
HttpServletResponse response, String containerName,
String blobName) throws IOException {
HttpServletResponse response, BlobStore blobStore,
String containerName, String blobName) throws IOException {
String uploadId = FAKE_UPLOAD_ID + UUID.randomUUID().toString();
ByteSource payload = ByteSource.empty();
BlobBuilder.PayloadBlobBuilder builder = blobStore
@ -1242,8 +1274,9 @@ final class S3ProxyHandler extends AbstractHandler {
}
private void handleCompleteMultipartUpload(HttpServletRequest request,
HttpServletResponse response, String containerName,
String blobName, String uploadId) throws IOException, S3Exception {
HttpServletResponse response, BlobStore blobStore,
String containerName, String blobName, String uploadId)
throws IOException, S3Exception {
Collection<String> partNames = new ArrayList<>();
long totalContentLength = 0;
try (InputStream is = request.getInputStream()) {
@ -1317,6 +1350,7 @@ final class S3ProxyHandler extends AbstractHandler {
if (eTag != null) {
xml.writeStartElement("ETag");
String blobStoreType = getBlobStoreType(blobStore);
if (blobStoreType.equals("google-cloud-storage")) {
eTag = BaseEncoding.base16().lowerCase().encode(
BaseEncoding.base64().decode(eTag));
@ -1333,8 +1367,9 @@ final class S3ProxyHandler extends AbstractHandler {
}
private void handleAbortMultipartUpload(HttpServletRequest request,
HttpServletResponse response, String containerName,
String blobName, String uploadId) throws IOException, S3Exception {
HttpServletResponse response, BlobStore blobStore,
String containerName, String blobName, String uploadId)
throws IOException, S3Exception {
if (!blobStore.blobExists(containerName, uploadId)) {
throw new S3Exception(S3ErrorCode.NO_SUCH_UPLOAD);
}
@ -1353,8 +1388,9 @@ final class S3ProxyHandler extends AbstractHandler {
}
private void handleListParts(HttpServletRequest request,
HttpServletResponse response, String containerName,
String blobName, String uploadId) throws IOException {
HttpServletResponse response, BlobStore blobStore,
String containerName, String blobName, String uploadId)
throws IOException {
try (Writer writer = response.getWriter()) {
XMLStreamWriter xml = xmlOutputFactory.createXMLStreamWriter(
writer);
@ -1441,6 +1477,7 @@ final class S3ProxyHandler extends AbstractHandler {
String eTag = sm.getETag();
if (eTag != null) {
xml.writeStartElement("ETag");
String blobStoreType = getBlobStoreType(blobStore);
if (blobStoreType.equals("google-cloud-storage")) {
eTag = BaseEncoding.base16().lowerCase().encode(
BaseEncoding.base64().decode(eTag));
@ -1465,8 +1502,8 @@ final class S3ProxyHandler extends AbstractHandler {
}
private void handleUploadPart(HttpServletRequest request,
HttpServletResponse response, String containerName,
String blobName, String uploadId)
HttpServletResponse response, BlobStore blobStore,
String containerName, String blobName, String uploadId)
throws IOException, S3Exception {
// TODO: duplicated from handlePutBlob
String contentLengthString = null;
@ -1610,6 +1647,11 @@ final class S3ProxyHandler extends AbstractHandler {
}
}
public void setProviders(
Map<String, Map.Entry<String, BlobStore>> providers) {
this.providers = ImmutableMap.copyOf(providers);
}
static class S3Exception extends Exception {
private final S3ErrorCode error;