Рабоче-крестьянское резервное копирование под Windows
Постановка задачи
Допустим, вы системный администратор в малой/средней (да чего уж там, иногда даже большой) компании, перед которым стоит задача организации резервного копирования файлового сервера исключительно с помощью подручных средств. Или вы продвинутый пользователь, которому небезразлична судьба хранящихся на вашем жестком диске файлов.
Пусть задача будет звучать следующим образом:
- Необходимо организовать автоматическое резервное копирование определенных файлов компьютера на отдельный носитель.
- Компьютер работает под управлением Windows версий 7 / 2008 или более поздней.
- Объем данных большой, поэтому копирование должно поддерживаться как полное, так и дифференциальное.
- Д.б. возможность копировать любые файлы, в т.ч. системные, заблокированные на чтение и т. п.
- Сторонним платным софтом пользоваться категорически не хочется (ну, допустим, мы стеснены в средствах, а эти ваши торренты — не наш путь! Или религия не позволяет. Или миллион других причин.), а лучше вообще обойтись без любого стороннего софта, пользуясь лишь возможностями ОС .
Немного подумав, еще расширим список хотелок:
- Как продолжение предыдущего пункта, формат архива также должен быть открытым и распространенным, чтобы в случае чего его без проблем открыть откуда угодно с помощью чего-угодно
- Более того, он должен быть таким, чтобы из любого, даже дифференциального архива, можно было бы без труда вытащить любой файл, не распаковывая для этого весь архив.
- Глубина архивации должна настраиваться (что называется, backup rotate).
- Было бы неплохо также с архивом сохранять дескрипторы безопасности NTFS.
- И вообще, хочется максимальной расширяемости и настраиваемости, если завтра возникнет желание нагородить дополнительный функционал.
Что ж, требования сформулированы, дело за малым – спланировать и реализовать всё остальное.
Выбор средств
Поскольку сторонние средства мы решили отметать, то как такового выбора технологий у нас почти не остается:
- Алгоритмический язык и командлеты – Powershell (хотя тут при желании можно и VBScript)
- Доступ к файлам через службу теневого копирования тома (VSS)
- Формат архива – Zip
- Механизм копирования только измененных файлов – архивный бит файловой системы
- Автоматизация запусков – встроенный планировщик
- ПО архивации – ?
Несморя на то, что через пользовательский интерфейс Windows создать «Сжатую ZIP-папку» проще простого, в результате поиска встроенного аналога командной строки меня постиг облом №1. Для реализации, собственно, функции архивирования для оригинальной Windows из коробки, к сожалению, так или иначе требовалась либо доустановка NET Framework, либо сторонних командлетов Powershell, либо файлов из Resource Kit, либо чего-то еще.
Опробовав ряд вышеперечисленных вариантов меня постиг облом №2: на больших объемах архивируемых данных (начиная от пары сотен гигабайт) одни попросту вылетали, другие съедали всю память и начинали грузить сервер, третьи еще каким-то образом начинали чудить.
Глубоко вздохнув, приходится делать шаг в сторону от одного из вышеозначенных принципов и взять на роль архиватора готовое решение. С точки зрения опенсорсности и бесплатности опять же практически безальтернативно выбор падает на:
- ПО архивации – 7-Zip
План алгоритма
Итак, алгоритм предельно прост: пишем сценарий на Powershell, который должен:
- Исходя из переданных параметров, создать теневую копию интересуемого тома.
- Получить к ней доступ из операционной системы.
- В случае дифференциальной/инкрементной резервной копии составить список измененных файлов.
- Заархивировать нужные файлы.
- По возможности заархивировать NTFS ACL
- Удалить теневую копию.
- В случае создания полной/инкрементной резервной копии – сбросить с файлов архивный бит.
- В случае создания полной копии удалить старые бэкапы (старше заданной глубины архивации).
Скрипт
Ходить вокруг да около тут незачем, сразу публикую готовый скрипт:
Итоговый скрипт backup-files.ps1
################################################################################################## # Скрипт резервного копирования данных v0.9b # 25-12-2014 # Accel ################################################################################################## # #Поддерживаются полные и дифференциальные копии (на основе архивного атрибута файлов) # #Системные требования: # Windows 7+, 2008+ # Установленный архиватор 7-Zip (тестировалось на версии 9.30b) # #За один запуск скрипта возможно резервное копирование лишь с одного диска # #NTFS-полномочия на данный момент не сохраняются (определяется возможностями архиватора) # #Скрипт должен запускаться от пользователя, имеющего доступ к архивируемым файлам (с правами SYSTEM, Backup Operator и т.п.) $ErrorActionPreference="SilentlyContinue" Stop-Transcript | out-null $ErrorActionPreference = "Continue" ################################################################################################## #Начало блока переменных ################################################################################################## #Название задания архивирования #Используется в именовании архива и ссылки на теневую копию #Должно отвечать правилам именования файлов, наличие пробелов не рекомендуется, т.к. не тестировалось #Пример: $ArchiveTaskName="DiskE" $ArchiveTaskName="DiskE" #Путь до диска-источника резервного копирования #Перечень целевых папок этого диска определяется отдельно #Пример: $SrcDrivePath="D:\" $SrcDrivePath="D:\" #Путь до целевого диска #Пример: $BackupDrivePath="E:\" $BackupDrivePath="E:\" #Полный путь до файла со списком папок для архивирования на диске-источнике #Пример: $SubfoldersToBackupFile = "E:\Backup\src_dirs.txt" # * Каждая строка данного файла должна содержать одну папку, которую мы хотим включить в архив # * Путь д.б. относительным, т.е. не содержать буквы диска. # * Иными словами, если одна из папок резервного копирования у нас D:\Files\FilesToBackup, то в файле должна быть строка Files\FilesToBackup # * Кодировка - ANSI $SubfoldersToBackupFile = "E:\Backup\src_dirs.txt" #Путь до временного файла-списка файлов для архивации: #Пример: $BackupFilesList = "E:\Backup\backup-filelist.txt" $BackupFilesList = "E:\Backup\backup-filelist.txt" #Путь до целевой папки с архивами (В ней не должно быть никаких других файлов, иначе rotation их может удалить! Также лучше не использовать корень диска, а создать хоть одну подпапку.) #Пример: $ArchiveDstPath = $BackupDrivePath+"Backup\Script backup" $ArchiveDstPath = $BackupDrivePath+"Backup\Script backup" #Полный путь до файла журнала задания #Пример: $ScriptLogFile = "E:\Backup\BackupFiles.log" $ScriptLogFile = "E:\Backup\BackupFiles.log" #Путь до исполняемого файла архиватора 7-Zip #Пример: $SevenZipExecutablePath = "C:\Program files\7-Zip\7z.exe" $SevenZipExecutablePath = "C:\Program files\7-Zip\7z.exe" #Количество дней хранения архива (отсчет ведется с последнего полного бэкапа) #Пример: $BackupRotationIntervalDays=22 $BackupRotationIntervalDays=22 ################################################################################################## #Конец блока переменных ################################################################################################## $BackupFilesListTmp = $BackupFilesList+".tmp" $backuptype=$args[0] $VSCPath = $BackupDrivePath+"VSC_"+$ArchiveTaskName+"_$(Get-Date -format "yyyyMMdd")" Start-Transcript -path $ScriptLogFile $LogVars=1 if ($LogVars=1) { echo "=================================================================" echo "ArchiveTaskName: $ArchiveTaskName" echo "SrcDrivePath: $SrcDrivePath" echo "BackupDrivePath: $BackupDrivePath" echo "SubfoldersToBackupFile: $SubfoldersToBackupFile" echo "BackupFilesList: $BackupFilesList" echo "ArchiveDstPath: $ArchiveDstPath" echo "ScriptLogFile: $ScriptLogFile" echo "SevenZipExecutablePath: $SevenZipExecutablePath" echo "VSCPath: $VSCPath" echo "BackupRotationIntervalDays: $BackupRotationIntervalDays" echo "=================================================================" } echo "Backup started at: $(Get-Date)" function BackupFull { echo "Backup type: full" #Создаем теневую копию $s1 = (gwmi -List Win32_ShadowCopy).Create($SrcDrivePath, "ClientAccessible") $s2 = gwmi Win32_ShadowCopy | ? { $_.ID -eq $s1.ShadowID } $d = $s2.DeviceObject + "\" #Создаем на нее ярлык (удалим предыдущий, если остался после прерванной архивации) CMD /C rmdir "$VSCPath" cmd /c mklink /d $VSCPath "$d" #Составляем список папок для архивации "" | Set-Content $BackupFilesList Get-Content $SubfoldersToBackupFile | Foreach-Object {CMD /C "echo $VSCPath\$_\* >> $BackupFilesList" } #Создаем массив параметров для 7-Zip $Arg1="a" $Arg2=$ArchiveDstPath+"\"+$ArchiveTaskName+"_$(Get-Date -format "yyyy-MM-dd")_`(Full`).zip" $Arg3="-i@"+$BackupFilesList $Arg4="-w"+$ArchiveDstPath $Arg5="-mx=3" $Arg6="-mmt=on" $Arg7="-ssw" $Arg8="-scsUTF-8" $Arg9="-spf" #Зипуем & $SevenZipExecutablePath ($Arg1,$Arg2,$Arg3,$Arg4,$Arg5,$Arg6,$Arg7,$Arg8,$Arg9) Remove-Item $BackupFilesList #Если теневые копии имеют необъяснимую тенденцию копиться, лучше удалим их все #CMD /C "vssadmin delete shadows /All /Quiet" #Или можно удалить только конкретную созданную в рамках данного бекапа "vssadmin delete shadows /Shadow=""$($s2.ID.ToLower())"" /Quiet" | iex #Удаляем ярлык CMD /C rmdir $VSCPath #Снимаем архивный бит Get-Content $SubfoldersToBackupFile | Foreach-Object {CMD /C "attrib -A -H -S $SrcDrivePath$_\* /S /L" } #делаем rotation echo "Rotating old files..." CMD /C "forfiles /D -$BackupRotationIntervalDays /S /P ""$ArchiveDstPath"" /C ""CMD /C del @file""" } function BackupDiff { echo "Backup type: differential" #Создаем теневую копию $s1 = (gwmi -List Win32_ShadowCopy).Create($SrcDrivePath, "ClientAccessible") $s2 = gwmi Win32_ShadowCopy | ? { $_.ID -eq $s1.ShadowID } $d = $s2.DeviceObject + "\" #Создаем на нее ярлык (удалим предыдущий, если остался после прерванной архивации) CMD /C rmdir $VSCPath cmd /c mklink /d $VSCPath "$d" #Включаем UTF-8 CMD /C "chcp 65001 > nul" #Составляем список файлов, измененных с момента предыдущей архивации "" | Set-Content $BackupFilesList Get-Content $SubfoldersToBackupFile | Foreach-Object {CMD /C "dir $VSCPath\$_ /B /S /A:A >> $BackupFilesList" } CMD /C "chcp 866 > nul" $SearchPattern="^"+$BackupDrivePath.Substring(0,1)+"\:\\" #Отрезаем букву диска, иначе 7-zip при архивации по списочному файлу глючит, находя несуществующие дубли #(Get-Content $BackupFilesList) -replace $SearchPattern,'' > $BackupFilesListTmp Get-Content $BackupFilesList | ForEach-Object { $_ -replace $SearchPattern,"" } | Set-Content ($BackupFilesListTmp) Remove-Item $BackupFilesList Rename-Item $BackupFilesListTmp $BackupFilesList #Поскольку имя диска в путях удалили, нужно перейти в нужную директорию cd $BackupDrivePath #Создаем массив параметров для 7-Zip $Arg1="a" $Arg2=$ArchiveDstPath+"\"+$ArchiveTaskName+"_$(Get-Date -format "yyyy-MM-dd")_`(Diff`).zip" $Arg3="-i@"+$BackupFilesList $Arg4="-w"+$ArchiveDstPath $Arg5="-mx=3" $Arg6="-mmt=on" $Arg7="-ssw" $Arg8="-scsUTF-8" $Arg9="-spf" #Зипуем & $SevenZipExecutablePath ($Arg1,$Arg2,$Arg3,$Arg4,$Arg5,$Arg6,$Arg7,$Arg8,$Arg9) Remove-Item $BackupFilesList #Если теневые копии имеют необъяснимую тенденцию копиться, лучше удалим их все #CMD /C "vssadmin delete shadows /All /Quiet" #Или можно удалить только конкретную созданную в рамках данного бекапа "vssadmin delete shadows /Shadow=""$($s2.ID.ToLower())"" /Quiet" | iex #Удаляем ярлык CMD /C rmdir $VSCPath } if ($backuptype -eq "diff") { BackupDiff | Out-Host } elseif ($backuptype -eq "full") { BackupFull | Out-Host } else { echo $backuptype echo "None backup type parameter passed! Usage: scriptname.ps1 [ full | diff ]" } echo "Backup finished at: $(Get-Date)" Stop-Transcript
Для работы скрипта мы должны заполнить выделенный в начале файла блок переменных (пути архивации, глубина хранения архивов) и создать файл с папками-источниками, например, такой:
Файл папок-источников src_dirs.txt
bases files photos users/profiles
Использование
Складываем всё в одну папку, и всё, можно запускать в Powershell. Единственный параметр — тип запуска [full | diff], определяет полный/дифференциальный способ копирования.
PS E:\Backup> .\backup-files.ps1 full
Убедившись, что всё работает (или поправив параметры, если нет) создаём задание в планировщике (отдельно для полного копирования и отдельно для дифференциального).
Экспортированный пример задания: scheduled-backup-task-full.xml
<?xml version="1.0" encoding="UTF-16"?> <Task version="1.4" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task"> <RegistrationInfo> <Date>2014-10-14T23:14:45.0256428</Date> <Author>PC\Accel</Author> </RegistrationInfo> <Triggers /> <Principals> <Principal id="Author"> <UserId>PC\User</UserId> <LogonType>S4U</LogonType> <RunLevel>HighestAvailable</RunLevel> </Principal> </Principals> <Settings> <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy> <DisallowStartIfOnBatteries>true</DisallowStartIfOnBatteries> <StopIfGoingOnBatteries>true</StopIfGoingOnBatteries> <AllowHardTerminate>true</AllowHardTerminate> <StartWhenAvailable>true</StartWhenAvailable> <RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable> <IdleSettings> <StopOnIdleEnd>true</StopOnIdleEnd> <RestartOnIdle>false</RestartOnIdle> </IdleSettings> <AllowStartOnDemand>true</AllowStartOnDemand> <Enabled>true</Enabled> <Hidden>false</Hidden> <RunOnlyIfIdle>false</RunOnlyIfIdle> <DisallowStartOnRemoteAppSession>false</DisallowStartOnRemoteAppSession> <UseUnifiedSchedulingEngine>false</UseUnifiedSchedulingEngine> <WakeToRun>true</WakeToRun> <ExecutionTimeLimit>P3D</ExecutionTimeLimit> <Priority>7</Priority> </Settings> <Actions Context="Author"> <Exec> <Command>C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe</Command> <Arguments>-Command "& {E:\Backup\backup-files.ps1 full}"</Arguments> </Exec> </Actions> </Task>
Ну вот, в целом, и всё, можно настроить, представить папку с архивами системе мониторинга наличия резервных копий (если есть) и забыть.
Замечания
- Для корректной работы (в первую очередь для создания теневой копии) powershell должен быть запущен от имени администратора (или «с наивысшими привилегиями» в терминологии планировщика заданий).
- По идее способ также годится для горячего консистентного резервного копирования хоть баз MSSQL, хоть MS Exchange (при установке соотв. shadow copy providers, которые к подобному софту идут в комплекте), хотя конкретно в этих случаях удобнее пользоваться встроенными средствами.
- Инкрементным резервным копированием я не пользуюсь (много хлопот найти удаленный файл среди кучи инкрементных архивов), но если возникнет потребность, то он получается буквально комбинированием нескольких срочек из полного и дифференциального блоков скрипта.
- Здесь также не реализован механизм сохранения ACL (формат zip, да и 7zip не поддерживают хранение дескрипторов безопасности в архиве; RAR умеет, но это уже не свободное ПО, что сильно противоречит условиям задачи). В случае необходимости дескрипторы можно сохранять в файл встроенными утилитами типа icacls и добавлять полученный дамп в создаваемый архив.
- Увы и ах, алгоритм не подходит для XP/2003. Сложность возникает на моменте создания ярлыка на теневую копию (в этих ОС нет утилиты mklink, а по-быстрому обойти эти грабли у меня не вышло).
P.S.
Перед необходимостью изобретать свой велосипед автор за несколько лет намучался перепробовал большое количество разнообразного бесплатного готового ПО со похожей функциональностью (Cobian Backup, COMODO Backup и др.). Вдоволь находившись по разнообразным встроенным в упомянутый софт граблям, было принято решение написать что-то своё. На данный момент описанное решение успешно работает на серверах (Windows Server 2008 R2) и рабочих станциях (Windows 7 и Windows 8.1).
Самая крупная создаваемая полная резервная копия на данный момент составляет 1 Тб в исходных файлах, в архиве – 350 Гб. При архивировании с зеркала SAS (7200) на такой же локальный Volume-диск (оба работают внутри vSphere, будучи подключенными как RDM Passthrough-диски) операция занимает около 6 часов, что в условиях задачи является вполне приемлемым результатом.