Reprendre des téléversements en Java avec tus-java-client
Une requête de téléversement qui échoue ne devrait pas vous obliger à renvoyer tout le fichier. Ce
tutoriel utilise tus-java-client pour téléverser un fichier binaire, se remettre d’une
panne volontaire du serveur et vérifier les octets stockés. Vous exécuterez les deux côtés en local
et verrez la nouvelle tentative démarrer au décalage (offset) enregistré par le serveur.
Configurer tus-java-client
Les commandes ci-dessous ciblent Linux avec Bash, OpenJDK 21.0.12.1, Maven 3.9.16, Node.js 24.15.0, Corepack et Yarn 4.12.0. Ce sont les versions d’outils testées. Node exécute le serveur de test local ; votre application Java n’en a pas besoin pour se connecter à un point de terminaison tus existant.
Créez le projet en dehors de toute application existante. Le fichier de verrouillage vide donne à
Yarn sa propre limite de projet, et la configuration locale sélectionne node_modules
pour les dépendances du serveur. Ce bloc laisse votre terminal dans son répertoire d’origine et
refuse un répertoire java-tus-demo existant :
(
mkdir java-tus-demo &&
cd java-tus-demo &&
mkdir -p src/main/java &&
touch yarn.lock &&
printf 'nodeLinker: node-modules\n' > .yarnrc.yml
)
Ouvrez java-tus-demo dans votre éditeur et utilisez-le comme répertoire de travail
dans deux terminaux. Enregistrez ce pom.xml complet à sa racine :
<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>
Démarrer un serveur tus local
Enregistrez package.json à côté du 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"
}
}
Enregistrez ce qui suit sous le nom server.ts. Il utilise le
serveur tus Node officiel et son stockage sur disque.
Le commutateur de panne rejette une seule requête de téléversement, dès que son décalage de départ
atteint 1 MiB ; les octets précédents restent sur le disque. Laissez-le activé pour la première
exécution.
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`)
}
})
Cette surcharge est nécessaire avec le transport Java testé : le
téléverseur de la version 0.5.1
se rabat sur POST avec X-HTTP-Method-Override: PATCH. Transmettre cette
requête comme une requête de création ordinaire casserait le téléversement.
Dans le premier terminal, installez les dépendances du serveur et démarrez-le :
corepack yarn install && corepack yarn node server.ts 1080
Attendez l’affichage de l’URL d’écoute. Si le port 1080 est occupé, choisissez un autre port dans cette commande et dans la commande Java ci-dessous. Le serveur n’écoute que sur l’interface de bouclage et n’a aucune authentification. Gardez-le en local ; c’est un support d’apprentissage, pas une configuration de déploiement. Arrêtez-le avec Ctrl+C une fois terminé.
Utilisation de base
Enregistrez cette classe complète sous 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");
}
}
}
Chaque tentative rouvre le même fichier et partage un même client et un même stockage d’URL.
resumeOrCreateUpload() crée le téléversement lors de la première tentative ; lors d’une
nouvelle tentative, il utilise HEAD pour obtenir le décalage du serveur.
Ne modifiez pas le fichier source pendant toute la durée du transfert. L’empreinte de fichier de la
bibliothèque inclut son chemin et sa taille, pas un hachage du contenu.
Les deux réglages de taille ont des rôles différents. Le bloc de 64 KiB est le tampon de copie du client ; le réglage de charge utile de 1 MiB plafonne chaque requête HTTP de téléversement. Ces requêtes volontairement petites rendent l’interruption visible. Ce ne sont pas des recommandations de débit.
Dans le second terminal, créez un échantillon binaire légèrement plus grand que deux requêtes. Cette
commande refuse d’écraser un sample.bin existant :
corepack yarn node -e "require('node:fs').writeFileSync('sample.bin', require('node:crypto').randomBytes(2 * 1024 * 1024 + 4099), {flag: 'wx'})"
Compilez, copiez les dépendances d’exécution et lancez la classe. Le &&
empêche un build en échec d’exécuter des classes obsolètes. Le séparateur de classpath utilisé ici
est celui de Linux :
mvn -q compile dependency:copy-dependencies &&
java -cp 'target/classes:target/dependency/*' TusUploaderExample http://127.0.0.1:1080/files sample.bin
Fonctionnalités avancées
Lire les traces de la nouvelle tentative
Avec un serveur fraîchement démarré, la sortie Java devrait être :
Attempt 1
Starting at byte 0
Attempt 2
Starting at byte 1048576
Completed: http://127.0.0.1:1080/files/<upload-id>
Dans le terminal du serveur, repérez la requête de téléversement réussie qui démarre au décalage
0, le 503 volontaire à
1048576, puis le HEAD qui suit. L’octet de départ
de la nouvelle tentative Java provient de cette réponse HEAD. Les requêtes
réussies restantes démarrent à 1048576 et 2097152. Cela
suit le protocole de décalage tus : le serveur indique au client où
reprendre. Le premier MiB n’a pas besoin d’être renvoyé.
Remplacez UPLOAD_ID ci-dessous par le dernier segment de chemin de l’URL
terminée. Comparez le fichier réellement stocké dans le second terminal :
cmp sample.bin uploads/UPLOAD_ID && printf 'Files match\n'
Le message de fin signifie que le serveur a accusé réception du transfert.
cmp vérifie séparément les octets sur le disque. Il signale une
différence ou un fichier manquant par un code de sortie non nul.
Comprendre ce qui survit à une interruption
TusURLMemoryStore ne conserve l’URL de téléversement que tant que ce processus Java est
actif. La panne simulée laisse ce processus en cours d’exécution. Arrêter Java avec Ctrl+C puis le
relancer crée un nouveau téléversement ; cet exemple n’implémente pas la reprise après redémarrage.
Un TusURLStore persistant
et une politique fiable d’identification des fichiers sont nécessaires pour cette intégration.
Le serveur conserve les fichiers dans uploads/ après son arrêt. Relancer Java
crée un nouveau fichier serveur au nom aléatoire et laisse intacts les téléversements précédents.
Supprimez ces fichiers de test locaux lorsque vous n’en avez plus besoin. Redémarrez le serveur pour
réarmer sa panne unique, ou définissez failOnce sur
false pour la désactiver.
Bonnes pratiques
L’exemple autorise au plus trois tentatives : l’appel initial et deux nouvelles tentatives après
500 ms et 1 000 ms. Ces limites proviennent de
TusExecutor.setDelays().
Les délais d’expiration de connexion et de lecture limitent chaque attente individuelle ; ils ne
constituent pas une échéance globale pour le téléversement.
Un fichier vide est valide et se termine sans calcul de pourcentage. Si le fichier est manquant,
l’exécution échoue avant de contacter le serveur. Si le serveur reste indisponible, Java affiche un
échec et se termine avec un code non nul après avoir épuisé les nouvelles tentatives. Les erreurs de
protocole non réessayables peuvent échouer plus tôt. Vérifiez ce code de sortie dans vos scripts ;
un décalage de progression ne suffit pas à établir la fin du téléversement, car
finish() peut encore rejeter la réponse finale.
Compatibilité des plateformes
Ce tutoriel couvre un processus Java de bureau sous Linux. La bibliothèque Java prend aussi en charge Android, mais ce programme ne gère pas les événements du cycle de vie Android ni la planification en arrière-plan. Pour cette intégration distincte, commencez par tus-android-client.
