W3docs

Upload de Arquivo Retomável

Aprenda a criar uploads de arquivo retomáveis em JavaScript: transferência em chunks, retomada após interrupções, servidor Node.js, resumable.js e implementação nativa com File.slice + fetch.

Fazer upload de um vídeo de 2 GB por uma conexão móvel instável com uma única requisição fetch é arriscado: uma conexão interrompida a 95% e o usuário precisa começar do zero. Uploads de arquivo retomáveis resolvem isso dividindo o arquivo em pequenos pedaços, enviando-os um de cada vez e lembrando quais já chegaram — assim, um upload interrompido continua de onde parou em vez de reiniciar.

Esta página cobre o quadro completo: como uploads em chunks retomáveis funcionam conceitualmente, um servidor Node.js + Express funcional que armazena e remonta os chunks, um cliente construído com a biblioteca resumable.js e uma versão nativa sem dependências usando File.slice e fetch. Você também verá o bug comum de remontagem a evitar e dicas de endurecimento para produção.

Como Funcionam os Uploads Retomáveis

A ideia central é simples e se apoia em três partes que funcionam juntas:

  1. Dividir o arquivo em chunks. O navegador divide o arquivo selecionado em pedaços de tamanho fixo (por exemplo, 1 MB cada) usando o método Blob.slice que File herda. O arquivo em si nunca é carregado completamente na memória.
  2. Enviar chunks um (ou alguns) de cada vez. Cada chunk é uma requisição HTTP separada carregando seu índice (chunk 3 de 17), o total de chunks, o nome do arquivo e um identificador estável que identifica exclusivamente esta sessão de upload.
  3. Remontar no servidor. O servidor salva cada chunk no disco identificado pelo seu índice. Assim que todos os chunks chegarem, ele os concatena em ordem no arquivo final.

A capacidade de retomada vem do passo 3 mais uma etapa de verificar-antes-de-enviar no cliente. Antes de fazer upload de um chunk, o cliente pergunta ao servidor "você já tem o chunk N?" (normalmente via uma requisição HTTP HEAD). Se sim, pula esse chunk. Então, após uma falha ou atualização de página, o cliente varre novamente o arquivo e reenvia apenas as partes faltantes. O identificador estável é o que permite ao servidor reconhecer um upload retomado pela metade.

File (2.5 MB)
└─ slice into 1 MB chunks ──► [chunk 1] [chunk 2] [chunk 3 (0.5 MB)]
                                  │         │          │
              HEAD /upload?chunk=N  (already there? skip : send)
                                  ▼         ▼          ▼
                          POST /upload (one request per missing chunk)
                                  └────────┬─────────┘
                          server saves chunk-N.bin, then concatenates in order

Benefícios dos Uploads de Arquivo Retomáveis

  • Melhor experiência do usuário: Os usuários podem retomar uploads sem começar do zero.
  • Eficiência: Apenas as partes faltantes são transferidas após uma falha, não o arquivo inteiro.
  • Confiabilidade em redes ruins: Interrupções de rede são tratadas adequadamente, o que é mais importante para arquivos grandes e conexões móveis.
  • Menor pressão de memória: Trabalhar com pequenas fatias evita armazenar um arquivo de vários gigabytes na memória.

Implementando Uploads de Arquivo Retomáveis em JavaScript

Configurando o Ambiente

Antes de mergulhar na implementação, certifique-se de ter as seguintes ferramentas e bibliotecas:

  • Um navegador web moderno com suporte a JavaScript.
  • Um servidor capaz de lidar com uploads de arquivo.
  • A biblioteca resumable.js (ou uma biblioteca similar) para gerenciar a lógica do lado do cliente.

Instale as dependências Node.js necessárias:

npm install express cors

Configuração do Lado do Servidor

Primeiro, configure seu servidor para lidar com chunks de arquivo e armazenar metadados sobre os arquivos enviados. Aqui está um exemplo usando Node.js e Express. Note que resumable.js envia metadados de chunk na query string por padrão, então lemos de req.query e usamos um diretório temporário por arquivo para lidar com segurança com a chegada de chunks fora de ordem.

const express = require('express');
const cors = require('cors');
const fs = require('fs');
const path = require('path');
const app = express();
const port = 3000;

app.use(cors());

// Handle chunk verification for testChunks: true
app.head('/upload', (req, res) => {
  res.set('Access-Control-Allow-Origin', '*');
  const chunkNumber = parseInt(req.query.resumableChunkNumber);
  const identifier = req.query.resumableIdentifier;
  const chunkPath = path.join('uploads', identifier, `chunk-${chunkNumber}.bin`);
  fs.promises.access(chunkPath)
    .then(() => res.status(200).end())
    .catch(() => res.status(404).end());
});

