Implementación de Carga y Descarga de Directorios Grandes con Reanudación en .NET C#

Estrategia Técnica para la Transferencia de Volumenes de Datos Extensos

Se presenta un esquema técnico completo para gestionar la subida y bajada de directorios de gran tamaño, manteniendo la estructura jerárquica y la capacidad de reanudar operaciones interrumpidas.

1. Requisitos Fundamentales del Sistema

  • Capacidad para manejar archivos individuales y conjuntos de hasta 50GB.
  • Preservación de la jerarquía completa de carpetas durante transferencias bidireccionales.
  • Mecanismo de reanudación persistente que sobreviva al cierre de sesiones del navegador.
  • Compatibilidad multiplataforma (Windows, macOS, Linux) y con navegadores heredados.

2. Componentes Clave de la Solución

La arquitectura propuesta se basa en una división del trabajo entre el cliente y el servidor, apoyada en almacenamiento intermedio y bases de datos.

Lógica del Cliente (Frontend con Vue.js)

El gestor de carga en el navegador divide los archivos en bloques y persiste el progreso localmente.

class GestorCargaArchivos {
  constructor(archivo, tamanoBloque = 5 * 1024 * 1024) {
    this.archivo = archivo;
    this.tamanoBloque = tamanoBloque;
    this.numeroBloques = Math.ceil(archivo.size / tamanoBloque);
    this.bloquesSubidos = this.obtenerProgreso();
  }

  async obtenerProgreso() {
    return new Promise((resolucion) => {
      const solicitud = indexedDB.open('BDProgresoCarga', 1);
      solicitud.onsuccess = (evento) => {
        const baseDatos = evento.target.result;
        const transaccion = baseDatos.transaction('registros', 'readonly');
        const almacen = transaccion.objectStore('registros');
        const peticion = almacen.get(this.archivo.name + '-' + this.archivo.lastModified);
        
        peticion.onsuccess = () => {
          baseDatos.close();
          resolucion(peticion.result?.bloquesSubidos || 0);
        };
      };
    });
  }

  async enviarBloque(indice) {
    const inicio = indice * this.tamanoBloque;
    const fin = Math.min(inicio + this.tamanoBloque, this.archivo.size);
    const fragmento = this.archivo.slice(inicio, fin);
    
    const datosFormulario = new FormData();
    datosFormulario.append('archivo_binario', fragmento);
    datosFormulario.append('id_bloque', indice);
    datosFormulario.append('total_bloques', this.numeroBloques);
    datosFormulario.append('identificador_archivo', this.generarIdUnico());
    
    try {
      const respuesta = await fetch('/api/transferencia/upload', {
        method: 'POST',
        body: datosFormulario,
      });
      
      if (respuesta.ok) {
        this.guardarProgreso(indice + 1);
        return true;
      }
      return false;
    } catch (error) {
      console.error('Fallo en la subida:', error);
      return false;
    }
  }

  guardarProgreso(bloquesCompletados) {
    return new Promise((resolucion) => {
      const solicitud = indexedDB.open('BDProgresoCarga', 1);
      solicitud.onupgradeneeded = (evento) => {
        const baseDatos = evento.target.result;
        if (!baseDatos.objectStoreNames.contains('registros')) {
          baseDatos.createObjectStore('registros', { keyPath: 'claveArchivo' });
        }
      };
      
      solicitud.onsuccess = (evento) => {
        const baseDatos = evento.target.result;
        const transaccion = baseDatos.transaction('registros', 'readwrite');
        const almacen = transaccion.objectStore('registros');
        
        almacen.put({
          claveArchivo: this.archivo.name + '-' + this.archivo.lastModified,
          bloquesSubidos: bloquesCompletados
        });
        
        transaccion.oncomplete = () => {
          baseDatos.close();
          resolucion();
        };
      };
    });
  }
}

Manejador del Servidor (Backend con ASP.NET Core)

El controlador API recibe los fragmentos, los almacena temporalmente y los une al finalizar la transferencia.

[ApiController]
[Route("api/transferencia")]
public class ControladorArchivoController : ControllerBase
{
    private readonly IRepositorioTemporal _almacenTemporal;
    private readonly IServicioNube _servicioNube;

    public ControladorArchivoController(
        IRepositorioTemporal almacenTemporal,
        IServicioNube servicioNube)
    {
        _almacenTemporal = almacenTemporal;
        _servicioNube = servicioNube;
    }

    [HttpPost("upload")]
    public async Task<IActionResult> ProcesarSubida()
    {
        var coleccion = await Request.ReadFormAsync();
        var archivo = coleccion.Files.GetFile("archivo_binario");
        var idBloque = int.Parse(coleccion["id_bloque"]);
        var totalBloques = int.Parse(coleccion["total_bloques"]);
        var idArchivo = coleccion["identificador_archivo"];

        if (archivo == null)
        {
            return BadRequest("Datos de archivo no proporcionados.");
        }

        var rutaFragmento = _almacenTemporal.ObtenerRutaFragmento(idArchivo, idBloque);
        using (var flujo = new FileStream(rutaFragmento, FileMode.Create))
        {
            await archivo.CopyToAsync(flujo);
        }

        if (idBloque == totalBloques - 1)
        {
            var rutaCompleta = _almacenTemporal.ObtenerRutaFinal(idArchivo, Path.GetExtension(archivo.FileName));
            await FusionarTodosFragmentos(idArchivo, totalBloques, rutaCompleta);
            var urlRemota = await _servicioNube.SubirObjeto(rutaCompleta, idArchivo);
            _almacenTemporal.LimpiarTemporales(idArchivo);
            return Ok(new { exito = true, ubicacion = urlRemota });
        }

        return Accepted(new { exito = true, mensaje = $"Fragmento {idBloque} almacenado." });
    }

    private async Task FusionarTodosFragmentos(string idArchivo, int total, string destino)
    {
        using (var flujoFinal = new FileStream(destino, FileMode.Create))
        {
            for (var i = 0; i < total; i++)
            {
                var rutaFragmento = _almacenTemporal.ObtenerRutaFragmento(idArchivo, i);
                using (var flujoFragmento = new FileStream(rutaFragmento, FileMode.Open))
                {
                    await flujoFragmento.CopyToAsync(flujoFinal);
                }
            }
        }
    }
}

Modelo de Persistencia en Base de Datos

El esquema almacena tanto el progreso intermedio como los metdaatos del archivo finalizado.

CREATE TABLE ProgresoTransferencia (
    Identificador UNIQUEIDENTIFIER PRIMARY KEY DEFAULT NEWID(),
    ClaveArchivo NVARCHAR(300) NOT NULL,
    BloquesCompletados INT NOT NULL DEFAULT 0,
    TotalBloques INT NOT NULL,
    TamanoArchivo BIGINT NOT NULL,
    IdUsuario NVARCHAR(100) NOT NULL,
    FechaInicio DATETIME2 NOT NULL DEFAULT SYSDATETIME(),
    UltimaActualizacion DATETIME2 NOT NULL DEFAULT SYSDATETIME(),
    CONSTRAINT UQ_Archivo_Usuario UNIQUE (ClaveArchivo, IdUsuario)
);

CREATE TABLE ArchivosProcesados (
    Identificador UNIQUEIDENTIFIER PRIMARY KEY DEFAULT NEWID(),
    IdNegocio NVARCHAR(200) NOT NULL,
    NombreOriginal NVARCHAR(600) NOT NULL,
    RutaAlmacenamiento NVARCHAR(2000) NOT NULL,
    TamanoFinal BIGINT NOT NULL,
    TipoMIME NVARCHAR(150),
    IdResponsable NVARCHAR(100) NOT NULL,
    Estado SMALLINT NOT NULL DEFAULT 1,
    FechaProcesamiento DATETIME2 NOT NULL DEFAULT SYSDATETIME()
);

3. Proceso de Configuración del Entorno

Para el desarrollo y pruebas, se requiere establecer la infraestructura base.

  1. Instalar el SDK de .NET 6.0 o superior desde el sitio oficial de Microsoft.
  2. Crear un proyecto ASP.NET Core Web API y otro proyecto Vue.js 3.
  3. Configurar CORS en el backend para permitir solicitudes desde el dominio del frontend.
  4. Implementar el proveedor de almacenamiento (local, S3, Aliyun OSS) en una interfaz IServicioNube.
  5. Ejecutar las migraciones para crear las tablas en la base de datos seleccionada.

4. Consideraciones de Rendimiento y Fiabilidad

Para manejar archivos masivos sin agotar recursos, es crucial:

  • Utilizar streams en todo el proceso para evitar cargar archivos completos en memoria.
  • Implementar reintentos con retroceso exponencial en el cliente para fallos de red.
  • Monitorear el espacio en disco en el directorio temporal del servidor.
  • Ofrecer un endpoint para que el cliente consulte qué bloques ya fueron recibidos, facilitando la reanudación.

La arquitectura permite escalar horizontalmente añadiendo más nodos de procesamiento de subidas, ya que el estado se centraliza en la base de datos y el almacenamiento temporal puede ser compartido.

Etiquetas: C# .NET Core ASP.NET vue.js Archivos Grandes

Publicado el 6-20 16:22