/
AleM
/
Backup
Обзор
Документация
Войти
/
AleM
/
Backup
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
BACKUP.ps1
1 472 строки
69 KB
AleM
update BACKUP.ps1
27 окт 2025, 01:45
27 окт 2025, 01:45
5bd94fa
Код
Авторство
О чём код?
# ========================================= # Backup :PowerShell # Скрипт резервного копирования и архивации Delphi проекта. # ========================================= # ОПИСАНИЕ: # Выполняет полное резервное копирование проекта с возможностью # очистки временных файлов и архивации. Поддерживает проекты # Delphi для Windows (.exe) и Android (.apk). # # ВОЗМОЖНОСТИ: # 1. Dry Run (тестовый прогон без изменений на диске) # 2. Очистка временных файлов и папок. # 3. Удаление блоков <EnabledSysJars> из .dproj файлов. # 4. Создание бэкапа исполняемых файлов (.exe/.apk) # 5. Архивация с поддержкой 7-Zip и WinRAR. # 6. Обновление существующих архивов: # • Архив с текущей датой -> обновление без очистки Backup # • Архив с другой датой -> очистка Backup и создание нового # • Архивы не найдены -> создание нового # # РЕЗУЛЬТАТ: # - Бэкапы в папке Backup/<Название_Проекта>/ # - Архивы в Archive/: # • Найден архив с текущей датой -> обновляется без очистки Backup # • Найден архив с другой датой -> очищается Backup, создаётся новый # • Архивы не найдены -> создаётся новый # # ТРЕБОВАНИЯ: # - PowerShell 5.1 или PowerShell 7+ # - 7-Zip и/или WinRAR для архивации. # ========================================= <# HISTORY ------- 07\25 v1.0 + Добавлено: - Поддержка 7-Zip и WinRAR при архивации. - Удаление блоков <EnabledSysJars> из .dproj. - Очистка временных файлов/папок и исключение по маскам. - Бэкап .exe|.apk в Backup\<Название_Проекта>\ (Windows/Android). - Архивы проекта в Archive\<Название_Проекта>_<дата>.[7z|rar]. - Логирование ошибок в error.log. 08\25 v2.0 * Изменено: - Исправлено формирование аргументов исключений для 7-Zip/RAR. + Добавлено: - Единая установка UTF-8 для PS 5.1/7+. - Выбор версии PowerShell (5.1 / 7+) с автоперезапуском. - Режим DryRun (тестовый прогон без изменений на диске). - Test-DiskSpace: считает фактический размер проекта (до сжатия) и сравнивает с требуемым объёмом. - Инкрементальные архивы: обновление единого архива <Название_Проекта>.[7z|rar] (u), с автосозданием (a) при отсутствии. - Инкрементальный бэкап: копирование только изменённых файлов при сохранении в Backup. 10\25 v2.1 * Изменено: - Исключение всех файлов (архивы бинарников) *.7z и *.rar из папки Backup при создании архива проекта. - Функция Remove-TempContent теперь принимает списки папок и масок как параметры. - Удалено поле UseIncrementalArchive (поведение теперь управляется датами архивов) + Добавлено: - Оптимизирована логика работы с архивами по датам: • Архивы не найдены -> создаётся новый архив с текущей датой. • Найден архив с текущей датой -> обновляется без очистки Backup. • Найден архив с другой датой -> очищается Backup, создаётся новый архив. #> # ----------------------------------------- # Настройка окружения (UTF-8 для PS 5.1/7+) # ----------------------------------------- try { $enc = [System.Text.Encoding]::UTF8 [Console]::OutputEncoding = $enc [System.Console]::OutputEncoding = $enc $OutputEncoding = $enc $PSDefaultParameterValues['*:Encoding'] = 'utf8' } catch { Write-Warn "Не удалось установить UTF-8: $($_.Exception.Message)" Write-ErrorLog "Encoding: $($_.Exception.Message)" } # Пути к архиваторам $zipPath = "C:\Program Files\7-Zip\7z.exe" $rarPath = "C:\Program Files\WinRAR\RAR.exe" # ----------------------------------------- # Списки исключений для очистки, копирования и архивации # ----------------------------------------- # Временные папки Delphi и системные директории $tempFolders = @("__history", "__recovery", "tmp", "Win32", "Win64", ".BIN") # Временные файлы компиляции и отладки $tempFileMasks = @("*.tmp", "*.dcu") # Исключения по точным именам (файлы/папки) $NameExcludeList = @( "Thumbs.db", # Кэш эскизов Windows "BkUp.set", # Настройки бэкапа ".git", # Git репозиторий ".svn", # SVN репозиторий ".vscode" # Настройки VS Code ) # Исключения по маскам (расширения и шаблоны) $MaskExcludeList = @( "*.idsig", # Подписи Android "*.dcu", # Delphi Compiled Units "*.tmp", # Временные файлы "*.log" # Лог-файлы ) # ----------------------------------------- # Конфигурация скрипта # ----------------------------------------- $Config = @{ # Сохранение выбора версии PowerShell # Если $true, выбор сохраняется в BkUp.set и используется при следующих запусках RememberPowerShellChoice = $true # Минимальное свободное место на диске (в мегабайтах) # Абсолютный минимум, независимо от размера проекта MinimumFreeSpaceMB = 10 # Режим инкрементального копирования # Если $true: # - Сравнивает размер и хеш существующих файлов # - Копирует только изменённые или новые файлы # - Значительно ускоряет повторные бэкапы UseIncrementalBackup = $true # Режим тестового запуска (без изменений на диске) # Если $true: # - Показывает все планируемые действия # - Не выполняет реальных изменений # - Полезно для проверки конфигурации DryRun = $false # Алгоритм сравнения файлов при инкрементальном бэкапе # Чем сравниваем файлы: MD5|SHA1|SHA256|SHA384|SHA512|RIPEMD160 HashAlgorithm = 'MD5' PathCompactMaxChars = 80 # Целевая длина укороченного пути UseUnicodeEllipsis = $false # Использовать '…' вместо '...' # Коэффициент запаса места относительно фактического размера проекта (до сжатия) # Например, 2.0 означает «нужно в 2 раза больше, чем весит проект на диске» ReserveFactor = 1.0 } # Ошибки как исключения $ErrorActionPreference = "Stop" # ----------------------------------------- # Вспомогательные функции вывода # ----------------------------------------- function Write-Ok { param([string]$msg) Write-Host "[OK] $msg" -ForegroundColor Green } function Write-Fail { param([string]$msg) Write-Host "[FAIL] $msg" -ForegroundColor Red } function Write-Warn { param([string]$msg) Write-Host "[WARN] $msg" -ForegroundColor Gray } function Write-Info { param([string]$msg) Write-Host "[INFO] $msg" -ForegroundColor Cyan } function Write-Step { param([string]$msg) Write-Host ""; Write-Host "=== $msg ===" -ForegroundColor Magenta } function Write-Block { param([string]$msg1, [string]$msg2) Write-Host "" Write-Host "========================================" -ForegroundColor Magenta Write-Host " [ $msg1 ] $msg2" Write-Host "========================================" -ForegroundColor Magenta } # DRY RUN helpers function DRY { param([string]$msg) if ($Config.DryRun) { Write-Host "[DRYRUN] $msg" -ForegroundColor DarkGray } } function Invoke-IfNotDry { param( [Parameter(Mandatory = $true)][scriptblock]$Action, [Parameter(Mandatory = $true)][string]$Description ) if ($Config.DryRun) { DRY $Description } else { & $Action } } # ----------------------------------------- # Функция проверки свободного места на диске # ----------------------------------------- function Test-DiskSpace { <# .SYNOPSIS Проверяет, достаточно ли места на диске для резервного копирования/архивации. .DESCRIPTION 1) Считает общий размер исходных данных проекта (до сжатия), игнорируя служебные папки и файлы по маскам. 2) Требуемое место = max( MinimumFreeMB, ProjectSize * ReserveFactor ). 3) Проверяет свободное место на томе проекта. .PARAMETER Path Корень проекта. .PARAMETER MinimumFreeMB Абсолютный минимум свободного места (в мегабайтах). .PARAMETER ReserveFactor Множитель к реальному размеру проекта (до сжатия). .PARAMETER ExcludeDirs Список папок, которые надо исключить из подсчёта (например, Backup, Archive). .PARAMETER ExcludeMasks Маски файлов, которые не учитывать (например, *.dcu, *.tmp и т.д.). .OUTPUTS [bool] True/False #> param ( [Parameter(Mandatory = $true)][string]$Path, [int]$MinimumFreeMB = 0, [double]$ReserveFactor = 2.0, [string[]]$ExcludeDirs = @('Backup', 'Archive'), [string[]]$ExcludeMasks = @() ) if (-not (Test-Path $Path)) { Write-Fail "Путь '$Path' не найден." return $false } Write-Info "Подсчёт размера проекта (исключая служебные папки/маски)..." try { # Собираем файлы, исключая каталоги Backup/Archive (и любые из ExcludeDirs), # а также файлы по маскам (ExcludeMasks) $files = Get-ChildItem -Path $Path -File -Recurse -Force -ErrorAction SilentlyContinue | Where-Object { $full = $_.FullName # Исключить по каталогам (на любом уровне) foreach ($ed in $ExcludeDirs) { if ($full -match ("\\{0}(\$|\\)" -f [regex]::Escape($ed))) { return $false } } # Исключить по маскам имён foreach ($m in $ExcludeMasks) { if ($_.Name -like $m) { return $false } } return $true } $totalBytes = 0 if ($files) { $totalBytes = ($files | Measure-Object -Property Length -Sum).Sum } if (-not $totalBytes) { $totalBytes = 0 } $totalMB = [math]::Round($totalBytes / 1MB, 2) Write-Host ("Размер исходных данных (до сжатия): {0} MB" -f $totalMB) -ForegroundColor Cyan # Требование по размеру проекта × запас $requiredByFactorBytes = [math]::Ceiling($totalBytes * $ReserveFactor) $requiredByFactorMB = [math]::Round($requiredByFactorBytes / 1MB, 2) # Абсолютный минимум из конфига $requiredByMinBytes = [math]::Ceiling([math]::Max(0, $MinimumFreeMB) * 1MB) $requiredByMinMB = [math]::Round($requiredByMinBytes / 1MB, 2) # Итоговое требование — максимум из двух $requiredBytes = [math]::Max($requiredByFactorBytes, $requiredByMinBytes) $requiredMB = [math]::Round($requiredBytes / 1MB, 2) Write-Host ("Порог по размеру×запас: {0} MB (x{1})" -f $requiredByFactorMB, $ReserveFactor) -ForegroundColor Yellow if ($requiredByMinMB -gt 0) { Write-Host ("Порог абсолютного минимума: {0} MB" -f $requiredByMinMB) -ForegroundColor Yellow } Write-Host ("Итоговое требование свободного места: {0} MB" -f $requiredMB) -ForegroundColor Yellow # Определяем том проекта надёжно $root = [System.IO.Directory]::GetDirectoryRoot((Resolve-Path $Path)) $driveName = $root.TrimEnd('\').TrimEnd(':') $freeBytes = (Get-PSDrive -Name $driveName).Free $freeMB = [math]::Round($freeBytes / 1MB, 2) Write-Host ("Свободно на диске {0}: {1} MB" -f $root.TrimEnd('\'), $freeMB) -ForegroundColor Green if ($freeBytes -lt $requiredBytes) { Write-Fail ("Недостаточно места на диске {0}: требуется {1} MB, доступно {2} MB" -f $root.TrimEnd('\'), $requiredMB, $freeMB) return $false } Write-Ok ("Места на диске {0} достаточно." -f $root.TrimEnd('\')) return $true } catch { Write-ErrorLog "Test-DiskSpace: $($_.Exception.Message)" Write-Fail "Ошибка при оценке свободного места: $($_.Exception.Message)" return $false } } # ----------------------------------------- # Переходим в каталог скрипта # ----------------------------------------- $ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition Set-Location $ScriptDir if ($Config.DryRun) { Write-Block "DRY RUN" "Скрипт работает в режиме симуляции — изменений на диске не будет" } Write-Info ("Config: DryRun={0}, IncrementalBackup={1}" -f $Config.DryRun, $Config.UseIncrementalBackup) # ------------------------------------------------- # Выбор версии PowerShell и автоматический перезапуск # ------------------------------------------------- $CurrentPSMajor = $PSVersionTable.PSVersion.Major $psChoiceFile = Join-Path $ScriptDir "BkUp.set" $pwshPath = (Get-Command pwsh -ErrorAction SilentlyContinue).Source $powershellPath = (Get-Command powershell -ErrorAction SilentlyContinue).Source try { $pwshCmd = Get-Command pwsh.exe -ErrorAction SilentlyContinue; if ($pwshCmd) { $pwshPath = $pwshCmd.Source } } catch {} try { $psCmd = Get-Command powershell.exe -ErrorAction SilentlyContinue; if ($psCmd) { $powershellPath = $psCmd.Source } } catch {} $avail = @{} if ($pwshPath) { $avail["1"] = "PowerShell 7+ (pwsh.exe)" } if ($powershellPath) { $avail["2"] = "PowerShell 5.1 (powershell.exe)" } if ($avail.Count -ge 2) { $savedChoice = $null if ($Config.RememberPowerShellChoice -and (Test-Path $psChoiceFile)) { $savedChoice = Get-Content $psChoiceFile -ErrorAction SilentlyContinue if ($savedChoice -notin @("1", "2")) { $savedChoice = $null } if ($savedChoice) { Write-Info "Использую сохранённый выбор: $($avail[$savedChoice])" } } if (-not $savedChoice) { Write-Host "" Write-Host "========================================" -ForegroundColor Magenta Write-Host " Выберите версию PowerShell для запуска:" Write-Host "----------------------------------------" foreach ($k in $avail.Keys) { $cur = "" if (($k -eq "1" -and $CurrentPSMajor -ge 7) -or ($k -eq "2" -and $CurrentPSMajor -lt 7)) { $cur = " [ТЕКУЩАЯ]" } Write-Host "$k — $($avail[$k])$cur" -ForegroundColor Cyan } Write-Host "----------------------------------------" if ($Config.RememberPowerShellChoice) { Write-Host "(Выбор будет сохранён)" } Write-Host "========================================" -ForegroundColor Magenta $verSel = Read-Host "Введите номер версии (1/2)" if ($Config.RememberPowerShellChoice -and $verSel -in @("1", "2")) { $verSel | Set-Content $psChoiceFile -Force Write-Info "Выбор версии PowerShell сохранён" } } else { $verSel = $savedChoice } if ($verSel -ne "1" -and $verSel -ne "2") { $verSel = if ($CurrentPSMajor -ge 7) { "1" } else { "2" } } $targetExe = if ($verSel -eq "1") { $pwshPath } else { $powershellPath } $needSwitch = (($verSel -eq "1" -and $CurrentPSMajor -lt 7) -or ($verSel -eq "2" -and $CurrentPSMajor -ge 7)) if ($needSwitch) { Write-Host "" Write-Host "⏩ Перезапуск скрипта через выбранную версию PowerShell..." -ForegroundColor Cyan $argsList = @("-NoProfile", "-ExecutionPolicy", "Bypass", "-File", $MyInvocation.MyCommand.Definition) if ($args.Count -gt 0) { $argsList += $args } & $targetExe @argsList exit $LASTEXITCODE } } else { Write-Host "Доступна только одна версия PowerShell: $($avail.Values -join ', '). Скрипт продолжает выполнение." -ForegroundColor Yellow } # ----------------------------------------- # Запрос режима DryRun при запуске # ----------------------------------------- $answer = Read-Host "Включить DryRun (только симуляция действий, без изменений)? (y/N)" if ($answer -match '^(y|Y|д|Д)') { $Config.DryRun = $true Write-Block "DRY RUN" "Скрипт работает в режиме симуляции — изменений на диске не будет" } else { $Config.DryRun = $false } Write-Info ("Config: DryRun={0}, IncrementalBackup={1}" -f $Config.DryRun, $Config.UseIncrementalBackup) # ----------------------------------------- # Логирование ошибок в файл # ----------------------------------------- function Write-ErrorLog { param( [Parameter(Mandatory = $true, Position = 0)] [string]$Message ) $stamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss' $logPath = Join-Path $ScriptDir 'error.log' Add-Content -Path $logPath -Value "$stamp $Message" -Encoding UTF8 } function Get-RelCompactPath { <# .SYNOPSIS Возвращает компактный путь: относительный к Root и укороченный до MaxChars. .DESCRIPTION Комбо-логика (вариант 3): сначала строит относительный путь от Root, затем компактирует середину, сохраняя начало и имя файла. Не падает при ошибках — возвращает исходный Path. .PARAMETER Path Абсолютный путь к файлу/папке. .PARAMETER Root База для относительного пути. По умолчанию $ScriptDir. .PARAMETER MaxChars Целевая длина строки. По умолчанию $Config.PathCompactMaxChars (или 80). .PARAMETER UseUnicodeEllipsis Использовать '…' вместо '...'. .OUTPUTS [string] Относительный и компактированный путь. #> [CmdletBinding()] param( [Parameter(Mandatory)][string]$Path, [string]$Root = $ScriptDir, [int]$MaxChars = $(if ($Config.PathCompactMaxChars) { [int]$Config.PathCompactMaxChars } else { 80 }), [bool]$UseUnicodeEllipsis = $(if ($null -ne $Config.UseUnicodeEllipsis) { [bool]$Config.UseUnicodeEllipsis } else { $false }) ) try { if ([string]::IsNullOrWhiteSpace($Path)) { return $Path } if ($MaxChars -lt 4) { $MaxChars = 4 } # --- 1) Относительный путь от Root --- $rel = $null try { $uFrom = New-Object System.Uri((Resolve-Path -LiteralPath $Root).Path) $uTo = New-Object System.Uri((Resolve-Path -LiteralPath $Path).Path) $rel = $uFrom.MakeRelativeUri($uTo).ToString() -replace '/', [System.IO.Path]::DirectorySeparatorChar } catch { # Если не удалось — работаем с исходным абсолютным путём $rel = $Path } if ($rel.Length -le $MaxChars) { return $rel } # --- 2) Компактирование середины --- $ellipsis = if ($UseUnicodeEllipsis) { '…' } else { '...' } $sep = [System.IO.Path]::DirectorySeparatorChar $fileName = [System.IO.Path]::GetFileName($rel) if ([string]::IsNullOrEmpty($fileName)) { # На случай пути на папку — просто «отрезаем» середину строки $head = [Math]::Max(1, [int]([Math]::Ceiling(($MaxChars - $ellipsis.Length) / 2.0))) $tail = [Math]::Max(0, ($MaxChars - $ellipsis.Length - $head)) return ($rel.Substring(0, [Math]::Min($head, $rel.Length)) + $ellipsis + $rel.Substring([Math]::Max(0, $rel.Length - $tail))) } if ($fileName.Length + 10 -ge $MaxChars) { # Очень короткий лимит — укорачиваем само имя файла (сохраняем расширение) $ext = [System.IO.Path]::GetExtension($fileName) $base = $fileName.Substring(0, [Math]::Max(1, $fileName.Length - $ext.Length)) $head = [Math]::Max(1, [int]([Math]::Ceiling(($MaxChars - $ext.Length - $ellipsis.Length) / 2.0))) $tail = [Math]::Max(0, ($MaxChars - $ext.Length - $ellipsis.Length - $head)) return ($base.Substring(0, [Math]::Min($head, $base.Length)) + $ellipsis + $base.Substring([Math]::Max(0, $base.Length - $tail)) + $ext) } $dirPart = $rel.Substring(0, $rel.Length - $fileName.Length).TrimEnd($sep) $dirs = $dirPart.Split($sep, [System.StringSplitOptions]::RemoveEmptyEntries) # Базовый скелет: "...\<хвост директорий>\file" $neededMin = $ellipsis.Length + 1 + $fileName.Length # "...\" + file if ($neededMin -gt $MaxChars) { return "$ellipsis$sep$fileName" } $rightDirs = New-Object System.Collections.Generic.List[string] for ($i = $dirs.Length - 1; $i -ge 0; $i--) { $candidate = if ($rightDirs.Count -eq 0) { $dirs[$i] } else { $dirs[$i] + $sep + [string]::Join($sep, $rightDirs) } $probe = "$ellipsis$sep$candidate$sep$fileName" if ($probe.Length -le $MaxChars) { $rightDirs.Insert(0, $dirs[$i]) } else { break } } if ($rightDirs.Count -eq 0) { return "$ellipsis$sep$fileName" } else { return "$ellipsis$sep$([string]::Join($sep, $rightDirs))$sep$fileName" } } catch { if (Get-Command Write-Warn -ErrorAction SilentlyContinue) { Write-Warn "Get-RelCompactPath: $($_.Exception.Message)" } return $Path } } function Get-RelativePathSafe { <# .SYNOPSIS Возвращает относительный путь To относительно From. Кросс-версия для PS 5.1/7+. .PARAMETER From Базовая директория. .PARAMETER To Абсолютный путь к файлу/папке. .OUTPUTS [string] Относительный путь, либо исходный To при невозможности вычислить. #> [CmdletBinding()] param( [Parameter(Mandatory)][string]$From, [Parameter(Mandatory)][string]$To ) try { $uFrom = New-Object System.Uri((Resolve-Path -LiteralPath $From).Path) $uTo = New-Object System.Uri((Resolve-Path -LiteralPath $To).Path) $rel = $uFrom.MakeRelativeUri($uTo).ToString() # Uri даёт с '/' — нормализуем под Windows return $rel -replace '/', [System.IO.Path]::DirectorySeparatorChar } catch { if (Get-Command Write-Warn -ErrorAction SilentlyContinue) { Write-Warn "Get-RelativePathSafe: $($_.Exception.Message)" } return $To } } # ----------------------------------------- # Функция получения валидного алгоритма хеширования # ----------------------------------------- function Get-ConfiguredHashAlgorithm { <# .SYNOPSIS Возвращает валидный алгоритм хеширования из $Config.HashAlgorithm. .DESCRIPTION Нормализует к UPPER, валидирует по списку, при невалидном — возвращает дефолт. .OUTPUTS [string] MD5|SHA1|SHA256|SHA384|SHA512|RIPEMD160 #> [CmdletBinding()] param( [Parameter()] [string]$Default = 'MD5' ) $allowed = 'MD5', 'SHA1', 'SHA256', 'SHA384', 'SHA512', 'RIPEMD160' $alg = $Config.HashAlgorithm if ([string]::IsNullOrWhiteSpace($alg)) { return $Default } $alg = $alg.ToUpperInvariant() if ($allowed -notcontains $alg) { Write-Warn "Недопустимый Config.HashAlgorithm='$($Config.HashAlgorithm)'. Использую $Default." return $Default } return $alg } # ========================= # Хеширование файлов # ========================= function Get-FileHashSafe { <# .SYNOPSIS Стримингово считает HEX-хеш файла (без загрузки всего файла в память). .PARAMETER Path Путь к файлу (Literal). .PARAMETER Algorithm MD5|SHA1|SHA256|SHA384|SHA512|RIPEMD160. .OUTPUTS [string] HEX-вида без дефисов. #> [CmdletBinding()] param( [Parameter(Mandatory)][string]$Path, [ValidateSet('MD5', 'SHA1', 'SHA256', 'SHA384', 'SHA512', 'RIPEMD160')] [string]$Algorithm = 'MD5' ) # Если не передали явно — используем конфиг if (-not $PSBoundParameters.ContainsKey('Algorithm')) { $Algorithm = Get-ConfiguredHashAlgorithm } $fileStream = $null $hashAlgo = $null try { $fileStream = New-Object System.IO.FileStream( $Path, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read, [System.IO.FileShare]::ReadWrite, 65536, [System.IO.FileOptions]::SequentialScan ) switch ($Algorithm) { 'MD5' { $hashAlgo = [System.Security.Cryptography.MD5]::Create() } 'SHA1' { $hashAlgo = [System.Security.Cryptography.SHA1]::Create() } 'SHA256' { $hashAlgo = [System.Security.Cryptography.SHA256]::Create() } 'SHA384' { $hashAlgo = [System.Security.Cryptography.SHA384]::Create() } 'SHA512' { $hashAlgo = [System.Security.Cryptography.SHA512]::Create() } 'RIPEMD160' { $hashAlgo = [System.Security.Cryptography.RIPEMD160]::Create() } } if (-not $hashAlgo) { throw "Алгоритм '$Algorithm' недоступен на этой платформе." } $bytes = $hashAlgo.ComputeHash($fileStream) return ([System.BitConverter]::ToString($bytes)).Replace('-', '') } finally { if ($hashAlgo) { $hashAlgo.Dispose() } if ($fileStream) { $fileStream.Dispose() } } } function Test-FilesEqual { <# .SYNOPSIS Сравнивает два файла: сначала размер, при равенстве — хеш. .DESCRIPTION Возвращает [bool] и текстовую причину (out) для логирования. .PARAMETER Src Абсолютный путь к исходному файлу. .PARAMETER Dst Абсолютный путь к целевому файлу. .PARAMETER Algorithm Алгоритм хеширования. .OUTPUTS [bool] $true если файлы идентичны #> [CmdletBinding()] param( [Parameter(Mandatory)][string]$Src, [Parameter(Mandatory)][string]$Dst, # Важно: список совпадает с Get-FileHashSafe (без MACTripleDES) [ValidateSet('MD5', 'SHA1', 'SHA256', 'SHA384', 'SHA512', 'RIPEMD160')] [string]$Algorithm = 'MD5', [ref]$Reason = [ref]$null ) # Если не передали явно — из конфига if (-not $PSBoundParameters.ContainsKey('Algorithm')) { $Algorithm = Get-ConfiguredHashAlgorithm } try { if (-not (Test-Path -LiteralPath $Dst -PathType Leaf)) { $Reason.Value = "Целевой файл отсутствует" return $false } $srcInfo = Get-Item -LiteralPath $Src -ErrorAction Stop $dstInfo = Get-Item -LiteralPath $Dst -ErrorAction Stop if ($srcInfo.Length -ne $dstInfo.Length) { $Reason.Value = "разный размер ($($srcInfo.Length) vs $($dstInfo.Length))" return $false } # Размеры равны — проверяем хеши $srcHash = Get-FileHashSafe -Path $srcInfo.FullName -Algorithm $Algorithm $dstHash = Get-FileHashSafe -Path $dstInfo.FullName -Algorithm $Algorithm if ($srcHash -ieq $dstHash) { $Reason.Value = "совпали размер и хеш" return $true } else { $Reason.Value = "разный хеш ($Algorithm)" return $false } } catch { # Любая ошибка в сравнении — считаем 'не равны' и логируем $Reason.Value = "ошибка сравнения: $($_.Exception.Message)" return $false } } # ----------------------------------------- # Копирование каталога с расширенными опциями # ----------------------------------------- function Copy-RootContent { <# .SYNOPSIS Копирует содержимое исходной директории в целевую с инкрементальным режимом, исключениями и полной статистикой операций. .DESCRIPTION Основные возможности: • Инкрементальное копирование: - Активируется через $Config.UseIncrementalBackup - Пропускает неизменённые файлы (проверка размера и хеша) - Копирует только изменённые или новые файлы • Гибкая система исключений: - По точным именам ($NameExcludeList) - По маскам файлов ($MaskExcludeList) - Поддерживает рекурсивный обход • Подробная статистика: - Копирование/пропуск/исключение файлов - Подсчёт размеров по категориям - Хранится в $script:CopyStats • Безопасность: - Поддержка режима DryRun ($Config.DryRun) - Детальное логирование операций - Обработка ошибок с записью в лог #> [CmdletBinding()] param ( [string]$SourceDir, [string]$TargetDir, [array] $ExcludeList, [array] $ExcludeMaskList, # Список согласован с Test-FilesEqual/Get-FileHashSafe [ValidateSet('MD5', 'SHA1', 'SHA256', 'SHA384', 'SHA512', 'RIPEMD160')] [string]$HashAlgorithm = 'MD5' ) # Если не передали явно — из конфига if (-not $PSBoundParameters.ContainsKey('HashAlgorithm')) { $HashAlgorithm = Get-ConfiguredHashAlgorithm } if (-not $script:CopyStats) { $script:CopyStats = [ordered]@{ Copied = 0 Skipped = 0 Excluded = 0 CopiedSize = 0L SkippedSize = 0L ExcludedSize = 0L } } # Компилируем маски $maskPatterns = @() foreach ($m in $ExcludeMaskList) { if ([string]::IsNullOrWhiteSpace($m)) { continue } $maskPatterns += New-Object System.Management.Automation.WildcardPattern($m, 'IgnoreCase') } function Test-ExcludedName([string]$name) { if ($ExcludeList -contains $name) { return $true } foreach ($p in $maskPatterns) { if ($p.IsMatch($name)) { return $true } } return $false } if (-not (Test-Path -LiteralPath $TargetDir)) { Invoke-IfNotDry { New-Item -Path $TargetDir -ItemType Directory -Force | Out-Null } "Создать папку: $TargetDir" } $items = Get-ChildItem -LiteralPath $SourceDir -Force -ErrorAction SilentlyContinue foreach ($item in $items) { if (Test-ExcludedName $item.Name) { Write-Warn "Исключено: $($item.Name)" if (-not $item.PSIsContainer) { $script:CopyStats.Excluded++ $script:CopyStats.ExcludedSize += ($item.Length) } continue } $destPath = Join-Path $TargetDir $item.Name try { if ($item.PSIsContainer) { Copy-RootContent -SourceDir $item.FullName -TargetDir $destPath ` -ExcludeList $ExcludeList -ExcludeMaskList $ExcludeMaskList -HashAlgorithm $HashAlgorithm continue } $needCopy = -not $Config.UseIncrementalBackup if (-not $needCopy) { $compareReason = $null $eq = Test-FilesEqual -Src $item.FullName -Dst $destPath -Algorithm $HashAlgorithm -Reason ([ref]$compareReason) # Красивая печать: путь относительно $ScriptDir и компакт до лимита $nicePath = Get-RelCompactPath -Path $item.FullName -Root $ScriptDir if ($eq) { Write-Info ("Без изменений: {0} — {1}" -f $nicePath, $compareReason) $script:CopyStats.Skipped++ $script:CopyStats.SkippedSize += $item.Length continue } else { $needCopy = $true if ($compareReason) { Write-Info ("{0}: {1}" -f $compareReason, $nicePath) } } } if ($needCopy) { Invoke-IfNotDry { Copy-Item -LiteralPath $item.FullName -Destination $destPath -Force -ErrorAction Stop } ` "Копировать: $($item.FullName) -> $destPath" Write-Ok "Скопировано: $($item.Name)" $script:CopyStats.Copied++ $script:CopyStats.CopiedSize += $item.Length } } catch { Write-Fail "Не удалось скопировать $($item.Name): $($_.Exception.Message)" Write-ErrorLog "Copy: $($item.FullName) -> $destPath : $($_.Exception.Message)" } } } function Show-BackupStats { if (-not $script:CopyStats) { return } Write-Host "" Write-Host "=== Статистика копирования ===" -ForegroundColor Magenta Write-Host ("Скопировано файлов: {0} ({1:N2} MB)" -f $script:CopyStats.Copied, ($script:CopyStats.CopiedSize / 1MB)) -ForegroundColor Green Write-Host ("Пропущено неизменённых: {0} ({1:N2} MB)" -f $script:CopyStats.Skipped, ($script:CopyStats.SkippedSize / 1MB)) -ForegroundColor Cyan Write-Host ("Исключено правилами: {0} ({1:N2} MB)" -f $script:CopyStats.Excluded, ($script:CopyStats.ExcludedSize / 1MB)) -ForegroundColor Yellow Write-Host "==============================" -ForegroundColor Magenta Write-Host "" } function Reset-BackupStats { $script:CopyStats = [ordered]@{ Copied = 0 Skipped = 0 Excluded = 0 CopiedSize = 0L SkippedSize = 0L ExcludedSize = 0L } } # ----------------------------------------- # Очистка временных папок и файлов (DryRun) # ----------------------------------------- function Remove-TempContent { <# .SYNOPSIS Удаляет временные папки и файлы по маскам (учитывает DryRun) .PARAMETER TempFolders Список папок для удаления .PARAMETER TempFileMasks Список масок файлов для удаления #> param( [Parameter(Mandatory = $true)][string[]]$TempFolders, [Parameter(Mandatory = $true)][string[]]$TempFileMasks ) try { Write-Step "Очистка временных папок" foreach ($folder in Get-ChildItem -Recurse -Directory -Force -ErrorAction SilentlyContinue) { foreach ($name in $TempFolders) { if ($folder.Name -ieq $name) { try { Invoke-IfNotDry { Remove-Item $folder.FullName -Recurse -Force -ErrorAction Stop } "Удалить папку: $($folder.FullName)" Write-Ok "Удалена: $($folder.FullName)" } catch { Write-Fail "Не удалось удалить: $($folder.FullName) — $($_.Exception.Message)" Write-ErrorLog "DelFolder: $($folder.FullName): $($_.Exception.Message)" } break } } } Write-Step "Очистка временных файлов" foreach ($mask in $TempFileMasks) { Get-ChildItem -Recurse -Include $mask -File -Force -ErrorAction SilentlyContinue | ForEach-Object { try { Invoke-IfNotDry { Remove-Item $_.FullName -Force -ErrorAction Stop } "Удалить файл: $($_.FullName)" Write-Ok "Удалён: $($_.FullName)" } catch { Write-Fail "Не удалось удалить: $($_.FullName) — $($_.Exception.Message)" Write-ErrorLog "DelFile: $($_.FullName): $($_.Exception.Message)" } } } } catch { Write-Fail "Ошибка при очистке: $($_.Exception.Message)" Write-ErrorLog "CleanTemp: $($_.Exception.Message)" } } # ----------------------------------------- # Функция резервного копирования .exe/.apk (DryRun поддержан) # ----------------------------------------- function Invoke-BackupFiles { <# .SYNOPSIS Создает резервные копии файлов .exe и .apk .DESCRIPTION 1. Копирование в Backup (инкрементально при $Config.UseIncrementalBackup) 2. По желанию — упаковка подпапок Windows/Android в ZIP/RAR #> param ( [string]$ScriptDir, [string]$Root, [array]$ExeFiles, [array]$ApkFiles, [array]$NameExcludeList, [array]$MaskExcludeList ) $allFiles = @() if ($ExeFiles) { $allFiles += $ExeFiles } if ($ApkFiles) { $allFiles += $ApkFiles } if ($allFiles.Count -eq 0) { Write-Warn "Нет найденных файлов .exe или .apk для резервного копирования." Write-Host "" Write-Host "========================================" -ForegroundColor Magenta Write-Host "[INFO] Сохранение Backup пропущено — файлов нет." Write-Host "========================================" -ForegroundColor Magenta return } Write-Block "Backup" "Сохранение найденных файлов .exe и .apk" $backupDir = Join-Path $ScriptDir "Backup" if (!(Test-Path $backupDir)) { Invoke-IfNotDry { New-Item -Path $backupDir -ItemType Directory -Force | Out-Null } "Создать папку: $backupDir" } # $backupDate = Get-Date -Format "dd.MM.yy" $backupBaseName = "$Root" $backupBasePath = Join-Path $backupDir $backupBaseName Write-Host "Как сохранить найденные файлы .exe/.apk?" -ForegroundColor Yellow Write-Host "1 – Скопировать в Backup (по умолчанию)" -ForegroundColor Cyan Write-Host "2 – Упаковать в архив ZIP" -ForegroundColor Cyan Write-Host "3 – Упаковать в архив RAR" -ForegroundColor Cyan $backupMode = Read-Host "Выберите (1/2/3)" $windowsTarget = Join-Path $backupBasePath "Windows" $androidTarget = Join-Path $backupBasePath "Android" # Копирование .exe if ($ExeFiles) { foreach ($exeFile in $ExeFiles) { $exeDir = Split-Path $exeFile.FullName -Parent $exeTarget = Join-Path $windowsTarget $exeFile.BaseName Invoke-IfNotDry { New-Item -Path $exeTarget -ItemType Directory -Force | Out-Null } "Создать папку: $exeTarget" Copy-RootContent -SourceDir $exeDir -TargetDir $exeTarget ` -ExcludeList $NameExcludeList -ExcludeMaskList $MaskExcludeList ` -HashAlgorithm (Get-ConfiguredHashAlgorithm) } Show-BackupStats Reset-BackupStats } # Копирование .apk if ($ApkFiles) { foreach ($apkFile in $ApkFiles) { $apkDir = Split-Path $apkFile.FullName -Parent $apkTarget = Join-Path $androidTarget $apkFile.BaseName Invoke-IfNotDry { New-Item -Path $apkTarget -ItemType Directory -Force | Out-Null } "Создать папку: $apkTarget" Copy-RootContent -SourceDir $apkDir -TargetDir $apkTarget ` -ExcludeList $NameExcludeList -ExcludeMaskList $MaskExcludeList ` -HashAlgorithm (Get-ConfiguredHashAlgorithm) } Show-BackupStats Reset-BackupStats } # Архивация результата (по выбору) switch ($backupMode) { "2" { if (Test-Path $zipPath) { $zipFile = "$backupBasePath.7z" Write-Info "Упаковка всей структуры в архив ZIP ($zipFile)" Invoke-IfNotDry { & "$zipPath" a -tzip -mx=9 "$zipFile" "$backupBasePath\*" } "ZIP: $backupBasePath -> $zipFile" if (-not $Config.DryRun) { if ($LASTEXITCODE -eq 0) { Write-Ok "Структура файлов упакована в $zipFile" Invoke-IfNotDry { Remove-Item $backupBasePath -Recurse -Force } "Удалить временную папку бэкапа: $backupBasePath" } else { Write-Fail "Ошибка упаковки в ZIP, код: $LASTEXITCODE" } } } else { Write-Fail "7-Zip не найден, невозможно создать архив ZIP." } } "3" { if (Test-Path $rarPath) { $rarFile = "$backupBasePath.rar" Write-Info "Упаковка всей структуры в архив RAR ($rarFile)" Invoke-IfNotDry { & "$rarPath" a -ep1 -r -m5 "$rarFile" "$backupBasePath\*" } "RAR: $backupBasePath -> $rarFile" if (-not $Config.DryRun) { if ($LASTEXITCODE -eq 0) { Write-Ok "Структура файлов упакована в $rarFile" Invoke-IfNotDry { Remove-Item $backupBasePath -Recurse -Force } "Удалить временную папку бэкапа: $backupBasePath" } else { Write-Fail "Ошибка упаковки в RAR, код: $LASTEXITCODE" } } } else { Write-Fail "WinRAR не найден, невозможно создать архив RAR." } } default { Write-Ok "Файлы скопированы в Backup с сохранением структуры." } } } # ----------------------------------------- # Формирование exclude-файлов для RAR/7Z # ----------------------------------------- function New-7zExcludeFile { <# .SYNOPSIS Формирует набор ключей исключений для 7-Zip (-x!/ -xr!) .RETURNS [string[]] — массив параметров командной строки 7z (например: "-xr!__history\*", "-x!__history", ...) #> param( [string[]]$NameExcludeList, # "Thumbs.db", ".git", ".svn" [string[]]$MaskExcludeList, # "*.idsig", "*.dcu", "*.tmp", "*.log" [string[]]$TempFolders, # "__history", "__recovery", "tmp", ".BIN" [string[]]$TempFileMasks # "*.tmp", "*.dcu" ) $sw = @() # Каталоги (и их содержимое) — исключаем на всех уровнях foreach ($d in $TempFolders) { if ([string]::IsNullOrWhiteSpace($d)) { continue } $sw += "-xr!$d\*" # содержимое каталога в корне $sw += "-x!$d" # сам каталог в корне $sw += "-x!*\$d" # сам каталог на любом уровне $sw += "-xr!*\$d\*" # содержимое каталога на любом уровне } # Имена: могут быть и файлами, и папками (.git/.svn/Thumbs.db) — даём полный набор foreach ($n in $NameExcludeList) { if ([string]::IsNullOrWhiteSpace($n)) { continue } $sw += "-x!$n" # файл/каталог в корне $sw += "-x!*\$n" # файл/каталог на любом уровне $sw += "-xr!$n\*" # если это папка — её содержимое в корне $sw += "-xr!*\$n\*" # если это папка — её содержимое на любом уровне } # Маски файлов — в корне и на любом уровне foreach ($m in ($TempFileMasks + $MaskExcludeList)) { if ([string]::IsNullOrWhiteSpace($m)) { continue } $sw += "-x!$m" $sw += "-x!*\$m" } return $sw } function New-RarExcludeFile { <# .SYNOPSIS Формирует набор ключей исключений для WinRAR (-x<mask>) .RETURNS [string[]] — массив параметров командной строки RAR (например: "-x__history\*", "-x*\__history\*", "-x__history", ...) #> param( [string[]]$NameExcludeList, # "Thumbs.db", ".git", ".svn" [string[]]$MaskExcludeList, # "*.idsig", "*.dcu", "*.tmp", "*.log" [string[]]$TempFolders, # "__history", "__recovery", "tmp", ".BIN" [string[]]$TempFileMasks # "*.tmp", "*.dcu" ) $sw = @() # Каталоги — исключаем и объекты, и содержимое в корне и на любом уровне foreach ($d in $TempFolders) { if ([string]::IsNullOrWhiteSpace($d)) { continue } $sw += "-x$d\*" # содержимое в корне $sw += "-x*\$d\*" # содержимое на любом уровне $sw += "-x$d" # сам каталог в корне $sw += "-x*\$d" # сам каталог на любом уровне } # Имена (могут быть файл/каталог) — даём все варианты foreach ($n in $NameExcludeList) { if ([string]::IsNullOrWhiteSpace($n)) { continue } $sw += "-x$n" # файл/каталог в корне $sw += "-x*\$n" # файл/каталог на любом уровне $sw += "-x$n\*" # если это папка — содержимое в корне $sw += "-x*\$n\*" # если это папка — содержимое на любом уровне } # Маски файлов — в корне и на любом уровне foreach ($m in ($TempFileMasks + $MaskExcludeList)) { if ([string]::IsNullOrWhiteSpace($m)) { continue } $sw += "-x$m" $sw += "-x*\$m" } return $sw } # ----------------------------------------- # Архивация проекта с управлением по датам # ----------------------------------------- function Invoke-ProjectArchiving { <# .SYNOPSIS Архивация проекта через 7-Zip или WinRAR с управлением по датам архивов. .DESCRIPTION Логика работы с архивами: • Архивы не найдены: - Создаётся новый архив с текущей датой • Найден архив с текущей датой: - Выполняется обновление без очистки папки Backup • Найден архив с другой датой: - Очищается Backup во всех существующих архивах - Создаётся новый архив с текущей датой - Старые архивы удаляются Дополнительные особенности: • Исключения ($tempFolders, $tempFileMasks, $NameExcludeList, $MaskExcludeList) применяются ВСЕГДА и полностью убирают и содержимое, и сами каталоги • Все действия, меняющие диск, проходят через Invoke-IfNotDry (DryRun) • Поддерживается сохранение структуры папок и исключений для обоих архиваторов #> param ( [string]$ScriptDir, [string]$Root, [string]$ArchiveDate, [array]$NameExcludeList, [array]$MaskExcludeList, [array]$TempFolders, [array]$TempFileMasks ) # Выбор архиватора Write-Host "Какой архиватор использовать?" -ForegroundColor Yellow Write-Host "1 – 7-Zip (по умолчанию)" -ForegroundColor Yellow Write-Host "2 – WinRAR" -ForegroundColor Yellow $archiver = Read-Host "Выберите (1/2)" # Папка Archive $archiveFolder = Join-Path $ScriptDir "Archive" if (!(Test-Path $archiveFolder)) { Invoke-IfNotDry { New-Item -Path $archiveFolder -ItemType Directory -Force | Out-Null } "Создать папку: $archiveFolder" } # Имена архивов $isIncremental = $false # Определяется далее по наличию архива с текущей датой $archiveBaseName = "$Root" + "_$ArchiveDate" $archiveFilePath = Join-Path $archiveFolder $archiveBaseName $archivePassword = $Root.Trim() # Существующие архивы (для очистки и удаления) $allArchives = Get-ChildItem $archiveFolder -File | Where-Object { $_.Extension -in @('.7z', '.rar') } # Пути к архиваторам $zipExists = Test-Path $zipPath $rarExists = Test-Path $rarPath # --- Проверка существующих архивов и их дат --- $currentDatePattern = "_$ArchiveDate" $existingArchives = $allArchives | Where-Object { $_.Name -match [regex]::Escape($Root) } $todayArchive = $existingArchives | Where-Object { $_.Name -match [regex]::Escape($currentDatePattern) } $createNewArchive = $false $cleanBackupInArchive = $false if ($existingArchives.Count -eq 0) { # Архивы не найдены - создаём новый с текущей датой Write-Info "Архивы проекта не найдены. Будет создан новый архив." $createNewArchive = $true } elseif ($todayArchive) { # Найден архив с текущей датой - обновляем его Write-Info "Найден архив с текущей датой: $($todayArchive.Name)" Write-Info "Файлы в архиве будут обновлены без очистки папки Backup" $isIncremental = $true # Исключаем папку Archive при обновлении $exclude7z += "-x!Archive\*" $excludeRar += "-xArchive\*" } else { # Найден архив с другой датой - создаём новый, предварительно очистив Backup Write-Info "Найдены только архивы с другими датами. Будет создан новый архив." $createNewArchive = $true $cleanBackupInArchive = $true } # Унифицированные исключения (массивы ключей) — один стиль для обоих архиваторов # 7z: "-x!", "-xr!" $exclude7z = New-7zExcludeFile -NameExcludeList $NameExcludeList ` -MaskExcludeList $MaskExcludeList ` -TempFolders $TempFolders ` -TempFileMasks $TempFileMasks # Добавляем исключения для архивных файлов в папке Backup для 7-Zip $exclude7z += "-x!Backup\" + $Root + ".7z" $exclude7z += "-x!Backup\" + $Root + ".rar" # RAR: "-x<mask>" $excludeRar = New-RarExcludeFile -NameExcludeList $NameExcludeList ` -MaskExcludeList $MaskExcludeList ` -TempFolders $TempFolders ` -TempFileMasks $TempFileMasks # Добавляем исключения для архивных файлов в папке Backup для WinRAR $excludeRar += "-xBackup\" + $Root + ".7z" $excludeRar += "-xBackup\" + $Root + ".rar" # --- Очистка Backup в существующих архивах при необходимости --- if ($cleanBackupInArchive -and $existingArchives.Count -gt 0) { foreach ($archive in $existingArchives) { $fullName = $archive.FullName if ($archive.Extension -eq ".7z" -and $zipExists) { Write-Info "Удаляем Backup из $($archive.Name) (7-Zip)..." Invoke-IfNotDry { & "$zipPath" d "$fullName" "Backup\*" "-p$archivePassword" } "7z d `"$($archive.Name)`" Backup\*" if (-not $Config.DryRun -and $LASTEXITCODE -ne 0) { Write-Warn "Ошибка удаления из $($archive.Name) (7z), код: $LASTEXITCODE" } } elseif ($archive.Extension -eq ".rar" -and $rarExists) { Write-Info "Удаляем Backup из $($archive.Name) (RAR)..." Invoke-IfNotDry { & "$rarPath" d -r "-p$archivePassword" "$fullName" "Backup\*" } "rar d `"$($archive.Name)`" Backup\*" if (-not $Config.DryRun -and $LASTEXITCODE -ne 0) { Write-Warn "Ошибка удаления из $($archive.Name) (RAR), код: $LASTEXITCODE" } } else { Write-Warn "Нет подходящего архиватора для $($archive.Name) или архиватор не найден." } } } # --- Упаковка / инкрементальное обновление --- if ($archiver -eq "2") { # ========== WinRAR ========== if ($rarExists) { if ($isIncremental) { Write-Info "Инкрементальная архивация через WinRAR..." } else { Write-Info "Архивация через WinRAR..." } $rarOut = "$archiveFilePath.rar" Push-Location $ScriptDir try { if (-not $createNewArchive -and (Test-Path $rarOut)) { # UPDATE $excludeRar += "-xArchive" $excludeRar += "-xArchive\*" Invoke-IfNotDry { & "$rarPath" u -ep1 -r "-p$archivePassword" @excludeRar "$rarOut" * } ` ("RAR UPDATE: {0} из {1}" -f $rarOut, $ScriptDir) } else { # ADD - новый архив Invoke-IfNotDry { & "$rarPath" a -ep1 -r "-hp$archivePassword" -m5 @excludeRar "$rarOut" * } ` ("RAR ADD: {0} из {1}" -f $rarOut, $ScriptDir) } } finally { Pop-Location } # --- 3) Non-incremental: удалить старые архивы --- if (-not $isIncremental) { foreach ($archive in $allArchives) { if ($archive.FullName -ne $rarOut) { Invoke-IfNotDry { Remove-Item $archive.FullName -Force } "Удалить старый архив: $($archive.Name)" } } } } else { Write-Warn "WinRAR не найден, пробуем 7-Zip..." $archiver = "1" } } if ($archiver -ne "2") { # ========== 7-Zip ========== if ($zipExists) { if ($isIncremental) { Write-Info "Инкрементальная архивация через 7-Zip..." } else { Write-Info "Архивация через 7-Zip..." } $zipOut = "$archiveFilePath.7z" Push-Location $ScriptDir try { if (-not $createNewArchive -and (Test-Path $zipOut)) { # UPDATE $exclude7z += "-x!Archive" $exclude7z += "-x!Archive\*" Invoke-IfNotDry { & "$zipPath" u -r "-p$archivePassword" $exclude7z "$zipOut" "*" } ` ("7Z UPDATE: {0} из {1}" -f $zipOut, $ScriptDir) } else { # ADD - новый архив Invoke-IfNotDry { & "$zipPath" a -t7z -m0=lzma2 -mx=9 -r "-p$archivePassword" $exclude7z "$zipOut" "*" } ` ("7Z ADD: {0} из {1}" -f $zipOut, $ScriptDir) } } finally { Pop-Location } # --- Non-incremental: удалить старые архивы --- if (-not $isIncremental) { foreach ($archive in $allArchives) { if ($archive.FullName -ne $zipOut) { Invoke-IfNotDry { Remove-Item $archive.FullName -Force } "Удалить старый архив: $($archive.Name)" } } } } else { Write-Fail "7-Zip не найден! Архивация невозможна." Write-ErrorLog "Archive7Z: Not found" } } # Выодим пароль для архива if ($archivePassword) { Write-Block "Пароль к архиву" $archivePassword } } # ========================================= # ОСНОВНОЙ БЛОК # ========================================= # Порядок выполнения: # 1. Проверка свободного места на диске # • Расчёт размера проекта (с исключениями) # • Проверка минимума и коэффициента запаса # # 2. Выбор режима очистки временных файлов: # • Удаление перед бэкапом # • Только исключение при архивации # # 3. Очистка .dproj от JAR библиотек (опционально) # • Поиск и удаление блоков <EnabledSysJars> # # 4. Резервное копирование: # • Поиск .exe/.apk файлов # • Копирование с сохранением структуры # • Опциональная архивация бэкапа # # 5. Архивация проекта: # • Определение режима по датам # • Обработка существующих архивов # • Создание/обновление архива # ========================================= try { # Проверка достаточного количества свободного места на диске if (-not (Test-DiskSpace ` -Path $ScriptDir ` -MinimumFreeMB $Config.MinimumFreeSpaceMB ` -ReserveFactor $Config.ReserveFactor ` -ExcludeDirs @('Backup', 'Archive') ` -ExcludeMasks $MaskExcludeList)) { Write-Host "" $continue = Read-Host "Продолжить несмотря на нехватку места? (y/N)" if ($continue -ne "y") { Write-Host "Операция отменена пользователем." exit 0 } } # Выбор режима очистки Write-Block "ОЧИСТКА ПРОЕКТА" "Режимы:" Write-Host "1 – Очистить временные файлы/папки из проекта перед бэкапом" -ForegroundColor Yellow Write-Host "2 – Не удалять временные файлы/папки, а просто исключать их при архивации (по умолчанию)" -ForegroundColor Yellow $cleanMode = Read-Host "Выберите режим (1/2)" $isCleanBeforeBackup = ($cleanMode -eq "1") # Очистка .dproj от <EnabledSysJars> (опционально) Write-Block "JAR" "Очистка jar библиотек" Write-Host "Хотите удалить блоки <EnabledSysJars> из всех .dproj файлов?" -ForegroundColor White Write-Host "1 – Да" -ForegroundColor Yellow Write-Host "2 – Нет (по умолчанию)" -ForegroundColor Yellow $doCleanDproj = Read-Host "Выберите (1/2)" if ($doCleanDproj -eq "1") { $dprojFiles = Get-ChildItem -Recurse -Filter *.dproj -File -ErrorAction SilentlyContinue foreach ($file in $dprojFiles) { try { [xml]$xml = Get-Content $file.FullName -Raw $nsMgr = New-Object System.Xml.XmlNamespaceManager($xml.NameTable) $defaultNs = $xml.DocumentElement.GetNamespaceOfPrefix("") $nsMgr.AddNamespace("d", $defaultNs) $nodesToRemove = $xml.SelectNodes("//d:EnabledSysJars", $nsMgr) if ($nodesToRemove.Count -gt 0) { foreach ($node in $nodesToRemove) { $parent = $node.ParentNode if (-not $Config.DryRun) { $parent.RemoveChild($node) | Out-Null } else { DRY "Удалить узел <EnabledSysJars> в $($file.FullName)" } } if (-not $Config.DryRun) { $xml.Save($file.FullName) } Write-Ok "Обновлён: $($file.FullName) — удалено $($nodesToRemove.Count) блок(ов)" } else { Write-Info "Блоки <EnabledSysJars> не найдены в $($file.FullName)" } } catch { Write-Fail "Ошибка при обработке $($file.FullName): $($_.Exception.Message)" Write-ErrorLog "dproj: $($file.FullName): $($_.Exception.Message)" } } } # Подготовка информации о проекте $root = Split-Path $ScriptDir -Leaf $archiveDate = Get-Date -Format "dd.MM.yy" # Поиск .groupproj $groupProj = Get-ChildItem "$ScriptDir" -Filter *.groupproj | Select-Object -First 1 if ($groupProj) { $groupProjName = $groupProj.Name Write-Info "Найден groupproj: $groupProjName" } else { $groupProjName = $null Write-Info "groupproj не найден" } # Поиск .exe / .apk Write-Step "Поиск .exe" $exeFiles = @() try { $exeFiles = Get-ChildItem -Path $ScriptDir -Recurse -Filter *.exe -File -ErrorAction Stop | Where-Object { ($_.FullName -notmatch "\\Backup\\") -and ($_.FullName -notmatch "\\Archive\\") } } catch { Write-Warn "Ошибка при поиске .exe: $($_.Exception.Message)" Write-ErrorLog "FindExe: $($_.Exception.Message)" } if ($exeFiles.Count -gt 0) { foreach ($file in $exeFiles) { Write-Info "Найден .exe: $($file.FullName.Substring($ScriptDir.Length).TrimStart('\'))" } } else { Write-Info ".exe не найдены" } Write-Step "Поиск .apk" $apkFiles = @() try { $apkFiles = Get-ChildItem -Path $ScriptDir -Recurse -Filter *.apk -File -ErrorAction Stop | Where-Object { ($_.FullName -notmatch "\\Backup\\") -and ($_.FullName -notmatch "\\Archive\\") } } catch { Write-Warn "Ошибка при поиске .apk: $($_.Exception.Message)" Write-ErrorLog "FindApk: $($_.Exception.Message)" } if ($apkFiles.Count -gt 0) { foreach ($file in $apkFiles) { Write-Info "Найден .apk: $($file.FullName.Substring($ScriptDir.Length).TrimStart('\'))" } } else { Write-Info ".apk не найдены" } # Создание резервной копии .exe/.apk Invoke-BackupFiles -ScriptDir $ScriptDir -Root $root -ExeFiles $exeFiles -ApkFiles $apkFiles ` -NameExcludeList $NameExcludeList -MaskExcludeList $MaskExcludeList # Очистка временных файлов (если выбрано) if ($isCleanBeforeBackup) { Remove-TempContent -TempFolders $tempFolders -TempFileMasks $tempFileMasks } # Архивация всего проекта Write-Block "Архивация" "Упаковка всего проекта" Invoke-ProjectArchiving -ScriptDir $ScriptDir -Root $root -ArchiveDate $archiveDate ` -NameExcludeList $NameExcludeList -MaskExcludeList $MaskExcludeList ` -TempFolders $tempFolders -TempFileMasks $tempFileMasks # Готово Write-Host "" Write-Host "========================================" -ForegroundColor Magenta Write-Host "[SUCCESS] Архивирование и резервное копирование завершены!" Write-Host "========================================" -ForegroundColor Magenta } catch { Write-Host "" Write-Fail "Необработанная ошибка: $($_.Exception.Message)" Write-ErrorLog "Fatal: $($_.Exception.Message)" Read-Host -Prompt "Нажмите Enter для выхода" exit 1 } finally { Write-Host "" Write-Host "[INFO] === Скрипт завершил работу ===" -ForegroundColor Cyan Start-Sleep 4 }