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

box / box-java-sdk / #3905

11 Jul 2024 03:34PM CUT coverage: 72.44% (+0.002%) from 72.438%
#3905

Pull #1258

github

web-flow
Merge 5d85071f4 into f08844889
Pull Request #1258: test: support stream file upload

7683 of 10606 relevant lines covered (72.44%)

0.72 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 of parts.
24
 * When using this APIinstead of the single file upload API, a request failure means a client only needs to
25
 * retry upload of a single part instead of the entire file.  Parts can also be uploaded in parallel allowing
26
 * 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.
55
     *                        Only 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(InputStream stream, long offset, int partSize,
60
                                               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.
84
     *                        Only 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(byte[] data, long offset, int partSize,
89
                                               long totalSizeOfFile) {
90
        URL uploadPartURL = this.sessionInfo.getSessionEndpoints().getUploadPartEndpoint();
1✔
91

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

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

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

111
        //Creates the body
112
        request.setBody(new ByteArrayInputStream(data));
1✔
113
        return request.sendForUploadPart(this, offset);
1✔
114
    }
115

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

127
        QueryStringBuilder builder = new QueryStringBuilder();
×
128
        builder.appendParam(OFFSET_QUERY_STRING, offset);
×
129
        String queryString = builder.appendParam(LIMIT_QUERY_STRING, limit).toString();
×
130

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

134
        BoxJSONRequest request = new BoxJSONRequest(this.getAPI(), url, HttpMethod.GET);
×
135
        try (BoxJSONResponse response = request.send()) {
×
136
            JsonObject jsonObject = Json.parse(response.getJSON()).asObject();
×
137

138
            return new BoxFileUploadSessionPartList(jsonObject);
×
139
        }
140
    }
141

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

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

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

175
        URL commitURL = this.sessionInfo.getSessionEndpoints().getCommitEndpoint();
1✔
176
        BoxJSONRequest request = new BoxJSONRequest(this.getAPI(), commitURL, HttpMethod.POST);
1✔
177
        request.addHeader(HttpHeaders.DIGEST, DIGEST_HEADER_PREFIX_SHA + digest);
1✔
178
        request.addHeader(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON);
1✔
179

180
        if (ifMatch != null) {
1✔
181
            request.addHeader(HttpHeaders.IF_MATCH, ifMatch);
×
182
        }
183

184
        if (ifNoneMatch != null) {
1✔
185
            request.addHeader(HttpHeaders.IF_NONE_MATCH, ifNoneMatch);
×
186
        }
187

188
        //Creates the body of the request
189
        String body = this.getCommitBody(parts, attributes);
1✔
190
        request.setBody(body);
1✔
191

192
        try (BoxJSONResponse response = request.send()) {
1✔
193
            //Retry the commit operation after the given number of seconds if the HTTP response code is 202.
194
            if (response.getResponseCode() == 202) {
1✔
195
                String retryInterval = response.getHeaderField("retry-after");
×
196
                if (retryInterval != null) {
×
197
                    try {
198
                        Thread.sleep(new Integer(retryInterval) * 1000);
×
199
                    } catch (InterruptedException ie) {
×
200
                        throw new BoxAPIException("Commit retry failed. ", ie);
×
201
                    }
×
202

203
                    return this.commit(digest, parts, attributes, ifMatch, ifNoneMatch);
×
204
                }
205
            }
206

207
            //Create the file instance from the response
208
            return this.getFile(response);
1✔
209
        }
×
210
    }
211

212
    /*
213
     * Creates the file isntance from the JSON body of the response.
214
     */
215
    private BoxFile.Info getFile(BoxJSONResponse response) {
216
        JsonObject jsonObject = Json.parse(response.getJSON()).asObject();
1✔
217

218
        JsonArray array = (JsonArray) jsonObject.get("entries");
1✔
219
        JsonObject fileObj = (JsonObject) array.get(0);
1✔
220

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

223
        return file.new Info(fileObj);
1✔
224
    }
225

