Skip to content

Commit b1b3d07

Browse files
committed
Add file URL storage proof
1 parent 005bbd3 commit b1b3d07

4 files changed

Lines changed: 384 additions & 0 deletions

File tree

example/build.gradle

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ tasks.register('api2DevdockTusResumeUpload', JavaExec) {
1818
workingDir = rootProject.projectDir
1919
}
2020

21+
tasks.register('api2DevdockTusFileUrlStorageBackend', JavaExec) {
22+
classpath = sourceSets.main.runtimeClasspath
23+
mainClass = 'io.tus.java.example.Api2DevdockTusFileUrlStorageBackend'
24+
workingDir = rootProject.projectDir
25+
}
26+
2127
tasks.register('api2DevdockTusCreationWithUpload', JavaExec) {
2228
classpath = sourceSets.main.runtimeClasspath
2329
mainClass = 'io.tus.java.example.Api2DevdockTusCreationWithUpload'
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
package io.tus.java.example;
2+
3+
import io.tus.java.client.FingerprintNotFoundException;
4+
import io.tus.java.client.ProtocolException;
5+
import io.tus.java.client.ResumingNotEnabledException;
6+
import io.tus.java.client.TusClient;
7+
import io.tus.java.client.TusURLFileStore;
8+
import io.tus.java.client.TusUpload;
9+
import io.tus.java.client.TusUploader;
10+
import org.json.JSONObject;
11+
12+
import java.io.ByteArrayInputStream;
13+
import java.io.File;
14+
import java.io.IOException;
15+
import java.net.URL;
16+
17+
public final class Api2DevdockTusFileUrlStorageBackend {
18+
/**
19+
* Run the API2 devdock TUS file URL storage example.
20+
*
21+
* @param args ignored
22+
*/
23+
public static void main(String[] args) {
24+
try {
25+
System.setProperty("http.strictPostRedirect", "true");
26+
27+
final JSONObject scenario = Api2DevdockScenario.loadScenario();
28+
final JSONObject createResponse = Api2DevdockScenario.createResponse(scenario);
29+
final JSONObject result = uploadWithFileStorage(scenario, createResponse);
30+
Api2DevdockScenario.writeResult(result);
31+
32+
System.out.println(
33+
"Java TUS SDK devdock scenario "
34+
+ scenario.getString("scenarioId")
35+
+ " resumed with file URL storage "
36+
+ result.getString("uploadUrl")
37+
);
38+
} catch (Exception e) {
39+
e.printStackTrace();
40+
System.exit(1);
41+
}
42+
}
43+
44+
private static JSONObject uploadWithFileStorage(
45+
JSONObject scenario,
46+
JSONObject createResponse
47+
) throws IOException, ProtocolException, FingerprintNotFoundException,
48+
ResumingNotEnabledException {
49+
final JSONObject uploadConfig = scenario.getJSONObject("upload");
50+
final JSONObject resume = uploadConfig.getJSONObject("resume");
51+
final JSONObject urlStorageBackend = uploadConfig.getJSONObject("urlStorageBackend");
52+
final byte[] content = Api2DevdockScenario.scenarioBytes(uploadConfig);
53+
final int chunkSize = Api2DevdockScenario.fixedChunkSizeBytes(uploadConfig);
54+
final String fingerprint = resume.getString("fingerprint");
55+
final File storageFile = File.createTempFile("api2-devdock-tus-url-storage", ".properties");
56+
57+
try {
58+
final TusURLFileStore store = new TusURLFileStore(storageFile);
59+
final TusClient client = new TusClient();
60+
client.setUploadCreationURL(
61+
new URL(Api2DevdockScenario.tusUrl(uploadConfig, scenario, createResponse))
62+
);
63+
client.enableResuming(store);
64+
if (resume.getBoolean("removeFingerprintOnSuccess")) {
65+
client.enableRemoveFingerprintOnSuccess();
66+
}
67+
68+
final TusUpload firstUpload = uploadFor(
69+
scenario,
70+
createResponse,
71+
content,
72+
fingerprint
73+
);
74+
final TusUploader firstUploader = client.createUpload(firstUpload);
75+
firstUploader.setChunkSize(chunkSize);
76+
final int firstAcceptedBytes = firstUploader.uploadChunk();
77+
firstUploader.finish(false);
78+
79+
if (firstAcceptedBytes != resume.getInt("stopAfterAcceptedBytes")) {
80+
throw new IllegalStateException(
81+
"first upload accepted "
82+
+ firstAcceptedBytes
83+
+ " bytes, expected "
84+
+ resume.getInt("stopAfterAcceptedBytes")
85+
);
86+
}
87+
88+
final String firstUploadUrl = firstUploader.getUploadURL().toString();
89+
final int previousUploadCount = store.size();
90+
if (previousUploadCount != resume.getInt("expectedPreviousUploadCount")) {
91+
throw new IllegalStateException(
92+
"stored upload count "
93+
+ previousUploadCount
94+
+ ", expected "
95+
+ resume.getInt("expectedPreviousUploadCount")
96+
);
97+
}
98+
final boolean storedUploadKeyPrefixMatched = store.hasKeyWithPrefix(
99+
urlStorageBackend.getString("expectedStoredUploadKeyPrefix")
100+
);
101+
102+
final TusUpload secondUpload = uploadFor(
103+
scenario,
104+
createResponse,
105+
content,
106+
fingerprint
107+
);
108+
final TusUploader resumedUploader = client.resumeUpload(secondUpload);
109+
resumedUploader.setChunkSize(content.length);
110+
int uploadedChunkSize;
111+
do {
112+
uploadedChunkSize = resumedUploader.uploadChunk();
113+
} while (uploadedChunkSize > -1);
114+
resumedUploader.finish();
115+
116+
final String uploadUrl = resumedUploader.getUploadURL().toString();
117+
if (!firstUploadUrl.equals(uploadUrl)) {
118+
throw new IllegalStateException(
119+
"resumed upload URL " + uploadUrl + ", expected " + firstUploadUrl
120+
);
121+
}
122+
if (resumedUploader.getOffset() != content.length) {
123+
throw new IllegalStateException(
124+
"remote offset "
125+
+ resumedUploader.getOffset()
126+
+ ", expected "
127+
+ content.length
128+
);
129+
}
130+
131+
final int remainingPreviousUploadCount = store.size();
132+
if (remainingPreviousUploadCount != resume.getInt("expectedRemainingPreviousUploadCount")) {
133+
throw new IllegalStateException(
134+
"remaining stored upload count "
135+
+ remainingPreviousUploadCount
136+
+ ", expected "
137+
+ resume.getInt("expectedRemainingPreviousUploadCount")
138+
);
139+
}
140+
141+
return new JSONObject()
142+
.put("firstAcceptedBytes", firstAcceptedBytes)
143+
.put("firstUploadUrl", firstUploadUrl)
144+
.put("previousUploadCount", previousUploadCount)
145+
.put("remainingPreviousUploadCount", remainingPreviousUploadCount)
146+
.put("storageFileEntryCount", store.size())
147+
.put("storedUploadKeyPrefixMatched", storedUploadKeyPrefixMatched)
148+
.put("uploadUrl", uploadUrl)
149+
.put("urlStorageBackend", urlStorageBackend.getString("kind"));
150+
} finally {
151+
if (storageFile.exists() && !storageFile.delete()) {
152+
storageFile.deleteOnExit();
153+
}
154+
}
155+
}
156+
157+
private static TusUpload uploadFor(
158+
JSONObject scenario,
159+
JSONObject createResponse,
160+
byte[] content,
161+
String fingerprint
162+
) {
163+
final JSONObject uploadConfig = scenario.getJSONObject("upload");
164+
final TusUpload upload = new TusUpload();
165+
upload.setInputStream(new ByteArrayInputStream(content));
166+
upload.setSize(content.length);
167+
upload.setFingerprint(fingerprint);
168+
upload.setMetadata(
169+
Api2DevdockScenario.uploadMetadata(uploadConfig, scenario, createResponse)
170+
);
171+
return upload;
172+
}
173+
174+
private Api2DevdockTusFileUrlStorageBackend() {
175+
throw new IllegalStateException("Utility class");
176+
}
177+
}
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
package io.tus.java.client;
2+
3+
import org.jetbrains.annotations.NotNull;
4+
5+
import java.io.File;
6+
import java.io.FileInputStream;
7+
import java.io.FileOutputStream;
8+
import java.io.IOException;
9+
import java.net.MalformedURLException;
10+
import java.net.URL;
11+
import java.util.ArrayList;
12+
import java.util.Collections;
13+
import java.util.List;
14+
import java.util.Properties;
15+
import java.util.UUID;
16+
17+
/**
18+
* Persistent URL store backed by a local properties file.
19+
*/
20+
public class TusURLFileStore implements TusURLStore {
21+
private static final String NAMESPACE = "tus";
22+
private static final String SEPARATOR = "::";
23+
24+
private final File file;
25+
26+
/**
27+
* Create a persistent URL store.
28+
*
29+
* @param file File used for storing upload URLs.
30+
*/
31+
public TusURLFileStore(@NotNull File file) {
32+
this.file = file;
33+
}
34+
35+
/**
36+
* Stores the upload's fingerprint and URL.
37+
*
38+
* @param fingerprint An upload's fingerprint.
39+
* @param url The corresponding upload URL.
40+
*/
41+
@Override
42+
public synchronized void set(String fingerprint, URL url) {
43+
Properties properties = readProperties();
44+
properties.setProperty(newKey(fingerprint), url.toString());
45+
writeProperties(properties);
46+
}
47+
48+
/**
49+
* Returns the first stored upload URL for a fingerprint.
50+
*
51+
* @param fingerprint An upload's fingerprint.
52+
* @return The corresponding upload URL.
53+
*/
54+
@Override
55+
public synchronized URL get(String fingerprint) {
56+
Properties properties = readProperties();
57+
List<String> keys = keysForFingerprint(properties, fingerprint);
58+
if (keys.isEmpty()) {
59+
return null;
60+
}
61+
62+
try {
63+
return new URL(properties.getProperty(keys.get(0)));
64+
} catch (MalformedURLException error) {
65+
return null;
66+
}
67+
}
68+
69+
/**
70+
* Removes all stored upload URLs for a fingerprint.
71+
*
72+
* @param fingerprint An upload's fingerprint.
73+
*/
74+
@Override
75+
public synchronized void remove(String fingerprint) {
76+
Properties properties = readProperties();
77+
for (String key : keysForFingerprint(properties, fingerprint)) {
78+
properties.remove(key);
79+
}
80+
writeProperties(properties);
81+
}
82+
83+
/**
84+
* Returns the number of stored upload URLs.
85+
*
86+
* @return Stored upload URL count.
87+
*/
88+
public synchronized int size() {
89+
return readProperties().size();
90+
}
91+
92+
/**
93+
* Returns whether a stored key starts with the given prefix.
94+
*
95+
* @param prefix Key prefix.
96+
* @return True if a stored key starts with the prefix.
97+
*/
98+
public synchronized boolean hasKeyWithPrefix(String prefix) {
99+
Properties properties = readProperties();
100+
for (Object key : properties.keySet()) {
101+
if (String.valueOf(key).startsWith(prefix)) {
102+
return true;
103+
}
104+
}
105+
106+
return false;
107+
}
108+
109+
private String newKey(String fingerprint) {
110+
return keyPrefix(fingerprint) + UUID.randomUUID().toString();
111+
}
112+
113+
private static String keyPrefix(String fingerprint) {
114+
return NAMESPACE + SEPARATOR + fingerprint + SEPARATOR;
115+
}
116+
117+
private static List<String> keysForFingerprint(Properties properties, String fingerprint) {
118+
final String prefix = keyPrefix(fingerprint);
119+
final List<String> result = new ArrayList<String>();
120+
for (Object key : properties.keySet()) {
121+
final String stringKey = String.valueOf(key);
122+
if (stringKey.startsWith(prefix)) {
123+
result.add(stringKey);
124+
}
125+
}
126+
Collections.sort(result);
127+
return result;
128+
}
129+
130+
private Properties readProperties() {
131+
final Properties properties = new Properties();
132+
if (!file.exists()) {
133+
return properties;
134+
}
135+
136+
try (FileInputStream input = new FileInputStream(file)) {
137+
properties.load(input);
138+
} catch (IOException error) {
139+
throw new IllegalStateException("could not read TUS URL storage file", error);
140+
}
141+
142+
return properties;
143+
}
144+
145+
private void writeProperties(Properties properties) {
146+
final File parent = file.getParentFile();
147+
if (parent != null && !parent.exists() && !parent.mkdirs()) {
148+
throw new IllegalStateException("could not create TUS URL storage directory");
149+
}
150+
151+
try (FileOutputStream output = new FileOutputStream(file)) {
152+
properties.store(output, "tus-java-client URL storage");
153+
} catch (IOException error) {
154+
throw new IllegalStateException("could not write TUS URL storage file", error);
155+
}
156+
}
157+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package io.tus.java.client;
2+
3+
import org.junit.Test;
4+
5+
import java.io.File;
6+
import java.net.URL;
7+
import java.nio.file.Files;
8+
9+
import static org.junit.Assert.assertEquals;
10+
import static org.junit.Assert.assertNull;
11+
import static org.junit.Assert.assertTrue;
12+
13+
/**
14+
* Test class for {@link TusURLFileStore}.
15+
*/
16+
public class TestTusURLFileStore {
17+
/**
18+
* Tests if file-backed URL storage persists and removes upload URLs.
19+
*
20+
* @throws Exception if the temporary file or URL cannot be created.
21+
*/
22+
@Test
23+
public void test() throws Exception {
24+
File file = Files.createTempFile("tus-url-store", ".properties").toFile();
25+
assertTrue(file.delete());
26+
27+
URL url = new URL("https://tusd.tusdemo.net/files/hello");
28+
TusURLFileStore store = new TusURLFileStore(file);
29+
store.set("foo", url);
30+
31+
assertEquals(url, store.get("foo"));
32+
assertEquals(1, store.size());
33+
assertTrue(store.hasKeyWithPrefix("tus::foo::"));
34+
35+
TusURLFileStore restoredStore = new TusURLFileStore(file);
36+
assertEquals(url, restoredStore.get("foo"));
37+
38+
restoredStore.remove("foo");
39+
assertNull(restoredStore.get("foo"));
40+
assertEquals(0, restoredStore.size());
41+
42+
assertTrue(file.delete());
43+
}
44+
}

0 commit comments

Comments
 (0)