Reanuda subidas de archivos en Java con tus-java-client
Una solicitud de subida fallida no debería obligarte a reenviar todo el archivo. Este tutorial usa
tus-java-client para subir un archivo binario, recuperarse de un fallo deliberado del servidor y verificar los
bytes guardados. Ejecutarás ambos lados localmente y verás que el reintento empieza en el desplazamiento
guardado por el servidor.
Configura tus-java-client
Los siguientes comandos están pensados para Linux con Bash, OpenJDK 21.0.12.1, Maven 3.9.16, Node.js 24.15.0, Corepack y Yarn 4.12.0. Estas son las versiones de las herramientas que se probaron. Node ejecuta el servidor de pruebas local; tu aplicación Java no lo necesita al conectarse a un endpoint existente del protocolo tus.
Crea el proyecto fuera de una aplicación existente. El archivo de bloqueo vacío delimita un proyecto
propio para Yarn, y la configuración local selecciona node_modules para las dependencias del servidor. Este bloque
deja tu terminal en su directorio original y se niega a continuar si ya existe un directorio
java-tus-demo:
(
mkdir java-tus-demo &&
cd java-tus-demo &&
mkdir -p src/main/java &&
touch yarn.lock &&
printf 'nodeLinker: node-modules\n' > .yarnrc.yml
)
Abre java-tus-demo en tu editor y úsalo como directorio de trabajo en dos terminales. Guarda este
pom.xml completo en su raíz:
<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>
Inicia un servidor local del protocolo tus
Guarda package.json junto al 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"
}
}
Guarda lo siguiente como server.ts. Usa el
servidor del protocolo tus para Node y su almacenamiento en disco oficiales.
El interruptor de fallo rechaza una solicitud de subida cuando su desplazamiento inicial alcanza
1 MiB; los bytes anteriores permanecen en el disco. Déjalo activado durante la primera ejecución.
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`)
}
})
La anulación es necesaria con el transporte Java probado: el
componente de subida de la versión 0.5.1
recurre a POST con X-HTTP-Method-Override: PATCH. Reenviar esa solicitud como una solicitud
de creación ordinaria haría fallar la subida.
En la primera terminal, instala las dependencias del servidor e inícialo:
corepack yarn install && corepack yarn node server.ts 1080
Espera a que aparezca la URL de escucha. Si el puerto 1080 está ocupado, elige otro puerto en este comando y en el comando Java de abajo. El servidor solo escucha en la interfaz de bucle local y no tiene autenticación. Mantenlo en el entorno local; es un recurso de aprendizaje, no una configuración para despliegue. Deténlo con Ctrl+C al terminar.
Uso básico
Guarda esta clase 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 intento vuelve a abrir el mismo archivo y comparte un único cliente y almacén de URL. resumeOrCreateUpload()
crea la subida en el primer intento; al reintentar, usa HEAD para obtener el desplazamiento del servidor.
Mantén el archivo de origen sin cambios durante toda la transferencia. La huella del archivo que
utiliza la biblioteca incluye su ruta y longitud, no un hash del contenido.
Los dos ajustes de tamaño cumplen funciones distintas. El fragmento de 64 KiB es el búfer de copia del cliente; el ajuste de carga útil de 1 MiB limita cada solicitud HTTP de subida. Estas solicitudes, deliberadamente pequeñas, permiten observar la interrupción. No son recomendaciones de rendimiento.
En la segunda terminal, crea una muestra binaria un poco mayor que dos solicitudes. Este comando
se niega a sobrescribir un archivo sample.bin existente:
corepack yarn node -e "require('node:fs').writeFileSync('sample.bin', require('node:crypto').randomBytes(2 * 1024 * 1024 + 4099), {flag: 'wx'})"
Compila, copia las dependencias de ejecución y ejecuta la clase. El && impide que una compilación
fallida ejecute clases desactualizadas. El separador de classpath usado aquí corresponde a Linux:
mvn -q compile dependency:copy-dependencies &&
java -cp 'target/classes:target/dependency/*' TusUploaderExample http://127.0.0.1:1080/files sample.bin
Funciones avanzadas
Examina las pruebas del reintento
Con un servidor recién iniciado, la salida de Java debería ser:
Attempt 1
Starting at byte 0
Attempt 2
Starting at byte 1048576
Completed: http://127.0.0.1:1080/files/<upload-id>
En la terminal del servidor, busca la solicitud de subida exitosa que empieza en el desplazamiento 0, el
503 deliberado en 1048576 y el HEAD posterior. El byte inicial del reintento de Java procede de esa
respuesta HEAD. Las solicitudes exitosas restantes empiezan en 1048576 y 2097152. Esto sigue el
mecanismo de desplazamientos del protocolo tus: el servidor indica al
cliente dónde continuar. No hace falta volver a enviar el primer MiB.
Sustituye UPLOAD_ID a continuación por el segmento final de la ruta de la URL completada. Compara el archivo
realmente guardado desde la segunda terminal:
cmp sample.bin uploads/UPLOAD_ID && printf 'Files match\n'
El mensaje de finalización significa que el servidor confirmó la transferencia. Por separado, cmp comprueba los
bytes en el disco. Si no coinciden o falta el archivo, lo indica con un estado de salida distinto de cero.
Comprende qué se conserva tras una interrupción
TusURLMemoryStore conserva la URL de subida solo mientras este proceso Java está activo. El fallo simulado
deja ese proceso en ejecución. Detener Java con Ctrl+C y volver a iniciarlo crea una nueva subida;
este ejemplo no implementa la recuperación tras un reinicio. Para esa integración, se necesitan un
TusURLStore
persistente y una política fiable de identificación de archivos.
El servidor conserva los archivos en uploads/ después de detenerse. Volver a ejecutar Java crea un nuevo
archivo con nombre aleatorio en el servidor y deja intactas las subidas anteriores. Elimina esos
archivos de prueba locales cuando ya no los necesites. Reinicia el servidor para volver a activar su
fallo único, o establece failOnce en false para desactivarlo.
Buenas prácticas
El ejemplo permite como máximo tres intentos: la llamada inicial y dos reintentos después de 500 ms y
1.000 ms. Esos límites proceden de
TusExecutor.setDelays().
Los tiempos de espera de conexión y lectura limitan las esperas individuales; no constituyen un plazo
máximo para toda la subida.
Un archivo vacío es válido y su subida se completa sin calcular un porcentaje. Si falta el archivo,
el proceso falla antes de contactar al servidor. Si el servidor sigue sin estar disponible, Java
muestra un fallo y termina con un estado distinto de cero tras agotar los reintentos. Los errores de
protocolo que no admiten reintentos pueden provocar un fallo antes. Comprueba ese estado de salida en
los scripts; un desplazamiento de progreso por sí solo no demuestra la finalización, porque
finish() aún puede rechazar la respuesta final.
Compatibilidad con plataformas
Este tutorial cubre un proceso Java de escritorio en Linux. La biblioteca Java también es compatible con Android, pero este programa no gestiona eventos del ciclo de vida de Android ni la programación de tareas en segundo plano. Para esa integración independiente, empieza con tus-android-client.
