/**
* File storage and management for MCP server
*/
import fs from 'fs/promises';
import path from 'path';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
export class StorageManager {
constructor(dataDir = null) {
this.dataDir = dataDir || path.join(process.cwd(), 'data');
this.commandsDir = path.join(this.dataDir, 'commands');
this.outputsDir = path.join(this.dataDir, 'outputs');
this.tempDir = path.join(this.dataDir, 'temp');
}
/**
* Initialize storage directories
*/
async initialize() {
try {
await fs.mkdir(this.commandsDir, { recursive: true });
await fs.mkdir(this.outputsDir, { recursive: true });
await fs.mkdir(this.tempDir, { recursive: true });
} catch (error) {
throw new Error(`Failed to initialize storage directories: ${error.message}`);
}
}
/**
* Generate a unique filename with timestamp and random string
* @param {string} prefix - Filename prefix
* @param {string} extension - File extension (without dot)
* @returns {string}
*/
generateFilename(prefix, extension) {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const random = Math.random().toString(36).substring(2, 8);
return `${prefix}_${timestamp}_${random}.${extension}`;
}
/**
* Get full path for a command file
* @param {string} filename - Optional specific filename
* @returns {string}
*/
getCommandPath(filename) {
return path.join(this.commandsDir, filename || this.generateFilename('cmd', 'txt'));
}
/**
* Get full path for an output file
* @param {string} filename - Optional specific filename
* @returns {string}
*/
getOutputPath(filename) {
return path.join(this.outputsDir, filename || this.generateFilename('result', 'json'));
}
/**
* Get full path for a temporary file
* @param {string} filename - Optional specific filename
* @returns {string}
*/
getTempPath(filename) {
return path.join(this.tempDir, filename || this.generateFilename('temp', 'tmp'));
}
/**
* Save command file
* @param {string} content - Command file content
* @param {string} filename - Optional specific filename
* @returns {Promise<string>} Path to saved file
*/
async saveCommand(content, filename) {
const filepath = this.getCommandPath(filename);
try {
await fs.writeFile(filepath, content, 'utf8');
return filepath;
} catch (error) {
throw new Error(`Failed to save command file: ${error.message}`);
}
}
/**
* Read command file
* @param {string} filepath - Path to command file
* @returns {Promise<string>}
*/
async readCommand(filepath) {
try {
return await fs.readFile(filepath, 'utf8');
} catch (error) {
throw new Error(`Failed to read command file: ${error.message}`);
}
}
/**
* Check if output file exists and has content
* @param {string} filepath - Path to output file
* @returns {Promise<boolean>}
*/
async outputExists(filepath) {
try {
const stats = await fs.stat(filepath);
return stats.isFile() && stats.size > 0;
} catch {
return false;
}
}
/**
* List all output files with metadata
* @param {number} limit - Maximum number of results (default: 20)
* @param {string} sortBy - Sort by "date" (default) or "size"
* @returns {Promise<Array>}
*/
async listOutputs(limit = 20, sortBy = 'date') {
try {
const files = await fs.readdir(this.outputsDir);
const jsonFiles = files.filter(f => f.endsWith('.json'));
const fileStats = await Promise.all(
jsonFiles.map(async (filename) => {
const filepath = path.join(this.outputsDir, filename);
const stats = await fs.stat(filepath);
// Try to find associated command file
const cmdFilename = filename
.replace('result_', 'cmd_')
.replace('.json', '.txt');
const cmdPath = path.join(this.commandsDir, cmdFilename);
let associatedCommand = null;
try {
await fs.access(cmdPath);
associatedCommand = cmdPath;
} catch {}
return {
filename,
path: filepath,
size_mb: (stats.size / (1024 * 1024)).toFixed(2),
created_at: stats.birthtime.toISOString(),
associated_command: associatedCommand
};
})
);
// Sort
if (sortBy === 'size') {
fileStats.sort((a, b) => parseFloat(b.size_mb) - parseFloat(a.size_mb));
} else {
fileStats.sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
}
return fileStats.slice(0, limit);
} catch (error) {
throw new Error(`Failed to list outputs: ${error.message}`);
}
}
/**
* Clean up old files older than specified days
* @param {number} daysOld - Delete files older than this many days (default: 7)
* @returns {Promise<Object>} Count of deleted files
*/
async cleanOldFiles(daysOld = 7) {
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - daysOld);
const dirs = [this.commandsDir, this.outputsDir, this.tempDir];
let deletedCount = 0;
try {
for (const dir of dirs) {
try {
const files = await fs.readdir(dir);
for (const file of files) {
const filepath = path.join(dir, file);
const stats = await fs.stat(filepath);
if (stats.birthtime < cutoffDate) {
await fs.unlink(filepath);
deletedCount++;
}
}
} catch (error) {
// Directory might not exist yet, skip
}
}
return { deleted_count: deletedCount };
} catch (error) {
throw new Error(`Failed to clean old files: ${error.message}`);
}
}
/**
* Get storage statistics
* @returns {Promise<Object>}
*/
async getStats() {
try {
const getDirectorySize = async (dir) => {
try {
const files = await fs.readdir(dir);
let totalSize = 0;
for (const file of files) {
const filepath = path.join(dir, file);
const stats = await fs.stat(filepath);
if (stats.isFile()) {
totalSize += stats.size;
}
}
return totalSize;
} catch {
return 0;
}
};
const commandsSize = await getDirectorySize(this.commandsDir);
const outputsSize = await getDirectorySize(this.outputsDir);
const tempSize = await getDirectorySize(this.tempDir);
return {
commands_mb: (commandsSize / (1024 * 1024)).toFixed(2),
outputs_mb: (outputsSize / (1024 * 1024)).toFixed(2),
temp_mb: (tempSize / (1024 * 1024)).toFixed(2),
total_mb: ((commandsSize + outputsSize + tempSize) / (1024 * 1024)).toFixed(2)
};
} catch (error) {
throw new Error(`Failed to get storage stats: ${error.message}`);
}
}
}
export default StorageManager;