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

865 lines
31 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.

require('../../utils/fetch');
import crypto from 'crypto';
import { Md5 } from 'ts-md5';
import { Buffer } from 'buffer';
import { stringify } from 'querystring';
import { OakExternalException, OakNetworkException, } from 'oak-domain/lib/types/Exception';
import { url as URL } from '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 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;
}
export 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 OakNetworkException();
}
if (response.status !== 200) {
if (response.status === 614) {
throw new OakExternalException('qiniu', response.status.toString(), '直播流名已存在', {
status: response.status,
});
}
else {
const json = await response.json();
const { error } = json;
throw new 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 = 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 OakNetworkException();
}
if (response.status !== 200) {
const json = await response.json();
const { error } = json;
throw new 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(`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 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 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 OakNetworkException();
}
if (response.status === 200) {
const json = await response.json();
return json;
}
else {
const json = await response.json();
const { error } = json;
throw new OakExternalException('qiniu', response.status.toString(), error, {
status: response.status,
});
}
}
/**
* 创建块(分片上传第一步)
* https://developer.qiniu.com/kodo/1286/mkblk
* @param zone 区域
* @param blockSize 块大小,单位字节
* @param firstChunkData 第一个分片的数据
* @param bucket 存储空间名称
* @returns
*/
async mkblk(zone, blockSize, firstChunkData, bucket) {
const uploadHost = QINIU_ENDPOINT_LIST[zone].ul;
const path = `/mkblk/${blockSize}`;
const url = `https://${uploadHost}${path}`;
const uploadToken = this.generateKodoUploadToken(bucket);
// 将Buffer转换为Uint8Array然后获取buffer
const bodyData = firstChunkData instanceof Buffer
? firstChunkData.buffer.slice(firstChunkData.byteOffset, firstChunkData.byteOffset + firstChunkData.byteLength)
: firstChunkData.buffer.slice(firstChunkData.byteOffset, firstChunkData.byteOffset + firstChunkData.byteLength);
let response;
try {
response = await global.fetch(url, {
method: 'POST',
headers: {
'Authorization': `UpToken ${uploadToken}`,
'Content-Type': 'application/octet-stream',
},
body: bodyData,
});
}
catch (err) {
throw new OakNetworkException();
}
if (response.status === 200) {
const json = await response.json();
return json;
}
else {
const json = await response.json();
const { error } = json;
throw new OakExternalException('qiniu', response.status.toString(), error, {
status: response.status,
});
}
}
/**
* 上传分片数据(后续分片)
* https://developer.qiniu.com/kodo/1251/bput
* @param zone 区域
* @param ctx 前一次上传返回的块上下文
* @param offset 当前分片在块中的偏移量
* @param chunkData 分片数据
* @param bucket 存储空间名称
* @returns
*/
async bput(zone, ctx, offset, chunkData, bucket) {
const uploadHost = QINIU_ENDPOINT_LIST[zone].ul;
const path = `/bput/${ctx}/${offset}`;
const url = `https://${uploadHost}${path}`;
const uploadToken = this.generateKodoUploadToken(bucket);
// 将Buffer转换为Uint8Array然后获取buffer
const bodyData = chunkData instanceof Buffer
? chunkData.buffer.slice(chunkData.byteOffset, chunkData.byteOffset + chunkData.byteLength)
: chunkData.buffer.slice(chunkData.byteOffset, chunkData.byteOffset + chunkData.byteLength);
let response;
try {
response = await global.fetch(url, {
method: 'POST',
headers: {
'Authorization': `UpToken ${uploadToken}`,
'Content-Type': 'application/octet-stream',
},
body: bodyData,
});
}
catch (err) {
throw new OakNetworkException();
}
if (response.status === 200) {
const json = await response.json();
return json;
}
else {
const json = await response.json();
const { error } = json;
throw new OakExternalException('qiniu', response.status.toString(), error, {
status: response.status,
});
}
}
/**
* 创建文件(合并所有块)
* https://developer.qiniu.com/kodo/1287/mkfile
* @param zone 区域
* @param fileSize 文件总大小,单位字节
* @param key 文件名
* @param bucket 存储空间名称
* @param ctxList 所有块的ctx列表按顺序排列
* @param mimeType 文件MIME类型
* @param fileName 原始文件名
* @returns
*/
async mkfile(zone, fileSize, key, bucket, ctxList, mimeType, fileName) {
const uploadHost = QINIU_ENDPOINT_LIST[zone].ul;
const encodedKey = this.urlSafeBase64Encode(key);
let path = `/mkfile/${fileSize}/key/${encodedKey}`;
if (mimeType) {
const encodedMimeType = this.urlSafeBase64Encode(mimeType);
path += `/mimeType/${encodedMimeType}`;
}
if (fileName) {
const encodedFileName = this.urlSafeBase64Encode(fileName);
path += `/fname/${encodedFileName}`;
}
const url = `https://${uploadHost}${path}`;
const uploadToken = this.generateKodoUploadToken(`${bucket}:${key}`);
// ctx列表用逗号分隔作为请求体
const body = ctxList.join(',');
let response;
try {
response = await global.fetch(url, {
method: 'POST',
headers: {
'Authorization': `UpToken ${uploadToken}`,
'Content-Type': 'text/plain',
},
body,
});
}
catch (err) {
throw new OakNetworkException();
}
if (response.status === 200) {
const json = await response.json();
return json;
}
else {
const json = await response.json();
const { error } = json;
throw new 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(`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 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 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.createHmac('sha1', secretKey);
hmac.update(encodedFlags);
return hmac.digest('base64');
}
urlSafeBase64Encode(jsonFlags) {
const encoded = Buffer.from(jsonFlags).toString('base64');
return this.base64ToUrlSafe(encoded);
}
/**
* 获取预签名对象URL统一接口
* 生成七牛云对象的访问/下载/上传URL
*
* 注意:七牛云下载需要使用存储桶绑定的域名,不能使用内网域名
* 上传使用表单方式,返回 formdata 中包含上传凭证
*/
async presignObjectUrl(method, bucket, zone, key, options) {
const deadline = Math.floor(Date.now() / 1000) + (options?.expires || 3600);
if (method === 'GET') {
// 下载场景:需要提供存储桶绑定的域名
if (!options?.domain) {
throw new OakExternalException('qiniu', 'MISSING_DOMAIN', '七牛云下载需要提供存储桶绑定的域名domain 参数)', {}, 'oak-external-sdk', {});
}
const baseUrl = `https://${options.domain}/${encodeURIComponent(key)}`;
// 生成下载凭证
// SignStr = downloadUrl?e=<deadline>
const signStr = `${baseUrl}?e=${deadline}`;
const encodedSign = this.hmacSha1(signStr, this.secretKey);
const encodedSignSafe = this.base64ToUrlSafe(encodedSign);
const downloadToken = `${this.accessKey}:${encodedSignSafe}`;
const url = `${baseUrl}?e=${deadline}&token=${downloadToken}`;
return { url };
}
else if (method === 'PUT' || method === 'POST') {
// 上传场景:七牛使用表单上传方式
const scope = key ? `${bucket}:${key}` : bucket;
// 构造上传策略
const putPolicy = {
scope,
deadline,
};
// 生成上传凭证
const encodedPolicy = this.urlSafeBase64Encode(JSON.stringify(putPolicy));
const encodedSign = this.hmacSha1(encodedPolicy, this.secretKey);
const encodedSignSafe = this.base64ToUrlSafe(encodedSign);
const uploadToken = `${this.accessKey}:${encodedSignSafe}:${encodedPolicy}`;
const uploadHost = QINIU_ENDPOINT_LIST[zone].ul;
const url = `https://${uploadHost}`;
return {
url,
formdata: {
key,
token: uploadToken,
// file 字段需要调用方自行添加
},
};
}
else {
// DELETE 等操作需要使用管理凭证不支持预签名URL方式
throw new OakExternalException('qiniu', 'UNSUPPORTED_METHOD', `七牛云不支持 ${method} 方法的预签名URL请使用 removeKodoFile 等管理接口`, {}, 'oak-external-sdk', {});
}
}
}