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

tomdesair / tus-java-server / 30762898309

02 Aug 2026 07:13PM UTC coverage: 95.854% (-0.6%) from 96.487%
30762898309

Pull #111

github

web-flow
Merge 006364903 into 94d541272
Pull Request #111: feat: add S3 storage, locking, concatenation, and JSON metadata serialization

1714 of 1894 branches covered (90.5%)

Branch coverage included in aggregate %.

895 of 910 new or added lines in 14 files covered. (98.35%)

3835 of 3895 relevant lines covered (98.46%)

6.85 hits per line

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

94.38
/src/main/java/me/desair/tus/server/util/Utils.java
1
package me.desair.tus.server.util;
2

3
import static java.nio.file.StandardOpenOption.CREATE;
4
import static java.nio.file.StandardOpenOption.READ;
5
import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING;
6
import static java.nio.file.StandardOpenOption.WRITE;
7

8
import jakarta.servlet.http.HttpServletRequest;
9
import java.io.BufferedOutputStream;
10
import java.io.IOException;
11
import java.io.ObjectOutput;
12
import java.io.ObjectOutputStream;
13
import java.io.OutputStream;
14
import java.io.Serializable;
15
import java.nio.channels.Channels;
16
import java.nio.channels.FileChannel;
17
import java.nio.channels.FileLock;
18
import java.nio.file.Path;
19
import java.util.EnumSet;
20
import java.util.LinkedList;
21
import java.util.List;
22
import java.util.regex.Pattern;
23
import me.desair.tus.server.HttpHeader;
24
import me.desair.tus.server.HttpMethod;
25
import me.desair.tus.server.ProtocolVersion;
26
import me.desair.tus.server.checksum.ChecksumAlgorithm;
27
import me.desair.tus.server.upload.UploadInfo;
28
import me.desair.tus.server.upload.UploadStorageService;
29
import org.apache.commons.io.serialization.ValidatingObjectInputStream;
30
import org.apache.commons.lang3.StringUtils;
31
import org.apache.commons.lang3.Strings;
32
import org.slf4j.Logger;
33
import org.slf4j.LoggerFactory;
34

