package main import ( "bytes" "log" "main/cache" "main/config" "main/tools" "net/http" "os" "path/filepath" "strings" "text/template" "time" ) /* sudo systemctl stop server.service go build main.go sudo systemctl start server.service */ // App представляет основное состояние приложения type App struct { Templates *template.Template // Шаблоны страниц Config *config.Config // Конфиг StartTime int64 // Время запуска Cache *cache.Cache // Кэш (отрендеренные странички) } func main() { var app *App var err error // Инициализация приложения if app, err = AppInit("config.json"); err != nil { log.Fatal(err) } // Настройка маршрутов и запуск if app.SetupRouterAndRun() != nil { log.Fatal(err) } } // Запускает сервер на указанном IP и порту 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() // Запуск сервера if ok, err := tools.IsIPInUse(a.Config.ServerIP); err != nil { return err } else if ok { RunServer(a.Config.ServerIP, a.Config.Port, router) } else { RunServer(a.Config.LocalIP, a.Config.Port, router) } return nil } // Настраивает маршруты func (a *App) SetupRouter() *http.ServeMux { router := http.NewServeMux() // Обработка статических файлов с кэшированием router.Handle(a.Config.AssetsPath, a.StaticHandler()) // Обработка главной страницы router.Handle("/", a.MainPageHandler()) return router } // Обработчик статических файлов с кэшированием func (a *App) 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) }) } // Обработчик главной страницы 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) }