788 lines
30 KiB
PowerShell
788 lines
30 KiB
PowerShell
<#
|
|
AdHoc-Backup.ps1
|
|
#>
|
|
|
|
#Requires -Version 5.1
|
|
|
|
param(
|
|
[string]$Config,
|
|
[switch]$Quiet
|
|
)
|
|
|
|
Set-StrictMode -Version Latest
|
|
$ErrorActionPreference = 'Stop'
|
|
|
|
try {
|
|
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 -bor [Net.SecurityProtocolType]::Tls13
|
|
} catch {
|
|
try { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 } catch {}
|
|
}
|
|
#region ============================= Percorsi script & conf =========================
|
|
|
|
$ScriptPath = $MyInvocation.MyCommand.Path
|
|
$ScriptDir = Split-Path -Parent $ScriptPath
|
|
$MailScript = Join-Path $ScriptDir 'send-mail.ps1'
|
|
|
|
# Layout dell'installer GUI (se presente): .\bin\{RClone,7Zip,conf}
|
|
$InstallerBinRoot = Join-Path $ScriptDir 'bin'
|
|
$InstallerConfDir = Join-Path $InstallerBinRoot 'conf'
|
|
$InstallerRcloneExe = Join-Path $InstallerBinRoot 'RClone\rclone.exe'
|
|
$Installer7zExe = Join-Path $InstallerBinRoot '7Zip\7z.exe'
|
|
$Installer7zrExe = Join-Path $InstallerBinRoot '7Zip\7zr.exe'
|
|
$InstallerRcloneConf= Join-Path $InstallerConfDir 'rclone.conf'
|
|
|
|
# Se passato -Config usa quello, altrimenti priorità a .\bin\conf\backup.conf, poi .\backup.conf
|
|
if ($PSBoundParameters.ContainsKey('Config') -and -not [string]::IsNullOrWhiteSpace($Config)) {
|
|
try {
|
|
$ConfPath = (Resolve-Path -LiteralPath $Config).Path
|
|
} catch {
|
|
throw "Il file di configurazione specificato con -Config non esiste: $Config"
|
|
}
|
|
} elseif (Test-Path -LiteralPath (Join-Path $InstallerConfDir 'backup.conf')) {
|
|
$ConfPath = Join-Path $InstallerConfDir 'backup.conf'
|
|
} else {
|
|
$ConfPath = Join-Path $ScriptDir 'backup.conf'
|
|
}
|
|
|
|
#endregion ==========================================================================
|
|
|
|
|
|
|
|
#region ============================= Config loader =================================
|
|
|
|
function New-BackupConfTemplate {
|
|
param([string]$Path)
|
|
|
|
$tpl = @"
|
|
# backup.conf - Configurazione backup (esempi)
|
|
BackupRoot=C:\Backups_AdHoc
|
|
# Retention per logs e _sql_stage
|
|
RetentionDays=7
|
|
|
|
# SORGENTI FILE (separa con |). Esempio:
|
|
#ArchiveSources=C:\Zucchetti\ahr90|D:\Dati\Export|\\nas01\vol1\archivi
|
|
ArchiveSources=C:\Zucchetti\ahr90
|
|
|
|
# BACKUP FILE / SQL
|
|
EnableFileBackup=true
|
|
EnableSqlBackup=true
|
|
SqlInstance=localhost\SQLEXPRESS
|
|
SqlUseWindowsAuth=true
|
|
SqlUser=sa
|
|
SqlPassword=YourPassword!
|
|
DbInclude=
|
|
DbExclude=master|model|msdb|tempdb
|
|
SqlCompressStage=true
|
|
SqlDropBakAfterZip=true
|
|
|
|
# 7-ZIP
|
|
SevenZipCompressionLevel=3
|
|
|
|
# RCLONE UPLOAD
|
|
EnableRcloneUpload=true
|
|
RcloneRemoteDest=Backups_AdHoc:Backups/%COMPUTERNAME%
|
|
RcloneBwl=
|
|
RcloneExtraArgs=
|
|
|
|
# CONSERVAZIONE LOCALE
|
|
# true = mantieni copie locali (sposta in \Files e \Databases)
|
|
# false = non mantenere copie locali (cancella gli archivi dopo l'upload o a fine job)
|
|
KeepLocalArchives=true
|
|
|
|
# RETENTION
|
|
LocalRetentionDaysFiles=14
|
|
LocalRetentionDaysDb=30
|
|
RemoteRetentionDays=60
|
|
|
|
# EMAIL
|
|
MailEnabled=true
|
|
MailSmtpHost=relay.poloinformatico.it
|
|
MailSmtpPort=587
|
|
MailUseAuth=true
|
|
MailUser=username@example.com
|
|
MailPassword=PASSWORD
|
|
MailFrom=username@example.com
|
|
MailTo=it@poloinformatico.it
|
|
MailSubjectPref=[BACKUP ADHOC]
|
|
"@
|
|
|
|
$tpl | Out-File -LiteralPath $Path -Encoding UTF8 -Force
|
|
}
|
|
|
|
function ConvertTo-Bool {
|
|
param([string]$s)
|
|
if ($null -eq $s) { return $false }
|
|
switch ($s.Trim().ToLowerInvariant()) {
|
|
'1' { return $true }
|
|
'true' { return $true }
|
|
'yes' { return $true }
|
|
'y' { return $true }
|
|
default { return $false }
|
|
}
|
|
}
|
|
|
|
function ConvertTo-Int {
|
|
param([string]$s,[int]$default=0)
|
|
try {
|
|
if ([string]::IsNullOrWhiteSpace($s)) { $default } else { [int]$s.Trim() }
|
|
} catch { $default }
|
|
}
|
|
|
|
function Split-List {
|
|
param([string]$s)
|
|
if ([string]::IsNullOrWhiteSpace($s)) { @() } else { @($s -split '\|',0 | ForEach-Object { $_.Trim() } | Where-Object { $_ }) }
|
|
}
|
|
|
|
function Expand-Env {
|
|
param([string]$s)
|
|
if ($null -eq $s) { '' } else { [Environment]::ExpandEnvironmentVariables($s) }
|
|
}
|
|
|
|
function Load-Config {
|
|
param([string]$Path)
|
|
if (-not (Test-Path -LiteralPath $Path)) {
|
|
New-BackupConfTemplate -Path $Path
|
|
throw "File di configurazione non trovato. Ho creato un template: $Path. Compilalo e riesegui lo script."
|
|
}
|
|
|
|
$map = @{}
|
|
Get-Content -LiteralPath $Path | ForEach-Object {
|
|
$line = $_.Trim()
|
|
if ($line.Length -eq 0) { return }
|
|
if ($line.StartsWith('#') -or $line.StartsWith(';')) { return }
|
|
$idx = $line.IndexOf('=')
|
|
if ($idx -lt 1) { return }
|
|
$key = $line.Substring(0, $idx).Trim()
|
|
$val = $line.Substring( $idx + 1).Trim()
|
|
$val = ($val -replace '[\s]+#.*$', '').Trim()
|
|
if (($val.StartsWith('"') -and $val.EndsWith('"')) -or ($val.StartsWith("'") -and $val.EndsWith("'"))) {
|
|
$val = $val.Substring(1, $val.Length - 2)
|
|
}
|
|
$map[$key] = $val
|
|
}
|
|
|
|
$script:BackupRoot = Expand-Env ($map['BackupRoot']); if ([string]::IsNullOrWhiteSpace($script:BackupRoot)) { throw "Parametro obbligatorio mancante: BackupRoot" }
|
|
$script:RetentionDays = ConvertTo-Int ($map['RetentionDays'])
|
|
|
|
$script:ArchiveSources = Split-List ($map['ArchiveSources'])
|
|
|
|
$script:EnableFileBackup = if ($map.ContainsKey('EnableFileBackup')) { ConvertTo-Bool ($map['EnableFileBackup']) } else { $true }
|
|
$script:EnableSqlBackup = ConvertTo-Bool ($map['EnableSqlBackup'])
|
|
$script:SqlInstance = Expand-Env ($map['SqlInstance'])
|
|
$script:SqlUseWindowsAuth = ConvertTo-Bool ($map['SqlUseWindowsAuth'])
|
|
$script:SqlUser = $map['SqlUser']
|
|
$script:SqlPassword = $map['SqlPassword']
|
|
$script:DbInclude = Split-List ($map['DbInclude'])
|
|
$script:DbExclude = if ($map.ContainsKey('DbExclude')) { Split-List ($map['DbExclude']) } else { @('master','model','msdb','tempdb') }
|
|
$script:SqlCompressStage = ConvertTo-Bool ($map['SqlCompressStage'])
|
|
$script:SqlDropBakAfterZip = ConvertTo-Bool ($map['SqlDropBakAfterZip'])
|
|
|
|
if ($EnableSqlBackup) {
|
|
if ([string]::IsNullOrWhiteSpace($SqlInstance)) { throw "SqlInstance obbligatorio quando EnableSqlBackup=true" }
|
|
if (-not $SqlUseWindowsAuth -and ([string]::IsNullOrWhiteSpace($SqlUser) -or [string]::IsNullOrWhiteSpace($SqlPassword))) {
|
|
throw "SqlUser/SqlPassword obbligatori quando SqlUseWindowsAuth=false"
|
|
}
|
|
}
|
|
|
|
$script:SevenZipCompressionLevel = ConvertTo-Int ($map['SevenZipCompressionLevel'])
|
|
|
|
$script:EnableRcloneUpload = if ($map.ContainsKey('EnableRcloneUpload')) { ConvertTo-Bool ($map['EnableRcloneUpload']) } else { $true }
|
|
$script:RcloneRemoteDest = Expand-Env ($map['RcloneRemoteDest'])
|
|
$script:RcloneBwl = Expand-Env ($map['RcloneBwl'])
|
|
$script:RcloneExtraArgs = Split-List ($map['RcloneExtraArgs'])
|
|
|
|
# Conservazione locale
|
|
$script:KeepLocalArchives = if ($map.ContainsKey('KeepLocalArchives')) { ConvertTo-Bool ($map['KeepLocalArchives']) } else { $true }
|
|
|
|
# Retention knobs
|
|
$script:LocalRetentionDaysFiles = if ($map.ContainsKey('LocalRetentionDaysFiles')) { ConvertTo-Int ($map['LocalRetentionDaysFiles']) } else { 14 }
|
|
$script:LocalRetentionDaysDb = if ($map.ContainsKey('LocalRetentionDaysDb')) { ConvertTo-Int ($map['LocalRetentionDaysDb']) } else { 30 }
|
|
$script:RemoteRetentionDays = if ($map.ContainsKey('RemoteRetentionDays')) { ConvertTo-Int ($map['RemoteRetentionDays']) } else { -1 }
|
|
|
|
$script:MailEnabled = ConvertTo-Bool ($map['MailEnabled'])
|
|
$script:MailSmtpHost = $map['MailSmtpHost']
|
|
$script:MailSmtpPort = ConvertTo-Int ($map['MailSmtpPort'], 587)
|
|
$script:MailUseAuth = ConvertTo-Bool ($map['MailUseAuth'])
|
|
$script:MailUser = $map['MailUser']
|
|
$script:MailPassword = $map['MailPassword']
|
|
$script:MailFrom = $map['MailFrom']
|
|
$script:MailTo = Split-List ($map['MailTo'])
|
|
$script:MailSubjectPref = $map['MailSubjectPref']
|
|
}
|
|
Load-Config -Path $ConfPath
|
|
|
|
#endregion ==========================================================================
|
|
|
|
|
|
#region ============================= Paths, Globals ================================
|
|
|
|
function Now-IT { (Get-Date).ToString('dd-MM-yyyy HH:mm:ss') }
|
|
|
|
$DateStamp = (Get-Date).ToString('dd-MM-yyyy_HHmmss')
|
|
$HostName = $env:COMPUTERNAME
|
|
|
|
$Paths = [ordered]@{
|
|
Root = $BackupRoot
|
|
Bin = Join-Path $BackupRoot 'Bin'
|
|
Bin7z = Join-Path $BackupRoot 'Bin\7zip'
|
|
BinRclone = Join-Path $BackupRoot 'Bin\RClone'
|
|
Logs = Join-Path $BackupRoot 'logs'
|
|
Out = Join-Path $BackupRoot 'out'
|
|
SqlStage = Join-Path $BackupRoot '_sql_stage'
|
|
StoreFiles = Join-Path $BackupRoot 'Files'
|
|
StoreDb = Join-Path $BackupRoot 'Databases'
|
|
}
|
|
|
|
$Files = [ordered]@{
|
|
Log = Join-Path $Paths.Logs ("backup_$DateStamp.log")
|
|
SevenZipExe = Join-Path $Paths.Bin7z '7z.exe'
|
|
SevenZip7zr = Join-Path $Paths.Bin7z '7zr.exe'
|
|
RcloneExe = Join-Path $Paths.BinRclone 'rclone.exe'
|
|
RcloneZip = Join-Path $Paths.BinRclone 'rclone-current-windows-amd64.zip'
|
|
RcloneConf = Join-Path $Paths.BinRclone 'rclone.conf'
|
|
}
|
|
|
|
#endregion ==========================================================================# Preferisci gli strumenti nel layout dell'installer GUI, se presenti
|
|
if (Test-Path -LiteralPath $Installer7zExe) { $Files.SevenZipExe = $Installer7zExe }
|
|
elseif (Test-Path -LiteralPath $Installer7zrExe) { $Files.SevenZip7zr = $Installer7zrExe }
|
|
|
|
if (Test-Path -LiteralPath $InstallerRcloneExe) { $Files.RcloneExe = $InstallerRcloneExe }
|
|
|
|
# rclone.conf: prima quello in .\bin\conf, altrimenti quello sotto BackupRoot\Bin
|
|
if (Test-Path -LiteralPath $InstallerRcloneConf) { $Files.RcloneConf = $InstallerRcloneConf }
|
|
|
|
|
|
|
|
|
|
#region ============================= Utils & Logging ===============================
|
|
|
|
function Ensure-Dir {
|
|
param([string]$Path)
|
|
if (-not (Test-Path -LiteralPath $Path)) {
|
|
New-Item -ItemType Directory -Path $Path -Force | Out-Null
|
|
}
|
|
}
|
|
|
|
function Write-Log {
|
|
param(
|
|
[ValidateSet('INFO','WARN','ERROR')][string]$Level,
|
|
[string]$Message
|
|
)
|
|
$line = "[{0}] [{1}] {2}" -f (Now-IT), $Level, $Message
|
|
if (-not $Quiet) { Write-Host $line }
|
|
|
|
# Usa SEMPRE lo scope di script per il file log
|
|
Add-Content -LiteralPath $script:Files.Log -Value $line
|
|
}
|
|
|
|
function Download-File {
|
|
param(
|
|
[Parameter(Mandatory=$true)][string]$Url,
|
|
[Parameter(Mandatory=$true)][string]$Destination
|
|
)
|
|
Write-Log -Level INFO -Message "Scarico: $Url -> $Destination"
|
|
try {
|
|
$wc = New-Object System.Net.WebClient
|
|
$wc.DownloadFile($Url, $Destination)
|
|
} catch {
|
|
throw "Download fallito: $Url - $_"
|
|
}
|
|
if (-not (Test-Path -LiteralPath $Destination)) {
|
|
throw "Download non riuscito (manca $Destination)"
|
|
}
|
|
}
|
|
|
|
#endregion ==========================================================================
|
|
|
|
|
|
#region ============================= Tooling: 7-Zip ================================
|
|
|
|
$SevenZipPortableUrl = 'https://www.7-zip.org/a/7zr.exe'
|
|
|
|
function Ensure-7Zip {
|
|
Ensure-Dir $Paths.Bin7z
|
|
if (Test-Path -LiteralPath $Files.SevenZipExe) {
|
|
Write-Log INFO "7-Zip: $($Files.SevenZipExe)"
|
|
return $Files.SevenZipExe
|
|
}
|
|
if (Test-Path -LiteralPath $Files.SevenZip7zr) {
|
|
Write-Log INFO "7zr: $($Files.SevenZip7zr)"
|
|
return $Files.SevenZip7zr
|
|
}
|
|
Write-Log INFO "Scarico 7zr.exe..."
|
|
Download-File -Url $SevenZipPortableUrl -Destination $Files.SevenZip7zr
|
|
if (Test-Path -LiteralPath $Files.SevenZip7zr) { return $Files.SevenZip7zr }
|
|
throw "Impossibile preparare 7-Zip in $($Paths.Bin7z)."
|
|
}
|
|
|
|
function Invoke-7ZipArchive {
|
|
param(
|
|
[Parameter(Mandatory=$true)][string]$ArchivePath,
|
|
[Parameter(Mandatory=$true)][string[]]$InputPaths
|
|
)
|
|
|
|
$exe = Ensure-7Zip
|
|
|
|
# Argomenti non-interattivi: -y (assume Yes), -bd (no progress), -bso0/-bsp0 (silenzio), -bse1 (errori)
|
|
$a7zCommon = @('-y','-bd','-bso0','-bsp0','-bse1')
|
|
|
|
if ($exe -like '*\7zr.exe') {
|
|
$a7z = @('a',$ArchivePath) + $InputPaths + @("-mx=$SevenZipCompressionLevel") + $a7zCommon
|
|
} else {
|
|
$a7z = @('a','-t7z',"-mx=$SevenZipCompressionLevel",'-mmt=on','-r',$ArchivePath) + $InputPaths + $a7zCommon
|
|
}
|
|
|
|
Write-Log INFO "7-Zip cmd: `"$exe`" $($a7z -join ' ')"
|
|
|
|
& $exe @a7z
|
|
$ec = $LASTEXITCODE
|
|
|
|
switch ($ec) {
|
|
0 { Write-Log INFO "7-Zip OK" }
|
|
1 { Write-Log WARN "7-Zip WARNING (ExitCode=1)" }
|
|
default { throw "Errore compressione (ExitCode=$ec)" }
|
|
}
|
|
|
|
if (-not (Test-Path -LiteralPath $ArchivePath)) { throw "Archivio non creato: $ArchivePath" }
|
|
return $ArchivePath
|
|
}
|
|
|
|
#endregion ==========================================================================
|
|
|
|
|
|
#region ============================= Tooling: rclone ================================
|
|
|
|
function Ensure-Rclone {
|
|
Ensure-Dir $Paths.BinRclone
|
|
if (Test-Path -LiteralPath $Files.RcloneExe) {
|
|
Write-Log INFO "rclone: $($Files.RcloneExe)"
|
|
return $Files.RcloneExe
|
|
}
|
|
$url='https://downloads.rclone.org/rclone-current-windows-amd64.zip'
|
|
Write-Log INFO "Scarico rclone..."
|
|
Download-File -Url $url -Destination $Files.RcloneZip
|
|
try { Add-Type -AssemblyName System.IO.Compression.FileSystem | Out-Null } catch {}
|
|
[System.IO.Compression.ZipFile]::ExtractToDirectory($Files.RcloneZip, $Paths.BinRclone)
|
|
$found = Get-ChildItem -LiteralPath $Paths.BinRclone -Recurse -Filter 'rclone.exe' | Select-Object -First 1
|
|
if (-not $found) { throw "rclone.exe non trovato nello zip." }
|
|
Copy-Item -LiteralPath $found.FullName -Destination $Files.RcloneExe -Force
|
|
Get-ChildItem -LiteralPath $Paths.BinRclone -Directory | ForEach-Object { Remove-Item $_.FullName -Recurse -Force }
|
|
Write-Log INFO "rclone pronto: $($Files.RcloneExe)"
|
|
return $Files.RcloneExe
|
|
}
|
|
|
|
function Ensure-RcloneConfigOrLaunch {
|
|
if (-not (Test-Path -LiteralPath $Files.RcloneConf)) {
|
|
Write-Log WARN "rclone.conf assente. Apro 'rclone config' in una nuova finestra..."
|
|
$rclone = Ensure-Rclone
|
|
$cmd = " & '$rclone' --config '$($Files.RcloneConf)' config "
|
|
Start-Process -FilePath "powershell.exe" -ArgumentList "-NoExit","-Command",$cmd -WindowStyle Normal | Out-Null
|
|
throw "Completa la configurazione nella finestra aperta, poi riesegui lo script."
|
|
}
|
|
}
|
|
|
|
function Invoke-RcloneCopyTo {
|
|
param([string]$LocalFile,[string]$RemoteDest)
|
|
$rclone = Ensure-Rclone
|
|
$rArgs = @('copyto',$LocalFile,$RemoteDest,'--config',$Files.RcloneConf,'--transfers','4','--checkers','8','--retries','3','--low-level-retries','10','--stats','10s')
|
|
if ($RcloneBwl) { $rArgs += @('--bwlimit',$RcloneBwl) }
|
|
if ($RcloneExtraArgs -and $RcloneExtraArgs.Count -gt 0) { $rArgs += $RcloneExtraArgs }
|
|
Write-Log INFO "rclone copyto `"$LocalFile`" -> `"$RemoteDest`""
|
|
$p = Start-Process -FilePath $rclone -ArgumentList $rArgs -PassThru -Wait -NoNewWindow
|
|
if ($p.ExitCode -ne 0) { throw "rclone copyto fallito (ExitCode=$($p.ExitCode)) per $LocalFile" }
|
|
}
|
|
|
|
function Apply-RemoteRetention {
|
|
param([int]$Days)
|
|
if ($Days -lt 0) { return }
|
|
if (-not $EnableRcloneUpload) { return }
|
|
$rclone = Ensure-Rclone
|
|
$age = "{0}d" -f $Days
|
|
$delArgs = @('delete',$RcloneRemoteDest,'--config',$Files.RcloneConf,'--min-age',$age,'--fast-list')
|
|
if ($RcloneExtraArgs -and $RcloneExtraArgs.Count -gt 0) { $delArgs += $RcloneExtraArgs }
|
|
Write-Log INFO "rclone retention remoto: delete --min-age $age su $RcloneRemoteDest"
|
|
$p1 = Start-Process -FilePath $rclone -ArgumentList $delArgs -PassThru -Wait -NoNewWindow
|
|
if ($p1.ExitCode -ne 0) { Write-Log WARN "rclone delete (retention) ExitCode=$($p1.ExitCode)" }
|
|
|
|
$rdArgs = @('rmdirs',$RcloneRemoteDest,'--config',$Files.RcloneConf,'--leave-root','--fast-list')
|
|
if ($RcloneExtraArgs -and $RcloneExtraArgs.Count -gt 0) { $rdArgs += $RcloneExtraArgs }
|
|
$p2 = Start-Process -FilePath $rclone -ArgumentList $rdArgs -PassThru -Wait -NoNewWindow
|
|
if ($p2.ExitCode -ne 0) { Write-Log WARN "rclone rmdirs (retention) ExitCode=$($p2.ExitCode)" }
|
|
}
|
|
|
|
#endregion ==========================================================================
|
|
|
|
|
|
#region ============================= SQL Backup ====================================
|
|
|
|
function Resolve-Sqlcmd {
|
|
$cmd = Get-Command sqlcmd.exe -ErrorAction SilentlyContinue
|
|
if ($cmd -and (Test-Path -LiteralPath $cmd.Path)) { return $cmd.Path }
|
|
foreach ($root in @('C:\Program Files\Microsoft SQL Server\*','C:\Program Files (x86)\Microsoft SQL Server\*')) {
|
|
$paths = Get-ChildItem -Path $root -Directory -ErrorAction SilentlyContinue | ForEach-Object { $_.FullName }
|
|
foreach ($p in $paths) {
|
|
$maybe = Join-Path $p 'Tools\Binn\sqlcmd.exe'
|
|
if (Test-Path -LiteralPath $maybe) { return $maybe }
|
|
$maybe2 = Join-Path $p 'Client SDK\ODBC\*\Tools\Binn\SQLCMD.EXE'
|
|
$hit = Get-ChildItem -Path $maybe2 -ErrorAction SilentlyContinue | Select-Object -First 1
|
|
if ($hit) { return $hit.FullName }
|
|
}
|
|
}
|
|
return $null
|
|
}
|
|
|
|
function Build-SqlcmdArgs {
|
|
param([bool]$WindowsAuth,[string]$User,[string]$Password,[string]$Instance,[string]$InputFile,[string]$InlineQuery,[bool]$NoHeaderForQuery=$true)
|
|
|
|
$list = @()
|
|
|
|
if ($WindowsAuth) {
|
|
$list += '-E'
|
|
} else {
|
|
$list += @('-U',"$User",'-P',"$Password")
|
|
}
|
|
|
|
if (-not [string]::IsNullOrWhiteSpace($Instance)) {
|
|
$list += @('-S',"$Instance")
|
|
}
|
|
|
|
if (-not [string]::IsNullOrWhiteSpace($InputFile)) {
|
|
$list += @('-i',"$InputFile")
|
|
}
|
|
|
|
if (-not [string]::IsNullOrWhiteSpace($InlineQuery)) {
|
|
if ($NoHeaderForQuery) {
|
|
$list += @('-h','-1','-W')
|
|
}
|
|
$list += @('-Q',"$InlineQuery")
|
|
}
|
|
|
|
$list = @($list | ForEach-Object { if ($_ -ne $null) { $_.ToString().Trim() } else { $null } } | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
|
|
|
|
return ,$list
|
|
}
|
|
|
|
function Log-SqlcmdArgsSafe {
|
|
param([string[]]$Args)
|
|
$disp = @()
|
|
for ($i=0; $i -lt $Args.Count; $i++) {
|
|
if ($Args[$i] -eq '-P' -and ($i+1) -lt $Args.Count) {
|
|
$disp += '-P *****'
|
|
$i++
|
|
} else {
|
|
$disp += $Args[$i]
|
|
}
|
|
}
|
|
Write-Log INFO ("sqlcmd args: " + ($disp -join ' '))
|
|
}
|
|
|
|
function Get-OnlineUserDatabases {
|
|
param([string]$SqlcmdExe)
|
|
$sql = "SET NOCOUNT ON; SELECT name FROM sys.databases WHERE state = 0 AND database_id > 4 ORDER BY name;"
|
|
$sqlArgs = Build-SqlcmdArgs -WindowsAuth:$SqlUseWindowsAuth -User $SqlUser -Password $SqlPassword -Instance $SqlInstance -InlineQuery $sql -NoHeaderForQuery:$true
|
|
Log-SqlcmdArgsSafe -Args $sqlArgs
|
|
$res = @(& $SqlcmdExe @sqlArgs 2>$null)
|
|
if ($LASTEXITCODE -ne 0) { throw "sqlcmd fallito nell'enumerazione DB." }
|
|
$names = @()
|
|
foreach ($line in @($res)) {
|
|
$n = ($line | ForEach-Object { $_.Trim() })
|
|
if ($n) { $names += $n }
|
|
}
|
|
return $names
|
|
}
|
|
|
|
function Backup-SqlDatabases {
|
|
Ensure-Dir $Paths.SqlStage
|
|
try {
|
|
Write-Log INFO "Concedo '(OI)(CI)M' su '$($Paths.SqlStage)' a Everyone."
|
|
$acl = Get-Acl $Paths.SqlStage
|
|
$rule = New-Object System.Security.AccessControl.FileSystemAccessRule('Everyone','Modify','ContainerInherit, ObjectInherit','None','Allow')
|
|
$acl.SetAccessRule($rule); Set-Acl -LiteralPath $Paths.SqlStage -AclObject $acl
|
|
} catch { Write-Log WARN "ACL non impostate su $($Paths.SqlStage): $_" }
|
|
|
|
$sqlcmd = Resolve-Sqlcmd
|
|
if (-not $sqlcmd) { throw "sqlcmd.exe non trovato (PATH o Client Tools mancanti)." }
|
|
Write-Log INFO "sqlcmd: $sqlcmd"
|
|
|
|
$dbList = @()
|
|
if (@($DbInclude).Count -gt 0) {
|
|
$dbList = @($DbInclude)
|
|
} else {
|
|
$dbList = @(Get-OnlineUserDatabases -SqlcmdExe $sqlcmd)
|
|
if (@($DbExclude).Count -gt 0) {
|
|
$dbList = @($dbList | Where-Object { @($DbExclude) -notcontains $_ })
|
|
}
|
|
}
|
|
|
|
if (@($dbList).Count -eq 0) {
|
|
Write-Log WARN "Nessuna DB utente online trovata da backup."
|
|
return $null
|
|
}
|
|
Write-Log INFO ("DB trovati: " + (@($dbList) -join ', '))
|
|
|
|
$bakPaths = @()
|
|
foreach ($db in @($dbList)) {
|
|
$bak = Join-Path $Paths.SqlStage ("{0}_{1}.bak" -f $db, $DateStamp)
|
|
$sql = @"
|
|
BACKUP DATABASE [$db]
|
|
TO DISK = N'$bak'
|
|
WITH COPY_ONLY, INIT, SKIP, NOFORMAT, NAME = N'$db-FULL-$DateStamp', STATS=5;
|
|
GO
|
|
"@
|
|
$tmpSql = Join-Path $Paths.SqlStage ("backup_{0}.sql" -f $db)
|
|
$sql | Out-File -LiteralPath $tmpSql -Encoding ASCII -Force
|
|
|
|
Write-Log INFO "BACKUP [$db] -> $bak"
|
|
$sqlArgs = Build-SqlcmdArgs -WindowsAuth:$SqlUseWindowsAuth -User $SqlUser -Password $SqlPassword -Instance $SqlInstance -InputFile $tmpSql
|
|
Log-SqlcmdArgsSafe -Args $sqlArgs
|
|
& $sqlcmd @sqlArgs
|
|
if ($LASTEXITCODE -ne 0) { throw "Errore backup DB [$db] (sqlcmd ExitCode=$LASTEXITCODE)." }
|
|
if (-not (Test-Path -LiteralPath $bak)) { throw "File .bak non trovato dopo backup: $bak" }
|
|
|
|
$bakPaths += $bak
|
|
Remove-Item -LiteralPath $tmpSql -Force -ErrorAction SilentlyContinue
|
|
}
|
|
|
|
if ($SqlCompressStage -and $bakPaths.Count -gt 0) {
|
|
Ensure-Dir $Paths.Out
|
|
$sqlArchive = Join-Path $Paths.Out ("SQL_{0}_{1}.7z" -f $HostName, $DateStamp)
|
|
Invoke-7ZipArchive -ArchivePath $sqlArchive -InputPaths @($Paths.SqlStage)
|
|
if ($SqlDropBakAfterZip) {
|
|
Get-ChildItem -LiteralPath $Paths.SqlStage -File -Filter '*.bak' -ErrorAction SilentlyContinue | Remove-Item -Force -ErrorAction SilentlyContinue
|
|
}
|
|
return $sqlArchive
|
|
} else {
|
|
# Non compresso: .bak resta in _sql_stage (gestito da retention)
|
|
return $null
|
|
}
|
|
}
|
|
|
|
#endregion ==========================================================================
|
|
|
|
|
|
#region ============================= File/Folder Archiving =========================
|
|
|
|
function Archive-FileSources {
|
|
$sources = @()
|
|
foreach ($s in @($ArchiveSources)) {
|
|
$s2 = $s.Trim('"')
|
|
if (-not (Test-Path -LiteralPath $s2)) { Write-Log WARN "Sorgente non trovata: $s2 (ignoro)"; continue }
|
|
$sources += $s2
|
|
}
|
|
$sources = $sources | Select-Object -Unique
|
|
if (@($sources).Count -eq 0) {
|
|
Write-Log INFO "Nessuna sorgente file valida per l'archiviazione."
|
|
return $null
|
|
}
|
|
Ensure-Dir $Paths.Out
|
|
$archive = Join-Path $Paths.Out ("FILES_{0}_{1}.7z" -f $HostName, $DateStamp)
|
|
Invoke-7ZipArchive -ArchivePath $archive -InputPaths $sources
|
|
return $archive
|
|
}
|
|
|
|
#endregion ==========================================================================
|
|
|
|
|
|
#region ============================= Move / Cleanup out =============================
|
|
|
|
function Get-OutFiles {
|
|
if (-not (Test-Path -LiteralPath $Paths.Out)) { return @() }
|
|
return @(Get-ChildItem -LiteralPath $Paths.Out -File -ErrorAction SilentlyContinue)
|
|
}
|
|
|
|
function Move-OutFiles-ToStores {
|
|
$outFiles = Get-OutFiles
|
|
$moved = 0
|
|
if ($outFiles.Count -eq 0) { return 0 }
|
|
Ensure-Dir $Paths.StoreFiles
|
|
Ensure-Dir $Paths.StoreDb
|
|
foreach ($f in $outFiles) {
|
|
$destDir = if ($f.Name -like 'FILES_*') { $Paths.StoreFiles } else { $Paths.StoreDb }
|
|
$dest = Join-Path $destDir $f.Name
|
|
try { Move-Item -LiteralPath $f.FullName -Destination $dest -Force; $moved++; Write-Log INFO "Spostato: $($f.Name) -> $destDir" } catch { Write-Log WARN "Move fallito per $($f.Name): $_" }
|
|
}
|
|
return $moved
|
|
}
|
|
|
|
function Delete-OutFiles {
|
|
$outFiles = Get-OutFiles
|
|
$deleted = 0
|
|
foreach ($f in $outFiles) {
|
|
try { Remove-Item -LiteralPath $f.FullName -Force -ErrorAction Stop; $deleted++ } catch { Write-Log WARN "Delete fallito per $($f.Name): $_" }
|
|
}
|
|
if ($deleted -gt 0) { Write-Log INFO "Eliminati da 'out': $deleted file" }
|
|
}
|
|
|
|
function Empty-OutFolder {
|
|
if (-not (Test-Path -LiteralPath $Paths.Out)) { return }
|
|
try {
|
|
Get-ChildItem -LiteralPath $Paths.Out -File -ErrorAction SilentlyContinue | ForEach-Object {
|
|
try { Remove-Item -LiteralPath $_.FullName -Force -ErrorAction Stop } catch {}
|
|
}
|
|
Get-ChildItem -LiteralPath $Paths.Out -Directory -ErrorAction SilentlyContinue | ForEach-Object {
|
|
try { Remove-Item -LiteralPath $_.FullName -Recurse -Force -ErrorAction Stop } catch {}
|
|
}
|
|
Write-Log INFO "Cartella 'out' svuotata."
|
|
} catch {
|
|
Write-Log WARN "Impossibile svuotare 'out': $_"
|
|
}
|
|
}
|
|
|
|
#endregion ==========================================================================
|
|
|
|
|
|
#region ============================= Retention =====================================
|
|
|
|
function Apply-LocalArchivesRetention {
|
|
param([int]$DaysFiles,[int]$DaysDb)
|
|
|
|
if ($DaysFiles -ge 0) {
|
|
foreach ($dir in @($Paths.StoreFiles)) {
|
|
if (-not (Test-Path -LiteralPath $dir)) { continue }
|
|
$cut = (Get-Date).AddDays(-$DaysFiles)
|
|
Get-ChildItem -LiteralPath $dir -File -ErrorAction SilentlyContinue |
|
|
Where-Object { $_.LastWriteTime -lt $cut } | ForEach-Object {
|
|
try {
|
|
Remove-Item -LiteralPath $_.FullName -Force -ErrorAction Stop
|
|
Write-Log INFO "Retention FILES: eliminato $($_.FullName)"
|
|
} catch {
|
|
Write-Log WARN "Retention FILES: non riesco a eliminare $($_.FullName): $_"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($DaysDb -ge 0) {
|
|
foreach ($dir in @($Paths.StoreDb)) {
|
|
if (-not (Test-Path -LiteralPath $dir)) { continue }
|
|
$cut = (Get-Date).AddDays(-$DaysDb)
|
|
Get-ChildItem -LiteralPath $dir -File -ErrorAction SilentlyContinue |
|
|
Where-Object { $_.LastWriteTime -lt $cut } | ForEach-Object {
|
|
try {
|
|
Remove-Item -LiteralPath $_.FullName -Force -ErrorAction Stop
|
|
Write-Log INFO "Retention DB: eliminato $($_.FullName)"
|
|
} catch {
|
|
Write-Log WARN "Retention DB: non riesco a eliminare $($_.FullName): $_"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function Apply-GenericRetention {
|
|
param([int]$DaysToKeep)
|
|
if ($DaysToKeep -lt 0) { return }
|
|
$cut = (Get-Date).AddDays(-$DaysToKeep)
|
|
foreach ($p in @($Paths.Logs, $Paths.SqlStage)) {
|
|
if (-not (Test-Path -LiteralPath $p)) { continue }
|
|
Get-ChildItem -LiteralPath $p -Recurse -File -ErrorAction SilentlyContinue |
|
|
Where-Object { $_.LastWriteTime -lt $cut } | ForEach-Object {
|
|
try {
|
|
Remove-Item -LiteralPath $_.FullName -Force -ErrorAction Stop
|
|
Write-Log INFO "Retention (generico): eliminato $($_.FullName)"
|
|
} catch {
|
|
Write-Log WARN "Retention (generico): impossibile eliminare $($_.FullName): $_"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#endregion ==========================================================================
|
|
|
|
|
|
|
|
|
|
#region ============================= MAIN ==========================================
|
|
|
|
foreach ($d in @($Paths.Root,$Paths.Bin,$Paths.Bin7z,$Paths.BinRclone,$Paths.Logs,$Paths.Out,$Paths.SqlStage,$Paths.StoreFiles,$Paths.StoreDb)) { Ensure-Dir $d }
|
|
|
|
Write-Log INFO ("==== Avvio backup {0} ====" -f $DateStamp)
|
|
Write-Log INFO ("BackupRoot: {0} | KeepLocal: {1} | Ret(logs/stage): {2} | Ret(Files): {3} | Ret(DB): {4} | RemoteRet: {5}" -f $BackupRoot, $KeepLocalArchives, $RetentionDays, $LocalRetentionDaysFiles, $LocalRetentionDaysDb, $RemoteRetentionDays)
|
|
|
|
$summary = New-Object System.Text.StringBuilder
|
|
$summary.AppendLine("Host: $HostName") | Out-Null
|
|
$summary.AppendLine("Start: $(Now-IT)") | Out-Null
|
|
$summary.AppendLine("") | Out-Null
|
|
|
|
$hadWarning = $false
|
|
|
|
try {
|
|
# Preflight tool
|
|
$need7z = ($EnableFileBackup -eq $true) -or (($EnableSqlBackup -eq $true) -and ($SqlCompressStage -eq $true))
|
|
if ($need7z) { $null = Ensure-7Zip }
|
|
|
|
if ($EnableRcloneUpload) {
|
|
$null = Ensure-Rclone
|
|
Ensure-RcloneConfigOrLaunch
|
|
} else {
|
|
Write-Log INFO "Upload rclone disattivato da config."
|
|
}
|
|
|
|
# SQL
|
|
if ($EnableSqlBackup) {
|
|
$sqlArchive = Backup-SqlDatabases
|
|
if ($sqlArchive) { $summary.AppendLine("Archivio SQL: $sqlArchive") | Out-Null }
|
|
else { $summary.AppendLine("SQL: nessun archivio creato (vedi log).") | Out-Null }
|
|
} else {
|
|
Write-Log INFO "Backup SQL disattivato."
|
|
}
|
|
|
|
# Files
|
|
if ($EnableFileBackup) {
|
|
$filesArchive = Archive-FileSources
|
|
if ($filesArchive) { $summary.AppendLine("Archivio FILES: $filesArchive") | Out-Null }
|
|
} else {
|
|
Write-Log INFO "Backup file disattivato."
|
|
}
|
|
|
|
# Rileva cosa c'è davvero in \out
|
|
$outList = Get-OutFiles
|
|
Write-Log INFO ("File in 'out': " + $outList.Count)
|
|
if ($outList.Count -eq 0) { Write-Log WARN "Nessun archivio in 'out' da processare." }
|
|
|
|
# Upload
|
|
if ($EnableRcloneUpload -and $outList.Count -gt 0) {
|
|
if ([string]::IsNullOrWhiteSpace($RcloneRemoteDest) -or ($RcloneRemoteDest -notmatch ':')) {
|
|
throw "RcloneRemoteDest non valido in backup.conf. Esempio: 's3aruba:bucket/path'."
|
|
}
|
|
foreach ($f in $outList) {
|
|
$remote = ($RcloneRemoteDest.TrimEnd('/') + '/' + $DateStamp + '/' + $f.Name)
|
|
Invoke-RcloneCopyTo -LocalFile $f.FullName -RemoteDest $remote
|
|
}
|
|
$summary.AppendLine("Upload completati con rclone verso: $RcloneRemoteDest") | Out-Null
|
|
}
|
|
|
|
# ULTIMO: Move/Delete da out
|
|
if ($KeepLocalArchives) {
|
|
$m = Move-OutFiles-ToStores
|
|
Write-Log INFO "Spostati in locale: $m file"
|
|
Empty-OutFolder
|
|
} else {
|
|
Delete-OutFiles
|
|
Empty-OutFolder
|
|
}
|
|
|
|
# Retention
|
|
if ($EnableRcloneUpload) { Apply-RemoteRetention -Days $RemoteRetentionDays }
|
|
Apply-LocalArchivesRetention -DaysFiles $LocalRetentionDaysFiles -DaysDb $LocalRetentionDaysDb
|
|
Apply-GenericRetention -DaysToKeep $RetentionDays
|
|
|
|
$status = if ($hadWarning) { 'COMPLETATO CON WARNING' } else { 'COMPLETATO' }
|
|
Write-Log INFO "Backup $status."
|
|
$summary.Insert(0, "ESITO: $status`r`n") | Out-Null
|
|
|
|
if (Test-Path -LiteralPath $MailScript) { . $MailScript -Subject ($MailSubjectPref + "OK " + $HostName) -Body $summary.ToString() } else { Write-Log WARN "Script di invio mail non trovato: $MailScript" }
|
|
|
|
} catch {
|
|
$msg = $_.Exception.Message
|
|
Write-Log ERROR $msg
|
|
$summary.Insert(0, "ESITO: FALLITO`r`n") | Out-Null
|
|
$summary.AppendLine("") | Out-Null
|
|
$summary.AppendLine("ERRORE: $msg") | Out-Null
|
|
if (Test-Path -LiteralPath $MailScript) { . $MailScript -Subject ($MailSubjectPref + "ERRORE " + $HostName) -Body $summary.ToString() } else { Write-Log WARN "Script di invio mail non trovato: $MailScript" }
|
|
throw
|
|
} finally {
|
|
$summary.AppendLine("") | Out-Null
|
|
$summary.AppendLine("Fine: $(Now-IT)") | Out-Null
|
|
}
|
|
#endregion ========================================================================== |