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


В статье рассказывается, как сделать статические версии веб-страниц для их выдачи сервером и как поместить их в репозиторий для контроля версий и резервного копирования. При этом статические и медиафайлы могут храниться отдельно и архивироваться другими средствами (статика обычно помещается в репозиторий для программного кода сайта). Метод работает также для страниц с Unicode-именами (например, для кириллических доменов). В конце приведён работающий Makefile.


Автор пользуется стеком django/uwsgi/nginx, виртуальным выделенным сервером под управлением GNU/Linux, но содержание статьи почти не зависит от конкретных технологий.


Загружаем страницы


Сохранять страницы сайта мы будем с помощью стандартной программы wget. Мы будем сохранять каждый сайт в отдельную директорию (которая, возможно, не связана с доменным именем сайта).


Внутри каждой директории страницы будут сохраняться рекурсивно с помощью ключа wget -r (предполагается, что ко всем страницам можно перейти по ссылкам с главной). По умолчанию рекурсивное копирование идёт вплоть до 5-го уровня, но это можно изменить с помощью ключа -l.


Если мы храним медиа и статические файлы отдельно от текстовых страниц, то соответствующие директории игнорируются с помощью ключа -X.


Полная команда выглядит так:


mkdir primer
cd primer
wget -r -nH -X медиа,static --restrict-file-names=nocontrol пример.рф

-nH означает --no-host-directories. По умолчанию wget -r example.com поместит всё в директорию example.com/, а эта опция отменяет создание директории с именем хоста.


Опция --restrict-file-names указывает на экранирование символов в URL при создании локальных файлов. Значение nocontrol означает отключение экранирования и очень важно для сохранения страниц с кириллическими ссылками. Без неё страницы сохраняются в файлы с немного изменёнными именами, и не совсем понятно, как их выдавать серверу. К сожалению для пользователей Windows, --restrict-file-names=nocontrol у них не будет работать, это известная проблема.


Добавляем в git


Новый репозиторий создаётся с помощью команды git init. По умолчанию он создаётся внутри текущей директории в папке .git, однако мы хотим, чтобы серверу были доступны только файлы, которые соответствуют названиям открытых страниц сайта. Поэтому полная команда, создающая чистый (bare) репозиторий в папке ../.git-primer, выглядит так:


git init --bare ../.git-primer

Чтобы далее пользоваться таким нестандартным репозиторием, нужно передавать git опции git-dir и work-tree:


git --git-dir=../.git-primer --work-tree=. add .

Пишем Makefile


Начнём с объявления наших проектов:


SITES := example primer
all : $(SITES)
.PHONY : $(SITES)

В переменной SITES содержатся названия наших проектов. Целью по умолчанию является первая цель all, то есть для выполнения всех действий достаточно будет набрать одну команду make. Все цели в SITES являются фиктивными (PHONY): рецепт для каждой из них будет выполняться независимо от существования директории и времени её изменения.
Базовое введение в make можно прочитать, например, здесь, а основным руководством является info make (оригинал, перевод).


Правило для каждого из проектов выглядит так:


$(SITES) : 
    if [[ -d .git-$@ ]];     then         $(get-data);         $(mgit) add . &&         if [[ -n "`$(mgit) status --porcelain`" ]]; then             $(mgit) commit -m "Update $@.";         fi     else         $(init-git);     fi

Данное правило по сути является одной командой shell.
$@ — автоматическая переменная, в которой содержится название текущей цели (например, primer).
Сначала мы проверяем, существует ли директория .git-primer. Если да, то переходим в директорию проекта, скачиваем страницы, добавляем их в git.
Если содержание страниц не изменилось, то git ничего не добавит, но в этом случае commit будет вызывать ошибку и остановку исполнения Makefile. Поэтому сначала мы вызываем git status с опцией --porcelain, которая предназначена для использования в скриптах. Если длина строки вывода git status --porcelain не нулевая, то мы можем делать commit (отсюда).


get-data, mgit и init-git — это заготовленные (canned) рецепты в Makefile. Например, mgit — это вызов git c указанием директории с файлами репозитория и рабочего каталога:


define mgit =
    git --git-dir=../.git-$@ --work-tree=.
endef

