1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117 |
package main
import (
"context"
"fmt"
"os"
"sort"
"strings"
"unicode/utf8"
"git.pablomurad.com/pablo/patchwork/internal/config"
"git.pablomurad.com/pablo/patchwork/internal/doc"
"git.pablomurad.com/pablo/patchwork/internal/slug"
"git.pablomurad.com/pablo/patchwork/internal/source/folder"
"git.pablomurad.com/pablo/patchwork/internal/store"
)
// cmdDoctor checks everything it can without changing anything. It keeps
// going after a problem so one run shows them all.
func cmdDoctor(ctx context.Context, args []string) error {
fs := newFlags("doctor", "")
cfgPath := configFlag(fs)
if err := fs.Parse(args); err != nil {
return err
}
problems := 0
ok := func(format string, a ...any) { fmt.Fprintf(stdout, "ok "+format+"\n", a...) }
warn := func(format string, a ...any) { fmt.Fprintf(stdout, "warn "+format+"\n", a...) }
bad := func(format string, a ...any) { problems++; fmt.Fprintf(stdout, "FAIL "+format+"\n", a...) }
cfg, err := config.Load(*cfgPath)
if err != nil {
bad("config: %v", err)
return errProblems
}
ok("config %s", *cfgPath)
if cfg.Site.BaseURL == "" {
warn("site.base_url is empty; feeds need the public address of the site")
}
if cfg.Site.TagAuthority == "" {
warn("site.tag_authority is empty; feed entry ids need it, and it must never change once feeds are public")
}
if strings.HasPrefix(cfg.DB, `\\`) || strings.HasPrefix(cfg.DB, "//") {
warn("db %s is on a network share; SQLite locking is not safe there", cfg.DB)
}
st, err := store.Open(cfg.DB)
if err != nil {
bad("database: %v", err)
} else {
defer st.Close()
pages, err := st.Pages(ctx, "", false)
if err != nil {
bad("database: %v", err)
} else {
ok("database %s (%d active pages)", cfg.DB, len(pages))
}
}
m := folder.Matcher{Include: cfg.Source.Include, Exclude: cfg.Source.Exclude}
files, err := folder.List(cfg.Source.Path, m)
if err != nil {
bad("source: %v", err)
return errProblems
}
if len(files) == 0 {
warn("source %s has no files matching %v", cfg.Source.Path, cfg.Source.Include)
} else {
ok("source %s (%d files)", cfg.Source.Path, len(files))
}
rule, _ := slug.Lookup(cfg.Source.Slug)
opt := cfg.DocOptions()
bySlug := map[string][]string{}
for _, f := range files {
raw, err := os.ReadFile(f.Abs)
if err != nil {
bad("%s: %v", f.Rel, err)
continue
}
s := rule(f.Rel)
bySlug[s] = append(bySlug[s], f.Rel)
if s == "" {
bad("%s: the file name produces an empty slug, so the page has no URL; rename it", f.Rel)
}
if !utf8.Valid(raw) {
bad("%s: not valid UTF-8", f.Rel)
}
d := doc.Parse(raw, opt)
if d.FrontmatterErr != nil {
bad("%s: frontmatter is not valid YAML: %v", f.Rel, d.FrontmatterErr)
}
if !doc.Valid(d.Kind, doc.Kinds) {
bad("%s: %s %q should be one of %s", f.Rel, opt.KindKey, d.Kind, strings.Join(doc.Kinds, ", "))
}
if !doc.Valid(d.Maturity, doc.Maturities) {
bad("%s: %s %q should be one of %s", f.Rel, opt.MaturityKey, d.Maturity, strings.Join(doc.Maturities, ", "))
}
}
var slugs []string
for s, paths := range bySlug {
if s != "" && len(paths) > 1 {
slugs = append(slugs, s)
}
}
sort.Strings(slugs)
for _, s := range slugs {
bad("slug %q is shared by %s; only the first one seen gets the URL", s, strings.Join(bySlug[s], ", "))
}
if problems > 0 {
fmt.Fprintf(stdout, "\n%d problem(s)\n", problems)
return errProblems
}
fmt.Fprintln(stdout, "\nno problems found")
return nil
}
|