Загрузка данных


ftp.ps1
# ============================================================
#  ftp.ps1 — скачивание архивов с FTP и запуск downloader.ps1
# ============================================================
#  Логика:
#   1. Проверяем версии папок на FTP (маркер версии = имя папки).
#   2. Скачиваем RAR-архивы из EKP только для изменившихся версий.
#   3. Сохраняем архивы во временную локальную папку.
#   4. Вызываем downloader.ps1 — он устанавливает MSI,
#      архивирует установленные папки и кладёт результат на Z:\Documents.
#   5. Удаляем локальные архивы после успешной обработки.
# ============================================================

# --- Настройки FTP ---
$ftpHost     = "n5001-fap.dpc.tax.nalog.ru"
$ftpPath     = "/AisNalog3"
$ftpUser     = "anonymous"
$ftpPassword = ""

# --- Временная локальная папка (только для скачивания) ---
$localBasePath = "C:\Users\n4200-01-031\Desktop\App"

# --- Сетевая папка (куда downloader кладёт готовые архивы) ---
$networkBasePath = "Z:\Documents"

# --- Соответствие удалённых папок локальным ---
$folderMapping = @{
    "AisNalog3_PROM" = "KPE"
    "AisNalog3_PPK"  = "PPK"
    "OE"             = "KOE"
    "OE_IZ"          = "KOEIZ"
}

# --- Шаблон имени файла-маркера версии ---
$versionPattern = '^\d+_\d+_\d+_\d+$'

# --- Путь к downloader.ps1 (рядом с этим скриптом) ---
$scriptDir  = if ($PSScriptRoot) { $PSScriptRoot } else { Split-Path -Parent $PSCommandPath }
$downloader = Join-Path $scriptDir "downloader.ps1"

# --- Параметры скорости ---
$BufferSize      = 16MB
$ProgressEveryMs = 300
$ShowSpeed       = $true

# ============================================================
#  Вспомогательные функции
# ============================================================

function Show-ProgressLine {
    param(
        [string]$FileName,
        [int64]$Downloaded,
        [int64]$Total,
        [int]$Width = 30
    )
    if ($Total -gt 0) {
        $percent = [math]::Floor(($Downloaded / $Total) * 100)
        if ($percent -gt 100) { $percent = 100 }
        $mbNow   = [math]::Round($Downloaded / 1MB, 1)
        $mbTotal = [math]::Round($Total / 1MB, 1)
        $filled  = [math]::Floor($Width * $percent / 100)
        if ($filled -gt $Width) { $filled = $Width }
        $empty = $Width - $filled
        $bar   = ("*" * $filled) + ("." * $empty)
        Write-Host ("`r  {0} : [{1}] {2}%  {3}/{4} MB" -f $FileName, $bar, $percent, $mbNow, $mbTotal) -NoNewline -ForegroundColor Cyan
    }
    else {
        $mbNow = [math]::Round($Downloaded / 1MB, 1)
        Write-Host ("`r  {0} : {1} MB" -f $FileName, $mbNow) -NoNewline -ForegroundColor Cyan
    }
}

function Get-FtpFoldersWithDates {
    param([string]$ftpPath)
    $folders = @{}
    try {
        $uri = "ftp://$ftpHost$ftpPath/"
        $request = [System.Net.FtpWebRequest]::Create($uri)
        $request.Method      = [System.Net.WebRequestMethods+Ftp]::ListDirectoryDetails
        $request.Credentials = New-Object System.Net.NetworkCredential($ftpUser, $ftpPassword)
        $request.UseBinary   = $true
        $request.UsePassive  = $true
        $request.EnableSsl   = $false
        $request.KeepAlive   = $false
        $request.Timeout     = 30000

        $response = $request.GetResponse()
        $stream   = $response.GetResponseStream()
        $reader   = New-Object System.IO.StreamReader($stream)
        $listing  = $reader.ReadToEnd()
        $reader.Close(); $response.Close()

        $lines = $listing -split "`r?`n" | Where-Object { $_ -ne "" }

        foreach ($line in $lines) {
            $isDirectory = $false
            $folderName  = $null
            $dateTime    = $null

            if ($line -match "^d.*\s+(\S+)$") {
                $folderName  = $matches[1]
                $isDirectory = $true
                if ($line -match "(\w{3})\s+(\d{1,2})\s+(\d{2}:\d{2})") {
                    $month = $matches[1]; $day = $matches[2]; $time = $matches[3]
                    $currentYear = (Get-Date).Year
                    try {
                        $dateTime = [DateTime]::ParseExact("$month $day $currentYear $time", "MMM d yyyy HH:mm", [System.Globalization.CultureInfo]::InvariantCulture)
                    } catch {
                        try {
                            $dateTime = [DateTime]::ParseExact("$month $day $currentYear $time", "MMM dd yyyy HH:mm", [System.Globalization.CultureInfo]::InvariantCulture)
                        } catch { $dateTime = $null }
                    }
                }
            }
            elseif ($line -match "<DIR>\s+(\S+)$") {
                $folderName  = $matches[1]
                $isDirectory = $true
                if ($line -match "(\d{2}-\d{2}-\d{2})\s+(\d{2}:\d{2}[AP]M)") {
                    try {
                        $dateTime = [DateTime]::ParseExact("$($matches[1]) $($matches[2])", "MM-dd-yy hh:mmtt", [System.Globalization.CultureInfo]::InvariantCulture)
                    } catch { $dateTime = $null }
                }
            }

            if ($isDirectory -and $folderName -and $folderName -ne "." -and $folderName -ne "..") {
                if ($dateTime) { $folders[$folderName] = $dateTime }
                else           { $folders[$folderName] = [DateTime]::MinValue }
            }
        }
        return $folders
    }
    catch {
        Write-Host "  Ошибка при получении списка папок: $_" -ForegroundColor Red
        return @{}
    }
}

function Get-FtpFileList {
    param([string]$ftpPath)
    $items = @()
    try {
        $uri = "ftp://$ftpHost$ftpPath/"
        $request = [System.Net.FtpWebRequest]::Create($uri)
        $request.Method      = [System.Net.WebRequestMethods+Ftp]::ListDirectory
        $request.Credentials = New-Object System.Net.NetworkCredential($ftpUser, $ftpPassword)
        $request.UseBinary   = $true
        $request.UsePassive  = $true
        $request.EnableSsl   = $false
        $request.KeepAlive   = $false
        $request.Timeout     = 30000

        $response = $request.GetResponse()
        $stream   = $response.GetResponseStream()
        $reader   = New-Object System.IO.StreamReader($stream)
        $listing  = $reader.ReadToEnd()
        $reader.Close(); $response.Close()

        $lines = $listing -split "`r?`n" | Where-Object { $_ -ne "" }
        foreach ($line in $lines) { $items += $line.Trim() }
        return $items
    }
    catch {
        Write-Host "  Ошибка при получении списка файлов: $_" -ForegroundColor Red
        return @()
    }
}

function Get-FtpFileSize {
    param(
        [string]$remoteUri,
        [string]$ftpDirPath,
        [string]$fileName
    )
    try {
        $request = [System.Net.FtpWebRequest]::Create($remoteUri)
        $request.Method      = [System.Net.WebRequestMethods+Ftp]::GetFileSize
        $request.Credentials = New-Object System.Net.NetworkCredential($ftpUser, $ftpPassword)
        $request.UseBinary   = $true
        $request.UsePassive  = $true
        $request.EnableSsl   = $false
        $request.KeepAlive   = $false
        $request.Timeout     = 30000

        $response = $request.GetResponse()
        $size = $response.ContentLength
        $response.Close()
        if ($size -gt 0) { return [int64]$size }
    }
    catch {}

    try {
        $uri = "ftp://$ftpHost$ftpDirPath/"
        $request = [System.Net.FtpWebRequest]::Create($uri)
        $request.Method      = [System.Net.WebRequestMethods+Ftp]::ListDirectoryDetails
        $request.Credentials = New-Object System.Net.NetworkCredential($ftpUser, $ftpPassword)
        $request.UseBinary   = $true
        $request.UsePassive  = $true
        $request.EnableSsl   = $false
        $request.KeepAlive   = $false
        $request.Timeout     = 30000

        $response = $request.GetResponse()
        $stream   = $response.GetResponseStream()
        $reader   = New-Object System.IO.StreamReader($stream)
        $listing  = $reader.ReadToEnd()
        $reader.Close(); $response.Close()

        $lines = $listing -split "`r?`n" | Where-Object { $_ -ne "" }
        foreach ($line in $lines) {
            if ($line -match ("\s" + [regex]::Escape($fileName) + "\s*$")) {
                if ($line -match "^\d{2}-\d{2}-\d{2}\s+\d{2}:\d{2}[AP]M\s+(\d+)\s+") {
                    return [int64]$matches[1]
                }
                if ($line -match "^-.*\s+(\d+)\s+\w{3}\s+\d{1,2}\s+(\d{2}:\d{2}|\d{4})\s+") {
                    return [int64]$matches[1]
                }
            }
        }
    }
    catch {}
    return -1
}

function Get-VersionFile {
    param([string]$folderPath)
    if (!(Test-Path $folderPath)) { return $null }
    $files = Get-ChildItem -Path $folderPath -File -Force -ErrorAction SilentlyContinue
    foreach ($f in $files) {
        if ($f.BaseName -match $versionPattern) { return $f }
    }
    return $null
}

function Update-VersionFile {
    param(
        [string]$folderPath,
        [string]$latestFolder,
        [System.IO.FileInfo]$versionFile
    )
    try {
        if ($versionFile) {
            if ($versionFile.BaseName -eq $latestFolder) { return }
            $extension = $versionFile.Extension
            if ([string]::IsNullOrEmpty($extension)) { $extension = ".txt" }
            $newName = "$latestFolder$extension"
            Rename-Item -Path $versionFile.FullName -NewName $newName -Force
            Write-Host "  Файл версии переименован: $($versionFile.Name) -> $newName" -ForegroundColor Green
        }
        else {
            $newName = "$latestFolder.txt"
            $newPath = Join-Path $folderPath $newName
            New-Item -ItemType File -Path $newPath -Force | Out-Null
            Write-Host "  Создан файл версии: $newName" -ForegroundColor Green
        }
    }
    catch {
        Write-Host "  Ошибка при обновлении файла версии: $_" -ForegroundColor Red
    }
}

function Download-FtpFile {
    param(
        [string]$remoteUri,
        [string]$localPath,
        [string]$ftpDirPath = "",
        [string]$fileName   = ""
    )
    try {
        if (-not $fileName) { $fileName = Split-Path $localPath -Leaf }

        $knownSize = Get-FtpFileSize -remoteUri $remoteUri -ftpDirPath $ftpDirPath -fileName $fileName

        $resumeOffset = 0
        if (Test-Path $localPath) {
            $resumeOffset = (Get-Item $localPath).Length
            if ($knownSize -gt 0 -and $resumeOffset -ge $knownSize) {
                Write-Host "  Локальный файл уже полностью скачан ($([math]::Round($resumeOffset / 1MB, 2)) MB)." -ForegroundColor Green
                return $true
            }
            if ($resumeOffset -gt 0) {
                $mbHave = [math]::Round($resumeOffset / 1MB, 2)
                $mbAll  = if ($knownSize -gt 0) { [math]::Round($knownSize / 1MB, 2) } else { "?" }
                Write-Host "  Найден недокачанный файл: $mbHave / $mbAll MB. Пробуем докачать." -ForegroundColor Yellow
            }
        }

        $request = [System.Net.FtpWebRequest]::Create($remoteUri)
        $request.Method      = [System.Net.WebRequestMethods+Ftp]::DownloadFile
        $request.Credentials = New-Object System.Net.NetworkCredential($ftpUser, $ftpPassword)
        $request.UseBinary   = $true
        $request.UsePassive  = $true
        $request.EnableSsl   = $false
        $request.KeepAlive   = $false
        $request.Timeout     = 60000

        $resumeSupported = $false
        if ($resumeOffset -gt 0) {
            try {
                $request.ContentOffset = $resumeOffset
                $resumeSupported = $true
            } catch {
                Write-Host "  Сервер не поддерживает докачку: $($_.Exception.Message)" -ForegroundColor Yellow
                $resumeSupported = $false
            }
        }

        $response = $null
        try {
            $response = $request.GetResponse()
        } catch {
            if ($resumeSupported) {
                Write-Host "  Докачка не удалась, начинаем с нуля..." -ForegroundColor Yellow
                if (Test-Path $localPath) { Remove-Item $localPath -Force -ErrorAction SilentlyContinue }
                $resumeOffset = 0

                $request = [System.Net.FtpWebRequest]::Create($remoteUri)
                $request.Method      = [System.Net.WebRequestMethods+Ftp]::DownloadFile
                $request.Credentials = New-Object System.Net.NetworkCredential($ftpUser, $ftpPassword)
                $request.UseBinary   = $true
                $request.UsePassive  = $true
                $request.EnableSsl   = $false
                $request.KeepAlive   = $false
                $request.Timeout     = 60000
                $response = $request.GetResponse()
            } else { throw }
        }

        $remaining = $response.ContentLength
        if ($remaining -le 0) { $remaining = $knownSize - $resumeOffset }
        $totalBytes = if ($knownSize -gt 0) { $knownSize } else { $resumeOffset + $remaining }

        $ftpStream = $response.GetResponseStream()

        if ($resumeOffset -gt 0 -and $resumeSupported) {
            $localStream = [System.IO.File]::Open($localPath, [System.IO.FileMode]::Append, [System.IO.FileAccess]::Write, [System.IO.FileShare]::None)
        } else {
            $localStream = [System.IO.File]::Create($localPath)
            $resumeOffset = 0
        }

        $buffer      = New-Object Byte[] $BufferSize
        $bytesRead   = $ftpStream.Read($buffer, 0, $BufferSize)
        $downloaded  = $resumeOffset

        $lastRender = [DateTime]::MinValue
        $lastMb     = -1
        $width      = 30

        $speedSw = $null
        $speedStartBytes = 0
        if ($ShowSpeed) {
            $speedSw = [System.Diagnostics.Stopwatch]::StartNew()
            $speedStartBytes = $downloaded
        }

        while ($bytesRead -gt 0) {
            $localStream.Write($buffer, 0, $bytesRead)
            $downloaded += $bytesRead
            $bytesRead = $ftpStream.Read($buffer, 0, $BufferSize)

            $now = Get-Date
            if (($now - $lastRender).TotalMilliseconds -lt $ProgressEveryMs) { continue }
            $lastRender = $now

            if ($totalBytes -gt 0) {
                Show-ProgressLine -FileName $fileName -Downloaded $downloaded -Total $totalBytes -Width $width
            } else {
                $mbNow = [math]::Floor($downloaded / 1MB)
                if ($mbNow -eq $lastMb) { continue }
                $lastMb = $mbNow
                Show-ProgressLine -FileName $fileName -Downloaded $downloaded -Total 0 -Width $width
            }
        }

        $localStream.Close()
        $ftpStream.Close()
        $response.Close()

        if ($totalBytes -gt 0) {
            Show-ProgressLine -FileName $fileName -Downloaded $totalBytes -Total $totalBytes -Width $width
        }
        Write-Host ""

        if ($ShowSpeed -and $speedSw) {
            $speedSw.Stop()
            $sec = $speedSw.Elapsed.TotalSeconds
            if ($sec -gt 0) {
                $bytesTransferred = $downloaded - $speedStartBytes
                $speedMBs = [math]::Round(($bytesTransferred / 1MB) / $sec, 2)
                Write-Host ("  Скорость скачивания: {0} MB/s" -f $speedMBs) -ForegroundColor Yellow
            }
        }

        $fileSize = [math]::Round($downloaded / 1MB, 2)
        if ($resumeOffset -gt 0) {
            Write-Host ("  Файл докачан: {0} ({1} MB)" -f $localPath, $fileSize) -ForegroundColor Green
        } else {
            Write-Host ("  Файл скачан: {0} ({1} MB)" -f $localPath, $fileSize) -ForegroundColor Green
        }
        return $true
    }
    catch {
        Write-Host ""
        Write-Host "  Ошибка при скачивании: $_" -ForegroundColor Red
        return $false
    }
}

# ============================================================
#  Фаза 1: скачивание архивов с FTP
# ============================================================

function Invoke-DownloadPhase {
    param([hashtable]$mapping)

    $plan = @()

    foreach ($remoteFolder in $mapping.Keys) {
        $localFolder = $mapping[$remoteFolder]

        Write-Host ""
        Write-Host (">> [Скачивание] {0} -> {1}" -f $remoteFolder, $localFolder) -ForegroundColor Magenta

        $remoteBasePath = "$ftpPath/$remoteFolder"
        $networkFolder  = Join-Path $networkBasePath $localFolder

        if (!(Test-Path $localBasePath)) {
            New-Item -ItemType Directory -Path $localBasePath -Force | Out-Null
            Write-Host "  Создана временная папка: $localBasePath" -ForegroundColor Gray
        }

        if (!(Test-Path $networkFolder)) {
            New-Item -ItemType Directory -Path $networkFolder -Force | Out-Null
            Write-Host "  Создана сетевая папка: $networkFolder" -ForegroundColor Gray
        }

        Write-Host "  Получение списка папок из: ftp://$ftpHost$remoteBasePath/" -ForegroundColor Gray
        $foldersWithDates = Get-FtpFoldersWithDates -ftpPath $remoteBasePath

        if ($foldersWithDates.Count -eq 0) {
            Write-Host "  В папке $remoteFolder нет подпапок" -ForegroundColor Red
            return $null
        }

        Write-Host "  Найдено папок: $($foldersWithDates.Count)" -ForegroundColor Gray

        $latestFolder = $null
        $latestDate   = [DateTime]::MinValue
        foreach ($folder in $foldersWithDates.Keys) {
            $date = $foldersWithDates[$folder]
            if ($date -gt $latestDate) {
                $latestDate   = $date
                $latestFolder = $folder
            }
        }

        if (-not $latestFolder) {
            Write-Host "  Не удалось определить самую новую папку" -ForegroundColor Red
            return $null
        }

        Write-Host "  Самая новая папка: $latestFolder ($($latestDate.ToString('dd.MM.yyyy HH:mm')))" -ForegroundColor Green

        # === Проверка версии ===
        $versionFile = Get-VersionFile -folderPath $networkFolder
        if ($versionFile) {
            Write-Host "  Файл версии на сети: $($versionFile.Name)" -ForegroundColor Gray
            if ($versionFile.BaseName -eq $latestFolder) {
                Write-Host "  Версия актуальна ($($versionFile.BaseName) = $latestFolder). Пропускаем." -ForegroundColor Green
                continue
            } else {
                Write-Host "  Версия неактуальна ($($versionFile.BaseName) <> $latestFolder). Забираем архив." -ForegroundColor Yellow
            }
        } else {
            Write-Host "  Файл версии на сети не найден. Забираем архив." -ForegroundColor Yellow
        }

        $ekpPath = "$remoteBasePath/$latestFolder/EKP"
        Write-Host "  Проверка EKP: ftp://$ftpHost$ekpPath/" -ForegroundColor Gray

        $ekpFiles = Get-FtpFileList -ftpPath $ekpPath
        if ($ekpFiles.Count -eq 0) {
            Write-Host "  Папка EKP пуста или не существует" -ForegroundColor Red
            return $null
        }

        Write-Host "  Найдено файлов в EKP: $($ekpFiles.Count)" -ForegroundColor Gray

        $archives = @()
        foreach ($file in $ekpFiles) {
            if ($file -match "\.rar$") { $archives += $file }
        }

        if ($archives.Count -eq 0) {
            Write-Host "  В EKP не найдено RAR архивов" -ForegroundColor Red
            return $null
        }

        $archiveToDownload = $archives[0]
        Write-Host "  Выбран архив: $archiveToDownload" -ForegroundColor Gray

        $localFilePath = Join-Path $localBasePath $archiveToDownload

        # --- Проверяем локальный файл ---
        if (Test-Path $localFilePath) {
            $localInfo = Get-Item $localFilePath
            if ($localInfo.LastWriteTime -lt (Get-Date).AddDays(-1)) {
                Write-Host "  Удаляем устаревший локальный файл: $localFilePath" -ForegroundColor Gray
                Remove-Item $localFilePath -Force -ErrorAction SilentlyContinue
            }
        }

        $downloadUri = "ftp://$ftpHost$ekpPath/$archiveToDownload"
        Write-Host "  Скачивание: $archiveToDownload" -ForegroundColor Cyan

        if (-not (Download-FtpFile -remoteUri $downloadUri -localPath $localFilePath -ftpDirPath $ekpPath -fileName $archiveToDownload)) {
            Write-Host "  Не удалось скачать архив $archiveToDownload" -ForegroundColor Red
            return $null
        }

        $plan += [PSCustomObject]@{
            RemoteFolder    = $remoteFolder
            LocalFolder     = $localFolder
            LatestFolder    = $latestFolder
            NetworkFolder   = $networkFolder
            VersionFile     = $versionFile
            ArchiveName     = $archiveToDownload
            LocalFilePath   = $localFilePath
        }
    }

    return ,$plan
}

