Files
gitblog/internal/server/server.go
2025-09-15 04:02:11 +03:00

309 lines
7.7 KiB
Go

package server
import (
"fmt"
"log"
"net/http"
"strings"
"time"
"gitblog/internal/cache"
"gitblog/internal/config"
"gitblog/internal/content"
"gitblog/internal/github"
"gitblog/internal/templates"
"github.com/gorilla/handlers"
"github.com/gorilla/mux"
)
type Server struct {
config *config.Config
githubClient *github.Client
contentMgr *content.Manager
cacheMgr *cache.Manager
templateMgr *templates.Manager
updateTicker *time.Ticker
}
func New(cfg *config.Config) *Server {
githubClient := github.NewClient(cfg.GitHubToken, cfg.GitHubOwner, cfg.GitHubRepo)
contentMgr := content.NewManager(githubClient)
cacheMgr := cache.NewManager(cfg.CacheDuration, 5*time.Minute)
templateMgr := templates.NewManager()
return &Server{
config: cfg,
githubClient: githubClient,
contentMgr: contentMgr,
cacheMgr: cacheMgr,
templateMgr: templateMgr,
}
}
func (s *Server) Start() error {
if err := s.templateMgr.LoadTemplates(); err != nil {
return fmt.Errorf("failed to load templates: %w", err)
}
if err := s.contentMgr.LoadContent(); err != nil {
return fmt.Errorf("failed to load content: %w", err)
}
s.startContentUpdater()
router := s.setupRoutes()
corsHandler := handlers.CORS(
handlers.AllowedOrigins([]string{"*"}),
handlers.AllowedMethods([]string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}),
handlers.AllowedHeaders([]string{"X-Requested-With", "Content-Type", "Authorization"}),
)(router)
loggedRouter := handlers.LoggingHandler(log.Writer(), corsHandler)
server := &http.Server{
Addr: ":" + s.config.Port,
Handler: loggedRouter,
ReadTimeout: 15 * time.Second,
WriteTimeout: 15 * time.Second,
IdleTimeout: 60 * time.Second,
}
return server.ListenAndServe()
}
func (s *Server) setupRoutes() *mux.Router {
router := mux.NewRouter()
router.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir("web/static/"))))
router.HandleFunc("/", s.handleHome).Methods("GET")
router.HandleFunc("/post/{slug}", s.handlePost).Methods("GET")
router.HandleFunc("/category/{category}", s.handleCategory).Methods("GET")
router.HandleFunc("/page/{path:.*}", s.handlePage).Methods("GET")
router.HandleFunc("/rss.xml", s.handleRSS).Methods("GET")
router.HandleFunc("/api/update", s.handleUpdate).Methods("POST")
router.NotFoundHandler = http.HandlerFunc(s.handle404)
return router
}
func (s *Server) handleHome(w http.ResponseWriter, r *http.Request) {
theme := s.getTheme(r)
posts, found := s.cacheMgr.GetPosts()
if !found {
posts = s.contentMgr.GetAllPosts()
s.cacheMgr.SetPosts(posts, s.config.CacheDuration)
}
data := templates.PageData{
Title: "Home",
Posts: posts,
Navigation: s.contentMgr.GetNavigation(),
Theme: theme,
CurrentYear: time.Now().Year(),
}
html, err := s.templateMgr.Render("home", data)
if err != nil {
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Write([]byte(html))
}
func (s *Server) handlePost(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
slug := vars["slug"]
theme := s.getTheme(r)
post, found := s.cacheMgr.GetPost(slug)
if !found {
post, found = s.contentMgr.GetPost(slug)
if !found {
s.handle404(w, r)
return
}
s.cacheMgr.SetPost(slug, post, s.config.CacheDuration)
}
data := templates.PageData{
Title: post.Title,
Post: post,
Navigation: s.contentMgr.GetNavigation(),
Theme: theme,
CurrentYear: time.Now().Year(),
Meta: map[string]string{
"description": post.Excerpt,
},
}
html, err := s.templateMgr.Render("post", data)
if err != nil {
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Write([]byte(html))
}
func (s *Server) handleCategory(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
category := vars["category"]
theme := s.getTheme(r)
posts, found := s.cacheMgr.GetPostsByCategory(category)
if !found {
posts = s.contentMgr.GetPostsByCategory(category)
s.cacheMgr.SetPostsByCategory(category, posts, s.config.CacheDuration)
}
data := templates.PageData{
Title: strings.Title(category),
Posts: posts,
Navigation: s.contentMgr.GetNavigation(),
Theme: theme,
CurrentYear: time.Now().Year(),
}
html, err := s.templateMgr.Render("category", data)
if err != nil {
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Write([]byte(html))
}
func (s *Server) handlePage(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
path := vars["path"]
theme := s.getTheme(r)
page, found := s.cacheMgr.GetPage(path)
if !found {
page, found = s.contentMgr.GetPage(path)
if !found {
s.handle404(w, r)
return
}
s.cacheMgr.SetPage(path, page, s.config.CacheDuration)
}
data := templates.PageData{
Title: page.Title,
Page: page,
Navigation: s.contentMgr.GetNavigation(),
Theme: theme,
CurrentYear: time.Now().Year(),
}
html, err := s.templateMgr.Render("page", data)
if err != nil {
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Write([]byte(html))
}
func (s *Server) handleRSS(w http.ResponseWriter, r *http.Request) {
posts := s.contentMgr.GetAllPosts()
rss := `<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
<channel>
<title>GitBlog</title>
<description>A modern blog powered by GitHub</description>
<link>` + s.config.BaseURL + `</link>
<lastBuildDate>` + time.Now().Format(time.RFC1123Z) + `</lastBuildDate>
`
for _, post := range posts {
rss += ` <item>
<title>` + post.Title + `</title>
<description><![CDATA[` + post.Excerpt + `]]></description>
<link>` + s.config.BaseURL + `/post/` + post.Slug + `</link>
<pubDate>` + post.Date.Format(time.RFC1123Z) + `</pubDate>
</item>
`
}
rss += `</channel>
</rss>`
w.Header().Set("Content-Type", "application/rss+xml; charset=utf-8")
w.Write([]byte(rss))
}
func (s *Server) handleUpdate(w http.ResponseWriter, r *http.Request) {
if err := s.contentMgr.LoadContent(); err != nil {
http.Error(w, "Failed to update content", http.StatusInternalServerError)
return
}
s.cacheMgr.Clear()
w.WriteHeader(http.StatusOK)
w.Write([]byte("Content updated successfully"))
}
func (s *Server) handle404(w http.ResponseWriter, r *http.Request) {
theme := s.getTheme(r)
data := templates.PageData{
Title: "404 - Page Not Found",
Navigation: s.contentMgr.GetNavigation(),
Theme: theme,
CurrentYear: time.Now().Year(),
}
html, err := s.templateMgr.Render("404", data)
if err != nil {
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusNotFound)
w.Write([]byte(html))
}
func (s *Server) getTheme(r *http.Request) string {
cookie, err := r.Cookie("theme")
if err != nil {
return s.config.Theme
}
return cookie.Value
}
func (s *Server) startContentUpdater() {
s.updateTicker = time.NewTicker(s.config.UpdateInterval)
go func() {
for range s.updateTicker.C {
lastCommit, err := s.githubClient.GetLastCommit()
if err != nil {
log.Printf("Failed to check for updates: %v", err)
continue
}
if lastCommit.Commit.Committer.Date.After(s.contentMgr.GetLastUpdate()) {
log.Println("Content update detected, reloading...")
if err := s.contentMgr.LoadContent(); err != nil {
log.Printf("Failed to reload content: %v", err)
continue
}
s.cacheMgr.Clear()
}
}
}()
}