From 6fbb040be2cbabe39581c1c49b55ce12b0afd932 Mon Sep 17 00:00:00 2001
From: Manfred Heil Junior
Date: Thu, 1 Oct 2026 02:13:15 -0300
Subject: [PATCH 1/8] Guarda as opcoes do tecnico e esquece alvos
Quantidade de maiores arquivos, minimo dos duplicados e criterios do Sugerir passam a valer entre sessoes, num arquivo do perfil. Fecha as pendencias das fatias 3, 6 e 7.
Autores: Manfred Heil Junior
---
.../painel/dependencias-painel.cs | 3 +
src/mapdisk.nucleo/painel/painel-analises.cs | 6 +-
src/mapdisk.nucleo/painel/painel-principal.cs | 42 ++++++
src/mapdisk.nucleo/painel/preferencias.cs | 130 ++++++++++++++++++
src/mapdisk.nucleo/painel/ultimos-alvos.cs | 3 +
testes/mapdisk.testes/preferencias-testes.cs | 96 +++++++++++++
6 files changed, 279 insertions(+), 1 deletion(-)
create mode 100644 src/mapdisk.nucleo/painel/preferencias.cs
create mode 100644 testes/mapdisk.testes/preferencias-testes.cs
diff --git a/src/mapdisk.nucleo/painel/dependencias-painel.cs b/src/mapdisk.nucleo/painel/dependencias-painel.cs
index 6a0d1ee..a6ee175 100644
--- a/src/mapdisk.nucleo/painel/dependencias-painel.cs
+++ b/src/mapdisk.nucleo/painel/dependencias-painel.cs
@@ -21,6 +21,9 @@ public sealed class DependenciasPainel
/// Leitura de conteúdo para os duplicados. A demonstração troca por uma que não abre arquivo.
public ILeitorConteudo Leitor { get; init; } = new LeitorConteudo();
+ /// 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();
+
public IRegistroAcoes Registro { get; init; } = new RegistroEmMemoria();
public LocaisProtegidos Locais { get; init; } = LocaisProtegidos.DoSistema();
diff --git a/src/mapdisk.nucleo/painel/painel-analises.cs b/src/mapdisk.nucleo/painel/painel-analises.cs
index 420aa64..bdf8e29 100644
--- a/src/mapdisk.nucleo/painel/painel-analises.cs
+++ b/src/mapdisk.nucleo/painel/painel-analises.cs
@@ -30,6 +30,9 @@ public sealed class PainelAnalises : INotifyPropertyChanged
/// A aba Duplicados. O resultado diz de qual pasta é e só some numa nova varredura.
public PainelDuplicados Duplicados { get; }
+ /// Quantos arquivos a aba "Maiores arquivos" mostra. Vem das Opções.
+ public int QuantosMaiores { get; set; } = 100;
+
public event PropertyChangedEventHandler? PropertyChanged;
public AbaAnalise Aba { get; private set; }
@@ -80,8 +83,9 @@ public async Task CalcularAsync(NoPasta pasta, DateTime hoje)
{
_pasta = pasta;
var idade = Idade;
+ var quantos = QuantosMaiores;
var r = await Task.Run(() => (
- Maiores: Analises.MaioresArquivos(pasta),
+ Maiores: Analises.MaioresArquivos(pasta, quantos),
Antigos: Analises.ArquivosAntigos(pasta, idade, hoje),
Categorias: Analises.PorCategoria(pasta),
Extensoes: Analises.PorExtensao(pasta),
diff --git a/src/mapdisk.nucleo/painel/painel-principal.cs b/src/mapdisk.nucleo/painel/painel-principal.cs
index 4eedf5c..a1014f0 100644
--- a/src/mapdisk.nucleo/painel/painel-principal.cs
+++ b/src/mapdisk.nucleo/painel/painel-principal.cs
@@ -19,6 +19,7 @@ public sealed class PainelPrincipal : INotifyPropertyChanged
private readonly Func> _listarUnidades;
private readonly IHistoricoAlvos _historico;
private readonly Func _elevar;
+ private readonly IArmazemPreferencias _preferencias;
private CancellationTokenSource? _cancelar;
private Varredura? _varredura;
private InfoVolume? _volume;
@@ -36,6 +37,8 @@ public PainelPrincipal(DependenciasPainel dependencias)
LocalDoRegistro = dependencias.Registro.Local;
Locais = dependencias.Locais;
Analises = new PainelAnalises(dependencias.Leitor);
+ _preferencias = dependencias.Preferencias;
+ Aplicar(_preferencias.Ler());
Unidades = _listarUnidades();
TextoAlvo = Unidades.FirstOrDefault()?.Raiz ?? string.Empty;
MontarOpcoes();
@@ -57,8 +60,19 @@ public PainelPrincipal(IMotorVarredura motor, Func> li
Elevar = Elevacao.Reabrir,
Operacoes = new OperacoesArquivo(),
Registro = RegistroAcoes.Padrao(),
+ Preferencias = ArquivoPreferencias.Padrao(),
});
+ public Preferencias Preferencias { get; private set; } = new();
+
+ /// Volume da última varredura, para o relatório do técnico.
+ public InfoVolume? Volume => _volume;
+
+ /// A última varredura foi interrompida antes do fim.
+ public bool Interrompida { get; private set; }
+
+ public IReadOnlyList UltimosUsados => _historico.Ler();
+
/// O processo já roda como administrador.
public bool Administrador { get; }
@@ -381,6 +395,7 @@ private void Concluir()
{
var r = varredura.Conclusao.Result;
_volume = r.Volume;
+ Interrompida = r.Cancelada;
TextoEstado = r.Cancelada
? $"Varredura interrompida em {Formatador.Duracao(r.Duracao)}. Os números mostram só o que foi lido até ali."
: $"Varredura concluída em {Formatador.Duracao(r.Duracao)}.";
@@ -430,6 +445,33 @@ private void AtualizarTextos()
Avisar();
}
+ public void GravarPreferencias(Preferencias preferencias)
+ {
+ _preferencias.Gravar(preferencias);
+ Aplicar(preferencias);
+ }
+
+ public void EsquecerAlvo(string alvo)
+ {
+ _historico.Gravar(UltimosAlvos.Esquecer(_historico.Ler(), alvo));
+ MontarOpcoes();
+ Avisar();
+ }
+
+ public void LimparAlvos()
+ {
+ _historico.Gravar([]);
+ MontarOpcoes();
+ Avisar();
+ }
+
+ private void Aplicar(Preferencias p)
+ {
+ Preferencias = p;
+ Analises.QuantosMaiores = p.MaioresArquivos;
+ Analises.Duplicados.TamanhoMinimoMb = p.DuplicadosMinimoMb;
+ }
+
private void MontarOpcoes()
{
var unidades = Unidades.Select(u => new ItemAlvo(u.Raiz, u.Descricao)).ToList();
diff --git a/src/mapdisk.nucleo/painel/preferencias.cs b/src/mapdisk.nucleo/painel/preferencias.cs
new file mode 100644
index 0000000..4fdfe6d
--- /dev/null
+++ b/src/mapdisk.nucleo/painel/preferencias.cs
@@ -0,0 +1,130 @@
+using System.Globalization;
+
+namespace MapDisk.Nucleo;
+
+/// Escolhas do técnico que valem entre sessões, só nesta máquina.
+public sealed record Preferencias(
+ int MaioresArquivos = 100,
+ long DuplicadosMinimoMb = 1,
+ int SugerirPastas = 20,
+ int SugerirArquivos = 50,
+ int SugerirAnos = 2,
+ long SugerirMinimoMb = 100)
+{
+ private const long Mb = 1024 * 1024;
+
+ private static readonly Dictionary Faixas = new()
+ {
+ ["maiores-arquivos"] = (10, 1000),
+ ["duplicados-minimo-mb"] = (1, 102400),
+ ["sugerir-pastas"] = (0, 1000),
+ ["sugerir-arquivos"] = (0, 1000),
+ ["sugerir-anos"] = (1, 50),
+ ["sugerir-minimo-mb"] = (0, 1048576),
+ };
+
+ public CriteriosSugestao Criterios => new(SugerirPastas, SugerirArquivos, SugerirAnos, SugerirMinimoMb * Mb);
+
+ public Preferencias ComCriterios(CriteriosSugestao c) => this with
+ {
+ SugerirPastas = c.MaioresPastas,
+ SugerirArquivos = c.MaioresArquivos,
+ SugerirAnos = c.AnosSemAlteracao,
+ SugerirMinimoMb = c.TamanhoMinimo / Mb,
+ };
+
+ public IReadOnlyList ParaTexto() =>
+ [
+ $"maiores-arquivos={N(MaioresArquivos)}",
+ $"duplicados-minimo-mb={N(DuplicadosMinimoMb)}",
+ $"sugerir-pastas={N(SugerirPastas)}",
+ $"sugerir-arquivos={N(SugerirArquivos)}",
+ $"sugerir-anos={N(SugerirAnos)}",
+ $"sugerir-minimo-mb={N(SugerirMinimoMb)}",
+ ];
+
+ /// Linha que não se entende fica de fora; valor fora da faixa fica com o padrão.
+ public static Preferencias DeTexto(IEnumerable linhas)
+ {
+ var valores = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ foreach (var linha in linhas)
+ {
+ var i = linha.IndexOf('=');
+ if (i > 0)
+ {
+ valores[linha[..i].Trim()] = linha[(i + 1)..].Trim();
+ }
+ }
+
+ var p = new Preferencias();
+ return p with
+ {
+ MaioresArquivos = (int)Ler(valores, "maiores-arquivos", p.MaioresArquivos),
+ DuplicadosMinimoMb = Ler(valores, "duplicados-minimo-mb", p.DuplicadosMinimoMb),
+ SugerirPastas = (int)Ler(valores, "sugerir-pastas", p.SugerirPastas),
+ SugerirArquivos = (int)Ler(valores, "sugerir-arquivos", p.SugerirArquivos),
+ SugerirAnos = (int)Ler(valores, "sugerir-anos", p.SugerirAnos),
+ SugerirMinimoMb = Ler(valores, "sugerir-minimo-mb", p.SugerirMinimoMb),
+ };
+ }
+
+ /// Confere um valor digitado na tela de Opções.
+ public static bool Valido(string nome, string texto) =>
+ Faixas.TryGetValue(nome, out var f)
+ && long.TryParse(texto.Trim(), NumberStyles.None, CultureInfo.InvariantCulture, out var n)
+ && n >= f.Minimo && n <= f.Maximo;
+
+ private static long Ler(Dictionary valores, string nome, long padrao) =>
+ valores.TryGetValue(nome, out var texto) && Valido(nome, texto)
+ ? long.Parse(texto.Trim(), CultureInfo.InvariantCulture)
+ : padrao;
+
+ private static string N(long n) => n.ToString(CultureInfo.InvariantCulture);
+}
+
+public interface IArmazemPreferencias
+{
+ Preferencias Ler();
+
+ void Gravar(Preferencias preferencias);
+}
+
+/// Opções num arquivo do perfil do usuário. Falha de leitura ou de gravação deixa os padrões.
+public sealed class ArquivoPreferencias(string arquivo) : IArmazemPreferencias
+{
+ public static ArquivoPreferencias Padrao() => new(Path.Combine(
+ Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "MapDisk", "opcoes.txt"));
+
+ public Preferencias Ler()
+ {
+ try
+ {
+ return File.Exists(arquivo) ? Preferencias.DeTexto(File.ReadAllLines(arquivo)) : new Preferencias();
+ }
+ catch (Exception e) when (e is IOException or UnauthorizedAccessException)
+ {
+ return new Preferencias();
+ }
+ }
+
+ public void Gravar(Preferencias preferencias)
+ {
+ try
+ {
+ Directory.CreateDirectory(Path.GetDirectoryName(arquivo)!);
+ File.WriteAllLines(arquivo, preferencias.ParaTexto());
+ }
+ catch (Exception e) when (e is IOException or UnauthorizedAccessException)
+ {
+ }
+ }
+}
+
+public sealed class PreferenciasEmMemoria : IArmazemPreferencias
+{
+ private Preferencias _atual = new();
+
+ public Preferencias Ler() => _atual;
+
+ public void Gravar(Preferencias preferencias) => _atual = preferencias;
+}
diff --git a/src/mapdisk.nucleo/painel/ultimos-alvos.cs b/src/mapdisk.nucleo/painel/ultimos-alvos.cs
index f4a7d4b..6a7bbf0 100644
--- a/src/mapdisk.nucleo/painel/ultimos-alvos.cs
+++ b/src/mapdisk.nucleo/painel/ultimos-alvos.cs
@@ -64,4 +64,7 @@ public static IReadOnlyList Acrescentar(IReadOnlyList atuais, st
.Concat(atuais.Where(a => !string.Equals(a, alvo, StringComparison.OrdinalIgnoreCase)))
.Take(Maximo)
.ToList();
+
+ public static IReadOnlyList Esquecer(IReadOnlyList atuais, string alvo) =>
+ atuais.Where(a => !string.Equals(a, alvo, StringComparison.OrdinalIgnoreCase)).ToList();
}
diff --git a/testes/mapdisk.testes/preferencias-testes.cs b/testes/mapdisk.testes/preferencias-testes.cs
new file mode 100644
index 0000000..6971924
--- /dev/null
+++ b/testes/mapdisk.testes/preferencias-testes.cs
@@ -0,0 +1,96 @@
+namespace MapDisk.Testes;
+
+public class PreferenciasTestes
+{
+ [Fact]
+ public void Texto_vai_e_volta_igual()
+ {
+ var p = new Preferencias(MaioresArquivos: 250, DuplicadosMinimoMb: 10, SugerirPastas: 5, SugerirArquivos: 7, SugerirAnos: 3, SugerirMinimoMb: 500);
+ Assert.Equal(p, Preferencias.DeTexto(p.ParaTexto()));
+ }
+
+ [Fact]
+ public void Valor_estranho_ou_fora_da_faixa_volta_ao_padrao()
+ {
+ var p = Preferencias.DeTexto(["maiores-arquivos=5", "duplicados-minimo-mb=abc", "sugerir-anos=0", "outra=1", "sem igual", "sugerir-pastas=30"]);
+ Assert.Equal(100, p.MaioresArquivos);
+ Assert.Equal(1, p.DuplicadosMinimoMb);
+ Assert.Equal(2, p.SugerirAnos);
+ Assert.Equal(30, p.SugerirPastas);
+ Assert.True(Preferencias.Valido("maiores-arquivos", " 50 "));
+ Assert.False(Preferencias.Valido("maiores-arquivos", "2000"));
+ Assert.False(Preferencias.Valido("nao-existe", "1"));
+ }
+
+ [Fact]
+ public void Criterios_saem_em_bytes_e_voltam()
+ {
+ var p = new Preferencias(SugerirMinimoMb: 200);
+ Assert.Equal(200L * 1024 * 1024, p.Criterios.TamanhoMinimo);
+ var c = new CriteriosSugestao(1, 2, 3, 4L * 1024 * 1024);
+ Assert.Equal(c, p.ComCriterios(c).Criterios);
+ }
+
+ [Fact]
+ public void Arquivo_grava_le_e_tolera_falta()
+ {
+ var arquivo = Path.Combine(AppContext.BaseDirectory, "preferencias-" + Guid.NewGuid().ToString("N"), "opcoes.txt");
+ var armazem = new ArquivoPreferencias(arquivo);
+ Assert.Equal(new Preferencias(), armazem.Ler());
+ armazem.Gravar(new Preferencias(MaioresArquivos: 300));
+ Assert.Equal(300, armazem.Ler().MaioresArquivos);
+ Directory.Delete(Path.GetDirectoryName(arquivo)!, true);
+ }
+
+ [Fact]
+ public void Esquecer_tira_so_aquele_alvo()
+ {
+ Assert.Equal([@"D:\"], UltimosAlvos.Esquecer([@"C:\Dados", @"D:\"], @"c:\dados"));
+ }
+
+ [Fact]
+ public void Painel_aplica_e_grava_as_preferencias()
+ {
+ var armazem = new PreferenciasEmMemoria();
+ armazem.Gravar(new Preferencias(MaioresArquivos: 20, DuplicadosMinimoMb: 8));
+ var painel = new PainelPrincipal(new DependenciasPainel
+ {
+ Motor = Demonstracao.Motor(),
+ ListarUnidades = () => [],
+ Preferencias = armazem,
+ });
+ Assert.Equal(20, painel.Analises.QuantosMaiores);
+ Assert.Equal(8, painel.Analises.Duplicados.TamanhoMinimoMb);
+
+ painel.GravarPreferencias(new Preferencias(MaioresArquivos: 40));
+ Assert.Equal(40, armazem.Ler().MaioresArquivos);
+ Assert.Equal(40, painel.Analises.QuantosMaiores);
+ }
+
+ [Fact]
+ public void Painel_esquece_e_limpa_os_alvos()
+ {
+ var historico = new HistoricoEmMemoria();
+ historico.Gravar([@"E:\Fotos", @"F:\"]);
+ var painel = new PainelPrincipal(new DependenciasPainel
+ {
+ Motor = Demonstracao.Motor(),
+ ListarUnidades = () => [],
+ Historico = historico,
+ });
+ painel.EsquecerAlvo(@"E:\Fotos");
+ Assert.Equal([@"F:\"], painel.UltimosUsados);
+ Assert.DoesNotContain(painel.Opcoes, o => o.Caminho == @"E:\Fotos");
+ painel.LimparAlvos();
+ Assert.Empty(painel.UltimosUsados);
+ }
+
+ [Fact]
+ public async Task Analises_usam_a_quantidade_de_maiores()
+ {
+ var raiz = (await Demonstracao.Motor().Iniciar(@"C:\", CancellationToken.None).Conclusao).Raiz;
+ var analises = new PainelAnalises { QuantosMaiores = 3 };
+ await analises.CalcularAsync(raiz, new DateTime(2026, 10, 1));
+ Assert.Equal(3, analises.Maiores.Count);
+ }
+}
From 22955d398cd95f9560d970a90f144f6737ce5fbd Mon Sep 17 00:00:00 2001
From: Manfred Heil Junior
Date: Thu, 1 Oct 2026 02:14:40 -0300
Subject: [PATCH 2/8] Cria o relatorio HTML do tecnico
R17: um arquivo so, com o logo da MT e o grafico em blocos dentro, imprimivel em PDF pelo navegador. Pasta sem leitura aparece com o motivo, nunca com zero. Os ajudantes de HTML e as cores do grafico passam a ser comuns aos dois relatorios e a tela.
Autores: Manfred Heil Junior
---
src/mapdisk.nucleo/grafico/paleta.cs | 13 +
src/mapdisk.nucleo/painel/painel-grafico.cs | 15 +-
src/mapdisk.nucleo/relatorios/html.cs | 20 ++
.../relatorios/pagina-avaliacao.cs | 15 +-
.../relatorios/relatorio-tecnico.cs | 224 ++++++++++++++++++
.../relatorio-tecnico-testes.cs | 83 +++++++
6 files changed, 342 insertions(+), 28 deletions(-)
create mode 100644 src/mapdisk.nucleo/relatorios/html.cs
create mode 100644 src/mapdisk.nucleo/relatorios/relatorio-tecnico.cs
create mode 100644 testes/mapdisk.testes/relatorio-tecnico-testes.cs
diff --git a/src/mapdisk.nucleo/grafico/paleta.cs b/src/mapdisk.nucleo/grafico/paleta.cs
index f4c8879..46d4bb3 100644
--- a/src/mapdisk.nucleo/grafico/paleta.cs
+++ b/src/mapdisk.nucleo/grafico/paleta.cs
@@ -26,4 +26,17 @@ public static class Paleta
/// Grafite sobre as cores claras, branco sobre as escuras.
public static string CorTexto(string cor) => Claras.Contains(cor) ? "#202020" : "#FFFFFF";
+
+ /// As subpastas contam a posição entre elas, para a primeira ser sempre o verde da MT.
+ public static string[] CoresDe(IReadOnlyList itens)
+ {
+ var cores = new string[itens.Count];
+ var pasta = 0;
+ for (var i = 0; i < itens.Count; i++)
+ {
+ cores[i] = Cor(itens[i].Tipo == TipoItemGrafico.Pasta ? pasta++ : 0, itens[i].Tipo);
+ }
+
+ return cores;
+ }
}
diff --git a/src/mapdisk.nucleo/painel/painel-grafico.cs b/src/mapdisk.nucleo/painel/painel-grafico.cs
index b2f0f1b..7201776 100644
--- a/src/mapdisk.nucleo/painel/painel-grafico.cs
+++ b/src/mapdisk.nucleo/painel/painel-grafico.cs
@@ -89,7 +89,7 @@ private void Montar()
{
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)}";
- var cores = Cores(c.Itens);
+ var cores = Paleta.CoresDe(c.Itens);
if (Tipo == TipoGrafico.Blocos)
{
var r = Treemap.Dispor(c.Itens.Select(i => i.Valor).ToList(), _largura, _altura);
@@ -116,19 +116,6 @@ private void Montar()
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(string.Empty));
}
- // As subpastas contam a posição entre elas, para a primeira ser sempre o verde da MT.
- private static string[] Cores(IReadOnlyList itens)
- {
- var cores = new string[itens.Count];
- var pasta = 0;
- for (var i = 0; i < itens.Count; i++)
- {
- cores[i] = Paleta.Cor(itens[i].Tipo == TipoItemGrafico.Pasta ? pasta++ : 0, itens[i].Tipo);
- }
-
- return cores;
- }
-
private string TextoValor(long valor) => Modo == ModoExibicao.Contagem
? Formatador.Plural(valor, "arquivo", "arquivos")
: Formatador.Tamanho(valor);
diff --git a/src/mapdisk.nucleo/relatorios/html.cs b/src/mapdisk.nucleo/relatorios/html.cs
new file mode 100644
index 0000000..f8d8b5e
--- /dev/null
+++ b/src/mapdisk.nucleo/relatorios/html.cs
@@ -0,0 +1,20 @@
+using System.Net;
+
+namespace MapDisk.Nucleo;
+
+/// O que os dois relatórios HTML usam: escape e o logo da MT embutido.
+public static class Html
+{
+ public static string H(string texto) => WebUtility.HtmlEncode(texto);
+
+ /// Texto dentro de aspas no JavaScript: barra, aspas e o fim de script escapados.
+ public static string Js(string texto) => texto.Replace(@"\", @"\\").Replace("\"", "\\\"").Replace("<", "\\u003c");
+
+ public static string Logo()
+ {
+ using var fluxo = typeof(Html).Assembly.GetManifestResourceStream("mt-logo.png")!;
+ using var memoria = new MemoryStream();
+ fluxo.CopyTo(memoria);
+ return Convert.ToBase64String(memoria.ToArray());
+ }
+}
diff --git a/src/mapdisk.nucleo/relatorios/pagina-avaliacao.cs b/src/mapdisk.nucleo/relatorios/pagina-avaliacao.cs
index 6409400..2302d91 100644
--- a/src/mapdisk.nucleo/relatorios/pagina-avaliacao.cs
+++ b/src/mapdisk.nucleo/relatorios/pagina-avaliacao.cs
@@ -1,6 +1,6 @@
using System.Globalization;
-using System.Net;
using System.Text;
+using static MapDisk.Nucleo.Html;
namespace MapDisk.Nucleo;
@@ -110,17 +110,4 @@ private static void Resumo(StringBuilder s, Avaliacao a)
s.Append($"Sem alteração há mais de 2 anos {H(Formatador.Plural(antigos.Quantidade, "arquivo", "arquivos"))}, ");
s.Append($"{H(Formatador.Tamanho(antigos.Tamanho))}
");
}
-
- private static string H(string texto) => WebUtility.HtmlEncode(texto);
-
- // Texto dentro de aspas no JavaScript: barra, aspas e o fim de script escapados.
- private static string Js(string texto) => texto.Replace(@"\", @"\\").Replace("\"", "\\\"").Replace("<", "\\u003c");
-
- private static string Logo()
- {
- using var fluxo = typeof(PaginaAvaliacao).Assembly.GetManifestResourceStream("mt-logo.png")!;
- using var memoria = new MemoryStream();
- fluxo.CopyTo(memoria);
- return Convert.ToBase64String(memoria.ToArray());
- }
}
diff --git a/src/mapdisk.nucleo/relatorios/relatorio-tecnico.cs b/src/mapdisk.nucleo/relatorios/relatorio-tecnico.cs
new file mode 100644
index 0000000..65b75b4
--- /dev/null
+++ b/src/mapdisk.nucleo/relatorios/relatorio-tecnico.cs
@@ -0,0 +1,224 @@
+using System.Globalization;
+using System.Text;
+using static MapDisk.Nucleo.Html;
+
+namespace MapDisk.Nucleo;
+
+public sealed record DadosRelatorio(NoPasta Pasta, DateTime Gerado, string Computador, InfoVolume? Volume, bool Interrompida, int Quantos, ResultadoDuplicados? Duplicados);
+
+///
+/// Relatório do técnico (R17): um arquivo HTML só, com logo, estilo e gráfico dentro, sem nada
+/// da internet (regra 6). Pasta sem leitura aparece com o motivo, nunca com zero (regra 3).
+///
+public static class RelatorioTecnico
+{
+ private const int LarguraGrafico = 960;
+ private const int AlturaGrafico = 360;
+ private const int MaximoNoGrafico = 20;
+
+ private const string Estilo =
+ "body{font-family:Segoe UI,Arial,sans-serif;color:#202020;background:#F4F4F4;margin:0}" +
+ "header{display:flex;gap:16px;align-items:center;background:#006B2D;color:#fff;padding:12px 20px}" +
+ "header img{height:56px;background:#fff;border-radius:8px;padding:4px}header h1{margin:0;font-size:22px}header p{margin:4px 0 0}" +
+ "main{max-width:1100px;margin:0 auto;padding:16px 20px}h2{color:#006B2D;border-left:4px solid #43A92C;padding-left:8px}" +
+ ".aviso{background:#fff4d6;border:1px solid #e0b100;padding:8px 12px}" +
+ "table{width:100%;border-collapse:collapse;background:#fff}th,td{border:1px solid #ddd;padding:6px;font-size:13px;vertical-align:top}" +
+ "th{background:#eef6ea;text-align:left}td.n{text-align:right;white-space:nowrap}svg{max-width:100%;height:auto;background:#fff}" +
+ ".cor{display:inline-block;width:12px;height:12px;margin-right:6px;vertical-align:middle}" +
+ "footer{margin-top:24px;font-size:12px;color:#666}" +
+ "@media print{body{background:#fff}header{background:#fff;color:#006B2D;border-bottom:2px solid #006B2D}tr{page-break-inside:avoid}svg{page-break-inside:avoid}}";
+
+ public static void Gravar(DadosRelatorio dados, string arquivo) =>
+ File.WriteAllText(arquivo, Gerar(dados), new UTF8Encoding(false));
+
+ public static string Gerar(DadosRelatorio d)
+ {
+ var p = d.Pasta;
+ var caminho = p.CaminhoCompleto();
+ var s = new StringBuilder();
+ s.Append(" ");
+ s.Append(" ");
+ s.Append($"Espaço em disco - {H(caminho)} ");
+ s.Append($"");
+ if (d.Interrompida)
+ {
+ s.Append("A varredura foi interrompida antes do fim. Os números mostram só o que foi lido até ali.
");
+ }
+
+ Resumo(s, d);
+ Grafico(s, p);
+ MaioresPastas(s, p, d.Quantos);
+ Arquivos(s, "Maiores arquivos", Analises.MaioresArquivos(p, d.Quantos));
+ Tipos(s, p);
+ var antigos = Analises.ArquivosAntigos(p, Idade.DoisAnos, d.Gerado, d.Quantos);
+ s.Append($"Sem alteração há mais de 2 anos {H(Formatador.Plural(antigos.Quantidade, "arquivo", "arquivos"))}, {H(Formatador.Tamanho(antigos.Tamanho))}.
");
+ Arquivos(s, null, antigos.Maiores);
+ Usuarios(s, p);
+ if (d.Duplicados is { } dup)
+ {
+ Duplicados(s, dup, d.Quantos);
+ }
+
+ s.Append($"Gerado pelo MapDisk - MT {H(ExecutorCli.Versao)}, da MT - Manfred Tecnologia (www.manfred.com.br ). ");
+ s.Append(" ");
+ return s.ToString();
+ }
+
+ private static void Resumo(StringBuilder s, DadosRelatorio d)
+ {
+ var p = d.Pasta;
+ s.Append($"Resumo Total lido: {H(Formatador.Tamanho(p.Tamanho))} ({H(Formatador.Tamanho(p.Alocado))} alocados) ");
+ s.Append($"em {H(Formatador.Plural(p.ArquivosTotal, "arquivo", "arquivos"))} e {H(Formatador.Plural(p.PastasTotal, "pasta", "pastas"))}.");
+ if (d.Volume is { } v)
+ {
+ s.Append($" Unidade {H(v.Raiz)}: {H(Formatador.Tamanho(v.Livre))} livres de {H(Formatador.Tamanho(v.Total))} ({H(v.SistemaArquivos)}).");
+ }
+
+ s.Append("
");
+ var semLeitura = p.PastasSemAcesso + p.PastasComErro;
+ if (semLeitura > 0)
+ {
+ 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("Varrer como administrador lê as pastas locais sem acesso.
");
+ }
+ }
+
+ private static void Grafico(StringBuilder s, NoPasta p)
+ {
+ var c = ItensGrafico.DaPasta(p, ModoExibicao.Tamanho, MaximoNoGrafico);
+ if (c.Itens.Count == 0)
+ {
+ return;
+ }
+
+ var blocos = Treemap.Dispor(c.Itens.Select(i => i.Valor).ToList(), LarguraGrafico, AlturaGrafico);
+ var cores = Paleta.CoresDe(c.Itens);
+ s.Append($"Gráfico ");
+ for (var i = 0; i < blocos.Count; i++)
+ {
+ var b = blocos[i];
+ s.Append(FormattableString.Invariant($"{H(c.Itens[i].Nome)} "));
+ if (b.Largura >= 90 && b.Altura >= 28)
+ {
+ s.Append(FormattableString.Invariant($"{H(c.Itens[i].Nome)} "));
+ }
+ }
+
+ s.Append(" Item Tamanho % da pasta ");
+ for (var i = 0; i < c.Itens.Count; i++)
+ {
+ var item = c.Itens[i];
+ s.Append($" {H(item.Nome)}{H(Formatador.Tamanho(item.Valor))} ");
+ s.Append($"{H(Formatador.Porcentagem(c.Total > 0 ? (double)item.Valor / c.Total : 0))} ");
+ }
+
+ s.Append("
");
+ if (c.NaoLidas.Count > 0)
+ {
+ s.Append($"Fora do gráfico, sem leitura: {H(string.Join(", ", c.NaoLidas))}.
");
+ }
+ }
+
+ private static void MaioresPastas(StringBuilder s, NoPasta p, int quantos)
+ {
+ s.Append("Maiores pastas Pasta Tamanho % da pasta Arquivos Última alteração ");
+ 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);
+ foreach (var x in lidas)
+ {
+ s.Append($"{H(x.Nome)} {H(Formatador.Tamanho(x.Tamanho))} ");
+ s.Append($"{H(Formatador.Porcentagem(p.Tamanho > 0 ? (double)x.Tamanho / p.Tamanho : 0))} ");
+ s.Append($"{H(Formatador.Numero(x.ArquivosTotal))} {H(Formatador.Data(x.UltimaModificacao))} ");
+ }
+
+ foreach (var x in semLeitura)
+ {
+ var estado = x.Estado switch
+ {
+ EstadoPasta.SemAcesso => "sem acesso",
+ EstadoPasta.ErroLeitura => "erro de leitura",
+ _ => "não lida",
+ };
+ s.Append($"{H(x.Nome)} {estado} ");
+ }
+
+ s.Append("
");
+ }
+
+ private static void Arquivos(StringBuilder s, string? titulo, IReadOnlyList arquivos)
+ {
+ if (titulo is not null)
+ {
+ s.Append($"{H(titulo)} ");
+ }
+
+ if (arquivos.Count == 0)
+ {
+ s.Append("Nenhum.
");
+ return;
+ }
+
+ s.Append("Arquivo Pasta Tamanho Última alteração ");
+ foreach (var a in arquivos)
+ {
+ s.Append($"{H(a.Arquivo.Nome)} {H(a.Pasta.CaminhoCompleto())} ");
+ s.Append($"{H(Formatador.Tamanho(a.Arquivo.Tamanho))} {H(Formatador.Data(a.Arquivo.Modificacao))} ");
+ }
+
+ s.Append("
");
+ }
+
+ private static void Tipos(StringBuilder s, NoPasta p)
+ {
+ s.Append("Por tipo Tipo Arquivos Tamanho % da pasta ");
+ foreach (var t in Analises.PorCategoria(p))
+ {
+ s.Append($"{H(t.Nome)} {H(Formatador.Numero(t.Quantidade))} {H(Formatador.Tamanho(t.Tamanho))} ");
+ s.Append($"{H(Formatador.Porcentagem(p.Tamanho > 0 ? (double)t.Tamanho / p.Tamanho : 0))} ");
+ }
+
+ s.Append("
");
+ }
+
+ private static void Usuarios(StringBuilder s, NoPasta p)
+ {
+ var u = Analises.PorUsuario(p);
+ if (u.PastaDePerfis is null)
+ {
+ return;
+ }
+
+ s.Append("Por usuário Perfil Tamanho ");
+ foreach (var perfil in u.Perfis)
+ {
+ var valor = perfil.Estado == EstadoPasta.Lida ? Formatador.Tamanho(perfil.Tamanho) : "sem acesso";
+ s.Append($"{H(perfil.Nome)} {H(valor)} ");
+ }
+
+ s.Append("
");
+ }
+
+ private static void Duplicados(StringBuilder s, ResultadoDuplicados r, int quantos)
+ {
+ s.Append("Duplicados ");
+ if (r.Grupos.Count == 0)
+ {
+ s.Append("Nenhum arquivo repetido acima do tamanho mínimo da busca.
");
+ return;
+ }
+
+ var copias = r.Grupos.Sum(g => g.Repetido);
+ s.Append($"{H(Formatador.Plural(r.Grupos.Count, "grupo", "grupos"))} com {H(Formatador.Tamanho(copias))} em cópias.
");
+ s.Append("Grupo Arquivo Tamanho ");
+ foreach (var g in r.Grupos.Take(quantos))
+ {
+ foreach (var a in g.Arquivos)
+ {
+ s.Append($"{g.Numero} {H(a.Caminho)} {H(Formatador.Tamanho(g.Tamanho))} ");
+ }
+ }
+
+ s.Append("
");
+ }
+}
diff --git a/testes/mapdisk.testes/relatorio-tecnico-testes.cs b/testes/mapdisk.testes/relatorio-tecnico-testes.cs
new file mode 100644
index 0000000..22e5144
--- /dev/null
+++ b/testes/mapdisk.testes/relatorio-tecnico-testes.cs
@@ -0,0 +1,83 @@
+using System.Net;
+
+namespace MapDisk.Testes;
+
+public class RelatorioTecnicoTestes
+{
+ private static NoPasta Raiz() => Demonstracao.Motor().Iniciar(@"C:\", CancellationToken.None).Conclusao.Result.Raiz;
+
+ private static DadosRelatorio Dados(NoPasta pasta, bool interrompida = false, ResultadoDuplicados? duplicados = null) =>
+ new(pasta, new DateTime(2026, 10, 1, 14, 30, 0), "ESTACAO-01", new InfoVolume(@"C:\", "Sistema", "NTFS", 50L << 30, 200L << 30, 4096), interrompida, 10, duplicados);
+
+ // O HTML codifica os acentos; os testes de texto leem o que o navegador mostra.
+ private static string Texto(DadosRelatorio dados) => WebUtility.HtmlDecode(RelatorioTecnico.Gerar(dados));
+
+ [Fact]
+ public void Tem_marca_resumo_grafico_e_listas()
+ {
+ var html = Texto(Dados(Raiz()));
+ Assert.StartsWith("", html);
+ Assert.Contains("data:image/png;base64,", html);
+ Assert.Contains("ESTACAO-01", html);
+ Assert.Contains("01/10/2026 14:30", html);
+ Assert.Contains("{titulo}", html);
+ }
+
+ Assert.Contains($"{Formatador.Tamanho(50L << 30)} livres de {Formatador.Tamanho(200L << 30)}", html);
+ }
+
+ [Fact]
+ public void Pasta_sem_acesso_nunca_vira_zero()
+ {
+ var html = Texto(Dados(Raiz()));
+ Assert.Contains("System Volume Information", html);
+ Assert.Contains("sem acesso", html);
+ Assert.Contains("não entra nesta conta", html);
+ }
+
+ [Fact]
+ public void Varredura_interrompida_avisa_no_topo()
+ {
+ var html = Texto(Dados(Raiz(), interrompida: true));
+ Assert.Contains("A varredura foi interrompida", html);
+ }
+
+ [Fact]
+ public void Nao_carrega_nada_de_fora()
+ {
+ var html = RelatorioTecnico.Gerar(Dados(Raiz()));
+ Assert.DoesNotContain("src=\"http", html);
+ Assert.DoesNotContain(" pasta", raiz)]);
+ var html = RelatorioTecnico.Gerar(Dados(raiz));
+ Assert.DoesNotContain("pasta ", html);
+ Assert.Contains("<b>pasta</b>", html);
+ }
+
+ [Fact]
+ public void Duplicados_entram_so_quando_houve_busca()
+ {
+ Assert.DoesNotContain("Duplicados ", Texto(Dados(Raiz())));
+ var vazio = new ResultadoDuplicados(Raiz(), [], 0, false);
+ Assert.Contains("Duplicados ", Texto(Dados(Raiz(), duplicados: vazio)));
+ }
+
+ [Fact]
+ public void Grava_em_utf8()
+ {
+ var arquivo = Path.Combine(AppContext.BaseDirectory, "relatorio-" + Guid.NewGuid().ToString("N") + ".html");
+ RelatorioTecnico.Gravar(Dados(Raiz()), arquivo);
+ Assert.Contains("Relatório de espaço em disco", WebUtility.HtmlDecode(File.ReadAllText(arquivo)));
+ File.Delete(arquivo);
+ }
+}
From 424371d9de9807b463bf7f7f630964c105d482e5 Mon Sep 17 00:00:00 2001
From: Manfred Heil Junior
Date: Thu, 1 Oct 2026 02:15:06 -0300
Subject: [PATCH 3/8] Cria o item Analisar com MapDisk do Explorer
R19: tres chaves em HKCU, sem administrador, que saem por completo ao desligar. O argumento --abrir corrige a aspa que o Explorer deixa na raiz da unidade.
Autores: Manfred Heil Junior
---
.../integracao/integracao-explorer.cs | 115 ++++++++++++++++++
src/mapdisk.nucleo/sistema/abertura.cs | 16 +++
testes/mapdisk.testes/abertura-testes.cs | 19 +++
.../integracao-explorer-testes.cs | 44 +++++++
4 files changed, 194 insertions(+)
create mode 100644 src/mapdisk.nucleo/integracao/integracao-explorer.cs
create mode 100644 src/mapdisk.nucleo/sistema/abertura.cs
create mode 100644 testes/mapdisk.testes/abertura-testes.cs
create mode 100644 testes/mapdisk.testes/integracao-explorer-testes.cs
diff --git a/src/mapdisk.nucleo/integracao/integracao-explorer.cs b/src/mapdisk.nucleo/integracao/integracao-explorer.cs
new file mode 100644
index 0000000..b06083c
--- /dev/null
+++ b/src/mapdisk.nucleo/integracao/integracao-explorer.cs
@@ -0,0 +1,115 @@
+using Microsoft.Win32;
+
+namespace MapDisk.Nucleo;
+
+/// Chaves do registro do usuário atual. Os testes usam a versão em memória.
+public interface IChavesUsuario
+{
+ void Gravar(string chave, string? nome, string valor);
+
+ string? Ler(string chave, string? nome);
+
+ void ApagarArvore(string chave);
+}
+
+/// HKCU de verdade. Não precisa de administrador (regra 4).
+public sealed class ChavesUsuarioWindows : IChavesUsuario
+{
+ public void Gravar(string chave, string? nome, string valor)
+ {
+ using var k = Registry.CurrentUser.CreateSubKey(chave, writable: true);
+ k.SetValue(nome ?? string.Empty, valor, RegistryValueKind.String);
+ }
+
+ public string? Ler(string chave, string? nome)
+ {
+ using var k = Registry.CurrentUser.OpenSubKey(chave);
+ return k?.GetValue(nome ?? string.Empty) as string;
+ }
+
+ public void ApagarArvore(string chave) => Registry.CurrentUser.DeleteSubKeyTree(chave, throwOnMissingSubKey: false);
+}
+
+public sealed class ChavesEmMemoria : IChavesUsuario
+{
+ private readonly Dictionary<(string Chave, string Nome), string> _valores = [];
+
+ public IReadOnlyCollection Todas => _valores.Keys.Select(k => k.Chave).Distinct().ToList();
+
+ public void Gravar(string chave, string? nome, string valor) => _valores[(chave.ToLowerInvariant(), nome ?? string.Empty)] = valor;
+
+ public string? Ler(string chave, string? nome) =>
+ _valores.TryGetValue((chave.ToLowerInvariant(), nome ?? string.Empty), out var v) ? v : null;
+
+ public void ApagarArvore(string chave)
+ {
+ var prefixo = chave.ToLowerInvariant();
+ foreach (var k in _valores.Keys.Where(k => k.Chave == prefixo || k.Chave.StartsWith(prefixo + "\\", StringComparison.Ordinal)).ToList())
+ {
+ _valores.Remove(k);
+ }
+ }
+}
+
+public enum EstadoIntegracao
+{
+ Desligada,
+ Ligada,
+
+ /// Ligada, mas apontando para outro .exe (o programa mudou de lugar).
+ OutroLocal,
+}
+
+/// Item "Analisar com MapDisk" no menu clássico do Explorer (R19). Sai por completo ao desligar (regra 5).
+public static class IntegracaoExplorer
+{
+ public const string Texto = "Analisar com MapDisk";
+
+ public static readonly string[] Chaves =
+ [
+ @"Software\Classes\Directory\shell\MapDisk",
+ @"Software\Classes\Drive\shell\MapDisk",
+ @"Software\Classes\Directory\Background\shell\MapDisk",
+ ];
+
+ public static string ComandoDe(string exe, string marcador) => $"\"{exe}\" {Abertura.Argumento} \"{marcador}\"";
+
+ public static void Ligar(IChavesUsuario chaves, string exe)
+ {
+ foreach (var chave in Chaves)
+ {
+ var marcador = chave.Contains(@"\Background\", StringComparison.Ordinal) ? "%V" : "%1";
+ chaves.Gravar(chave, null, Texto);
+ chaves.Gravar(chave, "Icon", exe + ",0");
+ chaves.Gravar(chave + @"\command", null, ComandoDe(exe, marcador));
+ }
+ }
+
+ public static void Desligar(IChavesUsuario chaves)
+ {
+ foreach (var chave in Chaves)
+ {
+ chaves.ApagarArvore(chave);
+ }
+ }
+
+ /// O .exe que o item chama hoje, ou null quando desligado.
+ public static string? ExeRegistrado(IChavesUsuario chaves)
+ {
+ var comando = chaves.Ler(Chaves[0] + @"\command", null);
+ if (comando is null || !comando.StartsWith('"'))
+ {
+ return null;
+ }
+
+ var fim = comando.IndexOf('"', 1);
+ return fim > 1 ? comando[1..fim] : null;
+ }
+
+ public static EstadoIntegracao Estado(IChavesUsuario chaves, string exe) => ExeRegistrado(chaves) switch
+ {
+ null => EstadoIntegracao.Desligada,
+ var e when string.Equals(e, exe, StringComparison.OrdinalIgnoreCase) => EstadoIntegracao.Ligada,
+ _ => EstadoIntegracao.OutroLocal,
+ };
+}
diff --git a/src/mapdisk.nucleo/sistema/abertura.cs b/src/mapdisk.nucleo/sistema/abertura.cs
new file mode 100644
index 0000000..1884687
--- /dev/null
+++ b/src/mapdisk.nucleo/sistema/abertura.cs
@@ -0,0 +1,16 @@
+namespace MapDisk.Nucleo;
+
+/// Pedido do Explorer: abrir a janela já varrendo a pasta clicada.
+public static class Abertura
+{
+ public const string Argumento = "--abrir";
+
+ /// O Explorer manda a raiz da unidade como "C:\", e o Windows lê a barra antes da aspa como aspa escapada.
+ public static string Corrigir(string alvo) => alvo.EndsWith('"') ? alvo[..^1] + "\\" : alvo;
+
+ public static bool EhPedido(IReadOnlyList args, out string? alvo)
+ {
+ alvo = args is [var a, var b] && string.Equals(a, Argumento, StringComparison.OrdinalIgnoreCase) ? Corrigir(b) : null;
+ return alvo is not null;
+ }
+}
diff --git a/testes/mapdisk.testes/abertura-testes.cs b/testes/mapdisk.testes/abertura-testes.cs
new file mode 100644
index 0000000..a06a6b6
--- /dev/null
+++ b/testes/mapdisk.testes/abertura-testes.cs
@@ -0,0 +1,19 @@
+namespace MapDisk.Testes;
+
+public class AberturaTestes
+{
+ [Theory]
+ [InlineData("C:\"", @"C:\")]
+ [InlineData(@"D:\Dados", @"D:\Dados")]
+ [InlineData(@"\\servidor\pasta", @"\\servidor\pasta")]
+ public void Corrige_a_aspa_que_o_explorer_deixa(string recebido, string esperado) =>
+ Assert.Equal(esperado, Abertura.Corrigir(recebido));
+
+ [Fact]
+ public void Reconhece_o_pedido()
+ {
+ Assert.True(Abertura.EhPedido(["--abrir", "C:\""], out var alvo));
+ Assert.Equal(@"C:\", alvo);
+ Assert.False(Abertura.EhPedido(["varrer", "C:"], out _));
+ }
+}
diff --git a/testes/mapdisk.testes/integracao-explorer-testes.cs b/testes/mapdisk.testes/integracao-explorer-testes.cs
new file mode 100644
index 0000000..ef0a64c
--- /dev/null
+++ b/testes/mapdisk.testes/integracao-explorer-testes.cs
@@ -0,0 +1,44 @@
+namespace MapDisk.Testes;
+
+public class IntegracaoExplorerTestes
+{
+ private const string Exe = @"E:\Ferramentas\mapdisk.exe";
+
+ [Fact]
+ public void Ligar_grava_as_tres_chaves_no_usuario()
+ {
+ var chaves = new ChavesEmMemoria();
+ IntegracaoExplorer.Ligar(chaves, Exe);
+ foreach (var raiz in IntegracaoExplorer.Chaves)
+ {
+ Assert.StartsWith(@"Software\Classes\", raiz);
+ Assert.Equal("Analisar com MapDisk", chaves.Ler(raiz, null));
+ Assert.Equal(Exe + ",0", chaves.Ler(raiz, "Icon"));
+ Assert.Contains("--abrir", chaves.Ler(raiz + @"\command", null));
+ }
+
+ Assert.Equal("\"" + Exe + "\" --abrir \"%V\"", chaves.Ler(@"Software\Classes\Directory\Background\shell\MapDisk\command", null));
+ Assert.Equal("\"" + Exe + "\" --abrir \"%1\"", chaves.Ler(@"Software\Classes\Drive\shell\MapDisk\command", null));
+ }
+
+ [Fact]
+ public void Desligar_tira_tudo()
+ {
+ var chaves = new ChavesEmMemoria();
+ IntegracaoExplorer.Ligar(chaves, Exe);
+ IntegracaoExplorer.Desligar(chaves);
+ Assert.Empty(chaves.Todas);
+ Assert.Equal(EstadoIntegracao.Desligada, IntegracaoExplorer.Estado(chaves, Exe));
+ }
+
+ [Fact]
+ public void Estado_reconhece_outro_exe()
+ {
+ var chaves = new ChavesEmMemoria();
+ IntegracaoExplorer.Ligar(chaves, @"F:\antigo\mapdisk.exe");
+ Assert.Equal(EstadoIntegracao.OutroLocal, IntegracaoExplorer.Estado(chaves, Exe));
+ Assert.Equal(@"F:\antigo\mapdisk.exe", IntegracaoExplorer.ExeRegistrado(chaves));
+ IntegracaoExplorer.Ligar(chaves, Exe);
+ Assert.Equal(EstadoIntegracao.Ligada, IntegracaoExplorer.Estado(chaves, Exe));
+ }
+}
From b1d6b63da7122e4cd7546361adc8a9badcb7c3ce Mon Sep 17 00:00:00 2001
From: Manfred Heil Junior
Date: Thu, 1 Oct 2026 02:15:55 -0300
Subject: [PATCH 4/8] Completa a linha de comando
R18: --relatorio grava o HTML do tecnico, --integrar e --remover-integracao ligam e desligam o item do Explorer. A linha de comando segue sem apagar nem mover arquivo.
Autores: Manfred Heil Junior
---
.../linha-de-comando/argumentos.cs | 35 ++++++++++++++++---
.../linha-de-comando/executor-cli.cs | 28 ++++++++++++++-
testes/mapdisk.testes/argumentos-testes.cs | 25 +++++++++++++
testes/mapdisk.testes/executor-cli-testes.cs | 23 ++++++++++++
4 files changed, 105 insertions(+), 6 deletions(-)
diff --git a/src/mapdisk.nucleo/linha-de-comando/argumentos.cs b/src/mapdisk.nucleo/linha-de-comando/argumentos.cs
index 1d3094e..762ab02 100644
--- a/src/mapdisk.nucleo/linha-de-comando/argumentos.cs
+++ b/src/mapdisk.nucleo/linha-de-comando/argumentos.cs
@@ -8,6 +8,8 @@ public enum ComandoCli
Ajuda,
Versao,
Varrer,
+ Integrar,
+ RemoverIntegracao,
}
/// Argumentos da linha de comando já interpretados. A linha de comando só lê.
@@ -20,6 +22,8 @@ public sealed class ArgumentosCli
Uso:
mapdisk abre a janela
mapdisk varrer [opções] varre e mostra os maiores itens
+ mapdisk --integrar põe "Analisar com MapDisk" no menu do Explorer
+ mapdisk --remover-integracao tira o item do menu do Explorer
mapdisk --ajuda mostra esta ajuda
mapdisk --versao mostra a versão
@@ -27,11 +31,14 @@ mapdisk varrer [opções] varre e mostra os maiores itens
(\\servidor\pasta).
Opções de varrer:
- --csv grava todas as pastas num arquivo CSV
- --top quantos itens mostrar, de 1 a 1000 (padrão: 10)
+ --csv grava todas as pastas num arquivo CSV
+ --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)
Exemplos:
mapdisk varrer C:
+ mapdisk varrer D:\ --relatorio d.html
mapdisk varrer \\servidor\dados --csv dados.csv --top 30
A linha de comando só lê. Ela nunca apaga nem move arquivos.
@@ -44,6 +51,8 @@ A linha de comando só lê. Ela nunca apaga nem move arquivos.
public string? Csv { get; private set; }
+ public string? Relatorio { get; private set; }
+
public int Top { get; private set; } = 10;
public List Erros { get; } = [];
@@ -74,6 +83,17 @@ public static ArgumentosCli Interpretar(IReadOnlyList args)
a.Comando = ComandoCli.Varrer;
comandos++;
break;
+ case "--integrar":
+ a.Comando = ComandoCli.Integrar;
+ comandos++;
+ break;
+ case "--remover-integracao":
+ a.Comando = ComandoCli.RemoverIntegracao;
+ comandos++;
+ break;
+ case "--relatorio":
+ a.Relatorio = Valor(args, ref i, arg, a.Erros);
+ break;
case "--csv":
a.Csv = Valor(args, ref i, arg, a.Erros);
break;
@@ -101,7 +121,7 @@ public static ArgumentosCli Interpretar(IReadOnlyList args)
if (comandos > 1)
{
- a.Erros.Add("Use só um comando por vez: varrer, --ajuda ou --versao.");
+ a.Erros.Add("Use só um comando por vez: varrer, --integrar, --remover-integracao, --ajuda ou --versao.");
}
if (a.Comando == ComandoCli.Varrer)
@@ -127,6 +147,11 @@ public static ArgumentosCli Interpretar(IReadOnlyList args)
{
a.Erros.Add("O arquivo de --csv tem que terminar em .csv.");
}
+
+ if (a.Relatorio is { } rel && !rel.EndsWith(".html", StringComparison.OrdinalIgnoreCase))
+ {
+ a.Erros.Add("O arquivo de --relatorio tem que terminar em .html.");
+ }
}
else
{
@@ -135,9 +160,9 @@ public static ArgumentosCli Interpretar(IReadOnlyList args)
a.Erros.Add($"Comando desconhecido: {alvos[0]}. Use varrer ou --ajuda.");
}
- if (a.Csv != null || topDado)
+ if (a.Csv != null || a.Relatorio != null || topDado)
{
- a.Erros.Add("As opções --csv e --top pedem o comando varrer.");
+ a.Erros.Add("As opções --csv, --relatorio e --top 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 91d8908..967d4ac 100644
--- a/src/mapdisk.nucleo/linha-de-comando/executor-cli.cs
+++ b/src/mapdisk.nucleo/linha-de-comando/executor-cli.cs
@@ -11,7 +11,8 @@ public static class ExecutorCli
public static string Versao => typeof(ExecutorCli).Assembly.GetName().Version!.ToString(3);
- public static int Executar(ArgumentosCli argumentos, IMotorVarredura motor, TextWriter saida, TextWriter erro, CancellationToken cancelar)
+ /// As chaves e o .exe só mudam nos testes; o padrão é o registro do usuário e este programa.
+ public static int Executar(ArgumentosCli argumentos, IMotorVarredura motor, TextWriter saida, TextWriter erro, CancellationToken cancelar, IChavesUsuario? chaves = null, string? exe = null)
{
if (!argumentos.Valido)
{
@@ -32,6 +33,15 @@ public static int Executar(ArgumentosCli argumentos, IMotorVarredura motor, Text
case ComandoCli.Ajuda or ComandoCli.Janela:
saida.WriteLine(ArgumentosCli.TextoAjuda);
return CodigoSucesso;
+ case ComandoCli.Integrar:
+ IntegracaoExplorer.Ligar(chaves ?? new ChavesUsuarioWindows(), exe ?? Environment.ProcessPath!);
+ saida.WriteLine("Item \"Analisar com MapDisk\" ligado no menu das pastas e unidades do Explorer.");
+ saida.WriteLine("No Windows 11, ele fica em \"Mostrar mais opções\". Se o mapdisk.exe mudar de lugar, rode --integrar de novo.");
+ return CodigoSucesso;
+ case ComandoCli.RemoverIntegracao:
+ IntegracaoExplorer.Desligar(chaves ?? new ChavesUsuarioWindows());
+ saida.WriteLine("Item \"Analisar com MapDisk\" tirado do menu do Explorer.");
+ return CodigoSucesso;
}
saida.WriteLine($"Varrendo {argumentos.Caminho}...");
@@ -90,6 +100,22 @@ public static int Executar(ArgumentosCli argumentos, IMotorVarredura motor, Text
saida.WriteLine($"CSV gravado em {Path.GetFullPath(csv)}");
}
+ if (argumentos.Relatorio is { } relatorio)
+ {
+ try
+ {
+ RelatorioTecnico.Gravar(new DadosRelatorio(raiz, DateTime.Now, Environment.MachineName, r.Volume, r.Cancelada, argumentos.Top, null), relatorio);
+ }
+ catch (Exception e) when (e is IOException or UnauthorizedAccessException)
+ {
+ erro.WriteLine($"Não foi possível gravar o relatório: {e.Message}");
+ return CodigoFalha;
+ }
+
+ saida.WriteLine();
+ saida.WriteLine($"Relatório gravado em {Path.GetFullPath(relatorio)}");
+ }
+
return r.Cancelada ? CodigoCancelado : CodigoSucesso;
}
diff --git a/testes/mapdisk.testes/argumentos-testes.cs b/testes/mapdisk.testes/argumentos-testes.cs
index c39a922..3e6b1c8 100644
--- a/testes/mapdisk.testes/argumentos-testes.cs
+++ b/testes/mapdisk.testes/argumentos-testes.cs
@@ -63,4 +63,29 @@ public void A_ajuda_diz_que_a_linha_de_comando_so_le()
{
Assert.Contains("nunca apaga nem move", ArgumentosCli.TextoAjuda);
}
+
+ [Fact]
+ 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);
+ }
+
+ [Fact]
+ public void Le_integrar_e_remover()
+ {
+ Assert.Equal(ComandoCli.Integrar, ArgumentosCli.Interpretar(["--integrar"]).Comando);
+ Assert.Equal(ComandoCli.RemoverIntegracao, ArgumentosCli.Interpretar(["--remover-integracao"]).Comando);
+ Assert.False(ArgumentosCli.Interpretar(["--integrar", "varrer", "C:"]).Valido);
+ }
+
+ [Fact]
+ public void Ajuda_lista_todas_as_opcoes()
+ {
+ foreach (var opcao in new[] { "--relatorio", "--integrar", "--remover-integracao", "--csv", "--top" })
+ {
+ Assert.Contains(opcao, ArgumentosCli.TextoAjuda);
+ }
+ }
}
diff --git a/testes/mapdisk.testes/executor-cli-testes.cs b/testes/mapdisk.testes/executor-cli-testes.cs
index a3f93d4..77d8f5d 100644
--- a/testes/mapdisk.testes/executor-cli-testes.cs
+++ b/testes/mapdisk.testes/executor-cli-testes.cs
@@ -128,4 +128,27 @@ public void Fim_com_pastas_nao_lidas_avisa_na_linha_de_comando()
Assert.Contains("Atenção: 2 pastas não foram lidas", saida);
}
+
+ [Fact]
+ public void Grava_o_relatorio_pedido()
+ {
+ var arquivo = Path.Combine(AppContext.BaseDirectory, "cli-" + Guid.NewGuid().ToString("N") + ".html");
+ var codigo = Rodar(Demonstracao.Motor(), out var saida, out _, "varrer", "C:", "--relatorio", arquivo);
+ Assert.Equal(ExecutorCli.CodigoSucesso, codigo);
+ Assert.Contains("Relatório gravado em", saida);
+ Assert.Contains("Relatório de espaço em disco", File.ReadAllText(arquivo));
+ File.Delete(arquivo);
+ }
+
+ [Fact]
+ public void Integrar_e_remover_mexem_so_nas_chaves()
+ {
+ var chaves = new ChavesEmMemoria();
+ var s = new StringWriter();
+ Assert.Equal(ExecutorCli.CodigoSucesso, ExecutorCli.Executar(ArgumentosCli.Interpretar(["--integrar"]), Demonstracao.Motor(), s, new StringWriter(), CancellationToken.None, chaves, @"E:\mapdisk.exe"));
+ Assert.Equal(EstadoIntegracao.Ligada, IntegracaoExplorer.Estado(chaves, @"E:\mapdisk.exe"));
+ Assert.Contains("Mostrar mais opções", s.ToString());
+ ExecutorCli.Executar(ArgumentosCli.Interpretar(["--remover-integracao"]), Demonstracao.Motor(), s, new StringWriter(), CancellationToken.None, chaves, @"E:\mapdisk.exe");
+ Assert.Empty(chaves.Todas);
+ }
}
From c22579ef95e904fb4a895b04f2a4c7f05f365c04 Mon Sep 17 00:00:00 2001
From: Manfred Heil Junior
Date: Thu, 1 Oct 2026 02:19:59 -0300
Subject: [PATCH 5/8] Liga exportar, opcoes e a abertura pelo Explorer na
janela
O tecnico exporta HTML e CSV da pasta das analises, liga o item do Explorer e guarda as opcoes numa tela so. O registro de acoes passa para as Opcoes. O --abrir abre a janela ja varrendo, e o relatorio para o cliente comeca com os criterios guardados.
Autores: Manfred Heil Junior
---
.../relatorios/relatorio-tecnico.cs | 10 +-
src/mapdisk/janela-opcoes.xaml | 104 +++++++++++++++++
src/mapdisk/janela-opcoes.xaml.cs | 108 ++++++++++++++++++
src/mapdisk/janela-principal.xaml | 15 ++-
src/mapdisk/janela-principal.xaml.cs | 79 +++++++++++--
src/mapdisk/janela-relatorio.xaml.cs | 6 +-
src/mapdisk/programa.cs | 11 +-
testes/mapdisk.testes/recursos-testes.cs | 18 ++-
.../relatorio-tecnico-testes.cs | 8 ++
9 files changed, 343 insertions(+), 16 deletions(-)
create mode 100644 src/mapdisk/janela-opcoes.xaml
create mode 100644 src/mapdisk/janela-opcoes.xaml.cs
diff --git a/src/mapdisk.nucleo/relatorios/relatorio-tecnico.cs b/src/mapdisk.nucleo/relatorios/relatorio-tecnico.cs
index 65b75b4..ee9b08e 100644
--- a/src/mapdisk.nucleo/relatorios/relatorio-tecnico.cs
+++ b/src/mapdisk.nucleo/relatorios/relatorio-tecnico.cs
@@ -28,6 +28,14 @@ public static class RelatorioTecnico
"footer{margin-top:24px;font-size:12px;color:#666}" +
"@media print{body{background:#fff}header{background:#fff;color:#006B2D;border-bottom:2px solid #006B2D}tr{page-break-inside:avoid}svg{page-break-inside:avoid}}";
+ /// Nome sugerido, sem extensão: "espaco-" mais a pasta e a data. A raiz "C:\" vira "c".
+ public static string NomeDoArquivo(NoPasta pasta, DateTime agora)
+ {
+ var invalidos = Path.GetInvalidFileNameChars();
+ var nome = new string(pasta.Nome.Where(c => !invalidos.Contains(c) && c != ':').ToArray()).Trim().ToLowerInvariant();
+ return $"espaco-{(nome.Length > 0 ? nome : "pasta")}-{agora.ToString("yyyyMMdd-HHmm", CultureInfo.InvariantCulture)}";
+ }
+
public static void Gravar(DadosRelatorio dados, string arquivo) =>
File.WriteAllText(arquivo, Gerar(dados), new UTF8Encoding(false));
@@ -80,7 +88,7 @@ private static void Resumo(StringBuilder s, DadosRelatorio d)
if (semLeitura > 0)
{
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("Varrer como administrador lê as pastas locais sem acesso.
");
+ s.Append("O botão \"Varrer como administrador\" lê as pastas locais que ficaram sem acesso.
");
}
}
diff --git a/src/mapdisk/janela-opcoes.xaml b/src/mapdisk/janela-opcoes.xaml
new file mode 100644
index 0000000..86a4f38
--- /dev/null
+++ b/src/mapdisk/janela-opcoes.xaml
@@ -0,0 +1,104 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/mapdisk/janela-opcoes.xaml.cs b/src/mapdisk/janela-opcoes.xaml.cs
new file mode 100644
index 0000000..433ad84
--- /dev/null
+++ b/src/mapdisk/janela-opcoes.xaml.cs
@@ -0,0 +1,108 @@
+using System.Globalization;
+using System.IO;
+using System.Windows;
+using MapDisk.Nucleo;
+
+namespace MapDisk;
+
+/// Opções que valem entre sessões, o item do Explorer, os últimos alvos e o registro de ações.
+public partial class JanelaOpcoes : Window
+{
+ private readonly PainelPrincipal _painel;
+ private readonly IChavesUsuario _chaves = new ChavesUsuarioWindows();
+ private readonly string _exe = Environment.ProcessPath!;
+
+ public JanelaOpcoes(PainelPrincipal painel)
+ {
+ _painel = painel;
+ InitializeComponent();
+ var p = painel.Preferencias;
+ CampoMaiores.Text = N(p.MaioresArquivos);
+ CampoMinimoDuplicados.Text = N(p.DuplicadosMinimoMb);
+ CampoPastas.Text = N(p.SugerirPastas);
+ CampoArquivos.Text = N(p.SugerirArquivos);
+ CampoAnos.Text = N(p.SugerirAnos);
+ CampoMinimo.Text = N(p.SugerirMinimoMb);
+ TextoRegistro.Text = painel.LocalDoRegistro;
+ MostrarExplorer();
+ MostrarAlvos();
+ }
+
+ private void MostrarExplorer()
+ {
+ var estado = IntegracaoExplorer.Estado(_chaves, _exe);
+ TextoExplorer.Text = estado switch
+ {
+ EstadoIntegracao.Ligada => "Ligado para este mapdisk.exe.",
+ EstadoIntegracao.OutroLocal => $"O item aponta para outro local: {IntegracaoExplorer.ExeRegistrado(_chaves)}. Clique em Ligar para apontar para este.",
+ _ => "Desligado.",
+ };
+ BotaoDesligar.IsEnabled = estado != EstadoIntegracao.Desligada;
+ }
+
+ private void MostrarAlvos() => ListaAlvos.ItemsSource = _painel.UltimosUsados;
+
+ private void AoLigar(object sender, RoutedEventArgs e)
+ {
+ IntegracaoExplorer.Ligar(_chaves, _exe);
+ MostrarExplorer();
+ }
+
+ private void AoDesligar(object sender, RoutedEventArgs e)
+ {
+ IntegracaoExplorer.Desligar(_chaves);
+ MostrarExplorer();
+ }
+
+ private void AoEsquecer(object sender, RoutedEventArgs e)
+ {
+ if (ListaAlvos.SelectedItem is string alvo)
+ {
+ _painel.EsquecerAlvo(alvo);
+ MostrarAlvos();
+ }
+ }
+
+ private void AoLimpar(object sender, RoutedEventArgs e)
+ {
+ _painel.LimparAlvos();
+ MostrarAlvos();
+ }
+
+ private void AoAbrirRegistro(object sender, RoutedEventArgs e)
+ {
+ if (File.Exists(_painel.LocalDoRegistro))
+ {
+ Shell.MostrarNoExplorer(_painel.LocalDoRegistro, ehArquivo: true);
+ }
+ else
+ {
+ MessageBox.Show(this, $"Nenhuma ação registrada ainda. O registro fica em {_painel.LocalDoRegistro}.", "MapDisk - MT",
+ MessageBoxButton.OK, MessageBoxImage.Information);
+ }
+ }
+
+ private void AoSalvar(object sender, RoutedEventArgs e)
+ {
+ var campos = new (string Nome, string Texto)[]
+ {
+ ("maiores-arquivos", CampoMaiores.Text),
+ ("duplicados-minimo-mb", CampoMinimoDuplicados.Text),
+ ("sugerir-pastas", CampoPastas.Text),
+ ("sugerir-arquivos", CampoArquivos.Text),
+ ("sugerir-anos", CampoAnos.Text),
+ ("sugerir-minimo-mb", CampoMinimo.Text),
+ };
+ if (!campos.All(c => Preferencias.Valido(c.Nome, c.Texto)))
+ {
+ MessageBox.Show(this, "Algum número está fora da faixa. Maiores arquivos vai de 10 a 1000, duplicados a partir de 1 MB e anos sem alteração a partir de 1.",
+ "MapDisk - MT", MessageBoxButton.OK, MessageBoxImage.Information);
+ return;
+ }
+
+ _painel.GravarPreferencias(Preferencias.DeTexto(campos.Select(c => $"{c.Nome}={c.Texto}")));
+ DialogResult = true;
+ }
+
+ private static string N(long n) => n.ToString(CultureInfo.InvariantCulture);
+}
diff --git a/src/mapdisk/janela-principal.xaml b/src/mapdisk/janela-principal.xaml
index 450c3d8..18303ca 100644
--- a/src/mapdisk/janela-principal.xaml
+++ b/src/mapdisk/janela-principal.xaml
@@ -78,6 +78,9 @@
+
+
-
+
+
+
+
+
+
+
+
+
diff --git a/src/mapdisk/janela-principal.xaml.cs b/src/mapdisk/janela-principal.xaml.cs
index 4d6f43d..70320c4 100644
--- a/src/mapdisk/janela-principal.xaml.cs
+++ b/src/mapdisk/janela-principal.xaml.cs
@@ -172,8 +172,12 @@ private void AoGerarRelatorio(object sender, RoutedEventArgs e)
}
var relatorio = new PainelRelatorio(raiz, Volumes.Ler(raiz.CaminhoCompleto())?.Livre, Environment.UserName, _painel.Locais,
- _painel.Analises.Duplicados.Resultado);
- new JanelaRelatorio(relatorio, () => _ultimaSelecao?.Cast().ToList() ?? []) { Owner = this }.ShowDialog();
+ _painel.Analises.Duplicados.Resultado)
+ {
+ Criterios = _painel.Preferencias.Criterios,
+ };
+ new JanelaRelatorio(relatorio, () => _ultimaSelecao?.Cast().ToList() ?? [],
+ c => _painel.GravarPreferencias(_painel.Preferencias.ComCriterios(c))) { Owner = this }.ShowDialog();
}
private void AoLerResposta(object sender, RoutedEventArgs e)
@@ -226,16 +230,77 @@ private async Task AgirSobre(IReadOnlyList itens, bool mover)
await Agir(new PedidoAcao(TipoAcao.Mover, selecionados, dialogo.FolderName));
}
- private void AoAbrirRegistro(object sender, RoutedEventArgs e)
+ private void AoAbrirOpcoes(object sender, RoutedEventArgs e)
{
- if (File.Exists(_painel.LocalDoRegistro))
+ if (new JanelaOpcoes(_painel) { Owner = this }.ShowDialog() == true)
{
- Shell.MostrarNoExplorer(_painel.LocalDoRegistro, ehArquivo: true);
+ // A quantidade de maiores arquivos pode ter mudado: calcula de novo.
+ _pastaAnalisada = null;
+ _ = AtualizarAnalises();
}
- else
+ }
+
+ private void AoExportarHtml(object sender, RoutedEventArgs e)
+ {
+ if (PastaParaExportar() is not { } pasta
+ || Destino("Onde gravar o relatório", RelatorioTecnico.NomeDoArquivo(pasta, DateTime.Now) + ".html", "Página HTML (*.html)|*.html") is not { } arquivo)
{
- MessageBox.Show(this, $"Nenhuma ação registrada ainda. O registro fica em {_painel.LocalDoRegistro}.", "MapDisk - MT",
+ return;
+ }
+
+ var dados = new DadosRelatorio(pasta, DateTime.Now, Environment.MachineName, _painel.Volume, _painel.Interrompida,
+ _painel.Preferencias.MaioresArquivos, _painel.Analises.Duplicados.Resultado);
+ if (Gravar(() => RelatorioTecnico.Gravar(dados, arquivo))
+ && MessageBox.Show(this, "Relatório gravado. Abrir agora?", "MapDisk - MT", MessageBoxButton.YesNo, MessageBoxImage.Question) == MessageBoxResult.Yes)
+ {
+ Shell.AbrirNoNavegador(arquivo);
+ }
+ }
+
+ private void AoExportarCsv(object sender, RoutedEventArgs e)
+ {
+ if (PastaParaExportar() is not { } pasta
+ || Destino("Onde gravar a planilha", RelatorioTecnico.NomeDoArquivo(pasta, DateTime.Now) + ".csv", "CSV (*.csv)|*.csv") is not { } arquivo)
+ {
+ return;
+ }
+
+ if (Gravar(() => ExportadorCsv.Gravar(pasta, arquivo)))
+ {
+ Shell.MostrarNoExplorer(arquivo, ehArquivo: true);
+ }
+ }
+
+ // A mesma pasta das análises: a selecionada na árvore, ou a raiz mostrada.
+ private NoPasta? PastaParaExportar()
+ {
+ if (_painel.Estado != EstadoPainel.Parado || _painel.PastaDasAnalises(Tabela.SelectedItem as LinhaArvore) is not { } pasta)
+ {
+ MessageBox.Show(this, "Varra uma unidade ou pasta antes de exportar e espere o fim da varredura.", "MapDisk - MT",
MessageBoxButton.OK, MessageBoxImage.Information);
+ return null;
+ }
+
+ return pasta;
+ }
+
+ private string? Destino(string titulo, string nome, string filtro)
+ {
+ var dialogo = new SaveFileDialog { Title = titulo, FileName = nome, Filter = filtro, OverwritePrompt = true };
+ return dialogo.ShowDialog(this) == true ? dialogo.FileName : null;
+ }
+
+ private bool Gravar(Action gravar)
+ {
+ try
+ {
+ gravar();
+ return true;
+ }
+ catch (Exception erro) when (erro is IOException or UnauthorizedAccessException)
+ {
+ MessageBox.Show(this, $"Não foi possível gravar: {erro.Message}", "MapDisk - MT", MessageBoxButton.OK, MessageBoxImage.Warning);
+ return false;
}
}
diff --git a/src/mapdisk/janela-relatorio.xaml.cs b/src/mapdisk/janela-relatorio.xaml.cs
index aef7a9d..939f8ae 100644
--- a/src/mapdisk/janela-relatorio.xaml.cs
+++ b/src/mapdisk/janela-relatorio.xaml.cs
@@ -12,11 +12,14 @@ public partial class JanelaRelatorio : Window
private const long Mb = 1024 * 1024;
private readonly PainelRelatorio _painel;
private readonly Func> _selecaoAtual;
+ private readonly Action _guardarCriterios;
- public JanelaRelatorio(PainelRelatorio painel, Func> selecaoAtual)
+ /// Guarda os critérios usados no Sugerir para o próximo relatório.
+ public JanelaRelatorio(PainelRelatorio painel, Func> selecaoAtual, Action guardarCriterios)
{
_painel = painel;
_selecaoAtual = selecaoAtual;
+ _guardarCriterios = guardarCriterios;
DataContext = painel;
InitializeComponent();
var c = painel.Criterios;
@@ -38,6 +41,7 @@ private void AoSugerir(object sender, RoutedEventArgs e)
}
_painel.Criterios = new CriteriosSugestao(pastas, arquivos, anos, minimo * Mb);
+ _guardarCriterios(_painel.Criterios);
_painel.Sugerir(DateTime.Now);
}
diff --git a/src/mapdisk/programa.cs b/src/mapdisk/programa.cs
index 6259223..1bbfd65 100644
--- a/src/mapdisk/programa.cs
+++ b/src/mapdisk/programa.cs
@@ -24,7 +24,10 @@ private static int Main(string[] args)
var demonstracao = args is [Demonstracao.Argumento];
var elevado = Elevacao.EhPedido(args, out var alvoElevado);
- if (args.Length > 0 && !demonstracao && !elevado)
+
+ // Pedido do item "Analisar com MapDisk" do Explorer: abre a janela já varrendo.
+ var aberto = Abertura.EhPedido(args, out var alvoAberto);
+ if (args.Length > 0 && !demonstracao && !elevado && !aberto)
{
return ModoLinhaDeComando.Executar(ArgumentosCli.Interpretar(args));
}
@@ -41,12 +44,12 @@ private static int Main(string[] args)
}
var painel = PainelPrincipal.Padrao();
- if (elevado)
+ if (elevado || aberto)
{
- painel.TextoAlvo = alvoElevado!;
+ painel.TextoAlvo = (alvoElevado ?? alvoAberto)!;
}
- return aplicativo.Run(new JanelaPrincipal(painel, administrador ? " (administrador)" : string.Empty, varrerAoAbrir: elevado));
+ return aplicativo.Run(new JanelaPrincipal(painel, administrador ? " (administrador)" : string.Empty, varrerAoAbrir: elevado || aberto));
}
///
diff --git a/testes/mapdisk.testes/recursos-testes.cs b/testes/mapdisk.testes/recursos-testes.cs
index d978c80..8223eca 100644
--- a/testes/mapdisk.testes/recursos-testes.cs
+++ b/testes/mapdisk.testes/recursos-testes.cs
@@ -69,7 +69,7 @@ public void Janela_tem_as_acoes_com_confirmacao()
var janela = File.ReadAllText(App("janela-principal.xaml"));
Assert.Contains("Click=\"AoRemover\"", janela);
Assert.Contains("Click=\"AoMover\"", janela);
- Assert.Contains("Click=\"AoAbrirRegistro\"", janela);
+ Assert.Contains("Click=\"AoAbrirRegistro\"", File.ReadAllText(App("janela-opcoes.xaml")));
Assert.Contains("SelectionMode=\"Extended\"", janela);
Assert.Contains("ToolTipService.ShowOnDisabled=\"True\"", janela);
Assert.Contains("{Binding TextoSessao}", janela);
@@ -89,6 +89,22 @@ public void Janela_tem_o_relatorio_para_o_cliente()
Assert.True(File.Exists(App("janela-resposta.xaml")));
}
+ [Fact]
+ public void Janela_tem_exportar_e_opcoes()
+ {
+ var janela = File.ReadAllText(App("janela-principal.xaml"));
+ Assert.Contains("Click=\"AoAbrirOpcoes\"", janela);
+ Assert.Contains("Click=\"AoExportarHtml\"", janela);
+ Assert.Contains("Click=\"AoExportarCsv\"", janela);
+ var opcoes = File.ReadAllText(App("janela-opcoes.xaml"));
+ foreach (var nome in new[] { "BotaoLigar", "BotaoDesligar", "CampoMaiores", "CampoMinimoDuplicados", "CampoPastas", "CampoArquivos", "CampoAnos", "CampoMinimo", "ListaAlvos", "TextoRegistro" })
+ {
+ Assert.Contains($"x:Name=\"{nome}\"", opcoes);
+ }
+
+ Assert.Contains("Abertura.EhPedido", File.ReadAllText(App("programa.cs")));
+ }
+
[Fact]
public void Janela_tem_a_aba_de_duplicados()
{
diff --git a/testes/mapdisk.testes/relatorio-tecnico-testes.cs b/testes/mapdisk.testes/relatorio-tecnico-testes.cs
index 22e5144..36a0486 100644
--- a/testes/mapdisk.testes/relatorio-tecnico-testes.cs
+++ b/testes/mapdisk.testes/relatorio-tecnico-testes.cs
@@ -72,6 +72,14 @@ public void Duplicados_entram_so_quando_houve_busca()
Assert.Contains("Duplicados ", Texto(Dados(Raiz(), duplicados: vazio)));
}
+ [Fact]
+ public void Nome_do_arquivo_vem_da_pasta_e_da_data()
+ {
+ var quando = new DateTime(2026, 10, 1, 9, 5, 0);
+ Assert.Equal("espaco-c-20261001-0905", RelatorioTecnico.NomeDoArquivo(new NoPasta(@"C:\", null), quando));
+ Assert.Equal("espaco-financeiro-20261001-0905", RelatorioTecnico.NomeDoArquivo(new NoPasta("Financeiro", null), quando));
+ }
+
[Fact]
public void Grava_em_utf8()
{
From 2e2525487cdc374ab25552397241bb59cd0bc543 Mon Sep 17 00:00:00 2001
From: Manfred Heil Junior
Date: Thu, 1 Oct 2026 02:25:09 -0300
Subject: [PATCH 6/8] Cria a pagina do programa
Pagina estatica para mapdisk.manfred.com.br, sem script e sem nada de fora, com telas do modo demonstracao. Os textos passaram pela verificacao juridica, registrada na secao 11: Uso autorizado cobre as unidades removiveis e mapeadas, e a privacidade diz o que a hospedagem registra. Um teste novo confere que o programa nao usa classe de rede. O campo do alvo fica mais estreito para o botao Opcoes caber em 1280 px. A publicacao e outro passo, com autorizacao.
Autores: Manfred Heil Junior
---
...ificacao-distribuicao-e-lgpd-2026-09-28.md | 49 +++++++
public/imagens/janela.png | Bin 0 -> 129697 bytes
public/imagens/mapdisk.png | Bin 0 -> 5627 bytes
public/imagens/mt-logo.png | Bin 0 -> 58954 bytes
public/imagens/relatorio.png | Bin 0 -> 42421 bytes
public/index.html | 129 ++++++++++++++++++
src/mapdisk/janela-principal.xaml | 2 +-
testes/mapdisk.testes/pagina-testes.cs | 36 +++++
testes/mapdisk.testes/sem-rede-testes.cs | 22 +++
9 files changed, 237 insertions(+), 1 deletion(-)
create mode 100644 public/imagens/janela.png
create mode 100644 public/imagens/mapdisk.png
create mode 100644 public/imagens/mt-logo.png
create mode 100644 public/imagens/relatorio.png
create mode 100644 public/index.html
create mode 100644 testes/mapdisk.testes/pagina-testes.cs
create mode 100644 testes/mapdisk.testes/sem-rede-testes.cs
diff --git a/docs/legal/verificacao-distribuicao-e-lgpd-2026-09-28.md b/docs/legal/verificacao-distribuicao-e-lgpd-2026-09-28.md
index 4774035..945d62f 100644
--- a/docs/legal/verificacao-distribuicao-e-lgpd-2026-09-28.md
+++ b/docs/legal/verificacao-distribuicao-e-lgpd-2026-09-28.md
@@ -162,3 +162,52 @@ Autorização, na página e na planilha:
**RECOMENDAÇÃO de processo, fora do programa.** O relatório tem nomes de pastas e de arquivos do cliente. No atendimento, enviar só ao contato do cliente que pediu o serviço e guardar a resposta junto da ordem de serviço. Não há pendência de validação.
+
+## 11. Página do programa (acréscimo de 01/10/2026)
+
+**Contexto.** A fatia 8 cria a página `public/index.html`, para `mapdisk.manfred.com.br`, com o download, a lista do que o programa faz, os textos "Uso autorizado" e "Licença e garantias" da seção 5 e uma nota de privacidade. A página fica num serviço de hospedagem, atrás do Cloudflare. A publicação é um passo separado, com autorização do Manfred.
+
+**Premissas.** Valem as da seção 3. Vale também a premissa de que a distribuição gratuita pode ser relação de consumo, adotada por cautela. Na página, a MT é quem decide sobre os registros de acesso que a hospedagem e o Cloudflare guardam, como o endereço IP. Nessa condição, ela é controladora desses dados, que são dados pessoais (LGPD, art. 5º, I e VI, verificados na seção 3).
+
+**FATO LEGAL.** CDC, art. 30, verificado no Planalto em 01/10/2026 (`https://www.planalto.gov.br/ccivil_03/leis/l8078compilado.htm`):
+
+> "Toda informação ou publicidade, suficientemente precisa, veiculada por qualquer forma ou meio de comunicação com relação a produtos e serviços oferecidos ou apresentados, obriga o fornecedor que a fizer veicular ou dela se utilizar e integra o contrato que vier a ser celebrado."
+
+**Incidência:** a página apresenta o programa ao público, perto do download, e cada frase precisa sobre o que ele faz passa a valer como oferta. O art. 37, § 1º, verificado na mesma fonte e na mesma data, diz que "É enganosa qualquer modalidade de informação ou comunicação de caráter publicitário, inteira ou parcialmente falsa, ou, por qualquer outro modo, mesmo por omissão, capaz de induzir em erro o consumidor [...]".
+
+**FATO LEGAL.** LGPD, art. 6º, VI, na biblioteca local (`fontes-md/leis/03-lgpd-lei-13709-2018-compilado.md`), em dia no relatório de 23/09/2026: "transparência: garantia, aos titulares, de informações claras, precisas e facilmente acessíveis sobre a realização do tratamento e os respectivos agentes de tratamento [...]".
+
+**INTERPRETAÇÃO, ponto 1: "Uso autorizado".** O texto aprovado fala de exclusão definitiva só em pastas de rede. Desde a seção 9, a exclusão definitiva vale também em unidade removível e em unidade mapeada. Uma página que cita só a rede omite um caso de risco, e o art. 37, § 1º, alcança a omissão capaz de induzir em erro. O texto passa a citar as duas situações, com a mesma frase da seção 9 ("o MapDisk não usa a Lixeira").
+
+**INTERPRETAÇÃO, ponto 2: frases sobre segurança.** As frases sobre segurança e funcionamento vinculam a MT (art. 30). Por isso, cada uma tem que corresponder ao que o programa faz e ao que os testes conferem:
+
+| Frase | Situação |
+|---|---|
+| "Feito para não dar susto" | Promessa vaga de resultado. Sai. O título passa a ser "Segurança" |
+| "O programa não envia nenhuma informação pela internet" | Já aprovada na seção 5. Fica. A seção 6 pedia um teste que confira isso. A fatia 8 cria esse teste |
+| "Pasta do sistema e raiz de unidade ficam bloqueadas" | Fica, com o alcance exato: bloqueadas para apagar e mover |
+| "Nunca como zero" | Fica: regra 3 do produto, conferida por teste |
+| Requisitos (Windows 10 e 11, Windows Server 2016 ou mais novo, 64 bits) | Fica. A página só vai ao ar depois do teste de lançamento do Manfred, que inclui um Windows Server |
+
+**INTERPRETAÇÃO, ponto 3: privacidade da página.** "Não usa cookies" seria uma afirmação absoluta sobre um serviço de terceiros que a MT não controla por completo: o Cloudflare pode gravar cookie técnico de segurança. O texto passa a dizer o que a página não tem e o que a hospedagem registra, para atender o art. 6º, VI, sem prometer o que depende de terceiros.
+
+**INTERPRETAÇÃO, ponto 4: GitHub.** O nome do GitHub aparece só para indicar onde está o download e o código, sem o logotipo e sem sugerir parceria. Não precisa de aviso de marca. A marca TreeSize não aparece, e um teste confere isso (seção 4.7).
+
+**RECOMENDAÇÃO aplicada.** Textos finais da página:
+
+> Segurança. O programa não envia nenhuma informação pela internet. Abre como usuário comum e só pede administrador pelo botão "Varrer como administrador". Não instala nada: roda de um pendrive ou de uma pasta de rede. Antes de apagar ou mover, mostra a lista, o total e o destino, e espera a sua confirmação. Cada ação fica registrada num arquivo do computador. As pastas do sistema do Windows e a raiz das unidades ficam bloqueadas para apagar e mover. Pasta que não pôde ser lida aparece como "sem acesso", nunca como zero.
+
+> Uso autorizado. O MapDisk - MT mostra o espaço ocupado em discos e pastas e permite enviar arquivos para a Lixeira, movê-los ou excluí-los definitivamente. Use o programa só em computadores e pastas que você tem autorização para administrar. Antes de apagar ou mover, confira a lista de itens e o destino. Em pastas de rede e em unidades removíveis ou mapeadas, o MapDisk não usa a Lixeira: a exclusão não pode ser desfeita pelo programa e os itens só voltam por uma cópia de segurança. O programa não envia nenhuma informação para fora do computador.
+
+> Licença e garantias. Texto da seção 5, sem mudança.
+
+> Privacidade. O programa não coleta nem envia dados. Esta página não tem formulário, propaganda nem ferramenta de estatística. A hospedagem e o Cloudflare, que entrega a página, registram dados técnicos de acesso, como o endereço IP, e podem gravar um cookie técnico de segurança.
+
+**Requisitos para implementação.**
+
+- [ ] Página com os quatro textos acima, "Uso autorizado" e "Licença e garantias" perto do botão de download (fatia 8).
+- [ ] README com o novo "Uso autorizado" e com "Licença e garantias" perto do download (fatia 8). Fecha o primeiro item da seção 6.
+- [ ] Teste que confere que o programa não usa classe de rede (fatia 8). Fecha o item "Nada sai da máquina" da seção 6.
+- [ ] Página publicada só depois do teste de lançamento, que inclui um Windows Server.
+
+Não há pendência de validação.
diff --git a/public/imagens/janela.png b/public/imagens/janela.png
new file mode 100644
index 0000000000000000000000000000000000000000..7187f5b3437a53f2733a78de0c01e4cd398f71ad
GIT binary patch
literal 129697
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