std::filesystem: Rutas y manipulación de archivos en C++

std::filesystem (C++17) es la biblioteca estándar que proporciona una interfaz unificada para interactuar con el sistema de archivos de una manera que no dependa de la plataforma. En lugar de lidiar con las diferencias de arquitectura entre los separadores de Windows (\) y los de sistemas POSIX (/), utilizas el objeto std::filesystem::path, que actúa como un modelo semántico de una ruta. Este objeto no es un simple std::string; es una entidad que entiende conceptos como raíces, directorios padres, extensiones y nombres de archivo.

Esta abstracción es necesaria porque la manipulación de rutas mediante concatenación de cadenas es una fuente constante de errores de portabilidad y errores de lógica (como duplicar separadores o no manejarlos correctamente). Utilizarás esta librería siempre que necesites navegar por un árbol de directorios, verificar si un archivo existe, obtener metadatos como el tamaño o la fecha de modificación, o realizar operaciones de gestión como crear carpetas, copiar o mover archivos. Si intentas implementar tu propia lógica de rutas o usar las APIs de C nativas (<dirent.h> o windows.h), tu código se volverá extremadamente difícil de mantener y propenso a fallos de seguridad o errores de segmentación al cambiar de sistema operativo.

Si no manejas correctamente las rutas o el estado del sistema de archivos, lo que suele ocurrir es que tu programa lance excepciones de tipo std::filesystem::filesystem_error (si usas las sobrecargas que las lanzan) o que las operaciones fallen silenciosamente si utilizas las versiones que aceptan std::error_code. Además, un error común es intentar usar fs::remove en un directorio que contiene archivos, lo cual fallará, o intentar fs::rename para mover archivos entre diferentes particiones/discos, lo cual puede lanzar excepciones según la implementación.

#include <iostream>
#include <filesystem>
#include <string>
#include <system_error>
#include <chrono>

namespace fs = std::filesystem;

// Función para procesar archivos de log y moverlos a un archivo
void archivar_logs(const fs::path& ruta_origen, const fs::path& ruta_destino) {
    std::error_code ec;

    // 1. Verificar si el origen existe y es un directorio
    if (!fs::exists(ruta_origen, ec) || !fs::is_directory(ruta_origen, ec)) {
        std::cerr << "Error: El origen no existe o no es un directorio: " << ruta_origen << "\n";
        return;
    }

    // 2. Crear el directorio de destino (recursivamente)
    // create_directories crea toda la jerarquía si no existe.
    // create_directory (singular) fallaría si los padres no existen.
    if (fs::create_directories(ruta_destino, ec)) {
        std::cout << "Directorio de destino creado: " << ruta_destino << "\n";
    } else if (ec) {
        std::cerr << "Error creando directorios: " << ec.message() << "\n";
        return;
    }

    // 3. Iterar sobre los archivos en el origen
    for (const auto& entrada : fs::directory_iterator(ruta_origen)) {
        // Usamos la entrada (que es un directory_entry) para inspeccionar el archivo
        if (entrada.is_regular_file() && entrada.path().extension() == ".log") {
            
            fs::path archivo_actual = entrada.path();
            // Manipulación de path: extraer stem (nombre sin extensión) y filename
            std::string nombre_base = archivo_actual.stem().string();
            fs::path destino_final = ruta_destino / (nombre_base + "_backup.log");

            // Obtener metadatos antes de mover
            auto tamaño = fs::file_size(archivo_actual, ec);
            auto ultima_mod = fs::last_write_time(archivo_actual, ec);

            if (!ec) {
                std::cout << "Procesando: " << archivo_actual.filename() 
                          << " [" << tamaño << " bytes]\n";

                // 4. Copiar y luego borrar (más seguro que rename para cruzar volúmenes)
                // copy_options::overwrite_existing asegura que si ya existe, lo reemplace
                fs::copy_file(archivo_actual, destino_final, fs::copy_options::overwrite_existing, ec);
                
                if (ec) {
                    std::cerr << "Error al copiar " << archivo_actual.filename() << ": " << ec.message() << "\n";
                } else {
                    // Eliminar el original tras un copiado exitoso
                    fs::remove(archivo_actual, ec);
                    std::cout << "Archivo archivado exitosamente en: " << destino_final << "\n";
                }
            }
            ec.clear(); // Limpiar el error para la siguiente iteración
        }
    }
}

