@xuanqikai/one-click-upload
Version:
A CLI tool for one-click file upload to cloud storage services (OSS, TOS)
507 lines • 24.5 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.UploadCommand = void 0;
const chalk_1 = __importDefault(require("chalk"));
const commander_1 = require("commander");
const inquirer_1 = __importDefault(require("inquirer"));
const ora_1 = __importDefault(require("ora"));
const path = __importStar(require("path"));
const fs = __importStar(require("fs-extra"));
const ConfigManager_1 = require("../config/ConfigManager");
const UploaderFactory_1 = require("../uploaders/UploaderFactory");
const fileUtils_1 = require("../utils/fileUtils");
class UploadCommand {
constructor() {
this.configManager = new ConfigManager_1.ConfigManager();
}
getCommand() {
const uploadCmd = new commander_1.Command("upload").description("Upload files or directories to cloud storage");
uploadCmd
.argument("<local-path>", "Local file or directory path to upload")
.argument("[remote-path]", "Remote path in the storage service")
.option("-s, --service <name>", "Service name to use for upload")
.option("-o, --overwrite", "Overwrite existing files", false)
.option("-r, --resume", "Enable resume upload for large files", true)
.option("--max-retries <number>", "Maximum retry attempts", "3")
.option("--chunk-size <size>", "Chunk size for multipart upload (in MB)", "1")
.option("--contents-only", "Upload folder contents only (without folder name)")
.option("--cache-control <value>", 'Custom cache control header (e.g., "no-cache")')
.action(async (localPath, remotePath, options) => {
await this.directUpload(localPath, remotePath, options);
});
return uploadCmd;
}
/**
* 交互式上传
*/
async interactiveUpload() {
try {
// 检查是否有配置的服务
const services = this.configManager.getAllServices();
if (services.length === 0) {
console.log(chalk_1.default.yellow("No services configured. Please add a service first:"));
console.log(chalk_1.default.cyan(" one-upload config add"));
return;
}
// 交互式输入上传参数
const answers = await inquirer_1.default.prompt([
{
type: "input",
name: "localPath",
message: "Local file or directory path:",
validate: async (input) => {
if (!input.trim()) {
return "Local path is required";
}
if (!(await (0, fileUtils_1.pathExists)(input.trim()))) {
return "Local path does not exist";
}
return true;
},
},
{
type: "input",
name: "remotePath",
message: "Remote path (optional):",
default: "",
},
{
type: "list",
name: "serviceName",
message: "Select service:",
choices: services.map((service) => ({
name: `${service.name}${service.isDefault ? " (default)" : ""} - ${UploaderFactory_1.UploaderFactory.getTypeDisplayName(service.type)}`,
value: service.name,
})),
default: this.configManager.getDefaultService()?.name,
},
{
type: "confirm",
name: "overwrite",
message: "Overwrite existing files?",
default: false,
},
{
type: "confirm",
name: "enableResume",
message: "Enable resume upload for large files?",
default: true,
},
]);
const options = {
serviceName: answers.serviceName,
localPath: answers.localPath.trim(),
remotePath: answers.remotePath.trim() || path.basename(answers.localPath.trim()),
overwrite: !!answers.overwrite,
enableResume: answers.enableResume,
maxRetries: 3,
chunkSize: 1024 * 1024, // 1MB
};
await this.performUpload(options);
}
catch (error) {
console.error(chalk_1.default.red("Interactive upload failed:"), error.message || "Unknown error");
}
}
/**
* 直接上传
*/
async directUpload(localPath, remotePath, cliOptions) {
try {
// 检查命令行参数中的 overwrite 选项
const hasOverwriteFlag = process.argv.includes("--overwrite") || process.argv.includes("-o");
// 检查本地路径是否存在
if (!(await (0, fileUtils_1.pathExists)(localPath))) {
console.error(chalk_1.default.red(`Local path does not exist: ${localPath}`));
return;
}
// 确定使用的服务
let serviceName = cliOptions?.service;
if (!serviceName) {
const defaultService = this.configManager.getDefaultService();
if (defaultService) {
serviceName = defaultService.name;
}
else {
console.error(chalk_1.default.red("No service specified and no default service configured"));
console.log(chalk_1.default.cyan("Use --service option or set a default service with: one-upload config default <name>"));
return;
}
}
// 检查服务名是否有效(防止参数顺序错误)
if (serviceName && serviceName.startsWith("--")) {
console.error(chalk_1.default.red(`错误:参数顺序不正确`));
console.error(chalk_1.default.yellow(`检测到服务名 '${serviceName}' 看起来像是一个选项参数`));
console.log(chalk_1.default.cyan("\n正确的命令格式:"));
console.log(chalk_1.default.gray(" one-upload <local-path> <remote-path> [options]"));
console.log(chalk_1.default.cyan("\n示例:"));
console.log(chalk_1.default.gray(" one-upload file.txt remote/ --service my-oss --preload"));
console.log(chalk_1.default.gray(" one-upload file.txt remote/ --preload --service my-oss"));
console.log(chalk_1.default.gray("\n注意:--service 后面必须紧跟服务名称"));
return;
}
// 检查服务是否存在
const service = this.configManager.getService(serviceName);
if (!service) {
console.error(chalk_1.default.red(`Service '${serviceName}' not found`));
console.log(chalk_1.default.cyan("可用的服务:"));
const allServices = this.configManager.getAllServices();
if (allServices.length > 0) {
allServices.forEach((s) => {
console.log(chalk_1.default.gray(` - ${s.name}${s.isDefault ? " (default)" : ""}`));
});
}
else {
console.log(chalk_1.default.yellow(" 没有配置任何服务"));
console.log(chalk_1.default.cyan(" 请先运行: one-upload config add"));
}
return;
}
// 设置默认远程路径
if (!remotePath) {
remotePath = path.basename(localPath);
}
const options = {
serviceName,
localPath,
remotePath,
overwrite: hasOverwriteFlag,
enableResume: cliOptions?.resume !== false,
maxRetries: parseInt(cliOptions?.maxRetries) || 3,
chunkSize: (parseInt(cliOptions?.chunkSize) || 1) * 1024 * 1024, // Convert MB to bytes
customCacheControl: cliOptions?.cacheControl,
};
await this.performUpload(options);
}
catch (error) {
console.error(chalk_1.default.red("Direct upload failed:"), error.message || "Unknown error");
}
}
/**
* 执行上传操作
*/
async performUpload(options) {
const service = this.configManager.getService(options.serviceName);
if (!service) {
console.error(chalk_1.default.red(`Service '${options.serviceName}' not found`));
return;
}
console.log(chalk_1.default.blue(`\nUploading to ${UploaderFactory_1.UploaderFactory.getTypeDisplayName(service.type)} service: ${chalk_1.default.cyan(service.name)}`));
console.log(chalk_1.default.gray(`Local: ${options.localPath}`));
console.log(chalk_1.default.gray(`Remote: ${options.remotePath}`));
console.log("");
const spinner = (0, ora_1.default)("Initializing upload...").start();
try {
// 创建上传器
const uploader = UploaderFactory_1.UploaderFactory.createUploader(service);
// 设置进度回调
const onProgress = (progress) => {
const percentage = progress.percentage.toFixed(1);
const loaded = (0, fileUtils_1.formatFileSize)(progress.loaded);
const total = (0, fileUtils_1.formatFileSize)(progress.total);
spinner.text = `Uploading ${progress.fileName}: ${percentage}% (${loaded}/${total})`;
};
// 执行上传
const result = await uploader.upload(options.localPath, options.remotePath, {
overwrite: options.overwrite,
enableResume: options.enableResume,
maxRetries: options.maxRetries,
chunkSize: options.chunkSize,
onProgress,
});
spinner.stop();
// 显示上传结果
this.displayUploadResult(result);
// 记录成功的文件路径(用于后续 CDN 操作)
const isBatchResult = (res) => res && Array.isArray(res.results);
let preloadPaths = [];
let refreshTarget;
// 如果未来直接使用 uploadBatch,这里会直接使用批量结果
if (isBatchResult(result)) {
preloadPaths = result.results
.filter((r) => r.success)
.map((r) => r.remotePath);
// 对于批量结果,优先使用用户指定的远程路径作为刷新目标
refreshTarget = options.remotePath;
}
else if (result.success &&
result.remotePath) {
// 判断本次上传的是目录还是单个文件
const stats = await fs.stat(options.localPath).catch(() => null);
if (stats && stats.isDirectory()) {
// 目录上传:根据本地目录结构生成所有文件对应的远程路径(用于预热)
const files = await (0, fileUtils_1.scanDirectory)(options.localPath);
const contentsOnly = options.localPath.endsWith("/");
preloadPaths = files.map((file) => {
let remoteFilePath;
if (contentsOnly) {
// 只上传内容,不包含目录名称
const normalizedRelativePath = (0, fileUtils_1.normalizePath)(file.relativePath);
remoteFilePath = (0, fileUtils_1.generateRemoteFilePath)(options.remotePath, normalizedRelativePath);
}
else {
// 上传整个目录,包含目录名称
const localDirName = path.basename(options.localPath);
const normalizedRelativePath = (0, fileUtils_1.normalizePath)(file.relativePath);
const combinedPath = (0, fileUtils_1.normalizePath)(path.join(localDirName, normalizedRelativePath));
remoteFilePath = (0, fileUtils_1.generateRemoteFilePath)(options.remotePath, combinedPath);
}
return remoteFilePath;
});
// 刷新目标使用远程目录路径(例如 test-folder/test-1/)
refreshTarget = options.remotePath;
}
else {
// 单文件上传:预热和刷新都只针对该文件
preloadPaths = [result.remotePath];
refreshTarget = result.remotePath;
}
}
// 仅上传,不自动刷新/预热,转交给用户选择
if (preloadPaths.length > 0 && refreshTarget) {
await this.performPostUploadActions(uploader, refreshTarget, preloadPaths);
}
}
catch (error) {
spinner.fail("Upload failed");
console.error(chalk_1.default.red("Error:"), error.message || "Unknown error");
}
}
/**
* 显示上传结果
*/
displayUploadResult(result) {
console.log("");
if ("results" in result) {
// 批量上传结果
this.displayBatchResult(result);
}
else {
// 单个文件上传结果
this.displaySingleResult(result);
}
}
/**
* 显示单个文件上传结果
*/
displaySingleResult(result) {
if (result.success) {
console.log(chalk_1.default.green("✓ Upload completed successfully"));
console.log(chalk_1.default.gray(`File: ${result.fileName}`));
if (result.size) {
console.log(chalk_1.default.gray(`Size: ${(0, fileUtils_1.formatFileSize)(result.size)}`));
}
if (result.url) {
console.log(chalk_1.default.gray(`URL: ${result.url}`));
}
}
else {
console.log(chalk_1.default.red("✗ Upload failed"));
console.log(chalk_1.default.gray(`File: ${result.fileName}`));
if (result.error) {
console.log(chalk_1.default.red(`Error: ${result.error}`));
}
}
}
/**
* 显示批量上传结果
*/
displayBatchResult(result) {
const { totalFiles, successCount, failedCount, totalSize, duration } = result;
console.log(chalk_1.default.bold("Upload Summary:"));
console.log("─".repeat(40));
console.log(`Total files: ${totalFiles}`);
console.log(`Successful: ${chalk_1.default.green(successCount)}`);
console.log(`Failed: ${failedCount > 0 ? chalk_1.default.red(failedCount) : failedCount}`);
console.log(`Total size: ${(0, fileUtils_1.formatFileSize)(totalSize)}`);
console.log(`Duration: ${(0, fileUtils_1.formatDuration)(duration)}`);
if (failedCount > 0) {
console.log("\nFailed files:");
result.results
.filter((r) => !r.success)
.forEach((r) => {
console.log(chalk_1.default.red(` ✗ ${r.fileName}: ${r.error}`));
});
}
if (successCount > 0) {
console.log(chalk_1.default.green("\nSuccessful uploads:"));
result.results
.filter((r) => r.success)
.slice(0, 5) // 只显示前5个成功的文件
.forEach((r) => {
console.log(chalk_1.default.green(` ✓ ${r.fileName}`));
});
if (successCount > 5) {
console.log(chalk_1.default.gray(` ... and ${successCount - 5} more files`));
}
}
}
/**
* 执行预热操作
*/
async performPreload(uploader, result, options) {
try {
console.log(chalk_1.default.blue("\n🔥 Starting CDN preload..."));
if ("results" in result) {
// 批量上传结果,预热所有成功的文件
const successfulFiles = result.results
.filter((r) => r.success)
.map((r) => r.remotePath);
if (successfulFiles.length > 0) {
console.log(chalk_1.default.gray(`Preloading ${successfulFiles.length} files...`));
const preloadResult = await uploader.preloadCDNCacheBatch(successfulFiles, options.preloadOptions);
console.log(chalk_1.default.green(`✓ Preload completed: ${preloadResult.success} successful, ${preloadResult.failed} failed`));
}
}
else {
// 单个文件上传结果
if (result.success && result.remotePath) {
console.log(chalk_1.default.gray(`Preloading file: ${result.remotePath}`));
const preloadSuccess = await uploader.preloadCDNCache(result.remotePath, options.preloadOptions);
if (preloadSuccess) {
console.log(chalk_1.default.green("✓ Preload completed successfully"));
}
else {
console.log(chalk_1.default.yellow("⚠ Preload completed with warnings"));
}
}
}
}
catch (error) {
console.warn(chalk_1.default.yellow("⚠ Preload failed:"), error.message || "Unknown error");
}
}
/**
* 上传完成后的交互式操作(刷新/预热)
*/
async performPostUploadActions(uploader, refreshTarget, preloadPaths) {
try {
// 持续询问,直到用户选择“不执行”为止
// 每次只能选择一种操作(单选)
// 默认选中“不执行”
while (true) {
const { action } = await inquirer_1.default.prompt([
{
type: "list",
name: "action",
message: "是否继续进行 CDN 操作?",
choices: [
{ name: "不执行", value: "none" },
{ name: "刷新 CDN 缓存", value: "refresh" },
{ name: "预热 CDN 缓存", value: "preload" },
],
default: "none",
},
]);
if (!action || action === "none") {
return;
}
// 刷新
if (action === "refresh" && typeof uploader.refreshCDNCache === "function") {
const refreshSpinner = (0, ora_1.default)("Refreshing CDN cache...").start();
try {
// 优先使用目录刷新(当 refreshTarget 是目录且实现了 refreshCDNDirectory 时)
if (refreshTarget.endsWith("/") &&
typeof uploader.refreshCDNDirectory === "function") {
refreshSpinner.text = "Refreshing CDN directory cache...";
const ok = await uploader.refreshCDNDirectory(refreshTarget);
if (ok) {
refreshSpinner.succeed("Refresh completed successfully");
}
else {
refreshSpinner.warn("Refresh completed with warnings");
}
}
else if (preloadPaths.length > 1 &&
typeof uploader.refreshCDNCacheBatch === "function") {
refreshSpinner.text = `Refreshing CDN cache for ${preloadPaths.length} files...`;
const refreshResult = await uploader.refreshCDNCacheBatch(preloadPaths);
refreshSpinner.stop();
console.log(chalk_1.default.green(`✓ Refresh completed: ${refreshResult.success} successful, ${refreshResult.failed} failed`));
}
else {
const refreshSuccess = await uploader.refreshCDNCache(refreshTarget);
if (refreshSuccess) {
refreshSpinner.succeed("Refresh completed successfully");
}
else {
refreshSpinner.warn("Refresh completed with warnings");
}
}
}
catch (error) {
refreshSpinner.fail("Refresh failed");
throw error;
}
}
// 预热(始终按文件列表执行)
if (action === "preload" && typeof uploader.preloadCDNCache === "function") {
const preloadSpinner = (0, ora_1.default)("Starting CDN preload...").start();
try {
if (preloadPaths.length > 1 &&
typeof uploader.preloadCDNCacheBatch === "function") {
preloadSpinner.text = `Preloading ${preloadPaths.length} files to CDN...`;
const preloadResult = await uploader.preloadCDNCacheBatch(preloadPaths);
preloadSpinner.stop();
console.log(chalk_1.default.green(`✓ Preload completed: ${preloadResult.success} successful, ${preloadResult.failed} failed`));
}
else {
const preloadSuccess = await uploader.preloadCDNCache(refreshTarget);
if (preloadSuccess) {
preloadSpinner.succeed("Preload completed successfully");
}
else {
preloadSpinner.warn("Preload completed with warnings");
}
}
}
catch (error) {
preloadSpinner.fail("Preload failed");
throw error;
}
}
}
}
catch (error) {
console.warn(chalk_1.default.yellow("⚠ Post upload actions failed:"), error.message || "Unknown error");
}
}
}
exports.UploadCommand = UploadCommand;
//# sourceMappingURL=UploadCommand.js.map