@xuanqikai/one-click-upload
Version:
A CLI tool for one-click file upload to cloud storage services (OSS, TOS)
546 lines • 21.2 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.TOSCDNService = void 0;
const crypto = __importStar(require("crypto"));
class TOSCDNService {
constructor(config) {
this.endpoint = 'https://open.volcengineapi.com';
// 不参与加签过程的 header key
this.HEADER_KEYS_TO_IGNORE = new Set([
"authorization",
"content-type",
"content-length",
"user-agent",
"presigned-expires",
"expect",
]);
this.config = config;
}
/**
* 判断URL是文件还是目录
*/
isDirectory(url) {
// 如果URL以/结尾,或者没有文件扩展名,认为是目录
if (url.endsWith('/')) {
return true;
}
// 检查是否有文件扩展名
const pathParts = url.split('/');
const lastPart = pathParts[pathParts.length - 1];
const hasExtension = lastPart.includes('.') && !lastPart.startsWith('.');
return !hasExtension;
}
/**
* 刷新CDN缓存
*/
async refreshCDN(url) {
try {
console.log(`开始刷新CDN: ${url}`);
// 如果URL不是完整的HTTP/HTTPS URL,构建完整的CDN URL
let fullUrl = url;
if (!url.startsWith('http://') && !url.startsWith('https://')) {
if (this.config.cdnDomain) {
fullUrl = `https://${this.config.cdnDomain}/${url}`;
}
else {
throw new Error('CDN域名未配置,无法构建完整URL');
}
}
// 判断是文件还是目录,选择相应的刷新类型
const isDir = this.isDirectory(fullUrl);
const taskType = isDir ? 'refresh_dir' : 'refresh_url';
// 构建请求参数
const params = {
Action: 'CreatePurgePrefetchTask',
Version: '2021-04-01',
TaskType: taskType,
Urls: [fullUrl]
};
// 构建请求头(包含签名)
const headers = this.buildHeaders(params);
// 构建请求体
const requestBody = {
TaskType: params.TaskType,
Urls: params.Urls
};
// 发送请求
const response = await fetch(`${this.endpoint}/?Action=${params.Action}&Version=${params.Version}`, {
method: 'POST',
headers,
body: JSON.stringify(requestBody)
});
const responseData = await response.json();
if (responseData?.Result?.TaskId) {
console.log(`CDN刷新成功,任务ID: ${responseData.Result.TaskId}`);
return {
success: true,
taskId: responseData.Result.TaskId,
url
};
}
else if (responseData?.ResponseMetadata?.Error) {
const error = responseData.ResponseMetadata.Error;
console.error(`CDN刷新失败: ${error.Code} - ${error.Message}`);
return {
success: false,
url,
error: `${error.Code}: ${error.Message}`
};
}
else {
throw new Error('刷新请求失败:未获取到任务ID或错误信息');
}
}
catch (error) {
console.error('CDN刷新异常:', error.message);
return {
success: false,
url,
error: error.message || 'Unknown error'
};
}
}
/**
* 预热CDN缓存
*/
async preloadCDN(url) {
try {
console.log(`开始预热CDN: ${url}`);
// 如果URL不是完整的HTTP/HTTPS URL,构建完整的CDN URL
let fullUrl = url;
if (!url.startsWith('http://') && !url.startsWith('https://')) {
if (this.config.cdnDomain) {
fullUrl = `https://${this.config.cdnDomain}/${url}`;
}
else {
throw new Error('CDN域名未配置,无法构建完整URL');
}
}
// 构建请求参数
const params = {
Action: 'CreatePurgePrefetchTask',
Version: '2021-04-01',
TaskType: 'prefetch_url',
Urls: [fullUrl]
};
// 构建请求头(包含签名)
const headers = this.buildHeaders(params);
// 构建请求体
const requestBody = {
TaskType: params.TaskType,
Urls: params.Urls
};
// 发送请求
const response = await fetch(`${this.endpoint}/?Action=${params.Action}&Version=${params.Version}`, {
method: 'POST',
headers,
body: JSON.stringify(requestBody)
});
const responseData = await response.json();
if (responseData?.Result?.TaskId) {
console.log(`CDN预热成功,任务ID: ${responseData.Result.TaskId}`);
return {
success: true,
taskId: responseData.Result.TaskId,
url
};
}
else if (responseData?.ResponseMetadata?.Error) {
const error = responseData.ResponseMetadata.Error;
console.error(`CDN预热失败: ${error.Code} - ${error.Message}`);
return {
success: false,
url,
error: `${error.Code}: ${error.Message}`
};
}
else {
throw new Error('预热请求失败:未获取到任务ID或错误信息');
}
}
catch (error) {
console.error('CDN预热异常:', error.message);
return {
success: false,
url,
error: error.message || 'Unknown error'
};
}
}
/**
* 先刷新后预热CDN缓存(推荐使用)
*/
async refreshAndPreloadCDN(url) {
const results = [];
try {
console.log(`开始刷新并预热CDN: ${url}`);
// 第一步:先进行刷新
console.log('第一步:执行CDN刷新...');
const refreshResult = await this.refreshCDN(url);
results.push({
...refreshResult,
taskType: 'refresh'
});
// 检查刷新是否成功
if (!refreshResult.success || !refreshResult.taskId) {
console.error('CDN刷新失败,无法进行预热');
return results;
}
// 查询刷新前的配额
console.log('查询刷新前配额...');
const quotaBefore = await this.queryTaskQuota();
// 等待一小段时间,让刷新任务开始执行
console.log('等待刷新任务开始执行...');
await new Promise(resolve => setTimeout(resolve, 3000));
// 查询刷新后的配额,检查是否有变化
console.log('查询刷新后配额...');
const quotaAfter = await this.queryTaskQuota();
if (quotaBefore && quotaAfter) {
// 检查URL刷新配额是否有变化
const urlRefreshChanged = quotaBefore.UrlRemain !== quotaAfter.UrlRemain;
const dirRefreshChanged = quotaBefore.DirRemain !== quotaAfter.DirRemain;
if (urlRefreshChanged || dirRefreshChanged) {
console.log('检测到刷新配额变化,刷新任务已开始执行');
}
else {
console.log('刷新配额未变化,可能刷新任务还在排队中');
}
}
console.log('开始预热...');
// 第二步:再进行预热
console.log('第二步:执行CDN预热...');
const preloadResult = await this.preloadCDN(url);
results.push({
...preloadResult,
taskType: 'prefetch'
});
console.log('CDN刷新并预热完成');
return results;
}
catch (error) {
console.error('CDN刷新并预热异常:', error.message);
return results;
}
}
/**
* 构建请求头(包含签名)
*/
buildHeaders(params) {
const now = new Date();
// 根据官方文档,时间格式应该是 YYYYMMDDTHHMMSSZ
const timestamp = now.getUTCFullYear().toString() +
(now.getUTCMonth() + 1).toString().padStart(2, '0') +
now.getUTCDate().toString().padStart(2, '0') + 'T' +
now.getUTCHours().toString().padStart(2, '0') +
now.getUTCMinutes().toString().padStart(2, '0') +
now.getUTCSeconds().toString().padStart(2, '0') + 'Z';
const shortDate = now.getUTCFullYear().toString() +
(now.getUTCMonth() + 1).toString().padStart(2, '0') +
now.getUTCDate().toString().padStart(2, '0');
const region = this.config.region;
const service = 'dcdn';
// 构建请求体
const requestBody = {
TaskType: params.TaskType,
Urls: params.Urls
};
// 计算签名
const signature = this.calculateSignature(params, requestBody, timestamp, shortDate, region, service);
// 构建Authorization头
const authorization = `HMAC-SHA256 Credential=${this.config.accessKeyId}/${shortDate}/${region}/${service}/request, SignedHeaders=x-date, Signature=${signature}`;
return {
'Authorization': authorization,
'X-Date': timestamp,
'Content-Type': 'application/json'
};
}
/**
* 为GET请求构建请求头(包含签名)
*/
buildHeadersForGet(params) {
const now = new Date();
// 根据官方文档,时间格式应该是 YYYYMMDDTHHMMSSZ
const timestamp = now.getUTCFullYear().toString() +
(now.getUTCMonth() + 1).toString().padStart(2, '0') +
now.getUTCDate().toString().padStart(2, '0') + 'T' +
now.getUTCHours().toString().padStart(2, '0') +
now.getUTCMinutes().toString().padStart(2, '0') +
now.getUTCSeconds().toString().padStart(2, '0') + 'Z';
const shortDate = now.getUTCFullYear().toString() +
(now.getUTCMonth() + 1).toString().padStart(2, '0') +
now.getUTCDate().toString().padStart(2, '0');
const region = this.config.region;
const service = 'dcdn';
// 计算签名 - GET请求没有请求体
const signature = this.calculateSignatureForGet(params, timestamp, shortDate, region, service);
// 构建Authorization头
const authorization = `HMAC-SHA256 Credential=${this.config.accessKeyId}/${shortDate}/${region}/${service}/request, SignedHeaders=x-date, Signature=${signature}`;
console.log('Debug - GET X-Date:', timestamp);
return {
'Authorization': authorization,
'X-Date': timestamp
};
}
/**
* 计算签名 - 完全按照官方代码逻辑
*/
calculateSignature(params, requestBody, timestamp, shortDate, region, service) {
// 构建头部
const headers = {
'X-Date': timestamp
};
// 创建正规化请求
// 查询字符串只包含 Action 和 Version,其他参数在请求体中
const queryParams = {
Action: params.Action,
Version: params.Version
};
const [signedHeaders, canonicalHeaders] = this.getSignHeaders(headers, []);
const canonicalRequest = [
'POST',
'/',
this.queryParamsToString(queryParams) || '',
`${canonicalHeaders}\n`,
signedHeaders,
this.getBodySha(requestBody) || this.hash(''),
].join('\n');
const credentialScope = [shortDate, region, service, "request"].join('/');
// 创建签名字符串
const stringToSign = ["HMAC-SHA256", timestamp, credentialScope, this.hash(canonicalRequest)].join('\n');
// 计算签名 - 完全按照官方代码逻辑
const kDate = this.hmac(this.config.accessKeySecret, shortDate);
const kRegion = this.hmac(kDate, region);
const kService = this.hmac(kRegion, service);
const kSigning = this.hmac(kService, "request");
const signature = this.hmac(kSigning, stringToSign).toString('hex');
return signature;
}
/**
* 为GET请求计算签名
*/
calculateSignatureForGet(params, timestamp, shortDate, region, service) {
// 构建头部
const headers = {
'X-Date': timestamp
};
// 创建正规化请求 - GET请求没有请求体
const [signedHeaders, canonicalHeaders] = this.getSignHeaders(headers, []);
const canonicalRequest = [
'GET',
'/',
this.queryParamsToString(params) || '',
`${canonicalHeaders}\n`,
signedHeaders,
this.hash(''), // GET请求没有请求体,使用空字符串的哈希
].join('\n');
const credentialScope = [shortDate, region, service, "request"].join('/');
// 创建签名字符串
const stringToSign = ["HMAC-SHA256", timestamp, credentialScope, this.hash(canonicalRequest)].join('\n');
// 计算签名
const kDate = this.hmac(this.config.accessKeySecret, shortDate);
const kRegion = this.hmac(kDate, region);
const kService = this.hmac(kRegion, service);
const kSigning = this.hmac(kService, "request");
const signature = this.hmac(kSigning, stringToSign).toString('hex');
return signature;
}
/**
* HMAC-SHA256 计算 - 返回二进制数据
*/
hmac(secret, data) {
if (typeof secret === 'string') {
return crypto.createHmac('sha256', secret).update(data, 'utf8').digest();
}
else {
return crypto.createHmac('sha256', secret).update(data, 'utf8').digest();
}
}
/**
* SHA256 哈希计算
*/
hash(data) {
return crypto.createHash('sha256').update(data, 'utf8').digest('hex');
}
/**
* 获取请求体的 SHA256 哈希
*/
getBodySha(body) {
if (typeof body === 'string') {
return this.hash(body);
}
else {
return this.hash(JSON.stringify(body));
}
}
/**
* 构建查询参数字符串
*/
queryParamsToString(params) {
return Object.keys(params)
.sort()
.map((key) => {
const val = params[key];
if (typeof val === 'undefined' || val === null) {
return undefined;
}
const escapedKey = this.uriEscape(key);
if (!escapedKey) {
return undefined;
}
if (Array.isArray(val)) {
return `${escapedKey}=${val.map(this.uriEscape.bind(this)).sort().join(`&${escapedKey}=`)}`;
}
return `${escapedKey}=${this.uriEscape(val)}`;
})
.filter((v) => v)
.join('&');
}
/**
* 获取签名头部
*/
getSignHeaders(originHeaders, needSignHeaders) {
function trimHeaderValue(header) {
return header.toString?.().trim().replace(/\s+/g, ' ') ?? '';
}
let h = Object.keys(originHeaders);
// 根据 needSignHeaders 过滤
if (Array.isArray(needSignHeaders)) {
const needSignSet = new Set([...needSignHeaders, 'x-date'].map((k) => k.toLowerCase()));
h = h.filter((k) => needSignSet.has(k.toLowerCase()));
}
// 根据 ignore headers 过滤
h = h.filter((k) => !this.HEADER_KEYS_TO_IGNORE.has(k.toLowerCase()));
const signedHeaderKeys = h
.slice()
.map((k) => k.toLowerCase())
.sort()
.join(';');
const canonicalHeaders = h
.sort((a, b) => (a.toLowerCase() < b.toLowerCase() ? -1 : 1))
.map((k) => `${k.toLowerCase()}:${trimHeaderValue(originHeaders[k])}`)
.join('\n');
return [signedHeaderKeys, canonicalHeaders];
}
/**
* URI 转义
*/
uriEscape(str) {
try {
return encodeURIComponent(str)
.replace(/[^A-Za-z0-9_.~\-%]+/g, escape)
.replace(/[*]/g, (ch) => `%${ch.charCodeAt(0).toString(16).toUpperCase()}`);
}
catch (e) {
return '';
}
}
/**
* 批量预热CDN
*/
async preloadCDNBatch(urls) {
let successCount = 0;
let failedCount = 0;
const results = [];
for (const url of urls) {
try {
const result = await this.preloadCDN(url);
results.push(result);
if (result.success) {
successCount++;
}
else {
failedCount++;
}
}
catch (error) {
failedCount++;
results.push({
success: false,
url,
error: error instanceof Error ? error.message : 'Unknown error'
});
}
}
return { success: successCount, failed: failedCount, results };
}
/**
* 查询CDN任务配额
*/
async queryTaskQuota() {
try {
console.log('查询CDN任务配额...');
// 构建请求参数 - 使用正确的API
const params = {
Action: 'GetPurgePrefetchTaskQuota',
Version: '2021-04-01'
};
// 构建请求头(包含签名)- GET请求需要特殊处理
const headers = this.buildHeadersForGet(params);
// 发送请求
const response = await fetch(`${this.endpoint}/?Action=${params.Action}&Version=${params.Version}`, {
method: 'GET',
headers
});
const responseData = await response.json();
if (responseData?.Result) {
const quota = responseData.Result;
console.log(`配额信息: URL刷新配额${quota.UrlQuota}/剩余${quota.UrlRemain}, 目录刷新配额${quota.DirQuota}/剩余${quota.DirRemain}, URL预热配额${quota.PrefetchUrlQuota}/剩余${quota.PrefetchUrlRemain}`);
return quota;
}
else if (responseData?.ResponseMetadata?.Error) {
const error = responseData.ResponseMetadata.Error;
console.error(`查询任务配额失败: ${error.Code} - ${error.Message}`);
return null;
}
else {
console.warn('未找到配额信息');
return null;
}
}
catch (error) {
console.error('查询任务配额异常:', error.message);
return null;
}
}
/**
* 验证配置
*/
validateConfig() {
return !!(this.config.accessKeyId && this.config.accessKeySecret && this.config.region);
}
}
exports.TOSCDNService = TOSCDNService;
//# sourceMappingURL=TOSCDNService.js.map