# Brasiliana Fotográfica → acervo arquivístico em S3
> **Retomando o trabalho?** Comece por [`tasks/PLANO.md`](tasks/PLANO.md) — estado atual, achados que não devem ser redescobertos e runbook.
Captura integral do acervo da [Brasiliana Fotográfica](https://brasilianafotografica.bn.gov.br/)
— **13.060 itens de 16 instituições** — para o bucket `s3://brasiliana`
(Hetzner Object Storage, `hel1.your-objectstorage.com`).
Mural 3D do acervo no ar:
A Biblioteca Nacional foi consultada: confirmou que o material é público e que
não tem equipe técnica para fornecer um dump. A coleta por scraping é, portanto,
o caminho combinado. Todo o desenho abaixo assume ritmo cortês e identificação.
---
## Estado atual
**Coleta concluída em 2026-09-13.** Medido contra catálogo × ledger de upload:
| | |
|---|--:|
| Itens no catálogo | 13.060 |
| Com imagem de preservação | 12.985 |
| Com thumbnail do DSpace | 13.059 |
| Com miniatura derivada (`derivados/thumb256/`) | 13.059 |
| Cópia legada (objdigital) | 2.137 |
| Posts do blog WordPress | 636 |
| **Objetos no bucket (ledger)** | **41.444** |
| Peças no mural 3D | 13.059 |
Os **75 itens sem imagem de preservação** estão em [`errors.md`](errors.md):
a página do item responde `200`, mas a origem serve o bitstream com `HTTP 500`.
Nenhum fica fora do mural — todos têm thumbnail. A fila local está zerada:
tudo que foi baixado está no bucket.
---
## O acervo
DSpace 4.2, handles com prefixo `20.500.12156.1`. **13.036 itens**, conferidos
comunidade a comunidade — a soma das 15 fecha exatamente com o total da raiz,
sem sobra nem duplicata.
| Instituição | Itens | | Instituição | Itens |
|---|--:|---|---|--:|
| Instituto Moreira Salles | 5.133 | | Fiocruz | 548 |
| Biblioteca Nacional | 2.726 | | Marinha (DPHDM) | 456 |
| Museu Aeroespacial | 1.143 | | Arquivo Nacional | 383 |
| Museu da República | 906 | | Arq. Geral da Cidade do RJ | 248 |
| Museu Histórico Nacional | 649 | | Leibniz-Institut, Leipzig | 216 |
| IHGB | 206 | | Fund. Joaquim Nabuco | 190 |
| Museu Goeldi | 95 | | FGV CPDOC | 79 |
| IBGE | 58 | | | |
**Espaço projetado: ~116 GB** (média de 9,08 MB em 6 arquivos medidos ×
13.036). Mediana 3,99 MB dá ~51 GB; a distribuição tem cauda pesada, então a
média é o estimador certo para o total. Amostra pequena — confirmar com `sizes`.
---
## Cinco descobertas que definem o desenho
### 1. O sitemap está sete anos desatualizado
`htmlmap`/`sitemap` foram gerados em **abril de 2019** e listam ~5.300 itens.
Quem enumerar por eles — o caminho óbvio, e o que ferramentas de mirror usam —
perde **59% do acervo** sem erro aparente: o crawl termina limpo, pela metade.
Fonte correta: o índice de navegação, construído ao vivo.
`browse?type=title&rpp=100&offset=N`, percorrido por comunidade (o que de quebra
etiqueta a instituição de origem, que a página do item não expõe de forma
confiável).
### 2. O arquivo de preservação está no DSpace, não no objdigital
O campo `DC.identifier` aponta para `objdigital.bn.br`, e é natural supor que
ali esteja o master. **Medido, é o contrário:**
| Item 6066 | Resolução | Peso |
|---|---|---|
| Bitstream do DSpace | 5533×4065 (22 MP) | 7,39 MB |
| Cópia no objdigital | ~2600×2100 (5,5 MP) | 147 KB |
O maior bitstream medido chega a **8679×6484 (34,7 MB)**. O EXIF mostra
digitalização em Hasselblad H3D II-39MS a 7216×5412 — preservação de verdade.
O PDF de 71,9 MB que aparece no objdigital do item 14582 **não** é a mesma
imagem em melhor qualidade: é outro objeto, um álbum de várias páginas.
> **Regra de captura:** o bitstream do DSpace é o arquivo de preservação e vai
> sempre. A cópia do objdigital entra como procedência, em `legado/`, nunca
> como substituta. Inverter isso arquiva o acervo a um quarto da resolução, e
> nada no site avisa.
### 3. O Cloudflare bloqueia automação de navegador, não IP nem geografia
Medido na mesma máquina, mesmo IP, mesmo instante:
| Cliente | Páginas HTML |
|---|---|
| Chrome do usuário | ✅ 200 |
| `fetch()` de dentro da página | ✅ 200 (>100 requisições seguidas) |
| Playwright *headed* | ❌ 200 na primeira, 403 depois |
| Playwright *headless* | ❌ 403 |
| curl / httpx | ❌ 403 |
| Servidor na Finlândia | ❌ 403 |
Não é reputação de IP: o Chrome comum carregava normalmente enquanto o
Playwright levava 403 na mesma rede. É detecção de controle remoto (CDP).
**Consequência:** proxies não ajudam — o bloqueio dispara na segunda requisição
da mesma sessão, não por origem. Testado e descartado.
### 4. Bitstreams passam por curl; páginas HTML não
Curioso e muito útil: `bitstream/...` responde 200 a `curl -L` simples, mesmo
com UA de curl. Só as páginas HTML são desafiadas. Isso permite baixar os
116 GB fora do navegador — inclusive de um servidor.
### 5. Nomes de arquivo com percent-encoding levam 403
Aqui mora a pendência. Testado com cinco user-agents (Chrome, Safari, Firefox,
Googlebot, curl): **todos 403** numa URL com `%28`/`%20`, enquanto `curl/8.7.1`
puro dá **200** numa URL de nome simples. O gatilho é o caminho, não o cliente
— variar user-agent não resolve.
| Arquivo | curl | navegador |
|---|---|---|
| `imagem.jpg` | ✅ 200 | ✅ 200 |
| `LB.001%288%29.dvft.JPG` | ❌ 403 | ✅ 200 |
| `GV%20dvft%20...Get%c3%balio...JPG` | ❌ 403 | ✅ 200 |
Itens da Biblioteca Nacional usam `imagem.jpg`; os do IMS usam nomes
descritivos com espaços e acentos. Como o IMS sozinho são 5.133 itens, a
fração afetada é grande — mas **o número exato sai do catálogo, sem nenhuma
requisição extra**: basta contar quantos `preservation_url` precisam de
encoding.
---
## Arquitetura: duas trilhas
```
páginas HTML (13.036) arquivos (116 GB)
│ │
coletor.js ──────────────┘ │
(Chrome real, in-page fetch) │
→ catalogo.jsonl │
┌────────────────┴────────────────┐
│ │
nome simples │ nome com encoding
│ │
TRILHA A ▼ TRILHA B ▼
baixador.py na Finlândia navegador na máquina local
curl → sha256 → S3 → apaga fetch → download → S3 → apaga
4,7 MB/s, mesmo datacenter (uploader por construir)
```
A cópia legada do `objdigital` **não é alcançável da Finlândia** (conexão
morta) — essas rodam na máquina local, com `--somente-legado`. São ~328 KB em
média, ~1,5 GB no total.
**Nada acumula em disco.** Cada arquivo é baixado, somado, enviado e apagado;
pico de uso local é um arquivo. O `baixador.py` aborta se o disco livre cair
abaixo de 2 GB (a Finlândia tem só 31 GB).
---
## Runbook
Todos os comandos abaixo assumem `cd pipeline/` primeiro — é onde vivem os
scripts, o catálogo e o estado (`.sqlite`, `.log`, `.tmp/`).
### Fase 1 — metadados (navegador, ~7 h)
1. Abra
2. DevTools → Console → cole `pipeline/coletor.js` inteiro
3. `BRAS.descobrir()` — ~5 min, enumera os 13.036 handles
4. `BRAS.coletar()` — o passo longo; `BRAS.status()` acompanha, `BRAS.parar()` interrompe
5. `BRAS.exportar()` — baixa `brasiliana-catalogo-AAAA-MM-DD.jsonl`
O estado fica em `localStorage`: pode fechar e retomar colando o arquivo de novo.
### Fase 2 — arquivos (trilha A, servidor)
```bash
scp pipeline/baixador.py pipeline/catalogo.jsonl finland:/root/brasiliana/
ssh finland
set -a; . /root/.s3env; set +a
cd /root/brasiliana
python3 baixador.py --catalogo catalogo.jsonl --pular-legado
python3 baixador.py --status
```
### Fase 3 — trilha B, arquivos com encoding (máquina local)
Na mesma aba do coletor, cole `pipeline/baixador-navegador.js`, depois:
```
BAIXA.quantos() // quantos precisam desta trilha
BAIXA.iniciar() // os arquivos caem em ~/Downloads
```
Em outro terminal, em paralelo — sobe cada arquivo e apaga na hora:
```bash
cd pipeline
set -a; . .s3env; set +a
python3 subidor.py --vigiar --catalogo catalogo.jsonl
```
### Fase 4 — cópias legadas (máquina local)
```bash
cd pipeline
set -a; . .s3env; set +a
python3 baixador.py --catalogo catalogo.jsonl --somente-legado
```
---
## Estrutura no bucket
```
s3://brasiliana/
MANIFEST.md o que é, como foi coletado, quando
catalogo.jsonl uma linha por item
wordpress-posts.jsonl uma linha por post do blog
falhas-500-2026-09-13.txt itens que a origem não serve
itens/20.500.12156.1_6066/
metadata.json Dublin Core completo + procedência
preservacao/imagem.jpg 5533×4065, 7,4 MB
thumbnail/thumb_2.jpg derivado do DSpace (721–3.709 px)
legado/icon843660.jpg 147 KB, referência de origem
manifest-sha256.txt checksum de cada arquivo do item
derivados/thumb256/
20.500.12156.1_6066.webp miniatura gerada por nós, 256 px
```
`itens/` é material de origem; `derivados/` é o que geramos e pode ser
reconstruído do zero. A separação é proposital — misturar apagaria a
procedência que o resto do manifesto garante.
Dois pontos não negociáveis: **SHA-256 de tudo** (prova de integridade) e
**URL de origem + data/hora UTC de captura** em cada `metadata.json`
(procedência). Sem os dois é uma pasta de imagens, não um arquivo. O formato
é compatível com BagIt se um dia for depositado em outro repositório.
---
## Estrutura do repositório
```
brasiliano/
README.md este arquivo
tasks/ retomada de sessão e plano de trabalho
PLANO.md estado atual, achados, runbook (comece por aqui)
TODO.md lista de trabalho da sessão corrente
PROMPT_TERMINAL.md runbook original (parcialmente superado, ver PLANO.md)
pipeline/ scripts ativos + estado em disco (catálogo, .sqlite, .log, .tmp/, credenciais)
coletor.js Fase 1 — metadados, console do navegador
coletor-resume.js idem, com resume + reparo juntos
coletor-reparo.js reprocessa itens marcados com erro
coletor-chrome.js trilha paralela (2ª aba/navegador)
baixador-navegador.js Fase 3 — trilha B, baixa no navegador p/ ~/Downloads
baixador-thumb.js baixa miniaturas
baixador-extras.js baixa bitstreams extras de itens com múltiplas imagens
reparo-multibitstream.js detecta itens com mais de um bitstream real
baixador.py Fases 2 e 4 — curl → sha256 → S3, um arquivo por vez
subidor.py Fase 3 — Downloads → S3, um por vez (APOSENTADO, ver TODO)
subidor-paralelo.py substituto: boto3 + threads, ~19 GB/h; valida por
assinatura (JPEG/PNG/TIFF/BMP/GIF/JP2/WebP) e
nunca apaga o que recusa — manda pra rejeitados/
rodar-paralelo.sh drena a fila, limpa duplicatas, regrava manifests
vigia-30min.sh a cada 30 min sobe o que chegar e apaga o que subiu
baixar-faltantes-v3.js console do navegador: baixa os itens que faltam
falhas-500-2026-09-13.txt os 75 que a origem não serve (ver errors.md)
catalogo.jsonl catálogo corrente, uma linha por item
catalogo-teste.jsonl dois itens, teste de fumaça
.env, .s3env, .s3env.exemplo credenciais AWS (reais e modelo)
*.sqlite, *.log, .tmp/ estado e logs dos scripts acima — não versionar
web/ visualizações do acervo
wall.html mural 3D
wall2.html linha do tempo
build-atlas.js gera atlas/; use --thumbs=DIR com derivados/thumb256/
atlas/ atlas-0.webp + atlas-map.json (saída do build)
examples/
exemplo-item/ um item completo, estrutura de referência do bucket
archive/
censo.py versão anterior (Playwright) — não funciona, mantida pelo parsing
```
## Arquivos deste projeto
| Arquivo | O quê |
|---|---|
| `pipeline/coletor.js` | Coletor de metadados; roda no console do Chrome |
| `pipeline/baixador.py` | Baixa arquivos e deposita no S3, um por vez |
| `pipeline/baixador-navegador.js` | Trilha B: baixa no navegador os arquivos que o curl não alcança |
| `pipeline/subidor.py` | Trilha B: leva da pasta Downloads para o S3 e apaga |
| `archive/censo.py` | Versão anterior, transporte Playwright — **não funciona**, mantida pelo parsing e pelo modelo de dados |
| `pipeline/.s3env.exemplo` | Modelo de credenciais; preencher as chaves AWS |
| `pipeline/catalogo-teste.jsonl` | Dois itens para teste de fumaça |
---
## Pendências
Lista completa e com contexto em [`tasks/TODO.md`](tasks/TODO.md). As principais:
1. **75 itens sem preservação** ([`errors.md`](errors.md)) — depende da origem
voltar a servir esses bitstreams; retomar com `FALTA3.retomar()` na aba.
2. **30 bitstreams `extra_seqN`** apagados pelo `subidor.py` antigo (que
confundia TIFF com texto de licença) precisam ser enumerados item a item
com `reparo-multibitstream.js` antes de rebaixar.
3. **30 objetos `(N)` com tamanho diferente do gêmeo** — não são duplicata,
são versões distintas; precisam de conferência individual.
4. **Reconciliar 13.058 × 13.060** — o MANIFEST fala em 13.058 itens reais e o
catálogo tem 13.060 handles distintos; a diferença nunca foi explicada.
5. **PDFs de álbum** no objdigital são objetos distintos e precisam de decisão
própria — o de 71,9 MB do item 14582 provavelmente não é caso isolado.
## Direitos
Acervo majoritariamente em domínio público por antiguidade (fotografia do séc.
XIX e início do XX), mas as 16 instituições têm políticas distintas. O campo
`dc.rights` e a instituição de origem são preservados **por item** — a decisão
de redistribuir é item a item, não do acervo em bloco. Arquivar para si é uma
coisa; republicar é outra.