---
title: "Download de Arquivos em Zig com HTTP, SHA-256 e Escrita Atômica"
url: "https://ziglang.com.br/artigos/zig-download-arquivos-http-checksum/"
markdown_url: "https://ziglang.com.br/artigos/zig-download-arquivos-http-checksum.MD"
description: "Como baixar arquivos em Zig com std.http.Client, limitar memória, validar status e SHA-256, gravar em arquivo temporário e publicar com rename atômico."
date: "2026-08-09"
author: ""
---

# Download de Arquivos em Zig com HTTP, SHA-256 e Escrita Atômica

Como baixar arquivos em Zig com std.http.Client, limitar memória, validar status e SHA-256, gravar em arquivo temporário e publicar com rename atômico.


Para fazer um **download de arquivo em Zig** com segurança, não basta executar um GET e salvar qualquer resposta recebida. O fluxo robusto é: usar `std.http.Client`, aceitar apenas o status esperado, impor um limite de tamanho, calcular o SHA-256 dos bytes, gravar primeiro em um arquivo temporário e renomeá-lo somente depois que todas as validações passarem.

Esse padrão evita três problemas comuns: salvar uma página de erro como se fosse o artefato, consumir memória sem limite e deixar um arquivo final incompleto após uma queda. O exemplo deste guia é adequado para binários, arquivos de configuração, modelos pequenos, pacotes e assets com tamanho máximo conhecido. Para downloads grandes, a arquitetura continua igual, mas a leitura e o hash devem acontecer em streaming.

Como `std.http` pode mudar entre versões anteriores ao Zig 1.0, confira `zig version` e a definição de `Client.fetch` instalada no projeto. O contrato operacional — limite, status, checksum, temporário e rename — é mais durável que qualquer assinatura específica da biblioteca padrão.

## Resposta rápida

| Necessidade | Decisão recomendada |
|---|---|
| Baixar arquivo pequeno ou médio | `std.http.Client.fetch` com resposta em buffer limitado |
| Evitar salvar uma resposta 404/500 | Validar `result.status` antes de escrever |
| Confirmar integridade | Comparar SHA-256 esperado com o hash calculado |
| Evitar arquivo final parcial | Gravar em `.part` e renomear após sucesso |
| Repetir o download com segurança | Remover ou substituir apenas o temporário |
| Baixar arquivo grande | Ler em blocos, atualizar o hash e escrever no disco em streaming |
| URL externa ou redirecionamento | Definir política explícita de hosts, HTTPS e redirects |

Se você ainda está começando com requisições, leia antes o tutorial de [`std.http.Client`, GET, POST e headers](/tutoriais/zig-http-client/). Para o caminho inverso, veja como fazer [upload multipart em Zig](/artigos/zig-http-multipart-upload-arquivos/).

## Por que escrever em arquivo temporário?

Imagine que o programa escreva diretamente em `zig.tar.xz`. A conexão cai depois de 60% do download, mas o arquivo continua existindo com o nome definitivo. Outro processo pode encontrá-lo, tentar extrair o pacote e produzir um erro que parece ser de formato, não de rede.

Com um temporário, o estado fica explícito:

```text
zig.tar.xz.part   # download em andamento ou interrompido
zig.tar.xz        # arquivo validado e publicado
```

O nome final só aparece depois de quatro condições:

1. a requisição HTTP terminou;
2. o status pertence ao conjunto aceito;
3. o tamanho está dentro do limite;
4. o checksum corresponde ao valor esperado, quando fornecido.

No mesmo filesystem, `rename` normalmente oferece a publicação atômica que queremos: consumidores observam o arquivo antigo ou o novo, não uma sequência de bytes parcialmente escrita. Isso não transforma todo filesystem em banco transacional, mas elimina a janela mais perigosa do fluxo comum.

## Exemplo completo para arquivo com limite conhecido

O código abaixo baixa a resposta para memória, valida o SHA-256 opcional e só então grava o arquivo. Essa abordagem é simples e previsível quando o limite é pequeno o suficiente para o processo.

```zig
const std = @import("std");

const DownloadError = error{
    StatusHttpInesperado,
    RespostaVazia,
    ArquivoGrandeDemais,
    ChecksumInvalido,
};

fn sha256Hex(bytes: []const u8, output: *[64]u8) []const u8 {
    var digest: [32]u8 = undefined;
    std.crypto.hash.sha2.Sha256.hash(bytes, &digest, .{});
    return std.fmt.bufPrint(output, "{s}", .{
        std.fmt.fmtSliceHexLower(&digest),
    }) catch unreachable;
}

fn writeAtomically(
    destination: []const u8,
    bytes: []const u8,
) !void {
    var temp_name_buf: [std.fs.max_path_bytes]u8 = undefined;
    const temp_name = try std.fmt.bufPrint(
        &temp_name_buf,
        "{s}.part",
        .{destination},
    );

    const file = try std.fs.cwd().createFile(temp_name, .{
        .truncate = true,
    });
    errdefer std.fs.cwd().deleteFile(temp_name) catch {};
    defer file.close();

    try file.writeAll(bytes);
    try file.sync();

    std.fs.cwd().rename(temp_name, destination) catch |err| {
        // Em alguns ambientes, substituir um destino existente exige
        // uma política própria. Não apague o arquivo antigo por surpresa.
        return err;
    };
}

fn download(
    allocator: std.mem.Allocator,
    url: []const u8,
    destination: []const u8,
    expected_sha256: ?[]const u8,
    max_bytes: usize,
) !void {
    var client = std.http.Client{ .allocator = allocator };
    defer client.deinit();

    var body = std.ArrayList(u8).init(allocator);
    defer body.deinit();

    const result = try client.fetch(.{
        .location = .{ .url = url },
        .extra_headers = &.{
            .{ .name = "Accept", .value = "application/octet-stream" },
            .{ .name = "User-Agent", .value = "zig-downloader/1.0" },
        },
        .response_storage = .{ .dynamic = &body },
        .max_redirects = 3,
    });

    if (result.status != .ok) {
        return DownloadError.StatusHttpInesperado;
    }

    if (body.items.len == 0) {
        return DownloadError.RespostaVazia;
    }

    if (body.items.len > max_bytes) {
        return DownloadError.ArquivoGrandeDemais;
    }

    var hash_buf: [64]u8 = undefined;
    const actual_sha256 = sha256Hex(body.items, &hash_buf);

    if (expected_sha256) |expected| {
        if (!std.ascii.eqlIgnoreCase(expected, actual_sha256)) {
            return DownloadError.ChecksumInvalido;
        }
    }

    std.debug.print("SHA-256: {s}\n", .{actual_sha256});
    try writeAtomically(destination, body.items);
}

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    try download(
        allocator,
        "https://downloads.example.com/ferramenta.tar.xz",
        "ferramenta.tar.xz",
        null, // Em produção, prefira informar o SHA-256 esperado.
        32 * 1024 * 1024,
    );
}
```

A opção exata usada para limitar uma resposta dinâmica varia conforme o release de Zig. A comparação posterior com `max_bytes` detecta excesso, mas não impede que um servidor faça o buffer crescer antes dessa validação. Em código de produção, aplique o teto durante a própria leitura ou use um writer que recuse bytes depois do limite. O modo streaming descrito abaixo é o caminho mais seguro quando a origem não é totalmente controlada.

## Validando status HTTP antes do body

Um servidor pode devolver uma página HTML de erro com corpo perfeitamente válido. Se o programa olhar apenas para os bytes, pode salvar isto como `ferramenta.tar.xz`:

```html
<h1>404 Not Found</h1>
```

Por isso o status vem primeiro. Para download comum, `200 OK` é o resultado esperado. Respostas `206 Partial Content` só devem ser aceitas quando seu código implementa retomada com `Range` e valida os metadados da parte recebida.

Redirecionamentos também merecem política. Seguir até três redirects pode ser conveniente para uma CDN, mas não permita que uma URL confiável leve silenciosamente a qualquer host. Em ferramentas internas, considere validar o hostname final, exigir HTTPS e bloquear destinos privados quando a URL vier de usuário. Essa última proteção reduz risco de SSRF.

## SHA-256: integridade não é autenticidade por si só

O SHA-256 responde: “estes bytes são exatamente os bytes esperados?”. Ele não responde sozinho: “quem publicou esse hash é confiável?”. Se o arquivo e o checksum forem baixados do mesmo local comprometido, um invasor pode substituir ambos.

Use o checksum esperado a partir de uma fonte controlada, por exemplo:

- valor fixado no repositório e revisado em pull request;
- manifesto de release assinado;
- metadado entregue por um canal autenticado diferente;
- configuração gerenciada pelo seu sistema de deploy.

A comparação deve acontecer antes do rename. Se o hash falhar, remova o `.part`, registre o hash observado sem expor dados sensíveis e mantenha o destino anterior intacto.

Para artefatos críticos, combine checksum com assinatura digital e política de origem. O guia de [supply chain e releases em Zig](/artigos/zig-supply-chain-dependencias-releases/) explica por que versão, URL e hash devem fazer parte do contrato de build.

## Como fazer streaming para arquivos grandes

Carregar 2 GB em um `ArrayList` é uma má ideia mesmo quando a máquina possui memória suficiente. O download concorre com o restante do processo, pode fragmentar memória e duplica dados durante algumas operações.

No modo streaming, mantenha um buffer pequeno e repita:

```text
ler bloco HTTP
    -> verificar teto acumulado
    -> atualizar SHA-256
    -> escrever bloco no arquivo .part
    -> continuar até EOF
sincronizar arquivo
comparar checksum
rename para o destino final
```

O estado de hash incremental segue esta ideia:

```zig
var hasher = std.crypto.hash.sha2.Sha256.init(.{});

while (try reader.read(&buffer)) |n| {
    if (n == 0) break;
    total += n;
    if (total > max_bytes) return error.ArquivoGrandeDemais;

    hasher.update(buffer[0..n]);
    try file.writeAll(buffer[0..n]);
}

var digest: [32]u8 = undefined;
hasher.final(&digest);
```

A obtenção do `reader` da resposta depende da API de baixo nível de `std.http.Client` disponível no release usado. Isole essa parte em uma função pequena. Assim, uma mudança da stdlib não contamina validação, hash, escrita temporária e publicação do arquivo.

## Retomada com `Range`: só implemente com validação

Retomar downloads parece simples: descubra o tamanho do `.part` e envie `Range: bytes=N-`. Mas existem armadilhas:

- o arquivo remoto pode ter mudado;
- o servidor pode ignorar `Range` e devolver `200` com o arquivo inteiro;
- o `Content-Range` pode não começar em `N`;
- o temporário local pode pertencer a outra URL;
- o checksum final ainda precisa ser recalculado ou continuado corretamente.

Uma implementação segura armazena metadados como URL, tamanho esperado, ETag e `Last-Modified`. Ao retomar, envia `If-Range`, exige `206 Partial Content` e valida `Content-Range`. Se qualquer condição divergir, descarte o temporário e recomece. Para arquivos pequenos, recomeçar costuma ser mais barato e menos arriscado que manter toda essa lógica.

## Nome de arquivo e diretório de destino

Não derive o destino diretamente do último segmento de uma URL controlada por usuário. Normalize o nome e imponha um diretório previamente aberto. Bloqueie:

- `../` e caminhos absolutos;
- nomes vazios ou especiais;
- caracteres de controle;
- colisões com arquivos de configuração;
- symlinks inesperados no diretório de destino.

Quando possível, abra o diretório permitido e opere com caminhos relativos a ele. A mesma preocupação aparece em [projetos práticos de Zig](/projetos/): path traversal não é apenas um problema de servidor; um downloader também pode sobrescrever locais indevidos se aceitar nomes sem validação.

## Erros que devem aparecer separadamente

Não transforme tudo em `DownloadFailed`. Diferencie pelo menos:

| Erro | Significado operacional |
|---|---|
| DNS, TLS ou conexão | falha de transporte |
| `404` | URL ou versão inexistente |
| `401` ou `403` | credencial ou autorização |
| `429` | limite do provedor |
| `500`, `502`, `503`, `504` | falha possivelmente transitória |
| limite excedido | resposta maior que a política local |
| checksum inválido | bytes corrompidos ou origem inesperada |
| erro de escrita | disco cheio, permissão ou filesystem |
| rename falhou | colisão ou política de substituição |

Retry só faz sentido para parte desses erros. Não repita automaticamente um checksum inválido vindo da mesma origem dezenas de vezes. Para uma política de tentativas, backoff e falhas transitórias, veja [circuit breaker, timeout e retry em Zig](/artigos/zig-circuit-breaker-timeout-retry/).

## Testes que valem a pena

Use um servidor HTTP local ou fake e cubra:

1. `200` com arquivo e hash corretos;
2. `404` com corpo HTML;
3. corpo vazio;
4. resposta um byte acima do limite;
5. checksum incorreto;
6. conexão interrompida no meio;
7. destino existente;
8. falha de rename;
9. redirect acima do limite;
10. `206` recebido quando retomada não está habilitada.

Depois de cada falha, verifique o filesystem: o arquivo final antigo deve continuar íntegro e o temporário não deve ser confundido com resultado válido. Testar apenas o erro retornado deixa metade do contrato sem cobertura.

## Checklist de produção

- [ ] A URL usa HTTPS e possui política de redirects.
- [ ] O status HTTP é validado antes de publicar o arquivo.
- [ ] Existe limite máximo de bytes aplicado durante a leitura.
- [ ] Downloads grandes usam streaming.
- [ ] O SHA-256 esperado vem de fonte confiável.
- [ ] O arquivo é escrito primeiro em um temporário.
- [ ] O temporário é sincronizado antes do rename quando durabilidade importa.
- [ ] O destino não é derivado de caminho não confiável.
- [ ] Logs não incluem tokens, cookies ou query strings sensíveis.
- [ ] Retry possui limite e classificação de erro.
- [ ] Métricas registram bytes, duração, status e motivo da falha.
- [ ] Testes confirmam que falhas não deixam um arquivo final parcial.

## Conclusão

Um downloader confiável em Zig é uma pequena pipeline de validação, não apenas uma chamada HTTP. Receba os bytes com limite, valide o status, calcule o SHA-256, escreva em `.part` e publique com rename somente no fim. Para arquivos grandes, troque o buffer integral por streaming, mas preserve exatamente as mesmas etapas.

Esse desenho combina com Zig porque torna explícitos os pontos que costumam ficar escondidos: memória máxima, origem, integridade, durabilidade e estado parcial. Continue pela referência de [`std.http.Client`]({{< relref "stdlib/std-http-client.md" >}}), pelo tutorial de File I/O em Zig e pelo guia de [TLS, HTTPS e certificados](/artigos/zig-tls-https-certificados-mtls/) para adaptar o fluxo ao seu ambiente de produção.
