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

com.citrix.sharefile.api.https.upload.SFUploadRunnable Maven / Gradle / Ivy

package com.citrix.sharefile.api.https.upload;

import com.citrix.sharefile.api.SFApiClient;
import com.citrix.sharefile.api.SFConnectionManager;
import com.citrix.sharefile.api.SFSDKDefaultAccessScope;
import com.citrix.sharefile.api.constants.SFKeywords;
import com.citrix.sharefile.api.constants.SFSdkGlobals;
import com.citrix.sharefile.api.enumerations.SFSafeEnum;
import com.citrix.sharefile.api.exceptions.SFCanceledException;
import com.citrix.sharefile.api.exceptions.SFInvalidStateException;
import com.citrix.sharefile.api.exceptions.SFNotAuthorizedException;
import com.citrix.sharefile.api.exceptions.SFOAuthTokenRenewException;
import com.citrix.sharefile.api.exceptions.SFOtherException;
import com.citrix.sharefile.api.exceptions.SFResetUploadException;
import com.citrix.sharefile.api.exceptions.SFSDKException;
import com.citrix.sharefile.api.exceptions.SFServerException;
import com.citrix.sharefile.api.gson.SFGsonHelper;
import com.citrix.sharefile.api.https.SFCookieManager;
import com.citrix.sharefile.api.https.SFHttpsCaller;
import com.citrix.sharefile.api.https.TransferRunnable;
import com.citrix.sharefile.api.interfaces.ISFQuery;
import com.citrix.sharefile.api.log.Logger;
import com.citrix.sharefile.api.models.SFUploadMethod;
import com.citrix.sharefile.api.models.SFUploadRequestParams;
import com.citrix.sharefile.api.models.SFUploadSpecification;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;

import java.io.ByteArrayInputStream;
import java.io.DataOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.security.MessageDigest;
import java.util.Date;

import javax.net.ssl.HttpsURLConnection;

import static com.citrix.sharefile.api.https.upload.UploadHelper.closeStream;

/**
 *
 *  {
 "Method": "Streamed",
 "ChunkUri": "https://storage-ec2-34.sharefile.com/upload-streaming-2.aspx?batchid=723ffb79-d720-4de8-b95d-087be1e6462a&overwrite=true&uploadid=rsu-9c48c04a5fb84f9d8fc93875231b01ca&raw=1&encparams=phT2SuROQY1XrVtmjxwO2yRkHK2uBy1BtAjWhNYAu-lvRQr3eTm-Xh6zYbz3Lc9FfRoDvzBCdI7SyPDVrxL0bR9-cb7IsTFbIcnPRRJ8xLn0nJ0yxIeKXV-m-gb0HWzHGdHOnKTBEEgwD969n-8rbXu6jvkZ3UhEwY6I79GfdTogsf1vi7lFrfPrTe8TV5isPOPSpSINN3qxBI_qWJoy8D_AUnK009vuFHJP4t9yzPi6rDqmfU-7ZhmhncG4rsBfTdEWL3wQiOdU5kNnFl5WkSzjO8dg1vmjSidVSreUN_YJeQw95i8w5XaVA7BGuWDDNXjE8rV53cyQ9p2blo6Xovj4fxQi6wSf0J6CqLKZnFgbHTd7IVvexhdXhTMYAWce01eMBr7iWD9Kd2J3O_nLX3oEWKMDgqCg6SCSd8baO9_EBiQ8QnDwfh8tWeOQq60_gXeX-wBn_TAPNUPTmV1RyD5nt9sn5Q77Rd6YtiOiSjNI98z2mCTSiLp3TYMm4gB38CawObllVnJuWIERHKGbyEwJhE5PNxdMQC4PeM4$",
 "IsResume": false,
 "ResumeIndex": 0,
 "ResumeOffset": 0,
 "ResumeFileHash": "",
 "odata.metadata": "https://nilesh.sf-api.com/sf/v3/$metadata#UploadSpecification/ShareFile.Api.Models.UploadSpecification@Element"
 }
 */
public class SFUploadRunnable extends TransferRunnable
{
    private static final String TAG = "SFUploadRunnable";

