Compare commits

...

64 Commits

Author SHA1 Message Date
serr c8f89013c6 добавил смену тайтла раз в секунду 2025-06-12 23:20:34 +03:00
serr ad08bc441e изменил стили details блока, добавил любимые сайты в блок things i love под details 2025-06-12 22:56:12 +03:00
serr e73956733c marquee added 2025-06-12 01:40:56 +03:00
serr 1124172331 some small changes 2025-06-12 00:32:55 +03:00
serr fc0c5c43f1 small change 2025-06-08 22:52:49 +03:00
serr dec305dcd7 some changes 2025-06-08 20:54:08 +03:00
serr 0d33167beb фикс подсветки ссылок при нажатии на телефоне 2025-06-08 20:32:38 +03:00
serr dc2c414e26 из конфига убрал домен, так как это не нужно 2025-06-08 18:09:11 +03:00
serr 9c43e3694f change site title 2025-06-08 18:01:50 +03:00
serr efde4b7ea3 логи теперь идут не в системный файл, а в файл для логов, который указывается в конфиге 2025-06-08 17:37:17 +03:00
serr b88809e888 чуть поменял конфиг 2025-06-08 16:02:27 +03:00
serr 6d2403d2f5 переписал конфиг чуть чуть, разбил на встраиваемые структуры 2025-06-08 15:55:23 +03:00
serr ca6bd5bd63 количество страниц в кэше теперь логируется раз в указанный в конфиге интервал 2025-06-08 14:31:11 +03:00
serr db9e51d623 раз в полчаса сообщение в лог о кол-ве элементов в кэше страниц 2025-06-08 14:15:56 +03:00
serr a0fe27424d закругленные края у блоков с кодом 2025-06-08 13:57:48 +03:00
serr 9fe32a1a7a update logging middleware 2025-06-08 01:03:46 +03:00
serr 8dae9dd8ba some changes 2025-06-08 00:49:51 +03:00
serr af16f6dc7b some change 2025-06-08 00:32:40 +03:00
serr b3ada8c5ab some change 2025-06-08 00:31:06 +03:00
serr 17ff94aa54 some change 2025-06-08 00:28:10 +03:00
serr e88a869167 добавил атрибут data-scroll-top опциональный к элементам которых могут вызвать hx-swap, наличие атрибута говорит о том, что после свапа надо проскроллить страничку в самый верх 2025-06-08 00:22:39 +03:00
serr 71084be5e3 ссылки, ведущие на мой же сайт, сделаны градиентом 2025-06-08 00:09:46 +03:00
serr 2cea7c8bb4 some changes 2025-06-08 00:00:32 +03:00
serr 185ac4bf71 обновление трека через htmx 2025-06-07 23:48:37 +03:00
serr 2d4a6c77a5 сделал ajax с использованием htmx 2025-06-07 23:42:10 +03:00
serr 0540bdf1d2 some changes 2025-06-06 22:00:09 +03:00
serr 16483803a5 номер текущей странички выводится в блоке пагинации в /posts 2025-06-06 18:34:17 +03:00
serr 21ce6944f8 теперь посты сортируются по времени создания, убрал суффикс .md в url постов 2025-06-06 18:19:08 +03:00
serr e1e95848fa сделал пагинацию /posts 2025-06-06 18:06:34 +03:00
serr 380cec2c5a сделал пагинацию /posts 2025-06-06 18:04:58 +03:00
serr 4327a14052 small css change 2025-06-06 01:30:42 +03:00
serr 71e2c43dbb закругления у <a>, чуть чуть поменял текст в header 2025-06-06 01:23:22 +03:00
serr 6dbfdf7193 new font-family 2025-06-06 00:24:27 +03:00
serr 6f6e807d60 old font size 2025-06-06 00:04:27 +03:00
serr 2e58c76f75 увеличил размер шрфита для десктопов 2025-06-04 19:12:47 +03:00
serr 46ef8967b1 убрал конвертацию string в []byte в контроллере главной страницы, сделал запись в w http.ResponseWriter через fprintf 2025-05-25 19:12:06 +03:00
serr cc3fe33474 пофиксил расстояния от краев div до блока pre 2025-05-24 23:49:02 +03:00
serr a12896c134 some styles.css changes 2025-05-24 23:29:15 +03:00
serr 462bc656cf в details > summary сделал обычный cursor: pointer при наведении 2025-05-24 21:59:19 +03:00
serr 0ae0f2d6d2 сдел цвет выделения феолетовым 2025-05-24 15:47:23 +03:00
serr 6c46ce117c символ 🅴 заменяю на [E] в строке с треком 2025-05-20 14:26:09 +03:00
serr 07ef43e7fd функция renderpostpage теперь является методом post 2025-05-19 18:47:49 +03:00
serr e6b1229d6b добавил mod time в шаблон поста 2025-05-19 18:38:00 +03:00
serr edddfece3f в блок с последним прослушанным треком добавил предупреждение о том, что необходим js 2025-05-19 18:18:03 +03:00
serr 06a0553978 some changes 2025-05-19 17:10:51 +03:00
serr 8c5c5d6150 теперь сервер раз в 30 секунд обновляет последний прослушанный трек, добавил поле под него в app 2025-05-19 17:03:18 +03:00
serr 9aa39d8f65 добавил config.json в качестве одной из целей в eye.sh 2025-05-18 14:29:52 +03:00
serr b28d08f95e Данные от ластфм вынесены в конфиг 2025-05-18 14:11:59 +03:00
serr 4e2f70668d change footer pic 2025-05-18 13:41:24 +03:00
serr 8619472986 добавил вращение изображений, пофиксил index out of range в получении треков с lastfm, добавил открывающийся по нажатию блок с дополнительной системной информацией 2025-05-17 21:31:56 +03:00
serr 9643b76d92 добавил свечение в правой верхней четверти картинки из header 2025-05-17 20:15:10 +03:00
serr 8485185430 неразрывные пробелы у символов музыкальных нот в блоке lastfm 2025-05-17 18:07:42 +03:00
serr dd0f516fb6 add lastfm last track 2025-05-17 15:45:21 +03:00
serr 6691fea238 some change 2025-05-17 13:15:30 +03:00
serr 8c2062f86e del some styles 2025-05-17 13:11:11 +03:00
serr a90cadb48d delete some pics, add favi 2025-05-17 12:38:33 +03:00
serr 1aab4ce6e4 some redesign 2025-05-17 12:20:48 +03:00
serr 70022c3b78 медиа-запрос для мобилок до 800 пикс. 2025-05-10 00:28:43 +03:00
serr 9726b272fd some redesign moments 2025-05-09 20:13:52 +03:00
serr 9f376fbca5 закругленные края блоков 2025-05-09 18:34:03 +03:00
serr b76c69a349 обновил гифки по умолчанию, показывающиеся до получения ответа с количеством реквестов 2025-05-09 18:24:14 +03:00
serr 75f16f65a4 обработку страничек постов вынес из лекс.блока с обработкой главных страничек 2025-05-09 18:10:56 +03:00
serr 761760875f del ul-li structures 2025-05-01 00:48:39 +03:00
serr bb7bc5c006 add code blocks 2025-05-01 00:28:51 +03:00
51 changed files with 900 additions and 303 deletions

4
.gitignore vendored
View File

