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
nulloundefined - 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)