int main() {
    // Definición de rutas usando el operador / para concatenar de forma segura
    // El operador / construye la ruta insertando el separador correcto de la plataforma
    fs::path base_path = fs::current_path();
    fs::path logs_dir = base_path / "logs";
    fs::path archive_dir = base_path / "archive";

    // Preparación de entorno para el ejemplo
    fs::create_directories(logs_dir);
    
    // Crear un archivo de prueba
    fs::path test_file = logs_dir / "sistema_error.log";
    // Nota: En un entorno real, crearíamos contenido; aquí solo aseguramos que exista
    if (!fs::exists(test_file)) {
        // Usamos una técnica de bajo nivel para crear un archivo vacío rápidamente
        // o simplemente creamos un archivo de texto.
        std::error_code dummy_ec;
        // Para este ejemplo, asumimos que el archivo ya existe o se crea externamente.
    }

    std::cout << "Iniciando proceso de archivado...\n";
    archivar_logs(logs_dir, archive_dir);
    
    return 0;
}

Desglose del código

  • Manipulación de rutas: En ruta_destino / (nombre_base + "_backup.log"), el operador / no es una división matemática; es una sobrecarga que concatena rutas insertando el separador nativo del sistema (ej. \ en Windows, / en Linux). Esto evita errores de “slash” faltante.
  • stem() vs filename(): Para un archivo llamado error.log, filename() devuelve error.log, mientras que stem() devuelve únicamente error. Esto es vital para manipular nombres de archivos sin usar operaciones de strings manuales y propensas a errores.
  • create_directories vs create_directory: Si intentas crear a/b/c con create_directory y a/b no existe, la función fallará. create_directories (en plural) es la opción robusta porque crea toda la jerarquía de carpetas necesaria.
  • directory_iterator: Proporciona una forma eficiente de recorrer un directorio. A diferencia de las funciones de C como readdir, este iterador devuelve objetos directory_entry que suelen contener metadatos ya cacheteados (como si es un archivo o un directorio), optimizando las llamadas al sistema.
  • Gestión de errores: He utilizado dos enfoques. Para la lógica de negocio principal (donde un error es esperado, como que un archivo esté bloqueado), uso std::error_code ec. Esto evita el coste de lanzar y capturar excepciones en un bucle de iteración. Sin embargo, en la lógica estructural, donde un error indica un estado inválido del programa, se dejan las excepciones para que las capturan las llamadas a alto nivel.
  • copy_file con opciones: El uso de fs::copy_options::overwrite_existing es una decisión de diseño para asegurar la idempotencia del script de archivado: si el proceso se interrumpe y se vuelve a ejecutar, no fallará por el archivo de backup ya existente.

El error frecuente

Un error muy común al trabajar con directorios es confundir fs::remove con fs::remove_all. Si intentas usar fs::remove(path) sobre una ruta que apunta a un directorio que no está vacío, la operación fallará (devolverá false o lanzará una excepción, dependiendo de la sobrecarga).

fs::path dir = "mi_carpeta_con_contenido";
// ... la carpeta contiene archivos ...
fs::remove(dir, ec); // FALLARÁ: el directorio no está vacío.

Si tu intención es eliminar un directorio y todo su contenido de forma recursiva, debes usar fs::remove_all(path). Ten cuidado: remove_all es una operación destructiva y pesada; si la ruta es incorrecta debido a un error de lógica en la construcción de la ruta, podrías borrar accidentalmente directorios críticos del sistema si la aplicación se ejecuta con privilegios suficientes.

94

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio