From d2ca2c3ca40ac420540e8e35e0f7da545b19908a Mon Sep 17 00:00:00 2001 From: Manfred Heil Junior Date: Thu, 1 Oct 2026 09:36:19 -0300 Subject: [PATCH 1/6] Cria as regras de pastas excluidas da varredura Nome vale em qualquer nivel e caminho completo vale so para aquela pasta, sem curinga, para o tecnico saber o que ficou fora da conta. A raiz da unidade e a do compartilhamento sao recusadas. A lista fica num arquivo do perfil, uma regra por linha. Autores: Manfred Heil Junior --- .../varredura/regras-exclusao.cs | 146 ++++++++++++++++++ .../mapdisk.testes/regras-exclusao-testes.cs | 53 +++++++ 2 files changed, 199 insertions(+) create mode 100644 src/mapdisk.nucleo/varredura/regras-exclusao.cs create mode 100644 testes/mapdisk.testes/regras-exclusao-testes.cs diff --git a/src/mapdisk.nucleo/varredura/regras-exclusao.cs b/src/mapdisk.nucleo/varredura/regras-exclusao.cs new file mode 100644 index 0000000..0be964d --- /dev/null +++ b/src/mapdisk.nucleo/varredura/regras-exclusao.cs @@ -0,0 +1,146 @@ +namespace MapDisk.Nucleo; + +/// +/// Pastas que a varredura não lê. Nome (sem barra nem dois-pontos) vale em qualquer nível; +/// caminho completo vale só para aquela pasta. Sem curinga: o técnico precisa saber +/// exatamente o que ficou fora da conta (regra 3). +/// +public sealed class RegrasExclusao +{ + public const int Maximo = 100; + + public static readonly RegrasExclusao Nenhuma = new([]); + + private readonly HashSet _nomes; + private readonly HashSet _caminhos; + + private RegrasExclusao(IReadOnlyList regras) + { + Regras = regras; + _nomes = new HashSet(regras.Where(r => !EhCaminho(r)), StringComparer.OrdinalIgnoreCase); + _caminhos = new HashSet(regras.Where(EhCaminho), StringComparer.OrdinalIgnoreCase); + } + + public IReadOnlyList Regras { get; } + + public static RegrasExclusao De(IEnumerable textos) + { + var regras = new List(); + foreach (var texto in textos) + { + if (Validar(texto, out _) is { } regra + && !regras.Contains(regra, StringComparer.OrdinalIgnoreCase) + && regras.Count < Maximo) + { + regras.Add(regra); + } + } + + return new RegrasExclusao(regras); + } + + /// A regra que exclui esta pasta, ou null. + public string? Motivo(string caminhoDaPasta, string nome) + { + if (_nomes.Count > 0 && _nomes.TryGetValue(nome, out var porNome)) + { + return porNome; + } + + return _caminhos.Count > 0 && _caminhos.TryGetValue(caminhoDaPasta.TrimEnd('\\'), out var porCaminho) ? porCaminho : null; + } + + /// A regra limpa, pronta para guardar, ou null com o motivo. + public static string? Validar(string texto, out string? erro) + { + erro = null; + var t = texto.Trim().Trim('"').Trim(); + if (t.Length == 0) + { + erro = "Escreva o nome da pasta ou o caminho completo."; + return null; + } + + if (t.IndexOfAny(['*', '?']) >= 0) + { + erro = "Use o nome da pasta ou o caminho completo, sem * nem ?"; + return null; + } + + if (!t.Contains('\\') && !t.Contains('/') && !t.Contains(':')) + { + if (t.IndexOfAny(Path.GetInvalidFileNameChars()) >= 0) + { + erro = "O nome tem um caractere que o Windows não aceita em pasta."; + return null; + } + + return t; + } + + var caminho = Alvo.Normalizar(t, out _); + if (caminho is null || !(caminho.Length >= 3 && caminho[1] == ':' || caminho.StartsWith(@"\\", StringComparison.Ordinal))) + { + erro = "Use só o nome da pasta, como node_modules, ou o caminho completo, como D:\\Backup."; + return null; + } + + // Alvo.Normalizar devolve a raiz da unidade e a do compartilhamento terminando em barra. + if (caminho.EndsWith('\\')) + { + erro = "A raiz da unidade não pode ser excluída. Escolha outra unidade para varrer."; + return null; + } + + return caminho.TrimEnd('\\'); + } + + private static bool EhCaminho(string regra) => regra.Contains('\\'); +} + +public interface IArmazemExclusoes +{ + RegrasExclusao Ler(); + + void Gravar(RegrasExclusao regras); +} + +/// Uma regra por linha, só nesta máquina. Falha de leitura ou de gravação deixa a lista vazia. +public sealed class ArquivoExclusoes(string arquivo) : IArmazemExclusoes +{ + public static ArquivoExclusoes Padrao() => new(Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "MapDisk", "excluidas.txt")); + + public RegrasExclusao Ler() + { + try + { + return File.Exists(arquivo) ? RegrasExclusao.De(File.ReadAllLines(arquivo)) : RegrasExclusao.Nenhuma; + } + catch (Exception e) when (e is IOException or UnauthorizedAccessException) + { + return RegrasExclusao.Nenhuma; + } + } + + public void Gravar(RegrasExclusao regras) + { + try + { + Directory.CreateDirectory(Path.GetDirectoryName(arquivo)!); + File.WriteAllLines(arquivo, regras.Regras); + } + catch (Exception e) when (e is IOException or UnauthorizedAccessException) + { + } + } +} + +public sealed class ExclusoesEmMemoria : IArmazemExclusoes +{ + private RegrasExclusao _atual = RegrasExclusao.Nenhuma; + + public RegrasExclusao Ler() => _atual; + + public void Gravar(RegrasExclusao regras) => _atual = regras; +} diff --git a/testes/mapdisk.testes/regras-exclusao-testes.cs b/testes/mapdisk.testes/regras-exclusao-testes.cs new file mode 100644 index 0000000..be7bac8 --- /dev/null +++ b/testes/mapdisk.testes/regras-exclusao-testes.cs @@ -0,0 +1,53 @@ +namespace MapDisk.Testes; + +public class RegrasExclusaoTestes +{ + [Fact] + public void Nome_vale_em_qualquer_nivel_sem_diferenca_de_maiuscula() + { + var r = RegrasExclusao.De(["node_modules"]); + Assert.Equal("node_modules", r.Motivo(@"D:\Projetos\site\node_modules", "node_modules")); + Assert.Equal("node_modules", r.Motivo(@"D:\a\b\NODE_MODULES", "NODE_MODULES")); + Assert.Null(r.Motivo(@"D:\Projetos\node_modules_velho", "node_modules_velho")); + } + + [Fact] + public void Caminho_vale_so_para_aquela_pasta() + { + var r = RegrasExclusao.De([@"D:\Backup\Veeam\"]); + Assert.Equal(@"D:\Backup\Veeam", r.Motivo(@"d:\backup\veeam", "veeam")); + Assert.Null(r.Motivo(@"E:\Backup\Veeam", "Veeam")); + Assert.Null(r.Motivo(@"D:\Backup\Veeam2", "Veeam2")); + } + + [Theory] + [InlineData("", "Escreva o nome da pasta ou o caminho completo.")] + [InlineData("*.tmp", "Use o nome da pasta ou o caminho completo, sem * nem ?")] + [InlineData(@"C:\", "A raiz da unidade não pode ser excluída. Escolha outra unidade para varrer.")] + [InlineData(@"\\servidor\dados", "A raiz da unidade não pode ser excluída. Escolha outra unidade para varrer.")] + [InlineData(@"Projetos\velho", "Use só o nome da pasta, como node_modules, ou o caminho completo, como D:\\Backup.")] + public void Regra_invalida_diz_o_motivo(string texto, string erro) + { + Assert.Null(RegrasExclusao.Validar(texto, out var motivo)); + Assert.Equal(erro, motivo); + } + + [Fact] + public void Repetida_e_invalida_ficam_de_fora_e_o_teto_e_100() + { + var r = RegrasExclusao.De(["node_modules", "NODE_MODULES", "*.tmp", ".git"]); + Assert.Equal(["node_modules", ".git"], r.Regras); + Assert.Equal(100, RegrasExclusao.De(Enumerable.Range(0, 150).Select(i => $"pasta{i}")).Regras.Count); + } + + [Fact] + public void Arquivo_grava_e_le_uma_regra_por_linha() + { + var arquivo = Path.Combine(AppContext.BaseDirectory, "exclusoes-" + Guid.NewGuid().ToString("N"), "excluidas.txt"); + var armazem = new ArquivoExclusoes(arquivo); + Assert.Empty(armazem.Ler().Regras); + armazem.Gravar(RegrasExclusao.De(["node_modules", @"D:\Backup"])); + Assert.Equal(["node_modules", @"D:\Backup"], armazem.Ler().Regras); + Directory.Delete(Path.GetDirectoryName(arquivo)!, true); + } +} From 94b8eff400ffa34099a37dfae9a21a9754827989 Mon Sep 17 00:00:00 2001 From: Manfred Heil Junior Date: Thu, 1 Oct 2026 09:37:21 -0300 Subject: [PATCH 2/6] Marca as pastas excluidas na varredura A pasta que bate com uma regra nao e lida, nao soma e entra num contador proprio, como sem acesso e erro. As regras sao lidas no comeco de cada varredura. O alvo nunca e excluido, entao Atualizar esta pasta le a pasta excluida. Autores: Manfred Heil Junior --- src/mapdisk.nucleo/arvore/no-pasta.cs | 31 ++++++++++---- .../varredura/motor-varredura.cs | 22 ++++++++-- .../mapdisk.testes/regras-exclusao-testes.cs | 41 +++++++++++++++++++ 3 files changed, 84 insertions(+), 10 deletions(-) diff --git a/src/mapdisk.nucleo/arvore/no-pasta.cs b/src/mapdisk.nucleo/arvore/no-pasta.cs index 184aa17..efe00e2 100644 --- a/src/mapdisk.nucleo/arvore/no-pasta.cs +++ b/src/mapdisk.nucleo/arvore/no-pasta.cs @@ -10,6 +10,9 @@ public enum EstadoPasta /// Junção ou link simbólico. Não é seguido e não soma. Link, + + /// Pasta que bate com uma regra das Opções. Não é lida e não soma; aparece como "excluída". + Excluida, } [Flags] @@ -50,6 +53,7 @@ public sealed class NoPasta private long _pastasTotal; private long _semAcesso; private long _comErro; + private long _excluidas; private long _modificacao; public NoPasta(string nome, NoPasta? pai, DateTime modificacao = default) @@ -95,6 +99,9 @@ public NoPasta(string nome, NoPasta? pai, DateTime modificacao = default) public long PastasComErro => Interlocked.Read(ref _comErro); + /// Pastas excluídas da varredura nesta subárvore, contando esta. + public long PastasExcluidas => Interlocked.Read(ref _excluidas); + public DateTime UltimaModificacao => new(Interlocked.Read(ref _modificacao)); public int Nivel @@ -142,6 +149,7 @@ internal void DescontarAcima() var pastas = PastasTotal; var semAcesso = PastasSemAcesso; var comErro = PastasComErro; + var excluidas = PastasExcluidas; for (var no = Pai; no != null; no = no.Pai) { Interlocked.Add(ref no._tamanho, -tamanho); @@ -150,6 +158,7 @@ internal void DescontarAcima() Interlocked.Add(ref no._pastasTotal, -pastas); Interlocked.Add(ref no._semAcesso, -semAcesso); Interlocked.Add(ref no._comErro, -comErro); + Interlocked.Add(ref no._excluidas, -excluidas); } } @@ -157,7 +166,7 @@ internal void DescontarAcima() internal void RemoverSubpasta(NoPasta sub) { sub.DescontarAcima(); - Somar(0, 0, 0, -1, 0, 0, 0); + Somar(0, 0, 0, -1, 0, 0, 0, 0); Volatile.Write(ref _subpastas, Volatile.Read(ref _subpastas).Where(s => s != sub).ToArray()); } @@ -174,7 +183,7 @@ internal void RemoverArquivo(ArquivoInfo arquivo) var alocado = arquivo.Soma ? arquivo.Alocado : 0; TamanhoProprio -= tamanho; AlocadoProprio -= alocado; - Somar(-tamanho, -alocado, -1, 0, 0, 0, 0); + Somar(-tamanho, -alocado, -1, 0, 0, 0, 0, 0); Volatile.Write(ref _arquivos, lista.ToArray()); } @@ -259,31 +268,38 @@ public void Preencher(ArquivoInfo[] arquivos, NoPasta[] subpastas) Volatile.Write(ref _arquivos, arquivos); Volatile.Write(ref _subpastas, subpastas); _estado = EstadoPasta.Lida; - Somar(tamanho, alocado, arquivos.Length, subpastas.Length, 0, 0, recente); + Somar(tamanho, alocado, arquivos.Length, subpastas.Length, 0, 0, 0, recente); } public void MarcarSemAcesso(string motivo) { Motivo = motivo; _estado = EstadoPasta.SemAcesso; - Somar(0, 0, 0, 0, 1, 0, ModificacaoPropria.Ticks); + Somar(0, 0, 0, 0, 1, 0, 0, ModificacaoPropria.Ticks); } public void MarcarErro(string motivo) { Motivo = motivo; _estado = EstadoPasta.ErroLeitura; - Somar(0, 0, 0, 0, 0, 1, ModificacaoPropria.Ticks); + Somar(0, 0, 0, 0, 0, 1, 0, ModificacaoPropria.Ticks); } public void MarcarLink(string? destino) { DestinoLink = destino; _estado = EstadoPasta.Link; - Somar(0, 0, 0, 0, 0, 0, ModificacaoPropria.Ticks); + Somar(0, 0, 0, 0, 0, 0, 0, ModificacaoPropria.Ticks); + } + + public void MarcarExcluida(string regra) + { + Motivo = regra; + _estado = EstadoPasta.Excluida; + Somar(0, 0, 0, 0, 0, 0, 1, ModificacaoPropria.Ticks); } - private void Somar(long tamanho, long alocado, long arquivos, long pastas, long semAcesso, long comErro, long modificacao) + private void Somar(long tamanho, long alocado, long arquivos, long pastas, long semAcesso, long comErro, long excluidas, long modificacao) { for (var no = this; no != null; no = no.Pai) { @@ -293,6 +309,7 @@ private void Somar(long tamanho, long alocado, long arquivos, long pastas, long Interlocked.Add(ref no._pastasTotal, pastas); Interlocked.Add(ref no._semAcesso, semAcesso); Interlocked.Add(ref no._comErro, comErro); + Interlocked.Add(ref no._excluidas, excluidas); long atual; while (modificacao > (atual = Interlocked.Read(ref no._modificacao)) diff --git a/src/mapdisk.nucleo/varredura/motor-varredura.cs b/src/mapdisk.nucleo/varredura/motor-varredura.cs index daf1c40..1abface 100644 --- a/src/mapdisk.nucleo/varredura/motor-varredura.cs +++ b/src/mapdisk.nucleo/varredura/motor-varredura.cs @@ -51,6 +51,13 @@ public interface IMotorVarredura /// Começa a varrer o alvo, já normalizado, e devolve na hora, com a raiz para a tela. Varredura Iniciar(string alvo, CancellationToken cancelar); + /// Pastas que a próxima varredura não lê. O padrão não exclui nada. + RegrasExclusao Exclusoes + { + get => RegrasExclusao.Nenhuma; + set { } + } + /// Lê de novo só esta pasta. O padrão não relê nada e devolve a mesma pasta. Varredura Reler(NoPasta pasta, CancellationToken cancelar) => new Varredura(pasta).Comecar(_ => Task.FromResult(new ResultadoVarredura @@ -68,6 +75,9 @@ Varredura Reler(NoPasta pasta, CancellationToken cancelar) => /// public sealed class MotorVarredura(int? tarefas = null) : IMotorVarredura { + /// Lidas no começo de cada varredura: mudar no meio vale só para a próxima. + public RegrasExclusao Exclusoes { get; set; } = RegrasExclusao.Nenhuma; + public Varredura Iniciar(string alvo, CancellationToken cancelar) => Comecar(new NoPasta(alvo, null), alvo, cancelar); /// @@ -96,10 +106,11 @@ public Varredura Reler(NoPasta pasta, CancellationToken cancelar) private Varredura Comecar(NoPasta raiz, string caminho, CancellationToken cancelar) { var quantas = tarefas ?? (Alvo.EhRede(caminho) ? 4 : Math.Clamp(Environment.ProcessorCount, 4, 16)); - return new Varredura(raiz).Comecar(v => Task.Run(() => Executar(v, caminho, quantas, cancelar), CancellationToken.None)); + var regras = Exclusoes; + return new Varredura(raiz).Comecar(v => Task.Run(() => Executar(v, caminho, quantas, regras, cancelar), CancellationToken.None)); } - private static ResultadoVarredura Executar(Varredura varredura, string caminhoDaRaiz, int tarefas, CancellationToken cancelar) + private static ResultadoVarredura Executar(Varredura varredura, string caminhoDaRaiz, int tarefas, RegrasExclusao regras, CancellationToken cancelar) { var relogio = Stopwatch.StartNew(); var raiz = varredura.Raiz; @@ -129,7 +140,7 @@ private static ResultadoVarredura Executar(Varredura varredura, string caminhoDa { foreach (var (no, caminho) in fila.GetConsumingEnumerable(cancelar)) { - LerPasta(varredura, no, caminho, alvoERaizDoVolume && no == raiz, primeiraVez, nomes, arquivos, entradas); + LerPasta(varredura, no, caminho, alvoERaizDoVolume && no == raiz, primeiraVez, nomes, regras, arquivos, entradas); foreach (var sub in no.Subpastas) { if (sub.Estado == EstadoPasta.Pendente) @@ -171,6 +182,7 @@ private static void LerPasta( bool raizDoVolume, Func primeiraVez, ConjuntoNomes nomes, + RegrasExclusao regras, List arquivos, List entradas) { @@ -204,6 +216,10 @@ private static void LerPasta( { sub.MarcarLink(DestinoDoLink(Alvo.Juntar(caminho, entrada.Nome))); } + else if (regras.Motivo(Alvo.Juntar(caminho, entrada.Nome), entrada.Nome) is { } regra) + { + sub.MarcarExcluida(regra); + } subpastas[i] = sub; } diff --git a/testes/mapdisk.testes/regras-exclusao-testes.cs b/testes/mapdisk.testes/regras-exclusao-testes.cs index be7bac8..013fd12 100644 --- a/testes/mapdisk.testes/regras-exclusao-testes.cs +++ b/testes/mapdisk.testes/regras-exclusao-testes.cs @@ -50,4 +50,45 @@ public void Arquivo_grava_e_le_uma_regra_por_linha() Assert.Equal(["node_modules", @"D:\Backup"], armazem.Ler().Regras); Directory.Delete(Path.GetDirectoryName(arquivo)!, true); } + + [Fact] + public async Task Varredura_nao_le_a_pasta_excluida_e_conta_ela() + { + using var p = new PastaTeste(); + p.Arquivo(@"site\index.html", 1000); + p.Arquivo(@"site\node_modules\pacote\a.js", 50_000); + p.Arquivo(@"backup\velho.zip", 70_000); + var motor = new MotorVarredura { Exclusoes = RegrasExclusao.De(["node_modules", p.Caminho("backup")]) }; + + var raiz = (await motor.Iniciar(p.Raiz, CancellationToken.None).Conclusao).Raiz; + + var site = raiz.Subpastas.Single(s => s.Nome == "site"); + var modulos = site.Subpastas.Single(); + Assert.Equal(EstadoPasta.Excluida, modulos.Estado); + Assert.Equal("node_modules", modulos.Motivo); + Assert.Empty(modulos.Subpastas); + Assert.Equal(1000, site.Tamanho); + Assert.Equal(EstadoPasta.Excluida, raiz.Subpastas.Single(s => s.Nome == "backup").Estado); + Assert.Equal(2, raiz.PastasExcluidas); + Assert.Equal(0, raiz.PastasSemAcesso + raiz.PastasComErro); + } + + [Fact] + public async Task Alvo_nunca_e_excluido_e_atualizar_le_a_pasta_excluida() + { + using var p = new PastaTeste(); + p.Arquivo(@"node_modules\a.js", 5000); + var motor = new MotorVarredura { Exclusoes = RegrasExclusao.De(["node_modules"]) }; + + var direto = (await motor.Iniciar(p.Caminho("node_modules"), CancellationToken.None).Conclusao).Raiz; + Assert.Equal(EstadoPasta.Lida, direto.Estado); + Assert.Equal(5000, direto.Tamanho); + + var raiz = (await motor.Iniciar(p.Raiz, CancellationToken.None).Conclusao).Raiz; + var excluida = raiz.Subpastas.Single(); + var relida = (await motor.Reler(excluida, CancellationToken.None).Conclusao).Raiz; + Assert.Equal(EstadoPasta.Lida, relida.Estado); + Assert.Equal(5000, raiz.Tamanho); + Assert.Equal(0, raiz.PastasExcluidas); + } } From e8a91b302ee40713d92f52058c5e95cdac8fc815 Mon Sep 17 00:00:00 2001 From: Manfred Heil Junior Date: Thu, 1 Oct 2026 09:39:52 -0300 Subject: [PATCH 3/6] Mostra as pastas excluidas na tela A arvore mostra excluida com a regra, a barra de status conta, as analises e o grafico avisam que ficaram fora, e as acoes bloqueiam com o caminho para ler a pasta. O painel guarda as regras e entrega ao motor. A demonstracao ganha a Lixeira excluida. Autores: Manfred Heil Junior --- src/mapdisk.nucleo/acoes/preparador-acoes.cs | 5 ++ src/mapdisk.nucleo/grafico/itens-grafico.cs | 11 +++- src/mapdisk.nucleo/painel/demonstracao.cs | 15 +++++- .../painel/dependencias-painel.cs | 3 ++ src/mapdisk.nucleo/painel/linha-arvore.cs | 4 +- src/mapdisk.nucleo/painel/painel-analises.cs | 15 ++++-- src/mapdisk.nucleo/painel/painel-grafico.cs | 5 +- src/mapdisk.nucleo/painel/painel-principal.cs | 22 ++++++++ testes/mapdisk.testes/acoes-testes.cs | 14 +++++ .../mapdisk.testes/painel-principal-testes.cs | 2 +- .../mapdisk.testes/regras-exclusao-testes.cs | 54 +++++++++++++++++++ 11 files changed, 141 insertions(+), 9 deletions(-) diff --git a/src/mapdisk.nucleo/acoes/preparador-acoes.cs b/src/mapdisk.nucleo/acoes/preparador-acoes.cs index 1b0e19d..ad16ca8 100644 --- a/src/mapdisk.nucleo/acoes/preparador-acoes.cs +++ b/src/mapdisk.nucleo/acoes/preparador-acoes.cs @@ -103,6 +103,11 @@ public AvaliacaoSelecao AvaliarItens(IReadOnlyList itens) return Bloqueado($"{item.Nome}: é link. Trate pelo Explorer.", unicos); } + if (item.EhPasta && item.Pasta.Estado == EstadoPasta.Excluida) + { + return Bloqueado($"{item.Nome}: pasta excluída da varredura. Use Atualizar esta pasta para ler antes de agir.", unicos); + } + if (item.EhPasta && item.Pasta.Estado != EstadoPasta.Lida) { return Bloqueado($"{item.Nome}: pasta sem leitura. Atualize ou varra como administrador antes.", unicos); diff --git a/src/mapdisk.nucleo/grafico/itens-grafico.cs b/src/mapdisk.nucleo/grafico/itens-grafico.cs index 9b80e2d..8f3723e 100644 --- a/src/mapdisk.nucleo/grafico/itens-grafico.cs +++ b/src/mapdisk.nucleo/grafico/itens-grafico.cs @@ -11,7 +11,7 @@ public enum TipoItemGrafico public sealed record ItemGrafico(string Nome, long Valor, TipoItemGrafico Tipo, NoPasta? Pasta); /// Itens do maior para o menor, o total deles e as subpastas sem leitura, que ficam fora do desenho. -public sealed record ConteudoGrafico(IReadOnlyList Itens, long Total, IReadOnlyList NaoLidas); +public sealed record ConteudoGrafico(IReadOnlyList Itens, long Total, IReadOnlyList NaoLidas, IReadOnlyList Excluidas); /// /// O que o gráfico desenha de uma pasta: um nível abaixo dela, no mesmo critério da árvore. @@ -25,6 +25,7 @@ public static ConteudoGrafico DaPasta(NoPasta pasta, ModoExibicao modo, int maxi { var itens = new List(); var naoLidas = new List(); + var excluidas = new List(); foreach (var sub in pasta.Subpastas) { if (sub.Estado == EstadoPasta.Link) @@ -32,6 +33,12 @@ public static ConteudoGrafico DaPasta(NoPasta pasta, ModoExibicao modo, int maxi continue; } + if (sub.Estado == EstadoPasta.Excluida) + { + excluidas.Add(sub.Nome); + continue; + } + if (sub.Estado != EstadoPasta.Lida) { naoLidas.Add(sub.Nome); @@ -68,7 +75,7 @@ public static ConteudoGrafico DaPasta(NoPasta pasta, ModoExibicao modo, int maxi null)); } - return new ConteudoGrafico(itens, itens.Sum(i => i.Valor), naoLidas); + return new ConteudoGrafico(itens, itens.Sum(i => i.Valor), naoLidas, excluidas); } private static long Valor(NoPasta pasta, ModoExibicao modo) => modo switch diff --git a/src/mapdisk.nucleo/painel/demonstracao.cs b/src/mapdisk.nucleo/painel/demonstracao.cs index eb42a20..2a970c2 100644 --- a/src/mapdisk.nucleo/painel/demonstracao.cs +++ b/src/mapdisk.nucleo/painel/demonstracao.cs @@ -20,11 +20,22 @@ public static class Demonstracao Operacoes = new OperacoesDemonstracao(), Registro = new RegistroEmMemoria(), Leitor = new LeitorDemonstracao(), + Exclusoes = ExclusoesDaDemonstracao(), Demonstracao = true, }); + private static ExclusoesEmMemoria ExclusoesDaDemonstracao() + { + var exclusoes = new ExclusoesEmMemoria(); + exclusoes.Gravar(RegrasExclusao.De([Lixeira])); + return exclusoes; + } + public static IMotorVarredura Motor() => new MotorDemonstracao(); + // A pasta da Lixeira entra excluída, para a tela de exemplo mostrar o estado "excluída". + private const string Lixeira = "$Recycle.Bin"; + internal static NoPasta Montar(string alvo) { var d = new DateTime(2026, 9, 25, 10, 0, 0); @@ -34,13 +45,15 @@ internal static NoPasta Montar(string alvo) var programas = new NoPasta("Program Files", raiz, d); var dados = new NoPasta("Dados", raiz, d); var sistema = new NoPasta("System Volume Information", raiz, d); + var lixeira = new NoPasta(Lixeira, raiz, d); raiz.Preencher( [ new ArquivoInfo("pagefile.sys", 16 * Gb, 16 * Gb, d, MarcaArquivo.Sistema), new ArquivoInfo("hiberfil.sys", 12 * Gb, 12 * Gb, d, MarcaArquivo.Sistema), ], - [users, windows, programas, dados, sistema]); + [users, windows, programas, dados, sistema, lixeira]); sistema.MarcarSemAcesso("acesso negado"); + lixeira.MarcarExcluida(Lixeira); var ana = new NoPasta("ana.souza", users, d); var bruno = new NoPasta("bruno.lima", users, d); diff --git a/src/mapdisk.nucleo/painel/dependencias-painel.cs b/src/mapdisk.nucleo/painel/dependencias-painel.cs index a6ee175..3716932 100644 --- a/src/mapdisk.nucleo/painel/dependencias-painel.cs +++ b/src/mapdisk.nucleo/painel/dependencias-painel.cs @@ -24,6 +24,9 @@ public sealed class DependenciasPainel /// Opções guardadas. O padrão fica só na memória; o Padrao() do painel liga o arquivo. public IArmazemPreferencias Preferencias { get; init; } = new PreferenciasEmMemoria(); + /// Pastas excluídas da varredura. O padrão fica só na memória; o Padrao() do painel liga o arquivo. + public IArmazemExclusoes Exclusoes { get; init; } = new ExclusoesEmMemoria(); + public IRegistroAcoes Registro { get; init; } = new RegistroEmMemoria(); public LocaisProtegidos Locais { get; init; } = LocaisProtegidos.DoSistema(); diff --git a/src/mapdisk.nucleo/painel/linha-arvore.cs b/src/mapdisk.nucleo/painel/linha-arvore.cs index 885754e..3837177 100644 --- a/src/mapdisk.nucleo/painel/linha-arvore.cs +++ b/src/mapdisk.nucleo/painel/linha-arvore.cs @@ -86,7 +86,7 @@ internal LinhaArvore(TipoLinha tipo, NoPasta pasta, ArquivoInfo arquivo = defaul /// A pasta ainda não lida aparece durante a varredura e depois de uma varredura interrompida. /// public bool SemValor => Tipo == TipoLinha.Pasta - && Pasta.Estado is EstadoPasta.Pendente or EstadoPasta.SemAcesso or EstadoPasta.ErroLeitura or EstadoPasta.Link; + && Pasta.Estado is EstadoPasta.Pendente or EstadoPasta.SemAcesso or EstadoPasta.ErroLeitura or EstadoPasta.Link or EstadoPasta.Excluida; public string Rotulo => Tipo switch { @@ -95,6 +95,7 @@ internal LinhaArvore(TipoLinha tipo, NoPasta pasta, ArquivoInfo arquivo = defaul { EstadoPasta.ErroLeitura => $"erro de leitura: {Pasta.Motivo}", EstadoPasta.Link => Pasta.DestinoLink is { } destino ? $"link para {destino}" : "link", + EstadoPasta.Excluida => $"excluída da varredura (regra: {Pasta.Motivo})", EstadoPasta.Lida when Pasta.PastasSemAcesso + Pasta.PastasComErro is var n and > 0 => $"{Formatador.Plural(n, "pasta", "pastas")} sem leitura dentro", _ => string.Empty, @@ -169,6 +170,7 @@ internal void Atualizar(ModoExibicao modo, UnidadeExibicao unidade, long valorDo EstadoPasta.Pendente => "não lida", EstadoPasta.SemAcesso => "sem acesso", EstadoPasta.Link => "link", + EstadoPasta.Excluida => "excluída", _ => "erro", }; Fracao = 0; diff --git a/src/mapdisk.nucleo/painel/painel-analises.cs b/src/mapdisk.nucleo/painel/painel-analises.cs index bdf8e29..ffbfb21 100644 --- a/src/mapdisk.nucleo/painel/painel-analises.cs +++ b/src/mapdisk.nucleo/painel/painel-analises.cs @@ -98,9 +98,18 @@ public async Task CalcularAsync(NoPasta pasta, DateTime hoje) Grafico.Carregar(pasta); Titulo = $"Análise de {pasta.CaminhoCompleto()}"; var semLeitura = pasta.PastasSemAcesso + pasta.PastasComErro; - Aviso = semLeitura > 0 - ? $"{Formatador.Plural(semLeitura, "pasta sem leitura não entra", "pastas sem leitura não entram")} nesta conta" - : string.Empty; + var avisos = new List(); + if (semLeitura > 0) + { + avisos.Add($"{Formatador.Plural(semLeitura, "pasta sem leitura não entra", "pastas sem leitura não entram")} nesta conta"); + } + + if (pasta.PastasExcluidas > 0) + { + avisos.Add($"{Formatador.Plural(pasta.PastasExcluidas, "pasta excluída da varredura não entra", "pastas excluídas da varredura não entram")} nesta conta"); + } + + Aviso = string.Join(". ", avisos); Maiores = r.Maiores.Select(Linha).ToList(); AplicarAntigos(r.Antigos, idade); var total = pasta.Tamanho; diff --git a/src/mapdisk.nucleo/painel/painel-grafico.cs b/src/mapdisk.nucleo/painel/painel-grafico.cs index 7201776..31ceac4 100644 --- a/src/mapdisk.nucleo/painel/painel-grafico.cs +++ b/src/mapdisk.nucleo/painel/painel-grafico.cs @@ -44,6 +44,8 @@ public sealed class PainelGrafico : INotifyPropertyChanged public string TextoNaoLidas { get; private set; } = string.Empty; + public string TextoExcluidas { get; private set; } = string.Empty; + public bool MostrarBlocos => Tipo == TipoGrafico.Blocos; public bool MostrarPizza => Tipo == TipoGrafico.Pizza; @@ -84,11 +86,12 @@ private void Montar() { Blocos = []; Fatias = []; - TextoNaoLidas = string.Empty; + TextoNaoLidas = TextoExcluidas = string.Empty; if (_pasta is { } pasta) { var c = ItensGrafico.DaPasta(pasta, Modo, Tipo == TipoGrafico.Blocos ? MaximoBlocos : MaximoFatias); TextoNaoLidas = c.NaoLidas.Count == 0 ? string.Empty : $"Sem leitura, fora do gráfico: {string.Join(", ", c.NaoLidas)}"; + TextoExcluidas = c.Excluidas.Count == 0 ? string.Empty : $"Excluídas da varredura, fora do gráfico: {string.Join(", ", c.Excluidas)}"; var cores = Paleta.CoresDe(c.Itens); if (Tipo == TipoGrafico.Blocos) { diff --git a/src/mapdisk.nucleo/painel/painel-principal.cs b/src/mapdisk.nucleo/painel/painel-principal.cs index a1014f0..c6f17e0 100644 --- a/src/mapdisk.nucleo/painel/painel-principal.cs +++ b/src/mapdisk.nucleo/painel/painel-principal.cs @@ -20,6 +20,7 @@ public sealed class PainelPrincipal : INotifyPropertyChanged private readonly IHistoricoAlvos _historico; private readonly Func _elevar; private readonly IArmazemPreferencias _preferencias; + private readonly IArmazemExclusoes _exclusoes; private CancellationTokenSource? _cancelar; private Varredura? _varredura; private InfoVolume? _volume; @@ -39,6 +40,9 @@ public PainelPrincipal(DependenciasPainel dependencias) Analises = new PainelAnalises(dependencias.Leitor); _preferencias = dependencias.Preferencias; Aplicar(_preferencias.Ler()); + _exclusoes = dependencias.Exclusoes; + Exclusoes = _exclusoes.Ler(); + _motor.Exclusoes = Exclusoes; Unidades = _listarUnidades(); TextoAlvo = Unidades.FirstOrDefault()?.Raiz ?? string.Empty; MontarOpcoes(); @@ -61,8 +65,12 @@ public PainelPrincipal(IMotorVarredura motor, Func> li Operacoes = new OperacoesArquivo(), Registro = RegistroAcoes.Padrao(), Preferencias = ArquivoPreferencias.Padrao(), + Exclusoes = ArquivoExclusoes.Padrao(), }); + /// Pastas que a próxima varredura não lê. + public RegrasExclusao Exclusoes { get; private set; } = RegrasExclusao.Nenhuma; + public Preferencias Preferencias { get; private set; } = new(); /// Volume da última varredura, para o relatório do técnico. @@ -436,6 +444,11 @@ private void AtualizarTextos() partes.Add(Formatador.Plural(raiz.PastasComErro, "pasta com erro de leitura", "pastas com erro de leitura")); } + if (raiz.PastasExcluidas > 0) + { + partes.Add(Formatador.Plural(raiz.PastasExcluidas, "pasta excluída", "pastas excluídas")); + } + TextoSemLeitura = string.Join(" | ", partes); } @@ -451,6 +464,15 @@ public void GravarPreferencias(Preferencias preferencias) Aplicar(preferencias); } + /// Grava a lista e entrega ao motor. Vale na próxima varredura. + public void GravarExclusoes(RegrasExclusao regras) + { + _exclusoes.Gravar(regras); + Exclusoes = regras; + _motor.Exclusoes = regras; + Avisar(); + } + public void EsquecerAlvo(string alvo) { _historico.Gravar(UltimosAlvos.Esquecer(_historico.Ler(), alvo)); diff --git a/testes/mapdisk.testes/acoes-testes.cs b/testes/mapdisk.testes/acoes-testes.cs index fe24ea9..e835d98 100644 --- a/testes/mapdisk.testes/acoes-testes.cs +++ b/testes/mapdisk.testes/acoes-testes.cs @@ -448,6 +448,20 @@ public void Raiz_sistema_e_pasta_sem_leitura_bloqueiam_com_o_motivo() Preparador().Avaliar([LinhaDe(outra, "velha")]).Bloqueio); } + [Fact] + public void Pasta_excluida_bloqueia_e_diz_como_ler() + { + var raiz = new NoPasta(@"D:", null); + var modulos = new NoPasta("node_modules", raiz); + raiz.Preencher([], [modulos]); + modulos.MarcarExcluida("node_modules"); + var arvore = new ArvoreVisivel(); + arvore.Carregar(raiz); + + Assert.Equal("node_modules: pasta excluída da varredura. Use Atualizar esta pasta para ler antes de agir.", + Preparador().Avaliar([LinhaDe(arvore, "node_modules")]).Bloqueio); + } + [Fact] public void Sem_lixeira_a_remocao_vira_exclusao() { diff --git a/testes/mapdisk.testes/painel-principal-testes.cs b/testes/mapdisk.testes/painel-principal-testes.cs index 5cd7c74..0d541a6 100644 --- a/testes/mapdisk.testes/painel-principal-testes.cs +++ b/testes/mapdisk.testes/painel-principal-testes.cs @@ -195,7 +195,7 @@ public void Demonstracao_mostra_arvore_com_pasta_sem_acesso() p.Tique(); Assert.Equal(EstadoPainel.Parado, p.Estado); - Assert.Equal("1 pasta sem acesso", p.TextoSemLeitura); + Assert.Equal("1 pasta sem acesso | 1 pasta excluída", p.TextoSemLeitura); Assert.Equal(@"C:\", p.Arvore.Linhas[0].Nome); Assert.True(p.Arvore.Linhas.Count > 3); Assert.Contains("NTFS", p.TextoVolume); diff --git a/testes/mapdisk.testes/regras-exclusao-testes.cs b/testes/mapdisk.testes/regras-exclusao-testes.cs index 013fd12..357695a 100644 --- a/testes/mapdisk.testes/regras-exclusao-testes.cs +++ b/testes/mapdisk.testes/regras-exclusao-testes.cs @@ -91,4 +91,58 @@ public async Task Alvo_nunca_e_excluido_e_atualizar_le_a_pasta_excluida() Assert.Equal(5000, raiz.Tamanho); Assert.Equal(0, raiz.PastasExcluidas); } + + [Fact] + public void Painel_passa_as_regras_ao_motor_e_grava() + { + var motor = new MotorVarredura(); + var armazem = new ExclusoesEmMemoria(); + armazem.Gravar(RegrasExclusao.De(["node_modules"])); + var painel = new PainelPrincipal(new DependenciasPainel { Motor = motor, ListarUnidades = () => [], Exclusoes = armazem }); + Assert.Equal(["node_modules"], motor.Exclusoes.Regras); + + painel.GravarExclusoes(RegrasExclusao.De([".git"])); + Assert.Equal([".git"], armazem.Ler().Regras); + Assert.Equal([".git"], motor.Exclusoes.Regras); + Assert.Equal([".git"], painel.Exclusoes.Regras); + } + + [Fact] + public void Linha_grafico_e_analises_tratam_a_pasta_excluida() + { + var raiz = ArvoreComExcluida(); + + var linha = new LinhaArvore(TipoLinha.Pasta, raiz.Subpastas[1]); + Assert.True(linha.SemValor); + Assert.Equal("excluída da varredura (regra: node_modules)", linha.Rotulo); + + var grafico = ItensGrafico.DaPasta(raiz, ModoExibicao.Tamanho, 20); + Assert.Equal(["node_modules"], grafico.Excluidas); + Assert.Empty(grafico.NaoLidas); + Assert.Equal(1, raiz.PastasExcluidas); + Assert.Equal(1000, raiz.Tamanho); + } + + [Fact] + public async Task Analises_avisam_e_demonstracao_mostra_a_excluida() + { + var analises = new PainelAnalises(); + await analises.CalcularAsync(ArvoreComExcluida(), new DateTime(2026, 10, 1)); + Assert.Equal("1 pasta excluída da varredura não entra nesta conta", analises.Aviso); + + var demo = (await Demonstracao.Motor().Iniciar(@"C:\", CancellationToken.None).Conclusao).Raiz; + Assert.Equal(1, demo.PastasExcluidas); + Assert.Equal(["$Recycle.Bin"], Demonstracao.Painel().Exclusoes.Regras); + } + + private static NoPasta ArvoreComExcluida() + { + var raiz = new NoPasta(@"D:\", null); + var dados = new NoPasta("Dados", raiz); + var modulos = new NoPasta("node_modules", raiz); + raiz.Preencher([], [dados, modulos]); + dados.Preencher([new ArquivoInfo("a.txt", 1000, 4096, new DateTime(2026, 1, 1), MarcaArquivo.Nenhuma)], []); + modulos.MarcarExcluida("node_modules"); + return raiz; + } } From 7489e528d99aafe57d055f8859fcec0995b105d5 Mon Sep 17 00:00:00 2001 From: Manfred Heil Junior Date: Thu, 1 Oct 2026 09:40:50 -0300 Subject: [PATCH 4/6] Leva as pastas excluidas aos relatorios e a linha de comando CSV, relatorio do tecnico e pagina do relatorio para o cliente dizem o que ficou fora da conta. A linha de comando ganha --excluir, que pode repetir, e nao depende da lista das Opcoes. Autores: Manfred Heil Junior --- .../linha-de-comando/argumentos.cs | 24 +++++++++++++++++-- .../linha-de-comando/executor-cli.cs | 7 ++++++ .../relatorios/exportador-csv.cs | 1 + .../relatorios/pagina-avaliacao.cs | 5 ++++ .../relatorios/relatorio-tecnico.cs | 10 ++++++-- testes/mapdisk.testes/argumentos-testes.cs | 10 +++++++- testes/mapdisk.testes/executor-cli-testes.cs | 14 +++++++++++ .../mapdisk.testes/regras-exclusao-testes.cs | 17 +++++++++++++ 8 files changed, 83 insertions(+), 5 deletions(-) diff --git a/src/mapdisk.nucleo/linha-de-comando/argumentos.cs b/src/mapdisk.nucleo/linha-de-comando/argumentos.cs index 762ab02..e139aaa 100644 --- a/src/mapdisk.nucleo/linha-de-comando/argumentos.cs +++ b/src/mapdisk.nucleo/linha-de-comando/argumentos.cs @@ -35,6 +35,9 @@ mapdisk varrer [opções] varre e mostra os maiores itens --relatorio grava o relatório com a marca da MT e o gráfico --top quantos itens mostrar e pôr em cada lista do relatório, de 1 a 1000 (padrão: 10) + --excluir não lê esta pasta, que fica fora do total + (pode repetir: --excluir node_modules + --excluir D:\Backup\Veeam) Exemplos: mapdisk varrer C: @@ -53,6 +56,9 @@ A linha de comando só lê. Ela nunca apaga nem move arquivos. public string? Relatorio { get; private set; } + /// Pastas que esta varredura não lê, já conferidas. A lista das Opções não entra aqui. + public List Excluir { get; } = []; + public int Top { get; private set; } = 10; public List Erros { get; } = []; @@ -90,6 +96,20 @@ public static ArgumentosCli Interpretar(IReadOnlyList args) case "--remover-integracao": a.Comando = ComandoCli.RemoverIntegracao; comandos++; + break; + case "--excluir": + if (Valor(args, ref i, arg, a.Erros) is { } texto) + { + if (RegrasExclusao.Validar(texto, out var motivo) is { } regra) + { + a.Excluir.Add(regra); + } + else + { + a.Erros.Add($"--excluir {texto}: {motivo}"); + } + } + break; case "--relatorio": a.Relatorio = Valor(args, ref i, arg, a.Erros); @@ -160,9 +180,9 @@ public static ArgumentosCli Interpretar(IReadOnlyList args) a.Erros.Add($"Comando desconhecido: {alvos[0]}. Use varrer ou --ajuda."); } - if (a.Csv != null || a.Relatorio != null || topDado) + if (a.Csv != null || a.Relatorio != null || topDado || a.Excluir.Count > 0) { - a.Erros.Add("As opções --csv, --relatorio e --top pedem o comando varrer."); + a.Erros.Add("As opções --csv, --relatorio, --top e --excluir pedem o comando varrer."); } } diff --git a/src/mapdisk.nucleo/linha-de-comando/executor-cli.cs b/src/mapdisk.nucleo/linha-de-comando/executor-cli.cs index 967d4ac..b4fe083 100644 --- a/src/mapdisk.nucleo/linha-de-comando/executor-cli.cs +++ b/src/mapdisk.nucleo/linha-de-comando/executor-cli.cs @@ -45,6 +45,7 @@ public static int Executar(ArgumentosCli argumentos, IMotorVarredura motor, Text } saida.WriteLine($"Varrendo {argumentos.Caminho}..."); + motor.Exclusoes = RegrasExclusao.De(argumentos.Excluir); var r = motor.Iniciar(argumentos.Caminho!, cancelar).Conclusao.GetAwaiter().GetResult(); var raiz = r.Raiz; if (raiz.Estado is EstadoPasta.SemAcesso or EstadoPasta.ErroLeitura) @@ -72,6 +73,11 @@ public static int Executar(ArgumentosCli argumentos, IMotorVarredura motor, Text saida.WriteLine($"Atenção: {Formatador.Plural(raiz.PastasComErro, "pasta com erro de leitura", "pastas com erro de leitura")}. O total não inclui o que está nelas."); } + if (raiz.PastasExcluidas > 0) + { + saida.WriteLine($"Atenção: {Formatador.Plural(raiz.PastasExcluidas, "pasta excluída da varredura", "pastas excluídas da varredura")}, fora do total."); + } + if (!r.Cancelada && r.PastasNaoLidas > 0) { saida.WriteLine($"Atenção: {Formatador.Plural(r.PastasNaoLidas, "pasta não foi lida", "pastas não foram lidas")}. Rode de novo para conferir."); @@ -128,6 +134,7 @@ private static IEnumerable MaioresItens(NoPasta raiz, int quantos) EstadoPasta.SemAcesso => "sem acesso", EstadoPasta.ErroLeitura => "erro", EstadoPasta.Link => "link", + EstadoPasta.Excluida => "excluída", _ => Formatador.Tamanho(p.Tamanho), }, Lida: p.Estado == EstadoPasta.Lida)) .ToList(); diff --git a/src/mapdisk.nucleo/relatorios/exportador-csv.cs b/src/mapdisk.nucleo/relatorios/exportador-csv.cs index bd47992..7510c74 100644 --- a/src/mapdisk.nucleo/relatorios/exportador-csv.cs +++ b/src/mapdisk.nucleo/relatorios/exportador-csv.cs @@ -48,6 +48,7 @@ private static string Linha(NoPasta no, string caminho) EstadoPasta.SemAcesso => "sem acesso", EstadoPasta.ErroLeitura => "erro de leitura", EstadoPasta.Link => "link", + EstadoPasta.Excluida => "excluída", _ => "não lida", }; diff --git a/src/mapdisk.nucleo/relatorios/pagina-avaliacao.cs b/src/mapdisk.nucleo/relatorios/pagina-avaliacao.cs index 2302d91..256a446 100644 --- a/src/mapdisk.nucleo/relatorios/pagina-avaliacao.cs +++ b/src/mapdisk.nucleo/relatorios/pagina-avaliacao.cs @@ -94,6 +94,11 @@ private static void Resumo(StringBuilder s, Avaliacao a) s.Append($" {H(Formatador.Plural(semLeitura, "pasta não pôde ser lida e não entra", "pastas não puderam ser lidas e não entram"))} nesta conta."); } + if (p.PastasExcluidas > 0) + { + s.Append($" {H(Formatador.Plural(p.PastasExcluidas, "pasta foi excluída da varredura pelo técnico e não entra", "pastas foram excluídas da varredura pelo técnico e não entram"))} nesta conta."); + } + s.Append("

