qiniu查看kodo对象的status功能实现

This commit is contained in:
Xu Chang 2023-10-07 18:00:48 +08:00
parent 5a07ee4b87
commit 216a215095
8 changed files with 383 additions and 31 deletions

View File

@ -36,6 +36,10 @@ export declare class QiniuCloudInstance {
streamKey: string;
expireAt: number;
}>;
/**
* https://developer.qiniu.com/kodo/1308/stat
*/
getKodoStat(bucket: string, key: string): Promise<any>;
/**
*
* @param publishDomain
@ -57,7 +61,24 @@ export declare class QiniuCloudInstance {
expireAt: number;
};
getPlayBackUrl(hub: string, playBackDomain: string, streamTitle: string, start: number, end: number, method: 'GET' | 'POST' | 'PUT' | 'DELETE', host: string, rawQuery?: string): Promise<string>;
private getToken;
/**
* 访
* @param path
* @param method
* @param headers
* @param body
*/
private access;
/**
* https://developer.qiniu.com/kodo/1208/upload-token
* @param scope
* @returns
*/
private generateKodoUploadToken;
/**
* https://developer.qiniu.com/kodo/1201/access-token
*/
private genernateKodoAccessToken;
private base64ToUrlSafe;
private hmacSha1;
private urlSafeBase64Encode;

View File

