/** * 文件系统存储适配器实现 * 将数据存储在文件系统中的适配器,适用于轻量级应用和简单场景 */ import { StorageAdapter, StorageConfig, QueryCondition, SortOption } from '../../interfaces/StorageAdapter'; /** * 简单的ID生成函数替代nanoid */ function generateSimpleId(): string { return Date.now().toString(36) + Math.random().toString(36).substr(2, 5); } // @ts-ignore - 临时忽略 fs-extra 模块导入错误 import * as fs from 'fs-extra'; import * as path from 'path'; import { promisify } from 'util'; import { createReadStream, createWriteStream } from 'fs'; /** * 文件系统存储适配器配置接口 */ export interface FileSystemStorageConfig extends StorageConfig { /** * 存储根目录路径 */ rootDir: string; /** * 文件扩展名 */ fileExtension?: string; /** * 是否启用事务性操作 */ enableTransactions?: boolean; /** * 是否在保存前格式化JSON */ prettyPrint?: boolean; /** * 最大并发读写文件数量 */ maxConcurrentOps?: number; } /** * 文件系统存储适配器类 */ export class FileSystemStorageAdapter implements StorageAdapter { /** * 配置项 */ private config: FileSystemStorageConfig; /** * 是否已初始化 */ private initialized: boolean = false; /** * 并发操作控制计数器 */ private concurrentOps: number = 0; /** * 构造函数 */ constructor() { this.config = { rootDir: path.join(process.cwd(), 'data'), fileExtension: 'json', enableTransactions: false, prettyPrint: false, maxConcurrentOps: 100, } as any; } /** * 初始化适配器 * @param config 适配器配置 */ public async initialize(config: FileSystemStorageConfig): Promise { // 合并配置 this.config = { ...this.config, ...config, type: 'file' } as any; // 确保根目录存在 await fs.ensureDir(this.config.rootDir); this.initialized = true; } /** * 关闭适配器 */ public async close(): Promise { if (!this.initialized) { return; } // 等待所有并发操作完成 while (this.concurrentOps > 0) { await new Promise(resolve => setTimeout(resolve, 10)); } this.initialized = false; } /** * 创建新数据项 * @param namespace 命名空间 * @param data 数据对象 * @returns 创建的数据对象,包含ID */ public async create(namespace: string, data: Partial): Promise { this.ensureInitialized(); try { await this.ensureNamespaceExists(namespace); // 生成唯一ID const id = (data as any).id || generateSimpleId(); // 清理ID,防止路径遍历攻击并确保一致性 const safeId = id.replace(/[\\\/\:\*\?"\'\|\u0000-\u001f]/g, '_'); const timestamp = Date.now(); // 创建完整的数据对象 const item = { ...data, id: safeId, createdAt: timestamp, updatedAt: timestamp }; // 写入文件 const filePath = this.getItemFilePath(namespace, id); await this.writeWithConcurrencyControl(filePath, item); return item; } catch (error) { console.error(`Error creating item in namespace ${namespace}:`, error); throw error; // Re-throw for create operations since they should fail explicitly } } /** * 根据ID获取数据项 * @param namespace 命名空间 * @param id 数据项ID * @returns 数据项,如果不存在则返回null */ public async getById(namespace: string, id: string): Promise { this.ensureInitialized(); const filePath = this.getItemFilePath(namespace, id); try { // 检查文件是否存在 const exists = await fs.pathExists(filePath); if (!exists) { return null; } // 读取文件 this.concurrentOps++; const content = await fs.readFile(filePath, 'utf8'); this.concurrentOps--; // 解析JSON return JSON.parse(content) as T; } catch (error) { this.concurrentOps--; console.error(`Error reading file ${filePath}:`, error); return null; } } /** * 查找符合条件的数据项 * @param namespace 命名空间 * @param query 查询条件 * @param skip 跳过的记录数 * @param limit 返回的最大记录数 * @param sort 排序条件 * @returns 查询结果数组和总数 */ public async find( namespace: string, query?: QueryCondition, skip: number = 0, limit: number = 100, sort?: SortOption ): Promise<{ data: T[]; total: number }> { this.ensureInitialized(); // 获取所有文件路径 const namespacePath = this.getNamespacePath(namespace); let filePaths: string[] = []; try { const exists = await fs.pathExists(namespacePath); if (exists) { filePaths = await fs.readdir(namespacePath); filePaths = filePaths .filter(filename => filename.endsWith(this.config.fileExtension || '.json')) .map(filename => path.join(namespacePath, filename)); } } catch (error) { console.error(`Error reading namespace directory ${namespacePath}:`, error); return { data: [], total: 0 }; } // 读取所有文件内容 const items: T[] = []; for (const filePath of filePaths) { try { this.concurrentOps++; const content = await fs.readFile(filePath, 'utf8'); this.concurrentOps--; try { const item = JSON.parse(content) as T; items.push(item); } catch (parseError) { console.error(`Error parsing JSON from ${filePath}:`, parseError); // 跳过无效的JSON文件 continue; } } catch (readError) { this.concurrentOps--; console.error(`Error reading file ${filePath}:`, readError); // 跳过读取失败的文件 continue; } } // 应用查询条件 const filteredItems = this.applyQuery(items, query); const total = filteredItems.length; // 应用排序 const sortedItems = this.applySort(filteredItems, sort); // 应用分页 const paginatedItems = sortedItems.slice(skip, skip + limit); return { data: paginatedItems, total }; } /** * 更新数据项 * @param namespace 命名空间 * @param id 数据项ID * @param data 更新的数据 * @returns 更新后的数据项,如果不存在则返回null */ public async update(namespace: string, id: string, data: Partial): Promise { this.ensureInitialized(); // 首先检查数据项是否存在 const existingItem = await this.getById(namespace, id); if (!existingItem) { return null; } const timestamp = Date.now(); // 创建更新后的数据对象 const updatedItem: T = { ...existingItem, ...data, id, updatedAt: timestamp } as T; // 写入文件 - 优雅处理错误 try { const filePath = this.getItemFilePath(namespace, id); await this.writeWithConcurrencyControl(filePath, updatedItem); return updatedItem; } catch (error) { console.error(`Error updating item ${id} in namespace ${namespace}:`, error); return null; } } /** * 删除数据项 * @param namespace 命名空间 * @param id 数据项ID * @returns 是否成功删除 */ public async delete(namespace: string, id: string): Promise { this.ensureInitialized(); const filePath = this.getItemFilePath(namespace, id); try { const exists = await fs.pathExists(filePath); if (!exists) { return false; } this.concurrentOps++; await fs.unlink(filePath); this.concurrentOps--; return true; } catch (error) { if (this.concurrentOps > 0) { this.concurrentOps--; } console.error(`Error deleting file ${filePath}:`, error); return false; } } /** * 批量创建数据项 * @param namespace 命名空间 * @param dataList 数据对象数组 * @returns 创建的数据对象数组,包含ID */ public async bulkCreate(namespace: string, dataList: Partial[]): Promise { this.ensureInitialized(); await this.ensureNamespaceExists(namespace); const results: any[] = []; const timestamp = Date.now(); for (const data of dataList) { // 生成唯一ID const id = (data as any).id || generateSimpleId(); // 清理ID,防止路径遍历攻击并确保一致性 const safeId = id.replace(/[\\\/\:\*\?"\'\|\u0000-\u001f]/g, '_'); // 创建完整的数据对象 const item = { ...data, id: safeId, createdAt: timestamp, updatedAt: timestamp }; // 写入文件 const filePath = this.getItemFilePath(namespace, id); await this.writeWithConcurrencyControl(filePath, item); results.push(item as any); } return results; } /** * 检查适配器是否可用 * @returns 是否可用 */ public async isAvailable(): Promise { if (!this.initialized) { return false; } // 检查根目录是否可访问 try { await fs.access(this.config.rootDir, fs.constants.R_OK | fs.constants.W_OK); return true; } catch (error) { console.error(`FileSystemStorageAdapter root directory not accessible: ${this.config.rootDir}`, error); return false; } } /** * 确保适配器已初始化 */ private ensureInitialized(): void { if (!this.initialized) { throw new Error('FileSystemStorageAdapter is not initialized'); } } /** * 确保命名空间目录存在 * @param namespace 命名空间 */ private async ensureNamespaceExists(namespace: string): Promise { const namespacePath = this.getNamespacePath(namespace); await fs.ensureDir(namespacePath); } /** * 获取命名空间目录路径 * @param namespace 命名空间 * @returns 目录路径 */ private getNamespacePath(namespace: string): string { // 清理命名空间名称,防止路径遍历攻击 const safeNamespace = namespace.replace(/[\\\/\:\*\?"\'\|\u0000-\u001f]/g, '_'); return path.join(this.config.rootDir, safeNamespace); } /** * 获取数据项文件路径 * @param namespace 命名空间 * @param id 数据项ID * @returns 文件路径 */ private getItemFilePath(namespace: string, id: string): string { // 清理ID,防止路径遍历攻击 const safeId = id.replace(/[\\\/\:\*\?"\'\|\u0000-\u001f]/g, '_'); const fileExtension = this.config.fileExtension || 'json'; return path.join(this.getNamespacePath(namespace), `${safeId}.${fileExtension}`); } /** * 控制并发并写入文件 * @param filePath 文件路径 * @param data 要写入的数据 */ private async writeWithConcurrencyControl(filePath: string, data: any): Promise { // 等待直到并发操作数低于限制 while (this.concurrentOps >= (this.config.maxConcurrentOps || 100)) { await new Promise(resolve => setTimeout(resolve, 10)); } try { this.concurrentOps++; // 序列化数据 const jsonContent = this.config.prettyPrint ? JSON.stringify(data, null, 2) : JSON.stringify(data); // 确保文件目录存在 const dirPath = path.dirname(filePath); await fs.ensureDir(dirPath); // 如果启用了事务性操作,先写入临时文件,然后重命名 if (this.config.enableTransactions) { // Use a unique temporary filename for better concurrent handling const tempFilePath = `${filePath}.${Date.now()}.${Math.random().toString(36).substr(2, 9)}.tmp`; try { await fs.writeFile(tempFilePath, jsonContent, 'utf8'); await fs.rename(tempFilePath, filePath); } catch (renameError: any) { // Clean up temporary file if rename fails try { if (await fs.pathExists(tempFilePath)) { await fs.unlink(tempFilePath); } } catch (cleanupError) { console.error(`Error cleaning up temporary file ${tempFilePath}:`, cleanupError); } console.error(`Error writing to file ${filePath}:`, renameError); throw renameError; } } else { // 直接写入文件 await fs.writeFile(filePath, jsonContent, 'utf8'); } } catch (error: any) { console.error(`Error writing to file ${filePath}:`, error); // Don't throw - let the caller handle the error based on context throw error; } finally { this.concurrentOps--; } } /** * 应用查询条件过滤数据 * @param items 数据项数组 * @param query 查询条件 * @returns 过滤后的数据项数组 */ private applyQuery(items: T[], query?: QueryCondition): T[] { if (!query || Object.keys(query).length === 0) { return items; } return items.filter(item => { for (const [key, value] of Object.entries(query)) { const itemValue = (item as any)[key]; if (value === null || value === undefined) { if (itemValue !== null && itemValue !== undefined) { return false; } } else if (typeof value === 'object' && !Array.isArray(value)) { // 处理比较操作符 for (const [operator, compareValue] of Object.entries(value)) { switch (operator) { case '$eq': if (itemValue !== compareValue) return false; break; case '$neq': if (itemValue === compareValue) return false; break; case '$gt': if (itemValue <= (compareValue as number)) return false; break; case '$gte': if (itemValue < (compareValue as number)) return false; break; case '$lt': if (itemValue >= (compareValue as number)) return false; break; case '$lte': if (itemValue > (compareValue as number)) return false; break; case '$in': if (!Array.isArray(compareValue) || !compareValue.includes(itemValue)) { return false; } break; case '$nin': if (!Array.isArray(compareValue) || compareValue.includes(itemValue)) { return false; } break; case '$like': if (typeof compareValue === 'string' && typeof itemValue === 'string') { const regex = new RegExp(compareValue.replace(/%/g, '.*')); if (!regex.test(itemValue)) return false; } else { return false; } break; default: // 未知操作符,忽略 break; } } } else if (Array.isArray(value)) { // 数组包含查询 if (!Array.isArray(itemValue) || !value.every(v => itemValue.includes(v))) { return false; } } else { // 直接相等比较 if (itemValue !== value) { return false; } } } return true; }); } /** * 应用排序条件 * @param items 数据项数组 * @param sort 排序条件 * @returns 排序后的数据项数组 */ private applySort(items: T[], sort?: SortOption): T[] { if (!sort || Object.keys(sort).length === 0) { return [...items]; } return [...items].sort((a, b) => { for (const [key, direction] of Object.entries(sort)) { const aValue = (a as any)[key]; const bValue = (b as any)[key]; const dirValue = typeof direction === 'string' ? (direction === 'asc' ? 1 : -1) : direction; if (aValue < bValue) return dirValue === 1 ? -1 : 1; if (aValue > bValue) return dirValue === 1 ? 1 : -1; } return 0; }); } }