<# : @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