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)