@xuanqikai/one-click-upload
Version:
A CLI tool for one-click file upload to cloud storage services (OSS, TOS)
456 lines • 15.9 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.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