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

cn.leancloud.upload.QiniuAccessor Maven / Gradle / Ivy

package cn.leancloud.upload;


import cn.leancloud.LCException;
import cn.leancloud.LCLogger;
import cn.leancloud.codec.Base64;
import cn.leancloud.utils.LogUtil;
import cn.leancloud.utils.StringUtil;
import cn.leancloud.json.JSON;

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

import java.util.List;
import java.util.zip.CRC32;

/**
 * Qiniu REST API client for file uploading.
 * 1. slicing upload:
 * following is the architecture of a file which is composed by block and chunk.
 * ref: https://developer.qiniu.com/kodo/manual/1650/chunked-upload
 * |----------------------------------------------------------------------------|
 * | resource                                                                   |
 * |   |-------------|   |-------------|   |-------------|   |-------------|    |
 * |   | block1      |   | block2      |   | block3      |   | block4      |    |
 * |   | |---------| |   | |---------| |   | |---------| |   | |---------| |    |
 * |   | | chunk1  | |   | | chunk1  | |   | | chunk1  | |   | | chunk1  | |    |
 * |   | |---------| |   | |---------| |   | |---------| |   | |---------| |    |
 * |   | | chunk2  | |   | | chunk2  | |   | | chunk2  | |   | | chunk2  | |    |
 * |   | |---------| |   | |---------| |   | |---------| |   | |---------| |    |
 * |   | | chunk3  | |   | | chunk3  | |   | | chunk3  | |   | | chunk3  | |    |
 * |   | |---------| |   | |---------| |   | |---------| |   | |---------| |    |
 * |   | | chunk4  | |   | | chunk4  | |   | | chunk4  | |   | | chunk4  | |    |
 * |   | |---------| |   | |---------| |   | |---------| |   | |---------| |    |
 * |   |-------------|   |-------------|   |-------------|   |-------------|    |
 * |----------------------------------------------------------------------------|
 *
 * 2. main steps
 *   - 2.1 make block. ref: https://developer.qiniu.com/kodo/api/1286/mkblk
 *   - 2.2 upload chunks. ref: https://developer.qiniu.com/kodo/api/1251/bput
 *   - 2.3 merge to file. ref: https://developer.qiniu.com/kodo/api/1287/mkfile
 *
 * Created by fengjunwen on 2017/8/15.
 */

class QiniuAccessor {
  private static LCLogger LOGGER = LogUtil.getLogger(QiniuAccessor.class);

  static final String QINIU_HOST = "http://upload.qiniu.com";
  static final String QINIU_CREATE_BLOCK_EP = "%s/mkblk/%d";
  static final String QINIU_BRICK_UPLOAD_EP = "%s/bput/%s/%d";
  static final String QINIU_MKFILE_EP = "%s/mkfile/%d/key/%s";

  static final String HEAD_CONTENT_LENGTH = "Content-Length";
  static final String HEAD_CONTENT_TYPE = "Content-Type";
  static final String HEAD_AUTHORIZATION = "Authorization";
  static final String TEXT_CONTENT_TYPE = "text/plain";
  static final String DEFAULT_CONTENT_TYPE = "application/octet-stream";

  static final int WIFI_CHUNK_SIZE = 256 * 1024;
  static final int BLOCK_SIZE = 1024 * 1024 * 4;
  static final int NONWIFI_CHUNK_SIZE = 64 * 1024;

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

    public String getCtx() {
      return ctx;
    }

    public void setCtx(String ctx) {
      this.ctx = ctx;
    }

    public long getCrc32() {
      return crc32;
    }

    public void setCrc32(long crc32) {
      this.crc32 = crc32;
    }

    public int getOffset() {
      return offset;
    }

    public void setOffset(int offset) {
      this.offset = offset;
    }

    public String getHost() {
      return host;
    }

    public void setHost(String host) {
      this.host = host;
    }

    public String getChecksum() {
      return checksum;
    }

