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 tarefaindiceSegmento: Índice do segmento (inicia em 0)totalSegmentos: Número total de segmentoscaminhoArquivo: Caminho relativo do arquivodados: 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
-
Build do Frontend:``` npm run build
Resultado em diretório dist/
-
Build do Backend:``` ./mvnw clean package
Resultado: target/upload-segmentado-1.0.0.jar
-
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; } }
-
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