Operacoes de File System com Zig: Arquivos, Diretorios e Metadados

Apos entender como syscalls funcionam no artigo anterior, e hora de mergulhar nas operacoes de file system. O Linux segue a filosofia “tudo e um arquivo”, e dominar operacoes de I/O em arquivos e a base para qualquer programacao de sistemas seria. Neste artigo, exploramos como Zig torna essas operacoes seguras, eficientes e elegantes.

Leitura de Arquivos: Multiplas Abordagens

Zig oferece varias formas de ler arquivos, cada uma adequada para diferentes cenarios.

Leitura Completa em Memoria

Para arquivos pequenos a medios, a forma mais simples e carregar todo o conteudo na memoria:

const std = @import("std");

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

    // readToEndAlloc le todo o arquivo de uma vez
    // O segundo parametro e o limite maximo de bytes
    const conteudo = try std.fs.cwd().readFileAlloc(
        allocator,
        "config.txt",
        1024 * 1024, // max 1MB
    );
    defer allocator.free(conteudo);

    std.debug.print("Conteudo ({d} bytes):\n{s}\n", .{
        conteudo.len,
        conteudo,
    });
}

Leitura Linha por Linha

Para arquivos grandes ou quando voce precisa processar linha por linha:

const std = @import("std");

pub fn main() !void {
    const arquivo = try std.fs.cwd().openFile("dados.csv", .{});
    defer arquivo.close();

    var buf_reader = std.io.bufferedReader(arquivo.reader());
    var reader = buf_reader.reader();

    var linha_buf: [4096]u8 = undefined;
    var numero_linha: usize = 0;

    while (reader.readUntilDelimiterOrEof(&linha_buf, '\n')) |linha_opt| {
        if (linha_opt) |linha| {
            numero_linha += 1;
            std.debug.print("Linha {d}: {s}\n", .{ numero_linha, linha });
        } else {
            break; // EOF
        }
    } else |err| {
        std.debug.print("Erro na leitura: {}\n", .{err});
    }

    std.debug.print("\nTotal: {d} linhas\n", .{numero_linha});
}

Leitura com Buffer Fixo

Para controle maximo sobre a leitura, use buffers fixos:

const std = @import("std");

pub fn main() !void {
    const arquivo = try std.fs.cwd().openFile("grande.bin", .{});
    defer arquivo.close();

    var buffer: [4096]u8 = undefined;
    var total_bytes: usize = 0;

    while (true) {
        const bytes_lidos = try arquivo.read(&buffer);
        if (bytes_lidos == 0) break; // EOF

        total_bytes += bytes_lidos;
        // Processar buffer[0..bytes_lidos]
    }

    std.debug.print("Total lido: {d} bytes\n", .{total_bytes});
}

Escrita de Arquivos

Escrita Simples

const std = @import("std");

pub fn main() !void {
    // createFile cria ou trunca o arquivo
    const arquivo = try std.fs.cwd().createFile("relatorio.txt", .{});
    defer arquivo.close();

    const writer = arquivo.writer();

    try writer.writeAll("=== Relatorio de Sistema ===\n\n");
    try writer.print("Data: {s}\n", .{"2026-02-21"});
    try writer.print("Processos ativos: {d}\n", .{142});
    try writer.print("Memoria livre: {d} MB\n", .{3847});

    // Escrever bytes diretamente
    const dados_binarios = [_]u8{ 0xDE, 0xAD, 0xBE, 0xEF };
    try arquivo.writeAll(&dados_binarios);
}

Escrita com Buffer (Buffered Writer)

Para escrita frequente de pequenas quantidades de dados, use um buffered writer para reduzir o numero de syscalls:

const std = @import("std");

pub fn main() !void {
    const arquivo = try std.fs.cwd().createFile("log.txt", .{});
    defer arquivo.close();

    var buf_writer = std.io.bufferedWriter(arquivo.writer());
    var writer = buf_writer.writer();

    var i: usize = 0;
    while (i < 10000) : (i += 1) {
        try writer.print("[{d}] Entrada de log numero {d}\n", .{ i, i * 7 });
    }

    // Importante: flush para garantir que tudo foi escrito
    try buf_writer.flush();
}

Append em Arquivo Existente

const std = @import("std");

pub fn main() !void {
    // Abrir para escrita no final
    const arquivo = try std.fs.cwd().openFile("log.txt", .{
        .mode = .write_only,
    });
    defer arquivo.close();

    // Posicionar no final
    try arquivo.seekFromEnd(0);

    const writer = arquivo.writer();
    try writer.print("Nova entrada adicionada\n", .{});
}

Manipulacao de Diretorios

Listando Conteudo de um Diretorio

const std = @import("std");

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

    var dir = try std.fs.cwd().openDir(".", .{ .iterate = true });
    defer dir.close();

    var iterator = dir.iterate();

    std.debug.print("{s:<30} {s:<10} {s}\n", .{ "Nome", "Tipo", "Tamanho" });
    std.debug.print("{s}\n", .{"-" ** 50});

    while (try iterator.next()) |entrada| {
        const tipo_str = switch (entrada.kind) {
            .file => "arquivo",
            .directory => "diretorio",
            .sym_link => "symlink",
            else => "outro",
        };

        if (entrada.kind == .file) {
            const stat = dir.statFile(entrada.name) catch continue;
            std.debug.print("{s:<30} {s:<10} {d} bytes\n", .{
                entrada.name,
                tipo_str,
                stat.size,
            });
        } else {
            std.debug.print("{s:<30} {s:<10}\n", .{ entrada.name, tipo_str });
        }
    }
}

Travessia Recursiva de Diretorios

const std = @import("std");

fn percorrerRecursivo(
    allocator: std.mem.Allocator,
    dir_path: []const u8,
    profundidade: usize,
) !void {
    var dir = try std.fs.cwd().openDir(dir_path, .{ .iterate = true });
    defer dir.close();

    var iterator = dir.iterate();

    while (try iterator.next()) |entrada| {
        // Indentacao visual
        var i: usize = 0;
        while (i < profundidade) : (i += 1) {
            std.debug.print("  ", .{});
        }

        std.debug.print("{s}\n", .{entrada.name});

        if (entrada.kind == .directory) {
            const sub_path = try std.fs.path.join(allocator, &.{
                dir_path,
                entrada.name,
            });
            defer allocator.free(sub_path);

            percorrerRecursivo(allocator, sub_path, profundidade + 1) catch |err| {
                std.debug.print("  [Erro ao acessar: {}]\n", .{err});
            };
        }
    }
}

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

    try percorrerRecursivo(allocator, ".", 0);
}

Criacao e Remocao de Diretorios

const std = @import("std");

pub fn main() !void {
    const cwd = std.fs.cwd();

    // Criar diretorio simples
    try cwd.makeDir("novo_diretorio");

    // Criar diretorios aninhados (como mkdir -p)
    try cwd.makePath("caminho/completo/aninhado");

    // Remover diretorio vazio
    try cwd.deleteDir("novo_diretorio");

    // Remover arvore de diretorios (como rm -rf)
    try cwd.deleteTree("caminho");
}

Permissoes e Metadados

Obtendo Metadados de Arquivo

const std = @import("std");

pub fn main() !void {
    const stat = try std.fs.cwd().statFile("exemplo.txt");

    std.debug.print("Tamanho: {d} bytes\n", .{stat.size});
    std.debug.print("Inode: {d}\n", .{stat.inode});

    // Permissoes
    const mode = stat.mode;
    std.debug.print("Permissoes: {o}\n", .{mode & 0o777});

    // Verificar tipos de permissao
    const owner_read = (mode & 0o400) != 0;
    const owner_write = (mode & 0o200) != 0;
    const owner_exec = (mode & 0o100) != 0;

    std.debug.print("Dono: r={} w={} x={}\n", .{
        owner_read,
        owner_write,
        owner_exec,
    });
}

Manipulando Caminhos

Zig fornece funcoes utilitarias para trabalhar com caminhos de forma portavel:

const std = @import("std");

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

    const caminho = "/home/usuario/projetos/zig/main.zig";

    // Extrair componentes
    std.debug.print("Diretorio: {s}\n", .{std.fs.path.dirname(caminho) orelse "."});
    std.debug.print("Nome base: {s}\n", .{std.fs.path.basename(caminho)});
    std.debug.print("Extensao: {s}\n", .{std.fs.path.extension(caminho)});

    // Juntar caminhos
    const junto = try std.fs.path.join(allocator, &.{ "/home", "usuario", "docs" });
    defer allocator.free(junto);
    std.debug.print("Junto: {s}\n", .{junto});

    // Caminho absoluto
    const absoluto = try std.fs.cwd().realpathAlloc(allocator, ".");
    defer allocator.free(absoluto);
    std.debug.print("CWD absoluto: {s}\n", .{absoluto});
}

File Locking

File locking e essencial quando multiplos processos precisam acessar o mesmo arquivo de forma coordenada.

const std = @import("std");

pub fn main() !void {
    const arquivo = try std.fs.cwd().openFile("recurso_compartilhado.dat", .{
        .mode = .read_write,
    });
    defer arquivo.close();

    // Adquirir lock exclusivo (bloqueante)
    try arquivo.lock(.exclusive);
    defer arquivo.unlock();

    std.debug.print("Lock adquirido! Processando...\n", .{});

    // Operacoes seguras no arquivo
    try arquivo.seekTo(0);
    const writer = arquivo.writer();
    try writer.print("Escrita segura pelo processo {d}\n", .{
        std.os.linux.getpid(),
    });

    // Simular processamento
    std.time.sleep(2 * std.time.ns_per_s);

    std.debug.print("Processamento concluido. Lock sera liberado.\n", .{});
    // O defer cuida de desbloquear
}

Memory-Mapped Files (mmap)

Memory-mapped files permitem mapear um arquivo diretamente na memoria virtual do processo, oferecendo acesso eficiente especialmente para arquivos grandes.

const std = @import("std");
const posix = std.posix;

pub fn contarOcorrencias(dados: []const u8, byte_alvo: u8) usize {
    var contagem: usize = 0;
    for (dados) |b| {
        if (b == byte_alvo) contagem += 1;
    }
    return contagem;
}

pub fn main() !void {
    const arquivo = try std.fs.cwd().openFile("texto_grande.txt", .{});
    defer arquivo.close();

    const stat = try arquivo.stat();

    if (stat.size == 0) {
        std.debug.print("Arquivo vazio\n", .{});
        return;
    }

    // Mapear arquivo na memoria
    const mapeado = try posix.mmap(
        null,
        stat.size,
        posix.PROT.READ,
        .{ .TYPE = .PRIVATE },
        arquivo.handle,
        0,
    );
    defer posix.munmap(mapeado);

    const dados: []const u8 = @as([*]const u8, @ptrCast(mapeado))[0..stat.size];

    // Agora podemos acessar como um slice normal
    const linhas = contarOcorrencias(dados, '\n');
    std.debug.print("Arquivo: {d} bytes, {d} linhas\n", .{ stat.size, linhas });
}

Quando Usar mmap vs read

Cenariommapread
Arquivos grandes (>100MB)ExcelenteConsome muita RAM
Acesso aleatorioOtimoPrecisa de seek
Acesso sequencial unicoIgualLigeiramente melhor
Multiplos processos lendoMemoria compartilhadaCada um tem copia
Arquivos pequenos (<4KB)Overhead desnecessarioIdeal

Operacoes Atomicas com Arquivos

Para garantir que uma escrita em arquivo seja atomica (tudo ou nada), use o padrao write-rename:

const std = @import("std");

pub fn escritaAtomica(
    allocator: std.mem.Allocator,
    caminho: []const u8,
    conteudo: []const u8,
) !void {
    // 1. Escrever em arquivo temporario
    const tmp_path = try std.fmt.allocPrint(allocator, "{s}.tmp", .{caminho});
    defer allocator.free(tmp_path);

    const tmp_file = try std.fs.cwd().createFile(tmp_path, .{});
    try tmp_file.writeAll(conteudo);
    try tmp_file.sync(); // fsync para garantir persistencia
    tmp_file.close();

    // 2. Renomear atomicamente (rename e atomico no Linux)
    try std.fs.cwd().rename(tmp_path, caminho);
}

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

    try escritaAtomica(allocator, "config.json", "{\"versao\": 2}\n");
    std.debug.print("Escrita atomica concluida\n", .{});
}

Exercicios

  1. Monitor de diretorio: Crie um programa que monitore um diretorio e exiba uma mensagem quando novos arquivos forem criados (polling a cada segundo).

  2. Busca de arquivos: Implemente uma funcao que busque recursivamente arquivos por extensao (similar a find . -name "*.zig").

  3. Concatenador: Crie um programa que concatene multiplos arquivos em um unico arquivo de saida, adicionando cabecalhos entre eles.


Proximo Artigo

No proximo artigo, exploraremos processos e signals, aprendendo a criar processos filhos, comunicacao entre processos e tratamento de sinais.

Conteudo Relacionado


Duvidas sobre operacoes de file system? Deixe um comentario ou participe da comunidade Zig Brasil.

Continue aprendendo Zig

Explore mais tutoriais e artigos em português para dominar a linguagem Zig.