Compare commits
1 Commits
design
...
clientcach
Author | SHA1 | Date |
---|---|---|
|
975e7e8a4c |
|
@ -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 {
|
||||
|
@ -31,114 +27,32 @@ main {
|
|||
|
||||
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;
|
||||
}
|
||||
|
||||
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 {
|
||||
background-color: #77abbb;
|
||||
color: black;
|
||||
text-decoration: none;
|
||||
border-radius: 5px;
|
||||
padding: 0 5px;
|
||||
transition: background-color 0.1s ease;
|
||||
box-sizing: border-box;
|
||||
border-top: 1px solid;
|
||||
border-bottom: 1px solid;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
background-color: #778bbb;
|
||||
.count {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
pre {
|
||||
padding: 10px;
|
||||
background: #77abbb;
|
||||
border: 1px solid #000;
|
||||
overflow-x: auto;
|
||||
white-space: pre;
|
||||
word-wrap: normal;
|
||||
width: 0;
|
||||
min-width: calc(100% - 22px);
|
||||
.count img {
|
||||
height: 75px;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
@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: 91 KiB |
Before Width: | Height: | Size: 3.0 KiB |
2
eye.sh
|
@ -3,7 +3,7 @@ stty -echoctl # Отключает вывод управляющих симво
|
|||
|
||||
# НАСТРОЙКА СКРИПТА ТУТ ###########################################################
|
||||
DURATION=1 # Задержка между проверками в секундах
|
||||
WATCH_TARGETS=("assets" "mvc" "posts" "tools" "main.go" "config.json") # Массив целей для наблюдения (директории и файлы)
|
||||
WATCH_TARGETS=("assets" "mvc" "posts" "tools" "main.go") # Массив целей для наблюдения (директории и файлы)
|
||||
BINARY_PATH="./main" # Путь до бинарного файла
|
||||
BUILD_CMD="go build -o $BINARY_PATH main.go" # Команда для сборки
|
||||
###################################################################################
|
||||
|
|
30
main.go
|
@ -8,7 +8,6 @@ import (
|
|||
"main/mvc/models"
|
||||
"main/tools"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
@ -26,19 +25,6 @@ func main() {
|
|||
// Настройка маршрутов
|
||||
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)
|
||||
}
|
||||
|
||||
// Запуск сервера
|
||||
if ok, err := tools.IsIPInUse(app.Cfg.ServerIP); err != nil {
|
||||
log.Fatal(err)
|
||||
|
@ -65,17 +51,11 @@ func setupRoutes(app *models.App) *http.ServeMux {
|
|||
router.Handle("/", m(controllers_pages.MainPageHandler(app)))
|
||||
// Обработка страницы со списком постов
|
||||
router.Handle("/posts/", m(controllers_pages.PostsPageHandler(app)))
|
||||
}
|
||||
|
||||
// Обработка страничек постов
|
||||
for key := range app.Posts {
|
||||
postLink := string(key)
|
||||
router.Handle(postLink, m(controllers_pages.PostPageHandler(app)))
|
||||
}
|
||||
|
||||
// Api
|
||||
{
|
||||
router.Handle("/api/lastfm", m(controllers.LastFMHandler(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"
|
||||
|
@ -39,25 +38,35 @@ func MainPageHandler(app *models.App) http.HandlerFunc {
|
|||
}
|
||||
|
||||
// Страничка рендерится только если ее нет в кэше
|
||||
pageData, ok := app.PagesCache.Get(models_pages.MainPageTmplName)
|
||||
page, ok := app.PagesCache.Get(models_pages.MainPageTmplName)
|
||||
if !ok {
|
||||
pageData, err = models_pages.RenderMainPage(app.Templates, app.Version)
|
||||
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)
|
||||
ETag := models.GenerateETag(pageData)
|
||||
page = &models.Page{Data: pageData, ETag: ETag}
|
||||
app.PagesCache.Set(models_pages.MainPageTmplName, page)
|
||||
}
|
||||
|
||||
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
|
||||
|
@ -71,7 +80,7 @@ func sendCount(w http.ResponseWriter, data []byte) {
|
|||
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
|
||||
|
@ -79,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))
|
||||
|
||||
}
|
||||
|
|
|
@ -10,8 +10,6 @@ import (
|
|||
// Обработчик главной страницы
|
||||
func PostPageHandler(app *models.App) http.HandlerFunc {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
|
||||
postLink := r.URL.Path
|
||||
|
||||
// Ссылки на посты имеют вид postLink = /link/, а если прилетело что то типо /link/123123,
|
||||
|
@ -22,26 +20,33 @@ func PostPageHandler(app *models.App) http.HandlerFunc {
|
|||
}
|
||||
|
||||
// Страничка рендерится только если ее нет в кэше
|
||||
pageData, ok := app.PagesCache.Get(postLink)
|
||||
page, ok := app.PagesCache.Get(postLink)
|
||||
if !ok {
|
||||
|
||||
post := app.Posts[models_pages.PostLink(postLink)]
|
||||
|
||||
pageData, err = post.RenderPostPage(app.Templates, app.Version)
|
||||
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)
|
||||
ETag := models.GenerateETag(pageData)
|
||||
page = &models.Page{Data: pageData, ETag: ETag}
|
||||
app.PagesCache.Set(postLink, page)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -9,26 +9,33 @@ import (
|
|||
// Обработчик главной страницы
|
||||
func PostsPageHandler(app *models.App) http.HandlerFunc {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
|
||||
// Страничка рендерится только если ее нет в кэше
|
||||
pageData, ok := app.PagesCache.Get(models_pages.PostsPageTmplName)
|
||||
page, ok := app.PagesCache.Get(models_pages.PostsPageTmplName)
|
||||
if !ok {
|
||||
pageData, err = app.Posts.RenderPostsPage(app.Templates, app.Version)
|
||||
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)
|
||||
ETag := models.GenerateETag(pageData)
|
||||
page = &models.Page{Data: pageData, ETag: ETag}
|
||||
app.PagesCache.Set(models_pages.PostsPageTmplName, page)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
|
@ -7,12 +7,11 @@ import (
|
|||
)
|
||||
|
||||
type App struct {
|
||||
Cfg *Config // Сонфиг
|
||||
Posts models_pages.Posts // Посты
|
||||
Templates *template.Template // Шаблоны страниц
|
||||
PagesCache *Cache // Кэш (отрендеренные странички)
|
||||
LastfmLastTrack string // Последний трек, полученный с ластфм
|
||||
Version int64 // Время запуска
|
||||
Cfg *Config // Сонфиг
|
||||
Posts models_pages.Posts // Посты
|
||||
Templates *template.Template // Шаблоны страниц
|
||||
PagesCache *Cache // Кэш (отрендеренные странички)
|
||||
Version int64 // Время запуска
|
||||
}
|
||||
|
||||
// Инициализирует приложение
|
||||
|
@ -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,19 +10,16 @@ const (
|
|||
)
|
||||
|
||||
type Config struct {
|
||||
PostsDir string
|
||||
AssetsDir string
|
||||
TemplatesDir string
|
||||
TemplatesExt string
|
||||
LocalIP string
|
||||
LocalPort string
|
||||
ServerIP string
|
||||
ServerPort string
|
||||
ServerDomain string
|
||||
Port string
|
||||
LastFMUsername string
|
||||
LastFMToken string
|
||||
LastFMUpdateInterval time.Duration
|
||||
PostsDir string
|
||||
AssetsDir string
|
||||
TemplatesDir string
|
||||
TemplatesExt string
|
||||
LocalIP string
|
||||
LocalPort string
|
||||
ServerIP string
|
||||
ServerPort string
|
||||
ServerDomain string
|
||||
Port string
|
||||
}
|
||||
|
||||
func loadConfig(configPath string) (*Config, error) {
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -20,14 +20,13 @@ type Post struct {
|
|||
Timestamp int64
|
||||
}
|
||||
|
||||
func (p *Post) RenderPostPage(templates *template.Template, version int64) ([]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,
|
||||
}
|
||||
|
||||
if err := templates.ExecuteTemplate(&pageData, PostPageTmplName, context); err != nil {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<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">
|
||||
</head>
|
||||
{{ end }}
|
|
@ -1,13 +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">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>
|
||||
|
@ -18,31 +12,10 @@
|
|||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p>
|
||||
see <a href="/">main page</a> or go to <a href="/posts/">posts section</a>
|
||||
</p>
|
||||
<ul>
|
||||
<li><a href="/">main page</a></li>
|
||||
<li><a href="/posts/">posts section</a></li>
|
||||
</ul>
|
||||
</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>
|
||||
|
||||
<script>
|
||||
function updateLastfm() {
|
||||
fetch('/api/lastfm')
|
||||
.then(response => response.text())
|
||||
.then(text => {
|
||||
document.getElementById('lastfm').textContent = text;
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching lastfm:', error);
|
||||
});
|
||||
}
|
||||
|
||||
updateLastfm();
|
||||
|
||||
setInterval(updateLastfm, 30000);
|
||||
</script>
|
||||
</header>
|
||||
{{ end }}
|
|
@ -4,11 +4,6 @@
|
|||
<body>
|
||||
{{ template "header" . }}
|
||||
<main>
|
||||
<div>
|
||||
<p>
|
||||
<code>welcome to my little site</code>
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h1>
|
||||
$whoami
|
||||
|
@ -28,10 +23,10 @@
|
|||
what do i do?
|
||||
</h1>
|
||||
<p>
|
||||
programming is my lifestyle
|
||||
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: web development, low-level programming, formal grammars and a lot more!
|
||||
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
|
||||
|
@ -41,34 +36,34 @@
|
|||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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" . }}
|
||||
|
|
|
@ -6,9 +6,6 @@
|
|||
<main>
|
||||
<div>
|
||||
{{ .data }}
|
||||
<p>
|
||||
<code>mod time: {{ .modTimestamp }}</code>
|
||||
</p>
|
||||
</div>
|
||||
</main>
|
||||
{{ template "footer" . }}
|
||||
|
|
|
@ -7,66 +7,6 @@
|
|||
|
||||
Но в реальности все не так просто и при попытке что то положить по вычисленному адресу программа упадет с *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"
|
||||
}
|
||||
|
||||
```
|
||||
Код с пояснениями можно скачать [тут](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
|
||||
}
|