    //Server does not localize this error.
    private static final String INVALID_UPLOAD_ID = "Unrecognized Upload ID";

    private final long mResumeFromByteIndex;
    private final long mTotalBytes;
    private final InputStream mFileInputStream;
    private final String mDestinationFileName;

    private final String mDetails;

    //	private final String mParentId;
    private final String mV3Url;
    private final boolean mOverwrite;

    private SFUploadSpecification mUploadSpecification;
    private SFChunkUploadResponse mChunkUploadResponse=null;

    private String localFilePath;

    private static final long sOneMB = 1024 * 1024;

    private boolean mHasMultiThreadedCapability;

    public interface IUploadProgress extends IProgress {
        void bytesTransfered(long byteCount, long chunkIndex, long previousUploadedByteOffset);
    }

    public void setUploadSpec(String previousUploadSpec)
    {
        Gson gson = new Gson();
        mUploadSpecification = gson.fromJson(previousUploadSpec,SFUploadSpecification.class);
    }

    public String getUploadSpec() throws SFNotAuthorizedException, SFOAuthTokenRenewException, SFOtherException, SFInvalidStateException, SFServerException
    {
        if(mUploadSpecification == null)
        {
            mUploadSpecification = getSpecification();
        }

        Gson gson = new Gson();
        return gson.toJson(mUploadSpecification,SFUploadSpecification.class);
    }

    public SFUploadRunnable(
            String v3Url, boolean overwrite,
            long resumeFromByteIndex, long tolalBytes, String destinationName,
            InputStream inputStream, SFApiClient client, IUploadProgress progressListener,
            SFCookieManager cookieManager, String connUserName, String connPassword, String details) {
        super(client,progressListener,cookieManager,connUserName,connPassword);
        mResumeFromByteIndex = resumeFromByteIndex;
        mDestinationFileName = destinationName;
        mTotalBytes = tolalBytes;
        mFileInputStream = inputStream;
        mDetails = details;

        // mParentId = parentId;
        mV3Url = v3Url;
        mOverwrite = overwrite;
    }

    public SFUploadRunnable(
            String v3Url, boolean overwrite,
            long resumeFromByteIndex, long tolalBytes, String destinationName,
            InputStream inputStream, SFApiClient client, IUploadProgress progressListener,
            SFCookieManager cookieManager, String connUserName, String connPassword, String details,
            String localFilePath, boolean hasMultiThreadedCapability) {
        this(v3Url, overwrite, resumeFromByteIndex, tolalBytes, destinationName, inputStream, client, progressListener, cookieManager, connUserName, connPassword, details);
        this.localFilePath = localFilePath;
        mHasMultiThreadedCapability = hasMultiThreadedCapability;
    }

    private void abortIfCancelledRequested() throws SFCanceledException
    {
        if ( cancelRequested.get() )
        {
            throw new SFCanceledException("Upload Cancelled");
        }
    }

    private boolean shouldUseThreadedUpload() {
        if(!mHasMultiThreadedCapability) {
            return false;
        }

        if(mTotalBytes / sOneMB <= 8) {
            return false;
        }
        return true;

    }

    private int getNumberOfThreads() {
        if(mUploadSpecification != null && mUploadSpecification.getMaxNumberOfThreads() != null && mUploadSpecification.getMaxNumberOfThreads() > 0) {
            return mUploadSpecification.getMaxNumberOfThreads();
        }
        return 1;
    }

    protected void runInThisThread() throws SFSDKException
    {
        try
        {
            if(mUploadSpecification == null)
            {
                mUploadSpecification = getSpecification();// get spec
            }

            abortIfCancelledRequested();

			if(shouldUseThreadedUpload()) {
				MultiThreadedUploadManager manager = new MultiThreadedUploadManager(mUsername, mPassword, mCookieManager, mUploadSpecification,
						mResumeFromByteIndex, mFileInputStream, (IUploadProgress) mProgressListener, getNumberOfThreads(), mApiClient, mTotalBytes, cancelRequested, localFilePath);
				manager.execute();
			}
			else {
				upload();
			}

            abortIfCancelledRequested();
        }
        catch (SFSDKException e)
        {
            Logger.e(TAG, e);
            throw e;
        }
        catch(Exception e)
        {
            Logger.e(TAG, e);
            SFOtherException other = new SFOtherException(e);
            throw other;
        }
    }

