Compare commits

...

31 Commits

Author SHA1 Message Date
serr a6d69b0d4e значения в хэше теперь обязательно срезы байт 2025-04-19 01:36:27 +03:00
serr 09fe783f4a new favi 2025-04-19 01:21:55 +03:00
serr 3c34db6610 chess backgr + some shadows 2025-04-19 01:18:11 +03:00
serr cbcbcf9753 some css change 2025-04-15 20:22:08 +03:00
serr 6440319cd5 css changes 2025-04-15 19:43:21 +03:00
serr 1176c36fcc css changes 2025-04-15 19:40:07 +03:00
serr eee53cf492 delete some code 2025-04-15 19:09:41 +03:00
serr d7dcff28ac defer file.Close() added 2025-04-15 19:04:23 +03:00
serr 55b84454cd styles change 2025-04-14 21:20:55 +03:00
serr b61696c469 styles change 2025-04-14 21:20:42 +03:00
serr b0d2e10f7b styles change 2025-04-14 21:20:19 +03:00
serr 68c7390bd2 small change css 2025-04-14 21:07:00 +03:00
serr e223135501 eye.sh new ver 2025-04-13 17:25:35 +03:00
serr e10f131a5a eye.sh new ver 2025-04-13 17:06:27 +03:00
serr 5f52a4dd7b some changes 2025-04-13 16:26:57 +03:00
serr f616d45278 post link bug fixed 2025-04-13 12:58:20 +03:00
serr 3150c70e1c del post class 2025-04-13 12:32:19 +03:00
serr e2685ddde6 posts cache bug fixed 2025-04-13 12:26:48 +03:00
serr 1f0c0d22c8 eye.sh 2025-04-12 18:56:24 +03:00
serr 3c301dbefc sorted posts 2025-04-12 17:32:35 +03:00
serr 7e620f7308 stop tracking eye.sh 2025-04-12 17:01:14 +03:00
serr 980df5179a some changes 2025-04-12 16:59:45 +03:00
serr 5082221d78 some changes 2025-04-11 11:32:05 +03:00
serr cb0358dbb4 facade 2025-04-11 11:12:38 +03:00
serr b68851c862 some changes 2025-04-10 22:43:47 +03:00
serr 70d82d69d1 some changes 2025-04-10 22:07:44 +03:00
serr 5a34d18ed6 models_pages and controllers_pages 2025-04-10 21:50:10 +03:00
serr cbce343e51 выделил в cfg из app 2025-04-10 21:37:54 +03:00
serr 066a30ff7d конфиг выделить изи апп 2025-04-10 21:25:50 +03:00
serr 13ca4c403b some changes 2025-04-10 17:11:14 +03:00
serr d0162ffa33 начал писать систему постов 2025-04-10 16:59:00 +03:00
30 changed files with 619 additions and 386 deletions

2
.gitignore vendored
View File

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

View File

