Skip to content

Commit c95678a

Browse files
committed
Add Java TUS abort upload proof
1 parent 35a60e6 commit c95678a

7 files changed

Lines changed: 554 additions & 81 deletions

File tree

example/build.gradle

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,12 @@ tasks.register('api2DevdockTusDetailedError', JavaExec) {
5454
workingDir = rootProject.projectDir
5555
}
5656

57+
tasks.register('api2DevdockTusAbortUpload', JavaExec) {
58+
classpath = sourceSets.main.runtimeClasspath
59+
mainClass = 'io.tus.java.example.Api2DevdockTusAbortUpload'
60+
workingDir = rootProject.projectDir
61+
}
62+
5763
tasks.register('api2DevdockTusStartOptionValidation', JavaExec) {
5864
classpath = sourceSets.main.runtimeClasspath
5965
mainClass = 'io.tus.java.example.Api2DevdockTusStartOptionValidation'

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,27 @@ static Map<String, String> conformanceInputStringMapOption(
112112
String key
113113
) {
114114
final JSONObject values = conformanceInputJSONObjectOption(conformanceScenario, key);
115+
return scalarStringMap(values);
116+
}
117+
118+
static Map<String, String> conformanceInputStringMapOptionOrEmpty(
119+
JSONObject conformanceScenario,
120+
String key
121+
) {
122+
final Object value = conformanceInputOptionOrNull(conformanceScenario, key);
123+
if (value == null || JSONObject.NULL.equals(value)) {
124+
return new LinkedHashMap<String, String>();
125+
}
126+
if (!(value instanceof JSONObject)) {
127+
throw new IllegalArgumentException(
128+
"conformance input option " + key + " is not an object"
129+
);
130+
}
131+
132+
return scalarStringMap((JSONObject) value);
133+
}
134+
135+
private static Map<String, String> scalarStringMap(JSONObject values) {
115136
final Map<String, String> result = new LinkedHashMap<String, String>();
116137
for (String name : values.keySet()) {
117138
result.put(name, scalarString(values.get(name)));
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
package io.tus.java.example;
2+
3+
import io.tus.java.client.TusClient;
4+
import io.tus.java.client.TusURLMemoryStore;
5+
import io.tus.java.client.TusUpload;
6+
import io.tus.java.client.TusUploader;
7+
import org.json.JSONArray;
8+
import org.json.JSONObject;
9+
10+
import java.io.ByteArrayInputStream;
11+
import java.net.URL;
12+
import java.util.ArrayList;
13+
import java.util.List;
14+
import java.util.Map;
15+
import java.util.concurrent.atomic.AtomicBoolean;
16+
import java.util.concurrent.atomic.AtomicReference;
17+
18+
public final class Api2DevdockTusAbortUpload {
19+
/**
20+
* Run the API2 devdock TUS abort-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 result = uploadAndAbort(scenario);
30+
Api2DevdockScenario.writeResult(result);
31+
32+
System.out.println(
33+
"Java TUS SDK devdock scenario "
34+
+ scenario.getString("scenarioId")
35+
+ " aborted the upload"
36+
);
37+
} catch (Exception e) {
38+
e.printStackTrace();
39+
System.exit(1);
40+
}
41+
}
42+
43+
private static JSONObject uploadAndAbort(JSONObject scenario) throws Exception {
44+
final JSONObject conformanceScenario = Api2DevdockScenario.conformanceScenario(scenario);
45+
final byte[] content = Api2DevdockScenario.conformanceInputSourceBytes(
46+
conformanceScenario
47+
);
48+
final URL endpointOrigin = new URL(Api2DevdockScenario.conformanceInputStringOption(
49+
conformanceScenario,
50+
"endpointUrl"
51+
));
52+
final Map<String, String> metadata = Api2DevdockScenario.conformanceInputStringMapOption(
53+
conformanceScenario,
54+
"metadata"
55+
);
56+
final Map<String, String> headers = Api2DevdockScenario.conformanceInputStringMapOptionOrEmpty(
57+
conformanceScenario,
58+
"headers"
59+
);
60+
final boolean terminateUploadOnAbort = conformanceScenario
61+
.getJSONObject("runtimeSetup")
62+
.getJSONObject("abort")
63+
.getBoolean("terminateUpload");
64+
final String fingerprint = runtimeFingerprint(conformanceScenario, scenario);
65+
final JSONObject completion = conformanceScenario.getJSONObject("completion");
66+
67+
final TusClient client = new TusClient();
68+
final TusURLMemoryStore urlStore = new TusURLMemoryStore();
69+
final List<JSONObject> events = new ArrayList<JSONObject>();
70+
final AtomicReference<TusUploader> activeUploader = new AtomicReference<TusUploader>();
71+
final AtomicBoolean aborted = new AtomicBoolean(false);
72+
final AtomicBoolean successCalled = new AtomicBoolean(false);
73+
final AtomicReference<Exception> uploadError = new AtomicReference<Exception>();
74+
75+
try (Api2DevdockTusConformanceServer conformanceServer =
76+
new Api2DevdockTusConformanceServer(
77+
conformanceScenario,
78+
endpointOrigin,
79+
new Api2DevdockTusConformanceServer.RequestAbortHandler() {
80+
@Override
81+
public void abortRequest(
82+
Api2DevdockTusConformanceServer.RequestAbortContext context
83+
) throws Exception {
84+
aborted.set(true);
85+
events.add(new JSONObject()
86+
.put("kind", "request-abort")
87+
.put("method", context.method())
88+
.put("requestIndex", context.requestIndex())
89+
.put("url", context.url()));
90+
91+
final TusUploader uploader = activeUploader.get();
92+
if (uploader == null) {
93+
if (terminateUploadOnAbort) {
94+
throw new IllegalStateException(
95+
"abort scenario requested termination "
96+
+ "before uploader was available"
97+
);
98+
}
99+
client.abortUpload();
100+
return;
101+
}
102+
103+
client.abortUpload(uploader, false);
104+
}
105+
})) {
106+
client.setUploadCreationURL(conformanceServer.endpointUrl());
107+
client.enableResuming(urlStore);
108+
client.setHeaders(headers);
109+
110+
final TusUpload upload = uploadFor(content, fingerprint, metadata);
111+
final Thread uploadThread = new Thread(new Runnable() {
112+
@Override
113+
public void run() {
114+
try {
115+
final TusUploader uploader = client.resumeOrCreateUpload(upload);
116+
activeUploader.set(uploader);
117+
uploader.setChunkSize(content.length);
118+
uploader.setRequestPayloadSize(content.length);
119+
while (uploader.uploadChunk() > -1) { }
120+
uploader.finish();
121+
successCalled.set(true);
122+
} catch (Exception error) {
123+
if (!aborted.get()) {
124+
uploadError.set(error);
125+
}
126+
}
127+
}
128+
});
129+
uploadThread.start();
130+
uploadThread.join(5000);
131+
if (uploadThread.isAlive()) {
132+
uploadThread.interrupt();
133+
throw new IllegalStateException("timed out waiting for abort upload thread");
134+
}
135+
if (uploadError.get() != null) {
136+
throw uploadError.get();
137+
}
138+
if (!aborted.get()) {
139+
throw new IllegalStateException("abort scenario completed without aborting");
140+
}
141+
if (terminateUploadOnAbort) {
142+
final TusUploader uploader = activeUploader.get();
143+
if (uploader == null) {
144+
throw new IllegalStateException(
145+
"abort scenario requested termination before uploader was available"
146+
);
147+
}
148+
client.abortUpload(uploader, true);
149+
}
150+
151+
conformanceServer.assertExhausted();
152+
final JSONObject result = conformanceServer.result();
153+
result.put("completionKind", completion.getString("kind"));
154+
result.put("errorCalled", false);
155+
result.put("events", new JSONArray(events));
156+
result.put("requestCount", result.getJSONArray("requestMethods").length());
157+
result.put("successCalled", successCalled.get());
158+
result.put("uploadUrl", uploadUrlResult(conformanceServer, activeUploader.get()));
159+
160+
return result;
161+
}
162+
}
163+
164+
private static TusUpload uploadFor(
165+
byte[] content,
166+
String fingerprint,
167+
Map<String, String> metadata
168+
) {
169+
final TusUpload upload = new TusUpload();
170+
upload.setInputStream(new ByteArrayInputStream(content));
171+
upload.setSize(content.length);
172+
upload.setFingerprint(fingerprint);
173+
upload.setMetadata(metadata);
174+
return upload;
175+
}
176+
177+
private static Object uploadUrlResult(
178+
Api2DevdockTusConformanceServer conformanceServer,
179+
TusUploader uploader
180+
) {
181+
if (uploader == null || uploader.getUploadURL() == null) {
182+
return JSONObject.NULL;
183+
}
184+
185+
return conformanceServer.canonicalUrl(uploader.getUploadURL().toString());
186+
}
187+
188+
private static String runtimeFingerprint(JSONObject conformanceScenario, JSONObject scenario) {
189+
final JSONObject runtimeSetup = conformanceScenario.getJSONObject("runtimeSetup");
190+
if (!runtimeSetup.has("fingerprint")) {
191+
return scenario.getString("scenarioId") + "-java-abort-upload";
192+
}
193+
194+
final JSONObject fingerprint = runtimeSetup.getJSONObject("fingerprint");
195+
if (!fingerprint.optBoolean("install", false)) {
196+
return scenario.getString("scenarioId") + "-java-abort-upload";
197+
}
198+
199+
return fingerprint.getString("value");
200+
}
201+
202+
private Api2DevdockTusAbortUpload() {
203+
throw new IllegalStateException("Utility class");
204+
}
205+
}

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

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,20 +16,62 @@
1616
import java.nio.charset.StandardCharsets;
1717
import java.util.ArrayList;
1818
import java.util.List;
19+
import java.util.concurrent.ExecutorService;
20+
import java.util.concurrent.Executors;
1921

2022
final class Api2DevdockTusConformanceServer implements AutoCloseable {
23+
interface RequestAbortHandler {
24+
void abortRequest(RequestAbortContext context) throws Exception;
25+
}
26+
27+
static final class RequestAbortContext {
28+
private final int requestIndex;
29+
private final String method;
30+
private final String url;
31+
32+
RequestAbortContext(int requestIndex, String method, String url) {
33+
this.requestIndex = requestIndex;
34+
this.method = method;
35+
this.url = url;
36+
}
37+
38+
int requestIndex() {
39+
return requestIndex;
40+
}
41+
42+
String method() {
43+
return method;
44+
}
45+
46+
String url() {
47+
return url;
48+
}
49+
}
50+
2151
private final URL endpointOrigin;
2252
private final List<JSONObject> requests;
2353
private final HttpServer server;
54+
private final ExecutorService executor;
2455
private final List<String> errors;
2556
private final List<JSONObject> requestHeaders;
2657
private final List<String> requestMethods;
2758
private final List<String> requestUrls;
59+
private final RequestAbortHandler requestAbortHandler;
2860
private int nextRequestIndex;
2961

3062
Api2DevdockTusConformanceServer(JSONObject conformanceScenario, URL endpointOrigin)
3163
throws IOException {
64+
this(conformanceScenario, endpointOrigin, null);
65+
}
66+
67+
Api2DevdockTusConformanceServer(
68+
JSONObject conformanceScenario,
69+
URL endpointOrigin,
70+
RequestAbortHandler requestAbortHandler
71+
)
72+
throws IOException {
3273
this.endpointOrigin = endpointOrigin;
74+
this.requestAbortHandler = requestAbortHandler;
3375
this.requests = new ArrayList<JSONObject>();
3476
final JSONArray requestArray = conformanceScenario.getJSONArray("requests");
3577
for (int index = 0; index < requestArray.length(); index++) {
@@ -40,12 +82,14 @@ final class Api2DevdockTusConformanceServer implements AutoCloseable {
4082
this.requestMethods = new ArrayList<String>();
4183
this.requestUrls = new ArrayList<String>();
4284
this.server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
85+
this.executor = Executors.newCachedThreadPool();
4386
server.createContext("/", new HttpHandler() {
4487
@Override
4588
public void handle(HttpExchange exchange) throws IOException {
4689
handleRequest(exchange);
4790
}
4891
});
92+
server.setExecutor(executor);
4993
server.start();
5094
}
5195

@@ -68,6 +112,10 @@ void assertExhausted() {
68112
+ requests.size()
69113
+ " conformance request(s), got "
70114
+ nextRequestIndex
115+
+ "; observed methods "
116+
+ requestMethods
117+
+ "; observed URLs "
118+
+ requestUrls
71119
);
72120
}
73121

@@ -101,12 +149,17 @@ JSONObject result() {
101149
@Override
102150
public void close() {
103151
server.stop(0);
152+
executor.shutdownNow();
104153
}
105154

106155
private void handleRequest(HttpExchange exchange) throws IOException {
107156
try {
108157
final byte[] body = readRequestBody(exchange);
109158
final int requestIndex = observeRequest(exchange, body);
159+
if (requests.get(requestIndex).optBoolean("abort", false)) {
160+
abortRequest(exchange, requestIndex);
161+
return;
162+
}
110163
writeResponse(exchange, requests.get(requestIndex));
111164
} catch (Exception error) {
112165
errors.add(error.getMessage());
@@ -118,6 +171,19 @@ private void handleRequest(HttpExchange exchange) throws IOException {
118171
}
119172
}
120173

174+
private void abortRequest(HttpExchange exchange, int requestIndex) throws Exception {
175+
if (requestAbortHandler == null) {
176+
throw new IllegalStateException("request " + requestIndex + " expected abort handler");
177+
}
178+
179+
requestAbortHandler.abortRequest(new RequestAbortContext(
180+
requestIndex,
181+
requestMethods.get(requestIndex),
182+
requestUrls.get(requestIndex)
183+
));
184+
exchange.close();
185+
}
186+
121187
private int observeRequest(HttpExchange exchange, byte[] body) throws IOException {
122188
if (nextRequestIndex >= requests.size()) {
123189
throw new IllegalStateException(

0 commit comments

Comments
 (0)