Эта заметка является продолжением и развитием проекта, описанного в статье «Резервное копирование без лишних затрат»:
Тяжелой поступью по стране идет импортозамещение, хочешь или не хочешь, но с Windows платформы приходится уходить. Расскажу про один из таких шагов.
Уже очень давно у меня существовала система резервного копирования файлов, построенная на архиваторе 7-zip и обертки из js и bat файлов, которая позволила довольно простым и удобным (для меня) способом делать как полные, так и разностные архивы.
С внедрением серверов на платформе Linux, старую систему эксплуатировать, по понятным причинам, уже не получится. Возникла задача перенести существующий функционал под Linux.
Сначала я рассматривал возможность переделать все скрипты с помощью bash, но сервера на Windows пока совсем не исчезли, некоторые задачи на них «крутятся» и поддерживать две системы резервного копирования не самый хороший способ облегчить себе жизнь. Нужно создать универсальную систему, которая будет работать на обеих платформах.
После недолгих размышлений в качестве основы для разработки был взят язык Python. С его помощью можно сделать универсальную систему, которая будет одинаково функционировать на любой платформе, которая у меня используется.
Сразу отвечу на вопрос: почему не взять любую готовую систему резервного копирования и не изобретать велосипед?
Во первых, хорошая система резервного копирования – обычно платная. Я работаю в бюджетной организации, денег на это просто нет.
Во вторых, мне хотелось бы иметь полный контроль над резервной копией, чтобы архив был в понятном формате, чтобы в любой момент я мог в него попасть и достать любой нужный файл.
В третьих, у меня есть внутренний «самописный» программный комплекс (ссылка), который интегрирован с существующей системой резервного копирования - для получения статистики, формирования отчетности и т.д.
И в четвертых, мне просто интересно это написать самому.
Какие задачи должна решать моя система резервного копирования:
Создание полной архивной резервной копии какого-либо ресурса, с указанием каталогов, масок файлов и т.п.
Создание разностных архивных резервных копий для ресурсов, объем которых слишком велик для ежедневных полных копий.
Автоматическое удаление старых копий по истечению срока их хранения, который задается вручную.
Копирование уже подготовленных файлов, без процедуры их архивации, с автоматическим удалением старых файлов по истечении срока их хранения.
Формирование отчетности о выполнении резервного копирования в виде электронных писем, логов, CSV файлов.
Простота настройки резервной копии - желательно через очень простые конфигурационные файлы, которые можно править в любом текстовом редакторе.
Минимальный набор программного обеспечения необходимого для установки. Это касается как архиватора, так и среды исполнения.
Повторюсь, в качестве архиватора выступает программа 7-zip, она имеет функционал для создания как полных, так и разностных архивов, в которые попадают только изменения. Архиватор кроссплатформенный, существует в редакциях под Windows и Linux. В качестве основной Linux платформы мы выбрали Альт-Линукс (честно купив лицензии на сервера). В составе Альт-Линукс уже присутствует Python и архиватор 7-zip.
Но нужно помнить, и что для меня оказалось небольшим сюрпризом, что 7-zip в Альте существует в нескольких версиях. Если просто введем в консоли 7z, то запустится консольная версия 2017 года — 17.05. Более свежая версия 2023 года — 23.01, запускается по команде 7zz. Собственно как и с Python, командой python2 запускается вторая версия, python3 уже третья.
При создании системы я постарался использовать только те библиотеки, которые по умолчанию установлены в сборке Python. Никто не мешает доустановить то, что нужно, но сталкивался с ситуацией, когда сторонние библиотеки от версии к версии меняли свой интерфейс и функциональность, что разрушало работающий код на моей стороне. Чем меньше тащишь с собой, тем легче и надежнее.
Опишу общими словами систему, а сам код будет расположен в конце. Если кто‑то захочет использовать в своей работе, то берите на здоровье.
Вся система резервного копирования размещается в нескольких файлах.
Есть каталог «backup‑script», который содержит подкаталоги log, ini, ls. Также в нем располагается сам скрипт резервного копирования «backup_copy.py».
Подкаталоги:
log — служит для записи логов работы скрипта. В системе я собираю два вида логов. Первый создается внутри Python программы, на определенные действия происходит запись события с метками времени, в заранее описанный в настройках файл. Второй формируется при запуске на исполнение Python программы и помогает отследить сообщения об ошибках и сбоях в момент работы. В него перенаправляется консольный вывод команды.
ini — в нем хранятся INI файлы с настройками конкретных резервных копий, о них расскажу далее.
ls — здесь хранятся файлы описаний того, что именно нужно архивировать, для архиватора 7-zip.
Также в каталоге «backup-script» хранятся «sh» скрипты для запуска конкретной процедуры резервного копирования.
Например, у меня есть сетевой ресурс, с которого необходимо снять резервную копию, какой-то сетевой каталог «\\server\users». Размер каталога без сжатия, допустим, порядка терабайта. Если мне нужно хранить копии данных за пару недель, то можно каждую ночь, снимать полную копию и в итоге мне потребуется более 14 терабайт свободного места под резервные копии, что многовато.
Я знаю, что пользователи за сутки могут поменять от 1 до 10 гигабайт данных. Тогда, если полную резервную копию делать в ночь с субботы на воскресенье, а в остальные дни запускать только разностное ночное копирование, то получится что общий объем копий за две недели составит 1Тб+6*0,01Тб+1Тб+6*0.01Тб = около 2.2 Тб, а это уже влезет даже на один 3Тб, диск.
Поэтому делаем в расписании две задачи. Одну для полного резервного копирования, вторую для разностного. Я реализую это двумя «sh» скриптами (под Windows это делается «bat» файлами).
В качестве примера скрипт для запуска полного копирования «full-users.sh»:
#!/bin/bash
# Путь к папке скриптов.
spath=/mnt/backup-script
# Монтирование сетевых дисков
/sbin/mount.cifs -o username=backup_user,password=пароль,vers=1.0 //server/users /mnt/sdd/backup/netusers
# Старт полного резервного копирования users.
echo $(date) +++++++++++++++++++++++++++ >> "$spath"/log/cron-full-users.log
python3 "$spath"/backup_copy.py "$spath"/ini/users-full.ini >> "$spath"/log/cron-full-users.log 2>&1
# Отключаем сетевые ресурсы
umount /mnt/sdd/backup/netusers
Чтобы обратиться к сетевому каталогу «\\server\users» монтирую его в папку, которая располагается рядом с местом, где будет лежать его резервная копия. В дальнейшем обращаюсь к сетевому ресурсу по локальному пути, что упрощает взаимодействие. На сервере «server» заведен пользователь «backup_user», которому на чтение дан доступ к каталогу «\\server\users». Именно под этим пользователем читаются файлы. Таким способом сервер, с которого берутся файлы, не знает, где будет храниться резервная копия. Движение данных одностороннее, с исходного сервера на резервный. Сервер резервного копирования может располагаться вне домена. Если будет компрометация доменных учетных записей, то злоумышленник или вирусный агент не попадет на сервер резервной копии.
Запускается Python приложение «backup_copy.py» с параметром в виде INI файла, описывающего характеристики резервного копирования.
Логирование системной информации ведется с помощью перенаправления потока вывода в текстовый файл, который сохраняется в каталоге «log». К имени такого лог файла добавляю слово «cron-» и тогда логи формируемые программой и потоком можно легко отличить и они располагаются группой при сортировке.
После окончания работы программы создания резервной копии, отключаем смонтированный ранее сетевой ресурс с исходными файлами.
Есть, конечно, минус такой реализации, пароль к сетевому ресурсу хранится в открытом виде внутри скрипта, но это можно решить уровнями доступа к файлу.
Рассмотрим, что же хранится в файлах настроек.
Для других своих проектов на C++, в качестве настроечных, использую INI файлы. В Windows это является стандартом, поддерживаемым самой операционной системой еще с первых версий, хоть и уже устаревшим. Для Python есть стандартная библиотека «configparser» (ссылка). Она реализует обработку INI файлов, со своими полезными дополнениями.
Моя логика в организации настроек следующая: есть некое задание, которое должно быть запущено по расписанию в определенное время, оно должно создать один или несколько архивов или произвести копирование уже имеющихся файлов.
Например, мне нужно создать архив файлов из каталога А, затем архив файлов из каталога Б, и затем скопировать файлы из каталога Д в каталог Е. Эти действия исполняются последовательно. В данном примере существуют три единицы резервирования. Почему бы не поместить их описание в один настроечный файл, в нем сделав несколько разделов с описаниями резервных копий. И добавить еще раздел с описанием общих настроек для каждой резервной копии.
Общие настройки назовем «SETTINGS». Типичный блок с ними выглядит следующим образом (для вышеописанного примера с каталогом «\\server\users»):
[SETTINGS]
file_log = /mnt/backup-script/log/pack-full-users.log
archive = 7zz
subject = Полная копия Users
mail_to = backup_info@mymail.ru
mail_from = backup@mymail.ru
file_log_total_stat = /mnt/out_net_log/total_stat.log
file_log_total_mail = /mnt/out_net_log/total_mail.log
Параметры в INI файле задаются в формате: ИМЯ_ПАРАМЕТРА = ЗНАЧЕНИЕ
Для моих нужд достаточно следующих настроек:
file_log = Путь и имя детального лог файла, в котором сохранятся шаги архивации, формируемые python процедурой;
archive = Путь и имя используемого архиватора.
Для Windows это может выглядеть как: archive = "C:\Progra~1\7-Zip\7z.exe"
Для Linux: archive = 7zz
Пробелы в пути к архиватору не используются, почему – отвечу несколько позже.
subject = Тема письма для высылаемого лога резервного копирования;
mail_to = Адресат, кому будет отправлено письмо с логом резервного копирования. Если этот параметр не указан, то письмо отправляться не будет;
mail_from = От кого будет отправлено письмо с логом резервного копирования. Если этот параметр не указан, то письмо отправляться не будет.
file_log_total_stat = Путь и имя лог файла, в который записываются данные о созданной резервной копии в виде значений разделенных точкой с запятой. Эти данные впоследствии автоматически загружаются в базу данных для статистики. Если параметра нет, то запись осуществляться не будет.
file_log_total_mail = Путь и имя файла обобщенного лога, который накапливает информацию о всех произведенных резервных копиях за сутки и затем отправляется администратору, после чего файл удаляется. Если параметра нет, то запись осуществляться не будет.
Первые три параметра являются обязательными, остальные нет.
Теперь настройки для самих резервных копий:
[users]
path_to = /mnt/sde/backup/users
ls_file = /mnt/backup-script/ls/users.ls
kol_copy = 2
type_copy = full
path_to = путь к каталогу, в котором будут сохраняться архивы с резервными копиями.
ls_file = Путь и имя ls файла, который определяет каталоги для резервного копирования. Он используется архиватором 7-zip при поиске файлов для архивации.
Содержимое файла users.ls:
/mnt/sdd/backup/netusers/БиллГилберт/*
/mnt/sdd/backup/netusers/Пажитнов/*
/mnt/sdd/backup/netusers/Ромеро/*
В таком файле можно более подробно указать, что именно нужно забирать в архив, используя маски для отбора, как каталогов, так и файлов.
Резервное копирование может быть как полным, так и разностным, т. е. будет происходить сравнение изменений с полным архивом и в разностную копию попадут только новые или измененные файлы.
type_copy = Вид создаваемой резервной копии. Может принимать значение «full», «diff» или «copy».
full — Создается полная копия для объекта архивирования, по шаблону: ИМЯ_ОБЪЕКТА‑YYYY‑MM‑DD‑full.7z, где YYYY‑MM‑DD дата формирования полной копии.
diff — Создается разностная копия на текущую дату, архив формируется как отличие от полной копии с последней датой. Если полного архива нет, разностная не создается.
Формируется архивный файл по шаблону ИМЯ_ОБЪЕКТА‑YYYY‑MM‑DD‑yyyy‑mm‑dd.7z, где YYYY‑MM‑DD дата формирования полной копии, yyyy‑mm‑dd дата формирования разностной копии.
copy — Файлы копируются без архивации. Об этом режиме чуть позже.
kol_copy = Число сохраняемых полных копий. После каждого процесса создания полной копии запускается автоматическое удаление прошлых копий. Если, например, задан параметр равный 2, то будут сохраняться только две последние по дате полные копии, а все другие старшие копии и их разностные составляющие, будут удалены.
Таких блоков, описывающих одну резервную копию, в INI файле может быть несколько, исполняться они будут последовательно, в том порядке как расположены в файле.
Если нам нужно сделать разностную резервную копию, то настройки будут точно такими же, но параметр «type_copy» будет равен «diff». И конечно, разностную копию нужно по расписанию запускать отдельно от создания полной.
По окончании резервного копирования по пути указанному в параметре «path_to» окажется сформированный архив, который будет включать в себя данные описанные в файле указанном в параметре «ls_file».
Также будет сформирована соответствующая запись, как минимум, в двух лог файлах.
Например, вот такого вида запись окажется в файле указанном в параметре «file_log»:
2025-07-21 22:30:01.910786 : Начало архивации: users
2025-07-21 22:35:41.993847 : Результат архивации отличен от 0.
2025-07-21 22:35:41.998940 : Результат архивации, код: -1
2025-07-21 22:35:41.999015 : Создан архив: [/mnt/sdd/backup/users/users-2025-07-18-2025-07-21.7z], размер 597221737 байт (0.56 Gb)
2025-07-21 22:35:41.999089 : Свободного места на диске [/mnt/sdd/backup/users]: 1857.49 Gb)
2025-07-21 22:42:34.150954 : Окончание работы скрипта. Заняло времени: 0:12:32.245048
Эта же информация будет выслана на почту, если указать соответствующие параметры в разделе «SETTINGS».
В файле с перехваченным консольным выводом, окажется что-то похожее представленному ниже:
Thu Jul 17 22:30:02 +07 2025 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
7-Zip (z) 23.01 (x64) : Copyright (c) 1999-2023 Igor Pavlov : 2023-06-20
64-bit locale=C.UTF-8 Threads:4 OPEN_MAX:16384
Open archive: /mnt/sdd/backup/users/users-2025-07-11-full.7z
--
Path = /mnt/sdd/backup/users/users-2025-07-11-full.7z
Type = 7z
Physical Size = 386770386856
Headers Size = 27286344
Method = Copy 7zAES
Solid = -
Blocks = 712087
Scanning the drive:
95653 folders, 716001 files, 390289939788 bytes (364 GiB)
Creating archive: /mnt/sdd/backup/users/users-2025-07-11-2025-07-17.7z
Add new data to archive: 1451 folders, 4143 files, 4215884672 bytes (4021 MiB)
23 anti-folders, 302 anti-files
Files read from disk: 4139
Archive size: 4216106970 bytes (4021 MiB)
Everything is Ok
Теперь о копировании файлов без архивации. В моем случае это применяется для забора уже сформированных резервных копий на отдельный сервер резервного хранения.
Отличия в настройках заключаются в следующем:
[1c]
type_copy = copy
path_to = /mnt/sdb/backup/1c
path_from = /mnt/sdb/backup/net1c
file_end = .7z
kol_copy = 7
Параметр «type_copy» будет равен «copy».
Поскольку архивация не происходит и листинг файла для архиватора не создается, то появляется новый параметр «path_from», в котором указывается путь к папке где располагаются нужные архивы.
Параметр «kol_copy» в данном варианте уже не содержит число хранимых полных копий, а через него задается количество дней хранения архива. Если задано число 7, то если архив старше 7 дней от дня, когда запустился скрипт, то файл будет удален.
Согласен, что решение с параметром, который себя ведет по-разному в разных случаях не самое лучшее.
И появляется еще один новый параметр «file_end». В нем указывается окончание файла, по которому будет происходить поиск копируемых данных. Это не маска, а именно окончание, т.е. несколько символов справа строки имени файла. В моем случае это используется для отделения полных копий от разностных, по окончанию «–full.7z».
Немножко об особенностях реализации:
Для копирования файлов не используется системная функция. Копирование больших файлов в разных системах это не всегда тривиальная задача. Под Windows приходилось использовать отдельно устанавливаемую утилиту «robocopy.exe», потому что стандартные «copy» и «xcopy» не справлялись. Под Linux есть свои особенности.
Если использовать средства операционной системы, то нужно добавлять в скрипт реализации для разных платформ, что переусложняло программу. Поэтому использую функцию «shutil.copyfileobj» для копирования файла. Работает очень шустро.
Запуск архиватора осуществляется с помощью функции «check_call(exec_str)», где «exec_str» это список из параметров запуска. Т.е. я заранее формирую строку, как если бы вбивал её с консоли, а затем разбиваю её на куски командой «.split(« »)». Получается список параметров. Если в нем все равно внутри будет пробел, то функция выпадает в ошибку. Т.е. нельзя передать первым параметром строку "C:\Program Files\7-Zip\7z.exe", даже используя кавычки внутри. Поэтому для Windows варианта скрипта приходится использовать сокращенное имя «C:\Progra~1\7-Zip\7z.exe».
Ну и собственно сам скрипт (ссылка):
#--------------------------------------------------------------------------------------------------------------
# Универсальный модуль резервного копирования и архивации.
#
# Настройки копирования/архивации передаются в модуль через файл ini в качестве параметра при запуске. Имя файла
# указывается вместе с путем к нему.
# В ini файле должен быть обязательный блок [SETTINGS], в нем указываются следующие параметры:
# file_log - Имя файла для сохранения лога выполняемых действий, указывается вместе с путем.
# archive - Имя архиватора. Например для Linux - 7zz. Для Windows - C:\Progra~1\7-Zip\7z.exe. Путь должен быть
# без пробелов в имени.
# mail_to - Почтовый адрес того, кому будет отправлен лог выполненных действий по окончанию работы.
# Если этот параметр не указан, то письмо отправлено не будет.
# mail_from - Почтовый адрес для вставки в поле "От кого". Если он не указан, то письмо отправлено не будет.
# subject - Текстовая строка с заголовком письма, которое будет отправлено с логом.
# file_log_total_stat - Имя лога для накопительной статистики, указывается вместе с путем. Если оно не указано,
# то запись лога осуществляться не будет.
# file_log_total_mail - Имя лога для накопительной статистики, которая будет отправлена письмом из другой задачи.
# Указывается вместе с путем. Если параметра нет, то запись осуществляться не будет.
#
# Далее в ini файле указывается один или несколько блоков с параметрами единиц копирования/архивации. Где имя блока
# это имя единицы копирования/архивации. Например: [post].
# В каждом блоке возможны следующие параметры:
# type_copy - Может принимать значения copy, full, diff (если явно не задано, то значение по умолчанию copy).
# copy - Осуществление копирования файла.
# full - Полная архивация данных.
# diff - Разностная архивация данных.
# path_from - Место расположения исходных файлов для копирования. При архивации не указывается. Путь берется из ls файла.
# path_to - Место расположения куда копируются файлы или будет расположен архив.
# kol_copy - Если происходит копирование файлов, то файлы отличающиеся от копируемых на более указанного количества
# дней будут удалены.
# Если происходит полная архивация файлов, то будут удалены файлы полных копий, которые превышают по
# количеству указанное число файлов.
# ls_file - Указывается имя ls файла, который необходим архиватору 7-zip для указания более детального описания папок
# для архивации. Файл указывается с путем.
# file_end - Последние несколько символов в имени файла, по которым можно определить название полной копии при архивации
# или понять какие файлы можно удалять при копировании. Например -full.7z для признака полной копии или .7z
# для обычных копий.
##--------------------------------------------------------------------------------------------------------------
import datetime
import os
import sys
import shutil # Библиотека для копирования файлов и информации о диске.
import smtplib # Библиотека для отправки почты.
import configparser # Библиотека обработки ini файлов.
from subprocess import check_call # Библиотека для вызова внешней программы.
from email.mime.multipart import MIMEMultipart
TIME_START = datetime.datetime.now() # Запоминаем время начала архивации.
DATE_TODAY = str(TIME_START)[0:10] # Текущая дата для вставки в названия файлов.
DIFF_COPY = 'diff' # Константа для дифференциального архивирования.
FULL_COPY = 'full' # Константа для полного архивирования.
COPY_COPY = 'copy' # Константа для копирования файла.
#--------------------------------------------------------------------------------------------------------------
# Запись данных в лог. Одновременно формируется строка для отправки на почту.
# Параметры:
# msg_text : данные для записи
# start_string : символы для записи в начале строки перед меткой времени,
# если указать '\n', то лог будет разделен пустой строкой.
#--------------------------------------------------------------------------------------------------------------
def LogOut(msg_text, start_string = ''):
logFile.write(start_string + '\n' + str(datetime.datetime.now()) + ' : ' + msg_text)
global message
message = message + '\n' + str(datetime.datetime.now()) + ' : ' + str(msg_text)
logFile.flush()
return
#--------------------------------------------------------------------------------------------------------------
# Удаляем файлы в каталоге, где date > KolDay
# Параметры:
# PathToDel - Путь к каталогу с удаляемыми файлами.
# KolDay - Количество дней для удаления.
#--------------------------------------------------------------------------------------------------------------
def DelOldFile(PathToDel, KolDay):
try:
os.chdir(PathToDel)
for f in os.listdir('.'):
dateFile = datetime.datetime.fromtimestamp(os.stat(f).st_mtime)
timeLen = (TIME_START - dateFile).days
if timeLen > KolDay:
LogOut('\tУдаляем файл [' + f + '] после: ' + str(timeLen) + ' дней')
os.remove(f)
except:
LogOut('ERROR: Не найден путь [' + PathToDel + '] Удаление файла отменено!')
return
#--------------------------------------------------------------------------------------------------------------
# Быстрое копирование файла.
# Параметры:
# src - Исходный файл
# dst - Файл назначения
# param buffer_size - Размер буфера для чтения, по умолчанию 1 mb
#--------------------------------------------------------------------------------------------------------------
def copyFile(src, dst, buffer_size=1024*1024):
print('Копирование ' + src + ' в ' + dst)
buffer_size = min(buffer_size, os.path.getsize(src))
if(buffer_size == 0):
buffer_size = 1024
with open(src, 'rb') as file_src:
with open(dst, 'wb') as file_dst:
shutil.copyfileobj(file_src, file_dst, buffer_size)
shutil.copystat(src, dst)
return
#--------------------------------------------------------------------------------------------------------------
# Загрузка общих параметров в словарь settings содержащий общие настройки.
# Возвращает словарь.
#--------------------------------------------------------------------------------------------------------------
def readSettings(config):
settings = {}
settings['file_log'] = config.get('SETTINGS', 'file_log', fallback = None) # Файл с логами
settings['subject'] = config.get('SETTINGS', 'subject', fallback = 'Результат резервирования') # Заголовок письма
settings['mail_to'] = config.get('SETTINGS', 'mail_to', fallback = None) # Кому отправляется письмо
settings['mail_from'] = config.get('SETTINGS', 'mail_from', fallback = None) # От кого отправляется письмо
settings['archive'] = config.get('SETTINGS', 'archive', fallback = None) # Путь и название используемого архиватора
settings['file_log_total_stat'] = config.get('SETTINGS', 'file_log_total_stat', fallback = None) # Путь и название общего лога, если он используется
settings['file_log_total_mail'] = config.get('SETTINGS', 'file_log_total_mail', fallback = None) # Путь и название общего файла для отправки, если он используется
return settings
#--------------------------------------------------------------------------------------------------------------
# Загрузка параметров единицы архивации в словарь unit описывающий отдельную архивацию.
# Возвращает словарь.
#--------------------------------------------------------------------------------------------------------------
def readUnit(backup):
unit = {}
unit['path_to'] = os.path.realpath(config.get(backup, 'path_to', fallback = None)) # Путь к папке для хранения архива.
unit['path_from'] = os.path.realpath(config.get(backup, 'path_from', fallback = "")) # Путь к папке откуда забирать архив.
unit['file_end'] = config.get(backup, 'file_end', fallback = None) # Окончание в имени файла (включая расширение), по которому будет осуществляться поиск файла.
unit['kol_copy'] = config.getint(backup, 'kol_copy', fallback = 1) # Количество полных копий или количество дней хранения архива.
unit['ls_file'] = os.path.realpath(config.get(backup, 'ls_file', fallback = "")) # Путь и название ls файла с указанием файлов для архиватора
unit['type_copy'] = config.get(backup, 'type_copy', fallback = 'copy') # Вид обработки: COPY, FULL, DIFF
return unit
#--------------------------------------------------------------------------------------------------------------
# Преобразуем число байт в строку выраженную в гигабайтах.
# Параметры:
# bytes - количество байт в виде числа.
# Возвращает строку.
#--------------------------------------------------------------------------------------------------------------
def toGigabyte(bytes):
return f"{bytes/(2**30):.2f}"
#--------------------------------------------------------------------------------------------------------------
# Создание копии путем копирования.
# Параметры:
# backup - Раздел подлежащий копированию.
# unit - Словарь с параметрами
#--------------------------------------------------------------------------------------------------------------
def doCopy(backup, unit):
LogOut('Начало резервного копирования: ' + backup, '\n')
DelOldFile(unit['path_to'], unit['kol_copy']) # Удаляем старые файлы с превышающим сроком хранения.
try:
os.chdir(unit['path_from']) # Переходим в сетевую папку с исходными файлами для копирования.
except:
LogOut('ERROR: Не найден путь [' + unit['path_from'] + '] Копирование отменено!')
else:
for f in os.listdir('.'): # Получаем список файлов и с каждым работаем.
dateFile = datetime.datetime.fromtimestamp(os.stat(f).st_mtime)
timeLen = (TIME_START - dateFile).days # Вычисляем количество дней хранения текущего файла от текущей даты
if f[-len(unit['file_end']):] == unit['file_end'] and timeLen <= unit['kol_copy'] : # Если имя файла совпало с шаблоном и его количество дней хранения попадает в наш диапазон
if not os.path.exists(os.path.join(unit['path_to'], f)): # и он не существует в папке назначения.
LogOut('Копирование файла [' + f + '], размер ' + str(os.stat(f).st_size) + ' байт (' + toGigabyte(os.stat(f).st_size) + ' Gb)' )
copyFile(os.path.join(unit['path_from'], f), os.path.join(unit['path_to'], f)) # Копируем файл, собственной функцией.
LogOut('\tСвободное место в пути назначения [' + unit['path_to'] + ']: (' + toGigabyte(shutil.disk_usage(unit['path_to']).free) + ' Gb)')
#--------------------------------------------------------------------------------------------------------------
# Создание копии путем архивации.
# Параметры:
# backup - Раздел подлежащий копированию.
# unit - Словарь с параметрами
#--------------------------------------------------------------------------------------------------------------
def doPack(backup, unit, settings):
LogOut('Начало архивации: ' + backup, '\n')
cmd = '' # Строка с параметрами.
opt = '-pPassword -r -y -ssw -mx=0' # Дополнительные опции для архиватора.
fullBackupName = '' # Имя полной копии для архива.
diffBackupName = '' # Имя разностной копии для архива.
if unit['type_copy'] == FULL_COPY:
cmd = 'a'
fullBackupName = backup + '-' + DATE_TODAY + '-full.7z'
if os.path.exists(os.path.join(unit['path_to'], fullBackupName)):
LogOut('\tERROR: Файл [' + fullBackupName + '] по расположению [' + unit['path_to'] + '] уже существует. Архивация отменена!')
return
if unit['type_copy'] == DIFF_COPY:
cmd = 'u'
lastModifyDate = datetime.datetime.min
# В папке с архивами находим самый последний по дате файл полного архива.
try:
os.chdir(unit['path_to']) # Переходим в папку где хранятся архивы.
except:
LogOut('\tERROR: Не найден путь [' + unit['path_to'] + ']. Архивация отменена!')
return
else:
for f in sorted(os.listdir('.')): # Получаем список файлов и с каждым работаем.
dateFile = datetime.datetime.fromtimestamp(os.stat(f).st_mtime) # Получаем дату файла
if f[-len('-full.7z'):] == '-full.7z' and f[0:len(backup)] == backup: # Если имя файла совпало с шаблоном и его количество дней хранения попадает в наш диапазон
fullBackupName = f
lastModifyDate = dateFile
# По окончании имеем имя файла с последней датой полной копии.
if fullBackupName == "": # Если не найдена полная копия архива, то останавливаем архивацию по данному файлу.
LogOut('\tERROR: Не найден полный архив для [' + backup + '] по расположению [' + unit['path_to'] + ']. Архивация отменена!')
return
diffBackupName = fullBackupName[0:-len('full.7z')] + DATE_TODAY + ".7z"
if os.path.exists(os.path.join(unit['path_to'], diffBackupName)):
LogOut('\tERROR: Файл [' + diffBackupName + '] по расположению [' + unit['path_to'] + '] уже существует. Архивация отменена!')
return
# Задаем параметры для архиватора по формированию разностной копии:
# -u- : исходный архив не будет модифицирован
# -u : задаем параметры сравнения с полной копией
# p0 : если файл есть в архиве но не подходит по шаблонам - игнорировать
# q3 : если файл есть в архиве, но его нет на диске - элемент удаляется при распаковке
# x2 : если файл в архиве новее, чем на диске - добавляем в архив
# z0 : файл в архиве такой же как на диске - пропускать
# ! : идет имя создаваемого нового архива
opt = opt + ' -u- -up0q3x2z0!' + os.path.join(unit['path_to'], diffBackupName)
exec_str = (settings['archive'] + ' ' + cmd + " " + os.path.join(unit['path_to'], fullBackupName) + " " + opt + " @" + unit['ls_file']).split(' ')
result = -1
START = str(datetime.datetime.now()) # Время начала архивации.
LOG_DATE_START = START[0:10]
LOG_TIME_START = START[11:19]
try:
# Создаем архив с помощью внешнего архиватора.
result = check_call(exec_str)
except:
LogOut('\tРезультат архивации отличен от 0.')
END = str(datetime.datetime.now()) # Время окончания архивации.
LOG_DATE_END = END[0:10]
LOG_TIME_END = END[11:19]
created_file = ''
if unit['type_copy'] == DIFF_COPY:
created_file = os.path.join(unit['path_to'], diffBackupName)
if unit['type_copy'] == FULL_COPY:
created_file = os.path.join(unit['path_to'], fullBackupName)
LogOut('\tРезультат архивации, код: ' + str(result))
LogOut('\tСоздан архив: [' + created_file + '], размер ' + str(os.stat(created_file).st_size) + ' байт (' + toGigabyte(os.stat(created_file).st_size) + ' Gb)')
LogOut('\t\tСвободного места на диске [' + unit['path_to'] + ']: ' + toGigabyte(shutil.disk_usage(unit['path_to']).free) + ' Gb)')
# Сохраняем информацию об архиве для загрузки в базу статистики.
if settings['file_log_total_stat'] != None:
logFileTotalStat = open(settings['file_log_total_stat'],'a')
logFileTotalStat.write(backup + ';'+ LOG_DATE_START + ';' + LOG_TIME_START + ';' + LOG_DATE_END + ';' + LOG_TIME_END + ';' + str(os.stat(created_file).st_size) + '\r\n')
logFileTotalStat.close()
# Сохраняем информацию об архиве для отправки.
if settings['file_log_total_mail'] != None:
logFileTotalMail = open(settings['file_log_total_mail'], 'a')
logFileTotalMail.write((backup+'-'+unit['type_copy']).ljust(20,' ') + ('[' + created_file + ']').ljust(70,' ') + LOG_DATE_START + '\t' + LOG_TIME_START + '\t' + LOG_DATE_END + '\t' + LOG_TIME_END + '\t size ' + str(os.stat(created_file).st_size) + ' bytes (' + toGigabyte(os.stat(created_file).st_size) + ' Gb)' + '\r\n')
logFileTotalMail.close()
# Удаление старых полных архивов.
if unit['type_copy'] == FULL_COPY:
fileList = [] # Собираем названия файлов в список.
try:
os.chdir(unit['path_to']) # Переходим в папку где хранятся архивы.
except:
LogOut('\tERROR: Не найден путь [' + unit['path_to'] + ']. Удаление старых файлов не произведено!')
return
else:
for f in os.listdir('.'): # Формируем список полных копий в каталоге.
if f[-len('-full.7z'):] == '-full.7z' and f[0:len(backup)] == backup:
fileList.append(f)
index = 0
for f in sorted(fileList, reverse=True): # Сортируем список по убыванию имени.
index += 1
if index > unit['kol_copy']: # Отсекаем первые kol_full_copy файлы, остальные попадают под удаление.
for deleteFile in os.listdir('.'):
maskFile = f[:-len('-full.7z')] # Получаем шаблон для удаления по началу имени [OBJ-YYYY-MM-DD].
if deleteFile[:len(maskFile)] == maskFile: # Если начало имени файла совпадает с шаблоном, то удаляем файл.
os.remove(os.path.join(unit['path_to'], deleteFile))
LogOut('\t\tУдален архив: [' + os.path.join(unit['path_to'], deleteFile) + ']')
#--------------------------------------------------------------------------------------------------------------
config = configparser.ConfigParser() # Определяем файл с настройками копирования.
fileINI = sys.argv[1] # Получаем имя ini файла с настройками архивирования через первый параметр при вызове скрипта.
config.read(fileINI) # Загружаем настройки из INI файла.
settings = readSettings(config) # Считываем настроечные параметры.
logFile = open(settings['file_log'], 'a') # Открываем ЛОГ файл на основе настроек из settings.
list = config.sections() # Получаем список разделов для архивации из конфига.
list.remove("SETTINGS") # Удаляем блок с общими настройками из списка резервного копирования.
message = "" # Формируемое сообщение.
for backup in list:
unit = readUnit(backup) # Загружаем настройки единицы копирования/архивации.
if unit['type_copy'] == COPY_COPY: # Выполнение копирования файлов.
doCopy(backup, unit)
if unit['type_copy'] == FULL_COPY or unit['type_copy'] == DIFF_COPY: # Выполнение архивации файлов.
doPack(backup, unit, settings)
LogOut('Окончание работы скрипта. Заняло времени: ' + str(datetime.datetime.now() - TIME_START))
# Если заданы параметры для отправки сообщения, то формируем и отправляем письмо о результате работы.
if settings['mail_to'] != None and settings['mail_from'] != None:
message = (f"Subject: {settings['subject']}\n\n" + message).encode('utf-8') # Накопительная строка для формирования тела письма.
server = smtplib.SMTP(host='mail.mymail.ru', port=25) # Сервер для почтовой рассылки результатов.
msg = MIMEMultipart()
msg['From'] = settings['mail_from'] # От кого будет сформировано письмо с отчетом.
msg['To'] = settings['mail_to'] # Групповой почтовый ящик, на который будет выслан отчет.
try:
server.sendmail(msg['From'], msg['To'], message) # Отправка письма со статистикой.
except:
LogOut('ERROR: Отправка итогового письма завершилась неудачей!')
server.quit()
logFile.close()