# ============================================================
#  MAIN
# ============================================================

Write-Host "========================================" -ForegroundColor Cyan
Write-Host "FTP-хост:       $ftpHost" -ForegroundColor Yellow
Write-Host "FTP-путь:       $ftpPath" -ForegroundColor Yellow
Write-Host "Временная:      $localBasePath" -ForegroundColor Yellow
Write-Host "Сетевая:        $networkBasePath" -ForegroundColor Yellow
Write-Host "Downloader:     $downloader" -ForegroundColor Yellow
Write-Host "========================================" -ForegroundColor Cyan
Write-Host ""

if (!(Test-Path $networkBasePath)) {
    Write-Host "ВНИМАНИЕ: сетевой путь недоступен: $networkBasePath" -ForegroundColor Red
    Write-Host ""
}

if (!(Test-Path $downloader)) {
    Write-Host "ОШИБКА: downloader.ps1 не найден: $downloader" -ForegroundColor Red
    Read-Host "ENTER для закрытия"
    exit 1
}

# --- ФАЗА 1: скачивание ---
Write-Host "Шаг 1 - скачивание с FTP" -ForegroundColor Cyan
$plan = Invoke-DownloadPhase -mapping $folderMapping

if ($null -eq $plan) {
    Write-Host ""
    Write-Host "Фаза скачивания завершилась с ошибкой." -ForegroundColor Red
    Read-Host "ENTER для закрытия"
    exit 1
}

if ($plan.Count -eq 0) {
    Write-Host ""
    Write-Host "Все версии актуальны — нечего скачивать и устанавливать." -ForegroundColor Green
    Write-Host ""
    Read-Host "ENTER для закрытия"
    exit 0
}

Write-Host ""
Write-Host "Скачано архивов: $($plan.Count). Передаём управление downloader.ps1..." -ForegroundColor Green
Write-Host ""

# --- ФАЗА 2: вызов downloader.ps1 ---
# downloader сам разберётся, какие папки в $localBasePath обрабатывать,
# установит MSI, заархивирует результат и положит в $networkBasePath.
$dlArgs = @(
    "-NoProfile",
    "-ExecutionPolicy", "Bypass",
    "-File", "`"$downloader`""
)

Write-Host "Запуск: powershell.exe $($dlArgs -join ' ')" -ForegroundColor Cyan
$dlProc = Start-Process powershell.exe -ArgumentList $dlArgs -Wait -PassThru -NoNewWindow

if ($dlProc.ExitCode -ne 0) {
    Write-Host ""
    Write-Host "downloader.ps1 завершился с кодом $($dlProc.ExitCode)." -ForegroundColor Red
    Read-Host "ENTER для закрытия"
    exit $dlProc.ExitCode
}

Write-Host ""
Write-Host "downloader.ps1 успешно завершён (код 0)." -ForegroundColor Green

# --- ФАЗА 3: очистка временной папки ---
Write-Host ""
Write-Host "Шаг 3 - очистка временной папки" -ForegroundColor Cyan
foreach ($item in $plan) {
    if (Test-Path $item.LocalFilePath) {
        try {
            Remove-Item -Path $item.LocalFilePath -Force -ErrorAction Stop
            Write-Host "  Удалён: $($item.LocalFilePath)" -ForegroundColor Gray
        } catch {
            Write-Host "  Не удалось удалить $($item.LocalFilePath): $($_.Exception.Message)" -ForegroundColor Yellow
        }
    }
}

Write-Host ""
Write-Host "Скрипт завершён." -ForegroundColor Cyan
#Start-Sleep -Seconds 10
exit 0


downloader.ps1
# ============================================================
#  downloader.ps1 — установка MSI, архивация папок, выкладка на Z:
# ============================================================
#  Вызывается из ftp.ps1 после скачивания архивов.
#  Для каждой папки из $localBasePath:
#   1. Распаковывает RAR.
#   2. Находит MSI и устанавливает.
#   3. Ждёт/убивает CommonComponents.Catalog.IndexationUtility.
#   4. Запускает утилиту индексации.
#   5. Архивирует установленную папку программы в RAR.
#   6. Кладёт RAR в $networkBasePath\<LocalFolder>\.
#   7. Обновляет файл-маркер версии.
# ============================================================

# --- Проверка прав администратора ---
$currentPrincipal = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent())
$isAdmin = $currentPrincipal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)

if (-not $isAdmin) {
    Write-Host "ОШИБКА: скрипт запущен без прав администратора!" -ForegroundColor Red
    Start-Sleep -Seconds 5
    exit 1
}

Write-Host "=== УСТАНОВКА / ПЕРЕУСТАНОВКА ПО + АРХИВАЦИЯ ===" -ForegroundColor Cyan
Write-Host "Запущено от имени администратора" -ForegroundColor Green
Write-Host ""

# --- Пути ---
$localBasePath   = "C:\Users\n4200-01-031\Desktop\App"    # сюда ftp.ps1 скачал RAR
$networkBasePath = "Z:\Documents"                          # сюда кладём готовые архивы

$baseDir = if ($PSScriptRoot) { $PSScriptRoot } else { Split-Path -Parent $PSCommandPath }
if (-not $baseDir) { $baseDir = Get-Location }
$workBase = $baseDir

# --- WinRAR ---
$winRarPath = "C:\Program Files\WinRAR\WinRAR.exe"
if (!(Test-Path $winRarPath)) { $winRarPath = "C:\Program Files (x86)\WinRAR\WinRAR.exe" }
if (!(Test-Path $winRarPath)) {
    Write-Host "ОШИБКА: WinRAR не найден!" -ForegroundColor Red
    Start-Sleep -Seconds 10
    exit 1
}

Write-Host "Источник RAR: $localBasePath" -ForegroundColor Cyan
Write-Host "Рабочая папка: $workBase" -ForegroundColor Cyan
Write-Host "Сеть:          $networkBasePath" -ForegroundColor Cyan
Write-Host "WinRAR:        $winRarPath" -ForegroundColor Cyan
Write-Host ""

# --- Список программ ---
$programs = @(
    @{ LocalFolder = "KPE";   Name = "Клиент АИС Налог-3 Пром версия";     InstallFolder = "Ais3Prom" }
    @{ LocalFolder = "PPK";   Name = "Клиент АИС Налог-3 ПредПром версия"; InstallFolder = "Ais3PredProm" }
    @{ LocalFolder = "KOE";   Name = "Клиент АИС Налог-3 ОЭ версия";       InstallFolder = "Ais3FB3OE" }
    @{ LocalFolder = "KOEIZ"; Name = "Клиент АИС Налог-3 ОЭ ИЗ версия";    InstallFolder = "Ais3OEIZ" }
)

# ============================================================
#  Вспомогательные функции
# ============================================================

function Find-InstalledProgram {
    param([string]$exactName)
    Write-Host "Поиск программы: $exactName" -ForegroundColor Cyan
    $foundPackage = Get-Package -Name $exactName -ErrorAction SilentlyContinue | Select-Object -First 1
    if (-not $foundPackage) {
        $foundPackage = Get-Package -Name "*$exactName*" -ErrorAction SilentlyContinue | Select-Object -First 1
    }
    if ($foundPackage) {
        Write-Host "Найдена: $($foundPackage.Name) (v$($foundPackage.Version))" -ForegroundColor Green
    } else {
        Write-Host "Не найдена" -ForegroundColor Gray
    }
    return $foundPackage
}

function Uninstall-Program {
    param([string]$displayName, $package)
    Write-Host "Удаление: $displayName" -ForegroundColor Yellow
    try {
        Uninstall-Package -InputObject $package -Force -ErrorAction Stop
        Write-Host "Удаление успешно" -ForegroundColor Green
        Start-Sleep -Seconds 3
    }
    catch {
        Write-Host "Ошибка удаления: $($_.Exception.Message)" -ForegroundColor Red
    }
}

function Clear-WorkFolder {
    param([string]$FolderPath)
    if (-not (Test-Path $FolderPath)) { return }
    Write-Host "Очистка рабочей папки (кроме *.ps1): $FolderPath" -ForegroundColor Cyan
    Get-ChildItem -Path $FolderPath -Force -ErrorAction SilentlyContinue |
        Where-Object { $_.Extension -ne ".ps1" } |
        ForEach-Object {
            Write-Host "   Удаление: $($_.FullName)" -ForegroundColor Gray
            Remove-Item -Path $_.FullName -Recurse -Force -ErrorAction SilentlyContinue
        }
}

function Extract-RarArchive {
    param([string]$archivePath, [string]$destinationPath)
    Write-Host "Распаковка: $archivePath" -ForegroundColor Cyan
    Write-Host "     в:    $destinationPath" -ForegroundColor Gray
    try {
        $arguments = "x -o+ -y `"$archivePath`" `"$destinationPath\`""
        $process = Start-Process -FilePath $winRarPath -ArgumentList $arguments -Wait -PassThru -NoNewWindow
        if ($process.ExitCode -eq 0) {
            Write-Host "Распаковка успешна!" -ForegroundColor Green
            return $true
        } else {
            Write-Host "Ошибка распаковки! Код: $($process.ExitCode)" -ForegroundColor Red
            return $false
        }
    }
    catch {
        Write-Host "Ошибка распаковки: $($_.Exception.Message)" -ForegroundColor Red
        return $false
    }
}

function Compress-ProgramFolder {
    param(
        [string]$sourceFolder,   # полный путь к папке программы
        [string]$rarPath,        # полный путь к выходному .rar
        [string]$baseFolderName  # имя папки для сохранения внутри архива
    )

    Write-Host "Архивация: $sourceFolder -> $rarPath" -ForegroundColor Cyan

    try {
        # WinRAR: a -r -ep1 <rar> <папка>
        # -ep1 — исключить базовый путь, сохранить только имя папки
        $parent = Split-Path -Parent $sourceFolder
        $leaf   = Split-Path -Leaf   $sourceFolder

        $arguments = "a -r -ep1 -o+ -y -ibck `"$rarPath`" `"$sourceFolder\`""
        $process = Start-Process -FilePath $winRarPath -ArgumentList $arguments -Wait -PassThru -NoNewWindow

        # Код 0 — успех, код 1 — предупреждения (всё равно считаем успехом)
        if ($process.ExitCode -eq 0 -or $process.ExitCode -eq 1) {
            $sizeMb = [math]::Round((Get-Item $rarPath).Length / 1MB, 2)
            Write-Host "Архив создан: $rarPath ($sizeMb MB)" -ForegroundColor Green
            return $true
        } else {
            Write-Host "Ошибка архивации! Код: $($process.ExitCode)" -ForegroundColor Red
            return $false
        }
    }
    catch {
        Write-Host "Ошибка архивации: $($_.Exception.Message)" -ForegroundColor Red
        return $false
    }
}

function Install-Program {
    param(
        [string]$msiPath,
        [string]$programName,
        [string]$installFolder
    )

    $processToKill = "CommonComponents.Catalog.IndexationUtility"
    $exeToRun      = "CommonComponents.Catalog.IndexationUtility.exe"

    Write-Host "Установка MSI: $msiPath" -ForegroundColor Cyan

    $installArguments = "/i `"$msiPath`" /quiet"
    $installProcess = Start-Process -FilePath "msiexec.exe" `
        -ArgumentList $installArguments -PassThru -WindowStyle Hidden

    Write-Host "Установка запущена (PID: $($installProcess.Id))" -ForegroundColor Green

    # --- Ожидание процесса индексации ---
    $targetProcess = $null
    $timeout = 1000
    $elapsed = 0
    Write-Host "Ожидание $processToKill..." -ForegroundColor Cyan
    while ($elapsed -lt $timeout) {
        $targetProcess = Get-Process -Name $processToKill -ErrorAction SilentlyContinue
        if ($targetProcess) {
            Write-Host "Процесс обнаружен! PID: $($targetProcess.Id)" -ForegroundColor Green
            Write-Host "Завершение..." -ForegroundColor Yellow
            Stop-Process -Id $targetProcess.Id -Force
            Write-Host "Процесс завершён." -ForegroundColor Green
            break
        }
        Start-Sleep -Seconds 2
        $elapsed += 2
    }
    if (-not $targetProcess) {
        Write-Host "Внимание: процесс $processToKill не обнаружен" -ForegroundColor Red
    }

    Write-Host "Ожидание завершения установки..." -ForegroundColor Cyan
    Wait-Process -Id $installProcess.Id -ErrorAction SilentlyContinue
    Write-Host "Установка MSI завершена." -ForegroundColor Green
    Start-Sleep -Seconds 3

    # --- Поиск папки программы ---
    $programsPath = "C:\Program Files (x86)"
    $programFolder = $null
    if (Test-Path $programsPath) {
        $programFolder = Get-ChildItem $programsPath -Directory -ErrorAction SilentlyContinue |
            Where-Object { $_.Name -eq $installFolder } | Select-Object -First 1
        if (-not $programFolder) {
            $programFolder = Get-ChildItem $programsPath -Directory -ErrorAction SilentlyContinue |
                Where-Object { $_.Name -like "*$installFolder*" } | Select-Object -First 1
        }
    }

    if (-not $programFolder) {
        Write-Host "Внимание: папка '$installFolder' не найдена" -ForegroundColor Yellow
        return $null
    }
    Write-Host "Папка программы: $($programFolder.FullName)" -ForegroundColor Green

    # --- Запуск утилиты индексации ---
    $exePath     = Join-Path $programFolder.FullName "Client" | Join-Path -ChildPath $exeToRun
    $exePathRoot = Join-Path $programFolder.FullName $exeToRun
    $finalExe = $null
    if (Test-Path $exePath) { $finalExe = $exePath }
    elseif (Test-Path $exePathRoot) { $finalExe = $exePathRoot }

    if (-not $finalExe) {
        Write-Host "Внимание: утилита $exeToRun не найдена" -ForegroundColor Red
        return $programFolder.FullName
    }

    Write-Host "Запуск утилиты: $finalExe" -ForegroundColor Cyan
    try {
        $utilityProcess = Start-Process -FilePath $finalExe -Wait -PassThru -WindowStyle Hidden -ErrorAction Stop
        Write-Host "Утилита завершила работу (ExitCode: $($utilityProcess.ExitCode))" -ForegroundColor Green
    }
    catch {
        Write-Host "ОШИБКА запуска утилиты: $($_.Exception.Message)" -ForegroundColor Red
    }

    return $programFolder.FullName
}

# ============================================================
#  Обработка одной программы
# ============================================================

function Process-Program {
    param(
        [string]$localFolder,
        [string]$programName,
        [string]$installFolder
    )

    Write-Host ""
    Write-Host "=====================================================" -ForegroundColor Magenta
    Write-Host ">>> $programName (папка $localFolder)" -ForegroundColor Magenta
    Write-Host "=====================================================" -ForegroundColor Magenta

    # 1. Ищем RAR в локальной папке (ftp.ps1 уже скачал)
    $sourceFolder = Join-Path $localBasePath $localFolder
    if (-not (Test-Path $sourceFolder)) {
        Write-Host "Папка-источник не найдена: $sourceFolder" -ForegroundColor Red
        return $false
    }

    $rarFiles = Get-ChildItem -Path $sourceFolder -Filter "*.rar" -File -Recurse -ErrorAction SilentlyContinue
    if ($rarFiles.Count -eq 0) {
        Write-Host "В $localFolder не найдено .rar" -ForegroundColor Red
        return $false
    }
    $rarToUse = $rarFiles | Select-Object -First 1
    Write-Host "Найден архив: $($rarToUse.Name)" -ForegroundColor Green

    # 2. Чистим рабочую папку от предыдущих распаковок
    Clear-WorkFolder -FolderPath $workBase

    # 3. Копируем RAR локально
    $localRarPath = Join-Path $workBase $rarToUse.Name
    Write-Host "Копирование: $($rarToUse.FullName) -> $localRarPath" -ForegroundColor Cyan
    try {
        Copy-Item -Path $rarToUse.FullName -Destination $localRarPath -Force -ErrorAction Stop
        $sizeMb = [math]::Round((Get-Item $localRarPath).Length / 1MB, 2)
        Write-Host "Скопировано ($sizeMb MB)" -ForegroundColor Green
    }
    catch {
        Write-Host "ОШИБКА копирования: $($_.Exception.Message)" -ForegroundColor Red
        return $false
    }

    # 4. Распаковка
    if (-not (Extract-RarArchive -archivePath $localRarPath -destinationPath $workBase)) {
        Write-Host "Распаковка не удалась" -ForegroundColor Red
        return $false
    }

    # 5. Поиск MSI
    $msiFile = Get-ChildItem -Path $workBase -Filter "*.msi" -File -Recurse -ErrorAction SilentlyContinue |
        Select-Object -First 1
    if (-not $msiFile) {
        Write-Host "MSI не найден" -ForegroundColor Red
        return $false
    }
    Write-Host "Найден установщик: $($msiFile.Name)" -ForegroundColor Green

    # 6. Удаление старой версии
    $existingPackage = Find-InstalledProgram -exactName $programName
    if ($existingPackage) {
        Uninstall-Program -displayName $existingPackage.Name -package $existingPackage
    } else {
        Write-Host "Программа не найдена, будет установлена" -ForegroundColor Green
    }

    # 7. Установка MSI + утилита индексации
    $installedFolder = Install-Program -msiPath $msiFile.FullName -programName $programName -installFolder $installFolder

    if (-not $installedFolder -or -not (Test-Path $installedFolder)) {
        Write-Host "Не удалось определить папку установки — пропускаем архивацию" -ForegroundColor Red
        Clear-WorkFolder -FolderPath $workBase
        return $false
    }

    # 8. Архивация установленной папки
    $targetNetworkFolder = Join-Path $networkBasePath $localFolder
    if (!(Test-Path $targetNetworkFolder)) {
        New-Item -ItemType Directory -Path $targetNetworkFolder -Force | Out-Null
        Write-Host "Создана сетевая папка: $targetNetworkFolder" -ForegroundColor Gray
    }

    # Определяем имя итогового RAR.
    # Используем имя исходного RAR (или можно взять имя папки версии).
    $finalRarName = "Installed_$localFolder.rar"
    $finalRarPath = Join-Path $targetNetworkFolder $finalRarName

    if (-not (Compress-ProgramFolder -sourceFolder $installedFolder -rarPath $finalRarPath -baseFolderName (Split-Path -Leaf $installedFolder))) {
        Clear-WorkFolder -FolderPath $workBase
        return $false
    }

    # 9. Чистим рабочую папку
    Clear-WorkFolder -FolderPath $workBase

    # 10. Чистим локальный RAR из sourceFolder (ftp.ps1 уже отработал)
    try {
        Remove-Item -Path $rarToUse.FullName -Force -ErrorAction Stop
        Write-Host "Локальный RAR удалён: $($rarToUse.FullName)" -ForegroundColor Gray
    } catch {
        Write-Host "Не удалось удалить локальный RAR: $($_.Exception.Message)" -ForegroundColor Yellow
    }

    Write-Host "Программа '$programName' обработана." -ForegroundColor Green
    Write-Host "---------------------------------------------------" -ForegroundColor Gray
    return $true
}

# ============================================================
#  MAIN
# ============================================================

Write-Host ""
Write-Host "=====================================================" -ForegroundColor Cyan
Write-Host "НАЧАЛО ОБРАБОТКИ" -ForegroundColor Cyan
Write-Host "=====================================================" -ForegroundColor Cyan

$successCount = 0
$totalCount   = $programs.Count

foreach ($program in $programs) {
    $ok = Process-Program `
        -localFolder   $program.LocalFolder `
        -programName   $program.Name `
        -installFolder $program.InstallFolder
    if ($ok) { $successCount++ }
}

Write-Host ""
Write-Host "=====================================================" -ForegroundColor Green
Write-Host "ЗАВЕРШЕНО: $successCount из $totalCount" -ForegroundColor Green
Write-Host "=====================================================" -ForegroundColor Green

if ($successCount -eq $totalCount) {
    exit 0
} else {
    exit 1
}


watcher.ps1
# ============================================================
#  watcher.ps1 — наблюдение за Z:\Documents и распаковка RAR
# ============================================================
#  Логика:
#   - Следим за 4 папками: KPE, PPK, KOE, KOEIZ в $watchRoot.
#   - При появлении нового/изменённого .rar:
#       * ждём стабилизации размера;
#       * распаковываем RAR в папку рядом со скриптом ($PSScriptRoot);
#       * помечаем файл как обработанный (.rar.done).
#   - Архиватор: WinRAR (приоритет) -> 7-Zip -> ошибка.
# ============================================================

# --- Права администратора ---
if (-NOT ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")) {
    Write-Host "Требуются права администратора. Перезапуск..." -ForegroundColor Yellow
    Start-Process PowerShell -Verb RunAs -ArgumentList "-NoProfile -ExecutionPolicy Bypass -File `"$PSCommandPath`""
    exit
}

Write-Host "=== WATCHER: наблюдение за Z:\Documents ===" -ForegroundColor Cyan
Write-Host "Запущено от имени администратора" -ForegroundColor Green
Write-Host ""

# --- Пути ---
$watchRoot  = "Z:\Documents"
$targetDirs = @("KPE", "PPK", "KOE", "KOEIZ")

# --- Куда распаковывать ---
$scriptDir = if ($PSScriptRoot) { $PSScriptRoot } else { Split-Path -Parent $PSCommandPath }
if (-not $scriptDir) { $scriptDir = Get-Location }
$extractBase = $scriptDir

# --- Lock-файл ---
$lockFile = Join-Path $extractBase "watcher.processing.lock"

# ============================================================
#  Поиск архиватора: WinRAR -> 7-Zip -> ошибка
# ============================================================

function Find-Archiver {
    $result = @{
        Type    = $null      # "WinRAR" | "7Zip"
        Path    = $null
        ExeName = $null
    }

    # --- 1. WinRAR ---
    $winRarCandidates = @(
        "C:\Program Files\WinRAR\WinRAR.exe"
        "C:\Program Files (x86)\WinRAR\WinRAR.exe"
    )
    # также попробуем найти в PATH
    $winRarInPath = Get-Command "WinRAR.exe" -ErrorAction SilentlyContinue
    if ($winRarInPath) { $winRarCandidates += $winRarInPath.Source }

    foreach ($p in $winRarCandidates) {
        if ($p -and (Test-Path $p)) {
            $result.Type    = "WinRAR"
            $result.Path    = $p
            $result.ExeName = "WinRAR.exe"
            return $result
        }
    }

    # --- 2. 7-Zip ---
    $sevenZipCandidates = @(
        "C:\Program Files\7-Zip\7z.exe"
        "C:\Program Files (x86)\7-Zip\7z.exe"
        "C:\Program Files\7-Zip\7za.exe"
        "C:\Program Files (x86)\7-Zip\7za.exe"
    )
    # также попробуем найти в PATH
    foreach ($name in @("7z.exe", "7za.exe")) {
        $inPath = Get-Command $name -ErrorAction SilentlyContinue
        if ($inPath) { $sevenZipCandidates += $inPath.Source }
    }

    foreach ($p in $sevenZipCandidates) {
        if ($p -and (Test-Path $p)) {
            $result.Type    = "7Zip"
            $result.Path    = $p
            $result.ExeName = Split-Path -Leaf $p
            return $result
        }
    }

    return $result   # Type = $null -> ничего не нашли
}

$archiver = Find-Archiver

if (-not $archiver.Type) {
    Write-Host "=====================================================" -ForegroundColor Red
    Write-Host "ОШИБКА: не найден ни WinRAR, ни 7-Zip!" -ForegroundColor Red
    Write-Host "=====================================================" -ForegroundColor Red
    Write-Host "Проверьте, что установлен один из архиваторов:" -ForegroundColor Yellow
    Write-Host "  - WinRAR:  C:\Program Files\WinRAR\WinRAR.exe" -ForegroundColor Gray
    Write-Host "             C:\Program Files (x86)\WinRAR\WinRAR.exe" -ForegroundColor Gray
    Write-Host "  - 7-Zip:   C:\Program Files\7-Zip\7z.exe" -ForegroundColor Gray
    Write-Host "             C:\Program Files (x86)\7-Zip\7z.exe" -ForegroundColor Gray
    Write-Host "  - или добавьте WinRAR.exe / 7z.exe в PATH" -ForegroundColor Gray
    Write-Host ""
    Read-Host "ENTER для выхода"
    exit 1
}

Write-Host "Наблюдаем за:     $watchRoot" -ForegroundColor Cyan
Write-Host "Подпапки:         $($targetDirs -join ', ')" -ForegroundColor Cyan
Write-Host "Распаковка в:     $extractBase" -ForegroundColor Cyan
Write-Host "Архиватор:        $($archiver.Type) -> $($archiver.Path)" -ForegroundColor Cyan
Write-Host ""

if (-not (Test-Path $watchRoot)) {
    Write-Host "ОШИБКА: путь не существует: $watchRoot" -ForegroundColor Red
    Read-Host "ENTER для выхода"
    exit 1
}

# ============================================================
#  Распаковка через выбранный архиватор
# ============================================================

function Invoke-ExtractArchive {
    param(
        [string]$ArchivePath,
        [string]$TargetDir,
        [hashtable]$Archiver
    )

    if ($Archiver.Type -eq "WinRAR") {
        # WinRAR: x -o+ -y "<archive>" "<dest>\"
        $args = "x -o+ -y `"$ArchivePath`" `"$TargetDir\`""
    }
    elseif ($Archiver.Type -eq "7Zip") {
        # 7-Zip: x -y -o"<dest>" "<archive>"
        $args = "x -y -o`"$TargetDir`" `"$ArchivePath`""
    }
    else {
        return @{ Success = $false; Message = "Неизвестный архиватор" }
    }

    try {
        $process = Start-Process -FilePath $Archiver.Path `
            -ArgumentList $args `
            -Wait -PassThru -NoNewWindow

        # WinRAR: 0 — успех, 1 — предупреждения (считаем успехом)
        # 7-Zip:  0 — успех, 1 — предупреждения (считаем успехом)
        if ($process.ExitCode -eq 0 -or $process.ExitCode -eq 1) {
            return @{ Success = $true; ExitCode = $process.ExitCode }
        } else {
            return @{
                Success  = $false
                ExitCode = $process.ExitCode
                Message  = "Код возврата: $($process.ExitCode)"
            }
        }
    }
    catch {
        return @{ Success = $false; Message = $_.Exception.Message }
    }
}

