UNPKG

t-comm

Version:

专业、稳定、纯粹的工具库

218 lines (192 loc) 6.93 kB
/** * 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, };