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

box / box-java-sdk / #5641

12 Nov 2025 04:30PM UTC coverage: 13.3% (+0.008%) from 13.292%
#5641

Pull #1562

github

web-flow
Merge 298170338 into d9fee1425
Pull Request #1562: chore(boxsdkgen): Update `.codegen.json` with commit hash of `codegen` and `openapi` spec [skip ci]

8374 of 62964 relevant lines covered (13.3%)

0.13 hits per line

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

64.67
/src/main/java/com/box/sdk/BoxFileUploadSession.java
1
package com.box.sdk;
2

3
import com.box.sdk.http.ContentType;
4
import com.box.sdk.http.HttpHeaders;
5
import com.box.sdk.http.HttpMethod;
6
import com.eclipsesource.json.Json;
7
import com.eclipsesource.json.JsonArray;
8
import com.eclipsesource.json.JsonObject;
9
import com.eclipsesource.json.JsonValue;
10
import java.io.ByteArrayInputStream;
11
import java.io.IOException;
12
import java.io.InputStream;
13
import java.net.MalformedURLException;
14
import java.net.URL;
15
import java.security.MessageDigest;
16
import java.security.NoSuchAlgorithmException;
17
import java.text.ParseException;
18
import java.util.Date;
19
import java.util.List;
20
import java.util.Map;
21

22
/**
23
 * This API provides a way to reliably upload larger files to Box by chunking them into a sequence
24
 * of parts. When using this APIinstead of the single file upload API, a request failure means a
25
 * client only needs to retry upload of a single part instead of the entire file. Parts can also be
26
 * uploaded in parallel allowing for potential performance improvement.
27
 */
28
@BoxResourceType("upload_session")
29
public class BoxFileUploadSession extends BoxResource {
30

31
  private static final String DIGEST_HEADER_PREFIX_SHA = "sha=";
32
  private static final String DIGEST_ALGORITHM_SHA1 = "SHA1";
33

34
  private static final String OFFSET_QUERY_STRING = "offset";
35
  private static final String LIMIT_QUERY_STRING = "limit";
36

37
  private Info sessionInfo;
38

39
  /**
40
   * Constructs a BoxFileUploadSession for a file with a given ID.
41
   *
42
   * @param api the API connection to be used by the upload session.
43
   * @param id the ID of the upload session.
44
   */
45
  BoxFileUploadSession(BoxAPIConnection api, String id) {
46
    super(api, id);
1✔
47
  }
1✔
48

49
  /**
50
   * Uploads chunk of a stream to an open upload session.
51
   *
52
   * @param stream the stream that is used to read the chunck using the offset and part size.
53
   * @param offset the byte position where the chunk begins in the file.
54
   * @param partSize the part size returned as part of the upload session instance creation. Only
55
   *     the last chunk can have a lesser value.
56
   * @param totalSizeOfFile The total size of the file being uploaded.
57
   * @return the part instance that contains the part id, offset and part size.
58
   */
59
  public BoxFileUploadSessionPart uploadPart(
60
      InputStream stream, long offset, int partSize, long totalSizeOfFile) {
61

62
    URL uploadPartURL = this.sessionInfo.getSessionEndpoints().getUploadPartEndpoint();
×
63

64
    BoxAPIRequest request = new BoxAPIRequest(this.getAPI(), uploadPartURL, HttpMethod.PUT);
×
65
    request.addHeader(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_OCTET_STREAM);
×
66

67
    // Read the partSize bytes from the stream
68
    byte[] bytes = new byte[partSize];
×
69
    try {
70
      stream.read(bytes);
×
71
    } catch (IOException ioe) {
×
72
      throw new BoxAPIException("Reading data from stream failed.", ioe);
×
73
    }
×
74

75
    return this.uploadPart(bytes, offset, partSize, totalSizeOfFile);
×
76
  }
77

78
  /**
79
   * Uploads bytes to an open upload session.
80
   *
81
   * @param data data
82
   * @param offset the byte position where the chunk begins in the file.
83
   * @param partSize the part size returned as part of the upload session instance creation. Only
84
   *     the last chunk can have a lesser value.
85
   * @param totalSizeOfFile The total size of the file being uploaded.
86
   * @return the part instance that contains the part id, offset and part size.
87
   */
88
  public BoxFileUploadSessionPart uploadPart(
89
      byte[] data, long offset, int partSize, long totalSizeOfFile) {
90
    URL uploadPartURL = this.sessionInfo.getSessionEndpoints().getUploadPartEndpoint();
1✔
91

92
    BoxAPIRequest request =
1✔
93
        new BoxAPIRequest(
94
            this.getAPI(),
1✔
95
            uploadPartURL,
96
            HttpMethod.PUT.name(),
1✔
97
            ContentType.APPLICATION_OCTET_STREAM);
98

99
    MessageDigest digestInstance;
100
    try {
101
      digestInstance = MessageDigest.getInstance(DIGEST_ALGORITHM_SHA1);
1✔
102
    } catch (NoSuchAlgorithmException ae) {
×
103
      throw new BoxAPIException("Digest algorithm not found", ae);
×
104
    }
1✔
105

106
    // Creates the digest using SHA1 algorithm. Then encodes the bytes using Base64.
107
    byte[] digestBytes = digestInstance.digest(data);
1✔
108
    String digest = Base64.encode(digestBytes);
1✔
109
    request.addHeader(HttpHeaders.DIGEST, DIGEST_HEADER_PREFIX_SHA + digest);
1✔
110
    // Content-Range: bytes offset-part/totalSize
111
    request.addHeader(
1✔
112
        HttpHeaders.CONTENT_RANGE,
113
        "bytes " + offset + "-" + (offset + partSize - 1) + "/" + totalSizeOfFile);
114

115
    // Creates the body
116
    request.setBody(new ByteArrayInputStream(data));
1✔
117
    return request.sendForUploadPart(this, offset);
1✔
118
  }
119

120
  /**
121
   * Returns a list of all parts that have been uploaded to an upload session.
122
   *
123
   * @param offset paging marker for the list of parts.
124
   * @param limit maximum number of parts to return.
125
   * @return the list of parts.
126
   */
127
  public BoxFileUploadSessionPartList listParts(int offset, int limit) {
128
    URL listPartsURL = this.sessionInfo.getSessionEndpoints().getListPartsEndpoint();
×
129
    URLTemplate template = new URLTemplate(listPartsURL.toString());
×
130

131
    QueryStringBuilder builder = new QueryStringBuilder();
×
132
    builder.appendParam(OFFSET_QUERY_STRING, offset);
×
133
    String queryString = builder.appendParam(LIMIT_QUERY_STRING, limit).toString();
×
134

135
    // Template is initalized with the full URL. So empty string for the path.
136
    URL url = template.buildWithQuery("", queryString);
×
137

138
    BoxJSONRequest request = new BoxJSONRequest(this.getAPI(), url, HttpMethod.GET);
×
139
    try (BoxJSONResponse response = request.send()) {
×
140
      JsonObject jsonObject = Json.parse(response.getJSON()).asObject();
×
141

142
      return new BoxFileUploadSessionPartList(jsonObject);
×
143
    }
144
  }
145

146
  /**
147
   * Returns a list of all parts that have been uploaded to an upload session.
148
   *
149
   * @return the list of parts.
150
   */
151
  protected Iterable<BoxFileUploadSessionPart> listParts() {
152
    URL listPartsURL = this.sessionInfo.getSessionEndpoints().getListPartsEndpoint();
1✔
153
    int limit = 100;
1✔
154
    return new BoxResourceIterable<BoxFileUploadSessionPart>(this.getAPI(), listPartsURL, limit) {
1✔
155

156
      @Override
157
      protected BoxFileUploadSessionPart factory(JsonObject jsonObject) {
158
        return new BoxFileUploadSessionPart(jsonObject);
1✔
159
      }
160
    };
161
  }
162

163
  /**
164
   * Commit an upload session after all parts have been uploaded, creating the new file or the
165
   * version.
166
   *
167
   * @param digest the base64-encoded SHA-1 hash of the file being uploaded.
168
   * @param parts the list of uploaded parts to be committed.
169
   * @param attributes the key value pairs of attributes from the file instance.
170
   * @param ifMatch ensures that your app only alters files/folders on Box if you have the current
171
   *     version.
172
   * @param ifNoneMatch ensure that it retrieve unnecessary data if the most current version of file
173
   *     is on-hand.
174
   * @return the created file instance.
175
   */
176
  public BoxFile.Info commit(
177
      String digest,
178
      List<BoxFileUploadSessionPart> parts,
179
      Map<String, String> attributes,
180
      String ifMatch,
181
      String ifNoneMatch) {
182

183
    URL commitURL = this.sessionInfo.getSessionEndpoints().getCommitEndpoint();
1✔
184
    BoxJSONRequest request = new BoxJSONRequest(this.getAPI(), commitURL, HttpMethod.POST);
1✔
185
    request.addHeader(HttpHeaders.DIGEST, DIGEST_HEADER_PREFIX_SHA + digest);
1✔
186
    request.addHeader(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON);
1✔
187

188
    if (ifMatch != null) {
1✔
189
      request.addHeader(HttpHeaders.IF_MATCH, ifMatch);
×
190
    }
191

192
    if (ifNoneMatch != null) {
1✔
193
      request.addHeader(HttpHeaders.IF_NONE_MATCH, ifNoneMatch);
×
194
    }
195

196
    // Creates the body of the request
197
    String body = this.getCommitBody(parts, attributes);
1✔
198
    request.setBody(body);
1✔
199

200
    try (BoxJSONResponse response = request.send()) {
1✔
201
      // Retry the commit operation after the given number of seconds if the HTTP response code is
202
      // 202.
203
      if (response.getResponseCode() == 202) {
1✔
204
        String retryInterval = response.getHeaderField("retry-after");
×
205
        if (retryInterval != null) {
×
206
          try {
207
            Thread.sleep(new Integer(retryInterval) * 1000);
×
208
          } catch (InterruptedException ie) {
×
209
            throw new BoxAPIException("Commit retry failed. ", ie);
×
210
          }
×
211

212
          return this.commit(digest, parts, attributes, ifMatch, ifNoneMatch);
×
213
        }
214
      }
215

216
      // Create the file instance from the response
217
      return this.getFile(response);
1✔
218
    }
×
219
  }
220

221
  /*
222
   * Creates the file isntance from the JSON body of the response.
223
   */
224
  private BoxFile.Info getFile(BoxJSONResponse response) {
225
    JsonObject jsonObject = Json.parse(response.getJSON()).asObject();
1✔
226

227
    JsonArray array = (JsonArray) jsonObject.get("entries");
1✔
228
    JsonObject fileObj = (JsonObject) array.get(0);
1✔
229

230
    BoxFile file = new BoxFile(this.getAPI(), fileObj.get("id").asString());
1✔
231

232
    return file.new Info(fileObj);
1✔
233
  }
234

235
  /*
236
   * Creates the JSON body for the commit request.
237
   */
238
  private String getCommitBody(
239
      List<BoxFileUploadSessionPart> parts, Map<String, String> attributes) {
240
    JsonObject jsonObject = new JsonObject();
1✔
241

242
    JsonArray array = new JsonArray();
1✔
243
    for (BoxFileUploadSessionPart part : parts) {
1✔
244
      JsonObject partObj = new JsonObject();
1✔
245
      partObj.add("part_id", part.getPartId());
1✔
246
      partObj.add("offset", part.getOffset());
1✔
247
      partObj.add("size", part.getSize());
1✔
248

249
      array.add(partObj);
1✔
250
    }
1✔
251
    jsonObject.add("parts", array);
1✔
252

253
    if (attributes != null) {
1✔
254
      JsonObject attrObj = new JsonObject();
1✔
255
      for (String key : attributes.keySet()) {
1✔
256
        attrObj.add(key, attributes.get(key));
1✔
257
      }
1✔
258
      jsonObject.add("attributes", attrObj);
1✔
259
    }
260

261
    return jsonObject.toString();
1✔
262
  }
263

264
  /**
265
   * Get the status of the upload session. It contains the number of parts that are processed so
266
   * far, the total number of parts required for the commit and expiration date and time of the
267
   * upload session.
268
   *
269
   * @return the status.
270
   */
271
  public BoxFileUploadSession.Info getStatus() {
272
    URL statusURL = this.sessionInfo.getSessionEndpoints().getStatusEndpoint();
×
273
    BoxJSONRequest request = new BoxJSONRequest(this.getAPI(), statusURL, HttpMethod.GET);
×
274
    try (BoxJSONResponse response = request.send()) {
×
275
      JsonObject jsonObject = Json.parse(response.getJSON()).asObject();
×
276

277
      this.sessionInfo.update(jsonObject);
×
278

279
      return this.sessionInfo;
×
280
    }
281
  }
282

283
  /** Abort an upload session, discarding any chunks that were uploaded to it. */
284
  public void abort() {
285
    URL abortURL = this.sessionInfo.getSessionEndpoints().getAbortEndpoint();
×
286
    BoxAPIRequest request = new BoxAPIRequest(this.getAPI(), abortURL, HttpMethod.DELETE);
×
287
    request.send().close();
×
288
  }
×
289

290
  /** Model contains the upload session information. */
291
  public class Info extends BoxResource.Info {
1✔
292

293
    private Date sessionExpiresAt;
294
    private String uploadSessionId;
295
    private Endpoints sessionEndpoints;
296
    private int partSize;
297
    private int totalParts;
298
    private int partsProcessed;
299

300
    /**
301
     * Constructs an Info object by parsing information from a JSON string.
302
     *
303
     * @param json the JSON string to parse.
304
     */
305
    public Info(String json) {
306
      this(Json.parse(json).asObject());
×
307
    }
×
308

309
    /**
310
     * Constructs an Info object using an already parsed JSON object.
311
     *
312
     * @param jsonObject the parsed JSON object.
313
     */
314
    Info(JsonObject jsonObject) {
1✔
315
      super(jsonObject);
1✔
316
      BoxFileUploadSession.this.sessionInfo = this;
1✔
317
    }
1✔
318

319
    /**
320
     * Returns the BoxFileUploadSession isntance to which this object belongs to.
321
     *
322
     * @return the instance of upload session.
323
     */
324
    public BoxFileUploadSession getResource() {
325
      return BoxFileUploadSession.this;
1✔
326
    }
327

328
    /**
329
     * Returns the total parts of the file that is uploaded in the upload session.
330
     *
331
     * @return the total number of parts.
332
     */
333
    public int getTotalParts() {
334
      return this.totalParts;
×
335
    }
336

337
    /**
338
     * Returns the parts that are processed so for.
339
     *
340
     * @return the number of the processed parts.
341
     */
342
    public int getPartsProcessed() {
343
      return this.partsProcessed;
×
344
    }
345

346
    /**
347
     * Returns the date and time at which the upload session expires.
348
     *
349
     * @return the date and time in UTC format.
350
     */
351
    public Date getSessionExpiresAt() {
352
      return this.sessionExpiresAt;
×
353
    }
354

355
    /**
356
     * Returns the upload session id.
357
     *
358
     * @return the id string.
359
     */
360
    public String getUploadSessionId() {
361
      return this.uploadSessionId;
×
362
    }
363

364
    /**
365
     * Returns the session endpoints that can be called for this upload session.
366
     *
367
     * @return the Endpoints instance.
368
     */
369
    public Endpoints getSessionEndpoints() {
370
      return this.sessionEndpoints;
1✔
371
    }
372

373
    /**
374
     * Returns the size of the each part. Only the last part of the file can be lessor than this
375
     * value.
376
     *
377
     * @return the part size.
378
     */
379
    public int getPartSize() {
380
      return this.partSize;
1✔
381
    }
382

383
    @Override
384
    protected void parseJSONMember(JsonObject.Member member) {
385

386
      String memberName = member.getName();
1✔
387
      JsonValue value = member.getValue();
1✔
388
      if (memberName.equals("session_expires_at")) {
1✔
389
        try {
390
          String dateStr = value.asString();
1✔
391
          this.sessionExpiresAt =
1✔
392
              BoxDateFormat.parse(dateStr.substring(0, dateStr.length() - 1) + "-00:00");
1✔
393
        } catch (ParseException pe) {
×
394
          assert false : "A ParseException indicates a bug in the SDK.";
×
395
        }
1✔
396
      } else if (memberName.equals("id")) {
1✔
397
        this.uploadSessionId = value.asString();
1✔
398
      } else if (memberName.equals("part_size")) {
1✔
399
        this.partSize = Integer.parseInt(value.toString());
1✔
400
      } else if (memberName.equals("session_endpoints")) {
1✔
401
        this.sessionEndpoints = new Endpoints(value.asObject());
1✔
402
      } else if (memberName.equals("total_parts")) {
1✔
403
        this.totalParts = value.asInt();
1✔
404
      } else if (memberName.equals("num_parts_processed")) {
1✔
405
        this.partsProcessed = value.asInt();
1✔
406
      }
407
    }
1✔
408
  }
409

410
  /** Represents the end points specific to an upload session. */
411
  public static class Endpoints extends BoxJSONObject {
1✔
412
    private URL listPartsEndpoint;
413
    private URL commitEndpoint;
414
    private URL uploadPartEndpoint;
415
    private URL statusEndpoint;
416
    private URL abortEndpoint;
417

418
    /**
419
     * Constructs an Endpoints object using an already parsed JSON object.
420
     *
421
     * @param jsonObject the parsed JSON object.
422
     */
423
    Endpoints(JsonObject jsonObject) {
424
      super(jsonObject);
1✔
425
    }
1✔
426

427
    /**
428
     * Returns the list parts end point.
429
     *
430
     * @return the url of the list parts end point.
431
     */
432
    public URL getListPartsEndpoint() {
433
      return this.listPartsEndpoint;
1✔
434
    }
435

436
    /**
437
     * Returns the commit end point.
438
     *
439
     * @return the url of the commit end point.
440
     */
441
    public URL getCommitEndpoint() {
442
      return this.commitEndpoint;
1✔
443
    }
444

445
    /**
446
     * Returns the upload part end point.
447
     *
448
     * @return the url of the upload part end point.
449
     */
450
    public URL getUploadPartEndpoint() {
451
      return this.uploadPartEndpoint;
1✔
452
    }
453

454
    /**
455
     * Returns the upload session status end point.
456
     *
457
     * @return the url of the session end point.
458
     */
459
    public URL getStatusEndpoint() {
460
      return this.statusEndpoint;
×
461
    }
462

463
    /**
464
     * Returns the abort upload session end point.
465
     *
466
     * @return the url of the abort end point.
467
     */
468
    public URL getAbortEndpoint() {
469
      return this.abortEndpoint;
×
470
    }
471

472
    @Override
473
    protected void parseJSONMember(JsonObject.Member member) {
474

475
      String memberName = member.getName();
1✔
476
      JsonValue value = member.getValue();
1✔
477
      try {
478
        if (memberName.equals("list_parts")) {
1✔
479
          this.listPartsEndpoint = new URL(value.asString());
1✔
480
        } else if (memberName.equals("commit")) {
1✔
481
          this.commitEndpoint = new URL(value.asString());
1✔
482
        } else if (memberName.equals("upload_part")) {
1✔
483
          this.uploadPartEndpoint = new URL(value.asString());
1✔
484
        } else if (memberName.equals("status")) {
1✔
485
          this.statusEndpoint = new URL(value.asString());
1✔
486
        } else if (memberName.equals("abort")) {
1✔
487
          this.abortEndpoint = new URL(value.asString());
1✔
488
        }
489
      } catch (MalformedURLException mue) {
×
490
        assert false : "A ParseException indicates a bug in the SDK.";
×
491
      }
1✔
492
    }
1✔
493
  }
494
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc