Утилита многосайтового резервного копирования с базовыми проверками работоспособности

У нас в работе большой набор проектов. Очень важно отслеживать их, и время от времени нам нужно запускать полное резервное копирование, не связанное с «обычным» резервным копированием, которое выполняет ИТ (моментальные снимки каждый день, неделю, месяц и т. Д.). Недавно у нас произошла большая миграция всех старых проектов. Чтобы убедиться, что во время миграции (в основном при обновлении версии) ничего не было непоправимо повреждено, перед обновлением создается резервная копия всего.

Здесь задействованы сетевые диски и медленные компьютеры (так что все связано с вводом-выводом), что делает использование ЦП в значительной степени несущественным.

Текущая скорость:

  • менее минуты / ГБ (с локального на локальный)
  • около 3 минут / ГБ (локально в сеть)
  • около 4 минут / ГБ (сеть -> локальная)
  • менее 2 минут / ГБ (сеть -> сеть)

Мы говорим о 60+ ГБ для исходного каталога, разбросанных по папкам 7k и файлам 180k. Если бы мы могли как-то иначе читать / писать, чтобы упростить работу с дисками и в сети, это было бы здорово.

Я работаю над приложением, которое в конечном итоге выполняет 3 функции:

  • резервное копирование
  • установить вторичную программу на исходные данные для миграции
  • уборка

Этот вопрос касается исключительно первой части этого процесса, остальная часть истории предназначена исключительно для объяснения контекста и мотивации. Его можно использовать отдельно, так что он готов к рассмотрению.

Эта программа выполнит некоторые базовые проверки работоспособности задействованных каталогов, проверит наличие необходимого пространства и выполнит резервное копирование в любое количество мест. В конце выводится количество записанных байтов для быстрой визуальной проверки, пошло ли что-нибудь не так. Важно, чтобы метаданные, такие как даты создания и последнего изменения, оставались нетронутыми.

Я разбил код на несколько разумных функций. Некоторые делают тяжелую работу, другие просто помощники, чтобы не повторяться. Я не доволен тем, как я реализовал проверку каталогов, отчеты и обработка исключений в лучшем случае дрянные, и я уверен, что другие функции можно улучшить с точки зрения повторного использования.

Я пробовал делать некоторые причудливые вещи при проверке каталогов, чтобы я мог рекурсивно определять достоверность, не заботясь о том, передал ли я строку, WindowsPath или словарь. Что ужасно провалилось. Я чувствую, что использую неоптимально pathlib, но, учитывая, что я унаследовал инструменты (я столкнулся с довольно странными PermissionError проблемы с shutil пока xcopy «просто работает») Не совсем уверен, что можно помочь. Я думаю, что все считывания и, по крайней мере, часть проверки могут быть выполнены с помощью декораторов, как здесь, но для меня это все еще довольно волшебно.

Я почти уверен, что все это не должно быть в одном файле, но, учитывая, что я бы просто взял все, кроме main и поместите это в utils.py, Я еще не пошел по этому пути. Я открыт для идей. Честно говоря, я удивлен, что мой код все еще работает, не похоже, что должен. Я попытался (не показано) преобразить его с помощью оберток и декораторов, но похоже, что это потребует переписывания, прежде чем это сработает. Пока данные копируются чисто, остальной код все еще может быть изменен любым способом. Спецификация не высечена в камне.

# -*- coding: utf-8 -*-
"""
Created on Wed May  5 13:10:37 2021

@author: Mast

Notes:
    # xcopy & psutil don't seem to handle pathlib.WindowsPath too well,
    # converting to string conveniently turns / into \
    # shutil kept running into PermissionError where xcopy has no trouble
"""

import os
import sys
import subprocess
import psutil
import time

from datetime import datetime
from pathlib import Path


PROJECTS_SOURCE = Path('Z:/EPLAN_DATA/Gegevens/Projecten/')

BACKUP_DIRECTORIES = {
    'local': Path('C:/backups/eplanprojects'),
    'network': Path('N:/BackupsEplan')
}

XCOPY_ARGS = "/e /h /i /q /s /z"

MAX_BACKUPS = 3
SLEEP_TIME = 60

FORCE = True


def to_megabytes(size):
    """
    Parameters
    ----------
    size : int

    Returns
    -------
    str
        Turn Bytes into rounded MegaBytes.

    """
    return "{0} MB".format(round(size / 1000000.0, 2))


def get_directory_size(directory):
    """
    Parameters
    ----------
    directory : pathlib.WindowsPath

    Returns
    -------
    int
        Size of the contents of the directory in Bytes.

    """
    print("Calculating size of {0}".format(directory))
    size = 0
    for path, _, files in os.walk(directory):
        for file in files:
            file_path = os.path.join(path, file)
            size += os.path.getsize(file_path)
    print(to_megabytes(size))
    return size


def free_space(directory):
    """
    Parameters
    ----------
    directory : pathlib.WindowsPath

    Returns
    -------
    int
        Free space available on partition the directory belongs to.

    """
    return psutil.disk_usage(str(directory)).free


def terminate_program(additional_info=""):
    """
    Parameters
    ----------
    additional_info : str (OPTIONAL)

    Print additional_info, wait & exit.

    """
    print("Program terminated before completion.")
    if additional_info:
        print(additional_info)
    time.sleep(SLEEP_TIME)
    sys.exit()


def validate_directory(directory):
    """
    Parameters
    ----------
    directory : pathlib.WindowsPath

    Raise FileNotFoundError if directory does not exist.

    """
    if not directory.exists():
        print("Invalid directory: {0}".format(directory))
        raise FileNotFoundError


def verify_available_backup_space(source_size, backup_directories):
    """
    Parameters
    ----------
    source_size : int
    backup_directories : dict of pathlib.WindowsPath

    Validate backup directories exist and are big enough to hold source.
    Terminate on failure.

    """
    for backup_directory in backup_directories.values():
        validate_directory(backup_directory)
        backup_space_available = free_space(backup_directory)
        if source_size > backup_space_available:
            # WARNING: If multiple back-up locations are on the SAME partition,
            # this check is insufficient
            print("That's not going to fit.nTarget: {0} available.nSource: {1}".format(
                backup_space_available, source_size))
            terminate_program()


def backup_projects(source, backup_directories):
    """
    Parameters
    ----------
    source : pathlib.WindowsPath
    backup_directories : dict of pathlib.WindowsPath

    Returns
    -------
    list of int : bytes_written

    Perform backup of source directory to (multiple) backup directories.

    """
    bytes_written = []
    for backup_directory in backup_directories:
        if len([f.path for f in os.scandir(backup_directories.get(backup_directory)) if f.is_dir()]) >= MAX_BACKUPS:
            print("Amount of immediate subdirectories in ({0}) is higher or equal to maximum amount of backups ({1}) configured.".format(
                backup_directory, MAX_BACKUPS))
            if not FORCE:
                terminate_program()
            else:
                print("Backup forced. Continuing.")
        print(backup_directories[backup_directory])

        start_time = datetime.now()
        print("Start copy {0}".format(start_time))
        try:
            subfolder = "_{}".format(start_time).replace(
                ':', '-').replace(' ', '_').split('.')[0]
            print(subfolder)
            syscall = "xcopy {source} {destination}\{subfolder} {args}".format(
                source=str(source),
                destination=str(backup_directories[backup_directory]),
                subfolder=subfolder,
                args=XCOPY_ARGS)

            print(syscall)
            subprocess.run(syscall, check=True)
        except PermissionError:
            print("Permission denied: {0}".format(syscall))
            terminate_program()
        end_time = datetime.now()
        print("Started: {0}nFinished: {1}nExecution time {2}".format(
            start_time,
            end_time,
            end_time - start_time)
        )
        bytes_written.append(get_directory_size(
            str(backup_directories[backup_directory]) + '\' + subfolder))
    for value in bytes_written:
        print(to_megabytes(value))


def main():
    validate_directory(PROJECTS_SOURCE)
    projects_source_size = get_directory_size(PROJECTS_SOURCE)

    verify_available_backup_space(projects_source_size, BACKUP_DIRECTORIES)

    backup_projects(PROJECTS_SOURCE, BACKUP_DIRECTORIES)


if __name__ == "__main__":
    main()
    print("Press any key...")
    input()

Реальный выход:

Calculating size of Z:EPLAN_DATAGegevensProjecten
60585.67 MB
C:backupseplanprojects
Start copy 2021-05-07 17:21:57.007150
_2021-05-07_17-21-57
xcopy Z:EPLAN_DATAGegevensProjecten C:backupseplanprojects_2021-05-07_17-21-57 /e /h /i /q /s /z
178642 File(s) copied
Started: 2021-05-07 17:21:57.007150
Finished: 2021-05-07 21:41:52.366467
Execution time 4:19:55.359317
Calculating size of C:backupseplanprojects_2021-05-07_17-21-57
60585.67 MB
N:BackupsEplan
Start copy 2021-05-07 21:43:31.600948
_2021-05-07_21-43-31
xcopy Z:EPLAN_DATAGegevensProjecten N:BackupsEplan_2021-05-07_21-43-31 /e /h /i /q /s /z
178642 File(s) copied
Started: 2021-05-07 21:43:31.600948
Finished: 2021-05-07 23:31:07.970629
Execution time 1:47:36.369681
Calculating size of N:BackupsEplan_2021-05-07_21-43-31
60585.67 MB
60585.67 MB
60585.67 MB
Press any key...

Все и вся подлежит рассмотрению. Нитпик прочь.

Python 3.8.5 в Windows 10 x64, на этот раз без ограничений по библиотекам и кроссплатформенности.

1 ответ
1

Вы можете использовать f-строки вместо .format(). Это делает код более читаемым, а также быстрее (см. здесь, и даже в моей системе f-струны примерно в 4,4 раза быстрее, чем .format() на моем компьютере с Windows и примерно в 3,5 раза быстрее на WSL2 с Ubuntu 20.04 LTS), хотя в большинстве случаев разница в скорости незначительна.

Например, в get_directory_size, ты можешь написать print(f"Calculating size of {directory}").

Точно так же в to_megabytes, ты можешь написать return f"{round(size / 1_000_000.0, 2)} MB".

  • 1

    f-строки имеют побочный эффект обработки обратных косых черт немного по-другому, как я это делаю в "Started: {0}nFinished: {1}nExecution time {2}".format(, но видимо есть обходные пути.

    — мачта

  • @Mast Да, это правда, что вы не можете использовать обратную косую черту внутри фигурных скобок в f-строках (спасибо, что указали на это, я на самом деле не знал об этом), но опубликованный вами пример совершенно законен, даже с f- струны. Но вы правы: определенно есть случаи, когда лучше использовать .format и держитесь подальше от струн для фа.

    — вермос

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *