Organizar/achar-duplicados.cmd (view raw)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 |
<# :
@echo off
powershell -NoLogo -NoProfile -ExecutionPolicy Bypass -Command "Invoke-Expression ([System.IO.File]::ReadAllText('%~f0',[System.Text.Encoding]::UTF8))"
exit /b %errorlevel%
#>
# ============================================================
# Achar duplicados - encontra arquivos iguais por conteúdo (hash)
# Desenvolvido por Pablo Murad - 2026
# ============================================================
try { [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 } catch {}
try { $Host.UI.RawUI.WindowTitle = 'Achar duplicados' } catch {}
function Pausar { Write-Host ''; Write-Host 'Pressione Enter para fechar...' -ForegroundColor DarkGray; [void][System.Console]::ReadLine() }
function Titulo($t) {
Write-Host ''
Write-Host '============================================================' -ForegroundColor DarkCyan
Write-Host " $t" -ForegroundColor Cyan
Write-Host '============================================================' -ForegroundColor DarkCyan
}
function Tam($bytes) { if ($bytes -ge 1GB) { '{0:N2} GB' -f ($bytes/1GB) } elseif ($bytes -ge 1MB) { '{0:N1} MB' -f ($bytes/1MB) } else { '{0:N0} KB' -f ($bytes/1KB) } }
Titulo 'Achar duplicados'
Write-Host ' Desenvolvido por Pablo Murad - 2026' -ForegroundColor DarkGray
$pasta = $null
while (-not $pasta) {
Write-Host ''
$p = (Read-Host 'Cole o caminho da pasta (verifica também as subpastas)').Trim().Trim('"')
if (Test-Path -LiteralPath $p -PathType Container) { $pasta = (Resolve-Path -LiteralPath $p).Path } else { Write-Host '[!] Pasta não encontrada.' -ForegroundColor Yellow }
}
Write-Host ''
Write-Host 'Analisando... (agrupa por tamanho e depois compara o conteúdo)' -ForegroundColor Gray
$todos = @(Get-ChildItem -LiteralPath $pasta -Recurse -File -ErrorAction SilentlyContinue | Where-Object { $_.Length -gt 0 })
# 1) agrupa por tamanho (rápido); 2) só calcula hash de quem tem tamanho repetido
function Hash-SHA256([string]$caminho) {
$sha = [System.Security.Cryptography.SHA256]::Create()
$fs = [System.IO.File]::OpenRead($caminho)
try { return ([BitConverter]::ToString($sha.ComputeHash($fs)) -replace '-', '') }
finally { $fs.Close(); $sha.Dispose() }
}
# Pre-filtro barato: hash so das PONTAS do arquivo (primeiros/ultimos 64 KB).
# So quem colide nas pontas paga o SHA256 do arquivo inteiro.
function Hash-Parcial([string]$caminho, [long]$tam) {
$n = 65536
try { $fs = [System.IO.File]::OpenRead($caminho) } catch { return "ERRO$caminho" }
try {
$md5 = [System.Security.Cryptography.MD5]::Create()
$buf = New-Object byte[] $n
$lido = $fs.Read($buf, 0, $n)
[void]$md5.TransformBlock($buf, 0, $lido, $buf, 0)
if ($tam -gt ($n * 2)) {
[void]$fs.Seek(-$n, [System.IO.SeekOrigin]::End)
$lido2 = $fs.Read($buf, 0, $n)
[void]$md5.TransformBlock($buf, 0, $lido2, $buf, 0)
}
[void]$md5.TransformFinalBlock((New-Object byte[] 0), 0, 0)
return ([BitConverter]::ToString($md5.Hash) + "_$tam")
} catch { return "ERRO$caminho" } finally { $fs.Close() }
}
$porTamanho = @($todos | Group-Object Length | Where-Object { $_.Count -gt 1 })
$grupos = @()
$gi = 0
foreach ($g in $porTamanho) {
$gi++
Write-Host ("`r Comparando... {0}/{1} grupos " -f $gi, $porTamanho.Count) -NoNewline -ForegroundColor Cyan
$porParcial = $g.Group | Group-Object { Hash-Parcial $_.FullName $_.Length }
foreach ($pp in ($porParcial | Where-Object { $_.Count -gt 1 })) {
$porHash = $pp.Group | Group-Object { Hash-SHA256 $_.FullName }
foreach ($h in ($porHash | Where-Object { $_.Count -gt 1 })) {
$grupos += ,@($h.Group | Sort-Object FullName)
}
}
}
if ($porTamanho.Count -gt 0) { Write-Host '' }
if ($grupos.Count -eq 0) { Titulo 'Resultado'; Write-Host 'Nenhum arquivo duplicado encontrado.' -ForegroundColor Green; Pausar; exit 0 }
$totalDup = 0; $espaco = 0
foreach ($grp in $grupos) { $totalDup += ($grp.Count - 1); $espaco += ($grp.Count - 1) * $grp[0].Length }
Titulo 'Duplicados encontrados'
Write-Host ("Grupos de iguais: {0} | Cópias extras: {1} | Espaço recuperável: {2}" -f $grupos.Count, $totalDup, (Tam $espaco)) -ForegroundColor Cyan
Write-Host ''
$mostra = [Math]::Min($grupos.Count, 15)
for ($i=0; $i -lt $mostra; $i++) {
$grp = $grupos[$i]
Write-Host ("Grupo {0} ({1} cópias, {2} cada):" -f ($i+1), $grp.Count, (Tam $grp[0].Length)) -ForegroundColor White
for ($j=0; $j -lt $grp.Count; $j++) {
$marca = if ($j -eq 0) { '[manter] ' } else { '[extra] ' }
$cor = if ($j -eq 0) { 'Green' } else { 'DarkGray' }
Write-Host (" $marca" + $grp[$j].FullName) -ForegroundColor $cor
}
}
if ($grupos.Count -gt $mostra) { Write-Host ("... e mais {0} grupo(s)." -f ($grupos.Count - $mostra)) -ForegroundColor DarkGray }
Titulo 'O que fazer com as cópias extras?'
Write-Host ' [1] Só listar (nada é alterado) - padrão' -ForegroundColor White
Write-Host ' [2] Mover as cópias extras para uma subpasta "_Duplicados"' -ForegroundColor White
Write-Host ' [3] Apagar as cópias extras (mantém sempre 1 original)' -ForegroundColor White
$acao = Read-Host 'Opção (Enter = 1)'
if ([string]::IsNullOrWhiteSpace($acao)) { $acao = '1' }
if ($acao -eq '1') { Write-Host ''; Write-Host 'Nada foi alterado. Relatório acima.' -ForegroundColor Gray; Pausar; exit 0 }
if ($acao -eq '3') {
Write-Host ''
Write-Host 'ATENÇÃO: isto APAGA as cópias extras permanentemente.' -ForegroundColor Red
$conf = Read-Host 'Digite APAGAR para confirmar'
if ($conf -cne 'APAGAR') { Write-Host 'Cancelado.' -ForegroundColor Yellow; Pausar; exit 0 }
}
$feitos=0;$err=0
$destDup = Join-Path $pasta '_Duplicados'
foreach ($grp in $grupos) {
for ($j=1; $j -lt $grp.Count; $j++) {
$f = $grp[$j]
try {
if ($acao -eq '2') {
New-Item -ItemType Directory -Path $destDup -Force | Out-Null
$base=[System.IO.Path]::GetFileNameWithoutExtension($f.Name); $ext=$f.Extension
$dest=Join-Path $destDup $f.Name; $n=1
while (Test-Path -LiteralPath $dest) { $dest=Join-Path $destDup ("$base ($n)$ext"); $n++ }
Move-Item -LiteralPath $f.FullName -Destination $dest -ErrorAction Stop
} else {
Remove-Item -LiteralPath $f.FullName -Force -ErrorAction Stop
}
$feitos++
} catch { Write-Host " [ERRO] $($f.Name): $($_.Exception.Message)" -ForegroundColor Red; $err++ }
}
}
Titulo 'Concluído'
$verbo = if ($acao -eq '2') { 'Movidas' } else { 'Apagadas' }
Write-Host "$verbo`: $feitos cópia(s) extra(s)" -ForegroundColor Green
Write-Host "Erros: $err" -ForegroundColor $(if ($err -gt 0){'Red'}else{'Gray'})
if ($acao -eq '2') { Write-Host "Movidos para: $destDup" }
Pausar
|