    private SFUploadSpecification getSpecification() throws SFInvalidStateException,
            SFServerException,
            SFNotAuthorizedException,
            SFOAuthTokenRenewException,
            SFOtherException{
        if(shouldUseThreadedUpload()) {
            return getThreadedSpecification();
        }
        return getStandardSpecification();
    }

    private SFUploadSpecification getStandardSpecification() throws SFInvalidStateException,
            SFServerException,
            SFNotAuthorizedException,
            SFOAuthTokenRenewException,
            SFOtherException
    {
        try
        {
            Date now = new Date();
            ISFQuery uploadQuery = mApiClient.items().upload(new URI(mV3Url)
                    ,new SFSafeEnum(SFUploadMethod.Streamed),
                    true,
                    mDestinationFileName,
                    mTotalBytes,
                    "",
                    false,
                    true,
                    false,
                    false,
                    "SFJavaSDK",
                    mOverwrite,
                    mDestinationFileName,
                    mDetails,
                    false,
                    "",
                    "",
                    1,
                    "json",
                    false, now,now);

            uploadQuery.setCredentials(mUsername,mPassword);

            return mApiClient.executeQuery(uploadQuery);
        }
        catch (URISyntaxException e)
        {
            Logger.e(TAG, e);
        }

        return null;
    }

    private SFUploadSpecification getThreadedSpecification() throws SFInvalidStateException,
            SFServerException,
            SFNotAuthorizedException,
            SFOAuthTokenRenewException,
            SFOtherException {
        try {
            ISFQuery uploadQuery = mApiClient.items().upload(new URI(mV3Url),
                    new SFSafeEnum<>(SFUploadMethod.Threaded),
                    true,
                    mDestinationFileName,
                    mTotalBytes,
                    "",
                    false,
                    false,
                    false,
                    false,
                    "SFJavaSDK",
                    true);

            uploadQuery.setCredentials(mUsername, mPassword);

            return mApiClient.executeQuery(uploadQuery);
        }
        catch (URISyntaxException e) {
            Logger.e(TAG, e);
        }
        return null;
    }

    private SFUploadRequestParams buildUploadRequestParams(String fileName,
                                                           String details, long fileSize)
    {
        Date now = new Date();

        SFUploadRequestParams uploadRequestParams = new SFUploadRequestParams();
        uploadRequestParams.setFileName(fileName);
        uploadRequestParams.setClientCreatedDate(now);
        uploadRequestParams.setClientModifiedDate(now);
        uploadRequestParams.setDetails(details);
        uploadRequestParams.setFileSize(fileSize);
        uploadRequestParams.setMethod(new SFSafeEnum(SFUploadMethod.Streamed));
        uploadRequestParams.setOverwrite(true);
        uploadRequestParams.setThreadCount(1);
        uploadRequestParams.setTitle(fileName);
        uploadRequestParams.setTool("SFV3JAVASDK");
        uploadRequestParams.setRaw(true);

        return uploadRequestParams;
    }

    private void seekInputStream()
    {
        try
        {
            if(mResumeFromByteIndex > 0)
            {
                Logger.d(TAG,"ResumeSupp:Resuming Upload from byte: " + mResumeFromByteIndex);
                mFileInputStream.skip(mResumeFromByteIndex);
            }
            else {
                Logger.d(TAG,"ResumeSupp:Brand new upload");
            }
        }
        catch(Exception e)
        {
            Logger.d(TAG, "Seek exception" , e);
        }
    }



    public static String md5ToString(MessageDigest md)
    {
        StringBuilder hash = new StringBuilder();
        byte digest[] = md.digest();
        String hex;
        for(byte part : digest) {
            hex = Integer.toHexString(0xff & part);
            if(hex.length() == 1)
                hash.append("0");
            hash.append(hex);
        }
        return hash.toString();
    }