@ -2,6 +2,9 @@ require('../../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';
const QINIU_CLOUD_HOST = 'rs.qiniuapi.com';
export class QiniuCloudInstance {
accessKey;
secretKey;
@ -20,7 +23,7 @@ export class QiniuCloudInstance {
getUploadInfo(uploadHost, bucket, key) {
try {
const scope = key ? `${bucket}:${key}` : bucket;
const uploadToken = this.getToken(scope);
const uploadToken = this.generateKodoUploadToken(scope);
return {
key,
uploadToken,
@ -89,6 +92,18 @@ export class QiniuCloudInstance {
const obj = this.getStreamObj(publishDomain, playDomain, hub, publishKey, playKey, streamTitle, expireAt);
return obj;
}
/**
* https://developer.qiniu.com/kodo/1308/stat
*/
async getKodoStat(bucket, key) {
const entry = `${bucket}:${key}`;
const encodedEntryURI = this.urlSafeBase64Encode(entry);
const path = `/stat/${encodedEntryURI}`;
const result = await this.access(path, undefined, 'Get', {
'Content-Type': 'application/x-www-form-urlencoded',
});
return result;
}
/**
* 计算直播流地址相关信息
* @param publishDomain
@ -146,7 +161,60 @@ export class QiniuCloudInstance {
});
return `https://${playBackDomain}/${streamTitle}.m3u8`;
}
getToken(scope) {
/**
* 管理端访问七牛云服务器
* @param path
* @param method
* @param headers
* @param body
*/
async access(path, query, method, headers, body) {
const contentType = headers && headers['Content-Type'];
const url = new URL(`https://${QINIU_CLOUD_HOST}${path}`);
if (query) {
url.search = typeof query === 'object' ? stringify(query) : query;
}
const accessToken = this.genernateKodoAccessToken(method || 'Get', QINIU_CLOUD_HOST, path, undefined, contentType);
let response;
try {
response = await fetch(`https://${QINIU_CLOUD_HOST}${path}`, {
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');
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
const { code, error } = json;
return new OakExternalException('qiniu', code, error);
}
return json;
}
else if (responseType?.toLocaleLowerCase().match(/application\/octet-stream/i)) {
const result = await response.arrayBuffer();
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,
@ -159,6 +227,31 @@ export class QiniuCloudInstance {
const uploadToken = this.accessKey + ':' + encodedSign + ':' + encodedFlags;
return uploadToken;
}
/**
* https://developer.qiniu.com/kodo/1201/access-token
*/
genernateKodoAccessToken(method, host, path, query, contentType, body, xHeaders) {
let signingStr = method + ' ' + path;
if (query) {
signingStr += '?' + query;
}
signingStr += '\nHost: ' + host;
if (contentType) {
signingStr += '\nContent-Type: ' + contentType;
}
if (xHeaders) {
const ks = Object.keys(xHeaders);
ks.sort((e1, e2) => e1 < e2 ? -1 : 1);
ks.forEach((k) => signingStr += `\n${k}: ${xHeaders[k]}`);
}
signingStr += '\n\n';
if (body) {
signingStr += body;
}
const sign = this.hmacSha1(signingStr, this.secretKey);
const encodedSign = this.urlSafeBase64Encode(sign);
return `${this.accessKey}:${encodedSign}`;
}
base64ToUrlSafe(v) {
return v.replace(/\//g, '_').replace(/\+/g, '-');
}

View File

@ -3,7 +3,8 @@ import crypto from 'crypto';
import { Buffer } from 'buffer';
import URL from 'url';
import FormData from 'form-data';
import { OakNetworkException, OakServerProxyException } from 'oak-domain/lib/types/Exception';
import { OakExternalException, OakNetworkException, OakServerProxyException } from 'oak-domain/lib/types/Exception';
import assert from 'assert';
export class WechatPublicInstance {
appId;
appSecret;
@ -54,7 +55,7 @@ export class WechatPublicInstance {
if ([40001, 42001].includes(json.errcode)) {
return this.refreshAccessToken(url, init);
}
throw new Error(`调用微信接口返回出错code是${json.errcode},信息是${json.errmsg}`);
throw new OakExternalException('wechatPublic', json.errcode, json.errmsg);
}
return json;
}
@ -70,7 +71,7 @@ export class WechatPublicInstance {
if ([40001, 42001].includes(json.errcode)) {
return this.refreshAccessToken(url, init);
}
throw new Error(`调用微信接口返回出错code是${json.errcode},信息是${json.errmsg}`);
throw new OakExternalException('wechatPublic', json.errcode, json.errmsg);
}
return json;
}
@ -259,9 +260,7 @@ export class WechatPublicInstance {
}
async getQrCode(options) {
const { sceneId, sceneStr, expireSeconds, isPermanent } = options;
if (!sceneId && !sceneStr) {
throw new Error('Missing sceneId or sceneStr');
}
assert(sceneId || sceneStr);
const scene = sceneId
? {
scene_id: sceneId,
@ -396,7 +395,7 @@ export class WechatPublicInstance {
break;
}
default: {
throw new Error('当前消息类型暂不支持');
assert(false, '当前消息类型暂不支持');
}
}
const token = await this.getAccessToken();

View File

@ -36,6 +36,10 @@ export declare class QiniuCloudInstance {
streamKey: string;
expireAt: number;
}>;
/**
* https://developer.qiniu.com/kodo/1308/stat
*/
getKodoStat(bucket: string, key: string): Promise<any>;
/**
*
* @param publishDomain
@ -57,7 +61,24 @@ export declare class QiniuCloudInstance {
expireAt: number;
};
getPlayBackUrl(hub: string, playBackDomain: string, streamTitle: string, start: number, end: number, method: 'GET' | 'POST' | 'PUT' | 'DELETE', host: string, rawQuery?: string): Promise<string>;
private getToken;
/**
* 访
* @param path
* @param method
* @param headers
* @param body
*/
private access;
/**
* https://developer.qiniu.com/kodo/1208/upload-token
* @param scope
* @returns
*/
private generateKodoUploadToken;
/**
* https://developer.qiniu.com/kodo/1201/access-token
*/
private genernateKodoAccessToken;
private base64ToUrlSafe;
private hmacSha1;
private urlSafeBase64Encode;

View File

@ -6,6 +6,9 @@ require('../../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 QINIU_CLOUD_HOST = 'rs.qiniuapi.com';
class QiniuCloudInstance {
accessKey;
secretKey;
@ -24,7 +27,7 @@ class QiniuCloudInstance {
getUploadInfo(uploadHost, bucket, key) {
try {
const scope = key ? `${bucket}:${key}` : bucket;
const uploadToken = this.getToken(scope);
const uploadToken = this.generateKodoUploadToken(scope);
return {
key,
uploadToken,
@ -93,6 +96,18 @@ class QiniuCloudInstance {
const obj = this.getStreamObj(publishDomain, playDomain, hub, publishKey, playKey, streamTitle, expireAt);
return obj;
}
/**
* https://developer.qiniu.com/kodo/1308/stat
*/
async getKodoStat(bucket, key) {
const entry = `${bucket}:${key}`;
const encodedEntryURI = this.urlSafeBase64Encode(entry);
const path = `/stat/${encodedEntryURI}`;
const result = await this.access(path, undefined, 'Get', {
'Content-Type': 'application/x-www-form-urlencoded',
});
return result;
}
/**
* 计算直播流地址相关信息
* @param publishDomain
@ -150,7 +165,60 @@ class QiniuCloudInstance {
});
return `https://${playBackDomain}/${streamTitle}.m3u8`;
}
getToken(scope) {
/**
* 管理端访问七牛云服务器
* @param path
* @param method
* @param headers
* @param body
*/
async access(path, query, method, headers, body) {
const contentType = headers && headers['Content-Type'];
const url = new URL(`https://${QINIU_CLOUD_HOST}${path}`);
if (query) {
url.search = typeof query === 'object' ? (0, querystring_1.stringify)(query) : query;
}
const accessToken = this.genernateKodoAccessToken(method || 'Get', QINIU_CLOUD_HOST, path, undefined, contentType);
let response;
try {
response = await fetch(`https://${QINIU_CLOUD_HOST}${path}`, {
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');
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
const { code, error } = json;
return new Exception_1.OakExternalException('qiniu', code, error);
}
return json;
}
else if (responseType?.toLocaleLowerCase().match(/application\/octet-stream/i)) {
const result = await response.arrayBuffer();
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,
@ -163,6 +231,31 @@ class QiniuCloudInstance {
const uploadToken = this.accessKey + ':' + encodedSign + ':' + encodedFlags;
return uploadToken;
}
/**
* https://developer.qiniu.com/kodo/1201/access-token
*/
genernateKodoAccessToken(method, host, path, query, contentType, body, xHeaders) {
let signingStr = method + ' ' + path;
if (query) {
signingStr += '?' + query;
}
signingStr += '\nHost: ' + host;
if (contentType) {
signingStr += '\nContent-Type: ' + contentType;
}
if (xHeaders) {
const ks = Object.keys(xHeaders);
ks.sort((e1, e2) => e1 < e2 ? -1 : 1);
ks.forEach((k) => signingStr += `\n${k}: ${xHeaders[k]}`);
}
signingStr += '\n\n';
if (body) {
signingStr += body;
}
const sign = this.hmacSha1(signingStr, this.secretKey);
const encodedSign = this.urlSafeBase64Encode(sign);
return `${this.accessKey}:${encodedSign}`;
}
base64ToUrlSafe(v) {
return v.replace(/\//g, '_').replace(/\+/g, '-');
}

View File

@ -8,6 +8,7 @@ const buffer_1 = require("buffer");
const url_1 = tslib_1.__importDefault(require("url"));
const form_data_1 = tslib_1.__importDefault(require("form-data"));
const Exception_1 = require("oak-domain/lib/types/Exception");
const assert_1 = tslib_1.__importDefault(require("assert"));
class WechatPublicInstance {
appId;
appSecret;
@ -58,7 +59,7 @@ class WechatPublicInstance {
if ([40001, 42001].includes(json.errcode)) {
return this.refreshAccessToken(url, init);
}
throw new Error(`调用微信接口返回出错code是${json.errcode},信息是${json.errmsg}`);
throw new Exception_1.OakExternalException('wechatPublic', json.errcode, json.errmsg);
}
return json;
}
@ -74,7 +75,7 @@ class WechatPublicInstance {
if ([40001, 42001].includes(json.errcode)) {
return this.refreshAccessToken(url, init);
}
throw new Error(`调用微信接口返回出错code是${json.errcode},信息是${json.errmsg}`);
throw new Exception_1.OakExternalException('wechatPublic', json.errcode, json.errmsg);
}
return json;
}
@ -263,9 +264,7 @@ class WechatPublicInstance {
}
async getQrCode(options) {
const { sceneId, sceneStr, expireSeconds, isPermanent } = options;
if (!sceneId && !sceneStr) {
throw new Error('Missing sceneId or sceneStr');
}
(0, assert_1.default)(sceneId || sceneStr);
const scene = sceneId
? {
scene_id: sceneId,
@ -400,7 +399,7 @@ class WechatPublicInstance {
break;
}
default: {
throw new Error('当前消息类型暂不支持');
(0, assert_1.default)(false, '当前消息类型暂不支持');
}
}
const token = await this.getAccessToken();

View File

@ -1,7 +1,13 @@
require('../../fetch');
import crypto from 'crypto';
import { UrlObject } from 'url';
import { Md5 } from 'ts-md5';
import { Buffer } from 'buffer';
import { stringify } from 'querystring';
import { OakExternalException, OakNetworkException } from 'oak-domain/lib/types/Exception';
const QINIU_CLOUD_HOST = 'rs.qiniuapi.com';
type X_Header = `X-Qiniu-${string}`;
export class QiniuCloudInstance {
private accessKey: string;
@ -27,7 +33,7 @@ export class QiniuCloudInstance {
) {
try {
const scope = key ? `${bucket}:${key}` : bucket;
const uploadToken = this.getToken(scope);
const uploadToken = this.generateKodoUploadToken(scope);
return {
key,
uploadToken,
@ -126,6 +132,29 @@ export class QiniuCloudInstance {
return obj;
}
/**
* https://developer.qiniu.com/kodo/1308/stat
*/
async getKodoStat(bucket: string, key: string) {
const entry = `${bucket}:${key}`;
const encodedEntryURI = this.urlSafeBase64Encode(entry);
const path = `/stat/${encodedEntryURI}`;
const result = await this.access(path, undefined, 'Get', {
'Content-Type': 'application/x-www-form-urlencoded',
});
return result as {
fsize: number;
hash: string;
mimeType: string;
type: 0 | 1 | 2 | 3;
putTime: number;
};
}
/**
*
* @param publishDomain
@ -210,7 +239,71 @@ export class QiniuCloudInstance {
return `https://${playBackDomain}/${streamTitle}.m3u8`;
}
private getToken(scope: string) {
/**
* 访
* @param path
* @param method
* @param headers
* @param body
*/
private async access(
path: string,
query?: UrlObject['query'],
method?: RequestInit['method'],
headers?: Record<string, string>,
body?: RequestInit['body']
) {
const contentType = headers && headers['Content-Type'];
const url = new URL(`https://${QINIU_CLOUD_HOST}${path}`);
if (query) {
url.search = typeof query === 'object' ? stringify(query) : query;
}
const accessToken = this.genernateKodoAccessToken(method || 'Get', QINIU_CLOUD_HOST, path, undefined, contentType);
let response: Response;
try {
response = await fetch(`https://${QINIU_CLOUD_HOST}${path}`, {
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');
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
const { code, error } = json;
return new OakExternalException('qiniu', code, error);
}
return json;
}
else if (responseType?.toLocaleLowerCase().match(/application\/octet-stream/i)) {
const result = await response.arrayBuffer();
return result;
}
else {
throw new Error(`尚不支持的content-type类型${responseType}`);
}
}
/**
* https://developer.qiniu.com/kodo/1208/upload-token
* @param scope
* @returns
*/
private generateKodoUploadToken(scope: string) {
// 构造策略
const putPolicy = {
scope: scope,
@ -227,6 +320,43 @@ export class QiniuCloudInstance {
return uploadToken;
}
/**
* https://developer.qiniu.com/kodo/1201/access-token
*/
private genernateKodoAccessToken(
method: string,
host: string,
path: string,
query?: string,
contentType?: string,
body?: string,
xHeaders?: Record<X_Header, string>
) {
let signingStr = method + ' ' + path;
if (query) {
signingStr += '?' + query;
}
signingStr += '\nHost: ' + host;
if (contentType) {
signingStr += '\nContent-Type: ' + contentType;
}
if (xHeaders) {
const ks = Object.keys(xHeaders);
ks.sort((e1, e2) => e1 < e2 ? -1 : 1);
ks.forEach(
(k) => signingStr += `\n${k}: ${xHeaders[k as X_Header]}`,
);
}
signingStr += '\n\n';
if (body) {
signingStr += body;
}
const sign = this.hmacSha1(signingStr, this.secretKey);
const encodedSign = this.urlSafeBase64Encode(sign);
return `${this.accessKey}:${encodedSign}`;
}
private base64ToUrlSafe(v: string) {
return v.replace(/\//g, '_').replace(/\+/g, '-');
}
@ -236,6 +366,7 @@ export class QiniuCloudInstance {
hmac.update(encodedFlags);
return hmac.digest('base64');
}
private urlSafeBase64Encode(jsonFlags: string) {
const encoded = Buffer.from(jsonFlags).toString('base64');
return this.base64ToUrlSafe(encoded);

View File

@ -4,6 +4,7 @@ import { Buffer } from 'buffer';
import URL from 'url';
import FormData from 'form-data';
import { OakExternalException, OakNetworkException, OakServerProxyException } from 'oak-domain/lib/types/Exception';
import assert from 'assert';
// 目前先支持text和news, 其他type文档https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Service_Center_messages.html
// type ServeMessageType = 'text' | 'news' | 'mpnews' | 'mpnewsarticle' | 'image' | 'voice' | 'video' | 'music' | 'msgmenu';/
@ -106,9 +107,7 @@ export class WechatPublicInstance {
if ([40001, 42001].includes(json.errcode)) {
return this.refreshAccessToken(url, init);
}
throw new Error(
`调用微信接口返回出错code是${json.errcode},信息是${json.errmsg}`
);
throw new OakExternalException('wechatPublic', json.errcode, json.errmsg);
}
return json;
}
@ -126,9 +125,7 @@ export class WechatPublicInstance {
if ([40001, 42001].includes(json.errcode)) {
return this.refreshAccessToken(url, init);
}
throw new Error(
`调用微信接口返回出错code是${json.errcode},信息是${json.errmsg}`
);
throw new OakExternalException('wechatPublic', json.errcode, json.errmsg);
}
return json;
}
@ -379,9 +376,7 @@ export class WechatPublicInstance {
isPermanent?: boolean;
}) {
const { sceneId, sceneStr, expireSeconds, isPermanent } = options;
if (!sceneId && !sceneStr) {
throw new Error('Missing sceneId or sceneStr');
}
assert(sceneId || sceneStr);
const scene = sceneId
? {
scene_id: sceneId,
@ -534,7 +529,7 @@ export class WechatPublicInstance {
break;
}
default: {
throw new Error('当前消息类型暂不支持');
assert(false, '当前消息类型暂不支持');
}
}
const token = await this.getAccessToken();