Skip to main content
Glama
156554395

Tencent Cloud COS MCP Server

by 156554395
cosService.js32.5 kB
import COS from 'cos-nodejs-sdk-v5'; import path from 'path'; import fs from 'fs/promises'; import crypto from 'crypto'; import { cosConfig, hasValidConfig } from './config.js'; import { saveProgress, loadProgress, clearProgress } from './uploadProgress.js'; import { TEMP_DIRS, getTempDirStats, cleanupTempFiles } from './tempManager.js'; /** * 腾讯云COS服务封装类 * 提供文件上传、下载、管理等操作的统一接口 */ class TencentCOSService { /** * 构造函数 - 初始化COS服务实例 */ constructor() { this.configValid = hasValidConfig(); if (this.configValid) { this.cos = new COS({ SecretId: cosConfig.SecretId, SecretKey: cosConfig.SecretKey }); } this.config = cosConfig; } /** * 检查COS配置是否有效 * @throws {Error} 当配置无效时抛出错误 * @private */ _checkConfig() { if (!this.configValid) { throw new Error('COS配置无效,请检查环境变量设置'); } } /** * 上传单个文件到COS * @param {string} localPath - 本地文件路径 * @param {Object} options - 上传选项 * @param {string} [options.key] - COS中的对象键,不提供则使用文件名 * @param {string} [options.customDomain] - 自定义访问域名 * @param {boolean} [options.useSliceUpload] - 强制使用分片上传 * @param {number} [options.chunkSize] - 分片大小(字节),默认1MB * @param {number} [options.concurrency] - 并发上传数,默认3 * @param {Function} [options.onProgress] - 进度回调函数 * @returns {Promise<Object>} 上传结果,包含URL、键名、ETag等信息 * @throws {Error} 上传失败时抛出错误 */ async uploadFile(localPath, { key = null, customDomain = null, useSliceUpload = false, chunkSize = 1024 * 1024, // 1MB concurrency = 3, onProgress = null } = {}) { this._checkConfig(); try { // 获取文件状态 const stats = await fs.stat(localPath); const fileSize = stats.size; // 生成key,如果没提供则使用文件名 const objectKey = key || path.basename(localPath); // 判断是否使用分片上传 (文件大于5MB或强制指定) const shouldUseSliceUpload = useSliceUpload || fileSize > (5 * 1024 * 1024); if (shouldUseSliceUpload) { return await this._uploadLargeFile(localPath, objectKey, { customDomain, chunkSize, concurrency, onProgress, fileSize }); } // 小文件直接上传 const buffer = await fs.readFile(localPath); const params = { Bucket: this.config.Bucket, Region: this.config.Region, Key: objectKey, Body: buffer, }; const response = await this.cos.putObject(params); // 上传成功后清理相关临时文件 try { await this._cleanupUploadTempFiles(objectKey); } catch (clearErr) { // 清理临时文件失败时静默处理,不影响上传成功的结果 } // 生成访问URL const domain = customDomain || this.config.Domain || `https://${this.config.Bucket}.cos.${this.config.Region}.myqcloud.com`; const fileUrl = `${domain}/${objectKey}`; return { success: true, url: fileUrl, key: objectKey, etag: response.ETag, location: response.Location, size: fileSize, uploadType: 'direct' }; } catch (error) { // 上传失败时,清理可能产生的临时文件 try { await this._cleanupUploadTempFiles(objectKey); } catch (clearErr) { // 清理失败时静默处理 } console.error('上传失败:', error); throw new Error(`文件上传失败: ${error.message}`); } } /** * 大文件分片上传核心方法,支持断点续传 * @param {string} localPath - 本地文件路径 * @param {string} objectKey - COS对象键 * @param {Object} options - 上传选项 * @returns {Promise<Object>} 上传结果 * @private */ async _uploadLargeFile(localPath, objectKey, options) { const { customDomain, chunkSize = 1024 * 1024, // 1MB concurrency = 3, onProgress, fileSize } = options; // 生成上传会话ID用于断点续传 const sessionId = await this._generateUploadSessionId(localPath, objectKey, chunkSize); try { // 检查是否有未完成的上传 const savedProgress = await loadProgress(sessionId); if (savedProgress && savedProgress.uploadId) { // 发现未完成的上传,继续上传 (静默处理) } return new Promise((resolve, reject) => { let currentUploadId = null; // 使用腾讯云COS SDK的分片上传功能 this.cos.sliceUploadFile({ Bucket: this.config.Bucket, Region: this.config.Region, Key: objectKey, FilePath: localPath, ChunkSize: chunkSize, AsyncLimit: concurrency, // 并发数 UploadIdCacheLimit: 500, // 缓存UploadId数量 onProgress: async (progressData) => { // 保存进度信息 if (currentUploadId) { try { await saveProgress(sessionId, { uploadId: currentUploadId, filePath: localPath, objectKey, fileSize, chunkSize, percent: progressData.percent, speed: progressData.speed || 0 }); } catch (err) { // 保存进度失败时静默处理,不影响上传 } } if (onProgress) { onProgress({ loaded: Math.round(progressData.percent * fileSize / 100), total: fileSize, percent: progressData.percent, speed: progressData.speed || 0, sessionId: sessionId }); } }, onTaskReady: (taskId) => { currentUploadId = taskId; // 分片上传开始 (静默处理) } }, async (err, data) => { if (err) { // 上传失败时保持进度文件,便于下次续传 // 但清理其他临时文件以避免磁盘空间占用 try { await this._cleanupFailedUpload(sessionId, objectKey); } catch (clearErr) { // 清理失败时静默处理 } reject(new Error(`分片上传失败: ${err.message}`)); } else { // 上传成功,清理进度文件和相关临时资源 try { await clearProgress(sessionId); // 清理可能的临时上传文件 await this._cleanupUploadTempFiles(objectKey); } catch (clearErr) { // 清理临时文件失败时静默处理,不影响上传成功的结果 } // 生成访问URL const domain = customDomain || this.config.Domain || `https://${this.config.Bucket}.cos.${this.config.Region}.myqcloud.com`; const fileUrl = `${domain}/${objectKey}`; resolve({ success: true, url: fileUrl, key: objectKey, etag: data.ETag, location: data.Location, size: fileSize, uploadType: 'slice', uploadId: data.UploadId || currentUploadId, sessionId: sessionId }); } }); }); } catch (error) { // 分片上传过程中发生异常时,清理临时文件但保留进度文件 try { await this._cleanupFailedUpload(sessionId, objectKey); } catch (clearErr) { // 清理失败时静默处理 } throw new Error(`断点续传上传失败: ${error.message}`); } } /** * 生成上传会话ID,用于断点续传标识 * @param {string} filePath - 文件路径 * @param {string} objectKey - 对象键 * @param {number} chunkSize - 分片大小 * @returns {Promise<string>} 会话ID * @private */ async _generateUploadSessionId(filePath, objectKey, chunkSize) { const stats = await fs.stat(filePath); const content = `${filePath}-${objectKey}-${stats.size}-${chunkSize}-${stats.mtime.getTime()}`; return crypto.createHash('md5').update(content).digest('hex'); } /** * 清理上传过程中产生的临时文件 * @param {string} objectKey - 对象键 * @returns {Promise<void>} * @private */ async _cleanupUploadTempFiles(objectKey) { try { // 清理可能的临时上传文件(基于对象键生成的临时文件) const tempFileName = `${objectKey.replace(/[^a-zA-Z0-9]/g, '_')}_temp`; const tempFilePaths = [ path.join(TEMP_DIRS.UPLOADS, tempFileName), path.join(TEMP_DIRS.UPLOADS, `${tempFileName}.part`), path.join(TEMP_DIRS.UPLOADS, `${tempFileName}.tmp`) ]; for (const tempFilePath of tempFilePaths) { try { await fs.unlink(tempFilePath); } catch (error) { // 临时文件可能不存在,忽略错误 } } // 清理相关的缓存文件 const cacheFileName = `${objectKey.replace(/[^a-zA-Z0-9]/g, '_')}_cache.json`; const cacheFilePaths = [ path.join(TEMP_DIRS.CACHE, cacheFileName), path.join(TEMP_DIRS.CACHE, `${cacheFileName}.bak`), path.join(TEMP_DIRS.CACHE, `upload_${cacheFileName}`) ]; for (const cacheFilePath of cacheFilePaths) { try { await fs.unlink(cacheFilePath); } catch (error) { // 缓存文件可能不存在,忽略错误 } } // 清理可能的分片临时目录 const chunkDirName = `chunks_${objectKey.replace(/[^a-zA-Z0-9]/g, '_')}`; const chunkDirPath = path.join(TEMP_DIRS.UPLOADS, chunkDirName); try { // 先清理目录内的文件,再删除目录 const { readdir, rmdir } = await import('fs/promises'); const files = await readdir(chunkDirPath); for (const file of files) { await fs.unlink(path.join(chunkDirPath, file)); } await rmdir(chunkDirPath); } catch (error) { // 目录可能不存在,忽略错误 } } catch (error) { // 清理失败时静默处理,不影响主流程 } } /** * 在上传失败或中断时清理所有相关的临时资源 * @param {string} sessionId - 会话ID * @param {string} objectKey - 对象键 * @returns {Promise<void>} * @private */ async _cleanupFailedUpload(sessionId, objectKey) { try { // 注意:失败时保留进度文件以支持断点续传 // 但清理其他临时文件 await this._cleanupUploadTempFiles(objectKey); } catch (error) { // 清理失败时静默处理 } } /** * 获取所有未完成的上传进度 * @param {string} [sessionId] - 特定会话ID,不提供则返回所有 * @returns {Promise<Object>} 进度信息 */ async getUploadProgress(sessionId = null) { try { if (sessionId) { const progress = await loadProgress(sessionId); return { success: true, sessionId, progress: progress || null }; } // 获取所有进度文件 const { readdir } = await import('fs/promises'); try { const files = await readdir(TEMP_DIRS.PROGRESS); const progressList = []; for (const file of files) { if (file.endsWith('.json')) { const id = file.replace('.json', ''); const progress = await loadProgress(id); if (progress) { progressList.push({ sessionId: id, ...progress }); } } } return { success: true, totalUploads: progressList.length, uploads: progressList }; } catch (err) { // 目录不存在时返回空列表 return { success: true, totalUploads: 0, uploads: [] }; } } catch (error) { throw new Error(`获取上传进度失败: ${error.message}`); } } /** * 清理指定的上传进度 * @param {string} sessionId - 会话ID * @returns {Promise<Object>} 清理结果 */ async clearUploadProgress(sessionId) { try { await clearProgress(sessionId); return { success: true, sessionId, message: '进度记录已清理' }; } catch (error) { throw new Error(`清理进度失败: ${error.message}`); } } /** * 管理临时文件 * @param {string} action - 操作类型:'stats' 或 'cleanup' * @param {Object} [options] - 选项 * @param {string} [options.type='all'] - 文件类型 * @param {number} [options.olderThanDays=7] - 清理天数 * @returns {Promise<Object>} 操作结果 */ async manageTempFiles(action, options = {}) { const { type = 'all', olderThanDays = 7 } = options; try { if (action === 'stats') { const stats = await getTempDirStats(); return { success: true, action: 'stats', data: stats }; } else if (action === 'cleanup') { const cleanedCount = await cleanupTempFiles(type, olderThanDays); return { success: true, action: 'cleanup', type, olderThanDays, cleanedFiles: cleanedCount, message: `成功清理 ${cleanedCount} 个临时文件` }; } else { throw new Error(`不支持的操作类型: ${action}`); } } catch (error) { throw new Error(`临时文件管理失败: ${error.message}`); } } /** * 批量上传多个文件到COS,支持智能并发控制和重试 * @param {Array<Object>} files - 文件数组 * @param {string} files[].path - 文件本地路径 * @param {string} [files[].key] - COS中的对象键 * @param {Object} [options] - 批量上传选项 * @param {number} [options.concurrency=3] - 并发数量 * @param {number} [options.maxRetries=3] - 最大重试次数 * @param {Function} [options.onProgress] - 进度回调 * @returns {Promise<Array>} 上传结果数组,每个结果包含成功状态和详细信息 */ async uploadMultipleFiles(files = [], options = {}) { const { concurrency = 3, maxRetries = 3, onProgress = null } = options; if (!Array.isArray(files) || files.length === 0) { throw new Error('文件列表不能为空'); } const results = []; const totalFiles = files.length; let completedFiles = 0; let successCount = 0; let failureCount = 0; // 并发控制队列 const executeWithConcurrency = async (fileList, concurrentLimit) => { const executing = []; for (const file of fileList) { const promise = this._uploadSingleFileWithRetry(file, maxRetries) .then(result => { completedFiles++; if (result.success) { successCount++; } else { failureCount++; } // 触发进度回调 if (onProgress) { onProgress({ completed: completedFiles, total: totalFiles, success: successCount, failed: failureCount, percent: (completedFiles / totalFiles * 100).toFixed(1) }); } return result; }) .catch(error => { completedFiles++; failureCount++; if (onProgress) { onProgress({ completed: completedFiles, total: totalFiles, success: successCount, failed: failureCount, percent: (completedFiles / totalFiles * 100).toFixed(1) }); } return { success: false, error: error.message, originalPath: file.path }; }); executing.push(promise); if (executing.length >= concurrentLimit) { await Promise.race(executing); // 移除已完成的promise const index = executing.findIndex(p => p.resolved); if (index !== -1) { executing.splice(index, 1); } } } return Promise.all(executing); }; try { const batchResults = await executeWithConcurrency(files, concurrency); // 批量上传完成后,清理可能残留的批量上传临时文件 try { await this._cleanupBatchUploadTempFiles(); } catch (clearErr) { // 清理失败时静默处理,不影响上传结果 } return batchResults.flat(); } catch (error) { // 批量上传发生异常时,也尝试清理临时文件 try { await this._cleanupBatchUploadTempFiles(); } catch (clearErr) { // 清理失败时静默处理 } throw new Error(`批量上传失败: ${error.message}`); } } /** * 单文件上传带重试机制 * @param {Object} file - 文件对象 * @param {number} maxRetries - 最大重试次数 * @returns {Promise<Object>} 上传结果 * @private */ async _uploadSingleFileWithRetry(file, maxRetries = 3) { let lastError; let retryCount = 0; while (retryCount <= maxRetries) { try { const result = await this.uploadFile(file.path, { key: file.key }); return { ...result, originalPath: file.path, retries: retryCount }; } catch (error) { lastError = error; retryCount++; // 检查是否应该重试 if (retryCount > maxRetries || !this._shouldRetry(error)) { break; } // 指数退避延迟 const delay = Math.min(1000 * Math.pow(2, retryCount - 1), 30000); // 延迟重试 (静默处理) await new Promise(resolve => setTimeout(resolve, delay)); } } throw new Error(`文件 ${file.path} 上传失败(重试${retryCount}次): ${lastError.message}`); } /** * 判断错误是否应该重试 * @param {Error} error - 错误对象 * @returns {boolean} 是否应该重试 * @private */ _shouldRetry(error) { const retryableErrors = [ 'ECONNRESET', 'ETIMEDOUT', 'ENOTFOUND', 'ECONNREFUSED', 'RequestTimeout', 'ServiceUnavailable', 'InternalError', 'SlowDown' ]; const errorMessage = error.message || ''; return retryableErrors.some(retryableError => errorMessage.includes(retryableError) ); } /** * 清理批量上传过程中产生的临时文件 * @returns {Promise<void>} * @private */ async _cleanupBatchUploadTempFiles() { try { // 清理批量上传专用的临时文件 const batchTempFiles = [ 'batch_upload_queue.json', 'batch_upload_status.json', 'batch_upload_errors.json' ]; for (const tempFile of batchTempFiles) { try { await fs.unlink(path.join(TEMP_DIRS.UPLOADS, tempFile)); } catch (error) { // 文件可能不存在,忽略错误 } } // 清理可能的批量上传缓存目录 const batchCacheDir = path.join(TEMP_DIRS.CACHE, 'batch_uploads'); try { const { readdir, rmdir } = await import('fs/promises'); const files = await readdir(batchCacheDir); for (const file of files) { await fs.unlink(path.join(batchCacheDir, file)); } await rmdir(batchCacheDir); } catch (error) { // 目录可能不存在,忽略错误 } // 清理过期的上传锁文件 const lockPattern = /^upload_lock_\d+\.lock$/; try { const { readdir } = await import('fs/promises'); const files = await readdir(TEMP_DIRS.UPLOADS); const currentTime = Date.now(); for (const file of files) { if (lockPattern.test(file)) { const filePath = path.join(TEMP_DIRS.UPLOADS, file); try { const stats = await fs.stat(filePath); // 清理超过1小时的锁文件 if (currentTime - stats.mtime.getTime() > 3600000) { await fs.unlink(filePath); } } catch (error) { // 文件可能已被删除,忽略错误 } } } } catch (error) { // 目录访问失败时静默处理 } } catch (error) { // 批量清理失败时静默处理 } } /** * 获取对象的临时签名URL * @param {string} key - COS对象键 * @param {number} [expireTime=3600] - URL有效期(秒),默认1小时 * @returns {Promise<Object>} 包含临时URL的结果对象 * @throws {Error} 获取失败时抛出错误 */ async getSignedUrl(key, expireTime = 3600) { this._checkConfig(); try { const params = { Bucket: this.config.Bucket, Region: this.config.Region, Key: key, Sign: true, Expires: expireTime }; const url = await this.cos.getObjectUrl(params); return { success: true, url }; } catch (error) { throw new Error(`获取签名URL失败: ${error.message}`); } } /** * 列出存储桶中的对象 * @param {string} [prefix=''] - 对象键前缀过滤 * @returns {Promise<Object>} 包含文件列表的结果对象 * @throws {Error} 列举失败时抛出错误 */ async listObjects(prefix = '') { this._checkConfig(); try { const params = { Bucket: this.config.Bucket, Region: this.config.Region, Prefix: prefix, MaxKeys: 1000 }; const response = await this.cos.getBucket(params); return { success: true, files: response.Contents || [], prefix, total: response.Contents?.length || 0 }; } catch (error) { throw new Error(`列举文件失败: ${error.message}`); } } /** * 删除单个对象 * @param {string} key - 要删除的对象键 * @returns {Promise<Object>} 删除结果 * @throws {Error} 删除失败时抛出错误 */ async deleteObject(key) { this._checkConfig(); try { const params = { Bucket: this.config.Bucket, Region: this.config.Region, Key: key }; await this.cos.deleteObject(params); return { success: true, key }; } catch (error) { throw new Error(`删除文件失败: ${error.message}`); } } /** * 复制COS对象 * @param {string} sourceKey - 源对象键 * @param {string} targetKey - 目标对象键 * @param {Object} [options] - 复制选项 * @param {string} [options.targetBucket] - 目标存储桶,不指定则为当前存储桶 * @returns {Promise<Object>} 复制结果,包含新对象的URL和元数据 * @throws {Error} 复制失败时抛出错误 */ async copyObject(sourceKey, targetKey, { targetBucket = null } = {}) { this._checkConfig(); try { const sourceBucket = targetBucket || this.config.Bucket; // 对sourceKey进行URL编码以支持中文字符 const encodedSourceKey = encodeURIComponent(sourceKey); const params = { Bucket: this.config.Bucket, Region: this.config.Region, Key: targetKey, CopySource: `${sourceBucket}.cos.${this.config.Region}.myqcloud.com/${encodedSourceKey}` }; const response = await this.cos.putObjectCopy(params); // 生成访问URL const domain = this.config.Domain || `https://${this.config.Bucket}.cos.${this.config.Region}.myqcloud.com`; const fileUrl = `${domain}/${targetKey}`; return { success: true, sourceKey, targetKey, url: fileUrl, etag: response.ETag, lastModified: response.LastModified }; } catch (error) { throw new Error(`复制文件失败: ${error.message}`); } } /** * 移动COS对象(复制+删除原文件) * @param {string} sourceKey - 源对象键 * @param {string} targetKey - 目标对象键 * @param {Object} [options] - 移动选项 * @param {string} [options.targetBucket] - 目标存储桶,不指定则为当前存储桶 * @returns {Promise<Object>} 移动结果,包含新对象的URL和元数据 * @throws {Error} 移动失败时抛出错误 */ async moveObject(sourceKey, targetKey, { targetBucket = null } = {}) { this._checkConfig(); try { // 先复制文件 const copyResult = await this.copyObject(sourceKey, targetKey, { targetBucket }); // 复制成功后删除源文件 await this.deleteObject(sourceKey); return { success: true, sourceKey, targetKey, url: copyResult.url, etag: copyResult.etag, operation: 'move' }; } catch (error) { throw new Error(`移动文件失败: ${error.message}`); } } /** * 重命名COS对象(基于移动操作实现) * @param {string} oldKey - 原对象键 * @param {string} newKey - 新对象键 * @returns {Promise<Object>} 重命名结果 * @throws {Error} 重命名失败时抛出错误 */ async renameObject(oldKey, newKey) { this._checkConfig(); try { // 重命名实际上是移动操作 const result = await this.moveObject(oldKey, newKey); return { success: true, oldKey, newKey, url: result.url, etag: result.etag, operation: 'rename' }; } catch (error) { throw new Error(`重命名文件失败: ${error.message}`); } } /** * 批量删除多个COS对象 * @param {Array<string>} keys - 要删除的对象键数组 * @returns {Promise<Object>} 批量删除结果,包含成功和失败的对象列表 * @throws {Error} 批量删除失败时抛出错误 */ async deleteMultipleObjects(keys = []) { this._checkConfig(); if (!Array.isArray(keys) || keys.length === 0) { throw new Error('键列表不能为空'); } try { const params = { Bucket: this.config.Bucket, Region: this.config.Region, Delete: { Objects: keys.map(key => ({ Key: key })), Quiet: false } }; const response = await this.cos.deleteMultipleObject(params); return { success: true, deleted: response.Deleted || [], errors: response.Error || [], total: keys.length }; } catch (error) { throw new Error(`批量删除文件失败: ${error.message}`); } } /** * 创建文件夹(在COS中创建以"/"结尾的空对象) * @param {string} folderPath - 文件夹路径 * @returns {Promise<Object>} 创建结果 * @throws {Error} 创建失败时抛出错误 */ async createFolder(folderPath) { this._checkConfig(); try { // 确保文件夹路径以/结尾 const folderKey = folderPath.endsWith('/') ? folderPath : `${folderPath}/`; const params = { Bucket: this.config.Bucket, Region: this.config.Region, Key: folderKey, Body: '' }; await this.cos.putObject(params); return { success: true, folderPath: folderKey, operation: 'create_folder' }; } catch (error) { throw new Error(`创建文件夹失败: ${error.message}`); } } /** * 删除文件夹 * @param {string} folderPath - 文件夹路径 * @param {Object} [options] - 删除选项 * @param {boolean} [options.recursive=false] - 是否递归删除文件夹内的所有内容 * @returns {Promise<Object>} 删除结果 * @throws {Error} 删除失败时抛出错误 */ async deleteFolder(folderPath, { recursive = false } = {}) { this._checkConfig(); try { // 确保文件夹路径以/结尾 const folderKey = folderPath.endsWith('/') ? folderPath : `${folderPath}/`; if (recursive) { // 递归删除:先列出文件夹中的所有对象 const listResult = await this.listObjects(folderKey); if (listResult.files && listResult.files.length > 0) { const keys = listResult.files.map(file => file.Key); await this.deleteMultipleObjects(keys); } // 删除文件夹本身 await this.deleteObject(folderKey); return { success: true, folderPath: folderKey, deletedCount: listResult.files ? listResult.files.length + 1 : 1, operation: 'delete_folder_recursive' }; } else { // 非递归删除:只删除空文件夹 await this.deleteObject(folderKey); return { success: true, folderPath: folderKey, operation: 'delete_folder' }; } } catch (error) { throw new Error(`删除文件夹失败: ${error.message}`); } } /** * 以文件夹视图列出COS内容,区分文件夹和文件 * @param {string} [prefix=''] - 路径前缀 * @returns {Promise<Object>} 包含文件夹和文件列表的结果 * @throws {Error} 列举失败时抛出错误 */ async listFolders(prefix = '') { this._checkConfig(); try { const params = { Bucket: this.config.Bucket, Region: this.config.Region, Prefix: prefix, Delimiter: '/', MaxKeys: 1000 }; const response = await this.cos.getBucket(params); return { success: true, folders: response.CommonPrefixes?.map(item => item.Prefix) || [], files: response.Contents?.filter(item => !item.Key.endsWith('/')) || [], prefix, total: (response.CommonPrefixes?.length || 0) + (response.Contents?.length || 0) }; } catch (error) { throw new Error(`列举文件夹失败: ${error.message}`); } } /** * 获取文件夹统计信息,包括文件数量、总大小、文件类型分布等 * @param {string} [folderPath] - 文件夹路径,为空则统计整个存储桶 * @returns {Promise<Object>} 包含详细统计信息的结果 * @throws {Error} 获取统计失败时抛出错误 */ async getFolderStats(folderPath) { this._checkConfig(); try { // 确保文件夹路径以/结尾 const folderKey = folderPath ? (folderPath.endsWith('/') ? folderPath : `${folderPath}/`) : ''; const listResult = await this.listObjects(folderKey); let totalSize = 0; let fileCount = 0; const fileTypes = {}; if (listResult.files) { for (const file of listResult.files) { if (!file.Key.endsWith('/')) { // 排除文件夹对象 fileCount++; totalSize += file.Size || 0; // 统计文件类型 const ext = file.Key.split('.').pop()?.toLowerCase() || 'unknown'; fileTypes[ext] = (fileTypes[ext] || 0) + 1; } } } return { success: true, folderPath: folderKey, fileCount, totalSize, totalSizeFormatted: this._formatFileSize(totalSize), fileTypes, averageFileSize: fileCount > 0 ? Math.round(totalSize / fileCount) : 0 }; } catch (error) { throw new Error(`获取文件夹统计失败: ${error.message}`); } } /** * 格式化文件大小为人类可读的字符串 * @param {number} bytes - 字节数 * @returns {string} 格式化后的大小字符串(如"1.5 MB") * @private */ _formatFileSize(bytes) { if (bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; } } export const cosService = new TencentCOSService();

Implementation Reference

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/156554395/tx-cos-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server