Upload Segmentado de Arquivos Grandes com Vue.js 3: Implementação Completa com Retomada de Progresso e Compatibilidade com IE9

Esta implementação aborda o desafio de upload de arquivos volumosos (até 20GB) em aplicações web modernas, mantendo compatibilidade com navegadores legados. A arquitetura utiliza segmentação de arquivos, retomada de progresso persistente, preservação de hierarquia de diretórios e criptografia de ponta a ponta.

Recursos Principais

  • Transmissão de arquivos até 20GB: Segmentação em blocos de 10MB, com retomada de progresso via localStorage e banco de dados
  • Preservação de estrutura de diretórios: Processamento recursivo da árvore de arquivos, mantendo caminhos relativos
  • Criptografia AES-256: Cada segmento é criptografado no cliente antes da transmissão
  • Compatibilidade total: Suporte desde IE9 (com polyfills) até navegadores modernos (Chrome, Firefox, Edge)
  • Stack tecnológica: Vue.js 3, Axios, Crypto-JS, sem dependências comerciais

O componente Vue.js 3 a seguir gerencia o upload segmentado com retomada automática de progresso.

// ==================== Polyfills para Compatibilidade (IE9+) ====================
import 'es6-promise/auto';
import 'whatwg-fetch';
import 'blob-polyfill';
import { Buffer } from 'buffer';

if (!window.Buffer) window.Buffer = Buffer;
if (!window.console) {
  window.console = { log: () => {}, warn: () => {}, error: () => {} };
}

// ==================== Bibliotecas de Terceiros ====================
import { ref, reactive, computed, onMounted } from 'vue';
import axios from 'axios';
import CryptoJS from 'crypto-js';
import { MD5 } from 'crypto-js';

export default {
  name: 'UploaderSegmentado',
  setup() {
    // Estado reativo do componente
    const tarefas = reactive([]);
    const tamanhoSegmento = 10 * 1024 * 1024; // 10MB por segmento
    const chaveCriptografia = ref('');
    const processoAtivo = ref(false);
    const filaProcessamento = ref([]);
    const maxConcorrencia = 3;

    // Computed para estatísticas
    const progressoTotal = computed(() => {
      if (tarefas.length === 0) return 0;
      const total = tarefas.reduce((acc, t) => acc + (t.progresso || 0), 0);
      return Math.round(total / tarefas.length);
    });

    // Inicialização do componente
    onMounted(() => {
      inicializarCriptografia();
      carregarTarefasPendentes();
    });

    /**
     * Obtém chave de criptografia do backend
     */
    async function inicializarCriptografia() {
      try {
        const resposta = await axios.get('/api/criptografia/chave');
        chaveCriptografia.value = resposta.data.chave;
      } catch (erro) {
        console.error('Falha ao obter chave de criptografia:', erro);
      }
    }

    /**
     * Recupera tarefas incompletas do localStorage
     */
    function carregarTarefasPendentes() {
      const dadosSalvos = localStorage.getItem('uploads_pendentes');
      if (dadosSalvos) {
        const tarefasSalvas = JSON.parse(dadosSalvos);
        tarefas.push(...tarefasSalvas);
      }
    }

    /**
     * Processa seleção de arquivos e diretórios
     */
    function processarSelecao(evento) {
      const arquivos = Array.from(evento.target.files);
      arquivos.forEach(arquivo => {
        const idTarefa = gerarIdUnico(arquivo);
        const totalSegmentos = Math.ceil(arquivo.size / tamanhoSegmento);
        
        const tarefa = {
          id: idTarefa,
          nome: arquivo.name,
          tamanho: arquivo.size,
          caminho: arquivo.webkitRelativePath || arquivo.name,
          arquivoOriginal: arquivo,
          totalSegmentos,
          segmentosEnviados: 0,
          progresso: 0,
          status: 'pendente',
          timestamp: Date.now()
        };
        
        tarefas.push(tarefa);
        salvarEstado();
      });
    }

    /**
     * Gera identificador único para arquivo
     */
    function gerarIdUnico(arquivo) {
      return `${arquivo.name}-${arquivo.size}-${arquivo.lastModified}`;
    }

    /**
     * Inicia processamento das tarefas
     */
    async function iniciarUpload() {
      if (processoAtivo.value) return;
      
      processoAtivo.value = true;
      const tarefasPendentes = tarefas.filter(t => t.status === 'pendente');
      
      for (const tarefa of tarefasPendentes) {
        await processarTarefa(tarefa);
      }
      
      processoAtivo.value = false;
    }

    /**
     * Processa upload de uma tarefa específica
     */
    async function processarTarefa(tarefa) {
      tarefa.status = 'processando';
      
      for (let i = tarefa.segmentosEnviados; i < tarefa.totalSegmentos; i++) {
        const inicio = i * tamanhoSegmento;
        const fim = Math.min(inicio + tamanhoSegmento, tarefa.tamanho);
        const segmento = tarefa.arquivoOriginal.slice(inicio, fim);
        
        const segmentoCriptografado = criptografarSegmento(segmento);
        await enviarSegmento(tarefa, i, segmentoCriptografado);
        
        tarefa.segmentosEnviados++;
        tarefa.progresso = Math.round((tarefa.segmentosEnviados / tarefa.totalSegmentos) * 100);
        salvarEstado();
      }
      
      tarefa.status = 'concluido';
      salvarEstado();
    }

    /**
     * Criptografa segmento usando AES-256
     */
    function criptografarSegmento(segmento) {
      return CryptoJS.AES.encrypt(segmento, chaveCriptografia.value).toString();
    }

    /**
     * Envia segmento para o backend
     */
    async function enviarSegmento(tarefa, indice, dados) {
      const formData = new FormData();
      formData.append('idTarefa', tarefa.id);
      formData.append('indiceSegmento', indice);
      formData.append('totalSegmentos', tarefa.totalSegmentos);
      formData.append('caminhoArquivo', tarefa.caminho);
      formData.append('dados', dados);

      await axios.post('/api/upload/segmento', formData, {
        headers: { 'Content-Type': 'multipart/form-data' }
      });
    }

    /**
     * Persiste estado no localStorage
     */
    function salvarEstado() {
      const estado = tarefas.map(t => ({
        id: t.id,
        nome: t.nome,
        tamanho: t.tamanho,
        caminho: t.caminho,
        totalSegmentos: t.totalSegmentos,
        segmentosEnviados: t.segmentosEnviados,
        progresso: t.progresso,
        status: t.status
      }));
      localStorage.setItem('uploads_pendentes', JSON.stringify(estado));
    }

    /**
     * Retorna classe CSS baseada no status
     */
    function obterClasseStatus(status) {
      const classes = {
        pendente: 'status-pendente',
        processando: 'status-processando',
        concluido: 'status-concluido',
        falha: 'status-falha'
      };
      return classes[status] || '';
    }

    return {
      tarefas,
      processarSelecao,
      iniciarUpload,
      progressoTotal,
      obterClasseStatus
    };
  }
};

