Módulo std::fs em Rust

Referência completa do módulo std::fs em Rust: read_to_string, write, copy, rename, remove, create_dir_all, read_dir, metadata e symlink.

Módulo std::fs em Rust

O módulo std::fs fornece funções de alto nível para operações no sistema de arquivos: ler, escrever, copiar, renomear, remover arquivos e diretórios, consultar metadados e criar links simbólicos. Diferente do tipo File (que requer abrir e fechar handles manualmente), as funções de std::fs são operações completas em uma única chamada — ideais para tarefas simples e scripts.

Visão geral e tipos-chave

Funções de arquivo

  • fs::read_to_string(path) — lê arquivo inteiro como String (UTF-8)
  • fs::read(path) — lê arquivo inteiro como Vec<u8> (bytes)
  • fs::write(path, conteudo) — escreve conteúdo em arquivo (cria ou sobrescreve)
  • fs::copy(src, dst) — copia arquivo preservando permissões
  • fs::rename(src, dst) — move/renomeia arquivo ou diretório
  • fs::remove_file(path) — remove um arquivo

Funções de diretório

  • fs::create_dir(path) — cria um diretório (pai deve existir)
  • fs::create_dir_all(path) — cria diretório e todos os pais necessários
  • fs::remove_dir(path) — remove diretório vazio
  • fs::remove_dir_all(path) — remove diretório e todo seu conteúdo
  • fs::read_dir(path) — retorna iterador sobre entradas do diretório

Funções de metadados

  • fs::metadata(path) — metadados seguindo symlinks
  • fs::symlink_metadata(path) — metadados sem seguir symlinks
  • fs::set_permissions(path, perm) — altera permissões

Padrões comuns com código

Leitura e escrita rápida

use std::fs;
use std::io;

fn main() -> io::Result<()> {
    // Escrever texto em arquivo (cria ou sobrescreve)
    fs::write("notas.txt", "Aprender Rust\nEstudar std::fs\n")?;

    // Ler arquivo inteiro como String
    let conteudo = fs::read_to_string("notas.txt")?;
    println!("Notas ({} bytes):\n{}", conteudo.len(), conteudo);

    // Ler como bytes (para binários)
    let bytes = fs::read("notas.txt")?;
    println!("Primeiros bytes: {:?}", &bytes[..10.min(bytes.len())]);

    Ok(())
}

Gerenciar estrutura de diretórios

use std::fs;
use std::io;

fn criar_estrutura_projeto(nome: &str) -> io::Result<()> {
    // create_dir_all cria toda a hierarquia de uma vez
    fs::create_dir_all(format!("{}/src/models", nome))?;
    fs::create_dir_all(format!("{}/src/controllers", nome))?;
    fs::create_dir_all(format!("{}/src/views", nome))?;
    fs::create_dir_all(format!("{}/tests", nome))?;
    fs::create_dir_all(format!("{}/docs", nome))?;

    // Criar arquivos iniciais
    fs::write(
        format!("{}/src/main.rs", nome),
        "fn main() {\n    println!(\"Hello, world!\");\n}\n",
    )?;
    fs::write(
        format!("{}/Cargo.toml", nome),
        format!(
            "[package]\nname = \"{}\"\nversion = \"0.1.0\"\nedition = \"2024\"\n",
            nome
        ),
    )?;

    println!("Projeto '{}' criado com sucesso", nome);
    Ok(())
}

fn main() -> io::Result<()> {
    criar_estrutura_projeto("meu_app")?;
    Ok(())
}

Listar conteúdo de diretório

use std::fs;
use std::io;

