Importar arquivos do Amazon S3 em Java
Para importar um objeto do Amazon S3 para um arquivo local, use o getObject
do AWS SDK for Java v2 com um transformador de resposta para arquivo. O importador abaixo baixa o
arquivo para um local temporário, verifica um checksum SHA-256 confiável e publica o arquivo de
destino somente depois que o download completo passa pela verificação. Ele também oferece suporte
ao cliente assíncrono padrão, ao cliente AWS CRT e ao S3 Transfer Manager.
Introdução ao Amazon S3 e ao Java
Este exemplo recupera uma chave conhecida de um bucket S3 de uso geral. Você precisa da região do bucket, de permissão para ler esse objeto e de um SHA-256 fornecido por quem o publicou ou calculado a partir do arquivo original antes do upload. Mantenha o objeto inalterado durante o download. O checksum identifica os bytes que você pretende importar, inclusive no caso de um objeto vazio ou de um arquivo binário.
Configurar o AWS SDK v2
Os comandos usam Bash no Linux, a configuração regional C.UTF-8 e instalações
separadas do JDK e do Maven. A configuração regional UTF-8 mantém intactos os argumentos de chave e
nome de arquivo com caracteres fora do conjunto ASCII. As versões testadas são OpenJDK 21.0.12.1,
Maven 3.10.0, AWS SDK v2 2.55.13 e AWS CRT 0.48.5. As versões das
dependências abaixo são fixadas para testes reproduzíveis; consulte as
versões do AWS SDK ao atualizá-las.
Crie um projeto novo fora de um checkout do Maven que tenha um diretório
.mvn. O Maven pode herdar as opções da JVM, extensões e opções de comando
desse diretório. O bloco de comandos a seguir verifica as ferramentas e rejeita essa configuração
herdada antes de criar arquivos; ele mantém seu shell no diretório atual. Se
s3-import já existir, use outro diretório novo em vez de excluir seus arquivos.
(
for tool in java javac mvn; do
command -v "$tool" >/dev/null || {
printf 'Missing prerequisite: %s\n' "$tool" >&2
exit 1
}
done
parent="$PWD"
while :; do
[ ! -e "$parent/.mvn" ] || {
printf 'Use a directory outside the enclosing Maven configuration.\n' >&2
exit 1
}
[ "$parent" != / ] || break
parent="${parent%/*}"
[ -n "$parent" ] || parent=/
done
mkdir s3-import && mkdir -p s3-import/src/main/java
)
Salve este pom.xml completo em s3-import/. O BOM mantém os
módulos do SDK na mesma versão; o CRT tem sua própria versão. O plugin de dependências copia os JARs
de runtime necessários para o inicializador do Java.
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>example</groupId>
<artifactId>s3-import</artifactId>
<version>1.0.0</version>
<properties>
<maven.compiler.release>21</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>bom</artifactId>
<version>2.55.13</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>s3</artifactId>
</dependency>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>s3-transfer-manager</artifactId>
</dependency>
<dependency>
<groupId>software.amazon.awssdk.crt</groupId>
<artifactId>aws-crt</artifactId>
<version>0.48.5</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-nop</artifactId>
<version>1.7.36</version>
<scope>runtime</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.14.1</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>3.8.1</version>
<executions>
<execution>
<phase>package</phase>
<goals><goal>copy-dependencies</goal></goals>
<configuration><includeScope>runtime</includeScope></configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
Configurar as credenciais da AWS
Configure as credenciais por meio do login da AWS ou da função de runtime que sua organização já
utiliza. Este programa usa a
cadeia padrão de provedores de credenciais do SDK, que verifica
propriedades de sistema do Java, variáveis de ambiente, perfis compartilhados e funções de runtime.
As credenciais temporárias de ambiente exigem os três valores: AWS_ACCESS_KEY_ID,
AWS_SECRET_ACCESS_KEY e AWS_SESSION_TOKEN. Não os inclua no código-fonte Java nem
nos arquivos do Maven.
As dependências do exemplo oferecem suporte a credenciais de ambiente e perfis compartilhados
básicos. Um perfil que obtém credenciais por SSO ou AssumeRole também precisa dos módulos
correspondentes do SDK, como sso, ssooidc ou
sts, na versão do BOM. Conclua esse login antes de executar o importador.
Configurar a região da AWS
Passe a região do bucket como segundo argumento do programa, por exemplo,
us-east-1. Cada cliente usa Region.of(regionName), portanto não depende
de uma região definida no seu shell ou perfil compartilhado. O argumento da chave é a chave S3
literal, não uma URL s3://; não faça por conta própria a codificação de
espaços ou sinais de porcentagem para URL.
Importar arquivos do S3
Salve o código a seguir como s3-import/src/main/java/S3FileImporter.java:
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.security.MessageDigest;
import java.util.HexFormat;
import java.util.Set;
import java.util.concurrent.CompletionException;
import software.amazon.awssdk.core.async.AsyncResponseTransformer;
import software.amazon.awssdk.core.sync.ResponseTransformer;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3AsyncClient;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
import software.amazon.awssdk.services.s3.model.S3Exception;
import software.amazon.awssdk.transfer.s3.S3TransferManager;
import software.amazon.awssdk.transfer.s3.model.DownloadFileRequest;
public class S3FileImporter {
public static void main(String[] args) {
try {
run(args);
} catch (Exception error) {
Throwable cause = error;
while (cause instanceof CompletionException && cause.getCause() != null) {
cause = cause.getCause();
}
if (cause instanceof S3Exception s3Error) {
System.err.println("S3 download failed (HTTP " + s3Error.statusCode() + ").");
} else if (cause instanceof IOException || cause instanceof IllegalArgumentException) {
System.err.println(cause.getMessage());
} else {
System.err.println("Download failed; check credentials, connectivity, and local disk space.");
}
System.exit(1);
}
}
private static void run(String[] args) throws Exception {
if (args.length != 6 || !Set.of("sync", "async", "crt", "transfer").contains(args[0])) {
throw new IllegalArgumentException(
"Usage: S3FileImporter <sync|async|crt|transfer> <region> <bucket> <key> <destination> <sha256>");
}
String expectedHash = args[5];
if (!expectedHash.matches("[0-9a-fA-F]{64}")) {
throw new IllegalArgumentException("Supply the original object's 64-digit SHA-256.");
}
Path destination = Path.of(args[4]).toAbsolutePath();
if (Files.exists(destination, LinkOption.NOFOLLOW_LINKS)) {
throw new FileAlreadyExistsException("Destination already exists: " + destination);
}
GetObjectRequest request = GetObjectRequest.builder().bucket(args[2]).key(args[3]).build();
Region region = Region.of(args[1]);
Path stagingDirectory = Files.createTempDirectory(destination.getParent(), ".s3-import-");
Path stagedFile = stagingDirectory.resolve("object.bin");
try {
download(args[0], region, request, stagedFile);
if (!sha256(stagedFile).equalsIgnoreCase(expectedHash)) {
throw new IOException("SHA-256 mismatch; destination was not published.");
}
Files.move(stagedFile, destination);
System.out.println("Downloaded " + Files.size(destination) + " bytes to " + destination);
} finally {
Files.deleteIfExists(stagedFile);
Files.delete(stagingDirectory);
}
}
private static void download(String mode, Region region, GetObjectRequest request, Path file) {
if (mode.equals("sync")) {
try (S3Client client = S3Client.builder().region(region)
.overrideConfiguration(c -> c.retryStrategy(r -> r.maxAttempts(1))).build()) {
client.getObject(request, ResponseTransformer.toFile(file));
}
return;
}
if (mode.equals("async")) {
try (S3AsyncClient client = S3AsyncClient.builder().region(region)
.overrideConfiguration(c -> c.retryStrategy(r -> r.maxAttempts(1))).build()) {
client.getObject(request, AsyncResponseTransformer.toFile(file)).join();
}
return;
}
try (S3AsyncClient client = S3AsyncClient.crtBuilder().region(region)
.minimumPartSizeInBytes(8L * 1024 * 1024).maxConcurrency(2)
.retryConfiguration(r -> r.numRetries(0)).build()) {
if (mode.equals("crt")) {
client.getObject(request, AsyncResponseTransformer.toFile(file)).join();
} else {
try (S3TransferManager manager = S3TransferManager.builder().s3Client(client).build()) {
manager.downloadFile(DownloadFileRequest.builder()
.getObjectRequest(request).destination(file).build()).completionFuture().join();
}
}
}
}
private static String sha256(Path file) throws Exception {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
try (InputStream input = Files.newInputStream(file)) {
byte[] buffer = new byte[8192];
int count;
while ((count = input.read(buffer)) != -1) {
digest.update(buffer, 0, count);
}
}
return HexFormat.of().formatHex(digest.digest());
}
}
Dentro de s3-import/, compile e execute o modo síncrono. Substitua o bucket, a
chave, a região e o checksum pelos valores do seu objeto. O diretório pai do destino deve existir,
e o próprio destino deve ser novo. As aspas no classpath preservam o curinga para que o Java o
expanda.
mvn -B package &&
LC_ALL=C.UTF-8 java -cp 'target/classes:target/dependency/*' S3FileImporter \
sync us-east-1 your-bucket-name 'path/to/your/file.bin' \
'./downloaded-file.bin' 'REPLACE_WITH_ORIGINAL_SHA256'
Um comando bem-sucedido imprime a quantidade de bytes baixados e o destino e termina com status
zero. A verificação do checksum ocorre depois que o SDK conclui o arquivo e antes que
Files.move o publique. Ela lê o arquivo completo, portanto a existência do
arquivo, um trecho inicial não vazio ou um future concluído não são suficientes. Não obtenha o
checksum esperado calculando o hash do próprio arquivo baixado.
Tratar exceções
O comando termina com status um se o destino for rejeitado, o checksum estiver incorreto, houver uma falha do SDK ou ocorrer um erro local de E/S. Uma falha de download ou de checksum capturada remove o arquivo temporário e não deixa um novo arquivo no destino final. Um destino existente é preservado. Execute os downloads sequencialmente para um mesmo destino; este exemplo não coordena publicações simultâneas.
Para uma chave inexistente, o S3 retorna 404 quando quem faz a chamada também tem
s3:ListBucket e pode retornar 403 sem essa permissão. Portanto, um 403 não permite
distinguir uma chave inexistente de um acesso negado. Verifique a chave exata e as permissões com
base nas regras de permissão do GetObject. Se o checksum não
corresponder, confirme o hash fornecido por quem publicou o objeto e verifique se o objeto não
mudou antes de tentar novamente com um novo destino. Se houver uma falha de credenciais ou de
rede, corrija esse pré-requisito e repita o comando. Não considere uma exceção impressa como uma
importação bem-sucedida.
Tipos de cliente S3
Escolha o modo alterando apenas o primeiro argumento do programa e usando um novo destino.
Cliente síncrono (S3Client)
sync bloqueia a execução dentro de getObject até que o
transformador de resposta para arquivo conclua seu trabalho. É um ponto de partida útil para um
comando que importa um objeto.
Cliente assíncrono (S3AsyncClient)
async usa o cliente assíncrono padrão do Java. Em uma aplicação, seu future
permite que outros trabalhos continuem enquanto o download ocorre. Este comando chama
join() antes de verificar o arquivo, fechar o cliente ou terminar; caso
contrário, o processo poderia terminar antes da importação.
Cliente com desempenho aprimorado (AWS CRT)
crt usa o
cliente S3 baseado no AWS CRT, uma implementação de
S3AsyncClient que pode recuperar objetos grandes por intervalos de bytes. Sua
dependência nativa está incluída na compilação com Maven. Ele tem menos opções de configuração que
o cliente padrão. Meça sua carga de trabalho antes de escolhê-lo pelo desempenho; estes comandos
não prometem uma taxa de transferência específica.
Otimizar o desempenho
Gravação em disco por streaming
Os quatro modos usam arquivos de destino em vez de carregar o objeto inteiro em um array de bytes
do Java. A etapa de verificação SHA-256 também lê os dados em buffers de tamanho fixo. Reserve
espaço em disco para o objeto inteiro e use um diretório de destino com permissão de escrita. Se o
processo for encerrado à força, inspecione e remova o diretório .s3-import-* que
restar antes de tentar novamente; o bloco finally do Java não consegue fazer
a limpeza após o encerramento forçado do processo.
Downloads em múltiplas partes
transfer usa o S3 Transfer Manager com o cliente
CRT fornecido explicitamente. Downloads grandes podem usar requisições paralelas de intervalos de
bytes; o exemplo define um tamanho mínimo de parte de 8 MiB e um máximo de duas conexões. São
intervalos de download, não uploads multipart do S3. O programa aguarda a conclusão do future do
gerenciador antes de verificar o checksum, e o gerenciador é fechado antes do seu cliente.
As novas tentativas estão desativadas nesta demonstração para que uma requisição com falha seja informada a quem fez a chamada. Em uma aplicação, escolha uma política com um limite de novas tentativas e tempos limite para o cliente selecionado, em vez de presumir que os builders padrão e CRT aceitam configurações idênticas. Caso contrário, um download pode ficar esperando em uma conexão lenta; este programa não define um prazo máximo para a transferência inteira.
Permissões do IAM
Para a versão atual de uma chave conhecida em um bucket de uso geral, conceda
s3:GetObject no ARN do objeto. Não é necessário listar o bucket para este
importador. Por exemplo, peça ao administrador da AWS que adapte esta política ao objeto de que
você precisa:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::your-bucket-name/path/to/your/file.bin"
}
]
}
Este guia pressupõe um objeto que possa ser recuperado imediatamente e esteja criptografado com chaves gerenciadas pelo S3. Objetos arquivados podem precisar de restauração, e a criptografia com KMS acrescenta requisitos de permissão para as chaves. Consulte o mapeamento de permissões do guia do usuário do S3 para esses casos. Este programa não solicita um ID de versão específico nem lida com chaves SSE-C.
Boas práticas
Mantenha um checksum confiável junto de cada objeto que você pretende importar. Se depois você integrar este comando a uma tarefa, repasse seu status diferente de zero ao agendador e permita que o processamento subsequente leia apenas o arquivo verificado no destino final. Escolha chaves imutáveis ou adicione recuperação com suporte a versões quando quem publica os objetos os substitui mantendo as mesmas chaves.
Para importar objetos do S3 para um fluxo gerenciado de processamento de arquivos, consulte o S3 Import Robot e o SDK Java da Transloadit.
