DEV Community

Pedro Alvarado
Pedro Alvarado

Posted on

Sistema de Archivos (fs) en Node.js

Tabla de Contenidos

  1. Introducción al Módulo fs
  2. Operaciones Síncronas vs Asíncronas
  3. Lectura de Archivos
  4. Escritura de Archivos
  5. Operaciones con Directorios
  6. Información de Archivos (Stats)
  7. Watchers - Monitoreo de Archivos
  8. Streams de Archivos
  9. Módulo Path
  10. Manejo de Errores
  11. Casos de Uso Reales
  12. Buenas Prácticas
  13. 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'; 
Enter fullscreen mode Exit fullscreen mode

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); } 
Enter fullscreen mode Exit fullscreen mode

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'); 
Enter fullscreen mode Exit fullscreen mode

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); } } 
Enter fullscreen mode Exit fullscreen mode

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) } 
Enter fullscreen mode Exit fullscreen mode

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; } } 
Enter fullscreen mode Exit fullscreen mode

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')); 
Enter fullscreen mode Exit fullscreen mode

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); }); } 
Enter fullscreen mode Exit fullscreen mode

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); } } 
Enter fullscreen mode Exit fullscreen mode

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); 
Enter fullscreen mode Exit fullscreen mode

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'); 
Enter fullscreen mode Exit fullscreen mode

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); } } 
Enter fullscreen mode Exit fullscreen mode

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'); 
Enter fullscreen mode Exit fullscreen mode

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); } } 
Enter fullscreen mode Exit fullscreen mode

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); }); 
Enter fullscreen mode Exit fullscreen mode

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); }); 
Enter fullscreen mode Exit fullscreen mode

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); }); 
Enter fullscreen mode Exit fullscreen mode

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); 
Enter fullscreen mode Exit fullscreen mode

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); 
Enter fullscreen mode Exit fullscreen mode

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); }); 
Enter fullscreen mode Exit fullscreen mode

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); }); 
Enter fullscreen mode Exit fullscreen mode

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'); } } ); 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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) 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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'); 
Enter fullscreen mode Exit fullscreen mode

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}`; } 
Enter fullscreen mode Exit fullscreen mode

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; } } 
Enter fullscreen mode Exit fullscreen mode

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'); 
Enter fullscreen mode Exit fullscreen mode

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' }); 
Enter fullscreen mode Exit fullscreen mode

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); }); 
Enter fullscreen mode Exit fullscreen mode

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... })); } 
Enter fullscreen mode Exit fullscreen mode

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); } }); 
Enter fullscreen mode Exit fullscreen mode

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); }); } 
Enter fullscreen mode Exit fullscreen mode

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); } 
Enter fullscreen mode Exit fullscreen mode

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; } } 
Enter fullscreen mode Exit fullscreen mode

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 o undefined
  • 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)