package home import ( "context" "fmt" "io/fs" "log" "net" "net/http" "os" "strings" "time" "source.toby3d.me/toby3d/home/internal/domain" pagefsrepo "source.toby3d.me/toby3d/home/internal/entry/repository/fs" pageucase "source.toby3d.me/toby3d/home/internal/entry/usecase" "source.toby3d.me/toby3d/home/internal/middleware" resourcehttpdelivery "source.toby3d.me/toby3d/home/internal/resource/delivery/http" resourcefsrepo "source.toby3d.me/toby3d/home/internal/resource/repository/fs" resourceucase "source.toby3d.me/toby3d/home/internal/resource/usecase" servercase "source.toby3d.me/toby3d/home/internal/server/usecase" sitehttpdelivery "source.toby3d.me/toby3d/home/internal/site/delivery/http" sitefsrepo "source.toby3d.me/toby3d/home/internal/site/repository/fs" siteucase "source.toby3d.me/toby3d/home/internal/site/usecase" statichttpdelivery "source.toby3d.me/toby3d/home/internal/static/delivery/http" staticfsrepo "source.toby3d.me/toby3d/home/internal/static/repository/fs" staticucase "source.toby3d.me/toby3d/home/internal/static/usecase" themehttpdelivery "source.toby3d.me/toby3d/home/internal/theme/delivery/http" themefsrepo "source.toby3d.me/toby3d/home/internal/theme/repository/fs" themeucase "source.toby3d.me/toby3d/home/internal/theme/usecase" "source.toby3d.me/toby3d/home/internal/urlutil" webfingerhttpdelivery "source.toby3d.me/toby3d/home/internal/webfinger/delivery/http" webfingerucase "source.toby3d.me/toby3d/home/internal/webfinger/usecase" ) type App struct { server *http.Server } func NewApp(logger *log.Logger, config *domain.Config) (*App, error) { themeDir := os.DirFS(config.ThemeDir) partialsDir, err := fs.Sub(themeDir, "partials") if err != nil { return nil, fmt.Errorf("cannot substitute into partials subdirectory: %w", err) } contentDir := os.DirFS(config.ContentDir) resources := resourcefsrepo.NewFileServerResourceRepository(contentDir) sites := sitefsrepo.NewFileSystemSiteRepository(contentDir) siter := siteucase.NewSiteUseCase(sites, resources) staticDir := os.DirFS(config.StaticDir) statics := staticfsrepo.NewFileServerStaticRepository(staticDir) staticer := staticucase.NewStaticUseCase(statics) resourcer := resourceucase.NewResourceUseCase(resources) themes := themefsrepo.NewFileSystemThemeRepository(themeDir) themer := themeucase.NewThemeUseCase(partialsDir, themes) entries := pagefsrepo.NewFileSystemPageRepository(contentDir) entrier := pageucase.NewEntryUseCase(entries, resources) serverer := servercase.NewServerUseCase(sites) webfingerer := webfingerucase.NewWebFingerUseCase(sites) webfingerHandler := webfingerhttpdelivery.NewHandler(webfingerer) themeHandler := themehttpdelivery.NewHandler(themer) resourceHandler := resourcehttpdelivery.NewHandler(resourcer, contentDir) staticHandler := statichttpdelivery.NewHandler(staticer) siteHandler := sitehttpdelivery.NewHandler(siter) handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // NOTE(toby3d): any file in $HOME_STATIC_DIR is public and // unprotected by design, so it's safe to search it first before // deep down to any page or it's resource which might be // protected by middleware or something else. if handler, err := staticHandler.Handle(r.Context(), r.URL.Path); err == nil { handler.ServeHTTP(w, r) return } head, tail := urlutil.ShiftPath(r.URL.Path) switch strings.ToLower(head) { case ".well-known": switch strings.ToLower(tail) { case "/webfinger": webfingerHandler.ServeHTTP(w, r) return } } // NOTE(toby3d): read $HOME_CONTENT_DIR/index.md as a source of // truth and global settings for any child entry. site, handler, err := siteHandler.Handle(r.Context(), head) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } if handler != nil { handler.ServeHTTP(w, r) return } if site.IsMultiLingual() { r.URL.Path = tail } if handler, err = resourceHandler.Handle(r.Context(), r.URL.Path); err == nil { handler.ServeHTTP(w, r) return } entry, err := entrier.Do(r.Context(), site.Language, r.URL.Path) if err != nil { http.NotFound(w, r) return } themeHandler.Handle(site, entry).ServeHTTP(w, r) }) chain := middleware.Chain{ // middleware.LogFmt(), middleware.Redirect(middleware.RedirectConfig{Serverer: serverer}), middleware.Header(middleware.HeaderConfig{Serverer: serverer}), } return &App{server: &http.Server{ Addr: config.AddrPort().String(), Handler: chain.Handler(handler), ErrorLog: logger, WriteTimeout: 500 * time.Millisecond, }}, nil } func (a *App) Run(ln net.Listener) error { if err := a.server.Serve(ln); 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 }