app.post('/upload', async (req, res) => {
  try {
    const chunkNumber = parseInt(req.query.resumableChunkNumber);
    const totalChunks = parseInt(req.query.resumableTotalChunks);
    const identifier = req.query.resumableIdentifier;
    const fileName = req.query.resumableFilename;

    const chunkDir = path.join('uploads', identifier);
    await fs.promises.mkdir(chunkDir, { recursive: true });

    // Read raw body (resumable.js sends chunks as application/octet-stream)
    const buffer = await new Promise((resolve, reject) => {
      const chunks = [];
      req.on('data', chunk => chunks.push(chunk));
      req.on('end', () => resolve(Buffer.concat(chunks)));
      req.on('error', reject);
    });

    const chunkPath = path.join(chunkDir, `chunk-${chunkNumber}.bin`);
    await fs.promises.writeFile(chunkPath, buffer);

    const receivedChunks = (await fs.promises.readdir(chunkDir)).length;
    if (receivedChunks === totalChunks) {
      // Concatenate chunks IN ORDER, one at a time (see warning below).
      const finalPath = path.join('uploads', fileName);
      await fs.promises.writeFile(finalPath, ''); // start with an empty file
      for (let i = 1; i <= totalChunks; i++) {
        const data = await fs.promises.readFile(
          path.join(chunkDir, `chunk-${i}.bin`)
        );
        await fs.promises.appendFile(finalPath, data);
      }
      await fs.promises.rm(chunkDir, { recursive: true, force: true });
      res.status(200).send('File uploaded successfully');
    } else {
      // resumable.js expects a 200 OK for successful chunk uploads
      res.status(200).send('Chunk uploaded successfully');
    }
  } catch (error) {
    console.error('Upload error:', error);
    res.status(500).send('Server error during upload');
  }
});

app.listen(port, () => {
  console.log(`Server running at http://localhost:${port}`);
});
Aviso

Monte os chunks sequencialmente, não de forma concorrente. Um bug comum é redirecionar o stream de leitura de cada chunk para um único stream de escrita de uma vez (fs.createReadStream(...).pipe(writeStream) dentro de um loop). Os streams competem entre si, então os bytes se entrelaçam na ordem errada e o primeiro stream a terminar fecha o stream de escrita prematuramente — produzindo um arquivo corrompido. Leia e anexe um chunk de cada vez, como mostrado acima.

Implementação do Lado do Cliente

Agora, vamos implementar a lógica do lado do cliente usando JavaScript e a biblioteca resumable.js. Certifique-se de incluir a biblioteca resumable.js no seu projeto. Usamos a v2.1.0 para compatibilidade moderna. Para ambientes de produção, considere o protocolo padronizado tus ou o File.slice nativo com fetch para melhor controle e suporte entre plataformas.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Resumable File Upload</title>
</head>
<body>
  <input type="file" id="fileInput" />
  <button id="uploadButton">Upload</button>
  <p id="progress">Ready</p>

  <script src="https://unpkg.com/[email protected]/resumable.min.js"></script>
  <script>
    const fileInput = document.getElementById('fileInput');
    const uploadButton = document.getElementById('uploadButton');
    const progressEl = document.getElementById('progress');

    const r = new Resumable({
      target: '/upload',
      chunkSize: 1 * 1024 * 1024, // 1MB chunks
      simultaneousUploads: 1,
      testChunks: true,
      throttleProgressCallbacks: 1,
    });

    r.assignBrowse(fileInput);

    uploadButton.addEventListener('click', () => {
      if (r.files.length > 0) {
        r.upload();
      } else {
        alert('Please select a file to upload.');
      }
    });

    r.on('progress', (file, loaded, total) => {
      const percent = Math.round((loaded / total) * 100);
      progressEl.textContent = `Uploading ${file.fileName}: ${percent}%`;
    });

    r.on('fileSuccess', (file, message) => {
      console.log(`File ${file.fileName} uploaded successfully.`);
      progressEl.textContent = 'Upload complete!';
    });

    r.on('fileError', (file, message) => {
      console.error(`Error uploading file ${file.fileName}: ${message}`);
      progressEl.textContent = 'Upload failed.';
    });
  </script>
</body>
</html>

Alternativa Nativa: File.slice + fetch