@ -1,2 +1,4 @@
config.json config.json
hikan.ru hikan.ru
*.server*
*.log

View File

@ -1,13 +1,17 @@
/* Design idea from here https://mo.rijndael.cc/ */ ::selection {
background: purple;
color: #fff;
}
body { body {
font-family: -apple-system,system-ui,blinkmacsystemfont,Segoe UI,roboto,oxygen,ubuntu,Helvetica Neue,arial,sans-serif;
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
margin: 0; margin: 0;
align-items: flex-start; align-items: flex-start;
padding: 10px; padding: 10px;
background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHdpZHRoPSc0MCcgaGVpZ2h0PSc0MCc+PHJlY3QgZmlsbD0nIzkxZWFlNCcgeD0nMCcgeT0nMCcgd2lkdGg9JzIwcHgnIGhlaWdodD0nMjBweCcvPjxyZWN0IGZpbGw9JyM4NmE4ZTcnIHg9JzAnIHk9JzIwJyB3aWR0aD0nMjBweCcgaGVpZ2h0PScyMHB4Jy8+PHJlY3QgZmlsbD0nIzkxZWFlNCcgeD0nMjAnIHk9JzIwJyB3aWR0aD0nMjBweCcgaGVpZ2h0PScyMHB4Jy8+PHJlY3QgZmlsbD0nIzg2YThlNycgeD0nMjAnIHk9JzAnIHdpZHRoPScyMHB4JyBoZWlnaHQ9JzIwcHgnLz48L3N2Zz4="); background-image: url("/assets/pic/star.gif");
background-repeat: repeat; background-color: #000;
} }
header, main, footer { header, main, footer {
@ -21,38 +25,154 @@ header, footer {
flex: 2; flex: 2;
} }
marquee {
font-weight: bold;
color: #b1cfd8;
}
main { main {
flex: 3; flex: 3;
} }
div { div {
text-align: left; text-align: left;
background-color: white; background-color: #b1cfd8;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.5); box-shadow: 0 0 3px 3px rgba(135, 182, 196, 0.95);
box-sizing: border-box; box-sizing: border-box;
border: 1px solid; border: 1px solid;
border-radius: 10px;
width: 100%; width: 100%;
padding: 0 20px; padding: 0 20px;
overflow: hidden;
}
.pagi {
color: #b1cfd8;
text-align: center;
background-color: transparent;
box-shadow: none;
border: none;
font-size: 1.5em;
} }
h1 { h1 {
text-align: center; text-align: center;
box-sizing: border-box; padding: 30px;
border-top: 1px solid; position: relative;
border-bottom: 1px solid; }
h1::before, h1::after {
content: "";
position: absolute;
left: 0;
right: 0;
height: 10px;
background: url('data:image/svg+xml;utf8,<svg viewBox="0 0 100 10" xmlns="http://www.w3.org/2000/svg"><path d="M0,5 Q25,10 50,5 T100,5" fill="none" stroke="%236a8498" stroke-width="2"/></svg>');
}
h1::before { top: 0; }
h1::after { bottom: 0; }
a {
cursor: pointer;
color: black;
text-decoration: none;
padding: 0 5px;
background-image: linear-gradient(to bottom, transparent 60%, #77abbb 60%);
background-repeat: no-repeat;
border-radius: 2px;
} }
.count { a:hover {
display: flex; background-image: linear-gradient(to bottom, transparent 60%, #778bbb 60%);
justify-content: center;
} }
.count img { a[hx-get] {
height: 75px; background-image: linear-gradient(to right, #77abbb, #778bbb);
} }
@media (max-width: 1200px) { a[hx-get]:hover {
background-image: linear-gradient(to right, #778bbb, #778bbb);
}
pre {
padding: 10px;
background: #77abbb;
border: 1px solid #000;
border-radius: 2px;
overflow-x: auto;
white-space: pre;
word-wrap: normal;
width: 0;
min-width: calc(100% - 22px);
}
details > summary {
cursor: pointer;
display: inline-block;
width: auto;
}
details[open] > summary {
cursor: pointer;
display: inline-block;
width: auto;
}
.rotating-pic-container {
width: 100%;
height: 100%;
overflow: hidden;
}
.rotating-pic {
width: 100%;
height: 100%;
animation: swing 1.5s ease-in-out infinite alternate;
}
@keyframes swing {
0% { transform: rotate(-10deg); }
100% { transform: rotate(10deg); }
}
.pulse-container {
text-align: center;
}
.pulse-text {
font-weight: bold;
color: purple;
animation: scalePulse 2s infinite ease-in-out;
}
@keyframes scalePulse {
0%, 100% {
transform: scale(1);
}
50% {
transform: scale(1.05);
}
}
@media (max-width: 800px) {
a:hover {
background-image: linear-gradient(to bottom, transparent 60%, #77abbb 60%);
}
a[hx-get]:hover {
background-image: linear-gradient(to right, #77abbb, #778bbb);
}
header, footer, main { header, footer, main {
flex: 1 100%; flex: 1 100%;
} }
details > summary {
cursor: default;
}
details[open] > summary {
cursor: default;
}
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

BIN
assets/pic/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 488 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

BIN
assets/pic/heh.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

BIN
assets/pic/miffy.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 KiB

BIN
assets/pic/star.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

1
assets/scripts/htmx.js Normal file

File diff suppressed because one or more lines are too long

4
eye.sh
View File

@ -3,8 +3,8 @@ stty -echoctl # Отключает вывод управляющих симво
# НАСТРОЙКА СКРИПТА ТУТ ########################################################### # НАСТРОЙКА СКРИПТА ТУТ ###########################################################
DURATION=1 # Задержка между проверками в секундах DURATION=1 # Задержка между проверками в секундах
WATCH_TARGETS=("assets" "mvc" "posts" "tools" "main.go") # Массив целей для наблюдения (директории и файлы) WATCH_TARGETS=("assets" "mvc" "posts" "tools" "main.go" "config.json") # Массив целей для наблюдения (директории и файлы)
BINARY_PATH="./main" # Путь до бинарного файла BINARY_PATH="./main.server" # Путь до бинарного файла
BUILD_CMD="go build -o $BINARY_PATH main.go" # Команда для сборки BUILD_CMD="go build -o $BINARY_PATH main.go" # Команда для сборки
################################################################################### ###################################################################################

66
main.go
View File

@ -8,19 +8,55 @@ import (
"main/mvc/models" "main/mvc/models"
"main/tools" "main/tools"
"net/http" "net/http"
"os"
"time"
) )
func main() { func main() {
var err error var err error
var app *models.App var app *models.App
var logFile *os.File
defer logFile.Close()
// Инициализация приложения // Инициализация приложения
if app, err = models.InitApp(); err != nil { if app, err = models.InitApp(); err != nil {
log.Fatal(err) log.Fatal(err)
} }
// Запуск тикеров
{
// Переоткрытие файла с логами периодически чтобы он не стал слишком большим
tools.Ticker(func() {
newLogFile, err := tools.SetupLogging(app.Cfg.LogFilePath)
if err != nil {
log.Fatal(err)
}
logFile.Close()
logFile = newLogFile
log.Println("Logging system restarted")
}, time.Second*app.Cfg.LogSystemRestartInterval)
// Добавление префикса в виде домена сервера к записям в лог // Обновление последнего прослушанного трека ластфм
log.SetPrefix(fmt.Sprintf("%s | ", app.Cfg.ServerDomain)) tools.Ticker(func() {
var lastTrack string
if lastTrack, err = models.LastFMGetLastTrack(app.Cfg.LastFMUsername, app.Cfg.LastFMToken); err != nil {
log.Println(err)
return
}
app.LastfmLastTrack = lastTrack
}, time.Second*app.Cfg.LastFMUpdateInterval)
// Сообщение в лог о количестве элементов в кэше
tools.Ticker(func() {
log.Println("Pages in cache:")
if len(app.PagesCache.Data) != 0 {
for key := range app.PagesCache.Data {
log.Println(key)
}
} else {
log.Println("No pages in the cache")
}
}, time.Second*app.Cfg.CacheLogInterval)
}
// Настройка маршрутов // Настройка маршрутов
router := setupRoutes(app) router := setupRoutes(app)
@ -49,13 +85,25 @@ func setupRoutes(app *models.App) *http.ServeMux {
{ {
// Обработка главной страницы // Обработка главной страницы
router.Handle("/", m(controllers_pages.MainPageHandler(app))) router.Handle("/", m(controllers_pages.MainPageHandler(app)))
// Обработка страницы со списком постов }
router.Handle("/posts/", m(controllers_pages.PostsPageHandler(app)))
// Обработка страничек постов // Странички со списками постов
for key := range app.Posts { postsPagesCount := (len(app.PostsMap) + app.Cfg.PostsMaxCountOnPage - 1) / app.Cfg.PostsMaxCountOnPage
postLink := string(key) for i := range postsPagesCount {
router.Handle(postLink, m(controllers_pages.PostPageHandler(app))) router.Handle(
} fmt.Sprintf("/posts/%d", i),
m(controllers_pages.PostsPageHandler(app)))
}
// Обработка страничек постов
for key := range app.PostsMap {
postLink := string(key)
router.Handle(postLink, m(controllers_pages.PostPageHandler(app)))
}
// Api
{
router.Handle("/api/lastfm", m(controllers.LastFMHandler(app)))
} }
return router return router

View File

@ -1,6 +1,7 @@
package controllers_pages package controllers_pages
import ( import (
"fmt"
"log" "log"
"main/mvc/models" "main/mvc/models"
@ -13,12 +14,15 @@ import (
func MainPageHandler(app *models.App) http.HandlerFunc { func MainPageHandler(app *models.App) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var err error var (
pageData []byte
err error
)
// Количество запросов, обработанных сервером за 24ч // Количество запросов, обработанных сервером за 24ч
if r.Method == "COUNT" { if r.Method == "COUNT" {
var count []byte var count int
if count, err = tools.GetJournalctlLogsCount("server", app.Cfg.ServerDomain, 24); err != nil { if count, err = tools.GetLogFileEntriesCount(app.Cfg.LogFilePath, "duration: ", 24); err != nil {
log.Printf("%s", err.Error()) log.Printf("%s", err.Error())
} }
sendCount(w, count) sendCount(w, count)
@ -37,17 +41,24 @@ func MainPageHandler(app *models.App) http.HandlerFunc {
return return
} }
// Страничка рендерится только если ее нет в кэше cacheKey := models_pages.MainPageTmplName
pageData, ok := app.PagesCache.Get(models_pages.MainPageTmplName) ajax := r.URL.Query().Get("ajax") == "true"
if !ok { if ajax {
pageData, err = models_pages.RenderMainPage(app.Templates, app.Version) cacheKey += "?ajax=true"
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
app.PagesCache.Set(models_pages.MainPageTmplName, pageData)
} }
if pageData, ok := app.PagesCache.Get(cacheKey); ok {
sendMainPage(w, pageData)
return
}
pageData, err = models_pages.RenderMainPage(app.Templates, app.Version, ajax)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
app.PagesCache.Set(cacheKey, pageData)
sendMainPage(w, pageData) sendMainPage(w, pageData)
}) })
} }
@ -60,17 +71,17 @@ func sendMainPage(w http.ResponseWriter, data []byte) {
} }
// Ответ на метод COUNT // Ответ на метод COUNT
func sendCount(w http.ResponseWriter, data []byte) { func sendCount(w http.ResponseWriter, data int) {
w.Header().Set("Content-Type", "text/plain; charset=utf-8") w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
w.Write(data) fmt.Fprint(w, data)
} }
// Ответ на метод LOVE // Ответ на метод LOVE
func sendLove(w http.ResponseWriter) { func sendLove(w http.ResponseWriter) {
w.Header().Set("Content-Type", "text/plain; charset=utf-8") w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
w.Write([]byte("13.01.2005\n")) fmt.Fprint(w, "13.01.2005\n")
} }
// Ответ на метод LIMINAL // Ответ на метод LIMINAL
@ -78,6 +89,5 @@ func sendLiminal(w http.ResponseWriter) {
w.Header().Set("Content-Type", "text/plain; charset=utf-8") w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
text := "If you're not careful and you slip out of reality in the wrong place, you'll end up in the Backstage, where there's nothing but the stench of old damp carpet, yellow-colored madness, the endless unbearable hum of fluorescent lights, and roughly six hundred million square miles of randomly arranged empty rooms.\n" text := "If you're not careful and you slip out of reality in the wrong place, you'll end up in the Backstage, where there's nothing but the stench of old damp carpet, yellow-colored madness, the endless unbearable hum of fluorescent lights, and roughly six hundred million square miles of randomly arranged empty rooms.\n"
w.Write([]byte(text)) fmt.Fprint(w, text)
} }

View File

@ -4,37 +4,37 @@ import (
"main/mvc/models" "main/mvc/models"
"main/mvc/models/models_pages" "main/mvc/models/models_pages"
"net/http" "net/http"
"strings"
) )
// Обработчик главной страницы // Обработчик страницы поста
func PostPageHandler(app *models.App) http.HandlerFunc { func PostPageHandler(app *models.App) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var err error link := r.URL.Path
cacheKey := link
postLink := r.URL.Path ajax := r.URL.Query().Get("ajax") == "true"
if ajax {
// Ссылки на посты имеют вид postLink = /link/, а если прилетело что то типо /link/123123, cacheKey += "?ajax=true"
// то надо оставить только часть /link/
secondSlash := strings.IndexByte(postLink[1:], '/')
if secondSlash != -1 {
postLink = postLink[:secondSlash+2]
} }
// Страничка рендерится только если ее нет в кэше if pageData, ok := app.PagesCache.Get(cacheKey); ok {
pageData, ok := app.PagesCache.Get(postLink) sendPostPage(w, pageData)
if !ok { return
post := app.Posts[models_pages.PostLink(postLink)]
pageData, err = models_pages.RenderPostPage(app.Templates, app.Version, post.Data)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
app.PagesCache.Set(postLink, pageData)
} }
post := app.PostsMap[models_pages.PostLink(link)]
var (
pageData []byte
err error
)
pageData, err = post.RenderPostPage(app.Templates, app.Version, ajax)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
app.PagesCache.Set(cacheKey, pageData)
sendPostPage(w, pageData) sendPostPage(w, pageData)
}) })
} }

View File

@ -4,26 +4,45 @@ import (
"main/mvc/models" "main/mvc/models"
"main/mvc/models/models_pages" "main/mvc/models/models_pages"
"net/http" "net/http"
"path"
"strconv"
) )
// Обработчик главной страницы // Обработчик странички со списком постов
func PostsPageHandler(app *models.App) http.HandlerFunc { func PostsPageHandler(app *models.App) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
var err error link := r.URL.Path
cacheKey := link
// Страничка рендерится только если ее нет в кэше ajax := r.URL.Query().Get("ajax") == "true"
pageData, ok := app.PagesCache.Get(models_pages.PostsPageTmplName) if ajax {
if !ok { cacheKey += "?ajax=true"
pageData, err = app.Posts.RenderPostsPage(app.Templates, app.Version)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
app.PagesCache.Set(models_pages.PostsPageTmplName, pageData)
} }
if pageData, ok := app.PagesCache.Get(cacheKey); ok {
sendPostsPage(w, pageData)
return
}
postsList := app.PostsMap.PostsList()
pageNumber, _ := strconv.Atoi(path.Base(link))
page := models_pages.CreatePostsPage(postsList, pageNumber, app.Cfg.PostsMaxCountOnPage)
var (
pageData []byte
err error
)
pageData, err = page.RenderPostsPage(app.Templates, app.Version, ajax)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
app.PagesCache.Set(cacheKey, pageData)
sendPostsPage(w, pageData) sendPostsPage(w, pageData)
}) }
} }
// Отправляет страницу // Отправляет страницу

21
mvc/controllers/lastfm.go Normal file
View File

@ -0,0 +1,21 @@
package controllers
import (
"fmt"
"main/mvc/models"
"net/http"
)
// Обработчик главной страницы
func LastFMHandler(app *models.App) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
sendLastFM(w, app.LastfmLastTrack)
})
}
// Отправляет страницу
func sendLastFM(w http.ResponseWriter, data string) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, data)
}

View File

@ -35,6 +35,12 @@ func loggingMiddleware(next http.Handler) http.Handler {
next.ServeHTTP(w, r) next.ServeHTTP(w, r)
log.Printf("%s %s %v", r.Method, r.URL.Path, time.Since(start)) fullURL := r.URL.String()
log.Printf("%s %s | client: %s | duration: %v",
r.Method,
fullURL,
r.RemoteAddr,
time.Since(start))
}) })
} }

View File

@ -7,11 +7,12 @@ import (
) )
type App struct { type App struct {
Cfg *Config // Сонфиг Cfg *Config // Сонфиг
Posts models_pages.Posts // Посты PostsMap models_pages.PostsMap // Посты
Templates *template.Template // Шаблоны страниц Templates *template.Template // Шаблоны страниц
PagesCache *Cache // Кэш (отрендеренные странички) PagesCache *Cache // Кэш (отрендеренные странички)
Version int64 // Время запуска LastfmLastTrack string // Последний трек, полученный с ластфм
Version int64 // Время запуска
} }
// Инициализирует приложение // Инициализирует приложение
@ -27,7 +28,7 @@ func InitApp() (*App, error) {
return nil, err return nil, err
} }
// Загрузка постов // Загрузка постов
if app.Posts, err = models_pages.LoadPosts(app.Cfg.PostsDir); err != nil { if app.PostsMap, err = models_pages.LoadPosts(app.Cfg.PostsDir); err != nil {
return nil, err return nil, err
} }
// Загрузка шаблонов // Загрузка шаблонов
@ -36,5 +37,7 @@ func InitApp() (*App, error) {
} }
// Инициализация кэша // Инициализация кэша
app.PagesCache = initCache() app.PagesCache = initCache()
// Строка по умолчанию для последнего прослушанного трека
app.LastfmLastTrack = "None"
return app, nil return app, nil
} }

View File

@ -3,6 +3,7 @@ package models
import ( import (
"encoding/json" "encoding/json"
"os" "os"
"time"
) )
const ( const (
@ -10,27 +11,49 @@ const (
) )
type Config struct { type Config struct {
PostsDir string pathsConfig
AssetsDir string serverConfig
TemplatesDir string lastFMConfig
TemplatesExt string cacheConfig
LocalIP string
LocalPort string
ServerIP string
ServerPort string
ServerDomain string
Port string
} }
func loadConfig(configPath string) (*Config, error) { type pathsConfig struct {
cfg := &Config{} PostsDir string `json:"posts_dir"`
configFile, err := os.ReadFile(configPath) AssetsDir string `json:"assets_dir"`
if err != nil { TemplatesDir string `json:"templates_dir"`
return nil, err TemplatesExt string `json:"templates_ext"`
} LogFilePath string `json:"log_file_path"`
err = json.Unmarshal(configFile, cfg) LogSystemRestartInterval time.Duration `json:"log_system_restart_interval"`
if err != nil { PostsMaxCountOnPage int `json:"max_posts_per_page"`
return nil, err }
}
return cfg, nil type serverConfig struct {
LocalIP string `json:"local_ip"`
LocalPort string `json:"local_port"`
ServerIP string `json:"server_ip"`
ServerPort string `json:"server_port"`
}
type lastFMConfig struct {
LastFMUsername string `json:"lastfm_username"`
LastFMToken string `json:"lastfm_token"`
LastFMUpdateInterval time.Duration `json:"lastfm_update_interval"`
}
type cacheConfig struct {
CacheLogInterval time.Duration `json:"cache_log_interval"`
}
func loadConfig(path string) (*Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
var cfg Config
if err := json.Unmarshal(data, &cfg); err != nil {
return nil, err
}
return &cfg, nil
} }

63
mvc/models/lastfm.go Normal file
View File

@ -0,0 +1,63 @@
package models
import (
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
)
type lastFMTrack struct {
Name string `json:"name"`
Artist struct {
Name string `json:"#text"`
} `json:"artist"`
Album struct {
Name string `json:"#text"`
} `json:"album"`
Date struct {
Unix string `json:"#text"`
} `json:"date"`
}
type lastFMResponse struct {
RecentTracks struct {
Tracks []lastFMTrack `json:"track"`
} `json:"recenttracks"`
}
func lastFMGetRecentTracks(username, apiKey string) (*lastFMResponse, error) {
url := fmt.Sprintf("https://ws.audioscrobbler.com/2.0/?method=user.getrecenttracks&user=%s&api_key=%s&format=json", username, apiKey)
resp, err := http.Get(url)
if err != nil {
return nil, fmt.Errorf("lastfm get resp err: %v", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("lastfm read resp body err: %v", err)
}
var lastFMResponse lastFMResponse
if err := json.Unmarshal(body, &lastFMResponse); err != nil {
return nil, fmt.Errorf("lastfm unmarshal err: %v", err)
}
return &lastFMResponse, nil
}
func LastFMGetLastTrack(username, apiKey string) (string, error) {
resp, err := lastFMGetRecentTracks(username, apiKey)
if err != nil {
return "", err
}
var data string
if len(resp.RecentTracks.Tracks) > 0 {
track := resp.RecentTracks.Tracks[0]
data = fmt.Sprintf("%s - %s", track.Name, track.Artist.Name)
data = strings.ReplaceAll(data, "🅴", "[E]")
} else {
return "", fmt.Errorf("len(resp.RecentTracks.Tracks) <= 0")
}
return data, nil
}

View File

@ -8,10 +8,11 @@ import (
const ( const (
// Имя соответствующего шаблона // Имя соответствующего шаблона
MainPageTmplName = "main.gohtml" MainPageTmplName = "main.gohtml"
MainPageTmplNameAjax = "main_ajax.gohtml"
) )
func RenderMainPage(templates *template.Template, version int64) ([]byte, error) { func RenderMainPage(templates *template.Template, version int64, ajax bool) ([]byte, error) {
var pageData bytes.Buffer var pageData bytes.Buffer
context := map[string]any{ context := map[string]any{
@ -19,7 +20,12 @@ func RenderMainPage(templates *template.Template, version int64) ([]byte, error)
"renderingTimestamp": time.Now().Unix(), "renderingTimestamp": time.Now().Unix(),
} }
if err := templates.ExecuteTemplate(&pageData, MainPageTmplName, context); err != nil { templateName := MainPageTmplName
if ajax {
templateName = MainPageTmplNameAjax
}
if err := templates.ExecuteTemplate(&pageData, templateName, context); err != nil {
return nil, err return nil, err
} }

View File

@ -6,13 +6,13 @@ import (
"time" "time"
) )
type PostLink string
const ( const (
// Имя соответствующего шаблона // Имя соответствующего шаблона
PostPageTmplName = "post.gohtml" PostPageTmplName = "post.gohtml"
PostPageTmplNameAjax = "post_ajax.gohtml"
) )
type PostLink string
type Post struct { type Post struct {
Link PostLink Link PostLink
Preview template.HTML Preview template.HTML
@ -20,23 +20,29 @@ type Post struct {
Timestamp int64 Timestamp int64
} }
func RenderPostPage(templates *template.Template, version int64, data template.HTML) ([]byte, error) { func (p *Post) RenderPostPage(templates *template.Template, version int64, ajax bool) ([]byte, error) {
var pageData bytes.Buffer var pageData bytes.Buffer
context := map[string]any{ context := map[string]any{
"version": version, "version": version,
"renderingTimestamp": time.Now().Unix(), "renderingTimestamp": time.Now().Unix(),
"data": data, "data": p.Data,
"modTimestamp": p.Timestamp,
} }
if err := templates.ExecuteTemplate(&pageData, PostPageTmplName, context); err != nil { templateName := PostPageTmplName
if ajax {
templateName = PostPageTmplNameAjax
}
if err := templates.ExecuteTemplate(&pageData, templateName, context); err != nil {
return nil, err return nil, err
} }
return pageData.Bytes(), nil return pageData.Bytes(), nil
} }
func newPost(link string, data []byte, timestamp int64) *Post { func createPost(link string, data []byte, timestamp int64) *Post {
previewBuf := make([]byte, 0, 503) previewBuf := make([]byte, 0, 503)
if len(data) > 500 { if len(data) > 500 {

View File

@ -4,24 +4,33 @@ import (
"bytes" "bytes"
"fmt" "fmt"
"html/template" "html/template"
"log"
"main/tools" "main/tools"
"os" "os"
"path/filepath" "path/filepath"
"sort" "sort"
"strconv"
"strings" "strings"
"time" "time"
) )
const ( const (
// Имя соответствующего шаблона // Имя соответствующего шаблона
PostsPageTmplName = "posts.gohtml" PostsPageTmplName = "posts.gohtml"
PostsPageTmplNameAjax = "posts_ajax.gohtml"
) )
type Posts map[PostLink]*Post type PostsMap map[PostLink]*Post
type PostsList []*Post
type PostsPage struct {
PostsListOnPage PostsList // список постов на странице
PageNumber int // номер страницы
PagesCount int // общее количество страниц
}
func LoadPosts(dir string) (Posts, error) { func LoadPosts(dir string) (PostsMap, error) {
posts := Posts{} posts := PostsMap{}
err := filepath.Walk(dir, func(path string, f os.FileInfo, err error) error { err := filepath.Walk(dir, func(path string, f os.FileInfo, err error) error {
if err != nil { if err != nil {
@ -35,9 +44,23 @@ func LoadPosts(dir string) (Posts, error) {
} }
html := tools.MdToHTML(md) html := tools.MdToHTML(md)
link := fmt.Sprintf("/%s/", strings.TrimSuffix(filepath.Base(path), ".md"))
timestamp := f.ModTime().Unix() name := filepath.Base(path)
posts[PostLink(link)] = newPost(link, html, timestamp) lowLineIndex := strings.Index(name, "_")
if lowLineIndex == -1 {
// Обработка случая, если "_" нет в имени
log.Fatal(`post name parse error`)
}
timestampStr := name[:lowLineIndex]
timestamp, err := strconv.ParseInt(timestampStr, 10, 64)
if err != nil {
// Ошибка парсинга timestamp
log.Fatal(`post name parse error`)
}
link := fmt.Sprintf("/%s", strings.TrimSuffix(name[lowLineIndex+1:], ".md"))
posts[PostLink(link)] = createPost(link, html, timestamp)
} }
return nil return nil
}) })
@ -49,26 +72,65 @@ func LoadPosts(dir string) (Posts, error) {
return posts, nil return posts, nil
} }
func (p *Posts) RenderPostsPage(templates *template.Template, version int64) ([]byte, error) { // Получение из мапы постов списка постов, отсортированного по ModTimestamp (новые сначала)
var pageData bytes.Buffer func (p *PostsMap) PostsList() PostsList {
postsSlice := make(PostsList, 0, len(*p))
postsSlice := make([]*Post, 0, len(*p))
for _, post := range *p { for _, post := range *p {
postsSlice = append(postsSlice, post) postsSlice = append(postsSlice, post)
} }
// Сортирую по ModTimestamp (новые сначала)
sort.Slice(postsSlice, func(i, j int) bool { sort.Slice(postsSlice, func(i, j int) bool {
return postsSlice[i].Timestamp > postsSlice[j].Timestamp return postsSlice[i].Timestamp > postsSlice[j].Timestamp
}) })
context := map[string]any{ return postsSlice
"version": version, }
"renderingTimestamp": time.Now().Unix(),
"posts": postsSlice, func CreatePostsPage(postsList PostsList, pageNumber, postsMaxCountOnPage int) *PostsPage {
// Общее количество страниц
pagesCount := (len(postsList) + postsMaxCountOnPage - 1) / postsMaxCountOnPage
startIndex := pageNumber * postsMaxCountOnPage
endIndex := startIndex + postsMaxCountOnPage
endIndex = min(endIndex, len(postsList))
postsSublistForPageNumber := postsList[startIndex:endIndex]
return &PostsPage{
PostsListOnPage: postsSublistForPageNumber,
PageNumber: pageNumber,
PagesCount: pagesCount,
}
}
func (p *PostsPage) RenderPostsPage(templates *template.Template, version int64, ajax bool) ([]byte, error) {
var pageData bytes.Buffer
var prevPageNumber, nextPageNumber int
if p.PageNumber == 0 {
prevPageNumber = p.PagesCount - 1
} else {
prevPageNumber = p.PageNumber - 1
} }
if err := templates.ExecuteTemplate(&pageData, PostsPageTmplName, context); err != nil { nextPageNumber = (p.PageNumber + 1) % p.PagesCount
context := map[string]any{
"prevPageNumber": prevPageNumber,
"pageNumberInc": p.PageNumber + 1,
"pagesCount": p.PagesCount,
"nextPageNumber": nextPageNumber,
"version": version,
"renderingTimestamp": time.Now().Unix(),
"posts": p.PostsListOnPage,
}
templateName := PostsPageTmplName
if ajax {
templateName = PostsPageTmplNameAjax
}
if err := templates.ExecuteTemplate(&pageData, templateName, context); err != nil {
return nil, err return nil, err
} }

View File

@ -1,29 +1,28 @@
{{ define "footer" }} {{ define "footer" }}
<footer> <footer>
<div> <div class="rotating-pic-container">
<img src="/assets/pic/footer.webp?v={{ .version }}" width="100%" height="100%"> <img src="/assets/pic/heh.png?v={{ .version }}" class="rotating-pic">
</div>
<div>
<p>
and also you can subscribe to my <a href="https://t.me/lolistack" target="_blank">telegram channel </a> with pictures!
</p>
</div> </div>
<div> <div>
<p> <p>
<strong>some system information</strong>: <strong>some system information</strong>:
</p> </p>
<ul> <p>
<li>unix timestamp of page rendering: <code>{{ .renderingTimestamp }}</code></li> > unix timestamp of page rendering: <code>{{ .renderingTimestamp }}</code><br/>
<li><code>curl -X LIMINAL https://hikan.ru</code> - what do you know about liminal spaces?</li> > <code>curl -X LIMINAL https://hikan.ru</code> - what do you know about liminal spaces<br/>
<li>this site code repository - <a href="https://git.hikan.ru/serr/hikan.ru" target="_blank">git.hikan.ru/serr/hikan.ru</a></li> > <code>curl -X COUNT https://hikan.ru</code> - 24-hour server request count<br/>
<li>24-hour server request count (<code>curl -X COUNT https://hikan.ru</code>):</li>
</ul>
<p class="count">
<img src="/assets/pic/0.gif?v={{ .version }}">
<img src="/assets/pic/0.gif?v={{ .version }}">
<img src="/assets/pic/0.gif?v={{ .version }}">
<img src="/assets/pic/0.gif?v={{ .version }}">
</p> </p>
<p>
<details>
<summary class="pulse-text">click here if you want more system&nbsp;details</summary>
<p>
> to check the latest changes to the site you can use it (someday i hope to add a cool log page):
<pre>curl -s "https://git.hikan.ru/api/v1/repos/serr/hikan.ru/commits?limit=10" | jq -r '.[] | .commit.author.date + " | " + (.commit.message | rtrimstr("\n"))'</pre>
or look in the repository <a href="https://git.hikan.ru/serr/hikan.ru" target="_blank">git.hikan.ru/serr/hikan.ru</a>
</p>
</details>
</p>
</div> </div>
<div> <div>
<p> <p>
@ -33,17 +32,6 @@
<code>2024 - now</code> <code>2024 - now</code>
</p> </p>
</div> </div>
<marquee>programming / music / video games / cinema</marquee>
</footer> </footer>
<script>
fetch('/', {method: 'COUNT'})
.then(r => r.text())
.then(num => num.trim())
.then(number => {
if (number.length > 0) {
document.querySelector('p.count').innerHTML =
[...number].map(d => `<img src="/assets/pic/${d}.gif?v={{ .version }}">`).join('');
}
})
.catch(console.error);
</script>
{{ end }} {{ end }}

View File

@ -1,9 +1,23 @@
{{ define "head" }} {{ define "head" }}
<head> <head>
<title>hikan.ru</title> <title>хикан.ру</title>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="shortcut icon" href="/assets/pic/favicon.png?v={{ .version }}" type="image/x-icon"> <link rel="shortcut icon" href="/assets/pic/favicon.ico?v={{ .version }}" type="image/x-icon">
<link rel="stylesheet" href="/assets/css/styles.css?v={{ .version }}" type="text/css"> <link rel="stylesheet" href="/assets/css/styles.css?v={{ .version }}" type="text/css">
<script src="/assets/scripts/htmx.js?v={{ .version }}"></script>
<script>
const titles = ["freedom","love","music","flowers","breathing"]; let i = 0;
function update() { document.title = titles[i++%5]; } update();
setInterval(update, 1000);
</script>
<script>
document.addEventListener('htmx:afterSwap', function(evt) {
const triggeringElement = evt.detail.requestConfig.elt;
if (triggeringElement.hasAttribute('data-scroll-top')) {
window.scrollTo(0,0);
}
});
</script>
</head> </head>
{{ end }} {{ end }}

View File

@ -1,7 +1,17 @@
{{ define "header" }} {{ define "header" }}
<header> <header>
<div> <div class="rotating-pic-container">
<img src="/assets/pic/header.webp?v={{ .version }}" width="100%" height="100%"> <img class="rotating-pic" src="/assets/pic/miffy.png?v={{ .version }}"
style="width: 100%; height: 100%;">
</div>
<div class="pulse-container">
<p class="pulse-text">
<code>🎵&nbsp;last played track: </code><code id="lastfm"
hx-get="/api/lastfm"
hx-trigger="load, every 30s"
hx-target="#lastfm"
hx-swap="innerHTML">waiting for a response from server... (JavaScript is required)</code><code>&nbsp;🎵</code>
</p>
</div> </div>
<div> <div>
<h1> <h1>
@ -12,10 +22,22 @@
</p> </p>
</div> </div>
<div> <div>
<ul> <p>
<li><a href="/">main page</a></li> see <a href="/"
<li><a href="/posts/">posts section</a></li> hx-get="/?ajax=true"
</ul> hx-target="main"
hx-swap="outerHTML"
hx-push-url="/"
data-scroll-top="true">
main page</a> or go to
<a href="/posts/0"
hx-get="/posts/0?ajax=true"
hx-target="main"
hx-swap="outerHTML"
hx-push-url="/posts/0"
data-scroll-top="true">
posts section</a>, and also you can subscribe to my <a href="https://t.me/lolistack" target="_blank">telegram channel</a> with pictures!
</p>
</div> </div>
</header> </header>
{{ end }} {{ end }}

View File

@ -1,71 +0,0 @@
<!DOCTYPE html>
<html lang="en">
{{ template "head" . }}
<body>
{{ template "header" . }}
<main>
<div>
<h1>
$whoami
</h1>
<p>
my name is serr (you can easily guess my real name if you speak Russian :d), and i didn't come up with that nickname, i just started being called it
</p>
<p>
i was born in 2003, i'm currently a cybersecurity major at university
</p>
<p>
<code>pronouns: he/him</code>
</p>
</div>
<div>
<h1>
what do i do?
</h1>
<p>
programming is my everything - my job, my hobby, my lifestyle
</p>
<p>
i love growing in all areas of programming - i am literally interested in everything: cybersecurity (chaotically breaking things, analyzing code, writing automated analyzers, and moving bytes back and forth), concurrency/multithreading, web development, low-level programming, cryptography and a lot more!
</p>
<p>
i like the idea of <a href="https://en.wikipedia.org/wiki/Symbolic_execution" target="_blank">symbolic</a>/concolic execution and virtual code execution in general
</p>
</div>
<div>
<h1>
things i love
</h1>
<ul>
<li><strong>coffee</strong>. i REALLY love coffee. almost any. and a lot of</li>
<li><strong>movies and TV series</strong> (especially TV series). i watch something almost every day</li>
<li><strong>true crime</strong>. i'm obsessed with serial killer cases, mysterious disappearances, unsolved murders - all that dark stuff</li>
<li><strong>russian underground rap</strong> like Slava KPSS, Zamay, MB Packet, Ovsyankin etc.</li>
<li><strong>simple and extensible code</strong>. i think if your code is overly complex, it means you are doing something wrong. most things are simpler than they seem</li>
</ul>
</div>
<div>
<h1>
projects
</h1>
<ul>
<li><a href="https://git.hikan.ru/serr" target="_blank">git.hikan.ru/serr</a> - check my repos</li>
<li><del>telegram bot with schedule for SPBPU - <a href="https://t.me/polysched_bot" target="_blank">polysched_bot</a></del> (transferred to a more proactive owner)</li>
<li><del>telegram bot with schedule for SPMI - <a href="https://t.me/gornischedule_bot" target="_blank">gornischedule_bot</a></del> (closed)</li>
</ul>
</div>
<div>
<h1>
nice links
</h1>
<ul>
<li><a href="https://mo.rijndael.cc/" target="_blank">Mo</a>, thx for design idea!</li>
<li>huge collection of Xakep issues - <a href="https://図書館.きく.コム/" target="_blank">図書館.きく.コム</a></li>
<li>i love this website highlighting the Small Web - <a href="https://smallweb.cc/" target="_blank">smallweb</a></li>
<li>very atmospheric forum about black metal - <a href="https://www.lycanthropia.net/" target="_blank">lycanthropia</a></li>
</ul>
</div>
</main>
{{ template "footer" . }}
</body>
</html>

View File

@ -0,0 +1,9 @@
<!DOCTYPE html>
<html lang="en">
{{ template "head" . }}
<body>
{{ template "header" . }}
{{ template "main_ajax" . }}
{{ template "footer" . }}
</body>
</html>

View File

@ -0,0 +1,74 @@
{{ template "main_ajax" . }}
{{ define "main_ajax" }}
<main>
<div>
<p>
<code>welcome to my little site</code>
</p>
</div>
<div>
<h1>
$whoami
</h1>
<p>
my name is serr (you can easily guess my real name if you speak Russian :d), and i didn't come up with that nickname, i just started being called it
</p>
<p>
i was born in 2003, i'm currently a cybersecurity major at university
</p>
<p>
<code>pronouns: he/him</code>
</p>
</div>
<div>
<h1>
what do i do?
</h1>
<p>
programming is my lifestyle
</p>
<p>
i love growing in all areas of programming - i am literally interested in everything: web development, low-level programming, formal grammars and a lot more!
</p>
<p>
i like the idea of <a href="https://en.wikipedia.org/wiki/Symbolic_execution" target="_blank">symbolic</a>/concolic execution and virtual code execution in general
</p>
</div>
<div>
<h1>
things i love
</h1>
<p>
> <strong>coffee</strong>. i REALLY love coffee. almost any. and a lot of<br/>
> <strong>movies and TV series</strong> (especially TV series). i watch something almost every day<br/>
> <strong>true crime</strong>. i'm obsessed with serial killer cases, mysterious disappearances, unsolved murders - all that dark stuff<br/>
> <strong>russian underground rap</strong> like Slava KPSS, Zamay, MB Packet, Ovsyankin etc.<br/>
> <strong>simple and extensible code</strong>. i think if your code is overly complex, it means you are doing something wrong. most things are simpler than they seem
</p>
<p>
<details>
<summary class="pulse-text">click here if you want to see the sites&nbsp;i&nbsp;like</summary>
<p>
> huge collection of Xakep issues - <a href="https://図書館.きく.コム/" target="_blank">図書館.きく.コム</a><br/>
> i like to surf here - <a href="https://neocities.org/browse" target="_blank">neocities.org/browse</a><br/>
> very atmospheric forum about black metal - <a href="https://www.lycanthropia.net/" target="_blank">lycanthropia</a><br/>
> animated gif search from internet archive - <a href="https://gifcities.org/" target="_blank">gifcities.org</a><br/>
> very cool design - <a href="https://combatbaby.neocities.org/" target="_blank">combatbaby.neocities.org</a><br/>
</p>
</details>
</p>
</div>
<div>
<h1>
projects
</h1>
<p>
> <a href="https://git.hikan.ru/serr/candycache" target="_blank">git.hikan.ru/serr/candycache</a> - simple and efficient Go cache<br/>
> <a href="https://git.hikan.ru/serr/eye-hot-reloader" target="_blank">git.hikan.ru/serr/eye-hot-reloader</a> - lightweight directories monitor with automatic rebuild and restart functionality for any project type<br/>
> <del>telegram bot with schedule for SPBPU - <a href="https://t.me/polysched_bot" target="_blank">polysched_bot</a></del> (transferred to a more proactive owner)<br/>
> <del>telegram bot with schedule for SPMI - <a href="https://t.me/gornischedule_bot" target="_blank">gornischedule_bot</a></del> (closed)<br/>
</p>
</div>
</main>
{{ end }}

View File

@ -3,11 +3,7 @@
{{ template "head" . }} {{ template "head" . }}
<body> <body>
{{ template "header" . }} {{ template "header" . }}
<main> {{ template "post_ajax" . }}
<div>
{{ .data }}
</div>
</main>
{{ template "footer" . }} {{ template "footer" . }}
</body> </body>
</html> </html>

View File

@ -0,0 +1,12 @@
{{ template "post_ajax" . }}
{{ define "post_ajax" }}
<main>
<div>
{{ .data }}
<p>
<code>create time: {{ .modTimestamp }}</code>
</p>
</div>
</main>
{{ end }}

View File

@ -1,29 +0,0 @@
<!DOCTYPE html>
<html lang="en">
{{ template "head" . }}
<body>
{{ template "header" . }}
<main>
<div>
<p>
<code>posts sorted by last mod time</code>
</p>
</div>
{{ range $ind, $post := .posts }}
<div>
<p>
{{ $post.Preview }}
</p>
<p>
<a href="{{ $post.Link }}">read more</a>
</p>
<p>
<code>mod time: {{ $post.Timestamp }}</code>
</p>
</div>
{{ end }}
</main>
{{ template "footer" . }}
</body>
</html>

View File

@ -0,0 +1,9 @@
<!DOCTYPE html>
<html lang="en">
{{ template "head" . }}
<body>
{{ template "header" . }}
{{ template "posts_ajax" . }}
{{ template "footer" . }}
</body>
</html>

View File

@ -0,0 +1,44 @@
{{ template "posts_ajax" . }}
{{ define "posts_ajax" }}
<main>
<div>
<p>
<code>posts sorted by create time</code>
</p>
</div>
{{ range $ind, $post := .posts }}
<div>
<p>
{{ $post.Preview }}
</p>
<p>
<a href="{{ $post.Link }}"
hx-get="{{ $post.Link }}?ajax=true"
hx-target="main"
hx-swap="outerHTML"
hx-push-url="{{ $post.Link }}"
data-scroll-top="true">read more</a>
</p>
<p>
<code>create time: {{ $post.Timestamp }}</code>
</p>
</div>
{{ end }}
<div class="pagi">
<a href="/posts/{{ .prevPageNumber }}"
hx-get="/posts/{{ .prevPageNumber }}?ajax=true"
hx-target="main"
hx-swap="outerHTML"
hx-push-url="/posts/{{ .prevPageNumber }}"
data-scroll-top="true"><-</a>
{{ .pageNumberInc }}/{{ .pagesCount }}
<a href="/posts/{{ .nextPageNumber }}"
hx-get="/posts/{{ .nextPageNumber }}?ajax=true"
hx-target="main"
hx-swap="outerHTML"
hx-push-url="/posts/{{ .nextPageNumber }}"
data-scroll-top="true">-></a>
</div>
</main>
{{ end }}

View File

@ -0,0 +1,72 @@
# Как же все таки изменить байты строки в Go?
Просто захотелось чуть чуть поиграться с пакетом unsafe в Go.
Строки (тип string) в Go являются *immutable*, то есть изменять их нельзя. Ну вообще конечно можно, но не напрямую.
Строка в Go под капотом является структурой вида: **указатель на данные, длина данных.** И первое, что приходит в голову чтобы изменить строку - добраться до поля с указателем, прибавить к нему индекс байта который надо поменять, разыменовать полученный адрес и что то ему присвоить.
Но в реальности все не так просто и при попытке что то положить по вычисленному адресу программа упадет с *segmentation fault (SIGSEGV)*. Чтобы этого избежать, предварительно надо выдать права на запись в страничку памяти где находится целевая строка. Сделать это можно через системные вызовы.
```
package main
import (
"fmt"
"syscall"
"unsafe"
)
func main() {
s := "hello"
fmt.Println("Original:", s)
// Строки в Go - структуры с двумя полями: указатель на данные и длина
// тут строка интерпретируется как структура
strHeader := (*struct {
data unsafe.Pointer
len int
})(unsafe.Pointer(&s))
// Вычисляем начало страницы памяти
pageSize := syscall.Getpagesize()
pageStart := uintptr(strHeader.data) - (uintptr(strHeader.data) % uintptr(pageSize))
fmt.Printf(
"\nАдрес строки = %d\nразмер страницы памяти = %d\nадрес строки относительно начала страницы = %d\nадрес страницы = %d\n\n",
uintptr(strHeader.data),
uintptr(pageSize),
(uintptr(strHeader.data) % uintptr(pageSize)),
pageStart)
// Имея адрес страницы и ее размер даю права на запись на данной странице памяти
ret, _, err := syscall.Syscall(
syscall.SYS_MPROTECT,
pageStart,
uintptr(pageSize),
uintptr(syscall.PROT_READ|syscall.PROT_WRITE),
)
if ret != 0 {
panic("mprotect failed: " + err.Error())
}
// Теперь можно менять строку
// поменяю, например, пятый байт
fifthBytePtr := (*byte)(unsafe.Pointer(uintptr(strHeader.data) + 4))
*fifthBytePtr = 0
// Восстанавливаю дефолтные права
ret, _, err = syscall.Syscall(
syscall.SYS_MPROTECT,
pageStart,
uintptr(pageSize),
uintptr(syscall.PROT_READ),
)
if ret != 0 {
panic("mprotect restore failed: " + err.Error())
}
fmt.Println("Modified:", s) // Должно быть "hell"
}
```
Тестил на **go version go1.22.2 linux/amd64**

View File

@ -1,12 +0,0 @@
# Как же все таки изменить байты строки в Go?
Просто захотелось чуть чуть поиграться с пакетом unsafe в Go.
Строки (тип string) в Go являются *immutable*, то есть изменять их нельзя. Ну вообще конечно можно, но не напрямую.
Строка в Go под капотом является структурой вида: **указатель на данные, длина данных.** И первое, что приходит в голову чтобы изменить строку - добраться до поля с указателем, прибавить к нему индекс байта который надо поменять, разыменовать полученный адрес и что то ему присвоить.
Но в реальности все не так просто и при попытке что то положить по вычисленному адресу программа упадет с *segmentation fault (SIGSEGV)*. Чтобы этого избежать, предварительно надо выдать права на запись в страничку памяти где находится целевая строка. Сделать это можно через системные вызовы.
Код с пояснениями можно скачать [тут](https://git.hikan.ru/serr/unsafe-change-string-go)
Тестил на **go version go1.22.2 linux/amd64**

View File

@ -1,29 +1,81 @@
package tools package tools
import ( import (
"bytes" "bufio"
"fmt" "fmt"
"os/exec" "log"
"runtime" "os"
"strings"
"time"
) )
func GetJournalctlLogsCount(serviceName, grepPattern string, hoursAgo int) ([]byte, error) { func SetupLogging(logFilePath string) (*os.File, error) {
if runtime.GOOS != "linux" { // Проверяем размер файла
return nil, fmt.Errorf("not a linux") var append = true
if fi, err := os.Stat(logFilePath); err == nil {
if fi.Size() > 10*1024*1024 { // 10 МБ
append = false
}
} }
cmd := exec.Command("sh", "-c", // Логи дописываются в файл если он меньше 10 МБ, иначе
fmt.Sprintf("journalctl -u %s --since '%d hours ago' | grep -c '%s'", // файл перезаписывается
serviceName, hoursAgo, grepPattern)) var flags int
if append {
flags = os.O_CREATE | os.O_WRONLY | os.O_APPEND
} else {
flags = os.O_CREATE | os.O_WRONLY | os.O_TRUNC
}
var output bytes.Buffer file, err := os.OpenFile(logFilePath, flags, 0666)
cmd.Stdout = &output
err := cmd.Run()
if err != nil { if err != nil {
return nil, err return nil, err
} }
return output.Bytes(), nil log.SetOutput(file)
return file, nil
}
func GetLogFileEntriesCount(logFilePath, grepPattern string, hoursAgo int) (int, error) {
// Если файл не существует - ошибка
if _, err := os.Stat(logFilePath); os.IsNotExist(err) {
return 0, fmt.Errorf("log file not found: %s", logFilePath)
}
// Открывает файл для чтения
file, err := os.Open(logFilePath)
if err != nil {
return 0, fmt.Errorf("failed to open log file: %v", err)
}
defer file.Close()
// Вычисляется временная граница (текущее время - hoursAgo)
timeThreshold := time.Now().Add(-time.Duration(hoursAgo) * time.Hour)
count := 0
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
// Парсим дату из строки (формат "2006/01/02 15:04:05")
if len(line) < 20 { // Минимальная длина для даты
continue
}
logTime, err := time.Parse("2006/01/02 15:04:05", line[:19])
if err != nil {
continue // Пропускаются строки где не дата
}
// Проверка попадает ли дана во временной интервал
if logTime.After(timeThreshold) && strings.Contains(line, grepPattern) {
count++
}
}
if err := scanner.Err(); err != nil {
return 0, fmt.Errorf("error reading log file: %v", err)
}
return count, nil
} }

27
tools/ticker.go Normal file
View File

@ -0,0 +1,27 @@
package tools
import "time"
// Ticker запускает функцию fn с указанным интервалом в отдельной горутине
// Возвращает канал для остановки тикера
func Ticker(fn func(), interval time.Duration) chan struct{} {
if interval <= 0 {
interval = time.Second
}
ticker := time.NewTicker(interval)
stopChan := make(chan struct{})
go func() {
fn()
for {
select {
case <-ticker.C:
fn()
case <-stopChan:
ticker.Stop()
return
}
}
}()
return stopChan
}