UNPKG

@xuanqikai/one-click-upload

Version:

A CLI tool for one-click file upload to cloud storage services (OSS, TOS)

546 lines 21.2 kB
"use strict"; 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