Para projetos que preferem zero dependências, você pode implementar uploads retomáveis nativamente usando o método File.slice e fetch. Isso dá a você controle total sobre cabeçalhos, novas tentativas e — principalmente — a lógica de retomada. A função abaixo constrói a query string de cada chunk, pergunta ao servidor se o chunk já existe com uma requisição HEAD e envia apenas os que estão faltando. Chamá-la novamente após uma interrupção pula tudo que já passou:

async function uploadFileNative(file) {
  const chunkSize = 1 * 1024 * 1024; // 1MB
  const totalChunks = Math.ceil(file.size / chunkSize);
  // A stable identifier so a re-run resumes the same upload session.
  const identifier = `${file.name}-${file.size}`;

  for (let i = 0; i < totalChunks; i++) {
    const params = new URLSearchParams({
      resumableChunkNumber: i + 1,
      resumableTotalChunks: totalChunks,
      resumableIdentifier: identifier,
      resumableFilename: file.name,
    });
    const url = `/upload?${params}`;

    // Resume support: skip chunks the server already has.
    const probe = await fetch(url, { method: 'HEAD' });
    if (probe.status === 200) continue;

    const start = i * chunkSize;
    const end = Math.min(start + chunkSize, file.size);
    const chunk = file.slice(start, end); // a Blob, sent as the request body

    await fetch(url, { method: 'POST', body: chunk });
  }
  console.log('Native upload complete');
}

Para tornar isso adequado para produção, você envolveria cada POST em um loop de nova tentativa com backoff exponencial e daria suporte ao cancelamento com um AbortController.

Gerenciando Metadados

É fundamental gerenciar metadados sobre o arquivo enviado e seus chunks — o índice do chunk, o total, o nome do arquivo e o identificador estável. Essa informação é o que permite ao servidor retomar um upload a partir do chunk correto após uma interrupção. A lógica do servidor para rastrear e montar chunks é abordada na seção anterior.

Para produção, evite depender apenas do sistema de arquivos para rastrear o progresso: ele não oferece garantias de persistência e não é seguro quando vários chunks chegam ao mesmo tempo (a verificação de comprimento do readdir pode sofrer condição de corrida). Use um banco de dados ou cache (como Redis) para registrar quais chunks foram concluídos e monte o arquivo somente após confirmar todos os índices. Se você precisar enviar metadados estruturados extras junto com um chunk, a API FormData permite agrupar campos e o blob binário em uma única requisição.

Exemplo: Upload de Arquivos Grandes

A configuração do cliente permanece idêntica ao exemplo anterior. Para otimizar para arquivos grandes, você pode aumentar o chunkSize (por exemplo, para 5 MB) e ajustar simultaneousUploads com base na capacidade do seu servidor e nas condições de rede.

Dicas Profissionais para Uploads de Arquivo Retomáveis

  • Otimize o Tamanho do Chunk: Ajuste o tamanho do chunk com base na velocidade média de rede e no tamanho do arquivo para equilibrar velocidade de upload e confiabilidade.
  • Tratamento de Erros: Implemente mecanismos robustos de tratamento de erros para lidar com interrupções de rede e problemas no servidor.
  • Feedback ao Usuário: Forneça feedback em tempo real aos usuários sobre o progresso do upload e quaisquer problemas encontrados.
  • Segurança: Certifique-se de que o processo de upload de arquivo seja seguro, validando tipos de arquivo e implementando autenticação e autorização adequadas.
  • Alternativas Modernas: Para ambientes de produção, considere protocolos padronizados como tus ou o File.slice nativo com fetch para melhor controle, capacidade de retomada e compatibilidade entre plataformas.

Seguindo essas diretrizes e exemplos, você pode implementar um sistema de upload de arquivo retomável robusto e eficiente em JavaScript — um que sobrevive a redes instáveis e dá aos usuários a confiança de que um upload grande não será desperdiçado.

Tópicos Relacionados

  • Fetch API — a forma moderna de enviar cada chunk para o servidor.
  • Fetch: Progresso de download — leia um corpo de resposta em stream para reportar o progresso.
  • Fetch: Abort — cancele um upload em andamento com AbortController.
  • Blob — o tipo retornado por File.slice, que representa cada chunk.
  • File e FileReader — lendo o arquivo que o usuário selecionou.
  • FormData — agrupe dados binários com campos extras em uma única requisição.

Prática

Prática
Quais dos itens a seguir são benefícios do uso de uploads de arquivo retomáveis?
Quais dos itens a seguir são benefícios do uso de uploads de arquivo retomáveis?
Was this page helpful?