forked from mirror/oddmu
Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b64d56a648 | ||
|
|
ce64d04dde | ||
|
|
de5bd2d23e | ||
|
|
29842fe685 | ||
|
|
4042be68f3 | ||
|
|
87846e15b9 | ||
|
|
1390d82e29 | ||
|
|
bb5bd1c629 | ||
|
|
777c498700 | ||
|
|
803025f56a | ||
|
|
dce66ec5a1 | ||
|
|
b6d596cb08 | ||
|
|
3be26b9af1 |
@@ -48,30 +48,26 @@ It's not `)}
|
||||
}
|
||||
|
||||
func TestAddAppendChanges(t *testing.T) {
|
||||
cleanup(t, "testdata/notification2", "changes.md", "changes.md~")
|
||||
restore(t, "index.md")
|
||||
os.Remove("changes.md")
|
||||
cleanup(t, "testdata/append")
|
||||
today := time.Now().Format(time.DateOnly)
|
||||
|
||||
p := &Page{Name: "testdata/notification2/" + today + "-water", Body: []byte(`# Water
|
||||
p := &Page{Name: "testdata/append/" + today + "-water", Body: []byte(`# Water
|
||||
Sunlight dancing fast
|
||||
Blue and green and pebbles gray
|
||||
`)}
|
||||
p.save()
|
||||
|
||||
data := url.Values{}
|
||||
data.Set("body", "Stand in cold water")
|
||||
data.Add("notify", "on")
|
||||
HTTPRedirectTo(t, makeHandler(appendHandler, true),
|
||||
"POST", "/append/testdata/notification2/"+today+"-water",
|
||||
data, "/view/testdata/notification2/"+today+"-water")
|
||||
"POST", "/append/testdata/append/"+today+"-water",
|
||||
data, "/view/testdata/append/"+today+"-water")
|
||||
// The changes.md file was created
|
||||
s, err := os.ReadFile("changes.md")
|
||||
s, err := os.ReadFile("testdata/append/changes.md")
|
||||
assert.NoError(t, err)
|
||||
d := time.Now().Format(time.DateOnly)
|
||||
assert.Equal(t, "# Changes\n\n## "+d+"\n* [Water](testdata/notification2/"+today+"-water)\n", string(s))
|
||||
assert.Equal(t, "# Changes\n\n## "+today+"\n* [Water]("+today+"-water)\n", string(s))
|
||||
// Link added to index.md file
|
||||
s, err = os.ReadFile("index.md")
|
||||
s, err = os.ReadFile("testdata/append/index.md")
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, string(s), "\n* [Water](testdata/notification2/"+today+"-water)\n")
|
||||
// New index contains just the link
|
||||
assert.Equal(t, string(s), "* [Water]("+today+"-water)\n")
|
||||
}
|
||||
|
||||
43
changes.go
43
changes.go
@@ -1,42 +1,54 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// notify adds a link to the "changes" page, as well as to all the existing hashtag pages. If the "changes" page does
|
||||
// not exist, it is created. If the hashtag page does not exist, it is not. Hashtag pages are considered optional.
|
||||
// notify adds a link to the "changes" page, the "index" page, as well as to all the existing hashtag pages. The link to
|
||||
// the "index" page is only added if the page being edited is a blog page for the current year. The link to existing
|
||||
// hashtag pages is only added for blog pages. If the "changes" page does not exist, it is created. If the hashtag page
|
||||
// does not exist, it is not. Hashtag pages are considered optional. If the page that's being edited is in a
|
||||
// subdirectory, then the "changes", "index" and hashtag pages of that particular subdirectory are affected. Every
|
||||
// subdirectory is treated like a potentially independent wiki.
|
||||
func (p *Page) notify() error {
|
||||
p.handleTitle(false)
|
||||
if p.Title == "" {
|
||||
p.Title = p.Name
|
||||
}
|
||||
esc := nameEscape(p.Name)
|
||||
esc := nameEscape(path.Base(p.Name))
|
||||
link := "* [" + p.Title + "](" + esc + ")\n"
|
||||
re := regexp.MustCompile(`(?m)^\* \[[^\]]+\]\(` + esc + `\)\n`)
|
||||
dir := path.Dir(p.Name)
|
||||
// Recent changes for all pages
|
||||
err := addLinkWithDate("changes", link, re)
|
||||
if dir != "." {
|
||||
err := os.MkdirAll(dir, 0755)
|
||||
if err != nil {
|
||||
log.Printf("Creating directory %s failed: %s", dir, err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
err := addLinkWithDate(path.Join(dir, "changes"), link, re)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// For blog pages only…
|
||||
if p.isBlog() {
|
||||
// Add to the index only if the blog post is for the current year
|
||||
if strings.HasPrefix(path.Base(p.Name), time.Now().Format("2006")) {
|
||||
err := addLink("index", link, re)
|
||||
err := addLink(path.Join(dir, "index"), true, link, re)
|
||||
if err != nil {
|
||||
log.Printf("Updating index in %s failed: %s", dir, err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
// Update hashtag pages
|
||||
p.renderHtml() // to set hashtags
|
||||
for _, hashtag := range p.Hashtags {
|
||||
err := addLink(path.Join(dir, hashtag), link, re)
|
||||
err := addLink(path.Join(dir, hashtag), false, link, re)
|
||||
if err != nil {
|
||||
log.Printf("Updating hashtag %s in %s failed: %s", hashtag, dir, err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -53,7 +65,7 @@ func addLinkWithDate(name, link string, re *regexp.Regexp) error {
|
||||
c, err := loadPage(name)
|
||||
if err != nil {
|
||||
// create a new page
|
||||
c = &Page{Name: "changes", Body: []byte("# Changes\n\n## " + date + "\n" + link)}
|
||||
c = &Page{Name: name, Body: []byte("# Changes\n\n## " + date + "\n" + link)}
|
||||
} else {
|
||||
org = string(c.Body)
|
||||
// remove the old match, if one exists
|
||||
@@ -136,11 +148,16 @@ func addLinkWithDate(name, link string, re *regexp.Regexp) error {
|
||||
|
||||
// addLink adds a link to a named page, if the page exists and doesn't contain the link. If the link exists but with a
|
||||
// different title, the title is fixed.
|
||||
func addLink(name, link string, re *regexp.Regexp) error {
|
||||
func addLink(name string, mandatory bool, link string, re *regexp.Regexp) error {
|
||||
c, err := loadPage(name)
|
||||
if err != nil {
|
||||
// Skip non-existing files: no error
|
||||
return nil
|
||||
if (mandatory) {
|
||||
c = &Page{Name: name, Body: []byte(link)}
|
||||
return c.save()
|
||||
} else {
|
||||
// Skip non-existing files: no error
|
||||
return nil
|
||||
}
|
||||
}
|
||||
org := string(c.Body)
|
||||
// if a link exists, that's the place to insert the new link (in which case loc[0] and loc[1] differ)
|
||||
|
||||
127
changes_test.go
127
changes_test.go
@@ -10,32 +10,29 @@ import (
|
||||
// Note TestEditSaveChanges and TestAddAppendChanges.
|
||||
|
||||
func TestChanges(t *testing.T) {
|
||||
cleanup(t, "changes.md", "changes.md~")
|
||||
restore(t, "index.md")
|
||||
os.Remove("changes.md")
|
||||
cleanup(t, "testdata/washing")
|
||||
today := time.Now().Format(time.DateOnly)
|
||||
p := &Page{Name: "testdata/" + today + "-machine",
|
||||
p := &Page{Name: "testdata/washing/" + today + "-machine",
|
||||
Body: []byte(`# Washing machine
|
||||
Churning growling thing
|
||||
Water spraying in a box
|
||||
Out of sight and dark`)}
|
||||
p.notify()
|
||||
// Link added to changes.md file
|
||||
s, err := os.ReadFile("changes.md")
|
||||
s, err := os.ReadFile("testdata/washing/changes.md")
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, string(s), "[Washing machine](testdata/"+today+"-machine)")
|
||||
assert.Contains(t, string(s), "[Washing machine]("+today+"-machine)")
|
||||
// Link added to index.md file
|
||||
s, err = os.ReadFile("index.md")
|
||||
s, err = os.ReadFile("testdata/washing/index.md")
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, string(s), "\n* [Washing machine](testdata/"+today+"-machine)\n")
|
||||
// New index contains just the link
|
||||
assert.Equal(t, string(s), "* [Washing machine]("+today+"-machine)\n")
|
||||
}
|
||||
|
||||
func TestChangesWithHashtag(t *testing.T) {
|
||||
cleanup(t, "changes.md", "changes.md~")
|
||||
restore(t, "index.md")
|
||||
os.Remove("changes.md")
|
||||
cleanup(t, "testdata/changes")
|
||||
intro := "# Haiku\n"
|
||||
line := "* [Hotel room](testdata/changes/2023-10-27-hotel)\n"
|
||||
line := "* [Hotel room](2023-10-27-hotel)\n"
|
||||
h := &Page{Name: "testdata/changes/Haiku", Body: []byte(intro)}
|
||||
h.save()
|
||||
p := &Page{Name: "testdata/changes/2023-10-27-hotel",
|
||||
@@ -46,7 +43,7 @@ Home away from home
|
||||
|
||||
#Haiku #Poetry`)}
|
||||
p.notify()
|
||||
s, err := os.ReadFile("changes.md")
|
||||
s, err := os.ReadFile("testdata/changes/changes.md")
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, string(s), line)
|
||||
s, err = os.ReadFile("testdata/changes/Haiku.md")
|
||||
@@ -56,144 +53,154 @@ Home away from home
|
||||
}
|
||||
|
||||
func TestChangesWithList(t *testing.T) {
|
||||
cleanup(t, "changes.md", "changes.md~")
|
||||
cleanup(t, "testdata/changes")
|
||||
intro := "# Changes\n\nThis is a paragraph.\n\n"
|
||||
line := "* [a change](change)\n"
|
||||
d := "## " + time.Now().Format(time.DateOnly) + "\n"
|
||||
os.WriteFile("changes.md", []byte(intro+d+line), 0644)
|
||||
line := "* [a change](change)\n"
|
||||
assert.NoError(t, os.MkdirAll("testdata/changes", 0755))
|
||||
assert.NoError(t, os.WriteFile("testdata/changes/changes.md", []byte(intro+d+line), 0644))
|
||||
p := &Page{Name: "testdata/changes/alex", Body: []byte(`Hallo!`)}
|
||||
p.notify()
|
||||
s, err := os.ReadFile("changes.md")
|
||||
s, err := os.ReadFile("testdata/changes/changes.md")
|
||||
assert.NoError(t, err)
|
||||
new_line := "* [testdata/changes/alex](testdata/changes/alex)\n"
|
||||
new_line := "* [testdata/changes/alex](alex)\n"
|
||||
// new line was added at the beginning of the list
|
||||
assert.Equal(t, intro+d+new_line+line, string(s))
|
||||
}
|
||||
|
||||
func TestChangesWithOldList(t *testing.T) {
|
||||
cleanup(t, "changes.md", "changes.md~")
|
||||
cleanup(t, "testdata/changes")
|
||||
intro := "# Changes\n\nThis is a paragraph.\n\n"
|
||||
line := "* [a change](change)\n"
|
||||
y := "## " + time.Now().Add(-24*time.Hour).Format(time.DateOnly) + "\n"
|
||||
d := "## " + time.Now().Format(time.DateOnly) + "\n"
|
||||
os.WriteFile("changes.md", []byte(intro+y+line), 0644)
|
||||
assert.NoError(t, os.MkdirAll("testdata/changes", 0755))
|
||||
assert.NoError(t, os.WriteFile("testdata/changes/changes.md", []byte(intro+y+line), 0644))
|
||||
p := &Page{Name: "testdata/changes/alex", Body: []byte(`Hallo!`)}
|
||||
p.notify()
|
||||
s, err := os.ReadFile("changes.md")
|
||||
s, err := os.ReadFile("testdata/changes/changes.md")
|
||||
assert.NoError(t, err)
|
||||
new_line := "* [testdata/changes/alex](testdata/changes/alex)\n"
|
||||
new_line := "* [testdata/changes/alex](alex)\n"
|
||||
// new line was added at the beginning of the list
|
||||
assert.Equal(t, intro+d+new_line+"\n"+y+line, string(s))
|
||||
}
|
||||
|
||||
func TestChangesWithOldDisappearingListAtTheEnd(t *testing.T) {
|
||||
cleanup(t, "changes.md", "changes.md~")
|
||||
cleanup(t, "testdata/changes")
|
||||
intro := "# Changes\n\nThis is a paragraph.\n\n"
|
||||
line := "* [a change](testdata/changes/alex)\n"
|
||||
line := "* [a change](alex)\n"
|
||||
y := "## " + time.Now().Add(-24*time.Hour).Format(time.DateOnly) + "\n"
|
||||
d := "## " + time.Now().Format(time.DateOnly) + "\n"
|
||||
os.WriteFile("changes.md", []byte(intro+y+line), 0644)
|
||||
assert.NoError(t, os.MkdirAll("testdata/changes", 0755))
|
||||
assert.NoError(t, os.WriteFile("testdata/changes/changes.md", []byte(intro+y+line), 0644))
|
||||
p := &Page{Name: "testdata/changes/alex", Body: []byte(`Hallo!`)}
|
||||
p.notify()
|
||||
s, err := os.ReadFile("changes.md")
|
||||
s, err := os.ReadFile("testdata/changes/changes.md")
|
||||
assert.NoError(t, err)
|
||||
new_line := "* [testdata/changes/alex](testdata/changes/alex)\n"
|
||||
new_line := "* [testdata/changes/alex](alex)\n"
|
||||
// new line was added at the beginning of the list, with the new date, and the old date disappeared
|
||||
assert.Equal(t, intro+d+new_line, string(s))
|
||||
}
|
||||
|
||||
func TestChangesWithOldDisappearingListInTheMiddle(t *testing.T) {
|
||||
cleanup(t, "changes.md", "changes.md~")
|
||||
cleanup(t, "testdata/changes")
|
||||
intro := "# Changes\n\nThis is a paragraph.\n\n"
|
||||
line := "* [a change](testdata/changes/alex)\n"
|
||||
other := "* [other change](testdata/changes/whatever)\n"
|
||||
line := "* [a change](alex)\n"
|
||||
other := "* [other change](whatever)\n"
|
||||
yy := "## " + time.Now().Add(-48*time.Hour).Format(time.DateOnly) + "\n"
|
||||
y := "## " + time.Now().Add(-24*time.Hour).Format(time.DateOnly) + "\n"
|
||||
d := "## " + time.Now().Format(time.DateOnly) + "\n"
|
||||
os.WriteFile("changes.md", []byte(intro+y+line+"\n"+yy+other), 0644)
|
||||
assert.NoError(t, os.MkdirAll("testdata/changes", 0755))
|
||||
assert.NoError(t, os.WriteFile("testdata/changes/changes.md", []byte(intro+y+line+"\n"+yy+other), 0644))
|
||||
p := &Page{Name: "testdata/changes/alex", Body: []byte(`Hallo!`)}
|
||||
p.notify()
|
||||
s, err := os.ReadFile("changes.md")
|
||||
s, err := os.ReadFile("testdata/changes/changes.md")
|
||||
assert.NoError(t, err)
|
||||
new_line := "* [testdata/changes/alex](testdata/changes/alex)\n"
|
||||
new_line := "* [testdata/changes/alex](alex)\n"
|
||||
// new line was added at the beginning of the list, with the new date, and the old date disappeared
|
||||
assert.Equal(t, intro+d+new_line+"\n"+yy+other, string(s))
|
||||
}
|
||||
|
||||
func TestChangesWithListAtTheTop(t *testing.T) {
|
||||
cleanup(t, "testdata/changes", "changes.md", "changes.md~")
|
||||
cleanup(t, "testdata/changes")
|
||||
line := "* [a change](change)\n"
|
||||
d := "## " + time.Now().Format(time.DateOnly) + "\n"
|
||||
os.WriteFile("changes.md", []byte(line), 0644)
|
||||
assert.NoError(t, os.MkdirAll("testdata/changes", 0755))
|
||||
assert.NoError(t, os.WriteFile("testdata/changes/changes.md", []byte(line), 0644))
|
||||
p := &Page{Name: "testdata/changes/alex", Body: []byte(`Hallo!`)}
|
||||
p.notify()
|
||||
s, err := os.ReadFile("changes.md")
|
||||
s, err := os.ReadFile("testdata/changes/changes.md")
|
||||
assert.NoError(t, err)
|
||||
new_line := "* [testdata/changes/alex](testdata/changes/alex)\n"
|
||||
new_line := "* [testdata/changes/alex](alex)\n"
|
||||
// new line was added at the top, no error due to missing introduction
|
||||
assert.Equal(t, d+new_line+line, string(s))
|
||||
}
|
||||
|
||||
func TestChangesWithNoList(t *testing.T) {
|
||||
cleanup(t, "testdata/changes", "changes.md", "changes.md~")
|
||||
cleanup(t, "testdata/changes")
|
||||
intro := "# Changes\n\nThis is a paragraph."
|
||||
d := "## " + time.Now().Format(time.DateOnly) + "\n"
|
||||
os.WriteFile("changes.md", []byte(intro), 0644)
|
||||
assert.NoError(t, os.MkdirAll("testdata/changes", 0755))
|
||||
assert.NoError(t, os.WriteFile("testdata/changes/changes.md", []byte(intro), 0644))
|
||||
p := &Page{Name: "testdata/changes/alex", Body: []byte(`Hallo!`)}
|
||||
p.notify()
|
||||
s, err := os.ReadFile("changes.md")
|
||||
s, err := os.ReadFile("testdata/changes/changes.md")
|
||||
assert.NoError(t, err)
|
||||
new_line := "* [testdata/changes/alex](testdata/changes/alex)\n"
|
||||
new_line := "* [testdata/changes/alex](alex)\n"
|
||||
// into is still there and a new list was started
|
||||
assert.Equal(t, intro+"\n\n"+d+new_line, string(s))
|
||||
}
|
||||
|
||||
func TestChangesWithUpdate(t *testing.T) {
|
||||
cleanup(t, "testdata/changes", "changes.md", "changes.md~")
|
||||
cleanup(t, "testdata/changes")
|
||||
intro := "# Changes\n\nThis is a paragraph.\n\n"
|
||||
other := "* [other change](testdata/changes/whatever)\n"
|
||||
other := "* [other change](whatever)\n"
|
||||
d := "## " + time.Now().Format(time.DateOnly) + "\n"
|
||||
line := "* [a change](testdata/changes/alex)\n"
|
||||
os.WriteFile("changes.md", []byte(intro+d+other+line), 0644)
|
||||
line := "* [a change](alex)\n"
|
||||
assert.NoError(t, os.MkdirAll("testdata/changes", 0755))
|
||||
assert.NoError(t, os.WriteFile("testdata/changes/changes.md", []byte(intro+d+other+line), 0644))
|
||||
p := &Page{Name: "testdata/changes/alex", Body: []byte(`Hallo!`)}
|
||||
p.notify()
|
||||
s, err := os.ReadFile("changes.md")
|
||||
s, err := os.ReadFile("testdata/changes/changes.md")
|
||||
assert.NoError(t, err)
|
||||
new_line := "* [testdata/changes/alex](testdata/changes/alex)\n"
|
||||
new_line := "* [testdata/changes/alex](alex)\n"
|
||||
// the change was already listed, but now it moved up and has a new title
|
||||
assert.Equal(t, intro+d+new_line+other, string(s))
|
||||
}
|
||||
|
||||
func TestChangesWithNoChangeToTheOrder(t *testing.T) {
|
||||
cleanup(t, "testdata/changes", "changes.md", "changes.md~")
|
||||
cleanup(t, "testdata/changes")
|
||||
intro := "# Changes\n\nThis is a paragraph.\n\n"
|
||||
other := "* [other change](testdata/changes/whatever)\n"
|
||||
line := "* [a change](testdata/changes/alex)\n"
|
||||
d := "## " + time.Now().Format(time.DateOnly) + "\n"
|
||||
os.WriteFile("changes.md", []byte(intro+d+line+other), 0644)
|
||||
line := "* [a change](alex)\n"
|
||||
other := "* [other change](whatever)\n"
|
||||
assert.NoError(t, os.MkdirAll("testdata/changes", 0755))
|
||||
assert.NoError(t, os.WriteFile("testdata/changes/changes.md", []byte(intro+d+line+other), 0644))
|
||||
p := &Page{Name: "testdata/changes/alex", Body: []byte(`Hallo!`)}
|
||||
p.notify()
|
||||
s, err := os.ReadFile("changes.md")
|
||||
s, err := os.ReadFile("testdata/changes/changes.md")
|
||||
assert.NoError(t, err)
|
||||
new_line := "* [testdata/changes/alex](testdata/changes/alex)\n"
|
||||
new_line := "* [testdata/changes/alex](alex)\n"
|
||||
// the change was already listed at the top, so just use the new title
|
||||
assert.Equal(t, intro+d+new_line+other, string(s))
|
||||
// since the file has changed, a backup was necessary
|
||||
assert.FileExists(t, "testdata/changes/changes.md~")
|
||||
}
|
||||
|
||||
func TestChangesWithNoChanges(t *testing.T) {
|
||||
cleanup(t, "testdata/changes", "changes.md", "changes.md~")
|
||||
cleanup(t, "testdata/changes")
|
||||
intro := "# Changes\n\nThis is a paragraph.\n\n"
|
||||
other := "* [other change](testdata/changes/whatever)\n"
|
||||
line := "* [a change](testdata/changes/alex)\n"
|
||||
d := "## " + time.Now().Format(time.DateOnly) + "\n"
|
||||
os.Remove("changes.md~")
|
||||
os.WriteFile("changes.md", []byte(intro+d+line+other), 0644)
|
||||
line := "* [a change](alex)\n"
|
||||
other := "* [other change](whatever)\n"
|
||||
assert.NoError(t, os.MkdirAll("testdata/changes", 0755))
|
||||
assert.NoError(t, os.WriteFile("testdata/changes/changes.md", []byte(intro+d+line+other), 0644))
|
||||
p := &Page{Name: "testdata/changes/alex", Body: []byte("# a change\nHallo!")}
|
||||
p.notify()
|
||||
s, err := os.ReadFile("changes.md")
|
||||
s, err := os.ReadFile("testdata/changes/changes.md")
|
||||
assert.NoError(t, err)
|
||||
// the change was already listed at the top, so no change was necessary
|
||||
assert.Equal(t, intro+d+line+other, string(s))
|
||||
// since the file hasn't changed, no backup was necessary
|
||||
assert.NoFileExists(t, "changes.md~")
|
||||
assert.NoFileExists(t, "testdata/changes/changes.md~")
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<title>{{.Title}}</title>
|
||||
<style>
|
||||
html { max-width: 65ch; padding: 1ch; margin: auto; color: #111; background-color: #ffe; }
|
||||
html { max-width: 70ch; padding: 1ch; margin: auto; color: #111; background-color: #ffe; }
|
||||
body { hyphens: auto; }
|
||||
del { background-color: #fab }
|
||||
ins { background-color: #af8 }
|
||||
|
||||
@@ -37,26 +37,25 @@ func TestEditSave(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestEditSaveChanges(t *testing.T) {
|
||||
cleanup(t, "testdata/notification", "changes.md")
|
||||
restore(t, "index.md")
|
||||
os.Remove("changes.md")
|
||||
cleanup(t, "testdata/notification")
|
||||
data := url.Values{}
|
||||
data.Set("body", "Hallo!")
|
||||
data.Add("notify", "on")
|
||||
today := time.Now().Format("2006-01-02")
|
||||
// Posting to the save URL saves a page
|
||||
HTTPRedirectTo(t, makeHandler(saveHandler, true),
|
||||
"POST", "/save/testdata/notification/2023-10-28-alex",
|
||||
data, "/view/testdata/notification/2023-10-28-alex")
|
||||
"POST", "/save/testdata/notification/" + today,
|
||||
data, "/view/testdata/notification/" + today)
|
||||
// The changes.md file was created
|
||||
s, err := os.ReadFile("changes.md")
|
||||
s, err := os.ReadFile("testdata/notification/changes.md")
|
||||
assert.NoError(t, err)
|
||||
d := time.Now().Format(time.DateOnly)
|
||||
assert.Equal(t, "# Changes\n\n## "+d+
|
||||
"\n* [testdata/notification/2023-10-28-alex](testdata/notification/2023-10-28-alex)\n",
|
||||
"\n* [testdata/notification/"+today+"]("+today+")\n",
|
||||
string(s))
|
||||
// Link added to index.md file
|
||||
s, err = os.ReadFile("index.md")
|
||||
s, err = os.ReadFile("testdata/notification/index.md")
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, string(s),
|
||||
"\n* [testdata/notification/2023-10-28-alex](testdata/notification/2023-10-28-alex)\n")
|
||||
// New index contains just the link
|
||||
assert.Equal(t, string(s), "* [testdata/notification/"+today+"]("+today+")\n")
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
.nh
|
||||
.ad l
|
||||
.\" Begin generated content:
|
||||
.TH "ODDMU-APACHE" "5" "2023-11-05"
|
||||
.TH "ODDMU-APACHE" "5" "2024-01-27"
|
||||
.PP
|
||||
.SH NAME
|
||||
.PP
|
||||
@@ -15,6 +15,8 @@ oddmu-apache - how to setup Apache as a reverse proxy for Oddmu
|
||||
.PP
|
||||
The oddmu program serves the current working directory as a wiki on port 8080.\&
|
||||
This is an unpriviledged port so an ordinary user account can do this.\&
|
||||
Alternatively, you can reverse proxy HTTP over a Unix-domain socket,
|
||||
as shown later.\&
|
||||
.PP
|
||||
The best way to protect the wiki against vandalism and spam is to use a regular
|
||||
web server as reverse proxy.\& This page explains how to setup Apache on Debian to
|
||||
@@ -38,13 +40,14 @@ MDCertificateAgreement accepted
|
||||
<VirtualHost *:80>
|
||||
ServerName transjovian\&.org
|
||||
RewriteEngine on
|
||||
RewriteRule ^/(\&.*) https://%{HTTP_HOST}/$1 [redirect]
|
||||
RewriteRule "^/(\&.*)" "https://%{HTTP_HOST}/$1" [redirect]
|
||||
</VirtualHost>
|
||||
<VirtualHost *:443>
|
||||
ServerAdmin alex@alexschroeder\&.ch
|
||||
ServerName transjovian\&.org
|
||||
SSLEngine on
|
||||
ProxyPassMatch "^/((view|diff|edit|save|add|append|upload|drop|search)/(\&.*))?$" "http://localhost:8080/$1"
|
||||
ProxyPassMatch "^/((view|diff|edit|save|add|append|upload|drop|search)/(\&.*))?$"
|
||||
"http://localhost:8080/$1"
|
||||
</VirtualHost>
|
||||
.fi
|
||||
.RE
|
||||
@@ -81,6 +84,99 @@ second – instead just proxy to the wiki like you did for the second virtual
|
||||
host: use a copy of the "ProxyPassMatch" directive instead of "RewriteEngine on"
|
||||
and "RewriteRule".\&
|
||||
.PP
|
||||
.SS Allow HTTP for viewing
|
||||
.PP
|
||||
When looking at pages, you might want to allow HTTP since no password is
|
||||
required.\&
|
||||
.PP
|
||||
.nf
|
||||
.RS 4
|
||||
MDomain transjovian\&.org
|
||||
MDCertificateAgreement accepted
|
||||
|
||||
<VirtualHost *:80>
|
||||
ServerName transjovian\&.org
|
||||
RewriteEngine on
|
||||
ProxyPassMatch "^/((view|diff|search)/(\&.*))?$"
|
||||
"http://localhost:8080/$1"
|
||||
RewriteRule "^/((edit|save|add|append|upload|drop)/(\&.*))?$"
|
||||
"https://%{HTTP_HOST}/$1" [redirect]
|
||||
</VirtualHost>
|
||||
<VirtualHost *:443>
|
||||
ServerAdmin alex@alexschroeder\&.ch
|
||||
ServerName transjovian\&.org
|
||||
SSLEngine on
|
||||
ProxyPassMatch "^/((view|diff|edit|save|add|append|upload|drop|search)/(\&.*))?$"
|
||||
"http://localhost:8080/$1"
|
||||
</VirtualHost>
|
||||
.fi
|
||||
.RE
|
||||
.PP
|
||||
.SS Using a Unix-domain Socket
|
||||
.PP
|
||||
Instead of having Oddmu listen on a TCP port, you can have it listen on a
|
||||
Unix-domain socket.\& This requires socket activation.\& An example of configuring
|
||||
the service is given in \fIoddmu.\&service(5)\fR.\&
|
||||
.PP
|
||||
To test just the unix domain socket, use \fIncat(1)\fR:
|
||||
.PP
|
||||
.nf
|
||||
.RS 4
|
||||
echo -e "GET /view/index HTTP/1\&.1rnHost: localhostrnrn"
|
||||
| ncat --unixsock /run/oddmu/oddmu\&.sock
|
||||
.fi
|
||||
.RE
|
||||
.PP
|
||||
On the Apache side, you can proxy to the socket directly.\& This sends all
|
||||
requests to the socket:
|
||||
.PP
|
||||
.nf
|
||||
.RS 4
|
||||
ProxyPass "/" "unix:/run/oddmu/oddmu\&.sock|http://localhost/"
|
||||
.fi
|
||||
.RE
|
||||
.PP
|
||||
Now, all traffic between the web server and the wiki goes over the socket at
|
||||
"/run/oddmu/oddmu.\&sock".\&
|
||||
.PP
|
||||
To test it on the command-line, use a tool like \fIcurl(1)\fR.\& Make sure to provide
|
||||
the correct servername!\&
|
||||
.PP
|
||||
.nf
|
||||
.RS 4
|
||||
curl http://transjovian\&.org/view/index
|
||||
.fi
|
||||
.RE
|
||||
.PP
|
||||
You probably want to serve some static files as well (see \fBServe static files\fR).\&
|
||||
In that case, you need to use the ProxyPassMatch directive.\&
|
||||
.PP
|
||||
.nf
|
||||
.RS 4
|
||||
ProxyPassMatch "^/((view|diff|edit|save|add|append|upload|drop|search)/(\&.*))?$"
|
||||
"unix:/run/oddmu/oddmu\&.sock|http://localhost/$1"
|
||||
.fi
|
||||
.RE
|
||||
.PP
|
||||
There'\&s a curious problem with this expression, however.\& If you use \fIcurl(1)\fR to
|
||||
get the root path, Apache hangs:
|
||||
.PP
|
||||
.nf
|
||||
.RS 4
|
||||
curl http://transjovian\&.org/
|
||||
.fi
|
||||
.RE
|
||||
.PP
|
||||
A workaround is to add the redirect manually and drop the question-mark:
|
||||
.PP
|
||||
.nf
|
||||
.RS 4
|
||||
RedirectMatch "^/$" "/view/index"
|
||||
ProxyPassMatch "^/((view|diff|edit|save|add|append|upload|drop|search)/(\&.*))$"
|
||||
"unix:/run/oddmu/oddmu\&.sock|http://localhost/$1"
|
||||
.fi
|
||||
.RE
|
||||
.PP
|
||||
.SS Access
|
||||
.PP
|
||||
Access control is not part of Oddmu.\& By default, the wiki is editable by all.\&
|
||||
|
||||
@@ -8,6 +8,8 @@ oddmu-apache - how to setup Apache as a reverse proxy for Oddmu
|
||||
|
||||
The oddmu program serves the current working directory as a wiki on port 8080.
|
||||
This is an unpriviledged port so an ordinary user account can do this.
|
||||
Alternatively, you can reverse proxy HTTP over a Unix-domain socket,
|
||||
as shown later.
|
||||
|
||||
The best way to protect the wiki against vandalism and spam is to use a regular
|
||||
web server as reverse proxy. This page explains how to setup Apache on Debian to
|
||||
@@ -30,13 +32,14 @@ MDCertificateAgreement accepted
|
||||
<VirtualHost *:80>
|
||||
ServerName transjovian.org
|
||||
RewriteEngine on
|
||||
RewriteRule ^/(.*) https://%{HTTP_HOST}/$1 [redirect]
|
||||
RewriteRule "^/(.*)" "https://%{HTTP_HOST}/$1" [redirect]
|
||||
</VirtualHost>
|
||||
<VirtualHost *:443>
|
||||
ServerAdmin alex@alexschroeder.ch
|
||||
ServerName transjovian.org
|
||||
SSLEngine on
|
||||
ProxyPassMatch "^/((view|diff|edit|save|add|append|upload|drop|search)/(.*))?$" "http://localhost:8080/$1"
|
||||
ProxyPassMatch "^/((view|diff|edit|save|add|append|upload|drop|search)/(.*))?$" \
|
||||
"http://localhost:8080/$1"
|
||||
</VirtualHost>
|
||||
```
|
||||
|
||||
@@ -64,6 +67,85 @@ second – instead just proxy to the wiki like you did for the second virtual
|
||||
host: use a copy of the "ProxyPassMatch" directive instead of "RewriteEngine on"
|
||||
and "RewriteRule".
|
||||
|
||||
## Allow HTTP for viewing
|
||||
|
||||
When looking at pages, you might want to allow HTTP since no password is
|
||||
required.
|
||||
|
||||
```
|
||||
MDomain transjovian.org
|
||||
MDCertificateAgreement accepted
|
||||
|
||||
<VirtualHost *:80>
|
||||
ServerName transjovian.org
|
||||
RewriteEngine on
|
||||
ProxyPassMatch "^/((view|diff|search)/(.*))?$" \
|
||||
"http://localhost:8080/$1"
|
||||
RewriteRule "^/((edit|save|add|append|upload|drop)/(.*))?$" \
|
||||
"https://%{HTTP_HOST}/$1" [redirect]
|
||||
</VirtualHost>
|
||||
<VirtualHost *:443>
|
||||
ServerAdmin alex@alexschroeder.ch
|
||||
ServerName transjovian.org
|
||||
SSLEngine on
|
||||
ProxyPassMatch "^/((view|diff|edit|save|add|append|upload|drop|search)/(.*))?$" \
|
||||
"http://localhost:8080/$1"
|
||||
</VirtualHost>
|
||||
```
|
||||
|
||||
## Using a Unix-domain Socket
|
||||
|
||||
Instead of having Oddmu listen on a TCP port, you can have it listen on a
|
||||
Unix-domain socket. This requires socket activation. An example of configuring
|
||||
the service is given in _oddmu.service(5)_.
|
||||
|
||||
To test just the unix domain socket, use _ncat(1)_:
|
||||
|
||||
```
|
||||
echo -e "GET /view/index HTTP/1.1\r\nHost: localhost\r\n\r\n" \
|
||||
| ncat --unixsock /run/oddmu/oddmu.sock
|
||||
```
|
||||
|
||||
On the Apache side, you can proxy to the socket directly. This sends all
|
||||
requests to the socket:
|
||||
|
||||
```
|
||||
ProxyPass "/" "unix:/run/oddmu/oddmu.sock|http://localhost/"
|
||||
```
|
||||
|
||||
Now, all traffic between the web server and the wiki goes over the socket at
|
||||
"/run/oddmu/oddmu.sock".
|
||||
|
||||
To test it on the command-line, use a tool like _curl(1)_. Make sure to provide
|
||||
the correct servername!
|
||||
|
||||
```
|
||||
curl http://transjovian.org/view/index
|
||||
```
|
||||
|
||||
You probably want to serve some static files as well (see *Serve static files*).
|
||||
In that case, you need to use the ProxyPassMatch directive.
|
||||
|
||||
```
|
||||
ProxyPassMatch "^/((view|diff|edit|save|add|append|upload|drop|search)/(.*))?$" \
|
||||
"unix:/run/oddmu/oddmu.sock|http://localhost/$1"
|
||||
```
|
||||
|
||||
There's a curious problem with this expression, however. If you use _curl(1)_ to
|
||||
get the root path, Apache hangs:
|
||||
|
||||
```
|
||||
curl http://transjovian.org/
|
||||
```
|
||||
|
||||
A workaround is to add the redirect manually and drop the question-mark:
|
||||
|
||||
```
|
||||
RedirectMatch "^/$" "/view/index"
|
||||
ProxyPassMatch "^/((view|diff|edit|save|add|append|upload|drop|search)/(.*))$" \
|
||||
"unix:/run/oddmu/oddmu.sock|http://localhost/$1"
|
||||
```
|
||||
|
||||
## Access
|
||||
|
||||
Access control is not part of Oddmu. By default, the wiki is editable by all.
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
.nh
|
||||
.ad l
|
||||
.\" Begin generated content:
|
||||
.TH "ODDMU-NOTIFY" "1" "2023-11-06"
|
||||
.TH "ODDMU-NOTIFY" "1" "2024-01-17"
|
||||
.PP
|
||||
.SH NAME
|
||||
.PP
|
||||
@@ -20,20 +20,22 @@ oddmu-notify - add links to changes.\&md, index.\&md, and hashtag pages
|
||||
The "notify" subcommand takes all the page names provided (without the ".\&md"
|
||||
extension) and adds links to it from other pages.\&
|
||||
.PP
|
||||
A new link is added to the \fBchanges\fR page if it doesn'\&t exist.\& The current date
|
||||
of the machine Oddmu is running on is used as the heading.\& If the requested link
|
||||
already exists on the changes page, it is moved up to the current date.\& If that
|
||||
leaves an old date without any links, that date heading is removed.\&
|
||||
A new link is added to the \fBchanges\fR page in the current directory if it doesn'\&t
|
||||
exist.\& The current date of the machine Oddmu is running on is used as the
|
||||
heading.\& If the requested link already exists on the changes page, it is moved
|
||||
up to the current date.\& If that leaves an old date without any links, that date
|
||||
heading is removed.\&
|
||||
.PP
|
||||
A page whose name starts with an ISO date (YYYY-MM-DD, e.\&g.\& "2023-10-28") is
|
||||
called a \fBblog\fR page.\&
|
||||
.PP
|
||||
A link is created from the \fBindex\fR page to blog pages if and only if the blog
|
||||
pages are from the current year.\& The idea is that the front page contains a lot
|
||||
of links to blog posts but eventually the blog post links are moved onto archive
|
||||
pages (one per year, for example), or simply deleted.\& As when editing older
|
||||
pages, links to those pages should not get added to the index as if those older
|
||||
pages were new again.\& A link on the changes page is enough.\&
|
||||
A link is created from the \fBindex\fR page in the current directory to blog pages
|
||||
if and only if the blog pages are from the current year.\& The idea is that the
|
||||
front page contains a lot of links to blog posts but eventually the blog post
|
||||
links are moved onto archive pages (one per year, for example), or simply
|
||||
deleted.\& As when editing older pages, links to those pages should not get added
|
||||
to the index as if those older pages were new again.\& A link on the changes page
|
||||
is enough.\&
|
||||
.PP
|
||||
For every \fBhashtag\fR used on the pages named, another link might be created.\& If a
|
||||
page named like the hashtag exists, a backlink is added to it.\& A hashtag
|
||||
@@ -59,6 +61,44 @@ oddmu notify 2023-11-05-climate
|
||||
.fi
|
||||
.RE
|
||||
.PP
|
||||
The changes file might look as follows:
|
||||
.PP
|
||||
.nf
|
||||
.RS 4
|
||||
# Changes
|
||||
|
||||
This page lists all the changes made to the wiki\&.
|
||||
|
||||
## 2023-11-05
|
||||
|
||||
* [Global warming](2023-11-05-climate)
|
||||
.fi
|
||||
.RE
|
||||
.PP
|
||||
The index file might look as follows:
|
||||
.PP
|
||||
.nf
|
||||
.RS 4
|
||||
# Blog
|
||||
|
||||
This page links to all the blog posts\&.
|
||||
|
||||
* [Global warming](2023-11-05-climate)
|
||||
.fi
|
||||
.RE
|
||||
.PP
|
||||
The hashtag file might look as follows:
|
||||
.PP
|
||||
.nf
|
||||
.RS 4
|
||||
# Climate
|
||||
|
||||
This page links to all the blog posts tagged #Climate\&.
|
||||
|
||||
* [Global warming](2023-11-05-climate)
|
||||
.fi
|
||||
.RE
|
||||
.PP
|
||||
.SH SEE ALSO
|
||||
.PP
|
||||
\fIoddmu\fR(1)
|
||||
|
||||
@@ -13,20 +13,22 @@ oddmu-notify - add links to changes.md, index.md, and hashtag pages
|
||||
The "notify" subcommand takes all the page names provided (without the ".md"
|
||||
extension) and adds links to it from other pages.
|
||||
|
||||
A new link is added to the *changes* page if it doesn't exist. The current date
|
||||
of the machine Oddmu is running on is used as the heading. If the requested link
|
||||
already exists on the changes page, it is moved up to the current date. If that
|
||||
leaves an old date without any links, that date heading is removed.
|
||||
A new link is added to the *changes* page in the current directory if it doesn't
|
||||
exist. The current date of the machine Oddmu is running on is used as the
|
||||
heading. If the requested link already exists on the changes page, it is moved
|
||||
up to the current date. If that leaves an old date without any links, that date
|
||||
heading is removed.
|
||||
|
||||
A page whose name starts with an ISO date (YYYY-MM-DD, e.g. "2023-10-28") is
|
||||
called a *blog* page.
|
||||
|
||||
A link is created from the *index* page to blog pages if and only if the blog
|
||||
pages are from the current year. The idea is that the front page contains a lot
|
||||
of links to blog posts but eventually the blog post links are moved onto archive
|
||||
pages (one per year, for example), or simply deleted. As when editing older
|
||||
pages, links to those pages should not get added to the index as if those older
|
||||
pages were new again. A link on the changes page is enough.
|
||||
A link is created from the *index* page in the current directory to blog pages
|
||||
if and only if the blog pages are from the current year. The idea is that the
|
||||
front page contains a lot of links to blog posts but eventually the blog post
|
||||
links are moved onto archive pages (one per year, for example), or simply
|
||||
deleted. As when editing older pages, links to those pages should not get added
|
||||
to the index as if those older pages were new again. A link on the changes page
|
||||
is enough.
|
||||
|
||||
For every *hashtag* used on the pages named, another link might be created. If a
|
||||
page named like the hashtag exists, a backlink is added to it. A hashtag
|
||||
@@ -50,6 +52,38 @@ it exists):
|
||||
oddmu notify 2023-11-05-climate
|
||||
```
|
||||
|
||||
The changes file might look as follows:
|
||||
|
||||
```
|
||||
# Changes
|
||||
|
||||
This page lists all the changes made to the wiki.
|
||||
|
||||
## 2023-11-05
|
||||
|
||||
* [Global warming](2023-11-05-climate)
|
||||
```
|
||||
|
||||
The index file might look as follows:
|
||||
|
||||
```
|
||||
# Blog
|
||||
|
||||
This page links to all the blog posts.
|
||||
|
||||
* [Global warming](2023-11-05-climate)
|
||||
```
|
||||
|
||||
The hashtag file might look as follows:
|
||||
|
||||
```
|
||||
# Climate
|
||||
|
||||
This page links to all the blog posts tagged #Climate.
|
||||
|
||||
* [Global warming](2023-11-05-climate)
|
||||
```
|
||||
|
||||
# SEE ALSO
|
||||
|
||||
_oddmu_(1)
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
.nh
|
||||
.ad l
|
||||
.\" Begin generated content:
|
||||
.TH "ODDMU-STATIC" "1" "2023-11-05"
|
||||
.TH "ODDMU-STATIC" "1" "2024-01-17"
|
||||
.PP
|
||||
.SH NAME
|
||||
.PP
|
||||
@@ -18,8 +18,8 @@ oddmu-static - create a static copy of the site
|
||||
.SH DESCRIPTION
|
||||
.PP
|
||||
The "static" subcommand generates a static copy of the pages in the current
|
||||
directory and saves them in the given target directory.\& The target directory
|
||||
must not exist to unser no existing files are clobbered.\&
|
||||
directory and saves them in the given destination directory.\& The destination
|
||||
directory must not exist.\&
|
||||
.PP
|
||||
All pages (files with the ".\&md" extension) are turned into HTML files (with the
|
||||
".\&html" extension) using the "static.\&html" template.\& Links pointing to existing
|
||||
@@ -28,7 +28,18 @@ pages get ".\&html" appended.\&
|
||||
Hidden files and directories (starting with a ".\&") and backup files (ending with
|
||||
a "~") are skipped.\&
|
||||
.PP
|
||||
All other files are \fIlinked\fR into the same directory.\&
|
||||
All other files are \fIhard linked\fR.\& This is done to save space: on a typical blog
|
||||
the images take a lot more space than the text.\& On my blog in 2023 I had 2.\&62
|
||||
GiB of JPG files and 0.\&02 GiB of Markdown files.\& There is no point in copying
|
||||
all those images, most of the time.\&
|
||||
.PP
|
||||
Note, however: Hard links cannot span filesystems.\& A hard link is just an extra
|
||||
name for the same file.\& This is why the destination directory for the static
|
||||
site has to be on same filesystem as the current directory, if it contains any
|
||||
other place.\&
|
||||
.PP
|
||||
Furthermore, in-place editing changes the file for all names.\& Avoid editing the
|
||||
image files in the destination directory, just to be on the safe side.\&
|
||||
.PP
|
||||
.SH EXAMPLE
|
||||
.PP
|
||||
@@ -51,7 +62,14 @@ you to migrate static folders and applications.\&
|
||||
.SH ENVIRONMENT
|
||||
.PP
|
||||
The ODDMU_WEBFINGER environment variable has no effect in this situation.\&
|
||||
Fediverse accounts are not linked to their profile pages.\&
|
||||
Fediverse accounts are not linked to their profile pages.\& Since the data isn'\&t
|
||||
cached, every run of this command would trigger a webfinger request for every
|
||||
fediverse account mentioned.\&
|
||||
.PP
|
||||
If the site is large, determining the language of a page slows things down.\& Set
|
||||
the ODDMU_LANGUAGES environment variable to a comma-separated list of ISO 639-1
|
||||
codes, e.\&g.\& "en" or "en,de,fr,pt" to limit the languages loaded and thereby
|
||||
speed language determination up.\&
|
||||
.PP
|
||||
.SH SEE ALSO
|
||||
.PP
|
||||
|
||||
@@ -11,8 +11,8 @@ oddmu-static - create a static copy of the site
|
||||
# DESCRIPTION
|
||||
|
||||
The "static" subcommand generates a static copy of the pages in the current
|
||||
directory and saves them in the given target directory. The target directory
|
||||
must not exist to unser no existing files are clobbered.
|
||||
directory and saves them in the given destination directory. The destination
|
||||
directory must not exist.
|
||||
|
||||
All pages (files with the ".md" extension) are turned into HTML files (with the
|
||||
".html" extension) using the "static.html" template. Links pointing to existing
|
||||
@@ -21,7 +21,18 @@ pages get ".html" appended.
|
||||
Hidden files and directories (starting with a ".") and backup files (ending with
|
||||
a "~") are skipped.
|
||||
|
||||
All other files are _linked_ into the same directory.
|
||||
All other files are _hard linked_. This is done to save space: on a typical blog
|
||||
the images take a lot more space than the text. On my blog in 2023 I had 2.62
|
||||
GiB of JPG files and 0.02 GiB of Markdown files. There is no point in copying
|
||||
all those images, most of the time.
|
||||
|
||||
Note, however: Hard links cannot span filesystems. A hard link is just an extra
|
||||
name for the same file. This is why the destination directory for the static
|
||||
site has to be on same filesystem as the current directory, if it contains any
|
||||
other place.
|
||||
|
||||
Furthermore, in-place editing changes the file for all names. Avoid editing the
|
||||
image files in the destination directory, just to be on the safe side.
|
||||
|
||||
# EXAMPLE
|
||||
|
||||
@@ -42,7 +53,14 @@ you to migrate static folders and applications.
|
||||
# ENVIRONMENT
|
||||
|
||||
The ODDMU_WEBFINGER environment variable has no effect in this situation.
|
||||
Fediverse accounts are not linked to their profile pages.
|
||||
Fediverse accounts are not linked to their profile pages. Since the data isn't
|
||||
cached, every run of this command would trigger a webfinger request for every
|
||||
fediverse account mentioned.
|
||||
|
||||
If the site is large, determining the language of a page slows things down. Set
|
||||
the ODDMU_LANGUAGES environment variable to a comma-separated list of ISO 639-1
|
||||
codes, e.g. "en" or "en,de,fr,pt" to limit the languages loaded and thereby
|
||||
speed language determination up.
|
||||
|
||||
# SEE ALSO
|
||||
|
||||
|
||||
24
man/oddmu.1
24
man/oddmu.1
@@ -5,7 +5,7 @@
|
||||
.nh
|
||||
.ad l
|
||||
.\" Begin generated content:
|
||||
.TH "ODDMU" "1" "2023-12-17"
|
||||
.TH "ODDMU" "1" "2024-01-17"
|
||||
.PP
|
||||
.SH NAME
|
||||
.PP
|
||||
@@ -60,6 +60,17 @@ See \fIoddmu-templates\fR(5) for more.\&
|
||||
.PP
|
||||
You can change the port served by setting the ODDMU_PORT environment variable.\&
|
||||
.PP
|
||||
You can change the address served by setting the ODDMU_ADDRESS environment
|
||||
variable to either an IPv4 address or an IPv6 address.\& If ODDMU_ADDRESS is
|
||||
unset, then the program listens on all available unicast addresses, both IPv4
|
||||
and IPv6.\& Here are a few example addresses:
|
||||
.PP
|
||||
ODDMU_ADDRESS=127.\&0.\&0.\&1 # The loopback IPv4 address.\&
|
||||
ODDMU_ADDRESS=2001:db8::3:1 # An IPv6 address.\&
|
||||
.PP
|
||||
See the Socket Activation section for an alternative method of listening which
|
||||
supports Unix-domain sockets.\&
|
||||
.PP
|
||||
In order to limit language-detection to the languages you actually use, set the
|
||||
environment variable ODDMU_LANGUAGES to a comma-separated list of ISO 639-1
|
||||
codes, e.\&g.\& "en" or "en,de,fr,pt".\&
|
||||
@@ -67,6 +78,15 @@ codes, e.\&g.\& "en" or "en,de,fr,pt".\&
|
||||
You can enable webfinger to link fediverse accounts to their correct profile
|
||||
pages by setting ODDMU_WEBFINGER to "1".\& See \fIoddmu\fR(5).\&
|
||||
.PP
|
||||
.SH Socket Activation
|
||||
.PP
|
||||
Instead of specifying ODDMU_ADDRESS or ODDMU_PORT, you can start the service
|
||||
through socket activation.\& The advantage of this method is that you can use a
|
||||
Unix-domain socket instead of a TCP socket, and the permissions and ownership of
|
||||
the socket are set before the program starts.\& See \fIoddmu.\&service\fR(5) and
|
||||
\fIoddmu-apache\fR(5) for an example of how to use socket activation with a
|
||||
Unix-domain socket under systemd and Apache.\&
|
||||
.PP
|
||||
.SH SECURITY
|
||||
.PP
|
||||
If the machine you are running Oddmu on is accessible from the Internet, you
|
||||
@@ -79,6 +99,8 @@ Oddmu assumes that all the users that can edit pages or upload files are trusted
|
||||
users and therefore their content is trusted.\& Therefore, Oddmu doesn'\&t perform
|
||||
HTML sanitization!\&
|
||||
.PP
|
||||
For an extra dose of security, consider using a Unix-domain socket.\&
|
||||
.PP
|
||||
.SH OPTIONS
|
||||
.PP
|
||||
The oddmu program can be run on the command-line using various subcommands.\&
|
||||
|
||||
@@ -53,6 +53,17 @@ See _oddmu-templates_(5) for more.
|
||||
|
||||
You can change the port served by setting the ODDMU_PORT environment variable.
|
||||
|
||||
You can change the address served by setting the ODDMU_ADDRESS environment
|
||||
variable to either an IPv4 address or an IPv6 address. If ODDMU_ADDRESS is
|
||||
unset, then the program listens on all available unicast addresses, both IPv4
|
||||
and IPv6. Here are a few example addresses:
|
||||
|
||||
ODDMU_ADDRESS=127.0.0.1 # The loopback IPv4 address.
|
||||
ODDMU_ADDRESS=2001:db8::3:1 # An IPv6 address.
|
||||
|
||||
See the Socket Activation section for an alternative method of listening which
|
||||
supports Unix-domain sockets.
|
||||
|
||||
In order to limit language-detection to the languages you actually use, set the
|
||||
environment variable ODDMU_LANGUAGES to a comma-separated list of ISO 639-1
|
||||
codes, e.g. "en" or "en,de,fr,pt".
|
||||
@@ -60,6 +71,15 @@ codes, e.g. "en" or "en,de,fr,pt".
|
||||
You can enable webfinger to link fediverse accounts to their correct profile
|
||||
pages by setting ODDMU_WEBFINGER to "1". See _oddmu_(5).
|
||||
|
||||
# Socket Activation
|
||||
|
||||
Instead of specifying ODDMU_ADDRESS or ODDMU_PORT, you can start the service
|
||||
through socket activation. The advantage of this method is that you can use a
|
||||
Unix-domain socket instead of a TCP socket, and the permissions and ownership of
|
||||
the socket are set before the program starts. See _oddmu.service_(5) and
|
||||
_oddmu-apache_(5) for an example of how to use socket activation with a
|
||||
Unix-domain socket under systemd and Apache.
|
||||
|
||||
# SECURITY
|
||||
|
||||
If the machine you are running Oddmu on is accessible from the Internet, you
|
||||
@@ -72,6 +92,8 @@ Oddmu assumes that all the users that can edit pages or upload files are trusted
|
||||
users and therefore their content is trusted. Therefore, Oddmu doesn't perform
|
||||
HTML sanitization!
|
||||
|
||||
For an extra dose of security, consider using a Unix-domain socket.
|
||||
|
||||
# OPTIONS
|
||||
|
||||
The oddmu program can be run on the command-line using various subcommands.
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
.nh
|
||||
.ad l
|
||||
.\" Begin generated content:
|
||||
.TH "ODDMU.SERVICE" "5" "2023-10-28"
|
||||
.TH "ODDMU.SERVICE" "5" "2024-01-17"
|
||||
.PP
|
||||
.SH NAME
|
||||
.PP
|
||||
@@ -80,9 +80,30 @@ sudo ln -sf /home/oddmu/oddmu\&.service
|
||||
.fi
|
||||
.RE
|
||||
.PP
|
||||
.SH Socket Activation
|
||||
.PP
|
||||
Alternatively, you can let systemd handle the creation of the listening socket,
|
||||
passing it to Oddmu.\& See "oddmu-unix-domain.\&service" and
|
||||
"oddmu-unix-domain.\&socket" for a fully worked example of how to do this with a
|
||||
Unix domain socket.\& Take note of "Accept=no" in the .\&socket file and
|
||||
"StandardInput=socket" in the .\&service file.\& The option "StandardInput=socket"
|
||||
tells systemd to pass the socket to the service as its standard input.\&
|
||||
"Accept=no" tells systemd to pass a listening socket, rather than to try calling
|
||||
Oddmu for each connection.\&
|
||||
.PP
|
||||
The instructions for starting and enabling the systemd service are almost
|
||||
exactly the same as those in the previous section, with "oddmu.\&service" replaced
|
||||
by "oddmu-unix-domain.\&service".\& You'\&ll also need to run the following:
|
||||
.PP
|
||||
.nf
|
||||
.RS 4
|
||||
ln -s /home/oddmu/oddmu-unix-domain\&.socket /etc/systemd/system
|
||||
.fi
|
||||
.RE
|
||||
.PP
|
||||
.SH SEE ALSO
|
||||
.PP
|
||||
\fIoddmu\fR(1), \fIsystemd.\&exec\fR(5), \fIcapabilities\fR(7)
|
||||
\fIoddmu\fR(1), \fIsystemd.\&exec\fR(5), \fIsystemd.\&socket(5), \fRcapabilities_(7)
|
||||
.PP
|
||||
.SH AUTHORS
|
||||
.PP
|
||||
|
||||
@@ -61,9 +61,28 @@ sudo ln -sf /home/oddmu/oddmu.service \
|
||||
/etc/systemd/system/multi-user.target.wants/
|
||||
```
|
||||
|
||||
# Socket Activation
|
||||
|
||||
Alternatively, you can let systemd handle the creation of the listening socket,
|
||||
passing it to Oddmu. See "oddmu-unix-domain.service" and
|
||||
"oddmu-unix-domain.socket" for a fully worked example of how to do this with a
|
||||
Unix domain socket. Take note of "Accept=no" in the .socket file and
|
||||
"StandardInput=socket" in the .service file. The option "StandardInput=socket"
|
||||
tells systemd to pass the socket to the service as its standard input.
|
||||
"Accept=no" tells systemd to pass a listening socket, rather than to try calling
|
||||
Oddmu for each connection.
|
||||
|
||||
The instructions for starting and enabling the systemd service are almost
|
||||
exactly the same as those in the previous section, with "oddmu.service" replaced
|
||||
by "oddmu-unix-domain.service". You'll also need to run the following:
|
||||
|
||||
```
|
||||
ln -s /home/oddmu/oddmu-unix-domain.socket /etc/systemd/system
|
||||
```
|
||||
|
||||
# SEE ALSO
|
||||
|
||||
_oddmu_(1), _systemd.exec_(5), _capabilities_(7)
|
||||
_oddmu_(1), _systemd.exec_(5), _systemd.socket(5), _capabilities_(7)
|
||||
|
||||
# AUTHORS
|
||||
|
||||
|
||||
53
oddmu-unix-domain.service
Normal file
53
oddmu-unix-domain.service
Normal file
@@ -0,0 +1,53 @@
|
||||
[Unit]
|
||||
Description=Oddmu
|
||||
After=network.target
|
||||
Requires=oddmu-unix-domain.socket
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
[Service]
|
||||
Type=simple
|
||||
Restart=always
|
||||
StandardInput=socket
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
DynamicUser=true
|
||||
MemoryMax=256M
|
||||
MemoryHigh=128M
|
||||
ExecStart=/home/oddmu/oddmu
|
||||
WorkingDirectory=/home/oddmu
|
||||
Environment="ODDMU_PORT=8080"
|
||||
Environment="ODDMU_WEBFINGER=1"
|
||||
|
||||
# (man "systemd.exec")
|
||||
ReadWritePaths=/home/oddmu
|
||||
ProtectHostname=yes
|
||||
RestrictSUIDSGID=yes
|
||||
RemoveIPC=yes
|
||||
MemoryDenyWriteExecute=yes
|
||||
|
||||
# Sandboxing options to harden security
|
||||
NoNewPrivileges=yes
|
||||
PrivateTmp=yes
|
||||
PrivateDevices=yes
|
||||
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
|
||||
RestrictNamespaces=yes
|
||||
RestrictRealtime=yes
|
||||
DevicePolicy=closed
|
||||
ProtectSystem=full
|
||||
ProtectControlGroups=yes
|
||||
ProtectKernelModules=yes
|
||||
ProtectKernelTunables=yes
|
||||
LockPersonality=yes
|
||||
SystemCallFilter=~@clock @debug @module @mount @obsolete @reboot @setuid @swap
|
||||
|
||||
# Denying access to capabilities that should not be relevant
|
||||
# (man "capabilities")
|
||||
CapabilityBoundingSet=~CAP_RAWIO CAP_MKNOD
|
||||
CapabilityBoundingSet=~CAP_AUDIT_CONTROL CAP_AUDIT_READ CAP_AUDIT_WRITE
|
||||
CapabilityBoundingSet=~CAP_SYS_BOOT CAP_SYS_TIME CAP_SYS_MODULE CAP_SYS_PACCT
|
||||
CapabilityBoundingSet=~CAP_LEASE CAP_LINUX_IMMUTABLE CAP_IPC_LOCK
|
||||
CapabilityBoundingSet=~CAP_BLOCK_SUSPEND CAP_WAKE_ALARM
|
||||
CapabilityBoundingSet=~CAP_SYS_TTY_CONFIG
|
||||
CapabilityBoundingSet=~CAP_MAC_ADMIN CAP_MAC_OVERRIDE
|
||||
CapabilityBoundingSet=~CAP_NET_ADMIN CAP_NET_BROADCAST CAP_NET_RAW
|
||||
CapabilityBoundingSet=~CAP_SYS_ADMIN CAP_SYS_PTRACE CAP_SYSLOG
|
||||
14
oddmu-unix-domain.socket
Normal file
14
oddmu-unix-domain.socket
Normal file
@@ -0,0 +1,14 @@
|
||||
[Unit]
|
||||
Description=Oddmu server socket
|
||||
|
||||
[Socket]
|
||||
ListenStream=/run/oddmu/oddmu.sock
|
||||
SocketGroup=www-data
|
||||
# Systemd manages the socket, so may as well let it be owned by root.
|
||||
SocketUser=root
|
||||
# But it needs to be readable and writable by the web server.
|
||||
SocketMode=0660
|
||||
Accept=no
|
||||
|
||||
[Install]
|
||||
WantedBy=sockets.target
|
||||
@@ -6,7 +6,7 @@
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<title>Search for {{.Query}}</title>
|
||||
<style>
|
||||
html { max-width: 65ch; padding: 2ch; margin: auto; color: #111; background-color: #ffe; }
|
||||
html { max-width: 70ch; padding: 2ch; margin: auto; color: #111; background-color: #ffe; }
|
||||
body { hyphens: auto; }
|
||||
header a { margin-right: 1ch; }
|
||||
form { display: inline-block; }
|
||||
|
||||
@@ -37,20 +37,33 @@ func (cmd *staticCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface
|
||||
fmt.Println("Exactly one target directory is required")
|
||||
return subcommands.ExitFailure
|
||||
}
|
||||
return staticCli(filepath.Clean(args[0]))
|
||||
return staticCli(filepath.Clean(args[0]), false)
|
||||
}
|
||||
|
||||
func staticCli(dir string) subcommands.ExitStatus {
|
||||
// staticCli generates a static site in the designated directory. The quiet flag is used to suppress output when running
|
||||
// tests.
|
||||
func staticCli(dir string, quiet bool) subcommands.ExitStatus {
|
||||
err := os.Mkdir(dir, 0755)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return subcommands.ExitFailure
|
||||
}
|
||||
initAccounts()
|
||||
templates := loadTemplates();
|
||||
if (!quiet) {
|
||||
fmt.Printf("Loaded %d languages\n", loadLanguages())
|
||||
}
|
||||
templates := loadTemplates()
|
||||
n := 0;
|
||||
err = filepath.Walk(".", func(path string, info fs.FileInfo, err error) error {
|
||||
n++
|
||||
if (!quiet && (n < 100 || n < 1000 && n % 10 == 0 || n % 100 == 0)) {
|
||||
fmt.Fprintf(os.Stdout, "\r%d", n)
|
||||
}
|
||||
return staticFile(path, dir, info, templates, err)
|
||||
})
|
||||
if (!quiet) {
|
||||
fmt.Printf("\r%d\n", n)
|
||||
}
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return subcommands.ExitFailure
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
|
||||
func TestStatusCmd(t *testing.T) {
|
||||
cleanup(t, "testdata/static")
|
||||
s := staticCli("testdata/static")
|
||||
s := staticCli("testdata/static", true)
|
||||
assert.Equal(t, subcommands.ExitSuccess, s)
|
||||
// pages
|
||||
assert.FileExists(t, "testdata/static/index.html")
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<title>{{.Title}}</title>
|
||||
<style>
|
||||
html { max-width: 65ch; padding: 1ch; margin: auto; color: #111; background-color: #ffe; }
|
||||
html { max-width: 70ch; padding: 1ch; margin: auto; color: #111; background-color: #ffe; }
|
||||
body { hyphens: auto; }
|
||||
header a { margin-right: 1ch; }
|
||||
form { display: inline-block; }
|
||||
|
||||
46
wiki.go
46
wiki.go
@@ -3,12 +3,16 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"github.com/google/subcommands"
|
||||
"html/template"
|
||||
"io/fs"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// validPath is a regular expression where the second group matches a page, so when the editHandler is called, a URL
|
||||
@@ -65,6 +69,37 @@ func getPort() string {
|
||||
return port
|
||||
}
|
||||
|
||||
// When stdin is a socket, getListener returns a listener that listens
|
||||
// on the socket passed as stdin. This allows systemd-style socket
|
||||
// activation.
|
||||
// Otherwise, getListener returns a net.Listener listening on the address from
|
||||
// ODDMU_ADDRESS and the port from ODDMU_PORT.
|
||||
// ODDMU_ADDRESS may be either an IPV4 address or an IPv6 address.
|
||||
// If ODDMU_ADDRESS is unspecified, then the
|
||||
// listener listens on all available unicast addresses, both IPv4 and IPv6.
|
||||
func getListener() (net.Listener, error) {
|
||||
address := os.Getenv("ODDMU_ADDRESS")
|
||||
port := getPort()
|
||||
|
||||
stat, err := os.Stdin.Stat()
|
||||
if stat == nil {
|
||||
return nil, err
|
||||
}
|
||||
if stat.Mode().Type() == fs.ModeSocket {
|
||||
// Listening socket passed on stdin, through systemd socket
|
||||
// activation or similar:
|
||||
log.Println("Serving a wiki on a listening socket passed by systemd.")
|
||||
return net.FileListener(os.Stdin)
|
||||
}
|
||||
if strings.ContainsRune(address, ':') {
|
||||
address = fmt.Sprintf("[%s]:%s", address, port)
|
||||
} else {
|
||||
address = fmt.Sprintf("%s:%s", address, port)
|
||||
}
|
||||
log.Printf("Serving a wiki at address %s", address)
|
||||
return net.Listen("tcp", address)
|
||||
}
|
||||
|
||||
// scheduleLoadIndex calls index.load and prints some messages before and after. For testing, call index.load directly
|
||||
// and skip the messages.
|
||||
func scheduleLoadIndex() {
|
||||
@@ -111,11 +146,14 @@ func serve() {
|
||||
go scheduleLoadIndex()
|
||||
go scheduleLoadLanguages()
|
||||
initAccounts()
|
||||
port := getPort()
|
||||
log.Printf("Serving a wiki on port %s", port)
|
||||
err := http.ListenAndServe(":"+port, nil)
|
||||
if err != nil {
|
||||
listener, err := getListener()
|
||||
if listener == nil {
|
||||
log.Println(err)
|
||||
} else {
|
||||
err := http.Serve(listener, nil)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
57
wiki_test.go
57
wiki_test.go
@@ -86,63 +86,22 @@ func HTTPStatusCodeIfModifiedSince(t *testing.T, handler http.HandlerFunc, url s
|
||||
assert.Equal(t, http.StatusNotModified, w.Code)
|
||||
}
|
||||
|
||||
// restore remembers the file content before the test starts and restores the file at the end. Important for files such
|
||||
// as "index.md".
|
||||
func restore(t *testing.T, files ...string) {
|
||||
data := make(map[string][]byte)
|
||||
stat := make(map[string]os.FileInfo)
|
||||
for _, file := range files {
|
||||
s, err := os.Stat(file)
|
||||
if err != nil {
|
||||
t.Log("Could not stat ", file, ": ", err)
|
||||
continue
|
||||
}
|
||||
c, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
t.Log("Could not read ", file, ": ", err)
|
||||
continue
|
||||
}
|
||||
stat[file] = s
|
||||
data[file] = c
|
||||
|
||||
}
|
||||
// cleanup deletes a directory mentioned and removes all pages in that directory from the index.
|
||||
func cleanup(t *testing.T, dir string) {
|
||||
t.Cleanup(func() {
|
||||
for file, c := range data {
|
||||
m := stat[file].Mode()
|
||||
err := os.WriteFile(file, c, m)
|
||||
if err != nil {
|
||||
t.Log("Could not restore ", file, ": ", err)
|
||||
}
|
||||
t := stat[file].ModTime()
|
||||
os.Chtimes(file, t, t)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// cleanup deletes any directories mentioned and removes all pages in those directories from the index. Incidentally, if
|
||||
// a filename such as "changes.md" or "changes.md~" is provided instead of a directory, then that page file is removed
|
||||
// and any mention of it is removed from the index.
|
||||
func cleanup(t *testing.T, dirs ...string) {
|
||||
t.Cleanup(func() {
|
||||
for _, dir := range dirs {
|
||||
_ = os.RemoveAll(dir)
|
||||
}
|
||||
_ = os.RemoveAll(dir)
|
||||
index.Lock()
|
||||
defer index.Unlock()
|
||||
for name := range index.titles {
|
||||
for _, dir := range dirs {
|
||||
if strings.HasPrefix(name, dir) {
|
||||
delete(index.titles, name)
|
||||
}
|
||||
if strings.HasPrefix(name, dir) {
|
||||
delete(index.titles, name)
|
||||
}
|
||||
}
|
||||
ids := []docid{}
|
||||
for id, name := range index.documents {
|
||||
for _, dir := range dirs {
|
||||
if strings.HasPrefix(name, dir) {
|
||||
delete(index.documents, id)
|
||||
ids = append(ids, id)
|
||||
}
|
||||
if strings.HasPrefix(name, dir) {
|
||||
delete(index.documents, id)
|
||||
ids = append(ids, id)
|
||||
}
|
||||
}
|
||||
for hashtag, docs := range index.token {
|
||||
|
||||
Reference in New Issue
Block a user