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

tomdesair / tus-java-server / 39154067638

18 Oct 2026 06:25PM UTC coverage: 94.4% (-0.3%) from 94.73%
39154067638

Pull #143

github

web-flow
Merge ff59744d4 into 46144000a
Pull Request #143: feat: upload deduplication, mandatory locking service, and lock cleanup consolidation

3203 of 3638 branches covered (88.04%)

Branch coverage included in aggregate %.

883 of 930 new or added lines in 26 files covered. (94.95%)

2 existing lines in 2 files now uncovered.

6961 of 7129 relevant lines covered (97.64%)

6.4 hits per line

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

95.18
/src/main/java/me/desair/tus/server/digest/HttpDigestsPostPutPatchRequestHandler.java
1
package me.desair.tus.server.digest;
2

3
import java.io.IOException;
4
import java.io.InputStream;
5
import java.security.DigestInputStream;
6
import java.security.MessageDigest;
7
import java.util.Map;
8
import me.desair.tus.server.HttpHeader;
9
import me.desair.tus.server.HttpMethod;
10
import me.desair.tus.server.HttpProblemDetails;
11
import me.desair.tus.server.checksum.ChecksumAlgorithm;
12
import me.desair.tus.server.checksum.ChecksumKey;
13
import me.desair.tus.server.digest.validation.HttpDigestsValidator;
14
import me.desair.tus.server.exception.TusException;
15
import me.desair.tus.server.exception.UploadDigestMismatchException;
16
import me.desair.tus.server.upload.UploadInfo;
17
import me.desair.tus.server.upload.UploadLockingService;
18
import me.desair.tus.server.upload.UploadStorageService;
19
import me.desair.tus.server.util.AbstractRequestHandler;
20
import me.desair.tus.server.util.TusServletRequest;
21
import me.desair.tus.server.util.TusServletResponse;
22
import me.desair.tus.server.util.Utils;
23
import org.apache.commons.codec.binary.Base64;
24
import org.apache.commons.lang3.StringUtils;
25
import org.apache.commons.lang3.Strings;
26

27
/** Unified request handler for RFC 9530 HTTP Digests verification in RUFH protocol. */
28
public class HttpDigestsPostPutPatchRequestHandler extends AbstractRequestHandler {
6 ✔
29

30
  @Override
31
  public boolean supports(HttpMethod method) {
32
    return HttpMethod.POST.equals(method)
14 ✔
33
        || HttpMethod.PUT.equals(method)
8 ✔
34
        || HttpMethod.PATCH.equals(method);
10 ✔
35
  }
36

37
  @Override
38
  public HttpProblemDetails process(
39
      HttpMethod method,
40
      TusServletRequest servletRequest,
41
      TusServletResponse servletResponse,
42
      UploadStorageService uploadStorageService,
43
      UploadLockingService uploadLockingService,
44
      String ownerKey,
45
      TusException exception)
46
      throws IOException, TusException {
47

48
    // 1. Verify Content-Digest for the request chunk
49
    verifyContentDigest(method, servletRequest);
8 ✔
50

51
    // Resolve UploadInfo using centralized helpers
52
    String uploadUri = Utils.getUploadUri(servletRequest, servletResponse);
8 ✔
53
    UploadInfo uploadInfo = null;
4 ✔
54
    if (uploadUri != null) {
4 ✔
55
      try {
56
        uploadInfo = uploadStorageService.getUploadInfo(uploadUri, ownerKey);
10 ✔
57
      } catch (Exception e) {
1 ✔
58
        // Ignored
59
      }
2 ✔
60
    }
61

62
    // 4. Provide Repr-Digest / Content-Digest response header if requested
63
    String wantContentDigest = servletRequest.getHeader(HttpHeader.WANT_CONTENT_DIGEST);
8 ✔
64
    if (StringUtils.isNotBlank(wantContentDigest)) {
6 ✔
65
      ChecksumAlgorithm preferredAlg = ChecksumAlgorithm.selectBestAlgorithm(wantContentDigest);
3 ✔
66
      if (preferredAlg != null) {
2 ✔
67
        String calculatedVal = servletRequest.getCalculatedChecksum(preferredAlg);
4 ✔
68
        if (calculatedVal != null) {
2 ✔
69
          servletResponse.setHeader(
4 ✔
70
              HttpHeader.CONTENT_DIGEST,
71
              preferredAlg.getHttpDigestNames().get(0) + "=:" + calculatedVal + ":");
6 ✔
72
        }
73
      }
74
    }
75

76
    if (uploadInfo != null) {
4 ✔
77
      // 2. Capture client Repr-Digest and Want-Repr-Digest
78
      captureDigestPreferences(servletRequest, uploadInfo, uploadStorageService);
10 ✔
79

80
      // 3. Verify Repr-Digest on upload completion
81
      verifyReprDigestOnCompletion(
14 ✔
82
          servletRequest, uploadInfo, uploadUri, ownerKey, uploadStorageService);
83

84
      // 4. Provide Repr-Digest response header if requested
85
      addReprDigestResponseHeader(
14 ✔
86
          servletResponse, uploadInfo, uploadUri, ownerKey, uploadStorageService);
87
    }
88

89
    return null;
4 ✔
90
  }
91

92
  private void verifyContentDigest(HttpMethod method, TusServletRequest servletRequest)
93
      throws TusException, IOException {
94
    String headerVal = servletRequest.getHeader(HttpHeader.CONTENT_DIGEST);
8 ✔
95
    if (StringUtils.isBlank(headerVal)) {
6 ✔
96
      return;
2 ✔
97
    }
98

99
    // Revalidate trailing header syntax
100
    new HttpDigestsValidator().validate(method, servletRequest, null, null);
16 ✔
101

102
    Map<ChecksumAlgorithm, String> expectedDigests = ChecksumAlgorithm.parseDigestHeader(headerVal);
6 ✔
103
    for (Map.Entry<ChecksumAlgorithm, String> entry : expectedDigests.entrySet()) {
22 ✔
104
      ChecksumAlgorithm alg = entry.getKey();
8 ✔
105
      String expectedValue = entry.getValue();
8 ✔
106
      String calculatedValue = servletRequest.getCalculatedChecksum(alg);
8 ✔
107

108
      if (calculatedValue != null && !Strings.CS.equals(expectedValue, calculatedValue)) {
14 !
109
        throw new UploadDigestMismatchException(
6 ✔
110
            "Content-Digest mismatch for algorithm "
111
                + alg.getHttpDigestNames().get(0)
18 ✔
112
                + ". Expected: "
113
                + expectedValue
114
                + " but was: "
115
                + calculatedValue);
116
      }
117
    }
2 ✔
118
  }
2 ✔
119

120
  private void captureDigestPreferences(
121
      TusServletRequest servletRequest,
122
      UploadInfo uploadInfo,
123
      UploadStorageService uploadStorageService)
124
      throws IOException, TusException {
125
    boolean updated = false;
4 ✔
126

127
    // Capture Repr-Digest
128
    String reprDigestHeader = servletRequest.getHeader(HttpHeader.REPR_DIGEST);
8 ✔
129
    if (StringUtils.isNotBlank(reprDigestHeader) && uploadInfo.getRepresentationDigest() == null) {
9 ✔
130
      uploadInfo.setRepresentationDigest(reprDigestHeader);
3 ✔
131
      updated = true;
2 ✔
132
    }
133

134
    // Capture Want-Repr-Digest
135
    String wantReprDigestHeader = servletRequest.getHeader(HttpHeader.WANT_REPR_DIGEST);
8 ✔
136
    if (StringUtils.isNotBlank(wantReprDigestHeader)
7 ✔
137
        && uploadInfo.getRequestedRepresentationDigests() == null) {
2 ✔
138
      uploadInfo.setRequestedRepresentationDigests(wantReprDigestHeader);
3 ✔
139
      updated = true;
2 ✔
140
    }
141

142
    if (updated) {
4 ✔
143
      uploadStorageService.update(uploadInfo);
3 ✔
144
    }
145
  }
2 ✔
146

147
  private void verifyReprDigestOnCompletion(
148
      TusServletRequest servletRequest,
149
      UploadInfo uploadInfo,
150
      String uploadUri,
151
      String ownerKey,
152
      UploadStorageService uploadStorageService)
153
      throws IOException, TusException {
154

155
    if (!uploadInfo.isUploadInProgress()) {
6 ✔
156
      String reprDigest = uploadInfo.getRepresentationDigest();
6 ✔
157
      if (reprDigest == null && servletRequest != null) {
8 !
158
        reprDigest = servletRequest.getHeader(HttpHeader.CONTENT_DIGEST);
8 ✔
159
      }
160
      if (reprDigest != null) {
4 ✔
161
        Map<ChecksumAlgorithm, String> expectedDigests =
2 ✔
162
            ChecksumAlgorithm.parseDigestHeader(reprDigest);
4 ✔
163

164
        for (Map.Entry<ChecksumAlgorithm, String> entry : expectedDigests.entrySet()) {
22 ✔
165
          ChecksumAlgorithm alg = entry.getKey();
8 ✔
166
          String expectedValue = entry.getValue();
8 ✔
167
          String calculatedValue =
10 ✔
168
              calculateEntireFileDigest(uploadUri, ownerKey, alg, uploadStorageService);
4 ✔
169

170
          if (calculatedValue != null) {
4 !
171
            if (!Strings.CS.equals(expectedValue, calculatedValue)) {
10 ✔
172
              throw new UploadDigestMismatchException(
3 ✔
173
                  "Repr-Digest mismatch for algorithm "
174
                      + alg.getHttpDigestNames().get(0)
9 ✔
175
                      + ". Expected: "
176
                      + expectedValue
177
                      + " but was: "
178
                      + calculatedValue);
179
            } else {
180
              // Always save verified checksum on the uploadInfo
181
              uploadInfo.setChecksum(expectedValue);
6 ✔
182
              uploadInfo.setChecksumAlgorithm(alg);
6 ✔
183

184
              if (uploadStorageService.isUploadDeduplicationEnabled()
8 ✔
185
                  && uploadInfo.getDuplicatesUploadId() == null) {
4 ✔
186
                try {
187
                  ChecksumKey key = ChecksumKey.of(expectedValue, alg);
8 ✔
188
                  UploadInfo deduped = uploadStorageService.deduplicate(uploadInfo, key);
10 ✔
189
                  if (deduped != null) {
4 !
190
                    uploadInfo = deduped;
4 ✔
191
                  }
NEW
192
                } catch (Exception e) {
×
NEW
193
                  uploadStorageService.update(uploadInfo);
×
194
                }
2 ✔
195
              } else {
196
                uploadStorageService.update(uploadInfo);
6 ✔
197
              }
198
            }
199
          }
200
        }
2 ✔
201
      }
202
    }
203
  }
2 ✔
204

205
  private void addReprDigestResponseHeader(
206
      TusServletResponse servletResponse,
207
      UploadInfo uploadInfo,
208
      String uploadUri,
209
      String ownerKey,
210
      UploadStorageService uploadStorageService)
211
      throws IOException, TusException {
212

213
    String requestedDigests = uploadInfo.getRequestedRepresentationDigests();
6 ✔
214
    if (StringUtils.isNotBlank(requestedDigests)) {
6 ✔
215
      ChecksumAlgorithm preferredAlg = ChecksumAlgorithm.selectBestAlgorithm(requestedDigests);
3 ✔
216
      if (preferredAlg != null) {
2 ✔
217
        String calculatedValue =
5 ✔
218
            calculateEntireFileDigest(uploadUri, ownerKey, preferredAlg, uploadStorageService);
2 ✔
219
        if (calculatedValue != null) {
2 !
220
          servletResponse.setHeader(
4 ✔
221
              HttpHeader.REPR_DIGEST,
222
              preferredAlg.getHttpDigestNames().get(0) + "=:" + calculatedValue + ":");
6 ✔
223
        }
224
      }
225
    }
226
  }
2 ✔
227

228
  private String calculateEntireFileDigest(
229
      String uploadUri,
230
      String ownerKey,
231
      ChecksumAlgorithm alg,
232
      UploadStorageService uploadStorageService)
233
      throws IOException, TusException {
234
    MessageDigest md = alg.getMessageDigest();
6 ✔
235
    try (InputStream is = uploadStorageService.getUploadedBytes(uploadUri, ownerKey)) {
10 ✔
236
      if (is == null) {
4 ✔
237
        return null;
4 ✔
238
      }
239
      try (DigestInputStream dis = new DigestInputStream(is, md)) {
12 ✔
240
        byte[] buffer = new byte[8192];
6 ✔
241
        while (dis.read(buffer) != -1) {
12 ✔
242
          // do nothing, let stream update digest
243
        }
244
      }
245
    }
2 !
246
    return Base64.encodeBase64String(md.digest());
8 ✔
247
  }
248
}
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