Compare commits
No commits in common. "design" and "posts" have entirely different histories.
|
@ -1,3 +1,2 @@
|
||||||
config.json
|
config.json
|
||||||
hikan.ru
|
hikan.ru
|
||||||
*.server*
|
|
|
@ -1,17 +1,13 @@
|
||||||
::selection {
|
/* Design idea from here https://mo.rijndael.cc/ */
|
||||||
background: purple;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: -apple-system,system-ui,blinkmacsystemfont,Segoe UI,roboto,oxygen,ubuntu,Helvetica Neue,arial,sans-serif;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
background-image: url("/assets/pic/star.gif");
|
background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHdpZHRoPSc0MCcgaGVpZ2h0PSc0MCc+PHJlY3QgZmlsbD0nIzkxZWFlNCcgeD0nMCcgeT0nMCcgd2lkdGg9JzIwcHgnIGhlaWdodD0nMjBweCcvPjxyZWN0IGZpbGw9JyM4NmE4ZTcnIHg9JzAnIHk9JzIwJyB3aWR0aD0nMjBweCcgaGVpZ2h0PScyMHB4Jy8+PHJlY3QgZmlsbD0nIzkxZWFlNCcgeD0nMjAnIHk9JzIwJyB3aWR0aD0nMjBweCcgaGVpZ2h0PScyMHB4Jy8+PHJlY3QgZmlsbD0nIzg2YThlNycgeD0nMjAnIHk9JzAnIHdpZHRoPScyMHB4JyBoZWlnaHQ9JzIwcHgnLz48L3N2Zz4=");
|
||||||
background-color: #000;
|
background-repeat: repeat;
|
||||||
}
|
}
|
||||||
|
|
||||||
header, main, footer {
|
header, main, footer {
|
||||||
|
@ -31,140 +27,32 @@ main {
|
||||||
|
|
||||||
div {
|
div {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
background-color: #b1cfd8;
|
background-color: white;
|
||||||
box-shadow: 0 0 3px 3px rgba(135, 182, 196, 0.95);
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
border: 1px solid;
|
border: 1px solid;
|
||||||
border-radius: 10px;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 0 20px;
|
padding: 0 20px;
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pagi {
|
|
||||||
color: #b1cfd8;
|
|
||||||
text-align: center;
|
|
||||||
background-color: transparent;
|
|
||||||
box-shadow: none;
|
|
||||||
border: none;
|
|
||||||
font-size: 1.5em;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 30px;
|
box-sizing: border-box;
|
||||||
position: relative;
|
border-top: 1px solid;
|
||||||
|
border-bottom: 1px solid;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1::before, h1::after {
|
.count {
|
||||||
content: "";
|
display: flex;
|
||||||
position: absolute;
|
justify-content: center;
|
||||||
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; }
|
.count img {
|
||||||
h1::after { bottom: 0; }
|
height: 75px;
|
||||||
|
|
||||||
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;
|
|
||||||
transition: background-color 0.1s ease;
|
|
||||||
border-radius: 2px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
a:hover {
|
@media (max-width: 1200px) {
|
||||||
background-image: linear-gradient(to bottom, transparent 60%, #778bbb 60%);
|
|
||||||
}
|
|
||||||
|
|
||||||
a[hx-get] {
|
|
||||||
background-image: linear-gradient(to right, #77abbb, #778bbb);
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
details[open] > summary {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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-color: #77abbb;
|
|
||||||
}
|
|
||||||
|
|
||||||
a[hx-get]:hover {
|
|
||||||
background: linear-gradient(to right, #77abbb, #778bbb);
|
|
||||||
}
|
|
||||||
|
|
||||||
header, footer, main {
|
header, footer, main {
|
||||||
flex: 1 100%;
|
flex: 1 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
details > summary {
|
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
|
|
||||||
details[open] > summary {
|
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
}
|
}
|
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: 91 KiB |
Before Width: | Height: | Size: 3.0 KiB |
4
eye.sh
|
@ -3,8 +3,8 @@ stty -echoctl # Отключает вывод управляющих симво
|
||||||
|
|
||||||
# НАСТРОЙКА СКРИПТА ТУТ ###########################################################
|
# НАСТРОЙКА СКРИПТА ТУТ ###########################################################
|
||||||
DURATION=1 # Задержка между проверками в секундах
|
DURATION=1 # Задержка между проверками в секундах
|
||||||
WATCH_TARGETS=("assets" "mvc" "posts" "tools" "main.go" "config.json") # Массив целей для наблюдения (директории и файлы)
|
WATCH_TARGETS=("assets" "mvc" "posts" "tools" "main.go") # Массив целей для наблюдения (директории и файлы)
|
||||||
BINARY_PATH="./main.server" # Путь до бинарного файла
|
BINARY_PATH="./main" # Путь до бинарного файла
|
||||||
BUILD_CMD="go build -o $BINARY_PATH main.go" # Команда для сборки
|
BUILD_CMD="go build -o $BINARY_PATH main.go" # Команда для сборки
|
||||||
###################################################################################
|
###################################################################################
|
||||||
|
|
||||||
|
|
52
main.go
|
@ -8,7 +8,6 @@ import (
|
||||||
"main/mvc/models"
|
"main/mvc/models"
|
||||||
"main/tools"
|
"main/tools"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
@ -26,31 +25,6 @@ func main() {
|
||||||
// Настройка маршрутов
|
// Настройка маршрутов
|
||||||
router := setupRoutes(app)
|
router := setupRoutes(app)
|
||||||
|
|
||||||
// Запуск тикеров
|
|
||||||
{
|
|
||||||
// Обновление последнего прослушанного трека ластфм
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Запуск сервера
|
// Запуск сервера
|
||||||
if ok, err := tools.IsIPInUse(app.Cfg.ServerIP); err != nil {
|
if ok, err := tools.IsIPInUse(app.Cfg.ServerIP); err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
|
@ -75,25 +49,13 @@ func setupRoutes(app *models.App) *http.ServeMux {
|
||||||
{
|
{
|
||||||
// Обработка главной страницы
|
// Обработка главной страницы
|
||||||
router.Handle("/", m(controllers_pages.MainPageHandler(app)))
|
router.Handle("/", m(controllers_pages.MainPageHandler(app)))
|
||||||
}
|
// Обработка страницы со списком постов
|
||||||
|
router.Handle("/posts/", m(controllers_pages.PostsPageHandler(app)))
|
||||||
// Странички со списками постов
|
// Обработка страничек постов
|
||||||
postsPagesCount := (len(app.PostsMap) + app.Cfg.PostsMaxCountOnPage - 1) / app.Cfg.PostsMaxCountOnPage
|
for key := range app.Posts {
|
||||||
for i := range postsPagesCount {
|
postLink := string(key)
|
||||||
router.Handle(
|
router.Handle(postLink, m(controllers_pages.PostPageHandler(app)))
|
||||||
fmt.Sprintf("/posts/%d", i),
|
}
|
||||||
m(controllers_pages.PostsPageHandler(app)))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Обработка страничек постов
|
|
||||||
for key := range app.PostsMap {
|
|
||||||
postLink := string(key)
|
|
||||||
router.Handle(postLink, m(controllers_pages.PostPageHandler(app)))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Api
|
|
||||||
{
|
|
||||||
router.Handle("/api/lastfm", m(controllers.LastFMHandler(app)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return router
|
return router
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package controllers_pages
|
package controllers_pages
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
"main/mvc/models"
|
"main/mvc/models"
|
||||||
|
@ -14,15 +13,12 @@ import (
|
||||||
func MainPageHandler(app *models.App) http.HandlerFunc {
|
func MainPageHandler(app *models.App) http.HandlerFunc {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
var (
|
var err error
|
||||||
pageData []byte
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
|
|
||||||
// Количество запросов, обработанных сервером за 24ч
|
// Количество запросов, обработанных сервером за 24ч
|
||||||
if r.Method == "COUNT" {
|
if r.Method == "COUNT" {
|
||||||
var count []byte
|
var count []byte
|
||||||
if count, err = tools.GetJournalctlLogsCount("server", "duration: ", 24); err != nil {
|
if count, err = tools.GetJournalctlLogsCount("server", app.Cfg.ServerDomain, 24); err != nil {
|
||||||
log.Printf("%s", err.Error())
|
log.Printf("%s", err.Error())
|
||||||
}
|
}
|
||||||
sendCount(w, count)
|
sendCount(w, count)
|
||||||
|
@ -41,24 +37,17 @@ func MainPageHandler(app *models.App) http.HandlerFunc {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
cacheKey := models_pages.MainPageTmplName
|
// Страничка рендерится только если ее нет в кэше
|
||||||
ajax := r.URL.Query().Get("ajax") == "true"
|
pageData, ok := app.PagesCache.Get(models_pages.MainPageTmplName)
|
||||||
if ajax {
|
if !ok {
|
||||||
cacheKey += "?ajax=true"
|
pageData, err = models_pages.RenderMainPage(app.Templates, app.Version)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
app.PagesCache.Set(models_pages.MainPageTmplName, pageData)
|
||||||
}
|
}
|
||||||
|
|
||||||
if pageData, ok := app.PagesCache.Get(cacheKey); ok {
|
|
||||||
sendMainPage(w, pageData)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
pageData, err = models_pages.RenderMainPage(app.Templates, app.Version, ajax)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
app.PagesCache.Set(cacheKey, pageData)
|
|
||||||
|
|
||||||
sendMainPage(w, pageData)
|
sendMainPage(w, pageData)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -81,7 +70,7 @@ func sendCount(w http.ResponseWriter, data []byte) {
|
||||||
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)
|
||||||
fmt.Fprint(w, "13.01.2005\n")
|
w.Write([]byte("13.01.2005\n"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ответ на метод LIMINAL
|
// Ответ на метод LIMINAL
|
||||||
|
@ -89,5 +78,6 @@ 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"
|
||||||
fmt.Fprint(w, text)
|
w.Write([]byte(text))
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,37 +4,37 @@ import (
|
||||||
"main/mvc/models"
|
"main/mvc/models"
|
||||||
"main/mvc/models/models_pages"
|
"main/mvc/models/models_pages"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Обработчик страницы поста
|
// Обработчик главной страницы
|
||||||
func PostPageHandler(app *models.App) http.HandlerFunc {
|
func PostPageHandler(app *models.App) http.HandlerFunc {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
link := r.URL.Path
|
var err error
|
||||||
cacheKey := link
|
|
||||||
ajax := r.URL.Query().Get("ajax") == "true"
|
postLink := r.URL.Path
|
||||||
if ajax {
|
|
||||||
cacheKey += "?ajax=true"
|
// Ссылки на посты имеют вид 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)
|
pageData, ok := app.PagesCache.Get(postLink)
|
||||||
return
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
post := app.PostsMap[models_pages.PostLink(link)]
|
|
||||||
|
|
||||||
var (
|
|
||||||
pageData []byte
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
|
|
||||||
pageData, err = post.RenderPostPage(app.Templates, app.Version, ajax)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
app.PagesCache.Set(cacheKey, pageData)
|
|
||||||
|
|
||||||
sendPostPage(w, pageData)
|
sendPostPage(w, pageData)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,45 +4,26 @@ import (
|
||||||
"main/mvc/models"
|
"main/mvc/models"
|
||||||
"main/mvc/models/models_pages"
|
"main/mvc/models/models_pages"
|
||||||
"net/http"
|
"net/http"
|
||||||
"path"
|
|
||||||
"strconv"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Обработчик странички со списком постов
|
// Обработчик главной страницы
|
||||||
func PostsPageHandler(app *models.App) http.HandlerFunc {
|
func PostsPageHandler(app *models.App) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
link := r.URL.Path
|
var err error
|
||||||
cacheKey := link
|
|
||||||
|
|
||||||
ajax := r.URL.Query().Get("ajax") == "true"
|
// Страничка рендерится только если ее нет в кэше
|
||||||
if ajax {
|
pageData, ok := app.PagesCache.Get(models_pages.PostsPageTmplName)
|
||||||
cacheKey += "?ajax=true"
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
if pageData, ok := app.PagesCache.Get(cacheKey); ok {
|
|
||||||
sendPostsPage(w, pageData)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
postsList := app.PostsMap.PostsList()
|
|
||||||
pageNumber, _ := strconv.Atoi(path.Base(link))
|
|
||||||
page := models_pages.CreatePostsPage(postsList, pageNumber, app.Cfg.PostsMaxCountOnPage)
|
|
||||||
|
|
||||||
var (
|
|
||||||
pageData []byte
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
|
|
||||||
pageData, err = page.RenderPostsPage(app.Templates, app.Version, ajax)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
app.PagesCache.Set(cacheKey, pageData)
|
|
||||||
sendPostsPage(w, pageData)
|
sendPostsPage(w, pageData)
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Отправляет страницу
|
// Отправляет страницу
|
||||||
|
|
|
@ -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)
|
next.ServeHTTP(w, r)
|
||||||
|
|
||||||
fullURL := r.URL.String()
|
log.Printf("%s %s %v", r.Method, r.URL.Path, time.Since(start))
|
||||||
|
|
||||||
log.Printf("%s %s | client: %s | duration: %v",
|
|
||||||
r.Method,
|
|
||||||
fullURL,
|
|
||||||
r.RemoteAddr,
|
|
||||||
time.Since(start))
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,12 +7,11 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type App struct {
|
type App struct {
|
||||||
Cfg *Config // Сонфиг
|
Cfg *Config // Сонфиг
|
||||||
PostsMap models_pages.PostsMap // Посты
|
Posts models_pages.Posts // Посты
|
||||||
Templates *template.Template // Шаблоны страниц
|
Templates *template.Template // Шаблоны страниц
|
||||||
PagesCache *Cache // Кэш (отрендеренные странички)
|
PagesCache *Cache // Кэш (отрендеренные странички)
|
||||||
LastfmLastTrack string // Последний трек, полученный с ластфм
|
Version int64 // Время запуска
|
||||||
Version int64 // Время запуска
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Инициализирует приложение
|
// Инициализирует приложение
|
||||||
|
@ -28,7 +27,7 @@ func InitApp() (*App, error) {
|
||||||
return nil, err
|
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
|
return nil, err
|
||||||
}
|
}
|
||||||
// Загрузка шаблонов
|
// Загрузка шаблонов
|
||||||
|
@ -37,7 +36,5 @@ func InitApp() (*App, error) {
|
||||||
}
|
}
|
||||||
// Инициализация кэша
|
// Инициализация кэша
|
||||||
app.PagesCache = initCache()
|
app.PagesCache = initCache()
|
||||||
// Строка по умолчанию для последнего прослушанного трека
|
|
||||||
app.LastfmLastTrack = "None"
|
|
||||||
return app, nil
|
return app, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,6 @@ package models
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"os"
|
"os"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -11,48 +10,27 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
pathsConfig
|
PostsDir string
|
||||||
serverConfig
|
AssetsDir string
|
||||||
lastFMConfig
|
TemplatesDir string
|
||||||
cacheConfig
|
TemplatesExt string
|
||||||
|
LocalIP string
|
||||||
|
LocalPort string
|
||||||
|
ServerIP string
|
||||||
|
ServerPort string
|
||||||
|
ServerDomain string
|
||||||
|
Port string
|
||||||
}
|
}
|
||||||
|
|
||||||
type pathsConfig struct {
|
func loadConfig(configPath string) (*Config, error) {
|
||||||
PostsDir string `json:"posts_dir"`
|
cfg := &Config{}
|
||||||
AssetsDir string `json:"assets_dir"`
|
configFile, err := os.ReadFile(configPath)
|
||||||
TemplatesDir string `json:"templates_dir"`
|
|
||||||
TemplatesExt string `json:"templates_ext"`
|
|
||||||
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"`
|
|
||||||
ServerDomain string `json:"server_domain"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type lastFMConfig struct {
|
|
||||||
LastFMUsername string `json:"lastfm_username"`
|
|
||||||
LastFMToken string `json:"lastfm_token"`
|
|
||||||
LastFMUpdateInterval time.Duration `json:"lastfm_update_interval"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type cacheConfig struct {
|
|
||||||
CacheLogInterval time.Duration `json:"cache_log_interval"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func loadConfig(path string) (*Config, error) {
|
|
||||||
data, err := os.ReadFile(path)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
err = json.Unmarshal(configFile, cfg)
|
||||||
var cfg Config
|
if err != nil {
|
||||||
if err := json.Unmarshal(data, &cfg); err != nil {
|
|
||||||
return nil, err
|
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 (
|
const (
|
||||||
// Имя соответствующего шаблона
|
// Имя соответствующего шаблона
|
||||||
MainPageTmplName = "main.gohtml"
|
MainPageTmplName = "main.gohtml"
|
||||||
MainPageTmplNameAjax = "main_ajax.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
|
var pageData bytes.Buffer
|
||||||
|
|
||||||
context := map[string]any{
|
context := map[string]any{
|
||||||
|
@ -20,12 +19,7 @@ func RenderMainPage(templates *template.Template, version int64, ajax bool) ([]b
|
||||||
"renderingTimestamp": time.Now().Unix(),
|
"renderingTimestamp": time.Now().Unix(),
|
||||||
}
|
}
|
||||||
|
|
||||||
templateName := MainPageTmplName
|
if err := templates.ExecuteTemplate(&pageData, MainPageTmplName, context); err != nil {
|
||||||
if ajax {
|
|
||||||
templateName = MainPageTmplNameAjax
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := templates.ExecuteTemplate(&pageData, templateName, context); err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,13 +6,13 @@ import (
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type PostLink string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// Имя соответствующего шаблона
|
// Имя соответствующего шаблона
|
||||||
PostPageTmplName = "post.gohtml"
|
PostPageTmplName = "post.gohtml"
|
||||||
PostPageTmplNameAjax = "post_ajax.gohtml"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type PostLink string
|
|
||||||
type Post struct {
|
type Post struct {
|
||||||
Link PostLink
|
Link PostLink
|
||||||
Preview template.HTML
|
Preview template.HTML
|
||||||
|
@ -20,29 +20,23 @@ type Post struct {
|
||||||
Timestamp int64
|
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
|
var pageData bytes.Buffer
|
||||||
|
|
||||||
context := map[string]any{
|
context := map[string]any{
|
||||||
"version": version,
|
"version": version,
|
||||||
"renderingTimestamp": time.Now().Unix(),
|
"renderingTimestamp": time.Now().Unix(),
|
||||||
"data": p.Data,
|
"data": data,
|
||||||
"modTimestamp": p.Timestamp,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
templateName := PostPageTmplName
|
if err := templates.ExecuteTemplate(&pageData, PostPageTmplName, context); err != nil {
|
||||||
if ajax {
|
|
||||||
templateName = PostPageTmplNameAjax
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := templates.ExecuteTemplate(&pageData, templateName, context); err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return pageData.Bytes(), nil
|
return pageData.Bytes(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func createPost(link string, data []byte, timestamp int64) *Post {
|
func newPost(link string, data []byte, timestamp int64) *Post {
|
||||||
previewBuf := make([]byte, 0, 503)
|
previewBuf := make([]byte, 0, 503)
|
||||||
|
|
||||||
if len(data) > 500 {
|
if len(data) > 500 {
|
||||||
|
|
|
@ -4,33 +4,24 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
"log"
|
|
||||||
"main/tools"
|
"main/tools"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// Имя соответствующего шаблона
|
// Имя соответствующего шаблона
|
||||||
PostsPageTmplName = "posts.gohtml"
|
PostsPageTmplName = "posts.gohtml"
|
||||||
PostsPageTmplNameAjax = "posts_ajax.gohtml"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type PostsMap map[PostLink]*Post
|
type Posts map[PostLink]*Post
|
||||||
type PostsList []*Post
|
|
||||||
type PostsPage struct {
|
|
||||||
PostsListOnPage PostsList // список постов на странице
|
|
||||||
PageNumber int // номер страницы
|
|
||||||
PagesCount int // общее количество страниц
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
err := filepath.Walk(dir, func(path string, f os.FileInfo, err error) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -44,23 +35,9 @@ func LoadPosts(dir string) (PostsMap, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
html := tools.MdToHTML(md)
|
html := tools.MdToHTML(md)
|
||||||
|
link := fmt.Sprintf("/%s/", strings.TrimSuffix(filepath.Base(path), ".md"))
|
||||||
name := filepath.Base(path)
|
timestamp := f.ModTime().Unix()
|
||||||
lowLineIndex := strings.Index(name, "_")
|
posts[PostLink(link)] = newPost(link, html, timestamp)
|
||||||
|
|
||||||
if lowLineIndex == -1 {
|
|
||||||
// Обработка случая, если "_" нет в имени
|
|
||||||
log.Fatal(`post name parse error`)
|
|
||||||
}
|
|
||||||
|
|
||||||
timestampStr := name[:lowLineIndex]
|
|
||||||
timestamp, err := strconv.ParseInt(timestampStr, 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
// Ошибка парсинга timestamp
|
|
||||||
log.Fatal(`post name parse error`)
|
|
||||||
}
|
|
||||||
link := fmt.Sprintf("/%s", strings.TrimSuffix(name[lowLineIndex+1:], ".md"))
|
|
||||||
posts[PostLink(link)] = createPost(link, html, timestamp)
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
@ -72,65 +49,26 @@ func LoadPosts(dir string) (PostsMap, error) {
|
||||||
return posts, nil
|
return posts, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Получение из мапы постов списка постов, отсортированного по ModTimestamp (новые сначала)
|
func (p *Posts) RenderPostsPage(templates *template.Template, version int64) ([]byte, error) {
|
||||||
func (p *PostsMap) PostsList() PostsList {
|
var pageData bytes.Buffer
|
||||||
postsSlice := make(PostsList, 0, len(*p))
|
|
||||||
|
postsSlice := make([]*Post, 0, len(*p))
|
||||||
for _, post := range *p {
|
for _, post := range *p {
|
||||||
postsSlice = append(postsSlice, post)
|
postsSlice = append(postsSlice, post)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Сортирую по ModTimestamp (новые сначала)
|
||||||
sort.Slice(postsSlice, func(i, j int) bool {
|
sort.Slice(postsSlice, func(i, j int) bool {
|
||||||
return postsSlice[i].Timestamp > postsSlice[j].Timestamp
|
return postsSlice[i].Timestamp > postsSlice[j].Timestamp
|
||||||
})
|
})
|
||||||
|
|
||||||
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{
|
context := map[string]any{
|
||||||
"prevPageNumber": prevPageNumber,
|
|
||||||
"pageNumberInc": p.PageNumber + 1,
|
|
||||||
"pagesCount": p.PagesCount,
|
|
||||||
"nextPageNumber": nextPageNumber,
|
|
||||||
"version": version,
|
"version": version,
|
||||||
"renderingTimestamp": time.Now().Unix(),
|
"renderingTimestamp": time.Now().Unix(),
|
||||||
"posts": p.PostsListOnPage,
|
"posts": postsSlice,
|
||||||
}
|
}
|
||||||
|
|
||||||
templateName := PostsPageTmplName
|
if err := templates.ExecuteTemplate(&pageData, PostsPageTmplName, context); err != nil {
|
||||||
if ajax {
|
|
||||||
templateName = PostsPageTmplNameAjax
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := templates.ExecuteTemplate(&pageData, templateName, context); err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,28 +1,29 @@
|
||||||
{{ define "footer" }}
|
{{ define "footer" }}
|
||||||
<footer>
|
<footer>
|
||||||
<div class="rotating-pic-container">
|
<div>
|
||||||
<img src="/assets/pic/heh.png?v={{ .version }}" class="rotating-pic">
|
<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>
|
||||||
<div>
|
<div>
|
||||||
<p>
|
<p>
|
||||||
<strong>some system information</strong>:
|
<strong>some system information</strong>:
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<ul>
|
||||||
> unix timestamp of page rendering: <code>{{ .renderingTimestamp }}</code><br/>
|
<li>unix timestamp of page rendering: <code>{{ .renderingTimestamp }}</code></li>
|
||||||
> <code>curl -X LIMINAL https://hikan.ru</code> - what do you know about liminal spaces<br/>
|
<li><code>curl -X LIMINAL https://hikan.ru</code> - what do you know about liminal spaces?</li>
|
||||||
> <code>curl -X COUNT https://hikan.ru</code> - 24-hour server request count<br/>
|
<li>this site code repository - <a href="https://git.hikan.ru/serr/hikan.ru" target="_blank">git.hikan.ru/serr/hikan.ru</a></li>
|
||||||
|
<li>24-hour server request count (<code>curl -X COUNT https://hikan.ru</code>):</li>
|
||||||
|
</ul>
|
||||||
|
<p class="count">
|
||||||
|
<img src="/assets/pic/0.gif?v={{ .version }}">
|
||||||
|
<img src="/assets/pic/0.gif?v={{ .version }}">
|
||||||
|
<img src="/assets/pic/0.gif?v={{ .version }}">
|
||||||
|
<img src="/assets/pic/0.gif?v={{ .version }}">
|
||||||
</p>
|
</p>
|
||||||
<p>
|
|
||||||
<details>
|
|
||||||
<summary class="pulse-text">click here if you want more system details</summary>
|
|
||||||
<p>
|
|
||||||
> to check the latest changes to the site you can use it (someday i hope to add a cool log page):
|
|
||||||
<pre>curl -s "https://git.hikan.ru/api/v1/repos/serr/hikan.ru/commits?limit=10" | jq -r '.[] | .commit.author.date + " | " + (.commit.message | rtrimstr("\n"))'</pre>
|
|
||||||
or look in the repository <a href="https://git.hikan.ru/serr/hikan.ru" target="_blank">git.hikan.ru/serr/hikan.ru</a>
|
|
||||||
</p>
|
|
||||||
</details>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p>
|
<p>
|
||||||
|
@ -33,4 +34,16 @@
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
<script>
|
||||||
|
fetch('/', {method: 'COUNT'})
|
||||||
|
.then(r => r.text())
|
||||||
|
.then(num => num.trim())
|
||||||
|
.then(number => {
|
||||||
|
if (number.length > 0) {
|
||||||
|
document.querySelector('p.count').innerHTML =
|
||||||
|
[...number].map(d => `<img src="/assets/pic/${d}.gif?v={{ .version }}">`).join('');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(console.error);
|
||||||
|
</script>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
|
@ -3,16 +3,7 @@
|
||||||
<title>hikan.ru</title>
|
<title>hikan.ru</title>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<link rel="shortcut icon" href="/assets/pic/favicon.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">
|
<link rel="stylesheet" href="/assets/css/styles.css?v={{ .version }}" type="text/css">
|
||||||
<script src="/assets/scripts/htmx.js?v={{ .version }}"></script>
|
|
||||||
<script>
|
|
||||||
document.addEventListener('htmx:afterSwap', function(evt) {
|
|
||||||
const triggeringElement = evt.detail.requestConfig.elt;
|
|
||||||
if (triggeringElement.hasAttribute('data-scroll-top')) {
|
|
||||||
window.scrollTo(0,0);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</head>
|
</head>
|
||||||
{{ end }}
|
{{ end }}
|
|
@ -1,17 +1,7 @@
|
||||||
{{ define "header" }}
|
{{ define "header" }}
|
||||||
<header>
|
<header>
|
||||||
<div class="rotating-pic-container">
|
<div>
|
||||||
<img class="rotating-pic" src="/assets/pic/miffy.png?v={{ .version }}"
|
<img src="/assets/pic/header.webp?v={{ .version }}" width="100%" height="100%">
|
||||||
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>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h1>
|
<h1>
|
||||||
|
@ -22,22 +12,10 @@
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p>
|
<ul>
|
||||||
see <a href="/"
|
<li><a href="/">main page</a></li>
|
||||||
hx-get="/?ajax=true"
|
<li><a href="/posts/">posts section</a></li>
|
||||||
hx-target="main"
|
</ul>
|
||||||
hx-swap="outerHTML"
|
|
||||||
hx-push-url="/"
|
|
||||||
data-scroll-top="true">
|
|
||||||
main page</a> or go to
|
|
||||||
<a href="/posts/0"
|
|
||||||
hx-get="/posts/0?ajax=true"
|
|
||||||
hx-target="main"
|
|
||||||
hx-swap="outerHTML"
|
|
||||||
hx-push-url="/posts/0"
|
|
||||||
data-scroll-top="true">
|
|
||||||
posts section</a>, and also you can subscribe to my <a href="https://t.me/lolistack" target="_blank">telegram channel</a> with pictures!
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
{{ end }}
|
{{ end }}
|
|
@ -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,72 +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>
|
|
||||||
</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>
|
|
||||||
<div>
|
|
||||||
<h1>
|
|
||||||
nice links
|
|
||||||
</h1>
|
|
||||||
<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/>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
{{ end }}
|
|
|
@ -3,7 +3,11 @@
|
||||||
{{ template "head" . }}
|
{{ template "head" . }}
|
||||||
<body>
|
<body>
|
||||||
{{ template "header" . }}
|
{{ template "header" . }}
|
||||||
{{ template "post_ajax" . }}
|
<main>
|
||||||
|
<div>
|
||||||
|
{{ .data }}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
{{ template "footer" . }}
|
{{ template "footer" . }}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</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,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{})
|
|
||||||
fn()
|
|
||||||
go func() {
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ticker.C:
|
|
||||||
fn()
|
|
||||||
case <-stopChan:
|
|
||||||
ticker.Stop()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
return stopChan
|
|
||||||
}
|
|