//go:generate go install github.com/valyala/quicktemplate/qtc@latest //go:generate qtc -dir=web/template //go:generate go install golang.org/x/text/cmd/gotext@latest //go:generate gotext -srclang=en update -out=catalog_gen.go -lang=en,ru package main import ( "bytes" "context" "errors" "flag" "fmt" "io/fs" "log" "net" "net/http" "os" "os/signal" "path/filepath" "runtime" "runtime/pprof" "syscall" "time" _ "time/tzdata" "github.com/caarlos0/env/v10" "golang.org/x/text/language" "golang.org/x/text/message" "source.toby3d.me/toby3d/home/internal/common" "source.toby3d.me/toby3d/home/internal/domain" "source.toby3d.me/toby3d/home/internal/page" pagefsrepo "source.toby3d.me/toby3d/home/internal/page/repository/fs" pageucase "source.toby3d.me/toby3d/home/internal/page/usecase" sitefsrepo "source.toby3d.me/toby3d/home/internal/site/repository/fs" siteucase "source.toby3d.me/toby3d/home/internal/site/usecase" staticfsrepo "source.toby3d.me/toby3d/home/internal/static/repository/fs" staticucase "source.toby3d.me/toby3d/home/internal/static/usecase" "source.toby3d.me/toby3d/home/internal/templateutil" themefsrepo "source.toby3d.me/toby3d/home/internal/theme/repository/fs" themeucase "source.toby3d.me/toby3d/home/internal/theme/usecase" ) type ( App struct { server *http.Server } Context struct { Site *domain.Site Page *domain.Page } ) var ( config = new(domain.Config) logger = log.New(os.Stdout, "home\t", log.Lmsgprefix|log.LstdFlags|log.LUTC) ) var cpuProfilePath, memProfilePath string func NewApp(ctx context.Context, config *domain.Config) (*App, error) { themeDir := os.DirFS(config.ThemeDir) contentDir := os.DirFS(config.ContentDir) statics := staticfsrepo.NewFileServerStaticRepository(contentDir) sites := sitefsrepo.NewFileSystemSiteRepository(contentDir) siter := siteucase.NewSiteUseCase(sites, statics) funcMap, err := templateutil.New(themeDir, siter) if err != nil { logger.Fatalln("cannot setup template.FuncMap for templates: %w", err) } staticer := staticucase.NewStaticUseCase(statics) themes := themefsrepo.NewFileSystemThemeRepository(themeDir, funcMap) themer := themeucase.NewThemeUseCase(themes) pages := pagefsrepo.NewFileSystemPageRepository(contentDir) pager := pageucase.NewPageUseCase(pages, statics) matcher := language.NewMatcher(message.DefaultCatalog.Languages()) server := &http.Server{ Addr: config.AddrPort().String(), Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { tags, _, err := language.ParseAcceptLanguage(r.Header.Get(common.HeaderAcceptLanguage)) if err != nil || len(tags) == 0 { tags = append(tags, language.English) } lang, _, _ := matcher.Match(tags...) s, err := siter.Do(r.Context(), lang) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } p, err := pager.Do(r.Context(), lang, r.URL.Path) if err != nil { if !errors.Is(err, page.ErrNotExist) { http.Error(w, err.Error(), http.StatusInternalServerError) return } f, err := staticer.Do(r.Context(), r.URL.Path) if err != nil { if errors.Is(err, fs.ErrNotExist) { http.Error(w, err.Error(), http.StatusNotFound) return } http.Error(w, err.Error(), http.StatusInternalServerError) return } http.ServeContent(w, r, f.LogicalName(), f.Updated, bytes.NewReader(f.Content)) return } tpl, err := themer.Do(r.Context()) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.Header().Set(common.HeaderContentType, common.MIMETextHTMLCharsetUTF8) if err = tpl.Execute(w, &Context{ Site: s, Page: p, }); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } }), ErrorLog: logger, BaseContext: func(ln net.Listener) context.Context { return ctx }, WriteTimeout: 500 * time.Millisecond, } return &App{server: server}, nil } func (a *App) Run(ln net.Listener) error { var err error if ln != nil { err = a.server.Serve(ln) } else { err = a.server.ListenAndServe() } if err != nil { return fmt.Errorf("cannot listen and serve server: %w", err) } return nil } func (a *App) Stop(ctx context.Context) error { if err := a.server.Shutdown(ctx); err != nil { return fmt.Errorf("failed to shutdown app: %w", err) } return nil } func main() { flag.StringVar(&cpuProfilePath, "cpuprofile", "", "set path to saving CPU memory profile") flag.StringVar(&memProfilePath, "memprofile", "", "set path to saving pprof memory profile") flag.Parse() var err error if err = env.ParseWithOptions(config, env.Options{Prefix: "HOME_"}); err != nil { logger.Fatalln("cannot unmarshal configuration into domain:", err) } for _, dir := range []*string{ &config.ContentDir, &config.ThemeDir, } { if *dir, err = filepath.Abs(filepath.Clean(*dir)); err != nil { logger.Fatalf("cannot format '%s' into absolute path: %s", *dir, err) } if _, err = os.Stat(*dir); err != nil { if errors.Is(err, os.ErrExist) { return } if !errors.Is(err, os.ErrNotExist) { logger.Fatalf("cannot check '%s' path: %v", *dir, err) } if err = os.MkdirAll(*dir, os.ModePerm); err != nil { logger.Fatalf("cannot create directory on '%s': %v", *dir, err) } } } ctx := context.Background() app, err := NewApp(ctx, config) if err != nil { logger.Fatalln("cannot setup app:", err) } done := make(chan os.Signal, 1) signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) if cpuProfilePath != "" { cpuProfile, err := os.Create(cpuProfilePath) if err != nil { logger.Fatalln("could not create CPU profile:", err) } defer cpuProfile.Close() if err = pprof.StartCPUProfile(cpuProfile); err != nil { logger.Fatalln("could not start CPU profile:", err) } defer pprof.StopCPUProfile() } go func() { logger.Printf("starting server on %d...", config.Port) if err = app.Run(nil); err != nil { logger.Fatalln("cannot run app:", err) } }() <-done if err = app.Stop(ctx); err != nil { logger.Fatalln("failed shutdown server:", err) } if memProfilePath == "" { return } memProfile, err := os.Create(memProfilePath) if err != nil { logger.Fatalln("could not create memory profile:", err) } defer memProfile.Close() runtime.GC() // NOTE(toby3d): get up-to-date statistics if err = pprof.WriteHeapProfile(memProfile); err != nil { logger.Fatalln("could not write memory profile:", err) } }