entry : headers.entrySet()) {
370 | connection.addRequestProperty(entry.getKey(), entry.getValue());
371 | }
372 | }
373 | }
374 |
375 | /**
376 | * Actions to be performed after a successful upload completion.
377 | * Manages URL removal from the URL store if remove fingerprint on success is enabled
378 | *
379 | * @param upload that has been finished
380 | */
381 | protected void uploadFinished(@NotNull TusUpload upload) {
382 | if (resumingEnabled && removeFingerprintOnSuccessEnabled) {
383 | urlStore.remove(upload.getFingerprint());
384 | }
385 | }
386 | }
387 |
--------------------------------------------------------------------------------
/src/main/java/io/tus/java/client/TusExecutor.java:
--------------------------------------------------------------------------------
1 | package io.tus.java.client;
2 |
3 | import java.io.IOException;
4 |
5 | /**
6 | * TusExecutor is a wrapper class which you can build around your uploading mechanism and any
7 | * exception thrown by it will be caught and may result in a retry. This way you can easily add
8 | * retrying functionality to your application with defined delays between them.
9 | *
10 | * This can be achieved by extending TusExecutor and implementing the abstract makeAttempt() method:
11 | *
12 | * {@code
13 | * TusExecutor executor = new TusExecutor() {
14 | * {@literal @}Override
15 | * protected void makeAttempt() throws ProtocolException, IOException {
16 | * TusUploader uploader = client.resumeOrCreateUpload(upload);
17 | * while(uploader.uploadChunk() > -1) {}
18 | * uploader.finish();
19 | * }
20 | * };
21 | * executor.makeAttempts();
22 | * }
23 | *
24 | *
25 | * The retries are basically just calling the {@link #makeAttempt()} method which should then
26 | * retrieve an {@link TusUploader} using {@link TusClient#resumeOrCreateUpload(TusUpload)} and then
27 | * invoke {@link TusUploader#uploadChunk()} as long as possible without catching
28 | * {@link ProtocolException}s or {@link IOException}s as this is taken over by this class.
29 | *
30 | * The current attempt can be interrupted using {@link Thread#interrupt()} which will cause the
31 | * {@link #makeAttempts()} method to return false
immediately.
32 | */
33 | public abstract class TusExecutor {
34 | private int[] delays = new int[]{500, 1000, 2000, 3000};
35 |
36 | /**
37 | * Set the delays at which TusExecutor will issue a retry if {@link #makeAttempt()} throws an
38 | * exception. If the methods call fails for the first time it will wait delays[0]
ms
39 | * before calling it again. If this second calls also does not return normally
40 | * delays[1]
ms will be waited on so on.
41 | * It total delays.length
retries may be issued, resulting in up to
42 | * delays.length + 1
calls to {@link #makeAttempt()}.
43 | * The default delays are set to 500ms, 1s, 2s and 3s.
44 | *
45 | * @see #getDelays()
46 | *
47 | * @param delays The desired delay values to be used
48 | */
49 | public void setDelays(int[] delays) {
50 | this.delays = delays;
51 | }
52 |
53 | /**
54 | * Get the delays which will be used for waiting before attempting retries.
55 | *
56 | * @see #setDelays(int[])
57 | *
58 | * @return The dalys previously set
59 | */
60 | public int[] getDelays() {
61 | return delays;
62 | }
63 |
64 | /**
65 | * This method is basically just calling the {@link #makeAttempt()} method which should then
66 | * retrieve an {@link TusUploader} using {@link TusClient#resumeOrCreateUpload(TusUpload)} and then
67 | * invoke {@link TusUploader#uploadChunk()} as long as possible without catching
68 | * {@link ProtocolException}s or {@link IOException}s as this is taken over by this class.
69 | *
70 | * The current attempt can be interrupted using {@link Thread#interrupt()} which will cause the
71 | * method to return false
immediately.
72 | *
73 | * @return true
if the {@link #makeAttempt()} method returned normally and
74 | * false
if the thread was interrupted while sleeping until the next attempt.
75 | *
76 | * @throws ProtocolException
77 | * @throws IOException
78 | */
79 | public boolean makeAttempts() throws ProtocolException, IOException {
80 | int attempt = -1;
81 | while (true) {
82 | attempt++;
83 |
84 | try {
85 | makeAttempt();
86 | // Returning true is the signal that the makeAttempt() function exited without
87 | // throwing an error.
88 | return true;
89 | } catch (ProtocolException e) {
90 | // Do not attempt a retry, if the Exception suggests so.
91 | if (!e.shouldRetry()) {
92 | throw e;
93 | }
94 |
95 | if (attempt >= delays.length) {
96 | // We exceeds the number of maximum retries. In this case the latest exception
97 | // is thrown.
98 | throw e;
99 | }
100 | } catch (IOException e) {
101 | if (attempt >= delays.length) {
102 | // We exceeds the number of maximum retries. In this case the latest exception
103 | // is thrown.
104 | throw e;
105 | }
106 | }
107 |
108 | try {
109 | // Sleep for the specified delay before attempting the next retry.
110 | Thread.sleep(delays[attempt]);
111 | } catch (InterruptedException e) {
112 | // If we get interrupted while waiting for the next retry, the user has cancelled
113 | // the upload willingly and we return false as a signal.
114 | return false;
115 | }
116 | }
117 | }
118 |
119 | /**
120 | * This method must be implemented by the specific caller. It will be invoked once or multiple
121 | * times by the {@link #makeAttempts()} method.
122 | * A proper implementation should retrieve an {@link TusUploader} using
123 | * {@link TusClient#resumeOrCreateUpload(TusUpload)} and then invoke
124 | * {@link TusUploader#uploadChunk()} as long as possible without catching
125 | * {@link ProtocolException}s or {@link IOException}s as this is taken over by this class.
126 | *
127 | * @throws ProtocolException
128 | * @throws IOException
129 | */
130 | protected abstract void makeAttempt() throws ProtocolException, IOException;
131 | }
132 |
--------------------------------------------------------------------------------
/src/main/java/io/tus/java/client/TusInputStream.java:
--------------------------------------------------------------------------------
1 | package io.tus.java.client;
2 |
3 | import java.io.BufferedInputStream;
4 | import java.io.IOException;
5 | import java.io.InputStream;
6 |
7 | /**
8 | * TusInputStream is an internal abstraction above an InputStream which allows seeking to a
9 | * position relative to the beginning of the stream. In comparision {@link InputStream#skip(long)}
10 | * only supports skipping bytes relative to the current position.
11 | */
12 | class TusInputStream {
13 | private InputStream stream;
14 | private long bytesRead;
15 | private long lastMark = -1;
16 |
17 | /**
18 | * Create a new TusInputStream which reads from and operates on the supplied stream.
19 | *
20 | * @param stream The stream to read from
21 | */
22 | TusInputStream(InputStream stream) {
23 | if (!stream.markSupported()) {
24 | stream = new BufferedInputStream(stream);
25 | }
26 |
27 | this.stream = stream;
28 | }
29 |
30 | /**
31 | * Read a specific amount of bytes from the stream and write them to the start of the supplied
32 | * buffer.
33 | * See {@link InputStream#read(byte[], int, int)} for more details.
34 | *
35 | * @param buffer The array to write the bytes to
36 | * @param length Number of bytes to read at most
37 | * @return Actual number of bytes read
38 | * @throws IOException
39 | */
40 | public int read(byte[] buffer, int length) throws IOException {
41 | int bytesReadNow = stream.read(buffer, 0, length);
42 | bytesRead += bytesReadNow;
43 | return bytesReadNow;
44 | }
45 |
46 | /**
47 | * Seek to the position relative to the start of the stream.
48 | *
49 | * @param position Absolute position to seek to
50 | * @throws IOException
51 | */
52 | public void seekTo(long position) throws IOException {
53 | if (lastMark != -1) {
54 | stream.reset();
55 | stream.skip(position - lastMark);
56 | lastMark = -1;
57 | } else {
58 | stream.skip(position);
59 | }
60 |
61 | bytesRead = position;
62 | }
63 |
64 | /**
65 | * Mark the current position to allow seeking to a position after this mark.
66 | * See {@link InputStream#mark(int)} for details.
67 | *
68 | * @param readLimit Number of bytes to read before this mark gets invalidated
69 | */
70 | public void mark(int readLimit) {
71 | lastMark = bytesRead;
72 | stream.mark(readLimit);
73 | }
74 |
75 | /**
76 | * Close the underlying instance of InputStream.
77 | *
78 | * @throws IOException
79 | */
80 | public void close() throws IOException {
81 | stream.close();
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/src/main/java/io/tus/java/client/TusURLMemoryStore.java:
--------------------------------------------------------------------------------
1 | package io.tus.java.client;
2 |
3 | import java.net.URL;
4 | import java.util.HashMap;
5 | import java.util.Map;
6 |
7 | /**
8 | * This class is used to map an upload's fingerprint with the corresponding upload URL by storing
9 | * the entries in a {@link HashMap}. This functionality is used to allow resuming uploads. The
10 | * fingerprint is usually retrieved using {@link TusUpload#getFingerprint()}.
11 | *
12 | * The values will only be stored as long as the application is running. This store will not
13 | * keep the values after your application crashes or restarts.
14 | */
15 | public class TusURLMemoryStore implements TusURLStore {
16 | private Map store = new HashMap();
17 |
18 | /**
19 | * Stores the upload's fingerprint and url.
20 | * @param fingerprint An upload's fingerprint.
21 | * @param url The corresponding upload URL.
22 | */
23 | @Override
24 | public void set(String fingerprint, URL url) {
25 | store.put(fingerprint, url);
26 | }
27 |
28 | /**
29 | * Returns the corresponding Upload URL to a given fingerprint.
30 | * @param fingerprint An upload's fingerprint.
31 | * @return The corresponding upload URL.
32 | */
33 | @Override
34 | public URL get(String fingerprint) {
35 | return store.get(fingerprint);
36 | }
37 |
38 | /**
39 | * Removes the corresponding entry to a fingerprint from the {@link TusURLMemoryStore}.
40 | * @param fingerprint An upload's fingerprint.
41 | */
42 | @Override
43 | public void remove(String fingerprint) {
44 | store.remove(fingerprint);
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/main/java/io/tus/java/client/TusURLStore.java:
--------------------------------------------------------------------------------
1 | package io.tus.java.client;
2 |
3 | import java.net.URL;
4 |
5 | /**
6 | * Implementations of this interface are used to map an upload's fingerprint with the corresponding
7 | * upload URL. This functionality is used to allow resuming uploads. The fingerprint is usually
8 | * retrieved using {@link TusUpload#getFingerprint()}.
9 | */
10 | public interface TusURLStore {
11 | /**
12 | * Store a new fingerprint and its upload URL.
13 | *
14 | * @param fingerprint An upload's fingerprint.
15 | * @param url The corresponding upload URL.
16 | */
17 | void set(String fingerprint, URL url);
18 |
19 | /**
20 | * Retrieve an upload's URL for a fingerprint. If no matching entry is found this method will
21 | * return null
.
22 | *
23 | * @param fingerprint An upload's fingerprint.
24 | * @return The corresponding upload URL.
25 | */
26 | URL get(String fingerprint);
27 |
28 | /**
29 | * Remove an entry from the store. Calling {@link #get(String)} with the same fingerprint will
30 | * return null
. If no entry exists for this fingerprint no exception should be
31 | * thrown.
32 | *
33 | * @param fingerprint An upload's fingerprint.
34 | */
35 | void remove(String fingerprint);
36 | }
37 |
--------------------------------------------------------------------------------
/src/main/java/io/tus/java/client/TusUpload.java:
--------------------------------------------------------------------------------
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.FileNotFoundException;
8 | import java.io.InputStream;
9 | import java.util.HashMap;
10 | import java.util.Map;
11 |
12 | /**
13 | * This class contains information about a file which will be uploaded later. Uploading is not
14 | * done using this class but using {@link TusUploader} whose instances are returned by
15 | * {@link TusClient#createUpload(TusUpload)}, {@link TusClient#createUpload(TusUpload)} and
16 | * {@link TusClient#resumeOrCreateUpload(TusUpload)}.
17 | */
18 | public class TusUpload {
19 | private long size;
20 | private InputStream input;
21 | private TusInputStream tusInputStream;
22 | private String fingerprint;
23 | private Map metadata;
24 |
25 | /**
26 | * Create a new TusUpload object.
27 | */
28 | public TusUpload() {
29 | }
30 |
31 | /**
32 | * Create a new TusUpload object using the supplied file object. The corresponding {@link
33 | * InputStream}, size and fingerprint will be automatically set.
34 | *
35 | * @param file The file whose content should be later uploaded.
36 | * @throws FileNotFoundException Thrown if the file cannot be found.
37 | */
38 | public TusUpload(@NotNull File file) throws FileNotFoundException {
39 | size = file.length();
40 | setInputStream(new FileInputStream(file));
41 |
42 | fingerprint = String.format("%s-%d", file.getAbsolutePath(), size);
43 |
44 | metadata = new HashMap();
45 | metadata.put("filename", file.getName());
46 | }
47 |
48 | /**
49 | * Returns the file size of the upload.
50 | * @return File size in bytes
51 | */
52 | public long getSize() {
53 | return size;
54 | }
55 |
56 | /**
57 | * Set the file's size in bytes.
58 | *
59 | * @param size File's size in bytes.
60 | */
61 | public void setSize(long size) {
62 | this.size = size;
63 | }
64 |
65 | /**
66 | * Returns the file specific fingerprint.
67 | * @return Fingerprint as String.
68 | */
69 | public String getFingerprint() {
70 | return fingerprint;
71 | }
72 |
73 | /**
74 | * Sets a fingerprint for this upload. This fingerprint must be unique and file specific, because it is used
75 | * for upload identification.
76 | * @param fingerprint String of fingerprint information.
77 | */
78 | public void setFingerprint(String fingerprint) {
79 | this.fingerprint = fingerprint;
80 | }
81 |
82 | /**
83 | * Returns the input stream of the file to upload.
84 | * @return {@link InputStream}
85 | */
86 | public InputStream getInputStream() {
87 | return input;
88 | }
89 |
90 | /**
91 | * This method returns the {@link TusInputStream}, which was derived from the file's {@link InputStream}.
92 | * @return {@link TusInputStream}
93 | */
94 | TusInputStream getTusInputStream() {
95 | return tusInputStream;
96 | }
97 |
98 | /**
99 | * Set the source from which will be read if the file will be later uploaded.
100 | *
101 | * @param inputStream The stream which will be read.
102 | */
103 | public void setInputStream(InputStream inputStream) {
104 | input = inputStream;
105 | tusInputStream = new TusInputStream(inputStream);
106 | }
107 |
108 | /**
109 | * This methods allows it to send Metadata alongside with the upload. The Metadata must be provided as
110 | * a Map with Key - Value pairs of Type String.
111 | * @param metadata Key-value pairs of Type String
112 | */
113 | public void setMetadata(Map metadata) {
114 | this.metadata = metadata;
115 | }
116 |
117 | /**
118 | * This method returns the upload's metadata as Map.
119 | * @return {@link Map} of metadata Key - Value pairs.
120 | */
121 | public Map getMetadata() {
122 | return metadata;
123 | }
124 |
125 | /**
126 | * Encode the metadata into a string according to the specification, so it can be
127 | * used as the value for the Upload-Metadata header.
128 | *
129 | * @return Encoded metadata
130 | */
131 | public String getEncodedMetadata() {
132 | if (metadata == null || metadata.size() == 0) {
133 | return "";
134 | }
135 |
136 | String encoded = "";
137 |
138 | boolean firstElement = true;
139 | for (Map.Entry entry : metadata.entrySet()) {
140 | if (!firstElement) {
141 | encoded += ",";
142 | }
143 | encoded += entry.getKey() + " " + base64Encode(entry.getValue().getBytes());
144 |
145 | firstElement = false;
146 | }
147 |
148 | return encoded;
149 | }
150 |
151 | /**
152 | * Encode a byte-array using Base64. This is a sligtly modified version from an implementation
153 | * published on Wikipedia (https://en.wikipedia.org/wiki/Base64#Sample_Implementation_in_Java)
154 | * under the Creative Commons Attribution-ShareAlike License.
155 | * @param in input Byte array for Base64 encoding.
156 | * @return Base64 encoded String derived from input Bytes.
157 | */
158 | static String base64Encode(byte[] in) {
159 | StringBuilder out = new StringBuilder((in.length * 4) / 3);
160 | String codes = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
161 |
162 | int b;
163 | for (int i = 0; i < in.length; i += 3) {
164 | b = (in[i] & 0xFC) >> 2;
165 | out.append(codes.charAt(b));
166 | b = (in[i] & 0x03) << 4;
167 | if (i + 1 < in.length) {
168 | b |= (in[i + 1] & 0xF0) >> 4;
169 | out.append(codes.charAt(b));
170 | b = (in[i + 1] & 0x0F) << 2;
171 | if (i + 2 < in.length) {
172 | b |= (in[i + 2] & 0xC0) >> 6;
173 | out.append(codes.charAt(b));
174 | b = in[i + 2] & 0x3F;
175 | out.append(codes.charAt(b));
176 | } else {
177 | out.append(codes.charAt(b));
178 | out.append('=');
179 | }
180 | } else {
181 | out.append(codes.charAt(b));
182 | out.append("==");
183 | }
184 | }
185 |
186 | return out.toString();
187 | }
188 | }
189 |
--------------------------------------------------------------------------------
/src/main/java/io/tus/java/client/TusUploader.java:
--------------------------------------------------------------------------------
1 | package io.tus.java.client;
2 |
3 | import java.io.IOException;
4 | import java.io.OutputStream;
5 | import java.net.HttpURLConnection;
6 | import java.net.Proxy;
7 | import java.net.URL;
8 | import java.net.URLConnection;
9 |
10 | /**
11 | * This class is used for doing the actual upload of the files. Instances are returned by
12 | * {@link TusClient#createUpload(TusUpload)}, {@link TusClient#createUpload(TusUpload)} and
13 | * {@link TusClient#resumeOrCreateUpload(TusUpload)}.
14 | *
15 | * After obtaining an instance you can upload a file by following these steps:
16 | *
17 | * - Upload a chunk using {@link #uploadChunk()}
18 | * - Optionally get the new offset ({@link #getOffset()} to calculate the progress
19 | * - Repeat step 1 until the {@link #uploadChunk()} returns -1
20 | * - Close HTTP connection and InputStream using {@link #finish()} to free resources
21 | *
22 | */
23 | public class TusUploader {
24 | private URL uploadURL;
25 | private Proxy proxy;
26 | private TusInputStream input;
27 | private long offset;
28 | private TusClient client;
29 | private TusUpload upload;
30 | private byte[] buffer;
31 | private int requestPayloadSize = 10 * 1024 * 1024;
32 | private int bytesRemainingForRequest;
33 |
34 | private HttpURLConnection connection;
35 | private OutputStream output;
36 |
37 | /**
38 | * Begin a new upload request by opening a PATCH request to specified upload URL. After this
39 | * method returns a connection will be ready and you can upload chunks of the file.
40 | *
41 | * @param client Used for preparing a request ({@link TusClient#prepareConnection(HttpURLConnection)}
42 | * @param upload {@link TusUpload} to be uploaded.
43 | * @param uploadURL URL to send the request to
44 | * @param input Stream to read (and seek) from and upload to the remote server
45 | * @param offset Offset to read from
46 | * @throws IOException Thrown if an exception occurs while issuing the HTTP request.
47 | */
48 | public TusUploader(TusClient client, TusUpload upload, URL uploadURL, TusInputStream input, long offset)
49 | throws IOException {
50 | this.uploadURL = uploadURL;
51 | this.input = input;
52 | this.offset = offset;
53 | this.client = client;
54 | this.upload = upload;
55 |
56 | input.seekTo(offset);
57 |
58 | setChunkSize(2 * 1024 * 1024);
59 | }
60 |
61 | private void openConnection() throws IOException, ProtocolException {
62 | // Only open a connection, if we have none open.
63 | if (connection != null) {
64 | return;
65 | }
66 |
67 | bytesRemainingForRequest = requestPayloadSize;
68 | input.mark(requestPayloadSize);
69 |
70 | if (proxy != null) {
71 | connection = (HttpURLConnection) uploadURL.openConnection(proxy);
72 | } else {
73 | connection = (HttpURLConnection) uploadURL.openConnection();
74 | }
75 | client.prepareConnection(connection);
76 | connection.setRequestProperty("Upload-Offset", Long.toString(offset));
77 | connection.setRequestProperty("Content-Type", "application/offset+octet-stream");
78 | connection.setRequestProperty("Expect", "100-continue");
79 |
80 | try {
81 | connection.setRequestMethod("PATCH");
82 | // Check whether we are running on a buggy JRE
83 | } catch (java.net.ProtocolException pe) {
84 | connection.setRequestMethod("POST");
85 | connection.setRequestProperty("X-HTTP-Method-Override", "PATCH");
86 | }
87 |
88 | connection.setDoOutput(true);
89 | connection.setChunkedStreamingMode(0);
90 | try {
91 | output = connection.getOutputStream();
92 | } catch (java.net.ProtocolException pe) {
93 | // If we already have a response code available, our expectation using the "Expect: 100-
94 | // continue" header failed and we should handle this response.
95 | if (connection.getResponseCode() != -1) {
96 | finish();
97 | }
98 |
99 | throw pe;
100 | }
101 | }
102 |
103 | /**
104 | * Sets the used chunk size. This number is used by {@link #uploadChunk()} to indicate how
105 | * much data is uploaded in a single take. When choosing a value for this parameter you need to
106 | * consider that uploadChunk() will only return once the specified number of bytes has been
107 | * sent. For slow internet connections this may take a long time. In addition, a buffer with
108 | * the chunk size is allocated and kept in memory.
109 | *
110 | * @param size The new chunk size
111 | */
112 | public void setChunkSize(int size) {
113 | buffer = new byte[size];
114 | }
115 |
116 | /**
117 | * Returns the current chunk size set using {@link #setChunkSize(int)}.
118 | *
119 | * @return Current chunk size
120 | */
121 | public int getChunkSize() {
122 | return buffer.length;
123 | }
124 |
125 | /**
126 | * Set the maximum payload size for a single request counted in bytes. This is useful for splitting
127 | * bigger uploads into multiple requests. For example, if you have a resource of 2MB and
128 | * the payload size set to 1MB, the upload will be transferred by two requests of 1MB each.
129 | *
130 | * The default value for this setting is 10 * 1024 * 1024 bytes (10 MiB).
131 | *
132 | * Be aware that setting a low maximum payload size (in the low megabytes or even less range) will result in
133 | * decreased performance since more requests need to be used for an upload. Each request will come with its overhead
134 | * in terms of longer upload times.
135 | *
136 | * Be aware that setting a high maximum payload size may result in a high memory usage since
137 | * tus-java-client usually allocates a buffer with the maximum payload size (this buffer is used
138 | * to allow retransmission of lost data if necessary). If the client is running on a memory-
139 | * constrained device (e.g. mobile app) and the maximum payload size is too high, it might
140 | * result in an {@link OutOfMemoryError}.
141 | *
142 | * This method must not be called when the uploader has currently an open connection to the
143 | * remote server. In general, try to set the payload size before invoking {@link #uploadChunk()}
144 | * the first time.
145 | *
146 | * @see #getRequestPayloadSize()
147 | *
148 | * @param size Number of bytes for a single payload
149 | * @throws IllegalStateException Thrown if the uploader currently has a connection open
150 | */
151 | public void setRequestPayloadSize(int size) throws IllegalStateException {
152 | if (connection != null) {
153 | throw new IllegalStateException("payload size for a single request must not be "
154 | + "modified as long as a request is in progress");
155 | }
156 |
157 | requestPayloadSize = size;
158 | }
159 |
160 | /**
161 | * Get the current maximum payload size for a single request.
162 | *
163 | * @see #setChunkSize(int)
164 | *
165 | * @return Number of bytes for a single payload
166 | */
167 | public int getRequestPayloadSize() {
168 | return requestPayloadSize;
169 | }
170 |
171 | /**
172 | * Upload a part of the file by reading a chunk from the InputStream and writing
173 | * it to the HTTP request's body. If the number of available bytes is lower than the chunk's
174 | * size, all available bytes will be uploaded and nothing more.
175 | * No new connection will be established when calling this method, instead the connection opened
176 | * in the previous calls will be used.
177 | * The size of the read chunk can be obtained using {@link #getChunkSize()} and changed
178 | * using {@link #setChunkSize(int)}.
179 | * In order to obtain the new offset, use {@link #getOffset()} after this method returns.
180 | *
181 | * @return Number of bytes read and written.
182 | * @throws IOException Thrown if an exception occurs while reading from the source or writing
183 | * to the HTTP request.
184 | */
185 | public int uploadChunk() throws IOException, ProtocolException {
186 | openConnection();
187 |
188 | int bytesToRead = Math.min(getChunkSize(), bytesRemainingForRequest);
189 |
190 | int bytesRead = input.read(buffer, bytesToRead);
191 | if (bytesRead == -1) {
192 | // No bytes were read since the input stream is empty
193 | return -1;
194 | }
195 |
196 | // Do not write the entire buffer to the stream since the array will
197 | // be filled up with 0x00s if the number of read bytes is lower then
198 | // the chunk's size.
199 | output.write(buffer, 0, bytesRead);
200 | output.flush();
201 |
202 | offset += bytesRead;
203 | bytesRemainingForRequest -= bytesRead;
204 |
205 | if (bytesRemainingForRequest <= 0) {
206 | finishConnection();
207 | }
208 |
209 | return bytesRead;
210 | }
211 |
212 | /**
213 | * Upload a part of the file by read a chunks specified size from the InputStream and writing
214 | * it to the HTTP request's body. If the number of available bytes is lower than the chunk's
215 | * size, all available bytes will be uploaded and nothing more.
216 | * No new connection will be established when calling this method, instead the connection opened
217 | * in the previous calls will be used.
218 | * In order to obtain the new offset, use {@link #getOffset()} after this method returns.
219 | *
220 | * This method ignored the payload size per request, which may be set using
221 | * {@link #setRequestPayloadSize(int)}. Please, use {@link #uploadChunk()} instead.
222 | *
223 | * @deprecated This method is inefficient and has been replaced by {@link #setChunkSize(int)}
224 | * and {@link #uploadChunk()} and should not be used anymore. The reason is, that
225 | * this method allocates a new buffer with the supplied chunk size for each time
226 | * it's called without reusing it. This results in a high number of memory
227 | * allocations and should be avoided. The new methods do not have this issue.
228 | *
229 | * @param chunkSize Maximum number of bytes which will be uploaded. When choosing a value
230 | * for this parameter you need to consider that the method call will only
231 | * return once the specified number of bytes have been sent. For slow
232 | * internet connections this may take a long time.
233 | * @return Number of bytes read and written.
234 | * @throws IOException Thrown if an exception occurs while reading from the source or writing
235 | * to the HTTP request.
236 | */
237 | @Deprecated public int uploadChunk(int chunkSize) throws IOException, ProtocolException {
238 | openConnection();
239 |
240 | byte[] buf = new byte[chunkSize];
241 | int bytesRead = input.read(buf, chunkSize);
242 | if (bytesRead == -1) {
243 | // No bytes were read since the input stream is empty
244 | return -1;
245 | }
246 |
247 | // Do not write the entire buffer to the stream since the array will
248 | // be filled up with 0x00s if the number of read bytes is lower then
249 | // the chunk's size.
250 | output.write(buf, 0, bytesRead);
251 | output.flush();
252 |
253 | offset += bytesRead;
254 |
255 | return bytesRead;
256 | }
257 |
258 | /**
259 | * Get the current offset for the upload. This is the number of all bytes uploaded in total and
260 | * in all requests (not only this one). You can use it in conjunction with
261 | * {@link TusUpload#getSize()} to calculate the progress.
262 | *
263 | * @return The upload's current offset.
264 | */
265 | public long getOffset() {
266 | return offset;
267 | }
268 |
269 | /**
270 | * This methods returns the destination {@link URL} of the upload.
271 | * @return The {@link URL} of the upload.
272 | */
273 | public URL getUploadURL() {
274 | return uploadURL;
275 | }
276 |
277 | /**
278 | * Set the proxy that will be used when uploading.
279 | *
280 | * @param proxy Proxy to use
281 | */
282 | public void setProxy(Proxy proxy) {
283 | this.proxy = proxy;
284 | }
285 |
286 | /**
287 | * This methods returns the proxy used when uploading.
288 | *
289 | * @return The {@link Proxy} used for the upload or null when not set.
290 | */
291 | public Proxy getProxy() {
292 | return proxy;
293 | }
294 |
295 | /**
296 | * Finish the request by closing the HTTP connection and the InputStream.
297 | * You can call this method even before the entire file has been uploaded. Use this behavior to
298 | * enable pausing uploads.
299 | * This method is equivalent to calling {@code finish(false)}.
300 | *
301 | * @throws ProtocolException Thrown if the server sends an unexpected status
302 | * code
303 | * @throws IOException Thrown if an exception occurs while cleaning up.
304 | */
305 | public void finish() throws ProtocolException, IOException {
306 | finish(true);
307 | }
308 |
309 | /**
310 | * Finish the request by closing the HTTP connection. You can choose whether to close the InputStream or not.
311 | * You can call this method even before the entire file has been uploaded. Use this behavior to
312 | * enable pausing uploads.
313 | * Be aware that it doesn't automatically release local resources if {@code closeStream == false} and you do
314 | * not close the InputStream on your own. To be safe use {@link TusUploader#finish()}.
315 | * @param closeInputStream Determines whether the InputStream is closed with the HTTP connection. Not closing the
316 | * Input Stream may be useful for future upload a future continuation of the upload.
317 | * @throws ProtocolException Thrown if the server sends an unexpected status code
318 | * @throws IOException Thrown if an exception occurs while cleaning up.
319 | */
320 | public void finish(boolean closeInputStream) throws ProtocolException, IOException {
321 | finishConnection();
322 | if (upload.getSize() == offset) {
323 | client.uploadFinished(upload);
324 | }
325 |
326 | // Close the TusInputStream after checking the response and closing the connection to ensure
327 | // that we will not need to read from it again in the future.
328 | if (closeInputStream) {
329 | input.close();
330 | }
331 | }
332 |
333 | private void finishConnection() throws ProtocolException, IOException {
334 | if (output != null) {
335 | output.close();
336 | }
337 |
338 | if (connection != null) {
339 | int responseCode = connection.getResponseCode();
340 | connection.disconnect();
341 |
342 | if (!(responseCode >= 200 && responseCode < 300)) {
343 | throw new ProtocolException("unexpected status code (" + responseCode + ") while uploading chunk",
344 | connection);
345 | }
346 |
347 | // TODO detect changes and seek accordingly
348 | long serverOffset = getHeaderFieldLong(connection, "Upload-Offset");
349 | if (serverOffset == -1) {
350 | throw new ProtocolException("response to PATCH request contains no or invalid Upload-Offset header",
351 | connection);
352 | }
353 | if (offset != serverOffset) {
354 | throw new ProtocolException(
355 | String.format("response contains different Upload-Offset value (%d) than expected (%d)",
356 | serverOffset,
357 | offset),
358 | connection);
359 | }
360 |
361 | connection = null;
362 | }
363 | }
364 |
365 | private long getHeaderFieldLong(URLConnection connection, String field) {
366 | String value = connection.getHeaderField(field);
367 | if (value == null) {
368 | return -1;
369 | }
370 |
371 | try {
372 | return Long.parseLong(value);
373 | } catch (NumberFormatException e) {
374 | return -1;
375 | }
376 | }
377 | }
378 |
--------------------------------------------------------------------------------
/src/main/java/io/tus/java/client/package-info.java:
--------------------------------------------------------------------------------
1 | /**
2 | * This package provides all necessary classes for the TUS - Upload Client.
3 | */
4 | package io.tus.java.client;
5 |
--------------------------------------------------------------------------------
/src/main/resources/tus-java-client-version/version.properties:
--------------------------------------------------------------------------------
1 | versionNumber='0.5.1'
2 |
--------------------------------------------------------------------------------
/src/test/java/io/tus/java/client/MockServerProvider.java:
--------------------------------------------------------------------------------
1 | package io.tus.java.client;
2 |
3 | import org.junit.After;
4 | import org.junit.Before;
5 | import org.mockserver.client.MockServerClient;
6 | import org.mockserver.socket.PortFactory;
7 |
8 | import java.net.URL;
9 |
10 | import static org.mockserver.integration.ClientAndServer.startClientAndServer;
11 |
12 | /**
13 | * This class provides a MockServer.
14 | */
15 | public class MockServerProvider {
16 | protected MockServerClient mockServer;
17 | protected URL mockServerURL;
18 | protected URL creationUrl;
19 |
20 | /**
21 | * Test configuration before running.
22 | * @throws Exception
23 | */
24 | @Before
25 | public void setUp() throws Exception {
26 | creationUrl = new URL("http://tusd.tusdemo.net");
27 | int port = PortFactory.findFreePort();
28 | mockServerURL = new URL("http://localhost:" + port + "/files");
29 | mockServer = startClientAndServer(port);
30 | }
31 |
32 | /**
33 | * Clean up after finishing the test-run.
34 | */
35 | @After
36 | public void tearDown() {
37 | mockServer.stop();
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/test/java/io/tus/java/client/TestTusClient.java:
--------------------------------------------------------------------------------
1 | package io.tus.java.client;
2 |
3 | import java.io.ByteArrayInputStream;
4 | import java.io.IOException;
5 | import java.net.HttpURLConnection;
6 | import java.net.InetSocketAddress;
7 | import java.net.MalformedURLException;
8 | import java.net.Proxy;
9 | import java.net.Proxy.Type;
10 | import java.net.URL;
11 | import java.util.HashMap;
12 | import java.util.LinkedHashMap;
13 | import java.util.Map;
14 |
15 | import org.junit.Test;
16 | import org.mockserver.model.HttpRequest;
17 | import org.mockserver.model.HttpResponse;
18 |
19 | import static org.junit.Assert.assertEquals;
20 | import static org.junit.Assert.assertFalse;
21 | import static org.junit.Assert.assertNull;
22 | import static org.junit.Assert.assertTrue;
23 | import static org.junit.Assert.fail;
24 |
25 | /**
26 | * Class to test the tus-Client.
27 | */
28 | public class TestTusClient extends MockServerProvider {
29 |
30 | /**
31 | * Tests if the client object is set up correctly.
32 | */
33 | @Test
34 | public void testTusClient() {
35 | TusClient client = new TusClient();
36 | assertNull(client.getUploadCreationURL());
37 | }
38 |
39 | /**
40 | * Checks if upload URLS are set correctly.
41 | * @throws MalformedURLException if the provided URL is malformed.
42 | */
43 | @Test
44 | public void testTusClientURL() throws MalformedURLException {
45 | TusClient client = new TusClient();
46 | client.setUploadCreationURL(creationUrl);
47 | assertEquals(client.getUploadCreationURL(), creationUrl);
48 | }
49 |
50 | /**
51 | * Checks if upload URLS are set correctly.
52 | * @throws MalformedURLException if the provided URL is malformed.
53 | */
54 | @Test
55 | public void testSetUploadCreationURL() throws MalformedURLException {
56 | TusClient client = new TusClient();
57 | client.setUploadCreationURL(new URL("http://tusd.tusdemo.net"));
58 | assertEquals(client.getUploadCreationURL(), new URL("http://tusd.tusdemo.net"));
59 | }
60 |
61 | /**
62 | * Tests if resumable uploads can be turned off and on.
63 | */
64 | @Test
65 | public void testEnableResuming() {
66 | TusClient client = new TusClient();
67 | assertFalse(client.resumingEnabled());
68 |
69 | TusURLStore store = new TusURLMemoryStore();
70 | client.enableResuming(store);
71 | assertTrue(client.resumingEnabled());
72 |
73 | client.disableResuming();
74 | assertFalse(client.resumingEnabled());
75 | }
76 |
77 | /**
78 | * Verifies if uploads can be created with the tus client.
79 | * @throws IOException if upload data cannot be read.
80 | * @throws ProtocolException if the upload cannot be constructed.
81 | */
82 | @Test
83 | public void testCreateUpload() throws IOException, ProtocolException {
84 | mockServer.when(new HttpRequest()
85 | .withMethod("POST")
86 | .withPath("/files")
87 | .withHeader("Connection", "keep-alive")
88 | .withHeader("Tus-Resumable", TusClient.TUS_VERSION)
89 | .withHeader("Upload-Metadata", "foo aGVsbG8=,bar d29ybGQ=")
90 | .withHeader("Upload-Length", "10"))
91 | .respond(new HttpResponse()
92 | .withStatusCode(201)
93 | .withHeader("Tus-Resumable", TusClient.TUS_VERSION)
94 | .withHeader("Location", mockServerURL + "/foo"));
95 |
96 | Map metadata = new LinkedHashMap();
97 | metadata.put("foo", "hello");
98 | metadata.put("bar", "world");
99 |
100 | TusClient client = new TusClient();
101 | client.setUploadCreationURL(mockServerURL);
102 | TusUpload upload = new TusUpload();
103 | upload.setSize(10);
104 | upload.setInputStream(new ByteArrayInputStream(new byte[10]));
105 | upload.setMetadata(metadata);
106 | TusUploader uploader = client.createUpload(upload);
107 |
108 | assertEquals(uploader.getUploadURL(), new URL(mockServerURL + "/foo"));
109 | }
110 | /**
111 | * Verifies if uploads can be created with the tus client through a proxy.
112 | * @throws IOException if upload data cannot be read.
113 | * @throws ProtocolException if the upload cannot be constructed.
114 | */
115 | @Test
116 | public void testCreateUploadWithProxy() throws IOException, ProtocolException {
117 | mockServer.when(new HttpRequest()
118 | .withMethod("POST")
119 | .withPath("/files")
120 | .withHeader("Proxy-Connection", "keep-alive")
121 | .withHeader("Tus-Resumable", TusClient.TUS_VERSION)
122 | .withHeader("Upload-Metadata", "foo aGVsbG8=,bar d29ybGQ=")
123 | .withHeader("Upload-Length", "11"))
124 | .respond(new HttpResponse()
125 | .withStatusCode(201)
126 | .withHeader("Tus-Resumable", TusClient.TUS_VERSION)
127 | .withHeader("Location", mockServerURL + "/foo"));
128 |
129 | Map metadata = new LinkedHashMap();
130 | metadata.put("foo", "hello");
131 | metadata.put("bar", "world");
132 |
133 | TusClient client = new TusClient();
134 | client.setUploadCreationURL(mockServerURL);
135 | client.setProxy(new Proxy(Type.HTTP, new InetSocketAddress("localhost", mockServer.getPort())));
136 | TusUpload upload = new TusUpload();
137 | upload.setSize(11);
138 | upload.setInputStream(new ByteArrayInputStream(new byte[11]));
139 | upload.setMetadata(metadata);
140 | TusUploader uploader = client.createUpload(upload);
141 |
142 | assertEquals(uploader.getUploadURL(), new URL(mockServerURL + "/foo"));
143 | }
144 |
145 | /**
146 | * Tests if a missing location header causes an exception as expected.
147 | * @throws Exception if unreachable code has been reached.
148 | */
149 | @Test
150 | public void testCreateUploadWithMissingLocationHeader() throws Exception {
151 | mockServer.when(new HttpRequest()
152 | .withMethod("POST")
153 | .withPath("/files")
154 | .withHeader("Tus-Resumable", TusClient.TUS_VERSION)
155 | .withHeader("Upload-Length", "10"))
156 | .respond(new HttpResponse()
157 | .withStatusCode(201)
158 | .withHeader("Tus-Resumable", TusClient.TUS_VERSION));
159 |
160 | TusClient client = new TusClient();
161 | client.setUploadCreationURL(mockServerURL);
162 | TusUpload upload = new TusUpload();
163 | upload.setSize(10);
164 | upload.setInputStream(new ByteArrayInputStream(new byte[10]));
165 | try {
166 | TusUploader uploader = client.createUpload(upload);
167 | throw new Exception("unreachable code reached");
168 | } catch (ProtocolException e) {
169 | assertEquals(e.getMessage(), "missing upload URL in response for creating upload");
170 | }
171 | }
172 |
173 | /**
174 | * Tests if uploads with relative upload destinations are working.
175 | * @throws Exception
176 | */
177 | @Test
178 | public void testCreateUploadWithRelativeLocation() throws Exception {
179 | // We need to enable strict following for POST requests first
180 | System.setProperty("http.strictPostRedirect", "true");
181 |
182 | // Attempt a real redirect
183 | mockServer.when(new HttpRequest()
184 | .withMethod("POST")
185 | .withPath("/filesRedirect")
186 | .withHeader("Tus-Resumable", TusClient.TUS_VERSION)
187 | .withHeader("Upload-Length", "10"))
188 | .respond(new HttpResponse()
189 | .withStatusCode(301)
190 | .withHeader("Location", mockServerURL + "Redirected/"));
191 |
192 | mockServer.when(new HttpRequest()
193 | .withMethod("POST")
194 | .withPath("/filesRedirected/")
195 | .withHeader("Tus-Resumable", TusClient.TUS_VERSION)
196 | .withHeader("Upload-Length", "10"))
197 | .respond(new HttpResponse()
198 | .withStatusCode(201)
199 | .withHeader("Tus-Resumable", TusClient.TUS_VERSION)
200 | .withHeader("Location", "foo"));
201 |
202 | TusClient client = new TusClient();
203 | client.setUploadCreationURL(new URL(mockServerURL + "Redirect"));
204 | TusUpload upload = new TusUpload();
205 | upload.setSize(10);
206 | upload.setInputStream(new ByteArrayInputStream(new byte[10]));
207 | TusUploader uploader = client.createUpload(upload);
208 |
209 | // The upload URL must be relative to the URL of the request by which it was returned,
210 | // not the upload creation URL. In most cases, there is no difference between those two,
211 | // but it's still important to be correct here.
212 | assertEquals(uploader.getUploadURL(), new URL(mockServerURL + "Redirected/foo"));
213 | }
214 |
215 | /**
216 | * Tests if {@link TusClient#resumeUpload(TusUpload)} works.
217 | * @throws ResumingNotEnabledException
218 | * @throws FingerprintNotFoundException
219 | * @throws IOException
220 | * @throws ProtocolException
221 | */
222 | @Test
223 | public void testResumeUpload() throws ResumingNotEnabledException, FingerprintNotFoundException, IOException,
224 | ProtocolException {
225 | mockServer.when(new HttpRequest()
226 | .withMethod("HEAD")
227 | .withPath("/files/foo")
228 | .withHeader("Tus-Resumable", TusClient.TUS_VERSION))
229 | .respond(new HttpResponse()
230 | .withStatusCode(204)
231 | .withHeader("Tus-Resumable", TusClient.TUS_VERSION)
232 | .withHeader("Upload-Offset", "3"));
233 |
234 | TusClient client = new TusClient();
235 | client.setUploadCreationURL(mockServerURL);
236 | client.enableResuming(new TestResumeUploadStore());
237 |
238 | TusUpload upload = new TusUpload();
239 | upload.setSize(10);
240 | upload.setInputStream(new ByteArrayInputStream(new byte[10]));
241 | upload.setFingerprint("test-fingerprint");
242 |
243 | TusUploader uploader = client.resumeUpload(upload);
244 |
245 | assertEquals(uploader.getUploadURL(), new URL(mockServerURL.toString() + "/foo"));
246 | assertEquals(uploader.getOffset(), 3);
247 | }
248 |
249 | /**
250 | * Test Implementation for a {@link TusURLStore}.
251 | */
252 | private final class TestResumeUploadStore implements TusURLStore {
253 | public void set(String fingerprint, URL url) {
254 | fail("set method must not be called");
255 | }
256 |
257 | public URL get(String fingerprint) {
258 | assertEquals(fingerprint, "test-fingerprint");
259 |
260 | try {
261 | return new URL(mockServerURL.toString() + "/foo");
262 | } catch (Exception ignored) { }
263 | return null;
264 | }
265 |
266 | public void remove(String fingerprint) {
267 | fail("remove method must not be called");
268 | }
269 | }
270 |
271 | /**
272 | * Tests if an upload gets started if {@link TusClient#resumeOrCreateUpload(TusUpload)} gets called.
273 | * @throws IOException
274 | * @throws ProtocolException
275 | */
276 | @Test
277 | public void testResumeOrCreateUpload() throws IOException, ProtocolException {
278 | mockServer.when(new HttpRequest()
279 | .withMethod("POST")
280 | .withPath("/files")
281 | .withHeader("Connection", "keep-alive")
282 | .withHeader("Tus-Resumable", TusClient.TUS_VERSION)
283 | .withHeader("Upload-Length", "10"))
284 | .respond(new HttpResponse()
285 | .withStatusCode(201)
286 | .withHeader("Tus-Resumable", TusClient.TUS_VERSION)
287 | .withHeader("Location", mockServerURL + "/foo"));
288 |
289 | TusClient client = new TusClient();
290 | client.setUploadCreationURL(mockServerURL);
291 | TusUpload upload = new TusUpload();
292 | upload.setSize(10);
293 | upload.setInputStream(new ByteArrayInputStream(new byte[10]));
294 | TusUploader uploader = client.resumeOrCreateUpload(upload);
295 |
296 | assertEquals(uploader.getUploadURL(), new URL(mockServerURL + "/foo"));
297 | }
298 |
299 | /**
300 | * Tests if an upload gets started when {@link TusClient#resumeOrCreateUpload(TusUpload)} gets called with
301 | * a proxy set.
302 | * @throws IOException
303 | * @throws ProtocolException
304 | */
305 | @Test
306 | public void testResumeOrCreateUploadWithProxy() throws IOException, ProtocolException {
307 | mockServer.when(new HttpRequest()
308 | .withMethod("POST")
309 | .withPath("/files")
310 | .withHeader("Proxy-Connection", "keep-alive")
311 | .withHeader("Tus-Resumable", TusClient.TUS_VERSION)
312 | .withHeader("Upload-Length", "11"))
313 | .respond(new HttpResponse()
314 | .withStatusCode(201)
315 | .withHeader("Tus-Resumable", TusClient.TUS_VERSION)
316 | .withHeader("Location", mockServerURL + "/foo"));
317 |
318 | TusClient client = new TusClient();
319 | client.setUploadCreationURL(mockServerURL);
320 | Proxy proxy = new Proxy(Type.HTTP, new InetSocketAddress("localhost", mockServer.getPort()));
321 | client.setProxy(proxy);
322 | TusUpload upload = new TusUpload();
323 | upload.setSize(11);
324 | upload.setInputStream(new ByteArrayInputStream(new byte[11]));
325 | TusUploader uploader = client.resumeOrCreateUpload(upload);
326 |
327 | assertEquals(proxy, client.getProxy());
328 | assertEquals(uploader.getUploadURL(), new URL(mockServerURL + "/foo"));
329 | }
330 |
331 | /**
332 | * Checks if a new upload attempt is started in case of a serverside 404-error, without having an Exception thrown.
333 | * @throws IOException
334 | * @throws ProtocolException
335 | */
336 | @Test
337 | public void testResumeOrCreateUploadNotFound() throws IOException, ProtocolException {
338 | mockServer.when(new HttpRequest()
339 | .withMethod("HEAD")
340 | .withPath("/files/not_found")
341 | .withHeader("Tus-Resumable", TusClient.TUS_VERSION))
342 | .respond(new HttpResponse()
343 | .withStatusCode(404));
344 |
345 | mockServer.when(new HttpRequest()
346 | .withMethod("POST")
347 | .withPath("/files")
348 | .withHeader("Tus-Resumable", TusClient.TUS_VERSION)
349 | .withHeader("Upload-Length", "10"))
350 | .respond(new HttpResponse()
351 | .withStatusCode(201)
352 | .withHeader("Tus-Resumable", TusClient.TUS_VERSION)
353 | .withHeader("Location", mockServerURL + "/foo"));
354 |
355 | TusClient client = new TusClient();
356 | client.setUploadCreationURL(mockServerURL);
357 |
358 | TusURLStore store = new TusURLMemoryStore();
359 | store.set("fingerprint", new URL(mockServerURL + "/not_found"));
360 | client.enableResuming(store);
361 |
362 | TusUpload upload = new TusUpload();
363 | upload.setSize(10);
364 | upload.setInputStream(new ByteArrayInputStream(new byte[10]));
365 | upload.setFingerprint("fingerprint");
366 | TusUploader uploader = client.resumeOrCreateUpload(upload);
367 |
368 | assertEquals(uploader.getUploadURL(), new URL(mockServerURL + "/foo"));
369 | }
370 |
371 | /**
372 | * Tests if {@link TusClient#beginOrResumeUploadFromURL(TusUpload, URL)} works.
373 | * @throws IOException
374 | * @throws ProtocolException
375 | */
376 | @Test
377 | public void testBeginOrResumeUploadFromURL() throws IOException, ProtocolException {
378 | mockServer.when(new HttpRequest()
379 | .withMethod("HEAD")
380 | .withPath("/files/fooFromURL")
381 | .withHeader("Tus-Resumable", TusClient.TUS_VERSION))
382 | .respond(new HttpResponse()
383 | .withStatusCode(204)
384 | .withHeader("Tus-Resumable", TusClient.TUS_VERSION)
385 | .withHeader("Upload-Offset", "3"));
386 |
387 | TusClient client = new TusClient();
388 | URL uploadURL = new URL(mockServerURL.toString() + "/fooFromURL");
389 |
390 | TusUpload upload = new TusUpload();
391 | upload.setSize(10);
392 | upload.setInputStream(new ByteArrayInputStream(new byte[10]));
393 |
394 | TusUploader uploader = client.beginOrResumeUploadFromURL(upload, uploadURL);
395 |
396 | assertEquals(uploader.getUploadURL(), uploadURL);
397 | assertEquals(uploader.getOffset(), 3);
398 | }
399 |
400 | /**
401 | * Tests if connections are prepared correctly, which means all header are getting set.
402 | * @throws IOException
403 | */
404 | @Test
405 | public void testPrepareConnection() throws IOException {
406 | HttpURLConnection connection = (HttpURLConnection) mockServerURL.openConnection();
407 | TusClient client = new TusClient();
408 | client.prepareConnection(connection);
409 |
410 | assertEquals(connection.getRequestProperty("Tus-Resumable"), TusClient.TUS_VERSION);
411 | }
412 |
413 | /**
414 | * Tests if HTTP - Headers are set correctly.
415 | * @throws IOException
416 | */
417 | @Test
418 | public void testSetHeaders() throws IOException {
419 | HttpURLConnection connection = (HttpURLConnection) mockServerURL.openConnection();
420 | TusClient client = new TusClient();
421 |
422 | Map headers = new HashMap();
423 | headers.put("Greeting", "Hello");
424 | headers.put("Important", "yes");
425 | headers.put("Tus-Resumable", "evil");
426 |
427 | assertNull(client.getHeaders());
428 | client.setHeaders(headers);
429 | assertEquals(headers, client.getHeaders());
430 |
431 | client.prepareConnection(connection);
432 |
433 | assertEquals(connection.getRequestProperty("Greeting"), "Hello");
434 | assertEquals(connection.getRequestProperty("Important"), "yes");
435 | }
436 |
437 | /**
438 | * Tests if connection timeouts are set correctly.
439 | * @throws IOException
440 | */
441 | @Test
442 | public void testSetConnectionTimeout() throws IOException {
443 | HttpURLConnection connection = (HttpURLConnection) mockServerURL.openConnection();
444 | TusClient client = new TusClient();
445 |
446 | assertEquals(client.getConnectTimeout(), 5000);
447 | client.setConnectTimeout(3000);
448 | assertEquals(client.getConnectTimeout(), 3000);
449 |
450 | client.prepareConnection(connection);
451 |
452 | assertEquals(connection.getConnectTimeout(), 3000);
453 | }
454 |
455 | /**
456 | * Tests whether the connection follows redirects only after explicitly enabling this feature.
457 | * @throws Exception
458 | */
459 | @Test
460 | public void testFollowRedirects() throws Exception {
461 | HttpURLConnection connection = (HttpURLConnection) mockServerURL.openConnection();
462 | TusClient client = new TusClient();
463 |
464 | // Should not follow by default
465 | client.prepareConnection(connection);
466 | assertFalse(connection.getInstanceFollowRedirects());
467 |
468 | // Only follow if we enable strict redirects
469 | System.setProperty("http.strictPostRedirect", "true");
470 | client.prepareConnection(connection);
471 | assertTrue(connection.getInstanceFollowRedirects());
472 |
473 | // Attempt a real redirect
474 | mockServer.when(new HttpRequest()
475 | .withMethod("POST")
476 | .withPath("/filesRedirect")
477 | .withHeader("Tus-Resumable", TusClient.TUS_VERSION)
478 | .withHeader("Upload-Length", "10"))
479 | .respond(new HttpResponse()
480 | .withStatusCode(301)
481 | .withHeader("Location", mockServerURL + "Redirected"));
482 |
483 | mockServer.when(new HttpRequest()
484 | .withMethod("POST")
485 | .withPath("/filesRedirected")
486 | .withHeader("Tus-Resumable", TusClient.TUS_VERSION)
487 | .withHeader("Upload-Length", "10"))
488 | .respond(new HttpResponse()
489 | .withStatusCode(201)
490 | .withHeader("Tus-Resumable", TusClient.TUS_VERSION)
491 | .withHeader("Location", mockServerURL + "/foo"));
492 |
493 | client.setUploadCreationURL(new URL(mockServerURL + "Redirect"));
494 | TusUpload upload = new TusUpload();
495 | upload.setSize(10);
496 | upload.setInputStream(new ByteArrayInputStream(new byte[10]));
497 | TusUploader uploader = client.createUpload(upload);
498 |
499 | assertEquals(uploader.getUploadURL(), new URL(mockServerURL + "/foo"));
500 | }
501 |
502 | /**
503 | * Tests if the fingerprint in the {@link TusURLStore} does not get removed after upload success.
504 | * @throws IOException
505 | * @throws ProtocolException
506 | */
507 | @Test
508 | public void testRemoveFingerprintOnSuccessDisabled() throws IOException, ProtocolException {
509 |
510 | TusClient client = new TusClient();
511 |
512 | TusURLStore store = new TusURLMemoryStore();
513 | URL dummyURL = new URL("http://dummy-url/files/dummy");
514 | store.set("fingerprint", dummyURL);
515 | client.enableResuming(store);
516 |
517 | assertFalse(client.removeFingerprintOnSuccessEnabled());
518 |
519 | TusUpload upload = new TusUpload();
520 | upload.setFingerprint("fingerprint");
521 |
522 | client.uploadFinished(upload);
523 |
524 | assertEquals(dummyURL, store.get("fingerprint"));
525 |
526 | }
527 |
528 | /**
529 | * Tests if the fingerprint in the {@link TusURLStore} does get removed after upload success,
530 | * after this feature has been enabled via the {@link TusClient#enableRemoveFingerprintOnSuccess()} - method.
531 | * @throws IOException
532 | * @throws ProtocolException
533 | */
534 | @Test
535 | public void testRemoveFingerprintOnSuccessEnabled() throws IOException, ProtocolException {
536 |
537 | TusClient client = new TusClient();
538 |
539 | TusURLStore store = new TusURLMemoryStore();
540 | URL dummyURL = new URL("http://dummy-url/files/dummy");
541 | store.set("fingerprint", dummyURL);
542 | client.enableResuming(store);
543 | client.enableRemoveFingerprintOnSuccess();
544 |
545 | assertTrue(client.removeFingerprintOnSuccessEnabled());
546 |
547 | TusUpload upload = new TusUpload();
548 | upload.setFingerprint("fingerprint");
549 |
550 | client.uploadFinished(upload);
551 |
552 | assertNull(store.get("fingerprint"));
553 |
554 | }
555 | }
556 |
--------------------------------------------------------------------------------
/src/test/java/io/tus/java/client/TestTusExecutor.java:
--------------------------------------------------------------------------------
1 | package io.tus.java.client;
2 |
3 | import org.junit.Test;
4 |
5 | import java.io.IOException;
6 | import java.net.HttpURLConnection;
7 | import java.net.MalformedURLException;
8 | import java.net.URL;
9 |
10 | import static org.junit.Assert.assertArrayEquals;
11 | import static org.junit.Assert.assertEquals;
12 | import static org.junit.Assert.assertFalse;
13 | import static org.junit.Assert.assertTrue;
14 |
15 | /**
16 | * Test class for a {@link TusExecutor}.
17 | */
18 | public class TestTusExecutor {
19 |
20 | /**
21 | * Tests if the delays for connection attempts are set in the right manner.
22 | */
23 | @Test
24 | public void testSetDelays() {
25 | CountingExecutor exec = new CountingExecutor();
26 |
27 | assertArrayEquals(exec.getDelays(), new int[]{500, 1000, 2000, 3000});
28 | exec.setDelays(new int[]{1, 2, 3});
29 | assertArrayEquals(exec.getDelays(), new int[]{1, 2, 3});
30 | assertEquals(exec.getCalls(), 0);
31 | }
32 |
33 | /**
34 | * Tests if a running execution is interuptable.
35 | * @throws Exception
36 | */
37 | @Test
38 | public void testInterrupting() throws Exception {
39 | TusExecutor exec = new TusExecutor() {
40 | @Override
41 | protected void makeAttempt() throws ProtocolException, IOException {
42 | throw new IOException();
43 | }
44 | };
45 |
46 | exec.setDelays(new int[]{100000});
47 |
48 | final Thread executorThread = Thread.currentThread();
49 | Thread waiterThread = new Thread(new Runnable() {
50 | @Override
51 | public void run() {
52 | try {
53 | Thread.sleep(100);
54 | executorThread.interrupt();
55 | } catch (InterruptedException e) {
56 | e.printStackTrace();
57 | }
58 | }
59 | });
60 | waiterThread.start();
61 |
62 | assertFalse(exec.makeAttempts());
63 | }
64 |
65 |
66 | /**
67 | * Tests if {@link TusExecutor#makeAttempts()} actually makes attempts.
68 | * @throws Exception
69 | */
70 | @Test
71 | public void testMakeAttempts() throws Exception {
72 | CountingExecutor exec = new CountingExecutor();
73 |
74 | exec.setDelays(new int[]{1, 2, 3});
75 | assertTrue(exec.makeAttempts());
76 | assertEquals(exec.getCalls(), 1);
77 | }
78 |
79 |
80 | /**
81 | * Tests if every attempt can throw an IOException.
82 | * @throws Exception
83 | */
84 | @Test(expected = IOException.class)
85 | public void testMakeAllAttemptsThrowIOException() throws Exception {
86 | CountingExecutor exec = new CountingExecutor() {
87 | @Override
88 | protected void makeAttempt() throws ProtocolException, IOException {
89 | super.makeAttempt();
90 | throw new IOException();
91 | }
92 | };
93 |
94 | exec.setDelays(new int[]{1, 2, 3});
95 | try {
96 | exec.makeAttempts();
97 | } finally {
98 | assertEquals(exec.getCalls(), 4);
99 | }
100 | }
101 |
102 | /**
103 | * Tests if every attempt can throw a {@link ProtocolException}.
104 | * @throws Exception
105 | */
106 | @Test(expected = ProtocolException.class)
107 | public void testMakeAllAttemptsThrowProtocolException() throws Exception {
108 | CountingExecutor exec = new CountingExecutor() {
109 | @Override
110 | protected void makeAttempt() throws ProtocolException, IOException {
111 | super.makeAttempt();
112 | throw new ProtocolException("something happened", new MockHttpURLConnection(500));
113 | }
114 | };
115 |
116 | exec.setDelays(new int[]{1, 2, 3});
117 | try {
118 | exec.makeAttempts();
119 | } finally {
120 | assertEquals(exec.getCalls(), 4);
121 | }
122 | }
123 |
124 | /**
125 | * Tests if an Exception can be thrown also at single attempts.
126 | * @throws Exception
127 | */
128 | @Test(expected = ProtocolException.class)
129 | public void testMakeOneAttempt() throws Exception {
130 | CountingExecutor exec = new CountingExecutor() {
131 | @Override
132 | protected void makeAttempt() throws ProtocolException, IOException {
133 | super.makeAttempt();
134 | throw new ProtocolException("something happened", new MockHttpURLConnection(404));
135 | }
136 | };
137 |
138 | exec.setDelays(new int[]{1, 2, 3});
139 | try {
140 | exec.makeAttempts();
141 | } finally {
142 | assertEquals(exec.getCalls(), 1);
143 | }
144 | }
145 |
146 | /**
147 | * A mocked HttpURLConnection which always returns the specified response code.
148 | */
149 | private class MockHttpURLConnection extends HttpURLConnection {
150 | private int statusCode;
151 |
152 | MockHttpURLConnection(int statusCode) throws MalformedURLException {
153 | super(new URL("http://localhost/"));
154 | this.statusCode = statusCode;
155 | }
156 |
157 | @Override
158 | public int getResponseCode() {
159 | return statusCode;
160 | }
161 |
162 | @Override
163 | public boolean usingProxy() {
164 | return false;
165 | }
166 |
167 | @Override
168 | public void disconnect() { }
169 |
170 | @Override
171 | public void connect() { }
172 | }
173 |
174 | /**
175 | * A TusExecutor implementation which counts the calls to makeAttempt().
176 | */
177 | private class CountingExecutor extends TusExecutor {
178 | private int calls;
179 |
180 | @Override
181 | protected void makeAttempt() throws ProtocolException, IOException {
182 | calls++;
183 | }
184 |
185 | public int getCalls() {
186 | return calls;
187 | }
188 | }
189 | }
190 |
--------------------------------------------------------------------------------
/src/test/java/io/tus/java/client/TestTusURLMemoryStore.java:
--------------------------------------------------------------------------------
1 | package io.tus.java.client;
2 |
3 | import java.net.MalformedURLException;
4 | import java.net.URL;
5 |
6 | import org.junit.Test;
7 |
8 | import static org.junit.Assert.assertEquals;
9 |
10 | /**
11 | * Test class for {@link TusURLMemoryStore}.
12 | */
13 | public class TestTusURLMemoryStore {
14 |
15 | /**
16 | * Tests if setting and deleting of an url in the {@link TusURLMemoryStore} works.
17 | * @throws MalformedURLException
18 | */
19 | @Test
20 | public void test() throws MalformedURLException {
21 | TusURLStore store = new TusURLMemoryStore();
22 | URL url = new URL("https://tusd.tusdemo.net/files/hello");
23 | String fingerprint = "foo";
24 | store.set(fingerprint, url);
25 |
26 | assertEquals(store.get(fingerprint), url);
27 |
28 | store.remove(fingerprint);
29 |
30 | assertEquals(store.get(fingerprint), null);
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/test/java/io/tus/java/client/TestTusUpload.java:
--------------------------------------------------------------------------------
1 | package io.tus.java.client;
2 |
3 | import org.junit.Test;
4 |
5 | import static org.junit.Assert.assertEquals;
6 | import static org.junit.Assert.assertNotSame;
7 |
8 | import java.io.File;
9 | import java.io.FileOutputStream;
10 | import java.io.IOException;
11 | import java.io.OutputStream;
12 | import java.util.LinkedHashMap;
13 | import java.util.Map;
14 |
15 | /**
16 | * Test class for {@link TusUpload}.
17 | */
18 | public class TestTusUpload {
19 | /**
20 | * Tests if uploading a file works.
21 | * @throws IOException
22 | */
23 | @Test
24 | public void testTusUploadFile() throws IOException {
25 | String content = "hello world";
26 |
27 | File file = File.createTempFile("tus-upload-test", ".tmp");
28 | OutputStream output = new FileOutputStream(file);
29 | output.write(content.getBytes());
30 | output.close();
31 |
32 | TusUpload upload = new TusUpload(file);
33 |
34 | Map metadata = new LinkedHashMap();
35 | metadata.put("foo", "hello");
36 | metadata.put("bar", "world");
37 | metadata.putAll(upload.getMetadata());
38 |
39 | assertEquals(metadata.get("filename"), file.getName());
40 |
41 | upload.setMetadata(metadata);
42 | assertEquals(upload.getMetadata(), metadata);
43 | assertEquals(
44 | upload.getEncodedMetadata(),
45 | "foo aGVsbG8=,bar d29ybGQ=,filename " + TusUpload.base64Encode(file.getName().getBytes()));
46 |
47 | assertEquals(upload.getSize(), content.length());
48 | assertNotSame(upload.getFingerprint(), "");
49 | byte[] readContent = new byte[content.length()];
50 | assertEquals(upload.getInputStream().read(readContent), content.length());
51 | assertEquals(new String(readContent), content);
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/test/java/io/tus/java/client/TestTusUploader.java:
--------------------------------------------------------------------------------
1 | package io.tus.java.client;
2 |
3 | import static org.junit.Assert.assertEquals;
4 | import static org.junit.Assert.assertTrue;
5 | import static org.mockito.Mockito.mock;
6 | import static org.mockito.Mockito.verify;
7 | import static org.mockito.Mockito.times;
8 |
9 | import java.io.BufferedReader;
10 | import java.io.ByteArrayInputStream;
11 | import java.io.IOException;
12 | import java.io.InputStreamReader;
13 | import java.io.OutputStream;
14 | import java.net.InetSocketAddress;
15 | import java.net.MalformedURLException;
16 | import java.net.Proxy;
17 | import java.net.Proxy.Type;
18 | import java.net.ServerSocket;
19 | import java.net.Socket;
20 | import java.net.URL;
21 | import java.util.Arrays;
22 |
23 | import org.junit.Assume;
24 | import org.junit.Test;
25 | import org.mockserver.model.HttpRequest;
26 | import org.mockserver.model.HttpResponse;
27 | import org.mockserver.socket.PortFactory;
28 |
29 | /**
30 | * Test class for {@link TusUploader}.
31 | */
32 | public class TestTusUploader extends MockServerProvider {
33 | private boolean isOpenJDK6 = System.getProperty("java.version").startsWith("1.6")
34 | && System.getProperty("java.vm.name").contains("OpenJDK");
35 |
36 | /**
37 | * Tests if the {@link TusUploader} actually uploads files and fixed chunk sizes.
38 | * @throws IOException
39 | * @throws ProtocolException
40 | */
41 | @Test
42 | public void testTusUploader() throws IOException, ProtocolException {
43 | byte[] content = "hello world".getBytes();
44 |
45 | mockServer.when(new HttpRequest()
46 | .withPath("/files/foo")
47 | .withHeader("Tus-Resumable", TusClient.TUS_VERSION)
48 | .withHeader("Upload-Offset", "3")
49 | .withHeader("Content-Type", "application/offset+octet-stream")
50 | .withHeader("Connection", "keep-alive")
51 | .withBody(Arrays.copyOfRange(content, 3, 11)))
52 | .respond(new HttpResponse()
53 | .withStatusCode(204)
54 | .withHeader("Tus-Resumable", TusClient.TUS_VERSION)
55 | .withHeader("Upload-Offset", "11"));
56 |
57 | TusClient client = new TusClient();
58 | URL uploadUrl = new URL(mockServerURL + "/foo");
59 | TusInputStream input = new TusInputStream(new ByteArrayInputStream(content));
60 | long offset = 3;
61 |
62 | TusUpload upload = new TusUpload();
63 |
64 | TusUploader uploader = new TusUploader(client, upload, uploadUrl, input, offset);
65 |
66 | uploader.setChunkSize(5);
67 | assertEquals(uploader.getChunkSize(), 5);
68 |
69 | assertEquals(5, uploader.uploadChunk());
70 | assertEquals(3, uploader.uploadChunk(5));
71 | assertEquals(-1, uploader.uploadChunk());
72 | assertEquals(-1, uploader.uploadChunk(5));
73 | assertEquals(11, uploader.getOffset());
74 | uploader.finish();
75 | }
76 |
77 | /**
78 | * Tests if the {@link TusUploader} actually uploads files through a proxy.
79 | * @throws IOException
80 | * @throws ProtocolException
81 | */
82 | @Test
83 | public void testTusUploaderWithProxy() throws IOException, ProtocolException {
84 | byte[] content = "hello world with proxy".getBytes();
85 |
86 | mockServer.when(new HttpRequest()
87 | .withPath("/files/foo")
88 | .withHeader("Tus-Resumable", TusClient.TUS_VERSION)
89 | .withHeader("Upload-Offset", "0")
90 | .withHeader("Content-Type", "application/offset+octet-stream")
91 | .withHeader("Proxy-Connection", "keep-alive")
92 | .withBody(Arrays.copyOf(content, content.length)))
93 | .respond(new HttpResponse()
94 | .withStatusCode(204)
95 | .withHeader("Tus-Resumable", TusClient.TUS_VERSION)
96 | .withHeader("Upload-Offset", "22"));
97 |
98 | TusClient client = new TusClient();
99 | URL uploadUrl = new URL(mockServerURL + "/foo");
100 | Proxy proxy = new Proxy(Type.HTTP, new InetSocketAddress("localhost", mockServer.getPort()));
101 | TusInputStream input = new TusInputStream(new ByteArrayInputStream(content));
102 | long offset = 0;
103 |
104 | TusUpload upload = new TusUpload();
105 |
106 | TusUploader uploader = new TusUploader(client, upload, uploadUrl, input, offset);
107 | uploader.setProxy(proxy);
108 |
109 | assertEquals(proxy, uploader.getProxy());
110 | assertEquals(22, uploader.uploadChunk());
111 | uploader.finish();
112 | }
113 |
114 | /**
115 | * Verifies, that {@link TusClient#uploadFinished(TusUpload)} gets called after a proper upload has been finished.
116 | * @throws IOException
117 | * @throws ProtocolException
118 | */
119 | @Test
120 | public void testTusUploaderClientUploadFinishedCalled() throws IOException, ProtocolException {
121 |
122 | TusClient client = mock(TusClient.class);
123 |
124 | byte[] content = "hello world".getBytes();
125 |
126 | URL uploadUrl = new URL("http://dummy-url/foo");
127 | TusInputStream input = new TusInputStream(new ByteArrayInputStream(content));
128 | long offset = 10;
129 |
130 | TusUpload upload = new TusUpload();
131 | upload.setSize(10);
132 |
133 | TusUploader uploader = new TusUploader(client, upload, uploadUrl, input, offset);
134 | uploader.finish();
135 |
136 | // size and offset are the same, so uploadfinished() should be called
137 | verify(client).uploadFinished(upload);
138 | }
139 |
140 | /**
141 | * Verifies, that {@link TusClient#uploadFinished(TusUpload)} doesn't get called if the actual upload size is
142 | * greater than the offset.
143 | * @throws IOException
144 | * @throws ProtocolException
145 | */
146 | @Test
147 | public void testTusUploaderClientUploadFinishedNotCalled() throws IOException, ProtocolException {
148 |
149 | TusClient client = mock(TusClient.class);
150 |
151 | byte[] content = "hello world".getBytes();
152 |
153 | URL uploadUrl = new URL("http://dummy-url/foo");
154 | TusInputStream input = new TusInputStream(new ByteArrayInputStream(content));
155 | long offset = 0;
156 |
157 | TusUpload upload = new TusUpload();
158 | upload.setSize(10);
159 |
160 | TusUploader uploader = new TusUploader(client, upload, uploadUrl, input, offset);
161 | uploader.finish();
162 |
163 | // size is greater than offset, so uploadfinished() should not be called
164 | verify(client, times(0)).uploadFinished(upload);
165 | }
166 |
167 | /**
168 | * Verifies, that an Exception gets thrown, if the upload server isn't satisfied with the client's headers.
169 | * @throws IOException
170 | * @throws ProtocolException
171 | */
172 | @Test
173 | public void testTusUploaderFailedExpectation() throws IOException, ProtocolException {
174 | Assume.assumeFalse(isOpenJDK6);
175 |
176 | FailingExpectationServer server = new FailingExpectationServer();
177 | server.start();
178 |
179 | byte[] content = "hello world".getBytes();
180 |
181 | TusClient client = new TusClient();
182 | URL uploadUrl = new URL(server.getURL() + "/expect");
183 | TusInputStream input = new TusInputStream(new ByteArrayInputStream(content));
184 | long offset = 3;
185 | TusUpload upload = new TusUpload();
186 | boolean exceptionThrown = false;
187 | TusUploader uploader = new TusUploader(client, upload, uploadUrl, input, offset);
188 | try {
189 | uploader.uploadChunk();
190 | } catch (ProtocolException e) {
191 | assertTrue(e.getMessage().contains("500"));
192 | exceptionThrown = true;
193 | } finally {
194 | assertTrue(exceptionThrown);
195 | }
196 | }
197 |
198 | /**
199 | * FailingExpectationServer is a HTTP/1.1 server which will always respond with a 500 Internal
200 | * Error. This is meant to simulate failing expectations when the request contains the
201 | * expected header. The org.mockserver packages do not support this and will always send the
202 | * 100 Continue status code. therefore, we built our own stupid mocking server.
203 | */
204 | private class FailingExpectationServer extends Thread {
205 | private final byte[] response = "HTTP/1.1 500 Internal Server Error\r\n\r\n".getBytes();
206 | private ServerSocket serverSocket;
207 | private int port;
208 |
209 | FailingExpectationServer() throws IOException {
210 | port = PortFactory.findFreePort();
211 |
212 | serverSocket = new ServerSocket(port);
213 | }
214 |
215 | @Override
216 | public void run() {
217 | try {
218 | Socket socket = serverSocket.accept();
219 |
220 | OutputStream output = socket.getOutputStream();
221 | BufferedReader input = new BufferedReader(new InputStreamReader(socket.getInputStream()));
222 | while (!input.readLine().isEmpty()) {
223 | output.write(response);
224 | break;
225 | }
226 |
227 | socket.close();
228 | } catch (IOException e) {
229 | e.printStackTrace();
230 | }
231 | }
232 |
233 | public URL getURL() {
234 | try {
235 | return new URL("http://localhost:" + port);
236 | } catch (MalformedURLException e) {
237 | return null;
238 | }
239 | }
240 | }
241 |
242 | /**
243 | * Verifies, that {@link TusUploader#setRequestPayloadSize(int)} effectively limits the size a payload.
244 | * @throws Exception
245 | */
246 | @Test
247 | public void testSetRequestPayloadSize() throws Exception {
248 | byte[] content = "hello world".getBytes();
249 |
250 | mockServer.when(new HttpRequest()
251 | .withPath("/files/payload")
252 | .withHeader("Tus-Resumable", TusClient.TUS_VERSION)
253 | .withHeader("Upload-Offset", "0")
254 | .withHeader("Content-Type", "application/offset+octet-stream")
255 | .withBody(Arrays.copyOfRange(content, 0, 5)))
256 | .respond(new HttpResponse()
257 | .withStatusCode(204)
258 | .withHeader("Tus-Resumable", TusClient.TUS_VERSION)
259 | .withHeader("Upload-Offset", "5"));
260 |
261 | mockServer.when(new HttpRequest()
262 | .withPath("/files/payload")
263 | .withHeader("Tus-Resumable", TusClient.TUS_VERSION)
264 | .withHeader("Upload-Offset", "5")
265 | .withHeader("Content-Type", "application/offset+octet-stream")
266 | .withBody(Arrays.copyOfRange(content, 5, 10)))
267 | .respond(new HttpResponse()
268 | .withStatusCode(204)
269 | .withHeader("Tus-Resumable", TusClient.TUS_VERSION)
270 | .withHeader("Upload-Offset", "10"));
271 |
272 | mockServer.when(new HttpRequest()
273 | .withPath("/files/payload")
274 | .withHeader("Tus-Resumable", TusClient.TUS_VERSION)
275 | .withHeader("Upload-Offset", "10")
276 | .withHeader("Content-Type", "application/offset+octet-stream")
277 | .withBody(Arrays.copyOfRange(content, 10, 11)))
278 | .respond(new HttpResponse()
279 | .withStatusCode(204)
280 | .withHeader("Tus-Resumable", TusClient.TUS_VERSION)
281 | .withHeader("Upload-Offset", "11"));
282 |
283 | TusClient client = new TusClient();
284 | URL uploadUrl = new URL(mockServerURL + "/payload");
285 | TusInputStream input = new TusInputStream(new ByteArrayInputStream(content));
286 | TusUpload upload = new TusUpload();
287 |
288 | TusUploader uploader = new TusUploader(client, upload, uploadUrl, input, 0);
289 |
290 | assertEquals(uploader.getRequestPayloadSize(), 10 * 1024 * 1024);
291 | uploader.setRequestPayloadSize(5);
292 | assertEquals(uploader.getRequestPayloadSize(), 5);
293 |
294 | uploader.setChunkSize(4);
295 |
296 | // First request
297 | assertEquals(4, uploader.uploadChunk());
298 | assertEquals(1, uploader.uploadChunk());
299 |
300 | // Second request
301 | uploader.setChunkSize(100);
302 | assertEquals(5, uploader.uploadChunk());
303 |
304 | // Third request
305 | assertEquals(1, uploader.uploadChunk());
306 | uploader.finish();
307 | }
308 |
309 |
310 | /**
311 | * Verifies, that an exception is thrown if {@link TusUploader#setRequestPayloadSize(int)} is called while the
312 | * client has already an upload connection opened.
313 | * @throws Exception
314 | */
315 | @Test(expected = IllegalStateException.class)
316 | public void testSetRequestPayloadSizeThrows() throws Exception {
317 | byte[] content = "hello world".getBytes();
318 |
319 | TusClient client = new TusClient();
320 | URL uploadUrl = new URL(mockServerURL + "/payloadException");
321 | TusInputStream input = new TusInputStream(new ByteArrayInputStream(content));
322 | TusUpload upload = new TusUpload();
323 |
324 | TusUploader uploader = new TusUploader(client, upload, uploadUrl, input, 0);
325 |
326 | uploader.setChunkSize(4);
327 | uploader.uploadChunk();
328 |
329 | // Throws IllegalStateException
330 | uploader.setRequestPayloadSize(100);
331 | }
332 |
333 | /**
334 | * Verifies, that an Exception is thrown if the UploadOffsetHeader is missing.
335 | * @throws Exception
336 | */
337 | @Test
338 | public void testMissingUploadOffsetHeader() throws Exception {
339 | byte[] content = "hello world".getBytes();
340 |
341 | mockServer.when(new HttpRequest()
342 | .withPath("/files/missingHeader"))
343 | .respond(new HttpResponse()
344 | .withStatusCode(204)
345 | .withHeader("Tus-Resumable", TusClient.TUS_VERSION));
346 |
347 | TusClient client = new TusClient();
348 | URL uploadUrl = new URL(mockServerURL + "/missingHeader");
349 | TusInputStream input = new TusInputStream(new ByteArrayInputStream(content));
350 | TusUpload upload = new TusUpload();
351 |
352 | TusUploader uploader = new TusUploader(client, upload, uploadUrl, input, 0);
353 |
354 | boolean exceptionThrown = false;
355 | try {
356 | assertEquals(11, uploader.uploadChunk());
357 | uploader.finish();
358 | } catch (ProtocolException e) {
359 | assertTrue(e.getMessage().contains("no or invalid Upload-Offset header"));
360 | exceptionThrown = true;
361 | } finally {
362 | assertTrue(exceptionThrown);
363 | }
364 | }
365 |
366 | /**
367 | * Verifies, that an Exception is thrown if the UploadOffsetHeader of the server's response does not match the
368 | * clients upload offset value.
369 | * @throws Exception
370 | */
371 | @Test
372 | public void testUnmatchingUploadOffsetHeader() throws Exception {
373 | byte[] content = "hello world".getBytes();
374 |
375 | mockServer.when(new HttpRequest()
376 | .withPath("/files/unmatchingHeader"))
377 | .respond(new HttpResponse()
378 | .withStatusCode(204)
379 | .withHeader("Tus-Resumable", TusClient.TUS_VERSION)
380 | .withHeader("Upload-Offset", "44"));
381 |
382 | TusClient client = new TusClient();
383 | URL uploadUrl = new URL(mockServerURL + "/unmatchingHeader");
384 | TusInputStream input = new TusInputStream(new ByteArrayInputStream(content));
385 | TusUpload upload = new TusUpload();
386 |
387 | TusUploader uploader = new TusUploader(client, upload, uploadUrl, input, 0);
388 |
389 | boolean exceptionThrown = false;
390 | try {
391 | assertEquals(11, uploader.uploadChunk());
392 | uploader.finish();
393 | } catch (ProtocolException e) {
394 | assertTrue(e.getMessage().contains("different Upload-Offset value (44) than expected (11)"));
395 | exceptionThrown = true;
396 | } finally {
397 | assertTrue(exceptionThrown);
398 | }
399 | }
400 | }
401 |
--------------------------------------------------------------------------------
/src/test/java/io/tus/java/client/package-info.java:
--------------------------------------------------------------------------------
1 | /**
2 | * This package contains methods for unit testing.
3 | **/
4 | package io.tus.java.client;
5 |
--------------------------------------------------------------------------------