# ============================================================
#  Ожидание стабилизации файла
# ============================================================

function Wait-FileStable {
    param(
        [string]$Path,
        [int]$StableSeconds  = 3,
        [int]$TimeoutSeconds = 300
    )
    $elapsed     = 0
    $lastSize    = -1
    $stableStart = $null

    while ($elapsed -lt $TimeoutSeconds) {
        if (-not (Test-Path $Path)) { return $false }
        try {
            $size = (Get-Item $Path).Length
        } catch {
            Start-Sleep -Milliseconds 500
            $elapsed += 0.5
            continue
        }

        if ($size -ne $lastSize) {
            $lastSize    = $size
            $stableStart = Get-Date
        } else {
            if ($stableStart -and ((Get-Date) - $stableStart).TotalSeconds -ge $StableSeconds) {
                return $true
            }
        }

        Start-Sleep -Milliseconds 500
        $elapsed += 0.5
    }
    return $false
}

# ============================================================
#  Обработка одного RAR
# ============================================================

function Invoke-ExtractRar {
    param(
        [string]$RarPath,
        [string]$TargetDir
    )

    Write-Host ""
    Write-Host "========================================" -ForegroundColor Cyan
    Write-Host "ОБНАРУЖЕН АРХИВ" -ForegroundColor Green
    Write-Host "Файл: $RarPath" -ForegroundColor Yellow
    Write-Host "Время: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" -ForegroundColor Yellow
    Write-Host "========================================" -ForegroundColor Cyan

    # --- Lock ---
    if (Test-Path $lockFile) {
        Write-Host "Обработка уже идёт (lock). Пропускаем." -ForegroundColor Yellow
        return
    }
    try {
        New-Item -ItemType File -Path $lockFile -Force -ErrorAction Stop | Out-Null
    } catch {
        Write-Host "ОШИБКА создания lock: $($_.Exception.Message)" -ForegroundColor Red
        return
    }

    try {
        # --- Ждём стабилизации размера ---
        Write-Host "Ожидание завершения записи файла..." -ForegroundColor Gray
        if (-not (Wait-FileStable -Path $RarPath -StableSeconds 3 -TimeoutSeconds 300)) {
            Write-Host "Файл не стабилизировался — пропускаем" -ForegroundColor Red
            return
        }

        if (-not (Test-Path $RarPath)) {
            Write-Host "Файл исчез до распаковки" -ForegroundColor Red
            return
        }

        $sizeMb = [math]::Round((Get-Item $RarPath).Length / 1MB, 2)
        Write-Host "Размер: $sizeMb MB" -ForegroundColor Gray

        if (!(Test-Path $TargetDir)) {
            New-Item -ItemType Directory -Path $TargetDir -Force | Out-Null
            Write-Host "Создана папка: $TargetDir" -ForegroundColor Gray
        }

        Write-Host "Распаковка в: $TargetDir (через $($archiver.Type))" -ForegroundColor Cyan

        $res = Invoke-ExtractArchive -ArchivePath $RarPath -TargetDir $TargetDir -Archiver $archiver

        if ($res.Success) {
            Write-Host "Распаковка успешна!" -ForegroundColor Green

            # --- Помечаем обработанный RAR ---
            try {
                $doneName = $RarPath + ".done"
                if (Test-Path $doneName) { Remove-Item $doneName -Force -ErrorAction SilentlyContinue }
                Move-Item -Path $RarPath -Destination $doneName -Force -ErrorAction Stop
                Write-Host "Файл помечен как обработанный: $doneName" -ForegroundColor Gray
            } catch {
                Write-Host "Не удалось переименовать: $($_.Exception.Message)" -ForegroundColor Yellow
            }
        } else {
            Write-Host "Ошибка распаковки: $($res.Message)" -ForegroundColor Red
        }
    }
    finally {
        Remove-Item $lockFile -Force -ErrorAction SilentlyContinue
        Write-Host "Lock снят" -ForegroundColor Gray
        Write-Host "========================================" -ForegroundColor Cyan
        Write-Host ""
    }
}

