/* * Copyright (C) 2015 Actor LLC. <https://actor.im> */ package im.actor.core.modules.file; import im.actor.core.api.rpc.RequestCommitFileUpload; import im.actor.core.api.rpc.RequestGetFileUploadPartUrl; import im.actor.core.api.rpc.RequestGetFileUploadUrl; import im.actor.core.api.rpc.ResponseCommitFileUpload; import im.actor.core.api.rpc.ResponseGetFileUploadUrl; import im.actor.core.entity.FileReference; import im.actor.core.modules.ModuleContext; import im.actor.core.modules.ModuleActor; import im.actor.core.network.RpcCallback; import im.actor.core.network.RpcException; import im.actor.runtime.HTTP; import im.actor.runtime.Log; import im.actor.runtime.Storage; import im.actor.runtime.actors.ActorRef; import im.actor.runtime.actors.ActorCancellable; import im.actor.runtime.crypto.CRC32; import im.actor.runtime.files.FileSystemReference; import im.actor.runtime.files.InputFile; import im.actor.runtime.files.OutputFile; import im.actor.runtime.http.HTTPError; import im.actor.runtime.http.HTTPResponse; import im.actor.runtime.promise.Promise; public class UploadTask extends ModuleActor { // j2objc workaround private static final HTTPResponse DUMB = null; private static final int SIM_BLOCKS_COUNT = 4; private static final int NOTIFY_THROTTLE = 1000; private static final int DEFAULT_RETRY = 15; private final String TAG; private final boolean LOG; private long rid; private String fileName; private String descriptor; private boolean isWriteToDestProvider = false; private FileSystemReference srcReference; private InputFile inputFile; private FileSystemReference destReference; private OutputFile outputFile; private ActorRef manager; private boolean isCompleted = false; private int blockSize = 128 * 1024; private int blocksCount; private int nextBlock = 0; private int uploaded; private int uploadCount; private long lastNotifyDate; private ActorCancellable notifyCancellable; private byte[] uploadConfig; private CRC32 crc32; private float currentProgress; private boolean alreadyInTemp; public UploadTask(long rid, String descriptor, String fileName, ActorRef manager, ModuleContext context) { super(context); this.LOG = context.getConfiguration().isEnableFilesLogging(); this.rid = rid; this.fileName = fileName; this.descriptor = descriptor; this.manager = manager; this.TAG = "UploadTask{" + rid + "}"; } @Override public void preStart() { alreadyInTemp = false;//Storage.isAlreadyInTemp(descriptor); isWriteToDestProvider = Storage.isFsPersistent() && !alreadyInTemp; srcReference = Storage.fileFromDescriptor(descriptor); if (srcReference == null) { if (LOG) { Log.d(TAG, "Error during file reference creating"); } reportError(); return; } if (isWriteToDestProvider) { destReference = Storage.createTempFile(); if (destReference == null) { if (LOG) { Log.w(TAG, "Error during file dest reference creating"); } reportError(); return; } } srcReference.openRead() .flatMap(f -> { inputFile = f; if (isWriteToDestProvider) { return destReference.openWrite(srcReference.getSize()); } else { return Promise.success(null); } }) .flatMap(f -> { outputFile = f; crc32 = new CRC32(); blocksCount = srcReference.getSize() / blockSize; if (srcReference.getSize() % blockSize != 0) { blocksCount++; } if (LOG) { Log.d(TAG, "Starting uploading " + blocksCount + " blocks"); Log.d(TAG, "Requesting upload config..."); } return api(new RequestGetFileUploadUrl(srcReference.getSize())); }) .then(r -> { if (LOG) { Log.d(TAG, "Upload config loaded"); } uploadConfig = r.getUploadKey(); checkQueue(); }) .failure(e -> { if (LOG) { Log.w(TAG, "Error during initialization of upload"); } reportError(); }); } private void checkQueue() { if (isCompleted) { return; } if (nextBlock == blocksCount && uploadCount == 0) { if (LOG) { Log.d(TAG, "Completing..."); } long crc = crc32.getValue(); if (LOG) { Log.d(TAG, "Src #" + crc); Log.d(TAG, "Closing files..."); } inputFile.close(); if (isWriteToDestProvider) { outputFile.close(); } request(new RequestCommitFileUpload(uploadConfig, fileName), new RpcCallback<ResponseCommitFileUpload>() { @Override public void onResult(ResponseCommitFileUpload response) { if (LOG) { Log.d(TAG, "Upload completed..."); } FileReference location = new FileReference(response.getUploadedFileLocation(), fileName, srcReference.getSize()); if (isWriteToDestProvider || alreadyInTemp) { FileSystemReference reference = Storage.commitTempFile(alreadyInTemp ? srcReference : destReference, location.getFileId(), location.getFileName()); reportComplete(location, reference); } else { reportComplete(location, srcReference); } } @Override public void onError(RpcException e) { if (LOG) { Log.w(TAG, "Upload complete error"); } reportError(); } }); return; } if (nextBlock < blocksCount && uploadCount < SIM_BLOCKS_COUNT) { loadPart(nextBlock++); } } private void loadPart(final int blockIndex) { int size = blockSize; int fileOffset = blockIndex * blockSize; if ((blockIndex + 1) * blockSize > srcReference.getSize()) { size = srcReference.getSize() - blockIndex * blockSize; } // TODO: Validate file part load ordering inputFile.read(fileOffset, size).then(filePart -> { if (isCompleted) { return; } if (LOG) { Log.d(TAG, "Block #" + blockIndex + " read"); } if (isWriteToDestProvider) { if (!outputFile.write(fileOffset, filePart.getContents(), 0, filePart.getPartLength())) { if (LOG) { Log.w(TAG, "write #" + blockIndex + " error"); } reportError(); return; } } crc32.update(filePart.getContents(), 0, filePart.getPartLength()); if (LOG) { Log.d(TAG, "Starting block upload #" + blockIndex); } uploadCount++; uploadPart(blockIndex, filePart.getContents(), 0); checkQueue(); }).failure(e -> { if (isCompleted) { return; } if (LOG) { Log.w(TAG, "Block #" + blockIndex + " read failure"); } reportError(); }); } private void retryPart(int blockIndex, byte[] data, int attempt) { if (isCompleted) { return; } if (LOG) { Log.d(TAG, "Retrying block upload #" + blockIndex); } uploadPart(blockIndex, data, attempt); } private void uploadPart(final int blockIndex, final byte[] data, final int attempt) { api(new RequestGetFileUploadPartUrl(blockIndex, blockSize, uploadConfig)) .flatMap(r -> HTTP.putMethod(r.getUrl(), data)) .then(r -> { if (LOG) { Log.d(TAG, "Block #" + blockIndex + " uploaded"); } uploadCount--; uploaded++; reportProgress(uploaded / (float) blocksCount); checkQueue(); }) .failure(e -> { if (e instanceof HTTPError) { HTTPError httpError = (HTTPError) e; if ((httpError.getErrorCode() >= 500 && httpError.getErrorCode() < 600) || httpError.getErrorCode() == 0) { // Is Server Error or unknown error int retryInSecs = DEFAULT_RETRY; if (LOG) { Log.w(TAG, "Block #" + blockIndex + " upload error #" + httpError.getErrorCode() + " trying again in " + retryInSecs + " sec, attempt #" + (attempt + 1)); } schedule(new Retry(blockIndex, data, attempt + 1), retryInSecs * 1000L); return; } if (LOG) { Log.w(TAG, "Block #" + blockIndex + " upload failure"); } reportError(); } }); } private void reportError() { if (LOG) { Log.d(TAG, "Reporting error"); } if (isCompleted) { return; } isCompleted = true; manager.send(new UploadManager.UploadTaskError(rid)); } private void reportProgress(float progress) { if (isCompleted) { return; } if (progress > currentProgress) { currentProgress = progress; } if (notifyCancellable != null) { notifyCancellable.cancel(); notifyCancellable = null; } long delta = im.actor.runtime.Runtime.getActorTime() - lastNotifyDate; if (delta > NOTIFY_THROTTLE) { lastNotifyDate = im.actor.runtime.Runtime.getActorTime(); performReportProgress(); } else { notifyCancellable = schedule((Runnable) () -> performReportProgress(), delta); } } private void performReportProgress() { if (isCompleted) { return; } manager.send(new UploadManager.UploadTaskProgress(rid, currentProgress)); } private void reportComplete(FileReference location, FileSystemReference reference) { if (isCompleted) { return; } isCompleted = true; manager.send(new UploadManager.UploadTaskComplete(rid, location, reference)); } @Override public void onReceive(Object message) { if (message instanceof Retry) { Retry retry = (Retry) message; retryPart(retry.getBlockIndex(), retry.getData(), retry.getAttempt()); } else { super.onReceive(message); } } private class Retry { private int blockIndex; private byte[] data; private int attempt; public Retry(int blockIndex, byte[] data, int attempt) { this.blockIndex = blockIndex; this.data = data; this.attempt = attempt; } public int getBlockIndex() { return blockIndex; } public byte[] getData() { return data; } public int getAttempt() { return attempt; } } }