oak-external-sdk/lib/service/qiniu/QiniuCloud.js

674 lines
24 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.QiniuCloudInstance = void 0;
const tslib_1 = require("tslib");
require('../../utils/fetch');
const crypto_1 = tslib_1.__importDefault(require("crypto"));
const ts_md5_1 = require("ts-md5");
const buffer_1 = require("buffer");
const querystring_1 = require("querystring");
const Exception_1 = require("oak-domain/lib/types/Exception");
const url_1 = require("oak-domain/lib/utils/url");
/**
* qiniu endpoint list
* https://developer.qiniu.com/kodo/1671/region-endpoint-fq
*/
const QINIU_ENDPOINT_LIST = {
z0: {
bm: 'uc.qiniuapi.com',
ul: 'upload.qiniup.com',
sul: 'up.qiniup.com',
sdl: 'iovip.qiniuio.com',
om: 'rs-z0.qiniuapi.com',
ol: 'rsf-z0.qiniuapi.com',
sq: 'api.qiniuapi.com',
},
'cn-east-2': {
bm: 'uc.qiniuapi.com',
ul: 'upload-cn-east-2.qiniup.com',
sul: 'up-cn-east-2.qiniup.com',
sdl: 'iovip-cn-east-2.qiniuio.com',
om: 'rs-cn-east-2.qiniuapi.com',
ol: 'rsf-cn-east-2.qiniuapi.com',
sq: 'api.qiniuapi.com',
},
z1: {
bm: 'uc.qiniuapi.com',
ul: 'upload-z1.qiniup.com',
sul: 'up-z1.qiniup.com',
sdl: 'iovip-z1.qiniuio.com',
om: 'rs-z1.qiniuapi.com',
ol: 'rsf-z1.qiniuapi.com',
sq: 'api.qiniuapi.com',
},
z2: {
bm: 'uc.qiniuapi.com',
ul: 'upload-z2.qiniup.com',
sul: 'up-z2.qiniup.com',
sdl: 'iovip-z2.qiniuio.com',
om: 'rs-z2.qiniuapi.com',
ol: 'rsf-z2.qiniuapi.com',
sq: 'api.qiniuapi.com',
},
na0: {
bm: 'uc.qiniuapi.com',
ul: 'upload-na0.qiniup.com',
sul: 'up-na0.qiniup.com',
sdl: 'iovip-na0.qiniuio.com',
om: 'rs-na0.qiniuapi.com',
ol: 'rsf-na0.qiniuapi.com',
sq: 'api.qiniuapi.com',
},
as0: {
bm: 'uc.qiniuapi.com',
ul: 'upload-as0.qiniup.com',
sul: 'up-as0.qiniup.com',
sdl: 'iovip-as0.qiniuio.com',
om: 'rs-as0.qiniuapi.com',
ol: 'rsf-as0.qiniuapi.com',
sq: 'api.qiniuapi.com',
},
};
function getQueryString(query) {
if (typeof query === 'string') {
return query;
}
return (0, querystring_1.stringify)(query);
}
/**
* from qiniu sdk
* @param date
* @param layout
* @returns
*/
function formatUTC(date, layout) {
function pad(num, digit) {
const d = digit || 2;
let result = num.toString();
while (result.length < d) {
result = '0' + result;
}
return result;
}
const d = new Date(date);
const year = d.getUTCFullYear();
const month = d.getUTCMonth() + 1;
const day = d.getUTCDate();
const hour = d.getUTCHours();
const minute = d.getUTCMinutes();
const second = d.getUTCSeconds();
const millisecond = d.getUTCMilliseconds();
let result = layout || 'YYYY-MM-DDTHH:MM:ss.SSSZ';
result = result
.replace(/YYYY/g, year.toString())
.replace(/MM/g, pad(month))
.replace(/DD/g, pad(day))
.replace(/HH/g, pad(hour))
.replace(/mm/g, pad(minute))
.replace(/ss/g, pad(second))
.replace(/SSS/g, pad(millisecond, 3));
return result;
}
class QiniuCloudInstance {
accessKey;
secretKey;
constructor(accessKey, secretKey) {
this.accessKey = accessKey;
this.secretKey = secretKey;
}
/**
* 计算客户端上传七牛需要的凭证
* https://developer.qiniu.com/kodo/1312/upload
* @param bucket
* @param zone
* @param key
* @returns
*/
getKodoUploadInfo(bucket, zone, key) {
try {
const scope = key ? `${bucket}:${key}` : bucket;
const uploadToken = this.generateKodoUploadToken(scope);
return {
key,
uploadToken,
uploadHost: `https://${QINIU_ENDPOINT_LIST[zone].ul}`,
bucket,
};
}
catch (err) {
throw err;
}
}
/**
* https://developer.qiniu.com/kodo/1308/stat
* 文档里写的是GET方法从nodejs-sdk里看是POST方法
*/
async getKodoFileStat(bucket, zone, key, mockData) {
const entry = `${bucket}:${key}`;
const encodedEntryURI = this.urlSafeBase64Encode(entry);
const path = `/stat/${encodedEntryURI}`;
const result = await this.access(QINIU_ENDPOINT_LIST[zone].om, path, {
'Content-Type': 'application/x-www-form-urlencoded',
}, undefined, 'POST', undefined, mockData);
return result;
}
/**
* https://developer.qiniu.com/kodo/1257/delete
* @param bucket
* @param key
* @param mockData
* @returns
*/
async removeKodoFile(bucket, zone, key, mockData) {
const entry = `${bucket}:${key}`;
const encodedEntryURI = this.urlSafeBase64Encode(entry);
const path = `/delete/${encodedEntryURI}`;
await this.access(QINIU_ENDPOINT_LIST[zone].om, path, {
'Content-Type': 'application/x-www-form-urlencoded',
}, undefined, 'POST', undefined, mockData);
return true;
}
/**
* 列举kodo资源列表
* https://developer.qiniu.com/kodo/1284/list
* @param bucket
* @param marker
* @param limit
* @param prefix
* @param delimiter
* @param mockData
* @returns
*/
async getKodoFileList(bucket, zone, marker, limit, prefix, delimiter, mockData) {
const path = '/list';
const result = await this.access(QINIU_ENDPOINT_LIST[zone].ol, path, {
'Content-Type': 'application/x-www-form-urlencoded',
}, {
bucket,
marker,
limit,
prefix,
delimiter,
}, 'POST', undefined, mockData);
return result;
}
async moveKodoFile(srcBucket, zone, srcKey, destBucket, destKey, force, mockData) {
const srcEntry = `${srcBucket}:${srcKey}`;
const srcEncodedEntryURI = this.urlSafeBase64Encode(srcEntry);
const destEntry = `${destBucket}:${destKey}`;
const destEncodedEntryURI = this.urlSafeBase64Encode(destEntry);
let path = `/move/${srcEncodedEntryURI}/${destEncodedEntryURI}`;
if (force) {
path += '/force/true';
}
await this.access(QINIU_ENDPOINT_LIST[zone].om, path, {
'Content-Type': 'application/x-www-form-urlencoded',
}, undefined, 'POST', undefined, mockData);
}
async copyKodoFile(srcBucket, zone, srcKey, destBucket, destKey, force, mockData) {
const srcEntry = `${srcBucket}:${srcKey}`;
const srcEncodedEntryURI = this.urlSafeBase64Encode(srcEntry);
const destEntry = `${destBucket}:${destKey}`;
const destEncodedEntryURI = this.urlSafeBase64Encode(destEntry);
let path = `/copy/${srcEncodedEntryURI}/${destEncodedEntryURI}`;
if (force) {
path += '/force/true';
}
await this.access(QINIU_ENDPOINT_LIST[zone].om, path, {
'Content-Type': 'application/x-www-form-urlencoded',
}, undefined, 'POST', undefined, mockData);
}
/**
* 计算直播需要的token
* @param method
* @param path
* @param host
* @param rawQuery
* @param contentType
* @param bodyStr
* @returns
*/
getLiveToken(method, path, host, rawQuery, contentType, bodyStr) {
// 1. 添加 Path
let data = `${method} ${path}`;
if (rawQuery) {
data += `?${rawQuery}`;
}
data += `\nHost: ${host}`;
if (contentType) {
data += `\nContent-Type: ${contentType}`;
}
data += '\n\n';
if (bodyStr &&
contentType &&
contentType !== 'application/octet-stream') {
data += bodyStr;
}
const sign = this.hmacSha1(data, this.secretKey);
const encodedSign = this.base64ToUrlSafe(sign);
const toke = 'Qiniu ' + this.accessKey + ':' + encodedSign;
return toke;
}
/**
* 创建直播流
* @param hub
* @param streamTitle
* @param host
* @param publishDomain
* @param playDomainType
* @param playDomain
* @param expireAt
* @param publishSecurity
* @param publishKey
* @param playKey
* @returns
*/
async createLiveStream(hub, streamTitle, host, publishDomain, playDomainType, playDomain, expireAt, publishSecurity, publishKey, playKey) {
// 七牛创建直播流接口路径
const path = `/v2/hubs/${hub}/streams`;
// 如果用户没给streamTitle那么随机生成一个
let key = streamTitle;
if (!key) {
key = `class${new Date().getTime()}`;
}
const bodyStr = JSON.stringify({
key,
});
const contentType = 'application/json';
const token = this.getLiveToken('POST', path, host, undefined, contentType, bodyStr);
const url = `https://pili.qiniuapi.com/v2/hubs/${hub}/streams`;
let response;
try {
response = await global.fetch(url, {
method: 'POST',
headers: {
Authorization: token,
'Content-Type': contentType,
},
body: bodyStr,
// mode: 'no-cors',
});
}
catch (err) {
throw new Exception_1.OakNetworkException();
}
if (response.status !== 200) {
if (response.status === 614) {
throw new Exception_1.OakExternalException('qiniu', response.status.toString(), '直播流名已存在', {
status: response.status,
});
}
else {
const json = await response.json();
const { error } = json;
throw new Exception_1.OakExternalException('qiniu', response.status.toString(), error, {
status: response.status,
});
}
}
const obj = this.getStreamObj(hub, streamTitle, expireAt, publishDomain, playDomainType, playDomain, publishSecurity, publishKey, playKey);
return obj;
}
/**
* 计算直播流地址相关信息
* @param hub
* @param streamTitle
* @param expireAt
* @param publishDomain
* @param playDomainType
* @param playDomain
* @param publishSecurity
* @param publishKey
* @param playKey
* @returns
*/
getStreamObj(hub, streamTitle, expireAt, publishDomain, playDomainType, playDomain, publishSecurity, publishKey, playKey) {
// 根据推流鉴权方式生成推流地址 rtmp推流
const pcPushUrl = `rtmp://${publishDomain}/${hub}/`;
let rtmpPushUrl = `rtmp://${publishDomain}/${hub}/${streamTitle}`, streamKey = `${streamTitle}`;
if (publishSecurity === 'none') {
//无校验鉴权 rtmp://<RTMPPublishDomain>/<Hub>/<streamTitle>
rtmpPushUrl = `rtmp://${publishDomain}/${hub}/${streamTitle}`;
streamKey = `${streamTitle}`;
}
else if (publishSecurity === 'static') {
//静态鉴权 rtmp://<RTMPPublishDomain>/<Hub>/<streamTitle>?key=<PublishKey>
rtmpPushUrl = `rtmp://${publishDomain}/${hub}/${streamTitle}?key=${publishKey}`;
streamKey = `${streamTitle}?key=${publishKey}`;
}
else if (publishSecurity === 'expiry') {
//限时鉴权 rtmp://<RTMPPublishDomain>/<Hub>/<streamTitle>?expire=<ExpireAt>&token=<Token>
const signStr = `/${hub}/${streamTitle}?expire=${expireAt}`;
const token = this.base64ToUrlSafe(this.hmacSha1(signStr, publishKey));
rtmpPushUrl = `rtmp://${publishDomain}${signStr}&token=${token}`;
streamKey = `${streamTitle}?expire=${expireAt}&token=${token}`;
}
else if (publishSecurity === 'expiry_sk') {
//限时鉴权sk rtmp://<RTMPPublishDomain>/<Hub>/<streamTitle>?e=<ExpireAt>&token=<Token>
const signStr = `/${hub}/${streamTitle}?e=${expireAt}`;
const token = this.accessKey + ':' + this.base64ToUrlSafe(this.hmacSha1(signStr, this.secretKey));
rtmpPushUrl = `rtmp://${publishDomain}${signStr}&token=${token}`;
streamKey = `${streamTitle}?e=${expireAt}&token=${token}`;
}
//根据拉流域名类型生成播放地址
let playUrl = '', playPath = '';
if (playDomainType === 'rtmp') {
playPath = `/${hub}/${streamTitle}`;
playUrl = `rtmp://${playDomain}${playPath}`;
}
else if (playDomainType === 'hls') {
playPath = `/${hub}/${streamTitle}.m3u8`;
playUrl = `http://${playDomain}${playPath}`;
}
else if (playDomainType === 'flv') {
playPath = `/${hub}/${streamTitle}.flv`;
playUrl = `http://${playDomain}${playPath}`;
}
if (playKey && playKey !== '') {
//开启时间戳防盗链
const t = expireAt.toString(16).toLowerCase();
const playSign = ts_md5_1.Md5.hashStr(playKey + playPath + t).toString().toLowerCase();
playUrl += `?sign=${playSign}&t=${t}`;
}
return {
streamTitle,
hub,
rtmpPushUrl,
playUrl,
pcPushUrl,
streamKey,
expireAt,
};
}
async getPlayBackUrl(hub, playBackDomain, streamTitle, start, end, method, host, rawQuery) {
const encodeStreamTitle = this.base64ToUrlSafe(streamTitle);
const path = `/v2/hubs/${hub}/streams/${encodeStreamTitle}/saveas`;
const bodyStr = JSON.stringify({
fname: streamTitle,
start,
end,
});
const contentType = 'application/json';
const token = this.getLiveToken(method, path, host, rawQuery, contentType, bodyStr);
const url = `https://pili.qiniuapi.com${path}`;
await global.fetch(url, {
method: 'POST',
headers: {
Authorization: token,
'Content-Type': contentType,
},
body: bodyStr,
mode: 'no-cors',
});
return `https://${playBackDomain}/${streamTitle}.m3u8`;
}
/**
* 禁用直播流
* @param hub
* @param streamTitle
* @param host
* @param disabledTill
* @param disablePeriodSecond
*/
async disabledStream(hub, streamTitle, host, disabledTill, //禁播结束时间 -1永久禁播0解除禁播
disablePeriodSecond) {
const encodedStreamTitle = this.urlSafeBase64Encode(streamTitle);
const path = `/v2/hubs/${hub}/streams/${encodedStreamTitle}/disabled`;
const bodyStr = JSON.stringify({
disabledTill,
disablePeriodSecond,
});
const contentType = 'application/json';
const token = this.getLiveToken('POST', path, host, undefined, contentType, bodyStr);
const url = `https://pili.qiniuapi.com${path}`;
let response;
try {
response = await global.fetch(url, {
method: 'POST',
headers: {
Authorization: token,
'Content-Type': contentType,
},
body: bodyStr,
});
}
catch (err) {
throw new Exception_1.OakNetworkException();
}
if (response.status !== 200) {
const json = await response.json();
const { error } = json;
throw new Exception_1.OakExternalException('qiniu', response.status.toString(), error, {
status: response.status,
});
}
}
/**
* 查询直播流列表
* @param hub
* @param host
* @param liveOnly
* @param prefix
* @param limit
* @param marker
* @returns
*/
async getLiveStreamList(hub, host, liveOnly, prefix, limit, //取值范围0~5000
marker) {
const path = `/v2/hubs/${hub}/streams`;
const url = new url_1.url(`https://${host}${path}`);
if (liveOnly) {
url.searchParams.set('liveOnly', 'true');
}
if (prefix && prefix !== '') {
url.searchParams.set('prefix', prefix);
}
if (limit) {
url.searchParams.set('limit', limit.toString());
}
if (marker && marker !== '') {
url.searchParams.set('prefix', marker);
}
const contentType = 'application/x-www-form-urlencoded';
const token = this.getLiveToken('GET', path, host, undefined, contentType);
let response;
try {
response = await fetch(url.toString(), {
method: 'GET',
headers: {
Authorization: token,
'Content-Type': contentType,
},
});
}
catch (err) {
throw new Exception_1.OakNetworkException();
}
if (response.status === 200) {
const json = await response.json();
const streamTitles = json.items?.map((ele) => ele.key);
return {
streamTitles,
marker: json.marker,
};
}
else {
const json = await response.json();
const { error } = json;
throw new Exception_1.OakExternalException('qiniu', response.status.toString(), error, {
status: response.status,
});
}
}
/**
* 查询直播流信息
* @param hub
* @param streamTitle
* @param host
* @returns
*/
async getLiveStreamInfo(hub, streamTitle, host) {
const encodedStreamTitle = this.urlSafeBase64Encode(streamTitle);
const path = `/v2/hubs/${hub}/streams/${encodedStreamTitle}`;
const contentType = 'application/x-www-form-urlencoded';
const token = this.getLiveToken('GET', path, host, undefined, contentType);
const url = `https://pili.qiniuapi.com${path}`;
let response;
try {
response = await global.fetch(url, {
method: 'GET',
headers: {
Authorization: token,
'Content-Type': contentType,
},
});
}
catch (err) {
throw new Exception_1.OakNetworkException();
}
if (response.status === 200) {
const json = await response.json();
return json;
}
else {
const json = await response.json();
const { error } = json;
throw new Exception_1.OakExternalException('qiniu', response.status.toString(), error, {
status: response.status,
});
}
}
/**
* 管理端访问七牛云服务器
* @param path
* @param method
* @param headers
* @param body
*/
async access(host, path, headers, query, method, body, mockData) {
const query2 = query && getQueryString(query);
/**
* web/server环境测试通过小程序没测by Xc
*/
const url = new url_1.url(`https://${host}${path}`);
if (process.env.NODE_ENV === 'development' && mockData) {
console.warn(`mocking access qiniu api: url: ${url.toString()}, body: ${JSON.stringify(body)}, method: ${method}`, mockData);
return mockData;
}
if (query2) {
url.search = query2;
}
const now = formatUTC(new Date(), 'YYYYMMDDTHHmmssZ');
headers['X-Qiniu-Date'] = now;
if (!headers['Content-Type']) {
headers['Content-Type'] = 'application/x-www-form-urlencoded';
}
const accessToken = this.generateKodoAccessToken(method || 'GET', host, path, headers, query2, body);
let response;
try {
response = await fetch(url.toString(), {
method,
headers: {
Authorization: `Qiniu ${accessToken}`,
...headers,
},
body,
});
}
catch (err) {
// fetch返回异常一定是网络异常
throw new Exception_1.OakNetworkException();
}
const responseType = response.headers.get('Content-Type') ||
response.headers.get('content-type');
// qiniu如果返回空结果类型也是application/json(delete kodo file)
const contentLength = response.headers.get('Content-Length') ||
response.headers.get('content-length');
if (Number(contentLength) === 0) {
return;
}
if (responseType?.toLocaleLowerCase().match(/application\/json/i)) {
const json = await response.json();
if (response.status > 299) {
// 七牛服务器返回异常根据文档一定是json实测发现返回和文档不一样
// https://developer.qiniu.com/kodo/3928/error-responses
// qiniu的status是重要的返回信息
const { error_code, error } = json;
throw new Exception_1.OakExternalException('qiniu', error_code, error, {
status: response.status,
});
}
return json;
}
else if (responseType?.toLocaleLowerCase().match(/application\/octet-stream/i)) {
const result = await response.arrayBuffer();
return result;
}
else if (responseType?.toLocaleLowerCase().match(/text\/plain/i)) {
const result = await response.text();
return result;
}
else {
throw new Error(`尚不支持的content-type类型${responseType}`);
}
}
/**
* https://developer.qiniu.com/kodo/1208/upload-token
* @param scope
* @returns
*/
generateKodoUploadToken(scope) {
// 构造策略
const putPolicy = {
scope: scope,
deadline: 3600 + Math.floor(Date.now() / 1000),
};
// 构造凭证
const encodedFlags = this.urlSafeBase64Encode(JSON.stringify(putPolicy));
const encoded = this.hmacSha1(encodedFlags, this.secretKey);
const encodedSign = this.base64ToUrlSafe(encoded);
const uploadToken = this.accessKey + ':' + encodedSign + ':' + encodedFlags;
return uploadToken;
}
/**
* https://developer.qiniu.com/kodo/1201/access-token
*/
generateKodoAccessToken(method, host, path, headers, query, body) {
let signingStr = method + ' ' + path;
if (query) {
signingStr += '?' + query;
}
signingStr += '\nHost: ' + host;
const contentType = headers && headers['Content-Type'];
if (contentType) {
signingStr += '\nContent-Type: ' + contentType;
}
if (headers) {
const ks = Object.keys(headers).filter((ele) => ele.startsWith('X-Qiniu-'));
ks.sort((e1, e2) => (e1 < e2 ? -1 : 1));
ks.forEach((k) => (signingStr += `\n${k}: ${headers[k]}`));
}
signingStr += '\n\n';
if (body) {
signingStr += body.toString();
}
const sign = this.hmacSha1(signingStr, this.secretKey);
const encodedSign = this.base64ToUrlSafe(sign);
const result = `${this.accessKey}:${encodedSign}`;
return result;
}
base64ToUrlSafe(v) {
return v.replace(/\//g, '_').replace(/\+/g, '-');
}
hmacSha1(encodedFlags, secretKey) {
const hmac = crypto_1.default.createHmac('sha1', secretKey);
hmac.update(encodedFlags);
return hmac.digest('base64');
}
urlSafeBase64Encode(jsonFlags) {
const encoded = buffer_1.Buffer.from(jsonFlags).toString('base64');
return this.base64ToUrlSafe(encoded);
}
}
exports.QiniuCloudInstance = QiniuCloudInstance;