Skip to main content
Glama
storage.ts6.11 kB
import { promises as fs } from 'fs'; import path from 'path'; import { logger } from './logger.js'; export interface StorageItem { content: string; metadata: { createdAt: string; updatedAt: string; version: number; }; } export class FileStorage { private baseDir: string; private locks: Map<string, Promise<void>>; constructor(baseDir: string = './data') { this.baseDir = baseDir; this.locks = new Map(); this.initializeStorage(); } private async initializeStorage() { try { await fs.mkdir(this.baseDir, { recursive: true }); logger.info('Storage initialized', { baseDir: this.baseDir }); } catch (error) { logger.error('Failed to initialize storage', error as Error, { baseDir: this.baseDir }); throw error; } } private getFilePath(key: string): string { // セキュリティ: パストラバーサル対策 const safeName = key.replace(/[^a-zA-Z0-9_-]/g, '_'); return path.join(this.baseDir, `${safeName}.json`); } // 非同期書き込み制御(ロック機構) private async acquireLock(key: string): Promise<() => void> { while (this.locks.has(key)) { await this.locks.get(key); } let releaseLock: () => void; const lockPromise = new Promise<void>((resolve) => { releaseLock = resolve; }); this.locks.set(key, lockPromise); return () => { this.locks.delete(key); releaseLock!(); }; } async read(key: string, requestId: string): Promise<string | null> { const filePath = this.getFilePath(key); try { logger.debug('Reading file', { operation: 'read', key, filePath, requestId }); const data = await fs.readFile(filePath, 'utf-8'); const item: StorageItem = JSON.parse(data); logger.info('File read successfully', { operation: 'read', key, version: item.metadata.version, requestId }); return item.content; } catch (error) { if ((error as NodeJS.ErrnoException).code === 'ENOENT') { logger.warn('File not found', { operation: 'read', key, requestId }); return null; } logger.error('Failed to read file', error as Error, { operation: 'read', key, requestId }); throw error; } } async write(key: string, content: string, requestId: string): Promise<void> { const releaseLock = await this.acquireLock(key); const filePath = this.getFilePath(key); try { logger.debug('Writing file', { operation: 'write', key, filePath, requestId }); let currentVersion = 0; let createdAt = new Date().toISOString(); try { const existing = await fs.readFile(filePath, 'utf-8'); const existingItem: StorageItem = JSON.parse(existing); currentVersion = existingItem.metadata.version; createdAt = existingItem.metadata.createdAt; } catch { // ファイルが存在しない場合は新規作成 } const item: StorageItem = { content, metadata: { createdAt, updatedAt: new Date().toISOString(), version: currentVersion + 1 } }; await fs.writeFile(filePath, JSON.stringify(item, null, 2), 'utf-8'); logger.info('File written successfully', { operation: 'write', key, version: item.metadata.version, requestId }); } catch (error) { logger.error('Failed to write file', error as Error, { operation: 'write', key, requestId }); throw error; } finally { releaseLock(); } } async delete(key: string, requestId: string): Promise<boolean> { const releaseLock = await this.acquireLock(key); const filePath = this.getFilePath(key); try { logger.debug('Deleting file', { operation: 'delete', key, filePath, requestId }); await fs.unlink(filePath); logger.info('File deleted successfully', { operation: 'delete', key, requestId }); return true; } catch (error) { if ((error as NodeJS.ErrnoException).code === 'ENOENT') { logger.warn('File not found for deletion', { operation: 'delete', key, requestId }); return false; } logger.error('Failed to delete file', error as Error, { operation: 'delete', key, requestId }); throw error; } finally { releaseLock(); } } async list(pattern?: string, requestId?: string): Promise<string[]> { try { logger.debug('Listing files', { operation: 'list', pattern, requestId }); const files = await fs.readdir(this.baseDir); const jsonFiles = files .filter(f => f.endsWith('.json')) .map(f => f.replace('.json', '').replace(/_/g, '/')); const filtered = pattern ? jsonFiles.filter(f => f.includes(pattern)) : jsonFiles; logger.info('Files listed successfully', { operation: 'list', count: filtered.length, pattern, requestId }); return filtered; } catch (error) { logger.error('Failed to list files', error as Error, { operation: 'list', requestId }); throw error; } } async search(query: string, requestId: string): Promise<Array<{ key: string; content: string }>> { try { logger.debug('Searching files', { operation: 'search', query, requestId }); const keys = await this.list(undefined, requestId); const results: Array<{ key: string; content: string }> = []; for (const key of keys) { const content = await this.read(key, requestId); if (content && content.toLowerCase().includes(query.toLowerCase())) { results.push({ key, content }); } } logger.info('Search completed', { operation: 'search', query, resultsCount: results.length, requestId }); return results; } catch (error) { logger.error('Failed to search files', error as Error, { operation: 'search', query, requestId }); throw error; } } }

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/Amana03/universal-mcp-server'

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