UNPKG

@xuanqikai/one-click-upload

Version:

A CLI tool for one-click file upload to cloud storage services (OSS, TOS)

507 lines 24.5 kB
"use strict"; 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