Skip to content

Commit f8f97d8

Browse files
authored
Merge pull request #18 from tomdesair/feature/generic-upload-id-factory
Support custom UploadIdFactory implementations that are not UUID-based
2 parents e21b148 + a870d63 commit f8f97d8

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+914
-257
lines changed

README.md

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ You can add the latest stable version of this library to your application using
1313
<dependency>
1414
<groupId>me.desair.tus</groupId>
1515
<artifactId>tus-java-server</artifactId>
16-
<version>1.0.0-1.3</version>
16+
<version>1.0.0-2.0</version>
1717
</dependency>
1818

1919
The main entry point of the library is the `me.desair.tus.server.TusFileUploadService.process(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)` method. You can call this method inside a `javax.servlet.http.HttpServlet`, a `javax.servlet.Filter` or any REST API controller of a framework that gives you access to `HttpServletRequest` and `HttpServletResponse` objects. In the following list, you can find some example implementations:
@@ -49,20 +49,25 @@ The first step is to create a `TusFileUploadService` object using its constructo
4949
* `withDownloadFeature()`: Enable the unofficial `download` extension that also allows you to download uploaded bytes.
5050
* `addTusExtension(TusExtension)`: Add a custom (application-specific) extension that implements the `me.desair.tus.server.TusExtension` interface. For example you can add your own extension that checks authentication and authorization policies within your application for the user doing the upload.
5151
* `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.
52+
* `withUploadIdFactory(UploadIdFactory)`: Provide a custom `UploadIdFactory` implementation that should be used to generate identifiers for the different uploads. The default implementation generates identifiers using a UUID (`UUIDUploadIdFactory`). Another example implementation of a custom ID factory is the system-time based `TimeBasedUploadIdFactory` class.
5253

5354

54-
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.
55+
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.
5556

5657
### 2. Processing an upload
5758
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)).
5859

5960
Optionally you can also pass a `String ownerKey` parameter. The `ownerKey` can be used to have a hard separation between uploads of different users, groups or tenants in a multi-tenant setup. Examples of `ownerKey` values are user ID's, group names, client ID's...
6061

6162
### 3. Retrieving the uploaded bytes and metadata within the application
62-
Once the upload has been completed by the user, the business logic layer of your application needs to retrieve and do something with the uploaded bytes. This can be achieved by using the `me.desair.tus.server.TusFileUploadService.getUploadedBytes(String uploadURL)` method. The passed `uploadURL` value should be the upload url used by the client to which the file was uploaded. Therefor your application should pass the upload URL of completed uploads to the backend. Optionally, you can also pass an `ownerKey` value to this method in case your application chooses to process uploads using owner keys. Examples of values that can be used as an `ownerKey` are: an internal user identifier, a session ID, the name of the subpart of your application...
63+
Once the upload has been completed by the user, the business logic layer of your application needs to retrieve and do something with the uploaded bytes. For example it could read the contents of the file, or move the uploaded bytes to their final persistent storage location. Retrieving the uploaded bytes in the backend can be achieved by using the `me.desair.tus.server.TusFileUploadService.getUploadedBytes(String uploadURL)` method. The passed `uploadURL` value should be the upload url used by the client to which the file was uploaded. Therefor your application should pass the upload URL of completed uploads to the backend. Optionally, you can also pass an `ownerKey` value to this method in case your application chooses to process uploads using owner keys. Examples of values that can be used as an `ownerKey` are: an internal user identifier, a session ID, the name of the subpart of your application...
64+
65+
Using the `me.desair.tus.server.TusFileUploadService.getUploadInfo(String uploadURL)` method you can retrieve metadata about a specific upload process. This includes metadata provided by the client as well as metadata kept by the library like creation timestamp, creator ip-address list, upload length... The method `UploadInfo.getId()` will return the unique identifier of this upload encapsulated in an `UploadId` instance. The original (custom generated) identifier object of this upload can be retrieved using `UploadId.getOriginalObject()`. A URL safe string representation of the identifier is returned by `UploadId.toString()`. It is highly recommended to consult the [JavaDoc of both classes](https://tus.desair.me/).
6366

6467
### 4. Upload cleanup
65-
After having processed the uploaded bytes on the server backend, it's important to cleanup the uploaded bytes. This can be done by calling the `me.desair.tus.server.TusFileUploadService.deleteUpload(String uploadURI)` method. This will remove the uploaded bytes and any associated upload information from the storage backend. Alternatively, a client can also remove an (in-progress) upload using the [termination extension](https://tus.io/protocols/resumable-upload.html#termination).
68+
After having processed the uploaded bytes on the server backend (e.g. copy them to their final persistent location), it's important to cleanup the (temporary) uploaded bytes. This can be done by calling the `me.desair.tus.server.TusFileUploadService.deleteUpload(String uploadURI)` method. This will remove the uploaded bytes and any associated upload information from the storage backend. Alternatively, a client can also remove an (in-progress) upload using the [termination extension](https://tus.io/protocols/resumable-upload.html#termination).
69+
70+
Next to removing uploads after they have been completed and processed by the backend, it is also recommended to schedule a regular maintenance task to clean up any expired uploads or locks. Cleaning up expired uploads and locks can be achieved using the `me.desair.tus.server.TusFileUploadService.cleanup()` method.
6671

6772
## Compatible Client Implementations
6873
This tus protocol implementation has been [tested](https://github.com/tomdesair/tus-java-server-spring-demo) with the [Uppy file upload client](https://uppy.io/). This repository also contains [many automated integration tests](https://github.com/tomdesair/tus-java-server/blob/master/src/test/java/me/desair/tus/server/ITTusFileUploadService.java) that validate the tus protocol server implementation using plain HTTP requests. So in theory this means we're compatible with any tus 1.0.0 compliant client.
@@ -71,4 +76,4 @@ This tus protocol implementation has been [tested](https://github.com/tomdesair/
7176
This artifact is versioned as `A.B.C-X.Y` where `A.B.C` is the version of the implemented tus protocol (currently 1.0.0) and `X.Y` is the version of this library.
7277

7378
## Contributing
74-
This library comes without any warranty and is released under a [MIT license](https://github.com/tomdesair/tus-java-server/blob/master/LICENSE). If you encounter any bugs or if you have an idea for a useful improvement you are welcome to [open a new issue](https://github.com/tomdesair/tus-java-server/issues) or to [create a pull request](https://github.com/tomdesair/tus-java-server/pulls) with the proposed implementation. Please note that any contributed code needs to be accompanied by automated unit and/or integration tests and comply with the [define code-style](https://github.com/tomdesair/tus-java-server/blob/master/checkstyle.xml).
79+
This library comes without any warranty and is released under a [MIT license](https://github.com/tomdesair/tus-java-server/blob/master/LICENSE). If you encounter any bugs or if you have an idea for a useful improvement you are welcome to [open a new issue](https://github.com/tomdesair/tus-java-server/issues) or to [create a pull request](https://github.com/tomdesair/tus-java-server/pulls) with the proposed implementation. Please note that any contributed code needs to be accompanied by automated unit and/or integration tests and comply with the [defined code-style](https://github.com/tomdesair/tus-java-server/blob/master/checkstyle.xml).

pom.xml

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
<groupId>me.desair.tus</groupId>
66
<artifactId>tus-java-server</artifactId>
7-
<version>1.0.0-1.4-SNAPSHOT</version>
7+
<version>1.0.0-2.0-SNAPSHOT</version>
88
<packaging>jar</packaging>
99

1010
<name>${project.groupId}:${project.artifactId}</name>
@@ -178,11 +178,8 @@
178178
<configuration>
179179
<!-- Sets the VM argument line used when unit tests are run. -->
180180
<argLine>${surefireArgLine}</argLine>
181-
<!-- Disable SLF4j logging on Maven builds -->
182-
<systemPropertyVariables>
183-
<org.slf4j.simpleLogger.defaultLogLevel>off</org.slf4j.simpleLogger.defaultLogLevel>
184-
<org.slf4j.simpleLogger.showDateTime>true</org.slf4j.simpleLogger.showDateTime>
185-
</systemPropertyVariables>
181+
<!-- Sonar test count workaround -->
182+
<reportsDirectory>${project.build.directory}/surefire-reports</reportsDirectory>
186183
</configuration>
187184
</plugin>
188185

@@ -202,11 +199,6 @@
202199
<argLine>${failsafeArgLine}</argLine>
203200
<!-- Sonar test count workaround -->
204201
<reportsDirectory>${project.build.directory}/surefire-reports</reportsDirectory>
205-
<!-- Disable SLF4j logging on Maven builds -->
206-
<systemPropertyVariables>
207-
<org.slf4j.simpleLogger.defaultLogLevel>off</org.slf4j.simpleLogger.defaultLogLevel>
208-
<org.slf4j.simpleLogger.showDateTime>true</org.slf4j.simpleLogger.showDateTime>
209-
</systemPropertyVariables>
210202
</configuration>
211203
<executions>
212204
<execution>

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,12 @@ public class HttpHeader {
9292
*/
9393
public static final String TUS_CHECKSUM_ALGORITHM = "Tus-Checksum-Algorithm";
9494

95+
/**
96+
* The X-Forwarded-For (XFF) HTTP header field is a common method for identifying the originating IP address of a
97+
* client connecting to a web server through an HTTP proxy or load balancer.
98+
*/
99+
public static final String X_FORWARDED_FOR = "X-Forwarded-For";
100+
95101
private HttpHeader() {
96102
//This is an utility class to hold constants
97103
}

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

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import me.desair.tus.server.exception.TusException;
1818
import me.desair.tus.server.expiration.ExpirationExtension;
1919
import me.desair.tus.server.termination.TerminationExtension;
20+
import me.desair.tus.server.upload.UUIDUploadIdFactory;
2021
import me.desair.tus.server.upload.UploadIdFactory;
2122
import me.desair.tus.server.upload.UploadInfo;
2223
import me.desair.tus.server.upload.UploadLock;
@@ -44,7 +45,7 @@ public class TusFileUploadService {
4445

4546
private UploadStorageService uploadStorageService;
4647
private UploadLockingService uploadLockingService;
47-
private UploadIdFactory idFactory = new UploadIdFactory();
48+
private UploadIdFactory idFactory = new UUIDUploadIdFactory();
4849
private final LinkedHashMap<String, TusExtension> enabledFeatures = new LinkedHashMap<>();
4950
private final Set<HttpMethod> supportedHttpMethods = EnumSet.noneOf(HttpMethod.class);
5051
private boolean isThreadLocalCacheEnabled = false;
@@ -93,6 +94,24 @@ public TusFileUploadService withMaxUploadSize(Long maxUploadSize) {
9394
return this;
9495
}
9596

97+
/**
98+
* Provide a custom {@link UploadIdFactory} implementation that should be used to generate identifiers for
99+
* the different uploads. Example implementation are {@link me.desair.tus.server.upload.UUIDUploadIdFactory} and
100+
* {@link me.desair.tus.server.upload.TimeBasedUploadIdFactory}.
101+
*
102+
* @param uploadIdFactory The custom {@link UploadIdFactory} implementation
103+
* @return The current service
104+
*/
105+
public TusFileUploadService withUploadIdFactory(UploadIdFactory uploadIdFactory) {
106+
Validate.notNull(uploadIdFactory, "The UploadIdFactory cannot be null");
107+
String previousUploadURI = this.idFactory.getUploadURI();
108+
this.idFactory = uploadIdFactory;
109+
this.idFactory.setUploadURI(previousUploadURI);
110+
this.uploadStorageService.setIdFactory(this.idFactory);
111+
this.uploadLockingService.setIdFactory(this.idFactory);
112+
return this;
113+
}
114+
96115
/**
97116
* Provide a custom {@link UploadStorageService} implementation that should be used to store uploaded bytes and
98117
* metadata ({@link UploadInfo}).
@@ -108,7 +127,7 @@ public TusFileUploadService withUploadStorageService(UploadStorageService upload
108127
uploadStorageService.setIdFactory(this.idFactory);
109128
//Update the upload storage service
110129
this.uploadStorageService = uploadStorageService;
111-
prepareCacheIfEnable();
130+
prepareCacheIfEnabled();
112131
return this;
113132
}
114133

@@ -125,7 +144,7 @@ public TusFileUploadService withUploadLockingService(UploadLockingService upload
125144
uploadLockingService.setIdFactory(this.idFactory);
126145
//Update the upload storage service
127146
this.uploadLockingService = uploadLockingService;
128-
prepareCacheIfEnable();
147+
prepareCacheIfEnabled();
129148
return this;
130149
}
131150

@@ -138,9 +157,9 @@ public TusFileUploadService withUploadLockingService(UploadLockingService upload
138157
*/
139158
public TusFileUploadService withStoragePath(String storagePath) {
140159
Validate.notBlank(storagePath, "The storage path cannot be blank");
141-
withUploadStorageService(new DiskStorageService(idFactory, storagePath));
142-
withUploadLockingService(new DiskLockingService(idFactory, storagePath));
143-
prepareCacheIfEnable();
160+
withUploadStorageService(new DiskStorageService(storagePath));
161+
withUploadLockingService(new DiskLockingService(storagePath));
162+
prepareCacheIfEnabled();
144163
return this;
145164
}
146165

@@ -152,7 +171,7 @@ public TusFileUploadService withStoragePath(String storagePath) {
152171
*/
153172
public TusFileUploadService withThreadLocalCache(boolean isEnabled) {
154173
this.isThreadLocalCacheEnabled = isEnabled;
155-
prepareCacheIfEnable();
174+
prepareCacheIfEnabled();
156175
return this;
157176
}
158177

@@ -450,7 +469,7 @@ private void updateSupportedHttpMethods() {
450469
}
451470
}
452471

453-
private void prepareCacheIfEnable() {
472+
private void prepareCacheIfEnabled() {
454473
if (isThreadLocalCacheEnabled && uploadStorageService != null && uploadLockingService != null) {
455474
ThreadLocalCachedStorageAndLockingService service =
456475
new ThreadLocalCachedStorageAndLockingService(
@@ -461,4 +480,5 @@ private void prepareCacheIfEnable() {
461480
this.uploadLockingService = service;
462481
}
463482
}
483+
464484
}

src/main/java/me/desair/tus/server/concatenation/ConcatenationPostRequestHandler.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ public void process(HttpMethod method, TusServletRequest servletRequest,
4444
//reset the length, just to be sure
4545
uploadInfo.setLength(null);
4646
uploadInfo.setUploadType(UploadType.CONCATENATED);
47-
uploadInfo.setConcatenationParts(Utils.parseConcatenationIDsFromHeader(uploadConcatValue));
47+
uploadInfo.setConcatenationPartIds(Utils.parseConcatenationIDsFromHeader(uploadConcatValue));
4848

4949
uploadStorageService.getUploadConcatenationService().merge(uploadInfo);
5050

src/main/java/me/desair/tus/server/core/CorePatchRequestHandler.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,11 @@ public void process(HttpMethod method, TusServletRequest servletRequest,
5555
if (found) {
5656
servletResponse.setHeader(HttpHeader.UPLOAD_OFFSET, Objects.toString(uploadInfo.getOffset()));
5757
servletResponse.setStatus(HttpServletResponse.SC_NO_CONTENT);
58+
59+
if (!uploadInfo.isUploadInProgress()) {
60+
log.info("Upload with ID {} at location {} finished successfully",
61+
uploadInfo.getId(), servletRequest.getRequestURI());
62+
}
5863
} else {
5964
log.error("The patch request handler could not find the upload for URL " + servletRequest.getRequestURI()
6065
+ ". This means something is really wrong the request validators!");

src/main/java/me/desair/tus/server/creation/CreationPostRequestHandler.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,12 @@ public void process(HttpMethod method, TusServletRequest servletRequest,
4747
servletResponse.setHeader(HttpHeader.LOCATION, url);
4848
servletResponse.setStatus(HttpServletResponse.SC_CREATED);
4949

50-
log.debug("Create upload location {}", url);
50+
log.info("Created upload with ID {} at {} for ip address {} with location {}",
51+
info.getId(), info.getCreationTimestamp(), info.getCreatorIpAddresses(), url);
5152
}
5253

5354
private UploadInfo buildUploadInfo(HttpServletRequest servletRequest) {
54-
UploadInfo info = new UploadInfo();
55+
UploadInfo info = new UploadInfo(servletRequest);
5556

5657
Long length = Utils.getLongHeader(servletRequest, HttpHeader.UPLOAD_LENGTH);
5758
if (length != null) {
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package me.desair.tus.server.upload;
2+
3+
import java.io.Serializable;
4+
5+
import org.apache.commons.lang3.StringUtils;
6+
7+
/**
8+
* Alternative {@link UploadIdFactory} implementation that uses the current system time to generate ID's.
9+
* Since time is not unique, this upload ID factory should not be used in busy, clustered production systems.
10+
*/
11+
public class TimeBasedUploadIdFactory extends UploadIdFactory {
12+
13+
@Override
14+
protected Serializable getIdValueIfValid(String extractedUrlId) {
15+
Long id = null;
16+
17+
if (StringUtils.isNotBlank(extractedUrlId)) {
18+
try {
19+
id = Long.parseLong(extractedUrlId);
20+
} catch (NumberFormatException ex) {
21+
id = null;
22+
}
23+
}
24+
25+
return id;
26+
}
27+
28+
@Override
29+
public synchronized UploadId createId() {
30+
return new UploadId(System.currentTimeMillis());
31+
}
32+
33+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package me.desair.tus.server.upload;
2+
3+
import java.io.Serializable;
4+
import java.util.UUID;
5+
6+
/**
7+
* Factory to create unique upload IDs. This factory can also parse the upload identifier
8+
* from a given upload URL.
9+
*/
10+
public class UUIDUploadIdFactory extends UploadIdFactory {
11+
12+
@Override
13+
protected Serializable getIdValueIfValid(String extractedUrlId) {
14+
UUID id = null;
15+
try {
16+
id = UUID.fromString(extractedUrlId);
17+
} catch (IllegalArgumentException ex) {
18+
id = null;
19+
}
20+
21+
return id;
22+
}
23+
24+
@Override
25+
public synchronized UploadId createId() {
26+
return new UploadId(UUID.randomUUID());
27+
}
28+
29+
}

0 commit comments

Comments
 (0)