master
serr 2025-04-05 19:44:26 +03:00
parent 7884d972d2
commit e7c0d09456
8 changed files with 161 additions and 138 deletions

152
main.go
View File

@ -1,17 +1,11 @@
package main package main
import ( import (
"bytes"
"log" "log"
"main/cache" "main/mvc/controllers"
"main/config" "main/mvc/models"
"main/tools" "main/tools"
"net/http" "net/http"
"os"
"path/filepath"
"strings"
"text/template"
"time"
) )
/* /*
@ -20,163 +14,53 @@ go build main.go
sudo systemctl start server.service sudo systemctl start server.service
*/ */
// App представляет основное состояние приложения
type app struct {
templates *template.Template // Шаблоны страниц
config *config.Config // Конфиг
startTime int64 // Время запуска
cache *cache.Cache // Кэш (отрендеренные странички)
}
func main() { func main() {
var app *app var app *models.App
var err error var err error
// Инициализация приложения // Инициализация приложения
if app, err = appInit("config.json"); err != nil { if app, err = models.AppInit("config.json"); err != nil {
log.Fatal(err) log.Fatal(err)
} }
// Настройка маршрутов и запуск // Настройка маршрутов и запуск
if app.setupRouterAndRun() != nil { if setupRoutesAndRun(app) != nil {
log.Fatal(err) log.Fatal(err)
} }
} }
// Запускает сервер на указанном IP и порту func setupRoutesAndRun(a *models.App) error {
func runServer(ip, port string, router http.Handler) {
addr := ip + port
log.Println("Run on", addr)
log.Fatal(http.ListenAndServe(addr, router))
}
// Инициализирует приложение
func appInit(configPath string) (*app, error) {
a := &app{
startTime: time.Now().Unix(),
config: config.Init(),
cache: cache.Init(),
}
// Загрузка конфига
if err := a.config.Load(configPath); err != nil {
return nil, err
}
// Загрузка шаблонов
if err := a.loadTemplates(a.config.TemplatesPath, a.config.TemplatesExt); err != nil {
log.Fatal(err)
}
return a, nil
}
func (a *app) setupRouterAndRun() error {
// Настройка маршрутов // Настройка маршрутов
router := a.setupRouter() router := setupRoutes(a)
// Запуск сервера // Запуск сервера
if ok, err := tools.IsIPInUse(a.config.ServerIP); err != nil { if ok, err := tools.IsIPInUse(a.Config.ServerIP); err != nil {
return err return err
} else if ok { } else if ok {
runServer(a.config.ServerIP, a.config.Port, router) runServer(a.Config.ServerIP, a.Config.Port, router)
} else { } else {
runServer(a.config.LocalIP, a.config.Port, router) runServer(a.Config.LocalIP, a.Config.Port, router)
} }
return nil return nil
} }
// Настраивает маршруты // Настраивает маршруты
func (a *app) setupRouter() *http.ServeMux { func setupRoutes(a *models.App) *http.ServeMux {
router := http.NewServeMux() router := http.NewServeMux()
// Обработка статических файлов с кэшированием // Обработка статических файлов с кэшированием
router.Handle(a.config.AssetsPath, a.staticHandler()) router.Handle(a.Config.AssetsPath, controllers.StaticHandler())
// Обработка главной страницы // Обработка главной страницы
router.Handle("/", a.mainPageHandler()) router.Handle("/", controllers.MainPageHandler(a))
return router return router
} }
// Обработчик статических файлов с кэшированием // Обертка над ListenAndServe, запускает сервер на указанном IP, PORT
func (a *app) staticHandler() http.HandlerFunc { func runServer(ip, port string, router http.Handler) {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { addr := ip + port
w.Header().Add("Cache-Control", "public, max-age=31536000, immutable") log.Println("Run on", addr)
// Здесь используется встроенный файловый сервер Go (http.FileServer), который: log.Fatal(http.ListenAndServe(addr, router))
// Реализует интерфейс http.Handler (и поэтому имеет метод ServeHTTP)
// Автоматически обслуживает статические файлы из файловой системы
// Сам обрабатывает HTTP-запросы, определяет MIME-типы, отправляет правильные заголовки и т.д.
http.FileServer(http.Dir(".")).ServeHTTP(w, r)
})
}
// Обработчик главной страницы
func (a *app) mainPageHandler() http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var err error
tmplName := "main" + a.config.TemplatesExt
// Страничка рендерится только если ее нет в кэше
pageData, ok := a.cache.Get(tmplName)
if !ok {
context := map[string]any{
"version": a.startTime,
"renderingTimestamp": time.Now().Unix(),
}
pageData, err = a.renderPage(tmplName, context)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
a.cache.Set(tmplName, pageData)
}
a.sendPage(w, pageData.([]byte))
})
}
// Загрузка шаблонов
func (a *app) loadTemplates(templatesPath string, ext string) error {
tmpls := template.New("")
err := filepath.Walk(templatesPath, func(path string, f os.FileInfo, err error) error {
if err != nil {
return err
}
if !f.IsDir() && strings.HasSuffix(f.Name(), ext) {
_, err = tmpls.ParseFiles(path)
if err != nil {
return err
}
}
return nil
})
if err != nil {
return err
}
a.templates = tmpls
return nil
}
// Рендерит шаблон в срез байт
func (a *app) renderPage(tmplName string, context any) ([]byte, error) {
var pageData bytes.Buffer
if err := a.templates.ExecuteTemplate(&pageData, tmplName, context); err != nil {
return nil, err
}
return pageData.Bytes(), nil
}
// Отправляет страницу
func (a *app) sendPage(w http.ResponseWriter, data []byte) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusOK)
w.Write(data)
} }

View File

@ -0,0 +1,33 @@
package controllers
import (
"main/mvc/models"
"net/http"
)
// Обработчик главной страницы
func MainPageHandler(a *models.App) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var err error
// Страничка рендерится только если ее нет в кэше
pageData, ok := a.Cache.Get(models.MainPageTmplName)
if !ok {
pageData, err = a.RenderMainPage()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
a.Cache.Set(models.MainPageTmplName, pageData)
}
SendMainPage(w, pageData.([]byte))
})
}
// Отправляет страницу
func SendMainPage(w http.ResponseWriter, data []byte) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusOK)
w.Write(data)
}

15
mvc/controllers/static.go Normal file
View File

@ -0,0 +1,15 @@
package controllers
import "net/http"
// Обработчик статических файлов с кэшированием
func StaticHandler() http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Cache-Control", "public, max-age=31536000, immutable")
// Здесь используется встроенный файловый сервер Go (http.FileServer), который:
// Реализует интерфейс http.Handler (и поэтому имеет метод ServeHTTP)
// Автоматически обслуживает статические файлы из файловой системы
// Сам обрабатывает HTTP-запросы, определяет MIME-типы, отправляет правильные заголовки и т.д.
http.FileServer(http.Dir(".")).ServeHTTP(w, r)
})
}

65
mvc/models/app.go Normal file
View File

@ -0,0 +1,65 @@
package models
import (
"html/template"
"log"
"os"
"path/filepath"
"strings"
"time"
)
// App хранит информацию о приложении
type App struct {
Config *Config // Конфиг
Templates *template.Template // Шаблоны страниц
Cache *Cache // Кэш (отрендеренные странички)
StartTime int64 // Время запуска
}
// Инициализирует приложение
func AppInit(configPath string) (*App, error) {
a := &App{
StartTime: time.Now().Unix(),
Config: ConfigInit(),
Cache: CacheInit(),
}
// Загрузка конфига
if err := a.Config.Load(configPath); err != nil {
return nil, err
}
// Загрузка шаблонов
if err := a.loadTemplates(a.Config.TemplatesPath, a.Config.TemplatesExt); err != nil {
log.Fatal(err)
}
return a, nil
}
// Загрузка шаблонов
func (a *App) loadTemplates(templatesPath string, ext string) error {
tmpls := template.New("")
err := filepath.Walk(templatesPath, func(path string, f os.FileInfo, err error) error {
if err != nil {
return err
}
if !f.IsDir() && strings.HasSuffix(f.Name(), ext) {
_, err = tmpls.ParseFiles(path)
if err != nil {
return err
}
}
return nil
})
if err != nil {
return err
}
a.Templates = tmpls
return nil
}

View File

@ -1,4 +1,4 @@
package cache package models
import "sync" import "sync"
@ -7,7 +7,7 @@ type Cache struct {
Mu sync.RWMutex Mu sync.RWMutex
} }
func Init() *Cache { func CacheInit() *Cache {
return &Cache{Data: make(map[string]any)} return &Cache{Data: make(map[string]any)}
} }

View File

@ -1,4 +1,4 @@
package config package models
import ( import (
"encoding/json" "encoding/json"
@ -14,7 +14,7 @@ type Config struct {
Port string Port string
} }
func Init() *Config { func ConfigInit() *Config {
return &Config{} return &Config{}
} }

26
mvc/models/main_page.go Normal file
View File

@ -0,0 +1,26 @@
package models
import (
"bytes"
"time"
)
const (
// Имя соответствующего шаблона
MainPageTmplName = "main_page.gohtml"
)
func (a *App) RenderMainPage() ([]byte, error) {
var pageData bytes.Buffer
context := map[string]any{
"version": a.StartTime,
"renderingTimestamp": time.Now().Unix(),
}
if err := a.Templates.ExecuteTemplate(&pageData, MainPageTmplName, context); err != nil {
return nil, err
}
return pageData.Bytes(), nil
}