diff --git a/main.go b/main.go index 372d956..817c6c4 100644 --- a/main.go +++ b/main.go @@ -1,17 +1,11 @@ package main import ( - "bytes" "log" - "main/cache" - "main/config" + "main/mvc/controllers" + "main/mvc/models" "main/tools" "net/http" - "os" - "path/filepath" - "strings" - "text/template" - "time" ) /* @@ -20,163 +14,53 @@ 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 app *models.App var err error // Инициализация приложения - if app, err = appInit("config.json"); err != nil { + if app, err = models.AppInit("config.json"); err != nil { log.Fatal(err) } // Настройка маршрутов и запуск - if app.setupRouterAndRun() != nil { + if setupRoutesAndRun(app) != 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 { +func setupRoutesAndRun(a *models.App) 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 } else if ok { - runServer(a.config.ServerIP, a.config.Port, router) + runServer(a.Config.ServerIP, a.Config.Port, router) } else { - runServer(a.config.LocalIP, a.config.Port, router) + runServer(a.Config.LocalIP, a.Config.Port, router) } return nil } // Настраивает маршруты -func (a *app) setupRouter() *http.ServeMux { +func setupRoutes(a *models.App) *http.ServeMux { 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 } -// Обработчик статических файлов с кэшированием -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) +// Обертка над ListenAndServe, запускает сервер на указанном IP, PORT +func runServer(ip, port string, router http.Handler) { + addr := ip + port + log.Println("Run on", addr) + log.Fatal(http.ListenAndServe(addr, router)) } diff --git a/mvc/controllers/main_page.go b/mvc/controllers/main_page.go new file mode 100644 index 0000000..1e78e3c --- /dev/null +++ b/mvc/controllers/main_page.go @@ -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) +} diff --git a/mvc/controllers/static.go b/mvc/controllers/static.go new file mode 100644 index 0000000..93806fd --- /dev/null +++ b/mvc/controllers/static.go @@ -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) + }) +} diff --git a/mvc/models/app.go b/mvc/models/app.go new file mode 100644 index 0000000..b4a2fcf --- /dev/null +++ b/mvc/models/app.go @@ -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 +} diff --git a/cache/cache.go b/mvc/models/cache.go similarity index 89% rename from cache/cache.go rename to mvc/models/cache.go index 9c5a616..5554aa6 100644 --- a/cache/cache.go +++ b/mvc/models/cache.go @@ -1,4 +1,4 @@ -package cache +package models import "sync" @@ -7,7 +7,7 @@ type Cache struct { Mu sync.RWMutex } -func Init() *Cache { +func CacheInit() *Cache { return &Cache{Data: make(map[string]any)} } diff --git a/config/config.go b/mvc/models/config.go similarity index 90% rename from config/config.go rename to mvc/models/config.go index 68d5d78..162db30 100644 --- a/config/config.go +++ b/mvc/models/config.go @@ -1,4 +1,4 @@ -package config +package models import ( "encoding/json" @@ -14,7 +14,7 @@ type Config struct { Port string } -func Init() *Config { +func ConfigInit() *Config { return &Config{} } diff --git a/mvc/models/main_page.go b/mvc/models/main_page.go new file mode 100644 index 0000000..63ee2d7 --- /dev/null +++ b/mvc/models/main_page.go @@ -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 +} diff --git a/assets/templates/main.gohtml b/mvc/views/main_page.gohtml similarity index 100% rename from assets/templates/main.gohtml rename to mvc/views/main_page.gohtml