package website import ( "net/http" "slices" "strings" "gitlab.com/tozd/go/errors" "go.alanpearce.eu/homestead/internal/builder" "go.alanpearce.eu/homestead/internal/config" "go.alanpearce.eu/homestead/internal/events" ihttp "go.alanpearce.eu/homestead/internal/http" "go.alanpearce.eu/homestead/internal/server" "go.alanpearce.eu/homestead/internal/storage" "go.alanpearce.eu/homestead/internal/storage/sqlite" "go.alanpearce.eu/homestead/internal/vcs" "go.alanpearce.eu/homestead/templates" "go.alanpearce.eu/x/log" "github.com/benpate/digit" "github.com/osdevisnot/sorvor/pkg/livereload" ) type Options struct { Source string `conf:"default:../website"` DBPath string `conf:"default:site.db"` Redirect bool `conf:"default:true"` Development bool `conf:"default:false,flag:dev"` BaseURL config.URL `conf:"default:localhost"` VCS struct { Branch string `conf:"default:main"` RemoteURL config.URL `conf:"default:https://git.alanpearce.eu/website"` } LiveReload *livereload.LiveReload `conf:"-"` } 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, } 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 { opts.DBPath = ":memory:" 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 } db, err := sqlite.OpenDB(opts.DBPath) if err != nil { return nil, errors.WithMessage(err, "could not open database") } builderOptions.Storage, err = sqlite.NewWriter(db, log.Named("storage"), &sqlite.Options{ Compress: true, }) if err != nil { return nil, errors.WithMessage(err, "could not create storage writer") } 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 := events.NewFileWatcher( log.Named("watcher"), opts.Source, "templates", ) if err != nil { return nil, errors.WithMessage(err, "could not create file listener") } go func(events chan events.Event, errs chan error) { err := fw.Wait(events, errs) if err != nil { log.Panic("could not start update listener", "error", err) } for event := range events { filename := event.FileEvents[0].Name log.Info("rebuilding site", "changed_file", filename) db.Close() db, err = sqlite.OpenDB(opts.DBPath) if err != nil { log.Error("error opening database", "error", err) } website.reader, err = sqlite.NewReader(db, log.Named("reader")) if err != nil { log.Error("error creating sqlite reader", "error", err) } builderOptions.Storage, err = sqlite.NewWriter( db, log.Named("storage"), &sqlite.Options{}, ) if err != nil { log.Error("error creating sqlite writer", "error", err) } err := rebuild(builderOptions, cfg, log) if err != nil { log.Error("error rebuilding site", "error", err) } opts.LiveReload.Reload() } }(make(chan events.Event, 1), make(chan error, 1)) } website.reader, err = sqlite.NewReader(db, 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 rebuild(builderConfig *builder.Options, config *config.Config, log *log.Logger) error { err := builder.BuildSite(builderConfig, config, log.Named("builder")) if err != nil { return errors.WithMessage(err, "could not build site") } return nil }