com.box.sdk.BoxFileUploadSession Maven / Gradle / Ivy
The newest version!
package com.box.sdk;
import com.box.sdk.http.ContentType;
import com.box.sdk.http.HttpHeaders;
import com.box.sdk.http.HttpMethod;
import com.eclipsesource.json.Json;
import com.eclipsesource.json.JsonArray;
import com.eclipsesource.json.JsonObject;
import com.eclipsesource.json.JsonValue;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.ParseException;
import java.util.Date;
import java.util.List;
import java.util.Map;
/**
* This API provides a way to reliably upload larger files to Box by chunking them into a sequence of parts.
* When using this APIinstead of the single file upload API, a request failure means a client only needs to
* retry upload of a single part instead of the entire file. Parts can also be uploaded in parallel allowing
* for potential performance improvement.
*/
@BoxResourceType("upload_session")
public class BoxFileUploadSession extends BoxResource {
private static final String DIGEST_HEADER_PREFIX_SHA = "sha=";
private static final String DIGEST_ALGORITHM_SHA1 = "SHA1";
private static final String OFFSET_QUERY_STRING = "offset";
private static final String LIMIT_QUERY_STRING = "limit";
private Info sessionInfo;
/**
* Constructs a BoxFileUploadSession for a file with a given ID.
*
* @param api the API connection to be used by the upload session.
* @param id the ID of the upload session.
*/
BoxFileUploadSession(BoxAPIConnection api, String id) {
super(api, id);
}
/**
* Uploads chunk of a stream to an open upload session.
*
* @param stream the stream that is used to read the chunck using the offset and part size.
* @param offset the byte position where the chunk begins in the file.
* @param partSize the part size returned as part of the upload session instance creation.
* Only the last chunk can have a lesser value.
* @param totalSizeOfFile The total size of the file being uploaded.
* @return the part instance that contains the part id, offset and part size.
*/
public BoxFileUploadSessionPart uploadPart(InputStream stream, long offset, int partSize,
long totalSizeOfFile) {
URL uploadPartURL = this.sessionInfo.getSessionEndpoints().getUploadPartEndpoint();
BoxAPIRequest request = new BoxAPIRequest(this.getAPI(), uploadPartURL, HttpMethod.PUT);
request.addHeader(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_OCTET_STREAM);
//Read the partSize bytes from the stream
byte[] bytes = new byte[partSize];
try {
stream.read(bytes);
} catch (IOException ioe) {
throw new BoxAPIException("Reading data from stream failed.", ioe);
}
return this.uploadPart(bytes, offset, partSize, totalSizeOfFile);
}
/**
* Uploads bytes to an open upload session.
*
* @param data data
* @param offset the byte position where the chunk begins in the file.
* @param partSize the part size returned as part of the upload session instance creation.
* Only the last chunk can have a lesser value.
* @param totalSizeOfFile The total size of the file being uploaded.
* @return the part instance that contains the part id, offset and part size.
*/
public BoxFileUploadSessionPart uploadPart(byte[] data, long offset, int partSize,
long totalSizeOfFile) {
URL uploadPartURL = this.sessionInfo.getSessionEndpoints().getUploadPartEndpoint();
BoxAPIRequest request = new BoxAPIRequest(
this.getAPI(), uploadPartURL, HttpMethod.PUT.name(), ContentType.APPLICATION_OCTET_STREAM
);
MessageDigest digestInstance;
try {
digestInstance = MessageDigest.getInstance(DIGEST_ALGORITHM_SHA1);
} catch (NoSuchAlgorithmException ae) {
throw new BoxAPIException("Digest algorithm not found", ae);
}
//Creates the digest using SHA1 algorithm. Then encodes the bytes using Base64.
byte[] digestBytes = digestInstance.digest(data);
String digest = Base64.encode(digestBytes);
request.addHeader(HttpHeaders.DIGEST, DIGEST_HEADER_PREFIX_SHA + digest);
//Content-Range: bytes offset-part/totalSize
request.addHeader(HttpHeaders.CONTENT_RANGE,
"bytes " + offset + "-" + (offset + partSize - 1) + "/" + totalSizeOfFile);
//Creates the body
request.setBody(new ByteArrayInputStream(data));
return request.sendForUploadPart(this, offset);
}
/**
* Returns a list of all parts that have been uploaded to an upload session.
*
* @param offset paging marker for the list of parts.
* @param limit maximum number of parts to return.
* @return the list of parts.
*/
public BoxFileUploadSessionPartList listParts(int offset, int limit) {
URL listPartsURL = this.sessionInfo.getSessionEndpoints().getListPartsEndpoint();
URLTemplate template = new URLTemplate(listPartsURL.toString());
QueryStringBuilder builder = new QueryStringBuilder();
builder.appendParam(OFFSET_QUERY_STRING, offset);
String queryString = builder.appendParam(LIMIT_QUERY_STRING, limit).toString();
//Template is initalized with the full URL. So empty string for the path.
URL url = template.buildWithQuery("", queryString);
BoxJSONRequest request = new BoxJSONRequest(this.getAPI(), url, HttpMethod.GET);
try (BoxJSONResponse response = request.send()) {
JsonObject jsonObject = Json.parse(response.getJSON()).asObject();
return new BoxFileUploadSessionPartList(jsonObject);
}
}
/**
* Returns a list of all parts that have been uploaded to an upload session.
*
* @return the list of parts.
*/
protected Iterable listParts() {
URL listPartsURL = this.sessionInfo.getSessionEndpoints().getListPartsEndpoint();
int limit = 100;
return new BoxResourceIterable(
this.getAPI(),
listPartsURL,
limit) {
@Override
protected BoxFileUploadSessionPart factory(JsonObject jsonObject) {
return new BoxFileUploadSessionPart(jsonObject);
}
};
}
/**
* Commit an upload session after all parts have been uploaded, creating the new file or the version.
*
* @param digest the base64-encoded SHA-1 hash of the file being uploaded.
* @param parts the list of uploaded parts to be committed.
* @param attributes the key value pairs of attributes from the file instance.
* @param ifMatch ensures that your app only alters files/folders on Box if you have the current version.
* @param ifNoneMatch ensure that it retrieve unnecessary data if the most current version of file is on-hand.
* @return the created file instance.
*/
public BoxFile.Info commit(String digest, List parts,
Map attributes, String ifMatch, String ifNoneMatch) {
URL commitURL = this.sessionInfo.getSessionEndpoints().getCommitEndpoint();
BoxJSONRequest request = new BoxJSONRequest(this.getAPI(), commitURL, HttpMethod.POST);
request.addHeader(HttpHeaders.DIGEST, DIGEST_HEADER_PREFIX_SHA + digest);
request.addHeader(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON);
if (ifMatch != null) {
request.addHeader(HttpHeaders.IF_MATCH, ifMatch);
}
if (ifNoneMatch != null) {
request.addHeader(HttpHeaders.IF_NONE_MATCH, ifNoneMatch);
}
//Creates the body of the request
String body = this.getCommitBody(parts, attributes);
request.setBody(body);
try (BoxJSONResponse response = request.send()) {
//Retry the commit operation after the given number of seconds if the HTTP response code is 202.
if (response.getResponseCode() == 202) {
String retryInterval = response.getHeaderField("retry-after");
if (retryInterval != null) {
try {
Thread.sleep(new Integer(retryInterval) * 1000);
} catch (InterruptedException ie) {
throw new BoxAPIException("Commit retry failed. ", ie);
}
return this.commit(digest, parts, attributes, ifMatch, ifNoneMatch);
}
}
//Create the file instance from the response
return this.getFile(response);
}
}
/*
* Creates the file isntance from the JSON body of the response.
*/
private BoxFile.Info getFile(BoxJSONResponse response) {
JsonObject jsonObject = Json.parse(response.getJSON()).asObject();
JsonArray array = (JsonArray) jsonObject.get("entries");
JsonObject fileObj = (JsonObject) array.get(0);
BoxFile file = new BoxFile(this.getAPI(), fileObj.get("id").asString());
return file.new Info(fileObj);
}
/*
* Creates the JSON body for the commit request.
*/
private String getCommitBody(List parts, Map attributes) {
JsonObject jsonObject = new JsonObject();
JsonArray array = new JsonArray();
for (BoxFileUploadSessionPart part : parts) {
JsonObject partObj = new JsonObject();
partObj.add("part_id", part.getPartId());
partObj.add("offset", part.getOffset());
partObj.add("size", part.getSize());
array.add(partObj);
}
jsonObject.add("parts", array);
if (attributes != null) {
JsonObject attrObj = new JsonObject();
for (String key : attributes.keySet()) {
attrObj.add(key, attributes.get(key));
}
jsonObject.add("attributes", attrObj);
}
return jsonObject.toString();
}
/**
* Get the status of the upload session. It contains the number of parts that are processed so far,
* the total number of parts required for the commit and expiration date and time of the upload session.
*
* @return the status.
*/
public BoxFileUploadSession.Info getStatus() {
URL statusURL = this.sessionInfo.getSessionEndpoints().getStatusEndpoint();
BoxJSONRequest request = new BoxJSONRequest(this.getAPI(), statusURL, HttpMethod.GET);
try (BoxJSONResponse response = request.send()) {
JsonObject jsonObject = Json.parse(response.getJSON()).asObject();
this.sessionInfo.update(jsonObject);
return this.sessionInfo;
}
}
/**
* Abort an upload session, discarding any chunks that were uploaded to it.
*/
public void abort() {
URL abortURL = this.sessionInfo.getSessionEndpoints().getAbortEndpoint();
BoxAPIRequest request = new BoxAPIRequest(this.getAPI(), abortURL, HttpMethod.DELETE);
request.send().close();
}
/**
* Model contains the upload session information.
*/
public class Info extends BoxResource.Info {
private Date sessionExpiresAt;
private String uploadSessionId;
private Endpoints sessionEndpoints;
private int partSize;
private int totalParts;
private int partsProcessed;
/**
* Constructs an Info object by parsing information from a JSON string.
*
* @param json the JSON string to parse.
*/
public Info(String json) {
this(Json.parse(json).asObject());
}
/**
* Constructs an Info object using an already parsed JSON object.
*
* @param jsonObject the parsed JSON object.
*/
Info(JsonObject jsonObject) {
super(jsonObject);
BoxFileUploadSession.this.sessionInfo = this;
}
/**
* Returns the BoxFileUploadSession isntance to which this object belongs to.
*
* @return the instance of upload session.
*/
public BoxFileUploadSession getResource() {
return BoxFileUploadSession.this;
}
/**
* Returns the total parts of the file that is uploaded in the upload session.
*
* @return the total number of parts.
*/
public int getTotalParts() {
return this.totalParts;
}
/**
* Returns the parts that are processed so for.
*
* @return the number of the processed parts.
*/
public int getPartsProcessed() {
return this.partsProcessed;
}
/**
* Returns the date and time at which the upload session expires.
*
* @return the date and time in UTC format.
*/
public Date getSessionExpiresAt() {
return this.sessionExpiresAt;
}
/**
* Returns the upload session id.
*
* @return the id string.
*/
public String getUploadSessionId() {
return this.uploadSessionId;
}
/**
* Returns the session endpoints that can be called for this upload session.
*
* @return the Endpoints instance.
*/
public Endpoints getSessionEndpoints() {
return this.sessionEndpoints;
}
/**
* Returns the size of the each part. Only the last part of the file can be lessor than this value.
*
* @return the part size.
*/
public int getPartSize() {
return this.partSize;
}
@Override
protected void parseJSONMember(JsonObject.Member member) {
String memberName = member.getName();
JsonValue value = member.getValue();
if (memberName.equals("session_expires_at")) {
try {
String dateStr = value.asString();
this.sessionExpiresAt = BoxDateFormat.parse(dateStr.substring(0, dateStr.length() - 1) + "-00:00");
} catch (ParseException pe) {
assert false : "A ParseException indicates a bug in the SDK.";
}
} else if (memberName.equals("id")) {
this.uploadSessionId = value.asString();
} else if (memberName.equals("part_size")) {
this.partSize = Integer.parseInt(value.toString());
} else if (memberName.equals("session_endpoints")) {
this.sessionEndpoints = new Endpoints(value.asObject());
} else if (memberName.equals("total_parts")) {
this.totalParts = value.asInt();
} else if (memberName.equals("num_parts_processed")) {
this.partsProcessed = value.asInt();
}
}
}
/**
* Represents the end points specific to an upload session.
*/
public static class Endpoints extends BoxJSONObject {
private URL listPartsEndpoint;
private URL commitEndpoint;
private URL uploadPartEndpoint;
private URL statusEndpoint;
private URL abortEndpoint;
/**
* Constructs an Endpoints object using an already parsed JSON object.
*
* @param jsonObject the parsed JSON object.
*/
Endpoints(JsonObject jsonObject) {
super(jsonObject);
}
/**
* Returns the list parts end point.
*
* @return the url of the list parts end point.
*/
public URL getListPartsEndpoint() {
return this.listPartsEndpoint;
}
/**
* Returns the commit end point.
*
* @return the url of the commit end point.
*/
public URL getCommitEndpoint() {
return this.commitEndpoint;
}
/**
* Returns the upload part end point.
*
* @return the url of the upload part end point.
*/
public URL getUploadPartEndpoint() {
return this.uploadPartEndpoint;
}
/**
* Returns the upload session status end point.
*
* @return the url of the session end point.
*/
public URL getStatusEndpoint() {
return this.statusEndpoint;
}
/**
* Returns the abort upload session end point.
*
* @return the url of the abort end point.
*/
public URL getAbortEndpoint() {
return this.abortEndpoint;
}
@Override
protected void parseJSONMember(JsonObject.Member member) {
String memberName = member.getName();
JsonValue value = member.getValue();
try {
if (memberName.equals("list_parts")) {
this.listPartsEndpoint = new URL(value.asString());
} else if (memberName.equals("commit")) {
this.commitEndpoint = new URL(value.asString());
} else if (memberName.equals("upload_part")) {
this.uploadPartEndpoint = new URL(value.asString());
} else if (memberName.equals("status")) {
this.statusEndpoint = new URL(value.asString());
} else if (memberName.equals("abort")) {
this.abortEndpoint = new URL(value.asString());
}
} catch (MalformedURLException mue) {
assert false : "A ParseException indicates a bug in the SDK.";
}
}
}
}