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

com.avos.avoscloud.upload.QiniuUploader Maven / Gradle / Ivy

package com.avos.avoscloud.upload;

import com.alibaba.fastjson.JSON;
import com.avos.avoscloud.AVErrorUtils;
import com.avos.avoscloud.AVException;
import com.avos.avoscloud.AVExceptionHolder;
import com.avos.avoscloud.AVFile;
import com.avos.avoscloud.AVOSCloud;
import com.avos.avoscloud.AVUtils;
import com.avos.avoscloud.upload.FileUploader.ProgressCalculator;
import com.avos.avoscloud.LogUtil;
import com.avos.avoscloud.ProgressCallback;
import com.avos.avoscloud.SaveCallback;

import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.zip.CRC32;

import okhttp3.Call;
import okhttp3.MediaType;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;

/**
 * User: summer,dennis Date: 13-4-15 Time: PM4:12
 */
class QiniuUploader extends HttpClientUploader {
  private String token;
  private String[] uploadFileCtx;
  private int blockCount;
  private String fileKey;

  static final String QINIU_HOST = "http://upload.qiniu.com";
  static final String QINIU_CREATE_BLOCK_EP = QINIU_HOST + "/mkblk/%d";
  static final String QINIU_BRICK_UPLOAD_EP = QINIU_HOST + "/bput/%s/%d";
  static final String QINIU_MKFILE_EP = QINIU_HOST + "/mkfile/%d/key/%s";
  static final int WIFI_CHUNK_SIZE = 256 * 1024;
  static final int BLOCK_SIZE = 1024 * 1024 * 4;
  static final int NONWIFI_CHUNK_SIZE = 64 * 1024;

  private ProgressCalculator progressCalculator;
  private volatile Call mergeFileRequestCall;
  private volatile Future[] tasks;

  QiniuUploader(AVFile avFile, String token, String fileKey, SaveCallback saveCallback, ProgressCallback progressCallback) {
    super(avFile, saveCallback,progressCallback);

    this.token = token;
    this.fileKey = fileKey;
  }

  int uploadChunkSize = WIFI_CHUNK_SIZE;
  // // FIXME: 2017/8/14 why not use executorService declared in parent class(HttpClientUploader.ThreadPoolExecutor)??
  static final ExecutorService fileUploadExecutor = Executors.newFixedThreadPool(10);

  @Override
  public AVException doWork() {
      boolean isWifi = AVUtils.isWifi(AVOSCloud.applicationContext);
      if (!isWifi) {
        // 从七牛的接口来看block size为4M不可变,但是chunkSize是可以调整的
        uploadChunkSize = NONWIFI_CHUNK_SIZE;
      }
      if (AVOSCloud.isDebugLogEnabled()) {
        LogUtil.avlog.d("uploading with chunk size:" + uploadChunkSize);
      }
      // here to try
      return uploadWithBlocks();

  }

  private Request.Builder addAuthHeader(Request.Builder builder) throws Exception {
    if (token != null) {
      builder.addHeader("Authorization", "UpToken " + token);
    }
    return builder;
  }

  private AVException uploadWithBlocks() {

    try {
      byte[] bytes = avFile.getData();
      blockCount = (bytes.length / BLOCK_SIZE) + (bytes.length % BLOCK_SIZE > 0 ? 1 : 0);
      uploadFileCtx = new String[blockCount];

      // 2.按照分片进行上传
      QiniuBlockResponseData respBlockData = null;
      CountDownLatch latch = new CountDownLatch(blockCount);
      progressCalculator = new ProgressCalculator(blockCount, new FileUploader.FileUploadProgressCallback() {
        public void onProgress(int progress) {
          publishProgress(progress);
        }
      });
      tasks = new Future[blockCount];
      synchronized (tasks) {
        for (int blockOffset = 0; blockOffset < blockCount; blockOffset++) {
          tasks[blockOffset] =
              fileUploadExecutor.submit(new FileBlockUploadTask(bytes, blockOffset, latch,
                  uploadChunkSize, progressCalculator, uploadFileCtx, this));
        }
      }
      latch.await();
      if (AVExceptionHolder.exists()) {
        for (Future task : tasks) {
          if (!task.isDone()) {
            task.cancel(true);
          }
        }

        throw AVExceptionHolder.remove();
      }
      // 3 merge文件
      QiniuMKFileResponseData mkfileResp = makeFile(bytes.length, fileKey, DEFAULT_RETRY_TIMES);

      if (!isCancelled()) {
        // qiniu's status code is 200, but should be 201 like parse..
        if (mkfileResp == null || !mkfileResp.key.equals(fileKey)) {
          return AVErrorUtils.createException(AVException.OTHER_CAUSE, "upload file failure");
        }
      }
    } catch (Exception e) {
      e.printStackTrace();
      return new AVException(e);
    }
    return null;
  }

