Compare commits
No commits in common. "posts" and "master" have entirely different histories.
|
@ -1,2 +1,2 @@
|
||||||
config.json
|
config.json
|
||||||
hikan.ru
|
restart.sh
|
|
@ -6,9 +6,6 @@ body {
|
||||||
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-repeat: repeat;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
header, main, footer {
|
header, main, footer {
|
||||||
|
@ -26,18 +23,28 @@ main {
|
||||||
flex: 3;
|
flex: 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
div {
|
header > div,
|
||||||
text-align: left;
|
footer > div,
|
||||||
background-color: white;
|
main > div {
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
|
box-shadow: 5px 5px 0 0 lightgrey;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
border: 1px solid;
|
border: 1px solid;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 0 20px;
|
text-align: center;
|
||||||
|
color: black;
|
||||||
|
padding-left: 10px;
|
||||||
|
padding-right: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
header > div > ul,
|
||||||
text-align: center;
|
footer > div > ul,
|
||||||
|
main > div > ul {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
header > div > h1,
|
||||||
|
footer > div > h1,
|
||||||
|
main > div > h1 {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
border-top: 1px solid;
|
border-top: 1px solid;
|
||||||
border-bottom: 1px solid;
|
border-bottom: 1px solid;
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 1.2 KiB |
110
eye.sh
110
eye.sh
|
@ -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
2
go.mod
|
@ -1,5 +1,3 @@
|
||||||
module main
|
module main
|
||||||
|
|
||||||
go 1.23.2
|
go 1.23.2
|
||||||
|
|
||||||
require github.com/gomarkdown/markdown v0.0.0-20250311123330-531bef5e742b
|
|
||||||
|
|
2
go.sum
2
go.sum
|
@ -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
52
main.go
|
@ -4,58 +4,62 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"main/mvc/controllers"
|
"main/mvc/controllers"
|
||||||
"main/mvc/controllers/controllers_pages"
|
controllers_pages "main/mvc/controllers/pages"
|
||||||
"main/mvc/models"
|
"main/mvc/models"
|
||||||
"main/tools"
|
"main/tools"
|
||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
var err error
|
|
||||||
var app *models.App
|
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.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Добавление префикса в виде домена сервера к записям в лог
|
// Добавление префикса в виде домена сервера к записям в лог
|
||||||
log.SetPrefix(fmt.Sprintf("%s | ", app.Cfg.ServerDomain))
|
log.SetPrefix(fmt.Sprintf("%s | ", app.Config.ServerDomain))
|
||||||
|
|
||||||
// Настройка маршрутов
|
// Настройка маршрутов и запуск
|
||||||
router := setupRoutes(app)
|
if setupRoutesAndRun(app) != nil {
|
||||||
|
|
||||||
// Запуск сервера
|
|
||||||
if ok, err := tools.IsIPInUse(app.Cfg.ServerIP); err != nil {
|
|
||||||
log.Fatal(err)
|
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()
|
router := http.NewServeMux()
|
||||||
|
|
||||||
// Цепочка обработчиков, которые сработают до отдачи страницы юзеру
|
// Цепочка обработчиков, которые сработают до отдачи страницы юзеру
|
||||||
m := controllers.MiddlewaresChain
|
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("/", m(controllers_pages.MainPageHandler(a)))
|
||||||
// Обработка страницы со списком постов
|
|
||||||
router.Handle("/posts/", m(controllers_pages.PostsPageHandler(app)))
|
|
||||||
// Обработка страничек постов
|
|
||||||
for key := range app.Posts {
|
|
||||||
postLink := string(key)
|
|
||||||
router.Handle(postLink, m(controllers_pages.PostPageHandler(app)))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return router
|
return router
|
||||||
|
|
|
@ -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)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Отправляет страницу
|
|
||||||
func sendPostPage(w http.ResponseWriter, data []byte) {
|
|
||||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
w.Write(data)
|
|
||||||
}
|
|
|
@ -9,18 +9,21 @@ import (
|
||||||
type Middleware func(http.Handler) http.Handler
|
type Middleware func(http.Handler) http.Handler
|
||||||
|
|
||||||
var (
|
var (
|
||||||
MiddlewaresChain = createMiddlewaresChain(
|
MiddlewaresChain = CreateMiddlewaresChain(
|
||||||
loggingMiddleware,
|
LoggingMiddleware,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Возвращает один middleware, который объединяет все переданные
|
Возвращает один middleware, который объединяет все переданные
|
||||||
|
|
||||||
createMiddlewaresChain(m1, m2, m3)
|
CreateMiddlewaresChain(m1, m2, m3)
|
||||||
= func(next http.Handler) http.Handler { return m1(m2(m3(final))) }
|
= 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 {
|
return func(final http.Handler) http.Handler {
|
||||||
for i := len(middlewares) - 1; i >= 0; i-- {
|
for i := len(middlewares) - 1; i >= 0; i-- {
|
||||||
final = middlewares[i](final)
|
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) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
|
|
||||||
|
|
|
@ -1,16 +1,15 @@
|
||||||
package controllers_pages
|
package controllers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
"main/mvc/models"
|
"main/mvc/models"
|
||||||
"main/mvc/models/models_pages"
|
models_pages "main/mvc/models/pages"
|
||||||
"main/tools"
|
"main/tools"
|
||||||
"net/http"
|
"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) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
|
@ -18,63 +17,63 @@ func MainPageHandler(app *models.App) http.HandlerFunc {
|
||||||
// Количество запросов, обработанных сервером за 24ч
|
// Количество запросов, обработанных сервером за 24ч
|
||||||
if r.Method == "COUNT" {
|
if r.Method == "COUNT" {
|
||||||
var count []byte
|
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())
|
log.Printf("%s", err.Error())
|
||||||
}
|
}
|
||||||
sendCount(w, count)
|
SendCount(w, count)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Пасхалка
|
// Пасхалка
|
||||||
if r.Method == "LOVE" {
|
if r.Method == "LOVE" {
|
||||||
sendLove(w)
|
SendLove(w)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Пасхалка 2
|
// Пасхалка 2
|
||||||
if r.Method == "LIMINAL" {
|
if r.Method == "LIMINAL" {
|
||||||
sendLiminal(w)
|
SendLiminal(w)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Страничка рендерится только если ее нет в кэше
|
// Страничка рендерится только если ее нет в кэше
|
||||||
pageData, ok := app.PagesCache.Get(models_pages.MainPageTmplName)
|
pageData, ok := a.Cache.Get(models_pages.MainPageTmplName)
|
||||||
if !ok {
|
if !ok {
|
||||||
pageData, err = models_pages.RenderMainPage(app.Templates, app.Version)
|
pageData, err = models_pages.RenderMainPage(a.Templates, a.Version)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
app.PagesCache.Set(models_pages.MainPageTmplName, pageData)
|
a.Cache.Set(models_pages.MainPageTmplName, pageData)
|
||||||
}
|
}
|
||||||
|
|
||||||
sendMainPage(w, pageData)
|
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.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
w.Write(data)
|
w.Write(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ответ на метод COUNT
|
// Ответ на метод 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.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
w.Write(data)
|
w.Write(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"))
|
w.Write([]byte("13.01.2005\n"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ответ на метод LIMINAL
|
// Ответ на метод LIMINAL
|
||||||
func sendLiminal(w http.ResponseWriter) {
|
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"
|
|
@ -1,34 +1,34 @@
|
||||||
package controllers_pages
|
package controllers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"main/mvc/models"
|
"main/mvc/models"
|
||||||
"main/mvc/models/models_pages"
|
models_pages "main/mvc/models/pages"
|
||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Обработчик главной страницы
|
// Обработчик главной страницы
|
||||||
func PostsPageHandler(app *models.App) http.HandlerFunc {
|
func MainRuPageHandler(a *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 err error
|
||||||
|
|
||||||
// Страничка рендерится только если ее нет в кэше
|
// Страничка рендерится только если ее нет в кэше
|
||||||
pageData, ok := app.PagesCache.Get(models_pages.PostsPageTmplName)
|
pageData, ok := a.Cache.Get(models_pages.MainRuPageTmplName)
|
||||||
if !ok {
|
if !ok {
|
||||||
pageData, err = app.Posts.RenderPostsPage(app.Templates, app.Version)
|
pageData, err = models_pages.RenderMainRuPage(a.Templates, a.Version)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
app.PagesCache.Set(models_pages.PostsPageTmplName, pageData)
|
a.Cache.Set(models_pages.MainRuPageTmplName, pageData)
|
||||||
}
|
}
|
||||||
|
|
||||||
sendPostsPage(w, pageData)
|
SendMainPage(w, pageData.([]byte))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Отправляет страницу
|
// Отправляет страницу
|
||||||
func sendPostsPage(w http.ResponseWriter, data []byte) {
|
func SendMainRuPage(w http.ResponseWriter, data []byte) {
|
||||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
w.Write(data)
|
w.Write(data)
|
||||||
}
|
}
|
|
@ -2,39 +2,64 @@ package models
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"html/template"
|
"html/template"
|
||||||
"main/mvc/models/models_pages"
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// App хранит информацию о приложении
|
||||||
type App struct {
|
type App struct {
|
||||||
Cfg *Config // Сонфиг
|
Config *Config // Конфиг
|
||||||
Posts models_pages.Posts // Посты
|
Templates *template.Template // Шаблоны страниц
|
||||||
Templates *template.Template // Шаблоны страниц
|
Cache *Cache // Кэш (отрендеренные странички)
|
||||||
PagesCache *Cache // Кэш (отрендеренные странички)
|
Version int64 // Время запуска
|
||||||
Version int64 // Время запуска
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Инициализирует приложение
|
// Инициализирует приложение
|
||||||
func InitApp() (*App, error) {
|
func AppInit(configPath string) (*App, error) {
|
||||||
var err 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 {
|
if err := a.Config.Load(configPath); err != nil {
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
// Загрузка постов
|
|
||||||
if app.Posts, err = models_pages.LoadPosts(app.Cfg.PostsDir); err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Загрузка шаблонов
|
// Загрузка шаблонов
|
||||||
if app.Templates, err = loadTemplates(app.Cfg.TemplatesDir, app.Cfg.TemplatesExt); err != nil {
|
if err := a.loadTemplates(a.Config.TemplatesPath, a.Config.TemplatesExt); err != nil {
|
||||||
return nil, err
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
// Инициализация кэша
|
|
||||||
app.PagesCache = initCache()
|
return a, nil
|
||||||
return app, 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
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,22 +3,22 @@ package models
|
||||||
import "sync"
|
import "sync"
|
||||||
|
|
||||||
type Cache struct {
|
type Cache struct {
|
||||||
Data map[string][]byte
|
Data map[string]any
|
||||||
Mu sync.RWMutex
|
Mu sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
func initCache() *Cache {
|
func CacheInit() *Cache {
|
||||||
return &Cache{Data: make(map[string][]byte)}
|
return &Cache{Data: make(map[string]any)}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Cache) Get(key string) ([]byte, bool) {
|
func (c *Cache) Get(key string) (any, bool) {
|
||||||
c.Mu.RLock()
|
c.Mu.RLock()
|
||||||
pageData, ok := c.Data[key]
|
pageData, ok := c.Data[key]
|
||||||
c.Mu.RUnlock()
|
c.Mu.RUnlock()
|
||||||
return pageData, ok
|
return pageData, ok
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Cache) Set(key string, data []byte) {
|
func (c *Cache) Set(key string, data any) {
|
||||||
c.Mu.Lock()
|
c.Mu.Lock()
|
||||||
c.Data[key] = data
|
c.Data[key] = data
|
||||||
c.Mu.Unlock()
|
c.Mu.Unlock()
|
||||||
|
|
|
@ -5,32 +5,30 @@ import (
|
||||||
"os"
|
"os"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
ConfigPath = "config.json"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
PostsDir string
|
AssetsPath string
|
||||||
AssetsDir string
|
TemplatesPath string
|
||||||
TemplatesDir string
|
TemplatesExt string
|
||||||
TemplatesExt string
|
LocalIP string
|
||||||
LocalIP string
|
LocalPort string
|
||||||
LocalPort string
|
ServerIP string
|
||||||
ServerIP string
|
ServerPort string
|
||||||
ServerPort string
|
ServerDomain string
|
||||||
ServerDomain string
|
Port string
|
||||||
Port string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadConfig(configPath string) (*Config, error) {
|
func ConfigInit() *Config {
|
||||||
cfg := &Config{}
|
return &Config{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) Load(configPath string) error {
|
||||||
configFile, err := os.ReadFile(configPath)
|
configFile, err := os.ReadFile(configPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
err = json.Unmarshal(configFile, cfg)
|
err = json.Unmarshal(configFile, c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
return cfg, nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -1,4 +1,4 @@
|
||||||
package models_pages
|
package models
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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 }}
|
|
|
@ -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.png?v={{ .version }}" type="image/x-icon">
|
|
||||||
<link rel="stylesheet" href="/assets/css/styles.css?v={{ .version }}" type="text/css">
|
|
||||||
</head>
|
|
||||||
{{ end }}
|
|
|
@ -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 }}
|
|
|
@ -1,8 +1,31 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<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>
|
<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>
|
<main>
|
||||||
<div>
|
<div>
|
||||||
<h1>
|
<h1>
|
||||||
|
@ -66,6 +89,37 @@
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</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>
|
</body>
|
||||||
</html>
|
</html>
|
|
@ -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>
|
|
@ -1,13 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
{{ template "head" . }}
|
|
||||||
<body>
|
|
||||||
{{ template "header" . }}
|
|
||||||
<main>
|
|
||||||
<div>
|
|
||||||
{{ .data }}
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
{{ template "footer" . }}
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -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>
|
|
|
@ -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) ровно таким, какой был мне нужен, и уже контролирует работу серверной программы этого сайта.
|
|
|
@ -1,6 +0,0 @@
|
||||||
# Это тестовый пост
|
|
||||||
[Этот](/test/) пост был *написан* в файле **формата** .md.
|
|
||||||
|
|
||||||
Тестирую систему загрузки постов на сайт, исходно находящихся в markdown.
|
|
||||||
|
|
||||||
Используемый фреймворк: [github.com/gomarkdown/markdown](https://github.com/gomarkdown/markdown)
|
|
|
@ -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**
|
|
|
@ -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)
|
|
||||||
}
|
|
Loading…
Reference in New Issue