Datei-Uploads in Java mit tus-java-client fortsetzen
Eine fehlgeschlagene Upload-Anfrage sollte Sie nicht zwingen, die gesamte Datei erneut zu senden.
Diese Anleitung nutzt tus-java-client, um eine Binärdatei hochzuladen, einen absichtlich
ausgelösten Serverfehler abzufangen und die gespeicherten Bytes zu verifizieren. Sie führen beide
Seiten lokal aus und sehen, wie der erneute Versuch am gespeicherten Offset des Servers beginnt.
tus-java-client einrichten
Die folgenden Befehle sind für Linux mit Bash, OpenJDK 21.0.12.1, Maven 3.9.16, Node.js 24.15.0, Corepack und Yarn 4.12.0 ausgelegt. Dies sind die getesteten Tool-Versionen. Node führt den lokalen Testserver aus; Ihre Java-Anwendung benötigt es nicht, wenn sie sich mit einem bestehenden tus-Endpunkt verbindet.
Erstellen Sie das Projekt außerhalb einer bestehenden Anwendung. Die leere Lockdatei grenzt für
Yarn ein eigenes Projekt ab, und die lokale Konfiguration wählt node_modules für die
Serverabhängigkeiten aus. Dieser Block belässt Ihr Terminal im ursprünglichen Verzeichnis und
bricht ab, wenn das Verzeichnis java-tus-demo bereits existiert:
(
mkdir java-tus-demo &&
cd java-tus-demo &&
mkdir -p src/main/java &&
touch yarn.lock &&
printf 'nodeLinker: node-modules\n' > .yarnrc.yml
)
Öffnen Sie java-tus-demo in Ihrem Editor und verwenden Sie es in zwei Terminals als
Arbeitsverzeichnis. Speichern Sie diese vollständige Datei pom.xml im
Stammverzeichnis des Projekts:
<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>
Einen lokalen tus-Server starten
Speichern Sie package.json neben der POM-Datei:
{
"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"
}
}
Speichern Sie Folgendes als server.ts. Es nutzt den offiziellen
tus-Server für Node und dessen Festplattenspeicher.
Der Fehlerschalter weist eine Upload-Anfrage zurück, sobald ihr Start-Offset 1 MiB erreicht; die
zuvor übertragenen Bytes bleiben auf der Festplatte. Lassen Sie ihn beim ersten Durchlauf aktiviert.
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`)
}
})
Die Überschreibung ist beim getesteten Java-Transport erforderlich: Der
Uploader in Version 0.5.1
fällt auf POST mit X-HTTP-Method-Override: PATCH zurück. Würde diese Anfrage
als gewöhnliche Erstellungsanfrage weitergeleitet, würde der Upload fehlschlagen.
Installieren Sie im ersten Terminal die Serverabhängigkeiten und starten Sie den Server:
corepack yarn install && corepack yarn node server.ts 1080
Warten Sie, bis die URL angezeigt wird, unter der der Server erreichbar ist. Wenn Port 1080 belegt ist, wählen Sie in diesem Befehl und im Java-Befehl weiter unten einen anderen Port. Der Server bindet sich nur an Loopback und hat keine Authentifizierung. Betreiben Sie ihn nur lokal; er ist eine Lern-Fixture, keine Konfiguration für die Bereitstellung. Beenden Sie ihn abschließend mit Strg+C.
Grundlegende Nutzung
Speichern Sie diese vollständige Klasse als 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");
}
}
}
Jeder Versuch öffnet dieselbe Datei erneut und verwendet denselben Client und URL-Speicher.
resumeOrCreateUpload() erstellt den Upload beim ersten Versuch; beim erneuten Versuch nutzt
es HEAD, um den Offset des Servers abzurufen. Lassen Sie die Quelldatei
während der gesamten Übertragung unverändert. Der Datei-Fingerabdruck der Bibliothek enthält den
Pfad und die Länge der Datei, keinen Hash ihres Inhalts.
Die beiden Größeneinstellungen haben unterschiedliche Aufgaben. Der Chunk mit 64 KiB ist der Kopierpuffer des Clients; die Payload-Einstellung von 1 MiB begrenzt jede HTTP-Upload-Anfrage. Diese bewusst kleinen Anfragen machen die Unterbrechung sichtbar. Sie sind keine Empfehlungen für den Durchsatz.
Erstellen Sie im zweiten Terminal eine binäre Beispieldatei, die etwas größer als zwei Anfragen
ist. Dieser Befehl verweigert das Überschreiben einer vorhandenen Datei
sample.bin:
corepack yarn node -e "require('node:fs').writeFileSync('sample.bin', require('node:crypto').randomBytes(2 * 1024 * 1024 + 4099), {flag: 'wx'})"
Kompilieren Sie den Code, kopieren Sie die Laufzeitabhängigkeiten und führen Sie die Klasse aus.
&& verhindert, dass nach einem fehlgeschlagenen Build veraltete Klassen
ausgeführt werden. Das hier verwendete Classpath-Trennzeichen gilt für Linux:
mvn -q compile dependency:copy-dependencies &&
java -cp 'target/classes:target/dependency/*' TusUploaderExample http://127.0.0.1:1080/files sample.bin
Erweiterte Funktionen
Den erneuten Versuch in der Ausgabe nachvollziehen
Bei einem frisch gestarteten Server sollte Java Folgendes ausgeben:
Attempt 1
Starting at byte 0
Attempt 2
Starting at byte 1048576
Completed: http://127.0.0.1:1080/files/<upload-id>
Suchen Sie im Serverterminal die erfolgreiche Upload-Anfrage mit dem Start-Offset
0, den absichtlich ausgelösten 503 bei
1048576 und das anschließende HEAD. Das Startbyte des
erneuten Java-Versuchs stammt aus dieser Antwort auf HEAD. Die übrigen
erfolgreichen Anfragen beginnen bei 1048576 und
2097152. Dies folgt dem
tus-Offset-Protokoll: Der Server teilt dem Client mit, wo er
fortfahren soll. Das erste MiB muss nicht erneut gesendet werden.
Ersetzen Sie unten UPLOAD_ID durch das letzte Pfadsegment der URL des
abgeschlossenen Uploads. Vergleichen Sie die tatsächlich gespeicherte Datei im zweiten Terminal:
cmp sample.bin uploads/UPLOAD_ID && printf 'Files match\n'
Die Abschlussmeldung bedeutet, dass der Server die Übertragung bestätigt hat.
cmp prüft separat die Bytes auf der Festplatte. Bei einer Abweichung oder
einer fehlenden Datei meldet es einen Exit-Status ungleich null.
Verstehen, was eine Unterbrechung überdauert
TusURLMemoryStore bewahrt die Upload-URL nur auf, solange dieser Java-Prozess läuft.
Beim simulierten Fehler läuft der Prozess weiter. Wenn Sie Java mit Strg+C beenden und erneut
starten, wird ein neuer Upload erstellt; dieses Beispiel implementiert keine Wiederaufnahme nach
einem Neustart. Für diese Integration sind eine persistente Implementierung von
TusURLStore
und eine zuverlässige Strategie zur Dateiidentifizierung erforderlich.
Der Server behält Dateien nach dem Beenden in uploads/. Wenn Sie Java erneut
ausführen, wird eine neue, zufällig benannte Serverdatei erstellt; frühere Uploads bleiben erhalten.
Entfernen Sie diese lokalen Testdateien, wenn Sie sie nicht mehr benötigen. Starten Sie den Server
neu, um seinen einmaligen Fehler erneut zu aktivieren, oder setzen Sie
failOnce auf false, um ihn zu deaktivieren.
Bewährte Verfahren
Das Beispiel erlaubt höchstens drei Versuche: den ersten Aufruf und zwei erneute Versuche nach
500 ms und 1.000 ms. Diese Grenzen stammen aus
TusExecutor.setDelays().
Verbindungs- und Lese-Timeouts begrenzen einzelne Wartezeiten; sie sind keine Gesamtfrist für den
Upload.
Eine leere Datei ist gültig und wird ohne Prozentberechnung vollständig übertragen. Bei einer
fehlenden Datei schlägt der Vorgang fehl, bevor der Server kontaktiert wird. Bleibt der Server
unerreichbar, gibt Java nach Ausschöpfen der Versuche einen Fehler aus und beendet sich mit einem
Exit-Status ungleich null. Protokollfehler, bei denen kein erneuter Versuch möglich ist, können
früher zum Fehlschlag führen. Prüfen Sie diesen Exit-Status in Skripten; ein Fortschritts-Offset
allein belegt keinen Abschluss, da finish() die letzte Antwort noch zurückweisen
kann.
Plattformkompatibilität
Diese Anleitung behandelt einen Desktop-Java-Prozess unter Linux. Die Java-Bibliothek unterstützt auch Android, doch dieses Programm verarbeitet weder Android-Lebenszyklusereignisse noch die Planung von Hintergrundaufgaben. Beginnen Sie für diese separate Integration mit tus-android-client.
