all repos — homestead @ f9feb2339cf736d5df28d4e37307c90595d5ab98

Code for my website

split website/mux

Alan Pearce
commit

f9feb2339cf736d5df28d4e37307c90595d5ab98

parent

88ef9f538edea96b82a71e91b1a2953ccdddc476

1 file changed, 173 insertions(+), 0 deletions(-)

changed files
A internal/website/website.go
@@ -0,0 +1,173 @@
+package website + +import ( + "fmt" + "net/http" + "os" + "slices" + "strings" + + "gitlab.com/tozd/go/errors" + "go.alanpearce.eu/website/internal/builder" + "go.alanpearce.eu/website/internal/config" + ihttp "go.alanpearce.eu/website/internal/http" + "go.alanpearce.eu/website/internal/server" + "go.alanpearce.eu/website/internal/storage" + "go.alanpearce.eu/website/internal/storage/files" + "go.alanpearce.eu/website/internal/vcs" + "go.alanpearce.eu/website/internal/watcher" + "go.alanpearce.eu/website/templates" + "go.alanpearce.eu/x/log" + + "github.com/benpate/digit" +) + +type Website struct { + config *config.Config + log *log.Logger + reader storage.Reader + me digit.Resource + acctResource string + *server.App +} + +func New( + opts *Options, + log *log.Logger, +) (*Website, error) { + website := &Website{ + log: log, + App: &server.App{ + Shutdown: func() {}, + }, + } + builderOptions := &builder.Options{ + Source: opts.Source, + Development: opts.Development, + Destination: opts.Destination, + } + + repo, exists, err := vcs.CloneOrOpen(&vcs.Options{ + LocalPath: opts.Source, + RemoteURL: opts.VCS.RemoteURL, + Branch: opts.VCS.Branch, + }, log.Named("vcs")) + if err != nil { + return nil, errors.WithMessage(err, "could not open repository") + } + builderOptions.Repo = repo + + if exists && !opts.Development { + _, err := repo.Update() + if err != nil { + return nil, errors.WithMessage(err, "could not update repository") + } + } + + log.Debug("getting config from source", "source", opts.Source) + cfg, err := config.GetConfig(opts.Source, log) + if err != nil { + return nil, errors.WithMessage(err, "could not load configuration") + } + + mux := ihttp.NewServeMux() + mux.HandleError(func(err *ihttp.Error, w http.ResponseWriter, r *http.Request) { + if strings.Contains(r.Header.Get("Accept"), "text/html") { + w.WriteHeader(err.Code) + err := templates.Error(cfg, r.URL.Path, err).Render(r.Context(), w) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + } else { + http.Error(w, err.Message, err.Code) + } + }) + if opts.Development { + tmpdir, err := os.MkdirTemp("", "website") + if err != nil { + log.Fatal("could not create temporary directory", "error", err) + } + log.Info("using temporary directory", "dir", tmpdir) + website.App.Shutdown = func() { + os.RemoveAll(tmpdir) + } + builderOptions.Destination = tmpdir + + cfg.CSP.ScriptSrc = slices.Insert(cfg.CSP.ScriptSrc, 0, "'unsafe-inline'") + cfg.CSP.ConnectSrc = slices.Insert(cfg.CSP.ConnectSrc, 0, "'self'") + + cfg.BaseURL = opts.BaseURL + } + + website.Domain = cfg.BaseURL.Hostname() + + err = rebuild(builderOptions, cfg, log) + if err != nil { + return nil, errors.WithMessage(err, "could not build site") + } + + if opts.Development { + fw, err := watcher.New(log.Named("watcher")) + if err != nil { + return nil, errors.WithMessage(err, "could not create file watcher") + } + err = fw.AddRecursive(opts.Source) + if err != nil { + return nil, errors.WithMessage( + err, + "could not add directory to file watcher", + ) + } + err = fw.AddRecursive("templates") + if err != nil { + return nil, errors.WithMessage( + err, + "could not add templates directory to file watcher", + ) + } + + go fw.Start(func(filename string) { + log.Info("rebuilding site", "changed_file", filename) + err := rebuild(builderOptions, cfg, log) + if err != nil { + log.Error("error rebuilding site", "error", err) + } + opts.LiveReload.Reload() + }) + } + + website.reader, err = files.NewReader(builderOptions.Destination, log.Named("reader")) + if err != nil { + return nil, errors.WithMessage(err, "error creating sqlite reader") + } + + website.acctResource = "acct:" + cfg.Email + website.me = digit.NewResource(website.acctResource). + Link("http://openid.net/specs/connect/1.0/issuer", "", cfg.OIDCHost.String()) + + mux.Handle("/", website) + mux.HandleFunc("/.well-known/webfinger", website.webfinger) + const oidcPath = "/.well-known/openid-configuration" + mux.ServeMux.Handle(oidcPath, ihttp.RedirectHandler(cfg.OIDCHost.JoinPath(oidcPath), 302)) + + website.config = cfg + website.App.Handler = mux + + return website, nil +} + +func updateCSPHashes(config *config.Config, r *builder.Result) { + for i, h := range r.Hashes { + config.CSP.StyleSrc[i] = fmt.Sprintf("'%s'", h) + } +} + +func rebuild(builderConfig *builder.Options, config *config.Config, log *log.Logger) error { + r, err := builder.BuildSite(builderConfig, config, log.Named("builder")) + if err != nil { + return errors.WithMessage(err, "could not build site") + } + updateCSPHashes(config, r) + + return nil +}