Во второй части плагин научился спрашивать обновления у своего сервера. Теперь сам сервер: где хранятся архивы, как в него попадает новая версия, что он отдаёт клиенту и как узнать, на скольких сайтах стоит плагин, не собирая их адресов. Весь сервер — плагин к этому же сайту на WordPress, около 1200 строк.

Это третья часть цикла «Мастерская разработчика».

Три адреса

АдресЧто делает
GET /wp-json/ach-repo/v1/plugins/<slug>справка о плагине в формате каталога wordpress.org — её читает клиент
POST /wp-json/ach-repo/v1/plugins/<slug>/releaseновый релиз из CI, по токену
GET /plugins/<slug>/download/[?v=1.2.3]архив со счётчиком скачиваний

Релиз одной командой из сборки

curl -fsS -H "Authorization: Bearer $REPO_TOKEN" \
  -F "package=@ach-thumbnail.zip" -F "changelog=$(cat CHANGES.txt)" \
  https://alekseychikun.ru/wp-json/ach-repo/v1/plugins/ach-thumbnail/release

Токен проверяется сравнением за постоянное время — hash_equals, а не ===, чтобы по времени ответа нельзя было подбирать его посимвольно:

function ach_repo_rest_check_token(WP_REST_Request $req): bool
{
    $token = (string) get_option('ach_repo_token', '');
    $auth  = (string) $req->get_header('authorization');
    return $token !== '' && preg_match('~^Bearer\s+(.+)$~i', $auth, $m) && hash_equals($token, trim($m[1]));
}

Функция стоит в permission_callback маршрута. Это важно: REST-маршрут без проверки прав — открытая дверь, и мы уже видели плагин, который так отдавал заявки. Для справки о плагине __return_true — она публичная сознательно.

Источник правды — сам архив

Версию, требования и список изменений сервер берёт не из формы и не из параметров запроса, а из архива: заголовок главного файла плагина и readme.txt. Так версия на сайте не может разойтись с версией в коде. Перед приёмом архив проверяется:

  • всё лежит в папке <slug>/ — иначе WordPress распакует плагин в папку с другим именем, и он встанет рядом со старым вместо обновления;
  • в папке есть файл с заголовком Plugin Name и корректным Version;
  • в заголовке есть Update URI с доменом сервера. Без него сайты, поставившие эту версию, больше никогда не получат обновление. Такую ошибку легче отклонить при загрузке, чем лечить на каждом сайте;
  • такой версии ещё нет — выпущенный архив не перезаписывается: сайты, уже поставившие эту версию, о замене не узнают. Исправление — это новая версия.

Для принятого релиза считается SHA-256, его видно в справке и в заголовке ответа при скачивании.

Кэш страниц мимо API

Сайт работает за кэшем страниц nginx, а справка обязана меняться сразу после релиза, скачивание — считаться. Поэтому ответы API и архивы уходят с X-Accel-Expires: 0: nginx их не хранит. А страницы, где показана версия (карточка плагина, главная, список плагинов), при релизе встают в очередь точечной чистки кэша.

Архивы лежат внутри uploads, чтобы попадать в резервную копию медиа, но прямой доступ к папке закрыт в nginx. Скачать можно только через /plugins/<slug>/download/ — иначе счётчик скачиваний можно обойти.

Счётчик скачиваний без потерь

Два скачивания в одну секунду не должны съесть друг друга. Обычное «прочитать, прибавить, записать» через update_post_meta теряет одно из них, поэтому прибавление делается одним запросом в базе:

$updated = $wpdb->query($wpdb->prepare(
    "UPDATE {$wpdb->postmeta} SET meta_value = meta_value + 1 WHERE post_id = %d AND meta_key = %s",
    $postId, $key
));
if (!$updated) {
    // первого скачивания ещё не было; если параллельный запрос успел добавить — прибавляем к его записи
    add_post_meta($postId, $key, 1, true) || $wpdb->query(/* тот же UPDATE */);
}
wp_cache_delete($postId, 'post_meta');

Сколько сайтов, без адресов

Каждый исходящий запрос WordPress подписывает адресом сайта в User-Agent: WordPress/6.8; https://example.ru. Клиент обновлений его не меняет, и в запросе ещё передаёт установленную версию (?installed=1.3.0). Этого хватает для статистики «сколько сайтов и на какой версии», но адреса хранить незачем:

  • от адреса хранится только хэш (первые 16 символов md5) и время;
  • запись не чаще раза в 6 часов на сайт — WordPress спрашивает обновления дважды в сутки и при заходах в админку;
  • «активный сайт» — обращался за последние 30 дней, старые записи вычищаются.

На странице плагина в админке — «активных сайтов: N» и разбивка по версиям. Видно, сколько сайтов до сих пор сидит на старой версии и стоит ли держать её совместимость.

Нужен свой сервер обновлений для плагинов или расширений?

Напишите — соберу выпуск из CI, проверку архива и учёт установок под ваш процесс.

· полезно?
АЧ
Алексей Чикун веб-разработчик, команда TRIOHEAD

Делаю сайты и сервисы целиком: от вёрстки до сервера. Пишу о том, что проверил на своих проектах.

Комментарии

// загружаю комментарии…

Комментарии публикуются после проверки. Ссылки и HTML не принимаются.