Compare commits

..

No commits in common. "posts" and "master" have entirely different histories.

29 changed files with 384 additions and 613 deletions

2
.gitignore vendored
View File

@ -1,2 +1,2 @@
config.json
hikan.ru
restart.sh

View File

@ -23,17 +23,28 @@ main {
flex: 3;
}
div {
text-align: left;
header > div,
footer > div,
main > div {
box-shadow: 5px 5px 0 0 lightgrey;
box-sizing: border-box;
border: 1px solid;
width: 100%;
padding: 0 10px;
text-align: center;
color: black;
padding-left: 10px;
padding-right: 10px;
}
h1 {
text-align: center;
header > div > ul,
footer > div > ul,
main > div > ul {
text-align: left;
}
header > div > h1,
footer > div > h1,
main > div > h1 {
box-sizing: border-box;
border-top: 1px solid;
border-bottom: 1px solid;

110
eye.sh
View File

@ -1,110 +0,0 @@
#!/bin/bash
stty -echoctl # Отключает вывод управляющих символов по типу ^C
# НАСТРОЙКА СКРИПТА ТУТ ###########################################################
DURATION=1 # Задержка между проверками в секундах
WATCH_TARGETS=("assets" "mvc" "posts" "tools" "main.go") # Массив целей для наблюдения (директории и файлы)
BINARY_PATH="./main" # Путь до бинарного файла
BUILD_CMD="go build -o $BINARY_PATH main.go" # Команда для сборки
###################################################################################
# Массивы для хранения информации о целях
declare -A LAST_MODS
declare -A LAST_COUNTS
CLEANUP_DONE=0
# Вывод в синем цвете
blue() {
echo -e "\033[34mEYE | $1\033[0m"
}
# Очистка при завершении работы скрипта
cleanup() {
[ $CLEANUP_DONE -eq 1 ] && exit 0
blue "cleanup..."
kill_proc $1
rm -f $BINARY_PATH
blue "see you later!"
CLEANUP_DONE=1
exit 0
}
# Убийство процесса по его pid
kill_proc() {
local pid=$1
if [ -n "$pid" ] && kill -0 $pid 2>/dev/null; then
kill $pid
blue "process killed (PID: $pid)"
fi
}
# Проверка изменений в целях
check_changes() {
local changed=0
for target in "${WATCH_TARGETS[@]}"; do
local current_mod=0
local current_count=0
if [ -f "$target" ]; then
# Обработка файла
current_mod=$(stat -c %Y "$target")
current_count=1
elif [ -d "$target" ]; then
# Обработка директории
current_mod=$(find "$target" -type f -exec stat -c %Y {} \; | sort -nr | head -1)
current_count=$(find "$target" -type f | wc -l)
fi
if { [ -n "$current_mod" ] && [ "${LAST_MODS[$target]:-0}" -lt "$current_mod" ]; } || [ "${LAST_COUNTS[$target]:-0}" -ne "$current_count" ]; then
changed=1
LAST_MODS["$target"]=$current_mod
LAST_COUNTS["$target"]=$current_count
[ -n "$1" ] && blue "changes detected in \033[94m$target\033[0m"
fi
done
return $changed
}
# Основная функция
main() {
local pid=""
# Ловушка для сигналов завершения
trap 'cleanup $pid' SIGINT SIGTERM SIGHUP SIGQUIT EXIT
# Инициализация массивов
for target in "${WATCH_TARGETS[@]}"; do
if [ -f "$target" ]; then
LAST_MODS["$target"]=0
LAST_COUNTS["$target"]=1
elif [ -d "$target" ]; then
LAST_MODS["$target"]=0
LAST_COUNTS["$target"]=$(find "$target" -type f | wc -l)
fi
blue "started watching \033[94m$target\033[0m"
done
# Основной цикл работы скрипта
while true; do
check_changes $pid
if [ $? -eq 1 ]; then
blue "rebuilding..."
$BUILD_CMD
if [ $? -eq 0 ]; then
blue "build successful. restarting..."
kill_proc $pid
$BINARY_PATH &
pid=$!
blue "started new process (PID: $pid)"
else
blue "build failed"
fi
fi
sleep $DURATION
done
}
main

2
go.mod
View File

@ -1,5 +1,3 @@
module main
go 1.23.2
require github.com/gomarkdown/markdown v0.0.0-20250311123330-531bef5e742b

2
go.sum
View File

@ -1,2 +0,0 @@
github.com/gomarkdown/markdown v0.0.0-20250311123330-531bef5e742b h1:EY/KpStFl60qA17CptGXhwfZ+k1sFNJIUNR8DdbcuUk=
github.com/gomarkdown/markdown v0.0.0-20250311123330-531bef5e742b/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=

52
main.go
View File

@ -4,58 +4,62 @@ import (
"fmt"
"log"
"main/mvc/controllers"
"main/mvc/controllers/controllers_pages"
controllers_pages "main/mvc/controllers/pages"
"main/mvc/models"
"main/tools"
"net/http"
)
func main() {
var err error
var app *models.App
var err error
// Инициализация приложения
if app, err = models.InitApp(); err != nil {
if app, err = models.AppInit("config.json"); err != nil {
log.Fatal(err)
}
// Добавление префикса в виде домена сервера к записям в лог
log.SetPrefix(fmt.Sprintf("%s | ", app.Cfg.ServerDomain))
log.SetPrefix(fmt.Sprintf("%s | ", app.Config.ServerDomain))
// Настройка маршрутов
router := setupRoutes(app)
// Запуск сервера
if ok, err := tools.IsIPInUse(app.Cfg.ServerIP); err != nil {
// Настройка маршрутов и запуск
if setupRoutesAndRun(app) != nil {
log.Fatal(err)
} else if ok {
runServer(app.Cfg.ServerIP, app.Cfg.ServerPort, router)
} else {
runServer(app.Cfg.LocalIP, app.Cfg.LocalPort, router)
}
}
func setupRoutesAndRun(a *models.App) error {
// Настройка маршрутов
router := setupRoutes(a)
// Запуск сервера
if ok, err := tools.IsIPInUse(a.Config.ServerIP); err != nil {
return err
} else if ok {
runServer(a.Config.ServerIP, a.Config.ServerPort, router)
} else {
runServer(a.Config.LocalIP, a.Config.LocalPort, router)
}
return nil
}
// Настраивает маршруты
func setupRoutes(app *models.App) *http.ServeMux {
func setupRoutes(a *models.App) *http.ServeMux {
router := http.NewServeMux()
// Цепочка обработчиков, которые сработают до отдачи страницы юзеру
m := controllers.MiddlewaresChain
// Обработка статических файлов
router.Handle(app.Cfg.AssetsDir, m(controllers.StaticHandler()))
router.Handle(a.Config.AssetsPath, m(controllers.StaticHandler()))
// Главные странички
// Странички
{
// Обработка главной страницы (русская версия)
router.Handle("/ru/", m(controllers_pages.MainRuPageHandler(a)))
// Обработка главной страницы
router.Handle("/", m(controllers_pages.MainPageHandler(app)))
// Обработка страницы со списком постов
router.Handle("/posts/", m(controllers_pages.PostsPageHandler(app)))
// Обработка страничек постов
for key := range app.Posts {
postLink := string(key)
router.Handle(postLink, m(controllers_pages.PostPageHandler(app)))
}
router.Handle("/", m(controllers_pages.MainPageHandler(a)))
}
return router

View File

@ -1,47 +0,0 @@
package controllers_pages
import (
"main/mvc/models"
"main/mvc/models/models_pages"
"net/http"
"strings"
)
// Обработчик главной страницы
func PostPageHandler(app *models.App) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var err error
postLink := r.URL.Path
// Ссылки на посты имеют вид postLink = /link/, а если прилетело что то типо /link/123123,
// то надо оставить только часть /link/
secondSlash := strings.IndexByte(postLink[1:], '/')
if secondSlash != -1 {
postLink = postLink[:secondSlash+2]
}
// Страничка рендерится только если ее нет в кэше
pageData, ok := app.PagesCache.Get(postLink)
if !ok {
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)
}
sendPostPage(w, pageData.([]byte))
})
}
// Отправляет страницу
func sendPostPage(w http.ResponseWriter, data []byte) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusOK)
w.Write(data)
}

View File

@ -9,18 +9,21 @@ import (
type Middleware func(http.Handler) http.Handler
var (
MiddlewaresChain = createMiddlewaresChain(
loggingMiddleware,
MiddlewaresChain = CreateMiddlewaresChain(
LoggingMiddleware,
)
)
/*
Возвращает один middleware, который объединяет все переданные
createMiddlewaresChain(m1, m2, m3)
CreateMiddlewaresChain(m1, m2, m3)
= func(next http.Handler) http.Handler { return m1(m2(m3(final))) }
CreateMiddlewaresChain(LoggingMiddleware)
= func(next http.Handler) http.Handler { return LoggingMiddleware(final) }
*/
func createMiddlewaresChain(middlewares ...Middleware) Middleware {
func CreateMiddlewaresChain(middlewares ...Middleware) Middleware {
return func(final http.Handler) http.Handler {
for i := len(middlewares) - 1; i >= 0; i-- {
final = middlewares[i](final)
@ -29,7 +32,7 @@ func createMiddlewaresChain(middlewares ...Middleware) Middleware {
}
}
func loggingMiddleware(next http.Handler) http.Handler {
func LoggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()

View File

@ -1,16 +1,15 @@
package controllers_pages
package controllers
import (
"log"
"main/mvc/models"
"main/mvc/models/models_pages"
models_pages "main/mvc/models/pages"
"main/tools"
"net/http"
)
// Обработчик главной страницы
func MainPageHandler(app *models.App) http.HandlerFunc {
func MainPageHandler(a *models.App) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var err error
@ -18,63 +17,63 @@ func MainPageHandler(app *models.App) http.HandlerFunc {
// Количество запросов, обработанных сервером за 24ч
if r.Method == "COUNT" {
var count []byte
if count, err = tools.GetJournalctlLogsCount("server", app.Cfg.ServerDomain, 24); err != nil {
if count, err = tools.GetJournalctlLogsCount("server", a.Config.ServerDomain, 24); err != nil {
log.Printf("%s", err.Error())
}
sendCount(w, count)
SendCount(w, count)
return
}
// Пасхалка
if r.Method == "LOVE" {
sendLove(w)
SendLove(w)
return
}
// Пасхалка 2
if r.Method == "LIMINAL" {
sendLiminal(w)
SendLiminal(w)
return
}
// Страничка рендерится только если ее нет в кэше
pageData, ok := app.PagesCache.Get(models_pages.MainPageTmplName)
pageData, ok := a.Cache.Get(models_pages.MainPageTmplName)
if !ok {
pageData, err = models_pages.RenderMainPage(app.Templates, app.Version)
pageData, err = models_pages.RenderMainPage(a.Templates, a.Version)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
app.PagesCache.Set(models_pages.MainPageTmplName, pageData)
a.Cache.Set(models_pages.MainPageTmplName, pageData)
}
sendMainPage(w, pageData.([]byte))
SendMainPage(w, pageData.([]byte))
})
}
// Отправляет страницу
func sendMainPage(w http.ResponseWriter, data []byte) {
func SendMainPage(w http.ResponseWriter, data []byte) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusOK)
w.Write(data)
}
// Ответ на метод COUNT
func sendCount(w http.ResponseWriter, data []byte) {
func SendCount(w http.ResponseWriter, data []byte) {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.WriteHeader(http.StatusOK)
w.Write(data)
}
// Ответ на метод LOVE
func sendLove(w http.ResponseWriter) {
func SendLove(w http.ResponseWriter) {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.WriteHeader(http.StatusOK)
w.Write([]byte("13.01.2005\n"))
}
// Ответ на метод LIMINAL
func sendLiminal(w http.ResponseWriter) {
func SendLiminal(w http.ResponseWriter) {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
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"

View File

@ -1,34 +1,34 @@
package controllers_pages
import (
"main/mvc/models"
"main/mvc/models/models_pages"
"net/http"
)
// Обработчик главной страницы
func PostsPageHandler(app *models.App) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var err error
// Страничка рендерится только если ее нет в кэше
pageData, ok := app.PagesCache.Get(models_pages.PostsPageTmplName)
if !ok {
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)
}
sendPostsPage(w, pageData.([]byte))
})
}
// Отправляет страницу
func sendPostsPage(w http.ResponseWriter, data []byte) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusOK)
w.Write(data)
}
package controllers
import (
"main/mvc/models"
models_pages "main/mvc/models/pages"
"net/http"
)
// Обработчик главной страницы
func MainRuPageHandler(a *models.App) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var err error
// Страничка рендерится только если ее нет в кэше
pageData, ok := a.Cache.Get(models_pages.MainRuPageTmplName)
if !ok {
pageData, err = models_pages.RenderMainRuPage(a.Templates, a.Version)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
a.Cache.Set(models_pages.MainRuPageTmplName, pageData)
}
SendMainPage(w, pageData.([]byte))
})
}
// Отправляет страницу
func SendMainRuPage(w http.ResponseWriter, data []byte) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusOK)
w.Write(data)
}

