From ebb5eabecd920ef4951f979a49cc9b0525bfc3b5 Mon Sep 17 00:00:00 2001 From: Juan Jose Rodriguez Date: Tue, 21 May 2019 14:41:37 +0200 Subject: [PATCH 1/2] tus/tus-java-client#26 Add support for removeFingerprintOnSuccess config property --- build.gradle | 1 + .../java/io/tus/java/client/TusClient.java | 48 +++++++++- .../java/io/tus/java/client/TusUploader.java | 8 +- .../io/tus/java/client/TestTusClient.java | 50 ++++++++++- .../io/tus/java/client/TestTusUploader.java | 87 +++++++++++++++---- 5 files changed, 171 insertions(+), 23 deletions(-) diff --git a/build.gradle b/build.gradle index 027c102..db60000 100644 --- a/build.gradle +++ b/build.gradle @@ -18,6 +18,7 @@ dependencies { compile 'org.jetbrains:annotations:13.0' testCompile 'junit:junit:4.12' testCompile 'org.mock-server:mockserver-netty:5.2.2' + testCompile 'org.mockito:mockito-core:2.+' } task sourcesJar(type: Jar, dependsOn: classes) { diff --git a/src/main/java/io/tus/java/client/TusClient.java b/src/main/java/io/tus/java/client/TusClient.java index dcd8f37..3ea3d69 100644 --- a/src/main/java/io/tus/java/client/TusClient.java +++ b/src/main/java/io/tus/java/client/TusClient.java @@ -20,6 +20,7 @@ public class TusClient { private URL uploadCreationURL; private boolean resumingEnabled; + private boolean removeFingerprintOnSuccessEnabled; private TusURLStore urlStore; private Map headers; private int connectTimeout = 5000; @@ -83,6 +84,37 @@ public void disableResuming() { public boolean resumingEnabled() { return resumingEnabled; } + + /** + * Enable removing fingerprints on success. + * + * @see #disableRemoveFingerprintOnSuccess() + */ + public void enableRemoveFingerprintOnSuccess() { + removeFingerprintOnSuccessEnabled = true; + } + + /** + * Disable removing fingerprints on success. + * + * @see #enableRemoveFingerprintOnSuccess() + */ + public void disableRemoveFingerprintOnSuccess() { + removeFingerprintOnSuccessEnabled = false; + } + + /** + * Get the current status if removing fingerprints on success. + * + * @see #enableRemoveFingerprintOnSuccess() + * @see #disableRemoveFingerprintOnSuccess() + * + * @return True if resuming has been enabled using {@link #enableResuming(TusURLStore)} + */ + public boolean removeFingerprintOnSuccessEnabled() { + return removeFingerprintOnSuccessEnabled; + } + /** * Set headers which will be added to every HTTP requestes made by this TusClient instance. @@ -165,7 +197,7 @@ public TusUploader createUpload(@NotNull TusUpload upload) throws ProtocolExcept urlStore.set(upload.getFingerprint(), uploadURL); } - return new TusUploader(this, uploadURL, upload.getTusInputStream(), 0); + return new TusUploader(this, upload, uploadURL, upload.getTusInputStream(), 0); } /** @@ -233,7 +265,7 @@ public TusUploader beginOrResumeUploadFromURL(@NotNull TusUpload upload, @NotNul } long offset = Long.parseLong(offsetStr); - return new TusUploader(this, uploadURL, upload.getTusInputStream(), offset); + return new TusUploader(this, upload, uploadURL, upload.getTusInputStream(), offset); } /** @@ -286,4 +318,16 @@ public void prepareConnection(@NotNull HttpURLConnection connection) { } } } + + /** + * Actions to be performed after a successful upload completion. + * Manages URL removal from the URL store if remove fingerprint on success is enabled + * + * @param upload that has been finished + */ + public void uploadFinished(@NotNull TusUpload upload) { + if (resumingEnabled && removeFingerprintOnSuccessEnabled) { + urlStore.remove(upload.getFingerprint()); + } + } } diff --git a/src/main/java/io/tus/java/client/TusUploader.java b/src/main/java/io/tus/java/client/TusUploader.java index c7bbf7d..5a7a8c7 100644 --- a/src/main/java/io/tus/java/client/TusUploader.java +++ b/src/main/java/io/tus/java/client/TusUploader.java @@ -24,6 +24,7 @@ public class TusUploader { private TusInputStream input; private long offset; private TusClient client; + private TusUpload upload; private byte[] buffer; private int requestPayloadSize = 10 * 1024 * 1024; private int bytesRemainingForRequest; @@ -41,11 +42,12 @@ public class TusUploader { * @param offset Offset to read from * @throws IOException Thrown if an exception occurs while issuing the HTTP request. */ - public TusUploader(TusClient client, URL uploadURL, TusInputStream input, long offset) throws IOException { + public TusUploader(TusClient client, TusUpload upload, URL uploadURL, TusInputStream input, long offset) throws IOException { this.uploadURL = uploadURL; this.input = input; this.offset = offset; this.client = client; + this.upload = upload; input.seekTo(offset); @@ -270,6 +272,10 @@ public URL getUploadURL() { */ public void finish() throws ProtocolException, IOException { finishConnection(); + if (upload.getSize() == offset) { + client.uploadFinished(upload); + } + // Close the TusInputStream after checking the response and closing the connection to ensure // that we will not need to read from it again in the future. input.close(); diff --git a/src/test/java/io/tus/java/client/TestTusClient.java b/src/test/java/io/tus/java/client/TestTusClient.java index 6d1e9d7..6eecec8 100644 --- a/src/test/java/io/tus/java/client/TestTusClient.java +++ b/src/test/java/io/tus/java/client/TestTusClient.java @@ -1,5 +1,10 @@ package io.tus.java.client; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + import java.io.ByteArrayInputStream; import java.io.IOException; import java.net.HttpURLConnection; @@ -13,8 +18,6 @@ import org.mockserver.model.HttpRequest; import org.mockserver.model.HttpResponse; -import static org.junit.Assert.*; - public class TestTusClient extends MockServerProvider { @Test @@ -351,4 +354,47 @@ public void testFollowRedirects() throws Exception { assertEquals(uploader.getUploadURL(), new URL(mockServerURL + "/foo")); } + + @Test + public void testRemoveFingerprintOnSuccessDisabled() throws IOException, ProtocolException { + + TusClient client = new TusClient(); + + TusURLStore store = new TusURLMemoryStore(); + URL dummyURL = new URL("http://dummy-url/files/dummy"); + store.set("fingerprint", dummyURL); + client.enableResuming(store); + + assertTrue(!client.removeFingerprintOnSuccessEnabled()); + + TusUpload upload = new TusUpload(); + upload.setFingerprint("fingerprint"); + + client.uploadFinished(upload); + + assertTrue(dummyURL.equals(store.get("fingerprint"))); + + } + + @Test + public void testRemoveFingerprintOnSuccessEnabled() throws IOException, ProtocolException { + + TusClient client = new TusClient(); + + TusURLStore store = new TusURLMemoryStore(); + URL dummyURL = new URL("http://dummy-url/files/dummy"); + store.set("fingerprint", dummyURL); + client.enableResuming(store); + client.enableRemoveFingerprintOnSuccess(); + + assertTrue(client.removeFingerprintOnSuccessEnabled()); + + TusUpload upload = new TusUpload(); + upload.setFingerprint("fingerprint"); + + client.uploadFinished(upload); + + assertTrue(store.get("fingerprint") == null); + + } } diff --git a/src/test/java/io/tus/java/client/TestTusUploader.java b/src/test/java/io/tus/java/client/TestTusUploader.java index d5355da..59c9aaf 100644 --- a/src/test/java/io/tus/java/client/TestTusUploader.java +++ b/src/test/java/io/tus/java/client/TestTusUploader.java @@ -1,15 +1,14 @@ package io.tus.java.client; -import org.junit.Assume; -import org.junit.Test; -import org.mockserver.model.HttpRequest; -import org.mockserver.model.HttpResponse; -import org.mockserver.socket.PortFactory; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.times; import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.IOException; -import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.net.MalformedURLException; @@ -18,7 +17,11 @@ import java.net.URL; import java.util.Arrays; -import static org.junit.Assert.*; +import org.junit.Assume; +import org.junit.Test; +import org.mockserver.model.HttpRequest; +import org.mockserver.model.HttpResponse; +import org.mockserver.socket.PortFactory; public class TestTusUploader extends MockServerProvider { private boolean isOpenJDK6 = System.getProperty("java.version").startsWith("1.6") && @@ -45,7 +48,9 @@ public void testTusUploader() throws IOException, ProtocolException { TusInputStream input = new TusInputStream(new ByteArrayInputStream(content)); long offset = 3; - TusUploader uploader = new TusUploader(client, uploadUrl, input, offset); + TusUpload upload = new TusUpload(); + + TusUploader uploader = new TusUploader(client, upload, uploadUrl, input, offset); uploader.setChunkSize(5); assertEquals(uploader.getChunkSize(), 5); @@ -57,6 +62,48 @@ public void testTusUploader() throws IOException, ProtocolException { assertEquals(11, uploader.getOffset()); uploader.finish(); } + + @Test + public void testTusUploaderClientUploadFinishedCalled() throws IOException, ProtocolException { + + TusClient client = mock(TusClient.class); + + byte[] content = "hello world".getBytes(); + + URL uploadUrl = new URL("http://dummy-url/foo"); + TusInputStream input = new TusInputStream(new ByteArrayInputStream(content)); + long offset = 10; + + TusUpload upload = new TusUpload(); + upload.setSize(10); + + TusUploader uploader = new TusUploader(client, upload, uploadUrl, input, offset); + uploader.finish(); + + // size and offset are the same, so uploadfinished() should be called + verify(client).uploadFinished(upload); + } + + @Test + public void testTusUploaderClientUploadFinishedNotCalled() throws IOException, ProtocolException { + + TusClient client = mock(TusClient.class); + + byte[] content = "hello world".getBytes(); + + URL uploadUrl = new URL("http://dummy-url/foo"); + TusInputStream input = new TusInputStream(new ByteArrayInputStream(content)); + long offset = 0; + + TusUpload upload = new TusUpload(); + upload.setSize(10); + + TusUploader uploader = new TusUploader(client, upload, uploadUrl, input, offset); + uploader.finish(); + + // size is greater than offset, so uploadfinished() should not be called + verify(client,times(0)).uploadFinished(upload); + } @Test public void testTusUploaderFailedExpectation() throws IOException, ProtocolException { @@ -71,9 +118,9 @@ public void testTusUploaderFailedExpectation() throws IOException, ProtocolExcep URL uploadUrl = new URL(server.getURL() + "/expect"); TusInputStream input = new TusInputStream(new ByteArrayInputStream(content)); long offset = 3; - + TusUpload upload = new TusUpload(); boolean exceptionThrown = false; - TusUploader uploader = new TusUploader(client, uploadUrl, input, offset); + TusUploader uploader = new TusUploader(client, upload, uploadUrl, input, offset); try { uploader.uploadChunk(); } catch(ProtocolException e) { @@ -168,8 +215,9 @@ public void testSetRequestPayloadSize() throws Exception { TusClient client = new TusClient(); URL uploadUrl = new URL(mockServerURL + "/payload"); TusInputStream input = new TusInputStream(new ByteArrayInputStream(content)); - - TusUploader uploader = new TusUploader(client, uploadUrl, input, 0); + TusUpload upload = new TusUpload(); + + TusUploader uploader = new TusUploader(client, upload, uploadUrl, input, 0); assertEquals(uploader.getRequestPayloadSize(), 10 * 1024 * 1024); uploader.setRequestPayloadSize(5); @@ -197,8 +245,9 @@ public void testSetRequestPayloadSizeThrows() throws Exception { TusClient client = new TusClient(); URL uploadUrl = new URL(mockServerURL + "/payloadException"); TusInputStream input = new TusInputStream(new ByteArrayInputStream(content)); - - TusUploader uploader = new TusUploader(client, uploadUrl, input, 0); + TusUpload upload = new TusUpload(); + + TusUploader uploader = new TusUploader(client, upload, uploadUrl, input, 0); uploader.setChunkSize(4); uploader.uploadChunk(); @@ -220,8 +269,9 @@ public void testMissingUploadOffsetHeader() throws Exception { TusClient client = new TusClient(); URL uploadUrl = new URL(mockServerURL + "/missingHeader"); TusInputStream input = new TusInputStream(new ByteArrayInputStream(content)); - - TusUploader uploader = new TusUploader(client, uploadUrl, input, 0); + TusUpload upload = new TusUpload(); + + TusUploader uploader = new TusUploader(client, upload, uploadUrl, input, 0); boolean exceptionThrown = false; try { @@ -249,8 +299,9 @@ public void testUnmatchingUploadOffsetHeader() throws Exception { TusClient client = new TusClient(); URL uploadUrl = new URL(mockServerURL + "/unmatchingHeader"); TusInputStream input = new TusInputStream(new ByteArrayInputStream(content)); - - TusUploader uploader = new TusUploader(client, uploadUrl, input, 0); + TusUpload upload = new TusUpload(); + + TusUploader uploader = new TusUploader(client, upload, uploadUrl, input, 0); boolean exceptionThrown = false; try { From b5807b84aca25c594acc6cdff55f4e07b8b81781 Mon Sep 17 00:00:00 2001 From: Juan Jose Rodriguez Date: Tue, 4 Jun 2019 17:16:59 +0200 Subject: [PATCH 2/2] Change uploadFinished visibility to protected --- .../java/io/tus/java/client/TusClient.java | 666 +++++++++--------- 1 file changed, 333 insertions(+), 333 deletions(-) diff --git a/src/main/java/io/tus/java/client/TusClient.java b/src/main/java/io/tus/java/client/TusClient.java index 3ea3d69..abe6019 100644 --- a/src/main/java/io/tus/java/client/TusClient.java +++ b/src/main/java/io/tus/java/client/TusClient.java @@ -1,333 +1,333 @@ -package io.tus.java.client; - -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.io.IOException; -import java.net.HttpURLConnection; -import java.net.URL; -import java.util.Map; - -/** - * This class is used for creating or resuming uploads. - */ -public class TusClient { - /** - * Version of the tus protocol used by the client. The remote server needs to support this - * version, too. - */ - public final static String TUS_VERSION = "1.0.0"; - - private URL uploadCreationURL; - private boolean resumingEnabled; - private boolean removeFingerprintOnSuccessEnabled; - private TusURLStore urlStore; - private Map headers; - private int connectTimeout = 5000; - - /** - * Create a new tus client. - */ - public TusClient() { - - } - - /** - * Set the URL used for creating new uploads. This is required if you want to initiate new - * uploads using {@link #createUpload} or {@link #resumeOrCreateUpload} but is not used if you - * only resume existing uploads. - * - * @param uploadCreationURL Absolute upload creation URL - */ - public void setUploadCreationURL(URL uploadCreationURL) { - this.uploadCreationURL = uploadCreationURL; - } - - /** - * Get the current upload creation URL - * - * @return Current upload creation URL - */ - public URL getUploadCreationURL() { - return uploadCreationURL; - } - - /** - * Enable resuming already started uploads. This step is required if you want to use - * {@link #resumeUpload(TusUpload)}. - * - * @param urlStore Storage used to save and retrieve upload URLs by its fingerprint. - */ - public void enableResuming(@NotNull TusURLStore urlStore) { - resumingEnabled = true; - this.urlStore = urlStore; - } - - /** - * Disable resuming started uploads. - * - * @see #enableResuming(TusURLStore) - */ - public void disableResuming() { - resumingEnabled = false; - this.urlStore = null; - } - - /** - * Get the current status if resuming. - * - * @see #enableResuming(TusURLStore) - * @see #disableResuming() - * - * @return True if resuming has been enabled using {@link #enableResuming(TusURLStore)} - */ - public boolean resumingEnabled() { - return resumingEnabled; - } - - /** - * Enable removing fingerprints on success. - * - * @see #disableRemoveFingerprintOnSuccess() - */ - public void enableRemoveFingerprintOnSuccess() { - removeFingerprintOnSuccessEnabled = true; - } - - /** - * Disable removing fingerprints on success. - * - * @see #enableRemoveFingerprintOnSuccess() - */ - public void disableRemoveFingerprintOnSuccess() { - removeFingerprintOnSuccessEnabled = false; - } - - /** - * Get the current status if removing fingerprints on success. - * - * @see #enableRemoveFingerprintOnSuccess() - * @see #disableRemoveFingerprintOnSuccess() - * - * @return True if resuming has been enabled using {@link #enableResuming(TusURLStore)} - */ - public boolean removeFingerprintOnSuccessEnabled() { - return removeFingerprintOnSuccessEnabled; - } - - - /** - * Set headers which will be added to every HTTP requestes made by this TusClient instance. - * These may to overwrite tus-specific headers, which can be identified by their Tus-* - * prefix, and can cause unexpected behavior. - * - * @see #getHeaders() - * @see #prepareConnection(HttpURLConnection) - * - * @param headers The map of HTTP headers - */ - public void setHeaders(@Nullable Map headers) { - this.headers = headers; - } - - /** - * Get the HTTP headers which should be contained in every request and were configured using - * {@link #setHeaders(Map)}. - * - * @see #setHeaders(Map) - * @see #prepareConnection(HttpURLConnection) - * - * @return The map of configured HTTP headers - */ - @Nullable - public Map getHeaders() { - return headers; - } - - public void setConnectTimeout(int timeout) { - connectTimeout = timeout; - } - - public int getConnectTimeout() { - return connectTimeout; - } - - /** - * Create a new upload using the Creation extension. Before calling this function, an "upload - * creation URL" must be defined using {@link #setUploadCreationURL(URL)} or else this - * function will fail. - * In order to create the upload a POST request will be issued. The file's chunks must be - * uploaded manually using the returned {@link TusUploader} object. - * - * @param upload The file for which a new upload will be created - * @return Use {@link TusUploader} to upload the file's chunks. - * @throws ProtocolException Thrown if the remote server sent an unexpected response, e.g. - * wrong status codes or missing/invalid headers. - * @throws IOException Thrown if an exception occurs while issuing the HTTP request. - */ - public TusUploader createUpload(@NotNull TusUpload upload) throws ProtocolException, IOException { - HttpURLConnection connection = (HttpURLConnection) uploadCreationURL.openConnection(); - connection.setRequestMethod("POST"); - prepareConnection(connection); - - String encodedMetadata = upload.getEncodedMetadata(); - if(encodedMetadata.length() > 0) { - connection.setRequestProperty("Upload-Metadata", encodedMetadata); - } - - connection.addRequestProperty("Upload-Length", Long.toString(upload.getSize())); - connection.connect(); - - int responseCode = connection.getResponseCode(); - if(!(responseCode >= 200 && responseCode < 300)) { - throw new ProtocolException("unexpected status code (" + responseCode + ") while creating upload", connection); - } - - String urlStr = connection.getHeaderField("Location"); - if(urlStr == null || urlStr.length() == 0) { - throw new ProtocolException("missing upload URL in response for creating upload", connection); - } - - // The upload URL must be relative to the URL of the request by which is was returned, - // not the upload creation URL. In most cases, there is no difference between those two - // but there may be cases in which the POST request is redirected. - URL uploadURL = new URL(connection.getURL(), urlStr); - - if(resumingEnabled) { - urlStore.set(upload.getFingerprint(), uploadURL); - } - - return new TusUploader(this, upload, uploadURL, upload.getTusInputStream(), 0); - } - - /** - * Try to resume an already started upload. Before call this function, resuming must be - * enabled using {@link #enableResuming(TusURLStore)}. This method will look up the URL for this - * upload in the {@link TusURLStore} using the upload's fingerprint (see - * {@link TusUpload#getFingerprint()}). After a successful lookup a HEAD request will be issued - * to find the current offset without uploading the file, yet. - * - * @param upload The file for which an upload will be resumed - * @return Use {@link TusUploader} to upload the remaining file's chunks. - * @throws FingerprintNotFoundException Thrown if no matching fingerprint has been found in - * {@link TusURLStore}. Use {@link #createUpload(TusUpload)} to create a new upload. - * @throws ResumingNotEnabledException Throw if resuming has not been enabled using {@link - * #enableResuming(TusURLStore)}. - * @throws ProtocolException Thrown if the remote server sent an unexpected response, e.g. - * wrong status codes or missing/invalid headers. - * @throws IOException Thrown if an exception occurs while issuing the HTTP request. - */ - public TusUploader resumeUpload(@NotNull TusUpload upload) throws FingerprintNotFoundException, ResumingNotEnabledException, ProtocolException, IOException { - if (!resumingEnabled) { - throw new ResumingNotEnabledException(); - } - - URL uploadURL = urlStore.get(upload.getFingerprint()); - if (uploadURL == null) { - throw new FingerprintNotFoundException(upload.getFingerprint()); - } - - return beginOrResumeUploadFromURL(upload, uploadURL); - } - - /** - * Begin an upload or alternatively resume it if the upload has already been started before. In contrast to - * {@link #createUpload(TusUpload)} and {@link #resumeOrCreateUpload(TusUpload)} this method will not create a new - * upload. The user must obtain the upload location URL on their own as this method will not send the POST request - * which is normally used to create a new upload. - * Therefore, this method is only useful if you are uploading to a service which takes care of creating the tus - * upload for yourself. One example of such a service is the Vimeo API. - * When called a HEAD request will be issued to find the current offset without uploading the file, yet. - * The uploading can be started by using the returned {@link TusUploader} object. - * - * @param upload The file for which an upload will be resumed - * @param uploadURL The upload location URL at which has already been created and this file should be uploaded to. - * @return Use {@link TusUploader} to upload the remaining file's chunks. - * @throws ProtocolException Thrown if the remote server sent an unexpected response, e.g. - * wrong status codes or missing/invalid headers. - * @throws IOException Thrown if an exception occurs while issuing the HTTP request. - */ - public TusUploader beginOrResumeUploadFromURL(@NotNull TusUpload upload, @NotNull URL uploadURL) throws ProtocolException, IOException { - HttpURLConnection connection = (HttpURLConnection) uploadURL.openConnection(); - connection.setRequestMethod("HEAD"); - prepareConnection(connection); - - connection.connect(); - - int responseCode = connection.getResponseCode(); - if(!(responseCode >= 200 && responseCode < 300)) { - throw new ProtocolException("unexpected status code (" + responseCode + ") while resuming upload", connection); - } - - String offsetStr = connection.getHeaderField("Upload-Offset"); - if(offsetStr == null || offsetStr.length() == 0) { - throw new ProtocolException("missing upload offset in response for resuming upload", connection); - } - long offset = Long.parseLong(offsetStr); - - return new TusUploader(this, upload, uploadURL, upload.getTusInputStream(), offset); - } - - /** - * Try to resume an upload using {@link #resumeUpload(TusUpload)}. If the method call throws - * an {@link ResumingNotEnabledException} or {@link FingerprintNotFoundException}, a new upload - * will be created using {@link #createUpload(TusUpload)}. - * - * @param upload The file for which an upload will be resumed - * @throws ProtocolException Thrown if the remote server sent an unexpected response, e.g. - * wrong status codes or missing/invalid headers. - * @throws IOException Thrown if an exception occurs while issuing the HTTP request. - */ - public TusUploader resumeOrCreateUpload(@NotNull TusUpload upload) throws ProtocolException, IOException { - try { - return resumeUpload(upload); - } catch(FingerprintNotFoundException e) { - return createUpload(upload); - } catch(ResumingNotEnabledException e) { - return createUpload(upload); - } catch(ProtocolException e) { - // If the attempt to resume returned a 404 Not Found, we immediately try to create a new - // one since TusExectuor would not retry this operation. - HttpURLConnection connection = e.getCausingConnection(); - if(connection != null && connection.getResponseCode() == 404) { - return createUpload(upload); - } - - throw e; - } - } - - /** - * Set headers used for every HTTP request. Currently, this will add the Tus-Resumable header - * and any custom header which can be configured using {@link #setHeaders(Map)}, - * - * @param connection The connection whose headers will be modified. - */ - public void prepareConnection(@NotNull HttpURLConnection connection) { - // Only follow redirects, if the POST methods is preserved. If http.strictPostRedirect is - // disabled, a POST request will be transformed into a GET request which is not wanted by us. - // See: http://grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/7u40-b43/sun/net/www/protocol/http/HttpURLConnection.java#2372 - connection.setInstanceFollowRedirects(Boolean.getBoolean("http.strictPostRedirect")); - - connection.setConnectTimeout(connectTimeout); - connection.addRequestProperty("Tus-Resumable", TUS_VERSION); - - if(headers != null) { - for (Map.Entry entry : headers.entrySet()) { - connection.addRequestProperty(entry.getKey(), entry.getValue()); - } - } - } - - /** - * Actions to be performed after a successful upload completion. - * Manages URL removal from the URL store if remove fingerprint on success is enabled - * - * @param upload that has been finished - */ - public void uploadFinished(@NotNull TusUpload upload) { - if (resumingEnabled && removeFingerprintOnSuccessEnabled) { - urlStore.remove(upload.getFingerprint()); - } - } -} +package io.tus.java.client; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.Map; + +/** + * This class is used for creating or resuming uploads. + */ +public class TusClient { + /** + * Version of the tus protocol used by the client. The remote server needs to support this + * version, too. + */ + public final static String TUS_VERSION = "1.0.0"; + + private URL uploadCreationURL; + private boolean resumingEnabled; + private boolean removeFingerprintOnSuccessEnabled; + private TusURLStore urlStore; + private Map headers; + private int connectTimeout = 5000; + + /** + * Create a new tus client. + */ + public TusClient() { + + } + + /** + * Set the URL used for creating new uploads. This is required if you want to initiate new + * uploads using {@link #createUpload} or {@link #resumeOrCreateUpload} but is not used if you + * only resume existing uploads. + * + * @param uploadCreationURL Absolute upload creation URL + */ + public void setUploadCreationURL(URL uploadCreationURL) { + this.uploadCreationURL = uploadCreationURL; + } + + /** + * Get the current upload creation URL + * + * @return Current upload creation URL + */ + public URL getUploadCreationURL() { + return uploadCreationURL; + } + + /** + * Enable resuming already started uploads. This step is required if you want to use + * {@link #resumeUpload(TusUpload)}. + * + * @param urlStore Storage used to save and retrieve upload URLs by its fingerprint. + */ + public void enableResuming(@NotNull TusURLStore urlStore) { + resumingEnabled = true; + this.urlStore = urlStore; + } + + /** + * Disable resuming started uploads. + * + * @see #enableResuming(TusURLStore) + */ + public void disableResuming() { + resumingEnabled = false; + this.urlStore = null; + } + + /** + * Get the current status if resuming. + * + * @see #enableResuming(TusURLStore) + * @see #disableResuming() + * + * @return True if resuming has been enabled using {@link #enableResuming(TusURLStore)} + */ + public boolean resumingEnabled() { + return resumingEnabled; + } + + /** + * Enable removing fingerprints on success. + * + * @see #disableRemoveFingerprintOnSuccess() + */ + public void enableRemoveFingerprintOnSuccess() { + removeFingerprintOnSuccessEnabled = true; + } + + /** + * Disable removing fingerprints on success. + * + * @see #enableRemoveFingerprintOnSuccess() + */ + public void disableRemoveFingerprintOnSuccess() { + removeFingerprintOnSuccessEnabled = false; + } + + /** + * Get the current status if removing fingerprints on success. + * + * @see #enableRemoveFingerprintOnSuccess() + * @see #disableRemoveFingerprintOnSuccess() + * + * @return True if resuming has been enabled using {@link #enableResuming(TusURLStore)} + */ + public boolean removeFingerprintOnSuccessEnabled() { + return removeFingerprintOnSuccessEnabled; + } + + + /** + * Set headers which will be added to every HTTP requestes made by this TusClient instance. + * These may to overwrite tus-specific headers, which can be identified by their Tus-* + * prefix, and can cause unexpected behavior. + * + * @see #getHeaders() + * @see #prepareConnection(HttpURLConnection) + * + * @param headers The map of HTTP headers + */ + public void setHeaders(@Nullable Map headers) { + this.headers = headers; + } + + /** + * Get the HTTP headers which should be contained in every request and were configured using + * {@link #setHeaders(Map)}. + * + * @see #setHeaders(Map) + * @see #prepareConnection(HttpURLConnection) + * + * @return The map of configured HTTP headers + */ + @Nullable + public Map getHeaders() { + return headers; + } + + public void setConnectTimeout(int timeout) { + connectTimeout = timeout; + } + + public int getConnectTimeout() { + return connectTimeout; + } + + /** + * Create a new upload using the Creation extension. Before calling this function, an "upload + * creation URL" must be defined using {@link #setUploadCreationURL(URL)} or else this + * function will fail. + * In order to create the upload a POST request will be issued. The file's chunks must be + * uploaded manually using the returned {@link TusUploader} object. + * + * @param upload The file for which a new upload will be created + * @return Use {@link TusUploader} to upload the file's chunks. + * @throws ProtocolException Thrown if the remote server sent an unexpected response, e.g. + * wrong status codes or missing/invalid headers. + * @throws IOException Thrown if an exception occurs while issuing the HTTP request. + */ + public TusUploader createUpload(@NotNull TusUpload upload) throws ProtocolException, IOException { + HttpURLConnection connection = (HttpURLConnection) uploadCreationURL.openConnection(); + connection.setRequestMethod("POST"); + prepareConnection(connection); + + String encodedMetadata = upload.getEncodedMetadata(); + if(encodedMetadata.length() > 0) { + connection.setRequestProperty("Upload-Metadata", encodedMetadata); + } + + connection.addRequestProperty("Upload-Length", Long.toString(upload.getSize())); + connection.connect(); + + int responseCode = connection.getResponseCode(); + if(!(responseCode >= 200 && responseCode < 300)) { + throw new ProtocolException("unexpected status code (" + responseCode + ") while creating upload", connection); + } + + String urlStr = connection.getHeaderField("Location"); + if(urlStr == null || urlStr.length() == 0) { + throw new ProtocolException("missing upload URL in response for creating upload", connection); + } + + // The upload URL must be relative to the URL of the request by which is was returned, + // not the upload creation URL. In most cases, there is no difference between those two + // but there may be cases in which the POST request is redirected. + URL uploadURL = new URL(connection.getURL(), urlStr); + + if(resumingEnabled) { + urlStore.set(upload.getFingerprint(), uploadURL); + } + + return new TusUploader(this, upload, uploadURL, upload.getTusInputStream(), 0); + } + + /** + * Try to resume an already started upload. Before call this function, resuming must be + * enabled using {@link #enableResuming(TusURLStore)}. This method will look up the URL for this + * upload in the {@link TusURLStore} using the upload's fingerprint (see + * {@link TusUpload#getFingerprint()}). After a successful lookup a HEAD request will be issued + * to find the current offset without uploading the file, yet. + * + * @param upload The file for which an upload will be resumed + * @return Use {@link TusUploader} to upload the remaining file's chunks. + * @throws FingerprintNotFoundException Thrown if no matching fingerprint has been found in + * {@link TusURLStore}. Use {@link #createUpload(TusUpload)} to create a new upload. + * @throws ResumingNotEnabledException Throw if resuming has not been enabled using {@link + * #enableResuming(TusURLStore)}. + * @throws ProtocolException Thrown if the remote server sent an unexpected response, e.g. + * wrong status codes or missing/invalid headers. + * @throws IOException Thrown if an exception occurs while issuing the HTTP request. + */ + public TusUploader resumeUpload(@NotNull TusUpload upload) throws FingerprintNotFoundException, ResumingNotEnabledException, ProtocolException, IOException { + if (!resumingEnabled) { + throw new ResumingNotEnabledException(); + } + + URL uploadURL = urlStore.get(upload.getFingerprint()); + if (uploadURL == null) { + throw new FingerprintNotFoundException(upload.getFingerprint()); + } + + return beginOrResumeUploadFromURL(upload, uploadURL); + } + + /** + * Begin an upload or alternatively resume it if the upload has already been started before. In contrast to + * {@link #createUpload(TusUpload)} and {@link #resumeOrCreateUpload(TusUpload)} this method will not create a new + * upload. The user must obtain the upload location URL on their own as this method will not send the POST request + * which is normally used to create a new upload. + * Therefore, this method is only useful if you are uploading to a service which takes care of creating the tus + * upload for yourself. One example of such a service is the Vimeo API. + * When called a HEAD request will be issued to find the current offset without uploading the file, yet. + * The uploading can be started by using the returned {@link TusUploader} object. + * + * @param upload The file for which an upload will be resumed + * @param uploadURL The upload location URL at which has already been created and this file should be uploaded to. + * @return Use {@link TusUploader} to upload the remaining file's chunks. + * @throws ProtocolException Thrown if the remote server sent an unexpected response, e.g. + * wrong status codes or missing/invalid headers. + * @throws IOException Thrown if an exception occurs while issuing the HTTP request. + */ + public TusUploader beginOrResumeUploadFromURL(@NotNull TusUpload upload, @NotNull URL uploadURL) throws ProtocolException, IOException { + HttpURLConnection connection = (HttpURLConnection) uploadURL.openConnection(); + connection.setRequestMethod("HEAD"); + prepareConnection(connection); + + connection.connect(); + + int responseCode = connection.getResponseCode(); + if(!(responseCode >= 200 && responseCode < 300)) { + throw new ProtocolException("unexpected status code (" + responseCode + ") while resuming upload", connection); + } + + String offsetStr = connection.getHeaderField("Upload-Offset"); + if(offsetStr == null || offsetStr.length() == 0) { + throw new ProtocolException("missing upload offset in response for resuming upload", connection); + } + long offset = Long.parseLong(offsetStr); + + return new TusUploader(this, upload, uploadURL, upload.getTusInputStream(), offset); + } + + /** + * Try to resume an upload using {@link #resumeUpload(TusUpload)}. If the method call throws + * an {@link ResumingNotEnabledException} or {@link FingerprintNotFoundException}, a new upload + * will be created using {@link #createUpload(TusUpload)}. + * + * @param upload The file for which an upload will be resumed + * @throws ProtocolException Thrown if the remote server sent an unexpected response, e.g. + * wrong status codes or missing/invalid headers. + * @throws IOException Thrown if an exception occurs while issuing the HTTP request. + */ + public TusUploader resumeOrCreateUpload(@NotNull TusUpload upload) throws ProtocolException, IOException { + try { + return resumeUpload(upload); + } catch(FingerprintNotFoundException e) { + return createUpload(upload); + } catch(ResumingNotEnabledException e) { + return createUpload(upload); + } catch(ProtocolException e) { + // If the attempt to resume returned a 404 Not Found, we immediately try to create a new + // one since TusExectuor would not retry this operation. + HttpURLConnection connection = e.getCausingConnection(); + if(connection != null && connection.getResponseCode() == 404) { + return createUpload(upload); + } + + throw e; + } + } + + /** + * Set headers used for every HTTP request. Currently, this will add the Tus-Resumable header + * and any custom header which can be configured using {@link #setHeaders(Map)}, + * + * @param connection The connection whose headers will be modified. + */ + public void prepareConnection(@NotNull HttpURLConnection connection) { + // Only follow redirects, if the POST methods is preserved. If http.strictPostRedirect is + // disabled, a POST request will be transformed into a GET request which is not wanted by us. + // See: http://grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/7u40-b43/sun/net/www/protocol/http/HttpURLConnection.java#2372 + connection.setInstanceFollowRedirects(Boolean.getBoolean("http.strictPostRedirect")); + + connection.setConnectTimeout(connectTimeout); + connection.addRequestProperty("Tus-Resumable", TUS_VERSION); + + if(headers != null) { + for (Map.Entry entry : headers.entrySet()) { + connection.addRequestProperty(entry.getKey(), entry.getValue()); + } + } + } + + /** + * Actions to be performed after a successful upload completion. + * Manages URL removal from the URL store if remove fingerprint on success is enabled + * + * @param upload that has been finished + */ + protected void uploadFinished(@NotNull TusUpload upload) { + if (resumingEnabled && removeFingerprintOnSuccessEnabled) { + urlStore.remove(upload.getFingerprint()); + } + } +}