Compare commits
1 Commits
design
...
clientcach
Author | SHA1 | Date |
---|---|---|
|
975e7e8a4c |
|
@ -1,4 +1,2 @@
|
|||
config.json
|
||||
hikan.ru
|
||||
*.server*
|
||||
*.log
|
||||
hikan.ru
|
|
@ -1,17 +1,13 @@
|
|||
::selection {
|
||||
background: purple;
|
||||
color: #fff;
|
||||
}
|
||||
/* Design idea from here https://mo.rijndael.cc/ */
|
||||
|
||||
body {
|
||||
font-family: -apple-system,system-ui,blinkmacsystemfont,Segoe UI,roboto,oxygen,ubuntu,Helvetica Neue,arial,sans-serif;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin: 0;
|
||||
align-items: flex-start;
|
||||
padding: 10px;
|
||||
background-image: url("/assets/pic/star.gif");
|
||||
background-color: #000;
|
||||
background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHdpZHRoPSc0MCcgaGVpZ2h0PSc0MCc+PHJlY3QgZmlsbD0nIzkxZWFlNCcgeD0nMCcgeT0nMCcgd2lkdGg9JzIwcHgnIGhlaWdodD0nMjBweCcvPjxyZWN0IGZpbGw9JyM4NmE4ZTcnIHg9JzAnIHk9JzIwJyB3aWR0aD0nMjBweCcgaGVpZ2h0PScyMHB4Jy8+PHJlY3QgZmlsbD0nIzkxZWFlNCcgeD0nMjAnIHk9JzIwJyB3aWR0aD0nMjBweCcgaGVpZ2h0PScyMHB4Jy8+PHJlY3QgZmlsbD0nIzg2YThlNycgeD0nMjAnIHk9JzAnIHdpZHRoPScyMHB4JyBoZWlnaHQ9JzIwcHgnLz48L3N2Zz4=");
|
||||
background-repeat: repeat;
|
||||
}
|
||||
|
||||
header, main, footer {
|
||||
|
@ -25,154 +21,38 @@ header, footer {
|
|||
flex: 2;
|
||||
}
|
||||
|
||||
marquee {
|
||||
font-weight: bold;
|
||||
color: #b1cfd8;
|
||||
}
|
||||
|
||||
main {
|
||||
flex: 3;
|
||||
}
|
||||
|
||||
div {
|
||||
text-align: left;
|
||||
background-color: #b1cfd8;
|
||||
box-shadow: 0 0 3px 3px rgba(135, 182, 196, 0.95);
|
||||
background-color: white;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
|
||||
box-sizing: border-box;
|
||||
border: 1px solid;
|
||||
border-radius: 10px;
|
||||
width: 100%;
|
||||
padding: 0 20px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.pagi {
|
||||
color: #b1cfd8;
|
||||
text-align: center;
|
||||
background-color: transparent;
|
||||
box-shadow: none;
|
||||
border: none;
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
padding: 30px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
h1::before, h1::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 10px;
|
||||
background: url('data:image/svg+xml;utf8,<svg viewBox="0 0 100 10" xmlns="http://www.w3.org/2000/svg"><path d="M0,5 Q25,10 50,5 T100,5" fill="none" stroke="%236a8498" stroke-width="2"/></svg>');
|
||||
}
|
||||
|
||||
h1::before { top: 0; }
|
||||
h1::after { bottom: 0; }
|
||||
|
||||
a {
|
||||
cursor: pointer;
|
||||
color: black;
|
||||
text-decoration: none;
|
||||
padding: 0 5px;
|
||||
background-image: linear-gradient(to bottom, transparent 60%, #77abbb 60%);
|
||||
background-repeat: no-repeat;
|
||||
border-radius: 2px;
|
||||
box-sizing: border-box;
|
||||
border-top: 1px solid;
|
||||
border-bottom: 1px solid;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
background-image: linear-gradient(to bottom, transparent 60%, #778bbb 60%);
|
||||
.count {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
a[hx-get] {
|
||||
background-image: linear-gradient(to right, #77abbb, #778bbb);
|
||||
.count img {
|
||||
height: 75px;
|
||||
}
|
||||
|
||||
a[hx-get]:hover {
|
||||
background-image: linear-gradient(to right, #778bbb, #778bbb);
|
||||
}
|
||||
|
||||
pre {
|
||||
padding: 10px;
|
||||
background: #77abbb;
|
||||
border: 1px solid #000;
|
||||
border-radius: 2px;
|
||||
overflow-x: auto;
|
||||
white-space: pre;
|
||||
word-wrap: normal;
|
||||
width: 0;
|
||||
min-width: calc(100% - 22px);
|
||||
}
|
||||
|
||||
details > summary {
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
details[open] > summary {
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.rotating-pic-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.rotating-pic {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
animation: swing 1.5s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
@keyframes swing {
|
||||
0% { transform: rotate(-10deg); }
|
||||
100% { transform: rotate(10deg); }
|
||||
}
|
||||
|
||||
.pulse-container {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.pulse-text {
|
||||
font-weight: bold;
|
||||
color: purple;
|
||||
animation: scalePulse 2s infinite ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes scalePulse {
|
||||
0%, 100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 800px) {
|
||||
a:hover {
|
||||
background-image: linear-gradient(to bottom, transparent 60%, #77abbb 60%);
|
||||
}
|
||||
|
||||
a[hx-get]:hover {
|
||||
background-image: linear-gradient(to right, #77abbb, #778bbb);
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
header, footer, main {
|
||||
flex: 1 100%;
|
||||
}
|
||||
|
||||
details > summary {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
details[open] > summary {
|
||||
cursor: default;
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 6.5 KiB |
After Width: | Height: | Size: 6.9 KiB |
After Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 9.7 KiB |
After Width: | Height: | Size: 9.3 KiB |
After Width: | Height: | Size: 8.3 KiB |
After Width: | Height: | Size: 8.1 KiB |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 5.0 KiB |
Before Width: | Height: | Size: 15 KiB |
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 488 B |
After Width: | Height: | Size: 9.4 KiB |
After Width: | Height: | Size: 60 KiB |
Before Width: | Height: | Size: 132 KiB |
Before Width: | Height: | Size: 153 KiB |
Before Width: | Height: | Size: 3.0 KiB |
4
eye.sh
|
@ -3,8 +3,8 @@ stty -echoctl # Отключает вывод управляющих симво
|
|||
|
||||
# НАСТРОЙКА СКРИПТА ТУТ ###########################################################
|
||||
DURATION=1 # Задержка между проверками в секундах
|
||||
WATCH_TARGETS=("assets" "mvc" "posts" "tools" "main.go" "config.json") # Массив целей для наблюдения (директории и файлы)
|
||||
BINARY_PATH="./main.server" # Путь до бинарного файла
|
||||
WATCH_TARGETS=("assets" "mvc" "posts" "tools" "main.go") # Массив целей для наблюдения (директории и файлы)
|
||||
BINARY_PATH="./main" # Путь до бинарного файла
|
||||
BUILD_CMD="go build -o $BINARY_PATH main.go" # Команда для сборки
|
||||
###################################################################################
|
||||
|
||||
|
|
66
main.go
|
@ -8,55 +8,19 @@ import (
|
|||
"main/mvc/models"
|
||||
"main/tools"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var err error
|
||||
var app *models.App
|
||||
var logFile *os.File
|
||||
defer logFile.Close()
|
||||
|
||||
// Инициализация приложения
|
||||
if app, err = models.InitApp(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
// Запуск тикеров
|
||||
{
|
||||
// Переоткрытие файла с логами периодически чтобы он не стал слишком большим
|
||||
tools.Ticker(func() {
|
||||
newLogFile, err := tools.SetupLogging(app.Cfg.LogFilePath)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
logFile.Close()
|
||||
logFile = newLogFile
|
||||
log.Println("Logging system restarted")
|
||||
}, time.Second*app.Cfg.LogSystemRestartInterval)
|
||||
|
||||
// Обновление последнего прослушанного трека ластфм
|
||||
tools.Ticker(func() {
|
||||
var lastTrack string
|
||||
if lastTrack, err = models.LastFMGetLastTrack(app.Cfg.LastFMUsername, app.Cfg.LastFMToken); err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
app.LastfmLastTrack = lastTrack
|
||||
}, time.Second*app.Cfg.LastFMUpdateInterval)
|
||||
|
||||
// Сообщение в лог о количестве элементов в кэше
|
||||
tools.Ticker(func() {
|
||||
log.Println("Pages in cache:")
|
||||
if len(app.PagesCache.Data) != 0 {
|
||||
for key := range app.PagesCache.Data {
|
||||
log.Println(key)
|
||||
}
|
||||
} else {
|
||||
log.Println("No pages in the cache")
|
||||
}
|
||||
}, time.Second*app.Cfg.CacheLogInterval)
|
||||
}
|
||||
// Добавление префикса в виде домена сервера к записям в лог
|
||||
log.SetPrefix(fmt.Sprintf("%s | ", app.Cfg.ServerDomain))
|
||||
|
||||
// Настройка маршрутов
|
||||
router := setupRoutes(app)
|
||||
|
@ -85,25 +49,13 @@ func setupRoutes(app *models.App) *http.ServeMux {
|
|||
{
|
||||
// Обработка главной страницы
|
||||
router.Handle("/", m(controllers_pages.MainPageHandler(app)))
|
||||
}
|
||||
|
||||
// Странички со списками постов
|
||||
postsPagesCount := (len(app.PostsMap) + app.Cfg.PostsMaxCountOnPage - 1) / app.Cfg.PostsMaxCountOnPage
|
||||
for i := range postsPagesCount {
|
||||
router.Handle(
|
||||
fmt.Sprintf("/posts/%d", i),
|
||||
m(controllers_pages.PostsPageHandler(app)))
|
||||
}
|
||||
|
||||
// Обработка страничек постов
|
||||
for key := range app.PostsMap {
|
||||
postLink := string(key)
|
||||
router.Handle(postLink, m(controllers_pages.PostPageHandler(app)))
|
||||
}
|
||||
|
||||
// Api
|
||||
{
|
||||
router.Handle("/api/lastfm", m(controllers.LastFMHandler(app)))
|
||||
// Обработка страницы со списком постов
|
||||
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
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package controllers_pages
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"main/mvc/models"
|
||||
|
@ -14,15 +13,12 @@ import (
|
|||
func MainPageHandler(app *models.App) http.HandlerFunc {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
var (
|
||||
pageData []byte
|
||||
err error
|
||||
)
|
||||
var err error
|
||||
|
||||
// Количество запросов, обработанных сервером за 24ч
|
||||
if r.Method == "COUNT" {
|
||||
var count int
|
||||
if count, err = tools.GetLogFileEntriesCount(app.Cfg.LogFilePath, "duration: ", 24); err != nil {
|
||||
var count []byte
|
||||
if count, err = tools.GetJournalctlLogsCount("server", app.Cfg.ServerDomain, 24); err != nil {
|
||||
log.Printf("%s", err.Error())
|
||||
}
|
||||
sendCount(w, count)
|
||||
|
@ -41,47 +37,50 @@ func MainPageHandler(app *models.App) http.HandlerFunc {
|
|||
return
|
||||
}
|
||||
|
||||
cacheKey := models_pages.MainPageTmplName
|
||||
ajax := r.URL.Query().Get("ajax") == "true"
|
||||
if ajax {
|
||||
cacheKey += "?ajax=true"
|
||||
// Страничка рендерится только если ее нет в кэше
|
||||
page, ok := app.PagesCache.Get(models_pages.MainPageTmplName)
|
||||
if !ok {
|
||||
pageData, err := models_pages.RenderMainPage(app.Templates, app.Version)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
ETag := models.GenerateETag(pageData)
|
||||
page = &models.Page{Data: pageData, ETag: ETag}
|
||||
app.PagesCache.Set(models_pages.MainPageTmplName, page)
|
||||
}
|
||||
|
||||
if pageData, ok := app.PagesCache.Get(cacheKey); ok {
|
||||
sendMainPage(w, pageData)
|
||||
return
|
||||
}
|
||||
|
||||
pageData, err = models_pages.RenderMainPage(app.Templates, app.Version, ajax)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
app.PagesCache.Set(cacheKey, pageData)
|
||||
|
||||
sendMainPage(w, pageData)
|
||||
sendMainPage(r, w, page)
|
||||
})
|
||||
}
|
||||
|
||||
// Отправляет страницу
|
||||
func sendMainPage(w http.ResponseWriter, data []byte) {
|
||||
func sendMainPage(r *http.Request, w http.ResponseWriter, page *models.Page) {
|
||||
|
||||
w.Header().Set("Cache-Control", "public, max-age=31536000")
|
||||
w.Header().Set("ETag", page.ETag)
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write(data)
|
||||
|
||||
if match := r.Header.Get("If-None-Match"); match == page.ETag {
|
||||
w.WriteHeader(http.StatusNotModified)
|
||||
return
|
||||
}
|
||||
|
||||
w.Write(page.Data)
|
||||
}
|
||||
|
||||
// Ответ на метод COUNT
|
||||
func sendCount(w http.ResponseWriter, data int) {
|
||||
func sendCount(w http.ResponseWriter, data []byte) {
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
fmt.Fprint(w, data)
|
||||
w.Write(data)
|
||||
}
|
||||
|
||||
// Ответ на метод LOVE
|
||||
func sendLove(w http.ResponseWriter) {
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
fmt.Fprint(w, "13.01.2005\n")
|
||||
w.Write([]byte("13.01.2005\n"))
|
||||
}
|
||||
|
||||
// Ответ на метод LIMINAL
|
||||
|
@ -89,5 +88,6 @@ 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"
|
||||
fmt.Fprint(w, text)
|
||||
w.Write([]byte(text))
|
||||
|
||||
}
|
||||
|
|
|
@ -4,44 +4,49 @@ 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) {
|
||||
link := r.URL.Path
|
||||
cacheKey := link
|
||||
ajax := r.URL.Query().Get("ajax") == "true"
|
||||
if ajax {
|
||||
cacheKey += "?ajax=true"
|
||||
postLink := r.URL.Path
|
||||
|
||||
// Ссылки на посты имеют вид postLink = /link/, а если прилетело что то типо /link/123123,
|
||||
// то надо оставить только часть /link/
|
||||
secondSlash := strings.IndexByte(postLink[1:], '/')
|
||||
if secondSlash != -1 {
|
||||
postLink = postLink[:secondSlash+2]
|
||||
}
|
||||
|
||||
if pageData, ok := app.PagesCache.Get(cacheKey); ok {
|
||||
sendPostPage(w, pageData)
|
||||
return
|
||||
// Страничка рендерится только если ее нет в кэше
|
||||
page, 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
|
||||
}
|
||||
ETag := models.GenerateETag(pageData)
|
||||
page = &models.Page{Data: pageData, ETag: ETag}
|
||||
app.PagesCache.Set(postLink, page)
|
||||
}
|
||||
|
||||
post := app.PostsMap[models_pages.PostLink(link)]
|
||||
|
||||
var (
|
||||
pageData []byte
|
||||
err error
|
||||
)
|
||||
|
||||
pageData, err = post.RenderPostPage(app.Templates, app.Version, ajax)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
app.PagesCache.Set(cacheKey, pageData)
|
||||
|
||||
sendPostPage(w, pageData)
|
||||
sendPostPage(r, w, page)
|
||||
})
|
||||
}
|
||||
|
||||
// Отправляет страницу
|
||||
func sendPostPage(w http.ResponseWriter, data []byte) {
|
||||
func sendPostPage(r *http.Request, w http.ResponseWriter, page *models.Page) {
|
||||
w.Header().Set("Cache-Control", "public, max-age=31536000")
|
||||
w.Header().Set("ETag", page.ETag)
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write(data)
|
||||
|
||||
if match := r.Header.Get("If-None-Match"); match == page.ETag {
|
||||
w.WriteHeader(http.StatusNotModified)
|
||||
return
|
||||
}
|
||||
|
||||
w.Write(page.Data)
|
||||
}
|
||||
|
|
|
@ -4,50 +4,38 @@ import (
|
|||
"main/mvc/models"
|
||||
"main/mvc/models/models_pages"
|
||||
"net/http"
|
||||
"path"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// Обработчик странички со списком постов
|
||||
// Обработчик главной страницы
|
||||
func PostsPageHandler(app *models.App) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
link := r.URL.Path
|
||||
cacheKey := link
|
||||
|
||||
ajax := r.URL.Query().Get("ajax") == "true"
|
||||
if ajax {
|
||||
cacheKey += "?ajax=true"
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Страничка рендерится только если ее нет в кэше
|
||||
page, 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
|
||||
}
|
||||
ETag := models.GenerateETag(pageData)
|
||||
page = &models.Page{Data: pageData, ETag: ETag}
|
||||
app.PagesCache.Set(models_pages.PostsPageTmplName, page)
|
||||
}
|
||||
|
||||
if pageData, ok := app.PagesCache.Get(cacheKey); ok {
|
||||
sendPostsPage(w, pageData)
|
||||
return
|
||||
}
|
||||
|
||||
postsList := app.PostsMap.PostsList()
|
||||
pageNumber, _ := strconv.Atoi(path.Base(link))
|
||||
page := models_pages.CreatePostsPage(postsList, pageNumber, app.Cfg.PostsMaxCountOnPage)
|
||||
|
||||
var (
|
||||
pageData []byte
|
||||
err error
|
||||
)
|
||||
|
||||
pageData, err = page.RenderPostsPage(app.Templates, app.Version, ajax)
|
||||
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
app.PagesCache.Set(cacheKey, pageData)
|
||||
sendPostsPage(w, pageData)
|
||||
}
|
||||
sendPostsPage(r, w, page)
|
||||
})
|
||||
}
|
||||
|
||||
// Отправляет страницу
|
||||
func sendPostsPage(w http.ResponseWriter, data []byte) {
|
||||
func sendPostsPage(r *http.Request, w http.ResponseWriter, page *models.Page) {
|
||||
w.Header().Set("Cache-Control", "public, max-age=31536000")
|
||||
w.Header().Set("ETag", page.ETag)
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write(data)
|
||||
|
||||
if match := r.Header.Get("If-None-Match"); match == page.ETag {
|
||||
w.WriteHeader(http.StatusNotModified)
|
||||
return
|
||||
}
|
||||
|
||||
w.Write(page.Data)
|
||||
}
|
||||
|
|
|
@ -1,21 +0,0 @@
|
|||
package controllers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"main/mvc/models"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// Обработчик главной страницы
|
||||
func LastFMHandler(app *models.App) http.HandlerFunc {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
sendLastFM(w, app.LastfmLastTrack)
|
||||
})
|
||||
}
|
||||
|
||||
// Отправляет страницу
|
||||
func sendLastFM(w http.ResponseWriter, data string) {
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
fmt.Fprint(w, data)
|
||||
}
|
|
@ -35,12 +35,6 @@ func loggingMiddleware(next http.Handler) http.Handler {
|
|||
|
||||
next.ServeHTTP(w, r)
|
||||
|
||||
fullURL := r.URL.String()
|
||||
|
||||
log.Printf("%s %s | client: %s | duration: %v",
|
||||
r.Method,
|
||||
fullURL,
|
||||
r.RemoteAddr,
|
||||
time.Since(start))
|
||||
log.Printf("%s %s %v", r.Method, r.URL.Path, time.Since(start))
|
||||
})
|
||||
}
|
||||
|
|
|
@ -7,12 +7,11 @@ import (
|
|||
)
|
||||
|
||||
type App struct {
|
||||
Cfg *Config // Сонфиг
|
||||
PostsMap models_pages.PostsMap // Посты
|
||||
Templates *template.Template // Шаблоны страниц
|
||||
PagesCache *Cache // Кэш (отрендеренные странички)
|
||||
LastfmLastTrack string // Последний трек, полученный с ластфм
|
||||
Version int64 // Время запуска
|
||||
Cfg *Config // Сонфиг
|
||||
Posts models_pages.Posts // Посты
|
||||
Templates *template.Template // Шаблоны страниц
|
||||
PagesCache *Cache // Кэш (отрендеренные странички)
|
||||
Version int64 // Время запуска
|
||||
}
|
||||
|
||||
// Инициализирует приложение
|
||||
|
@ -28,7 +27,7 @@ func InitApp() (*App, error) {
|
|||
return nil, err
|
||||
}
|
||||
// Загрузка постов
|
||||
if app.PostsMap, err = models_pages.LoadPosts(app.Cfg.PostsDir); err != nil {
|
||||
if app.Posts, err = models_pages.LoadPosts(app.Cfg.PostsDir); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Загрузка шаблонов
|
||||
|
@ -37,7 +36,5 @@ func InitApp() (*App, error) {
|
|||
}
|
||||
// Инициализация кэша
|
||||
app.PagesCache = initCache()
|
||||
// Строка по умолчанию для последнего прослушанного трека
|
||||
app.LastfmLastTrack = "None"
|
||||
return app, nil
|
||||
}
|
||||
|
|
|
@ -1,25 +1,40 @@
|
|||
package models
|
||||
|
||||
import "sync"
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type Page struct {
|
||||
Data []byte
|
||||
ETag string
|
||||
}
|
||||
|
||||
type Cache struct {
|
||||
Data map[string][]byte
|
||||
Data map[string]*Page
|
||||
Mu sync.RWMutex
|
||||
}
|
||||
|
||||
func initCache() *Cache {
|
||||
return &Cache{Data: make(map[string][]byte)}
|
||||
return &Cache{Data: make(map[string]*Page)}
|
||||
}
|
||||
|
||||
func (c *Cache) Get(key string) ([]byte, bool) {
|
||||
func (c *Cache) Get(key string) (*Page, bool) {
|
||||
c.Mu.RLock()
|
||||
pageData, ok := c.Data[key]
|
||||
page, ok := c.Data[key]
|
||||
c.Mu.RUnlock()
|
||||
return pageData, ok
|
||||
return page, ok
|
||||
}
|
||||
|
||||
func (c *Cache) Set(key string, data []byte) {
|
||||
func (c *Cache) Set(key string, page *Page) {
|
||||
c.Mu.Lock()
|
||||
c.Data[key] = data
|
||||
c.Data[key] = page
|
||||
c.Mu.Unlock()
|
||||
}
|
||||
|
||||
func GenerateETag(data []byte) string {
|
||||
hash := md5.Sum(data)
|
||||
return fmt.Sprintf(`"%s"`, hex.EncodeToString(hash[:]))
|
||||
}
|
||||
|
|
|
@ -3,7 +3,6 @@ package models
|
|||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -11,49 +10,27 @@ const (
|
|||
)
|
||||
|
||||
type Config struct {
|
||||
pathsConfig
|
||||
serverConfig
|
||||
lastFMConfig
|
||||
cacheConfig
|
||||
PostsDir string
|
||||
AssetsDir string
|
||||
TemplatesDir string
|
||||
TemplatesExt string
|
||||
LocalIP string
|
||||
LocalPort string
|
||||
ServerIP string
|
||||
ServerPort string
|
||||
ServerDomain string
|
||||
Port string
|
||||
}
|
||||
|
||||
type pathsConfig struct {
|
||||
PostsDir string `json:"posts_dir"`
|
||||
AssetsDir string `json:"assets_dir"`
|
||||
TemplatesDir string `json:"templates_dir"`
|
||||
TemplatesExt string `json:"templates_ext"`
|
||||
LogFilePath string `json:"log_file_path"`
|
||||
LogSystemRestartInterval time.Duration `json:"log_system_restart_interval"`
|
||||
PostsMaxCountOnPage int `json:"max_posts_per_page"`
|
||||
}
|
||||
|
||||
type serverConfig struct {
|
||||
LocalIP string `json:"local_ip"`
|
||||
LocalPort string `json:"local_port"`
|
||||
ServerIP string `json:"server_ip"`
|
||||
ServerPort string `json:"server_port"`
|
||||
}
|
||||
|
||||
type lastFMConfig struct {
|
||||
LastFMUsername string `json:"lastfm_username"`
|
||||
LastFMToken string `json:"lastfm_token"`
|
||||
LastFMUpdateInterval time.Duration `json:"lastfm_update_interval"`
|
||||
}
|
||||
|
||||
type cacheConfig struct {
|
||||
CacheLogInterval time.Duration `json:"cache_log_interval"`
|
||||
}
|
||||
|
||||
func loadConfig(path string) (*Config, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
func loadConfig(configPath string) (*Config, error) {
|
||||
cfg := &Config{}
|
||||
configFile, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var cfg Config
|
||||
if err := json.Unmarshal(data, &cfg); err != nil {
|
||||
err = json.Unmarshal(configFile, cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &cfg, nil
|
||||
return cfg, nil
|
||||
}
|
||||
|
|
|
@ -1,63 +0,0 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type lastFMTrack struct {
|
||||
Name string `json:"name"`
|
||||
Artist struct {
|
||||
Name string `json:"#text"`
|
||||
} `json:"artist"`
|
||||
Album struct {
|
||||
Name string `json:"#text"`
|
||||
} `json:"album"`
|
||||
Date struct {
|
||||
Unix string `json:"#text"`
|
||||
} `json:"date"`
|
||||
}
|
||||
|
||||
type lastFMResponse struct {
|
||||
RecentTracks struct {
|
||||
Tracks []lastFMTrack `json:"track"`
|
||||
} `json:"recenttracks"`
|
||||
}
|
||||
|
||||
func lastFMGetRecentTracks(username, apiKey string) (*lastFMResponse, error) {
|
||||
url := fmt.Sprintf("https://ws.audioscrobbler.com/2.0/?method=user.getrecenttracks&user=%s&api_key=%s&format=json", username, apiKey)
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("lastfm get resp err: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("lastfm read resp body err: %v", err)
|
||||
}
|
||||
var lastFMResponse lastFMResponse
|
||||
if err := json.Unmarshal(body, &lastFMResponse); err != nil {
|
||||
return nil, fmt.Errorf("lastfm unmarshal err: %v", err)
|
||||
}
|
||||
|
||||
return &lastFMResponse, nil
|
||||
}
|
||||
|
||||
func LastFMGetLastTrack(username, apiKey string) (string, error) {
|
||||
resp, err := lastFMGetRecentTracks(username, apiKey)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var data string
|
||||
if len(resp.RecentTracks.Tracks) > 0 {
|
||||
track := resp.RecentTracks.Tracks[0]
|
||||
data = fmt.Sprintf("%s - %s", track.Name, track.Artist.Name)
|
||||
data = strings.ReplaceAll(data, "🅴", "[E]")
|
||||
} else {
|
||||
return "", fmt.Errorf("len(resp.RecentTracks.Tracks) <= 0")
|
||||
}
|
||||
return data, nil
|
||||
}
|
|
@ -8,11 +8,10 @@ import (
|
|||
|
||||
const (
|
||||
// Имя соответствующего шаблона
|
||||
MainPageTmplName = "main.gohtml"
|
||||
MainPageTmplNameAjax = "main_ajax.gohtml"
|
||||
MainPageTmplName = "main.gohtml"
|
||||
)
|
||||
|
||||
func RenderMainPage(templates *template.Template, version int64, ajax bool) ([]byte, error) {
|
||||
func RenderMainPage(templates *template.Template, version int64) ([]byte, error) {
|
||||
var pageData bytes.Buffer
|
||||
|
||||
context := map[string]any{
|
||||
|
@ -20,12 +19,7 @@ func RenderMainPage(templates *template.Template, version int64, ajax bool) ([]b
|
|||
"renderingTimestamp": time.Now().Unix(),
|
||||
}
|
||||
|
||||
templateName := MainPageTmplName
|
||||
if ajax {
|
||||
templateName = MainPageTmplNameAjax
|
||||
}
|
||||
|
||||
if err := templates.ExecuteTemplate(&pageData, templateName, context); err != nil {
|
||||
if err := templates.ExecuteTemplate(&pageData, MainPageTmplName, context); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
|
|
@ -6,13 +6,13 @@ import (
|
|||
"time"
|
||||
)
|
||||
|
||||
type PostLink string
|
||||
|
||||
const (
|
||||
// Имя соответствующего шаблона
|
||||
PostPageTmplName = "post.gohtml"
|
||||
PostPageTmplNameAjax = "post_ajax.gohtml"
|
||||
PostPageTmplName = "post.gohtml"
|
||||
)
|
||||
|
||||
type PostLink string
|
||||
type Post struct {
|
||||
Link PostLink
|
||||
Preview template.HTML
|
||||
|
@ -20,29 +20,23 @@ type Post struct {
|
|||
Timestamp int64
|
||||
}
|
||||
|
||||
func (p *Post) RenderPostPage(templates *template.Template, version int64, ajax bool) ([]byte, error) {
|
||||
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": p.Data,
|
||||
"modTimestamp": p.Timestamp,
|
||||
"data": data,
|
||||
}
|
||||
|
||||
templateName := PostPageTmplName
|
||||
if ajax {
|
||||
templateName = PostPageTmplNameAjax
|
||||
}
|
||||
|
||||
if err := templates.ExecuteTemplate(&pageData, templateName, context); err != nil {
|
||||
if err := templates.ExecuteTemplate(&pageData, PostPageTmplName, context); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return pageData.Bytes(), nil
|
||||
}
|
||||
|
||||
func createPost(link string, data []byte, timestamp int64) *Post {
|
||||
func newPost(link string, data []byte, timestamp int64) *Post {
|
||||
previewBuf := make([]byte, 0, 503)
|
||||
|
||||
if len(data) > 500 {
|
||||
|
|
|
@ -4,33 +4,24 @@ import (
|
|||
"bytes"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"log"
|
||||
"main/tools"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
// Имя соответствующего шаблона
|
||||
PostsPageTmplName = "posts.gohtml"
|
||||
PostsPageTmplNameAjax = "posts_ajax.gohtml"
|
||||
PostsPageTmplName = "posts.gohtml"
|
||||
)
|
||||
|
||||
type PostsMap map[PostLink]*Post
|
||||
type PostsList []*Post
|
||||
type PostsPage struct {
|
||||
PostsListOnPage PostsList // список постов на странице
|
||||
PageNumber int // номер страницы
|
||||
PagesCount int // общее количество страниц
|
||||
}
|
||||
type Posts map[PostLink]*Post
|
||||
|
||||
func LoadPosts(dir string) (PostsMap, error) {
|
||||
func LoadPosts(dir string) (Posts, error) {
|
||||
|
||||
posts := PostsMap{}
|
||||
posts := Posts{}
|
||||
|
||||
err := filepath.Walk(dir, func(path string, f os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
|
@ -44,23 +35,9 @@ func LoadPosts(dir string) (PostsMap, error) {
|
|||
}
|
||||
|
||||
html := tools.MdToHTML(md)
|
||||
|
||||
name := filepath.Base(path)
|
||||
lowLineIndex := strings.Index(name, "_")
|
||||
|
||||
if lowLineIndex == -1 {
|
||||
// Обработка случая, если "_" нет в имени
|
||||
log.Fatal(`post name parse error`)
|
||||
}
|
||||
|
||||
timestampStr := name[:lowLineIndex]
|
||||
timestamp, err := strconv.ParseInt(timestampStr, 10, 64)
|
||||
if err != nil {
|
||||
// Ошибка парсинга timestamp
|
||||
log.Fatal(`post name parse error`)
|
||||
}
|
||||
link := fmt.Sprintf("/%s", strings.TrimSuffix(name[lowLineIndex+1:], ".md"))
|
||||
posts[PostLink(link)] = createPost(link, html, timestamp)
|
||||
link := fmt.Sprintf("/%s/", strings.TrimSuffix(filepath.Base(path), ".md"))
|
||||
timestamp := f.ModTime().Unix()
|
||||
posts[PostLink(link)] = newPost(link, html, timestamp)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
@ -72,65 +49,26 @@ func LoadPosts(dir string) (PostsMap, error) {
|
|||
return posts, nil
|
||||
}
|
||||
|
||||
// Получение из мапы постов списка постов, отсортированного по ModTimestamp (новые сначала)
|
||||
func (p *PostsMap) PostsList() PostsList {
|
||||
postsSlice := make(PostsList, 0, len(*p))
|
||||
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
|
||||
})
|
||||
|
||||
return postsSlice
|
||||
}
|
||||
|
||||
func CreatePostsPage(postsList PostsList, pageNumber, postsMaxCountOnPage int) *PostsPage {
|
||||
// Общее количество страниц
|
||||
pagesCount := (len(postsList) + postsMaxCountOnPage - 1) / postsMaxCountOnPage
|
||||
|
||||
startIndex := pageNumber * postsMaxCountOnPage
|
||||
endIndex := startIndex + postsMaxCountOnPage
|
||||
endIndex = min(endIndex, len(postsList))
|
||||
postsSublistForPageNumber := postsList[startIndex:endIndex]
|
||||
|
||||
return &PostsPage{
|
||||
PostsListOnPage: postsSublistForPageNumber,
|
||||
PageNumber: pageNumber,
|
||||
PagesCount: pagesCount,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PostsPage) RenderPostsPage(templates *template.Template, version int64, ajax bool) ([]byte, error) {
|
||||
var pageData bytes.Buffer
|
||||
|
||||
var prevPageNumber, nextPageNumber int
|
||||
|
||||
if p.PageNumber == 0 {
|
||||
prevPageNumber = p.PagesCount - 1
|
||||
} else {
|
||||
prevPageNumber = p.PageNumber - 1
|
||||
}
|
||||
|
||||
nextPageNumber = (p.PageNumber + 1) % p.PagesCount
|
||||
|
||||
context := map[string]any{
|
||||
"prevPageNumber": prevPageNumber,
|
||||
"pageNumberInc": p.PageNumber + 1,
|
||||
"pagesCount": p.PagesCount,
|
||||
"nextPageNumber": nextPageNumber,
|
||||
"version": version,
|
||||
"renderingTimestamp": time.Now().Unix(),
|
||||
"posts": p.PostsListOnPage,
|
||||
"posts": postsSlice,
|
||||
}
|
||||
|
||||
templateName := PostsPageTmplName
|
||||
if ajax {
|
||||
templateName = PostsPageTmplNameAjax
|
||||
}
|
||||
|
||||
if err := templates.ExecuteTemplate(&pageData, templateName, context); err != nil {
|
||||
if err := templates.ExecuteTemplate(&pageData, PostsPageTmplName, context); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
|
|
@ -1,28 +1,23 @@
|
|||
{{ define "footer" }}
|
||||
<footer>
|
||||
<div class="rotating-pic-container">
|
||||
<img src="/assets/pic/heh.png?v={{ .version }}" class="rotating-pic">
|
||||
<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>
|
||||
<p>
|
||||
> unix timestamp of page rendering: <code>{{ .renderingTimestamp }}</code><br/>
|
||||
> <code>curl -X LIMINAL https://hikan.ru</code> - what do you know about liminal spaces<br/>
|
||||
> <code>curl -X COUNT https://hikan.ru</code> - 24-hour server request count<br/>
|
||||
</p>
|
||||
<p>
|
||||
<details>
|
||||
<summary class="pulse-text">click here if you want more system details</summary>
|
||||
<p>
|
||||
> to check the latest changes to the site you can use it (someday i hope to add a cool log page):
|
||||
<pre>curl -s "https://git.hikan.ru/api/v1/repos/serr/hikan.ru/commits?limit=10" | jq -r '.[] | .commit.author.date + " | " + (.commit.message | rtrimstr("\n"))'</pre>
|
||||
or look in the repository <a href="https://git.hikan.ru/serr/hikan.ru" target="_blank">git.hikan.ru/serr/hikan.ru</a>
|
||||
</p>
|
||||
</details>
|
||||
</p>
|
||||
|
||||
<ul>
|
||||
<li>unix timestamp of page rendering: <code>{{ .renderingTimestamp }}</code></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>
|
||||
<li><code>curl -X COUNT https://hikan.ru</code> - 24-hour server request count</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<p>
|
||||
|
@ -32,6 +27,5 @@
|
|||
<code>2024 - now</code>
|
||||
</p>
|
||||
</div>
|
||||
<marquee>programming / music / video games / cinema</marquee>
|
||||
</footer>
|
||||
{{ end }}
|
||||
|
|
|
@ -1,23 +1,9 @@
|
|||
{{ define "head" }}
|
||||
<head>
|
||||
<title>хикан.ру</title>
|
||||
<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.ico?v={{ .version }}" type="image/x-icon">
|
||||
<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">
|
||||
<script src="/assets/scripts/htmx.js?v={{ .version }}"></script>
|
||||
<script>
|
||||
const titles = ["freedom","love","music","flowers","breathing"]; let i = 0;
|
||||
function update() { document.title = titles[i++%5]; } update();
|
||||
setInterval(update, 1000);
|
||||
</script>
|
||||
<script>
|
||||
document.addEventListener('htmx:afterSwap', function(evt) {
|
||||
const triggeringElement = evt.detail.requestConfig.elt;
|
||||
if (triggeringElement.hasAttribute('data-scroll-top')) {
|
||||
window.scrollTo(0,0);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
{{ end }}
|
|
@ -1,17 +1,7 @@
|
|||
{{ define "header" }}
|
||||
<header>
|
||||
<div class="rotating-pic-container">
|
||||
<img class="rotating-pic" src="/assets/pic/miffy.png?v={{ .version }}"
|
||||
style="width: 100%; height: 100%;">
|
||||
</div>
|
||||
<div class="pulse-container">
|
||||
<p class="pulse-text">
|
||||
<code>🎵 last played track: </code><code id="lastfm"
|
||||
hx-get="/api/lastfm"
|
||||
hx-trigger="load, every 30s"
|
||||
hx-target="#lastfm"
|
||||
hx-swap="innerHTML">waiting for a response from server... (JavaScript is required)</code><code> 🎵</code>
|
||||
</p>
|
||||
<div>
|
||||
<img src="/assets/pic/header.webp?v={{ .version }}" width="100%" height="100%">
|
||||
</div>
|
||||
<div>
|
||||
<h1>
|
||||
|
@ -22,22 +12,10 @@
|
|||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p>
|
||||
see <a href="/"
|
||||
hx-get="/?ajax=true"
|
||||
hx-target="main"
|
||||
hx-swap="outerHTML"
|
||||
hx-push-url="/"
|
||||
data-scroll-top="true">
|
||||
main page</a> or go to
|
||||
<a href="/posts/0"
|
||||
hx-get="/posts/0?ajax=true"
|
||||
hx-target="main"
|
||||
hx-swap="outerHTML"
|
||||
hx-push-url="/posts/0"
|
||||
data-scroll-top="true">
|
||||
posts section</a>, and also you can subscribe to my <a href="https://t.me/lolistack" target="_blank">telegram channel</a> with pictures!
|
||||
</p>
|
||||
<ul>
|
||||
<li><a href="/">main page</a></li>
|
||||
<li><a href="/posts/">posts section</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</header>
|
||||
{{ end }}
|
|
@ -0,0 +1,71 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
{{ template "head" . }}
|
||||
<body>
|
||||
{{ template "header" . }}
|
||||
<main>
|
||||
<div>
|
||||
<h1>
|
||||
$whoami
|
||||
</h1>
|
||||
<p>
|
||||
my name is serr (you can easily guess my real name if you speak Russian :d), and i didn't come up with that nickname, i just started being called it
|
||||
</p>
|
||||
<p>
|
||||
i was born in 2003, i'm currently a cybersecurity major at university
|
||||
</p>
|
||||
<p>
|
||||
<code>pronouns: he/him</code>
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h1>
|
||||
what do i do?
|
||||
</h1>
|
||||
<p>
|
||||
programming is my everything - my job, my hobby, my lifestyle
|
||||
</p>
|
||||
<p>
|
||||
i love growing in all areas of programming - i am literally interested in everything: cybersecurity (chaotically breaking things, analyzing code, writing automated analyzers, and moving bytes back and forth), concurrency/multithreading, web development, low-level programming, cryptography and a lot more!
|
||||
</p>
|
||||
<p>
|
||||
i like the idea of <a href="https://en.wikipedia.org/wiki/Symbolic_execution" target="_blank">symbolic</a>/concolic execution and virtual code execution in general
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h1>
|
||||
things i love
|
||||
</h1>
|
||||
<ul>
|
||||
<li><strong>coffee</strong>. i REALLY love coffee. almost any. and a lot of</li>
|
||||
<li><strong>movies and TV series</strong> (especially TV series). i watch something almost every day</li>
|
||||
<li><strong>true crime</strong>. i'm obsessed with serial killer cases, mysterious disappearances, unsolved murders - all that dark stuff</li>
|
||||
<li><strong>russian underground rap</strong> like Slava KPSS, Zamay, MB Packet, Ovsyankin etc.</li>
|
||||
<li><strong>simple and extensible code</strong>. i think if your code is overly complex, it means you are doing something wrong. most things are simpler than they seem</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h1>
|
||||
projects
|
||||
</h1>
|
||||
<ul>
|
||||
<li><a href="https://git.hikan.ru/serr" target="_blank">git.hikan.ru/serr</a> - check my repos</li>
|
||||
<li><del>telegram bot with schedule for SPBPU - <a href="https://t.me/polysched_bot" target="_blank">polysched_bot</a></del> (transferred to a more proactive owner)</li>
|
||||
<li><del>telegram bot with schedule for SPMI - <a href="https://t.me/gornischedule_bot" target="_blank">gornischedule_bot</a></del> (closed)</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h1>
|
||||
nice links
|
||||
</h1>
|
||||
<ul>
|
||||
<li><a href="https://mo.rijndael.cc/" target="_blank">Mo</a>, thx for design idea!</li>
|
||||
<li>huge collection of Xakep issues - <a href="https://図書館.きく.コム/" target="_blank">図書館.きく.コム</a></li>
|
||||
<li>i love this website highlighting the Small Web - <a href="https://smallweb.cc/" target="_blank">smallweb</a></li>
|
||||
<li>very atmospheric forum about black metal - <a href="https://www.lycanthropia.net/" target="_blank">lycanthropia</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</main>
|
||||
{{ template "footer" . }}
|
||||
</body>
|
||||
</html>
|
|
@ -1,9 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
{{ template "head" . }}
|
||||
<body>
|
||||
{{ template "header" . }}
|
||||
{{ template "main_ajax" . }}
|
||||
{{ template "footer" . }}
|
||||
</body>
|
||||
</html>
|
|
@ -1,74 +0,0 @@
|
|||
{{ template "main_ajax" . }}
|
||||
|
||||
{{ define "main_ajax" }}
|
||||
<main>
|
||||
<div>
|
||||
<p>
|
||||
<code>welcome to my little site</code>
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h1>
|
||||
$whoami
|
||||
</h1>
|
||||
<p>
|
||||
my name is serr (you can easily guess my real name if you speak Russian :d), and i didn't come up with that nickname, i just started being called it
|
||||
</p>
|
||||
<p>
|
||||
i was born in 2003, i'm currently a cybersecurity major at university
|
||||
</p>
|
||||
<p>
|
||||
<code>pronouns: he/him</code>
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h1>
|
||||
what do i do?
|
||||
</h1>
|
||||
<p>
|
||||
programming is my lifestyle
|
||||
</p>
|
||||
<p>
|
||||
i love growing in all areas of programming - i am literally interested in everything: web development, low-level programming, formal grammars and a lot more!
|
||||
</p>
|
||||
<p>
|
||||
i like the idea of <a href="https://en.wikipedia.org/wiki/Symbolic_execution" target="_blank">symbolic</a>/concolic execution and virtual code execution in general
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h1>
|
||||
things i love
|
||||
</h1>
|
||||
<p>
|
||||
> <strong>coffee</strong>. i REALLY love coffee. almost any. and a lot of<br/>
|
||||
> <strong>movies and TV series</strong> (especially TV series). i watch something almost every day<br/>
|
||||
> <strong>true crime</strong>. i'm obsessed with serial killer cases, mysterious disappearances, unsolved murders - all that dark stuff<br/>
|
||||
> <strong>russian underground rap</strong> like Slava KPSS, Zamay, MB Packet, Ovsyankin etc.<br/>
|
||||
> <strong>simple and extensible code</strong>. i think if your code is overly complex, it means you are doing something wrong. most things are simpler than they seem
|
||||
</p>
|
||||
<p>
|
||||
<details>
|
||||
<summary class="pulse-text">click here if you want to see the sites i like</summary>
|
||||
<p>
|
||||
> huge collection of Xakep issues - <a href="https://図書館.きく.コム/" target="_blank">図書館.きく.コム</a><br/>
|
||||
> i like to surf here - <a href="https://neocities.org/browse" target="_blank">neocities.org/browse</a><br/>
|
||||
> very atmospheric forum about black metal - <a href="https://www.lycanthropia.net/" target="_blank">lycanthropia</a><br/>
|
||||
> animated gif search from internet archive - <a href="https://gifcities.org/" target="_blank">gifcities.org</a><br/>
|
||||
> very cool design - <a href="https://combatbaby.neocities.org/" target="_blank">combatbaby.neocities.org</a><br/>
|
||||
</p>
|
||||
</details>
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h1>
|
||||
projects
|
||||
</h1>
|
||||
<p>
|
||||
> <a href="https://git.hikan.ru/serr/candycache" target="_blank">git.hikan.ru/serr/candycache</a> - simple and efficient Go cache<br/>
|
||||
> <a href="https://git.hikan.ru/serr/eye-hot-reloader" target="_blank">git.hikan.ru/serr/eye-hot-reloader</a> - lightweight directories monitor with automatic rebuild and restart functionality for any project type<br/>
|
||||
> <del>telegram bot with schedule for SPBPU - <a href="https://t.me/polysched_bot" target="_blank">polysched_bot</a></del> (transferred to a more proactive owner)<br/>
|
||||
> <del>telegram bot with schedule for SPMI - <a href="https://t.me/gornischedule_bot" target="_blank">gornischedule_bot</a></del> (closed)<br/>
|
||||
</p>
|
||||
</div>
|
||||
</main>
|
||||
{{ end }}
|
|
@ -3,7 +3,11 @@
|
|||
{{ template "head" . }}
|
||||
<body>
|
||||
{{ template "header" . }}
|
||||
{{ template "post_ajax" . }}
|
||||
<main>
|
||||
<div>
|
||||
{{ .data }}
|
||||
</div>
|
||||
</main>
|
||||
{{ template "footer" . }}
|
||||
</body>
|
||||
</html>
|
|
@ -1,12 +0,0 @@
|
|||
{{ template "post_ajax" . }}
|
||||
|
||||
{{ define "post_ajax" }}
|
||||
<main>
|
||||
<div>
|
||||
{{ .data }}
|
||||
<p>
|
||||
<code>create time: {{ .modTimestamp }}</code>
|
||||
</p>
|
||||
</div>
|
||||
</main>
|
||||
{{ end }}
|
|
@ -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>
|
|
@ -1,9 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
{{ template "head" . }}
|
||||
<body>
|
||||
{{ template "header" . }}
|
||||
{{ template "posts_ajax" . }}
|
||||
{{ template "footer" . }}
|
||||
</body>
|
||||
</html>
|
|
@ -1,44 +0,0 @@
|
|||
{{ template "posts_ajax" . }}
|
||||
|
||||
{{ define "posts_ajax" }}
|
||||
<main>
|
||||
<div>
|
||||
<p>
|
||||
<code>posts sorted by create time</code>
|
||||
</p>
|
||||
</div>
|
||||
{{ range $ind, $post := .posts }}
|
||||
<div>
|
||||
<p>
|
||||
{{ $post.Preview }}
|
||||
</p>
|
||||
<p>
|
||||
<a href="{{ $post.Link }}"
|
||||
hx-get="{{ $post.Link }}?ajax=true"
|
||||
hx-target="main"
|
||||
hx-swap="outerHTML"
|
||||
hx-push-url="{{ $post.Link }}"
|
||||
data-scroll-top="true">read more</a>
|
||||
</p>
|
||||
<p>
|
||||
<code>create time: {{ $post.Timestamp }}</code>
|
||||
</p>
|
||||
</div>
|
||||
{{ end }}
|
||||
<div class="pagi">
|
||||
<a href="/posts/{{ .prevPageNumber }}"
|
||||
hx-get="/posts/{{ .prevPageNumber }}?ajax=true"
|
||||
hx-target="main"
|
||||
hx-swap="outerHTML"
|
||||
hx-push-url="/posts/{{ .prevPageNumber }}"
|
||||
data-scroll-top="true"><-</a>
|
||||
{{ .pageNumberInc }}/{{ .pagesCount }}
|
||||
<a href="/posts/{{ .nextPageNumber }}"
|
||||
hx-get="/posts/{{ .nextPageNumber }}?ajax=true"
|
||||
hx-target="main"
|
||||
hx-swap="outerHTML"
|
||||
hx-push-url="/posts/{{ .nextPageNumber }}"
|
||||
data-scroll-top="true">-></a>
|
||||
</div>
|
||||
</main>
|
||||
{{ end }}
|
|
@ -1,72 +0,0 @@
|
|||
# Как же все таки изменить байты строки в Go?
|
||||
Просто захотелось чуть чуть поиграться с пакетом unsafe в Go.
|
||||
|
||||
Строки (тип string) в Go являются *immutable*, то есть изменять их нельзя. Ну вообще конечно можно, но не напрямую.
|
||||
|
||||
Строка в Go под капотом является структурой вида: **указатель на данные, длина данных.** И первое, что приходит в голову чтобы изменить строку - добраться до поля с указателем, прибавить к нему индекс байта который надо поменять, разыменовать полученный адрес и что то ему присвоить.
|
||||
|
||||
Но в реальности все не так просто и при попытке что то положить по вычисленному адресу программа упадет с *segmentation fault (SIGSEGV)*. Чтобы этого избежать, предварительно надо выдать права на запись в страничку памяти где находится целевая строка. Сделать это можно через системные вызовы.
|
||||
|
||||
```
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
func main() {
|
||||
s := "hello"
|
||||
fmt.Println("Original:", s)
|
||||
|
||||
// Строки в Go - структуры с двумя полями: указатель на данные и длина
|
||||
// тут строка интерпретируется как структура
|
||||
strHeader := (*struct {
|
||||
data unsafe.Pointer
|
||||
len int
|
||||
})(unsafe.Pointer(&s))
|
||||
|
||||
// Вычисляем начало страницы памяти
|
||||
pageSize := syscall.Getpagesize()
|
||||
pageStart := uintptr(strHeader.data) - (uintptr(strHeader.data) % uintptr(pageSize))
|
||||
fmt.Printf(
|
||||
"\nАдрес строки = %d\nразмер страницы памяти = %d\nадрес строки относительно начала страницы = %d\nадрес страницы = %d\n\n",
|
||||
uintptr(strHeader.data),
|
||||
uintptr(pageSize),
|
||||
(uintptr(strHeader.data) % uintptr(pageSize)),
|
||||
pageStart)
|
||||
|
||||
// Имея адрес страницы и ее размер даю права на запись на данной странице памяти
|
||||
ret, _, err := syscall.Syscall(
|
||||
syscall.SYS_MPROTECT,
|
||||
pageStart,
|
||||
uintptr(pageSize),
|
||||
uintptr(syscall.PROT_READ|syscall.PROT_WRITE),
|
||||
)
|
||||
if ret != 0 {
|
||||
panic("mprotect failed: " + err.Error())
|
||||
}
|
||||
|
||||
// Теперь можно менять строку
|
||||
// поменяю, например, пятый байт
|
||||
fifthBytePtr := (*byte)(unsafe.Pointer(uintptr(strHeader.data) + 4))
|
||||
*fifthBytePtr = 0
|
||||
|
||||
// Восстанавливаю дефолтные права
|
||||
ret, _, err = syscall.Syscall(
|
||||
syscall.SYS_MPROTECT,
|
||||
pageStart,
|
||||
uintptr(pageSize),
|
||||
uintptr(syscall.PROT_READ),
|
||||
)
|
||||
if ret != 0 {
|
||||
panic("mprotect restore failed: " + err.Error())
|
||||
}
|
||||
|
||||
fmt.Println("Modified:", s) // Должно быть "hell"
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
Тестил на **go version go1.22.2 linux/amd64**
|
|
@ -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**
|
|
@ -1,81 +1,29 @@
|
|||
package tools
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
func SetupLogging(logFilePath string) (*os.File, error) {
|
||||
func GetJournalctlLogsCount(serviceName, grepPattern string, hoursAgo int) ([]byte, error) {
|
||||
|
||||
// Проверяем размер файла
|
||||
var append = true
|
||||
if fi, err := os.Stat(logFilePath); err == nil {
|
||||
if fi.Size() > 10*1024*1024 { // 10 МБ
|
||||
append = false
|
||||
}
|
||||
if runtime.GOOS != "linux" {
|
||||
return nil, fmt.Errorf("not a linux")
|
||||
}
|
||||
|
||||
// Логи дописываются в файл если он меньше 10 МБ, иначе
|
||||
// файл перезаписывается
|
||||
var flags int
|
||||
if append {
|
||||
flags = os.O_CREATE | os.O_WRONLY | os.O_APPEND
|
||||
} else {
|
||||
flags = os.O_CREATE | os.O_WRONLY | os.O_TRUNC
|
||||
}
|
||||
cmd := exec.Command("sh", "-c",
|
||||
fmt.Sprintf("journalctl -u %s --since '%d hours ago' | grep -c '%s'",
|
||||
serviceName, hoursAgo, grepPattern))
|
||||
|
||||
file, err := os.OpenFile(logFilePath, flags, 0666)
|
||||
var output bytes.Buffer
|
||||
cmd.Stdout = &output
|
||||
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.SetOutput(file)
|
||||
return file, nil
|
||||
}
|
||||
|
||||
func GetLogFileEntriesCount(logFilePath, grepPattern string, hoursAgo int) (int, error) {
|
||||
// Если файл не существует - ошибка
|
||||
if _, err := os.Stat(logFilePath); os.IsNotExist(err) {
|
||||
return 0, fmt.Errorf("log file not found: %s", logFilePath)
|
||||
}
|
||||
|
||||
// Открывает файл для чтения
|
||||
file, err := os.Open(logFilePath)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to open log file: %v", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// Вычисляется временная граница (текущее время - hoursAgo)
|
||||
timeThreshold := time.Now().Add(-time.Duration(hoursAgo) * time.Hour)
|
||||
count := 0
|
||||
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
|
||||
// Парсим дату из строки (формат "2006/01/02 15:04:05")
|
||||
if len(line) < 20 { // Минимальная длина для даты
|
||||
continue
|
||||
}
|
||||
logTime, err := time.Parse("2006/01/02 15:04:05", line[:19])
|
||||
if err != nil {
|
||||
continue // Пропускаются строки где не дата
|
||||
}
|
||||
|
||||
// Проверка попадает ли дана во временной интервал
|
||||
if logTime.After(timeThreshold) && strings.Contains(line, grepPattern) {
|
||||
count++
|
||||
}
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
return 0, fmt.Errorf("error reading log file: %v", err)
|
||||
}
|
||||
|
||||
return count, nil
|
||||
return output.Bytes(), nil
|
||||
}
|
||||
|
|
|
@ -1,27 +0,0 @@
|
|||
package tools
|
||||
|
||||
import "time"
|
||||
|
||||
// Ticker запускает функцию fn с указанным интервалом в отдельной горутине
|
||||
// Возвращает канал для остановки тикера
|
||||
func Ticker(fn func(), interval time.Duration) chan struct{} {
|
||||
if interval <= 0 {
|
||||
interval = time.Second
|
||||
}
|
||||
ticker := time.NewTicker(interval)
|
||||
stopChan := make(chan struct{})
|
||||
go func() {
|
||||
fn()
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
fn()
|
||||
case <-stopChan:
|
||||
ticker.Stop()
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return stopChan
|
||||
}
|