1. Obtenção de Chave de Criptografia

  • Método: GET
  • Endpoint: /api/criptografia/chave
  • Descrição: Retorna chave AES-256 de 32 bytes gerada dinamicamente
  • Resposta: { "chave": "a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456" }

2. Upload de Segmento

  • Método: POST
  • Endpoint: /api/upload/segmento
  • Parâmetros (FormData):
    • idTarefa: Identificador único da tarefa
    • indiceSegmento: Índice do segmento (inicia em 0)
    • totalSegmentos: Número total de segmentos
    • caminhoArquivo: Caminho relativo do arquivo
    • dados: Conteúdo do segmento criptografado
  • Resposta: { "status": "sucesso", "mensagem": "Segmento armazenado" }

3. Download de Arquivo

  • Método: GET
  • Endpoint: /api/download/arquivo
  • Parâmetros:
    • caminho: Caminho completo do arquivo no servidor
  • Resposta: Stream binário do arquivo (application/octet-stream)

Dependências do Projeto

# Instalação via npm
npm install vue@3 axios crypto-js blob-polyfill es6-promise whatwg-fetch buffer

# ou via yarn
yarn add vue@3 axios crypto-js blob-polyfill es6-promise whatwg-fetch buffer

Configuração do Vue CLI

Adicione transpilação para compatibilidade com IE9 no arquivo vue.config.js:

module.exports = {
  transpileDependencies: true,
  chainWebpack: config => {
    config.entry('polyfill').add('./src/polyfills.js');
  }
};

Arquivo de Polyfills (src/polyfills.js)

// Centralização de polyfills para navegadores legados
import 'es6-promise/auto';
import 'whatwg-fetch';
import 'blob-polyfill';
import { Buffer } from 'buffer';

if (!window.Buffer) {
  window.Buffer = Buffer;
}

Ambiente Recomendado

  • Frontend: Node.js 16+, Vue CLI 5.x
  • Back end: Spring Boot 2.7+, Java 11+
  • Banco de Dados: MySQL 5.7+ ou PostgreSQL 12+
  • Servidor Web: Nginx 1.18+

Passos de Deploy

  1. Build do Frontend:``` npm run build

    Resultado em diretório dist/

  2. Build do Backend:``` ./mvnw clean package

    Resultado: target/upload-segmentado-1.0.0.jar

  3. Configuração Nginx:``` server { listen 80; server_name seu-dominio.com;

    location / { root /var/www/frontend/dist; try_files $uri $uri/ /index.html; }

    location /api/ { proxy_pass http://localhost:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }

  4. Execução do Backend:``` java -jar -Dserver.port=8080 upload-segmentado-1.0.0.jar

    
    
Problema Causa Provável Solução
IE9 não processa arquivos Polyfills não carregados Verifique inclusão de blob-polyfill e es6-promise no index.html
Chave AES inválida Endpoint de criptorgafia não respondendo Confira logs do Spring Boot e firewall da aplicação
Progresso não persiste localStorage indisponível Certifique-se de que o domínio não está em modo de navegação anônima restrita
Segmentos falham após 50% Limite de tamanho no servidor Ajuste spring.servlet.multipart.max-file-size em applicasion.properties
  • Implemente autenticação JWT para todos os endpoints da API
  • Valide caminhos de arquivo no backend para evitar path traversal
  • Utilize HTTPS em produção (configure certificado SSL no Nginx)
  • Limite taxa de upload por IP para prevenir abuso
  • Armazene chaves AES em Key Management Service (KMS) em vez de gerar dinamicamente

Tags: vue3 chunked-upload resume-upload SpringBoot aes-encryption

Publicado em 8-27 03:26