ensure reasonable ordering of declarations in source
20 files changed, 488 insertions(+), 482 deletions(-)
changed files
- .golangci.yaml
- domain/analytics/goatcounter/count.go
- domain/calendar/calendar.go
- domain/content/builder/build/main.go
- domain/content/builder/builder.go
- domain/content/fetcher/fetcher.go
- domain/content/posts.go
- domain/content/templates/list.go
- domain/identity/webfinger/service.go
- domain/indieweb/atom/atom.go
- domain/web/server/server.go
- domain/web/templates/layout.go
- shared/config/config.go
- shared/events/file.go
- shared/events/redis.go
- shared/http/error.go
- shared/storage/files/reader.go
- shared/storage/files/writer.go
- shared/storage/sqlite/writer.go
- shared/vcs/filelog.go
M .golangci.yaml → .golangci.yaml
@@ -6,6 +6,8 @@ linters: disable: - errcheck enable: + - decorder + - funcorder - gocritic - gosec - govet@@ -15,6 +17,12 @@ - nlreturn - noctx - paralleltest settings: + decorder: + ignore-underscore-vars: true + disable-dec-order-check: false + disable-init-func-first-check: false + disable-dec-num-check: false + disable-type-dec-num-check: true staticcheck: dot-import-whitelist: - "alin.ovh/gomponents/html"
M domain/analytics/goatcounter/count.go → domain/analytics/goatcounter/count.go
@@ -14,8 +14,6 @@ "github.com/Southclaws/fault" "github.com/Southclaws/fault/fmsg" ) -const timeout = 5 * time.Second - type Options struct { URL *config.URL Logger *log.Logger@@ -46,6 +44,8 @@ type countBody struct { NoSessions bool `json:"no_sessions"` Hits []hit `json:"hits"` } + +const timeout = 5 * time.Second func New(options *Options) *Goatcounter { baseURL := options.URL
M domain/calendar/calendar.go → domain/calendar/calendar.go
@@ -20,11 +20,6 @@ "alin.ovh/homestead/shared/cache" "alin.ovh/homestead/shared/config" ) -const ( - Filename = "calendar.ics" - Refresh = 30 * time.Minute -) - type Options struct { URL config.URL Timezone config.Timezone@@ -54,6 +49,11 @@ type CalendarDate struct { Date BusyPeriods []*Busy } + +const ( + Filename = "calendar.ics" + Refresh = 30 * time.Minute +) func New(opts *Options, logger *log.Logger) *Calendar { if opts.URL.Scheme == "webcal" {@@ -110,51 +110,6 @@ return err } -func (c *Calendar) open() (*os.File, error) { - f, err := cache.Root.Open(Filename) - if err != nil { - return nil, fault.Wrap(err, fmsg.With("could not open calendar file")) - } - - return f, nil -} - -func (c *Calendar) fetch(ctx context.Context) error { - c.log.Debug("fetching calendar", "url", c.opts.URL.String()) - - f, err := cache.Root.OpenFile(Filename, os.O_RDWR|os.O_CREATE, 0o600) - if err != nil { - return fault.Wrap(err, fmsg.With("could not create temp file")) - } - defer f.Close() - - req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.opts.URL.String(), nil) - if err != nil { - return fault.Wrap(err, fmsg.With("could not create request")) - } - - res, err := c.client.Do(req) - if err != nil { - return fault.Wrap(err, fmsg.With("could not fetch calendar")) - } - defer res.Body.Close() - - if res.StatusCode != http.StatusOK { - return fault.New(fmt.Sprintf("unexpected status code %d", res.StatusCode)) - } - - if _, err := io.Copy(f, res.Body); err != nil { - return fault.Wrap(err, fmsg.With("could not write calendar to file")) - } - - err = f.Sync() - if err != nil { - return fault.Wrap(err, fmsg.With("could not sync file")) - } - - return nil -} - func (c *Calendar) EventsBetween(from time.Time, to time.Time) ([]*Busy, error) { if c.Calendar == nil { return nil, fault.New("calendar not initialised")@@ -244,6 +199,51 @@ cds = append(cds, cd) } return cds, nil +} + +func (c *Calendar) open() (*os.File, error) { + f, err := cache.Root.Open(Filename) + if err != nil { + return nil, fault.Wrap(err, fmsg.With("could not open calendar file")) + } + + return f, nil +} + +func (c *Calendar) fetch(ctx context.Context) error { + c.log.Debug("fetching calendar", "url", c.opts.URL.String()) + + f, err := cache.Root.OpenFile(Filename, os.O_RDWR|os.O_CREATE, 0o600) + if err != nil { + return fault.Wrap(err, fmsg.With("could not create temp file")) + } + defer f.Close() + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.opts.URL.String(), nil) + if err != nil { + return fault.Wrap(err, fmsg.With("could not create request")) + } + + res, err := c.client.Do(req) + if err != nil { + return fault.Wrap(err, fmsg.With("could not fetch calendar")) + } + defer res.Body.Close() + + if res.StatusCode != http.StatusOK { + return fault.New(fmt.Sprintf("unexpected status code %d", res.StatusCode)) + } + + if _, err := io.Copy(f, res.Body); err != nil { + return fault.Wrap(err, fmsg.With("could not write calendar to file")) + } + + err = f.Sync() + if err != nil { + return fault.Wrap(err, fmsg.With("could not sync file")) + } + + return nil } func (d Date) Between(lower, upper Date) bool {
M domain/content/builder/build/main.go → domain/content/builder/build/main.go
@@ -19,14 +19,14 @@ "github.com/ardanlabs/conf/v3" ) -const branch = "main" - type Options struct { *builder.Options Destination string `conf:"default:./public,short:d,flag:dest"` Compress bool `conf:"default:true"` Writer string `conf:"default:files,help:Output type (files|sqlite)"` } + +const branch = "main" func main() { options := &Options{}
M domain/content/builder/builder.go → domain/content/builder/builder.go
@@ -26,12 +26,6 @@ "github.com/Southclaws/fault/fmsg" mapset "github.com/deckarep/golang-set/v2" ) -var feedHeaders = map[string]string{ - "Access-Control-Allow-Origin": "*", - "Access-Control-Allow-Methods": "GET, OPTIONS", - "Access-Control-Max-Age": "3600", -} - type Options struct { Source string `conf:"default:.,short:s,flag:src"` Development bool `conf:"default:false,flag:dev"`@@ -39,6 +33,12 @@ VCSRemoteURL config.URL `conf:"default:https://git.alin.ovh/website"` Storage storage.Writer `conf:"-"` Repo *vcs.Repository `conf:"-"` +} + +var feedHeaders = map[string]string{ + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "GET, OPTIONS", + "Access-Control-Max-Age": "3600", } func joinSourcePath(src string) func(string) string {
M domain/content/fetcher/fetcher.go → domain/content/fetcher/fetcher.go
@@ -23,12 +23,6 @@ "github.com/Southclaws/fault/fmsg" "github.com/google/renameio/v2" ) -var ( - files = []string{"config.toml", "site.db"} - numericFilename = regexp.MustCompile("[0-9]{3,}") - timeout = 10 * time.Second -) - type Fetcher struct { options *Options log *log.Logger@@ -43,6 +37,12 @@ FetchURL config.URL Listener events.Listener } +var ( + files = []string{"config.toml", "site.db"} + numericFilename = regexp.MustCompile("[0-9]{3,}") + timeout = 10 * time.Second +) + func New(log *log.Logger, options *Options) Fetcher { return Fetcher{ log: log,@@ -51,6 +51,87 @@ updater: options.Listener, } } +func (f *Fetcher) CleanOldRevisions() error { + contents, err := os.ReadDir(f.options.Root) + if err != nil { + return fault.Wrap(err, fmsg.With("could not read root directory")) + } + for _, file := range contents { + name := file.Name() + if name == "current" { + continue + } + if numericFilename.MatchString(name) { + v, err := strconv.ParseUint(name, 10, 64) + if err != nil { + return fault.Wrap( + err, + fmsg.With(fmt.Sprintf("could not parse numeric filename %s", name)), + ) + } + if v < f.current-1 { + err := os.RemoveAll(filepath.Join(f.options.Root, name)) + if err != nil { + return fault.Wrap(err, fmsg.With("could not remove folder")) + } + } + } + } + + return nil +} + +func (f *Fetcher) Subscribe() (<-chan string, error) { + err := f.checkFolder() + if err != nil { + return nil, err + } + + var root string + f.current, err = f.getCurrentVersion() + if err != nil { + f.log.Warn("could not get current version", "error", err) + } + + if !f.options.RedisEnabled { + root = f.path(f.current) + } else { + runID, err := f.initialiseStorage() + if err != nil { + return nil, err + } + root = f.path(runID) + } + + ch := make(chan string, 1) + go func() { + var err error + var attempt uint + for { + err = f.connect(root, ch) + if err == nil { + return + } + + next := expBackoff(attempt) + attempt++ + f.log.Warn( + "could not connect to update listener", + "error", + err, + "attempt", + attempt, + "next_try", + next, + ) + + <-time.After(next) + } + }() + + return ch, nil +} + func (f *Fetcher) getArtefacts(run uint64) error { runID := strconv.FormatUint(run, 10) f.log.Debug("getting artefacts", "run_id", runID)@@ -93,36 +174,6 @@ if len(badFiles) > 0 { return fault.Wrap( fault.Newf("unexpected files in root directory: %s", strings.Join(badFiles, ", ")), ) - } - - return nil -} - -func (f *Fetcher) CleanOldRevisions() error { - contents, err := os.ReadDir(f.options.Root) - if err != nil { - return fault.Wrap(err, fmsg.With("could not read root directory")) - } - for _, file := range contents { - name := file.Name() - if name == "current" { - continue - } - if numericFilename.MatchString(name) { - v, err := strconv.ParseUint(name, 10, 64) - if err != nil { - return fault.Wrap( - err, - fmsg.With(fmt.Sprintf("could not parse numeric filename %s", name)), - ) - } - if v < f.current-1 { - err := os.RemoveAll(filepath.Join(f.options.Root, name)) - if err != nil { - return fault.Wrap(err, fmsg.With("could not remove folder")) - } - } - } } return nil@@ -208,57 +259,6 @@ return latest, nil } return f.current, nil -} - -func (f *Fetcher) Subscribe() (<-chan string, error) { - err := f.checkFolder() - if err != nil { - return nil, err - } - - var root string - f.current, err = f.getCurrentVersion() - if err != nil { - f.log.Warn("could not get current version", "error", err) - } - - if !f.options.RedisEnabled { - root = f.path(f.current) - } else { - runID, err := f.initialiseStorage() - if err != nil { - return nil, err - } - root = f.path(runID) - } - - ch := make(chan string, 1) - go func() { - var err error - var attempt uint - for { - err = f.connect(root, ch) - if err == nil { - return - } - - next := expBackoff(attempt) - attempt++ - f.log.Warn( - "could not connect to update listener", - "error", - err, - "attempt", - attempt, - "next_try", - next, - ) - - <-time.After(next) - } - }() - - return ch, nil } func (f *Fetcher) connect(root string, ch chan string) error {
M domain/content/posts.go → domain/content/posts.go
@@ -22,11 +22,6 @@ "github.com/adrg/frontmatter" mapset "github.com/deckarep/golang-set/v2" ) -var SkipList = []string{ - "LICENSE", - "taplo.toml", -} - type PostMatter struct { Date time.Time Description string@@ -62,72 +57,21 @@ StaticFiles []string Tags mapset.Set[string] } -var postURLReplacer = strings.NewReplacer( - "index.md", "", - ".md", "/", -) - -func (cc *Collection) GetPost(filename string) (*Post, error) { - fp := filepath.Join(cc.config.Root, filename) - url := path.Join("/", postURLReplacer.Replace(filename)) + "/" - cs, err := cc.config.Repo.GetFileLog(filename) - if err != nil { - return nil, fault.Wrap( - err, - fmsg.With(fmt.Sprintf("could not get commit log for file %s", filename)), - ) +var ( + postURLReplacer = strings.NewReplacer( + "index.md", "", + ".md", "/", + ) + pageURLReplacer = strings.NewReplacer( + "index.md", "", + ".md", "", + ) + SkipList = []string{ + "LICENSE", + "taplo.toml", } - post := &Post{ - Input: filename, - Basename: filepath.Base(url), - URL: url, - PostMatter: &PostMatter{}, - Commits: cs, - } - - err = parse(fp, post) - if err != nil { - return nil, err - } - - return post, nil -} - -var pageURLReplacer = strings.NewReplacer( - "index.md", "", - ".md", "", ) -func (cc *Collection) GetPage(filename string) (*Post, error) { - fp := filepath.Join(cc.config.Root, filename) - url := path.Join("/", pageURLReplacer.Replace(filename)) - cs, err := cc.config.Repo.GetFileLog(filename) - if err != nil { - return nil, fault.Wrap( - err, - fmsg.With(fmt.Sprintf("could not get commit log for file %s", filename)), - ) - } - post := &Post{ - Input: filename, - Basename: filepath.Base(url), - URL: url, - PostMatter: &PostMatter{}, - Commits: cs, - } - - err = parse(fp, post) - if err != nil { - return nil, err - } - - if post.Date.IsZero() && len(cs) > 1 { - post.Date = cs[0].Date - } - - return post, nil -} - func parse(fp string, post *Post) error { content, err := os.Open(fp) if err != nil {@@ -158,6 +102,40 @@ return buf.String(), nil } +func NewContentCollection(config *Config, log *log.Logger) (*Collection, error) { + cc := &Collection{ + Posts: []*Post{}, + Tags: mapset.NewSet[string](), + Pages: []*Post{}, + StaticFiles: []string{}, + config: config, + log: log, + } + + err := filepath.WalkDir(config.Root, func(filename string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + filename, err = filepath.Rel(config.Root, filename) + if err != nil { + return err + } + + log.Debug("walking", "filename", filename) + + return cc.HandleFile(filename, d) + }) + + slices.SortFunc(cc.Posts, func(a, b *Post) int { + return b.Date.Compare(a.Date) + }) + + if err != nil { + return nil, fault.Wrap(err, fmsg.With("could not walk directory")) + } + + return cc, nil +} func (cc *Collection) HandleFile(filename string, d fs.DirEntry) error { switch { case strings.HasPrefix(filename, ".") &&@@ -195,37 +173,58 @@ return nil } -func NewContentCollection(config *Config, log *log.Logger) (*Collection, error) { - cc := &Collection{ - Posts: []*Post{}, - Tags: mapset.NewSet[string](), - Pages: []*Post{}, - StaticFiles: []string{}, - config: config, - log: log, +func (cc *Collection) GetPost(filename string) (*Post, error) { + fp := filepath.Join(cc.config.Root, filename) + url := path.Join("/", postURLReplacer.Replace(filename)) + "/" + cs, err := cc.config.Repo.GetFileLog(filename) + if err != nil { + return nil, fault.Wrap( + err, + fmsg.With(fmt.Sprintf("could not get commit log for file %s", filename)), + ) } - - err := filepath.WalkDir(config.Root, func(filename string, d fs.DirEntry, err error) error { - if err != nil { - return err - } - filename, err = filepath.Rel(config.Root, filename) - if err != nil { - return err - } + post := &Post{ + Input: filename, + Basename: filepath.Base(url), + URL: url, + PostMatter: &PostMatter{}, + Commits: cs, + } - log.Debug("walking", "filename", filename) + err = parse(fp, post) + if err != nil { + return nil, err + } - return cc.HandleFile(filename, d) - }) + return post, nil +} - slices.SortFunc(cc.Posts, func(a, b *Post) int { - return b.Date.Compare(a.Date) - }) +func (cc *Collection) GetPage(filename string) (*Post, error) { + fp := filepath.Join(cc.config.Root, filename) + url := path.Join("/", pageURLReplacer.Replace(filename)) + cs, err := cc.config.Repo.GetFileLog(filename) + if err != nil { + return nil, fault.Wrap( + err, + fmsg.With(fmt.Sprintf("could not get commit log for file %s", filename)), + ) + } + post := &Post{ + Input: filename, + Basename: filepath.Base(url), + URL: url, + PostMatter: &PostMatter{}, + Commits: cs, + } + err = parse(fp, post) if err != nil { - return nil, fault.Wrap(err, fmsg.With("could not walk directory")) + return nil, err + } + + if post.Date.IsZero() && len(cs) > 1 { + post.Date = cs[0].Date } - return cc, nil + return post, nil }
M domain/content/templates/list.go → domain/content/templates/list.go
@@ -13,6 +13,10 @@ Tag string Posts []*content.Post } +type ListPageVars struct { + Posts []*content.Post +} + func TagPage(site templates.SiteSettings, vars TagPageVars) g.Node { return templates.Layout(site, templates.PageSettings{ Title: vars.Tag,@@ -27,10 +31,6 @@ ), ), list(vars.Posts), )) -} - -type ListPageVars struct { - Posts []*content.Post } func ListPage(site templates.SiteSettings, vars ListPageVars) g.Node {
M domain/identity/webfinger/service.go → domain/identity/webfinger/service.go
@@ -21,6 +21,8 @@ providers []ResourceProvider corsOrigin string } +type Option func(*Service) + var ( ErrMissingResourceParameter = ihttp.NewError( "Missing resource parameter",@@ -32,8 +34,6 @@ http.StatusInternalServerError, ) ErrNotFound = ihttp.NewError("Resource not found", http.StatusNotFound) ) - -type Option func(*Service) func WithCORSOrigin(origin string) Option { return func(s *Service) {
M domain/indieweb/atom/atom.go → domain/indieweb/atom/atom.go
@@ -10,40 +10,11 @@ "alin.ovh/homestead/shared/config" "github.com/Southclaws/fault" ) -func MakeTagURI(config *config.Config, specific string) string { - return "tag:" + config.OriginalDomain + "," + config.DomainStartDate + ":" + specific -} - -func LinkXSL(w *bytes.Buffer, url string) error { - _, err := w.WriteString(`<?xml-stylesheet href="`) - if err != nil { - return fault.Wrap(err) - } - err = xml.EscapeText(w, []byte(url)) - if err != nil { - return fault.Wrap(err) - } - _, err = w.WriteString(`" type="text/xsl"?>`) - if err != nil { - return fault.Wrap(err) - } - - return nil -} - type Link struct { XMLName xml.Name `xml:"link"` Rel string `xml:"rel,attr,omitempty"` Type string `xml:"type,attr,omitempty"` Href string `xml:"href,attr"` -} - -func MakeLink(url *url.URL) Link { - return Link{ - Rel: "alternate", - Type: "text/html", - Href: url.String(), - } } type FeedContent struct {@@ -70,3 +41,32 @@ ID string `xml:"id"` Updated time.Time `xml:"updated"` Entries []*FeedEntry `xml:"entry"` } + +func MakeTagURI(config *config.Config, specific string) string { + return "tag:" + config.OriginalDomain + "," + config.DomainStartDate + ":" + specific +} + +func LinkXSL(w *bytes.Buffer, url string) error { + _, err := w.WriteString(`<?xml-stylesheet href="`) + if err != nil { + return fault.Wrap(err) + } + err = xml.EscapeText(w, []byte(url)) + if err != nil { + return fault.Wrap(err) + } + _, err = w.WriteString(`" type="text/xsl"?>`) + if err != nil { + return fault.Wrap(err) + } + + return nil +} + +func MakeLink(url *url.URL) Link { + return Link{ + Rel: "alternate", + Type: "text/html", + Href: url.String(), + } +}
M domain/web/server/server.go → domain/web/server/server.go
@@ -15,17 +15,6 @@ "github.com/Southclaws/fault" "github.com/Southclaws/fault/fmsg" ) -var ( - CommitSHA = "local" - ShortSHA = "local" - serverHeader = fmt.Sprintf("homestead (%s)", ShortSHA) - - ReadHeaderTimeout = 10 * time.Second - ReadTimeout = 1 * time.Minute - WriteTimeout = 2 * time.Minute - IdleTimeout = 10 * time.Minute -) - type Options struct { Redirect bool `conf:"default:true"` ListenAddress string `conf:"default:::"`@@ -45,6 +34,17 @@ options *Options log *log.Logger server *http.Server } + +var ( + CommitSHA = "local" + ShortSHA = "local" + serverHeader = fmt.Sprintf("homestead (%s)", ShortSHA) + + ReadHeaderTimeout = 10 * time.Second + ReadTimeout = 1 * time.Minute + WriteTimeout = 2 * time.Minute + IdleTimeout = 10 * time.Minute +) func serverHeaderHandler(wrappedHandler http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
M domain/web/templates/layout.go → domain/web/templates/layout.go
@@ -28,6 +28,13 @@ BodyAttrs Attrs HeadExtra []g.Node } +var LiveReload = Script(Defer(), g.Raw(` + new EventSource("/_/reload").onmessage = event => { + console.log("got message", event) + window.location.reload() + }; +`)) + func ExtendAttrs(base Attrs, attrs Attrs) g.Node { m := base for key, value := range attrs {@@ -99,13 +106,6 @@ ), ), ))) } - -var LiveReload = Script(Defer(), g.Raw(` - new EventSource("/_/reload").onmessage = event => { - console.log("got message", event) - window.location.reload() - }; -`)) func MenuLink(item config.MenuItem) g.Node { return A(