1. Introdução
Em programação, segurança ao trabalhar com arquivos não significa só proteção contra vírus e hackers, mas também lidar corretamente com erros, locks, permissões, caminhos inválidos e outras armadilhas. Uma abordagem descuidada pode levar à perda de dados, travamento do programa, bugs estranhos ou à temida exceção IOException.
Aqui estão as situações clássicas que queremos saber tratar:
- O arquivo não existe, mas tentamos lê-lo (ou, ao contrário: já existe e queremos escrever no modo "criar apenas se não existir").
- O arquivo está aberto por outro programa e bloqueado.
- O usuário não tem permissões de leitura ou escrita nesse arquivo ou pasta.
- O caminho para o arquivo está incorreto ou contém caracteres proibidos.
- A operação foi interrompida inesperadamente (por exemplo, acabou o disco).
- Mau hábito: deixar arquivos abertos, não fechar streams.
Felizmente, o .NET fornece todas as ferramentas pra resolver esses problemas. Elas são simples — mas, como o cinto de segurança, o importante é não esquecer de usar.
2. Princípios básicos para trabalhar com arquivos com segurança
Cheque a existência do arquivo e permissões antecipadamente
Antes de ler um arquivo, verifique que ele existe (por exemplo, com File.Exists), especialmente se o caminho vem do usuário:
string path = "test.txt";
if (!File.Exists(path))
{
Console.WriteLine("Erro: arquivo não encontrado.");
return;
}
Antes de escrever, certifique-se de que o diretório existe e que você tem permissão de escrita (ou propague o erro pra um nível superior).
Nunca deixe streams abertos
Use a palavra-chave using pra fechar automaticamente os streams — é uma prática recomendada:
using var writer = new StreamWriter("output.txt");
writer.WriteLine("Hello, files!");
// Aqui o arquivo já foi fechado, mesmo se ocorreu um erro!
Isso protege contra locks "presos" e leaks de recursos.
Sempre capture exceções
Qualquer operação de arquivo pode lançar uma exceção. Mesmo que o arquivo estivesse lá um segundo atrás — ele pode ser removido/movido no momento do acesso. Use a construção try-catch:
try
{
using var reader = new StreamReader("data.txt");
string line = reader.ReadLine();
Console.WriteLine(line);
}
catch (FileNotFoundException)
{
Console.WriteLine("Arquivo não encontrado.");
}
catch (UnauthorizedAccessException)
{
Console.WriteLine("Sem acesso ao arquivo.");
}
catch (IOException ex)
{
Console.WriteLine($"Erro de I/O: {ex.Message}");
}
Não confie na entrada do usuário
Se o caminho vem do usuário, ele pode estar errado (ou até tentar quebrar seu programa). Recomenda-se validar o caminho (veja Path.GetInvalidPathChars()):
try
{
string userPath = Console.ReadLine()!;
if (string.IsNullOrWhiteSpace(userPath))
{
Console.WriteLine("O caminho não pode estar vazio!");
return;
}
// Adicional: checar caracteres proibidos
foreach (char c in Path.GetInvalidPathChars())
if (userPath.Contains(c))
{
Console.WriteLine("O caminho contém caracteres inválidos.");
return;
}
// A partir daqui dá pra trabalhar com segurança no arquivo
}
catch (Exception ex)
{
Console.WriteLine("Erro na validação do caminho: " + ex.Message);
}
3. Exemplo prático: lendo um arquivo "direito"
Vamos melhorar um pouco nosso projeto de estudo. Imagine que precisamos ler um arquivo cujo nome foi digitado pelo usuário e mostrar o conteúdo na tela. Tudo com tratamento de erros e considerando a codificação.
Console.Write("Digite o caminho do arquivo: ");
string? path = Console.ReadLine();
// Validação do caminho
if (string.IsNullOrWhiteSpace(path))
{
Console.WriteLine("O caminho não pode estar vazio!");
return;
}
foreach (char c in Path.GetInvalidPathChars())
if (path.Contains(c))
{
Console.WriteLine("O caminho contém caracteres inválidos.");
return;
}
// Vamos tentar ler o arquivo
try
{
if (!File.Exists(path))
{
Console.WriteLine("Arquivo não encontrado.");
return;
}
// Especificar explicitamente a codificação (por exemplo, UTF-8)
using var reader = new StreamReader(path, Encoding.UTF8);
string content = reader.ReadToEnd();
Console.WriteLine("Conteúdo do arquivo:");
Console.WriteLine(content);
}
catch (UnauthorizedAccessException)
{
Console.WriteLine("Sem permissão para ler o arquivo.");
}
catch (IOException ex)
{
Console.WriteLine("Erro de I/O: " + ex.Message);
}
catch (Exception ex)
{
Console.WriteLine("Erro inesperado: " + ex.Message);
}
Observe que esse código não vai quebrar se o arquivo desaparecer entre a digitação do nome e a leitura: a exceção será capturada. O stream será fechado automaticamente ao sair do bloco using — mesmo se ocorrer um erro.
4. Arquivos para "escrita": evitando perda de dados
Quando abrimos um arquivo para escrita, especialmente no modo de sobrescrever (false no segundo parâmetro do construtor do StreamWriter), há risco de apagar dados importantes acidentalmente. Aqui vão algumas dicas:
Cheque se você não está sobrescrevendo um arquivo existente
Às vezes vale perguntar ao usuário se ele realmente quer sobrescrever:
if (File.Exists(path))
{
Console.WriteLine("Aviso: o arquivo já existe. Sobrescrever? (y/n)");
string answer = Console.ReadLine()!;
if (!answer.Equals("y", StringComparison.OrdinalIgnoreCase))
return;
}
Use o modo "append" quando for apropriado
using var writer = new StreamWriter("log.txt", append: true);
writer.WriteLine(DateTime.Now + ": nova entrada no log.");
As informações antigas não serão perdidas.
5. Proteção contra race conditions e conflitos
Às vezes vários programas podem abrir o mesmo arquivo simultaneamente (por exemplo, seu cliente C# e um editor como Notepad++). Isso pode causar erros. Por padrão, StreamReader e StreamWriter usam modos de compartilhamento baseados em FileShare — ou permitem que outros leiam, ou não.
Você pode controlar isso explicitamente:
using var stream = new FileStream("data.txt", FileMode.Open, FileAccess.Read, FileShare.Read);
using var reader = new StreamReader(stream, Encoding.UTF8);
// Leitura...
- FileShare.Read: outros só podem ler.
- FileShare.None: nenhum compartilhamento — o arquivo é "seu".
Se você precisa que o arquivo seja lido e escrito por programas diferentes ao mesmo tempo — use o modo apropriado, mas tome cuidado: isso só é seguro se você souber bem as consequências.
6. Exceções inesperadas e como lidar com elas
Mesmo seguindo todas as recomendações pode haver falhas, por exemplo falta de espaço em disco, pendrive corrompido ou vírus. Aqui vão algumas exceções "exóticas" e como capturá-las:
- PathTooLongException — o caminho é longo demais (mais de 260 caracteres em versões antigas do Windows).
- DirectoryNotFoundException — o diretório especificado não foi encontrado.
- DriveNotFoundException — por exemplo, se o caminho for "Z:\\file.txt" e o drive Z não existir.
- NotSupportedException — por exemplo, o caminho contém uma combinação inválida.
É recomendável ter blocos separados para essas exceções — pelo menos logá-las separadamente.
7. Use arquivos temporários para escrita atômica
Problema clássico: você está escrevendo um arquivo e o programa trava no meio do processo — o arquivo fica corrompido. Programas profissionais costumam usar a estratégia de escrita "atômica":
- Gravar o conteúdo num arquivo temporário (por exemplo, "file.txt.tmp").
- Mover o arquivo temporário (a operação costuma ser atômica no nível do FS) para cima do alvo (File.Replace).
- O arquivo antigo é substituído por inteiro ou não é alterado — não existem dados "meio gravados".
Exemplo:
string tempPath = path + ".tmp";
try
{
using var writer = new StreamWriter(tempPath, false, Encoding.UTF8);
// Gravamos tudo no arquivo temporário
writer.Write(contentForSave);
// Depois da gravação bem-sucedida substituímos o arquivo principal
File.Replace(tempPath, path, null); // Move o temp pra cima do alvo (operação atômica)
}
catch (Exception ex)
{
Console.WriteLine("Erro ao salvar o arquivo: " + ex.Message);
// É recomendável apagar tempPath se não for mais necessário
}
Na prática muitos editores e suítes de escritório fazem assim pra garantir consistência dos dados.
8. Use classes-wrapper para acesso seguro
O .NET tem métodos auxiliares pra operações "seguras" com arquivos. Por exemplo, File.ReadAllText e File.WriteAllText abrem, leem/escrevem e fecham o arquivo automaticamente. Mas mesmo eles devem ser colocados em try-catch:
try
{
string text = File.ReadAllText("settings.json", Encoding.UTF8);
// Trabalhar com os dados...
}
catch (Exception ex)
{
Console.WriteLine("Erro ao trabalhar com o arquivo: " + ex.Message);
}
Para arquivos grandes use streams e leia em partes, pra não consumir toda a memória.
GO TO FULL VERSION