UNPKG

@xuanqikai/one-click-upload

Version:

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

456 lines 15.9 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.CDNRefreshService = void 0; const crypto = __importStar(require("crypto")); /** * 阿里云 CDN 缓存刷新服务 * 基于阿里云 CDN API 实现缓存刷新功能 */ class CDNRefreshService { constructor(config) { this.config = config; // 默认使用阿里云 CDN API 的公共端点 this.endpoint = config.endpoint || 'https://cdn.aliyuncs.com'; } /** * 刷新单个 URL 的缓存 */ async refreshUrl(url) { try { const params = { Action: 'RefreshObjectCaches', Version: '2018-05-10', ObjectPath: url, ObjectType: 'File' }; const response = await this.makeRequest(params); if (response.RefreshTaskId) { return { success: true, taskId: response.RefreshTaskId, url: url }; } else { return { success: false, url: url, error: 'Failed to get refresh task ID' }; } } catch (error) { return { success: false, url: url, error: error.message || 'Unknown error' }; } } /** * 刷新目录的缓存 */ async refreshDirectory(dirUrl) { try { const params = { Action: 'RefreshObjectCaches', Version: '2018-05-10', ObjectPath: dirUrl, ObjectType: 'Directory' }; const response = await this.makeRequest(params); if (response.RefreshTaskId) { return { success: true, taskId: response.RefreshTaskId, url: dirUrl }; } else { return { success: false, url: dirUrl, error: 'Failed to get refresh task ID' }; } } catch (error) { return { success: false, url: dirUrl, error: error.message || 'Unknown error' }; } } /** * 预热单个 URL */ async preloadUrl(url, options) { try { const params = { Action: 'PushObjectCache', Version: '2018-05-10', ObjectPath: url }; // 添加可选参数 if (options?.area) { params.Area = options.area; } if (options?.l2Preload !== undefined) { params.L2Preload = options.l2Preload; } if (options?.withHeader) { params.WithHeader = JSON.stringify(options.withHeader); } if (options?.queryHashkey !== undefined) { params.QueryHashkey = options.queryHashkey; } if (options?.consistencyHash !== undefined) { params.ConsistencyHash = options.consistencyHash; } const response = await this.makeRequest(params); if (response.PushTaskId) { return { success: true, taskId: response.PushTaskId, url: url }; } else { return { success: false, url: url, error: 'Failed to get preload task ID' }; } } catch (error) { return { success: false, url: url, error: error.message || 'Unknown error' }; } } /** * 批量预热 URL */ async preloadUrls(urls, options) { const results = []; // 阿里云 CDN API 支持批量预热,最多 100 个 URL const batchSize = 100; for (let i = 0; i < urls.length; i += batchSize) { const batch = urls.slice(i, i + batchSize); try { const params = { Action: 'PushObjectCache', Version: '2018-05-10', ObjectPath: batch.join('\n') // 多个 URL 用换行符分隔 }; // 添加可选参数 if (options?.area) { params.Area = options.area; } if (options?.l2Preload !== undefined) { params.L2Preload = options.l2Preload; } if (options?.withHeader) { params.WithHeader = JSON.stringify(options.withHeader); } if (options?.queryHashkey !== undefined) { params.QueryHashkey = options.queryHashkey; } if (options?.consistencyHash !== undefined) { params.ConsistencyHash = options.consistencyHash; } const response = await this.makeRequest(params); if (response.PushTaskId) { // 批量预热返回一个任务 ID,所有 URL 共享 batch.forEach(url => { results.push({ success: true, taskId: response.PushTaskId, url: url }); }); } else { batch.forEach(url => { results.push({ success: false, url: url, error: 'Failed to get preload task ID' }); }); } } catch (error) { batch.forEach(url => { results.push({ success: false, url: url, error: error.message || 'Unknown error' }); }); } } return results; } /** * 批量刷新 URL 缓存 */ async refreshUrls(urls) { const results = []; // 阿里云 CDN API 支持批量刷新,最多 1000 个 URL const batchSize = 1000; for (let i = 0; i < urls.length; i += batchSize) { const batch = urls.slice(i, i + batchSize); try { const params = { Action: 'RefreshObjectCaches', Version: '2018-05-10', ObjectPath: batch.join('\n'), // 多个 URL 用换行符分隔 ObjectType: 'File' }; const response = await this.makeRequest(params); if (response.RefreshTaskId) { // 批量刷新返回一个任务 ID,所有 URL 共享 batch.forEach(url => { results.push({ success: true, taskId: response.RefreshTaskId, url: url }); }); } else { batch.forEach(url => { results.push({ success: false, url: url, error: 'Failed to get refresh task ID' }); }); } } catch (error) { batch.forEach(url => { results.push({ success: false, url: url, error: error.message || 'Unknown error' }); }); } } return results; } /** * 查询刷新任务状态 */ async getRefreshTaskStatus(taskId) { try { const params = { Action: 'DescribeRefreshTaskById', Version: '2018-05-10', TaskId: taskId }; const response = await this.makeRequest(params); if (response.Tasks && response.Tasks.length > 0) { const task = response.Tasks[0]; return { taskId: task.TaskId, url: task.ObjectPath, status: task.Status, progress: task.Progress, createTime: task.CreationTime, finishTime: task.FinishTime }; } return null; } catch (error) { console.error('Failed to get refresh task status:', error.message); return null; } } /** * 查询刷新配额 */ async getRefreshQuota() { try { const params = { Action: 'DescribeRefreshQuota', Version: '2018-05-10' }; return await this.makeRequest(params); } catch (error) { console.error('Failed to get refresh quota:', error.message); throw error; } } /** * 构建完整的 CDN URL */ buildCDNUrl(remotePath) { if (!this.config.cdnDomain) { throw new Error('CDN domain not configured'); } // 确保 CDN 域名格式正确 let domain = this.config.cdnDomain; if (!domain.startsWith('http://') && !domain.startsWith('https://')) { domain = `https://${domain}`; } // 确保路径格式正确 let path = remotePath; if (!path.startsWith('/')) { path = `/${path}`; } return `${domain}${path}`; } /** * 发送 API 请求 */ async makeRequest(params) { const timestamp = new Date().toISOString(); const nonce = Math.random().toString(36).substring(2, 15); // 构建签名参数 const signatureParams = { ...params, AccessKeyId: this.config.accessKeyId, SignatureMethod: 'HMAC-SHA1', SignatureVersion: '1.0', SignatureNonce: nonce, Timestamp: timestamp, Format: 'JSON' }; // 生成签名 const signature = this.generateSignature(signatureParams); signatureParams.Signature = signature; // 构建请求 URL const queryString = Object.keys(signatureParams) .sort() .map(key => `${encodeURIComponent(key)}=${encodeURIComponent(signatureParams[key])}`) .join('&'); const url = `${this.endpoint}/?${queryString}`; // 发送请求 const response = await fetch(url, { method: 'GET', headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }); if (!response.ok) { // 尝试解析错误响应 let errorMessage = `HTTP ${response.status}: ${response.statusText}`; try { const errorData = await response.json(); if (errorData.Message) { errorMessage = errorData.Message; } else if (errorData.message) { errorMessage = errorData.message; } } catch (e) { // 如果无法解析 JSON,使用默认错误信息 } // 根据状态码提供更详细的错误信息 if (response.status === 403) { errorMessage += '\n\n可能的原因:\n' + '1. CDN AccessKey 权限不足,请确保 AccessKey 具有 CDN 刷新权限\n' + '2. CDN API 功能未开通,请在阿里云 CDN 控制台开通 CDN API 功能\n' + '3. AccessKey 配置错误,请检查 CDN AccessKey ID 和 Secret 是否正确\n' + '4. 域名未绑定或未授权,请确保 CDN 域名已正确配置'; } else if (response.status === 401) { errorMessage += '\n\n可能的原因:\n' + '1. AccessKey ID 或 Secret 错误\n' + '2. 签名计算错误,请检查系统时间是否正确'; } throw new Error(errorMessage); } const result = await response.json(); if (result.Code && result.Code !== '200') { let errorMessage = result.Message || 'API request failed'; // 提供常见错误代码的说明 if (result.Code === 'InvalidAccessKeyId.NotFound') { errorMessage += '\n\nAccessKey ID 不存在,请检查配置是否正确'; } else if (result.Code === 'SignatureDoesNotMatch') { errorMessage += '\n\n签名验证失败,请检查 AccessKey Secret 是否正确'; } else if (result.Code === 'Forbidden.RAM') { errorMessage += '\n\nRAM 权限不足,请确保 AccessKey 具有 CDN 刷新权限'; } throw new Error(errorMessage); } return result; } /** * 生成 API 签名 */ generateSignature(params) { // 构建待签名字符串 const sortedParams = Object.keys(params) .sort() .map(key => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`) .join('&'); const stringToSign = `GET&${encodeURIComponent('/')}&${encodeURIComponent(sortedParams)}`; // 使用 HMAC-SHA1 生成签名 const signature = crypto .createHmac('sha1', `${this.config.accessKeySecret}&`) .update(stringToSign) .digest('base64'); return signature; } /** * 验证配置 */ async validateConfig() { try { await this.getRefreshQuota(); return true; } catch (error) { console.error('CDN config validation failed:', error.message); return false; } } } exports.CDNRefreshService = CDNRefreshService; //# sourceMappingURL=CDNRefreshService.js.map