Files
2026-03-31 21:04:10 +08:00

162 lines
4.6 KiB
JavaScript

#!/usr/bin/env node
'use strict';
const crypto = require('node:crypto');
const fs = require('node:fs');
const https = require('node:https');
// --- Argument parsing ---
function parseArgs(argv) {
const args = {};
for (let i = 2; i < argv.length; i += 2) {
const key = argv[i].replace(/^--/, '');
const val = argv[i + 1];
if (!val || val.startsWith('--')) {
console.error(`Missing value for --${key}`);
process.exit(1);
}
args[key] = val;
}
return args;
}
const REQUIRED = ['file', 'secret-id', 'secret-key', 'token', 'bucket', 'region', 'cos-key'];
// --- Crypto helpers ---
function hmacSha1(key, data) {
return crypto.createHmac('sha1', key).update(data).digest('hex');
}
function sha1(data) {
return crypto.createHash('sha1').update(data).digest('hex');
}
// --- COS Authorization header (PUT Object) ---
// Reference: https://cloud.tencent.com/document/product/436/7778
function buildAuthorization({ secretId, secretKey, method, pathname, headers, startTime, expiredTime }) {
const keyTime = `${startTime};${expiredTime}`;
// 1. SignKey = HMAC-SHA1(SecretKey, KeyTime)
const signKey = hmacSha1(secretKey, keyTime);
// 2. HttpString = method\npathname\nparams\nheaders\n
// For PUT, no query params; headers we sign: host, content-length
const headerKeys = Object.keys(headers).sort();
const httpHeaders = headerKeys.map((k) => `${k.toLowerCase()}=${encodeURIComponent(headers[k])}`).join('&');
const httpString = `${method.toLowerCase()}\n${pathname}\n\n${httpHeaders}\n`;
// 3. StringToSign = sha1\nKeyTime\nSHA1(HttpString)\n
const stringToSign = `sha1\n${keyTime}\n${sha1(httpString)}\n`;
// 4. Signature = HMAC-SHA1(SignKey, StringToSign)
const signature = hmacSha1(signKey, stringToSign);
// 5. Build Authorization
const headerList = headerKeys.map((k) => k.toLowerCase()).join(';');
return [
`q-sign-algorithm=sha1`,
`q-ak=${secretId}`,
`q-sign-time=${keyTime}`,
`q-key-time=${keyTime}`,
`q-header-list=${headerList}`,
`q-url-param-list=`,
`q-signature=${signature}`,
].join('&');
}
// --- Upload via PUT Object ---
function upload(args) {
const secretId = args['secret-id'];
const secretKey = args['secret-key'];
const { token } = args;
const { bucket } = args;
const { region } = args;
const cosKey = args['cos-key'];
const filePath = args.file;
const startTime = args['start-time'] || String(Math.floor(Date.now() / 1000));
const expiredTime = args['expired-time'] || String(Math.floor(Date.now() / 1000) + 3600);
const fileContent = fs.readFileSync(filePath);
const hostname = `${bucket}.cos.${region}.myqcloud.com`;
const pathname = `/${cosKey}`;
// Headers to sign
const signHeaders = {
'content-length': String(fileContent.length),
host: hostname,
};
const authorization = buildAuthorization({
secretId,
secretKey,
method: 'PUT',
pathname,
headers: signHeaders,
startTime,
expiredTime,
});
// Use the actual file content type if provided, otherwise fall back to octet-stream
const contentType = args['content-type'] || 'application/octet-stream';
const options = {
hostname,
port: 443,
path: pathname,
method: 'PUT',
headers: {
'Content-Type': contentType,
'Content-Length': fileContent.length,
Authorization: authorization,
'x-cos-security-token': token,
},
};
const req = https.request(options, (res) => {
let body = '';
res.on('data', (chunk) => (body += chunk));
res.on('end', () => {
if (res.statusCode >= 200 && res.statusCode < 300) {
console.log(`Upload successful (HTTP ${res.statusCode})`);
process.exit(0);
} else {
console.error(`COS upload failed (HTTP ${res.statusCode}): ${body}`);
process.exit(1);
}
});
});
req.on('error', (err) => {
console.error(`COS upload error: ${err.message}`);
process.exit(1);
});
req.write(fileContent);
req.end();
}
// --- Main ---
function main() {
const args = parseArgs(process.argv);
const missing = REQUIRED.filter((k) => !args[k]);
if (missing.length) {
console.error(`Missing required arguments: ${missing.map((k) => `--${k}`).join(', ')}`);
console.error(
`Usage: node cos-upload.cjs --file <path> --secret-id <sid> --secret-key <skey> --token <token> --bucket <bucket> --region <region> --cos-key <key> [--content-type <mime>] [--start-time <ts>] [--expired-time <ts>]`,
);
process.exit(1);
}
const filePath = args.file;
if (!fs.existsSync(filePath)) {
console.error(`File not found: ${filePath}`);
process.exit(1);
}
upload(args);
}
main();