358
src/cache/cache-manager.js
vendido
Archivo normal
358
src/cache/cache-manager.js
vendido
Archivo normal
@@ -0,0 +1,358 @@
|
||||
const path = require('path');
|
||||
const fs = require('fs-extra');
|
||||
const crypto = require('crypto');
|
||||
const zlib = require('zlib');
|
||||
const { promisify } = require('util');
|
||||
|
||||
const gzip = promisify(zlib.gzip);
|
||||
const gunzip = promisify(zlib.gunzip);
|
||||
|
||||
class CacheManager {
|
||||
constructor() {
|
||||
this.cacheDir = path.join(require('os').homedir(), '.alepm', 'cache');
|
||||
this.metadataFile = path.join(this.cacheDir, 'metadata.json');
|
||||
this.init();
|
||||
}
|
||||
|
||||
async init() {
|
||||
await fs.ensureDir(this.cacheDir);
|
||||
|
||||
if (!fs.existsSync(this.metadataFile)) {
|
||||
await this.saveMetadata({
|
||||
version: '1.0.0',
|
||||
entries: {},
|
||||
totalSize: 0,
|
||||
lastCleanup: Date.now()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async get(packageName, version) {
|
||||
const key = this.generateKey(packageName, version);
|
||||
const metadata = await this.loadMetadata();
|
||||
|
||||
if (!metadata.entries[key]) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const entry = metadata.entries[key];
|
||||
const filePath = path.join(this.cacheDir, entry.file);
|
||||
|
||||
if (!fs.existsSync(filePath)) {
|
||||
// Remove stale entry
|
||||
delete metadata.entries[key];
|
||||
await this.saveMetadata(metadata);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Verify integrity
|
||||
const fileHash = await this.calculateFileHash(filePath);
|
||||
if (fileHash !== entry.hash) {
|
||||
// Corrupted entry, remove it
|
||||
await fs.remove(filePath);
|
||||
delete metadata.entries[key];
|
||||
await this.saveMetadata(metadata);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Update access time
|
||||
entry.lastAccess = Date.now();
|
||||
await this.saveMetadata(metadata);
|
||||
|
||||
// Read and decompress
|
||||
const compressedData = await fs.readFile(filePath);
|
||||
const data = await gunzip(compressedData);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
async store(packageName, version, data) {
|
||||
const key = this.generateKey(packageName, version);
|
||||
const metadata = await this.loadMetadata();
|
||||
|
||||
// Compress data for storage efficiency
|
||||
const compressedData = await gzip(data);
|
||||
const hash = crypto.createHash('sha256').update(compressedData).digest('hex');
|
||||
const fileName = `${hash.substring(0, 16)}.bin`;
|
||||
const filePath = path.join(this.cacheDir, fileName);
|
||||
|
||||
// Store compressed data
|
||||
await fs.writeFile(filePath, compressedData);
|
||||
|
||||
// Update metadata
|
||||
const entry = {
|
||||
packageName,
|
||||
version,
|
||||
file: fileName,
|
||||
hash,
|
||||
size: compressedData.length,
|
||||
originalSize: data.length,
|
||||
timestamp: Date.now(),
|
||||
lastAccess: Date.now()
|
||||
};
|
||||
|
||||
// Remove old entry if exists
|
||||
if (metadata.entries[key]) {
|
||||
const oldEntry = metadata.entries[key];
|
||||
const oldFilePath = path.join(this.cacheDir, oldEntry.file);
|
||||
if (fs.existsSync(oldFilePath)) {
|
||||
await fs.remove(oldFilePath);
|
||||
metadata.totalSize -= oldEntry.size;
|
||||
}
|
||||
}
|
||||
|
||||
metadata.entries[key] = entry;
|
||||
metadata.totalSize += entry.size;
|
||||
|
||||
await this.saveMetadata(metadata);
|
||||
|
||||
// Check if cleanup is needed
|
||||
await this.maybeCleanup();
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
async remove(packageName, version) {
|
||||
const key = this.generateKey(packageName, version);
|
||||
const metadata = await this.loadMetadata();
|
||||
|
||||
if (!metadata.entries[key]) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const entry = metadata.entries[key];
|
||||
const filePath = path.join(this.cacheDir, entry.file);
|
||||
|
||||
if (fs.existsSync(filePath)) {
|
||||
await fs.remove(filePath);
|
||||
}
|
||||
|
||||
metadata.totalSize -= entry.size;
|
||||
delete metadata.entries[key];
|
||||
|
||||
await this.saveMetadata(metadata);
|
||||
return true;
|
||||
}
|
||||
|
||||
async clean() {
|
||||
const metadata = await this.loadMetadata();
|
||||
let cleanedSize = 0;
|
||||
|
||||
for (const [key, entry] of Object.entries(metadata.entries)) {
|
||||
const filePath = path.join(this.cacheDir, entry.file);
|
||||
|
||||
if (fs.existsSync(filePath)) {
|
||||
await fs.remove(filePath);
|
||||
cleanedSize += entry.size;
|
||||
}
|
||||
}
|
||||
|
||||
// Reset metadata
|
||||
const newMetadata = {
|
||||
version: metadata.version,
|
||||
entries: {},
|
||||
totalSize: 0,
|
||||
lastCleanup: Date.now()
|
||||
};
|
||||
|
||||
await this.saveMetadata(newMetadata);
|
||||
return cleanedSize;
|
||||
}
|
||||
|
||||
async verify() {
|
||||
const metadata = await this.loadMetadata();
|
||||
const corrupted = [];
|
||||
const missing = [];
|
||||
|
||||
for (const [key, entry] of Object.entries(metadata.entries)) {
|
||||
const filePath = path.join(this.cacheDir, entry.file);
|
||||
|
||||
if (!fs.existsSync(filePath)) {
|
||||
missing.push(key);
|
||||
continue;
|
||||
}
|
||||
|
||||
const fileHash = await this.calculateFileHash(filePath);
|
||||
if (fileHash !== entry.hash) {
|
||||
corrupted.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up missing and corrupted entries
|
||||
for (const key of [...missing, ...corrupted]) {
|
||||
const entry = metadata.entries[key];
|
||||
metadata.totalSize -= entry.size;
|
||||
delete metadata.entries[key];
|
||||
}
|
||||
|
||||
if (missing.length > 0 || corrupted.length > 0) {
|
||||
await this.saveMetadata(metadata);
|
||||
}
|
||||
|
||||
return {
|
||||
total: Object.keys(metadata.entries).length,
|
||||
corrupted: corrupted.length,
|
||||
missing: missing.length,
|
||||
valid: Object.keys(metadata.entries).length - corrupted.length - missing.length
|
||||
};
|
||||
}
|
||||
|
||||
async getStats() {
|
||||
const metadata = await this.loadMetadata();
|
||||
const entries = Object.values(metadata.entries);
|
||||
|
||||
return {
|
||||
totalEntries: entries.length,
|
||||
totalSize: metadata.totalSize,
|
||||
totalOriginalSize: entries.reduce((sum, entry) => sum + entry.originalSize, 0),
|
||||
compressionRatio: entries.length > 0
|
||||
? metadata.totalSize / entries.reduce((sum, entry) => sum + entry.originalSize, 0)
|
||||
: 0,
|
||||
oldestEntry: entries.length > 0
|
||||
? Math.min(...entries.map(e => e.timestamp))
|
||||
: null,
|
||||
newestEntry: entries.length > 0
|
||||
? Math.max(...entries.map(e => e.timestamp))
|
||||
: null,
|
||||
lastCleanup: metadata.lastCleanup
|
||||
};
|
||||
}
|
||||
|
||||
async maybeCleanup() {
|
||||
const metadata = await this.loadMetadata();
|
||||
const maxCacheSize = 1024 * 1024 * 1024; // 1GB
|
||||
const maxAge = 30 * 24 * 60 * 60 * 1000; // 30 days
|
||||
const timeSinceLastCleanup = Date.now() - metadata.lastCleanup;
|
||||
const weekInMs = 7 * 24 * 60 * 60 * 1000;
|
||||
|
||||
// Only run cleanup weekly or if cache is too large
|
||||
if (timeSinceLastCleanup < weekInMs && metadata.totalSize < maxCacheSize) {
|
||||
return;
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
const entries = Object.entries(metadata.entries);
|
||||
let removedSize = 0;
|
||||
|
||||
// Remove old entries
|
||||
for (const [key, entry] of entries) {
|
||||
if (now - entry.lastAccess > maxAge) {
|
||||
const filePath = path.join(this.cacheDir, entry.file);
|
||||
if (fs.existsSync(filePath)) {
|
||||
await fs.remove(filePath);
|
||||
}
|
||||
removedSize += entry.size;
|
||||
delete metadata.entries[key];
|
||||
}
|
||||
}
|
||||
|
||||
// If still over limit, remove least recently used entries
|
||||
if (metadata.totalSize - removedSize > maxCacheSize) {
|
||||
const sortedEntries = Object.entries(metadata.entries)
|
||||
.sort(([, a], [, b]) => a.lastAccess - b.lastAccess);
|
||||
|
||||
for (const [key, entry] of sortedEntries) {
|
||||
if (metadata.totalSize - removedSize <= maxCacheSize) break;
|
||||
|
||||
const filePath = path.join(this.cacheDir, entry.file);
|
||||
if (fs.existsExists(filePath)) {
|
||||
await fs.remove(filePath);
|
||||
}
|
||||
removedSize += entry.size;
|
||||
delete metadata.entries[key];
|
||||
}
|
||||
}
|
||||
|
||||
metadata.totalSize -= removedSize;
|
||||
metadata.lastCleanup = now;
|
||||
|
||||
await this.saveMetadata(metadata);
|
||||
}
|
||||
|
||||
generateKey(packageName, version) {
|
||||
return crypto.createHash('sha1')
|
||||
.update(`${packageName}@${version}`)
|
||||
.digest('hex');
|
||||
}
|
||||
|
||||
async calculateFileHash(filePath) {
|
||||
const data = await fs.readFile(filePath);
|
||||
return crypto.createHash('sha256').update(data).digest('hex');
|
||||
}
|
||||
|
||||
async loadMetadata() {
|
||||
try {
|
||||
return await fs.readJson(this.metadataFile);
|
||||
} catch (error) {
|
||||
// Return default metadata if file is corrupted
|
||||
return {
|
||||
version: '1.0.0',
|
||||
entries: {},
|
||||
totalSize: 0,
|
||||
lastCleanup: Date.now()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async saveMetadata(metadata) {
|
||||
await fs.writeJson(this.metadataFile, metadata, { spaces: 2 });
|
||||
}
|
||||
|
||||
// Binary storage optimization methods
|
||||
async packPackageData(packageData) {
|
||||
// Create efficient binary format for package data
|
||||
const buffer = Buffer.from(JSON.stringify(packageData));
|
||||
|
||||
// Add magic header for format identification
|
||||
const header = Buffer.from('ALEPM001', 'ascii'); // Version 1 format
|
||||
const length = Buffer.alloc(4);
|
||||
length.writeUInt32BE(buffer.length, 0);
|
||||
|
||||
return Buffer.concat([header, length, buffer]);
|
||||
}
|
||||
|
||||
async unpackPackageData(binaryData) {
|
||||
// Verify magic header
|
||||
const header = binaryData.slice(0, 8).toString('ascii');
|
||||
if (header !== 'ALEPM001') {
|
||||
throw new Error('Invalid package data format');
|
||||
}
|
||||
|
||||
// Read length
|
||||
const length = binaryData.readUInt32BE(8);
|
||||
|
||||
// Extract and parse package data
|
||||
const packageBuffer = binaryData.slice(12, 12 + length);
|
||||
return JSON.parse(packageBuffer.toString());
|
||||
}
|
||||
|
||||
async deduplicate() {
|
||||
const metadata = await this.loadMetadata();
|
||||
const hashMap = new Map();
|
||||
let savedSpace = 0;
|
||||
|
||||
// Find duplicate files by hash
|
||||
for (const [key, entry] of Object.entries(metadata.entries)) {
|
||||
if (hashMap.has(entry.hash)) {
|
||||
// Duplicate found, remove this entry
|
||||
const filePath = path.join(this.cacheDir, entry.file);
|
||||
if (fs.existsSync(filePath)) {
|
||||
await fs.remove(filePath);
|
||||
savedSpace += entry.size;
|
||||
}
|
||||
delete metadata.entries[key];
|
||||
metadata.totalSize -= entry.size;
|
||||
} else {
|
||||
hashMap.set(entry.hash, key);
|
||||
}
|
||||
}
|
||||
|
||||
if (savedSpace > 0) {
|
||||
await this.saveMetadata(metadata);
|
||||
}
|
||||
|
||||
return savedSpace;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = CacheManager;
|
||||
Referencia en una nueva incidencia
Block a user