From bb1284cb64380a58586765561bd81b5ece0b1c40 Mon Sep 17 00:00:00 2001 From: Mattia Tadini Date: Mon, 10 Nov 2025 11:02:45 +0000 Subject: [PATCH] Update install-AdHoc-Backup.ps1 --- install-AdHoc-Backup.ps1 | 708 ++++++++++++++++----------------------- 1 file changed, 298 insertions(+), 410 deletions(-) diff --git a/install-AdHoc-Backup.ps1 b/install-AdHoc-Backup.ps1 index 56d79bc..ff29742 100644 --- a/install-AdHoc-Backup.ps1 +++ b/install-AdHoc-Backup.ps1 @@ -1,458 +1,346 @@ -<# ===================================================================== - 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 +<# + install-AdHoc-Backup.ps1 + Menù di installazione / pianificazione / configurazione per AdHoc-Backup.ps1 +#> - Requisiti: - - Eseguire come Amministratore per creare la pianificazione con SYSTEM. - - Tenere nella stessa cartella: - - AdHoc-Backup.ps1 - - backup.conf +# ========================== +# CONFIGURAZIONE INIZIALE +# ========================== +# Cartella dove installeremo script e config +$InstallRoot = "C:\polo\script" +# Nome dello script di backup vero e proprio +$BackupScriptName = "AdHoc-Backup.ps1" +# Nome del file di configurazione +$ConfigFileName = "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. -===================================================================== #> +# (OPZIONALE) Base URL da cui scaricare i file +# Metti l'URL del tuo webserver / share http se li distribuisci così +$DownloadBaseUrl = "" # es. "https://intranet/polo/scripts" -[CmdletBinding()] -param( - [string]$ConfigPath, # Facoltativo: percorso a backup.conf (default: .\backup.conf) - [string]$BackupRootOverride # Facoltativo: forza BackupRoot (altrimenti legge da backup.conf) +# Elenco dei file da scaricare in modalità "installa lo script" +$FilesToDownload = @( + $BackupScriptName, + $ConfigFileName ) -$ErrorActionPreference = 'Stop' -Set-StrictMode -Version Latest +# ========================== +# FUNZIONI DI SUPPORTO +# ========================== -function Write-Title($text) { - Write-Host "" - Write-Host "=== $text ===" -ForegroundColor Cyan +function Ensure-Folder { + param([string]$Path) + if (-not (Test-Path $Path)) { + Write-Host "Creo la cartella $Path ..." -ForegroundColor Yellow + New-Item -ItemType Directory -Path $Path -Force | Out-Null + } } -function Pause-Enter($msg="Premi INVIO per continuare...") { - Write-Host "" - Read-Host $msg | Out-Null +function Download-File { + param( + [string]$Url, + [string]$Destination + ) + if ([string]::IsNullOrWhiteSpace($Url)) { + Write-Warning "URL non impostato per $Destination, salto il download." + return + } + try { + Write-Host "Scarico $Url -> $Destination" + Invoke-WebRequest -Uri $Url -OutFile $Destination -UseBasicParsing + } + catch { + Write-Warning "Impossibile scaricare $Url. Errore: $($_.Exception.Message)" + } } -function Read-Default($prompt, $default) { - if ([string]::IsNullOrEmpty($default)) { - return Read-Host "$prompt" +function Read-YesNo { + param( + [string]$Message, + [bool]$Default = $true + ) + if ($Default) { + $msg = "$Message [S/n]" } else { - $ans = Read-Host "$prompt [$default]" - if ([string]::IsNullOrEmpty($ans)) { return $default } - return $ans + $msg = "$Message [s/N]" } + $ans = Read-Host $msg + if ([string]::IsNullOrWhiteSpace($ans)) { + return $Default + } + $ans = $ans.Trim().ToLower() + return $ans.StartsWith("s") } -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 Get-ConfigValue { + param( + [string]$Path, + [string]$Key + ) + if (-not (Test-Path $Path)) { return "" } + $line = Get-Content $Path | Where-Object { $_ -match "^\s*$([regex]::Escape($Key))\s*=" } | Select-Object -First 1 + if ($null -eq $line) { return "" } + return ($line -split "=",2)[1].Trim() +} + +function Set-ConfigValue { + param( + [string]$Path, + [string]$Key, + [string]$Value + ) + $Value = $Value.Trim() + if (-not (Test-Path $Path)) { + "$Key=$Value" | Set-Content -Path $Path -Encoding UTF8 + return + } + $content = Get-Content $Path + $found = $false + for ($i = 0; $i -lt $content.Count; $i++) { + if ($content[$i] -match "^\s*$([regex]::Escape($Key))\s*=") { + $content[$i] = "$Key=$Value" + $found = $true + break } } + if (-not $found) { + $content += "$Key=$Value" + } + $content | Set-Content -Path $Path -Encoding UTF8 } -function Test-Admin { - $id = [Security.Principal.WindowsIdentity]::GetCurrent() - $p = New-Object Security.Principal.WindowsPrincipal($id) - return $p.IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator) +function Ensure-DefaultConfig { + param([string]$ConfigPath) + + if (Test-Path $ConfigPath) { return } + + Write-Host "File di configurazione non trovato, ne creo uno di base..." -ForegroundColor Yellow + + @" +# backup.conf generato automaticamente +# Modificalo con l'opzione 3 del menù. + +BackupRoot=C:\Backups_AdHoc +LocalRetentionDaysFiles=2 +LocalRetentionDaysDb=2 +RemoteRetentionDays=15 +KeepLocalArchives=true +EnableFileBackup=true +EnableRcloneUpload=true +ArchiveSources=C:\Zucchetti\ahr90|C:\Zucchetti\NetSetup + +EnableSqlBackup=true +SqlInstance=localhost\SQLEXPRESS +SqlUseWindowsAuth=true +SqlUser=sa +SqlPassword= + +DbInclude= +DbExclude=master|model|msdb|tempdb +SqlCompressStage=true +SqlDropBakAfterZip=true + +SevenZipCompressionLevel=1 + +RcloneRemoteDest=dropbox:/Backups_AdHoc/%COMPUTERNAME% +RcloneBwl= +RcloneExtraArgs= + +MailEnabled=true +MailSmtpHost=relay.poloinformatico.it +MailSmtpPort=587 +MailUseAuth=true +MailUser= +MailPassword= +MailFrom=backup@localhost +MailTo=it@poloinformatico.it +MailSubjectPref=[BACKUP ADHOC] +"@ | Set-Content -Path $ConfigPath -Encoding UTF8 } -# ---------- 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 = @{} - } -} +# ========================== +# FUNZIONE 1: INSTALLA +# ========================== +function Install-AdHocBackup { + Write-Host "== INSTALLAZIONE SCRIPT BACKUP ==" -ForegroundColor Cyan + Ensure-Folder -Path $InstallRoot -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" + foreach ($f in $FilesToDownload) { + $dest = Join-Path $InstallRoot $f + if (-not [string]::IsNullOrWhiteSpace($DownloadBaseUrl)) { + $url = ($DownloadBaseUrl.TrimEnd('/')) + "/" + $f + Download-File -Url $url -Destination $dest } 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)") + # Se non abbiamo URL, non sovrascriviamo: assumiamo che lo script sia già nella stessa cartella + if (-not (Test-Path $dest)) { + Write-Warning "File $f non trovato e nessun URL impostato. Copialo manualmente in $InstallRoot." } - $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 -} + $configPath = Join-Path $InstallRoot $ConfigFileName + Ensure-DefaultConfig -ConfigPath $configPath -# ---------- 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" } + # Chiede subito se vuole pianificare + if (Read-YesNo "Vuoi creare una pianificazione (operazione consigliata)?" $true) { + Show-ScheduleMenu } + + # Chiede subito se vuole modificare configurazione + if (Read-YesNo "Vuoi modificare adesso la configurazione del file backup.conf?" $false) { + Edit-BackupConfig -ConfigPath $configPath + } + + Write-Host "Installazione completata." -ForegroundColor Green } -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). 1–3 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).' } +# ========================== +# FUNZIONE 2: PIANIFICAZIONE +# ========================== +function Create-BackupScheduledTask { + param( + [ValidateSet("Daily","Weekly")] + [string]$Mode = "Daily" ) - $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 - } + $backupScriptPath = Join-Path $InstallRoot $BackupScriptName + if (-not (Test-Path $backupScriptPath)) { + Write-Warning "Lo script di backup non è stato trovato in $backupScriptPath. Installa prima (opzione 1)." + return } - 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 + # Ora di esecuzione + $hour = Read-Host "Inserisci l'ora di esecuzione (0-23) [default 22]" + if ([string]::IsNullOrWhiteSpace($hour)) { $hour = 22 } + $minute = Read-Host "Inserisci i minuti (0-59) [default 30]" + if ([string]::IsNullOrWhiteSpace($minute)) { $minute = 30 } - 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 - } + $time = New-TimeSpan -Hours ([int]$hour) -Minutes ([int]$minute) + $at = (Get-Date).Date + $time + + if ($Mode -eq "Weekly") { + $day = Read-Host "Giorno della settimana (Monday,Tuesday,Wednesday,Thursday,Friday,Saturday,Sunday) [default Monday]" + if ([string]::IsNullOrWhiteSpace($day)) { $day = "Monday" } + $trigger = New-ScheduledTaskTrigger -Weekly -DaysOfWeek $day -At $at } else { - Write-Host "Nessuna modifica apportata." -ForegroundColor Yellow + $trigger = New-ScheduledTaskTrigger -Daily -At $at } - 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 - + $action = New-ScheduledTaskAction -Execute "powershell.exe" -Argument "-NoProfile -ExecutionPolicy Bypass -File `"$backupScriptPath`"" + # Eseguiamo come SYSTEM con privilegi alti $principal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount -RunLevel Highest - $settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable ` - -RestartCount 3 -RestartInterval (New-TimeSpan -Minutes 5) ` - -MultipleInstances IgnoreNew + $settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries + + $taskName = "Backup AdHoc" 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 + Write-Host "Pianificazione '$taskName' creata in modalità $Mode." -ForegroundColor Green } -# ---------- 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" +function Show-ScheduleMenu { + Write-Host "== CREA PIANIFICAZIONE ==" -ForegroundColor Cyan + Write-Host "1) Giornaliera" + Write-Host "2) Settimanale" + Write-Host "0) Annulla" + $choice = Read-Host "Seleziona" 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 - } + "1" { Create-BackupScheduledTask -Mode Daily } + "2" { Create-BackupScheduledTask -Mode Weekly } + default { Write-Host "Nessuna pianificazione creata." } } } -Write-Host "Uscita." -ForegroundColor DarkGray +# ========================== +# FUNZIONE 3: MODIFICA CONFIG +# ========================== +function Edit-BackupConfig { + param( + [string]$ConfigPath + ) + + if (-not (Test-Path $ConfigPath)) { + Write-Warning "File $ConfigPath non trovato. Lo creo adesso." + Ensure-DefaultConfig -ConfigPath $ConfigPath + } + + # Descrizioni in italiano per aiutare l'utente + $Descriptions = [ordered]@{ + "BackupRoot" = "Cartella radice dove vanno archivi, log e tool." + "LocalRetentionDaysFiles"= "Giorni di conservazione FILE in locale." + "LocalRetentionDaysDb" = "Giorni di conservazione DATABASE in locale." + "RemoteRetentionDays" = "Giorni di conservazione su destinazione remota (rclone)." + "KeepLocalArchives" = "true = tiene una copia locale dopo l'upload." + "EnableFileBackup" = "true = esegue il backup delle cartelle in ArchiveSources." + "EnableRcloneUpload" = "true = dopo il backup carica su cloud tramite rclone." + "ArchiveSources" = "Elenco cartelle/sorgenti da salvare, separate da | (pipe)." + "EnableSqlBackup" = "true = fa anche i backup dei database SQL Server." + "SqlInstance" = "Nome o indirizzo dell'istanza SQL Server (es. localhost\SQLEXPRESS)." + "SqlUseWindowsAuth" = "true = usa l'utente Windows corrente; false = usa SqlUser/SqlPassword." + "SqlUser" = "Utente SQL (se SqlUseWindowsAuth=false)." + "SqlPassword" = "Password SQL (se SqlUseWindowsAuth=false)." + "DbInclude" = "Elenco DB da includere (se vuoto li prende tutti tranne gli esclusi)." + "DbExclude" = "DB da escludere quando DbInclude è vuoto." + "SqlCompressStage" = "true = comprime i .bak in uno .7z." + "SqlDropBakAfterZip" = "true = elimina i .bak dopo la compressione." + "SevenZipCompressionLevel" = "0..9 livello di compressione 7z (1=veloce, 9=max)." + "RcloneRemoteDest" = "Destinazione rclone: REMOTO:percorso." + "RcloneBwl" = "Limite banda rclone (es. 10M) oppure vuoto." + "RcloneExtraArgs" = "Argomenti extra rclone separati da |." + "MailEnabled" = "true = invia mail di report." + "MailSmtpHost" = "Server SMTP/relay." + "MailSmtpPort" = "Porta SMTP (es. 587)." + "MailUseAuth" = "true = il relay richiede utente/password." + "MailUser" = "Utente SMTP." + "MailPassword" = "Password SMTP." + "MailFrom" = "Mittente mail." + "MailTo" = "Destinatari (separati da |)." + "MailSubjectPref" = "Prefisso dell'oggetto mail." + } + + Write-Host "== MODIFICA GUIDATA backup.conf ==" -ForegroundColor Cyan + foreach ($key in $Descriptions.Keys) { + $current = Get-ConfigValue -Path $ConfigPath -Key $key + Write-Host "" + Write-Host "$key" -ForegroundColor Yellow + Write-Host " $($Descriptions[$key])" + Write-Host " Valore attuale: $current" + $newVal = Read-Host " Nuovo valore (invio per lasciare quello attuale)" + if (-not [string]::IsNullOrWhiteSpace($newVal)) { + Set-ConfigValue -Path $ConfigPath -Key $key -Value $newVal + Write-Host " -> Aggiornato." + } else { + Write-Host " -> Lasciato invariato." + } + } + + Write-Host "" + Write-Host "Configurazione aggiornata in $ConfigPath" -ForegroundColor Green +} + +# ========================== +# MENÙ PRINCIPALE +# ========================== +do { + Write-Host "" + Write-Host "==============================" -ForegroundColor DarkCyan + Write-Host " INSTALL ADHOC BACKUP - MENU" + Write-Host "==============================" -ForegroundColor DarkCyan + Write-Host "1) Installa / aggiorna script" + Write-Host "2) Crea pianificazione (giornaliera/settimanale)" + Write-Host "3) Modifica configurazione backup.conf" + Write-Host "0) Esci" + $sel = Read-Host "Seleziona" + + $configPath = Join-Path $InstallRoot $ConfigFileName + + switch ($sel) { + "1" { Install-AdHocBackup } + "2" { Show-ScheduleMenu } + "3" { Edit-BackupConfig -ConfigPath $configPath } + "0" { break } + default { Write-Host "Scelta non valida." -ForegroundColor Red } + } + +} while ($true) + +Write-Host "Uscita dallo script." -ForegroundColor Cyan