通過客戶端js上傳文件到又拍云(upyun)

本文參加又拍云原創(chuàng)技術(shù)征文活動https://www.upyun.com/tech/article/551/1.html

公司此前一直使用七牛云,最近為了向海外用戶提供更優(yōu)質(zhì)的網(wǎng)絡(luò)訪問服務(wù),開始入手又拍云,官方在客戶端新js框架(react/vue/angular)下提供的參考文檔和代碼都不夠完整,摸索了好久,最終在官方的在線支持下,實現(xiàn)了客戶端js(目前僅是react)配合Java服務(wù)端實現(xiàn)文件上傳的功能。
官方支持node.js處理上傳的代碼在github上有一個項目:https://www.npmjs.com/package/upyun,其中有兩行“精煉”的代碼:

const service = new upyun.Service('your service name')
const client = new upyun.Client(service, getSignHeader);

實際上,‘your service name'就是你的桶名;可能在又拍云某段時間的概念里,對象存儲的“桶”稱為服務(wù)(service)。那么這個是你的運維人員在又拍云控制臺里配置的,而getSignHeader是對應(yīng)服務(wù)端接口的Promise函數(shù),官方舉了一個node.js服務(wù)端實現(xiàn)的例子,其返回值又說得不明不白,在此處摸索了好一些時間,最終是在客服不間斷的支持下解決的……汗!具體請參考我的代碼:

……
import upyun from 'upyun';
……
const bucketName = "<YOUR-BUCKET-NAME>";
const service = new upyun.Bucket(bucketName);
const client = new upyun.Client(service, getUpyunUploadHeader);
……
// 返回 Promise
export async function getUpyunUploadHeader(bucket, method = "POST", path) {
  console.log('upyun bucket', bucket, method, path)
  return request(`${apiDomain}/upyun/sign/head?bucket=${bucket.bucketName}&method=${method}&path=${path}`);
}

注意:這個返回Promise的函數(shù)的入?yún)?,是由upyun包控制的:它給第1個參數(shù)bucket傳入一個對象,第2個參數(shù)傳入的值是PUT,第三個參數(shù)是文件上傳的目標路徑,如/demo/file1.pdf。
request函數(shù)可以在antd pro腳手架示例項目代碼里找到,文末附。
客戶端js部分主要就是這樣了,client.putFile(……)相關(guān)的代碼,比較簡單,根據(jù)官方例子寫就可以了。
服務(wù)端接口/upyun/sign/head返回什么樣的數(shù)據(jù)呢?官方也沒有說,事實上應(yīng)該返回一個json對象,里面至少需要包含Authoriztion這個值,即最后生成的簽名描述符(形式為UPYUN <用戶名>:<簽名>)——事實上還需要一個日期字符串,詳見下文。為快捷起見,我就直接用了Map,代碼如下:

    // 生成upyun js sdk需要的上傳參數(shù)
    public Map<String, String> uploadHeader(String bucket, String uri, String method) throws Exception {
        logger.debug("UPYUN uploadHeader: bucket={}, uri/path={}, method={}", bucket, uri, method);
        String key = username;
        String secret = md5(password);
        String date = getRfc1123Time();
        // 上傳,處理,內(nèi)容識別有存儲
        String s = sign(key, secret, method, "/" + bucket + uri, date, "", "");
        logger.debug("Generated {}", s);
        Map<String, String> map = new HashMap<>();
        map.put("x-date", date);
        map.put("Authorization", s);
        return map;
    }

其中md5、getRfc1123Time、sign這三個方法,官方API中Java部分有,直接照搬即可。要注意的是,參與簽名的path,是要拼上桶名的("/" + bucket + uri),最終生成的資源URL中也是帶桶名的(域名+桶名+資源路徑),這一點與七牛云不一樣。這個getRfc1123Time也有一些繞,某段官方文檔里是說,參與簽名計算的時間用于表示請求的有效期限,最好是半年……所以一開始調(diào)測失敗,我把這個時間用當前時間加上了一個月,后來又發(fā)現(xiàn)只需要用當前時間戳就可以……無語。
服務(wù)端接口返回的這兩個數(shù)據(jù)(x-date與authorization),將直接被客戶端加入請求頭中。使用x-date是因為date會被瀏覽器屏蔽(認為是個危險的頭),而x-date的值是參與了簽名計算,所以必須提供給客戶端用于請求(putFile)。

附一:md5、getRfc1123Time、sign三個方法的Java實現(xiàn):

    private static final String HMAC_SHA1_ALGORITHM = "HmacSHA1";

    private static String md5(String string) {
        byte[] hash;
        try {
            hash = MessageDigest.getInstance("MD5").digest(string.getBytes("UTF-8"));
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException("UTF-8 is unsupported", e);
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException("MessageDigest不支持MD5Util", e);
        }
        StringBuilder hex = new StringBuilder(hash.length * 2);
        for (byte b : hash) {
            if ((b & 0xFF) < 0x10) hex.append("0");
            hex.append(Integer.toHexString(b & 0xFF));
        }
        return hex.toString();
    }

    private static byte[] hashHmac(String data, String key)
            throws SignatureException, NoSuchAlgorithmException, InvalidKeyException {
        SecretKeySpec signingKey = new SecretKeySpec(key.getBytes(), HMAC_SHA1_ALGORITHM);
        Mac mac = Mac.getInstance(HMAC_SHA1_ALGORITHM);
        mac.init(signingKey);
        return mac.doFinal(data.getBytes());
    }

    private static String sign(String key, String secret, String method, String uri, String date, String policy,
                              String md5) throws Exception {
        String value = method + "&" + uri + "&" + date;
        if (policy != null && policy.length() > 0) {
            value = value + "&" + policy;
        }
        if (md5 != null && md5.length() > 0) {
            value = value + "&" + md5;
        }
        byte[] hmac = hashHmac(value, secret);
        String sign = Base64.getEncoder().encodeToString(hmac);
        return "UPYUN " + key + ":" + sign;
    }

    private static String getRfc1123Time() {
        Calendar calendar = Calendar.getInstance();
        SimpleDateFormat dateFormat = new SimpleDateFormat(
                "EEE, dd MMM yyyy HH:mm:ss z", Locale.US);
        dateFormat.setTimeZone(TimeZone.getTimeZone("GMT"));
        logger.debug("upyun token time (format) {}", calendar);
        return dateFormat.format(calendar.getTime());
    }

附二:客戶端request函數(shù):

/**
 * Requests a URL, returning a promise.
 *
 * @param  {string} url       The URL we want to request
 * @param  {object} [options] The options we want to pass to "fetch"
 * @return {object}           An object containing either "data" or "err"
 */
export default function request(url, options, isLogin = false) {
  const defaultOptions = {
    // credentials: 'include',
  };
  const newOptions = { ...defaultOptions, ...options };
  if (
    newOptions.method === 'POST' ||
    newOptions.method === 'PUT' ||
    newOptions.method === 'DELETE'
  ) {
    if (!(newOptions.data instanceof FormData)) {
      newOptions.headers = {
        Accept: 'application/json',
        'Content-Type': 'application/json; charset=utf-8',
        ...newOptions.headers,
      };
      newOptions.body = JSON.stringify(newOptions.data);
    } else {
      // newOptions.body is FormData
      newOptions.headers = {
        Accept: 'application/json',
        ...newOptions.headers,
      };
      newOptions.body = newOptions.data;
    }
  }

  if (!isLogin) {
    // 請求的時候,如果storage有token,則攜帶token訪問
    const session = getSession();
    if (session) {
      newOptions.headers = {
        'Access-Token': session.data.accessToken,
        ...newOptions.headers,
      }
    }
  }

  return fetch(url, newOptions)
    .then(checkStatus)
    .then(response => {
      // 處理圖片、PDF的情況
      const contentType = response.headers.get('Content-Type');
      if (contentType.indexOf('image') > -1 || contentType.indexOf('pdf') > -1) {
        return response.blob();
      }
      if (newOptions.method === 'DELETE' || response.status === 204) {
        return response.text();
      }
      // 返回的時候,如果服務(wù)端返回令牌過期,則清除令牌并返回轉(zhuǎn)到登錄頁面
      // code=20180/20181
      const p = Promise.resolve(response.json());
      p.then( r => {
        const { code } = r;
        if (code !== undefined && code !== 1) {
          // message.error(msg);
          if (code === 20180 || code === 20181) {
            clearSession();
            const { dispatch } = store;
            dispatch(routerRedux.push('/user/login'));
          }
        }
      });
      return p;
    })
    .catch(e => {
      const { dispatch } = store;
      const status = e.name;
      if (status === 401) {
        dispatch({
          type: 'login/logout',
        });
        return;
      }
      if (status === 403) {
        dispatch(routerRedux.push('/exception/403'));
        return;
      }
      if (status <= 504 && status >= 500) {
        dispatch(routerRedux.push('/exception/500'));
        return;
      }
      if (status >= 404 && status < 422) {
        dispatch(routerRedux.push('/exception/404'));
      }
    });
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容