    public void setChecksum(String checksum) {
      this.checksum = checksum;
    }

    @Override
    public String toString() {
      return "QiniuBlockResponseData{" +
              "ctx='" + ctx + '\'' +
              ", crc32=" + crc32 +
              ", offset=" + offset +
              ", host='" + host + '\'' +
              ", checksum='" + checksum + '\'' +
              '}';
    }
  }

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

    @Override
    public String toString() {
      return "QiniuMKFileResponseData{" +
              "key='" + key + '\'' +
              ", hash='" + hash + '\'' +
              '}';
    }
  }

  private OkHttpClient client;
  private String uploadToken;
  private String fileKey;
  private String uploadUrl = QINIU_HOST;

  QiniuAccessor(OkHttpClient client, String uploadToken, String fileKey, String uploadUrl) {
    this.client = client;
    this.uploadToken = uploadToken;
    this.fileKey = fileKey;
    if (!StringUtil.isEmpty(uploadUrl)) {
      this.uploadUrl = uploadUrl;
    }
  }

  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");
    } else if (null == resp.body()) {
      throw new Exception("invalid response");
    }
    String responseData = StringUtil.stringFromBytes(resp.body().bytes());
    try {
      if (code / 100 == 2) {
        T data = JSON.parseObject(responseData, clazz);
        return data;
      }
    } catch (Exception e) {
      LOGGER.w(e);
    }

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

  /**
   * REST API:
   * - POST /mkblk/ HTTP/1.1
   * - Host:           upload.qiniu.com
   * - Content-Type:   application/octet-stream
   * - Content-Length: 
   * - Authorization:  UpToken 
   * - 
   *
   * - Response
   * {
   *   "ctx":          "",
   *   "checksum":     "",
   *   "crc32":         ,
   *   "offset":        ,
   *   "host":         ""
   * }
   * @param blockSize
   * @param firstChunkSize
   * @param firstChunkData
   * @param retry
   * @return
   */
  public QiniuBlockResponseData createBlockInQiniu(int blockSize, int firstChunkSize,
                                                   final byte[] firstChunkData, int retry) {
    try {

      String endPoint = String.format(QINIU_CREATE_BLOCK_EP, this.uploadUrl, blockSize);
      Request.Builder builder = new Request.Builder();
      builder.url(endPoint);
      builder.addHeader(HEAD_CONTENT_TYPE, DEFAULT_CONTENT_TYPE);
      builder.addHeader(HEAD_CONTENT_LENGTH, String.valueOf(firstChunkSize));
      builder.addHeader(HEAD_AUTHORIZATION, "UpToken " + this.uploadToken);

      LOGGER.d("createBlockInQiniu with uploadUrl: " + endPoint);
      RequestBody requestBody = RequestBody.create(MediaType.parse(DEFAULT_CONTENT_TYPE), firstChunkData, 0, firstChunkSize);
      builder = builder.post(requestBody);

      Response response = this.client.newCall(builder.build()).execute();
      return parseQiniuResponse(response, QiniuBlockResponseData.class);
    } catch (Exception e) {
      if (retry-- > 0) {
        return createBlockInQiniu(blockSize, firstChunkSize, firstChunkData, retry);
      } else {
        LOGGER.w(e);
      }
    }
    return null;
  }

  /**
   * REST API:
   * POST /bput// HTTP/1.1
   * Host:           
   * Content-Type:   application/octet-stream
   * Content-Length: 
   * Authorization:  UpToken 
   * 
   *
   * Request Params:
   * - ctx: 前一次上传返回的块级上传控制信息。
   * - nextChunkOffset: 当前片在整个块中的起始偏移。
   * - nextChunkSize: 当前片数据大小
   * - nextChunkBinary: 当前片数据
   *
   * Response:
   * {
   *   "ctx":            "",
   *   "checksum":       "",
   *   "crc32":           ,
   *   "offset":          ,
   *   "host":           ""
   * }
   *
   * @param lastChunk
   * @param blockOffset
   * @param currentChunkData
   * @param currentChunkSize
   * @param retry
   * @return
   */
  public QiniuBlockResponseData putFileBlocksToQiniu(QiniuBlockResponseData lastChunk,
                                                     final int blockOffset,
                                                     final byte[] currentChunkData,
                                                     int currentChunkSize, int retry) {
    try {
      String endPoint = String.format(QINIU_BRICK_UPLOAD_EP, this.uploadUrl, lastChunk.ctx, lastChunk.offset);
      Request.Builder builder = new Request.Builder();
      builder.url(endPoint);
      builder.addHeader(HEAD_CONTENT_TYPE, DEFAULT_CONTENT_TYPE);
      builder.addHeader(HEAD_CONTENT_LENGTH, String.valueOf(currentChunkSize));
      builder.addHeader(HEAD_AUTHORIZATION, "UpToken " + this.uploadToken);

      LOGGER.d("putFileBlocksToQiniu with uploadUrl: " + endPoint);

      RequestBody requestBody = RequestBody.create(MediaType.parse(DEFAULT_CONTENT_TYPE),
              currentChunkData, 0, currentChunkSize);
      builder = builder.post(requestBody);
      Response response = this.client.newCall(builder.build()).execute();
      QiniuBlockResponseData respData = parseQiniuResponse(response, QiniuBlockResponseData.class);
      validateCrc32Value(respData, currentChunkData, 0, currentChunkSize);
      return respData;
    } catch (Exception e) {
      if (retry-- > 0) {
        return putFileBlocksToQiniu(lastChunk, blockOffset, currentChunkData, currentChunkSize, retry);
      } else {
        LOGGER.w(e);
      }
    }
    return null;
  }

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

  /**
   * REST API
   * POST /mkfile//key//mimeType//x:user-var/ HTTP/1.1
   * Host:           
   * Content-Type:   text/plain
   * Content-Length: 
   * Authorization:  UpToken 
   * 
   *
   * Request params:
   * - fileSize
   * - encodeKey
   * - encodedMimeType
   * - encodedUserVars
   *
   * Response:
   * {
   *   "hash": "",
   *   "key":  ""
   * }
   *
   * @param fileTotalSize
   * @param uploadFileCtxs
   * @param retry
   * @return
   * @throws Exception
   */
  public QiniuMKFileResponseData makeFile(int fileTotalSize, List uploadFileCtxs, int retry)
          throws Exception {
    try {
      String endPoint = String.format(QINIU_MKFILE_EP, this.uploadUrl, fileTotalSize,
              Base64.encodeToString(this.fileKey.getBytes(), Base64.URL_SAFE | Base64.NO_WRAP));
      final String joinedFileCtx = StringUtil.join(",", uploadFileCtxs);
      Request.Builder builder = new Request.Builder();
      builder.url(endPoint);
      builder.addHeader(HEAD_CONTENT_TYPE, TEXT_CONTENT_TYPE);
      builder.addHeader(HEAD_CONTENT_LENGTH, String.valueOf(joinedFileCtx.length()));
      builder.addHeader(HEAD_AUTHORIZATION, "UpToken " + this.uploadToken);

      LOGGER.d("makeFile to qiniu with uploadUrl: " + endPoint);
      builder = builder.post(RequestBody.create(MediaType.parse(TEXT_CONTENT_TYPE), joinedFileCtx));
      Response response = this.client.newCall(builder.build()).execute();
      return parseQiniuResponse(response, QiniuMKFileResponseData.class);
    } catch (Exception e) {
      if (retry-- > 0) {
        return makeFile(fileTotalSize, uploadFileCtxs, retry);
      } else {
        LOGGER.w(e);
      }
    }
    return null;
  }
}





© 2015 - 2025 Weber Informatics LLC | Privacy Policy