fn listar_diretorio(caminho: &str) -> io::Result<()> {
    println!("Conteúdo de '{}':", caminho);

    let mut entradas: Vec<_> = fs::read_dir(caminho)?
        .filter_map(|e| e.ok())
        .collect();

    // Ordenar por nome
    entradas.sort_by_key(|e| e.file_name());

    for entrada in &entradas {
        let meta = entrada.metadata()?;
        let tipo = if meta.is_dir() {
            "DIR "
        } else if meta.is_symlink() {
            "LINK"
        } else {
            "FILE"
        };
        let tamanho = if meta.is_file() {
            format!("{:>10} bytes", meta.len())
        } else {
            String::from("           -")
        };

        println!(
            "  [{}] {} {}",
            tipo,
            tamanho,
            entrada.file_name().to_string_lossy()
        );
    }

    println!("Total: {} entradas", entradas.len());
    Ok(())
}

fn main() -> io::Result<()> {
    listar_diretorio(".")?;
    Ok(())
}

Tabela de funções principais

FunçãoRetornoDescrição
fs::read_to_string(path)io::Result<String>Lê arquivo como texto UTF-8
fs::read(path)io::Result<Vec<u8>>Lê arquivo como bytes
fs::write(path, conteudo)io::Result<()>Escreve bytes/string em arquivo
fs::copy(src, dst)io::Result<u64>Copia arquivo, retorna bytes copiados
fs::rename(src, dst)io::Result<()>Move/renomeia arquivo ou diretório
fs::remove_file(path)io::Result<()>Remove arquivo
fs::create_dir(path)io::Result<()>Cria diretório (pai deve existir)
fs::create_dir_all(path)io::Result<()>Cria diretório com todos os pais
fs::remove_dir(path)io::Result<()>Remove diretório vazio
fs::remove_dir_all(path)io::Result<()>Remove diretório recursivamente
fs::read_dir(path)io::Result<ReadDir>Iterador sobre entradas
fs::metadata(path)io::Result<Metadata>Metadados (segue symlinks)
fs::symlink_metadata(path)io::Result<Metadata>Metadados (não segue symlinks)
fs::set_permissions(path, perm)io::Result<()>Altera permissões
fs::canonicalize(path)io::Result<PathBuf>Caminho absoluto canônico
fs::hard_link(src, dst)io::Result<()>Cria hard link
fs::soft_link(src, dst)io::Result<()>Cria link simbólico (Unix)

Exemplos práticos

Exemplo 1: Copiar e mover arquivos com verificação

use std::fs;
use std::io;
use std::path::Path;

fn copiar_seguro(origem: &str, destino: &str, sobrescrever: bool) -> io::Result<u64> {
    let path_destino = Path::new(destino);

    if path_destino.exists() && !sobrescrever {
        return Err(io::Error::new(
            io::ErrorKind::AlreadyExists,
            format!("Destino já existe: '{}'", destino),
        ));
    }

    // Garantir que o diretório pai existe
    if let Some(pai) = path_destino.parent() {
        fs::create_dir_all(pai)?;
    }

    let bytes = fs::copy(origem, destino)?;
    println!("Copiado '{}' -> '{}' ({} bytes)", origem, destino, bytes);
    Ok(bytes)
}

fn mover_arquivo(origem: &str, destino: &str) -> io::Result<()> {
    // rename é atômico no mesmo filesystem
    match fs::rename(origem, destino) {
        Ok(()) => {
            println!("Movido '{}' -> '{}'", origem, destino);
            Ok(())
        }
        Err(e) if e.raw_os_error() == Some(18) => {
            // EXDEV: cross-device — copiar e remover
            fs::copy(origem, destino)?;
            fs::remove_file(origem)?;
            println!("Movido (cross-device) '{}' -> '{}'", origem, destino);
            Ok(())
        }
        Err(e) => Err(e),
    }
}

fn main() -> io::Result<()> {
    fs::write("original.txt", "Dados importantes")?;
    copiar_seguro("original.txt", "backup/original.txt", false)?;
    mover_arquivo("original.txt", "arquivo/destino.txt")?;
    Ok(())
}

Exemplo 2: Busca recursiva de arquivos

use std::fs;
use std::io;
use std::path::{Path, PathBuf};

fn buscar_arquivos(
    diretorio: &Path,
    extensao: &str,
    resultados: &mut Vec<PathBuf>,
) -> io::Result<()> {
    if !diretorio.is_dir() {
        return Ok(());
    }

    for entrada in fs::read_dir(diretorio)? {
        let entrada = entrada?;
        let caminho = entrada.path();

        if caminho.is_dir() {
            buscar_arquivos(&caminho, extensao, resultados)?;
        } else if caminho.extension().and_then(|e| e.to_str()) == Some(extensao) {
            resultados.push(caminho);
        }
    }

    Ok(())
}

fn main() -> io::Result<()> {
    let mut arquivos_rs = Vec::new();
    buscar_arquivos(Path::new("."), "rs", &mut arquivos_rs)?;

    println!("Arquivos .rs encontrados:");
    for arquivo in &arquivos_rs {
        let meta = fs::metadata(arquivo)?;
        println!("  {} ({} bytes)", arquivo.display(), meta.len());
    }
    println!("Total: {} arquivos", arquivos_rs.len());

    Ok(())
}

Exemplo 3: Limpeza de arquivos temporários

use std::fs;
use std::io;
use std::path::Path;
use std::time::{Duration, SystemTime};

fn limpar_antigos(diretorio: &str, max_idade: Duration) -> io::Result<(usize, u64)> {
    let agora = SystemTime::now();
    let mut removidos = 0usize;
    let mut bytes_liberados = 0u64;

    for entrada in fs::read_dir(diretorio)? {
        let entrada = entrada?;
        let meta = entrada.metadata()?;

        if !meta.is_file() {
            continue;
        }

        let modificado = meta.modified()?;
        let idade = agora
            .duration_since(modificado)
            .unwrap_or(Duration::ZERO);

        if idade > max_idade {
            let tamanho = meta.len();
            let nome = entrada.file_name();
            match fs::remove_file(entrada.path()) {
                Ok(()) => {
                    println!("  Removido: {} ({} bytes)", nome.to_string_lossy(), tamanho);
                    removidos += 1;
                    bytes_liberados += tamanho;
                }
                Err(e) => {
                    eprintln!("  Erro ao remover {}: {}", nome.to_string_lossy(), e);
                }
            }
        }
    }

    Ok((removidos, bytes_liberados))
}

fn main() -> io::Result<()> {
    let max_dias = Duration::from_secs(30 * 24 * 60 * 60); // 30 dias
    let (removidos, bytes) = limpar_antigos("/tmp/meu_app", max_dias)?;
    println!(
        "Limpeza: {} arquivos removidos, {:.2} MB liberados",
        removidos,
        bytes as f64 / 1_048_576.0
    );
    Ok(())
}

Exemplo 4: Backup incremental de diretório

use std::fs;
use std::io;
use std::path::Path;

fn backup_diretorio(origem: &Path, destino: &Path) -> io::Result<(usize, usize)> {
    fs::create_dir_all(destino)?;

    let mut copiados = 0usize;
    let mut ignorados = 0usize;

    for entrada in fs::read_dir(origem)? {
        let entrada = entrada?;
        let tipo = entrada.file_type()?;
        let nome = entrada.file_name();
        let dest_path = destino.join(&nome);

        if tipo.is_dir() {
            let (c, i) = backup_diretorio(&entrada.path(), &dest_path)?;
            copiados += c;
            ignorados += i;
        } else if tipo.is_file() {
            let precisa_copiar = if dest_path.exists() {
                let meta_orig = entrada.metadata()?;
                let meta_dest = fs::metadata(&dest_path)?;
                // Copiar se o original for mais recente ou tamanho diferente
                meta_orig.len() != meta_dest.len()
                    || meta_orig.modified()? > meta_dest.modified()?
            } else {
                true
            };

            if precisa_copiar {
                fs::copy(entrada.path(), &dest_path)?;
                copiados += 1;
            } else {
                ignorados += 1;
            }
        }
    }

    Ok((copiados, ignorados))
}

fn main() -> io::Result<()> {
    let (copiados, ignorados) = backup_diretorio(
        Path::new("projeto"),
        Path::new("backup/projeto"),
    )?;
    println!("Backup: {} copiados, {} sem alteração", copiados, ignorados);
    Ok(())
}

Exemplo 5: Calcular tamanho total de diretório

use std::fs;
use std::io;
use std::path::Path;

fn tamanho_diretorio(caminho: &Path) -> io::Result<u64> {
    let mut total = 0u64;

    if caminho.is_file() {
        return Ok(fs::metadata(caminho)?.len());
    }

    for entrada in fs::read_dir(caminho)? {
        let entrada = entrada?;
        let meta = entrada.metadata()?;

        if meta.is_file() {
            total += meta.len();
        } else if meta.is_dir() {
            total += tamanho_diretorio(&entrada.path())?;
        }
    }

    Ok(total)
}

fn formatar_tamanho(bytes: u64) -> String {
    const KB: u64 = 1024;
    const MB: u64 = KB * 1024;
    const GB: u64 = MB * 1024;

    if bytes >= GB {
        format!("{:.2} GB", bytes as f64 / GB as f64)
    } else if bytes >= MB {
        format!("{:.2} MB", bytes as f64 / MB as f64)
    } else if bytes >= KB {
        format!("{:.2} KB", bytes as f64 / KB as f64)
    } else {
        format!("{} bytes", bytes)
    }
}

fn main() -> io::Result<()> {
    let caminhos = vec!["src", "target", "docs"];
    for caminho in caminhos {
        let path = Path::new(caminho);
        if path.exists() {
            let tamanho = tamanho_diretorio(path)?;
            println!("{}: {}", caminho, formatar_tamanho(tamanho));
        } else {
            println!("{}: (não encontrado)", caminho);
        }
    }
    Ok(())
}

Padrões de tratamento de erro para I/O

remove_dir_all com tratamento cuidadoso

use std::fs;
use std::io;

fn remover_seguro(caminho: &str) -> io::Result<()> {
    match fs::remove_dir_all(caminho) {
        Ok(()) => {
            println!("Removido: '{}'", caminho);
            Ok(())
        }
        Err(e) if e.kind() == io::ErrorKind::NotFound => {
            println!("'{}' já não existe — nada a fazer", caminho);
            Ok(()) // não é erro se já não existia
        }
        Err(e) if e.kind() == io::ErrorKind::PermissionDenied => {
            eprintln!("Sem permissão para remover: '{}'", caminho);
            Err(e)
        }
        Err(e) => Err(e),
    }
}

Verificação antes de operações destrutivas

use std::fs;
use std::io;
use std::path::Path;

fn substituir_arquivo(caminho: &str, novo_conteudo: &str) -> io::Result<()> {
    let path = Path::new(caminho);

    // Criar backup se existir
    if path.exists() {
        let backup = format!("{}.bak", caminho);
        fs::copy(caminho, &backup)?;
        println!("Backup criado: {}", backup);
    }

    fs::write(caminho, novo_conteudo)?;
    println!("Arquivo atualizado: {}", caminho);
    Ok(())
}

Dicas de desempenho

  • fs::read_to_string e fs::write são adequados para arquivos pequenos (< 10 MB). Para arquivos maiores, use File com BufReader/BufWriter.
  • read_dir é lazy — as entradas são lidas sob demanda. Colete em Vec apenas se precisar ordenar ou acessar múltiplas vezes.
  • fs::copy é otimizada pelo sistema operacional em muitas plataformas (usa sendfile no Linux, por exemplo).
  • rename é atômico no mesmo filesystem — use para atualizações seguras de arquivos.

Veja também