Files
qiniu-feishu-bot/src/qiniu-uploader.js
饭团 b00567762f Initial commit: 七牛云上传飞书机器人
功能:
- 飞书交互卡片支持
- 七牛云文件上传
- 自动 CDN 刷新
- 多存储桶配置
- 跨平台部署(Linux/macOS/Windows)
- Docker 支持
2026-03-05 14:22:26 +08:00

254 lines
7.1 KiB
JavaScript

/**
* 七牛云上传工具
*/
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const https = require('https');
const http = require('http');
class QiniuUploader {
constructor() {
this.configPath = process.env.QINIU_CONFIG_PATH ||
path.join(process.cwd(), 'config', 'qiniu-config.json');
this.config = this.loadConfig();
}
// 加载配置
loadConfig() {
if (!fs.existsSync(this.configPath)) {
// 创建默认配置
const defaultConfig = {
buckets: {
default: {
accessKey: process.env.QINIU_ACCESS_KEY || 'YOUR_ACCESS_KEY',
secretKey: process.env.QINIU_SECRET_KEY || 'YOUR_SECRET_KEY',
bucket: process.env.QINIU_BUCKET || 'your-bucket',
region: process.env.QINIU_REGION || 'z0',
domain: process.env.QINIU_DOMAIN || 'https://your-cdn.com'
}
}
};
const dir = path.dirname(this.configPath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
fs.writeFileSync(this.configPath, JSON.stringify(defaultConfig, null, 2));
return defaultConfig;
}
return JSON.parse(fs.readFileSync(this.configPath, 'utf-8'));
}
// 列出配置
async listConfig() {
const config = this.loadConfig();
const buckets = {};
for (const [name, bucket] of Object.entries(config.buckets)) {
buckets[name] = {
accessKey: this.maskKey(bucket.accessKey),
secretKey: this.maskKey(bucket.secretKey),
bucket: bucket.bucket,
region: bucket.region,
domain: bucket.domain
};
}
return { buckets };
}
// 设置配置值
async setConfigValue(keyPath, value) {
const config = this.loadConfig();
const keys = keyPath.split('.');
let current = config;
for (let i = 0; i < keys.length - 1; i++) {
if (!current[keys[i]]) {
current[keys[i]] = {};
}
current = current[keys[i]];
}
const lastKey = keys[keys.length - 1];
// 类型转换
if (value === 'true') value = true;
else if (value === 'false') value = false;
else if (!isNaN(value) && !value.includes('.')) value = Number(value);
current[lastKey] = value;
fs.writeFileSync(this.configPath, JSON.stringify(config, null, 2));
this.config = config;
}
// 上传文件
async upload(localFile, key, bucketName = 'default') {
const bucketConfig = this.config.buckets[bucketName];
if (!bucketConfig) {
throw new Error(`存储桶 "${bucketName}" 不存在`);
}
const { accessKey, secretKey, bucket, region, domain } = bucketConfig;
// 生成上传凭证
const uploadToken = this.generateUploadToken(accessKey, secretKey, bucket, key);
// 获取上传端点
const uploadEndpoint = this.getUploadEndpoint(region);
// 读取文件
const fileContent = fs.readFileSync(localFile);
const fileName = path.basename(localFile);
// 构建 multipart/form-data
const boundary = '----WebKitFormBoundary' + Math.random().toString(36).substring(2);
const bodyParts = [
`------${boundary}`,
'Content-Disposition: form-data; name="token"',
'',
uploadToken,
`------${boundary}`,
'Content-Disposition: form-data; name="key"',
'',
key,
`------${boundary}`,
`Content-Disposition: form-data; name="file"; filename="${fileName}"`,
'Content-Type: application/octet-stream',
'',
''
];
const bodyBuffer = Buffer.concat([
Buffer.from(bodyParts.join('\r\n'), 'utf-8'),
fileContent,
Buffer.from(`\r\n------${boundary}--\r\n`, 'utf-8')
]);
// 上传
const result = await this.httpRequest(`${uploadEndpoint}/`, {
method: 'POST',
headers: {
'Content-Type': `multipart/form-data; boundary=----${boundary}`,
'Content-Length': bodyBuffer.length
}
}, bodyBuffer);
if (result.status !== 200) {
throw new Error(`上传失败:${JSON.stringify(result.data)}`);
}
return {
key: result.data.key,
hash: result.data.hash,
url: `${domain}/${key}`
};
}
// 刷新 CDN
async refreshCDN(bucketName, key) {
const bucketConfig = this.config.buckets[bucketName];
if (!bucketConfig) return;
const { accessKey, secretKey, domain } = bucketConfig;
const fileUrl = `${domain}/${key}`;
const body = JSON.stringify({ urls: [fileUrl] });
const accessToken = this.generateAccessToken(accessKey, secretKey, 'POST', '/v2/tune/refresh', body);
await this.httpRequest('https://fusion.qiniuapi.com/v2/tune/refresh', {
method: 'POST',
headers: {
'Host': 'fusion.qiniuapi.com',
'Content-Type': 'application/json',
'Content-Length': body.length,
'Authorization': accessToken
}
}, body);
}
// 生成上传凭证
generateUploadToken(accessKey, secretKey, bucket, key) {
const deadline = Math.floor(Date.now() / 1000) + 3600;
const scope = key ? `${bucket}:${key}` : bucket;
const putPolicy = {
scope: scope,
deadline: deadline
};
const encodedPolicy = this.urlSafeBase64(JSON.stringify(putPolicy));
const encodedSignature = this.urlSafeBase64(this.hmacSha1(encodedPolicy, secretKey));
return `${accessKey}:${encodedSignature}:${encodedPolicy}`;
}
// 生成 CDN 刷新令牌
generateAccessToken(accessKey, secretKey, method, path, body) {
const signData = `${method} ${path}\nHost: fusion.qiniuapi.com\nContent-Type: application/json\n\n${body}`;
const signature = this.hmacSha1(signData, secretKey);
const encodedSign = this.urlSafeBase64(signature);
return `Qiniu ${accessKey}:${encodedSign}`;
}
// 获取上传端点
getUploadEndpoint(region) {
const endpoints = {
'z0': 'https://up.qiniup.com',
'z1': 'https://up-z1.qiniup.com',
'z2': 'https://up-z2.qiniup.com',
'na0': 'https://up-na0.qiniup.com',
'as0': 'https://up-as0.qiniup.com'
};
return endpoints[region] || endpoints['z0'];
}
// 工具函数
hmacSha1(data, secret) {
return crypto.createHmac('sha1', secret).update(data).digest();
}
urlSafeBase64(data) {
return Buffer.from(data).toString('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_');
}
maskKey(key) {
if (!key || key.length < 8) return '***';
return key.substring(0, 4) + '...' + key.substring(key.length - 4);
}
httpRequest(url, options, body = null) {
return new Promise((resolve, reject) => {
const protocol = url.startsWith('https') ? https : http;
const req = protocol.request(url, options, (res) => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => {
try {
const json = JSON.parse(data);
resolve({ status: res.statusCode, data: json });
} catch (e) {
resolve({ status: res.statusCode, data: data });
}
});
});
req.on('error', reject);
if (body) req.write(body);
req.end();
});
}
}
module.exports = { QiniuUploader };