2026-03-27 17:42:41 +08:00
|
|
|
|
#!/usr/bin/env node
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 腾讯云 COS Node.js SDK 操作脚本
|
|
|
|
|
|
* 作为 cos-mcp MCP 工具不可用时的降级方案
|
|
|
|
|
|
*
|
|
|
|
|
|
* 依赖:npm install cos-nodejs-sdk-v5
|
|
|
|
|
|
* 凭证通过环境变量读取:
|
|
|
|
|
|
* TENCENT_COS_SECRET_ID / TENCENT_COS_SECRET_KEY / TENCENT_COS_REGION / TENCENT_COS_BUCKET
|
|
|
|
|
|
*
|
|
|
|
|
|
* 用法:node cos_node.mjs <action> [options]
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
import { createRequire } from 'module';
|
|
|
|
|
|
import { createReadStream, createWriteStream, existsSync } from 'fs';
|
|
|
|
|
|
import { basename, resolve } from 'path';
|
|
|
|
|
|
import { pipeline } from 'stream/promises';
|
|
|
|
|
|
|
|
|
|
|
|
const require = createRequire(import.meta.url);
|
|
|
|
|
|
const COS = require('cos-nodejs-sdk-v5');
|
|
|
|
|
|
|
|
|
|
|
|
// 读取环境变量
|
|
|
|
|
|
const SecretId = process.env.TENCENT_COS_SECRET_ID;
|
|
|
|
|
|
const SecretKey = process.env.TENCENT_COS_SECRET_KEY;
|
|
|
|
|
|
const Region = process.env.TENCENT_COS_REGION;
|
|
|
|
|
|
const Bucket = process.env.TENCENT_COS_BUCKET;
|
|
|
|
|
|
|
|
|
|
|
|
// 可选的自定义域名配置
|
|
|
|
|
|
const Domain = process.env.TENCENT_COS_DOMAIN;
|
|
|
|
|
|
const ServiceDomain = process.env.TENCENT_COS_SERVICE_DOMAIN;
|
|
|
|
|
|
const Protocol = process.env.TENCENT_COS_PROTOCOL;
|
|
|
|
|
|
|
|
|
|
|
|
if (!SecretId || !SecretKey || !Region || !Bucket) {
|
|
|
|
|
|
console.error(JSON.stringify({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
error: '缺少环境变量,需要:TENCENT_COS_SECRET_ID, TENCENT_COS_SECRET_KEY, TENCENT_COS_REGION, TENCENT_COS_BUCKET',
|
|
|
|
|
|
}));
|
|
|
|
|
|
process.exit(1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const cosOptions = { SecretId, SecretKey };
|
|
|
|
|
|
|
|
|
|
|
|
if (Domain) {
|
|
|
|
|
|
cosOptions.Domain = Domain;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (ServiceDomain) {
|
|
|
|
|
|
cosOptions.ServiceDomain = ServiceDomain;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (Protocol) {
|
|
|
|
|
|
cosOptions.Protocol = Protocol;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const cos = new COS(cosOptions);
|
|
|
|
|
|
|
|
|
|
|
|
// 解析命令行参数
|
|
|
|
|
|
function parseArgs(args) {
|
|
|
|
|
|
const result = {};
|
|
|
|
|
|
for (let i = 0; i < args.length; i++) {
|
|
|
|
|
|
const arg = args[i];
|
|
|
|
|
|
if (arg.startsWith('--')) {
|
|
|
|
|
|
const key = arg.slice(2);
|
|
|
|
|
|
const next = args[i + 1];
|
|
|
|
|
|
if (next && !next.startsWith('--')) {
|
|
|
|
|
|
result[key] = next;
|
|
|
|
|
|
i++;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
result[key] = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return result;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 输出 JSON 结果
|
|
|
|
|
|
function output(data) {
|
|
|
|
|
|
console.log(JSON.stringify(data, null, 2));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 封装 COS SDK 回调为 Promise
|
|
|
|
|
|
function cosPromise(method, params) {
|
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
|
|
cos[method]({ Bucket, Region, ...params }, (err, data) => {
|
|
|
|
|
|
if (err) {
|
|
|
|
|
|
reject(err);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
resolve(data);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ========== 操作实现 ==========
|
|
|
|
|
|
|
|
|
|
|
|
async function upload(opts) {
|
|
|
|
|
|
const filePath = opts.file;
|
|
|
|
|
|
const key = opts.key || basename(filePath);
|
|
|
|
|
|
|
|
|
|
|
|
if (!filePath) {
|
|
|
|
|
|
throw new Error('缺少 --file 参数');
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!existsSync(filePath)) {
|
|
|
|
|
|
throw new Error(`文件不存在:${filePath}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const data = await cosPromise('putObject', {
|
|
|
|
|
|
Key: key,
|
|
|
|
|
|
Body: createReadStream(filePath),
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
output({
|
|
|
|
|
|
success: true,
|
|
|
|
|
|
action: 'upload',
|
|
|
|
|
|
key,
|
|
|
|
|
|
etag: data.ETag,
|
|
|
|
|
|
location: data.Location,
|
|
|
|
|
|
statusCode: data.statusCode,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function putString(opts) {
|
|
|
|
|
|
const content = opts.content;
|
|
|
|
|
|
const key = opts.key;
|
|
|
|
|
|
const contentType = opts['content-type'] || 'text/plain';
|
|
|
|
|
|
|
|
|
|
|
|
if (!content) {
|
|
|
|
|
|
throw new Error('缺少 --content 参数');
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!key) {
|
|
|
|
|
|
throw new Error('缺少 --key 参数');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const data = await cosPromise('putObject', {
|
|
|
|
|
|
Key: key,
|
|
|
|
|
|
Body: content,
|
|
|
|
|
|
ContentType: contentType,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
output({
|
|
|
|
|
|
success: true,
|
|
|
|
|
|
action: 'put-string',
|
|
|
|
|
|
key,
|
|
|
|
|
|
etag: data.ETag,
|
|
|
|
|
|
location: data.Location,
|
|
|
|
|
|
statusCode: data.statusCode,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function download(opts) {
|
|
|
|
|
|
const key = opts.key;
|
|
|
|
|
|
const outputPath = opts.output || basename(key);
|
|
|
|
|
|
|
|
|
|
|
|
if (!key) {
|
|
|
|
|
|
throw new Error('缺少 --key 参数');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const data = await cosPromise('getObject', {
|
|
|
|
|
|
Key: key,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const resolvedPath = resolve(outputPath);
|
|
|
|
|
|
const ws = createWriteStream(resolvedPath);
|
|
|
|
|
|
|
|
|
|
|
|
if (data.Body instanceof Buffer) {
|
|
|
|
|
|
ws.write(data.Body);
|
|
|
|
|
|
ws.end();
|
|
|
|
|
|
} else if (data.Body && typeof data.Body.pipe === 'function') {
|
|
|
|
|
|
await pipeline(data.Body, ws);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ws.write(String(data.Body));
|
|
|
|
|
|
ws.end();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
output({
|
|
|
|
|
|
success: true,
|
|
|
|
|
|
action: 'download',
|
|
|
|
|
|
key,
|
|
|
|
|
|
savedTo: resolvedPath,
|
|
|
|
|
|
contentLength: data.headers?.['content-length'],
|
|
|
|
|
|
statusCode: data.statusCode,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function list(opts) {
|
|
|
|
|
|
const prefix = opts.prefix || '';
|
|
|
|
|
|
const maxKeys = parseInt(opts['max-keys'], 10) || 100;
|
|
|
|
|
|
|
|
|
|
|
|
const data = await cosPromise('getBucket', {
|
|
|
|
|
|
Prefix: prefix,
|
|
|
|
|
|
MaxKeys: maxKeys,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const files = (data.Contents || []).map(item => ({
|
|
|
|
|
|
key: item.Key,
|
|
|
|
|
|
size: parseInt(item.Size, 10),
|
|
|
|
|
|
lastModified: item.LastModified,
|
|
|
|
|
|
etag: item.ETag,
|
|
|
|
|
|
storageClass: item.StorageClass,
|
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
|
|
output({
|
|
|
|
|
|
success: true,
|
|
|
|
|
|
action: 'list',
|
|
|
|
|
|
prefix,
|
|
|
|
|
|
count: files.length,
|
|
|
|
|
|
isTruncated: data.IsTruncated === 'true',
|
|
|
|
|
|
files,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function signUrl(opts) {
|
|
|
|
|
|
const key = opts.key;
|
|
|
|
|
|
const expires = parseInt(opts.expires, 10) || 3600;
|
|
|
|
|
|
|
|
|
|
|
|
if (!key) {
|
|
|
|
|
|
throw new Error('缺少 --key 参数');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const url = await new Promise((resolve, reject) => {
|
|
|
|
|
|
cos.getObjectUrl({
|
|
|
|
|
|
Bucket,
|
|
|
|
|
|
Region,
|
|
|
|
|
|
Key: key,
|
|
|
|
|
|
Expires: expires,
|
|
|
|
|
|
Sign: true,
|
|
|
|
|
|
}, (err, data) => {
|
|
|
|
|
|
if (err) {
|
|
|
|
|
|
reject(err);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
resolve(data.Url);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
output({
|
|
|
|
|
|
success: true,
|
|
|
|
|
|
action: 'sign-url',
|
|
|
|
|
|
key,
|
|
|
|
|
|
expires,
|
|
|
|
|
|
url,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function deleteObject(opts) {
|
|
|
|
|
|
const key = opts.key;
|
|
|
|
|
|
|
|
|
|
|
|
if (!key) {
|
|
|
|
|
|
throw new Error('缺少 --key 参数');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const data = await cosPromise('deleteObject', {
|
|
|
|
|
|
Key: key,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
output({
|
|
|
|
|
|
success: true,
|
|
|
|
|
|
action: 'delete',
|
|
|
|
|
|
key,
|
|
|
|
|
|
statusCode: data.statusCode,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function head(opts) {
|
|
|
|
|
|
const key = opts.key;
|
|
|
|
|
|
|
|
|
|
|
|
if (!key) {
|
|
|
|
|
|
throw new Error('缺少 --key 参数');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const data = await cosPromise('headObject', {
|
|
|
|
|
|
Key: key,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
output({
|
|
|
|
|
|
success: true,
|
|
|
|
|
|
action: 'head',
|
|
|
|
|
|
key,
|
|
|
|
|
|
contentLength: parseInt(data.headers?.['content-length'], 10),
|
|
|
|
|
|
contentType: data.headers?.['content-type'],
|
|
|
|
|
|
etag: data.headers?.etag,
|
|
|
|
|
|
lastModified: data.headers?.['last-modified'],
|
|
|
|
|
|
storageClass: data.headers?.['x-cos-storage-class'] || 'STANDARD',
|
|
|
|
|
|
statusCode: data.statusCode,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ========== 主入口 ==========
|
|
|
|
|
|
|
|
|
|
|
|
const args = process.argv.slice(2);
|
|
|
|
|
|
const action = args[0];
|
|
|
|
|
|
const opts = parseArgs(args.slice(1));
|
|
|
|
|
|
|
|
|
|
|
|
const actions = {
|
|
|
|
|
|
upload,
|
|
|
|
|
|
'put-string': putString,
|
|
|
|
|
|
download,
|
|
|
|
|
|
list,
|
|
|
|
|
|
'sign-url': signUrl,
|
|
|
|
|
|
delete: deleteObject,
|
|
|
|
|
|
head,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
if (!action || !actions[action]) {
|
|
|
|
|
|
output({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
error: `未知操作:${action || '(空)'}`,
|
|
|
|
|
|
availableActions: Object.keys(actions),
|
|
|
|
|
|
usage: 'node cos_node.mjs <action> [--option value ...]',
|
|
|
|
|
|
});
|
|
|
|
|
|
process.exit(1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
await actions[action](opts);
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
output({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
action,
|
|
|
|
|
|
error: err.message || String(err),
|
|
|
|
|
|
code: err.code,
|
|
|
|
|
|
});
|
|
|
|
|
|
process.exit(1);
|
|
|
|
|
|
}
|