# Ephemeral Downloader **Use a sua VPS como ponte de alta velocidade: ela baixa o arquivo do host lento, guarda por até 1 hora e entrega a você — tudo por uma SPA com progresso em tempo real.** ![Fastify](https://img.shields.io/badge/Fastify-5-informational) ![React](https://img.shields.io/badge/React-19-informational) ![Node.js](https://img.shields.io/badge/Node.js-%E2%89%A5%2020-informational) [Como funciona](#como-funciona) · [Execução local](#execu%C3%A7%C3%A3o-local) · [Produção](#produ%C3%A7%C3%A3o) · [Limitações](#limita%C3%A7%C3%B5es-conhecidas)
> [!NOTE] > **Estado do projeto:** funcional, sem testes automatizados e sem CI (7 commits, > o último em 2026-02-14). O fluxo atual **baixa primeiro para o disco da VPS** e > só depois entrega o arquivo ao navegador — o modelo antigo de streaming direto > (`GET /stream`, retomada por `offset`) foi aposentado e a rota responde `410`. > [!IMPORTANT] > **Não há autenticação, cota nem lista de destinos permitidos.** Qualquer cliente > que alcance a porta pode mandar o servidor buscar uma URL e ocupar disco. Coloque > atrás de VPN, proxy com autenticação ou firewall. ## Como funciona Por que existe: às vezes o gargalo não é a sua internet nem o servidor de origem, e sim o caminho entre eles. Com uma VPS no meio, o arquivo vem por uma rota melhor até ela, e dela até você. | Etapa | O que acontece | |---|---| | 1. Sessão | A SPA pede `/api/session`; o servidor cria uma sessão (UUID + pasta própria) e o navegador guarda o id em `localStorage` | | 2. Fila | Você cola um link e o servidor começa o download na hora (`queued` → `downloading` → `completed` ou `error`) | | 3. Progresso | A SPA consulta `/api/tasks` a cada 2 s: bytes recebidos/total, porcentagem e velocidade em MB/s e Mbps | | 4. Entrega | Com a tarefa concluída, **Save to Disk** baixa o arquivo da VPS para a sua máquina | | 5. Limpeza | Sem atividade por **1 hora**, a sessão expira e a pasta com os arquivos é apagada; um cronômetro na tela mostra o tempo restante | ```mermaid sequenceDiagram participant N as Navegador (SPA React) participant V as Fastify (VPS) participant O as Servidor de origem N->>V: GET /api/session V-->>N: x-session-id + expiresAt N->>V: POST /api/queue { url } V->>O: GET url (undici) O-->>V: bytes V->>V: grava em storage/{sessão}/{id}_{arquivo} loop a cada 2 s N->>V: GET /api/tasks V-->>N: progresso, velocidade end N->>V: GET /api/download/{taskId} V-->>N: arquivo Note over V: 1 h sem atividade → sessão e arquivos apagados ```
API do servidor e detalhes técnicos | Rota | Função | |---|---| | `GET /health` | `{ "status": "ok" }` | | `GET /api/session` | Cria ou recupera a sessão (cabeçalho `x-session-id`); devolve `sessionId`, `expiresAt` e `remainingMs` | | `POST /api/queue` | Corpo `{ "url": "..." }`; `400` se a URL for inválida; devolve a tarefa | | `GET /api/tasks` | Tarefas da sessão | | `GET /api/download/:taskId` | `404` se não existe, `403` se é de outra sessão, `400` se não concluída; senão envia o arquivo e renova o prazo da sessão | | `GET /stream` | Descontinuada: `410` | | `GET /` e demais `GET` | `index.html` do build do Vite (`web/dist`) | | Item | Implementação | |---|---| | Servidor | Fastify 5 + `@fastify/cors` (`origin: true`) + `@fastify/static`, validação com `zod` | | Cliente HTTP | `undici` com agente global (até 500 conexões, `keepAliveTimeout` 10 s, `connect` 30 s) e `User-Agent: Ephemeral-Downloader/2.0` | | Nome do arquivo | Da URL; sobrescrito por `Content-Disposition` quando existir | | Armazenamento | `server/storage/{sessão}/{id}_{nome}`, relativo ao diretório de trabalho (`process.cwd()`); coletor a cada 1 min remove sessões expiradas e pastas órfãs com mais de 1 h | | Frontend | React 19, Vite 7, Tailwind CSS 4, `lucide-react`; componentes em `web/src/components/{ui,feature}` | | Estado das tarefas | Em memória (`Map`) — um restart do servidor perde a lista de tarefas |
## Limitações conhecidas - **Sem retomada e sem lote de links.** Não há `Range`/`offset` no fluxo atual: download interrompido vira `error` e precisa ser refeito. O formulário aceita uma URL por vez. - **Redirecionamentos.** O `DownloadManager` não define `maxRedirections`, e o `undici.request` não segue redirects por padrão; links que respondem `3xx` (comum em hosts de mirror) podem não resultar no arquivo esperado. - **Entrega via memória do navegador.** O **Save to Disk** faz `fetch` do arquivo inteiro como `blob` antes de salvar; arquivos muito grandes pesam na memória da aba. - **Sessão sem validação de formato.** O `x-session-id` enviado pelo cliente é usado direto para montar o caminho da pasta da sessão. - **Sem limite de tamanho, de downloads simultâneos ou de espaço em disco** — todo link enfileirado começa imediatamente. - `server/dist/index.js` está versionado e é do modelo antigo (só `/stream`, sem `/api/queue`): rode `pnpm build` antes de `pnpm start`. ## Execução local Requisitos: **Node.js ≥ 20** e **pnpm ≥ 8**. ```bash pnpm install pnpm dev # servidor em :3000 (tsx) e Vite em :5173 pnpm build # compila server (tsc) e web (tsc -b + vite build) pnpm start # Fastify serve a API e o build do front pela porta 3000 ``` > [!WARNING] > **Defina `VITE_API_URL`.** O `SessionContext` usa `""` (mesma origem) quando a > variável falta, e o `vite.config.ts` não tem proxy — em `pnpm dev` as chamadas a > `/api` batem no Vite e falham. Já o botão **Save to Disk** (`DownloadItem.tsx`) > cai em `http://localhost:3000` quando falta. Crie `web/.env` com > `VITE_API_URL=http://localhost:3000` no dev e, em produção, defina a URL > pública **antes do `pnpm build`** (o valor é embutido no bundle). ## Produção O [`nixpacks.toml`](nixpacks.toml) descreve o build (`pnpm install --frozen-lockfile` e `pnpm build`) e o start (`pnpm start`). O servidor lê `PORT` e usa `3000` se a variável não existir; escuta em `0.0.0.0`. ## Avisos - A pasta [`.agent/`](.agent) contém um kit de agentes, skills e workflows para assistentes de IA (*Antigravity Kit*, segundo o `ARCHITECTURE.md`) e não faz parte da aplicação. - Use apenas para baixar conteúdo que você tem direito de baixar.