t-comm
Version:
专业、稳定、纯粹的工具库
218 lines (192 loc) • 6.93 kB
JavaScript
/**
* upload-cdn CLI 核心:将本地文件/URL 上传到研发平台 CDN
*
* 使用方式:
* t-comm upload-cdn ./a.png --rid xxx --rtoken yyy
* t-comm upload-cdn --url "https://..." --rid xxx --rtoken yyy
*
* 认证优先级:CLI 参数 --rid/--rtoken > 环境变量 PMD_RID/PMD_RTOKEN
*/
const crypto = require('crypto');
const fs = require('fs');
const os = require('os');
const path = require('path');
const axios = require('axios');
const FormData = require('form-data');
// ==================== 常量 ====================
const DEFAULT_API_URL = 'http://mobile.woa.com/rd-platform-cgi/open-api/image/mcp-upload';
const DEFAULT_SITE_NAME = 'cli-upload';
const DEFAULT_TOOL_NAME = 'upload-cdn-cli';
const SUPPORTED_IMAGE_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg'];
// ==================== 工具函数 ====================
const EXT_TO_MIME = {
'.svg': 'image/svg+xml',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.gif': 'image/gif',
'.webp': 'image/webp',
'.bmp': 'image/bmp',
'.ico': 'image/x-icon',
'.pdf': 'application/pdf',
'.json': 'application/json',
'.mp4': 'video/mp4',
};
function detectMime(filePath) {
const ext = path.extname(filePath).toLowerCase();
return EXT_TO_MIME[ext] || 'application/octet-stream';
}
function randomString(length) {
return crypto.randomBytes(Math.ceil(length / 2))
.toString('hex')
.slice(0, length);
}
function newFileName(originName, siteName = DEFAULT_SITE_NAME) {
const idx = originName.lastIndexOf('.');
const ext = idx < 0 ? '.png' : originName.substring(idx);
return `own_${siteName}_${randomString(16)}${ext}`;
}
function isImage(filePath) {
const ext = path.extname(filePath).toLowerCase();
return SUPPORTED_IMAGE_EXTENSIONS.includes(ext);
}
function getFileNameFromUrl(url, contentType) {
try {
const urlObj = new URL(url);
const baseName = path.basename(urlObj.pathname);
if (baseName && path.extname(baseName)) return baseName;
} catch { /* ignore */ }
const extMap = {
'image/jpeg': '.jpg', 'image/png': '.png', 'image/gif': '.gif',
'image/webp': '.webp', 'image/bmp': '.bmp',
'application/pdf': '.pdf', 'application/zip': '.zip', 'text/plain': '.txt',
};
const ext = contentType ? (extMap[contentType] || '.bin') : '.bin';
return `download_${randomString(8)}${ext}`;
}
function resolveCredentials(options) {
// 优先级:CLI 参数 > PMD_RID/PMD_RTOKEN
const rid = options.rid || process.env.PMD_RID || '';
const rtoken = options.rtoken || process.env.PMD_RTOKEN || '';
const xUsername = options.xUsername || process.env.T_COMM_X_USERNAME || '';
if (!rid || !rtoken) {
console.error('❌ 缺少认证凭证。请通过以下任一方式提供:\n'
+ ' 1. 命令行参数:--rid <rid> --rtoken <rtoken>\n'
+ ' 2. 环境变量:export PMD_RID=<rid> && export PMD_RTOKEN=<rtoken>\n'
+ ' 3. 在 ~/.zshrc 中配置上述环境变量');
process.exit(1);
}
return { rid, rtoken, xUsername };
}
// ==================== 上传核心 ====================
/**
* 上传本地文件到 CDN
*/
async function uploadFile(filePath, options) {
const { rid, rtoken, xUsername } = resolveCredentials(options);
const apiUrl = options.apiUrl || DEFAULT_API_URL;
const dir = (options.dir || '').replace(/^\/|\/$/g, '');
if (!fs.existsSync(filePath)) {
console.error(`❌ 文件不存在: ${filePath}`);
process.exit(1);
}
const originName = path.basename(filePath);
const formData = new FormData();
formData.append('file', fs.createReadStream(filePath), { contentType: detectMime(filePath) });
formData.append('useSlice', '1');
formData.append('cdn', 'image-1251917893.file.myqcloud.com');
formData.append('qiniu_key', newFileName(originName, options.siteName || DEFAULT_SITE_NAME));
formData.append('origin_name', originName);
formData.append('dir', dir);
formData.append('mcpName', 'upload-cdn-cli');
formData.append('mcpVersion', '1.0.0');
const ext = path.extname(filePath).toLowerCase();
if (isImage(filePath) && ext !== '.svg') {
formData.append('useTinypng', '1');
} else {
formData.append('uploadFile', '1');
// 显式传 content_type,确保后端在 COS 上设置正确的 MIME(如 image/svg+xml)
// 否则 uploadFile 模式后端默认存为 application/octet-stream
formData.append('content_type', detectMime(filePath));
}
try {
const resp = await axios.post(apiUrl, formData, {
headers: {
...formData.getHeaders(),
rid,
rtoken,
...(xUsername ? { 'x-username': xUsername } : {}),
'User-Agent': 'upload-cdn-cli/1.0.0',
'x-mcp-tool-name': DEFAULT_TOOL_NAME,
},
maxContentLength: Infinity,
maxBodyLength: Infinity,
});
return resp.data;
} catch (err) {
const msg = err.response?.data || err.message;
console.error('❌ 上传失败:', JSON.stringify(msg, null, 2));
process.exit(1);
}
}
/**
* 从 URL 下载文件 → 上传到 CDN → 清理临时文件
*/
async function uploadFromUrl(url, options) {
let tempFilePath;
try {
// 下载
const resp = await axios.get(url, {
responseType: 'arraybuffer',
maxContentLength: Infinity,
maxBodyLength: Infinity,
timeout: 60000,
});
const contentType = resp.headers['content-type'];
const fileName = getFileNameFromUrl(url, contentType);
tempFilePath = path.join(os.tmpdir(), `upload_cdn_${Date.now()}_${fileName}`);
fs.writeFileSync(tempFilePath, resp.data);
console.error(`📥 下载完成 → ${tempFilePath}`);
// 上传
const result = await uploadFile(tempFilePath, options);
return result;
} catch (err) {
if (err.response?.data) {
console.error('❌ 下载或上传失败:', JSON.stringify(err.response.data, null, 2));
} else {
console.error('❌ 下载或上传失败:', err.message);
}
process.exit(1);
} finally {
if (tempFilePath && fs.existsSync(tempFilePath)) {
try {
fs.unlinkSync(tempFilePath);
} catch { /* ignore */ }
}
}
}
// ==================== 输出 ====================
function printResult(result, source) {
if (result.r === 0 || result.code === 0) {
const cdnUrl = result.cdnUrl || result.url || '';
if (process.env.T_COMM_JSON_OUTPUT) {
console.log(JSON.stringify({ ok: true, cdnUrl }));
} else {
console.log(`✅ 上传成功\n📄 源:${source}\n🌐 CDN:${cdnUrl}`);
}
} else {
const msg = result.msg || '未知错误';
if (process.env.T_COMM_JSON_OUTPUT) {
console.log(JSON.stringify({ ok: false, error: msg }));
} else {
console.error(`❌ 上传失败: ${msg}`);
}
process.exit(1);
}
}
// ==================== 入口 ====================
module.exports = {
uploadFile,
uploadFromUrl,
printResult,
};