@ -6,6 +6,9 @@ body {
margin: 0;
align-items: flex-start;
padding: 10px;
background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHdpZHRoPSc0MCcgaGVpZ2h0PSc0MCc+PHJlY3QgZmlsbD0nIzkxZWFlNCcgeD0nMCcgeT0nMCcgd2lkdGg9JzIwcHgnIGhlaWdodD0nMjBweCcvPjxyZWN0IGZpbGw9JyM4NmE4ZTcnIHg9JzAnIHk9JzIwJyB3aWR0aD0nMjBweCcgaGVpZ2h0PScyMHB4Jy8+PHJlY3QgZmlsbD0nIzkxZWFlNCcgeD0nMjAnIHk9JzIwJyB3aWR0aD0nMjBweCcgaGVpZ2h0PScyMHB4Jy8+PHJlY3QgZmlsbD0nIzg2YThlNycgeD0nMjAnIHk9JzAnIHdpZHRoPScyMHB4JyBoZWlnaHQ9JzIwcHgnLz48L3N2Zz4=");
background-repeat: repeat;
}
header, main, footer {
@ -23,28 +26,18 @@ main {
flex: 3;
}
header > div,
footer > div,
main > div {
box-shadow: 5px 5px 0 0 lightgrey;
div {
text-align: left;
background-color: white;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
box-sizing: border-box;
border: 1px solid;
width: 100%;
padding: 0 20px;
}
h1 {
text-align: center;
color: black;
padding-left: 10px;
padding-right: 10px;
}
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;

BIN
assets/pic/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

110
eye.sh Executable file
View File

@ -0,0 +1,110 @@
#!/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,3 +1,5 @@
module main
go 1.23.2
require github.com/gomarkdown/markdown v0.0.0-20250311123330-531bef5e742b

2
go.sum Normal file
View File

@ -0,0 +1,2 @@
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=

44
main.go
View File

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

View File

@ -1,15 +1,16 @@
package controllers
package controllers_pages
import (
"log"
"main/mvc/models"
models_pages "main/mvc/models/pages"
"main/mvc/models/models_pages"
"main/tools"
"net/http"
)
// Обработчик главной страницы
func MainPageHandler(a *models.App) http.HandlerFunc {
func MainPageHandler(app *models.App) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var err error
@ -17,63 +18,63 @@ func MainPageHandler(a *models.App) http.HandlerFunc {
// Количество запросов, обработанных сервером за 24ч
if r.Method == "COUNT" {
var count []byte
if count, err = tools.GetJournalctlLogsCount("server", a.Config.ServerDomain, 24); err != nil {
if count, err = tools.GetJournalctlLogsCount("server", app.Cfg.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 := a.Cache.Get(models_pages.MainPageTmplName)
pageData, ok := app.PagesCache.Get(models_pages.MainPageTmplName)
if !ok {
pageData, err = models_pages.RenderMainPage(a.Templates, a.Version)
pageData, err = models_pages.RenderMainPage(app.Templates, app.Version)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
a.Cache.Set(models_pages.MainPageTmplName, pageData)
app.PagesCache.Set(models_pages.MainPageTmplName, pageData)
}
SendMainPage(w, pageData.([]byte))
sendMainPage(w, pageData)
})
}
// Отправляет страницу
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

@ -0,0 +1,47 @@
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)
}

View File

@ -1,34 +1,34 @@
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)
}
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)
})
}
// Отправляет страницу
func sendPostsPage(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,21 +9,18 @@ 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)
@ -32,7 +29,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

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

View File

@ -3,22 +3,22 @@ package models
import "sync"
type Cache struct {
Data map[string]any
Data map[string][]byte
Mu sync.RWMutex
}
func CacheInit() *Cache {
return &Cache{Data: make(map[string]any)}
func initCache() *Cache {
return &Cache{Data: make(map[string][]byte)}
}
func (c *Cache) Get(key string) (any, bool) {
func (c *Cache) Get(key string) ([]byte, bool) {
c.Mu.RLock()
pageData, ok := c.Data[key]
c.Mu.RUnlock()
return pageData, ok
}
func (c *Cache) Set(key string, data any) {
func (c *Cache) Set(key string, data []byte) {
c.Mu.Lock()
c.Data[key] = data
c.Mu.Unlock()

View File

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

View File

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

View File

@ -0,0 +1,55 @@
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

@ -0,0 +1,76 @@
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,27 +0,0 @@
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
}

31
mvc/models/templates.go Normal file
View File

@ -0,0 +1,31 @@
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

@ -0,0 +1,31 @@
{{ 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

@ -0,0 +1,9 @@
{{ 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 }}

View File

@ -0,0 +1,21 @@
{{ 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,31 +1,8 @@
<!DOCTYPE html>
<html lang="en">
<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>
{{ template "head" . }}
<body>
<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>
{{ template "header" . }}
<main>
<div>
<h1>
@ -89,37 +66,6 @@
</ul>
</div>
</main>
<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>
{{ template "footer" . }}
</body>
</html>

View File

@ -1,132 +0,0 @@
<!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

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

View File

@ -0,0 +1,29 @@
<!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,16 @@
# Система загрузки постов
Началось всё с того, что я задумался о том, как же всё таки лучше хранить посты на этом сайте.
Раньше в своих блогах я хранил посты записями в 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) ровно таким, какой был мне нужен, и уже контролирует работу серверной программы этого сайта.

6
posts/test.md Normal file
View File

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

View File

@ -0,0 +1,12 @@
# Как же все таки изменить байты строки в 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**

22
tools/parse.go Normal file
View File

@ -0,0 +1,22 @@
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)
}