Maiores pastas

    "); foreach (var sub in p.Subpastas.Where(x => x.Estado == EstadoPasta.Lida).OrderByDescending(x => x.Tamanho).Take(5)) { diff --git a/src/mapdisk.nucleo/relatorios/relatorio-tecnico.cs b/src/mapdisk.nucleo/relatorios/relatorio-tecnico.cs index ee9b08e..5334999 100644 --- a/src/mapdisk.nucleo/relatorios/relatorio-tecnico.cs +++ b/src/mapdisk.nucleo/relatorios/relatorio-tecnico.cs @@ -90,6 +90,11 @@ private static void Resumo(StringBuilder s, DadosRelatorio d) s.Append($"

    {H(Formatador.Plural(semLeitura, "pasta não pôde ser lida e não entra nesta conta", "pastas não puderam ser lidas e não entram nesta conta"))}. "); s.Append("O botão \"Varrer como administrador\" lê as pastas locais que ficaram sem acesso.

    "); } + + if (p.PastasExcluidas > 0) + { + s.Append($"

    {H(Formatador.Plural(p.PastasExcluidas, "pasta excluída da varredura não entra nesta conta", "pastas excluídas da varredura não entram nesta conta"))}.

    "); + } } private static void Grafico(StringBuilder s, NoPasta p) @@ -132,7 +137,7 @@ private static void MaioresPastas(StringBuilder s, NoPasta p, int quantos) { s.Append("

    Maiores pastas

    "); var lidas = p.Subpastas.Where(x => x.Estado == EstadoPasta.Lida).OrderByDescending(x => x.Tamanho).Take(quantos); - var semLeitura = p.Subpastas.Where(x => x.Estado is EstadoPasta.SemAcesso or EstadoPasta.ErroLeitura or EstadoPasta.Pendente); + var semLeitura = p.Subpastas.Where(x => x.Estado is EstadoPasta.SemAcesso or EstadoPasta.ErroLeitura or EstadoPasta.Pendente or EstadoPasta.Excluida); foreach (var x in lidas) { s.Append($""); @@ -146,9 +151,10 @@ private static void MaioresPastas(StringBuilder s, NoPasta p, int quantos) { EstadoPasta.SemAcesso => "sem acesso", EstadoPasta.ErroLeitura => "erro de leitura", + EstadoPasta.Excluida => $"excluída da varredura (regra: {x.Motivo})", _ => "não lida", }; - s.Append($""); + s.Append($""); } s.Append("
    PastaTamanho% da pastaArquivosÚltima alteração
    {H(x.Nome)}{H(Formatador.Tamanho(x.Tamanho))}
    {H(x.Nome)}{estado}
    {H(x.Nome)}{H(estado)}
    "); diff --git a/testes/mapdisk.testes/argumentos-testes.cs b/testes/mapdisk.testes/argumentos-testes.cs index 3e6b1c8..541911c 100644 --- a/testes/mapdisk.testes/argumentos-testes.cs +++ b/testes/mapdisk.testes/argumentos-testes.cs @@ -69,7 +69,7 @@ public void Le_o_relatorio_e_exige_html() { Assert.Equal("d.html", ArgumentosCli.Interpretar(["varrer", "D:", "--relatorio", "d.html"]).Relatorio); Assert.Contains("O arquivo de --relatorio tem que terminar em .html.", ArgumentosCli.Interpretar(["varrer", "D:", "--relatorio", "d.txt"]).Erros); - Assert.Contains("As opções --csv, --relatorio e --top pedem o comando varrer.", ArgumentosCli.Interpretar(["--relatorio", "d.html"]).Erros); + Assert.Contains("As opções --csv, --relatorio, --top e --excluir pedem o comando varrer.", ArgumentosCli.Interpretar(["--relatorio", "d.html"]).Erros); } [Fact] @@ -88,4 +88,12 @@ public void Ajuda_lista_todas_as_opcoes() Assert.Contains(opcao, ArgumentosCli.TextoAjuda); } } + + [Fact] + public void Le_excluir_repetido_e_recusa_regra_invalida() + { + var a = ArgumentosCli.Interpretar(["varrer", "D:", "--excluir", "node_modules", "--excluir", @"D:\Backup"]); + Assert.Equal(["node_modules", @"D:\Backup"], a.Excluir); + Assert.Contains("--excluir *.tmp: Use o nome da pasta ou o caminho completo, sem * nem ?", ArgumentosCli.Interpretar(["varrer", "D:", "--excluir", "*.tmp"]).Erros); + } } diff --git a/testes/mapdisk.testes/executor-cli-testes.cs b/testes/mapdisk.testes/executor-cli-testes.cs index de4d63d..978ed7c 100644 --- a/testes/mapdisk.testes/executor-cli-testes.cs +++ b/testes/mapdisk.testes/executor-cli-testes.cs @@ -151,4 +151,18 @@ public void Integrar_e_remover_mexem_so_nas_chaves() ExecutorCli.Executar(ArgumentosCli.Interpretar(["--remover-integracao"]), Demonstracao.Motor(), s, new StringWriter(), CancellationToken.None, chaves, @"E:\mapdisk.exe"); Assert.Empty(chaves.Todas); } + + [Fact] + public void Excluir_deixa_a_pasta_fora_e_avisa() + { + using var p = new PastaTeste(); + p.Arquivo(@"site\index.html", 1000); + p.Arquivo(@"node_modules\a.js", 50_000); + + var codigo = Rodar(new MotorVarredura(), out var saida, out _, "varrer", p.Raiz, "--excluir", "node_modules"); + + Assert.Equal(ExecutorCli.CodigoSucesso, codigo); + Assert.Contains("Atenção: 1 pasta excluída da varredura, fora do total.", saida); + Assert.Contains(saida.Split(Environment.NewLine), l => l.Contains("excluída") && l.Contains("node_modules")); + } } diff --git a/testes/mapdisk.testes/regras-exclusao-testes.cs b/testes/mapdisk.testes/regras-exclusao-testes.cs index 357695a..52af090 100644 --- a/testes/mapdisk.testes/regras-exclusao-testes.cs +++ b/testes/mapdisk.testes/regras-exclusao-testes.cs @@ -145,4 +145,21 @@ private static NoPasta ArvoreComExcluida() modulos.MarcarExcluida("node_modules"); return raiz; } + + [Fact] + public void Csv_e_relatorios_citam_a_pasta_excluida() + { + var raiz = new NoPasta(@"D:\", null); + var modulos = new NoPasta("node_modules", raiz); + raiz.Preencher([], [modulos]); + modulos.MarcarExcluida("node_modules"); + + var csv = new StringWriter(); + ExportadorCsv.Gravar(raiz, csv); + Assert.Contains(@"D:\node_modules;excluída;", csv.ToString()); + + var html = System.Net.WebUtility.HtmlDecode(RelatorioTecnico.Gerar(new DadosRelatorio(raiz, DateTime.Now, "PC", null, false, 10, null))); + Assert.Contains("1 pasta excluída da varredura não entra nesta conta", html); + Assert.Contains("excluída da varredura (regra: node_modules)", html); + } } From 1002da8d376b6897ebcefff71b23761bc07e1c82 Mon Sep 17 00:00:00 2001 From: Manfred Heil Junior Date: Thu, 1 Oct 2026 09:47:23 -0300 Subject: [PATCH 5/6] Liga as pastas excluidas na tela de Opcoes O tecnico acrescenta por nome, por caminho ou escolhendo a pasta, e tira da lista. A regra invalida diz o motivo, e a lista grava na hora e vale na proxima varredura. O grafico mostra as excluidas abaixo das sem leitura. Autores: Manfred Heil Junior --- src/mapdisk/janela-opcoes.xaml | 23 +++++++++++ src/mapdisk/janela-opcoes.xaml.cs | 51 ++++++++++++++++++++++++ src/mapdisk/janela-principal.xaml | 2 + testes/mapdisk.testes/recursos-testes.cs | 13 ++++++ 4 files changed, 89 insertions(+) diff --git a/src/mapdisk/janela-opcoes.xaml b/src/mapdisk/janela-opcoes.xaml index 86a4f38..b30ecff 100644 --- a/src/mapdisk/janela-opcoes.xaml +++ b/src/mapdisk/janela-opcoes.xaml @@ -53,6 +53,29 @@ + + + + + + + + + + + +