Canned recipes создаются, когда одна последовательность команд может использоваться в нескольких рецептах. Они могут состоять из нескольких строк, каждая из которых автоматически выделяется табуляцией в рецептах (более точно, символом .RECIPEPREFIX). В нашем примере отступ сделан лишь для удобства чтения Makefile.
Во время исполнения рецептов каждая строка заготовленных последовательностей интерпретируется как отдельная строка рецепта, то есть, в частности, в них можно использовать автоматические переменные для данной цели.


Полный Makefile выглядит так:


SITES          := primer example
SERVERHOST     := example
# пример.рф в punicode
SERVERHOSTNAME := xn--e1afmkfd.xn--p1ai
SERVERPATH     := ~/archive

all : $(SITES)

.PHONY : $(SITES)

# target-specific variables
primer : DOMAIN := пример.рф
primer : EXCLUDEDIRS := медиа,static
example : DOMAIN := example.com

ifeq ($(SERVERHOSTNAME),$(shell hostname))
# Server
define mgit =
    git --git-dir=../.git-$@ --work-tree=.
endef

define init-git =
    mkdir -p $@ &&     $(get-data) &&     git init --bare ../.git-$@ &&     $(mgit) add . &&     $(mgit) commit -m "Initial commit of $@."
endef

define get-data = 
    cd $@ &&     wget -r -nH -X $(EXCLUDEDIRS) --restrict-file-names=nocontrol $(DOMAIN)
endef

else
# Workstation
define init-git =
    git clone $(SERVERHOST):$(SERVERPATH)/.git-$@ $@
endef
endif

$(SITES) : 
ifeq ($(SERVERHOSTNAME),$(shell hostname))
# Server
    if [[ -d .git-$@ ]];     then         $(get-data);         $(mgit) add . &&         if [[ -n "`$(mgit) status --porcelain`" ]]; then             $(mgit) commit -m "Update $@.";         fi     else         $(init-git);     fi
else
# Workstation
    if [[ -d $@/.git ]];     then         cd $@ && git pull;     else         $(init-git);     fi
endif

В четвёртом абзаце идут целе-зависимые (target-specific) переменные: для каждой цели можно установить собственное значение этой переменной. Эти значения передаются также в зависимости (prerequisites) каждой из целей и в используемые заготовленные рецепты, то есть мы можем быть уверены, что рецепт для каждого сайта будет выполнен с правильным именем сайта и его директории.
Для каждого проекта мы можем передать свои не архивируемые директории через переменную EXCLUDEDIRS или оставить её пустой. Аналогично можно менять имя сервера для архивирования с рабочего компьютера (SERVERHOST) и путь на сервере к директории с архивом сайтов (SERVERPATH). Для простоты, в этом примере все сайты находятся на одном сервере и архивируются в одной директории.
Поскольку каждая строка рецепта (в том числе заготовленного) выполняется в отдельной оболочке shell, то чтобы переход в директорию оставался в силе для следующих команд, мы пользуемся оператором "и" && и экранированием конца строки \ .


Далее идёт условная конструкция Makefile: с помощью команды shell hostname мы проверяем, запускается ли make на сервере или на локальном компьютере. Строки, не удовлетворяющие текущей ветви условной директивы, полностью игнорируются Makefile.


Различие между локальным и серверным репозиториями

Локальный компьютер служит прежде всего для хранения данных, поэтому на него мы только копируем данные с сервера (git pull), а для удобства локальной работы с git (просмотра логов или версий файлов) мы пользуемся структурой репозитория по умолчанию (обычным репозиторием в папке .git).
В обоих случаях достаточно одной команды make. Для автоматического копирования можно использовать планировщик cron. Чтобы каждый раз не вводить пароль для доступа на сервер, генерируются ssh-ключи.


Для удобства работы на сервере можно из директории текущего сайта создать псевдоним (alias) git с заданной конфигурацией:


alias mgit="git --work-tree=. --git-dir=../.git-${PWD##*/}"

Переменная ${PWD##*/} содержит название текущей директории без пути к ней и входит в стандарт POSIX, то есть может использоваться во всех поддерживающих POSIX оболочках.


Условная директива может также использоваться и в рецептах, единственное ограничение — это что её начало и конец не могут находиться в разных файлах.


Сервер

После запуска make директория архивов выглядит так:


$ ls -a
.  ..  .git-example  .git-primer  Makefile  example primer
$ ls -a primer
.  ..  index.html  про-собак  про-котов
$ # предполагается, что с главной страницы пример.рф есть ссылки на пример.рф/про-собак и пример.рф/про-котов

Файл конфигурации nginx для пример.рф может выглядеть так:


server {
    server_name xn--e1afmkfd.xn--p1ai;
    charset     utf-8;

    location = / {
        root /home/user/archive/primer;
        try_files /index.html =404;
    }

    location / {
        root /home/user/archive/primer;
        default_type "text/html";
        try_files $uri =404;
    }

    location = /index.html { return 404; } 
}

Первый location соответствует главной странице сайта, пример.рф. Она сохраняется wget как файл index.html. Если он не находится, выдаётся ошибка 404.


Для всех остальных URI проверяются файлы в директории primer с названием URI. Если они не найдены, то выдаётся 404.


В конце, чтобы избежать дублирования контента, мы явно запрещаем доступ по ссылке пример.рф/index.html (404). Чуть более подробно об этой конфигурации написано здесь.


Заключение


Резервное копирование сайтов можно делать с помощью стандартных инструментов wget, git, make. Можно копировать все страницы сайта или исключать медиа- и ряд других файлов настолько точно, насколько это позволяет wget. Аналогично, с помощью .gitignore, можно контролировать, какие статические страницы будут добавляться в репозиторий для резервного копирования, а какие нет. Makefile позволяет гибко управлять различными конфигурациями для различных проектов. Полный пример Makefile для клиента и для сервера выше при этом содержит лишь около 60 строк.


Предполагается, что изменение и добавление контента сайта происходит через стандартные механизмы, то есть для этого запускается CMS или CMF. Если это происходит редко, то после работы их можно отключать, освобождая ресурсы системы и выдавая сохранённые статические страницы. Пример более полной автоматизации, возможно, заслуживает отдельной статьи.


Предложенный метод подходит прежде всего для небольших проектов, которые обновляются редко, поэтому вопросы производительности и безопасности здесь почти не рассматривались. Поскольку мы указали wget не экранировать символы из URI, то в случае, если произвольные пользователи могут добавлять файлы на сайт, экранирование или запрет на их добавление должны происходить сразу.


Сохранение версий содержимого сайта также может осуществляться через базу данных при изменении его страниц. Но для этого нужна поддержка версий моделями CMF, а также больший контроль за дампом базы данных (полностью копировать его после любого редактирования страниц). В предложенном методе в случае небольшого изменения содержимого в репозиторий будет добавлено только это изменение, и не требуется использование полной копии БД. Кроме того, сгенерированные статические страницы могут напрямую использоваться сервером или просматриваться в браузере (изменение дизайна или другого программного кода сайта при этом тоже будет копироваться).


Альтернативные программы для резервного копирования перечислены здесь. Для хранения и синхронизации медиафайлов стоит обратить внимание на git-annex. Отделение .git репозитория от рабочего дерева также успешно используется для управления конфигурационными файлами пользователя (dotfiles). Сегодня существуют серверы, которые напрямую поддерживают работу с git-репозиториями.

Комментарии (7)


  1. ynikitenko Автор
    03.10.2018 19:53
    +1

    К вопросу о разных методах резервного копирования
    image


  1. gecube
    03.10.2018 20:28
    +1

    Вот честно — ни черта не понял.
    Обычно когда пишут сайт на django, то предполагается динамический контент. Который меняется в зависимости от того, какой пользователь зашел (админка? блог? ...), либо просто сам по себе (например, можно придумать сайт, который выдает каждый раз случайное изречение на своей главной странице).
    В статье не описано, но данный механизм бекапа для таких сайтов не работает от слова совсем.

    НО! Если сайт статический, то можно пойти обратным путем. Взять любой (!) генератор статических сайтов вроде Jekyll, Hugo, Middleman, Harp, Hexo, или Brunch (и сотни их) и положить сам сайт в репозиторий. Тогда репозиторий будет автоматически и бекапом, и единым источником правды (т.е. содержит ЦЕЛЕВОЕ состояние системы). Вот.


    1. ynikitenko Автор
      03.10.2018 20:54
      -1

      Дело в том, что у меня несколько сайтов на django.
      Я пользуюсь виртуальным выделенным сервером за 90 рублей в месяц. За эти деньги дают ОС Linux с root-доступом, 10 гигабайт дискового пространства и 256 мегабайт памяти.
      При использовании этого тарифа хостер подчёркивает полный отказ от ответственности в случае потери данных.
      Сайты работают с помощью django/uwsgi/nginx, сам контент меняется редко. Поскольку памяти очень мало, то удаётся запускать uwsgi с его worker-ами только для одного сайта, в то время как другие могут храниться в виде статических страниц и выдаваться только сервером nginx.

      Что касается генераторов статических сайтов: их действительно много. Плюс этой статьи в том, что можно использовать для генерации статических страниц уже известные инструменты (если вы уже знаете django), не нужно выбирать генератор и учиться его конфигурировать.

      Это некая промежуточная стадия: если хотим, то запускаем полноценный динамический сайт, а если хотим — легко его архивируем, убиваем соответствующие процессы (освобождаем память) и выдаём страницы с помощью nginx (который один на весь сервер).


      1. gecube
        03.10.2018 20:57
        +1

        Смотрите.
        Вариант бекапа #2. Нам же по сути нужно только хранить код + содержимое БД, если она используется.
        Код сайта — прекрасно хранится в репозитории. Если туда же в репозиторий положить инструкции по разворачиванию кода (описание софта на VDS, конфигурации, процесс деплоя и т.д.) — мы получаем IaC (Infrastructure-as-Code), т.е. всегда можно восстановиться на конкретную версию сайта.
        Базы данных — если она небольшая — можно дампить mysql и точно так же класть в репозиторий, если она нужна. Тот же GitLab спокойно тянет репозитории до 2ГиБ.


        1. ynikitenko Автор
          03.10.2018 21:32
          -1

          Да, код, содержимое БД и медиафайлы.
          С кодом вопросов нет, для него отдельный репозиторий.
          Под кодом я имею в виду прежде всего сам код сайта на django (возможно, с парой вспомогательных вещей вроде requirements.txt). Однако я бы не хотел туда же отправлять код для разворачивания на сервере, т.к. он а) платформозависимый б) если сайтов несколько, то он может быть дублирован (в любом случае в отдельном репозитории должен быть код для разворачивания nginix на сервере и установки других системных пакетов — возможно, там же стоит хранить код для разворачивания отдельных сайтов).
          Насчёт хранения БД в репозитории — я не могу сейчас ответить точно; есть мнение, что git хранит изменённые куски больших файлов (а .sql может восприниматься как бинарный файл). С другой стороны, насколько мне известно, git лучше всего подходит именно для текстовых файлов. Хранение именно кода или текста в репозитории мне кажется более "чистым" и "правильным" (тем более не хочется хранить каждую версию БД в отдельном файле, если можно хранить только изменения). В целом я не утверждаю что мой метод подходит для всех — вероятно, есть разные вкусовые предпочтения.
          В предложенном методе плюсом является также то, что он подходит и для архивирования медиафайлов (картинки обычно не изменяются, хотя их тоже можно хранить в БД — но тогда и размер её будет существенно больше, чем у только текстовой).


          1. gecube
            03.10.2018 21:36
            +1

            Насчет дампа базы — я говорил именно о текстовом формате, а не бинарном.
            Картинки и прочие медиаматериалы, если они часто меняются — да, только хранить отдельно (Amazon S3? или можно еще в Git LFS)


  1. ynikitenko Автор
    04.10.2018 09:32

    Комментарий Ивана Бегтина из OpenDataRussiaChat
    https://t.me/opendatarussiachat:
    ynikitenko это полезно, спасибо, но когда страниц много у этого есть существенные ограничения. Есть похожий способ через сохранение файла в WARC файл интернет архива. wget и его более продвинутый аналог wpull умеют в него сохранять. После чего есть инструменты отображения страниц из сайта по аналогии с интернет архивом. Например, pywb. Если не сохранять в warc файл то при использовании wget'а и воспроизведении сайта через веб-сервер часто ломаются ссылки с кириллицей и ссылки которые отдают файлы с серверной логикой и через Content-disposition