Во второй части плагин научился спрашивать обновления у своего сервера. Теперь сам сервер: где хранятся архивы, как в него попадает новая версия, что он отдаёт клиенту и как узнать, на скольких сайтах стоит плагин, не собирая их адресов. Весь сервер — плагин к этому же сайту на 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, проверку архива и учёт установок под ваш процесс.
// загружаю комментарии…