View File

@ -2,39 +2,64 @@ package models
import (
"html/template"
"main/mvc/models/models_pages"
"log"
"os"
"path/filepath"
"strings"
"time"
)
// App хранит информацию о приложении
type App struct {
Cfg *Config // Сонфиг
Posts models_pages.Posts // Посты
Templates *template.Template // Шаблоны страниц
PagesCache *Cache // Кэш (отрендеренные странички)
Version int64 // Время запуска
Config *Config // Конфиг
Templates *template.Template // Шаблоны страниц
Cache *Cache // Кэш (отрендеренные странички)
Version int64 // Время запуска
}
// Инициализирует приложение
func InitApp() (*App, error) {
var err error
func AppInit(configPath string) (*App, error) {
app := &App{}
a := &App{
Version: time.Now().Unix(),
Config: ConfigInit(),
Cache: CacheInit(),
}
// Версия чтобы статика не кэшировалась
app.Version = time.Now().Unix()
// Загрузка конфига
if app.Cfg, err = loadConfig(ConfigPath); err != nil {
return nil, err
}
// Загрузка постов
if app.Posts, err = models_pages.LoadPosts(app.Cfg.PostsDir); err != nil {
if err := a.Config.Load(configPath); err != nil {
return nil, err
}
// Загрузка шаблонов
if app.Templates, err = loadTemplates(app.Cfg.TemplatesDir, app.Cfg.TemplatesExt); err != nil {
return nil, err
if err := a.loadTemplates(a.Config.TemplatesPath, a.Config.TemplatesExt); err != nil {
log.Fatal(err)
}
// Инициализация кэша
app.PagesCache = initCache()
return app, nil
return a, nil
}
// Загрузка шаблонов
func (a *App) loadTemplates(templatesPath string, ext string) error {
tmpls := template.New("")
err := filepath.Walk(templatesPath, func(path string, f os.FileInfo, err error) error {
if err != nil {
return err
}
if !f.IsDir() && strings.HasSuffix(f.Name(), ext) {
_, err = tmpls.ParseFiles(path)
if err != nil {
return err
}
}
return nil
})
if err != nil {
return err
}
a.Templates = tmpls
return nil
}

View File

@ -7,7 +7,7 @@ type Cache struct {
Mu sync.RWMutex
}
func initCache() *Cache {
func CacheInit() *Cache {
return &Cache{Data: make(map[string]any)}
}

View File

@ -5,32 +5,30 @@ import (
"os"
)
const (
ConfigPath = "config.json"
)
type Config struct {
PostsDir string
AssetsDir string
TemplatesDir string
TemplatesExt string
LocalIP string
LocalPort string
ServerIP string
ServerPort string
ServerDomain string
Port string
AssetsPath string
TemplatesPath string
TemplatesExt string
LocalIP string
LocalPort string
ServerIP string
ServerPort string
ServerDomain string
Port string
}
func loadConfig(configPath string) (*Config, error) {
cfg := &Config{}
func ConfigInit() *Config {
return &Config{}
}
func (c *Config) Load(configPath string) error {
configFile, err := os.ReadFile(configPath)
if err != nil {
return nil, err
return err
}
err = json.Unmarshal(configFile, cfg)
err = json.Unmarshal(configFile, c)
if err != nil {
return nil, err
return err
}
return cfg, nil
return nil
}

View File

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

View File

@ -1,76 +0,0 @@
package models_pages
import (
"bytes"
"fmt"
"html/template"
"main/tools"
"os"
"path/filepath"
"sort"
"strings"
"time"
)
const (
// Имя соответствующего шаблона
PostsPageTmplName = "posts.gohtml"
)
type Posts map[PostLink]*Post
func LoadPosts(dir string) (Posts, error) {
posts := Posts{}
err := filepath.Walk(dir, func(path string, f os.FileInfo, err error) error {
if err != nil {
return err
}
if !f.IsDir() && strings.HasSuffix(f.Name(), ".md") {
md, err := os.ReadFile(path)
if err != nil {
return err
}
html := tools.MdToHTML(md)
link := fmt.Sprintf("/%s/", strings.TrimSuffix(filepath.Base(path), ".md"))
timestamp := f.ModTime().Unix()
posts[PostLink(link)] = newPost(link, html, timestamp)
}
return nil
})
if err != nil {
return nil, err
}
return posts, nil
}
func (p *Posts) RenderPostsPage(templates *template.Template, version int64) ([]byte, error) {
var pageData bytes.Buffer
postsSlice := make([]*Post, 0, len(*p))
for _, post := range *p {
postsSlice = append(postsSlice, post)
}
// Сортирую по ModTimestamp (новые сначала)
sort.Slice(postsSlice, func(i, j int) bool {
return postsSlice[i].Timestamp > postsSlice[j].Timestamp
})
context := map[string]any{
"version": version,
"renderingTimestamp": time.Now().Unix(),
"posts": postsSlice,
}
if err := templates.ExecuteTemplate(&pageData, PostsPageTmplName, context); err != nil {
return nil, err
}
return pageData.Bytes(), nil
}

View File

@ -1,4 +1,4 @@
package models_pages
package models
import (
"bytes"

View File

@ -0,0 +1,27 @@
package models
import (
"bytes"
"html/template"
"time"
)
const (
// Имя соответствующего шаблона
MainRuPageTmplName = "main_ru.gohtml"
)
func RenderMainRuPage(templates *template.Template, version int64) ([]byte, error) {
var pageData bytes.Buffer
context := map[string]any{
"version": version,
"renderingTimestamp": time.Now().Unix(),
}
if err := templates.ExecuteTemplate(&pageData, MainRuPageTmplName, context); err != nil {
return nil, err
}
return pageData.Bytes(), nil
}

View File

@ -1,31 +0,0 @@
package models
import (
"html/template"
"os"
"path/filepath"
"strings"
)
func loadTemplates(templatesPath string, ext string) (*template.Template, error) {
tmpls := template.New("")
err := filepath.Walk(templatesPath, func(path string, f os.FileInfo, err error) error {
if err != nil {
return err
}
if !f.IsDir() && strings.HasSuffix(f.Name(), ext) {
_, err = tmpls.ParseFiles(path)
if err != nil {
return err
}
}
return nil
})
if err != nil {
return nil, err
}
return tmpls, nil
}

View File

@ -1,31 +0,0 @@
{{ define "footer" }}
<footer>
<div>
<img src="/assets/pic/footer.webp?v={{ .version }}" width="100%" height="100%">
</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>
<p>
<strong>some system information</strong>:
</p>
<ul>
<li>unix timestamp of page rendering: <code>{{ .renderingTimestamp }}</code></li>
<li><code>curl -X COUNT https://hikan.ru</code> - 24-hour server request count</li>
<li><code>curl -X LIMINAL https://hikan.ru</code> - what do you know about liminal spaces?</li>
<li>this site code repository - <a href="https://git.hikan.ru/serr/hikan.ru" target="_blank">git.hikan.ru/serr/hikan.ru</a></li>
</ul>
</div>
<div>
<p>
this site is written in Go, hosting is <a href="https://htk.ge" target="_blank">hostetski</a>, domain bought for the price of a can of beer
</p>
<p>
<code>2024 - now</code>
</p>
</div>
</footer>
{{ end }}

View File

@ -1,9 +0,0 @@
{{ define "head" }}
<head>
<title>hikan.ru</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="shortcut icon" href="/assets/pic/favicon.webp?v={{ .version }}" type="image/x-icon">
<link rel="stylesheet" href="/assets/css/styles.css?v={{ .version }}" type="text/css">
</head>
{{ end }}

View File

@ -1,21 +0,0 @@
{{ define "header" }}
<header>
<div>
<img src="/assets/pic/header.webp?v={{ .version }}" width="100%" height="100%">
</div>
<div>
<h1>
contacts
</h1>
<p>
you can message me on <a href="https://t.me/semaphoreslover" target="_blank">telegram</a> or <a href="https://mastodon.ml/@serr" target="_blank">mastodon</a>
</p>
</div>
<div>
<ul>
<li><a href="/">main page</a></li>
<li><a href="/posts/">posts section</a></li>
</ul>
</div>
</header>
{{ end }}

View File

@ -1,8 +1,31 @@
<!DOCTYPE html>
<html lang="en">
{{ template "head" . }}
<head>
<title>hikan.ru</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="shortcut icon" href="/assets/pic/favicon.webp?v={{ .version }}" type="image/x-icon">
<link rel="stylesheet" href="/assets/css/styles.css?v={{ .version }}" type="text/css">
</head>
<body>
{{ template "header" . }}
<header>
<div>
<img src="/assets/pic/header.webp?v={{ .version }}" width="100%" height="100%">
</div>
<div>
<h1>
contacts
</h1>
<p>
you can message me on <a href="https://t.me/semaphoreslover" target="_blank">telegram</a> or <a href="https://mastodon.ml/@serr" target="_blank">mastodon</a>
</p>
</div>
<div>
<ul>
<li><a href="/ru">switch to ru version (AI translation)</a></li>
</ul>
</div>
</header>
<main>
<div>
<h1>
@ -66,6 +89,37 @@
</ul>
</div>
</main>
{{ template "footer" . }}
<footer>
<div>
<img src="/assets/pic/footer.webp?v={{ .version }}" width="100%" height="100%">
</div>
<div>
<p>
and also you can subscribe to my Telegram channel with pictures!
</p>
<p>
<a href="https://t.me/lolistack" target="_blank">digital countryside</a>
</p>
</div>
<div>
<p>
_some system information_
</p>
<ul>
<li>unix timestamp of page rendering - <strong>{{ .renderingTimestamp }}</strong></li>
<li><code>curl -X COUNT https://hikan.ru</code> - 24-hour server request count</li>
<li><code>curl -X LIMINAL https://hikan.ru</code> - what do you know about liminal spaces?</li>
<li>this site code repository - <a href="https://git.hikan.ru/serr/hikan.ru" target="_blank">git.hikan.ru/serr/hikan.ru</a></li>
</ul>
</div>
<div>
<p>
this site is written in Go without using frameworks, hosting is <a href="https://htk.ge" target="_blank">hostetski</a>, domain bought for the price of a can of beer
</p>
<p>
<code>2024 - now</code>
</p>
</div>
</footer>
</body>
</html>

View File

@ -0,0 +1,132 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<title>hikan.ru</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="shortcut icon" href="/assets/pic/favicon.webp?v={{ .version }}" type="image/x-icon">
<link rel="stylesheet" href="/assets/css/styles.css?v={{ .version }}" type="text/css">
</head>
<body>
<header>
<div>
<img src="/assets/pic/header.webp?v={{ .version }}" width="100%" height="100%">
</div>
<div>
<h1>
контакты
</h1>
<p>
вы можете написать мне в <a href="https://t.me/semaphoreslover" target="_blank">telegram</a> или <a href="https://mastodon.ml/@serr" target="_blank">mastodon</a>
</p>
</div>
<div>
<ul>
<li><a href="/">английская версия</a></li>
</ul>
</div>
</header>
<main>
<div>
<p>
<code style="color: #0E53FF">
эту страничку на русский частично переводил DeepSeek, имейте в виду
</code>
</p>
</div>
<div>
<h1>
$whoami
</h1>
<p>
меня зовут serr (мое настоящее имя легко угадать, если вы говорите по-русски :d), и это не я придумал этот никнейм - просто меня стали так называть
</p>
<p>
я родился в 2003 году, сейчас учусь на специалиста по кибербезопасности
</p>
<p>
<code>местоимения: он/его</code>
</p>
</div>
<div>
<h1>
чем я занимаюсь?
</h1>
<p>
программирование - это моё всё: работа, хобби, стиль жизни
</p>
<p>
мне нравится развиваться во всех областях программирования - мне буквально интересно всё: кибербезопасность (хаотичный взлом вещей, анализ кода, написание автоматических анализаторов и перекладывание байтов туда-сюда), многопоточность, веб-разработка, низкоуровневое программирование, криптография и многое другое!
</p>
<p>
мне нравится идея <a href="https://en.wikipedia.org/wiki/Symbolic_execution" target="_blank">символьного</a>/конколического выполнения и виртуального выполнения кода в целом
</p>
</div>
<div>
<h1>
что я люблю
</h1>
<ul>
<li><strong>кофе</strong>. я ОЧЕНЬ люблю кофе. почти любой. и много</li>
<li><strong>фильмы и сериалы</strong> (особенно сериалы). я смотрю что-то почти каждый день</li>
<li><strong>true crime</strong>. я одержим делами о серийных убийцах, таинственных исчезновениях, нераскрытых убийствах - всем этим тёмным материалом</li>
<li><strong>русский андерграундный рэп</strong> типа Slava KPSS, Zamay, MB Packet, Ovsyankin и т.д.</li>
<li><strong>простой и расширяемый код</strong>. я считаю, что если ваш код слишком сложен, значит вы делаете что-то не так. большинство вещей проще, чем кажутся</li>
</ul>
</div>
<div>
<h1>
проекты
</h1>
<ul>
<li><a href="https://git.hikan.ru/serr" target="_blank">git.hikan.ru/serr</a> - мои репозитории</li>
<li><del>телеграм-бот с расписанием для СПбПУ - <a href="https://t.me/polysched_bot" target="_blank">polysched_bot</a></del> (передан более активному владельцу)</li>
<li><del>телеграм-бот с расписанием для Горного - <a href="https://t.me/gornischedule_bot" target="_blank">gornischedule_bot</a></del> (закрыт)</li>
</ul>
</div>
<div>
<h1>
интересные ссылки
</h1>
<ul>
<li><a href="https://mo.rijndael.cc/" target="_blank">Mo</a>, спасибо за идею дизайна!</li>
<li>огромная коллекция номеров Xakep - <a href="https://図書館.きく.コム/" target="_blank">図書館.きく.コム</a></li>
<li>мне нравится этот сайт о Small Web - <a href="https://smallweb.cc/" target="_blank">smallweb</a></li>
<li>очень атмосферный форум о блэк-метале - <a href="https://www.lycanthropia.net/" target="_blank">lycanthropia</a></li>
</ul>
</div>
</main>
<footer>
<div>
<img src="/assets/pic/footer.webp?v={{ .version }}" width="100%" height="100%">
</div>
<div>
<p>
а ещё можно подписаться на мой телеграм-канал с картинками!
</p>
<p>
<a href="https://t.me/lolistack" target="_blank">цифровая деревня</a>
</p>
</div>
<div>
<p>
емного системной информации_
</p>
<ul>
<li>unix-время генерации страницы - <strong>{{ .renderingTimestamp }}</strong></li>
<li><code>curl -X COUNT https://hikan.ru</code> - количество завпросов, обработанных сервером за 24ч</li>
<li><code>curl -X LIMINAL https://hikan.ru</code> - что ты знаешь о лиминальных пространствах?</li>
<li>репозиторий с кодом этого сайта - <a href="https://git.hikan.ru/serr/hikan.ru" target="_blank">git.hikan.ru/serr/hikan.ru</a></li>
</ul>
</div>
<div>
<p>
этот сайт написан на Go без использования фреймворков, хостинг - <a href="https://htk.ge" target="_blank">hostetski</a>, домен куплен по цене банки пива
</p>
<p>
<code>2024 - настоящее время</code>
</p>
</div>
</footer>
</body>
</html>

View File

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

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

@ -1,16 +0,0 @@
# Система загрузки постов
Началось всё с того, что я задумался о том, как же всё таки лучше хранить посты на этом сайте.
Раньше в своих блогах я хранил посты записями в sql базе данных. Ячейка с непосредственно данными поста содержала в себе его html разметку. Я также делал админ-панель на сайте чтобы эту разметку можно было редактировать прямо там.
Но писать что-то в html довольно неудобно, медленно и тд. Писать в *markdown* намного удобнее. Сразу же нашел инструмент [github.com/gomarkdown/markdown](https://github.com/gomarkdown/markdown), позволяющий легко ([пример](https://github.com/gomarkdown/markdown?tab=readme-ov-file#usage)) конвертировать md байты в html байты. А это все что мне нужно.
## Горячая перезагрузка
Подумал, что неплохо было бы написать hot-reloader, чтобы посты менялись на сайте если я как-то их меняю в папке на сервере.
**Идея выглядит так:** есть команда для сборки бинарника (может быть абсолютно любой, хоть cargo, хоть make, хоть gcc и тд., главное чтобы соответствующая система сборки была установлена на устройстве), путь до собранного бинарника, и список директорий, за которыми надо следить.
Далее, запускается скрипт, собирает указанной командой бинарник и начинает следить за директориями. При каких-либо изменениях в директориях, бинарник пересобирается и перезапускается.
На чем писать? Я хочу чтобы никаких зависимостей у скрипта не было вообще. Чтобы можно было его скачать, настроить за пару секунд и все. Решил писать на bash script. Который я кстати **вообще не знаю**. Посидел пару часов, спрашивая непонятные моменты у deepseek. Скрипт в итоге [получился](https://git.hikan.ru/serr/eye-hot-reloader) ровно таким, какой был мне нужен, и уже контролирует работу серверной программы этого сайта.

View File

@ -1,6 +0,0 @@
# Это тестовый пост
[Этот](/test/) пост был *написан* в файле **формата** .md.
Тестирую систему загрузки постов на сайт, исходно находящихся в markdown.
Используемый фреймворк: [github.com/gomarkdown/markdown](https://github.com/gomarkdown/markdown)

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,22 +0,0 @@
package tools
import (
"github.com/gomarkdown/markdown"
"github.com/gomarkdown/markdown/html"
"github.com/gomarkdown/markdown/parser"
)
// Принимает байты .md, отдает .html
func MdToHTML(md []byte) []byte {
// create markdown parser with extensions
extensions := parser.CommonExtensions | parser.NoEmptyLineBeforeBlock
p := parser.NewWithExtensions(extensions)
doc := p.Parse(md)
// create HTML renderer with extensions
htmlFlags := html.CommonFlags | html.HrefTargetBlank
opts := html.RendererOptions{Flags: htmlFlags}
renderer := html.NewRenderer(opts)
return markdown.Render(doc, renderer)
}