Сайт Романа ПарпалакаБлог20250724

Распаковка сжатых URL на сервере

24 июля 2025 года, 23:41

Недавно я рассказывал, как использовать API браузеров для сжатия адресов страниц. В продолжение опишу, как эти сжатые адреса распаковыать на сервере на примере того же сервиса генерации картинок с формулами.

Текущая версия конфига nginx для обработки и старых несжатых URL, и новых сжатых получилась такой:

location ~ ^(?s)/(?<ext>svg|png)(?<is_base64>b?)/(?<formula>.*)$ {
    gunzip        on;
    gzip_static   always;
    gzip_vary     on;
    gzip_proxied  expired no-cache no-store private auth;

    expires 1d;

    add_header 'Access-Control-Allow-Origin' '*' always;
    add_header 'Access-Control-Allow-Methods' 'GET, OPTIONS' always;

    set $compress_error 0;

    set_by_lua_block $file_path {
        local ext = ngx.var.ext
        local is_base64 = ngx.var.is_base64
        local formula = ngx.var.formula

        if is_base64 == "b" then
            local base64 = require "ngx.base64"
            local zlib = require "zlib"

            local compressed, err = base64.decode_base64url(formula)
            if not compressed then
                ngx.log(ngx.ERR, "base64 decode error: ", err)
                ngx.var.compress_error = 1
                return ""
            end

            local inflator = zlib.inflate(-15)
            local ok, decoded_formula = pcall(inflator, compressed)
            if not ok then
                ngx.log(ngx.ERR, "deflate decompress error: ", decoded_formula)
                ngx.var.compress_error = 1
                return ""
            end

            formula = decoded_formula
        end

        formula = formula:gsub("^%s*(.-)%s*$", "%1")

        local md5 = ngx.md5(formula)
        return md5:sub(1, 2) .. "/" .. md5:sub(3, 4) .. "/" .. md5:sub(5) .. "." .. ext
    }

    if ($compress_error) {
        return 400;
    }

    if (-f $document_root/_error/$file_path) {
        return 400;
    }

    rewrite ^ /_cache/$file_path break;
    error_page 404 = @s2_latex_renderer;
    log_not_found off;
}

location @s2_latex_renderer {
    add_header 'Access-Control-Allow-Origin' '*' always;
    add_header 'Access-Control-Allow-Methods' 'GET, OPTIONS' always;

    include         /etc/nginx/fastcgi.conf;
    fastcgi_pass    php-tex-sock;
    fastcgi_param   SCRIPT_FILENAME $document_root/render.php;
    fastcgi_param   SCRIPT_NAME /render.php;

    fastcgi_cache_key "$request_method$uri"; # В $uri УРЛ после rewrite, напр. "/_cache/4d/81/658b25df7544f9e2d0cb7f4dc402.svg"
    fastcgi_cache i_upmath;
    fastcgi_cache_valid 200 10m;
    fastcgi_cache_methods GET HEAD;
    fastcgi_cache_lock on;
    fastcgi_cache_lock_age 9s;
    fastcgi_cache_lock_timeout 9s;

    fastcgi_buffers 8 16k;
    fastcgi_buffer_size 32k;
    fastcgi_connect_timeout 90;
    fastcgi_send_timeout 90;
    fastcgi_read_timeout 300;
}

Чтобы встраивать lua-скрипты в конфиг nginx через set_by_lua_block, в Debian достаточно установить пакет nginx-extras. Для распаковки сжатого текста в этом скрипте через функции zlib также требуется установить пакет lua-zlib.

Напомню алгоритм обработки адресов картинок. Исходник формулы, например, x^2, извлекается из адреса и декодируется. Вычисляется md5-хеш от исходника и на основе хеша определяется путь к файлу с закешированной картинкой. Если такой файл после преобразования URL нашелся (rewrite ^ /_cache/$file_path break;), то nginx отдает его содержимое напрямую. Если файла нет, то запрос передается в php-скрипт, запускающий генерацию svg-картинки через TeX Live и оптимизацию через SVGO (error_page 404 = @s2_latex_renderer;).

Раньше вместо rewrite и error_page я использовал более современную и подходящую директиву try_files. Но она перестает работать после активации модуля gunzip. Этот модуль позволяет держать в файловом кеше только сжатые версии файлов с расширением .gz, экономя место на диске. Причем для обработки большинства запросов от нормальных браузеров, поддерживающих gzip-сжатие трафика, nginx даже не будет распаковывать gz-файлы, а отправлять их как есть. Почему rewrite корректно работает с модулем gunzip, а try_files — нет, не очень понятно. Но что есть, то есть.

В конфиге есть интересный момент, связанный с кешем внутри nginx (инструкции fastcgi_cache*). Он предотвращает race condition при одновременном запросе формулы, которой нет в файловом кеше. В противном случае nginx будет передавать в php-скрипт запросы с одинаковыми аргументами, и одна и та же формула будет рендериться параллельно. Об этой технике я писал отдельно.

Для распаковки полученного фрагмента URL в PHP подойдет следующий код:

public static function decodeCompressedFormula(string $compressed): string
{
    $base64     = strtr($compressed, '-_', '+/'); // URL-safe base64 to standard
    $compressed = base64_decode($base64);

    $result = @gzinflate($compressed);
    if ($result === false) {
        throw new \RuntimeException('Failed to decompress formula.');
    }
    return $result;
}

В PHP весь алгоритм уместился в три строки — как минимум в несколько раз короче, чем в JS и nginx/lua. Такими моментами PHP радует меня до сих пор.

Поделиться

Cайту 20 лет Ctrl

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

Отладка запросов к FastCGI из консоли
Обычно протокол FastCGI применяется для общения между веб-сервером и бэкендом. Например, связка nginx и PHP-FPM работает по этому протоколу.
2023
Латех и веб-технологии
В прошлый раз я рассказал о своем сервисе, который генерирует для веба картинки с математическими формулами на латехе.
2014
Кеширование в nginx
В прошлый раз мы рассмотрели, как в теории работает кеш и какие ошибки обычно совершают при его программировании.
2020
Нативное gzip-сжатие в JS
Я недавно закрыл тикет на гитхабе, который висел с 2017 года.
2025

Оставьте свой комментарий


Формулы на латехе: $$f(x) = x^2-\sqrt{x}$$ превратится в $$f(x) = x^2-\sqrt{x}$$.
Выделение текста: [i]курсивом[/i] или [b]жирным[/b].
Цитату оформляйте так: [q = имя автора]цитата[/q] или [q]еще цитата[/q].
Других команд или HTML-тегов здесь нет.

Записи