UNPKG

qmemory

Version:

A comprehensive production-ready Node.js utility library with MongoDB document operations, user ownership enforcement, Express.js HTTP utilities, environment-aware logging, and in-memory storage. Features 96%+ test coverage with comprehensive error handli

455 lines (397 loc) 12.9 kB
/** * Binary Storage Interface and Implementations * * Provides a unified interface for storing and retrieving binary data (Buffer objects) * with multiple implementation strategies: * - In-memory storage for development and testing * - Object storage for production persistence * - File system storage for local persistence * * Design Philosophy: * - Simple, async interface for all storage operations * - Graceful error handling with meaningful error messages * - Environment-aware implementation selection * - Buffer-based binary data handling for maximum flexibility */ const fs = require('fs').promises; const path = require('path'); const crypto = require('crypto'); /** * IStorage Interface Definition (JavaScript implementation) * * This interface defines the contract for binary data storage operations. * All implementations must provide these four core methods. */ class IStorage { /** * Store binary data with a unique key * @param {string} key - Unique identifier for the stored data * @param {Buffer} data - Binary data to store (e.g., processed images) * @returns {Promise<void>} */ async save(key, data) { throw new Error('save method must be implemented by storage provider'); } /** * Retrieve binary data by key * @param {string} key - Unique identifier for the data to retrieve * @returns {Promise<Buffer|null>} Binary data if found, null if not found */ async get(key) { throw new Error('get method must be implemented by storage provider'); } /** * Remove stored data by key * @param {string} key - Unique identifier for the data to delete * @returns {Promise<void>} */ async delete(key) { throw new Error('delete method must be implemented by storage provider'); } /** * Check if data exists for the given key * @param {string} key - Unique identifier to check for existence * @returns {Promise<boolean>} True if data exists, false otherwise */ async exists(key) { throw new Error('exists method must be implemented by storage provider'); } /** * Get storage statistics (optional method for monitoring) * @returns {Promise<Object>} Storage usage statistics */ async getStats() { return { type: 'unknown', itemCount: 0, totalSize: 0 }; } } /** * In-Memory Binary Storage Implementation * * Fast, volatile storage using JavaScript Map. * Perfect for development, testing, and caching scenarios. * Data is lost when the process restarts. * * Features: * - O(1) read/write performance * - Built-in size tracking * - Memory usage monitoring * - Automatic cleanup capabilities */ class MemoryBinaryStorage extends IStorage { constructor(maxSize = 100 * 1024 * 1024) { // 100MB default limit super(); this.storage = new Map(); // key -> Buffer mapping this.maxSize = maxSize; // Maximum total storage size in bytes this.currentSize = 0; // Current total size in bytes console.log(`Initialized MemoryBinaryStorage with ${maxSize} bytes limit`); } /** * Validate key format and ensure it's safe for storage */ _validateKey(key) { if (typeof key !== 'string' || key.length === 0) { throw new Error('Key must be a non-empty string'); } if (key.length > 250) { throw new Error('Key must be 250 characters or less'); } // Prevent path traversal and ensure safe key format if (key.includes('..') || key.includes('/') || key.includes('\\')) { throw new Error('Key cannot contain path separators or relative paths'); } } /** * Validate data is a Buffer object */ _validateData(data) { if (!Buffer.isBuffer(data)) { throw new Error('Data must be a Buffer object'); } } /** * Check if adding new data would exceed size limit */ _checkSizeLimit(newDataSize) { if (this.currentSize + newDataSize > this.maxSize) { throw new Error(`Storage size limit exceeded. Current: ${this.currentSize}, New: ${newDataSize}, Limit: ${this.maxSize}`); } } async save(key, data) { this._validateKey(key); this._validateData(data); // Calculate size difference for existing vs new data const existingSize = this.storage.has(key) ? this.storage.get(key).length : 0; const sizeDifference = data.length - existingSize; this._checkSizeLimit(sizeDifference); // Store the data and update size tracking this.storage.set(key, Buffer.from(data)); // Create copy to prevent external mutations this.currentSize += sizeDifference; console.log(`Stored ${data.length} bytes at key '${key}'. Total storage: ${this.currentSize} bytes`); } async get(key) { this._validateKey(key); const data = this.storage.get(key); if (!data) { return null; } // Return a copy to prevent external mutations return Buffer.from(data); } async delete(key) { this._validateKey(key); const data = this.storage.get(key); if (data) { this.currentSize -= data.length; this.storage.delete(key); console.log(`Deleted data at key '${key}'. Remaining storage: ${this.currentSize} bytes`); } } async exists(key) { this._validateKey(key); return this.storage.has(key); } async getStats() { return { type: 'memory', itemCount: this.storage.size, totalSize: this.currentSize, maxSize: this.maxSize, utilizationPercent: Math.round((this.currentSize / this.maxSize) * 100), keys: Array.from(this.storage.keys()) }; } /** * Clear all stored data (useful for testing) */ async clear() { this.storage.clear(); this.currentSize = 0; console.log('Cleared all data from memory storage'); } } /** * File System Binary Storage Implementation * * Persistent storage using the local file system. * Good for local development and single-server deployments. * * Features: * - Persistent across application restarts * - Configurable storage directory * - Atomic write operations * - Automatic directory creation */ class FileSystemBinaryStorage extends IStorage { constructor(storageDir = './data/binary-storage') { super(); this.storageDir = path.resolve(storageDir); this._ensureDirectoryExists(); console.log(`Initialized FileSystemBinaryStorage at ${this.storageDir}`); } async _ensureDirectoryExists() { try { await fs.mkdir(this.storageDir, { recursive: true }); } catch (error) { throw new Error(`Failed to create storage directory: ${error.message}`); } } _validateKey(key) { if (typeof key !== 'string' || key.length === 0) { throw new Error('Key must be a non-empty string'); } // Ensure safe file names if (key.includes('..') || key.includes('/') || key.includes('\\') || key.includes('\0')) { throw new Error('Key contains invalid characters for file system storage'); } } _getFilePath(key) { // Hash the key to ensure safe file names and avoid conflicts const hash = crypto.createHash('sha256').update(key).digest('hex'); return path.join(this.storageDir, `${hash}.bin`); } async save(key, data) { this._validateKey(key); if (!Buffer.isBuffer(data)) { throw new Error('Data must be a Buffer object'); } const filePath = this._getFilePath(key); const tempPath = `${filePath}.tmp`; try { // Atomic write: write to temp file then rename await fs.writeFile(tempPath, data); await fs.rename(tempPath, filePath); // Store key mapping for reverse lookup const metaPath = `${filePath}.meta`; await fs.writeFile(metaPath, JSON.stringify({ key, size: data.length, created: new Date().toISOString() })); console.log(`Stored ${data.length} bytes at key '${key}' in file system`); } catch (error) { // Clean up temp file if it exists try { await fs.unlink(tempPath); } catch (cleanupError) { // Ignore cleanup errors } throw new Error(`Failed to save data: ${error.message}`); } } async get(key) { this._validateKey(key); const filePath = this._getFilePath(key); try { const data = await fs.readFile(filePath); return data; } catch (error) { if (error.code === 'ENOENT') { return null; // File doesn't exist } throw new Error(`Failed to read data: ${error.message}`); } } async delete(key) { this._validateKey(key); const filePath = this._getFilePath(key); const metaPath = `${filePath}.meta`; try { await fs.unlink(filePath); try { await fs.unlink(metaPath); } catch (metaError) { // Meta file might not exist, ignore error } console.log(`Deleted data at key '${key}' from file system`); } catch (error) { if (error.code !== 'ENOENT') { throw new Error(`Failed to delete data: ${error.message}`); } // File doesn't exist, which is fine for delete operation } } async exists(key) { this._validateKey(key); const filePath = this._getFilePath(key); try { await fs.access(filePath); return true; } catch (error) { return false; } } async getStats() { try { const files = await fs.readdir(this.storageDir); const dataFiles = files.filter(f => f.endsWith('.bin')); let totalSize = 0; const keys = []; for (const file of dataFiles) { const filePath = path.join(this.storageDir, file); const metaPath = `${filePath}.meta`; try { const stats = await fs.stat(filePath); totalSize += stats.size; // Try to read the original key from meta file try { const metaData = await fs.readFile(metaPath, 'utf8'); const meta = JSON.parse(metaData); keys.push(meta.key); } catch (metaError) { keys.push(file.replace('.bin', '')); } } catch (statError) { // Skip files we can't read } } return { type: 'filesystem', itemCount: dataFiles.length, totalSize, storageDir: this.storageDir, keys }; } catch (error) { return { type: 'filesystem', itemCount: 0, totalSize: 0, error: error.message }; } } } /** * Storage Factory * * Creates the appropriate storage implementation based on environment * and configuration. Provides a unified way to get storage instances. */ class StorageFactory { /** * Create a storage instance based on configuration * @param {Object} options - Configuration options * @param {string} options.type - Storage type: 'memory', 'filesystem', 'object' * @param {Object} options.config - Type-specific configuration * @returns {IStorage} Storage implementation instance */ static createStorage(options = {}) { const { type = 'memory', config = {} } = options; switch (type.toLowerCase()) { case 'memory': return new MemoryBinaryStorage(config.maxSize); case 'filesystem': case 'file': return new FileSystemBinaryStorage(config.storageDir); case 'object': case 'cloud': try { const { ObjectStorageBinaryStorage } = require('./object-storage-binary'); return new ObjectStorageBinaryStorage(); } catch (error) { console.warn(`Failed to initialize object storage: ${error.message}, falling back to memory storage`); return new MemoryBinaryStorage(); } default: console.warn(`Unknown storage type '${type}', falling back to memory storage`); return new MemoryBinaryStorage(); } } /** * Create storage based on environment variables */ static createFromEnvironment() { const storageType = process.env.BINARY_STORAGE_TYPE || 'memory'; const config = {}; if (storageType === 'filesystem') { config.storageDir = process.env.BINARY_STORAGE_DIR; } else if (storageType === 'memory') { config.maxSize = process.env.BINARY_STORAGE_MAX_SIZE ? parseInt(process.env.BINARY_STORAGE_MAX_SIZE) : undefined; } return StorageFactory.createStorage({ type: storageType, config }); } } // Default storage instance for easy access let defaultStorage = null; /** * Get the default storage instance * Creates one if it doesn't exist yet */ function getDefaultStorage() { if (!defaultStorage) { defaultStorage = StorageFactory.createFromEnvironment(); } return defaultStorage; } module.exports = { IStorage, MemoryBinaryStorage, FileSystemBinaryStorage, StorageFactory, getDefaultStorage };