Skip to content

Commit e410b7c

Browse files
authored
Merge pull request #12 from tomdesair/feature/upload-URI-regex-support
Support regex expression in upload URI
2 parents 25feaa5 + b5b0cae commit e410b7c

File tree

8 files changed

+304
-20
lines changed

8 files changed

+304
-20
lines changed

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,9 @@ Besides the [core protocol](https://tus.io/protocols/resumable-upload.html#core-
3838
### 1. Setup
3939
The first step is to create a `TusFileUploadService` object using its constructor. You can make this object available as a (Spring bean) singleton or create a new instance for each request. After creating the object, you can configure it using the following methods:
4040

41-
* `withUploadURI(String)`: Set the relative URL under which the tus upload endpoint will be made available, for example `/files/upload`.
41+
* `withUploadURI(String)`: Set the relative URL under which the main tus upload endpoint will be made available, for example `/files/upload`. Optionally, this URI may contain regex parameters in order to support endpoints that contain URL parameters, for example `/users/[0-9]+/files/upload`.
4242
* `withMaxUploadSize(Long)`: Specify the maximum number of bytes that can be uploaded per upload. If you don't call this method, the maximum number of bytes is `Long.MAX_VALUE`.
43-
* `withStoragePath(String)`: If you're using the default filesystem-based storage service, you can use this method to specify the path where to store the uploaded bytes and upload information.
43+
* `withStoragePath(String)`: If you're using the default file system-based storage service, you can use this method to specify the path where to store the uploaded bytes and upload information.
4444
* `withChunkedTransferDecoding`: You can enable or disable the decoding of chunked HTTP requests by this library. Enable this feature in case the web container in which this service is running does not decode chunked transfers itself. By default, chunked decoding via this library is disabled (as modern frameworks tend to already do this for you).
4545
* `withThreadLocalCache(Boolean)`: Optionally you can enable (or disable) an in-memory (thread local) cache of upload request data to reduce load on the storage backend and potentially increase performance when processing upload requests.
4646
* `withUploadExpirationPeriod(Long)`: You can set the number of milliseconds after which an upload is considered as expired and available for cleanup.
@@ -49,7 +49,7 @@ The first step is to create a `TusFileUploadService` object using its constructo
4949
* `disableTusExtension(String)`: Disable the `TusExtension` for which the `getName()` method matches the provided string. The default extensions have names "creation", "checksum", "expiration", "concatenation", "termination" and "download". You cannot disable the "core" feature.
5050

5151

52-
For now this library only provides filesystem-based storage and locking options. You can however provide your own implementation of a `UploadStorageService` and `UploadLockingService` using the methods `withUploadStorageService(UploadStorageService)` and `withUploadLockingService(UploadLockingService)` in order to support different types of upload storage.
52+
For now this library only provides file system-based storage and locking options. You can however provide your own implementation of a `UploadStorageService` and `UploadLockingService` using the methods `withUploadStorageService(UploadStorageService)` and `withUploadLockingService(UploadLockingService)` in order to support different types of upload storage.
5353

5454
### 2. Processing an upload
5555
To process an upload request you have to pass the current `javax.servlet.http.HttpServletRequest` and `javax.servlet.http.HttpServletResponse` objects to the `me.desair.tus.server.TusFileUploadService.process()` method. Typical places were you can do this are inside Servlets, Filters or REST API Controllers (see [examples](#quick-start-and-examples)).

src/main/java/me/desair/tus/server/TusFileUploadService.java

Lines changed: 98 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -67,18 +67,39 @@ protected void initFeatures() {
6767
addTusExtension(new ConcatenationExtension());
6868
}
6969

70+
/**
71+
* Set the URI under which the main tus upload endpoint is hosted.
72+
* Optionally, this URI may contain regex parameters in order to support endpoints that contain
73+
* URL parameters, for example /users/[0-9]+/files/upload
74+
*
75+
* @param uploadURI The URI of the main tus upload endpoint
76+
* @return The current service
77+
*/
7078
public TusFileUploadService withUploadURI(String uploadURI) {
71-
Validate.notBlank(uploadURI, "The upload URI cannot be blank");
7279
this.idFactory.setUploadURI(uploadURI);
7380
return this;
7481
}
7582

83+
/**
84+
* Specify the maximum number of bytes that can be uploaded per upload.
85+
* If you don't call this method, the maximum number of bytes is Long.MAX_VALUE.
86+
*
87+
* @param maxUploadSize The maximum upload length that is allowed
88+
* @return The current service
89+
*/
7690
public TusFileUploadService withMaxUploadSize(Long maxUploadSize) {
7791
Validate.exclusiveBetween(0, Long.MAX_VALUE, maxUploadSize, "The max upload size must be bigger than 0");
7892
this.uploadStorageService.setMaxUploadSize(maxUploadSize);
7993
return this;
8094
}
8195

96+
/**
97+
* Provide a custom {@link UploadStorageService} implementation that should be used to store uploaded bytes and
98+
* metadata ({@link UploadInfo}).
99+
*
100+
* @param uploadStorageService The custom {@link UploadStorageService} implementation
101+
* @return The current service
102+
*/
82103
public TusFileUploadService withUploadStorageService(UploadStorageService uploadStorageService) {
83104
Validate.notNull(uploadStorageService, "The UploadStorageService cannot be null");
84105
//Copy over any previous configuration
@@ -91,6 +112,14 @@ public TusFileUploadService withUploadStorageService(UploadStorageService upload
91112
return this;
92113
}
93114

115+
/**
116+
* Provide a custom {@link UploadLockingService} implementation that should be used when processing uploads.
117+
* The upload locking service is responsible for locking an upload that is being processed so that it cannot
118+
* be corrupted by simultaneous or delayed requests.
119+
*
120+
* @param uploadLockingService The {@link UploadLockingService} implementation to use
121+
* @return The current service
122+
*/
94123
public TusFileUploadService withUploadLockingService(UploadLockingService uploadLockingService) {
95124
Validate.notNull(uploadLockingService, "The UploadStorageService cannot be null");
96125
uploadLockingService.setIdFactory(this.idFactory);
@@ -100,6 +129,13 @@ public TusFileUploadService withUploadLockingService(UploadLockingService upload
100129
return this;
101130
}
102131

132+
/**
133+
* If you're using the default file system-based storage service, you can use this method to
134+
* specify the path where to store the uploaded bytes and upload information.
135+
*
136+
* @param storagePath The file system path where uploads can be stored (temporarily)
137+
* @return The current service
138+
*/
103139
public TusFileUploadService withStoragePath(String storagePath) {
104140
Validate.notBlank(storagePath, "The storage path cannot be blank");
105141
withUploadStorageService(new DiskStorageService(idFactory, storagePath));
@@ -133,48 +169,102 @@ public TusFileUploadService withChunkedTransferDecoding(boolean isEnabled) {
133169
return this;
134170
}
135171

172+
/**
173+
* You can set the number of milliseconds after which an upload is considered as expired and available for cleanup.
174+
*
175+
* @param expirationPeriod The number of milliseconds after which an upload expires and can be removed
176+
* @return The current service
177+
*/
136178
public TusFileUploadService withUploadExpirationPeriod(Long expirationPeriod) {
137179
uploadStorageService.setUploadExpirationPeriod(expirationPeriod);
138180
return this;
139181
}
140182

183+
/**
184+
* Enable the unofficial `download` extension that also allows you to download uploaded bytes.
185+
* By default this feature is disabled.
186+
*
187+
* @return The current service
188+
*/
141189
public TusFileUploadService withDownloadFeature() {
142190
addTusExtension(new DownloadExtension());
143191
return this;
144192
}
145193

194+
/**
195+
* Add a custom (application-specific) extension that implements the {@link me.desair.tus.server.TusExtension}
196+
* interface. For example you can add your own extension that checks authentication and authorization policies
197+
* within your application for the user doing the upload.
198+
*
199+
* @param feature The custom extension implementation
200+
* @return The current service
201+
*/
146202
public TusFileUploadService addTusExtension(TusExtension feature) {
147203
Validate.notNull(feature, "A custom feature cannot be null");
148204
enabledFeatures.put(feature.getName(), feature);
149205
updateSupportedHttpMethods();
150206
return this;
151207
}
152208

153-
public TusFileUploadService disableTusExtension(String featureName) {
154-
Validate.notNull(featureName, "The feature name cannot be null");
155-
156-
if (StringUtils.equals("core", featureName)) {
157-
throw new IllegalArgumentException("The core protocol cannot be disabled");
158-
}
209+
/**
210+
* Disable the TusExtension for which the getName() method matches the provided string. The default extensions
211+
* have names "creation", "checksum", "expiration", "concatenation", "termination" and "download".
212+
* You cannot disable the "core" feature.
213+
*
214+
* @param extensionName The name of the extension to disable
215+
* @return The current service
216+
*/
217+
public TusFileUploadService disableTusExtension(String extensionName) {
218+
Validate.notNull(extensionName, "The extension name cannot be null");
219+
Validate.isTrue(!StringUtils.equals("core", extensionName), "The core protocol cannot be disabled");
159220

160-
enabledFeatures.remove(featureName);
221+
enabledFeatures.remove(extensionName);
161222
updateSupportedHttpMethods();
162223
return this;
163224
}
164225

226+
/**
227+
* Get all HTTP methods that are supported by this TusUploadService based on the enabled and/or disabled
228+
* tus extensions
229+
*
230+
* @return The set of enabled HTTP methods
231+
*/
165232
public Set<HttpMethod> getSupportedHttpMethods() {
166233
return EnumSet.copyOf(supportedHttpMethods);
167234
}
168235

236+
/**
237+
* Get the set of enabled Tus extensions
238+
* @return The set of active extensions
239+
*/
169240
public Set<String> getEnabledFeatures() {
170241
return new LinkedHashSet<>(enabledFeatures.keySet());
171242
}
172243

244+
/**
245+
* Process a tus upload request.
246+
* Use this method to process any request made to the main and sub tus upload endpoints. This corresponds to
247+
* the path specified in the withUploadURI() method and any sub-path of that URI.
248+
*
249+
* @param servletRequest The {@link HttpServletRequest} of the request
250+
* @param servletResponse The {@link HttpServletResponse} of the request
251+
* @throws IOException When saving bytes or information of this requests fails
252+
*/
173253
public void process(HttpServletRequest servletRequest, HttpServletResponse servletResponse)
174254
throws IOException {
175255
process(servletRequest, servletResponse, null);
176256
}
177257

258+
/**
259+
* Process a tus upload request that belongs to a specific owner.
260+
* Use this method to process any request made to the main and sub tus upload endpoints. This corresponds to
261+
* the path specified in the withUploadURI() method and any sub-path of that URI.
262+
*
263+
* @param servletRequest The {@link HttpServletRequest} of the request
264+
* @param servletResponse The {@link HttpServletResponse} of the request
265+
* @param ownerKey A unique identifier of the owner (group) of this upload
266+
* @throws IOException When saving bytes or information of this requests fails
267+
*/
178268
public void process(HttpServletRequest servletRequest, HttpServletResponse servletResponse,
179269
String ownerKey) throws IOException {
180270
Validate.notNull(servletRequest, "The HTTP Servlet request cannot be null");

src/main/java/me/desair/tus/server/creation/validation/PostURIValidator.java

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,31 @@
11
package me.desair.tus.server.creation.validation;
22

3+
import java.util.regex.Matcher;
4+
import java.util.regex.Pattern;
5+
36
import javax.servlet.http.HttpServletRequest;
47

58
import me.desair.tus.server.HttpMethod;
69
import me.desair.tus.server.RequestValidator;
710
import me.desair.tus.server.exception.PostOnInvalidRequestURIException;
811
import me.desair.tus.server.exception.TusException;
912
import me.desair.tus.server.upload.UploadStorageService;
10-
import org.apache.commons.lang3.StringUtils;
1113

1214
/**
1315
* The Client MUST send a POST request against a known upload creation URL to request a new upload resource.
1416
*/
1517
public class PostURIValidator implements RequestValidator {
1618

19+
private Pattern uploadUriPattern = null;
20+
1721
@Override
1822
public void validate(HttpMethod method, HttpServletRequest request,
1923
UploadStorageService uploadStorageService, String ownerKey)
2024
throws TusException {
2125

22-
if (!StringUtils.equals(request.getRequestURI(), uploadStorageService.getUploadURI())) {
26+
Matcher uploadUriMatcher = getUploadUriPattern(uploadStorageService).matcher(request.getRequestURI());
27+
28+
if (!uploadUriMatcher.matches()) {
2329
throw new PostOnInvalidRequestURIException("POST requests have to be send to "
2430
+ uploadStorageService.getUploadURI());
2531
}
@@ -30,4 +36,12 @@ public boolean supports(HttpMethod method) {
3036
return HttpMethod.POST.equals(method);
3137
}
3238

39+
private Pattern getUploadUriPattern(UploadStorageService uploadStorageService) {
40+
if (uploadUriPattern == null) {
41+
//A POST request should match the full URI
42+
uploadUriPattern = Pattern.compile("^" + uploadStorageService.getUploadURI() + "$");
43+
}
44+
return uploadUriPattern;
45+
}
46+
3347
}

src/main/java/me/desair/tus/server/upload/UploadIdFactory.java

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,48 @@
11
package me.desair.tus.server.upload;
22

33
import java.util.UUID;
4+
import java.util.regex.Matcher;
5+
import java.util.regex.Pattern;
46

57
import org.apache.commons.lang3.StringUtils;
68
import org.apache.commons.lang3.Validate;
79

10+
/**
11+
* Factory to create unique upload IDs. This factory can also parse the upload identifier
12+
* from a given upload URL.
13+
*/
814
public class UploadIdFactory {
915

1016
private String uploadURI = "/";
17+
private Pattern uploadUriPattern = null;
1118

19+
/**
20+
* Set the URI under which the main tus upload endpoint is hosted.
21+
* Optionally, this URI may contain regex parameters in order to support endpoints that contain
22+
* URL parameters, for example /users/[0-9]+/files/upload
23+
*
24+
* @param uploadURI The URI of the main tus upload endpoint
25+
*/
1226
public void setUploadURI(String uploadURI) {
13-
Validate.notNull(uploadURI, "The upload URI cannot be null");
27+
Validate.notBlank(uploadURI, "The upload URI pattern cannot be blank");
28+
Validate.isTrue(StringUtils.startsWith(uploadURI, "/"), "The upload URI should start with /");
29+
Validate.isTrue(!StringUtils.endsWith(uploadURI, "$"), "The upload URI should not end with $");
1430
this.uploadURI = uploadURI;
31+
this.uploadUriPattern = null;
1532
}
1633

34+
/**
35+
* Read the upload identifier from the given URL.
36+
* <p/>
37+
* Clients will send requests to upload URLs or provided URLs of completed uploads. This method is able to
38+
* parse those URLs and provide the user with the corresponding upload ID.
39+
*
40+
* @param url The URL provided by the client
41+
* @return The corresponding Upload identifier
42+
*/
1743
public UUID readUploadId(String url) {
18-
String pathId = StringUtils.substringAfter(url, uploadURI + (StringUtils.endsWith(uploadURI, "/") ? "" : "/"));
44+
Matcher uploadUriMatcher = getUploadUriPattern().matcher(StringUtils.trimToEmpty(url));
45+
String pathId = uploadUriMatcher.replaceFirst("");
1946
UUID id = null;
2047

2148
if (StringUtils.isNotBlank(pathId)) {
@@ -29,11 +56,29 @@ public UUID readUploadId(String url) {
2956
return id;
3057
}
3158

59+
/**
60+
* Return the URI of the main tus upload endpoint. Note that this value possibly contains regex parameters.
61+
* @return The URI of the main tus upload endpoint.
62+
*/
3263
public String getUploadURI() {
3364
return uploadURI;
3465
}
3566

67+
/**
68+
* Create a new unique upload ID
69+
* @return A new unique upload ID
70+
*/
3671
public synchronized UUID createId() {
3772
return UUID.randomUUID();
3873
}
74+
75+
private Pattern getUploadUriPattern() {
76+
if (uploadUriPattern == null) {
77+
//We will extract the upload ID's by removing the upload URI from the start of the request URI
78+
uploadUriPattern = Pattern.compile("^.*"
79+
+ uploadURI
80+
+ (StringUtils.endsWith(uploadURI, "/") ? "" : "/?"));
81+
}
82+
return uploadUriPattern;
83+
}
3984
}

src/test/java/me/desair/tus/server/ITTusFileUploadService.java

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1157,20 +1157,20 @@ public void testConcatenationUnfinished() throws Exception {
11571157
}
11581158

11591159
@Test
1160-
public void testChunkedDecodingDisabled() throws Exception {
1160+
public void testChunkedDecodingDisabledAndRegexUploadURI() throws Exception {
11611161
String chunkedContent = "1B;test=value\r\nThis upload looks chunked, \r\n"
11621162
+ "D\r\nbut it's not!\r\n"
11631163
+ "\r\n0\r\n";
11641164

11651165
//Create service without chunked decoding
11661166
tusFileUploadService = new TusFileUploadService()
1167-
.withUploadURI(UPLOAD_URI)
1167+
.withUploadURI("/users/[0-9]+/files/upload")
11681168
.withStoragePath(storagePath.toAbsolutePath().toString())
11691169
.withDownloadFeature();
11701170

11711171
//Create upload
11721172
servletRequest.setMethod("POST");
1173-
servletRequest.setRequestURI(UPLOAD_URI);
1173+
servletRequest.setRequestURI("/users/98765/files/upload");
11741174
servletRequest.addHeader(HttpHeader.CONTENT_LENGTH, 0);
11751175
servletRequest.addHeader(HttpHeader.UPLOAD_LENGTH, "67");
11761176
servletRequest.addHeader(HttpHeader.TUS_RESUMABLE, "1.0.0");
@@ -1183,8 +1183,7 @@ public void testChunkedDecodingDisabled() throws Exception {
11831183
assertResponseHeaderNull(HttpHeader.UPLOAD_EXPIRES);
11841184
assertResponseStatus(HttpServletResponse.SC_CREATED);
11851185

1186-
String location = UPLOAD_URI +
1187-
StringUtils.substringAfter(servletResponse.getHeader(HttpHeader.LOCATION), UPLOAD_URI);
1186+
String location = servletResponse.getHeader(HttpHeader.LOCATION);
11881187

11891188
//Upload content
11901189
reset();

0 commit comments

Comments
 (0)