Заметки из жизни и работы

Linux, Программирование и CMS

Бекап сайта и mysql на яндекс диск REST API + Bash

ODiN – 17 августа 2015

Резервная копия сайта нужна не тогда, когда всё работает, а в тот день, когда хостинг «упал», обновление плагина сломало шаблон или кто-то случайно удалил базу. Хранить копии на том же сервере бессмысленно: если пропадёт сервер, пропадут и они. Удобное и недорогое решение для небольшого проекта — отправлять архивы в облако. Разберём, как это сделать при помощи обычного bash-скрипта и REST API Яндекс Диска, без сторонних программ и клиентов.

Что понадобится

  • сервер или хостинг с доступом по SSH (Linux) и планировщиком cron;
  • установленные curl, tar, gzip и mysqldump (клиент MySQL);
  • аккаунт на Яндексе с Яндекс Диском и свободным местом;
  • OAuth-токен, которым скрипт будет подписывать запросы к API.

Получаем OAuth-токен

API Яндекс Диска работает с OAuth-авторизацией. Токен выдаётся приложению, которое нужно зарегистрировать на странице oauth.yandex.ru: указываем название, выбираем платформу «Веб-сервисы» и в списке доступов отмечаем права для Яндекс Диска (достаточно записи в отдельную папку приложения, но можно выдать и полный доступ). После создания приложения вы получите идентификатор (client_id). Откройте в браузере адрес авторизации приложения с этим идентификатором, подтвердите доступ, и сервис вернёт токен в адресной строке или на специальной странице.

Как устроена загрузка файла через REST API

Загрузка выполняется в два шага. Сначала мы просим API выдать адрес, на который можно отправить файл, затем отправляем сам файл методом PUT на этот адрес.

  1. Запрос адреса: GET https://cloud-api.yandex.net/v1/disk/resources/upload с параметром path (путь на Диске, закодированный как часть URL) и заголовком Authorization: OAuth ВАШ_ТОКЕН. Параметр overwrite=true разрешает заменить файл с тем же именем.
  2. В ответе приходит JSON с полями href, method и templated. Адрес в href действует около 30 минут.
  3. Отправка файла: PUT на полученный href. Токен на этом шаге не нужен. Код ответа 201 означает, что файл загружен, 202 — что он принят и скоро появится на Диске.

Папку для копий создаёт запрос PUT https://cloud-api.yandex.net/v1/disk/resources?path=%2Fbackups. Если папка уже существует, API вернёт ошибку 409, и скрипту это не мешает. Удаление выполняется запросом DELETE на тот же адрес ресурса, а параметр permanently=true минует корзину.

Скрипт

Ниже рабочий вариант. Он собирает архив каталога сайта, делает дамп базы, загружает оба файла в папку backups на Диске и удаляет локальные копии. Старые копии на Диске удаляются по имени файла: хранятся копии за последние 14 дней.

#!/bin/bash
# /usr/local/bin/ya-backup.sh

TOKEN="ВАШ_OAUTH_ТОКЕН"
SITE_DIR="/var/www/site/public_html"
DB_NAME="wordpress"
TMP_DIR="/var/backups/site"
REMOTE_DIR="backups"
KEEP_DAYS=14

API="https://cloud-api.yandex.net/v1/disk/resources"
DATE=$(date +%F)
OLD=$(date -d "-${KEEP_DAYS} days" +%F)

mkdir -p "$TMP_DIR"

enc() { printf '%s' "$1" | sed 's#/#%2F#g'; }

upload() {
  local file="$1" remote="$2" href code
  href=$(curl -s -H "Authorization: OAuth $TOKEN" \
    "$API/upload?path=$(enc "$remote")&overwrite=true" \
    | sed -n 's/.*"href": *"\([^"]*\)".*/\1/p')
  if [ -z "$href" ]; then
    echo "$(date '+%F %T') нет адреса загрузки для $remote"
    return 1
  fi
  code=$(curl -s -o /dev/null -w '%{http_code}' -T "$file" "$href")
  echo "$(date '+%F %T') $remote -> HTTP $code"
}

remove() {
  curl -s -o /dev/null -X DELETE -H "Authorization: OAuth $TOKEN" \
    "$API?path=$(enc "$1")&permanently=true"
}

# папка на Диске (если уже есть, API вернёт 409)
curl -s -o /dev/null -X PUT -H "Authorization: OAuth $TOKEN" \
  "$API?path=$(enc "$REMOTE_DIR")"

# архив файлов сайта
tar -czf "$TMP_DIR/files_$DATE.tar.gz" -C "$(dirname "$SITE_DIR")" "$(basename "$SITE_DIR")"

# дамп базы (логин и пароль берутся из ~/.my.cnf)
mysqldump --single-transaction "$DB_NAME" | gzip > "$TMP_DIR/db_$DATE.sql.gz"

upload "$TMP_DIR/files_$DATE.tar.gz" "$REMOTE_DIR/files_$DATE.tar.gz"
upload "$TMP_DIR/db_$DATE.sql.gz"    "$REMOTE_DIR/db_$DATE.sql.gz"

# чистим локальные копии и старые файлы на Диске
rm -f "$TMP_DIR/files_$DATE.tar.gz" "$TMP_DIR/db_$DATE.sql.gz"
remove "$REMOTE_DIR/files_$OLD.tar.gz"
remove "$REMOTE_DIR/db_$OLD.sql.gz"

Пароль к базе

Не стоит передавать пароль в командной строке mysqldump: его можно увидеть в списке процессов. Надёжнее создать в домашнем каталоге пользователя, от имени которого запускается скрипт, файл ~/.my.cnf с правами 600:

[client]
user=backup
password=пароль_пользователя_backup

Пользователю достаточно прав на чтение нужной базы (SELECT, LOCK TABLES, SHOW VIEW, TRIGGER). Создайте для резервного копирования отдельную учётную запись, а не используйте root.

Запуск по расписанию

Сделайте скрипт исполняемым и ограничьте доступ к нему, потому что внутри лежит токен:

chmod 700 /usr/local/bin/ya-backup.sh

Затем добавьте задание в crontab -e, например запуск каждую ночь в 03:30 с записью журнала:

30 3 * * * /usr/local/bin/ya-backup.sh >> /var/log/ya-backup.log 2>&1

Проверка и восстановление

  • Запустите скрипт вручную и посмотрите журнал: в строках должен быть код 201 или 202.
  • Убедитесь, что файлы появились в папке backups на Диске и их размер не нулевой.
  • Хотя бы раз проверьте восстановление на тестовом сервере: распакуйте архив командой tar -xzf, а дамп загрузите командой gunzip -c db_ДАТА.sql.gz | mysql имя_базы. Копия, которую ни разу не пробовали развернуть, — это только предположение, что копия есть.

Что можно улучшить

  • Использовать утилиту jq для разбора ответа API вместо sed, если она установлена: jq -r .href.
  • Присылать себе уведомление на почту, если код ответа не 201 и не 202.
  • Для больших каталогов исключать из архива кэш и временные файлы с помощью параметра --exclude у tar.
  • Шифровать архивы (например, gpg), если на сайте хранятся персональные данные.

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

Читайте также