    /**
     * Chunk upload response can be of json type sample:
     *
     * //Sample error message {"error":true,"errorMessage":"Thread was being aborted.","errorCode":420}
     *
     */
    public static class SFChunkUploadResponse
    {
        public boolean mWasError;
        public int mErrorCode;
        public String mErrorMessage;
        public int mBytesTransferedInChunk;
        public String mItemId;

        @SFSDKDefaultAccessScope SFChunkUploadResponse(String jsonString, boolean isLastChunk)
        {
            try
            {
                JsonParser jsonParser = new JsonParser();
                JsonElement jsonElement = jsonParser.parse(jsonString);
                JsonObject jsonObject = jsonElement.getAsJsonObject();

                mWasError =  SFGsonHelper.getBoolean(jsonObject, "error", false);

                if(mWasError)
                {
                    mErrorMessage = SFGsonHelper.getString(jsonObject, "errorMessage", "");
                    mErrorCode = SFGsonHelper.getInt(jsonObject, "errorCode", 0);
                    Logger.d(TAG, "Parsed Chunk response: " + mErrorMessage);
                }
                // Parse for item id only in the last chunk
                else if (isLastChunk)
                {
                    String value = SFGsonHelper.getString(jsonObject, "value", "");
                    Logger.d(TAG, "Parsed Chunk response: value = " + value);
                    if (value!=null) {
                        JsonArray valueArray = jsonObject.getAsJsonArray("value");
                        if (valueArray != null && valueArray.size() >= 1) {
                            JsonObject firstValue = (JsonObject) valueArray.get(0);
                            mItemId = SFGsonHelper.getString(firstValue, "id", null);
                            Logger.d(TAG, "Parsed Chunk response : item id = " + mItemId);
                        }
                    }
                }

            }
            catch (Exception e)
            {
                Logger.e(TAG,new Exception(jsonString));
                mWasError = true;
                mErrorMessage = "exception parsing upload response";
                mErrorCode = SFSdkGlobals.INTERNAL_HTTP_ERROR;
            }
        }

    }

    /**
     *   This tries to upload a chunk. Returns a detialed object with the httpErrorCode and the ChunkResponse from the server.
     *   ChunkResonse will never be null. In case of http errors or exceptions we fill the chunk response with https err response string.
     */
    private long uploadChunk(byte[] fileChunk,int chunkLength,long index,boolean isLast, MessageDigest md, long previousChunkTotal) throws SFSDKException
    {
        long bytesUploaded = 0;
        HttpsURLConnection conn = null;
        String responseString = null;
        int httpErrorCode;

        try
        {
            //md5 hash buffer
            md.update(fileChunk, 0, chunkLength);

            //you need the RAW param or you'll have to do HTTP multi-part post...
            String append = UploadHelper.getAppendParams(mDestinationFileName, mDetails, mTotalBytes,isLast?1:0, isLast, md5ToString(md),index, previousChunkTotal);
            final String finalURL = mUploadSpecification.getChunkUri() + append;

            conn = UploadHelper.getChunkUploadConnection(finalURL, mApiClient, mUsername, mPassword, mCookieManager, chunkLength);
            SFConnectionManager.connect(conn);

            //small buffer between the chunk and the stream so we can interrupt and kill task quickly
            final byte[] buffer = new byte[1024];
            final ByteArrayInputStream in = new ByteArrayInputStream(fileChunk,0,chunkLength);
            int currentBytesRead;
            OutputStream poster = new DataOutputStream(conn.getOutputStream());

            while((currentBytesRead = in.read(buffer,0,1024)) >0)
            {
                poster.write(buffer,0,currentBytesRead);
                bytesUploaded+=(long)currentBytesRead;
                poster.flush();//needs to be here

                // only send notifications every 64kb
                if ( bytesUploaded % (64*1024) == 0 ) updateProgress(bytesUploaded+previousChunkTotal);

                abortIfCancelledRequested();
            }

            poster.close();

            httpErrorCode = SFHttpsCaller.safeGetResponseCode(conn);

            SFHttpsCaller.getAndStoreCookies(conn, new URL(finalURL),mCookieManager);

            switch(httpErrorCode )
            {
                case HttpsURLConnection.HTTP_OK:
                    responseString = SFHttpsCaller.readResponse(conn);
                    Logger.d(TAG, "Upload Response: " + responseString);

                    mChunkUploadResponse = new SFChunkUploadResponse(responseString, isLast);
                    if(!mChunkUploadResponse.mWasError)
                    {
                        mChunkUploadResponse.mBytesTransferedInChunk = (int) bytesUploaded;
                        mTotalBytesTransferredForThisFile +=bytesUploaded;
                        mItemId = mChunkUploadResponse.mItemId;
                        return bytesUploaded;
                    }
                    else
                    {
                        throw new SFServerException(httpErrorCode,mChunkUploadResponse.mErrorMessage);
                    }
                    //break;

                case HttpsURLConnection.HTTP_UNAUTHORIZED:
                    throw new SFNotAuthorizedException(SFKeywords.UN_AUTHORIZED);
                    //break;

                default:
                    responseString = SFHttpsCaller.readErrorResponse(conn);
                    Logger.d(TAG, "Upload Err Response: " + responseString);
                    throw new SFServerException(httpErrorCode,responseString);
                    //break
            }
        }
        catch (SFSDKException e)
        {
            throw e;
        }
        catch(Exception ex)
        {
            Logger.e(TAG,ex);
            SFOtherException other = new SFOtherException(ex);
            throw other;
        }
        finally
        {
            SFHttpsCaller.disconnect(conn);
        }
    }