  private static class QiniuBlockResponseData {
    public String ctx;
    public long crc32;
    public int offset;
    public String host;
    public String checksum;
  }

  private static class QiniuMKFileResponseData {
    public String key;
    public String hash;
  }

  private QiniuMKFileResponseData makeFile(int dataSize, String key, int retry) throws Exception {
    try {
      String endPoint = String.format(QINIU_MKFILE_EP, dataSize, AVUtils.base64Encode(key));
      List list = new LinkedList();
      Collections.addAll(list, uploadFileCtx);
      final String joinedFileCtx = AVUtils.joinCollection(list, ",");
      Request.Builder builder = new Request.Builder();
      builder.url(endPoint);

      builder = builder.post(RequestBody.create(MediaType.parse("text"), joinedFileCtx));
      builder = addAuthHeader(builder);
      mergeFileRequestCall = getOKHttpClient().newCall(builder.build());
      return parseQiniuResponse(mergeFileRequestCall.execute(), QiniuMKFileResponseData.class);
    } catch (Exception e) {
      if (retry-- > 0) {
        return makeFile(dataSize, key, retry);
      } else {
        LogUtil.log.e("Exception during file upload", e);
      }
    }
    return null;
  }

  private static  T parseQiniuResponse(Response resp, Class clazz) throws Exception {

    int code = resp.code();
    String phrase = resp.message();

    String h = resp.header("X-Log");

    if (code == 401) {
      throw new Exception("unauthorized to create Qiniu Block");
    }
    String responseData = AVUtils.stringFromBytes(resp.body().bytes());
    try {
      if (code / 100 == 2) {
        T data = JSON.parseObject(responseData, clazz);
        return data;
      }
    } catch (Exception e) {
    }

    if (responseData.length() > 0) {
      throw new Exception(code + ":" + responseData);
    }
    if (!AVUtils.isBlankString(h)) {
      throw new Exception(h);
    }
    throw new Exception(phrase);
  }

  private static class FileBlockUploadTask implements Runnable {
    private byte[] bytes;
    private int blockOffset;
    CountDownLatch latch;
    final int uploadChunkSize;
    ProgressCalculator progressCalculator;
    String[] uploadFileCtx;
    QiniuUploader parent;

    public FileBlockUploadTask(byte[] bytes, int blockOffset, CountDownLatch latch,
                               int uploadChunkSize, ProgressCalculator progressCalculator, String[] uploadFileCtx,
                               QiniuUploader parent) {
      this.bytes = bytes;
      this.blockOffset = blockOffset;
      this.latch = latch;
      this.uploadChunkSize = uploadChunkSize;
      this.progressCalculator = progressCalculator;
      this.uploadFileCtx = uploadFileCtx;
      this.parent = parent;
    }

    public void run() {
      QiniuBlockResponseData respBlockData;
      // 1.创建一个block,并且会上传第一个block的第一个chunk的数据
      int currentBlockSize =
          getCurrentBlockSize(bytes, blockOffset);
      respBlockData =
          createBlockInQiniu(blockOffset, currentBlockSize, DEFAULT_RETRY_TIMES, bytes);
      // 2.分片上传
      if (respBlockData != null) {
        respBlockData =
            putFileBlocksToQiniu(blockOffset, bytes, respBlockData, DEFAULT_RETRY_TIMES);
      }
      if (respBlockData != null) {
        uploadFileCtx[blockOffset] = respBlockData.ctx;
        progressCalculator.publishProgress(blockOffset, 100);
      } else {
        AVExceptionHolder.add(new AVException(AVException.OTHER_CAUSE, "Upload File failure"));
        long count = latch.getCount();
        for (; count > 0; count--) {
          latch.countDown();
        }
      }
      latch.countDown();
    }

