Skip to content
mrVanich
Назад до нотаток

Надійне копіювання папок у Windows: PowerShell + Robocopy

Копіювання папок у Windows здається простою задачею рівно до моменту, коли потрібно зробити це регулярно, не втратити вкладені файли, побачити помилку мережі й мати журнал виконання. Для одного файлу достатньо Copy-Item. Для великої папки, NAS або резервної копії практичніше використати robocopy, а PowerShell залишити для параметрів, перевірок і зрозумілого запуску.

Нижче – готовий скрипт для Windows 10/11. Він копіює лише потрібні зміни, створює папку призначення, пише лог, вміє працювати в режимі перевірки та коректно розпізнає коди завершення robocopy.

Що робить скрипт

  • перевіряє, що вихідна папка існує;
  • створює папку призначення, якщо її ще немає;
  • не дозволяє випадково копіювати папку саму в себе або у власну вкладену папку;
  • копіює вкладені папки й файли з повторними спробами при тимчасовій помилці мережі;
  • створює окремий файл логу в каталозі logs поруч зі скриптом;
  • підтримує -DryRun: показує план операції без запису на диск;
  • має окрему небезпечну опцію -Mirror для дзеркальної синхронізації.

Режим -Mirror видаляє у папці призначення файли, яких уже немає в джерелі. Не використовуйте його для першого запуску або для єдиної копії даних.

Скрипт Copy-Folder.ps1

Створіть файл Copy-Folder.ps1 і вставте код нижче.

[CmdletBinding()]
param(
    [Parameter(Mandatory)]
    [ValidateNotNullOrEmpty()]
    [string]$Source,

    [Parameter(Mandatory)]
    [ValidateNotNullOrEmpty()]
    [string]$Destination,

    # Увімкнення /MIR: видаляє у Destination те, чого немає в Source.
    [switch]$Mirror,

    # Показує дії Robocopy, але нічого не копіює.
    [switch]$DryRun
)

Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'

try {
    if (-not (Test-Path -LiteralPath $Source -PathType Container)) {
        throw "Вихідну папку не знайдено: $Source"
    }

    $sourcePath = (Resolve-Path -LiteralPath $Source).Path

    if (-not (Test-Path -LiteralPath $Destination -PathType Container)) {
        New-Item -ItemType Directory -Path $Destination -Force | Out-Null
    }

    $destinationPath = (Resolve-Path -LiteralPath $Destination).Path

    if ($sourcePath.TrimEnd('\') -eq $destinationPath.TrimEnd('\')) {
        throw 'Source і Destination не можуть бути однією папкою.'
    }

    $sourceWithSeparator = $sourcePath.TrimEnd('\') + '\'
    if ($destinationPath.StartsWith(
        $sourceWithSeparator,
        [System.StringComparison]::OrdinalIgnoreCase
    )) {
        throw 'Destination не може бути вкладеною папкою Source.'
    }

    $logDirectory = Join-Path -Path $PSScriptRoot -ChildPath 'logs'
    New-Item -ItemType Directory -Path $logDirectory -Force | Out-Null

    $timestamp = Get-Date -Format 'yyyyMMdd-HHmmss'
    $logFile = Join-Path -Path $logDirectory -ChildPath "copy-$timestamp.log"

    $robocopyArgs = @(
        $sourcePath
        $destinationPath
        '/E'          # Усі підпапки, включно з порожніми.
        '/COPY:DAT'   # Дані, атрибути та часові мітки.
        '/DCOPY:DAT'
        '/Z'          # Відновлюване копіювання при розриві мережі.
        '/FFT'        # Зручніше для NAS/FAT із грубішою точністю часу.
        '/XJ'         # Не проходити junction points.
        '/R:2'        # Не зависати: дві повторні спроби.
        '/W:3'        # Пауза між спробами - 3 секунди.
        '/NP'         # Не засмічувати лог відсотками.
        '/TEE'        # Показувати результат і в консолі, і в логу.
        "/LOG:$logFile"
    )

    if ($DryRun) {
        $robocopyArgs += '/L'
        Write-Host 'Режим перевірки: файли не будуть скопійовані.' -ForegroundColor Yellow
    }

    if ($Mirror) {
        $robocopyArgs += '/MIR'
        Write-Warning 'Увімкнено MIRROR: зайві файли в Destination буде видалено.'
    }

    Write-Host "Джерело:      $sourcePath"
    Write-Host "Призначення: $destinationPath"
    Write-Host "Лог:          $logFile"

    & robocopy @robocopyArgs
    $robocopyExitCode = $LASTEXITCODE

    # Для Robocopy коди 0-7 не означають фатальну помилку.
    if ($robocopyExitCode -ge 8) {
        throw "Robocopy завершився з помилкою. Код: $robocopyExitCode. Перевірте лог: $logFile"
    }

    Write-Host "Готово. Код Robocopy: $robocopyExitCode" -ForegroundColor Green
    exit 0
}
catch {
    Write-Error $_.Exception.Message
    exit 1
}

Як запускати

Відкрийте Windows PowerShell або сучасний PowerShell 7, перейдіть до папки зі скриптом і спочатку запустіть перевірку. Вона сформує лог і покаже, що саме потрібно скопіювати, але не змінить файли.

cd C:\Scripts
.\Copy-Folder.ps1 -Source "C:\Users\ivan\Documents" -Destination "D:\Backup\Documents" -DryRun

Якщо результат правильний – повторіть команду без -DryRun:

.\Copy-Folder.ps1 -Source "C:\Users\ivan\Documents" -Destination "D:\Backup\Documents"

Для мережевої папки використовується UNC-шлях. Користувач, від якого запускається скрипт, має вже мати доступ до цієї шари:

.\Copy-Folder.ps1 -Source "C:\Work" -Destination "\\nas01\backup\Work"

Дзеркальна синхронізація

Якщо потрібно, щоб призначення стало точною копією джерела, додайте -Mirror. Це еквівалент параметра /MIR у Robocopy: він не лише додає та оновлює файли, а й видаляє зайве у папці призначення.

.\Copy-Folder.ps1 -Source "C:\Project" -Destination "D:\Mirror\Project" -Mirror -DryRun

Після перевірки заберіть -DryRun. Для домашніх документів або єдиного резервного диска зазвичай краще не використовувати дзеркало: резервна копія має захищати і від випадкового видалення в оригіналі.

Що з Execution Policy

Не потрібно вимикати захист PowerShell для всього комп’ютера. Спочатку подивіться, які політики діють:

Get-ExecutionPolicy -List

Для власного локального скрипта зручний обережний варіант на одну сесію:

Set-ExecutionPolicy -Scope Process -ExecutionPolicy RemoteSigned

Налаштування Process зникає після закриття вікна PowerShell. Якщо файл був завантажений з інтернету, але ви його перевірили й довіряєте йому, зніміть позначку завантаження окремою командою:

Unblock-File -LiteralPath .\Copy-Folder.ps1

Copy-Item: коли його достатньо

Для одного файлу або разової простої операції стандартного cmdlet цілком вистачає. Він підтримує -WhatIf, тобто режим без виконання.

Copy-Item -LiteralPath "C:\Temp\report.pdf" -Destination "D:\Archive\report.pdf" -Force -WhatIf

Для регулярного копіювання великих каталогів, мережевих шляхів і логування краще лишитися з варіантом на Robocopy вище. Він штатно є у Windows 10/11 і повертає коди 0-7 навіть у випадках, які не є фатальними помилками; скрипт враховує це правильно.

Практичні правила

  • Спочатку завжди використовуйте -DryRun.
  • Не зберігайте пароль до NAS або Windows-шари прямо у .ps1.
  • Зберігайте логи: вони відповідають на запитання «що реально скопіювалося?».
  • Перевіряйте відновлення файлів, а не лише факт виконання резервної копії.
  • Для автоматичного запуску за розкладом використовуйте Планувальник завдань Windows від окремого облікового запису з мінімально потрібними правами.

Офіційна документація