226
    /*
227
     * Creates the JSON body for the commit request.
228
     */
229
    private String getCommitBody(List<BoxFileUploadSessionPart> parts, Map<String, String> attributes) {
230
        JsonObject jsonObject = new JsonObject();
1✔
231

232
        JsonArray array = new JsonArray();
1✔
233
        for (BoxFileUploadSessionPart part : parts) {
1✔
234
            JsonObject partObj = new JsonObject();
1✔
235
            partObj.add("part_id", part.getPartId());
1✔
236
            partObj.add("offset", part.getOffset());
1✔
237
            partObj.add("size", part.getSize());
1✔
238

239
            array.add(partObj);
1✔
240
        }
1✔
241
        jsonObject.add("parts", array);
1✔
242

243
        if (attributes != null) {
1✔
244
            JsonObject attrObj = new JsonObject();
1✔
245
            for (String key : attributes.keySet()) {
1✔
246
                attrObj.add(key, attributes.get(key));
1✔
247
            }
1✔
248
            jsonObject.add("attributes", attrObj);
1✔
249
        }
250

251
        return jsonObject.toString();
1✔
252
    }
253

254
    /**
255
     * Get the status of the upload session. It contains the number of parts that are processed so far,
256
     * the total number of parts required for the commit and expiration date and time of the upload session.
257
     *
258
     * @return the status.
259
     */
260
    public BoxFileUploadSession.Info getStatus() {
261
        URL statusURL = this.sessionInfo.getSessionEndpoints().getStatusEndpoint();
×
262
        BoxJSONRequest request = new BoxJSONRequest(this.getAPI(), statusURL, HttpMethod.GET);
×
263
        try (BoxJSONResponse response = request.send()) {
×
264
            JsonObject jsonObject = Json.parse(response.getJSON()).asObject();
×
265

266
            this.sessionInfo.update(jsonObject);
×
267

268
            return this.sessionInfo;
×
269
        }
270
    }
271

272
    /**
273
     * Abort an upload session, discarding any chunks that were uploaded to it.
274
     */
275
    public void abort() {
276
        URL abortURL = this.sessionInfo.getSessionEndpoints().getAbortEndpoint();
×
277
        BoxAPIRequest request = new BoxAPIRequest(this.getAPI(), abortURL, HttpMethod.DELETE);
×
278
        request.send().close();
×
279
    }
×
280

281
    /**
282
     * Model contains the upload session information.
283
     */