    private QiniuBlockResponseData createBlockInQiniu(final int blockOffset, int blockSize,
                                                      int retry, final byte[] data) {
      try {
        if (AVOSCloud.isDebugLogEnabled()) {
          LogUtil.avlog.d("try to mkblk");
        }
        String endPoint = String.format(QINIU_CREATE_BLOCK_EP, blockSize);
        Request.Builder builder = new Request.Builder();
        builder.url(endPoint);

        final int nextChunkSize =
            getNextChunkSize(blockOffset, data);

        RequestBody requestBody = RequestBody.create(MediaType.parse(AVFile.DEFAULTMIMETYPE),
            data, blockOffset * BLOCK_SIZE, nextChunkSize);

        builder = builder.post(requestBody);
        builder = parent.addAuthHeader(builder);
        return parseQiniuResponse(getOKHttpClient().newCall(builder.build()).execute(),
            QiniuBlockResponseData.class);
      } catch (Exception e) {
        e.printStackTrace();
        if (retry-- > 0) {
          return createBlockInQiniu(blockOffset, blockSize, retry, data);
        } else {
          LogUtil.log.e("Exception during file upload", e);
        }
      }
      return null;
    }


    private QiniuBlockResponseData putFileBlocksToQiniu(final int blockOffset, final byte[] data,
                                                        QiniuBlockResponseData lastChunk, int retry) {
      int currentBlockLength = getCurrentBlockSize(data, blockOffset);
      progressCalculator.publishProgress(blockOffset, 100 * lastChunk.offset / BLOCK_SIZE);

      int remainingBlockLength = currentBlockLength - lastChunk.offset;

      if (remainingBlockLength > 0 && lastChunk.offset > 0) {
        try {
          String endPoint = String.format(QINIU_BRICK_UPLOAD_EP, lastChunk.ctx, lastChunk.offset);
          Request.Builder builder = new Request.Builder();
          builder.url(endPoint);
          builder.addHeader("Content-Type", "application/octet-stream");

          final QiniuBlockResponseData chunkData = lastChunk;
          final int nextChunkSize =
              remainingBlockLength > uploadChunkSize ? uploadChunkSize : remainingBlockLength;

          RequestBody requestBody = RequestBody.create(MediaType.parse(AVFile.DEFAULTMIMETYPE),
              data,
              blockOffset * BLOCK_SIZE + chunkData.offset,
              nextChunkSize);

          builder = builder.post(requestBody);
          builder = parent.addAuthHeader(builder);
          QiniuBlockResponseData respData =
              parseQiniuResponse(getOKHttpClient().newCall(builder.build()).execute(),
                  QiniuBlockResponseData.class);
          validateCrc32Value(respData,data,blockOffset * BLOCK_SIZE + chunkData.offset,nextChunkSize);
          if (respData != null) {
            if (respData.offset < currentBlockLength) {
              return putFileBlocksToQiniu(blockOffset, data, respData, DEFAULT_RETRY_TIMES);
            } else {
              return respData;
            }
          }
        } catch (Exception e) {
          e.printStackTrace();
          if (retry-- > 0) {
            return putFileBlocksToQiniu(blockOffset, data, lastChunk, retry);
          } else {
            LogUtil.log.e("Exception during file upload", e);
          }
        }
      } else {
        // 这个应该是遇到多余的一个block里面的数据只够本block的第一个chunk塞,这部分数据已经在mkblk的上传过了,所以直接返回原来的resp就可以了
        return lastChunk;
      }
      return null;
    }

    private void validateCrc32Value(QiniuBlockResponseData respData, byte[] data, int offset, int nextChunkSize) throws AVException {
      CRC32 crc32 = new CRC32();
      crc32.update(data,offset,nextChunkSize);
      long localCRC32 = crc32.getValue();
      if(respData!=null && respData.crc32 != localCRC32){
        throw  new AVException(AVException.OTHER_CAUSE,"CRC32 validation failure for chunk upload");
      }
    }


    private int getCurrentBlockSize(byte[] bytes, int blockOffset) {
      return (bytes.length - blockOffset * BLOCK_SIZE) > BLOCK_SIZE
          ? BLOCK_SIZE
          : (bytes.length - blockOffset * BLOCK_SIZE);
    }

    private int getNextChunkSize(int blockOffset, byte[] data) {
      return ((data.length - blockOffset * BLOCK_SIZE) > uploadChunkSize)
          ? uploadChunkSize
          : (data.length - blockOffset * BLOCK_SIZE);
    }
  }

  @Override
  public void interruptImmediately() {
    super.interruptImmediately();

    if (tasks != null && tasks.length > 0) {
      synchronized (tasks) {
        for (int index = 0; index < tasks.length; index++) {
          Future task = tasks[index];
          if (task != null && !task.isDone() && !task.isCancelled()) {
            task.cancel(true);
          }
        }
      }
    }

    if (mergeFileRequestCall != null) {
      mergeFileRequestCall.cancel();
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy