Armazenamento de Arquivos
Todo backend de dados de app vem com um armazenamento de objetos privado ao lado das suas tabelas. Onde o backend de dados guarda suas linhas de séries temporais, o armazenamento de arquivos guarda tudo o que não cabe em uma linha: frames de câmera, relatórios em PDF, blobs de firmware, clipes de áudio, exportações.
Os dois foram projetados para serem usados juntos. Armazenar um arquivo lhe devolve uma URL permanente, e o padrão pretendido é escrever essa URL em uma coluna de tabela no mesmo gesto — um widget de dashboard então renderiza a imagem sem nenhum trabalho adicional:
Python
from datetime import datetime, timezone
info = await ironflock.files.put("part-1.jpg", jpeg_bytes, content_type="image/jpeg")
await ironflock.publish_to_table("inspections", {
"tsp": datetime.now(timezone.utc).isoformat(),
"part_id": "1",
"photo_url": info.url,
})A API completa do cliente — leitura, listagem, uso, links de compartilhamento, objetos grandes, códigos de erro — está documentada na referência do SDK. Esta página cobre o lado da plataforma: como o armazenamento é declarado, governado, compartilhado e operado.
Como Funciona
- Opcionalmente, declare uma seção
files:em.ironflock/data-template.yml. - Quando um usuário instala o seu app em um projeto, o IronFlock provisiona uma área de armazenamento privada para ele — exatamente como provisiona o banco de dados do projeto.
- O seu código de borda armazena e lê objetos através da API
filesdo SDK. - Desinstalar o app remove o seu armazenamento completamente: cada objeto e cada credencial, assim como o esquema de banco de dados é descartado.
Assim como o banco de dados, o armazenamento é por projeto. O mesmo app instalado em dois projetos recebe duas áreas de armazenamento totalmente separadas e, como desenvolvedor do app, você não tem acesso a nenhuma delas — os dados pertencem ao usuário que executa o seu app.
Configuração zero é uma configuração válida: um app sem uma seção files: ainda recebe um namespace chamado default, de modo que files.put(...) funciona de imediato para todo app. Esse default implícito só existe enquanto o data template não declara namespaces próprios: declarar qualquer um o substitui, então liste também um chamado default (como abaixo) se as chamadas sem namespace devem continuar funcionando — caso contrário, elas falham com NO_SUCH_NAMESPACE.
Declarando o Armazenamento no Data Template
A seção files: fica ao lado de data: no data-template.yml:
files:
description: Camera frames and generated inspection reports.
# Orçamento de armazenamento que o app SUGERE para si mesmo, em bytes (aqui 5 GiB).
# O usuário do projeto pode sobrescrevê-lo; a configuração dele é a que é imposta.
quotaBytes: 5368709120
namespaces:
# Mantenha o default: declarar namespaces substitui o implícito.
- name: default
description: Everything without rules of its own.
- name: frames
description: Raw camera frames, one JPEG per inspected part.
contentTypes: ["image/jpeg"]
maxObjectBytes: 20971520
retention: { deleteAfter: 30 days }
- name: reports
description: Generated PDF inspection reports.
contentTypes: ["application/pdf"]
private: trueNamespaces
Um namespace é um prefixo de chave que carrega política. Ele não é um bucket de armazenamento separado — todo namespace de um app vive dentro da única área de armazenamento do app, e o nome do namespace simplesmente se torna o primeiro segmento do caminho de armazenamento de cada objeto.
Essa distinção lhe diz quando declarar um:
- Organizando arquivos? Use caminhos de chave —
2026/03/part-1.jpg— dentro do namespacedefault. Chaves em estilo de pastas são o caso normal, e oiter/iteratedo SDK percorre uma pasta (um prefixo que termina em/), incluindo as subpastas. - Regras diferentes para um conjunto de objetos? Declare um namespace. Regras são a única coisa que um namespace adiciona.
As regras que um namespace pode carregar:
| Campo | Significado |
|---|---|
name | Letras minúsculas, dígitos e hifens, começando com uma letra. sys, system, ironflock e ironflock-* são reservados |
description | Exibida aos usuários e legível por agentes de IA |
private | Exclui o namespace do acesso entre apps. O padrão é false (compartilhado), exatamente como as tabelas |
contentTypes | Tipos MIME permitidos, globs são aceitos (image/*). Padrão: qualquer um |
maxObjectBytes | Maior objeto individual, até 5 GiB. Padrão de 100 MiB |
retention.deleteAfter | Objetos mais antigos que isso são excluídos automaticamente (30 days, 2 weeks, 1 year, …) |
share | Se os objetos podem ser abertos pela sua URL permanente: authenticated, link ou none. O padrão é authenticated — veja Compartilhando por URL |
O Orçamento de Armazenamento
quotaBytes é declarado uma vez para o app inteiro, não por namespace. Um namespace é apenas um prefixo de chave, então não há nada contra o que um orçamento por prefixo pudesse ser imposto — a cota se aplica à única área de armazenamento do app, e o próprio object store a impõe em todo caminho de escrita, incluindo uploads diretos.
Existem dois números, e a diferença é deliberada:
- A cota sugerida — o que o seu template pede. Aplicada quando o app é instalado pela primeira vez.
- A cota imposta — o que o usuário do projeto definiu. Uma vez que um usuário altera o orçamento nas configurações de armazenamento do app, o valor dele prevalece, e reimplantar o seu app não o redefine.
Se o seu template não disser nada, o padrão da plataforma se aplica (1 GiB para backends de desenvolvimento, 10 GiB para os de produção).
Retenção
Objetos que ultrapassam a idade deleteAfter do seu namespace são removidos automaticamente — o análogo, do lado dos arquivos, da política dropAfter das tabelas. A retenção roda dentro do object store onde o store a suporta, e como uma rotina diária da plataforma onde não a suporta (appliances on-premises), de modo que a retenção declarada se comporta da mesma forma em todos os lugares.
Compartilhando por URL
share decide se os objetos de um namespace podem ser abertos pela sua URL permanente:
share: authenticated(o padrão) — a URL abre para qualquer usuário autenticado com acesso READ neste backend de dados. Os dashboards,<img src>e o download por arquivo da visão de Dados dependem disso: mantenha o padrão para tudo o que os usuários visualizam.share: link— pensado para permitir, além disso, links com expiração, comlinkMaxTtlcomo o seu tempo de vida máximo. Isso ainda não está implementado: hojelinkse comporta exatamente comoauthenticated, elinkMaxTtlnão tem efeito.share: none— a URL responde com 404, mesmo para usuários com acesso READ, também em appliances e pelo túnel na nuvem deles. Use-o para objetos que apenas o código do seu app lê, comget— resultados intermediários de processamento, arquivos de modelo ou de calibração —, para que nenhuma URL armazenada em uma tabela ou copiada para outro lugar os abra. Os widgets não conseguem exibir esses objetos, e a visão de Dados os lista, mas não consegue baixá-los.
share governa a URL permanente e nada mais; ele não restringe quem pode ler os objetos. A leitura com get, os links com expiração de share_url, as URLs de upload, o assistente de IA e as credenciais S3 diretas funcionam da mesma forma em todos os namespaces, e o que outros apps podem ler é decidido apenas por private (veja Compartilhando Arquivos Entre Apps). Nenhum valor torna uma URL pública, e qualquer valor diferente desses três é reprovado na validação do data template, de modo que o app não pode ser instalado nem atualizado com ele.
Os SDKs não conseguem ver essa configuração, porque o catálogo não a informa: em um namespace com share: none, url e o campo url de um objeto ainda fornecem uma URL, então não a escreva em uma tabela.
A regra pertence ao namespace e é aplicada sempre que uma URL é solicitada; ela não é armazenada com os objetos. Por isso, quando um projeto atualiza o app para um release que altera share, o novo valor também se aplica aos objetos já armazenados: as URLs nas suas colunas de tabela continuam as mesmas e passam a funcionar ou deixam de funcionar. Nenhum objeto é movido ou excluído.
URLs Permanentes e Dashboards
Todo objeto armazenado tem uma URL estável no formato https://files.ironflock.com/f/<backend>/<namespace>/<key>. Três propriedades fazem dela a coisa certa para escrever em uma coluna de tabela:
- Ela nunca expira. A URL é um endereço puro; permanece válida durante toda a vida do objeto.
- Ela não é um link público. Toda requisição passa por um proxy de autenticação que verifica se o solicitante está autenticado e possui acesso READ neste backend de dados; o proxy mantém cada decisão em cache por até 5 minutos. Revogar o acesso de um usuário revoga a capacidade dele de buscar qualquer arquivo em até 5 minutos.
- Ela renderiza em um
<img>. O navegador envia o seu cookie de sessão automaticamente, de modo que um widget de dashboard pode usar a URL em<img src>,<video src>ou em um link de download sem nenhum JavaScript envolvido.
Um namespace declarado com share: none fica de fora: nele, a URL responde com 404.
Para entregar um arquivo a alguém fora do projeto, o share_url do SDK cunha, em vez disso, um link ao portador (bearer) com expiração — veja compartilhando objetos para saber quando usar cada um. Uma URL de upload (upload_url / uploadUrl) também é uma capacidade ao portador, uma que escreve: os tipos de conteúdo e o limite de tamanho do namespace e a cota são verificados quando ela é gerada, não quando é usada, então qualquer pessoa que a possua pode escrever qualquer conteúdo na sua chave até ela expirar.
Arquivos Grandes
Transferências de até 6 MiB viajam como uma única chamada através do sistema de mensagens. Qualquer coisa maior é transportada diretamente entre o dispositivo e o armazenamento de objetos via HTTPS — o SDK alterna automaticamente, e os helpers de arquivo dos SDKs Python e Go fazem streaming de e para o disco, de modo que um arquivo de múltiplos gigabytes nunca precisa caber na memória. O teto de upload único é de 5 GiB.
O caminho direto exige uma implantação capaz de pré-assinar URLs de armazenamento e um dispositivo que alcance o host de armazenamento de objetos (s3.ironflock.com), não apenas o roteador de mensagens. Onde falta um dos dois, os SDKs leem um objeto grande em faixas de bytes através do roteador (FleetFiles v0.1.33 e posteriores). As escritas não têm esse fallback: onde a implantação não consegue pré-assinar, um objeto maior não pode ser armazenado de forma alguma (TOO_LARGE), e se um proxy de fábrica permitir apenas o roteador, os uploads grandes falham com o código explícito PRESIGN_UNREACHABLE em vez de um erro genérico. Um relógio de dispositivo defasado em mais de 15 minutos falha com CLOCK_SKEW, o que é um chamado para verificar o NTP, não as credenciais.
Compartilhando Arquivos Entre Apps
O acesso a arquivos entre apps se apoia no mesmo consentimento que o acesso a tabelas entre apps. Há um único interruptor: quando um usuário do projeto concede ao app B acesso aos dados do app A (o consentimento data_access nas configurações do app), essa concessão cobre as tabelas de A e os namespaces de arquivos não privados de A. Revogá-la revoga ambos.
O que o seu app controla como provedor é o flag private por namespace:
private: false(o padrão) — apps que possuem uma concessão de data-access podem lê-lo (nunca escrevê-lo).private: true— o namespace é invisível para outros apps, ponto final, mesmo sob uma concessão.
Isso espelha exatamente consumindo dados de outros apps: namespaces e tabelas têm o mesmo padrão. Nada é compartilhado sem a concessão do usuário do projeto — private: apenas restringe o que um leitor já consentido enxerga, não é o consentimento em si.
A Visão do Usuário
Os usuários do projeto veem e governam o armazenamento do seu app em dois lugares:
- A visão de Dados mostra uma entrada Arquivos ao lado das tabelas e views de cada app — uma listagem pesquisável de cada objeto armazenado com tamanho, tipo e data de modificação, e download por arquivo.
- As configurações de armazenamento do app mostram o uso (bytes e contagem de objetos), o orçamento imposto ao lado da sugestão do seu app, um controle para alterar o orçamento e uma ação Excluir todos os arquivos — o gêmeo, do lado dos arquivos, de esvaziar todas as tabelas. A exclusão é confirmada digitando o nome do app e não pode ser desfeita.
- O assistente de IA pode listar, pesquisar e ler esses arquivos em nome de um usuário. A mesma verificação
DATABACKEND/READse aplica, portanto ele nunca exibe um arquivo que o usuário não pudesse abrir por conta própria. A busca é feita nos caminhos dos arquivos, não no conteúdo, e ele lê arquivos de texto, imagens e PDFs — arquivos compactados e outros formatos binários ele não consegue ler.
Como acontece com as tabelas, estes são os dados do usuário: ele pode inspecioná-los, limitá-los e excluí-los sem envolver você.
Acesso Direto ao S3
Para tudo o que vai além do SDK — um analista com DuckDB, um backup noturno com rclone, um pipeline de BI — um usuário do projeto pode emitir credenciais S3 somente leitura por app a partir das configurações de armazenamento do app.
Cada credencial é restrita à área de armazenamento daquele único app: uma credencial emitida para o app A não funciona para os arquivos do app B, estruturalmente — a política de armazenamento de B simplesmente nunca a nomeia. Um projeto pode manter várias credenciais por app (uma por consumidor: um job de CI, um script de backup, um laptop), e revogar uma deixa as demais funcionando. Essa é também a história da rotação: se uma chave vazar, emita uma segunda credencial, migre o consumidor para ela, revogue a primeira — nenhum outro consumidor é perturbado.
O segredo é exibido uma única vez, na criação. A emissão requer acesso de leitura ao backend de dados desse app — a mesma permissão que permite ler os arquivos em primeiro lugar, de modo que a credencial nunca pode ampliar o alcance de ninguém.
# rclone
rclone config create myapp s3 provider=Other \
endpoint=https://s3.ironflock.com \
access_key_id=ifs-key-3317-x7k2m secret_access_key=<shown once>
rclone ls myapp:if-1042-3317# DuckDB
CREATE SECRET (TYPE S3, KEY_ID 'ifs-key-3317-x7k2m', SECRET '<shown once>',
ENDPOINT 's3.ironflock.com');
SELECT * FROM read_parquet('s3://if-1042-3317/exports/*.parquet');As configurações de armazenamento mostram o endpoint e o nome exato do bucket para o qual apontar um cliente. Note que uma listagem de buckets de nível superior (aws s3 ls sem argumento) não retorna nada por design — a credencial não possui nenhum bucket; a ela é concedido acesso a um. Enderece o bucket diretamente, como acima.
Appliances On-Premises
O armazenamento de arquivos funciona de forma idêntica em um appliance on-premises, com quatro diferenças que decorrem do design do appliance:
- Os arquivos são servidos na mesma origem (same-origin) sob o próprio endereço do appliance (
/files/...) — sem nome DNS extra, sem certificado extra, e funciona em instalações de HTTP puro. As URLs permanentes e o<img src>se comportam exatamente como na nuvem. - O backend de armazenamento do appliance faz streaming dos downloads de arquivos através da plataforma, em vez de redirecionar para um host de armazenamento separado, de modo que o serviço de armazenamento nunca é exposto como uma segunda origem.
- Sem transferências diretas por padrão. Um appliance não fornece URLs de armazenamento pré-assinadas a menos que seja configurado para isso, então os SDKs leem objetos maiores que o limite inline (6 MiB) em faixas de bytes através da plataforma, e não conseguem armazenar objetos maiores que isso — a escrita falha com
TOO_LARGE. - Credenciais S3 diretas não estão disponíveis em appliances — o object store embarcado não consegue expressar concessões de acesso por credencial. A seção simplesmente não aparece nas configurações de armazenamento ali. Todo o resto, incluindo a retenção declarada, funciona da mesma forma.
Um appliance isolado (air-gapped) serve todo o tráfego de arquivos localmente: logos de apps, imagens de dashboard e downloads de arquivos não precisam de conectividade com a internet.
Limites em Resumo
| Limite | Valor | De onde vem |
|---|---|---|
| Transferência inline (chamada única) | 6 MiB | Reportado em tempo de execução; pode ser elevado no lado do servidor |
| Objeto individual | 100 MiB por padrão, 5 GiB no máximo | maxObjectBytes por namespace |
| Upload único | 5 GiB | Teto de PUT único do object store; multipart ainda não está disponível. Onde a implantação não consegue pré-assinar (um appliance, por padrão), o limite inline |
| Orçamento de armazenamento | 1 GiB dev / 10 GiB prod por padrão | Sugerido pelo template, decidido pelo usuário |
| Comprimento e caracteres da chave | UTF-8, caminhos separados por /; no máximo 900 bytes, 20 segmentos e 255 bytes por segmento | Sem / no início ou no fim, sem segmentos vazios, . ou .., sem caracteres de controle ou de formatação, sem caracteres como \, :, * ou ? e sem o prefixo reservado _sys/ — rejeitado com INVALID_KEY, ou INTERNAL pelo FleetFiles v0.2 (veja erros de armazenamento de arquivos) |