Retomar uploads de arquivos em Java com tus-java-client
Uma requisição de upload com falha não deveria obrigar você a reenviar o arquivo inteiro. Este
passo a passo usa tus-java-client para fazer upload de um arquivo binário, se recuperar de uma falha
proposital do servidor e verificar os bytes armazenados. Você vai executar os dois lados localmente
e ver a nova tentativa começar no offset salvo pelo servidor.
Configurar o tus-java-client
Os comandos abaixo têm como alvo Linux com Bash, OpenJDK 21.0.12.1, Maven 3.9.16, Node.js 24.15.0, Corepack e Yarn 4.12.0. Essas são as versões de ferramentas testadas. O Node executa o servidor de teste local; sua aplicação Java não precisa dele ao se conectar a um endpoint tus existente.
Crie o projeto fora de uma aplicação existente. O lockfile vazio dá ao Yarn um limite de projeto
próprio, e a configuração local seleciona node_modules para as dependências do servidor. Este bloco
mantém seu terminal no diretório original e se recusa a usar um diretório
java-tus-demo já existente:
(
mkdir java-tus-demo &&
cd java-tus-demo &&
mkdir -p src/main/java &&
touch yarn.lock &&
printf 'nodeLinker: node-modules\n' > .yarnrc.yml
)
Abra java-tus-demo no seu editor e use-o como diretório de trabalho em dois terminais. Salve este
pom.xml completo na raiz:
<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>java-tus-demo</artifactId>
<version>1.0.0</version>
<properties>
<maven.compiler.release>21</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>io.tus.java.client</groupId>
<artifactId>tus-java-client</artifactId>
<version>0.5.1</version>
</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.9.0</version>
</plugin>
</plugins>
</build>
</project>
Iniciar um servidor tus local
Salve package.json ao lado do POM:
{
"name": "java-tus-demo",
"private": true,
"type": "module",
"packageManager": "yarn@4.12.0",
"dependencies": {
"@tus/file-store": "2.1.1",
"@tus/server": "2.4.5"
}
}
Salve o conteúdo a seguir como server.ts. Ele usa o
servidor tus para Node e o armazenamento em disco oficiais.
A chave de falha rejeita uma requisição de upload assim que o offset inicial dela atinge 1 MiB; os
bytes anteriores permanecem no disco. Deixe-a ativada na primeira execução.
import { createServer } from 'node:http'
import { FileStore } from '@tus/file-store'
import { Server } from '@tus/server'
const port = Number(process.argv[2] ?? '1080')
if (!Number.isInteger(port) || port < 0 || port > 65535) {
throw new Error('Port must be an integer from 0 to 65535')
}
const tus = new Server({
path: '/files',
datastore: new FileStore({ directory: './uploads' }),
})
let failOnce = true
const http = createServer((request, response) => {
// Java's HttpURLConnection uses this override when it cannot send PATCH.
if (request.method === 'POST' && request.headers['x-http-method-override'] === 'PATCH') {
request.method = 'PATCH'
}
response.on('finish', () => {
console.log(`${request.method} ${request.url} -> ${response.statusCode}, request offset=${request.headers['upload-offset'] ?? '-'}`)
})
if (failOnce && request.method === 'PATCH' && Number(request.headers['upload-offset']) >= 1048576) {
failOnce = false
request.resume()
response.writeHead(503, { 'Tus-Resumable': '1.0.0' })
response.end('Deliberate interruption')
return
}
void tus.handle(request, response).catch((error: unknown) => {
console.error(error)
response.destroy()
})
})
http.on('error', (error) => {
console.error(`Server failed: ${error.message}`)
process.exitCode = 1
})
http.listen(port, '127.0.0.1', () => {
const address = http.address()
if (address && typeof address !== 'string') {
console.log(`Listening at http://127.0.0.1:${address.port}/files`)
}
})
Essa substituição é necessária com o transporte Java testado: o
uploader 0.5.1
recorre a POST com X-HTTP-Method-Override: PATCH. Encaminhar essa requisição como uma requisição de
criação comum quebraria o upload.
No primeiro terminal, instale as dependências do servidor e inicie-o:
corepack yarn install && corepack yarn node server.ts 1080
Aguarde a URL de escuta. Se a porta 1080 estiver ocupada, escolha outra porta neste comando e no comando Java abaixo. O servidor escuta apenas na interface de loopback e não tem autenticação. Mantenha-o local; ele é um ambiente de aprendizado, não uma configuração de implantação. Pare-o com Ctrl+C ao terminar.
Uso básico
Salve esta classe completa como src/main/java/TusUploaderExample.java:
import io.tus.java.client.ProtocolException;
import io.tus.java.client.TusClient;
import io.tus.java.client.TusExecutor;
import io.tus.java.client.TusUpload;
import io.tus.java.client.TusUploader;
import io.tus.java.client.TusURLMemoryStore;
import java.io.File;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
public class TusUploaderExample {
public static void main(String[] args) {
try {
run(args);
} catch (Exception error) {
System.err.println("Upload failed: " + error.getMessage());
System.exit(1);
}
}
private static void run(String[] args) throws Exception {
if (args.length != 2) {
throw new IllegalArgumentException("Usage: TusUploaderExample ENDPOINT FILE");
}
File file = new File(args[1]);
if (!file.isFile() || !file.canRead()) {
throw new IOException("Input must be a readable regular file: " + file);
}
List<HttpURLConnection> connections = new ArrayList<>();
TusClient client = new TusClient() {
@Override
public void prepareConnection(HttpURLConnection connection) {
super.prepareConnection(connection);
connection.setReadTimeout(5000);
connections.add(connection);
}
};
client.setConnectTimeout(5000);
client.setUploadCreationURL(URI.create(args[0]).toURL());
client.enableResuming(new TusURLMemoryStore());
TusExecutor executor = new TusExecutor() {
private int attempt = 0;
@Override
protected void makeAttempt() throws IOException, ProtocolException {
System.out.println("Attempt " + ++attempt);
TusUpload upload = new TusUpload(file);
URL completedUrl;
// Close the file even if creation, resuming, or finish() fails.
try (var input = upload.getInputStream()) {
TusUploader uploader = client.resumeOrCreateUpload(upload);
uploader.setChunkSize(64 * 1024);
uploader.setRequestPayloadSize(1024 * 1024);
System.out.println("Starting at byte " + uploader.getOffset());
try {
while (uploader.uploadChunk() != -1) {
// The server response is checked when the request finishes.
}
} finally {
uploader.finish();
}
completedUrl = uploader.getUploadURL();
} finally {
for (HttpURLConnection connection : connections) {
connection.disconnect();
}
connections.clear();
}
System.out.println("Completed: " + completedUrl);
}
};
executor.setDelays(new int[] {500, 1000});
if (!executor.makeAttempts()) {
throw new IOException("Upload interrupted while waiting to retry");
}
}
}
Cada tentativa reabre o mesmo arquivo e compartilha um único cliente e um único armazenamento de
URLs. resumeOrCreateUpload() cria o upload na primeira tentativa; na nova tentativa, ele usa
HEAD para obter o offset do servidor. Mantenha o arquivo de origem inalterado durante toda a
transferência. A impressão digital de arquivo da biblioteca inclui o caminho e o tamanho dele, não
um hash do conteúdo.
As duas configurações de tamanho têm funções diferentes. O chunk de 64 KiB é o buffer de cópia do cliente; a configuração de payload de 1 MiB limita cada requisição HTTP de upload. Essas requisições propositalmente pequenas tornam a interrupção visível. Elas não são recomendações de throughput.
No segundo terminal, crie uma amostra binária um pouco maior que duas requisições. Este comando se
recusa a sobrescrever um sample.bin existente:
corepack yarn node -e "require('node:fs').writeFileSync('sample.bin', require('node:crypto').randomBytes(2 * 1024 * 1024 + 4099), {flag: 'wx'})"
Compile, copie as dependências de runtime e execute a classe. O && impede que um build
malsucedido execute classes desatualizadas. O separador de classpath aqui é o do Linux:
mvn -q compile dependency:copy-dependencies &&
java -cp 'target/classes:target/dependency/*' TusUploaderExample http://127.0.0.1:1080/files sample.bin
Recursos avançados
Ler as evidências da nova tentativa
Com um servidor recém-iniciado, a saída do Java deve ser:
Attempt 1
Starting at byte 0
Attempt 2
Starting at byte 1048576
Completed: http://127.0.0.1:1080/files/<upload-id>
No terminal do servidor, encontre a requisição de upload bem-sucedida que começa no offset
0, o 503 proposital em 1048576 e o HEAD seguinte. O byte inicial da nova
tentativa em Java vem dessa resposta HEAD. As requisições bem-sucedidas restantes começam
em 1048576 e 2097152. Isso segue o
protocolo de offset do tus: o servidor informa ao
cliente onde continuar. O primeiro MiB não precisa ser enviado de novo.
Substitua UPLOAD_ID abaixo pelo segmento final do caminho da URL concluída. Compare o arquivo
realmente armazenado no segundo terminal:
cmp sample.bin uploads/UPLOAD_ID && printf 'Files match\n'
A mensagem de conclusão significa que o servidor confirmou a transferência. cmp verifica
separadamente os bytes no disco. Ele informa uma divergência ou um arquivo ausente com um status de
saída diferente de zero.
Entender o que sobrevive a uma interrupção
TusURLMemoryStore retém a URL de upload somente enquanto este processo Java está ativo. A falha
simulada mantém esse processo em execução. Parar o Java com Ctrl+C e iniciá-lo de novo cria um novo
upload; este exemplo não implementa recuperação após reinício. Um
TusURLStore
persistente e uma política confiável de identificação de arquivos são necessários para essa
integração.
O servidor mantém os arquivos em uploads/ depois de parar. Executar o Java novamente cria um
novo arquivo no servidor com nome aleatório, preservando os uploads anteriores. Remova esses
arquivos de teste locais quando não precisar mais deles. Reinicie o servidor para reativar a falha
única, ou defina failOnce como false para desativá-la.
Boas práticas
O exemplo permite no máximo três tentativas: a chamada inicial e duas novas tentativas após 500 ms
e 1.000 ms. Esses limites vêm de
TusExecutor.setDelays().
Os timeouts de conexão e de leitura limitam esperas individuais; eles não são um prazo geral para o
upload.
Um arquivo vazio é válido e é concluído sem cálculo de porcentagem. Um arquivo ausente falha antes
de contatar o servidor. Se o servidor continuar indisponível, o Java imprime uma falha e termina com
status de saída diferente de zero depois de esgotar as novas tentativas. Erros de protocolo que não
admitem nova tentativa podem falhar antes. Verifique esse status de saída em scripts; um offset de
progresso sozinho não comprova a conclusão, porque finish() ainda pode rejeitar a resposta
final.
Compatibilidade de plataformas
Este passo a passo abrange um processo Java de desktop no Linux. A biblioteca Java também oferece suporte ao Android, mas este programa não trata eventos de ciclo de vida do Android nem agendamento em segundo plano. Para essa integração separada, comece com o tus-android-client.
