feat(files): gerenciador de arquivos/anexos com árvore de pastas, upload, preview e picker

- store/files.go: pastas (parent, anti-ciclo), FileMeta (nota opcional = avulso),
  blobs com extensão neutralizada p/ tipos perigosos, sanitização de nome
- api/files.go: upload multipart (64MiB), tree, static com ETag/Range + CSP
  sandbox + attachment p/ html/svg/js (stored-XSS mitigado), move/rename/delete,
  CRUD de pastas
- web: overlay Ctrl+J / 📎 (modo seletor insere sintaxe md/html/txt e associa),
  filtros Todos/Anexos/Avulsos/Nota atual, busca, lightbox img/video/audio/pdf/texto,
  colar-e-arrastar no editor e no gerenciador, ações por card (preview/associar/
  mover/renomear/excluir), mobile-first (ações sempre visíveis)
- fix: api() já serializa o body — chamadas do gerenciador passavam JSON pré-string
  (dupla codificação); auth por cookie (sem Bearer manual no fetch)
- sw v10; testes store+api (traversal, ciclo, XSS headers, range); README
This commit is contained in:
lgcosta committed 2026-11-05 10:32:40 -03:00
1 parent ea6b3b915d
commit 119e6cefe9
10 files changed
+1605 -4

No files matched your search

+49
View File
@@ -301,6 +301,55 @@ No actix-web, o `fs::Files` já define MIME por extensão, mas registre antes:
"application/manifest+json")` e um `wrap_fn` que sobrepõe `Cache-Control` quando o caminho for
`/sw.js`. (No `tower-http` `SetResponseHeaderLayer` com `Match` por path é o caminho limpo.)
## Gerenciador de arquivos (anexos)
Árvore de pastas estilo sistema de arquivos + arquivos com **associação opcional
a uma nota** (sem nota = avulso). Abra com **Ctrl+J**, menu **Arquivo →
Gerenciador de arquivos…** ou o botão **📎 Anexo** (modo seletor: clicar num
arquivo insere no editor e vincula à nota).
- **Persistência (disc)**: `data/folders.json` (árvore `parent`), `data/files.json`
(metadados: nome, pasta, nota, tamanho, MIME, SHA-256) e blobs em
`data/files/<id>.<ext>` — a extensão física de tipos perigosos (html/js/svg) é
neutralizada (`.bin`/`.img`); o MIME real só vive no metadado.
- **API**:
- `POST /api/files/upload` — multipart (`file`, opcionais `folder_id`, `note_id`) → 201 com `url`
- `GET /api/files/tree` — hierarquia JSON (pastas aninhadas + arquivos + raiz)
- `GET /api/files/static/{id}` — serve com MIME real, **ETag (SHA-256) e Range**
(vídeos fazem seek), `CSP: sandbox` + `nosniff`; tipos não-inline-seguros
(html/svg/js…) vão como **`Content-Disposition: attachment`** — anexar um
`.html` malicioso **não executa same-origin** (stored-XSS mitigado)
- `PUT /api/files/{id}/move` — `{folder, note, name}` (mover/associar/desvincular/renomear)
- `DELETE /api/files/{id}` · `POST/PUT/DELETE /api/folders[/{id}]` (com anti-ciclo)
- Limite: 64 MiB/arquivo; nomes sanitizados (`filepath.Base` + whitelist); ids
gerados — path do disco nunca aparece na URL.
- **Editor**: colar (Ctrl+V) imagem/arquivo do clipboard faz upload automático e
insere `![…](url)` / `![]()` / link conforme o formato; arrastar-e-soltar
funciona no editor e no gerenciador; no WYSIWYG a inserção é visual (`<img>`/
`<video>`) e volta a Markdown na serialização; preview no gerenciador abre
lightbox com `<img>`/`<video>`/`<audio>`/PDF (iframe) ou texto puro.
### Instruções de teste
```bash
# 1) subir o servidor e criar uma nota pela UI (ou curl)
export T=seu-token; B=http://127.0.0.1:8080
# 2) upload avulso e em pasta
FID=$(curl -s -X POST $B/api/files/folders -H "Authorization: Bearer $T" \
-H 'Content-Type: application/json' -d '{"name":"docs","parent":""}' | jq -r .id)
curl -s -X POST $B/api/files/upload -H "Authorization: Bearer $T" \
-F file=@foto.png -F folder_id=$FID | jq .
# 3) árvore + estático com Range/headers
curl -s $B/api/files/tree -H "Authorization: Bearer $T" | jq
curl -sI -H "Authorization: Bearer $T" -H 'Range: bytes=0-9' $B/api/files/static/<id>
# 4) XSS guard: anexar .html deve vir attachment
printf '<script>alert(1)</script>' > x.html
curl -s -X POST $B/api/files/upload -H "Authorization: Bearer $T" -F file=@x.html | jq -r .id
curl -sI -H "Authorization: Bearer $T" $B/api/files/static/<id> | grep -i disposition
# 5) UI: Ctrl+J abre o gerenciador; cole uma imagem (Ctrl+V) no editor e arraste
# arquivos para dentro dele; use 📎 para inserir um arquivo existente.
```
## Como funciona o versionamento
- O conteúdo é guardado em `data/blobs/<aa>/<hash-sha256>` (content-addressed, como objetos do