about/internal/storage/storage.go

504 lines
18 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package storage
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"sync"
)
type Storage struct {
dataPath string
data map[string]interface{}
mutex sync.RWMutex
}
type PluginConfig struct {
Enabled bool `json:"enabled"`
Order int `json:"order"`
Settings map[string]interface{} `json:"settings"`
}
func New(dataPath string) *Storage {
return &Storage{
dataPath: dataPath,
data: make(map[string]interface{}),
}
}
func (s *Storage) Load() error {
s.mutex.Lock()
defer s.mutex.Unlock()
if err := os.MkdirAll(s.dataPath, 0755); err != nil {
return err
}
configFile := filepath.Join(s.dataPath, "config.json")
if _, err := os.Stat(configFile); os.IsNotExist(err) {
s.data = s.getDefaultConfig()
s.applyEnvOverrides()
return s.saveToFile()
}
data, err := os.ReadFile(configFile)
if err != nil {
return err
}
if err := json.Unmarshal(data, &s.data); err != nil {
s.data = s.getDefaultConfig()
s.applyEnvOverrides()
return s.saveToFile()
}
s.applyEnvOverrides()
return nil
}
func (s *Storage) Save() error {
s.mutex.Lock()
defer s.mutex.Unlock()
return s.saveToFile()
}
func (s *Storage) SetPluginConfig(pluginName string, config *PluginConfig) error {
s.mutex.Lock()
defer s.mutex.Unlock()
plugins, ok := s.data["plugins"].(map[string]interface{})
if !ok {
plugins = make(map[string]interface{})
s.data["plugins"] = plugins
}
plugins[pluginName] = map[string]interface{}{
"enabled": config.Enabled,
"order": config.Order,
"settings": config.Settings,
}
if err := s.saveToFile(); err != nil {
return fmt.Errorf("failed to save plugin config to file: %w", err)
}
return nil
}
func (s *Storage) saveToFile() error {
configFile := filepath.Join(s.dataPath, "config.json")
data, err := json.MarshalIndent(s.data, "", " ")
if err != nil {
return err
}
return os.WriteFile(configFile, data, 0644)
}
func (s *Storage) GetPluginConfig(pluginName string) *PluginConfig {
s.mutex.RLock()
defer s.mutex.RUnlock()
plugins, ok := s.data["plugins"].(map[string]interface{})
if !ok {
return &PluginConfig{Enabled: true, Order: 0, Settings: make(map[string]interface{})}
}
pluginData, ok := plugins[pluginName].(map[string]interface{})
if !ok {
return &PluginConfig{Enabled: true, Order: 0, Settings: make(map[string]interface{})}
}
config := &PluginConfig{
Enabled: true,
Order: 0,
Settings: make(map[string]interface{}),
}
if enabled, ok := pluginData["enabled"].(bool); ok {
config.Enabled = enabled
}
if order, ok := pluginData["order"].(float64); ok {
config.Order = int(order)
} else if orderInt, ok := pluginData["order"].(int); ok {
config.Order = orderInt
}
if settings, ok := pluginData["settings"].(map[string]interface{}); ok {
config.Settings = settings
}
return config
}
func (s *Storage) CreateBackup() error {
s.mutex.RLock()
defer s.mutex.RUnlock()
backupDir := filepath.Join(s.dataPath, "backups")
if err := os.MkdirAll(backupDir, 0755); err != nil {
return err
}
oldBackup := filepath.Join(backupDir, "config.json.bak")
_ = os.Remove(oldBackup)
configFile := filepath.Join(s.dataPath, "config.json")
backupFile := filepath.Join(backupDir, "config.json.bak")
data, err := os.ReadFile(configFile)
if err != nil {
return err
}
return os.WriteFile(backupFile, data, 0644)
}
func (s *Storage) getDefaultConfig() map[string]interface{} {
return map[string]interface{}{
"plugins": map[string]interface{}{
"profile": map[string]interface{}{
"enabled": true,
"order": 1,
"settings": map[string]interface{}{
"name": "sanspie",
"title": "Web FullStack Developer",
"subtitle": "DevSecOps",
"bio": "I'm a 19-year-old Python/Go developer from Russia, specialized in Django web apps. I'm passionate about web app security and Linux server administration. I've participated in developing monolithic websites using Django, Flask, and FastAPI, as well as microservices with REST and GraphQL. Magnum Opus hackathon team member and SCs ITMO student.",
"profileImage": "/static/images/sanspie.jpg",
},
},
"social": map[string]interface{}{
"enabled": true,
"order": 2,
"settings": map[string]interface{}{
"ui": map[string]interface{}{
"sectionTitle": "Links",
},
"links": []interface{}{
map[string]interface{}{"name": "Telegram", "url": "https://t.me/sanspie", "icon": "telegram"},
map[string]interface{}{"name": "GitHub", "url": "https://github.com/Alexander-D-Karpov", "icon": "github"},
map[string]interface{}{"name": "VK", "url": "https://vk.com/al.karpov", "icon": "vk"},
map[string]interface{}{"name": "LinkedIn", "url": "https://www.linkedin.com/in/alexandr-karpov-ba8891218", "icon": "linkedin"},
map[string]interface{}{"name": "Email", "url": "mailto:sanspie@akarpov.ru", "icon": "email"},
map[string]interface{}{"name": "Discord", "url": "https://discord.com/users/SansPie#9074", "icon": "discord"},
map[string]interface{}{"name": "CTFtime", "url": "https://ctftime.org/user/113621", "icon": "ctftime"},
map[string]interface{}{"name": "CodeWars", "url": "https://www.codewars.com/users/Alexander-D-Karpov", "icon": "codewars"},
map[string]interface{}{"name": "Last.fm", "url": "https://last.fm/user/sanspie", "icon": "lastfm"},
map[string]interface{}{"name": "Steam", "url": "https://steamcommunity.com/id/sanspie", "icon": "steam"},
map[string]interface{}{"name": "BeatLeader", "url": "https://beatleader.com/u/sanspie", "iconPath": "https://akarpov.ru/media/uploads/files/ZKJxiuUied.png"},
},
},
},
"techstack": map[string]interface{}{
"enabled": true,
"order": 3,
"settings": map[string]interface{}{
"technologies": []interface{}{
map[string]interface{}{"name": "Django", "icon": "django"},
map[string]interface{}{"name": "Python", "icon": "python"},
map[string]interface{}{"name": "FastAPI", "icon": "fastapi"},
map[string]interface{}{"name": "Go", "icon": "go"},
map[string]interface{}{"name": "Flask", "icon": "flask"},
map[string]interface{}{"name": "Linux", "icon": "linux"},
map[string]interface{}{"name": "Docker", "icon": "docker"},
map[string]interface{}{"name": "PostgreSQL", "icon": "postgresql"},
map[string]interface{}{"name": "Java", "icon": "java"},
map[string]interface{}{"name": "JavaScript", "icon": "javascript"},
map[string]interface{}{"name": "HTML", "icon": "html"},
map[string]interface{}{"name": "CSS", "icon": "css"},
map[string]interface{}{"name": "Nginx", "icon": "nginx"},
map[string]interface{}{"name": "Git", "icon": "git"},
map[string]interface{}{"name": "React", "icon": "react"},
map[string]interface{}{"name": "Bash", "icon": "bash"},
map[string]interface{}{"name": "Redis", "icon": "redis"},
map[string]interface{}{"name": "C++", "icon": "cpp"},
map[string]interface{}{"name": "GraphQL", "icon": "graphql"},
map[string]interface{}{"name": "PHP", "icon": "php"},
map[string]interface{}{"name": "C#", "icon": "csharp"},
},
},
},
"webring": map[string]interface{}{
"enabled": true,
"order": 4,
"settings": map[string]interface{}{
"webring_url": "https://webring.otomir23.me",
"username": "sanspie",
},
},
"neofetch": map[string]interface{}{
"enabled": true,
"order": 5,
"settings": map[string]interface{}{
"machines": []interface{}{
map[string]interface{}{
"name": "Desktop PC",
"output": " neofetch\n -` sanspie@TacOS\n .o+` -------------\n `ooo/ OS: Arch Linux x86_64\n `+oooo: Host: 21CBCTO1WW ThinkPad X1 Carbon Gen 10\n `+oooooo: Kernel: 6.16.8-arch2-1\n -+oooooo+: Uptime: 5 days, 5 hours, 2 mins\n `/:-:++oooo+: Packages: 1830 (pacman), 5 (flatpak)\n `/++++/+++++++: Shell: zsh 5.9\n `/++++++++++++++: Resolution: 2560x1600\n `/+++ooooooooooooo/` WM: i3\n ./ooosssso++osssssso+` Theme: Adwaita [GTK2], Arc-Dark [GTK3]\n .oossssso-````/ossssss+` Icons: Adwaita [GTK2], Papirus [GTK3]\n -osssssso. :ssssssso. Terminal: alacritty\n :osssssss/ osssso+++. CPU: 12th Gen Intel i7-1260P (16) @ 4.700GHz\n /ossssssss/ +ssssooo/- GPU: Intel Alder Lake-P GT2 [Iris Xe Graphics]\n `/ossssso+/:- -:/+osssso+- Memory: 14448MiB / 31795MiB\n `+sso+:-` `.-/+oso: Disk (/): 780G / 953G (83%)\n `++:. `-/+/ Battery0: 99% [Not charging]\n .` `/ Font: Cantarell 11 [GTK2], Sans 12 [GTK3]\n",
},
map[string]interface{}{
"name": "Laptop",
"output": " neofetch\n -` sanspie@TacOS\n .o+` -------------\n `ooo/ OS: Arch Linux x86_64\n `+oooo: Host: 21CBCTO1WW ThinkPad X1 Carbon Gen 10\n `+oooooo: Kernel: 6.16.8-arch2-1\n -+oooooo+: Uptime: 5 days, 5 hours, 2 mins\n `/:-:++oooo+: Packages: 1830 (pacman), 5 (flatpak)\n `/++++/+++++++: Shell: zsh 5.9\n `/++++++++++++++: Resolution: 2560x1600\n `/+++ooooooooooooo/` WM: i3\n ./ooosssso++osssssso+` Theme: Adwaita [GTK2], Arc-Dark [GTK3]\n .oossssso-````/ossssss+` Icons: Adwaita [GTK2], Papirus [GTK3]\n -osssssso. :ssssssso. Terminal: alacritty\n :osssssss/ osssso+++. CPU: 12th Gen Intel i7-1260P (16) @ 4.700GHz\n /ossssssss/ +ssssooo/- GPU: Intel Alder Lake-P GT2 [Iris Xe Graphics]\n `/ossssso+/:- -:/+osssso+- Memory: 14448MiB / 31795MiB\n `+sso+:-` `.-/+oso: Disk (/): 780G / 953G (83%)\n `++:. `-/+/ Battery0: 99% [Not charging]\n .` `/ Font: Cantarell 11 [GTK2], Sans 12 [GTK3]\n",
},
},
},
},
"lastfm": map[string]interface{}{
"enabled": true,
"order": 7,
"settings": map[string]interface{}{
"username": "sanspie",
"ui": map[string]interface{}{
"sectionTitle": "Music",
"showScrobbles": true,
"showPlayButton": true,
"showRecentTracks": true,
},
},
},
"beatleader": map[string]interface{}{
"enabled": true,
"order": 8,
"settings": map[string]interface{}{
"username": "sanspie",
"ui": map[string]interface{}{
"sectionTitle": "BeatLeader Stats",
"showPepeGif": true,
"showRecentMaps": true,
"showMainStats": true,
"loadingText": "Loading BeatLeader data...",
},
},
},
"steam": map[string]interface{}{
"enabled": true,
"order": 9,
"settings": map[string]interface{}{
"steamid": "76561198828323122",
},
},
"visitors": map[string]interface{}{
"enabled": true,
"order": 10,
"settings": map[string]interface{}{
"ui": map[string]interface{}{
"sectionTitle": "Visitors",
"showTotal": true,
"showToday": true,
"showVisitors": true,
},
},
},
"code": map[string]interface{}{
"enabled": true,
"order": 11,
"settings": map[string]interface{}{
"ui": map[string]interface{}{
"sectionTitle": "Coding Stats",
"showGitHub": true,
"showWakatime": true,
"showLanguages": true,
"showCommitGraph": true,
},
"github": map[string]interface{}{
"username": "Alexander-D-Karpov",
},
"wakatime": map[string]interface{}{
"api_key": "",
},
},
},
"services": map[string]interface{}{
"enabled": true,
"order": 12,
"settings": map[string]interface{}{
"ui": map[string]interface{}{
"sectionTitle": "Local Services",
"showStatus": true,
"showResponseTime": true,
},
"services": []interface{}{},
},
},
"personal": map[string]interface{}{
"enabled": true,
"order": 13,
"settings": map[string]interface{}{
"ui": map[string]interface{}{
"sectionTitle": "Personal Info",
"showImages": true,
"showCategories": true,
"layout": "grid",
},
"info": []interface{}{},
},
},
"meme": map[string]interface{}{
"enabled": true,
"order": 14,
"settings": map[string]interface{}{
"ui": map[string]interface{}{
"sectionTitle": "Random Meme",
"showMeme": true,
"autoRefresh": false,
"refreshInterval": 300,
},
"memes": []interface{}{
map[string]interface{}{
"type": "image",
"image": "/static/memes/test.webp",
"text": "really cool",
"category": "test",
},
map[string]interface{}{
"type": "image",
"image": "/static/memes/test2.jpg",
"text": "that says a lot about our society",
"category": "test",
},
},
},
},
"projects": map[string]interface{}{
"enabled": true,
"order": 98,
"settings": map[string]interface{}{
"projects": []interface{}{
map[string]interface{}{
"name": "AKarpov",
"description": "Personal website",
"github": "https://github.com/Alexander-D-Karpov/akarpov",
"live": "https://akarpov.ru",
"image": "https://akarpov.ru/media/uploads/images/E7BE3B27-5184-4C34-969D-D05E25DA69BE.jpeg",
"technologies": []interface{}{"Django", "Python", "PostgreSQL", "Docker", "Nginx"},
},
map[string]interface{}{
"name": "Personal About Page",
"description": "This page you're currently viewing",
"github": "https://github.com/Alexander-D-Karpov/about",
"live": "https://about.akarpov.ru",
"technologies": []interface{}{"Go", "WebSockets", "JavaScript", "HTML5", "CSS3"},
},
},
},
},
"info": map[string]interface{}{
"enabled": true,
"order": 99,
"settings": map[string]interface{}{
"ui": map[string]interface{}{
"sectionTitle": "Page Info",
"showServerInfo": true,
"showBuildInfo": false,
"showSourceCode": true,
"showSystemInfo": false,
},
"sourceCodeURL": "https://github.com/Alexander-D-Karpov/about",
},
},
},
}
}
func (s *Storage) applyEnvOverrides() {
s.setPluginString("profile", "name", os.Getenv("PROFILE_NAME"))
s.setPluginString("profile", "title", os.Getenv("PROFILE_TITLE"))
s.setPluginString("profile", "subtitle", os.Getenv("PROFILE_SUBTITLE"))
s.setPluginString("profile", "bio", os.Getenv("PROFILE_BIO"))
s.setPluginString("profile", "profileImage", os.Getenv("PROFILE_IMAGE"))
if j := os.Getenv("SOCIAL_LINKS_JSON"); j != "" {
var arr []map[string]interface{}
if json.Unmarshal([]byte(j), &arr) == nil {
s.setPluginValue("social", "links", toInterfaceSliceMap(arr))
}
}
if j := os.Getenv("TECHSTACK_JSON"); j != "" {
var arr []map[string]interface{}
if json.Unmarshal([]byte(j), &arr) == nil {
s.setPluginValue("techstack", "technologies", toInterfaceSliceMap(arr))
}
}
s.setPluginNestedString("code", []string{"github", "username"}, os.Getenv("GITHUB_USERNAME"))
s.setPluginNestedString("code", []string{"wakatime", "api_key"}, os.Getenv("WAKATIME_API_KEY"))
s.setPluginString("lastfm", "username", os.Getenv("LASTFM_USERNAME"))
s.setPluginNestedString("steam", []string{"steamid"}, os.Getenv("STEAM_ID"))
if u := os.Getenv("WEBRING_URL"); u != "" {
s.setPluginString("webring", "webring_url", u)
}
if u := os.Getenv("WEBRING_USER"); u != "" {
s.setPluginString("webring", "username", u)
}
}
func (s *Storage) ensurePlugin(name string) {
if s.data["plugins"] == nil {
s.data["plugins"] = map[string]interface{}{}
}
plugins := s.data["plugins"].(map[string]interface{})
if plugins[name] == nil {
plugins[name] = map[string]interface{}{
"enabled": true,
"order": 99,
"settings": map[string]interface{}{},
}
}
plug := plugins[name].(map[string]interface{})
if plug["settings"] == nil {
plug["settings"] = map[string]interface{}{}
}
}
func (s *Storage) setPluginString(plugin, key, val string) {
if val == "" {
return
}
s.ensurePlugin(plugin)
plugins := s.data["plugins"].(map[string]interface{})
settings := plugins[plugin].(map[string]interface{})["settings"].(map[string]interface{})
settings[key] = val
}
func (s *Storage) setPluginNestedString(plugin string, path []string, val string) {
if val == "" {
return
}
s.ensurePlugin(plugin)
plugins := s.data["plugins"].(map[string]interface{})
settings := plugins[plugin].(map[string]interface{})["settings"].(map[string]interface{})
cur := settings
for i, k := range path {
if i == len(path)-1 {
cur[k] = val
return
}
if next, ok := cur[k].(map[string]interface{}); ok {
cur = next
} else {
n := map[string]interface{}{}
cur[k] = n
cur = n
}
}
}
func (s *Storage) setPluginValue(plugin, key string, v interface{}) {
s.ensurePlugin(plugin)
plugins := s.data["plugins"].(map[string]interface{})
settings := plugins[plugin].(map[string]interface{})["settings"].(map[string]interface{})
settings[key] = v
}
func toInterfaceSliceMap(in []map[string]interface{}) []interface{} {
out := make([]interface{}, 0, len(in))
for _, v := range in {
out = append(out, v)
}
return out
}