Tabla de Contenidos
- Introducción al Módulo fs
- Operaciones Síncronas vs Asíncronas
- Lectura de Archivos
- Escritura de Archivos
- Operaciones con Directorios
- Información de Archivos (Stats)
- Watchers - Monitoreo de Archivos
- Streams de Archivos
- Módulo Path
- Manejo de Errores
- Casos de Uso Reales
- Buenas Prácticas
- Cuestionario de Entrevista
Introducción al Módulo fs
El módulo fs
(File System) de Node.js proporciona una API para interactuar con el sistema de archivos. Es uno de los módulos core más utilizados.
Importar el Módulo
// CommonJS
const fs = require('fs');
const fsPromises = require('fs').promises;
// ES Modules
import fs from 'fs';
import { promises as fsPromises } from 'fs';
Características Principales
- Multiplataforma: Funciona en Windows, macOS y Linux
- Múltiples APIs: Callbacks, Promises y síncrona
- Streams: Para archivos grandes
- Watchers: Monitoreo de cambios en tiempo real
Operaciones Síncronas vs Asíncronas
API Síncrona (Bloqueante)
const fs = require('fs');
try {
// Bloquea el Event Loop hasta completar
const data = fs.readFileSync('archivo.txt', 'utf8');
console.log('Contenido:', data);
console.log('Esta línea se ejecuta después');
} catch (error) {
console.error('Error:', error.message);
}
Cuándo usar:
- ✅ Al inicio de la aplicación (cargar configuración)
- ✅ Scripts simples
- ✅ Cuando el orden es crítico
- ❌ Durante manejo de peticiones HTTP
- ❌ En servidores con alta concurrencia
API de Callbacks (No Bloqueante)
const fs = require('fs');
// No bloquea el Event Loop
fs.readFile('archivo.txt', 'utf8', (err, data) => {
if (err) {
console.error('Error:', err.message);
return;
}
console.log('Contenido:', data);
});
console.log('Esta línea se ejecuta primero');
Patrón Error-First Callback:
- Primer parámetro: error (null si no hay error)
- Segundo parámetro: resultado
API de Promises (No Bloqueante)
const fs = require('fs').promises;
// Con .then()/.catch()
fs.readFile('archivo.txt', 'utf8')
.then(data => {
console.log('Contenido:', data);
})
.catch(err => {
console.error('Error:', err.message);
});
// Con async/await
async function leerArchivo() {
try {
const data = await fs.readFile('archivo.txt', 'utf8');
console.log('Contenido:', data);
} catch (error) {
console.error('Error:', error.message);
}
}
Comparación de Rendimiento
const fs = require('fs');
// ❌ SÍNCRONO - Bloquea todo
function leerArchivosSincrono() {
console.time('sincrono');
for (let i = 0; i < 5; i++) {
const data = fs.readFileSync(`archivo${i}.txt`, 'utf8');
console.log(`Archivo ${i} leído`);
}
console.timeEnd('sincrono'); // ~500ms (suma de todos)
}
// ✅ ASÍNCRONO - Paralelo
async function leerArchivosAsincrono() {
console.time('asincrono');
const promesas = [];
for (let i = 0; i < 5; i++) {
promesas.push(
fs.promises.readFile(`archivo${i}.txt`, 'utf8')
.then(data => console.log(`Archivo ${i} leído`))
);
}
await Promise.all(promesas);
console.timeEnd('asincrono'); // ~100ms (el más lento)
}
Lectura de Archivos
Leer Archivo Completo
const fs = require('fs');
// 1. SÍNCRONO
try {
const data = fs.readFileSync('config.json', 'utf8');
const config = JSON.parse(data);
console.log(config);
} catch (error) {
console.error('Error leyendo config:', error.message);
}
// 2. CALLBACK
fs.readFile('config.json', 'utf8', (err, data) => {
if (err) {
console.error('Error:', err.message);
return;
}
try {
const config = JSON.parse(data);
console.log(config);
} catch (parseError) {
console.error('Error parseando JSON:', parseError.message);
}
});
// 3. PROMISES
async function cargarConfig() {
try {
const data = await fs.promises.readFile('config.json', 'utf8');
const config = JSON.parse(data);
return config;
} catch (error) {
if (error.code === 'ENOENT') {
console.log('Archivo de configuración no existe, usando defaults');
return {};
}
throw error;
}
}
Encodings Soportados
const fs = require('fs');
// Sin encoding - retorna Buffer
const buffer = fs.readFileSync('imagen.jpg');
console.log(buffer); // <Buffer ff d8 ff e0 00 10 4a 46 49 46...>
// Con encoding - retorna string
const texto = fs.readFileSync('archivo.txt', 'utf8');
console.log(typeof texto); // string
// Encodings disponibles
const encodings = [
'utf8', // Por defecto para texto
'ascii', // Solo caracteres ASCII
'base64', // Base64
'hex', // Hexadecimal
'binary', // Binario (deprecated)
'utf16le' // UTF-16 Little Endian
];
// Ejemplo con diferentes encodings
const data = fs.readFileSync('archivo.txt');
console.log('UTF-8:', data.toString('utf8'));
console.log('Base64:', data.toString('base64'));
console.log('Hex:', data.toString('hex'));
Leer Archivos Grandes con Streams
const fs = require('fs');
// Para archivos grandes, usa streams
function leerArchivoGrande(filename) {
const stream = fs.createReadStream(filename, {
encoding: 'utf8',
highWaterMark: 64 * 1024 // 64KB chunks
});
let contenido = '';
stream.on('data', (chunk) => {
contenido += chunk;
console.log(`Leído chunk de ${chunk.length} caracteres`);
});
stream.on('end', () => {
console.log(`Archivo completo leído: ${contenido.length} caracteres`);
});
stream.on('error', (error) => {
console.error('Error leyendo archivo:', error.message);
});
}
Escritura de Archivos
Escribir Archivo Completo
const fs = require('fs');
const data = {
name: 'Mi App',
version: '1.0.0',
timestamp: new Date().toISOString()
};
// 1. SÍNCRONO
try {
fs.writeFileSync('output.json', JSON.stringify(data, null, 2));
console.log('Archivo escrito exitosamente');
} catch (error) {
console.error('Error escribiendo archivo:', error.message);
}
// 2. CALLBACK
fs.writeFile('output.json', JSON.stringify(data, null, 2), 'utf8', (err) => {
if (err) {
console.error('Error:', err.message);
return;
}
console.log('Archivo escrito exitosamente');
});
// 3. PROMISES
async function guardarDatos(datos) {
try {
await fs.promises.writeFile(
'output.json',
JSON.stringify(datos, null, 2),
'utf8'
);
console.log('Archivo guardado');
} catch (error) {
console.error('Error guardando:', error.message);
}
}
Agregar Contenido (Append)
const fs = require('fs');
// Agregar al final del archivo
const logEntry = `${new Date().toISOString()} - Usuario logueado\n`;
// SÍNCRONO
fs.appendFileSync('app.log', logEntry);
// ASÍNCRONO
fs.appendFile('app.log', logEntry, (err) => {
if (err) {
console.error('Error escribiendo log:', err.message);
return;
}
console.log('Log agregado');
});
// PROMISES
await fs.promises.appendFile('app.log', logEntry);
Escribir con Streams
const fs = require('fs');
// Para escritura continua o archivos grandes
function escribirArchivoGrande() {
const stream = fs.createWriteStream('salida.txt');
// Escribir múltiples chunks
for (let i = 0; i < 1000; i++) {
const data = `Línea ${i}: ${new Date().toISOString()}\n`;
stream.write(data);
}
// Cerrar el stream
stream.end();
stream.on('finish', () => {
console.log('Archivo escrito completamente');
});
stream.on('error', (error) => {
console.error('Error escribiendo:', error.message);
});
}
// Ejemplo: Logger con rotación
class FileLogger {
constructor(filename, maxSize = 10 * 1024 * 1024) { // 10MB
this.filename = filename;
this.maxSize = maxSize;
this.currentStream = null;
this.currentSize = 0;
this.initStream();
}
initStream() {
if (this.currentStream) {
this.currentStream.end();
}
this.currentStream = fs.createWriteStream(this.filename, { flags: 'a' });
this.currentSize = 0;
}
log(message) {
const entry = `${new Date().toISOString()} - ${message}\n`;
if (this.currentSize + entry.length > this.maxSize) {
// Rotar archivo
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const backupName = `${this.filename}.${timestamp}`;
this.currentStream.end();
fs.renameSync(this.filename, backupName);
this.initStream();
}
this.currentStream.write(entry);
this.currentSize += entry.length;
}
}
const logger = new FileLogger('app.log');
logger.log('Aplicación iniciada');
Operaciones con Directorios
Crear Directorios
const fs = require('fs');
const path = require('path');
// Crear directorio simple
fs.mkdir('nuevo-directorio', (err) => {
if (err) {
if (err.code === 'EEXIST') {
console.log('El directorio ya existe');
} else {
console.error('Error creando directorio:', err.message);
}
return;
}
console.log('Directorio creado');
});
// Crear directorio con subdirectorios (recursive)
const dirPath = path.join('proyecto', 'src', 'components');
// SÍNCRONO
try {
fs.mkdirSync(dirPath, { recursive: true });
console.log('Estructura de directorios creada');
} catch (error) {
console.error('Error:', error.message);
}
// ASÍNCRONO
fs.mkdir(dirPath, { recursive: true }, (err) => {
if (err) {
console.error('Error:', err.message);
return;
}
console.log('Estructura creada');
});
// PROMISES
async function crearEstructura() {
try {
await fs.promises.mkdir(dirPath, { recursive: true });
console.log('Estructura creada con promises');
} catch (error) {
console.error('Error:', error.message);
}
}
Leer Contenido de Directorios
const fs = require('fs');
const path = require('path');
// Listar archivos y directorios
fs.readdir('.', (err, files) => {
if (err) {
console.error('Error leyendo directorio:', err.message);
return;
}
console.log('Archivos y directorios:');
files.forEach(file => {
console.log(`- ${file}`);
});
});
// Con información detallada
fs.readdir('.', { withFileTypes: true }, (err, entries) => {
if (err) {
console.error('Error:', err.message);
return;
}
entries.forEach(entry => {
const type = entry.isDirectory() ? 'DIR' : 'FILE';
console.log(`${type}: ${entry.name}`);
});
});
// Función recursiva para listar todo
async function listarRecursivo(dir, nivel = 0) {
try {
const entries = await fs.promises.readdir(dir, { withFileTypes: true });
for (const entry of entries) {
const indent = ' '.repeat(nivel);
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
console.log(`${indent}📁 ${entry.name}/`);
await listarRecursivo(fullPath, nivel + 1);
} else {
console.log(`${indent}📄 ${entry.name}`);
}
}
} catch (error) {
console.error(`Error leyendo ${dir}:`, error.message);
}
}
// Uso
listarRecursivo('./src');
Eliminar Directorios
const fs = require('fs');
// Eliminar directorio vacío
fs.rmdir('directorio-vacio', (err) => {
if (err) {
if (err.code === 'ENOENT') {
console.log('El directorio no existe');
} else if (err.code === 'ENOTEMPTY') {
console.log('El directorio no está vacío');
} else {
console.error('Error:', err.message);
}
return;
}
console.log('Directorio eliminado');
});
// Eliminar directorio recursivamente (Node.js 14+)
fs.rm('directorio-con-contenido', { recursive: true, force: true }, (err) => {
if (err) {
console.error('Error eliminando:', err.message);
return;
}
console.log('Directorio eliminado recursivamente');
});
// Función para limpiar directorio (eliminar contenido pero mantener directorio)
async function limpiarDirectorio(dir) {
try {
const entries = await fs.promises.readdir(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
await fs.promises.rm(fullPath, { recursive: true, force: true });
} else {
await fs.promises.unlink(fullPath);
}
}
console.log(`Directorio ${dir} limpiado`);
} catch (error) {
console.error('Error limpiando directorio:', error.message);
}
}
Información de Archivos (Stats)
Obtener Estadísticas
const fs = require('fs');
// Obtener información de archivo/directorio
fs.stat('archivo.txt', (err, stats) => {
if (err) {
if (err.code === 'ENOENT') {
console.log('El archivo no existe');
} else {
console.error('Error:', err.message);
}
return;
}
console.log('Información del archivo:');
console.log('- Tamaño:', stats.size, 'bytes');
console.log('- Es archivo:', stats.isFile());
console.log('- Es directorio:', stats.isDirectory());
console.log('- Creado:', stats.birthtime);
console.log('- Modificado:', stats.mtime);
console.log('- Último acceso:', stats.atime);
console.log('- Permisos:', stats.mode.toString(8));
});
// Función utilitaria para información legible
function formatearTamaño(bytes) {
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
if (bytes === 0) return '0 Bytes';
const i = Math.floor(Math.log(bytes) / Math.log(1024));
return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i];
}
async function infoArchivo(filename) {
try {
const stats = await fs.promises.stat(filename);
return {
nombre: filename,
tamaño: formatearTamaño(stats.size),
tipo: stats.isFile() ? 'archivo' : 'directorio',
creado: stats.birthtime.toLocaleDateString(),
modificado: stats.mtime.toLocaleDateString(),
permisos: stats.mode.toString(8).slice(-3)
};
} catch (error) {
return { error: error.message };
}
}
// Ejemplo de uso
infoArchivo('package.json').then(info => {
console.log(info);
});
Verificar Existencia y Permisos
const fs = require('fs');
// Verificar si existe
fs.access('archivo.txt', fs.constants.F_OK, (err) => {
if (err) {
console.log('El archivo no existe');
} else {
console.log('El archivo existe');
}
});
// Verificar permisos específicos
fs.access('archivo.txt', fs.constants.R_OK | fs.constants.W_OK, (err) => {
if (err) {
console.log('No tienes permisos de lectura/escritura');
} else {
console.log('Tienes permisos de lectura y escritura');
}
});
// Constantes de permisos
const permisos = {
F_OK: 'existe',
R_OK: 'lectura',
W_OK: 'escritura',
X_OK: 'ejecución'
};
// Función para verificar múltiples permisos
async function verificarPermisos(filename) {
const resultados = {};
for (const [constante, nombre] of Object.entries(permisos)) {
try {
await fs.promises.access(filename, fs.constants[constante]);
resultados[nombre] = true;
} catch {
resultados[nombre] = false;
}
}
return resultados;
}
// Uso
verificarPermisos('archivo.txt').then(permisos => {
console.log('Permisos:', permisos);
});
Watchers - Monitoreo de Archivos
fs.watch() - Monitoreo Básico
const fs = require('fs');
// Monitorear archivo o directorio
const watcher = fs.watch('archivo.txt', (eventType, filename) => {
console.log(`Evento: ${eventType}`);
if (filename) {
console.log(`Archivo: ${filename}`);
}
});
// Eventos: 'rename' (crear, eliminar, renombrar) y 'change' (modificar)
// Detener el watcher después de 30 segundos
setTimeout(() => {
watcher.close();
console.log('Watcher cerrado');
}, 30000);
// Manejar errores
watcher.on('error', (error) => {
console.error('Error en watcher:', error.message);
});
fs.watchFile() - Monitoreo por Polling
const fs = require('fs');
// Monitoreo por polling (menos eficiente pero más confiable)
fs.watchFile('config.json', { interval: 1000 }, (curr, prev) => {
console.log('Archivo modificado:');
console.log(`- Tamaño anterior: ${prev.size}`);
console.log(`- Tamaño actual: ${curr.size}`);
console.log(`- Última modificación: ${curr.mtime}`);
});
// Detener el watcher
setTimeout(() => {
fs.unwatchFile('config.json');
console.log('Watchfile detenido');
}, 60000);
Watcher Avanzado para Directorio
const fs = require('fs');
const path = require('path');
class DirectoryWatcher {
constructor(directory) {
this.directory = directory;
this.watchers = new Map();
this.files = new Map();
this.init();
}
async init() {
// Escanear archivos existentes
await this.scanDirectory();
// Monitorear directorio
this.dirWatcher = fs.watch(this.directory, (eventType, filename) => {
if (filename) {
this.handleFileEvent(eventType, filename);
}
});
console.log(`Monitoreando directorio: ${this.directory}`);
}
async scanDirectory() {
try {
const files = await fs.promises.readdir(this.directory);
for (const file of files) {
const filepath = path.join(this.directory, file);
const stats = await fs.promises.stat(filepath);
if (stats.isFile()) {
this.files.set(file, {
size: stats.size,
mtime: stats.mtime.getTime()
});
}
}
} catch (error) {
console.error('Error escaneando directorio:', error.message);
}
}
async handleFileEvent(eventType, filename) {
const filepath = path.join(this.directory, filename);
try {
const stats = await fs.promises.stat(filepath);
const currentInfo = this.files.get(filename);
if (!currentInfo) {
// Archivo nuevo
console.log(`📄 Archivo creado: ${filename}`);
this.files.set(filename, {
size: stats.size,
mtime: stats.mtime.getTime()
});
} else if (stats.mtime.getTime() > currentInfo.mtime) {
// Archivo modificado
console.log(`✏️ Archivo modificado: ${filename}`);
console.log(` Tamaño: ${currentInfo.size} → ${stats.size} bytes`);
this.files.set(filename, {
size: stats.size,
mtime: stats.mtime.getTime()
});
}
} catch (error) {
if (error.code === 'ENOENT') {
// Archivo eliminado
if (this.files.has(filename)) {
console.log(`🗑️ Archivo eliminado: ${filename}`);
this.files.delete(filename);
}
} else {
console.error(`Error procesando ${filename}:`, error.message);
}
}
}
close() {
if (this.dirWatcher) {
this.dirWatcher.close();
}
console.log('Watcher cerrado');
}
}
// Uso
const watcher = new DirectoryWatcher('./src');
// Cerrar después de 5 minutos
setTimeout(() => {
watcher.close();
}, 5 * 60 * 1000);
Streams de Archivos
Readable Streams
const fs = require('fs');
// Crear stream de lectura
const readStream = fs.createReadStream('archivo-grande.txt', {
encoding: 'utf8',
start: 0, // Byte de inicio
end: 1000, // Byte de fin
highWaterMark: 64 * 1024 // Tamaño del buffer (64KB)
});
readStream.on('data', (chunk) => {
console.log(`Chunk recibido: ${chunk.length} caracteres`);
});
readStream.on('end', () => {
console.log('Lectura completada');
});
readStream.on('error', (error) => {
console.error('Error leyendo:', error.message);
});
Writable Streams
const fs = require('fs');
// Crear stream de escritura
const writeStream = fs.createWriteStream('salida.txt', {
flags: 'w', // 'w' = write, 'a' = append
encoding: 'utf8',
highWaterMark: 64 * 1024
});
// Escribir datos
writeStream.write('Primera línea\n');
writeStream.write('Segunda línea\n');
// Finalizar
writeStream.end('Última línea\n');
writeStream.on('finish', () => {
console.log('Escritura completada');
});
writeStream.on('error', (error) => {
console.error('Error escribiendo:', error.message);
});
Pipeline de Archivos
const fs = require('fs');
const { pipeline } = require('stream');
const { Transform } = require('stream');
// Transform que convierte a mayúsculas
const upperCaseTransform = new Transform({
transform(chunk, encoding, callback) {
this.push(chunk.toString().toUpperCase());
callback();
}
});
// Pipeline: leer → transformar → escribir
pipeline(
fs.createReadStream('entrada.txt'),
upperCaseTransform,
fs.createWriteStream('salida-mayusculas.txt'),
(error) => {
if (error) {
console.error('Pipeline falló:', error.message);
} else {
console.log('Pipeline completado exitosamente');
}
}
);
Módulo Path
El módulo path
es esencial para trabajar con rutas de archivos de manera multiplataforma.
const path = require('path');
// Información sobre rutas
console.log('Separador de directorios:', path.sep); // \ en Windows, / en Unix
console.log('Separador de PATH:', path.delimiter); // ; en Windows, : en Unix
const archivo = '/usuarios/juan/documentos/archivo.txt';
console.log('Directorio padre:', path.dirname(archivo)); // /usuarios/juan/documentos
console.log('Nombre del archivo:', path.basename(archivo)); // archivo.txt
console.log('Extensión:', path.extname(archivo)); // .txt
console.log('Nombre sin extensión:', path.basename(archivo, '.txt')); // archivo
// Parsear ruta completa
const parsed = path.parse(archivo);
console.log(parsed);
// {
// root: '/',
// dir: '/usuarios/juan/documentos',
// base: 'archivo.txt',
// ext: '.txt',
// name: 'archivo'
// }
// Construir rutas
const nuevaRuta = path.format({
dir: '/usuarios/maria',
name: 'config',
ext: '.json'
});
console.log(nuevaRuta); // /usuarios/maria/config.json
Unir Rutas (path.join vs path.resolve)
const path = require('path');
// path.join() - Une segmentos de ruta
console.log(path.join('usuarios', 'juan', 'documentos'));
// usuarios/juan/documentos (Unix) o usuarios\juan\documentos (Windows)
console.log(path.join('/usuarios', '../juan', './documentos'));
// /juan/documentos
// path.resolve() - Resuelve a ruta absoluta
console.log(path.resolve('archivo.txt'));
// /ruta/completa/actual/archivo.txt
console.log(path.resolve('/usuarios', 'juan', 'archivo.txt'));
// /usuarios/juan/archivo.txt
console.log(path.resolve('..', 'archivo.txt'));
// /ruta/padre/archivo.txt
// Diferencia clave
console.log(path.join('a', '/b', 'c')); // a/b/c
console.log(path.resolve('a', '/b', 'c')); // /b/c (absoluta desde /b)
Rutas Relativas y Absolutas
const path = require('path');
const rutaAbsoluta = '/usuarios/juan/proyecto/src/index.js';
const rutaBase = '/usuarios/juan/proyecto';
// Convertir absoluta a relativa
const rutaRelativa = path.relative(rutaBase, rutaAbsoluta);
console.log(rutaRelativa); // src/index.js
// Verificar si es absoluta
console.log(path.isAbsolute('/usuarios/juan')); // true
console.log(path.isAbsolute('src/index.js')); // false
// Normalizar ruta (eliminar .. y .)
console.log(path.normalize('/usuarios/juan/../maria/./documentos'));
// /usuarios/maria/documentos
Utilidades para Desarrollo
const path = require('path');
const fs = require('fs');
// Encontrar archivos con extensión específica
function encontrarArchivos(directorio, extension) {
const archivos = [];
function buscarRecursivo(dir) {
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
buscarRecursivo(fullPath);
} else if (path.extname(entry.name) === extension) {
archivos.push(fullPath);
}
}
}
buscarRecursivo(directorio);
return archivos;
}
// Crear estructura de proyecto
function crearEstructuraProyecto(nombre) {
const estructura = [
'src',
'src/components',
'src/utils',
'tests',
'docs',
'public'
];
const basePath = path.join(process.cwd(), nombre);
// Crear directorio base
fs.mkdirSync(basePath, { recursive: true });
// Crear subdirectorios
for (const dir of estructura) {
const dirPath = path.join(basePath, dir);
fs.mkdirSync(dirPath, { recursive: true });
}
// Crear archivos iniciales
const archivos = {
'package.json': JSON.stringify({
name: nombre,
version: '1.0.0',
description: '',
main: 'src/index.js'
}, null, 2),
'src/index.js': '// Punto de entrada de la aplicación\nconsole.log("Hola mundo");',
'README.md': `# ${nombre}\n\nDescripción del proyecto.`,
'.gitignore': 'node_modules/\n.env\n*.log'
};
for (const [archivo, contenido] of Object.entries(archivos)) {
const archivoPath = path.join(basePath, archivo);
fs.writeFileSync(archivoPath, contenido);
}
console.log(`Proyecto ${nombre} creado en ${basePath}`);
}
// Uso
// crearEstructuraProyecto('mi-nuevo-proyecto');
Manejo de Errores
Códigos de Error Comunes
const fs = require('fs');
// Manejar errores específicos
fs.readFile('archivo-inexistente.txt', (err, data) => {
if (err) {
switch (err.code) {
case 'ENOENT':
console.log('Archivo no encontrado');
break;
case 'EACCES':
console.log('Permiso denegado');
break;
case 'EISDIR':
console.log('Es un directorio, no un archivo');
break;
case 'EMFILE':
console.log('Demasiados archivos abiertos');
break;
case 'ENOSPC':
console.log('No hay espacio en disco');
break;
default:
console.log('Error desconocido:', err.message);
}
return;
}
console.log('Archivo leído exitosamente');
});
// Función helper para manejo de errores
function manejarErrorFS(error) {
const errores = {
ENOENT: 'Archivo o directorio no encontrado',
EACCES: 'Permiso denegado',
EEXIST: 'Archivo o directorio ya existe',
EISDIR: 'Es un directorio',
ENOTDIR: 'No es un directorio',
EMFILE: 'Demasiados archivos abiertos',
ENOSPC: 'No hay espacio en disco',
EIO: 'Error de entrada/salida'
};
return errores[error.code] || `Error desconocido: ${error.message}`;
}
Operaciones Seguras
const fs = require('fs');
const path = require('path');
// Función segura para leer archivo
async function leerArchivoSeguro(filename, defaultValue = null) {
try {
const data = await fs.promises.readFile(filename, 'utf8');
return data;
} catch (error) {
if (error.code === 'ENOENT') {
console.log(`Archivo ${filename} no existe, usando valor por defecto`);
return defaultValue;
}
throw error; // Re-lanzar otros errores
}
}
// Función segura para escribir archivo
async function escribirArchivoSeguro(filename, data) {
try {
// Crear directorio padre si no existe
const dir = path.dirname(filename);
await fs.promises.mkdir(dir, { recursive: true });
// Escribir a archivo temporal primero
const tempFile = `${filename}.tmp`;
await fs.promises.writeFile(tempFile, data);
// Renombrar (operación atómica)
await fs.promises.rename(tempFile, filename);
console.log(`Archivo ${filename} escrito exitosamente`);
} catch (error) {
console.error(`Error escribiendo ${filename}:`, manejarErrorFS(error));
throw error;
}
}
// Función para copiar archivo con validaciones
async function copiarArchivo(origen, destino) {
try {
// Verificar que el origen existe
await fs.promises.access(origen, fs.constants.F_OK);
// Verificar que el destino no existe o preguntar al usuario
try {
await fs.promises.access(destino, fs.constants.F_OK);
throw new Error(`El archivo ${destino} ya existe`);
} catch (error) {
if (error.code !== 'ENOENT') {
throw error;
}
}
// Crear directorio destino si no existe
const dirDestino = path.dirname(destino);
await fs.promises.mkdir(dirDestino, { recursive: true });
// Copiar archivo
await fs.promises.copyFile(origen, destino);
console.log(`Archivo copiado: ${origen} → ${destino}`);
} catch (error) {
console.error('Error copiando archivo:', manejarErrorFS(error));
throw error;
}
}
Casos de Uso Reales
1. Sistema de Configuración
const fs = require('fs');
const path = require('path');
class ConfigManager {
constructor(configPath = 'config.json') {
this.configPath = configPath;
this.config = {};
this.defaults = {
port: 3000,
host: 'localhost',
debug: false,
database: {
host: 'localhost',
port: 5432,
name: 'myapp'
}
};
this.load();
}
load() {
try {
if (fs.existsSync(this.configPath)) {
const data = fs.readFileSync(this.configPath, 'utf8');
this.config = { ...this.defaults, ...JSON.parse(data) };
} else {
this.config = { ...this.defaults };
this.save(); // Crear archivo con defaults
}
} catch (error) {
console.error('Error cargando configuración:', error.message);
this.config = { ...this.defaults };
}
}
save() {
try {
const dir = path.dirname(this.configPath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
fs.writeFileSync(this.configPath, JSON.stringify(this.config, null, 2));
} catch (error) {
console.error('Error guardando configuración:', error.message);
}
}
get(key) {
return key.split('.').reduce((obj, k) => obj && obj[k], this.config);
}
set(key, value) {
const keys = key.split('.');
const lastKey = keys.pop();
const target = keys.reduce((obj, k) => {
if (!obj[k]) obj[k] = {};
return obj[k];
}, this.config);
target[lastKey] = value;
this.save();
}
}
// Uso
const config = new ConfigManager();
console.log('Puerto:', config.get('port'));
console.log('Host DB:', config.get('database.host'));
config.set('debug', true);
config.set('database.name', 'production');
2. Sistema de Logs
const fs = require('fs');
const path = require('path');
class Logger {
constructor(options = {}) {
this.logDir = options.logDir || 'logs';
this.maxFileSize = options.maxFileSize || 10 * 1024 * 1024; // 10MB
this.maxFiles = options.maxFiles || 5;
this.level = options.level || 'info';
this.levels = {
error: 0,
warn: 1,
info: 2,
debug: 3
};
this.currentFile = null;
this.currentStream = null;
this.currentSize = 0;
this.init();
}
init() {
// Crear directorio de logs
if (!fs.existsSync(this.logDir)) {
fs.mkdirSync(this.logDir, { recursive: true });
}
this.rotateIfNeeded();
}
rotateIfNeeded() {
const today = new Date().toISOString().split('T')[0];
const logFile = path.join(this.logDir, `app-${today}.log`);
if (this.currentFile !== logFile) {
if (this.currentStream) {
this.currentStream.end();
}
this.currentFile = logFile;
this.currentStream = fs.createWriteStream(logFile, { flags: 'a' });
// Obtener tamaño actual
try {
const stats = fs.statSync(logFile);
this.currentSize = stats.size;
} catch {
this.currentSize = 0;
}
}
// Rotar por tamaño
if (this.currentSize > this.maxFileSize) {
this.rotateBySize();
}
}
rotateBySize() {
if (this.currentStream) {
this.currentStream.end();
}
// Renombrar archivos existentes
for (let i = this.maxFiles - 1; i > 0; i--) {
const oldFile = `${this.currentFile}.${i}`;
const newFile = `${this.currentFile}.${i + 1}`;
if (fs.existsSync(oldFile)) {
if (i === this.maxFiles - 1) {
fs.unlinkSync(oldFile); // Eliminar el más antiguo
} else {
fs.renameSync(oldFile, newFile);
}
}
}
// Renombrar archivo actual
fs.renameSync(this.currentFile, `${this.currentFile}.1`);
// Crear nuevo archivo
this.currentStream = fs.createWriteStream(this.currentFile, { flags: 'w' });
this.currentSize = 0;
}
log(level, message, meta = {}) {
if (this.levels[level] > this.levels[this.level]) {
return; // Nivel muy bajo
}
this.rotateIfNeeded();
const timestamp = new Date().toISOString();
const logEntry = {
timestamp,
level: level.toUpperCase(),
message,
...meta
};
const logLine = JSON.stringify(logEntry) + '\n';
this.currentStream.write(logLine);
this.currentSize += logLine.length;
// También mostrar en consola
console.log(`[${timestamp}] ${level.toUpperCase()}: ${message}`);
}
error(message, meta) { this.log('error', message, meta); }
warn(message, meta) { this.log('warn', message, meta); }
info(message, meta) { this.log('info', message, meta); }
debug(message, meta) { this.log('debug', message, meta); }
close() {
if (this.currentStream) {
this.currentStream.end();
}
}
}
// Uso
const logger = new Logger({
logDir: './logs',
level: 'debug',
maxFileSize: 1024 * 1024 // 1MB para testing
});
logger.info('Aplicación iniciada');
logger.error('Error de conexión', { host: 'localhost', port: 3000 });
logger.debug('Variable de debug', { variable: 'valor' });
3. Sistema de Backup
const fs = require('fs');
const path = require('path');
const { pipeline } = require('stream');
const zlib = require('zlib');
class BackupManager {
constructor(sourceDir, backupDir) {
this.sourceDir = sourceDir;
this.backupDir = backupDir;
if (!fs.existsSync(this.backupDir)) {
fs.mkdirSync(this.backupDir, { recursive: true });
}
}
async createBackup() {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const backupName = `backup-${timestamp}`;
const backupPath = path.join(this.backupDir, backupName);
console.log(`Iniciando backup: ${this.sourceDir} → ${backupPath}`);
try {
await this.copyDirectory(this.sourceDir, backupPath);
// Comprimir backup
const compressedPath = `${backupPath}.tar.gz`;
await this.compressDirectory(backupPath, compressedPath);
// Eliminar directorio sin comprimir
await this.removeDirectory(backupPath);
console.log(`Backup completado: ${compressedPath}`);
// Limpiar backups antiguos
await this.cleanOldBackups();
return compressedPath;
} catch (error) {
console.error('Error creando backup:', error.message);
throw error;
}
}
async copyDirectory(source, destination) {
await fs.promises.mkdir(destination, { recursive: true });
const entries = await fs.promises.readdir(source, { withFileTypes: true });
for (const entry of entries) {
const sourcePath = path.join(source, entry.name);
const destPath = path.join(destination, entry.name);
if (entry.isDirectory()) {
await this.copyDirectory(sourcePath, destPath);
} else {
await fs.promises.copyFile(sourcePath, destPath);
}
}
}
async compressDirectory(source, destination) {
// Crear archivo tar simple (sin usar librerías externas)
const files = await this.getAllFiles(source);
const writeStream = fs.createWriteStream(destination);
const gzip = zlib.createGzip();
return new Promise((resolve, reject) => {
pipeline(
this.createTarStream(files, source),
gzip,
writeStream,
(error) => {
if (error) reject(error);
else resolve();
}
);
});
}
async getAllFiles(dir) {
const files = [];
async function scan(currentDir) {
const entries = await fs.promises.readdir(currentDir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(currentDir, entry.name);
if (entry.isDirectory()) {
await scan(fullPath);
} else {
files.push(fullPath);
}
}
}
await scan(dir);
return files;
}
createTarStream(files, baseDir) {
const { Readable } = require('stream');
return new Readable({
read() {
if (files.length === 0) {
this.push(null);
return;
}
const file = files.shift();
const relativePath = path.relative(baseDir, file);
// Simplificado: solo concatenar archivos con separador
fs.readFile(file, (err, data) => {
if (err) {
this.emit('error', err);
return;
}
const header = `\n--- ${relativePath} ---\n`;
this.push(header);
this.push(data);
});
}
});
}
async removeDirectory(dir) {
await fs.promises.rm(dir, { recursive: true, force: true });
}
async cleanOldBackups(keepCount = 5) {
const backups = await fs.promises.readdir(this.backupDir);
const backupFiles = backups
.filter(file => file.startsWith('backup-') && file.endsWith('.tar.gz'))
.sort()
.reverse();
if (backupFiles.length > keepCount) {
const toDelete = backupFiles.slice(keepCount);
for (const file of toDelete) {
const filePath = path.join(this.backupDir, file);
await fs.promises.unlink(filePath);
console.log(`Backup antiguo eliminado: ${file}`);
}
}
}
async listBackups() {
const backups = await fs.promises.readdir(this.backupDir);
const backupFiles = backups
.filter(file => file.startsWith('backup-') && file.endsWith('.tar.gz'))
.sort()
.reverse();
const backupInfo = [];
for (const file of backupFiles) {
const filePath = path.join(this.backupDir, file);
const stats = await fs.promises.stat(filePath);
backupInfo.push({
name: file,
size: this.formatSize(stats.size),
created: stats.birthtime.toLocaleString()
});
}
return backupInfo;
}
formatSize(bytes) {
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
if (bytes === 0) return '0 Bytes';
const i = Math.floor(Math.log(bytes) / Math.log(1024));
return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i];
}
}
// Uso
const backupManager = new BackupManager('./src', './backups');
// Crear backup
backupManager.createBackup()
.then(backupPath => {
console.log('Backup creado:', backupPath);
return backupManager.listBackups();
})
.then(backups => {
console.log('Backups disponibles:');
backups.forEach(backup => {
console.log(`- ${backup.name} (${backup.size}) - ${backup.created}`);
});
})
.catch(error => {
console.error('Error:', error.message);
});
Buenas Prácticas
1. Usar Operaciones Asíncronas
// ❌ MAL - Bloquea el Event Loop
function procesarArchivos() {
const files = fs.readdirSync('./data');
for (const file of files) {
const content = fs.readFileSync(`./data/${file}`, 'utf8');
// procesar contenido...
}
}
// ✅ BIEN - No bloqueante
async function procesarArchivos() {
const files = await fs.promises.readdir('./data');
// Procesamiento en paralelo
await Promise.all(files.map(async (file) => {
const content = await fs.promises.readFile(`./data/${file}`, 'utf8');
// procesar contenido...
}));
}
2. Manejar Errores Correctamente
// ❌ MAL - No maneja errores específicos
fs.readFile('config.json', (err, data) => {
if (err) throw err; // Puede crashear la app
// usar data...
});
// ✅ BIEN - Manejo específico de errores
fs.readFile('config.json', 'utf8', (err, data) => {
if (err) {
if (err.code === 'ENOENT') {
console.log('Usando configuración por defecto');
return useDefaultConfig();
}
console.error('Error leyendo config:', err.message);
return;
}
try {
const config = JSON.parse(data);
// usar config...
} catch (parseErr) {
console.error('Error parseando JSON:', parseErr.message);
}
});
3. Usar Streams para Archivos Grandes
// ❌ MAL - Carga todo en memoria
function procesarArchivoGrande(filename) {
const data = fs.readFileSync(filename, 'utf8');
return data.split('\n').length; // Puede usar GBs de RAM
}
// ✅ BIEN - Memoria constante
function procesarArchivoGrande(filename) {
return new Promise((resolve, reject) => {
let lineCount = 0;
let buffer = '';
const stream = fs.createReadStream(filename, { encoding: 'utf8' });
stream.on('data', (chunk) => {
buffer += chunk;
const lines = buffer.split('\n');
buffer = lines.pop(); // Mantener línea incompleta
lineCount += lines.length;
});
stream.on('end', () => {
if (buffer) lineCount++; // Última línea
resolve(lineCount);
});
stream.on('error', reject);
});
}
4. Validar Rutas de Archivos
const path = require('path');
// ❌ MAL - Vulnerable a path traversal
function leerArchivo(filename) {
return fs.readFileSync(filename); // Puede acceder a ../../etc/passwd
}
// ✅ BIEN - Validar y normalizar rutas
function leerArchivo(filename, baseDir = './data') {
// Normalizar y resolver ruta
const safePath = path.resolve(baseDir, path.normalize(filename));
const safeBaseDir = path.resolve(baseDir);
// Verificar que la ruta está dentro del directorio permitido
if (!safePath.startsWith(safeBaseDir)) {
throw new Error('Acceso denegado: ruta fuera del directorio permitido');
}
return fs.readFileSync(safePath);
}
5. Usar Operaciones Atómicas
// ❌ MAL - No atómico, puede corromperse
async function guardarDatos(filename, data) {
await fs.promises.writeFile(filename, JSON.stringify(data));
}
// ✅ BIEN - Escritura atómica
async function guardarDatos(filename, data) {
const tempFile = `${filename}.tmp`;
try {
// Escribir a archivo temporal
await fs.promises.writeFile(tempFile, JSON.stringify(data, null, 2));
// Renombrar (operación atómica)
await fs.promises.rename(tempFile, filename);
} catch (error) {
// Limpiar archivo temporal en caso de error
try {
await fs.promises.unlink(tempFile);
} catch {} // Ignorar si no existe
throw error;
}
}
Cuestionario de Entrevista
Preguntas Fundamentales
1. ¿Cuál es la diferencia entre operaciones síncronas y asíncronas en fs?
Respuesta:
-
Síncronas: Bloquean el Event Loop hasta completar (ej:
readFileSync
) -
Asíncronas: No bloquean, usan callbacks o promises (ej:
readFile
) - Las síncronas son apropiadas solo al inicio de la app o en scripts simples
- Las asíncronas son esenciales para servidores y alta concurrencia
2. ¿Qué es el patrón Error-First Callback?
Respuesta:
- Convención donde el primer parámetro del callback es el error
- Si no hay error, es
null
oundefined
- El segundo parámetro contiene el resultado
- Permite manejo consistente de errores en APIs asíncronas
3. ¿Cuándo usarías streams vs leer archivos completos?
Respuesta:
- Streams: Archivos grandes, memoria limitada, procesamiento en tiempo real
- Lectura completa: Archivos pequeños, necesitas todos los datos, operaciones simples
- Streams mantienen memoria constante independiente del tamaño del archivo
4. ¿Qué diferencia hay entre path.join()
y path.resolve()
?
Respuesta:
-
path.join()
: Une segmentos de ruta, mantiene rutas relativas -
path.resolve()
: Resuelve a ruta absoluta desde el directorio actual -
join('a', 'b')
→a/b
,resolve('a', 'b')
→/ruta/actual/a/b
5. ¿Cómo manejarías el error ENOENT?
Respuesta:
- ENOENT significa "No such file or directory"
- Verificar si el archivo debe existir o usar valor por defecto
- Crear archivo/directorio si es necesario
- No debe crashear la aplicación, manejar gracefully
Preguntas Avanzadas
6. ¿Cómo implementarías un sistema de logs con rotación automática?
Respuesta:
- Monitorear tamaño del archivo actual
- Crear nuevo archivo cuando se alcance el límite
- Renombrar archivos antiguos (app.log → app.log.1)
- Eliminar archivos más antiguos que el límite configurado
- Usar streams para escritura eficiente
7. ¿Qué es una operación atómica y por qué es importante?
Respuesta:
- Operación que se completa totalmente o no se realiza
- Importante para evitar corrupción de datos
- Técnica: escribir a archivo temporal, luego renombrar
-
rename()
es atómico en la mayoría de sistemas de archivos
8. ¿Cómo prevenir ataques de path traversal?
Respuesta:
- Validar y normalizar todas las rutas de entrada
- Usar
path.resolve()
y verificar que la ruta esté dentro del directorio permitido - No confiar en input del usuario para rutas de archivos
- Usar whitelist de archivos permitidos cuando sea posible
9. ¿Qué diferencia hay entre fs.watch()
y fs.watchFile()
?
Respuesta:
-
fs.watch()
: Usa eventos del sistema operativo, más eficiente -
fs.watchFile()
: Usa polling, más confiable pero menos eficiente -
watch()
puede no funcionar en algunos sistemas de archivos de red -
watchFile()
funciona en todos lados pero consume más recursos
10. ¿Cómo optimizarías la lectura de miles de archivos pequeños?
Respuesta:
- Usar
Promise.all()
con límite de concurrencia - Implementar pool de workers para evitar saturar el sistema
- Considerar combinar archivos pequeños en uno grande
- Usar cache para evitar lecturas repetidas
- Monitorear uso de file descriptors
Ejercicios Prácticos
Ejercicio 1: Implementar un sistema de caché de archivos que almacene en memoria los archivos más accedidos.
Ejercicio 2: Crear un monitor de directorio que detecte cambios y ejecute acciones específicas.
Ejercicio 3: Desarrollar un sistema de backup incremental que solo copie archivos modificados.
Ejercicio 4: Implementar un servidor de archivos estáticos con soporte para ranges HTTP.
Ejercicio 5: Crear un sistema de migración de archivos que mueva archivos entre directorios basado en reglas.
Top comments (0)