利用蒲公英提供的接口,第三方开发者可以把蒲公英提供的应用上传托管、安装等功能,接入到自己的应用中,并且可以根据数据接口,获取蒲公英提供的各种应用数据,以方便开发者更容易的进行内测应用的分发。
[【蒲公英官方文档 API 2.0】:]
(https://www.pgyer.com/doc/view/api#listMyReleased)
因为API 1.0即将废弃,2.0 APP上传文档github:
快速上传 App
脚本上传app:包含四个种类:demo.sh
、AppUploadDemo.java
、demo.js
、demo.php
通过 API 快速上传 App 到蒲公英平台
。github.com代码调用示例:https://github.com/PGYER/pgyer_api_example
1、获取上传的 token
通过该接口,开发者可以获取预上传 url 和相关的签名参数
API地址
POST https://www.pgyer.com/apiv2/app/getCOSToken
POST参数
参数 | 类型 | 说明 |
---|---|---|
_api_key | String | (必填) API Key [点击获取_api_key](javascript:void(0)) |
buildType | String | (必填) 需要上传的应用类型,ios 或 android |
buildInstallType | Integer | (选填)应用安装方式,值为(1,2,3,默认为1 公开安装)。1:公开安装,2:密码安装,3:邀请安装 |
buildPassword | String | (选填) 设置App安装密码,密码为空时默认公开安装 |
buildUpdateDescription | String | (选填) 版本更新描述,请传空字符串,或不传。 |
buildInstallDate | Interger | (选填)是否设置安装有效期,值为:1 设置有效时间, 2 长期有效,如果不填写不修改上一次的设置 |
buildInstallStartDate | String | (选填)安装有效期开始时间,字符串型,如:2018-01-01 |
buildInstallEndDate | String | (选填)安装有效期结束时间,字符串型,如:2018-12-31 |
buildChannelShortcut | String | (选填)所需更新的指定渠道的下载短链接,只可指定一个渠道,字符串型,如:abcd |
返回数据
参数 | 类型 | 说明 |
---|---|---|
key | String | key 上传文件存储标识唯一 key |
endpoint | String | 上传文件的 URL |
params | Object | 上传文件需要的参数,包含signature、x-cos-security-token、key |
2、上传文件到第上一步获取的 URL
在这一步中上传 App 成功后,App 会自动进入服务器后台队列继续后续的发布流程。所以,在这一步中 App 上传完成后,并不代表 App 已经完成发布。一般来说,一般1分钟以内就能完成发布。要检查是否发布完成,请调用下一步中的 API。
API地址
POST 上一步响应中 endpoint
的值
POST参数
参数 | 类型 | 说明 |
---|---|---|
key | String | (必填) 从上一步响应中得到 |
signature | String | (必填) 从上一步响应中得到 |
x-cos-security-token | String | (必填) 从上一步响应中得到 |
file | String | (必填) App 文件的本地路径 |
返回数据
如果上传成功:返回的 http 状态码为 204,内容为空;如果上传失败:返回相应错误信息说明。
Linux下的curl命令上传App示例
curl -D - --form-string 'key={key}' --form-string 'signature={signature}' --form-string 'x-cos-security-token={x-cos-security-token}' -F 'file=@/tmp/example.ipa' {endpoint}
demo示例:
第一种 demo.sh:
#!/bin/bash
#
# 通过shell脚本来实现将本地app文件通过API上传到蒲公英
# https://www.pgyer.com/doc/view/api#fastUploadApp
##
# 参数说明:
# $1: 蒲公英api_key
# $2: 要上传的文件路径(ipa/apk)
#
readonly api_key=$1
readonly file=$2
printHelp() {
echo "Usage: $0 api_key file"
echo "Example: $0 <your_api_key> <your_apk_or_ipa_path>"
}
# check api_key exists
if [ -z "$api_key" ]; then
echo "api_key is empty"
printHelp
exit 1
fi
# check file exists
if [ ! -f "$file" ]; then
echo "file not exists"
printHelp
exit 1
fi
if [[ $file =~ ipa$ ]]; then
app_type="ios"
elif [[ $file =~ apk$ ]]; then
app_type="android"
else
echo "file type not support"
printHelp
exit 1
fi
# ---------------------------------------------------------------
# functions
# ---------------------------------------------------------------
log() {
echo "[$(date +'%Y-%m-%d %H:%M:%S')] $*"
}
logTitle() {
log "-------------------------------- $* --------------------------------"
}
execCommand() {
log "$@"
result=$(eval $@)
}
# ---------------------------------------------------------------
# 获取上传凭证
# ---------------------------------------------------------------
logTitle "获取凭证"
execCommand "curl -s -F '_api_key=${api_key}' -F 'buildType=${app_type}' http://www.pgyer.com/apiv2/app/getCOSToken"
[[ "${result}" =~ \"endpoint\":\"([\:\_\.\/\\A-Za-z0-9\-]+)\" ]] && endpoint=`echo ${BASH_REMATCH[1]} | sed 's!\\\/!/!g'`
[[ "${result}" =~ \"key\":\"([\.a-z0-9]+)\" ]] && key=`echo ${BASH_REMATCH[1]}`
[[ "${result}" =~ \"signature\":\"([\=\&\_\;A-Za-z0-9\-]+)\" ]] && signature=`echo ${BASH_REMATCH[1]}`
[[ "${result}" =~ \"x-cos-security-token\":\"([\_A-Za-z0-9\-]+)\" ]] && x_cos_security_token=`echo ${BASH_REMATCH[1]}`
if [ -z "$key" ] || [ -z "$signature" ] || [ -z "$x_cos_security_token" ] || [ -z "$endpoint" ]; then
log "get upload token failed"
exit 1
fi
# ---------------------------------------------------------------
# 上传文件
# ---------------------------------------------------------------
logTitle "上传文件"
execCommand "curl -s -o /dev/null -w '%{http_code}' --form-string 'key=${key}' --form-string 'signature=${signature}' --form-string 'x-cos-security-token=${x_cos_security_token}' -F 'file=@${file}' ${endpoint}"
if [ $result -ne 204 ]; then
log "Upload failed"
exit 1
fi
# ---------------------------------------------------------------
# 检查结果
# ---------------------------------------------------------------
logTitle "检查结果"
for i in {1..60}; do
execCommand "curl -s http://www.pgyer.com/apiv2/app/buildInfo?_api_key=${api_key}\&buildKey=${key}"
[[ "${result}" =~ \"code\":([0-9]+) ]] && code=`echo ${BASH_REMATCH[1]}`
if [ $code -eq 0 ]; then
log $result
break
else
sleep 1
fi
done
第二种 demo.php:
<?php
/**
* 通过API上传App到蒲公英的示例代码
*
* 更多信息请见: https://www.pgyer.com/doc/view/api#fastUploadApp
*/
const API_KEY_DEV = "";
const API_KEY_PRODUCTION = "";
const APP_PATH = "";
// 获取上传凭证和上传地址
$res = sendRequest("http://www.pgyer.com/apiv2/app/getCOSToken", [
"_api_key" => API_KEY_PRODUCTION, // 填写您在平台注册的 API Key
"buildType" => 'ios', // 填写应用类型,可选值为: ios、android
"buildInstallType" => 2, // 填写安装类型,可选值为: 1 公开, 2密码
"buildPassword" => '123456', // 填写密码
]);
$res = json_decode($res, true);
// _log($res);
if ($res['code'] != 0 || empty($res['data'])) {
exit($res['message']);
}
$key = $res['data']['key'];
// 开始上传
$params = $res['data']['params'];
$params['file'] = new CURLFile(APP_PATH); // 填写待上传文件的路径
$httpcode = 0;
$result = sendRequest($res['data']['endpoint'], $params, $httpcode);
if ($httpcode == 204) {
_log("upload success");
} else {
_log("upload failed");
_log($result);
exit;
}
// 获取版本信息
getBuildInfo("http://www.pgyer.com/apiv2/app/buildInfo?_api_key=" . API_KEY_PRODUCTION . "&buildKey=$key");
// functions
function sendRequest($url, $params = [], &$httpcode = 0)
{
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
if (!empty($params)) {
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $params);
}
$result = curl_exec($ch);
$httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
return $result;
}
function getBuildInfo($url)
{
_log($url);
for ($i = 0; $i < 60; $i++) {
$res = json_decode(sendRequest($url), true);
if ($res['code'] == 0) {
_log($res['data']);
break;
} else {
_log("[$i] get app build info...");
sleep(1);
}
}
}
function _log($message) {
if (is_array($message) || is_object($message)) {
$message = var_export($message, true);
}
echo date("Y-m-d H:i:s") . " " . $message . "\n";
}
第三种 AppUploadDemo.java:
package com.pgyer.app_upload_demo;
import org.json.JSONObject;
import java.io.File;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.Map;
import java.util.Timer;
/**
* @describe
* @author: Caoxy
* @date: 2022/8/19
*/
public class AppUploadDemo implements AppUploadDemo.ProgressHttpEntityWrapper.ProgressCallback{
public static final String API_KEY_DEV = "";//测试环境apikey
public static final String API_KEY_PRODUCTION = "";//线上环境apikey
public static final String APP_PATH = "";//文件地址
public static final String GET_TOKEN_URL = "https://www.pgyer.com/apiv2/app/getCOSToken";
public static final int FILE_UPLOAD_SUCCESSFUL = 1001;
private UploadFileToServiceCallback uploadFileToServiceCallback;
Timer timers = null;
//接口方法调用例子开始
/**
* 例子
*/
public void uploadApk(){
Map<String, String> params = new HashMap<>();
params.put("_api_key", API_KEY_DEV);
int buildInstallType = 1;
params.put("buildInstallType", buildInstallType+"");//buildInstallType 1,2,3,默认为1 公开安装 1:公开安装,2:密码安装,3:邀请安装
if (buildInstallType == 2) {
params.put("buildPassword", "");//需要安装密码
}
params.put("buildUpdateDescription", "");//版本更新日志
File uploadFile = new File(APP_PATH);//apk文件路径
params.put("buildType", "android");
String url = GET_TOKEN_URL;//
getToken(params, url, new HttpCallback() {
@java.lang.Override
public void onSuccess(int code, String data) {
JSONObject backData = new JSONObject(responseString);
int code = backData.getInt("code");
JSONObject jsonObject = backData.getJSONObject("data");
if(code == 0 && jsonObject != null){//获取成功
JSONObject jsonObjectparams = jsonObject.getJSONObject("params");
Map<String, String> params = new HashMap<>();
String key = jsonObjectparams.getString("key");
params.put("key",key);
params.put("signature",jsonObjectparams.getString("signature"));
params.put("x-cos-security-token",jsonObjectparams.getString("x-cos-security-token"));
String url = jsonObject.getString("endpoint");
uploadFilr(params, url, uploadFile, API_KEY_DEV, key);
} else {//获取失败
}
}
@java.lang.Override
public void onError(int code, String data) {
}
})
}
/**
* 例子 上传文件
* @param url
*/
public void uploadFilr(Map<String, String> params, String url ,File files, String apikey, String buildKey){
uploadFileToServer(params, url, files, new UploadFileToServiceCallback() {
@java.lang.Override
public void onUploadBack(int code, String msg) {
if(code == FILE_UPLOAD_SUCCESSFUL){
//数据同步需要时间延时5秒请求同步结果
timers = new Timer(5000, new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
if(timers != null){
timers.stop();
timers = null;
String url = "https://www.pgyer.com/apiv2/app/buildInfo?_api_key="+apikey+"&buildKey="+buildKey;
uploadResult(url,uploadFileToServiceCallback);
}
}
});
timers.start();
}
}
@java.lang.Override
public void onPackageSizeComputed(long param1Long) {//上传文件的大小 去做度更新UI
}
@java.lang.Override
public void onProgressChanged(float param1Long) {//上传文件的进 去做度更新UI
}
@java.lang.Override
public void onUploadError(int code, String error) {//文件上传失败返回
}
});
}
/**
* 例子 获取同步结果
* @param url
*/
public void dataSynchronous(String url){
uploadResult(url, new HttpCallback() {
@java.lang.Override
public void onSuccess(int code, String data) {
JSONObject backDatas = new JSONObject(responseString);
int code = backDatas.getInt("code");
if(code == 0){//上传成功
backData = responseString;
JSONObject data = backDatas.getJSONObject("data");
//返回成功后相关文件信息
if (uploadFileToServiceCallback != null) {
uploadFileToServiceCallback.onUploadBack(code,responseString);
}
} else if(code == 1246){//等待同步
//数据同步需要时间延时已经延时5秒还在同步中,再2秒后请求同步结果
timers = new Timer(2000, new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
if(timers != null){
timers.stop();
timers = null;
dataSynchronous(url);
}
}
});
timers.start();
} else {//上传失败
if (uploadFileToServiceCallback != null) {
uploadFileToServiceCallback.onUploadError(code,"上传失败");
}
}
}
@java.lang.Override
public void onError(int code, String data) {
}
});
}
//接口方法调用例子结束
/**
* 获取文件相关参数
* @param params
* @param url
* @param httpCallback
*/
public void getToken(Map<String, String> params, String url, HttpCallback httpCallback){
sendRequest("POST",url, params ,httpCallback);
}
/**
* 上传文件
* @param params
* @param url
* @param files
* @param apikey
* @param buildKey
* @param uploadFileToServiceCallback
*/
public void uploadFileToServer(final Map<String, String> params, String url ,final File files, UploadFileToServiceCallback uploadFileToServiceCallback) {
this.uploadFileToServiceCallback = uploadFileToServiceCallback;
// TODO Auto-generated method stub
new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
try {
uploadFiles(params, url, files, uploadFileToServiceCallback);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
uploadFileToServiceCallback.onUploadError(-1,e.getMessage());
}
}
}).start();
}
/**
* 上传文件实现
* @param params
* @param url
* @param files
* @param uploadFileToServiceCallback
*/
public void uploadFiles(final Map<String, String> params, String url ,final File files, UploadFileToServiceCallback uploadFileToServiceCallback,) {
// TODO Auto-generated method stub
HttpClient client = HttpClientBuilder.create().build();// 开启一个客户端 HTTP 请求
HttpPost post = new HttpPost(url);//创建 HTTP POST 请求
MultipartEntityBuilder builder = MultipartEntityBuilder.create();
builder.setMode(HttpMultipartMode.BROWSER_COMPATIBLE);//设置浏览器兼容模式
for(Map.Entry<String, String> entry:params.entrySet()){
builder.addTextBody(entry.getKey(), entry.getValue());//设置请求参数
}
builder.addBinaryBody("file", files);
if (this.uploadFileToServiceCallback != null)
this.uploadFileToServiceCallback.onPackageSizeComputed(100);
HttpEntity entity = builder.build();// 生成 HTTP POST 实体
post.setEntity(new ProgressHttpEntityWrapper(entity,this));//设置请求参数
HttpResponse response = client.execute(post);// 发起请求 并返回请求的响应
int httpCode = response.getStatusLine().getStatusCode();
if (httpCode == 204) {
if(uploadFileToServiceCallback != null) {
uploadFileToServiceCallback.onUploadBack(FILE_UPLOAD_SUCCESSFUL, "文件上传成功等待同步数据");
}
} else {
if(uploadFileToServiceCallback != null){
uploadFileToServiceCallback.onUploadError(httpCode,"上传失败!");
}
}
}
/**
* 获取上传文件后同步信息
* @param url
*/
public void uploadResult(String url,HttpCallback httpCallback){
sendRequest("GET",url, null ,httpCallback);
}
/**
* 发起http 请求
* @param httpModle
* @param url
* @param params
* @param httpCallback
*/
public void sendRequest(String httpModle, String url, Map<String, String> params, HttpCallback httpCallback){
new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
try {
HttpClient client = HttpClientBuilder.create().build();// 开启一个客户端 HTTP 请求
if(httpModle.equals("GET")){
HttpGet httpClient = new HttpGet(url);//创建 HTTP POST 请求
} else if(httpModle.equals("POST")){
HttpPost httpClient = new HttpPost(url);//创建 HTTP POST 请求
MultipartEntityBuilder builder = MultipartEntityBuilder.create();
builder.setMode(HttpMultipartMode.BROWSER_COMPATIBLE);//设置浏览器兼容模式
for(Map.Entry<String, String> entry:params.entrySet()){
builder.addTextBody(entry.getKey(), entry.getValue());//设置请求参数
if(entry.getKey().equals("buildUpdateDescription")){
builder.addTextBody(entry.getKey(), entry.getValue(), ContentType.create(entry.getValue(), Charset.forName("UTF-8")));
}
}
HttpEntity entity = builder.build();// 生成 HTTP POST 实体
httpClient.setEntity(new HttpEntityWrapper(entity));//设置请求参数
}
HttpResponse response = client.execute(httpClient);// 发起请求 并返回请求的响应
if (response.getStatusLine().getStatusCode() == 200) {
String responseString = EntityUtils.toString(response.getEntity(), "UTF-8");
JSONObject backDatas = new JSONObject(responseString);
int code = backDatas.getInt("code");
if(httpCallback != null){
httpCallback.onSuccess(code,responseString);
}
}
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
httpCallback.onError(-1,e.getMessage());
}
}
}).start();
}
@Override
public void progress(float progress) {
uploadFileToServiceCallback.onProgressChanged(progress);
}
/**
* 上传文件封装监听
*/
public class ProgressHttpEntityWrapper extends HttpEntityWrapper {
private final ProgressCallback progressCallback;
public static interface ProgressCallback {
public void progress(float progress);
}
public ProgressHttpEntityWrapper(final HttpEntity entity, final ProgressCallback progressCallback) {
super(entity);
this.progressCallback = progressCallback;
}
@Override
public void writeTo(final OutputStream out) throws IOException {
this.wrappedEntity.writeTo(out instanceof ProgressFilterOutputStream ? out : new ProgressFilterOutputStream(out, this.progressCallback, getContentLength()));
}
public static class ProgressFilterOutputStream extends FilterOutputStream {
private final ProgressCallback progressCallback;
private long transferred;
private long totalBytes;
ProgressFilterOutputStream(final OutputStream out, final ProgressCallback progressCallback, final long totalBytes) {
super(out);
this.progressCallback = progressCallback;
this.transferred = 0;
this.totalBytes = totalBytes;
}
@Override
public void write(final byte[] b, final int off, final int len) throws IOException {
//super.write(byte b[], int off, int len) calls write(int b)
out.write(b, off, len);
this.transferred += len;
this.progressCallback.progress(getCurrentProgress());
}
@Override
public void write(final int b) throws IOException {
out.write(b);
this.transferred++;
this.progressCallback.progress(getCurrentProgress());
}
private float getCurrentProgress() {
return ((float) this.transferred / this.totalBytes) * 100;
}
}
}
/**
* 上传文件监听回调
*/
public interface UploadFileToServiceCallback {
//上传成功 或者 同步数据接口成功返回
void onUploadBack(int code,String msg);
//上传文件大小
void onPackageSizeComputed(long param1Long);
//上传文件进度
void onProgressChanged(float param1Long);
//上传失败返回
void onUploadError(int code,String error);
}
/**
* http 请求回调
*/
public interface HttpCallback{
void onSuccess(int code, String data){
}
void onError(int code, String data){
}
}
}
第四种 demo.js:
// 此 Demo 用演示如何使用 PGYER API 上传 App
// 详细文档参照 https://www.pgyer.com/doc/view/api#fastUploadApp
// 适用于 nodejs 项目
// 本代码需要 npm 包 form-data 支持 运行 npm install --save form-data 即可
const https = require('https');
const fs = require('fs');
const querystring = require('querystring');
const FormData = require('form-data');
const API_KEY_PRODUCTION = '<your api key>';
const APP_PATH = '<your app path>';
function upload (apiKey, appPath, callback) {
function getUploadResult (uploadData) {
const uploadResultRequest = https.request({
hostname: 'www.pgyer.com',
path: '/apiv2/app/buildInfo?_api_key=' + apiKey + '&buildKey=' + uploadData.data.key,
method: 'POST',
headers: {
'Content-Type' : 'application/x-www-form-urlencoded',
'Content-Length' : 0
}
}, response => {
if (response.statusCode !== 200) {
throw new Error('PGYER Service is down.');
}
let responseData = '';
response.on('data', data => {
responseData += data.toString();
})
response.on('end', () => {
const responseText = responseData.toString();
try {
const responseInfo = JSON.parse(responseText);
if (responseInfo.code === 1247) {
console.log('Parsing App Data ... Please Wait ... \n');
setTimeout(() => getUploadResult(uploadData), 1000);
} else if (responseInfo.code) {
throw new Error('PGYER Service Error > ' + responseInfo.code + ': ' + responseInfo.message);
}
callback(responseInfo);
} catch (error) {
throw error;
}
})
})
uploadResultRequest.write(uploadTokenRequestData);
uploadResultRequest.end();
}
function uploadApp(uploadData) {
const uploadAppRequestData = new FormData()
uploadAppRequestData.append('signature', uploadData.data.params.signature);
uploadAppRequestData.append('x-cos-security-token', uploadData.data.params['x-cos-security-token']);
uploadAppRequestData.append('key', uploadData.data.params.key);
uploadAppRequestData.append('file', fs.createReadStream(appPath))
uploadAppRequestData.submit(uploadData.data.endpoint, function (error, response) {
if (response.statusCode === 204) {
setTimeout(() => getUploadResult(uploadData), 1000);
} else {
throw new Error('Upload Error!')
}
});
}
const uploadTokenRequestData = querystring.stringify({
_api_key: apiKey, // 填写您在平台注册的 API Key (具体参照文档)
buildType: 'ios', // 填写应用类型,可选值为: ios、android (具体参照文档)
buildInstallType: 2, // 填写安装类型,可选值为: 1 公开, 2密码 (具体参照文档)
buildPassword: '123456', // 填写密码安装时的密码 (具体参照文档)
});
const uploadTokenRequest = https.request({
hostname: 'www.pgyer.com',
path: '/apiv2/app/getCOSToken',
method: 'POST',
headers: {
'Content-Type' : 'application/x-www-form-urlencoded',
'Content-Length' : uploadTokenRequestData.length
}
}, response => {
if (response.statusCode !== 200) {
throw new Error('PGYER Service is down.');
}
let responseData = '';
response.on('data', data => {
responseData += data.toString();
})
response.on('end', () => {
const responseText = responseData.toString();
try {
const responseInfo = JSON.parse(responseText);
if (responseInfo.code) {
throw new Error('PGYER Service Error > ' + responseInfo.code + ': ' + responseInfo.message);
}
uploadApp(responseInfo);
} catch (error) {
throw error;
}
})
})
uploadTokenRequest.write(uploadTokenRequestData);
uploadTokenRequest.end();
}
// 调用说明:
// upload(apikey, appPath, successCallback): void
// apikey: 你的 API Key
// appPath: 你的 app 路径,绝对路径相对路径均可
// successCallback: 成功后的回调函数, 传入一个参数为 App 信息 类似如下对象描述
// {
// code: 0,
// message: '',
// data: {
// buildKey: 'xxx',
// buildType: '1',
// buildIsFirst: '0',
// buildIsLastest: '1',
// buildFileKey: 'xxx.ipa',
// buildFileName: '',
// buildFileSize: '40095060',
// buildName: 'xxx',
// buildVersion: '2.2.0',
// buildVersionNo: '1.0.1',
// buildBuildVersion: '9',
// buildIdentifier: 'xxx.xxx.xxx.xxx',
// buildIcon: 'xxx',
// buildDescription: '',
// buildUpdateDescription: '',
// buildScreenshots: '',
// buildShortcutUrl: 'xxxx',
// buildCreated: 'xxxx-xx-xx xx:xx:xx',
// buildUpdated: 'xxxx-xx-xx xx:xx:xx',
// buildQRCodeURL: 'https://www.pgyer.com/app/qrcodeHistory/xxxx'
// }
// }
upload(API_KEY_PRODUCTION, APP_PATH, console.log);
结合第四种js:package-lock.json
:
{
"requires": true,
"lockfileVersion": 1,
"dependencies": {
"asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"requires": {
"delayed-stream": "~1.0.0"
}
},
"delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="
},
"form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"requires": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
}
},
"mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="
},
"mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"requires": {
"mime-db": "1.52.0"
}
}
}
}
网友评论