• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

tomdesair / tus-java-server / 28847441985

07 Jul 2026 06:52AM UTC coverage: 95.749% (+0.9%) from 94.813%
28847441985

Pull #105

github

web-flow
Merge 5d520af31 into 4b2f45149
Pull Request #105: WIP feat: Implement IETF Resumable Uploads for HTTP (RUFH) Protocol

1089 of 1190 branches covered (91.51%)

Branch coverage included in aggregate %.

748 of 759 new or added lines in 37 files covered. (98.55%)

24 existing lines in 3 files now uncovered.

2560 of 2621 relevant lines covered (97.67%)

6.29 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.2
/src/main/java/me/desair/tus/server/TusFileUploadService.java
1
package me.desair.tus.server;
2

3
import jakarta.servlet.http.HttpServletRequest;
4
import jakarta.servlet.http.HttpServletResponse;
5
import java.io.File;
6
import java.io.IOException;
7
import java.io.InputStream;
8
import java.util.EnumSet;
9
import java.util.LinkedHashMap;
10
import java.util.LinkedHashSet;
11
import java.util.Objects;
12
import java.util.Set;
13
import me.desair.tus.server.checksum.ChecksumExtension;
14
import me.desair.tus.server.concatenation.ConcatenationExtension;
15
import me.desair.tus.server.core.CoreProtocol;
16
import me.desair.tus.server.creation.CreationExtension;
17
import me.desair.tus.server.digest.HttpDigestsExtension;
18
import me.desair.tus.server.download.DownloadExtension;
19
import me.desair.tus.server.exception.TusException;
20
import me.desair.tus.server.expiration.ExpirationExtension;
21
import me.desair.tus.server.rufh.ResumableUploadsForHttpProtocol;
22
import me.desair.tus.server.termination.TerminationExtension;
23
import me.desair.tus.server.upload.UploadIdFactory;
24
import me.desair.tus.server.upload.UploadInfo;
25
import me.desair.tus.server.upload.UploadLock;
26
import me.desair.tus.server.upload.UploadLockingService;
27
import me.desair.tus.server.upload.UploadStorageService;
28
import me.desair.tus.server.upload.UuidUploadIdFactory;
29
import me.desair.tus.server.upload.cache.ThreadLocalCachedStorageAndLockingService;
30
import me.desair.tus.server.upload.disk.DiskLockingService;
31
import me.desair.tus.server.upload.disk.DiskStorageService;
32
import me.desair.tus.server.util.TusServletRequest;
33
import me.desair.tus.server.util.TusServletResponse;
34
import org.apache.commons.io.FileUtils;
35
import org.apache.commons.lang3.Strings;
36
import org.apache.commons.lang3.Validate;
37
import org.slf4j.Logger;
38
import org.slf4j.LoggerFactory;
39

40
/** Helper class that implements the server side tus v1.0.0 upload protocol */
41
public class TusFileUploadService {
42

43
  public static final String TUS_API_VERSION = "1.0.0";
44

45
  private static final Logger log = LoggerFactory.getLogger(TusFileUploadService.class);
8✔
46

47
  private UploadStorageService uploadStorageService;
48
  private UploadLockingService uploadLockingService;
49
  private UploadIdFactory idFactory = new UuidUploadIdFactory();
10✔
50
  private final LinkedHashMap<String, TusExtension> enabledFeatures = new LinkedHashMap<>();
10✔
51
  private final Set<HttpMethod> supportedHttpMethods = EnumSet.noneOf(HttpMethod.class);
8✔
52
  private boolean isThreadLocalCacheEnabled = false;
6✔
53
  private boolean isChunkedTransferDecodingEnabled = false;
6✔
54
  private ProtocolVersion supportedProtocolVersion = ProtocolVersion.AUTO;
6✔
55

56
  /** Constructor. */
57
  public TusFileUploadService() {
4✔
58
    String storagePath = FileUtils.getTempDirectoryPath() + File.separator + "tus";
8✔
59
    this.uploadStorageService = new DiskStorageService(idFactory, storagePath);
16✔
60
    this.uploadLockingService = new DiskLockingService(idFactory, storagePath);
16✔
61
    initFeatures();
4✔
62
  }
2✔
63

64
  protected void initFeatures() {
65
    // The order of the features is important
66
    addTusExtension(new CoreProtocol());
12✔
67
    addTusExtension(new CreationExtension());
12✔
68
    addTusExtension(new ChecksumExtension());
12✔
69
    addTusExtension(new TerminationExtension());
12✔
70
    addTusExtension(new ExpirationExtension());
12✔
71
    addTusExtension(new ConcatenationExtension());
12✔
72
    addTusExtension(new ResumableUploadsForHttpProtocol());
12✔
73
    addTusExtension(new HttpDigestsExtension());
12✔
74
  }
2✔
75

76
  /**
77
   * Configure the supported protocol version(s) for this service.
78
   *
79
   * @param supportedProtocolVersion ProtocolVersion setting (TUS_1_0_0, RUFH, or AUTO)
80
   * @return The current service
81
   */
82
  public TusFileUploadService withSupportedProtocolVersions(
83
      ProtocolVersion supportedProtocolVersion) {
84
    if (supportedProtocolVersion != null) {
2✔
85
      this.supportedProtocolVersion = supportedProtocolVersion;
3✔
86
    }
87
    return this;
2✔
88
  }
89

90
  /**
91
   * Get the configured protocol version setting.
92
   *
93
   * @return Current ProtocolVersion configuration
94
   */
95
  public ProtocolVersion getSupportedProtocolVersion() {
96
    return supportedProtocolVersion;
3✔
97
  }
98

99
  /**
100
   * Set the URI under which the main tus upload endpoint is hosted. Optionally, this URI may
101
   * contain regex parameters in order to support endpoints that contain URL parameters, for example
102
   * /users/[0-9]+/files/upload
103
   *
104
   * @param uploadUri The URI of the main tus upload endpoint
105
   * @return The current service
106
   */
107
  public TusFileUploadService withUploadUri(String uploadUri) {
108
    this.idFactory.setUploadUri(uploadUri);
8✔
109
    return this;
4✔
110
  }
111

112
  /**
113
   * Specify the maximum number of bytes that can be uploaded per upload. If you don't call this
114
   * method, the maximum number of bytes is Long.MAX_VALUE.
115
   *
116
   * @param maxUploadSize The maximum upload length that is allowed
117
   * @return The current service
118
   */
119
  public TusFileUploadService withMaxUploadSize(Long maxUploadSize) {
120
    Validate.exclusiveBetween(
4✔
121
        0, Long.MAX_VALUE, maxUploadSize, "The max upload size must be bigger than 0");
2✔
122
    this.uploadStorageService.setMaxUploadSize(maxUploadSize);
4✔
123
    return this;
2✔
124
  }
125

126
  /**
127
   * Set the maximum size allowed for an individual upload append (PATCH) request in IETF Resumable
128
   * Uploads.
129
   *
130
   * @param maxAppendSize The maximum append size limit in bytes
131
   * @return The current service
132
   */
133
  public TusFileUploadService withMaxAppendSize(Long maxAppendSize) {
134
    if (maxAppendSize != null) {
2✔
135
      Validate.exclusiveBetween(
4✔
136
          0, Long.MAX_VALUE, maxAppendSize, "The max append size must be bigger than 0");
2✔
137
    }
138
    this.uploadStorageService.setMaxAppendSize(maxAppendSize);
4✔
139
    return this;
2✔
140
  }
141

142
  /**
143
   * Provide a custom {@link UploadIdFactory} implementation that should be used to generate
144
   * identifiers for the different uploads. Example implementation are {@link
145
   * me.desair.tus.server.upload.UuidUploadIdFactory} and {@link
146
   * me.desair.tus.server.upload.TimeBasedUploadIdFactory}.
147
   *
148
   * @param uploadIdFactory The custom {@link UploadIdFactory} implementation
149
   * @return The current service
150
   */
151
  public TusFileUploadService withUploadIdFactory(UploadIdFactory uploadIdFactory) {
152
    Objects.requireNonNull(uploadIdFactory, "The UploadIdFactory cannot be null");
4✔
153
    String previousUploadUri = this.idFactory.getUploadUri();
4✔
154
    this.idFactory = uploadIdFactory;
3✔
155
    this.idFactory.setUploadUri(previousUploadUri);
4✔
156
    this.uploadStorageService.setIdFactory(this.idFactory);
5✔
157
    this.uploadLockingService.setIdFactory(this.idFactory);
5✔
158
    return this;
2✔
159
  }
160

161
  /**
162
   * Provide a custom {@link UploadStorageService} implementation that should be used to store
163
   * uploaded bytes and metadata ({@link UploadInfo}).
164
   *
165
   * @param uploadStorageService The custom {@link UploadStorageService} implementation
166
   * @return The current service
167
   */
168
  public TusFileUploadService withUploadStorageService(UploadStorageService uploadStorageService) {
169
    Objects.requireNonNull(uploadStorageService, "The UploadStorageService cannot be null");
8✔
170
    // Copy over any previous configuration
171
    uploadStorageService.setMaxUploadSize(this.uploadStorageService.getMaxUploadSize());
12✔
172
    uploadStorageService.setMaxAppendSize(this.uploadStorageService.getMaxAppendSize());
10✔
173
    uploadStorageService.setUploadExpirationPeriod(
8✔
174
        this.uploadStorageService.getUploadExpirationPeriod());
2✔
175
    uploadStorageService.setIdFactory(this.idFactory);
8✔
176
    // Update the upload storage service
177
    this.uploadStorageService = uploadStorageService;
6✔
178
    prepareCacheIfEnabled();
4✔
179
    return this;
4✔
180
  }
181

182
  /**
183
   * Get the current {@link UploadStorageService} configured on this service.
184
   *
185
   * @return The current {@link UploadStorageService}
186
   */
187
  public UploadStorageService getUploadStorageService() {
188
    return this.uploadStorageService;
3✔
189
  }
190

191
  /**
192
   * Provide a custom {@link UploadLockingService} implementation that should be used when
193
   * processing uploads. The upload locking service is responsible for locking an upload that is
194
   * being processed so that it cannot be corrupted by simultaneous or delayed requests.
195
   *
196
   * @param uploadLockingService The {@link UploadLockingService} implementation to use
197
   * @return The current service
198
   */
199
  public TusFileUploadService withUploadLockingService(UploadLockingService uploadLockingService) {
200
    Objects.requireNonNull(uploadLockingService, "The UploadStorageService cannot be null");
8✔
201
    uploadLockingService.setIdFactory(this.idFactory);
8✔
202
    // Update the upload storage service
203
    this.uploadLockingService = uploadLockingService;
6✔
204
    prepareCacheIfEnabled();
4✔
205
    return this;
4✔
206
  }
207

208
  /**
209
   * If you're using the default file system-based storage service, you can use this method to
210
   * specify the path where to store the uploaded bytes and upload information.
211
   *
212
   * @param storagePath The file system path where uploads can be stored (temporarily)
213
   * @return The current service
214
   */
215
  public TusFileUploadService withStoragePath(String storagePath) {
216
    Validate.notBlank(storagePath, "The storage path cannot be blank");
12✔
217
    withUploadStorageService(new DiskStorageService(storagePath));
14✔
218
    withUploadLockingService(new DiskLockingService(storagePath));
14✔
219
    prepareCacheIfEnabled();
4✔
220
    return this;
4✔
221
  }
222

223
  /**
224
   * Enable or disable a thread-local based cache of upload data. This can reduce the load on the
225
   * storage backends. By default this cache is disabled.
226
   *
227
   * @param isEnabled True if the cache should be enabled, false otherwise
228
   * @return The current service
229
   */
230
  public TusFileUploadService withThreadLocalCache(boolean isEnabled) {
231
    this.isThreadLocalCacheEnabled = isEnabled;
3✔
232
    prepareCacheIfEnabled();
2✔
233
    return this;
2✔
234
  }
235

236
  /**
237
   * Instruct this service to (not) decode any requests with Transfer-Encoding value "chunked". Use
238
   * this method in case the web container in which this service is running does not decode chunked
239
   * transfers itself. By default, chunked decoding is disabled.
240
   *
241
   * @param isEnabled True if chunked requests should be decoded, false otherwise.
242
   * @return The current service
243
   */
244
  public TusFileUploadService withChunkedTransferDecoding(boolean isEnabled) {
245
    isChunkedTransferDecodingEnabled = isEnabled;
3✔
246
    return this;
2✔
247
  }
248

249
  /**
250
   * You can set the number of milliseconds after which an upload is considered as expired and
251
   * available for cleanup.
252
   *
253
   * @param expirationPeriod The number of milliseconds after which an upload expires and can be
254
   *     removed
255
   * @return The current service
256
   */
257
  public TusFileUploadService withUploadExpirationPeriod(Long expirationPeriod) {
258
    uploadStorageService.setUploadExpirationPeriod(expirationPeriod);
4✔
259
    return this;
2✔
260
  }
261

262
  /**
263
   * Enable the unofficial `download` extension that also allows you to download uploaded bytes. By
264
   * default this feature is disabled.
265
   *
266
   * @return The current service
267
   */
268
  public TusFileUploadService withDownloadFeature() {
269
    addTusExtension(new DownloadExtension());
6✔
270
    return this;
2✔
271
  }
272

273
  /**
274
   * Enable or disable duplicate file processing based on checksum hash.
275
   *
276
   * @param isEnabled True if duplicate file processing should be enabled, false otherwise
277
   * @return The current service
278
   */
279
  public TusFileUploadService withUploadDeduplication(boolean isEnabled) {
280
    this.uploadStorageService.setUploadDeduplicationEnabled(isEnabled);
8✔
281
    return this;
4✔
282
  }
283

284
  /**
285
   * Add a custom (application-specific) extension that implements the {@link
286
   * me.desair.tus.server.TusExtension} interface. For example you can add your own extension that
287
   * checks authentication and authorization policies within your application for the user doing the
288
   * upload.
289
   *
290
   * @param feature The custom extension implementation
291
   * @return The current service
292
   */
293
  public TusFileUploadService addTusExtension(TusExtension feature) {
294
    Objects.requireNonNull(feature, "A custom feature cannot be null");
8✔
295
    enabledFeatures.put(feature.getName(), feature);
14✔
296
    updateSupportedHttpMethods();
4✔
297
    return this;
4✔
298
  }
299

300
  /**
301
   * Disable the TusExtension for which the getName() method matches the provided string. The
302
   * default extensions have names "creation", "checksum", "expiration", "concatenation",
303
   * "termination" and "download". You cannot disable the "core" feature.
304
   *
305
   * @param extensionName The name of the extension to disable
306
   * @return The current service
307
   */
308
  public TusFileUploadService disableTusExtension(String extensionName) {
309
    Objects.requireNonNull(extensionName, "The extension name cannot be null");
4✔
310
    Validate.isTrue(
4✔
311
        !Strings.CS.equals("core", extensionName), "The core protocol cannot be disabled");
8✔
312

313
    enabledFeatures.remove(extensionName);
5✔
314
    updateSupportedHttpMethods();
2✔
315
    return this;
2✔
316
  }
317

318
  /**
319
   * Get all HTTP methods that are supported by this TusUploadService based on the enabled and/or
320
   * disabled tus extensions.
321
   *
322
   * @return The set of enabled HTTP methods
323
   */
324
  public Set<HttpMethod> getSupportedHttpMethods() {
325
    return EnumSet.copyOf(supportedHttpMethods);
4✔
326
  }
327

328
  /**
329
   * Get the set of enabled Tus extensions.
330
   *
331
   * @return The set of active extensions
332
   */
333
  public Set<String> getEnabledFeatures() {
334
    return new LinkedHashSet<>(enabledFeatures.keySet());
7✔
335
  }
336

337
  /**
338
   * Process a tus upload request. Use this method to process any request made to the main and sub
339
   * tus upload endpoints. This corresponds to the path specified in the withUploadUri() method and
340
   * any sub-path of that URI.
341
   *
342
   * @param servletRequest The {@link HttpServletRequest} of the request
343
   * @param servletResponse The {@link HttpServletResponse} of the request
344
   * @throws IOException When saving bytes or information of this requests fails
345
   */
346
  public void process(HttpServletRequest servletRequest, HttpServletResponse servletResponse)
347
      throws IOException {
348
    process(servletRequest, servletResponse, null);
5✔
349
  }
1✔
350

351
  /**
352
   * Process a tus upload request that belongs to a specific owner. Use this method to process any
353
   * request made to the main and sub tus upload endpoints. This corresponds to the path specified
354
   * in the withUploadUri() method and any sub-path of that URI.
355
   *
356
   * @param servletRequest The {@link HttpServletRequest} of the request
357
   * @param servletResponse The {@link HttpServletResponse} of the request
358
   * @param ownerKey A unique identifier of the owner (group) of this upload
359
   * @throws IOException When saving bytes or information of this requests fails
360
   */
361
  public void process(
362
      HttpServletRequest servletRequest, HttpServletResponse servletResponse, String ownerKey)
363
      throws IOException {
364
    Objects.requireNonNull(servletRequest, "The HTTP Servlet request cannot be null");
8✔
365
    Objects.requireNonNull(servletResponse, "The HTTP Servlet response cannot be null");
8✔
366

367
    HttpMethod method = HttpMethod.getMethodIfSupported(servletRequest, supportedHttpMethods);
10✔
368

369
    log.debug(
10✔
370
        "Processing request with method {} and URL {}", method, servletRequest.getRequestURL());
2✔
371

372
    TusServletRequest request =
14✔
373
        new TusServletRequest(servletRequest, isChunkedTransferDecodingEnabled);
374
    TusServletResponse response = new TusServletResponse(servletResponse);
10✔
375

376
    try (UploadLock lock = acquireUploadLock(method, request.getRequestURI())) {
12✔
377

378
      processLockedRequest(method, request, response, ownerKey);
12✔
379

380
    } catch (TusException e) {
2✔
381
      log.error("Unable to lock upload for request URI " + request.getRequestURI(), e);
12✔
382
      response.sendError(e.getStatus(), e.getMessage());
12✔
383
    }
2✔
384
  }
2✔
385

386
  protected UploadLock acquireUploadLock(HttpMethod method, String requestUri)
387
      throws TusException, IOException {
388
    UploadLock lock = null;
4✔
389
    int retries = 0;
4✔
390
    while (retries < 25) {
6✔
391
      try {
392
        lock = uploadLockingService.lockUploadByUri(requestUri);
10✔
393
        break;
2✔
394
      } catch (TusException e) {
2✔
395
        if (HttpMethod.HEAD.equals(method) || HttpMethod.DELETE.equals(method)) {
16✔
396
          uploadLockingService.requestLockRelease(requestUri);
8✔
397
          retries++;
2✔
398
          try {
399
            Thread.sleep(200L);
4✔
400
          } catch (InterruptedException ie) {
1✔
401
            Thread.currentThread().interrupt();
2✔
402
            throw new IOException("Lock acquisition retry interrupted", ie);
6✔
403
          }
2✔
404
        } else {
405
          throw e;
4✔
406
        }
407
      }
2✔
408
    }
409
    if (lock == null) {
4✔
410
      lock = uploadLockingService.lockUploadByUri(requestUri);
10✔
411
    }
412
    return lock;
4✔
413
  }
414

415
  /**
416
   * Method to retrieve the bytes that were uploaded to a specific upload URI.
417
   *
418
   * @param uploadUri The URI of the upload
419
   * @return An {@link InputStream} that will stream the uploaded bytes
420
   * @throws IOException When the retreiving the uploaded bytes fails
421
   * @throws TusException When the upload is still in progress or cannot be found
422
   */
423
  public InputStream getUploadedBytes(String uploadUri) throws IOException, TusException {
UNCOV
424
    return getUploadedBytes(uploadUri, null);
×
425
  }
426

427
  /**
428
   * Method to retrieve the bytes that were uploaded to a specific upload URI.
429
   *
430
   * @param uploadUri The URI of the upload
431
   * @param ownerKey The key of the owner of this upload
432
   * @return An {@link InputStream} that will stream the uploaded bytes
433
   * @throws IOException When the retreiving the uploaded bytes fails
434
   * @throws TusException When the upload is still in progress or cannot be found
435
   */
436
  public InputStream getUploadedBytes(String uploadUri, String ownerKey)
437
      throws IOException, TusException {
438

439
    try (UploadLock lock = uploadLockingService.lockUploadByUri(uploadUri)) {
5✔
440

441
      return uploadStorageService.getUploadedBytes(uploadUri, ownerKey);
8✔
442
    }
443
  }
444

445
  /**
446
   * Get the information on the upload corresponding to the given upload URI.
447
   *
448
   * @param uploadUri The URI of the upload
449
   * @return Information on the upload
450
   * @throws IOException When retrieving the upload information fails
451
   * @throws TusException When the upload is still in progress or cannot be found
452
   */
453
  public UploadInfo getUploadInfo(String uploadUri) throws IOException, TusException {
UNCOV
454
    return getUploadInfo(uploadUri, null);
×
455
  }
456

457
  /**
458
   * Get the information on the upload corresponding to the given upload URI.
459
   *
460
   * @param uploadUri The URI of the upload
461
   * @param ownerKey The key of the owner of this upload
462
   * @return Information on the upload
463
   * @throws IOException When retrieving the upload information fails
464
   * @throws TusException When the upload is still in progress or cannot be found
465
   */
466
  public UploadInfo getUploadInfo(String uploadUri, String ownerKey)
467
      throws IOException, TusException {
468
    try (UploadLock lock = uploadLockingService.lockUploadByUri(uploadUri)) {
10✔
469

470
      return uploadStorageService.getUploadInfo(uploadUri, ownerKey);
16✔
471
    }
472
  }
473

474
  /**
475
   * Method to delete an upload associated with the given upload URL. Invoke this method if you no
476
   * longer need the upload.
477
   *
478
   * @param uploadUri The upload URI
479
   */
480
  public void deleteUpload(String uploadUri) throws IOException, TusException {
UNCOV
481
    deleteUpload(uploadUri, null);
×
UNCOV
482
  }
×
483

484
  /**
485
   * Method to delete an upload associated with the given upload URL. Invoke this method if you no
486
   * longer need the upload.
487
   *
488
   * @param uploadUri The upload URI
489
   * @param ownerKey The key of the owner of this upload
490
   */
491
  public void deleteUpload(String uploadUri, String ownerKey) throws IOException, TusException {
492
    try (UploadLock lock = uploadLockingService.lockUploadByUri(uploadUri)) {
5✔
493
      UploadInfo uploadInfo = uploadStorageService.getUploadInfo(uploadUri, ownerKey);
6✔
494
      if (uploadInfo != null) {
2!
495
        uploadStorageService.terminateUpload(uploadInfo);
4✔
496
      }
497
    }
498
  }
1✔
499

500
  /**
501
   * This method should be invoked periodically. It will cleanup any expired uploads and stale locks
502
   *
503
   * @throws IOException When cleaning fails
504
   */
505
  public void cleanup() throws IOException {
506
    uploadLockingService.cleanupStaleLocks();
3✔
507
    uploadStorageService.cleanupExpiredUploads(uploadLockingService);
5✔
508
  }
1✔
509

510
  protected void processLockedRequest(
511
      HttpMethod method, TusServletRequest request, TusServletResponse response, String ownerKey)
512
      throws IOException {
513
    ProtocolVersion detectedVersion = detectProtocolVersion(request);
8✔
514

515
    try {
516
      validateRequest(method, request, ownerKey, detectedVersion);
12✔
517

518
      executeProcessingByFeatures(method, request, response, ownerKey, detectedVersion);
14✔
519

520
    } catch (TusException e) {
2✔
521
      processTusException(method, request, response, ownerKey, e, detectedVersion);
16✔
522
    }
2✔
523
  }
2✔
524

525
  public ProtocolVersion detectProtocolVersion(HttpServletRequest request) {
526
    if (supportedProtocolVersion == ProtocolVersion.TUS_1_0_0) {
8✔
527
      return ProtocolVersion.TUS_1_0_0;
2✔
528
    }
529
    if (supportedProtocolVersion == ProtocolVersion.RUFH) {
8✔
530
      return ProtocolVersion.RUFH;
2✔
531
    }
532

533
    // We're in AUTO mode, so we need to detect the protocol version based on the request headers
534
    if (request != null) {
4✔
535
      if (request.getHeader(HttpHeader.TUS_RESUMABLE) != null) {
8✔
536
        return ProtocolVersion.TUS_1_0_0;
4✔
537
      }
538
      if (request.getHeader(HttpHeader.UPLOAD_COMPLETE) != null
12✔
539
          || request.getHeader(HttpHeader.UPLOAD_DRAFT) != null
8✔
540
          || request.getHeader("upload-draft-interop-version") != null
10✔
541
          || Strings.CS.startsWith(
4✔
542
              request.getHeader(HttpHeader.CONTENT_TYPE), HttpHeader.CONTENT_TYPE_PARTIAL_UPLOAD)) {
4✔
543
        return ProtocolVersion.RUFH;
2✔
544
      }
545
    }
546
    return ProtocolVersion.TUS_1_0_0;
4✔
547
  }
548

549
  protected void executeProcessingByFeatures(
550
      HttpMethod method,
551
      TusServletRequest servletRequest,
552
      TusServletResponse servletResponse,
553
      String ownerKey,
554
      ProtocolVersion version)
555
      throws IOException, TusException {
556

557
    for (TusExtension feature : enabledFeatures.values()) {
24✔
558
      if (!servletRequest.isProcessedBy(feature)) {
8!
559
        servletRequest.addProcessor(feature);
6✔
560
        feature.process(
22✔
561
            method,
562
            servletRequest,
563
            servletResponse,
564
            uploadStorageService,
565
            uploadLockingService,
566
            ownerKey,
567
            version);
568
      }
569
    }
2✔
570
  }
2✔
571

572
  protected void validateRequest(
573
      HttpMethod method,
574
      HttpServletRequest servletRequest,
575
      String ownerKey,
576
      ProtocolVersion version)
577
      throws TusException, IOException {
578

579
    for (TusExtension feature : enabledFeatures.values()) {
24✔
580
      feature.validate(
20✔
581
          method, servletRequest, uploadStorageService, uploadLockingService, ownerKey, version);
582
    }
2✔
583
  }
2✔
584

585
  protected void processTusException(
586
      HttpMethod method,
587
      TusServletRequest request,
588
      TusServletResponse response,
589
      String ownerKey,
590
      TusException exception,
591
      ProtocolVersion version)
592
      throws IOException {
593

594
    int status = exception.getStatus();
6✔
595
    String message = exception.getMessage();
6✔
596

597
    log.warn(
24✔
598
        "Unable to process request {} {}. Sent response status {} with message \"{}\"",
599
        method,
600
        request.getRequestURL(),
10✔
601
        status,
12✔
602
        message);
603

604
    response.setStatus(status);
6✔
605

606
    HttpProblemDetails problemDetails = null;
4✔
607
    try {
608
      for (TusExtension feature : enabledFeatures.values()) {
24✔
609
        if (!request.isProcessedBy(feature) || feature.mustReprocessOnError(method, version)) {
18✔
610
          request.addProcessor(feature);
6✔
611
          HttpProblemDetails pd =
22✔
612
              feature.handleError(
4✔
613
                  method,
614
                  request,
615
                  response,
616
                  uploadStorageService,
617
                  uploadLockingService,
618
                  ownerKey,
619
                  version,
620
                  exception);
621
          if (pd != null) {
4✔
622
            problemDetails = pd;
2✔
623
          }
624
        }
625
      }
2✔
626

627
      // Since an error occurred, the bytes we have written are probably not valid. So remove them.
628
      UploadInfo uploadInfo = uploadStorageService.getUploadInfo(request.getRequestURI(), ownerKey);
14✔
629
      uploadStorageService.removeLastNumberOfBytes(uploadInfo, request.getBytesRead());
12✔
630

UNCOV
631
    } catch (TusException ex) {
×
UNCOV
632
      log.warn("An exception occurred while handling another exception", ex);
×
633
    }
2✔
634

635
    // If one of the features has already committed a response, we don't need to send an error
636
    // response. Otherwise, we send the error response with the status and message from the
637
    // exception.
638
    if (!response.isCommitted()) {
6✔
639
      if (problemDetails != null) {
4✔
640
        problemDetails.writeTo(response);
4✔
641
      } else {
642
        response.sendError(status, message);
8✔
643
      }
644
    }
645
  }
2✔
646

647
  private void updateSupportedHttpMethods() {
648
    supportedHttpMethods.clear();
6✔
649
    for (TusExtension tusFeature : enabledFeatures.values()) {
24✔
650
      supportedHttpMethods.addAll(tusFeature.getMinimalSupportedHttpMethods());
12✔
651
    }
2✔
652
  }
2✔
653

654
  private void prepareCacheIfEnabled() {
655
    if (isThreadLocalCacheEnabled && uploadStorageService != null && uploadLockingService != null) {
12!
656
      ThreadLocalCachedStorageAndLockingService service =
8✔
657
          new ThreadLocalCachedStorageAndLockingService(uploadStorageService, uploadLockingService);
658
      service.setIdFactory(this.idFactory);
4✔
659
      this.uploadStorageService = service;
3✔
660
      this.uploadLockingService = service;
3✔
661
    }
662
  }
2✔
663
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc