netakhoj-api / utils /fileStorage.js
Rakeshops71
Deploy app with LFS for large files
3eedfc9
Raw
History Blame Contribute Delete
15.3 kB
// utils/fileStorage.js - RESULT STORAGE SYSTEM
import fs from 'fs';
import path from 'path';
import crypto from 'crypto';
import { fileURLToPath } from 'url';
import { createLogger } from './logger.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const logger = createLogger('STORAGE');
class FileStorage {
constructor() {
this.storageDir = path.join(__dirname, '..', 'storage');
this.categories = {
candidates: path.join(this.storageDir, 'candidates'),
prs: path.join(this.storageDir, 'prs'),
analytics: path.join(this.storageDir, 'analytics'),
cache: path.join(this.storageDir, 'cache'),
images: path.join(this.storageDir, 'images')
};
this._ensureDirectories();
}
_ensureDirectories() {
Object.values(this.categories).forEach(dir => {
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
});
}
// ========================================================================
// SAVE METHODS
// ========================================================================
async saveFile(filename, data) {
const filepath = path.join(this.storageDir, filename);
try {
fs.writeFileSync(filepath, data, 'utf8');
logger.success('SAVE', `File saved: ${filename}`, { filepath });
return { success: true, filepath };
} catch (error) {
logger.error('SAVE', `Failed to save file: ${filename}`, error);
return { success: false, error: error.message };
}
}
async getFile(filename) {
const filepath = path.join(this.storageDir, filename);
try {
if (!fs.existsSync(filepath)) {
return { found: false, error: 'File not found' };
}
const data = fs.readFileSync(filepath, 'utf8');
logger.info('READ', `File retrieved: ${filename}`);
return { found: true, data };
} catch (error) {
logger.error('READ', `Failed to read file: ${filename}`, error);
return { found: false, error: error.message };
}
}
async saveCandidateData(name, data, metadata = {}) {
const filename = this._sanitizeFilename(name) + '.json';
const filepath = path.join(this.categories.candidates, filename);
const record = {
name,
timestamp: new Date().toISOString(),
metadata,
data,
version: '1.0'
};
try {
fs.writeFileSync(filepath, JSON.stringify(record, null, 2), 'utf8');
logger.success('SAVE', `Candidate data saved: ${name}`, { filepath });
// Also append to daily log
this._appendToDailyLog('candidates', record);
return { success: true, filepath };
} catch (error) {
logger.error('SAVE', `Failed to save candidate: ${name}`, error);
return { success: false, error: error.message };
}
}
async savePrsData(name, type, data, metadata = {}) {
const filename = this._sanitizeFilename(`${type}-${name}`) + '.json';
const filepath = path.join(this.categories.prs, filename);
const record = {
name,
type,
timestamp: new Date().toISOString(),
metadata,
data,
version: '1.0'
};
try {
fs.writeFileSync(filepath, JSON.stringify(record, null, 2), 'utf8');
logger.success('SAVE', `PRS data saved: ${name} (${type})`, { filepath });
this._appendToDailyLog('prs', record);
return { success: true, filepath };
} catch (error) {
logger.error('SAVE', `Failed to save PRS: ${name}`, error);
return { success: false, error: error.message };
}
}
async saveAnalytics(eventType, data) {
const filename = `${eventType}-${Date.now()}.json`;
const filepath = path.join(this.categories.analytics, filename);
const record = {
eventType,
timestamp: new Date().toISOString(),
data
};
try {
fs.writeFileSync(filepath, JSON.stringify(record, null, 2), 'utf8');
// Also append to aggregated analytics
this._appendToAnalyticsLog(record);
return { success: true, filepath };
} catch (error) {
logger.error('SAVE', 'Failed to save analytics', error);
return { success: false, error: error.message };
}
}
// ========================================================================
// READ METHODS
// ========================================================================
async getCandidateData(name) {
const filename = this._sanitizeFilename(name) + '.json';
const filepath = path.join(this.categories.candidates, filename);
try {
if (!fs.existsSync(filepath)) {
return { found: false, error: 'Not found in storage' };
}
const data = JSON.parse(fs.readFileSync(filepath, 'utf8'));
logger.info('READ', `Candidate data retrieved: ${name}`);
return { found: true, data };
} catch (error) {
logger.error('READ', `Failed to read candidate: ${name}`, error);
return { found: false, error: error.message };
}
}
async getPrsData(name, type) {
const filename = this._sanitizeFilename(`${type}-${name}`) + '.json';
const filepath = path.join(this.categories.prs, filename);
try {
if (!fs.existsSync(filepath)) {
return { found: false, error: 'Not found in storage' };
}
const data = JSON.parse(fs.readFileSync(filepath, 'utf8'));
logger.info('READ', `PRS data retrieved: ${name} (${type})`);
return { found: true, data };
} catch (error) {
logger.error('READ', `Failed to read PRS: ${name}`, error);
return { found: false, error: error.message };
}
}
// ========================================================================
// SEARCH & QUERY
// ========================================================================
searchCandidates(query) {
try {
const files = fs.readdirSync(this.categories.candidates);
const results = [];
const lowerQuery = query.toLowerCase();
files.forEach(file => {
if (!file.endsWith('.json')) return;
const filepath = path.join(this.categories.candidates, file);
try {
const data = JSON.parse(fs.readFileSync(filepath, 'utf8'));
if (data.name.toLowerCase().includes(lowerQuery)) {
results.push({
name: data.name,
timestamp: data.timestamp,
file: file
});
}
} catch (err) {
// Skip invalid files
}
});
logger.info('SEARCH', `Found ${results.length} candidates`, { query });
return results;
} catch (error) {
logger.error('SEARCH', 'Search failed', error);
return [];
}
}
// ========================================================================
// DAILY LOGS (Append-only logs)
// ========================================================================
_appendToDailyLog(category, record) {
const date = new Date().toISOString().split('T')[0];
const filename = `${date}-${category}.jsonl`; // JSON Lines format
const filepath = path.join(this.categories[category], filename);
try {
const logLine = JSON.stringify(record) + '\n';
fs.appendFileSync(filepath, logLine, 'utf8');
} catch (error) {
logger.error('LOG', 'Failed to append to daily log', error);
}
}
_appendToAnalyticsLog(record) {
const date = new Date().toISOString().split('T')[0];
const filename = `${date}-analytics.jsonl`;
const filepath = path.join(this.categories.analytics, filename);
try {
const logLine = JSON.stringify(record) + '\n';
fs.appendFileSync(filepath, logLine, 'utf8');
} catch (error) {
logger.error('LOG', 'Failed to append to analytics log', error);
}
}
// ========================================================================
// ANALYTICS & REPORTING
// ========================================================================
getAnalyticsSummary(days = 7) {
try {
const files = fs.readdirSync(this.categories.analytics);
const cutoffDate = Date.now() - (days * 24 * 60 * 60 * 1000);
const summary = {
totalEvents: 0,
eventsByType: {},
dateRange: {
from: new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString(),
to: new Date().toISOString()
}
};
files.forEach(file => {
if (!file.endsWith('.jsonl')) return;
const filepath = path.join(this.categories.analytics, file);
const stats = fs.statSync(filepath);
if (stats.mtimeMs < cutoffDate) return;
const content = fs.readFileSync(filepath, 'utf8');
const lines = content.split('\n').filter(Boolean);
lines.forEach(line => {
try {
const event = JSON.parse(line);
summary.totalEvents++;
summary.eventsByType[event.eventType] = (summary.eventsByType[event.eventType] || 0) + 1;
} catch (err) {
// Skip invalid lines
}
});
});
return summary;
} catch (error) {
logger.error('ANALYTICS', 'Failed to generate summary', error);
return { error: error.message };
}
}
// ========================================================================
// UTILITIES
// ========================================================================
_sanitizeFilename(name) {
return name
.toLowerCase()
.replace(/[^a-z0-9-]/g, '-')
.replace(/-+/g, '-')
.replace(/^-|-$/g, '');
}
getStats() {
const stats = {};
Object.entries(this.categories).forEach(([category, dir]) => {
try {
const files = fs.readdirSync(dir);
let totalSize = 0;
files.forEach(file => {
const filepath = path.join(dir, file);
totalSize += fs.statSync(filepath).size;
});
stats[category] = {
files: files.length,
size: `${(totalSize / 1024 / 1024).toFixed(2)} MB`
};
} catch (error) {
stats[category] = { error: error.message };
}
});
return stats;
}
clearCategory(category) {
if (!this.categories[category]) {
throw new Error(`Invalid category: ${category}`);
}
try {
const files = fs.readdirSync(this.categories[category]);
let deleted = 0;
files.forEach(file => {
const filepath = path.join(this.categories[category], file);
fs.unlinkSync(filepath);
deleted++;
});
logger.info('CLEAR', `Cleared ${deleted} files from ${category}`);
return { success: true, deleted };
} catch (error) {
logger.error('CLEAR', `Failed to clear ${category}`, error);
return { success: false, error: error.message };
}
}
// Export all data as backup
async createBackup() {
const backupDir = path.join(this.storageDir, 'backups');
if (!fs.existsSync(backupDir)) {
fs.mkdirSync(backupDir, { recursive: true });
}
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const backupFile = path.join(backupDir, `backup-${timestamp}.tar`);
// For simplicity, create a JSON backup
const backup = {
timestamp: new Date().toISOString(),
version: '1.0',
data: {}
};
Object.entries(this.categories).forEach(([category, dir]) => {
backup.data[category] = [];
const files = fs.readdirSync(dir);
files.forEach(file => {
if (file.endsWith('.json')) {
const filepath = path.join(dir, file);
const content = JSON.parse(fs.readFileSync(filepath, 'utf8'));
backup.data[category].push(content);
}
});
});
const backupPath = path.join(backupDir, `backup-${timestamp}.json`);
fs.writeFileSync(backupPath, JSON.stringify(backup, null, 2), 'utf8');
logger.success('BACKUP', `Backup created: ${backupPath}`);
return { success: true, filepath: backupPath };
}
// ========================================================================
// CLEANUP METHODS
// ========================================================================
// Cleanup old files in storage categories based on retention policies
cleanupOldFiles(retentionConfig = {}) {
const results = {
totalDeleted: 0,
categories: {}
};
Object.entries(this.categories).forEach(([category, dir]) => {
try {
const retentionValue = retentionConfig[category] || 30; // Default 30 days
let retentionMs;
// Check if retention is in hours (for storage) or days (for logs)
if (category === 'logs') {
retentionMs = retentionValue * 24 * 60 * 60 * 1000; // days to ms
} else {
retentionMs = retentionValue * 60 * 60 * 1000; // hours to ms
}
const cutoffDate = Date.now() - retentionMs;
const files = fs.readdirSync(dir);
let deleted = 0;
files.forEach(file => {
const filepath = path.join(dir, file);
const stats = fs.statSync(filepath);
if (stats.mtimeMs < cutoffDate) {
fs.unlinkSync(filepath);
deleted++;
}
});
results.categories[category] = {
deleted,
retentionValue,
retentionUnit: category === 'logs' ? 'days' : 'hours',
totalFiles: files.length
};
if (deleted > 0) {
logger.info('CLEANUP', `Cleaned ${deleted} old files from ${category} (${retentionValue} ${category === 'logs' ? 'days' : 'hours'} retention)`);
}
results.totalDeleted += deleted;
} catch (error) {
logger.error('CLEANUP', `Failed to cleanup ${category}`, error);
results.categories[category] = { error: error.message };
}
});
return results;
}
// Get cleanup stats (preview what would be deleted)
getCleanupStats(retentionConfig = {}) {
const stats = {
totalFiles: 0,
filesToDelete: 0,
categories: {}
};
Object.entries(this.categories).forEach(([category, dir]) => {
try {
const retentionValue = retentionConfig[category] || 30; // Default 30 days
let retentionMs;
// Check if retention is in hours (for storage) or days (for logs)
if (category === 'logs') {
retentionMs = retentionValue * 24 * 60 * 60 * 1000; // days to ms
} else {
retentionMs = retentionValue * 60 * 60 * 1000; // hours to ms
}
const cutoffDate = Date.now() - retentionMs;
const files = fs.readdirSync(dir);
let toDelete = 0;
files.forEach(file => {
const filepath = path.join(dir, file);
const stats = fs.statSync(filepath);
if (stats.mtimeMs < cutoffDate) {
toDelete++;
}
});
stats.categories[category] = {
totalFiles: files.length,
filesToDelete: toDelete,
retentionValue,
retentionUnit: category === 'logs' ? 'days' : 'hours'
};
stats.totalFiles += files.length;
stats.filesToDelete += toDelete;
} catch (error) {
stats.categories[category] = { error: error.message };
}
});
return stats;
}
}
export default new FileStorage();