Backups sao a ultima linha de defesa contra perda de dados. Um backup incremental inteligente copia apenas os arquivos que realmente mudaram, economizando tempo e espaco. Neste projeto, vamos construir uma ferramenta de backup incremental que usa hashes SHA-256 para detectar alteracoes, comprime os arquivos copiados com flate2 e mantem um manifesto JSON com o historico de cada operacao.
Voce aprendera a trabalhar com hashing criptografico, compressao de dados, manipulacao avancada do sistema de arquivos e serializacao estruturada.
O Que Vamos Construir
- Deteccao de mudancas baseada em hash SHA-256
- Compressao de arquivos com gzip via
flate2 - Manifesto JSON com metadados de cada backup
- Copia incremental (apenas arquivos novos ou modificados)
- Restauracao de backup a partir do manifesto
- Relatorio de operacoes com estatisticas
Estrutura do Projeto
backup-tool/
├── Cargo.toml
└── src/
├── main.rs
├── hasher.rs
├── compressor.rs
└── manifesto.rs
Configurando o Projeto
cargo new backup-tool
cd backup-tool
Edite o Cargo.toml:
[package]
name = "backup-tool"
version = "0.1.0"
edition = "2021"
[dependencies]
sha2 = "0.10"
flate2 = "1.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
chrono = "0.4"
clap = { version = "4.5", features = ["derive"] }
colored = "2.1"
walkdir = "2.5"
Passo 1: Calculo de Hash dos Arquivos
O modulo hasher calcula o SHA-256 de cada arquivo para detectar se houve mudancas. Crie src/hasher.rs:
use sha2::{Digest, Sha256};
use std::fs::File;
use std::io::{BufReader, Read};
use std::path::Path;
/// Calcula o hash SHA-256 de um arquivo e retorna como string hexadecimal.
pub fn calcular_hash(caminho: &Path) -> Result<String, std::io::Error> {
let arquivo = File::open(caminho)?;
let mut leitor = BufReader::new(arquivo);
let mut hasher = Sha256::new();
let mut buffer = [0u8; 8192];
loop {
let bytes_lidos = leitor.read(&mut buffer)?;
if bytes_lidos == 0 {
break;
}
hasher.update(&buffer[..bytes_lidos]);
}
let resultado = hasher.finalize();
Ok(format!("{:x}", resultado))
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
#[test]
fn teste_hash_consistente() {
let dir = std::env::temp_dir().join("teste_hash_backup");
std::fs::create_dir_all(&dir).unwrap();
let caminho = dir.join("teste.txt");
let mut arquivo = File::create(&caminho).unwrap();
arquivo.write_all(b"conteudo de teste").unwrap();
let hash1 = calcular_hash(&caminho).unwrap();
let hash2 = calcular_hash(&caminho).unwrap();
assert_eq!(hash1, hash2);
assert_eq!(hash1.len(), 64); // SHA-256 = 64 caracteres hex
std::fs::remove_dir_all(&dir).unwrap();
}
}
O hasher le o arquivo em blocos de 8KB para manter o uso de memoria baixo, mesmo com arquivos grandes. O hash SHA-256 garante que qualquer alteracao minima no conteudo seja detectada.
Passo 2: Compressao de Arquivos
O compressor reduz o tamanho dos arquivos no backup. Crie src/compressor.rs:
use flate2::read::GzDecoder;
use flate2::write::GzEncoder;
use flate2::Compression;
use std::fs::File;
use std::io::{self, BufReader, BufWriter, Read, Write};
use std::path::Path;
/// Comprime um arquivo usando gzip e salva com extensao .gz.
pub fn comprimir(origem: &Path, destino: &Path) -> io::Result<u64> {
let arquivo_origem = File::open(origem)?;
let leitor = BufReader::new(arquivo_origem);
let arquivo_destino = File::create(destino)?;
let escritor = BufWriter::new(arquivo_destino);
let mut encoder = GzEncoder::new(escritor, Compression::default());
let mut leitor_buf = BufReader::new(leitor);
let mut buffer = [0u8; 8192];
let mut total_bytes = 0u64;
loop {
let bytes_lidos = leitor_buf.read(&mut buffer)?;
if bytes_lidos == 0 {
break;
}
encoder.write_all(&buffer[..bytes_lidos])?;
total_bytes += bytes_lidos as u64;
}
encoder.finish()?;
Ok(total_bytes)
}
/// Descomprime um arquivo gzip.
pub fn descomprimir(origem: &Path, destino: &Path) -> io::Result<u64> {
let arquivo_origem = File::open(origem)?;
let decoder = GzDecoder::new(BufReader::new(arquivo_origem));
let arquivo_destino = File::create(destino)?;
let mut escritor = BufWriter::new(arquivo_destino);
let mut leitor = BufReader::new(decoder);
let mut buffer = [0u8; 8192];
let mut total_bytes = 0u64;
loop {
let bytes_lidos = leitor.read(&mut buffer)?;
if bytes_lidos == 0 {
break;
}
escritor.write_all(&buffer[..bytes_lidos])?;
total_bytes += bytes_lidos as u64;
}
escritor.flush()?;
Ok(total_bytes)
}
A compressao gzip oferece um bom equilibrio entre taxa de compressao e velocidade. Arquivos de texto, logs e codigos-fonte costumam ter reducoes de 60-80% no tamanho.
Passo 3: Manifesto de Backup
O manifesto registra quais arquivos foram copiados e seus hashes, permitindo backups incrementais futuros. Crie src/manifesto.rs:
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use std::path::Path;
/// Metadados de um arquivo no backup.
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct EntradaArquivo {
pub caminho_relativo: String,
pub hash: String,
pub tamanho_original: u64,
pub tamanho_comprimido: u64,
pub data_backup: DateTime<Utc>,
}
/// Manifesto completo de um backup.
#[derive(Debug, Serialize, Deserialize)]
pub struct Manifesto {
pub versao: String,
pub data_criacao: DateTime<Utc>,
pub diretorio_origem: String,
pub total_arquivos: usize,
pub tamanho_total_original: u64,
pub tamanho_total_comprimido: u64,
pub arquivos: HashMap<String, EntradaArquivo>,
}
impl Manifesto {
/// Cria um novo manifesto vazio.
pub fn new(diretorio_origem: &str) -> Self {
Manifesto {
versao: "1.0".to_string(),
data_criacao: Utc::now(),
diretorio_origem: diretorio_origem.to_string(),
total_arquivos: 0,
tamanho_total_original: 0,
tamanho_total_comprimido: 0,
arquivos: HashMap::new(),
}
}
/// Adiciona uma entrada de arquivo ao manifesto.
pub fn adicionar_entrada(&mut self, entrada: EntradaArquivo) {
self.tamanho_total_original += entrada.tamanho_original;
self.tamanho_total_comprimido += entrada.tamanho_comprimido;
self.total_arquivos += 1;
self.arquivos
.insert(entrada.caminho_relativo.clone(), entrada);
}
/// Salva o manifesto em um arquivo JSON.
pub fn salvar(&self, caminho: &Path) -> Result<(), Box<dyn std::error::Error>> {
let json = serde_json::to_string_pretty(self)?;
fs::write(caminho, json)?;
Ok(())
}
/// Carrega um manifesto de um arquivo JSON.
pub fn carregar(caminho: &Path) -> Result<Self, Box<dyn std::error::Error>> {
let conteudo = fs::read_to_string(caminho)?;
let manifesto: Manifesto = serde_json::from_str(&conteudo)?;
Ok(manifesto)
}
/// Verifica se um arquivo precisa ser atualizado no backup.
pub fn arquivo_mudou(&self, caminho_relativo: &str, hash_atual: &str) -> bool {
match self.arquivos.get(caminho_relativo) {
Some(entrada) => entrada.hash != hash_atual,
None => true, // Arquivo novo
}
}
}
O manifesto e a peca central do backup incremental. Ao comparar os hashes do manifesto anterior com os hashes atuais, o programa sabe exatamente quais arquivos precisam ser copiados novamente.
Passo 4: Juntando Tudo no main.rs
Crie src/main.rs:
mod compressor;
mod hasher;
mod manifesto;
use chrono::Utc;
use clap::{Parser, Subcommand};
use colored::*;
use manifesto::{EntradaArquivo, Manifesto};
use std::path::{Path, PathBuf};
use walkdir::WalkDir;
#[derive(Parser)]
#[command(name = "backup-tool")]
#[command(about = "Ferramenta de backup incremental com compressao")]
struct Argumentos {
#[command(subcommand)]
comando: Comando,
}
#[derive(Subcommand)]
enum Comando {
/// Criar backup de um diretorio
Backup {
/// Diretorio de origem
origem: PathBuf,
/// Diretorio de destino para o backup
destino: PathBuf,
},
/// Restaurar backup para um diretorio
Restaurar {
/// Diretorio do backup (contendo manifesto.json)
backup: PathBuf,
/// Diretorio de destino para restauracao
destino: PathBuf,
},
/// Exibir informacoes de um backup existente
Info {
/// Diretorio do backup (contendo manifesto.json)
backup: PathBuf,
},
}
fn executar_backup(origem: &Path, destino: &Path) -> Result<(), Box<dyn std::error::Error>> {
println!("{}", "=== Backup Incremental ===".green().bold());
println!("Origem: {}", origem.display());
println!("Destino: {}", destino.display());
// Criar diretorio de destino
std::fs::create_dir_all(destino)?;
// Tentar carregar manifesto anterior
let caminho_manifesto = destino.join("manifesto.json");
let manifesto_anterior = Manifesto::carregar(&caminho_manifesto).ok();
let mut novo_manifesto = Manifesto::new(&origem.to_string_lossy());
let mut arquivos_copiados = 0u32;
let mut arquivos_ignorados = 0u32;
let mut bytes_economizados = 0u64;
println!("\nAnalisando arquivos...\n");
// Percorrer todos os arquivos na origem
for entrada in WalkDir::new(origem)
.into_iter()
.filter_map(|e| e.ok())
.filter(|e| e.file_type().is_file())
{
let caminho_absoluto = entrada.path();
let caminho_relativo = caminho_absoluto
.strip_prefix(origem)
.unwrap()
.to_string_lossy()
.to_string();
// Calcular hash do arquivo atual
let hash = hasher::calcular_hash(caminho_absoluto)?;
// Verificar se precisa copiar
let precisa_copiar = match &manifesto_anterior {
Some(m) => m.arquivo_mudou(&caminho_relativo, &hash),
None => true,
};
let tamanho_original = entrada.metadata()?.len();
if precisa_copiar {
// Criar estrutura de diretorios no destino
let destino_arquivo = destino.join(&caminho_relativo);
if let Some(pai) = destino_arquivo.parent() {
std::fs::create_dir_all(pai)?;
}
// Comprimir e copiar
let destino_gz = destino.join(format!("{}.gz", caminho_relativo));
if let Some(pai) = destino_gz.parent() {
std::fs::create_dir_all(pai)?;
}
compressor::comprimir(caminho_absoluto, &destino_gz)?;
let tamanho_comprimido = std::fs::metadata(&destino_gz)?.len();
println!(
" {} {} ({} -> {} bytes)",
"[COPIADO]".green(),
caminho_relativo,
tamanho_original,
tamanho_comprimido
);
novo_manifesto.adicionar_entrada(EntradaArquivo {
caminho_relativo,
hash,
tamanho_original,
tamanho_comprimido,
data_backup: Utc::now(),
});
arquivos_copiados += 1;
} else {
// Manter entrada anterior no manifesto
if let Some(ref m) = manifesto_anterior {
if let Some(entrada_ant) = m.arquivos.get(&caminho_relativo) {
novo_manifesto.adicionar_entrada(entrada_ant.clone());
}
}
bytes_economizados += tamanho_original;
arquivos_ignorados += 1;
println!(
" {} {} (sem alteracao)",
"[IGNORADO]".dimmed(),
caminho_relativo.dimmed()
);
}
}
// Salvar manifesto
novo_manifesto.salvar(&caminho_manifesto)?;
// Relatorio final
println!("\n{}", "=== Relatorio ===".green().bold());
println!("Arquivos copiados: {}", arquivos_copiados.to_string().cyan());
println!("Arquivos inalterados: {}", arquivos_ignorados.to_string().dimmed());
println!(
"Tamanho original: {:.2} MB",
novo_manifesto.tamanho_total_original as f64 / 1_048_576.0
);
println!(
"Tamanho comprimido: {:.2} MB",
novo_manifesto.tamanho_total_comprimido as f64 / 1_048_576.0
);
if novo_manifesto.tamanho_total_original > 0 {
let taxa = (1.0
- novo_manifesto.tamanho_total_comprimido as f64
/ novo_manifesto.tamanho_total_original as f64)
* 100.0;
println!("Taxa de compressao: {:.1}%", taxa);
}
println!(
"Dados economizados: {:.2} MB (arquivos inalterados)",
bytes_economizados as f64 / 1_048_576.0
);
println!("Manifesto salvo em: {}", caminho_manifesto.display());
Ok(())
}
fn executar_restauracao(backup: &Path, destino: &Path) -> Result<(), Box<dyn std::error::Error>> {
println!("{}", "=== Restauracao de Backup ===".green().bold());
let caminho_manifesto = backup.join("manifesto.json");
let manifesto = Manifesto::carregar(&caminho_manifesto)?;
std::fs::create_dir_all(destino)?;
println!("Restaurando {} arquivos...\n", manifesto.total_arquivos);
for (caminho_rel, _entrada) in &manifesto.arquivos {
let origem_gz = backup.join(format!("{}.gz", caminho_rel));
let destino_arquivo = destino.join(caminho_rel);
if let Some(pai) = destino_arquivo.parent() {
std::fs::create_dir_all(pai)?;
}
if origem_gz.exists() {
compressor::descomprimir(&origem_gz, &destino_arquivo)?;
println!(" {} {}", "[RESTAURADO]".green(), caminho_rel);
} else {
println!(" {} {} (arquivo gz nao encontrado)", "[AVISO]".yellow(), caminho_rel);
}
}
println!("\nRestauracao concluida em: {}", destino.display());
Ok(())
}
fn exibir_info(backup: &Path) -> Result<(), Box<dyn std::error::Error>> {
let caminho_manifesto = backup.join("manifesto.json");
let manifesto = Manifesto::carregar(&caminho_manifesto)?;
println!("{}", "=== Informacoes do Backup ===".green().bold());
println!("Origem: {}", manifesto.diretorio_origem);
println!("Data: {}", manifesto.data_criacao.format("%Y-%m-%d %H:%M:%S UTC"));
println!("Total arquivos: {}", manifesto.total_arquivos);
println!(
"Tamanho original: {:.2} MB",
manifesto.tamanho_total_original as f64 / 1_048_576.0
);
println!(
"Tamanho comprimido: {:.2} MB",
manifesto.tamanho_total_comprimido as f64 / 1_048_576.0
);
println!("\nArquivos:");
for (caminho, entrada) in &manifesto.arquivos {
println!(
" {} ({} bytes, hash: {}...)",
caminho,
entrada.tamanho_original,
&entrada.hash[..16]
);
}
Ok(())
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let args = Argumentos::parse();
match args.comando {
Comando::Backup { origem, destino } => executar_backup(&origem, &destino)?,
Comando::Restaurar { backup, destino } => executar_restauracao(&backup, &destino)?,
Comando::Info { backup } => exibir_info(&backup)?,
}
Ok(())
}
O programa principal usa subcomandos para organizar as tres operacoes: backup, restaurar e info. O fluxo de backup percorre todos os arquivos, calcula hashes, compara com o manifesto anterior e copia apenas o que mudou.
Como Executar
# Compilar
cargo build --release
# Criar um diretorio de teste com alguns arquivos
mkdir -p /tmp/meus_dados/docs
echo "Relatorio importante" > /tmp/meus_dados/relatorio.txt
echo "Notas de reuniao" > /tmp/meus_dados/docs/notas.txt
echo "fn main() { println!(\"Ola\"); }" > /tmp/meus_dados/app.rs
# Primeiro backup (completo)
cargo run -- backup /tmp/meus_dados /tmp/meu_backup
# Modificar um arquivo
echo "Relatorio atualizado com novos dados" > /tmp/meus_dados/relatorio.txt
# Segundo backup (incremental - so copia o que mudou)
cargo run -- backup /tmp/meus_dados /tmp/meu_backup
# Ver informacoes do backup
cargo run -- info /tmp/meu_backup
# Restaurar backup em novo local
cargo run -- restaurar /tmp/meu_backup /tmp/dados_restaurados
Saida esperada do backup incremental:
=== Backup Incremental ===
Origem: /tmp/meus_dados
Destino: /tmp/meu_backup
Analisando arquivos...
[COPIADO] relatorio.txt (36 -> 54 bytes)
[IGNORADO] docs/notas.txt (sem alteracao)
[IGNORADO] app.rs (sem alteracao)
=== Relatorio ===
Arquivos copiados: 1
Arquivos inalterados: 2
Tamanho original: 0.00 MB
Tamanho comprimido: 0.00 MB
Taxa de compressao: 12.5%
Dados economizados: 0.00 MB (arquivos inalterados)
Manifesto salvo em: /tmp/meu_backup/manifesto.json
Desafios para Expandir
- Versionamento de backups: Em vez de sobrescrever, crie pastas com timestamp (ex:
backup_2026-01-15_103000/) e permita restaurar versoes anteriores especificas. - Backup remoto: Adicione suporte para enviar o backup para um servidor remoto via SCP/SFTP usando a crate
ssh2, ou para armazenamento S3 usandoaws-sdk-s3. - Exclusao por padrao: Implemente opcoes
--excluire um arquivo.backupignorecom padroes glob para ignorar arquivos indesejados como*.tmpounode_modules/. - Verificacao de integridade: Adicione um comando
verificarque recalcula os hashes de todos os arquivos no backup e compara com o manifesto para detectar corrupcao. - Criptografia: Implemente criptografia AES-256 dos arquivos de backup usando a crate
aes-gcm, protegendo dados sensiveis com uma senha fornecida pelo usuario.