# ============================================================
#  Настройка FileSystemWatcher для каждой подпапки
# ============================================================

$watchers = @()
$jobs     = @()

foreach ($sub in $targetDirs) {
    $fullPath = Join-Path $watchRoot $sub

    if (-not (Test-Path $fullPath)) {
        try {
            New-Item -ItemType Directory -Path $fullPath -Force | Out-Null
            Write-Host "Создана папка: $fullPath" -ForegroundColor Gray
        } catch {
            Write-Host "Не удалось создать $fullPath : $($_.Exception.Message)" -ForegroundColor Red
            continue
        }
    }

    $w = New-Object System.IO.FileSystemWatcher
    $w.Path = $fullPath
    $w.Filter = "*.rar"
    $w.NotifyFilter = [System.IO.NotifyFilters]::FileName -bor [System.IO.NotifyFilters]::LastWrite -bor [System.IO.NotifyFilters]::Size
    $w.EnableRaisingEvents = $true
    $w.IncludeSubdirectories = $false

    $action = {
        $path = $Event.SourceEventArgs.FullPath
        Invoke-ExtractRar -RarPath $path -TargetDir $extractBase
    }

    $job = Register-ObjectEvent -InputObject $w -EventName "Created" -Action $action
    $jobs     += $job
    $watchers += $w

    Write-Host "Наблюдаем: $fullPath" -ForegroundColor Gray
}

# --- Стартовый проход по уже существующим .rar ---
Write-Host ""
Write-Host "Стартовый проход по существующим .rar..." -ForegroundColor Cyan
foreach ($sub in $targetDirs) {
    $fullPath = Join-Path $watchRoot $sub
    if (Test-Path $fullPath) {
        $existing = Get-ChildItem -Path $fullPath -Filter "*.rar" -File -ErrorAction SilentlyContinue
        foreach ($f in $existing) {
            Invoke-ExtractRar -RarPath $f.FullName -TargetDir $extractBase
        }
    }
}

Write-Host ""
Write-Host "========================================" -ForegroundColor Cyan
Write-Host "Наблюдатель запущен!" -ForegroundColor Green
Write-Host "Архиватор: $($archiver.Type) ($($archiver.Path))" -ForegroundColor Yellow
Write-Host "Для остановки нажмите Ctrl+C" -ForegroundColor Red
Write-Host "========================================" -ForegroundColor Cyan
Write-Host ""

# --- Держим скрипт запущенным ---
try {
    while ($true) { Start-Sleep -Seconds 1 }
}
finally {
    Write-Host ""
    Write-Host "Остановка watcher..." -ForegroundColor Yellow
    foreach ($job in $jobs) {
        Unregister-Event -SubscriptionId $job.Id -ErrorAction SilentlyContinue
    }
    foreach ($w in $watchers) {
        $w.EnableRaisingEvents = $false
        $w.Dispose()
    }
    Remove-Item $lockFile -Force -ErrorAction SilentlyContinue
    Write-Host "Остановлен." -ForegroundColor Red
}