284
    public class Info extends BoxResource.Info {
1✔
285

286
        private Date sessionExpiresAt;
287
        private String uploadSessionId;
288
        private Endpoints sessionEndpoints;
289
        private int partSize;
290
        private int totalParts;
291
        private int partsProcessed;
292

293
        /**
294
         * Constructs an Info object by parsing information from a JSON string.
295
         *
296
         * @param json the JSON string to parse.
297
         */
298
        public Info(String json) {
299
            this(Json.parse(json).asObject());
×
300
        }
×
301

302
        /**
303
         * Constructs an Info object using an already parsed JSON object.
304
         *
305
         * @param jsonObject the parsed JSON object.
306
         */
307
        Info(JsonObject jsonObject) {
1✔
308
            super(jsonObject);
1✔
309
            BoxFileUploadSession.this.sessionInfo = this;
1✔
310
        }
1✔
311

312
        /**
313
         * Returns the BoxFileUploadSession isntance to which this object belongs to.
314
         *
315
         * @return the instance of upload session.
316
         */
317
        public BoxFileUploadSession getResource() {
318
            return BoxFileUploadSession.this;
1✔
319
        }
320

321
        /**
322
         * Returns the total parts of the file that is uploaded in the upload session.
323
         *
324
         * @return the total number of parts.
325
         */
326
        public int getTotalParts() {
327
            return this.totalParts;
×
328
        }
329

330
        /**
331
         * Returns the parts that are processed so for.
332
         *
333
         * @return the number of the processed parts.
334
         */
335
        public int getPartsProcessed() {
336
            return this.partsProcessed;
×
337
        }
338

339
        /**
340
         * Returns the date and time at which the upload session expires.
341
         *
342
         * @return the date and time in UTC format.
343
         */
344
        public Date getSessionExpiresAt() {
345
            return this.sessionExpiresAt;
×
346
        }
347

348
        /**
349
         * Returns the upload session id.
350
         *
351
         * @return the id string.
352
         */
353
        public String getUploadSessionId() {
354
            return this.uploadSessionId;
×
355
        }
356

357
        /**
358
         * Returns the session endpoints that can be called for this upload session.
359
         *
360
         * @return the Endpoints instance.
361
         */
362
        public Endpoints getSessionEndpoints() {
363
            return this.sessionEndpoints;
1✔
364
        }
365

366
        /**
367
         * Returns the size of the each part. Only the last part of the file can be lessor than this value.
368
         *
369
         * @return the part size.
370
         */
371
        public int getPartSize() {
372
            return this.partSize;
1✔
373
        }
374

375
        @Override
376
        protected void parseJSONMember(JsonObject.Member member) {
377

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

401
    /**
402
     * Represents the end points specific to an upload session.
403
     */
404
    public static class Endpoints extends BoxJSONObject {
1✔
405
        private URL listPartsEndpoint;
406
        private URL commitEndpoint;
407
        private URL uploadPartEndpoint;
408
        private URL statusEndpoint;
409
        private URL abortEndpoint;
410

411
        /**
412
         * Constructs an Endpoints object using an already parsed JSON object.
413
         *
414
         * @param jsonObject the parsed JSON object.
415
         */
416
        Endpoints(JsonObject jsonObject) {
417
            super(jsonObject);
1✔
418
        }
1✔
419

420
        /**
421
         * Returns the list parts end point.
422
         *
423
         * @return the url of the list parts end point.
424
         */
425
        public URL getListPartsEndpoint() {
426
            return this.listPartsEndpoint;
1✔
427
        }
428

429
        /**
430
         * Returns the commit end point.
431
         *
432
         * @return the url of the commit end point.
433
         */
434
        public URL getCommitEndpoint() {
435
            return this.commitEndpoint;
1✔
436
        }
437

438
        /**
439
         * Returns the upload part end point.
440
         *
441
         * @return the url of the upload part end point.
442
         */
443
        public URL getUploadPartEndpoint() {
444
            return this.uploadPartEndpoint;
1✔
445
        }
446

447
        /**
448
         * Returns the upload session status end point.
449
         *
450
         * @return the url of the session end point.
451
         */
452
        public URL getStatusEndpoint() {
453
            return this.statusEndpoint;
×
454
        }
455

456
        /**
457
         * Returns the abort upload session end point.
458
         *
459
         * @return the url of the abort end point.
460
         */
461
        public URL getAbortEndpoint() {
462
            return this.abortEndpoint;
×
463
        }
464

465
        @Override
466
        protected void parseJSONMember(JsonObject.Member member) {
467

468
            String memberName = member.getName();
1✔
469
            JsonValue value = member.getValue();
1✔
470
            try {
471
                if (memberName.equals("list_parts")) {
1✔
472
                    this.listPartsEndpoint = new URL(value.asString());
1✔
473
                } else if (memberName.equals("commit")) {
1✔
474
                    this.commitEndpoint = new URL(value.asString());
1✔
475
                } else if (memberName.equals("upload_part")) {
1✔
476
                    this.uploadPartEndpoint = new URL(value.asString());
1✔
477
                } else if (memberName.equals("status")) {
1✔
478
                    this.statusEndpoint = new URL(value.asString());
1✔
479
                } else if (memberName.equals("abort")) {
1✔
480
                    this.abortEndpoint = new URL(value.asString());
1✔
481
                }
482
            } catch (MalformedURLException mue) {
×
483
                assert false : "A ParseException indicates a bug in the SDK.";
×
484
            }
1✔
485
        }
1✔
486
    }
487
}
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