Skip to main content
Glama

Claude MCP Server Integration

by mokemoke0821
archive.ts14.2 kB
/** * ファイル圧縮・アーカイブ機能 * Phase 2: 実用機能の圧縮・アーカイブシステム */ import archiver from 'archiver'; import { createReadStream, createWriteStream, promises as fs } from 'fs'; import { basename, dirname, extname, join } from 'path'; import * as unzipper from 'unzipper'; import { ArchiveInfo, ArchiveOptions, OperationResult } from '../types/index.js'; import { canReadFile, createFailureResult, createSuccessResult, ensureDirectory, fileExists, generateSafeFilePath, getFilesRecursively } from '../utils/file-helper.js'; import { logger } from '../utils/logger.js'; export class FileArchive { private static readonly DEFAULT_OPTIONS: ArchiveOptions = { format: 'zip', compressionLevel: 6, excludePatterns: ['.DS_Store', 'Thumbs.db', '.git/**'], includePatterns: [] }; /** * ファイル・ディレクトリを圧縮 */ async createArchive( sourcePaths: string[], outputPath: string, options: Partial<ArchiveOptions> = {} ): Promise<OperationResult<ArchiveInfo>> { try { logger.info(`アーカイブ作成を開始: ${sourcePaths.length}アイテム -> ${outputPath}`); const fullOptions = { ...FileArchive.DEFAULT_OPTIONS, ...options }; // 入力パスの検証 for (const path of sourcePaths) { if (!await fileExists(path)) { throw new Error(`ソースが見つかりません: ${path}`); } } // 出力ディレクトリの作成 await ensureDirectory(dirname(outputPath)); // 安全な出力パス生成 const safeOutputPath = await generateSafeFilePath(outputPath); // 圧縮実行 const archiveInfo = await this.performCompression(sourcePaths, safeOutputPath, fullOptions); logger.info(`アーカイブ作成完了: ${archiveInfo.fileCount}ファイル, 圧縮率: ${(archiveInfo.compressionRatio * 100).toFixed(1)}%`); return createSuccessResult(`アーカイブが正常に作成されました: ${safeOutputPath}`, archiveInfo); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); logger.error(`アーカイブ作成エラー`, { error, sourcePaths, outputPath }); return createFailureResult(`アーカイブ作成に失敗しました: ${errorMessage}`, error instanceof Error ? error : new Error(errorMessage)); } } /** * アーカイブを展開 */ async extractArchive( archivePath: string, outputDirectory: string, password?: string ): Promise<OperationResult<string[]>> { try { logger.info(`アーカイブ展開を開始: ${archivePath} -> ${outputDirectory}`); if (!await fileExists(archivePath)) { throw new Error(`アーカイブファイルが見つかりません: ${archivePath}`); } if (!await canReadFile(archivePath)) { throw new Error(`アーカイブファイル読み取り権限がありません: ${archivePath}`); } // 出力ディレクトリの作成 await ensureDirectory(outputDirectory); // 拡張子に基づく形式判定 const format = this.detectArchiveFormat(archivePath); // 展開実行 const extractedFiles = await this.performExtraction(archivePath, outputDirectory, format, password); logger.info(`アーカイブ展開完了: ${extractedFiles.length}ファイル展開`); return createSuccessResult(`アーカイブが正常に展開されました: ${extractedFiles.length}ファイル`, extractedFiles); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); logger.error(`アーカイブ展開エラー: ${archivePath}`, { error }); return createFailureResult(`アーカイブ展開に失敗しました: ${errorMessage}`, error instanceof Error ? error : new Error(errorMessage)); } } /** * アーカイブ情報を取得 */ async getArchiveInfo(archivePath: string): Promise<OperationResult<ArchiveInfo>> { try { logger.info(`アーカイブ情報取得を開始: ${archivePath}`); if (!await fileExists(archivePath)) { throw new Error(`アーカイブファイルが見つかりません: ${archivePath}`); } const format = this.detectArchiveFormat(archivePath); const archiveInfo = await this.analyzeArchive(archivePath, format); logger.info(`アーカイブ情報取得完了: ${archiveInfo.fileCount}ファイル`); return createSuccessResult(`アーカイブ情報を正常に取得しました`, archiveInfo); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); logger.error(`アーカイブ情報取得エラー: ${archivePath}`, { error }); return createFailureResult(`アーカイブ情報取得に失敗しました: ${errorMessage}`, error instanceof Error ? error : new Error(errorMessage)); } } /** * 複数ディレクトリの一括圧縮 */ async createMultipleArchives( sourceDirectories: string[], outputDirectory: string, options: Partial<ArchiveOptions> = {} ): Promise<OperationResult<ArchiveInfo[]>> { try { logger.info(`複数アーカイブ作成を開始: ${sourceDirectories.length}ディレクトリ`); const results: ArchiveInfo[] = []; const errors: string[] = []; await ensureDirectory(outputDirectory); for (const sourceDir of sourceDirectories) { try { const dirName = basename(sourceDir); const outputPath = join(outputDirectory, `${dirName}.${options.format || 'zip'}`); const result = await this.createArchive([sourceDir], outputPath, options); if (result.success && result.data) { results.push(result.data); } else { errors.push(`${sourceDir}: ${result.message}`); } } catch (error) { errors.push(`${sourceDir}: ${error instanceof Error ? error.message : String(error)}`); } } if (errors.length > 0) { logger.warn(`複数アーカイブ作成で一部エラー発生`, { errors }); return createSuccessResult( `${results.length}/${sourceDirectories.length}ディレクトリのアーカイブ作成に成功。エラー: ${errors.join(', ')}`, results ); } logger.info(`複数アーカイブ作成完了: ${results.length}アーカイブ`); return createSuccessResult(`${results.length}個のアーカイブが正常に作成されました`, results); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); logger.error(`複数アーカイブ作成エラー`, { error }); return createFailureResult(`複数アーカイブ作成に失敗しました: ${errorMessage}`, error instanceof Error ? error : new Error(errorMessage)); } } /** * 実際の圧縮処理(内部用) */ private async performCompression( sourcePaths: string[], outputPath: string, options: ArchiveOptions ): Promise<ArchiveInfo> { return new Promise((resolve, reject) => { const output = createWriteStream(outputPath); const archive = archiver(options.format as any, { zlib: { level: options.compressionLevel } }); let fileCount = 0; let totalSize = 0; const files: any[] = []; // エラーハンドリング archive.on('error', reject); output.on('error', reject); // 進行状況追跡 archive.on('entry', (entry) => { fileCount++; totalSize += entry.stats?.size || 0; files.push({ name: entry.name, path: entry.name, size: entry.stats?.size || 0, compressedSize: 0, // 圧縮後に更新 isDirectory: entry.stats?.isDirectory() || false, modified: entry.stats?.mtime || new Date() }); }); // 完了処理 output.on('close', async () => { try { const stats = await fs.stat(outputPath); const compressedSize = stats.size; const compressionRatio = totalSize > 0 ? compressedSize / totalSize : 0; const archiveInfo: ArchiveInfo = { archivePath: outputPath, format: options.format, fileCount, totalSize, compressedSize, compressionRatio, created: new Date(), files }; resolve(archiveInfo); } catch (error) { reject(error); } }); // パイプ設定 archive.pipe(output); // ファイル追加 this.addFilesToArchive(archive, sourcePaths, options) .then(() => archive.finalize()) .catch(reject); }); } /** * 実際の展開処理(内部用) */ private async performExtraction( archivePath: string, outputDirectory: string, format: string, password?: string ): Promise<string[]> { const extractedFiles: string[] = []; if (format === 'zip') { return new Promise((resolve, reject) => { const stream = createReadStream(archivePath) .pipe(unzipper.Parse({ forceStream: true })); stream.on('entry', (entry: any) => { const fileName = entry.path; const type = entry.type; const outputPath = join(outputDirectory, fileName); if (type === 'File') { ensureDirectory(dirname(outputPath)) .then(() => { entry.pipe(createWriteStream(outputPath)); extractedFiles.push(outputPath); }) .catch(reject); } else { entry.autodrain(); } }); stream.on('error', reject); stream.on('close', () => resolve(extractedFiles)); }); } throw new Error(`未対応の形式: ${format}`); } /** * アーカイブ分析(内部用) */ private async analyzeArchive(archivePath: string, format: string): Promise<ArchiveInfo> { const stats = await fs.stat(archivePath); // 基本情報 const archiveInfo: ArchiveInfo = { archivePath, format, fileCount: 0, totalSize: 0, compressedSize: stats.size, compressionRatio: 0, created: stats.mtime, files: [] }; if (format === 'zip') { return new Promise((resolve, reject) => { const files: any[] = []; let totalSize = 0; createReadStream(archivePath) .pipe(unzipper.Parse({ forceStream: true })) .on('entry', (entry: any) => { files.push({ name: basename(entry.path), path: entry.path, size: entry.vars?.uncompressedSize || 0, compressedSize: entry.vars?.compressedSize || 0, isDirectory: entry.type === 'Directory', modified: entry.vars?.lastModified || new Date() }); totalSize += entry.vars?.uncompressedSize || 0; entry.autodrain(); }) .on('error', reject) .on('close', () => { archiveInfo.fileCount = files.length; archiveInfo.totalSize = totalSize; archiveInfo.compressionRatio = totalSize > 0 ? stats.size / totalSize : 0; archiveInfo.files = files; resolve(archiveInfo); }); }); } return archiveInfo; } /** * アーカイブにファイル追加(内部用) */ private async addFilesToArchive( archive: archiver.Archiver, sourcePaths: string[], options: ArchiveOptions ): Promise<void> { for (const sourcePath of sourcePaths) { const stats = await fs.stat(sourcePath); if (stats.isDirectory()) { // ディレクトリの場合、再帰的に追加 const files = await getFilesRecursively(sourcePath, { includeHidden: false, fileFilter: (filePath) => this.shouldIncludeFile(filePath, options) }); for (const file of files) { const relativePath = file.replace(sourcePath + '/', ''); archive.file(file, { name: relativePath }); } } else { // ファイルの場合、直接追加 if (this.shouldIncludeFile(sourcePath, options)) { archive.file(sourcePath, { name: basename(sourcePath) }); } } } } /** * ファイルをアーカイブに含めるかチェック(内部用) */ private shouldIncludeFile(filePath: string, options: ArchiveOptions): boolean { const fileName = basename(filePath); // 除外パターンチェック if (options.excludePatterns) { for (const pattern of options.excludePatterns) { if (this.matchPattern(filePath, pattern) || this.matchPattern(fileName, pattern)) { return false; } } } // 含むパターンチェック if (options.includePatterns && options.includePatterns.length > 0) { for (const pattern of options.includePatterns) { if (this.matchPattern(filePath, pattern) || this.matchPattern(fileName, pattern)) { return true; } } return false; } return true; } /** * パターンマッチング(内部用) */ private matchPattern(text: string, pattern: string): boolean { // 簡単なglob風パターンマッチング const regexPattern = pattern .replace(/\./g, '\\.') .replace(/\*\*/g, '.*') .replace(/\*/g, '[^/]*') .replace(/\?/g, '[^/]'); return new RegExp(`^${regexPattern}$`).test(text); } /** * アーカイブ形式検出(内部用) */ private detectArchiveFormat(archivePath: string): string { const ext = extname(archivePath).toLowerCase(); switch (ext) { case '.zip': return 'zip'; case '.tar': return 'tar'; case '.gz': case '.tgz': return 'tar.gz'; case '.bz2': return 'tar.bz2'; case '.7z': return '7z'; default: return 'zip'; // デフォルト } } }

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/mokemoke0821/claude-mcp-integration'

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