All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.box.sdk.LargeFileUpload Maven / Gradle / Ivy

The newest version!
package com.box.sdk;

import com.box.sdk.http.HttpMethod;
import com.eclipsesource.json.Json;
import com.eclipsesource.json.JsonObject;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.security.DigestInputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * Utility class for uploading large files.
 */
public final class LargeFileUpload {
    private static final String DIGEST_ALGORITHM_SHA1 = "SHA1";
    private static final int DEFAULT_CONNECTIONS = 3;
    private static final int DEFAULT_TIMEOUT = 1;
    private static final TimeUnit DEFAULT_TIMEUNIT = TimeUnit.HOURS;
    private static final int THREAD_POOL_WAIT_TIME_IN_MILLIS = 1000;
    private final ThreadPoolExecutor executorService;
    private final long timeout;
    private final TimeUnit timeUnit;

    /**
     * Creates a LargeFileUpload object.
     *
     * @param nParallelConnections number of parallel http connections to use
     * @param timeOut              time to wait before killing the job
     * @param unit                 time unit for the time wait value
     */
    public LargeFileUpload(int nParallelConnections, long timeOut, TimeUnit unit) {
        this.executorService = (ThreadPoolExecutor) Executors.newFixedThreadPool(nParallelConnections);
        this.timeout = timeOut;
        this.timeUnit = unit;
    }

    /**
     * Creates a LargeFileUpload object with a default number of parallel conections and timeout.
     */
    public LargeFileUpload() {
        this.executorService = (ThreadPoolExecutor) Executors.newFixedThreadPool(LargeFileUpload.DEFAULT_CONNECTIONS);
        this.timeout = LargeFileUpload.DEFAULT_TIMEOUT;
        this.timeUnit = LargeFileUpload.DEFAULT_TIMEUNIT;
    }

    private static byte[] getBytesFromStream(InputStream stream, int numBytes) {

        int bytesNeeded = numBytes;
        int offset = 0;
        byte[] bytes = new byte[numBytes];

        while (bytesNeeded > 0) {

            int bytesRead;
            try {
                bytesRead = stream.read(bytes, offset, bytesNeeded);
            } catch (IOException ioe) {
                throw new BoxAPIException("Reading data from stream failed.", ioe);
            }

            if (bytesRead == -1) {
                throw new BoxAPIException("Stream ended while upload was progressing");
            }

            bytesNeeded = bytesNeeded - bytesRead;
            offset = offset + bytesRead;
        }

        return bytes;
    }

    private BoxFileUploadSession.Info createUploadSession(BoxAPIConnection boxApi, String folderId,
                                                          URL url, String fileName, long fileSize) {

        BoxJSONRequest request = new BoxJSONRequest(boxApi, url, HttpMethod.POST);

        //Create the JSON body of the request
        JsonObject body = new JsonObject();
        body.add("folder_id", folderId);
        body.add("file_name", fileName);
        body.add("file_size", fileSize);
        request.setBody(body.toString());

        try (BoxJSONResponse response = request.send()) {
            JsonObject jsonObject = Json.parse(response.getJSON()).asObject();

            String sessionId = jsonObject.get("id").asString();
            BoxFileUploadSession session = new BoxFileUploadSession(boxApi, sessionId);

            return session.new Info(jsonObject);
        }
    }

    /**
     * Uploads a new large file.
     *
     * @param boxApi   the API connection to be used by the upload session.
     * @param folderId the id of the folder in which the file will be uploaded.
     * @param stream   the input stream that feeds the content of the file.
     * @param url      the upload session URL.
     * @param fileName the name of the file to be created.
     * @param fileSize the total size of the file.
     * @return the created file instance.
     * @throws InterruptedException when a thread gets interupted.
     * @throws IOException          when reading a stream throws exception.
     */
    public BoxFile.Info upload(BoxAPIConnection boxApi, String folderId, InputStream stream, URL url,
                               String fileName, long fileSize) throws InterruptedException, IOException {
        //Create a upload session
        BoxFileUploadSession.Info session = this.createUploadSession(boxApi, folderId, url, fileName, fileSize);
        return this.uploadHelper(session, stream, fileSize, null);
    }

    /**
     * Uploads a new large file and sets file attributes.
     *
     * @param boxApi         the API connection to be used by the upload session.
     * @param folderId       the id of the folder in which the file will be uploaded.
     * @param stream         the input stream that feeds the content of the file.
     * @param url            the upload session URL.
     * @param fileName       the name of the file to be created.
     * @param fileSize       the total size of the file.
     * @param fileAttributes file attributes to set
     * @return the created file instance.
     * @throws InterruptedException when a thread gets interupted.
     * @throws IOException          when reading a stream throws exception.
     */
    public BoxFile.Info upload(BoxAPIConnection boxApi, String folderId, InputStream stream, URL url,
                               String fileName, long fileSize, Map fileAttributes)
        throws InterruptedException, IOException {
        //Create a upload session
        BoxFileUploadSession.Info session = this.createUploadSession(boxApi, folderId, url, fileName, fileSize);
        return this.uploadHelper(session, stream, fileSize, fileAttributes);
    }

    /**
     * Creates a new version of a large file.
     *
     * @param boxApi   the API connection to be used by the upload session.
     * @param stream   the input stream that feeds the content of the file.
     * @param url      the upload session URL.
     * @param fileSize the total size of the file.
     * @return the file instance that also contains the version information.
     * @throws InterruptedException when a thread gets interupted.
     * @throws IOException          when reading a stream throws exception.
     */
    public BoxFile.Info upload(BoxAPIConnection boxApi, InputStream stream, URL url, long fileSize)
        throws InterruptedException, IOException {
        //creates a upload session
        BoxFileUploadSession.Info session = this.createUploadSession(boxApi, url, fileSize);
        return this.uploadHelper(session, stream, fileSize, null);
    }

    /**
     * Creates a new version of a large file and sets file attributes.
     *
     * @param boxApi         the API connection to be used by the upload session.
     * @param stream         the input stream that feeds the content of the file.
     * @param url            the upload session URL.
     * @param fileSize       the total size of the file.
     * @param fileAttributes file attributes to set.
     * @return the file instance that also contains the version information.
     * @throws InterruptedException when a thread gets interupted.
     * @throws IOException          when reading a stream throws exception.
     */
    public BoxFile.Info upload(BoxAPIConnection boxApi, InputStream stream, URL url, long fileSize,
                               Map fileAttributes)
        throws InterruptedException, IOException {
        //creates an upload session
        BoxFileUploadSession.Info session = this.createUploadSession(boxApi, url, fileSize);
        return this.uploadHelper(session, stream, fileSize, fileAttributes);
    }

    private BoxFile.Info uploadHelper(BoxFileUploadSession.Info session, InputStream stream, long fileSize,
                                      Map fileAttributes)
        throws InterruptedException {
        //Upload parts using the upload session
        MessageDigest digest;
        try {
            digest = MessageDigest.getInstance(DIGEST_ALGORITHM_SHA1);
        } catch (NoSuchAlgorithmException ae) {
            throw new BoxAPIException("Digest algorithm not found", ae);
        }
        DigestInputStream dis = new DigestInputStream(stream, digest);
        List parts = this.uploadParts(session, dis, fileSize);

        //Creates the file hash
        byte[] digestBytes = digest.digest();
        String digestStr = Base64.encode(digestBytes);

        //Commit the upload session. If there is a failure, abort the commit.
        try {
            return session.getResource().commit(digestStr, parts, fileAttributes, null, null);
        } catch (Exception e) {
            session.getResource().abort();
            throw new BoxAPIException("Unable to commit the upload session", e);
        }
    }

    private BoxFileUploadSession.Info createUploadSession(BoxAPIConnection boxApi, URL url, long fileSize) {
        BoxJSONRequest request = new BoxJSONRequest(boxApi, url, HttpMethod.POST);

        //Creates the body of the request
        JsonObject body = new JsonObject();
        body.add("file_size", fileSize);
        request.setBody(body.toString());

        try (BoxJSONResponse response = request.send()) {
            JsonObject jsonObject = Json.parse(response.getJSON()).asObject();

            String sessionId = jsonObject.get("id").asString();
            BoxFileUploadSession session = new BoxFileUploadSession(boxApi, sessionId);

            return session.new Info(jsonObject);
        }
    }

    /*
     * Upload parts of the file. The part size is retrieved from the upload session.
     */
    private List uploadParts(
        BoxFileUploadSession.Info session, InputStream stream, long fileSize
    ) throws InterruptedException {
        List parts = new ArrayList<>();

        int partSize = session.getPartSize();
        long offset = 0;
        long processed = 0;
        int partPostion = 0;
        //Set the Max Queue Size to 1.5x the number of processors
        double maxQueueSizeDouble = Math.ceil(this.executorService.getMaximumPoolSize() * 1.5);
        int maxQueueSize = Double.valueOf(maxQueueSizeDouble).intValue();
        while (processed < fileSize) {
            //Waiting for any thread to finish before
            long timeoutForWaitingInMillis = TimeUnit.MILLISECONDS.convert(this.timeout, this.timeUnit);
            if (this.executorService.getCorePoolSize() <= this.executorService.getActiveCount()) {
                if (timeoutForWaitingInMillis > 0) {
                    Thread.sleep(LargeFileUpload.THREAD_POOL_WAIT_TIME_IN_MILLIS);
                    timeoutForWaitingInMillis -= THREAD_POOL_WAIT_TIME_IN_MILLIS;
                } else {
                    throw new BoxAPIException("Upload parts timedout");
                }
            }
            if (this.executorService.getQueue().size() < maxQueueSize) {
                long diff = fileSize - processed;
                //The size last part of the file can be lesser than the part size.
                if (diff < (long) partSize) {
                    partSize = (int) diff;
                }
                parts.add(null);
                byte[] bytes = getBytesFromStream(stream, partSize);
                this.executorService.execute(
                    new LargeFileUploadTask(session.getResource(), bytes, offset,
                        partSize, fileSize, parts, partPostion)
                );

                //Increase the offset and proceesed bytes to calculate the Content-Range header.
                processed += partSize;
                offset += partSize;
                partPostion++;
            }
        }
        this.executorService.shutdown();
        this.executorService.awaitTermination(this.timeout, this.timeUnit);
        return parts;
    }

    /**
     * Generates the Base64 encoded SHA-1 hash for content available in the stream.
     * It can be used to calculate the hash of a file.
     *
     * @param stream the input stream of the file or data.
     * @return the Base64 encoded hash string.
     */
    public String generateDigest(InputStream stream) {
        MessageDigest digest;
        try {
            digest = MessageDigest.getInstance(DIGEST_ALGORITHM_SHA1);
        } catch (NoSuchAlgorithmException ae) {
            throw new BoxAPIException("Digest algorithm not found", ae);
        }

        //Calcuate the digest using the stream.
        DigestInputStream dis = new DigestInputStream(stream, digest);
        try {
            int value = dis.read();
            while (value != -1) {
                value = dis.read();
            }
        } catch (IOException ioe) {
            throw new BoxAPIException("Reading the stream failed.", ioe);
        }

        //Get the calculated digest for the stream
        byte[] digestBytes = digest.digest();
        return Base64.encode(digestBytes);
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy