/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.elasticsearch.cloud.azure.blobstore;
import com.microsoft.azure.storage.LocationMode;
import com.microsoft.azure.storage.StorageException;
import org.apache.logging.log4j.Logger;
import org.elasticsearch.common.Nullable;
import org.elasticsearch.common.blobstore.BlobMetaData;
import org.elasticsearch.common.blobstore.BlobPath;
import org.elasticsearch.common.blobstore.support.AbstractBlobContainer;
import org.elasticsearch.common.io.Streams;
import org.elasticsearch.common.logging.Loggers;
import org.elasticsearch.repositories.RepositoryException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URISyntaxException;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.NoSuchFileException;
import java.util.Map;
public class AzureBlobContainer extends AbstractBlobContainer {
protected final Logger logger = Loggers.getLogger(AzureBlobContainer.class);
protected final AzureBlobStore blobStore;
protected final String keyPath;
protected final String repositoryName;
public AzureBlobContainer(String repositoryName, BlobPath path, AzureBlobStore blobStore) {
super(path);
this.blobStore = blobStore;
this.keyPath = path.buildAsString();
this.repositoryName = repositoryName;
}
@Override
public boolean blobExists(String blobName) {
logger.trace("blobExists({})", blobName);
try {
return blobStore.blobExists(blobStore.container(), buildKey(blobName));
} catch (URISyntaxException | StorageException e) {
logger.warn("can not access [{}] in container {{}}: {}", blobName, blobStore.container(), e.getMessage());
}
return false;
}
@Override
public InputStream readBlob(String blobName) throws IOException {
logger.trace("readBlob({})", blobName);
if (blobStore.getLocationMode() == LocationMode.SECONDARY_ONLY && !blobExists(blobName)) {
// On Azure, if the location path is a secondary location, and the blob does not
// exist, instead of returning immediately from the getInputStream call below
// with a 404 StorageException, Azure keeps trying and trying for a long timeout
// before throwing a storage exception. This can cause long delays in retrieving
// snapshots, so we first check if the blob exists before trying to open an input
// stream to it.
throw new NoSuchFileException("Blob [" + blobName + "] does not exist");
}
try {
return blobStore.getInputStream(blobStore.container(), buildKey(blobName));
} catch (StorageException e) {
if (e.getHttpStatusCode() == HttpURLConnection.HTTP_NOT_FOUND) {
throw new NoSuchFileException(e.getMessage());
}
throw new IOException(e);
} catch (URISyntaxException e) {
throw new IOException(e);
}
}
@Override
public void writeBlob(String blobName, InputStream inputStream, long blobSize) throws IOException {
if (blobExists(blobName)) {
throw new FileAlreadyExistsException("blob [" + blobName + "] already exists, cannot overwrite");
}
logger.trace("writeBlob({}, stream, {})", blobName, blobSize);
try (OutputStream stream = createOutput(blobName)) {
Streams.copy(inputStream, stream);
}
}
private OutputStream createOutput(String blobName) throws IOException {
try {
return new AzureOutputStream(blobStore.getOutputStream(blobStore.container(), buildKey(blobName)));
} catch (StorageException e) {
if (e.getHttpStatusCode() == HttpURLConnection.HTTP_NOT_FOUND) {
throw new NoSuchFileException(e.getMessage());
}
throw new IOException(e);
} catch (URISyntaxException e) {
throw new IOException(e);
} catch (IllegalArgumentException e) {
throw new RepositoryException(repositoryName, e.getMessage());
}
}
@Override
public void deleteBlob(String blobName) throws IOException {
logger.trace("deleteBlob({})", blobName);
if (!blobExists(blobName)) {
throw new NoSuchFileException("Blob [" + blobName + "] does not exist");
}
try {
blobStore.deleteBlob(blobStore.container(), buildKey(blobName));
} catch (URISyntaxException | StorageException e) {
logger.warn("can not access [{}] in container {{}}: {}", blobName, blobStore.container(), e.getMessage());
throw new IOException(e);
}
}
@Override
public Map<String, BlobMetaData> listBlobsByPrefix(@Nullable String prefix) throws IOException {
logger.trace("listBlobsByPrefix({})", prefix);
try {
return blobStore.listBlobsByPrefix(blobStore.container(), keyPath, prefix);
} catch (URISyntaxException | StorageException e) {
logger.warn("can not access [{}] in container {{}}: {}", prefix, blobStore.container(), e.getMessage());
throw new IOException(e);
}
}
@Override
public void move(String sourceBlobName, String targetBlobName) throws IOException {
logger.trace("move({}, {})", sourceBlobName, targetBlobName);
try {
String source = keyPath + sourceBlobName;
String target = keyPath + targetBlobName;
logger.debug("moving blob [{}] to [{}] in container {{}}", source, target, blobStore.container());
blobStore.moveBlob(blobStore.container(), source, target);
} catch (URISyntaxException | StorageException e) {
logger.warn("can not move blob [{}] to [{}] in container {{}}: {}", sourceBlobName, targetBlobName, blobStore.container(), e.getMessage());
throw new IOException(e);
}
}
@Override
public Map<String, BlobMetaData> listBlobs() throws IOException {
logger.trace("listBlobs()");
return listBlobsByPrefix(null);
}
protected String buildKey(String blobName) {
return keyPath + (blobName == null ? "" : blobName);
}
}