Skip to content

Commit 74eae01

Browse files
committed
Add TUS terminate upload proof
1 parent f1400fc commit 74eae01

6 files changed

Lines changed: 246 additions & 13 deletions

File tree

example/build.gradle

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,12 @@ tasks.register('api2DevdockTusRequestIdHeaders', JavaExec) {
4848
workingDir = rootProject.projectDir
4949
}
5050

51+
tasks.register('api2DevdockTusTerminateUpload', JavaExec) {
52+
classpath = sourceSets.main.runtimeClasspath
53+
mainClass = 'io.tus.java.example.Api2DevdockTusTerminateUpload'
54+
workingDir = rootProject.projectDir
55+
}
56+
5157
tasks.register('api2DevdockTusUploadCallbacks', JavaExec) {
5258
classpath = sourceSets.main.runtimeClasspath
5359
mainClass = 'io.tus.java.example.Api2DevdockTusUploadCallbacks'

example/src/main/java/io/tus/java/example/Api2DevdockScenario.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,22 @@ static final class UploadCallbacksPlan {
5151
}
5252
}
5353

54+
static final class TerminationPlan {
55+
final int expectedVerificationStatus;
56+
final String method;
57+
final int minimumDeleteRequestCount;
58+
final int stopAfterAcceptedBytes;
59+
final String verificationMethod;
60+
61+
TerminationPlan(JSONObject termination) {
62+
expectedVerificationStatus = termination.getInt("expectedVerificationStatus");
63+
method = termination.getString("method");
64+
minimumDeleteRequestCount = termination.getInt("minimumDeleteRequestCount");
65+
stopAfterAcceptedBytes = termination.getInt("stopAfterAcceptedBytes");
66+
verificationMethod = termination.getString("verificationMethod");
67+
}
68+
}
69+
5470
static JSONObject loadScenario() throws IOException {
5571
String scenarioPath = System.getenv("API2_SDK_EXAMPLE_SCENARIO");
5672
if (scenarioPath == null || scenarioPath.isEmpty()) {
@@ -164,6 +180,10 @@ static UploadCallbacksPlan uploadCallbacks(JSONObject scenario) {
164180
);
165181
}
166182

183+
static TerminationPlan termination(JSONObject uploadConfig) {
184+
return new TerminationPlan(uploadConfig.getJSONObject("termination"));
185+
}
186+
167187
static String uploadCallbackEventKey(UploadCallbacksPlan plan, String... parts) {
168188
final StringBuilder key = new StringBuilder();
169189
for (int index = 0; index < parts.length; index++) {
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
package io.tus.java.example;
2+
3+
import io.tus.java.client.ProtocolException;
4+
import io.tus.java.client.TusClient;
5+
import io.tus.java.client.TusRequestLifecycleHooks;
6+
import io.tus.java.client.TusUpload;
7+
import io.tus.java.client.TusUploader;
8+
import org.json.JSONArray;
9+
import org.json.JSONObject;
10+
11+
import java.io.ByteArrayInputStream;
12+
import java.io.IOException;
13+
import java.net.HttpURLConnection;
14+
import java.net.URL;
15+
import java.util.ArrayList;
16+
import java.util.List;
17+
18+
public final class Api2DevdockTusTerminateUpload {
19+
/**
20+
* Run the API2 devdock TUS terminate-upload example.
21+
*
22+
* @param args ignored
23+
*/
24+
public static void main(String[] args) {
25+
try {
26+
System.setProperty("http.strictPostRedirect", "true");
27+
28+
final JSONObject scenario = Api2DevdockScenario.loadScenario();
29+
final JSONObject createResponse = Api2DevdockScenario.createResponse(scenario);
30+
final JSONObject result = uploadAndTerminate(scenario, createResponse);
31+
Api2DevdockScenario.writeResult(result);
32+
33+
System.out.println(
34+
"Java TUS SDK devdock scenario "
35+
+ scenario.getString("scenarioId")
36+
+ " terminated "
37+
+ result.getString("uploadUrl")
38+
);
39+
} catch (Exception e) {
40+
e.printStackTrace();
41+
System.exit(1);
42+
}
43+
}
44+
45+
private static JSONObject uploadAndTerminate(
46+
JSONObject scenario,
47+
JSONObject createResponse
48+
) throws IOException, ProtocolException {
49+
final JSONObject uploadConfig = scenario.getJSONObject("upload");
50+
final Api2DevdockScenario.TerminationPlan termination =
51+
Api2DevdockScenario.termination(uploadConfig);
52+
final byte[] content = Api2DevdockScenario.scenarioBytes(uploadConfig);
53+
final int chunkSize = Api2DevdockScenario.fixedChunkSizeBytes(uploadConfig);
54+
final List<String> requestMethods = new ArrayList<String>();
55+
56+
if (termination.stopAfterAcceptedBytes > content.length) {
57+
throw new IllegalStateException(
58+
"terminate upload stop-after bytes "
59+
+ termination.stopAfterAcceptedBytes
60+
+ " exceeds content length "
61+
+ content.length
62+
);
63+
}
64+
65+
final TusClient client = new TusClient();
66+
client.setUploadCreationURL(
67+
new URL(Api2DevdockScenario.tusUrl(uploadConfig, scenario, createResponse))
68+
);
69+
client.setRequestLifecycleHooks(new TusRequestLifecycleHooks(
70+
new TusRequestLifecycleHooks.BeforeRequest() {
71+
@Override
72+
public void beforeRequest(TusRequestLifecycleHooks.RequestContext context) {
73+
requestMethods.add(context.getMethod());
74+
}
75+
},
76+
null
77+
));
78+
79+
final TusUpload upload = new TusUpload();
80+
upload.setInputStream(new ByteArrayInputStream(content));
81+
upload.setSize(content.length);
82+
upload.setFingerprint(scenario.getString("scenarioId") + "-java-terminate-upload");
83+
upload.setMetadata(
84+
Api2DevdockScenario.uploadMetadata(uploadConfig, scenario, createResponse)
85+
);
86+
87+
final TusUploader uploader = client.createUpload(upload);
88+
uploader.setChunkSize(chunkSize);
89+
uploader.setRequestPayloadSize(termination.stopAfterAcceptedBytes);
90+
final int uploadedChunkSize = uploader.uploadChunk();
91+
uploader.finish();
92+
93+
if (uploadedChunkSize != termination.stopAfterAcceptedBytes) {
94+
throw new IllegalStateException(
95+
"terminate upload wrote "
96+
+ uploadedChunkSize
97+
+ " bytes, expected "
98+
+ termination.stopAfterAcceptedBytes
99+
);
100+
}
101+
if (uploader.getOffset() != termination.stopAfterAcceptedBytes) {
102+
throw new IllegalStateException(
103+
"terminate upload accepted "
104+
+ uploader.getOffset()
105+
+ " bytes, expected "
106+
+ termination.stopAfterAcceptedBytes
107+
);
108+
}
109+
if (uploader.getUploadURL() == null) {
110+
throw new IllegalStateException("terminate upload did not expose a URL");
111+
}
112+
113+
final URL uploadUrl = uploader.getUploadURL();
114+
client.terminateUpload(uploadUrl).disconnect();
115+
final int verificationStatus = verifyTerminatedUpload(
116+
client,
117+
termination.verificationMethod,
118+
uploadUrl
119+
);
120+
if (verificationStatus != termination.expectedVerificationStatus) {
121+
throw new IllegalStateException(
122+
"terminate upload verification status "
123+
+ verificationStatus
124+
+ ", expected "
125+
+ termination.expectedVerificationStatus
126+
);
127+
}
128+
129+
return new JSONObject()
130+
.put("acceptedBytes", (int) uploader.getOffset())
131+
.put("deleteRequestCount", countMethod(requestMethods, termination.method))
132+
.put("requestMethods", new JSONArray(requestMethods))
133+
.put("terminated", true)
134+
.put("uploadUrl", uploadUrl.toString())
135+
.put("verificationStatus", verificationStatus);
136+
}
137+
138+
private static int verifyTerminatedUpload(
139+
TusClient client,
140+
String method,
141+
URL uploadUrl
142+
) throws IOException {
143+
final HttpURLConnection connection = (HttpURLConnection) uploadUrl.openConnection();
144+
try {
145+
connection.setRequestMethod(method);
146+
client.prepareConnection(connection);
147+
connection.connect();
148+
return connection.getResponseCode();
149+
} finally {
150+
connection.disconnect();
151+
}
152+
}
153+
154+
private static int countMethod(List<String> methods, String expectedMethod) {
155+
int count = 0;
156+
for (String method : methods) {
157+
if (method.equals(expectedMethod)) {
158+
count += 1;
159+
}
160+
}
161+
162+
return count;
163+
}
164+
165+
private Api2DevdockTusTerminateUpload() {
166+
throw new IllegalStateException("Utility class");
167+
}
168+
}

src/main/java/io/tus/java/client/TusClient.java

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,7 @@ public int getConnectTimeout() {
239239
*/
240240
public TusUploader createUpload(@NotNull TusUpload upload) throws ProtocolException, IOException {
241241
HttpURLConnection connection = openConnection(uploadCreationURL);
242-
connection.setRequestMethod("POST");
242+
connection.setRequestMethod(TusProtocol.CREATE_UPLOAD_METHOD);
243243
prepareConnection(connection);
244244

245245
String encodedMetadata = upload.getEncodedMetadata();
@@ -252,12 +252,12 @@ public TusUploader createUpload(@NotNull TusUpload upload) throws ProtocolExcept
252252
} else {
253253
connection.addRequestProperty("Upload-Length", Long.toString(upload.getSize()));
254254
}
255-
runBeforeRequest("POST", connection);
255+
runBeforeRequest(TusProtocol.CREATE_UPLOAD_METHOD, connection);
256256
connection.connect();
257257

258258
int responseCode = connection.getResponseCode();
259-
runAfterResponse("POST", connection);
260-
if (!(responseCode >= 200 && responseCode < 300)) {
259+
runAfterResponse(TusProtocol.CREATE_UPLOAD_METHOD, connection);
260+
if (!TusProtocol.isSuccessfulResponseStatus(responseCode)) {
261261
throw new ProtocolException(
262262
"unexpected status code (" + responseCode + ") while creating upload", connection);
263263
}
@@ -279,6 +279,35 @@ public TusUploader createUpload(@NotNull TusUpload upload) throws ProtocolExcept
279279
return createUploader(upload, uploadURL, 0L);
280280
}
281281

282+
/**
283+
* Terminate an upload URL using the Termination extension.
284+
*
285+
* @param uploadURL The upload location URL to terminate.
286+
* @return The completed HTTP connection.
287+
* @throws ProtocolException Thrown if the remote server sent an unexpected response, e.g.
288+
* wrong status codes.
289+
* @throws IOException Thrown if an exception occurs while issuing the HTTP request.
290+
*/
291+
public HttpURLConnection terminateUpload(@NotNull URL uploadURL)
292+
throws ProtocolException, IOException {
293+
HttpURLConnection connection = openConnection(uploadURL);
294+
connection.setRequestMethod(TusProtocol.TERMINATE_UPLOAD_METHOD);
295+
prepareConnection(connection);
296+
297+
runBeforeRequest(TusProtocol.TERMINATE_UPLOAD_METHOD, connection);
298+
connection.connect();
299+
300+
int responseCode = connection.getResponseCode();
301+
runAfterResponse(TusProtocol.TERMINATE_UPLOAD_METHOD, connection);
302+
if (!TusProtocol.isSuccessfulResponseStatus(responseCode)) {
303+
throw new ProtocolException(
304+
"unexpected status code (" + responseCode + ") while terminating upload",
305+
connection);
306+
}
307+
308+
return connection;
309+
}
310+
282311
@NotNull
283312
private HttpURLConnection openConnection(@NotNull URL uploadURL) throws IOException {
284313
if (proxy != null) {
@@ -346,15 +375,15 @@ public TusUploader resumeUpload(@NotNull TusUpload upload) throws
346375
public TusUploader beginOrResumeUploadFromURL(@NotNull TusUpload upload, @NotNull URL uploadURL) throws
347376
ProtocolException, IOException {
348377
HttpURLConnection connection = openConnection(uploadURL);
349-
connection.setRequestMethod("HEAD");
378+
connection.setRequestMethod(TusProtocol.OFFSET_DISCOVERY_METHOD);
350379
prepareConnection(connection);
351380

352-
runBeforeRequest("HEAD", connection);
381+
runBeforeRequest(TusProtocol.OFFSET_DISCOVERY_METHOD, connection);
353382
connection.connect();
354383

355384
int responseCode = connection.getResponseCode();
356-
runAfterResponse("HEAD", connection);
357-
if (!(responseCode >= 200 && responseCode < 300)) {
385+
runAfterResponse(TusProtocol.OFFSET_DISCOVERY_METHOD, connection);
386+
if (!TusProtocol.isSuccessfulResponseStatus(responseCode)) {
358387
throw new ProtocolException(
359388
"unexpected status code (" + responseCode + ") while resuming upload", connection);
360389
}

src/main/java/io/tus/java/client/TusProtocol.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,24 @@
1616
* Generated TUS protocol constants used by the runtime client.
1717
*/
1818
final class TusProtocol {
19+
static final String CREATE_UPLOAD_METHOD = "POST";
1920
static final String DEFAULT_PROTOCOL_VERSION = "1.0.0";
2021
static final Map<String, String> DEFAULT_REQUEST_HEADERS = defaultRequestHeaders();
2122
static final Map<String, String> DEFAULT_RESPONSE_HEADERS = defaultResponseHeaders();
23+
static final String OFFSET_DISCOVERY_METHOD = "HEAD";
2224
static final String REQUEST_ID_HEADER_NAME = "X-Request-ID";
25+
static final int SUCCESS_RESPONSE_STATUS_CATEGORY = 200;
26+
static final String TERMINATE_UPLOAD_METHOD = "DELETE";
27+
static final String UPLOAD_CHUNK_METHOD = "PATCH";
2328

2429
private TusProtocol() {
2530
}
2631

32+
static boolean isSuccessfulResponseStatus(int responseStatusCode) {
33+
return responseStatusCode >= SUCCESS_RESPONSE_STATUS_CATEGORY
34+
&& responseStatusCode < SUCCESS_RESPONSE_STATUS_CATEGORY + 100;
35+
}
36+
2737
static void prepareRequestHeaders(
2838
HttpURLConnection connection,
2939
Map<String, String> customHeaders,

src/main/java/io/tus/java/client/TusUploader.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -117,16 +117,16 @@ private void openConnection() throws IOException, ProtocolException {
117117
connection.setRequestProperty("Expect", "100-continue");
118118

119119
try {
120-
connection.setRequestMethod("PATCH");
120+
connection.setRequestMethod(TusProtocol.UPLOAD_CHUNK_METHOD);
121121
// Check whether we are running on a buggy JRE
122122
} catch (java.net.ProtocolException pe) {
123123
connection.setRequestMethod("POST");
124-
connection.setRequestProperty("X-HTTP-Method-Override", "PATCH");
124+
connection.setRequestProperty("X-HTTP-Method-Override", TusProtocol.UPLOAD_CHUNK_METHOD);
125125
}
126126

127127
connection.setDoOutput(true);
128128
connection.setChunkedStreamingMode(0);
129-
client.runBeforeRequest("PATCH", connection);
129+
client.runBeforeRequest(TusProtocol.UPLOAD_CHUNK_METHOD, connection);
130130
try {
131131
output = connection.getOutputStream();
132132
} catch (java.net.ProtocolException pe) {
@@ -415,9 +415,9 @@ private void finishConnection() throws ProtocolException, IOException {
415415
HttpURLConnection currentConnection = connection;
416416
try {
417417
int responseCode = currentConnection.getResponseCode();
418-
client.runAfterResponse("PATCH", currentConnection);
418+
client.runAfterResponse(TusProtocol.UPLOAD_CHUNK_METHOD, currentConnection);
419419

420-
if (!(responseCode >= 200 && responseCode < 300)) {
420+
if (!TusProtocol.isSuccessfulResponseStatus(responseCode)) {
421421
throw new ProtocolException("unexpected status code (" + responseCode + ") while uploading chunk",
422422
currentConnection);
423423
}

0 commit comments

Comments
 (0)