Описание
Инструмент предназначен для создания файлов со статической информацией из базы данных, которая:
- редко меняется (например, справочники)
- подготовка требует длительного времени
- не требует актуальности в режиме реального времени (например, ТОП товаров)
PGHtml реализован как служба, которая создает HTML,JS,JSON и другие типы файлы с использованием данных, получаемых из базы данных PostgreSQL.
Файл создается из файла шаблона, в котором проводится подстановка(замена) SQL-запросов, тегов и переменных.
Шаблон имееет префикс расширения pg, т.е. на основе файла index.pghtml будет создан index.html
Подстановки
$$ [SQL query] $$ | - | результат выполнения SQL запроса |
<pghtml-include> [file] <> | - | содержимое файла |
${[variable]} | - | значение переменной |
<!-- единичное значение из базы данных --> $$ select user $$ <!-- множественые значения --> $$ select tablename from pg_tables where schemaname='pg_catalog' limit 5 $$ <!-- включение файла example_include_ru.html --> <pghtml-include>include-simple-ru.html</pghtml-include>
→
→
<!-- единичное значение из базы данных --> postgres <!-- множественые значения --> pg_statistic pg_type pg_foreign_table pg_proc pg_attribute <!-- включение файла example_include_ru.html --> [ файл example-header-ru.html ]
Шаблон
<!DOCTYPE html> <head> <title>${title}</title> </head> <body> <h2>${caption}</h2> ${text} </body> </html>
Исходная и результирующая страницы
<pghtml-var name="title">Example</pghtml-var> <pghtml-var name="caption">Multiline text</pghtml-var> <pghtml-var name="text"> Text line 1 <br> <i>Text line 2</i> </pghtml-var> <pghtml-include>/template/template-common.html</pghtml-include>
→
→
→
<!DOCTYPE html> <head> <title>Example</title> </head> <body> <h2>Multiline text</h2> Text line 1 <br> <i>Text line 2</i> </body> </html>
- Проверка на изменение - если содержимое результирующего файл не изменилось, то файл не перезаписывается и, соответственно, web-сервер не выполняет публикацию (deploy)
- Импорт - реализована поддержка импорта других файлов (например, фрагмента HTML или данных в формате JSON)
- Список изменений - возможно сформировать файл-список с измененными файлами
- Обработка ошибок - при возникновении ошибки (например, при выполнении SQL запроса) обработка продолжается, в лог (stderr) выводится расширенная информация и по окончании выполнения утилита возвращает в ОС признак неуспешного выполнения (1). Это позволяет настроить мониторинг обновления данных на сайте
- Указание обрабатываемых файлов - возможна обработка конкретных файлов, директорий с рекурсией и указанием расширений файлов. При этом определяются встроенные переменные, которые можно использовать при формировании ссылок и в SQL запросах. Рекомендуется указывать корневую директорию сайта
- Переменные - поддерживаются переменные из командной строки, встроенные (директории, пути до файлов и т.д) и атрибуты тега при импорте файла
- Неограниченная вложенность - поддерживается включение файлов внутри включаемого файла, многократное переопределение переменных и их использование внутри другой переменной
- Условия и циклы - программирование внутри файлов не поддерживается, для реализации логики необходимо использовать хранимые процедуры PostgreSQL
Вызов из командной строки
[postgres@pgsuite ~]$ pghtml execute -hd /tmp/example -d pgsuite PGHtml is HTML template engine using PostgreSQL version 24.2.7, linux 64 bits Parameters HTTP directory: /tmp/example sync interval: 600s port: 5480 database host: 127.0.0.1 port: 5432 database: pgsuite host for service: 127.0.0.1 user for service: postgres administration port: 15480 2024-07-06 10:05:11.404 INFO ADMIN_SERVER thread started, thread_id: 27809 2024-07-06 10:05:11.404 INFO ADMIN_SERVER listening socket bound to 127.0.0.1:15480 2024-07-06 10:05:11.404 INFO FILE_MAKER thread started, thread_id: 27810 2024-07-06 10:05:11.405 INFO FILE_MAKER connecting to database, URI: postgresql://127.0.0.1:5432/pgsuite?user=postgres 2024-07-06 10:05:11.411 INFO FILE_MAKER connected to database, pid: 27811, user: postgres, client_encoding: UTF8, server version: 140002 2024-07-06 10:05:11.411 INFO FILE_MAKER syncing /tmp/example directory 2024-07-06 10:05:11.411 INFO FILE_MAKER processing data/top-indexes-scans.pgjson template 2024-07-06 10:05:11.428 INFO FILE_MAKER file data/top-indexes-scans.json updated 2024-07-06 10:05:11.428 INFO FILE_MAKER processing example-full.pghtml template 2024-07-06 10:05:11.456 INFO FILE_MAKER file example-full.html updated 2024-07-06 10:05:11.456 INFO FILE_MAKER processing example-simple.pghtml template 2024-07-06 10:05:11.458 INFO FILE_MAKER file example-simple.html does not update required 2024-07-06 10:05:11.458 INFO FILE_MAKER processing example-template.pghtml template 2024-07-06 10:05:11.458 INFO FILE_MAKER file example-template.html does not update required 2024-07-06 10:05:11.458 INFO FILE_MAKER processing ru/example-simple-ru.pghtml template 2024-07-06 10:05:11.459 INFO FILE_MAKER file ru/example-simple-ru.html does not update required 2024-07-06 10:05:11.459 INFO FILE_MAKER directory synced successfully 2024-07-06 10:05:11.459 INFO FILE_MAKER disconnected from database, pid: 27811
Преобразование файла example.pghtml в example.html
<!DOCTYPE html> <html> <head> <!-- inclusion of styles for acceleration --> <style type="text/css"> <pghtml-include>/css/common.css</pghtml-include> </style> <!-- inclusion of title with two variables "G_SITE" and "description" --> <pghtml-include description="Example">/include/title.html</pghtml-include> </head> <body> <!-- single value from database --> <p>Database uptime</p> $$ select now() - pg_postmaster_start_time() $$ <br><br> <!-- dataset --> <p>Top 10 indexes by size, table</p> <table> <tr><th>Schema</th><th>Index</th><th>Size</th></tr> $$ select '<tr><td>'||schemaname||'</td><td>'||indexname||'</td><td class="td-number">'||size||'</td></tr>' from ( select schemaname,indexname,pg_total_relation_size((schemaname||'.'||tablename)::regclass) size from pg_indexes order by 3 desc limit 10 ) t $$ </table> <br><br> <!-- dataset from json --> <p>Top 10 indexes by scans, json</p> <div id="top_indexes_scans" style="width: 600px; height: 200px"> <button onclick="loadFile('top_indexes_scans', 'data/top_indexes_scans.json')"> Load file 'data/top_indexes_scans.json' </button> </div> <script> function loadFile(element_id, filename) { var xhr = new XMLHttpRequest(); xhr.onreadystatechange = function() { if (xhr.readyState == 4) { if (xhr.status == 200) { document.getElementById(element_id).innerHTML = "<pre>"+xhr.responseText+"</pre>"; document.getElementById(element_id).style.border = "1px solid black"; } else { document.getElementById(element_id).innerHTML = "Page must be opened via web server,<br>view file '"+filename+"' manually"; } } } xhr.open('GET', filename, true); xhr.send(); } </script> <br><br> <!-- variables --> <p>Variables</p> <table> <tr><th>Name</th><th>Value</th></tr> <tr> <td>${directory_root}</td> <td>${directory_root}</td> </tr> <tr> <td>${directory}</td> <td>${directory}</td> </tr> <tr> <td>${filename}</td> <td>${filename}</td> </tr> <tr> <td>${filepath_source}</td> <td>${filepath_source}</td> </tr> <tr> <td>${filepath_dest}</td> <td>${filepath_dest}</td> </tr> <tr> <td>${G_SITE}</td> <td>${G_SITE}</td> </tr> </table> <br><br> <pghtml-include>include/footer.html</pghtml-include> </body> </html>
→
→
→
→
<!DOCTYPE html> <html> <head> <!-- inclusion of styles for acceleration --> <style type="text/css"> html { font: 12px Verdana, Arial, Helvetica, sans-serif; } p { font-size: 16px; font-weight: bold; } table { border-collapse: collapse; } td, th { padding: 3px; border: 1px solid black; } th { background: silver; } .td-number { text-align: right; } </style> <!-- inclusion of title with two variables "G_SITE" and "description" --> <title>Example</title> </head> <body> <!-- single value from database --> <p>Database uptime</p> 12 days 00:30:43.890922 <br><br> <!-- dataset --> <p>Top 10 indexes by size, table</p> <table> <tr><th>Schema</th><th>Index</th><th>Size</th></tr> <tr><td>pg_catalog</td><td>pg_depend_reference_index</td><td class="td-number">1236992</td></tr> <tr><td>pg_catalog</td><td>pg_depend_depender_index</td><td class="td-number">1236992</td></tr> <tr><td>pg_catalog</td><td>pg_proc_oid_index</td><td class="td-number">1138688</td></tr> <tr><td>pg_catalog</td><td>pg_proc_proname_args_nsp_index</td><td class="td-number">1138688</td></tr> <tr><td>pg_catalog</td><td>pg_attribute_relid_attnam_index</td><td class="td-number">778240</td></tr> <tr><td>pg_catalog</td><td>pg_attribute_relid_attnum_index</td><td class="td-number">778240</td></tr> <tr><td>pg_catalog</td><td>pg_rewrite_rel_rulename_index</td><td class="td-number">688128</td></tr> <tr><td>pg_catalog</td><td>pg_rewrite_oid_index</td><td class="td-number">688128</td></tr> <tr><td>pg_catalog</td><td>pg_collation_oid_index</td><td class="td-number">540672</td></tr> <tr><td>pg_catalog</td><td>pg_collation_name_enc_nsp_index</td><td class="td-number">540672</td></tr> </table> <br><br> <!-- dataset from json --> <p>Top 10 indexes by scans, json</p> <div id="top_indexes_scans" style="width: 600px; height: 200px"> <button onclick="loadFile('top_indexes_scans', 'data/top_indexes_scans.json')"> Load file 'data/top_indexes_scans.json' </button> </div> <script> function loadFile(element_id, filename) { var xhr = new XMLHttpRequest(); xhr.onreadystatechange = function() { if (xhr.readyState == 4) { if (xhr.status == 200) { document.getElementById(element_id).innerHTML = "<pre>"+xhr.responseText+"</pre>"; document.getElementById(element_id).style.border = "1px solid black"; } else { document.getElementById(element_id).innerHTML = "Page must be opened via web server,<br>view file '"+filename+"' manually"; } } } xhr.open('GET', filename, true); xhr.send(); } </script> <br><br> <!-- variables --> <p>Variables</p> <table> <tr><th>Name</th><th>Value</th></tr> <tr> <td>${directory_root}</td> <td>${directory_root}</td> </tr> <tr> <td>${directory}</td> <td></td> </tr> <tr> <td>${filename}</td> <td>example-full.pghtml</td> </tr> <tr> <td>${filepath_source}</td> <td>/tmp/build/example/example-full.pghtml</td> </tr> <tr> <td>${filepath_dest}</td> <td>/tmp/build/example/example-full.html</td> </tr> <tr> <td>${G_SITE}</td> <td>${G_SITE}</td> </tr> </table> <br><br> ----- <br> <span style="font-size: 75%"> info updated at 2024-07-01 18:56:20.414276+03 </span> </body> </html>
Преобразование файла top_indexes_scans.pgjson в top_indexes_scans.json
$$ select json_agg(t) from ( select schemaname "schema",indexrelname "index",idx_scan scans from pg_catalog.pg_stat_all_indexes order by idx_scan desc limit 10 ) t; $$
→
→
[{"schema":"pg_catalog","index":"pg_db_role_setting_databaseid_rol_index","scans":282876}, {"schema":"pg_catalog","index":"pg_database_oid_index","scans":282760}, {"schema":"pg_catalog","index":"pg_attribute_relid_attnum_index","scans":71261}, {"schema":"pg_catalog","index":"pg_database_datname_index","scans":70752}, {"schema":"pg_catalog","index":"pg_class_oid_index","scans":35869}, {"schema":"pg_catalog","index":"pg_index_indexrelid_index","scans":17804}, {"schema":"pg_catalog","index":"pg_opclass_oid_index","scans":17343}, {"schema":"pg_catalog","index":"pg_amproc_fam_proc_index","scans":17336}, {"schema":"pg_catalog","index":"pg_index_indrelid_index","scans":653}, {"schema":"pg_catalog","index":"pg_class_relname_nsp_index","scans":411}]
Файлы примера находятся вместе с исходным кодом в общем архиве, который можно загрузить со страницы Загрузка
Обзорное видео размещено на Youtube