35
/** Utility class that contains various static helper methods */
36
public class Utils {
37

38
  private static final Logger log = LoggerFactory.getLogger(Utils.class);
6✔
39
  private static final int LOCK_FILE_RETRY_COUNT = 3;
40
  private static final long LOCK_FILE_SLEEP_TIME = 500;
41
  private static final Pattern CHECKSUM_VALUE_PATTERN = Pattern.compile("^[a-zA-Z0-9+/=\\-_]+$");
8✔
42

43
  private Utils() {
44
    // This is a utility class that only holds static utility methods
45
  }
46

47
  public static String getHeader(HttpServletRequest request, String header) {
48
    return StringUtils.trimToEmpty(request.getHeader(header));
10✔
49
  }
50

51
  public static Long getLongHeader(HttpServletRequest request, String header) {
52
    try {
53
      return Long.valueOf(getHeader(request, header));
10✔
54
    } catch (NumberFormatException ex) {
2✔
55
      return null;
4✔
56
    }
57
  }
58

59
  /**
60
   * Build a comma-separated list based on the remote address of the request and the
61
   * X-Forwareded-For header. The list is constructed as "client, proxy1, proxy2".
62
   *
63
   * @return A comma-separated list of ip-addresses
64
   */
65
  public static String buildRemoteIpList(HttpServletRequest servletRequest) {
66
    String ipAddresses = servletRequest.getRemoteAddr();
6✔
67
    String xforwardedForHeader = getHeader(servletRequest, HttpHeader.X_FORWARDED_FOR);
8✔
68
    if (xforwardedForHeader.length() > 0) {
6✔
69
      ipAddresses = xforwardedForHeader + ", " + ipAddresses;
8✔
70
    }
71
    return ipAddresses;
4✔
72
  }
73

74
  public static List<String> parseConcatenationIDsFromHeader(String uploadConcatValue) {
75
    List<String> output = new LinkedList<>();
8✔
76

77
    String idString = StringUtils.substringAfter(uploadConcatValue, ";");
8✔
78
    for (String id : StringUtils.trimToEmpty(idString).split("\\s")) {
38✔
79
      output.add(id);
8✔
80
    }
81

82
    return output;
4✔
83
  }
84

85
  public static <T> T readSerializable(Path path, Class<T> clazz) throws IOException {
86
    T info = null;
4✔
87
    if (path != null) {
4✔
88
      try (FileChannel channel = FileChannel.open(path, READ)) {
18✔
89
        // Lock will be released when the channel is closed
90
        if (lockFileShared(channel) != null) {
6!
91

92
          try (ValidatingObjectInputStream ois =
6✔
93
              new ValidatingObjectInputStream(Channels.newInputStream(channel))) {
6✔
94
            ois.accept("java.lang.*", "java.util.*", "me.desair.tus.server.*");
34✔
95
            info = clazz.cast(ois.readObject());
10✔
96
          } catch (ClassNotFoundException
1✔
97
              | java.io.EOFException
98
              | java.io.StreamCorruptedException e) {
99
            // File may be corrupted due to unexpected server shutdown
100
            log.warn("Unable to read serializable file {}: {}", path, e.getMessage());
6✔
101
            info = null;
2✔
102
          }
3✔
103
        } else {
104
          throw new IOException("Unable to lock file " + path);
×
105
        }
106
      }
107
    }
108
    return info;
4✔
109
  }
110

111
  public static void writeSerializable(Serializable object, Path path) throws IOException {
112
    if (path != null) {
4✔
113
      try (FileChannel channel = FileChannel.open(path, WRITE, CREATE, TRUNCATE_EXISTING)) {
34✔
114
        // Lock will be released when the channel is closed
115
        if (lockFileExclusively(channel) != null) {
6!
116

117
          try (OutputStream buffer = new BufferedOutputStream(Channels.newOutputStream(channel));
14✔
118
              ObjectOutput output = new ObjectOutputStream(buffer)) {
10✔
119

120
            output.writeObject(object);
6✔
121
          }
122
        } else {
123
          throw new IOException("Unable to lock file " + path);
×
124
        }
125
      }
126
    }
127
  }
2✔
128

129
  /**
130
   * Reads an object from a JSON file on disk, acquiring a shared file lock during the read
131
   * operation.
132
   *
133
   * @param <T> Target object type
134
   * @param path The file path to read from
135
   * @param clazz The target object class
136
   * @return Deserialized object instance, or null if reading fails or file does not exist
137
   * @throws IOException If file access or locking fails
138
   */
139
  public static <T> T readJson(Path path, Class<T> clazz) throws IOException {
140
    T info = null;
2✔
141
    if (path != null && java.nio.file.Files.exists(path)) {
7!
142
      try (FileChannel channel = FileChannel.open(path, READ)) {
9✔
143
        // Lock will be released when the channel is closed
144
        if (lockFileShared(channel) != null) {
3!
145
          try (java.io.InputStream is = Channels.newInputStream(channel)) {
3✔
146
            info = UploadInfoJsonSerializer.deserialize(is, clazz);
4✔
147
          } catch (Exception e) {
1✔
148
            log.warn("Unable to read JSON file {}: {}", path, e.getMessage());
6✔
149
            info = null;
2✔
150
          }
2✔
151
        } else {
NEW
152
          throw new IOException("Unable to lock file " + path);
×
153
        }
154
      }
155
    }
156
    return info;
2✔
157
  }
158

159
  /**
160
   * Writes an object to a file in JSON format, acquiring an exclusive file lock during the write
161
   * operation.
162
   *
163
   * @param object The object to serialize to JSON
164
   * @param path The file path to write to
165
   * @throws IOException If file access or locking fails
166
   */
167
  public static void writeJson(Object object, Path path) throws IOException {
168
    if (path != null) {
2!
169
      try (FileChannel channel = FileChannel.open(path, WRITE, CREATE, TRUNCATE_EXISTING)) {
17✔
170
        // Lock will be released when the channel is closed
171
        if (lockFileExclusively(channel) != null) {
3!
172
          try (OutputStream buffer = new BufferedOutputStream(Channels.newOutputStream(channel))) {
7✔
173
            UploadInfoJsonSerializer.serializeToStream(object, buffer);
3✔
174
          }
175
        } else {
NEW
176
          throw new IOException("Unable to lock file " + path);
×
177
        }
178
      }
179
    }
180
  }
1✔
181

182
  public static FileLock lockFileExclusively(FileChannel channel) throws IOException {
183
    return lockFile(channel, false);
8✔
184
  }
185

186
  public static FileLock lockFileShared(FileChannel channel) throws IOException {
187
    return lockFile(channel, true);
8✔
188
  }
189

190
  /**
191
   * Sleep the specified number of milliseconds
192
   *
193
   * @param sleepTimeMillis The time to sleep in milliseconds
194
   */
195
  public static void sleep(long sleepTimeMillis) {
196
    try {
197
      Thread.sleep(sleepTimeMillis);
4✔
198
    } catch (InterruptedException e) {
×
199
      log.warn("Sleep was interrupted");
×
200
      // Restore interrupted state...
201
      Thread.currentThread().interrupt();
×
202
    }
2✔
203
  }
2✔
204

205
  private static FileLock lockFile(FileChannel channel, boolean shared) throws IOException {
206
    int i = 0;
4✔
207
    FileLock lock = null;
4✔
208
    do {
209
      if (i > 0) {
4✔
210
        sleep(LOCK_FILE_SLEEP_TIME);
2✔
211
      }
212

213
      lock = channel.tryLock(0L, Long.MAX_VALUE, shared);
12✔
214

215
      i++;
2✔
216
    } while (lock == null && i < LOCK_FILE_RETRY_COUNT);
7✔
217

218
    return lock;
4✔
219
  }
220

221
  /** Helper class to store parsed checksum header information. */
222
  public static class ChecksumInfo {
223
    private final ChecksumAlgorithm algorithm;
224
    private final String value;
225

226
    public ChecksumInfo(ChecksumAlgorithm algorithm, String value) {
4✔
227
      this.algorithm = algorithm;
6✔
228
      this.value = value;
6✔
229
    }
2✔
230

231
    public ChecksumAlgorithm getAlgorithm() {
232
      return algorithm;
6✔
233
    }
234

235
    public String getValue() {
236
      return value;
6✔
237
    }
238
  }
239

240
  /**
241
   * Parse the Upload-Checksum header from the HTTP request.
242
   *
243
   * @param request The HttpServletRequest
244
   * @return ChecksumInfo if header is present and valid, null otherwise
245
   */
246
  public static ChecksumInfo parseUploadChecksumHeader(HttpServletRequest request) {
247
    String uploadChecksumHeader = request.getHeader(HttpHeader.UPLOAD_CHECKSUM);
8✔
248
    if (StringUtils.isNotBlank(uploadChecksumHeader)) {
6✔
249
      ChecksumAlgorithm algorithm = ChecksumAlgorithm.forUploadChecksumHeader(uploadChecksumHeader);
6✔
250
      String checksumValue =
4✔
251
          StringUtils.substringAfter(
4✔
252
              uploadChecksumHeader, ChecksumAlgorithm.CHECKSUM_VALUE_SEPARATOR);
253
      if (algorithm != null
6✔
254
          && StringUtils.isNotBlank(checksumValue)
8✔
255
          && CHECKSUM_VALUE_PATTERN.matcher(checksumValue).matches()) {
6✔
256
        return new ChecksumInfo(algorithm, checksumValue);
12✔
257
      }
258
    }
259
    return null;
2✔
260
  }
261

262
  /**
263
   * Resolves the upload URI from the HTTP request and response context.
264
   *
265
   * @param method The HttpMethod of the request
266
   * @param request The TusServletRequest
267
   * @param response The TusServletResponse
268
   * @return The upload URI string, or null if it cannot be determined
269
   */
270
  public static String getUploadUri(TusServletRequest request, TusServletResponse response) {
271
    HttpMethod method =
272
        request != null
4✔
273
            ? HttpMethod.getMethodIfSupported(request, EnumSet.allOf(HttpMethod.class))
10✔
274
            : null;
3✔
275

276
    if (HttpMethod.POST.equals(method) || HttpMethod.PUT.equals(method)) {
16✔
277
      return response != null ? response.getHeader(HttpHeader.LOCATION) : null;
15✔
278
    } else if (request != null) {
4✔
279
      return request.getRequestURI();
6✔
280
    }
281

282
    return null;
2✔
283
  }
284

285
  /**
286
   * Detects the active ProtocolVersion for an incoming HttpServletRequest.
287
   *
288
   * @param request The current HttpServletRequest
289
   * @param supportedProtocolVersion The configured ProtocolVersion setting
290
   * @return The detected ProtocolVersion (TUS_1_0_0 or RUFH)
291
   */
292
  public static ProtocolVersion detectProtocolVersion(
293
      HttpServletRequest request, ProtocolVersion supportedProtocolVersion) {
294
    if (supportedProtocolVersion == ProtocolVersion.TUS_1_0_0) {
6✔
295
      return ProtocolVersion.TUS_1_0_0;
4✔
296
    }
297
    if (supportedProtocolVersion == ProtocolVersion.RUFH) {
6✔
298
      return ProtocolVersion.RUFH;
2✔
299
    }
300

301
    if (request != null) {
4✔
302
      if (StringUtils.isNotBlank(request.getHeader(HttpHeader.TUS_RESUMABLE))) {
10✔
303
        return ProtocolVersion.TUS_1_0_0;
4✔
304
      }
305
      if (StringUtils.isNotBlank(request.getHeader(HttpHeader.UPLOAD_OFFSET))
14✔
306
          || StringUtils.isNotBlank(request.getHeader(HttpHeader.UPLOAD_COMPLETE))
10✔
307
          || StringUtils.isNotBlank(request.getHeader(HttpHeader.UPLOAD_DRAFT))
10✔
308
          || StringUtils.isNotBlank(request.getHeader("upload-draft-interop-version"))
12✔
309
          || Strings.CS.startsWith(
10✔
310
              request.getHeader(HttpHeader.CONTENT_TYPE), HttpHeader.CONTENT_TYPE_PARTIAL_UPLOAD)
4✔
311
          || Strings.CS.startsWith(
4✔
312
              request.getHeader(HttpHeader.CONTENT_TYPE), "application/offset+octet-stream")) {
4✔
313
        return ProtocolVersion.RUFH;
4✔
314
      }
315
      String method = request.getMethod();
6✔
316
      if (HttpMethod.HEAD.name().equalsIgnoreCase(method)
12✔
317
          || HttpMethod.GET.name().equalsIgnoreCase(method)
10✔
318
          || HttpMethod.DELETE.name().equalsIgnoreCase(method)) {
8✔
319
        return ProtocolVersion.RUFH;
4✔
320
      }
321
    }
322

323
    return ProtocolVersion.TUS_1_0_0;
4✔
324
  }
325

326
  /**
327
   * Determine if the given HTTP servlet request targets the upload creation base URI endpoint.
328
   *
329
   * @param request The HTTP request
330
   * @param uploadStorageService The storage service instance
331
   * @return {@code true} if request targets the base creation endpoint URI; {@code false} otherwise
332
   */
333
  public static boolean isCreationEndpoint(
334
      HttpServletRequest request, UploadStorageService uploadStorageService) {
335
    if (request == null || uploadStorageService == null) {
8✔
336
      return false;
2✔
337
    }
338
    String requestUri = request.getRequestURI();
6✔
339
    String baseUri = uploadStorageService.getUploadUri();
6✔
340
    return requestUri != null
14✔
341
        && baseUri != null
342
        && (requestUri.equals(baseUri) || requestUri.equals(baseUri + "/"));
18✔
343
  }
344

345
  /**
346
   * Determine if the given HTTP servlet request target URI represents an existing upload resource.
347
   *
348
   * @param request The HTTP request
349
   * @param uploadStorageService The storage service instance
350
   * @param ownerKey The owner key
351
   * @return {@code true} if the request targets an existing upload resource; {@code false}
352
   *     otherwise
353
   * @throws IOException If storage lookup encounters an IO error
354
   */
355
  public static boolean isExistingUploadResource(
356
      HttpServletRequest request, UploadStorageService uploadStorageService, String ownerKey)
357
      throws IOException {
358
    if (isCreationEndpoint(request, uploadStorageService)) {
8✔
359
      return false;
2✔
360
    }
361
    String requestUri = request != null ? request.getRequestURI() : null;
13✔
362
    UploadInfo existingUpload =
363
        (uploadStorageService != null && requestUri != null)
8✔
364
            ? uploadStorageService.getUploadInfo(requestUri, ownerKey)
10✔
365
            : null;
3✔
366
    return existingUpload != null && !existingUpload.isExpired();
17✔
367
  }
368

369
  /**
370
   * Builds the upload location URI for a newly created upload resource.
371
   *
372
   * @param uploadInfo The UploadInfo object containing the upload ID
373
   * @param servletRequest The current HttpServletRequest or TusServletRequest
374
   * @param storageService The current UploadStorageService
375
   * @return The location URI string for the created upload
376
   */
377
  public static String getUploadUriOnCreation(
378
      UploadInfo uploadInfo,
379
      HttpServletRequest servletRequest,
380
      UploadStorageService storageService) {
381
    String baseUri = storageService != null ? storageService.getUploadUri() : null;
14✔
382
    if (baseUri == null && servletRequest != null) {
8✔
383
      baseUri = servletRequest.getRequestURI();
6✔
384
    }
385
    if (baseUri == null) {
4✔
386
      baseUri = "";
2✔
387
    }
388
    String idStr =
389
        uploadInfo != null && uploadInfo.getId() != null ? uploadInfo.getId().toString() : "";
21✔
390
    return baseUri + (baseUri.endsWith("/") ? "" : "/") + idStr;
20✔
391
  }
392
}
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