package com.mediafire.sdk.uploader;
import com.mediafire.sdk.MFApiException;
import com.mediafire.sdk.MFException;
import com.mediafire.sdk.MFSessionNotStartedException;
import com.mediafire.sdk.MediaFire;
import com.mediafire.sdk.api.UploadApi;
import com.mediafire.sdk.api.responses.*;
import com.mediafire.sdk.api.responses.data_models.*;
import com.mediafire.sdk.util.HashUtil;
import java.io.*;
import java.io.File;
import java.net.URLEncoder;
import java.util.*;
/**
* Runnable which handles Web Uploads, Simple Uploads, and Resumable Uploads
*/
public class MediaFireUpload implements Runnable {
private static final int TIME_BETWEEN_POLLS_MILLIS = 1000 * 5;
private static final int MAX_POLLS = 24;
private static final long FOUR_MB = 4000000;
private static final String UTF8 = "UTF-8";
private static final String PARAM_RESPONSE_FORMAT = "response_format";
private static final String PARAM_URL = "url";
private static final String PARAM_FILENAME = "filename";
private static final String PARAM_FOLDER_KEY = "folder_key";
private static final String PARAM_FOLDER_PATH = "path";
private static final String PARAM_RESUMABLE = "resumable";
private static final String PARAM_SIZE = "size";
private static final String PARAM_HASH = "hash";
private static final String PARAM_KEY = "key";
private static final String PARAM_UPLOAD_KEY = "upload_key";
private static final String PARAM_ALL_WEB_UPLOADS = "all_web_uploads";
private static final String JSON = "json";
private static final String HEADER_CONTENT_TYPE = "Content-Type";
private static final String HEADER_CONTENT_LENGTH = "Content-Length";
private static final String HEADER_X_FILENAME = "x-filename";
private static final String HEADER_X_FILESIZE = "x-filesize";
private static final String HEADER_X_FILEHASH = "x-filehash";
private static final String HEADER_X_UNIT_HASH = "x-unit-hash";
private static final String HEADER_X_UNIT_ID = "x-unit-id";
private static final String HEADER_X_UNIT_SIZE = "x-unit-size";
private static final String CONTENT_TYPE_OCTET_STREAM = "application/octet-stream";
// args passed via constructor
private final MediaFire mediaFire;
private final int statusToFinish;
private File file;
private ActionOnInAccount actionOnInAccount;
// values set by class during execution
private String url;
private final MediaFireUploadHandler handler;
private String fileHash;
private List<Boolean> uploadUnits = new LinkedList<Boolean>();
// optional values that can be set
private final String filename;
private String folderKey;
private String folderPath;
private final long id;
public MediaFireUpload(MediaFire mediaFire, int statusToFinish, File file, String filename, String folderPath, ActionOnInAccount actionOnInAccount, MediaFireUploadHandler handler, long id) {
this.mediaFire = mediaFire;
this.statusToFinish = statusToFinish;
this.file = file;
this.filename = filename;
this.folderPath = folderPath;
this.actionOnInAccount = actionOnInAccount;
this.handler = handler;
this.id = id;
}
public MediaFireUpload(MediaFire mediaFire, int statusToFinish, String url, String filename, MediaFireUploadHandler handler, long id) {
this.mediaFire = mediaFire;
this.statusToFinish = statusToFinish;
this.url = url;
this.filename = filename;
this.handler = handler;
this.id = id;
}
private void setOptionalFolderKey(String folderKey) {
this.folderKey = folderKey;
}
private void setOptionalFolderPath(String folderPath) {
this.folderPath = folderPath;
}
public long getId() {
return this.id;
}
@Override
public void run() {
try {
if (this.url != null && !this.url.isEmpty()) {
addWebUpload();
} else if (file != null && file.exists()) {
if (this.file.length() < FOUR_MB) {
simpleUpload();
} else {
checkUpload();
}
} else {
throw new MFException("no url or file was passed to upload");
}
} catch (MFException e) {
if (this.handler != null) {
this.handler.uploadFailed(this.id, e);
}
} catch (MFApiException e) {
if (this.handler != null) {
this.handler.uploadFailed(this.id, e);
}
} catch (IOException e) {
if (this.handler != null) {
this.handler.uploadFailed(this.id, e);
}
} catch (InterruptedException e) {
if (this.handler != null) {
this.handler.uploadFailed(this.id, e);
}
} catch (MFSessionNotStartedException e) {
if (this.handler != null) {
this.handler.uploadFailed(this.id, e);
}
}
}
private void checkUpload() throws IOException, MFException, MFApiException, InterruptedException, MFSessionNotStartedException {
LinkedHashMap<String, Object> params = new LinkedHashMap<String, Object>();
params.put(PARAM_RESPONSE_FORMAT, JSON);
params.put(PARAM_RESUMABLE, "yes");
params.put(PARAM_SIZE, this.file.length());
params.put(PARAM_HASH, getFileHash());
params.put(PARAM_FILENAME, this.filename);
if (this.folderKey != null && !this.folderKey.isEmpty()) {
params.put(PARAM_FOLDER_KEY, this.folderKey);
}
if (this.folderPath != null && !this.folderPath.isEmpty()) {
params.put(PARAM_FOLDER_PATH, this.folderPath);
}
UploadCheckResponse response = UploadApi.check(this.mediaFire, params, "1.4", UploadCheckResponse.class);
String hashExists = response.getHashExists();
String inAccount = response.getInAccount();
String inFolder = response.getInFolder();
String duplicateQuickKey = response.getDuplicateQuickkey();
if ("yes".equals(inAccount)) {
switch (actionOnInAccount) {
case UPLOAD_ALWAYS:
instantUpload();
break;
case UPLOAD_IF_NOT_IN_FOLDER:
if ("no".equals(inFolder)) {
instantUpload();
} else {
uploadFinished(duplicateQuickKey);
}
break;
case DO_NOT_UPLOAD:
default:
uploadFinished(duplicateQuickKey);
break;
}
return;
}
if ("yes".equals(hashExists)) {
instantUpload();
} else {
ResumableUpload resumableUpload = response.getResumableUpload();
resumableUpload(resumableUpload);
}
}
private void instantUpload() throws MFException, MFApiException, IOException, MFSessionNotStartedException {
LinkedHashMap<String, Object> params = new LinkedHashMap<String, Object>();
params.put(PARAM_RESPONSE_FORMAT, JSON);
params.put(PARAM_SIZE, this.file.length());
params.put(PARAM_HASH, getFileHash());
params.put(PARAM_FILENAME, URLEncoder.encode(filename, UTF8));
if (this.folderKey != null && !this.folderKey.isEmpty()) {
params.put(PARAM_FOLDER_KEY, folderKey);
}
if (this.folderPath != null && !this.folderPath.isEmpty()) {
params.put(PARAM_FOLDER_PATH, this.folderPath);
}
UploadInstantResponse response = UploadApi.instant(this.mediaFire, params, "1.4", UploadInstantResponse.class);
String quickKey = response.getQuickKey();
String fileName = response.getFileName();
uploadFinished(quickKey, fileName);
}
private void resumableUpload(ResumableUpload resumableUpload) throws MFException, MFApiException, IOException, InterruptedException, MFSessionNotStartedException {
LinkedHashMap<String, Object> params = new LinkedHashMap<String, Object>();
params.put(PARAM_RESPONSE_FORMAT, JSON);
if (this.folderKey != null && !this.folderKey.isEmpty()) {
params.put(PARAM_FOLDER_KEY, folderKey);
}
if (this.folderPath != null && !this.folderPath.isEmpty()) {
params.put(PARAM_FOLDER_PATH, this.folderPath);
}
// base headers that don't change
Map<String, Object> headers = new HashMap<String, Object>();
headers.put(HEADER_X_FILESIZE, file.length());
headers.put(HEADER_X_FILEHASH, getFileHash());
headers.put(HEADER_CONTENT_TYPE, CONTENT_TYPE_OCTET_STREAM);
headers.put(HEADER_X_FILENAME, URLEncoder.encode(this.filename, UTF8));
int numUnits = resumableUpload.getNumberOfUnits();
int unitSize = resumableUpload.getUnitSize();
for (int chunkNumber = 0; chunkNumber < numUnits; chunkNumber++) {
if (isChunkUploaded(chunkNumber)) {
continue;
}
int chunkSize = getChunkSize(chunkNumber, numUnits, this.file.length(), unitSize);
byte[] chunk = makeChunk(unitSize, chunkNumber);
String chunkHash = HashUtil.sha256(chunk);
headers.put(HEADER_X_UNIT_ID, chunkNumber);
headers.put(HEADER_X_UNIT_SIZE, chunkSize);
headers.put(HEADER_X_UNIT_HASH, chunkHash);
UploadResumableResponse response = UploadApi.resumable(this.mediaFire, params, headers, chunk, "1.4", UploadResumableResponse.class);
ResumableDoUpload doUpload = response.getDoUpload();
ResumableUpload newResumableUpload = response.getResumableUpload();
String allUnitsReady = newResumableUpload.getAllUnitsReady();
if (allUnitsReady != null && "yes".equals(allUnitsReady) && doUpload != null) {
String uploadKey = doUpload.getKey();
pollUpload(uploadKey);
return;
}
ResumableBitmap bitmap = resumableUpload.getBitmap();
if (bitmap != null) {
int count = bitmap.getCount();
List<Integer> words = bitmap.getWords();
updateUploadBitmap(count, words);
}
int numUploaded = 0;
for (int chunkCount = 0; chunkCount < numUnits; chunkCount++) {
if (isChunkUploaded(chunkCount)) {
numUploaded++;
}
}
double percentFinished = (double) numUploaded / (double) numUnits;
percentFinished *= 100;
uploadProgress(percentFinished);
}
}
private void pollUpload(String uploadKey) throws MFException, MFApiException, InterruptedException, MFSessionNotStartedException {
final LinkedHashMap<String, Object> params = new LinkedHashMap<String, Object>();
params.put(PARAM_RESPONSE_FORMAT, JSON);
params.put(PARAM_KEY, uploadKey);
long pollCount = 0;
do {
UploadPollUploadResponse response = UploadApi.pollUpload(this.mediaFire, params, "1.4", UploadPollUploadResponse.class);
PollDoUpload doUpload = response.getDoUpload();
int fileErrorCode = doUpload.getFileErrorCode();
int resultCode = doUpload.getResultCode();
int statusCode = doUpload.getStatusCode();
String description = doUpload.getDescription();
String quickKey = doUpload.getQuickKey();
String filename = doUpload.getFilename();
if (quickKey != null && !quickKey.isEmpty()) {
uploadFinished(quickKey);
return;
}
if (statusCode >= statusToFinish) {
uploadFinished(quickKey, filename);
return;
}
if (fileErrorCode != 0) {
throw new MFApiException(fileErrorCode, "file error code " + fileErrorCode + " while polling");
}
if (resultCode != 0) {
throw new MFApiException(fileErrorCode, "resultCode code " + resultCode + " while polling");
}
if (handler != null) {
handler.uploadPolling(id, statusCode, description);
}
Thread.sleep(TIME_BETWEEN_POLLS_MILLIS);
pollCount++;
} while (pollCount <= MAX_POLLS);
}
private void getWebUploads(String uploadKey) throws MFException, MFApiException, InterruptedException, MFSessionNotStartedException {
final LinkedHashMap<String, Object> params = new LinkedHashMap<String, Object>();
params.put(PARAM_RESPONSE_FORMAT, JSON);
params.put(PARAM_UPLOAD_KEY, uploadKey);
params.put(PARAM_ALL_WEB_UPLOADS, "yes");
long pollCount = 0;
do {
UploadGetWebUploadsResponse response = UploadApi.getWebUploads(this.mediaFire, params, "1.4", UploadGetWebUploadsResponse.class);
WebUploads[] webUploadsArray = response.getWebUploads();
if (webUploadsArray == null || webUploadsArray.length == 0) {
pollCount++;
continue;
}
WebUploads webUpload = webUploadsArray[0];
int statusCode = webUpload.getStatusCode();
int errorStatus = webUpload.getErrorStatus();
String description = webUpload.getStatus();
String quickKey = webUpload.getQuickKey();
String filename = webUpload.getFilename();
if (quickKey != null && !quickKey.isEmpty()) {
uploadFinished(quickKey);
return;
}
if (statusCode >= statusToFinish) {
uploadFinished(quickKey, filename);
return;
}
if (errorStatus != 0) {
throw new MFApiException(errorStatus, "error status " + errorStatus + " while polling upload/get_web_uploads");
}
if (handler != null) {
handler.uploadPolling(id, statusCode, description);
}
Thread.sleep(TIME_BETWEEN_POLLS_MILLIS);
pollCount++;
} while (pollCount <= MAX_POLLS);
}
private void simpleUpload() throws MFException, MFApiException, IOException, InterruptedException, MFSessionNotStartedException {
LinkedHashMap<String, Object> params = new LinkedHashMap<String, Object>();
params.put(PARAM_RESPONSE_FORMAT, JSON);
if (this.folderKey != null && !this.folderKey.isEmpty()) {
params.put(PARAM_FOLDER_KEY, folderKey);
}
if (this.folderPath != null && !this.folderPath.isEmpty()) {
params.put(PARAM_FOLDER_PATH, this.folderPath);
}
Map<String, Object> headers = new HashMap<String, Object>();
headers.put(HEADER_X_FILENAME, filename);
headers.put(HEADER_X_FILESIZE, file.length());
headers.put(HEADER_CONTENT_TYPE, CONTENT_TYPE_OCTET_STREAM);
headers.put(HEADER_CONTENT_LENGTH, file.length());
byte[] payload = getFileBytes(file);
UploadSimpleResponse response = UploadApi.simple(this.mediaFire, params, headers, payload, "1.4", UploadSimpleResponse.class);
String uploadKey = response.getDoUpload().getUploadKey();
pollUpload(uploadKey);
}
private void addWebUpload() throws MFException, MFApiException, InterruptedException, UnsupportedEncodingException, MFSessionNotStartedException {
LinkedHashMap<String, Object> params = new LinkedHashMap<String, Object>();
params.put(PARAM_RESPONSE_FORMAT, JSON);
params.put(PARAM_URL, this.url);
params.put(PARAM_FILENAME, URLEncoder.encode(this.filename, UTF8));
if (this.folderKey != null && !this.folderKey.isEmpty()) {
params.put(PARAM_FOLDER_KEY, this.folderKey);
}
UploadAddWebUploadResponse response = UploadApi.addWebUpload(this.mediaFire, params, "1.4", UploadAddWebUploadResponse.class);
String uploadKey = response.getUploadKey();
getWebUploads(uploadKey);
}
private void uploadFinished() {
uploadFinished(null);
}
private void uploadFinished(String quickKey) {
uploadFinished(quickKey, null);
}
private void uploadFinished(String quickKey, String fileName) {
if (this.handler != null) {
this.handler.uploadFinished(this.id, quickKey, fileName);
}
}
private void uploadProgress(double percentFinished) {
if (this.handler != null) {
this.handler.uploadProgress(this.id, percentFinished);
}
}
private String getFileHash() throws IOException {
if (this.fileHash == null) {
this.fileHash = HashUtil.sha256(file);
}
return this.fileHash;
}
private byte[] makeChunk(int unitSize, int chunkNumber) throws IOException {
FileInputStream fis = new FileInputStream(this.file);
BufferedInputStream bis = new BufferedInputStream(fis);
byte[] uploadChunk = createUploadChunk(unitSize, chunkNumber, bis);
fis.close();
bis.close();
return uploadChunk;
}
private int getChunkSize(int chunkNumber, int numChunks, long fileSize, int unitSize) {
int chunkSize;
if (chunkNumber >= numChunks) {
chunkSize = 0; // represents bad size
} else {
if (fileSize % unitSize == 0) { // all units will be of unitSize
chunkSize = unitSize;
} else if (chunkNumber < numChunks - 1) { // this unit is of unitSize
chunkSize = unitSize;
} else { // this unit is "special" and is the modulo of fileSize and unitSize;
chunkSize = (int) (fileSize % unitSize);
}
}
return chunkSize;
}
private byte[] createUploadChunk(long unitSize, int chunkNumber, BufferedInputStream fileStream) throws IOException {
int offset = (int) (unitSize * chunkNumber);
fileStream.skip(offset);
ByteArrayOutputStream output = new ByteArrayOutputStream( (int) unitSize);
int bufferSize = 65536;
byte[] buffer = new byte[bufferSize];
int readSize;
int t = 0;
while ((readSize = fileStream.read(buffer)) > 0 && t <= unitSize) {
if (output.size() + readSize > unitSize) {
int actualReadSize = (int) unitSize - output.size();
output.write(buffer, 0, actualReadSize);
} else {
output.write(buffer, 0, readSize);
}
if (readSize > 0) {
t += readSize;
}
}
return output.toByteArray();
}
private boolean isChunkUploaded(int chunkId) {
if (this.uploadUnits.isEmpty()) {
return false;
}
return this.uploadUnits.get(chunkId);
}
private void updateUploadBitmap(int count, List<Integer> words) {
List<Boolean> uploadUnits = new LinkedList<Boolean>();
if (words == null || words.isEmpty()) {
this.uploadUnits = uploadUnits;
return;
}
//loop count times
for (int i = 0; i < count; i++) {
//convert words to binary string
String word = Integer.toBinaryString(words.get(i));
//ensure number is 16 bit by adding 0 until there are 16 bits
while (word.length() < 16) {
word = "0" + word;
}
//add boolean to collection depending on bit value
for (int j = 0; j < word.length(); j++) {
uploadUnits.add(i * 16 + j, word.charAt(15 - j) == '1');
}
}
this.uploadUnits = uploadUnits;
}
private static byte[] getFileBytes(File file) throws IOException {
byte[] fileBytes = new byte[(int) file.length()];
//convert file into array of bytes
FileInputStream fileInputStream = new FileInputStream(file);
fileInputStream.read(fileBytes);
fileInputStream.close();
return fileBytes;
}
public enum ActionOnInAccount {
UPLOAD_IF_NOT_IN_FOLDER, DO_NOT_UPLOAD, UPLOAD_ALWAYS,
}
@Override
public String toString() {
return "MediaFireUpload{" +
"id=" + id +
", file=" + file +
", url='" + url + '\'' +
", fileHash='" + fileHash + '\'' +
", filename='" + filename + '\'' +
", folderKey='" + folderKey + '\'' +
", folderPath='" + folderPath + '\'' +
", actionOnInAccount=" + actionOnInAccount +
", statusToFinish=" + statusToFinish +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
MediaFireUpload that = (MediaFireUpload) o;
return this.id == that.id && this.actionOnInAccount == that.actionOnInAccount
&& !(this.file != null ? !file.equals(that.file) : that.file != null)
&& !(this.fileHash != null ? !fileHash.equals(that.fileHash) : that.fileHash != null)
&& !(this.url != null ? !url.equals(that.url) : that.url != null);
}
@Override
public int hashCode() {
int result = file != null ? file.hashCode() : 0;
result = 31 * result + (actionOnInAccount != null ? actionOnInAccount.hashCode() : 0);
result = 31 * result + (url != null ? url.hashCode() : 0);
result = 31 * result + (fileHash != null ? fileHash.hashCode() : 0);
result = 31 * result + (int) (id ^ (id >>> 32));
return result;
}
}