Backup-AdHoc/install-AdHoc-Backup.ps1

459 lines
20 KiB
PowerShell
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<# =====================================================================
install-AdHoc-Backup.ps1
- Menù interattivo:
1) Installa lo script e l'ambiente
2) Crea pianificazione (Scheduled Task)
3) Modifica configurazione (backup.conf) con guida in italiano
Requisiti:
- Eseguire come Amministratore per creare la pianificazione con SYSTEM.
- Tenere nella stessa cartella:
- AdHoc-Backup.ps1
- backup.conf
Note:
- Rispettate le chiavi del tuo backup.conf e il suo "manuale" (liste con |,
commenti con #, ecc.). Alcune spiegazioni sono riportate nei prompt.
- Rclone: viene posizionato in $BackupRoot\RClone\rclone.exe. Se non presente,
lo script prova a copiarlo da installazioni esistenti o può scaricarlo.
===================================================================== #>
[CmdletBinding()]
param(
[string]$ConfigPath, # Facoltativo: percorso a backup.conf (default: .\backup.conf)
[string]$BackupRootOverride # Facoltativo: forza BackupRoot (altrimenti legge da backup.conf)
)
$ErrorActionPreference = 'Stop'
Set-StrictMode -Version Latest
function Write-Title($text) {
Write-Host ""
Write-Host "=== $text ===" -ForegroundColor Cyan
}
function Pause-Enter($msg="Premi INVIO per continuare...") {
Write-Host ""
Read-Host $msg | Out-Null
}
function Read-Default($prompt, $default) {
if ([string]::IsNullOrEmpty($default)) {
return Read-Host "$prompt"
} else {
$ans = Read-Host "$prompt [$default]"
if ([string]::IsNullOrEmpty($ans)) { return $default }
return $ans
}
}
function Read-YesNo($prompt, [bool]$default=$true) {
$defTxt = if ($default) { "S/n" } else { "s/N" }
while ($true) {
$ans = Read-Host "$prompt ($defTxt)"
if ([string]::IsNullOrWhiteSpace($ans)) { return $default }
switch ($ans.ToLower()) {
's' { return $true }
'si' { return $true }
'y' { return $true }
'n' { return $false }
'no' { return $false }
default { Write-Host "Inserisci 's' o 'n'." -ForegroundColor Yellow }
}
}
}
function Test-Admin {
$id = [Security.Principal.WindowsIdentity]::GetCurrent()
$p = New-Object Security.Principal.WindowsPrincipal($id)
return $p.IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator)
}
# ---------- Config I/O (preserva commenti e ordine) ----------
class ConfigDoc {
[System.Collections.Generic.List[string]]$Lines
[hashtable]$Map
[hashtable]$Index
ConfigDoc() {
$this.Lines = [System.Collections.Generic.List[string]]::new()
$this.Map = @{}
$this.Index = @{}
}
}
function Load-Config([string]$path) {
if (-not (Test-Path -LiteralPath $path)) {
throw "File di configurazione non trovato: $path"
}
$doc = [ConfigDoc]::new()
$raw = Get-Content -LiteralPath $path -Raw -Encoding UTF8 -ErrorAction Stop
foreach ($ln in ($raw -split "`r?`n", [System.StringSplitOptions]::None)) {
$doc.Lines.Add($ln)
}
for ($i=0; $i -lt $doc.Lines.Count; $i++) {
$line = $doc.Lines[$i]
if ($line -match '^\s*#') { continue }
if ($line -match '^\s*([A-Za-z0-9_]+)\s*=(.*)$') {
$k = $matches[1]
$v = ($matches[2]).Trim()
$doc.Map[$k] = $v
$doc.Index[$k] = $i
}
}
return $doc
}
function Save-Config([ConfigDoc]$doc, [string]$path) {
$backup = "$path.bak_$(Get-Date -Format 'yyyyMMdd_HHmmss')"
Copy-Item -LiteralPath $path -Destination $backup -Force -ErrorAction Stop
# Riscrivo solo le linee delle chiavi (preservo commenti/righe vuote)
foreach ($k in $doc.Map.Keys) {
$v = [string]$doc.Map[$k]
if ($doc.Index.ContainsKey($k)) {
$idx = [int]$doc.Index[$k]
$doc.Lines[$idx] = "$k=$v"
} else {
# Aggiungo nuove chiavi alla fine
if (-not $doc.Lines[$doc.Lines.Count-1].StartsWith('# AGGIUNTE AUTO')) {
$doc.Lines.Add("")
$doc.Lines.Add("# AGGIUNTE AUTO (create da install-AdHoc-Backup.ps1)")
}
$doc.Lines.Add("$k=$v")
}
}
[IO.File]::WriteAllLines($path, $doc.Lines, [Text.UTF8Encoding]::new($false))
return $backup
}
function Set-ConfigValue([ConfigDoc]$doc, [string]$key, [string]$value) {
$doc.Map[$key] = $value
}
# ---------- Validatori ----------
function Validate-IntNonNeg($valText, [int]$min=0, [int]$max=[int]::MaxValue) {
if ([string]::IsNullOrWhiteSpace($valText)) { return $null } # mantieni
[int]$n = 0
if (-not [int]::TryParse($valText, [ref]$n)) { throw "Numero non valido." }
if ($n -lt $min -or $n -gt $max) { throw "Valore fuori range ($min..$max)." }
return $n
}
function Validate-Port($valText) { return (Validate-IntNonNeg $valText 1 65535) }
function Normalize-BoolText($text, [bool]$current) {
if ([string]::IsNullOrWhiteSpace($text)) { return $current }
switch ($text.Trim().ToLower()) {
'true' { return $true }
'false' { return $false }
's' { return $true }
'si' { return $true }
'y' { return $true }
'n' { return $false }
'no' { return $false }
default { throw "Valore non valido. Usa: true/false oppure s/n" }
}
}
function Read-Secret($prompt, $currentPlain) {
Write-Host "$prompt (lascia vuoto per mantenere l'attuale)" -ForegroundColor Yellow
$sec = Read-Host -AsSecureString
if ($sec.Length -eq 0) { return $currentPlain }
return [Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($sec))
}
# ---------- Editor guidato backup.conf ----------
function Edit-BackupConfig {
param([string]$cfgPath)
Write-Title "Modifica configurazione (backup.conf)"
$doc = Load-Config $cfgPath
# Mappa variabili -> descrizione & validazione
$vars = @(
@{Key='BackupRoot'; Desc='Cartella radice dei backup (archivi, log, tool).'; Example='C:\Backups_AdHoc' },
@{Key='LocalRetentionDaysFiles'; Desc='Giorni di retention dei file in locale (\Files). 0=disabilita.'; Validate='int' },
@{Key='LocalRetentionDaysDb'; Desc='Giorni di retention dei database in locale (\Databases). 0=disabilita.'; Validate='int' },
@{Key='RemoteRetentionDays'; Desc='Giorni di retention su remoto (rclone).'; Validate='int' },
@{Key='KeepLocalArchives'; Desc='true=mantiene copie locali, false=le rimuove dopo upload.'; Validate='bool' },
@{Key='EnableFileBackup'; Desc='true=abilita backup di cartelle/sorgenti in .7z.'; Validate='bool' },
@{Key='EnableRcloneUpload'; Desc='true=abilita upload dei backup con rclone.'; Validate='bool' },
@{Key='ArchiveSources'; Desc='Sorgenti da archiviare, separa con | (es.: C:\Dati|D:\Export|\\nas\share).'; Example='C:\Zucchetti\ahr90|C:\Zucchetti\NetSetup' },
@{Key='EnableSqlBackup'; Desc='true=abilita backup dei DB SQL Server.'; Validate='bool' },
@{Key='SqlInstance'; Desc='Istanza SQL (es.: localhost, .\SQLEXPRESS, 192.168.1.10,1433, nome\istanza).'},
@{Key='SqlUseWindowsAuth'; Desc='true=Windows Authentication; false=SQL Auth (usa SqlUser/SqlPassword).'; Validate='bool' },
@{Key='SqlUser'; Desc='Utente SQL (usato solo se SqlUseWindowsAuth=false).'},
@{Key='SqlPassword'; Desc='Password SQL (usata solo se SqlUseWindowsAuth=false).'; Secret=$true },
@{Key='DbInclude'; Desc='Elenco DB da includere (|). Vuoto = auto-detect dei DB utente online.'; Example='DBProduzione|DBCRM|DBContabilita' },
@{Key='DbExclude'; Desc='DB da escludere se DbInclude è vuoto (default: master|model|msdb|tempdb).', Example='master|model|msdb|tempdb' },
@{Key='SqlCompressStage'; Desc='true= comprime la cartella _sql_stage in .7z dopo il backup.'; Validate='bool' },
@{Key='SqlDropBakAfterZip'; Desc='true= elimina i .bak dopo la compressione.'; Validate='bool' },
@{Key='SevenZipCompressionLevel'; Desc='Livello compressione 7-Zip (0..9). 13 spesso è il miglior compromesso.'; Validate='int' },
@{Key='RcloneRemoteDest'; Desc='Destinazione rclone in formato REMOTE:percorso (usa %COMPUTERNAME% se utile).'; Example='dropbox:/Backups_AdHoc/%COMPUTERNAME%' },
@{Key='RcloneBwl'; Desc='Limitazione di banda (es: 10M). Vuoto = nessun limite.'},
@{Key='RcloneExtraArgs'; Desc='Argomenti extra rclone separati da | (es: --fast-list|--s3-chunk-size=64M).'},
@{Key='MailEnabled'; Desc='true= invia una mail di report a fine job.'; Validate='bool' },
@{Key='MailSmtpHost'; Desc='Host SMTP (relay).'},
@{Key='MailSmtpPort'; Desc='Porta SMTP (tipico 587).'; Validate='port' },
@{Key='MailUseAuth'; Desc='true= il relay richiede autenticazione (compila utente/password).'; Validate='bool' },
@{Key='MailUser'; Desc='Utente SMTP (se richiesto).'},
@{Key='MailPassword'; Desc='Password SMTP (se richiesta).'; Secret=$true },
@{Key='MailFrom'; Desc='Indirizzo mittente.'},
@{Key='MailTo'; Desc='Destinatari separati da |.'; Example='it@azienda.it|sysadmin@azienda.it' },
@{Key='MailSubjectPref'; Desc='Prefisso oggetto (puoi lasciare spazio finale).' }
)
$changes = @{}
foreach ($v in $vars) {
$key = $v.Key
$cur = $doc.Map[$key]
Write-Host ""
Write-Host ">> $key" -ForegroundColor Green
Write-Host " $($v.Desc)"
if ($v.ContainsKey('Example') -and $v.Example) { Write-Host " Esempio: $($v.Example)" -ForegroundColor DarkGray }
if ($v.ContainsKey('Secret') -and $v.Secret) {
$newVal = Read-Secret "Nuovo valore per $key" $cur
} else {
$newValRaw = Read-Default "Inserisci un nuovo valore per $key (INVIO = mantieni)" $cur
# Validazione
if ($v.ContainsKey('Validate')) {
switch ($v.Validate) {
'int' { $n = Validate-IntNonNeg $newValRaw | Out-Null; if ($null -ne $n) { $newValRaw = [string]$n } }
'bool' { $b = Normalize-BoolText $newValRaw ($cur -eq 'true'); $newValRaw = if ($b) { 'true' } else { 'false' } }
'port' { $p = Validate-Port $newValRaw | Out-Null; if ($null -ne $p) { $newValRaw = [string]$p } }
}
}
$newVal = $newValRaw
}
if ($newVal -ne $cur) {
$changes[$key] = @{Old=$cur; New=$newVal}
Set-ConfigValue -doc $doc -key $key -value $newVal
}
}
if ($changes.Count -gt 0) {
Write-Host ""
Write-Title "Riepilogo modifiche"
$changes.GetEnumerator() | ForEach-Object {
"{0} : '{1}' -> '{2}'" -f $_.Key, $_.Value.Old, $_.Value.New
} | Write-Host
if (Read-YesNo "Confermi il salvataggio delle modifiche?") {
$bk = Save-Config -doc $doc -path $cfgPath
Write-Host "Configurazione salvata: $cfgPath" -ForegroundColor Cyan
Write-Host "Backup creato: $bk" -ForegroundColor DarkCyan
} else {
Write-Host "Annullato. Nessuna modifica scritta." -ForegroundColor Yellow
}
} else {
Write-Host "Nessuna modifica apportata." -ForegroundColor Yellow
}
Pause-Enter
}
# ---------- Installazione ambiente ----------
function Ensure-Folder($path) {
if (-not (Test-Path -LiteralPath $path)) {
New-Item -ItemType Directory -Path $path -Force | Out-Null
}
}
function Install-Environment {
param([string]$cfgPath)
Write-Title "Installazione ambiente"
$doc = Load-Config $cfgPath
# Determina BackupRoot
$backupRoot = if ($BackupRootOverride) { $BackupRootOverride } elseif ($doc.Map['BackupRoot']) { $doc.Map['BackupRoot'] } else { 'C:\Backups_AdHoc' }
$backupRoot = Read-Default "BackupRoot di destinazione" $backupRoot
Set-ConfigValue -doc $doc -key 'BackupRoot' -value $backupRoot
$null = Save-Config -doc $doc -path $cfgPath
# Struttura cartelle
$folders = @(
$backupRoot,
Join-Path $backupRoot 'Logs',
Join-Path $backupRoot 'Out',
Join-Path $backupRoot 'Files',
Join-Path $backupRoot 'Databases',
Join-Path $backupRoot '_sql_stage',
Join-Path $backupRoot 'Bin',
Join-Path $backupRoot 'RClone'
)
foreach ($f in $folders) { Ensure-Folder $f }
# Copia script principali
$srcScript = Join-Path $PSScriptRoot 'AdHoc-Backup.ps1'
if (-not (Test-Path -LiteralPath $srcScript)) {
throw "AdHoc-Backup.ps1 non trovato in $PSScriptRoot"
}
Copy-Item -LiteralPath $srcScript -Destination (Join-Path $backupRoot 'AdHoc-Backup.ps1') -Force
# Copia backup.conf
Copy-Item -LiteralPath $cfgPath -Destination (Join-Path $backupRoot 'backup.conf') -Force
# Rclone: binario + config locale nello stesso folder ($BackupRoot\RClone)
Ensure-Rclone -TargetRoot $backupRoot
Write-Host "Installazione completata in: $backupRoot" -ForegroundColor Cyan
Pause-Enter
}
function Ensure-Rclone {
param([string]$TargetRoot)
$rcloneDir = Join-Path $TargetRoot 'RClone'
$rcloneExe = Join-Path $rcloneDir 'rclone.exe'
$rcloneConf = Join-Path $rcloneDir 'rclone.conf'
Ensure-Folder $rcloneDir
if (-not (Test-Path -LiteralPath $rcloneExe)) {
Write-Host "rclone.exe non trovato in $rcloneDir, provo a reperirlo..." -ForegroundColor Yellow
$candidates = @(
"$env:ProgramFiles\rclone\rclone.exe",
"$env:ProgramFiles\Rclone\rclone.exe",
"$env:ProgramFiles(x86)\rclone\rclone.exe",
"$env:ProgramFiles(x86)\Rclone\rclone.exe",
"$env:SystemRoot\System32\rclone.exe"
) | Where-Object { Test-Path -LiteralPath $_ }
if ($candidates.Count -gt 0) {
Copy-Item -LiteralPath $candidates[0] -Destination $rcloneExe -Force
Write-Host "Copiato rclone da: $($candidates[0])" -ForegroundColor Green
}
else {
# Tentativo di download (puoi saltarlo se non vuoi traffico Internet)
try {
$zipUrl = "https://downloads.rclone.org/rclone-current-windows-amd64.zip"
$tmpZip = Join-Path $env:TEMP "rclone-current.zip"
Write-Host "Scarico rclone da $zipUrl ..." -ForegroundColor Yellow
Invoke-WebRequest -Uri $zipUrl -OutFile $tmpZip -UseBasicParsing
$tmpDir = Join-Path $env:TEMP "rclone_unzip_$([guid]::NewGuid().ToString('N'))"
Expand-Archive -LiteralPath $tmpZip -DestinationPath $tmpDir -Force
$found = Get-ChildItem -Path $tmpDir -Filter rclone.exe -Recurse | Select-Object -First 1
if ($null -ne $found) {
Copy-Item -LiteralPath $found.FullName -Destination $rcloneExe -Force
Write-Host "rclone scaricato e installato." -ForegroundColor Green
} else {
Write-Host "Impossibile trovare rclone.exe nello ZIP. Mettilo manualmente in $rcloneDir" -ForegroundColor Red
}
} catch {
Write-Host "Download rclone fallito: $($_.Exception.Message)" -ForegroundColor Red
Write-Host "Puoi copiare manualmente rclone.exe in $rcloneDir" -ForegroundColor Yellow
}
}
}
# Config rclone: importa quella utente se esiste, altrimenti crea skeleton
if (-not (Test-Path -LiteralPath $rcloneConf)) {
$userConf = Join-Path $env:APPDATA 'rclone\rclone.conf'
if (Test-Path -LiteralPath $userConf) {
Copy-Item -LiteralPath $userConf -Destination $rcloneConf -Force
Write-Host "Importata configurazione rclone da $userConf" -ForegroundColor Green
} else {
@"
# rclone.conf (skeleton) definisci qui il REMOTE usato in backup.conf (RcloneRemoteDest)
# Esempio dropbox:
# [dropbox]
# type = dropbox
"@ | Set-Content -LiteralPath $rcloneConf -Encoding UTF8 -NoNewline
Write-Host "Creato skeleton rclone.conf in $rcloneConf" -ForegroundColor Yellow
}
}
# NB: il nostro backup script userà --config "$BackupRoot\RClone\rclone.conf" come da linea guida.
}
# ---------- Pianificazione ----------
function Create-Schedule {
param([string]$cfgPath)
if (-not (Test-Admin)) {
throw "Per creare la pianificazione occorrono privilegi di Amministratore."
}
$doc = Load-Config $cfgPath
$backupRoot = if ($BackupRootOverride) { $BackupRootOverride } elseif ($doc.Map['BackupRoot']) { $doc.Map['BackupRoot'] } else { 'C:\Backups_AdHoc' }
$scriptPath = Join-Path $backupRoot 'AdHoc-Backup.ps1'
$confPath = Join-Path $backupRoot 'backup.conf'
if (-not (Test-Path -LiteralPath $scriptPath)) { throw "Script backup non trovato in $scriptPath (esegui prima l'installazione)." }
if (-not (Test-Path -LiteralPath $confPath)) { throw "Configurazione non trovata in $confPath (esegui prima l'installazione)." }
Write-Title "Crea pianificazione (Scheduled Task)"
$taskName = Read-Default "Nome attività" "AdHoc Backup Giornaliero"
# Ora (HH:mm)
while ($true) {
$timeTxt = Read-Default "Orario giornaliero (HH:mm)" "22:30"
if ([TimeSpan]::TryParse($timeTxt, [ref]([TimeSpan]$null))) { break }
try { [DateTime]::ParseExact($timeTxt, 'HH:mm', $null) | Out-Null; break } catch { Write-Host "Formato non valido. Usa HH:mm (es. 22:30)" -ForegroundColor Yellow }
}
$atTime = [DateTime]::ParseExact($timeTxt, 'HH:mm', $null).TimeOfDay
$trigger = New-ScheduledTaskTrigger -Daily -At $atTime
$psArgs = "-NoProfile -ExecutionPolicy Bypass -File `"$scriptPath`" -Conf `"$confPath`""
$action = New-ScheduledTaskAction -Execute "powershell.exe" -Argument $psArgs
$principal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount -RunLevel Highest
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable `
-RestartCount 3 -RestartInterval (New-TimeSpan -Minutes 5) `
-MultipleInstances IgnoreNew
Register-ScheduledTask -TaskName $taskName -Action $action -Trigger $trigger -Principal $principal -Settings $settings -Force | Out-Null
Write-Host "Attività pianificata creata: $taskName, orario $timeTxt (account SYSTEM)." -ForegroundColor Cyan
Pause-Enter
}
# ---------- Menù ----------
function Show-Menu {
Clear-Host
Write-Host ""
Write-Host "==========================" -ForegroundColor DarkCyan
Write-Host " INSTALL AZIENDALE BACKUP" -ForegroundColor DarkCyan
Write-Host "==========================" -ForegroundColor DarkCyan
Write-Host "1) Installa lo script e l'ambiente"
Write-Host "2) Crea pianificazione (Scheduled Task)"
Write-Host "3) Modifica configurazione (backup.conf)"
Write-Host "4) Esci"
Write-Host ""
}
# Percorso di default per backup.conf
if (-not $ConfigPath) { $ConfigPath = Join-Path $PSScriptRoot 'backup.conf' }
while ($true) {
Show-Menu
$choice = Read-Default "Seleziona un'opzione" "1"
switch ($choice) {
'1' {
Install-Environment -cfgPath $ConfigPath
}
'2' {
Create-Schedule -cfgPath $ConfigPath
}
'3' {
Edit-BackupConfig -cfgPath $ConfigPath
}
'4' { break }
default {
Write-Host "Opzione non valida." -ForegroundColor Yellow
Pause-Enter
}
}
}
Write-Host "Uscita." -ForegroundColor DarkGray