Постановка задачи
Допустим, вы системный администратор в малой/средней (да чего уж там, иногда даже большой) компании, перед которым стоит задача организации резервного копирования файлового сервера исключительно с помощью подручных средств. Или вы продвинутый пользователь, которому небезразлична судьба хранящихся на вашем жестком диске файлов.
Пусть задача будет звучать следующим образом:
- Необходимо организовать автоматическое резервное копирование определенных файлов компьютера на отдельный носитель.
- Компьютер работает под управлением 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
- Удалить теневую копию.
- В случае создания полной/инкрементной резервной копии – сбросить с файлов архивный бит.
- В случае создания полной копии удалить старые бэкапы (старше заданной глубины архивации).
Скрипт
Ходить вокруг да около тут незачем, сразу публикую готовый скрипт:
##################################################################################################
# Скрипт резервного копирования данных 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:Backupsrc_dirs.txt"
# * Каждая строка данного файла должна содержать одну папку, которую мы хотим включить в архив
# * Путь д.б. относительным, т.е. не содержать буквы диска.
# * Иными словами, если одна из папок резервного копирования у нас D:FilesFilesToBackup, то в файле должна быть строка FilesFilesToBackup
# * Кодировка - ANSI
$SubfoldersToBackupFile = "E:Backupsrc_dirs.txt"
#Путь до временного файла-списка файлов для архивации:
#Пример: $BackupFilesList = "E:Backupbackup-filelist.txt"
$BackupFilesList = "E:Backupbackup-filelist.txt"
#Путь до целевой папки с архивами (В ней не должно быть никаких других файлов, иначе rotation их может удалить! Также лучше не использовать корень диска, а создать хоть одну подпапку.)
#Пример: $ArchiveDstPath = $BackupDrivePath+"BackupScript backup"
$ArchiveDstPath = $BackupDrivePath+"BackupScript backup"
#Полный путь до файла журнала задания
#Пример: $ScriptLogFile = "E:BackupBackupFiles.log"
$ScriptLogFile = "E:BackupBackupFiles.log"
#Путь до исполняемого файла архиватора 7-Zip
#Пример: $SevenZipExecutablePath = "C:Program files7-Zip7z.exe"
$SevenZipExecutablePath = "C:Program files7-Zip7z.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
Для работы скрипта мы должны заполнить выделенный в начале файла блок переменных (пути архивации, глубина хранения архивов) и создать файл с папками-источниками, например, такой:
bases
files
photos
users/profiles
Использование
Складываем всё в одну папку, и всё, можно запускать в Powershell. Единственный параметр — тип запуска [full | diff], определяет полный/дифференциальный способ копирования.
PS E:Backup> .backup-files.ps1 full
Убедившись, что всё работает (или поправив параметры, если нет) создаём задание в планировщике (отдельно для полного копирования и отдельно для дифференциального).
<?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>PCAccel</Author>
</RegistrationInfo>
<Triggers />
<Principals>
<Principal id="Author">
<UserId>PCUser</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:WindowsSystem32WindowsPowerShellv1.0powershell.exe</Command>
<Arguments>-Command "& {E:Backupbackup-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 часов, что в условиях задачи является вполне приемлемым результатом.
Автор: Aclz
Здорово что приведен листинг кода, но мне кажется в нем нет обратных слешей совсем, к прмеру все пути съехали? можно ли ссылку на архив? сам в любом случае буду разбираться т.к. хочу у себя внедрить
Спасибо за классный скрипт! Но считаю неплохо было бы доработать, добавить список исключений.