Перейти к содержимому

Как сделать проверку бэкапов mssql через Powershell 7 и dbatools.

Задача автоматизировать проверку бэкапов mssql . 

Основные возможности:

  1. Сканирование структуры бэкапов — автоматическое обнаружение резервных копий баз данных в заданной папке

  2. Гибкая настройка именования — возможность переименования баз при восстановлении

  3. Управление дисковым пространством — автоматическое освобождение места при необходимости

  4. Параллельное восстановление — одновременное восстановление нескольких баз для ускорения процесса

  5. Проверка целостности — автоматическая проверка восстановленных баз (DBCC CHECKDB)

  6. Логирование и отчетность — детальное логирование и отправка отчетов по email

  7. Удаление старых баз — различные стратегии удаления существующих баз перед восстановлением

Режимы управления пространством:

  • Calculate/Smart — удаляет только минимально необходимые базы

  • All — удаляет все базы (кроме системных)

  • None — не удаляет ничего

Основные этапы выполнения:

  1. Инициализация — загрузка модулей, проверка параметров

  2. Сканирование — поиск резервных копий в указанной папке

  3. Анализ места — проверка свободного дискового пространства

  4. Удаление баз (опционально) — освобождение места при необходимости

  5. Восстановление — параллельное восстановление баз данных

  6. Проверка — проверка целостности восстановленных баз

  7. Отчетность — логирование и отправка отчетов

  8. Очистка — удаление старых логов

Создадим базу для мониторинга выполнения скрипта

USE [master]
GO

/****** Object:  Database [dba]    Script Date: 24.12.2025 12:52:53 ******/
CREATE DATABASE [dba]
 CONTAINMENT = NONE
 ON  PRIMARY 
( NAME = N'dba', FILENAME = N'C:\Program Files\Microsoft SQL Server\MSSQL14.MSSQLSERVER\MSSQL\DATA\dba.mdf' , SIZE = 73728KB , MAXSIZE = UNLIMITED, FILEGROWTH = 65536KB )
 LOG ON 
( NAME = N'dba_log', FILENAME = N'C:\Program Files\Microsoft SQL Server\MSSQL14.MSSQLSERVER\MSSQL\DATA\dba_log.ldf' , SIZE = 73728KB , MAXSIZE = 2048GB , FILEGROWTH = 65536KB )
GO

IF (1 = FULLTEXTSERVICEPROPERTY('IsFullTextInstalled'))
begin
EXEC [dba].[dbo].[sp_fulltext_database] @action = 'enable'
end
GO

ALTER DATABASE [dba] SET ANSI_NULL_DEFAULT OFF 
GO

ALTER DATABASE [dba] SET ANSI_NULLS OFF 
GO

ALTER DATABASE [dba] SET ANSI_PADDING OFF 
GO

ALTER DATABASE [dba] SET ANSI_WARNINGS OFF 
GO

ALTER DATABASE [dba] SET ARITHABORT OFF 
GO

ALTER DATABASE [dba] SET AUTO_CLOSE OFF 
GO

ALTER DATABASE [dba] SET AUTO_SHRINK OFF 
GO

ALTER DATABASE [dba] SET AUTO_UPDATE_STATISTICS ON 
GO

ALTER DATABASE [dba] SET CURSOR_CLOSE_ON_COMMIT OFF 
GO

ALTER DATABASE [dba] SET CURSOR_DEFAULT  GLOBAL 
GO

ALTER DATABASE [dba] SET CONCAT_NULL_YIELDS_NULL OFF 
GO

ALTER DATABASE [dba] SET NUMERIC_ROUNDABORT OFF 
GO

ALTER DATABASE [dba] SET QUOTED_IDENTIFIER OFF 
GO

ALTER DATABASE [dba] SET RECURSIVE_TRIGGERS OFF 
GO

ALTER DATABASE [dba] SET  ENABLE_BROKER 
GO

ALTER DATABASE [dba] SET AUTO_UPDATE_STATISTICS_ASYNC OFF 
GO

ALTER DATABASE [dba] SET DATE_CORRELATION_OPTIMIZATION OFF 
GO

ALTER DATABASE [dba] SET TRUSTWORTHY OFF 
GO

ALTER DATABASE [dba] SET ALLOW_SNAPSHOT_ISOLATION OFF 
GO

ALTER DATABASE [dba] SET PARAMETERIZATION SIMPLE 
GO

ALTER DATABASE [dba] SET READ_COMMITTED_SNAPSHOT OFF 
GO

ALTER DATABASE [dba] SET HONOR_BROKER_PRIORITY OFF 
GO

ALTER DATABASE [dba] SET RECOVERY FULL 
GO

ALTER DATABASE [dba] SET  MULTI_USER 
GO

ALTER DATABASE [dba] SET PAGE_VERIFY CHECKSUM  
GO

ALTER DATABASE [dba] SET DB_CHAINING OFF 
GO

ALTER DATABASE [dba] SET FILESTREAM( NON_TRANSACTED_ACCESS = OFF ) 
GO

ALTER DATABASE [dba] SET TARGET_RECOVERY_TIME = 60 SECONDS 
GO

ALTER DATABASE [dba] SET DELAYED_DURABILITY = DISABLED 
GO

ALTER DATABASE [dba] SET QUERY_STORE = OFF
GO

ALTER DATABASE [dba] SET  READ_WRITE 
GO


Таблицы

USE [dba]
GO

/****** Object:  Table [dbo].[DBCCResults]    Script Date: 24.12.2025 12:55:13 ******/
SET ANSI_NULLS ON
GO

SET QUOTED_IDENTIFIER ON
GO

CREATE TABLE [dbo].[DBCCResults](
    [ID] [int] IDENTITY(1,1) NOT NULL,
    [DatabaseName] [nvarchar](128) NOT NULL,
    [CheckDate] [datetime] NOT NULL,
    [Command] [nvarchar](max) NULL,
    [Outcome] [nvarchar](50) NULL,
    [DurationSeconds] [int] NULL,
    [ErrorCount] [int] NULL,
    [ConsistencyErrorCount] [int] NULL,
    [LogInfo] [nvarchar](max) NULL,
    [Log] [nvarchar](max) NULL,
 CONSTRAINT [PK_DBCCResults] PRIMARY KEY CLUSTERED 
(
    [ID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
GO

ALTER TABLE [dbo].[DBCCResults] ADD  DEFAULT (getdate()) FOR [CheckDate]
GO


USE [dba]
GO

/****** Object:  Table [dbo].[RestoreLog]    Script Date: 24.12.2025 12:55:52 ******/
SET ANSI_NULLS ON
GO

SET QUOTED_IDENTIFIER ON
GO

CREATE TABLE [dbo].[RestoreLog](
    [Id] [int] IDENTITY(1,1) NOT NULL,
    [DatabaseName] [nvarchar](128) NOT NULL,
    [OriginalDatabaseName] [nvarchar](128) NULL,
    [RestoreDate] [datetime] NOT NULL,
    [DurationSeconds] [int] NULL,
    [Status] [nvarchar](50) NOT NULL,
    [Details] [nvarchar](max) NULL,
    [LogFilePath] [nvarchar](500) NULL,
    [BackupType] [nvarchar](50) NULL,
    [WasDropped] [bit] NULL,
    [FinalStatus] [nvarchar](50) NULL,
    [EstimatedSizeMB] [decimal](18, 2) NULL,
    [DeletionMode] [nvarchar](50) NULL,
    [SegmentName] [nvarchar](128) NULL,
 CONSTRAINT [PK_RestoreLog] PRIMARY KEY CLUSTERED 
(
    [Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
GO

ALTER TABLE [dbo].[RestoreLog] ADD  DEFAULT (getdate()) FOR [RestoreDate]
GO


Сам скрипт восстановление на Powershell 7 и dbatools и использования для проверки целостности Ola Hallengren

# Подключаем модуль dbatools
Import-Module dbatools -Force

# --- ПАРАМЕТРЫ ---
# Основные пути и серверы
$SourceBackupRoot = "\\arc03.admibd.ru\MSK-DL02`$msk-g02"
$TargetServer = "db01"
$DataPath = "F:\data"
$LogPath = "e:\log"

# ФИЛЬТР по имени базы
#$DatabaseFilter = @("AI111")
$DatabaseFilter = @()
# Базы для исключения из восстановления
$ExcludedDatabases = @('master', 'model', 'msdb', 'tempdb')
# Папки серверов для исключения
$ExcludedServerFolders = @('DBAtools', 'TMP')
# Базы, которые НЕ удалять на целевом сервере
$DatabasesToExcludeFromDeletion = @('master', 'model', 'msdb', 'tempdb', 'dba')

# НАСТРОЙКА ФОРМИРОВАНИЯ ИМЕНИ БАЗЫ
$PathSegmentFromEnd = 2  # Берем вторую папку с конца (перед именем базы)

# РЕЖИМ УДАЛЕНИЯ
$DeletionMode = "Smart"  # Calculate | All | None | Smart

# КОЭФФИЦИЕНТ РАСШИРЕНИЯ БЭКАПА (для учета сжатия)
$BackupExpansionFactor = 7  # Например, 172 GB бэкап → 1165 GB база (172 * 6.8 ≈ 1170)

# Параметры параллельности
$RestoreThrottleLimit = 5
$DeletionThrottleLimit = 10

# Проверка целостности
$RunIntegrityCheck = $true
$CheckThrottleLimit = 1

# Параметры повторной проверки
$MaxRetryAttempts = 3
$RetryDelaySeconds = 5

# Очистка логов Ola Hallengren
$OlaLogsCleanupDays = 14

# Email
$SmtpServer = "tech.admibd.ru"
$SmtpPort = 25
$EmailFrom = "db01@tech.admibd.ru"
$EmailTo = "konstantin.moskvichev@admibd.ru"
$EmailEncoding = "UTF8"
$SendEmailReport = $true

# Транскрипт
$TranscriptPath = "C:\temp\RestoreTranscript_$(Get-Date -Format 'yyyyMMdd_HHmmss').log"

# Время ожидания между операциями
$WaitAfterDeletion = 30  # секунд

# Очистка логов
$CleanupOldLogs = $true
$LogCleanupDays = 7
# --- КОНЕЦ ПАРАМЕТРОВ ---

# Создаём папку для логов, если не существует
if (-not (Test-Path "C:\temp")) { New-Item -ItemType Directory -Path "C:\temp" -Force | Out-Null }

# Запуск транскрипта
Start-Transcript -Path $TranscriptPath -Force

# Функция логирования
function Write-Log {
    param([string]$Message)
    $Timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
    $LogEntry = "[$Timestamp] $Message"
    Write-Host $LogEntry
}

# Функция отправки email
function Send-EmailReport {
    param(
        [string]$Subject,
        [string]$Body,
        [bool]$IsHtml = $false,
        [array]$Attachments = @()
    )
    try {
        if ($SendEmailReport) {
            $mailParams = @{
                SmtpServer = $SmtpServer
                Port       = $SmtpPort
                From       = $EmailFrom
                To         = $EmailTo
                Subject    = $Subject
                Body       = $Body
                Encoding   = $EmailEncoding
                ErrorAction = 'Stop'
            }
            if ($IsHtml) { $mailParams['BodyAsHtml'] = $true }
            if ($Attachments.Count -gt 0) { $mailParams['Attachments'] = $Attachments }
            
            # Останавливаем транскрипт перед отправкой
            try {
                Stop-Transcript
                Write-Host "Транскрипт остановлен для отправки email" -ForegroundColor Yellow
            } catch {
                Write-Host "Ошибка остановки транскрипта: $($_.Exception.Message)" -ForegroundColor Yellow
            }
            
            Send-MailMessage @mailParams
            Write-Host "Email отчет отправлен успешно" -ForegroundColor Green
            
            # Запускаем транскрипт снова
            try {
                Start-Transcript -Path $TranscriptPath -Append
                Write-Host "Транскрипт возобновлен" -ForegroundColor Yellow
            } catch {
                Write-Host "Ошибка возобновления транскрипта: $($_.Exception.Message)" -ForegroundColor Yellow
            }
        }
    } catch {
        $errorMsg = "Ошибка отправки email: $($_.Exception.Message)"
        Write-Host $errorMsg -ForegroundColor Red
        
        # Пытаемся возобновить транскрипт даже при ошибке
        try {
            Start-Transcript -Path $TranscriptPath -Append
            Write-Host "Транскрипт возобновлен после ошибки email" -ForegroundColor Yellow
        } catch {
            Write-Host "Ошибка возобновления транскрипта: $($_.Exception.Message)" -ForegroundColor Yellow
        }
    }
}

# Функция для проверки соответствия базы фильтру
function Test-DatabaseFilter {
    param([string]$DatabaseName, [array]$Filter)
    if ($Filter.Count -eq 0) { return $true }
    foreach ($filterItem in $Filter) {
        if ($filterItem -like "*`**") {
            if ($DatabaseName -like $filterItem) { return $true }
        } else {
            if ($DatabaseName -eq $filterItem) { return $true }
        }
    }
    return $false
}

# УНИВЕРСАЛЬНАЯ функция сканирования бэкапов
function Get-BackupDatabases {
    param(
        [string]$BackupRootPath, 
        [array]$DatabaseFilter, 
        [array]$ExcludedDbs, 
        [array]$ExcludedServers,
        [int]$PathSegmentFromEnd = 2
    )
    
    Write-Log "Сканирование структуры бэкапов в: $BackupRootPath"
    Write-Log "Используемый сегмент пути от конца: $PathSegmentFromEnd"
    Write-Log "Коэффициент расширения бэкапа: $BackupExpansionFactor"
    
    $databaseList = @()
    try {
        # Проверяем доступность пути
        if (-not (Test-Path $BackupRootPath)) {
            throw "Путь $BackupRootPath не существует или недоступен!"
        }
        
        Write-Log "Поиск папок с подпапкой FULL (рекурсивно)..."
        
        # Рекурсивно ищем все папки
        $allFolders = Get-ChildItem -Path $BackupRootPath -Directory -Recurse -ErrorAction SilentlyContinue
        
        Write-Log "Всего найдено папок: $($allFolders.Count)"
        
        # Собираем уникальные папки баз данных (те, у которых есть папка FULL внутри)
        $databaseFolders = @()
        
        foreach ($folder in $allFolders) {
            # Проверяем, что это не служебная папка
            $folderName = $folder.Name
            if ($folderName -in @('FULL', 'DIFF', 'LOG', 'FULL_1', 'DIFF_1', 'LOG_1')) {
                continue
            }
            
            # Проверяем, есть ли внутри папка FULL
            $fullPath = Join-Path $folder.FullName "FULL"
            if (Test-Path $fullPath) {
                # Это папка базы данных
                $databaseFolders += $folder
            }
        }
        
        Write-Log "Найдено папок баз данных (с папкой FULL): $($databaseFolders.Count)"
        
        foreach ($dbFolder in $databaseFolders) {
            $dbName = $dbFolder.Name
            
            # Пропускаем исключенные базы
            if ($dbName -in $ExcludedDbs) {
                Write-Log "  База $dbName пропущена - в списке исключений"
                continue
            }
            
            # Проверяем фильтр базы
            if (-not (Test-DatabaseFilter -DatabaseName $dbName -Filter $DatabaseFilter)) {
                Write-Log "  База $dbName пропущена - не соответствует фильтру"
                continue
            }
            
            # Ищем FULL бэкап
            $fullBackupPath = Join-Path $dbFolder.FullName "FULL"
            $fullBackupFile = $null
            $backupSizeMB = 0
            
            if (Test-Path $fullBackupPath) {
                $fullBackups = Get-ChildItem -Path $fullBackupPath -Filter "*.bak" -ErrorAction SilentlyContinue |
                    Sort-Object LastWriteTime -Descending
                
                if ($fullBackups) {
                    $fullBackupFile = $fullBackups[0]
                    
                    # ПОЛУЧАЕМ РЕАЛЬНЫЙ РАЗМЕР ФАЙЛА БЭКАПА
                    try {
                        $backupSizeBytes = $fullBackupFile.Length
                        $backupSizeMB = [math]::Round($backupSizeBytes / 1MB, 2)
                        $backupSizeGB = [math]::Round($backupSizeBytes / 1GB, 2)
                        
                        Write-Log "  Размер бэкапа: $backupSizeMB MB ($backupSizeGB GB)"
                    } catch {
                        Write-Log "  Не удалось определить размер бэкапа, используется оценка 1024 MB"
                        $backupSizeMB = 1024  # Значение по умолчанию
                    }
                }
            }
            
            if ($fullBackupFile) {
                # Ищем DIFF бэкап и его размер
                $diffBackupFile = $null
                $diffBackupSizeMB = 0
                $diffBackupPath = Join-Path $dbFolder.FullName "DIFF"
                
                if (Test-Path $diffBackupPath) {
                    $diffBackups = Get-ChildItem -Path $diffBackupPath -Filter "*.bak" -ErrorAction SilentlyContinue |
                        Sort-Object LastWriteTime -Descending
                    
                    if ($diffBackups) { 
                        $diffBackupFile = $diffBackups[0] 
                        
                        # Получаем размер DIFF бэкапа
                        try {
                            $diffSizeBytes = $diffBackupFile.Length
                            $diffBackupSizeMB = [math]::Round($diffSizeBytes / 1MB, 2)
                        } catch {
                            Write-Log "  Не удалось определить размер DIFF бэкапа"
                        }
                    }
                }
                
                # Формируем полный путь к папке базы для определения сегментов
                $dbFolderPath = $dbFolder.FullName.Trim('\')
                $dbPathParts = $dbFolderPath.Split('\')
                
                Write-Log "  Найдена база: $dbName"
                Write-Log "  Полный путь: $dbFolderPath"
                Write-Log "  Части пути: $($dbPathParts -join ' | ')"
                
                # Определяем имя сегмента (отсчет от конца пути)
                $segmentName = "Unknown"
                if ($dbPathParts.Count -ge $PathSegmentFromEnd) {
                    $segmentName = $dbPathParts[-$PathSegmentFromEnd]
                    
                    # Очищаем имя сегмента (убираем $ и все после него)
                    if ($segmentName -contains '$') {
                        $segmentName = $segmentName.Split('$')[0]
                    }
                    
                    # Формируем имя базы: сегмент_имя_базы
                    $databaseNameOnTarget = "${segmentName}_${dbName}"
                    
                    Write-Log "  Сегмент #$PathSegmentFromEnd с конца: $segmentName"
                    Write-Log "  Имя на целевом сервере: $databaseNameOnTarget"
                } else {
                    $warningMsg = "Путь к базе $dbName содержит только $($dbPathParts.Count) частей, а запрошен сегмент $PathSegmentFromEnd. Используем имя базы как есть."
                    Write-Log "  ВНИМАНИЕ: $warningMsg"
                    # Используем имя базы как есть
                    $databaseNameOnTarget = $dbName
                }
                
                # Проверяем, что имя не слишком длинное (ограничение SQL Server - 128 символов)
                if ($databaseNameOnTarget.Length -gt 128) {
                    Write-Log "  Внимание: имя базы слишком длинное ($($databaseNameOnTarget.Length) символов), будет обрезано"
                    $databaseNameOnTarget = $databaseNameOnTarget.Substring(0, 128)
                }
                
                $backupInfo = [PSCustomObject]@{
                    OriginalDatabaseName = $dbName
                    ServerName           = $segmentName
                    DatabaseName         = $databaseNameOnTarget
                    FullBackupFile       = $fullBackupFile.FullName
                    DiffBackupFile       = if ($diffBackupFile) { $diffBackupFile.FullName } else { $null }
                    HasDiffBackup        = [bool]$diffBackupFile
                    FullBackupDate       = $fullBackupFile.LastWriteTime
                    DiffBackupDate       = if ($diffBackupFile) { $diffBackupFile.LastWriteTime } else { $null }
                    BackupSizeMB         = $backupSizeMB  # РЕАЛЬНЫЙ размер FULL бэкапа
                    DiffBackupSizeMB     = $diffBackupSizeMB  # РЕАЛЬНЫЙ размер DIFF бэкапа
                    TotalBackupSizeMB    = if ($diffBackupFile) { 
                        [math]::Round($backupSizeMB + $diffBackupSizeMB) 
                    } else { 
                        $backupSizeMB 
                    }
                    # РАСШИРЕННЫЙ размер (с учетом коэффициента сжатия)
                    ExpandedSizeMB       = if ($diffBackupFile) { 
                        [math]::Round(($backupSizeMB + $diffBackupSizeMB) * $BackupExpansionFactor) 
                    } else { 
                        [math]::Round($backupSizeMB * $BackupExpansionFactor) 
                    }
                }
                
                $databaseList += $backupInfo
                Write-Log "  ✓ Бэкап найден: $databaseNameOnTarget (оригинал: $dbName, тип: $($backupInfo.HasDiffBackup ? 'FULL+DIFF' : 'FULL'))"
                Write-Log "    Файл бэкапа: $($fullBackupFile.Name)"
                Write-Log "    Размер бэкапа: $backupSizeMB MB ($([math]::Round($backupSizeMB/1024, 2)) GB)"
                Write-Log "    Расширенный размер базы: $($backupInfo.ExpandedSizeMB) MB ($([math]::Round($backupInfo.ExpandedSizeMB/1024, 2)) GB)"
                Write-Log "    Дата бэкапа: $($fullBackupFile.LastWriteTime)"
                
            } else {
                Write-Log "  База $dbName пропущена - нет файлов .bak в папке FULL"
            }
        }
    } catch {
        $ErrorMessage = "Ошибка при сканировании структуры бэкапов: $($_.Exception.Message)"
        Write-Log $ErrorMessage
        throw
    }
    
    Write-Log "Всего найдено баз с бэкапами: $($databaseList.Count)"
    
    # Считаем общий реальный размер бэкапов и расширенный размер
    $totalBackupSizeMB = ($databaseList | Measure-Object -Property TotalBackupSizeMB -Sum).Sum
    $totalBackupSizeGB = [math]::Round($totalBackupSizeMB / 1024, 2)
    $totalExpandedSizeMB = ($databaseList | Measure-Object -Property ExpandedSizeMB -Sum).Sum
    $totalExpandedSizeGB = [math]::Round($totalExpandedSizeMB / 1024, 2)
    
    Write-Log "Общий размер всех бэкапов: $totalBackupSizeMB MB ($totalBackupSizeGB GB)"
    Write-Log "Общий расширенный размер баз после восстановления: $totalExpandedSizeMB MB ($totalExpandedSizeGB GB)"
    
    # Логируем детали найденных баз
    if ($databaseList.Count -gt 0) {
        Write-Log "Найденные базы для восстановления:"
        foreach ($db in $databaseList) {
            $backupSizeGB = [math]::Round($db.BackupSizeMB / 1024, 2)
            $expandedSizeGB = [math]::Round($db.ExpandedSizeMB / 1024, 2)
            Write-Log "  - $($db.DatabaseName) (оригинал: $($db.OriginalDatabaseName), сервер: $($db.ServerName))"
            Write-Log "    Бэкап: $backupSizeGB GB → База: ~$expandedSizeGB GB (x$BackupExpansionFactor)"
        }
    }
    
    return $databaseList
}

# Удаление всех баз кроме исключённых (параллельно)
function Remove-AllDatabasesParallel {
    param([string]$TargetServer, [array]$DatabasesToExcludeFromDeletion, [int]$ThrottleLimit)
    Write-Log "=== Начало удаления ВСЕХ баз кроме исключенных ==="
    try {
        $TargetConnection = Connect-DbaInstance -SqlInstance $TargetServer -TrustServerCertificate
        $AllDatabases = $TargetConnection | Get-DbaDatabase
        $DatabasesToDelete = $AllDatabases | Where-Object {
            $_.Name -notin $DatabasesToExcludeFromDeletion -and $_.Status -eq 'Normal'
        } | ForEach-Object {
            [PSCustomObject]@{
                DatabaseName = $_.Name
                SizeMB       = [math]::Round($_.Size, 2)
                DataSpaceMB  = [math]::Round($_.DataSpaceUsage.Megabyte, 2)
                LogSpaceMB   = [math]::Round($_.LogSpaceUsage.Megabyte, 2)
            }
        }
        $TargetConnection.ConnectionContext.Disconnect()
        Write-Log "Найдено баз для удаления: $($DatabasesToDelete.Count)"
        if ($DatabasesToDelete.Count -eq 0) {
            Write-Log "Нет баз для удаления."
            return @()
        }
        Write-Log "Будет удалено $($DatabasesToDelete.Count) баз:"
        $deleteResults = $DatabasesToDelete | ForEach-Object -Parallel {
            $dbInfo = $_
            $targetServer = $using:TargetServer
            Import-Module dbatools -Force
            $startTime = Get-Date
            try {
                Remove-DbaDatabase -SqlInstance $targetServer -Database $dbInfo.DatabaseName -Confirm:$false | Out-Null
                $endTime = Get-Date
                $duration = $endTime - $startTime
                return [PSCustomObject]@{
                    DatabaseName = $dbInfo.DatabaseName
                    Status       = "Успешно"
                    Details      = "Удалена за $("{0:hh\:mm\:ss}" -f $duration)"
                    SizeMB       = $dbInfo.SizeMB
                    DataSpaceMB  = $dbInfo.DataSpaceMB
                    LogSpaceMB   = $dbInfo.LogSpaceMB
                }
            } catch {
                $endTime = Get-Date
                return [PSCustomObject]@{
                    DatabaseName = $dbInfo.DatabaseName
                    Status       = "Ошибка"
                    Details      = "Ошибка удаления: $($_.Exception.Message)"
                    SizeMB       = $dbInfo.SizeMB
                    DataSpaceMB  = $dbInfo.DataSpaceMB
                    LogSpaceMB   = $dbInfo.LogSpaceMB
                }
            }
        } -ThrottleLimit $ThrottleLimit
        Write-Log "Параллельное удаление завершено"
        $SuccessCount = ($deleteResults | Where-Object { $_.Status -eq 'Успешно' }).Count
        $ErrorCount = ($deleteResults | Where-Object { $_.Status -eq 'Ошибка' }).Count
        Write-Log "Результаты удаления: успешно - $SuccessCount, с ошибкой - $ErrorCount"
        if ($SuccessCount -gt 0) {
            Write-Log "Ожидание ${WaitAfterDeletion} секунд для освобождения места..."
            Start-Sleep -Seconds $WaitAfterDeletion
            $DataDriveLetter = ($DataPath -split ':')[0]
            $FreeSpaceAfter = [math]::Round((Get-PSDrive -Name $DataDriveLetter).Free / 1MB, 2)
            Write-Log "Свободное место после удаления: $FreeSpaceAfter MB"
        }
        Write-Log "=== Удаление всех баз завершено ==="
        return $deleteResults
    } catch {
        $ErrorMessage = "Ошибка при удалении всех баз: $($_.Exception.Message)"
        Write-Log $ErrorMessage
        throw
    }
}

# Умное удаление только необходимых баз (от самых маленьких к большим)
function Remove-DatabasesSmart {
    param(
        [string]$TargetServer,
        [array]$DatabasesToExcludeFromDeletion,
        [array]$DatabasesToRestore,  # Массив баз для восстановления
        [int]$ThrottleLimit,
        [double]$RequiredSpaceMB     # Необходимый объем пространства
    )
    
    Write-Log "=== Начало умного удаления баз ==="
    Write-Log "Требуется освободить: $RequiredSpaceMB MB ($([math]::Round($RequiredSpaceMB/1024, 2)) GB)"
    
    try {
        # Получаем все базы на сервере, сортируем по размеру (от маленьких к большим)
        $TargetConnection = Connect-DbaInstance -SqlInstance $TargetServer -TrustServerCertificate
        $AllDatabases = $TargetConnection | Get-DbaDatabase | Where-Object {
            $_.Name -notin $DatabasesToExcludeFromDeletion -and $_.Status -eq 'Normal'
        } | ForEach-Object {
            [PSCustomObject]@{
                DatabaseName = $_.Name
                SizeMB       = [math]::Round($_.Size, 2)
                DataSpaceMB  = [math]::Round($_.DataSpaceUsage.Megabyte, 2)
                LogSpaceMB   = [math]::Round($_.LogSpaceUsage.Megabyte, 2)
            }
        } | Sort-Object SizeMB  # Сортируем от самых маленьких к большим
        
        $TargetConnection.ConnectionContext.Disconnect()
        
        Write-Log "Найдено кандидатов на удаление: $($AllDatabases.Count)"
        
        if ($AllDatabases.Count -eq 0) {
            Write-Log "Нет баз для удаления."
            return @()
        }
        
        # Отображаем размеры баз
        Write-Log "Размеры баз для возможного удаления (отсортированы по возрастанию):"
        foreach ($db in $AllDatabases) {
            $dbSizeGB = [math]::Round($db.SizeMB / 1024, 2)
            Write-Log "  $($db.DatabaseName): $($db.SizeMB) MB ($dbSizeGB GB) (Данные: $($db.DataSpaceMB) MB, Логи: $($db.LogSpaceMB) MB)"
        }
        
        # Планируем удаление: удаляем по одной базе, пока не наберем нужный объем
        $databasesToDelete = @()
        $freedSpaceMB = 0
        
        foreach ($db in $AllDatabases) {
            if ($freedSpaceMB -ge $RequiredSpaceMB) {
                Write-Log "  Уже освобождено достаточно места: $freedSpaceMB MB / требуется: $RequiredSpaceMB MB"
                break  # Уже освободили достаточно места
            }
            
            # Проверяем, не восстанавливаем ли мы эту же базу (чтобы не удалить то, что собираемся восстановить)
            $isBeingRestored = $DatabasesToRestore | Where-Object { $_.DatabaseName -eq $db.DatabaseName }
            if ($isBeingRestored) {
                Write-Log "  Пропускаем $($db.DatabaseName) - эта база будет восстановлена"
                continue
            }
            
            $databasesToDelete += $db
            $freedSpaceMB += $db.SizeMB
            $freedSpaceGB = [math]::Round($freedSpaceMB / 1024, 2)
            $requiredSpaceGB = [math]::Round($RequiredSpaceMB / 1024, 2)
            Write-Log "  Запланировано удаление: $($db.DatabaseName) ($([math]::Round($db.SizeMB/1024, 2)) GB). Всего освобождено: $freedSpaceGB GB / требуется: $requiredSpaceGB GB"
        }
        
        if ($databasesToDelete.Count -eq 0) {
            Write-Log "Нет подходящих баз для удаления."
            return @()
        }
        
        Write-Log "Будет удалено $($databasesToDelete.Count) баз для освобождения $([math]::Round($freedSpaceMB/1024, 2)) GB места:"
        foreach ($db in $databasesToDelete) {
            $dbSizeGB = [math]::Round($db.SizeMB / 1024, 2)
            Write-Log "  - $($db.DatabaseName) ($dbSizeGB GB)"
        }
        
        # Удаляем запланированные базы
        $deleteResults = $databasesToDelete | ForEach-Object -Parallel {
            $dbInfo = $_
            $targetServer = $using:TargetServer
            Import-Module dbatools -Force
            $startTime = Get-Date
            try {
                Remove-DbaDatabase -SqlInstance $targetServer -Database $dbInfo.DatabaseName -Confirm:$false | Out-Null
                $endTime = Get-Date
                $duration = $endTime - $startTime
                return [PSCustomObject]@{
                    DatabaseName = $dbInfo.DatabaseName
                    Status       = "Успешно"
                    Details      = "Удалена за $("{0:hh\:mm\:ss}" -f $duration)"
                    SizeMB       = $dbInfo.SizeMB
                    DataSpaceMB  = $dbInfo.DataSpaceMB
                    LogSpaceMB   = $dbInfo.LogSpaceMB
                }
            } catch {
                $endTime = Get-Date
                return [PSCustomObject]@{
                    DatabaseName = $dbInfo.DatabaseName
                    Status       = "Ошибка"
                    Details      = "Ошибка удаления: $($_.Exception.Message)"
                    SizeMB       = $dbInfo.SizeMB
                    DataSpaceMB  = $dbInfo.DataSpaceMB
                    LogSpaceMB   = $dbInfo.LogSpaceMB
                }
            }
        } -ThrottleLimit $ThrottleLimit
        
        Write-Log "Умное удаление завершено"
        $SuccessCount = ($deleteResults | Where-Object { $_.Status -eq 'Успешно' }).Count
        $ErrorCount = ($deleteResults | Where-Object { $_.Status -eq 'Ошибка' }).Count
        Write-Log "Результаты удаления: успешно - $SuccessCount, с ошибкой - $ErrorCount"
        
        if ($SuccessCount -gt 0) {
            Write-Log "Ожидание ${WaitAfterDeletion} секунд для освобождения места..."
            Start-Sleep -Seconds $WaitAfterDeletion
            
            # Проверяем, сколько места освободили
            $DataDriveLetter = ($DataPath -split ':')[0]
            $FreeSpaceAfter = [math]::Round((Get-PSDrive -Name $DataDriveLetter).Free / 1MB, 2)
            $FreeSpaceAfterGB = [math]::Round($FreeSpaceAfter / 1024, 2)
            
            # Вычисляем сколько места было до удаления (приблизительно)
            $totalFreedMB = ($deleteResults | Where-Object { $_.Status -eq 'Успешно' } | Measure-Object -Property SizeMB -Sum).Sum
            $totalFreedGB = [math]::Round($totalFreedMB / 1024, 2)
            Write-Log "Освобождено места: $totalFreedMB MB ($totalFreedGB GB)"
            Write-Log "Свободное место после удаления: $FreeSpaceAfter MB ($FreeSpaceAfterGB GB)"
        }
        
        Write-Log "=== Умное удаление баз завершено ==="
        return $deleteResults
        
    } catch {
        $ErrorMessage = "Ошибка при умном удалении баз: $($_.Exception.Message)"
        Write-Log $ErrorMessage
        throw
    }
}

# Режим Calculate - удаление только если не хватает места
function Calculate-SpaceAndDelete {
    param(
        [string]$TargetServer,
        [string]$DataPath,
        [array]$DatabasesInfo,
        [array]$DatabasesToExcludeFromDeletion,
        [int]$ThrottleLimit
    )
    Write-Log "=== Проверка места и расчет необходимости удаления (режим Calculate) ==="
    
    try {
        # 1. Проверяем текущее свободное место
        $DataDriveLetter = ($DataPath -split ':')[0]
        $CurrentFreeSpaceMB = [math]::Round((Get-PSDrive -Name $DataDriveLetter).Free / 1MB, 2)
        $CurrentFreeSpaceGB = [math]::Round($CurrentFreeSpaceMB / 1024, 2)
        $CurrentFreeSpaceTB = [math]::Round($CurrentFreeSpaceMB / 1048576, 2)
        Write-Log "Свободное место на $DataDriveLetter : $CurrentFreeSpaceMB MB ($CurrentFreeSpaceGB GB / $CurrentFreeSpaceTB TB)"
        
        # 2. Используем РАСШИРЕННЫЕ размеры баз (с учетом коэффициента сжатия)
        $TotalExpandedSizeMB = 0
        foreach ($db in $DatabasesInfo) {
            $dbSize = $db.ExpandedSizeMB
            $TotalExpandedSizeMB += $dbSize
            $dbSizeGB = [math]::Round($dbSize / 1024, 2)
            $backupSizeGB = [math]::Round($db.BackupSizeMB / 1024, 2)
            Write-Log "  $($db.DatabaseName): бэкап $backupSizeGB GB → база ~$dbSizeGB GB (x$BackupExpansionFactor)"
        }
        
        $TotalExpandedSizeGB = [math]::Round($TotalExpandedSizeMB / 1024, 2)
        Write-Log "Общий РАСШИРЕННЫЙ размер для восстановления $($DatabasesInfo.Count) баз: $TotalExpandedSizeMB MB ($TotalExpandedSizeGB GB)"
        
        # 3. Проверяем, достаточно ли места с учетом реального размера баз после восстановления
        # Добавляем запас 10% для индексов и роста
        $RequiredSpaceMB = $TotalExpandedSizeMB * 1.1
        $RequiredSpaceGB = [math]::Round($RequiredSpaceMB / 1024, 2)
        
        Write-Log "Требуется с запасом 10%: $RequiredSpaceMB MB ($RequiredSpaceGB GB)"
        Write-Log "Доступно: $CurrentFreeSpaceMB MB ($CurrentFreeSpaceGB GB)"
        
        if ($CurrentFreeSpaceMB -gt $RequiredSpaceMB) {
            Write-Log "Места достаточно для восстановления. Удаление не требуется."
            Write-Log "Доступно: $CurrentFreeSpaceMB MB > Требуется: $RequiredSpaceMB MB"
            return @()
        }
        
        # 4. Рассчитываем, сколько места нужно освободить
        $SpaceDeficitMB = $RequiredSpaceMB - $CurrentFreeSpaceMB
        $SpaceDeficitGB = [math]::Round($SpaceDeficitMB / 1024, 2)
        Write-Log "Места недостаточно. Требуется освободить: $SpaceDeficitMB MB ($SpaceDeficitGB GB)"
        
        # 5. Используем умное удаление
        Write-Log "Запуск умного удаления баз..."
        return Remove-DatabasesSmart -TargetServer $TargetServer `
            -DatabasesToExcludeFromDeletion $DatabasesToExcludeFromDeletion `
            -DatabasesToRestore $DatabasesInfo `
            -ThrottleLimit $ThrottleLimit `
            -RequiredSpaceMB $SpaceDeficitMB
        
    } catch {
        $ErrorMessage = "Ошибка при проверке места: $($_.Exception.Message)"
        Write-Log $ErrorMessage
        throw
    }
}

# Функция проверки и создания таблицы RestoreLog
function Ensure-RestoreLogTable {
    param([string]$TargetServer)
    
    Write-Log "Проверка и создание таблицы RestoreLog..."
    
    $checkAndCreateTable = @"
-- Проверяем существование таблицы
IF NOT EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dba].[dbo].[RestoreLog]') AND type in (N'U'))
BEGIN
    CREATE TABLE [dba].[dbo].[RestoreLog](
        [Id] [int] IDENTITY(1,1) NOT NULL,
        [DatabaseName] [nvarchar](128) NOT NULL,
        [OriginalDatabaseName] [nvarchar](128) NOT NULL,
        [RestoreDate] [datetime] NOT NULL,
        [DurationSeconds] [float] NULL,
        [Status] [nvarchar](50) NOT NULL,
        [Details] [nvarchar](max) NULL,
        [LogFilePath] [nvarchar](500) NULL,
        [BackupType] [nvarchar](50) NULL,
        [WasDropped] [bit] NULL,
        [FinalStatus] [nvarchar](50) NULL,
        [EstimatedSizeMB] [float] NULL,
        [DeletionMode] [nvarchar](50) NULL,
        [SegmentName] [nvarchar](128) NULL,
     CONSTRAINT [PK_RestoreLog] PRIMARY KEY CLUSTERED ([Id] ASC)
    )
    PRINT 'Таблица RestoreLog создана.'
END
ELSE
BEGIN
    -- Проверяем наличие столбца SegmentName
    IF COL_LENGTH('dba.dbo.RestoreLog', 'SegmentName') IS NULL
    BEGIN
        ALTER TABLE [dba].[dbo].[RestoreLog] ADD [SegmentName] [nvarchar](128) NULL;
        PRINT 'Столбец SegmentName добавлен в таблицу RestoreLog.'
    END
    PRINT 'Таблица RestoreLog уже существует.'
END
"@
    
    try {
        Invoke-DbaQuery -SqlInstance $TargetServer -Database "dba" -Query $checkAndCreateTable -EnableException
        Write-Log "Таблица RestoreLog проверена/создана успешно"
        return $true
    } catch {
        Write-Log "Ошибка при проверке/создании таблицы RestoreLog: $($_.Exception.Message)"
        return $false
    }
}

# Функция записи результата восстановления в таблицу
function Write-RestoreLogToDb {
    param(
        [string]$TargetServer,
        [string]$DatabaseName,
        [string]$OriginalDatabaseName,
        [string]$Status,
        [string]$Details,
        [string]$BackupType,
        [bool]$WasDropped,
        [string]$LogFilePath,
        [double]$DurationSeconds,
        [string]$FinalStatus,
        [double]$EstimatedSizeMB,
        [string]$DeletionMode,
        [string]$SegmentName
    )
    
    # Проверяем, что имя базы не пустое
    if ([string]::IsNullOrEmpty($DatabaseName)) {
        Write-Log "Пропуск записи в RestoreLog: пустое имя базы"
        return $false
    }
    
    # Проверяем, что база dba существует
    try {
        $dbCheck = Get-DbaDatabase -SqlInstance $TargetServer -Database "dba" -ErrorAction Stop
        if (-not $dbCheck) {
            Write-Log "База dba не существует на сервере $TargetServer"
            return $false
        }
    } catch {
        Write-Log "Ошибка проверки базы dba: $($_.Exception.Message)"
        return $false
    }
    
    # Укорачиваем Details если слишком длинные
    $safeDetails = if ($Details.Length -gt 4000) { $Details.Substring(0, 4000) } else { $Details }
    $safeDetails = $safeDetails -replace "'", "''"
    $safeLogFilePath = $LogFilePath -replace "'", "''"
    $safeSegmentName = if ($SegmentName) { $SegmentName -replace "'", "''" } else { '' }
    
    # Преобразуем WasDropped в bit (1 или 0)
    $wasDroppedBit = if ($WasDropped) { 1 } else { 0 }
    
    # Проверяем наличие столбца SegmentName
    $checkColumnQuery = @"
IF COL_LENGTH('dba.dbo.RestoreLog', 'SegmentName') IS NOT NULL
    SELECT 'SegmentName' as ColumnName
ELSE
    SELECT '' as ColumnName
"@
    
    try {
        $columnCheck = Invoke-DbaQuery -SqlInstance $TargetServer -Database "dba" -Query $checkColumnQuery -EnableException
        $hasSegmentNameColumn = ($columnCheck.ColumnName -eq 'SegmentName')
        
        if ($hasSegmentNameColumn) {
            $query = @"
INSERT INTO [dba].[dbo].[RestoreLog]
    ([DatabaseName], [OriginalDatabaseName], [RestoreDate], [DurationSeconds], [Status], [Details], 
     [LogFilePath], [BackupType], [WasDropped], [FinalStatus], [EstimatedSizeMB], [DeletionMode], [SegmentName])
VALUES
    ('$DatabaseName', '$OriginalDatabaseName', GETDATE(), $DurationSeconds, '$Status', '$safeDetails',
     '$safeLogFilePath', '$BackupType', $wasDroppedBit, '$FinalStatus', $EstimatedSizeMB, '$DeletionMode', '$safeSegmentName')
"@
        } else {
            $query = @"
INSERT INTO [dba].[dbo].[RestoreLog]
    ([DatabaseName], [OriginalDatabaseName], [RestoreDate], [DurationSeconds], [Status], [Details], 
     [LogFilePath], [BackupType], [WasDropped], [FinalStatus], [EstimatedSizeMB], [DeletionMode])
VALUES
    ('$DatabaseName', '$OriginalDatabaseName', GETDATE(), $DurationSeconds, '$Status', '$safeDetails',
     '$safeLogFilePath', '$BackupType', $wasDroppedBit, '$FinalStatus', $EstimatedSizeMB, '$DeletionMode')
"@
        }
        
        $result = Invoke-DbaQuery -SqlInstance $TargetServer -Database "dba" -Query $query -ErrorAction Stop
        Write-Log "Запись в RestoreLog для $DatabaseName выполнена успешно"
        return $true
    } catch {
        $errorMsg = "Ошибка записи в RestoreLog для $DatabaseName : $($_.Exception.Message)"
        Write-Log $errorMsg
        
        # Пытаемся создать таблицу и повторить вставку
        try {
            Write-Log "Попытка создать таблицу RestoreLog..."
            $tableCreated = Ensure-RestoreLogTable -TargetServer $TargetServer
            
            if ($tableCreated) {
                # Повторяем попытку вставки с проверкой наличия столбца
                $columnCheck = Invoke-DbaQuery -SqlInstance $TargetServer -Database "dba" -Query $checkColumnQuery -EnableException
                $hasSegmentNameColumn = ($columnCheck.ColumnName -eq 'SegmentName')
                
                if ($hasSegmentNameColumn) {
                    $query = @"
INSERT INTO [dba].[dbo].[RestoreLog]
    ([DatabaseName], [OriginalDatabaseName], [RestoreDate], [DurationSeconds], [Status], [Details], 
     [LogFilePath], [BackupType], [WasDropped], [FinalStatus], [EstimatedSizeMB], [DeletionMode], [SegmentName])
VALUES
    ('$DatabaseName', '$OriginalDatabaseName', GETDATE(), $DurationSeconds, '$Status', '$safeDetails',
     '$safeLogFilePath', '$BackupType', $wasDroppedBit, '$FinalStatus', $EstimatedSizeMB, '$DeletionMode', '$safeSegmentName')
"@
                } else {
                    $query = @"
INSERT INTO [dba].[dbo].[RestoreLog]
    ([DatabaseName], [OriginalDatabaseName], [RestoreDate], [DurationSeconds], [Status], [Details], 
     [LogFilePath], [BackupType], [WasDropped], [FinalStatus], [EstimatedSizeMB], [DeletionMode])
VALUES
    ('$DatabaseName', '$OriginalDatabaseName', GETDATE(), $DurationSeconds, '$Status', '$safeDetails',
     '$safeLogFilePath', '$BackupType', $wasDroppedBit, '$FinalStatus', $EstimatedSizeMB, '$DeletionMode')
"@
                }
                
                $result = Invoke-DbaQuery -SqlInstance $TargetServer -Database "dba" -Query $query -ErrorAction Stop
                Write-Log "Запись в RestoreLog для $DatabaseName выполнена после создания таблицы"
                return $true
            } else {
                return $false
            }
        } catch {
            Write-Log "Не удалось создать таблицу или записать данные: $($_.Exception.Message)"
            return $false
        }
    }
}

# Проверка целостности с повторными попытками
function CheckDatabaseWithRetry {
    param(
        [string]$ServerInstance,
        [string]$DatabaseName,
        [int]$MaxAttempts,
        [int]$RetryDelay
    )
    
    $Attempt = 1
    $LastError = ""
    
    while ($Attempt -le $MaxAttempts) {
        try {
            $DbStatus = Get-DbaDatabase -SqlInstance $ServerInstance -Database $DatabaseName -ErrorAction Stop
            
            if ($DbStatus.Status -ne 'Normal') {
                $Msg = "База '$DatabaseName' не ONLINE (Status: $($DbStatus.Status))"
                return [PSCustomObject]@{
                    DatabaseName = $DatabaseName
                    Status = "Пропущена"
                    Details = $Msg
                    Attempts = $Attempt
                }
            }

            $SqlCmd = "EXECUTE [master].dbo.DatabaseIntegrityCheck @Databases = '$DatabaseName', @CheckCommands = 'CHECKDB', @LogToTable = 'Y', @MaxDOP='40', @PhysicalOnly = 'Y';"
            Invoke-DbaQuery -SqlInstance $ServerInstance -Query $SqlCmd -EnableException

            return [PSCustomObject]@{
                DatabaseName = $DatabaseName
                Status = "Завершена"
                Details = "Проверка выполнена успешно (попытка $Attempt)"
                Attempts = $Attempt
            }
        } catch {
            $LastError = $_.Exception.Message
            
            if ($Attempt -lt $MaxAttempts) {
                Start-Sleep -Seconds $RetryDelay
            }
            
            $Attempt++
        }
    }
    
    return [PSCustomObject]@{
        DatabaseName = $DatabaseName
        Status = "Ошибка"
        Details = "Все $MaxAttempts попыток завершены ошибкой: $LastError"
        Attempts = $MaxAttempts
    }
}

# Функция проверки и создания таблицы DBCCResults
function Ensure-DBCCResultsTable {
    param([string]$TargetServer)
    
    Write-Log "Проверка и создание таблицы DBCCResults..."
    
    $checkAndCreateTable = @"
-- Проверяем существование таблица
IF NOT EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dba].[dbo].[DBCCResults]') AND type in (N'U'))
BEGIN
    CREATE TABLE [dba].[dbo].[DBCCResults](
        [ID] [int] IDENTITY(1,1) NOT NULL,
        [DatabaseName] [nvarchar](128) NOT NULL,
        [CheckDate] [datetime] NOT NULL,
        [Command] [nvarchar](max) NOT NULL,
        [Outcome] [nvarchar](50) NULL,
        [DurationSeconds] [int] NULL,
        [ErrorCount] [int] NULL,
        [ConsistencyErrorCount] [int] NULL,
        [LogInfo] [nvarchar](max) NULL,
        [Log] [nvarchar](max) NULL,
     CONSTRAINT [PK_DBCCResults] PRIMARY KEY CLUSTERED ([ID] ASC)
    )
    PRINT 'Таблица DBCCResults создана.'
END
ELSE
BEGIN
    -- Проверяем наличие столбца Log
    IF COL_LENGTH('dba.dbo.DBCCResults', 'Log') IS NULL
    BEGIN
        ALTER TABLE dba.dbo.DBCCResults ADD [Log] NVARCHAR(MAX) NULL;
        PRINT 'Столбец Log добавлен в таблицу DBCCResults.'
    END
    
    -- Проверяем наличие столбца LogInfo
    IF COL_LENGTH('dba.dbo.DBCCResults', 'LogInfo') IS NULL
    BEGIN
        ALTER TABLE dba.dbo.DBCCResults ADD [LogInfo] NVARCHAR(MAX) NULL;
        PRINT 'Столбец LogInfo добавлен в таблицу DBCCResults.'
    END
    PRINT 'Таблица DBCCResults уже существует.'
END
"@
    
    try {
        Invoke-DbaQuery -SqlInstance $TargetServer -Database "dba" -Query $checkAndCreateTable -EnableException
        Write-Log "Таблица DBCCResults проверена/создана успешно"
        return $true
    } catch {
        Write-Log "Ошибка при проверке/создании таблицы DBCCResults: $($_.Exception.Message)"
        return $false
    }
}

# Функция для записи результатов проверки в DBCCResults
function Write-DBCCResultsToDb {
    param(
        [string]$TargetServer,
        [array]$CheckResults
    )
    
    Write-Log "=== Запись результатов проверки в DBCCResults ==="
    
    # Проверяем и создаем таблицу
    $tableReady = Ensure-DBCCResultsTable -TargetServer $TargetServer
    if (-not $tableReady) {
        Write-Log "Не удалось подготовить таблицу DBCCResults"
        return
    }
    
    try {
        foreach ($res in $CheckResults) {
            $DbName = $res.DatabaseName.Replace("'", "''")
            $CheckDate = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
            $Command = "DBCC CHECKDB ([$DbName]) WITH ALL_ERRORMSGS"
            $Outcome = switch ($res.Status) {
                "Завершена" { "Succeeded" }
                "Ошибка"    { "Failed" }
                "Пропущена" { "Skipped" }
                default     { "Unknown" }
            }
            $DurationSec = 0
            $ErrorCount = if ($res.Status -eq "Ошибка") { 1 } else { 0 }
            $ConsistencyErrorCount = $ErrorCount
            $LogText = "$($res.Details) (попыток: $($res.Attempts))"
            $LogInfoText = "$($res.Details) (попыток: $($res.Attempts))"

            $SqlInsert = @"
INSERT INTO [dba].[dbo].[DBCCResults] 
(DatabaseName, CheckDate, Command, Outcome, DurationSeconds, ErrorCount, ConsistencyErrorCount, LogInfo, [Log])
VALUES 
(N'$DbName', '$CheckDate', N'$Command', N'$Outcome', $DurationSec, $ErrorCount, $ConsistencyErrorCount, N'$(($LogInfoText -replace "'", "''"))', N'$(($LogText -replace "'", "''"))');
"@
            Invoke-DbaQuery -SqlInstance $TargetServer -Query $SqlInsert -EnableException
        }
        Write-Log "Запись в DBCCResults завершена."
    } catch {
        Write-Log "Ошибка записи в DBCCResults: $($_.Exception.Message)"
    }
}

# Функция для очистки старых записей из логов
function Cleanup-OldLogs {
    param(
        [string]$TargetServer,
        [int]$OlaLogsCleanupDays
    )
    
    Write-Log "=== Очистка старых записей из логов ==="
    $Cutoff = (Get-Date).AddDays(-$OlaLogsCleanupDays).ToString("yyyy-MM-dd HH:mm:ss")
    try {
        # Проверяем наличие таблицы CommandLog
        $checkCommandLog = @"
IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[CommandLog]') AND type in (N'U'))
BEGIN
    DELETE FROM [dbo].[CommandLog] WHERE StartTime < '$Cutoff' AND CommandType = 'DBCC_CHECKDB';
END
"@
        Invoke-DbaQuery -SqlInstance $TargetServer -Query $checkCommandLog -EnableException
        
        # Проверяем наличие таблицы RestoreLog
        $checkRestoreLog = @"
IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dba].[dbo].[RestoreLog]') AND type in (N'U'))
BEGIN
    DELETE FROM [dba].[dbo].[RestoreLog] WHERE RestoreDate < '$Cutoff';
END
"@
        Invoke-DbaQuery -SqlInstance $TargetServer -Query $checkRestoreLog -EnableException
        
        Write-Log "Очистка логов завершена."
    } catch {
        Write-Log "Ошибка при очистке логов: $($_.Exception.Message)"
    }
}

# Создание HTML-отчёта
function New-HtmlReport {
    param(
        [array]$RestoreResults,
        [array]$DeleteResults,
        [array]$CheckResults,
        [string]$TotalDuration,
        [string]$CheckDuration,
        [string]$DeletionMode,
        [string]$TargetServer,
        [int]$TotalDatabases,
        [array]$DatabaseFilter,
        [int]$PathSegmentFromEnd,
        [string]$SourceBackupRoot,
        [double]$BackupExpansionFactor,
        [double]$TotalExpandedSizeGB
    )
    
    $CombinedResults = @()

    # Фильтруем пустые результаты восстановления
    $FilteredRestoreResults = $RestoreResults | Where-Object { -not [string]::IsNullOrEmpty($_.DatabaseName) }
    
    foreach ($r in $FilteredRestoreResults) {
        $CombinedResults += [PSCustomObject]@{
            Type = "Восстановление"
            DatabaseName = $r.DatabaseName
            Status = $r.Status
            Duration = $r.Duration
            Details = $r.Details
            SortPriority = if ($r.Status -eq "Ошибка") { 1 } elseif ($r.Status -eq "Успешно") { 2 } else { 3 }
        }
    }

    foreach ($cr in $CheckResults) {
        $DetailsText = "$($cr.Details) (попыток: $($cr.Attempts))"
        
        $CombinedResults += [PSCustomObject]@{
            Type = "Проверка"
            DatabaseName = $cr.DatabaseName
            Status = $cr.Status
            Duration = "N/A"
            Details = $DetailsText
            SortPriority = if ($cr.Status -eq "Ошибка") { 1 } elseif ($cr.Status -eq "Завершена") { 2 } else { 3 }
        }
    }

    $SortedResults = $CombinedResults | Sort-Object @{Expression = {$_.SortPriority}; Ascending = $true}, @{Expression = {$_.Type}; Ascending = $false}, @{Expression = {$_.DatabaseName}; Ascending = $true}

    $HtmlRows = @()
    foreach ($result in $SortedResults) {
        # Пропускаем пустые строки
        if ([string]::IsNullOrEmpty($result.DatabaseName)) {
            continue
        }
        
        $Bg = if ($result.Status -eq "Ошибка") { "#FFB6C1" } 
              elseif ($result.Status -in @("Успешно", "Завершена")) { "#90EE90" } 
              else { "#FFA07A" }
        $HtmlRows += "<tr style='background-color:$Bg'><td>$($result.Type)</td><td>$($result.DatabaseName)</td><td>$($result.Status)</td><td>$($result.Duration)</td><td>$($result.Details)</td></tr>"
    }

    $SuccessRestores = ($FilteredRestoreResults | Where-Object { $_.Status -eq "Успешно" }).Count
    $ErrorRestores = ($FilteredRestoreResults | Where-Object { $_.Status -eq "Ошибка" }).Count
    $SuccessChecks = ($CheckResults | Where-Object { $_.Status -eq "Завершена" }).Count
    $ErrorChecks = ($CheckResults | Where-Object { $_.Status -eq "Ошибка" }).Count
    $SkippedChecks = ($CheckResults | Where-Object { $_.Status -eq "Пропущена" }).Count
    
    $TotalDeleted = if ($DeleteResults) { ($DeleteResults | Where-Object { $_.Status -eq 'Успешно' }).Count } else { 0 }
    $TotalFreedGB = if ($DeleteResults) { [math]::Round(($DeleteResults | Where-Object { $_.Status -eq 'Успешно' } | Measure-Object -Property SizeMB -Sum).Sum / 1024, 2) } else { 0 }

    $SegmentDescription = switch ($PathSegmentFromEnd) {
        1 { "Имя базы" }
        2 { "Папка перед базой (имя сервера)" }
        3 { "Папка за 2 уровня до базы" }
        4 { "Папка за 3 уровня до базы" }
        default { "Сегмент $PathSegmentFromEnd с конца" }
    }

    $HtmlReport = @"
<!DOCTYPE html>
<html><head><meta charset='utf-8'><title>Отчёт db01 восстановление баз</title>
<style>
    body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; line-height: 1.6; color: #333; max-width: 1200px; margin: 0 auto; padding: 20px; }
    h2 { color: #2c3e50; border-bottom: 2px solid #3498db; padding-bottom: 10px; }
    table { border-collapse: collapse; width: 100%; margin: 20px 0; }
    th, td { border: 1px solid #ddd; padding: 12px; text-align: left; }
    th { background: #3498db; color: white; font-weight: bold; }
    .error-row { background-color: #FFB6C1; }
    .success-row { background-color: #90EE90; }
    .warning-row { background-color: #FFA07A; }
    .summary { background: #f8f9fa; border-left: 4px solid #3498db; padding: 15px; margin: 20px 0; }
    ul { padding-left: 20px; }
    .success-text { color: #28a745; font-weight: bold; }
    .error-text { color: #dc3545; font-weight: bold; }
    .warning-text { color: #ffc107; font-weight: bold; }
    .info-text { color: #17a2b8; font-weight: bold; }
</style></head>
<body>
<h2>Отчёт о восстановлении и проверке на $TargetServer</h2>
<div class="summary">
    <p><strong>Дата выполнения:</strong> $(Get-Date -Format "yyyy-MM-dd HH:mm:ss")</p>
    <p><strong>Общее время выполнения:</strong> $TotalDuration</p>
    <p><strong>Время проверки целостности:</strong> $CheckDuration</p>
    <p><strong>Источник бэкапов:</strong> $SourceBackupRoot</p>
    <p><strong>Коэффициент расширения бэкапа:</strong> $BackupExpansionFactor</p>
    <p><strong>Расчетный размер после восстановления:</strong> $TotalExpandedSizeGB GB</p>
    <p><strong>Фильтр баз:</strong> $(if ($DatabaseFilter.Count -eq 0) {'ВСЕ базы'} else {$($DatabaseFilter -join ', ')})</p>
    <p><strong>Режим удаления:</strong> $DeletionMode</p>
    <p><strong>Формирование имен:</strong> $SegmentDescription (сегмент $PathSegmentFromEnd с конца)</p>
</div>
<div class="summary">
    <p><strong>Статистика:</strong></p>
    <ul>
        <li>Всего баз для восстановления: $TotalDatabases</li>
        <li>Удалено баз для освобождения места: <span class="info-text">$TotalDeleted</span></li>
        <li>Освобождено места: <span class="info-text">$TotalFreedGB GB</span></li>
        <li>Успешных восстановлений: <span class="success-text">$SuccessRestores</span></li>
        <li>Ошибок восстановления: <span class="error-text">$ErrorRestores</span></li>
        <li>Успешных проверок: <span class="success-text">$SuccessChecks</span></li>
        <li>Ошибок проверки: <span class="error-text">$ErrorChecks</span></li>
        <li>Пропущенных проверок: <span class="warning-text">$SkippedChecks</span></li>
    </ul>
</div>
<p><em>Примечание: строки отсортированы по приоритету (ошибки → успешно → пропущено)</em></p>
<table>
    <thead>
        <tr>
            <th>Операция</th>
            <th>База</th>
            <th>Статус</th>
            <th>Время</th>
            <th>Детали</th>
        </tr>
    </thead>
    <tbody>
        $($HtmlRows -join '')
    </tbody>
</table>
<p><strong>Логи:</strong> $TranscriptPath</p>
<p><strong>Таблицы с логами:</strong> [dba].[dbo].[RestoreLog], [dba].[dbo].[DBCCResults]</p>
</body></html>
"@
    
    return $HtmlReport
}

# === ОСНОВНОЙ СКРИПТ ===
Write-Log "=== СТАРТ СКРИПТА ==="
Write-Log "Целевой сервер: $TargetServer"
Write-Log "Путь к бэкапам: $SourceBackupRoot"
Write-Log "Режим удаления: $DeletionMode"
Write-Log "Коэффициент расширения бэкапа: $BackupExpansionFactor"
Write-Log "Формирование имен (сегмент пути с конца): $PathSegmentFromEnd"
Write-Log "Фильтр баз: $($DatabaseFilter -join ', ')"

$TotalStartTime = Get-Date
$RestoreResults = @()
$DeleteResults = @()  # Инициализируем переменную заранее

try {
    # 0. Проверяем наличие базы dba
    Write-Log "Проверка наличия базы dba..."
    try {
        $dbaDb = Get-DbaDatabase -SqlInstance $TargetServer -Database "dba" -ErrorAction Stop
        if (-not $dbaDb) {
            Write-Log "База dba не существует на сервере $TargetServer. Создаем..."
            $createDbQuery = @"
IF NOT EXISTS (SELECT * FROM sys.databases WHERE name = 'dba')
BEGIN
    CREATE DATABASE [dba];
    PRINT 'База dba создана.'
END
ELSE
BEGIN
    PRINT 'База dba уже существует.'
END
"@
            Invoke-DbaQuery -SqlInstance $TargetServer -Query $createDbQuery -EnableException
            Write-Log "База dba проверена/создана"
        } else {
            Write-Log "База dba существует"
        }
    } catch {
        Write-Log "Ошибка при проверке/создании базы dba: $($_.Exception.Message)"
        throw
    }

    # 1. Сканирование бэкапов
    Write-Log "Этап 1: Сканирование бэкапов..."
    $BackupDatabases = Get-BackupDatabases -BackupRootPath $SourceBackupRoot `
        -DatabaseFilter $DatabaseFilter `
        -ExcludedDbs $ExcludedDatabases `
        -ExcludedServers $ExcludedServerFolders `
        -PathSegmentFromEnd $PathSegmentFromEnd
    
    if ($BackupDatabases.Count -eq 0) {
        $message = "Не найдено баз с бэкапами для восстановления по пути: $SourceBackupRoot"
        Write-Log $message
        Send-EmailReport -Subject "Ошибка: Нет баз для восстановления на $TargetServer" -Body $message
        Stop-Transcript
        exit 1
    }
    
    Write-Log "Найдено баз для восстановления: $($BackupDatabases.Count)"
    $totalBackupSizeMB = ($BackupDatabases | Measure-Object -Property TotalBackupSizeMB -Sum).Sum
    $totalBackupSizeGB = [math]::Round($totalBackupSizeMB / 1024, 2)
    $totalExpandedSizeMB = ($BackupDatabases | Measure-Object -Property ExpandedSizeMB -Sum).Sum
    $totalExpandedSizeGB = [math]::Round($totalExpandedSizeMB / 1024, 2)
    Write-Log "Общий размер всех бэкапов: $totalBackupSizeMB MB ($totalBackupSizeGB GB)"
    Write-Log "Общий расчетный размер баз после восстановления: $totalExpandedSizeMB MB ($totalExpandedSizeGB GB)"

    # 2. Удаление баз (в зависимости от режима)
    $DeleteResults = @()
    switch ($DeletionMode) {
        "All" {
            Write-Log "Этап 2: Удаление всех баз (режим All)..."
            $DeleteResults = Remove-AllDatabasesParallel -TargetServer $TargetServer `
                -DatabasesToExcludeFromDeletion $DatabasesToExcludeFromDeletion `
                -ThrottleLimit $DeletionThrottleLimit
        }
        "Calculate" {
            Write-Log "Этап 2: Проверка места и расчет необходимости удаления (режим Calculate)..."
            $DeleteResults = Calculate-SpaceAndDelete -TargetServer $TargetServer `
                -DataPath $DataPath `
                -DatabasesInfo $BackupDatabases `
                -DatabasesToExcludeFromDeletion $DatabasesToExcludeFromDeletion `
                -ThrottleLimit $DeletionThrottleLimit
        }
        "Smart" {
            Write-Log "Этап 2: Умное удаление баз (режим Smart)..."
            # Аналогично Calculate, но всегда удаляет минимально необходимые базы
            $DeleteResults = Calculate-SpaceAndDelete -TargetServer $TargetServer `
                -DataPath $DataPath `
                -DatabasesInfo $BackupDatabases `
                -DatabasesToExcludeFromDeletion $DatabasesToExcludeFromDeletion `
                -ThrottleLimit $DeletionThrottleLimit
        }
        "None" {
            Write-Log "Этап 2: Пропуск удаления баз (режим None)..."
        }
        default {
            Write-Log "Неизвестный режим удаления: $DeletionMode. Используется режим Calculate."
            $DeleteResults = Calculate-SpaceAndDelete -TargetServer $TargetServer `
                -DataPath $DataPath `
                -DatabasesInfo $BackupDatabases `
                -DatabasesToExcludeFromDeletion $DatabasesToExcludeFromDeletion `
                -ThrottleLimit $DeletionThrottleLimit
        }
    }

    # 3. Восстановление баз (параллельное)
    Write-Log "Этап 3: Параллельное восстановление $($BackupDatabases.Count) баз..."
    
    $RestoreResults = $BackupDatabases | ForEach-Object -Parallel {
        $dbInfo = $_
        $targetServer = $using:TargetServer
        $dataPath = $using:DataPath
        $logPath = $using:LogPath
        $transcriptPath = $using:TranscriptPath
        $deletionMode = $using:DeletionMode
        $backupExpansionFactor = $using:BackupExpansionFactor
        
        Import-Module dbatools -Force
        
        $startTime = Get-Date
        $dbName = $dbInfo.DatabaseName
        $originalName = $dbInfo.OriginalDatabaseName
        $serverName = $dbInfo.ServerName
        $fullBackupPath = $dbInfo.FullBackupFile
        $diffBackupPath = $dbInfo.DiffBackupFile
        $hasDiff = $dbInfo.HasDiffBackup
        $backupSizeMB = $dbInfo.BackupSizeMB
        $expandedSizeMB = $dbInfo.ExpandedSizeMB
        $wasDropped = $false
        
        try {
            # Проверяем, что имя базы не пустое
            if ([string]::IsNullOrEmpty($dbName)) {
                return $null
            }
            
            # Удаляем, если существует
            $existingDb = Get-DbaDatabase -SqlInstance $targetServer -Database $dbName -ErrorAction SilentlyContinue
            if ($existingDb) {
                Remove-DbaDatabase -SqlInstance $targetServer -Database $dbName -Confirm:$false
                $wasDropped = $true
            }
            
            # Получаем FileMapping из FULL бэкапа
            $filemap = @{}
            try {
                $backupHeader = Read-DbaBackupHeader -SqlInstance $targetServer -Path $fullBackupPath
                foreach ($file in $backupHeader.FileList) {
                    if ($file.Type -eq 'D') {
                        $newPath = Join-Path $dataPath "${dbName}_$($file.LogicalName).mdf"
                    } else {
                        $newPath = Join-Path $logPath "${dbName}_$($file.LogicalName).ldf"
                    }
                    $filemap[$file.LogicalName] = $newPath
                }
            } catch {
                # Продолжаем без маппинга
            }
            
            # Восстанавливаем FULL бэкап
            $fullRestoreParams = @{
                SqlInstance = $targetServer
                DatabaseName = $dbName
                Path = $fullBackupPath
                WithReplace = $true
                NoRecovery = $true
            }
            
            if ($filemap.Count -gt 0) {
                $fullRestoreParams['FileMapping'] = $filemap
            }
            
            Restore-DbaDatabase @fullRestoreParams | Out-Null
            
            # Восстанавливаем DIFF если есть
            $backupType = "FULL"
            if ($hasDiff -and $diffBackupPath -and (Test-Path $diffBackupPath)) {
                $diffRestoreParams = @{
                    SqlInstance = $targetServer
                    DatabaseName = $dbName
                    Path = $diffBackupPath
                    Continue = $true
                    NoRecovery = $false
                }
                
                if ($filemap.Count -gt 0) {
                    $diffRestoreParams['FileMapping'] = $filemap
                }
                
                Restore-DbaDatabase @diffRestoreParams | Out-Null
                $backupType = "FULL+DIFF"
            } else {
                # Восстанавливаем только FULL
                Invoke-DbaQuery -SqlInstance $targetServer -Query "RESTORE DATABASE [$dbName] WITH RECOVERY"
            }
            
            # Проверяем статус базы
            $finalDb = Get-DbaDatabase -SqlInstance $targetServer -Database $dbName -ErrorAction SilentlyContinue
            $endTime = Get-Date
            $duration = $endTime - $startTime
            
            if ($finalDb -and $finalDb.Status -eq 'Normal') {
                $status = "Успешно"
                $details = "Восстановлено $backupType (бэкап: $backupSizeMB MB → база: ~$expandedSizeMB MB)"
                if ($wasDropped) { $details += " (предварительно удалена)" }
            } else {
                $status = "Ошибка"
                $details = "База не ONLINE после восстановления"
                if ($wasDropped) { $details += " (предварительно удалена)" }
            }
            
            return [PSCustomObject]@{
                DatabaseName     = $dbName
                OriginalName     = $originalName
                ServerName       = $serverName
                Status           = $status
                Duration         = "{0:hh\:mm\:ss}" -f $duration
                Details          = $details
                BackupType       = $backupType
                WasDropped       = $wasDropped  # Убедимся, что это Boolean
                DurationSeconds  = $duration.TotalSeconds
                FinalStatus      = if ($finalDb) { $finalDb.Status } else { "Unknown" }
                EstimatedSizeMB  = $expandedSizeMB  # Используем расширенный размер
            }
            
        } catch {
            $endTime = Get-Date
            $duration = $endTime - $startTime
            $errorMsg = $_.Exception.Message
            
            # Возвращаем результат с ошибкой только если имя базы не пустое
            if (-not [string]::IsNullOrEmpty($dbName)) {
                return [PSCustomObject]@{
                    DatabaseName     = $dbName
                    OriginalName     = $originalName
                    ServerName       = $serverName
                    Status           = "Ошибка"
                    Duration         = "{0:hh\:mm\:ss}" -f $duration
                    Details          = "Ошибка: $errorMsg"
                    BackupType       = "Ошибка"
                    WasDropped       = $wasDropped  # Убедимся, что это Boolean
                    DurationSeconds  = $duration.TotalSeconds
                    FinalStatus      = "Error"
                    EstimatedSizeMB  = $expandedSizeMB  # Используем расширенный размер
                }
            } else {
                return $null
            }
        }
    } -ThrottleLimit $RestoreThrottleLimit

    # Фильтруем пустые результаты
    $RestoreResults = $RestoreResults | Where-Object { $_ -ne $null }
    
    Write-Log "Параллельное восстановление завершено. Обработано результатов: $($RestoreResults.Count)"

    # 4. Запись в RestoreLog
    Write-Log "Этап 4: Запись результатов восстановления в таблицу RestoreLog..."
    $logWriteSuccess = 0
    $logWriteFail = 0
    
    # Сначала убедимся, что таблица существует
    $tableReady = Ensure-RestoreLogTable -TargetServer $TargetServer
    if (-not $tableReady) {
        Write-Log "Предупреждение: не удалось создать таблицу RestoreLog"
    }
    
    foreach ($res in $RestoreResults) {
        # Убедимся, что WasDropped - это Boolean
        $wasDropped = if ($null -eq $res.WasDropped) { $false } else { [bool]$res.WasDropped }
        
        $result = Write-RestoreLogToDb -TargetServer $TargetServer `
            -DatabaseName $res.DatabaseName `
            -OriginalDatabaseName $res.OriginalName `
            -Status $res.Status `
            -Details $res.Details `
            -BackupType $res.BackupType `
            -WasDropped $wasDropped `
            -LogFilePath $TranscriptPath `
            -DurationSeconds $res.DurationSeconds `
            -FinalStatus $res.FinalStatus `
            -EstimatedSizeMB $res.EstimatedSizeMB `
            -DeletionMode $DeletionMode `
            -SegmentName $res.ServerName
        
        if ($result) {
            $logWriteSuccess++
        } else {
            $logWriteFail++
        }
    }
    
    Write-Log "Запись в RestoreLog завершена: успешно - $logWriteSuccess, с ошибкой - $logWriteFail"

    # 5. Проверка целостности с повторными попытками
    $CheckResults = @()
    if ($RunIntegrityCheck) {
        Write-Log "Ожидание 2 минуты для завершения всех операций восстановления..."
        Start-Sleep -Seconds 120
        
        $SuccessfulDatabases = $RestoreResults | Where-Object { $_.Status -eq "Успешно" } | ForEach-Object { $_.DatabaseName }
        
        if ($SuccessfulDatabases.Count -gt 0) {
            Write-Log "Этап 5: Проверка целостности восстановленных баз..."
            $CheckStartTime = Get-Date
            
            Write-Log "Запуск проверок для $($SuccessfulDatabases.Count) успешно восстановленных баз..."
            
            # Используем ThreadJob для проверки
            $CheckJobs = $SuccessfulDatabases | ForEach-Object {
                Start-ThreadJob -ScriptBlock $function:CheckDatabaseWithRetry -ArgumentList $TargetServer, $_, $MaxRetryAttempts, $RetryDelaySeconds -ThrottleLimit $CheckThrottleLimit
            }
            
            Write-Log "Ожидание завершения всех проверок..."
            $CheckResults = $CheckJobs | Wait-Job | Receive-Job
            $CheckJobs | Remove-Job -Force
            
            $CheckEndTime = Get-Date
            $CheckDuration = $CheckEndTime - $CheckStartTime
            Write-Log "Проверка целостности заняла: $($CheckDuration.ToString('hh\:mm\:ss'))"
            
            Write-Log "Всего обработано результатов проверки: $($CheckResults.Count)"
            Write-Log "Результаты проверки целостности:"
            foreach ($result in $CheckResults) {
                $msg = "База: $($result.DatabaseName), Статус: $($result.Status), Детали: $($result.Details), Попыток: $($result.Attempts)"
                Write-Log $msg
            }
            
            # Запись результатов проверки в DBCCResults
            Write-DBCCResultsToDb -TargetServer $TargetServer -CheckResults $CheckResults
            
            # Очистка старых логов Ola Hallengren
            Cleanup-OldLogs -TargetServer $TargetServer -OlaLogsCleanupDays $OlaLogsCleanupDays
            
        } else {
            Write-Log "Нет успешно восстановленных баз для проверки целостности"
        }
    }

    # 6. Очистка логов
    if ($CleanupOldLogs) {
        Write-Log "Этап 6: Очистка старых логов..."
        try {
            $OldTranscripts = Get-ChildItem -Path "C:\temp\" -Filter "RestoreTranscript_*.log" -Recurse |
                Where-Object { $_.CreationTime -lt (Get-Date).AddDays(-$LogCleanupDays) }
            if ($OldTranscripts) {
                $OldTranscripts | Remove-Item -Force
                Write-Log "Удалены старые транскрипты (старше $LogCleanupDays дней): $($OldTranscripts.Count) файлов"
            }
        } catch {
            Write-Log "Ошибка при очистке логов: $($_.Exception.Message)"
        }
    }

    # 7. Подготовка итогов
    $TotalEndTime = Get-Date
    $TotalDuration = "{0:hh\:mm\:ss}" -f ($TotalEndTime - $TotalStartTime)
    $CheckDurationStr = if ($RunIntegrityCheck -and $CheckResults.Count -gt 0) { $CheckDuration.ToString('hh\:mm\:ss') } else { "N/A" }
    
    $SuccessRestores = ($RestoreResults | Where-Object { $_.Status -eq "Успешно" }).Count
    $ErrorRestores = ($RestoreResults | Where-Object { $_.Status -eq "Ошибка" }).Count
    $SuccessChecks = ($CheckResults | Where-Object { $_.Status -eq "Завершена" }).Count
    $ErrorChecks = ($CheckResults | Where-Object { $_.Status -eq "Ошибка" }).Count
    $SkippedChecks = ($CheckResults | Where-Object { $_.Status -eq "Пропущена" }).Count

    # 8. Отправка отчета
    if ($SendEmailReport) {
        Write-Log "Этап 7: Отправка HTML-отчёта..."
        $htmlReport = New-HtmlReport -RestoreResults $RestoreResults `
            -DeleteResults $DeleteResults `
            -CheckResults $CheckResults `
            -TotalDuration $TotalDuration `
            -CheckDuration $CheckDurationStr `
            -DeletionMode $DeletionMode `
            -TargetServer $TargetServer `
            -TotalDatabases $BackupDatabases.Count `
            -DatabaseFilter $DatabaseFilter `
            -PathSegmentFromEnd $PathSegmentFromEnd `
            -SourceBackupRoot $SourceBackupRoot `
            -BackupExpansionFactor $BackupExpansionFactor `
            -TotalExpandedSizeGB $totalExpandedSizeGB
        
        $emailSubject = "Отчёт db01: Восстановление баз - $SuccessRestores/$($BackupDatabases.Count) успешно - $(Get-Date -Format 'yyyy-MM-dd HH:mm')"
        Send-EmailReport -Subject $emailSubject -Body $htmlReport -IsHtml $true -Attachments @($TranscriptPath)
    }

    # 9. Вывод итогов в консоль
    Write-Host "`n" + ("=" * 70) -ForegroundColor Cyan
    Write-Host "ИТОГИ ВОССТАНОВЛЕНИЯ" -ForegroundColor Yellow
    Write-Host ("=" * 70) -ForegroundColor Cyan
    Write-Host "Сервер: $TargetServer" -ForegroundColor Cyan
    Write-Host "Источник: $SourceBackupRoot" -ForegroundColor Cyan
    Write-Host "Коэффициент расширения: $BackupExpansionFactor" -ForegroundColor Cyan
    Write-Host "Формирование имен: сегмент $PathSegmentFromEnd с конца" -ForegroundColor Cyan
    Write-Host "Всего баз: $($BackupDatabases.Count)" -ForegroundColor White
    Write-Host "Размер бэкапов: $totalBackupSizeGB GB → Расчетный размер баз: $totalExpandedSizeGB GB" -ForegroundColor White
    Write-Host "Режим удаления: $DeletionMode" -ForegroundColor Cyan
    
    if ($DeleteResults.Count -gt 0) {
        $deletedCount = ($DeleteResults | Where-Object { $_.Status -eq 'Успешно' }).Count
        $freedGB = [math]::Round(($DeleteResults | Where-Object { $_.Status -eq 'Успешно' } | Measure-Object -Property SizeMB -Sum).Sum / 1024, 2)
        Write-Host "Удалено баз для освобождения места: $deletedCount" -ForegroundColor Magenta
        Write-Host "Освобождено места: $freedGB GB" -ForegroundColor Magenta
    }
    
    Write-Host "Успешно восстановлено: $SuccessRestores" -ForegroundColor $(if ($SuccessRestores -gt 0) { "Green" } else { "Red" })
    Write-Host "С ошибками восстановления: $ErrorRestores" -ForegroundColor $(if ($ErrorRestores -gt 0) { "Red" } else { "Green" })
    
    if ($RunIntegrityCheck -and $CheckResults.Count -gt 0) {
        Write-Host "Проверок целостности: $($CheckResults.Count)" -ForegroundColor White
        Write-Host "Успешных проверок: $SuccessChecks" -ForegroundColor Green
        Write-Host "Ошибок проверки: $ErrorChecks" -ForegroundColor $(if ($ErrorChecks -gt 0) { "Red" } else { "Green" })
        Write-Host "Пропущенных проверок: $SkippedChecks" -ForegroundColor Yellow
    }
    
    Write-Host "Общее время: $TotalDuration" -ForegroundColor Cyan
    Write-Host "Путь к логам: $TranscriptPath" -ForegroundColor Gray
    Write-Host "Записано в RestoreLog: $logWriteSuccess из $($RestoreResults.Count)" -ForegroundColor $(if ($logWriteSuccess -eq $RestoreResults.Count) { "Green" } else { "Yellow" })
    
    if ($SendEmailReport) {
        Write-Host "Отчет отправлен на email" -ForegroundColor Green
    }
    
    # Детали восстановления
    if ($RestoreResults.Count -gt 0) {
        Write-Host "`nДетали восстановления:" -ForegroundColor Yellow
        foreach ($result in $RestoreResults) {
            $color = if ($result.Status -eq "Успешно") { "Green" } else { "Red" }
            Write-Host "  $($result.DatabaseName)" -ForegroundColor $color -NoNewline
            Write-Host " [$($result.BackupType)]" -ForegroundColor Magenta -NoNewline
            Write-Host " - $($result.Status)" -ForegroundColor $color -NoNewline
            Write-Host " ($($result.Duration))" -ForegroundColor Gray
            if ($result.Status -eq "Ошибка") {
                Write-Host "    Ошибка: $($result.Details)" -ForegroundColor DarkRed
            }
        }
    } else {
        Write-Host "`nНет результатов восстановления для отображения" -ForegroundColor Yellow
    }
    
    # Детали удаления (если были)
    if ($DeleteResults.Count -gt 0) {
        Write-Host "`nДетали удаления баз:" -ForegroundColor Yellow
        foreach ($delete in $DeleteResults | Where-Object { $_.Status -eq 'Успешно' }) {
            $sizeGB = [math]::Round($delete.SizeMB / 1024, 2)
            Write-Host "  Удалено: $($delete.DatabaseName) ($sizeGB GB)" -ForegroundColor Magenta
        }
    }
    
    # Детали проверки целостности
    if ($RunIntegrityCheck -and $CheckResults.Count -gt 0) {
        Write-Host "`nДетали проверки целостности:" -ForegroundColor Yellow
        foreach ($check in $CheckResults) {
            $color = if ($check.Status -eq "Завершена") { "Green" } 
                    elseif ($check.Status -eq "Ошибка") { "Red" } 
                    else { "Yellow" }
            Write-Host "  $($check.DatabaseName)" -ForegroundColor $color -NoNewline
            Write-Host " - $($check.Status)" -ForegroundColor $color -NoNewline
            Write-Host " (попыток: $($check.Attempts))" -ForegroundColor Gray
            if ($check.Status -ne "Завершена") {
                Write-Host "    Детали: $($check.Details)" -ForegroundColor DarkYellow
            }
        }
    }
    
    Write-Host "`n" + ("=" * 70) -ForegroundColor Cyan
    Write-Host "СКРИПТ ЗАВЕРШЕН УСПЕШНО" -ForegroundColor Green
    Write-Host ("=" * 70) -ForegroundColor Cyan

} catch {
    $errorMessage = "КРИТИЧЕСКАЯ ОШИБКА: $($_.Exception.Message)"
    Write-Log $errorMessage
    Write-Host $errorMessage -ForegroundColor Red
    
    # Отправка email об ошибке
    if ($SendEmailReport) {
        $errorBody = @"
<h2>Критическая ошибка при восстановлении баз</h2>
<p><strong>Сервер:</strong> $TargetServer</p>
<p><strong>Время ошибки:</strong> $(Get-Date -Format 'dd.MM.yyyy HH:mm:ss')</p>
<p><strong>Источник:</strong> $SourceBackupRoot</p>
<p><strong>Коэффициент расширения:</strong> $BackupExpansionFactor</p>
<p><strong>Ошибка:</strong> $($_.Exception.Message)</p>
<p><strong>Формирование имен:</strong> сегмент $PathSegmentFromEnd с конца</p>
<p><strong>Файл логов:</strong> $TranscriptPath</p>
"@
        try {
            Send-EmailReport -Subject "КРИТИЧЕСКАЯ ОШИБКА: Сбой восстановления на $TargetServer" -Body $errorBody -IsHtml $true -Attachments @($TranscriptPath)
        } catch {
            Write-Host "Ошибка отправки email об ошибке: $($_.Exception.Message)" -ForegroundColor Red
        }
    }
    
    throw
} finally {
    Write-Log "=== ЗАВЕРШЕНИЕ РАБОТЫ СКРИПТА ==="
    try {
        Stop-Transcript
    } catch {
        Write-Host "Ошибка остановки транскрипта: $($_.Exception.Message)" -ForegroundColor Yellow
    }
}

Бэкапы делаются

-- В ночь с субботы на воскресение
--if(datepart(weekday, getdate()) = 1)
-- Для миграции на АГ сметили создание резервных копий для того что бы использовать верфицированые фулл бэкапы в процессе работ
-- В ночь с Пятницу на субботу  (В Субботу в 01.00 создать фулл бекап)

if(datepart(weekday, getdate()) = 7)
begin
    EXECUTE [dbo].[DatabaseBackup]
    @Databases = 'USER_DATABASES,-%_restore',
    @Directory = N'\\ARC03.adminbd.ru\sql_backup$',
    @BackupType = 'FULL',
    @Verify = 'Y',
    @Compress = 'Y',
    @CleanupTime = 312,
    @CleanupMode = 'AFTER_BACKUP',
    @CheckSum = 'Y',
    @LogToTable = 'Y',
    @DatabasesInParallel= 'Y';
end
--проверяем наличие баз для полного бэкапа (ни разу не делался)
declare @Databases as varchar(5000);
set @Databases = (
    select stuff(
        (
            select ',' + isnull(d.[name], bs.[database_name]) as [text()]
            from sys.databases d with(nolock)
            left join msdb.dbo.backupset bs with(nolock)
            on bs.[database_name] = d.[name] 
            and bs.backup_finish_date > dateadd(day, -30, GETDATE()) 
            --определяем наличие бэкапов именно на текущем сервере
            and bs.server_name = @@SERVERNAME
            where 1=1
            --user databases
            and d.database_id > 4
            and d.[name] not like '%restore'
            --online
            and d.[state] = 0
            and bs.[database_name] is null
            order by isnull(d.[name], bs.[database_name])
            for xml path('')
        ), 1, 1, ''
    )
);

if(@Databases is not null)
begin
    EXECUTE [dbo].[DatabaseBackup]
    @Databases = @Databases,
    @Directory = N'\\ARC03.adminbd.ru\sql_backup$',
    @BackupType = 'FULL',
    @Verify = 'Y',
    @Compress = 'Y',
    @CheckSum = 'Y',
    @LogToTable = 'Y',
    @DatabasesInParallel= 'Y';
end;

--делаем разностный бэкап
EXECUTE [dbo].[DatabaseBackup]
@Databases = 'USER_DATABASES,-%_restore',
@Directory = N'\\ARC03.adminbd.ru\sql_backup$',
@BackupType = 'DIFF',
@Verify = 'Y',
@Compress = 'Y',
@CleanupTime = 168,
@CleanupMode = 'AFTER_BACKUP',
@CheckSum = 'Y',
@LogToTable = 'Y',
@DatabasesInParallel= 'Y';

 

Similar Posts:

Метки:

Добавить комментарий

Яндекс.Метрика