    private void upload() throws SFSDKException
    {
        long startTime = System.currentTimeMillis();
        int chunkSize = 1024*1024;
        long previousChunkTotalBytes = mResumeFromByteIndex;

        try
        {
            Logger.d(TAG, "POST " + mUploadSpecification.getChunkUri());
            boolean isZeroBytesFile = (mFileInputStream.available() == 0);
            boolean isLast = false;
            seekInputStream();
            int chunkLength;
            final MessageDigest md = MessageDigest.getInstance("MD5");
            byte[] fileChunk = new byte[chunkSize];
            boolean done = false;
            long index = previousChunkTotalBytes/chunkSize;
            while(!done)
            {
                if(isZeroBytesFile)
                {
                    chunkLength = 0;
                    isLast = true;
                    //Don't break here so that we can get a chance to finalize the upload.
                }
                else {
                    chunkLength = mFileInputStream.read(fileChunk, 0, fileChunk.length);
                    if (chunkLength < 0) {
                        Logger.d(TAG, "Chunk < 0: " + chunkLength);
                        done = true;
                        break;
                    }

                    isLast = (mFileInputStream.available() == 0);
                }

                if(isLast)
                {
                    Logger.d(TAG,"isLast = true");
                    done = true;
                }

                previousChunkTotalBytes += uploadChunk(fileChunk,chunkLength, index++,isLast,md,previousChunkTotalBytes);

                abortIfCancelledRequested();

                Thread.yield();
            }
            long endTime = System.currentTimeMillis();
            long elapsedTime = endTime - startTime;

            Logger.d(TAG, "Streamed Upload Time to upload file " + elapsedTime);
            if(mProgressListener!=null)
            {
                mProgressListener.onComplete(mTotalBytesTransferredForThisFile, mItemId);
            }
        }
        catch (SFServerException e)
        {
            if(e.getLocalizedMessage().contains(INVALID_UPLOAD_ID))
            {
                e = new SFResetUploadException(e);
            }

            throw e;
        }
        catch (SFSDKException ex)
        {
            throw ex;
        }
        catch(Exception ex)
        {
            SFOtherException other = new SFOtherException(ex);
            throw other;
        }
        finally
        {
            closeStream(mFileInputStream);
        }
    }

    private void updateProgress(long uploadedBytes)
    {
        if(mProgressListener == null) {
            return;
        }

        try
        {
            Logger.d(TAG,"ResumeSupp: Bytes Uploaded = " + uploadedBytes);
            mProgressListener.bytesTransfered(uploadedBytes);
        }
        catch(Exception e)
        {
            Logger.d(TAG, "exception update progress", e);
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy