all repos — homestead @ 20dd619b8605660dac67fa435d711e5f22da3174

Code for my website

ensure reasonable ordering of declarations in source

Alan Pearce
commit

20dd619b8605660dac67fa435d711e5f22da3174

parent

6f41d182abaf1af16fd0cc697abf0f2e9ce54c5c

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.godomain/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.godomain/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.godomain/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.godomain/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.godomain/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.godomain/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.godomain/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.godomain/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.godomain/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.godomain/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.godomain/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(
M shared/config/config.goshared/config/config.go
@@ -28,29 +28,8 @@ type URL struct {
*url.URL } -func NewURL(rawURL string) URL { - u, err := url.Parse(rawURL) - if err != nil { - panic(err) - } - - return URL{u} -} - -func (u *URL) UnmarshalText(text []byte) (err error) { - u.URL, err = url.Parse(string(text)) - - return fault.Wrap(err, fmsg.With(fmt.Sprintf("could not parse URL %s", string(text)))) -} - type Timezone struct { *time.Location -} - -func (t *Timezone) UnmarshalText(text []byte) (err error) { - t.Location, err = time.LoadLocation(string(text)) - - return fault.Wrap(err, fmsg.With(fmt.Sprintf("could not parse timezone %s", string(text)))) } type GoPackagesConfig struct {
@@ -84,6 +63,27 @@ Menu []MenuItem
RelMe []MenuItem `toml:"rel_me"` Go GoPackagesConfig +} + +func NewURL(rawURL string) URL { + u, err := url.Parse(rawURL) + if err != nil { + panic(err) + } + + return URL{u} +} + +func (u *URL) UnmarshalText(text []byte) (err error) { + u.URL, err = url.Parse(string(text)) + + return fault.Wrap(err, fmsg.With(fmt.Sprintf("could not parse URL %s", string(text)))) +} + +func (t *Timezone) UnmarshalText(text []byte) (err error) { + t.Location, err = time.LoadLocation(string(text)) + + return fault.Wrap(err, fmsg.With(fmt.Sprintf("could not parse timezone %s", string(text)))) } func GetConfig(dir string, log *log.Logger) (*Config, error) {
M shared/events/file.goshared/events/file.go
@@ -17,6 +17,11 @@ "github.com/Southclaws/fault/fmsg"
"github.com/fsnotify/fsnotify" ) +type FileWatcher struct { + log *log.Logger + *fsnotify.Watcher +} + var ( ignores = []string{ "*.go",
@@ -24,11 +29,6 @@ "*-journal",
} checkSettleInterval = 200 * time.Millisecond ) - -type FileWatcher struct { - log *log.Logger - *fsnotify.Watcher -} func NewFileWatcher(logger *log.Logger, dirs ...string) (*FileWatcher, error) { fsn, err := fsnotify.NewWatcher()
@@ -52,21 +52,6 @@ }
} return fw, nil -} - -func (fw *FileWatcher) matches(name string) func(string) bool { - return func(pattern string) bool { - matched, err := path.Match(pattern, name) - if err != nil { - fw.log.Warn("error checking watcher ignores", "error", err) - } - - return matched - } -} - -func (fw *FileWatcher) ignored(pathname string) bool { - return slices.ContainsFunc(ignores, fw.matches(path.Base(pathname))) } func (fw *FileWatcher) AddRecursive(from string) error {
@@ -156,3 +141,18 @@ }()
return events, nil } + +func (fw *FileWatcher) matches(name string) func(string) bool { + return func(pattern string) bool { + matched, err := path.Match(pattern, name) + if err != nil { + fw.log.Warn("error checking watcher ignores", "error", err) + } + + return matched + } +} + +func (fw *FileWatcher) ignored(pathname string) bool { + return slices.ContainsFunc(ignores, fw.matches(path.Base(pathname))) +}
M shared/events/redis.goshared/events/redis.go
@@ -14,12 +14,6 @@ "github.com/redis/go-redis/v9"
"go.uber.org/zap" ) -const ( - db = 0 - key = "run_id" - fallbackRunID uint64 = 210 -) - type RedisOptions struct { Address string Username string `conf:"default:default"`
@@ -29,15 +23,21 @@ }
type Logger struct { log *zap.SugaredLogger -} - -func (l *Logger) Printf(_ context.Context, format string, v ...any) { - l.log.Infof(format, v...) } type RedisListener struct { client *redis.Client log *log.Logger +} + +const ( + db = 0 + key = "run_id" + fallbackRunID uint64 = 210 +) + +func (l *Logger) Printf(_ context.Context, format string, v ...any) { + l.log.Infof(format, v...) } func NewRedisListener(opts *RedisOptions, log *log.Logger) (*RedisListener, error) {
M shared/http/error.goshared/http/error.go
@@ -18,6 +18,13 @@ message string
cause error } +func NewError(message string, code int) httpError { + return httpError{ + code: code, + message: message, + } +} + func (e httpError) Error() string { if e.message == "" { e.message = http.StatusText(e.code)
@@ -40,13 +47,6 @@ }
func (e httpError) Unwrap() error { return e.cause -} - -func NewError(message string, code int) httpError { - return httpError{ - code: code, - message: message, - } } func (e httpError) WithCause(cause error) Error {
M shared/storage/files/reader.goshared/storage/files/reader.go
@@ -32,6 +32,30 @@
return r, nil } +func (r *Reader) GetFile(urlPath string) (*storage.File, error) { + return r.files[urlPath], nil +} + +func (r *Reader) CanonicalisePath(path string) (cPath string, differs bool) { + cPath = path + switch { + case strings.HasSuffix(path, "/index.html"): + cPath, differs = strings.CutSuffix(path, "index.html") + + case strings.HasSuffix(path, ".html"): + cPath, differs = strings.CutSuffix(path, ".html") + + case !strings.HasSuffix(path, "/") && r.files[path+"/"] != nil: + cPath, differs = path+"/", true + + case strings.HasSuffix(path, "/"): + if cPath, differs := strings.CutSuffix(path, "/"); differs && r.files[cPath] != nil { + return cPath, differs + } + } + + return cPath, differs +} func (r *Reader) registerFile(urlpath string, filepath string) error { file, err := r.OpenFile(urlpath, filepath) if err != nil {
@@ -76,28 +100,3 @@ }
return nil } - -func (r *Reader) GetFile(urlPath string) (*storage.File, error) { - return r.files[urlPath], nil -} - -func (r *Reader) CanonicalisePath(path string) (cPath string, differs bool) { - cPath = path - switch { - case strings.HasSuffix(path, "/index.html"): - cPath, differs = strings.CutSuffix(path, "index.html") - - case strings.HasSuffix(path, ".html"): - cPath, differs = strings.CutSuffix(path, ".html") - - case !strings.HasSuffix(path, "/") && r.files[path+"/"] != nil: - cPath, differs = path+"/", true - - case strings.HasSuffix(path, "/"): - if cPath, differs := strings.CutSuffix(path, "/"); differs && r.files[cPath] != nil { - return cPath, differs - } - } - - return cPath, differs -}
M shared/storage/files/writer.goshared/storage/files/writer.go
@@ -18,11 +18,6 @@ "github.com/andybalholm/brotli"
"github.com/klauspost/compress/zstd" ) -const ( - gzipLevel = 6 - brotliLevel = 9 -) - type Files struct { outputDirectory string options *Options
@@ -32,6 +27,11 @@
type Options struct { Compress bool } + +const ( + gzipLevel = 6 + brotliLevel = 9 +) func NewWriter(outputDirectory string, logger *log.Logger, opts *Options) (*Files, error) { return &Files{
@@ -101,6 +101,23 @@ func (f *Files) WriteFile(file *storage.File, content *buffer.Buffer) error {
return f.Write(file.Path, file.Title, content) } +func (f *Files) OpenFileAndVariants(filename string) (file.FileLike, error) { + if f.options.Compress { + return multiOpenFile(f.join(filename)) + } + + return openFileWrite(f.join(filename)) +} + +func (f *Files) Mkdirp(dir string) error { + err := os.MkdirAll(f.join(dir), 0o750) + if err != nil { + return fault.Wrap(err, fmsg.With("could not create directory")) + } + + return nil +} + func (f *Files) write(pathname string, content *buffer.Buffer) (file.FileLike, error) { filename := pathNameToFileName(pathname) err := f.Mkdirp(filepath.Dir(filename))
@@ -190,23 +207,6 @@ return nil, err
} return file.NewMultiFile(r, gz, br, zst), nil -} - -func (f *Files) OpenFileAndVariants(filename string) (file.FileLike, error) { - if f.options.Compress { - return multiOpenFile(f.join(filename)) - } - - return openFileWrite(f.join(filename)) -} - -func (f *Files) Mkdirp(dir string) error { - err := os.MkdirAll(f.join(dir), 0o750) - if err != nil { - return fault.Wrap(err, fmsg.With("could not create directory")) - } - - return nil } func (f *Files) join(filename string) string {
M shared/storage/sqlite/writer.goshared/storage/sqlite/writer.go
@@ -27,11 +27,6 @@ "github.com/Southclaws/fault/fmsg"
_ "modernc.org/sqlite" // import registers db/SQL driver ) -var encodings = []string{"gzip", "br", "zstd"} - -//go:embed schema.sql -var schema string - type Writer struct { options *Options log *log.Logger
@@ -41,6 +36,12 @@
type Options struct { Compress bool } + +var ( + encodings = []string{"gzip", "br", "zstd"} + //go:embed schema.sql + schema string +) func OpenDB(dbPath string) (*sql.DB, error) { db, err := sql.Open(
@@ -80,81 +81,6 @@ func (s *Writer) Mkdirp(string) error {
return nil } -func (s *Writer) storeURL(path string) (int64, error) { - id, err := s.queries.InsertURL(context.TODO(), path) - if err != nil { - return 0, fault.Wrap(err, fmsg.With(fmt.Sprintf("inserting URL %s into database", path))) - } - - return id, nil -} - -func (s *Writer) storeFile(urlID int64, file *storage.File) (int64, error) { - if file.ContentType == "" { - file.ContentType = http.DetectContentType(file.Encodings["identity"].Bytes()) - s.log.Warn( - "file has no content type, sniffing", - "path", - file.Path, - "sniffed", - file.ContentType, - ) - } - params := db.InsertFileParams{ - UrlID: urlID, - ContentType: file.ContentType, - LastModified: file.LastModified.Unix(), - Etag: file.Etag, - Title: file.Title, - Headers: []byte{}, - } - if file.Headers != nil { - var err error - params.Headers, err = json.Marshal(file.Headers) - if err != nil { - return 0, fault.Wrap(err, fmsg.With("marshalling headers to JSON")) - } - } - id, err := s.queries.InsertFile(context.TODO(), params) - if err != nil { - return 0, fault.Wrap(err, fmsg.With("inserting file into database")) - } - - return id, nil -} - -func (s *Writer) storeEncoding(fileID int64, encoding string, data []byte) error { - err := s.queries.InsertContent(context.TODO(), db.InsertContentParams{ - Fileid: fileID, - Encoding: encoding, - Body: data, - }) - if err != nil { - return fault.Wrap( - err, - fmsg.With(fmt.Sprintf("inserting encoding into database file_id: %d encoding: %s", - fileID, - encoding)), - ) - } - - return nil -} - -func etag(content []byte) (string, error) { - hash := fnv.New64a() - _, err := hash.Write(content) - if err != nil { - return "", fault.Wrap(err) - } - - return fmt.Sprintf(`W/"%x"`, hash.Sum(nil)), nil -} - -func contentType(pathname string) string { - return mime.TypeByExtension(filepath.Ext(pathNameToFileName(pathname))) -} - func (s *Writer) NewFileFromPost(post *content.Post) *storage.File { file := &storage.File{ Title: post.Title,
@@ -272,3 +198,77 @@ }
return compressed, nil } +func (s *Writer) storeURL(path string) (int64, error) { + id, err := s.queries.InsertURL(context.TODO(), path) + if err != nil { + return 0, fault.Wrap(err, fmsg.With(fmt.Sprintf("inserting URL %s into database", path))) + } + + return id, nil +} + +func (s *Writer) storeFile(urlID int64, file *storage.File) (int64, error) { + if file.ContentType == "" { + file.ContentType = http.DetectContentType(file.Encodings["identity"].Bytes()) + s.log.Warn( + "file has no content type, sniffing", + "path", + file.Path, + "sniffed", + file.ContentType, + ) + } + params := db.InsertFileParams{ + UrlID: urlID, + ContentType: file.ContentType, + LastModified: file.LastModified.Unix(), + Etag: file.Etag, + Title: file.Title, + Headers: []byte{}, + } + if file.Headers != nil { + var err error + params.Headers, err = json.Marshal(file.Headers) + if err != nil { + return 0, fault.Wrap(err, fmsg.With("marshalling headers to JSON")) + } + } + id, err := s.queries.InsertFile(context.TODO(), params) + if err != nil { + return 0, fault.Wrap(err, fmsg.With("inserting file into database")) + } + + return id, nil +} + +func (s *Writer) storeEncoding(fileID int64, encoding string, data []byte) error { + err := s.queries.InsertContent(context.TODO(), db.InsertContentParams{ + Fileid: fileID, + Encoding: encoding, + Body: data, + }) + if err != nil { + return fault.Wrap( + err, + fmsg.With(fmt.Sprintf("inserting encoding into database file_id: %d encoding: %s", + fileID, + encoding)), + ) + } + + return nil +} + +func etag(content []byte) (string, error) { + hash := fnv.New64a() + _, err := hash.Write(content) + if err != nil { + return "", fault.Wrap(err) + } + + return fmt.Sprintf(`W/"%x"`, hash.Sum(nil)), nil +} + +func contentType(pathname string) string { + return mime.TypeByExtension(filepath.Ext(pathNameToFileName(pathname))) +}
M shared/vcs/filelog.goshared/vcs/filelog.go
@@ -13,8 +13,6 @@ "github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing/object" ) -const hashLength = 7 - type Author struct { Name string Email string
@@ -31,6 +29,10 @@ Author Author
Date time.Time Link *url.URL } + +const hashLength = 7 + +var rewrap = regexp.MustCompile(`\r?\n(\w)`) func (r *Repository) makeCommitURL(hash string) *url.URL { return r.remoteURL.JoinPath("commit", hash)
@@ -80,8 +82,6 @@ }
return cs, nil } - -var rewrap = regexp.MustCompile(`\r?\n(\w)`) func cleanupDescription(description string) string { return strings.TrimSpace(rewrap.ReplaceAllString(description, " $1"))