$ErrorActionPreference = 'Stop' $root = Split-Path -Parent $MyInvocation.MyCommand.Path $stageRoot = Join-Path $root 'NovaPatch' $filesRoot = Join-Path $root 'files' $deleteFile = Join-Path $root 'delete.txt' $manifestFile = Join-Path $root 'manifest.json' $logsRoot = Join-Path $root 'logs' $deletedBackupRoot = Join-Path $root '_DeletedBackup' foreach ($dir in @($stageRoot, $filesRoot, $logsRoot, $deletedBackupRoot)) { if (!(Test-Path $dir)) { New-Item -ItemType Directory -Path $dir -Force | Out-Null } } function Get-CurrentVersion { param([string]$Path) if (!(Test-Path $Path)) { return '1.0.0' } try { $obj = Get-Content -Raw -Path $Path | ConvertFrom-Json if ($obj.Version) { return [string]$obj.Version } if ($obj.version) { return [string]$obj.version } } catch { $raw = Get-Content -Raw -Path $Path $m = [regex]::Match($raw, '"(?i:version)"\s*:\s*"([^"]+)"') if ($m.Success) { return $m.Groups[1].Value } } return '1.0.0' } function Get-NextVersion { param([string]$Version) $parts = $Version.Split('.') if ($parts.Count -ge 3) { $major = 0; $minor = 0; $patch = 0 if ([int]::TryParse($parts[0], [ref]$major) -and [int]::TryParse($parts[1], [ref]$minor) -and [int]::TryParse($parts[2], [ref]$patch)) { return "$major.$minor.$($patch + 1)" } } return $Version } function Normalize-RelativePath { param([string]$Value) if ([string]::IsNullOrWhiteSpace($Value)) { return $null } $p = $Value.Trim() -replace '\\','/' $p = $p.TrimStart('/') if ($p -match '(^|/)\.\.(/|$)') { throw "Caminho inseguro: $Value" } if ([System.IO.Path]::IsPathRooted($p)) { throw "Caminho absoluto nao permitido: $Value" } return $p } function Get-Sha256Lower { param([string]$Path) return (Get-FileHash -Algorithm SHA256 -Path $Path).Hash.ToLowerInvariant() } function Format-Bytes { param([long]$Bytes) if ($Bytes -ge 1GB) { return ('{0:N2} GB' -f ($Bytes / 1GB)) } if ($Bytes -ge 1MB) { return ('{0:N2} MB' -f ($Bytes / 1MB)) } if ($Bytes -ge 1KB) { return ('{0:N2} KB' -f ($Bytes / 1KB)) } return "$Bytes B" } $currentVersion = Get-CurrentVersion $manifestFile $suggestedVersion = Get-NextVersion $currentVersion Write-Host '' Write-Host '=========================================================' -ForegroundColor DarkYellow Write-Host ' CELESTIAL CLASSIC - PUBLICADOR DE PATCH V3' -ForegroundColor Yellow Write-Host '=========================================================' -ForegroundColor DarkYellow Write-Host "Versao atual detectada : $currentVersion" -ForegroundColor Cyan Write-Host "Proxima versao sugerida: $suggestedVersion" -ForegroundColor Green Write-Host '' # Le exclusoes SEM alterar nada ainda. $deletes = New-Object System.Collections.Generic.List[string] if (Test-Path $deleteFile) { foreach ($rawLine in (Get-Content -Path $deleteFile)) { $line = $rawLine.Trim() if ([string]::IsNullOrWhiteSpace($line) -or $line.StartsWith('#')) { continue } $entry = Normalize-RelativePath $line if ($entry -and -not $deletes.Contains($entry)) { $deletes.Add($entry) } } } # Analisa NovaPatch sem copiar ainda. $stageFiles = @(Get-ChildItem -Path $stageRoot -File -Recurse | Sort-Object FullName) $preview = New-Object System.Collections.Generic.List[object] $newCount = 0 $changedCount = 0 $unchangedCount = 0 $totalPatchBytes = [long]0 foreach ($f in $stageFiles) { $relative = $f.FullName.Substring($stageRoot.Length).TrimStart('\','/') -replace '\\','/' $relative = Normalize-RelativePath $relative if ($deletes.Contains($relative)) { $preview.Add([pscustomobject]@{ Status='IGNORADO'; Path=$relative; Size=[long]$f.Length; Source=$f.FullName }) continue } $dest = Join-Path $filesRoot ($relative -replace '/', [System.IO.Path]::DirectorySeparatorChar) if (!(Test-Path $dest -PathType Leaf)) { $status = 'NOVO' $newCount++ $totalPatchBytes += [long]$f.Length } else { $srcHash = Get-Sha256Lower $f.FullName $dstHash = Get-Sha256Lower $dest if ($srcHash -eq $dstHash) { $status = 'IGUAL' $unchangedCount++ } else { $status = 'ALTERADO' $changedCount++ $totalPatchBytes += [long]$f.Length } } $preview.Add([pscustomobject]@{ Status=$status; Path=$relative; Size=[long]$f.Length; Source=$f.FullName }) } $deleteExisting = 0 $deleteMissingMaster = 0 foreach ($entry in $deletes) { $localEntry = $entry -replace '/', [System.IO.Path]::DirectorySeparatorChar $masterTarget = Join-Path $filesRoot $localEntry if (Test-Path $masterTarget -PathType Leaf) { $deleteExisting++ } else { $deleteMissingMaster++ } } Write-Host '---------------- PREVIA DO PATCH ----------------' -ForegroundColor DarkYellow Write-Host ("Arquivos NOVOS : {0}" -f $newCount) -ForegroundColor Green Write-Host ("Arquivos ALTERADOS : {0}" -f $changedCount) -ForegroundColor Yellow Write-Host ("Arquivos sem alteracao: {0}" -f $unchangedCount) -ForegroundColor DarkGray Write-Host ("Arquivos para EXCLUIR : {0}" -f $deletes.Count) -ForegroundColor Magenta Write-Host ("Tamanho para download : {0}" -f (Format-Bytes $totalPatchBytes)) -ForegroundColor Cyan Write-Host '' if ($preview.Count -gt 0) { Write-Host '[ARQUIVOS EM NOVAPATCH]' -ForegroundColor Gray foreach ($p in $preview) { $sizeText = Format-Bytes $p.Size switch ($p.Status) { 'NOVO' { Write-Host (" [NOVO] {0} ({1})" -f $p.Path, $sizeText) -ForegroundColor Green } 'ALTERADO' { Write-Host (" [ALTERADO] {0} ({1})" -f $p.Path, $sizeText) -ForegroundColor Yellow } 'IGUAL' { Write-Host (" [IGUAL] {0} ({1})" -f $p.Path, $sizeText) -ForegroundColor DarkGray } 'IGNORADO' { Write-Host (" [IGNORADO] {0} (tambem esta no delete.txt)" -f $p.Path) -ForegroundColor DarkYellow } } } Write-Host '' } if ($deletes.Count -gt 0) { Write-Host '[EXCLUSOES]' -ForegroundColor Gray foreach ($entry in $deletes) { $localEntry = $entry -replace '/', [System.IO.Path]::DirectorySeparatorChar $masterTarget = Join-Path $filesRoot $localEntry if (Test-Path $masterTarget -PathType Leaf) { Write-Host " [EXCLUIR] $entry" -ForegroundColor Magenta } else { Write-Host " [EXCLUIR] $entry (nao esta no master; ainda sera enviado ao cliente)" -ForegroundColor DarkMagenta } } Write-Host '' } if (($newCount + $changedCount + $deletes.Count) -eq 0) { Write-Host '=========================================================' -ForegroundColor DarkYellow Write-Host 'NENHUM PATCH NECESSARIO.' -ForegroundColor Green Write-Host "A versao permanece em $currentVersion." -ForegroundColor Cyan Write-Host 'Os arquivos em NovaPatch sao identicos aos que ja estao publicados.' -ForegroundColor Gray Write-Host 'Nenhum arquivo, manifesto ou numero de versao foi alterado.' -ForegroundColor Gray Write-Host 'Coloque um arquivo novo/alterado em NovaPatch ou adicione uma exclusao no delete.txt.' -ForegroundColor Yellow Write-Host '=========================================================' -ForegroundColor DarkYellow exit 0 } $versionInput = Read-Host "Nova versao [ENTER = $suggestedVersion]" $version = if ([string]::IsNullOrWhiteSpace($versionInput)) { $suggestedVersion } else { $versionInput.Trim() } if ($version -notmatch '^\d+\.\d+\.\d+$') { throw "Versao invalida: '$version'. Use formato como 1.0.6" } Write-Host '' Write-Host '---------------- CONFIRMACAO ----------------' -ForegroundColor DarkYellow Write-Host "Versao a publicar : $version" -ForegroundColor Cyan Write-Host "Novos : $newCount" -ForegroundColor Green Write-Host "Alterados : $changedCount" -ForegroundColor Yellow Write-Host "Exclusoes : $($deletes.Count)" -ForegroundColor Magenta Write-Host "Download estimado : $(Format-Bytes $totalPatchBytes)" -ForegroundColor Cyan Write-Host '' $confirm = Read-Host 'CONFIRMA A PUBLICACAO? [S/N]' if ($confirm -notmatch '^(?i:s|sim)$') { Write-Host 'Publicacao cancelada. Nenhum arquivo foi modificado.' -ForegroundColor Gray exit 0 } # A PARTIR DAQUI a publicacao realmente altera files/manifest. $stamp = Get-Date -Format 'yyyyMMdd_HHmmss' $deletedBackupThisRun = Join-Path $deletedBackupRoot "${version}_$stamp" $deletedFromMaster = 0 foreach ($entry in $deletes) { $localEntry = $entry -replace '/', [System.IO.Path]::DirectorySeparatorChar $masterTarget = [System.IO.Path]::GetFullPath((Join-Path $filesRoot $localEntry)) $masterRootFull = [System.IO.Path]::GetFullPath($filesRoot).TrimEnd('\') + '\' if (-not $masterTarget.StartsWith($masterRootFull, [System.StringComparison]::OrdinalIgnoreCase)) { throw "Exclusao fora da pasta files bloqueada: $entry" } if (Test-Path $masterTarget -PathType Leaf) { $backupTarget = Join-Path $deletedBackupThisRun $localEntry $backupDir = Split-Path -Parent $backupTarget if (!(Test-Path $backupDir)) { New-Item -ItemType Directory -Path $backupDir -Force | Out-Null } Move-Item -Path $masterTarget -Destination $backupTarget -Force $deletedFromMaster++ Write-Host "REMOVIDO DO MASTER: $entry" -ForegroundColor Magenta } } $copied = New-Object System.Collections.Generic.List[string] $skippedSame = New-Object System.Collections.Generic.List[string] foreach ($p in $preview) { if ($p.Status -eq 'IGNORADO') { continue } if ($p.Status -eq 'IGUAL') { $skippedSame.Add($p.Path) continue } $dest = Join-Path $filesRoot ($p.Path -replace '/', [System.IO.Path]::DirectorySeparatorChar) $destDir = Split-Path -Parent $dest if (!(Test-Path $destDir)) { New-Item -ItemType Directory -Path $destDir -Force | Out-Null } Copy-Item -Path $p.Source -Destination $dest -Force $copied.Add($p.Path) Write-Host "PUBLICADO: $($p.Path)" -ForegroundColor Green } # Gera manifest CUMULATIVO com todo o master files. $items = @() Get-ChildItem -Path $filesRoot -File -Recurse | Sort-Object FullName | ForEach-Object { $relative = $_.FullName.Substring($filesRoot.Length).TrimStart('\','/') -replace '\\','/' $hash = Get-Sha256Lower $_.FullName $items += [ordered]@{ Path = $relative Url = "files/$relative" Sha256 = $hash Size = [long]$_.Length } } $manifest = [ordered]@{ Version = $version Files = $items Delete = @($deletes) } if (Test-Path $manifestFile) { Copy-Item $manifestFile "$manifestFile.bak" -Force } $json = $manifest | ConvertTo-Json -Depth 8 $utf8NoBom = New-Object System.Text.UTF8Encoding($false) [System.IO.File]::WriteAllText($manifestFile, $json, $utf8NoBom) try { $check = Get-Content -Raw -Path $manifestFile | ConvertFrom-Json if ([string]$check.Version -ne $version) { throw 'Versao divergente apos gerar o manifesto.' } } catch { if (Test-Path "$manifestFile.bak") { Copy-Item "$manifestFile.bak" $manifestFile -Force } throw "Manifesto gerado falhou na validacao. O anterior foi restaurado. Detalhe: $($_.Exception.Message)" } # Log detalhado. $logPath = Join-Path $logsRoot "patch_${version}_$stamp.txt" $log = New-Object System.Collections.Generic.List[string] $log.Add("Celestial Classic Patch $version") $log.Add("Data: $(Get-Date -Format 'dd/MM/yyyy HH:mm:ss')") $log.Add("Versao anterior: $currentVersion") $log.Add("Arquivos novos: $newCount") $log.Add("Arquivos alterados: $changedCount") $log.Add("Arquivos iguais ignorados: $unchangedCount") $log.Add("Arquivos copiados nesta publicacao: $($copied.Count)") $log.Add("Tamanho estimado do download: $(Format-Bytes $totalPatchBytes)") $log.Add("Arquivos no manifesto cumulativo: $($items.Count)") $log.Add("Exclusoes enviadas ao cliente: $($deletes.Count)") $log.Add('') $log.Add('[PUBLICADOS]') foreach ($x in $copied) { $log.Add($x) } $log.Add('') $log.Add('[IGNORADOS - JA ERAM IGUAIS]') foreach ($x in $skippedSame) { $log.Add($x) } $log.Add('') $log.Add('[DELETE]') foreach ($x in $deletes) { $log.Add($x) } [System.IO.File]::WriteAllLines($logPath, $log, $utf8NoBom) # Limpa NovaPatch depois do sucesso, inclusive arquivos iguais. foreach ($f in $stageFiles) { if (Test-Path $f.FullName) { Remove-Item $f.FullName -Force } } Get-ChildItem -Path $stageRoot -Directory -Recurse | Sort-Object FullName -Descending | ForEach-Object { if (-not (Get-ChildItem -Force -Path $_.FullName | Select-Object -First 1)) { Remove-Item $_.FullName -Force } } Write-Host '' Write-Host '=========================================================' -ForegroundColor DarkYellow Write-Host 'PATCH PUBLICADO COM SUCESSO!' -ForegroundColor Green Write-Host "Versao : $version" -ForegroundColor Cyan Write-Host "Novos : $newCount" -ForegroundColor Green Write-Host "Alterados : $changedCount" -ForegroundColor Yellow Write-Host "Iguais ignorados : $unchangedCount" -ForegroundColor DarkGray Write-Host "Exclusoes : $($deletes.Count)" -ForegroundColor Magenta Write-Host "Download estimado : $(Format-Bytes $totalPatchBytes)" -ForegroundColor Cyan Write-Host "Total no manifest : $($items.Count)" -ForegroundColor Cyan Write-Host "Manifesto : $manifestFile" -ForegroundColor Gray Write-Host "Log : $logPath" -ForegroundColor Gray Write-Host 'NovaPatch foi limpa e esta pronta para o proximo patch.' -ForegroundColor Yellow Write-Host '=========================================================' -ForegroundColor DarkYellow