forked from mirror/oddmu
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8e35336cb3 | ||
|
|
2a44c2a74f | ||
|
|
fe9a621f1e | ||
|
|
be663eed32 | ||
|
|
86ef305e9c | ||
|
|
1fd97ae717 | ||
|
|
d0fdf8c3c6 | ||
|
|
1786050e72 | ||
|
|
f12252e148 | ||
|
|
f5f997261e | ||
|
|
43408707c5 | ||
|
|
50ce79d60d |
28
Makefile
28
Makefile
@@ -1,4 +1,7 @@
|
||||
SHELL=/bin/bash
|
||||
PREFIX=${HOME}/.local
|
||||
|
||||
.PHONY: help build test run upload docs install missing
|
||||
|
||||
help:
|
||||
@echo Help for Oddmu
|
||||
@@ -13,7 +16,7 @@ help:
|
||||
@echo make docs
|
||||
@echo " create man pages from text files"
|
||||
@echo
|
||||
@echo go build
|
||||
@echo make build
|
||||
@echo " just build it"
|
||||
@echo
|
||||
@echo make install
|
||||
@@ -22,14 +25,18 @@ help:
|
||||
@echo make upload
|
||||
@echo " this is how I upgrade my server"
|
||||
|
||||
run:
|
||||
go run .
|
||||
build: oddmu
|
||||
|
||||
oddmu: *.go
|
||||
go build
|
||||
|
||||
test:
|
||||
go test -shuffle on .
|
||||
|
||||
upload:
|
||||
go build
|
||||
run:
|
||||
go run .
|
||||
|
||||
upload: build
|
||||
rsync --itemize-changes --archive oddmu sibirocobombus.root:/home/oddmu/
|
||||
ssh sibirocobombus.root "systemctl restart oddmu; systemctl restart alex; systemctl restart claudia; systemctl restart campaignwiki"
|
||||
@echo Changes to the template files need careful consideration
|
||||
@@ -37,11 +44,6 @@ upload:
|
||||
docs:
|
||||
cd man; make
|
||||
|
||||
install:
|
||||
make docs
|
||||
for n in 1 5 7; do install -D -t $$HOME/.local/share/man/man$$n man/*.$$n; done
|
||||
go build
|
||||
install -D -t $$HOME/.local/bin oddmu
|
||||
|
||||
missing:
|
||||
for f in man/*.txt; do grep --quiet "$$f" README.md || echo $$f is not in the README; done
|
||||
install: build docs
|
||||
for n in 1 5 7; do install -D -t ${PREFIX}/share/man/man$$n man/*.$$n; done
|
||||
install -D -t ${PREFIX}/.local/bin oddmu
|
||||
|
||||
90
README.md
90
README.md
@@ -48,6 +48,18 @@ links. Local links must use percent encoding for page names so there
|
||||
is a section about percent encoding. The man page also explains how
|
||||
feeds are generated.
|
||||
|
||||
[oddmu-releases(7)](/oddmu.git/blob/main/man/oddmu-releases.7.txt):
|
||||
This man page lists all the Oddmu versions and their user-visible
|
||||
changes.
|
||||
|
||||
[oddmu-releases(7)](/oddmu.git/blob/main/man/oddmu-releases.7.txt):
|
||||
This man page lists all the Oddmu versions and their user-visible
|
||||
changes.
|
||||
|
||||
[oddmu-version(1)](/oddmu.git/blob/main/man/oddmu-version.1.txt): This
|
||||
man page documents the "version" subcommand which you can use to get
|
||||
installed Oddmu version.
|
||||
|
||||
[oddmu-list(1)](/oddmu.git/blob/main/man/oddmu-list.1.txt): This man
|
||||
page documents the "list" subcommand which you can use to get page
|
||||
names and page titles.
|
||||
@@ -91,8 +103,12 @@ This man page documents how the templates can be changed (how they
|
||||
templates.
|
||||
|
||||
[oddmu-apache(5)](/oddmu.git/blob/main/man/oddmu-apache.5.txt): This
|
||||
man page documents how to set up the web server for various common
|
||||
tasks such as using logins to limit what visitors can edit.
|
||||
man page documents how to set up the Apache web server for various
|
||||
common tasks such as using logins to limit what visitors can edit.
|
||||
|
||||
[oddmu-nginx(5)](/oddmu.git/blob/main/man/oddmu-nginx.5.txt): This man
|
||||
page documents how to set up the freenginx web server for various
|
||||
common tasks such as using logins to limit what visitors can edit.
|
||||
|
||||
[oddmu.service(5)](/oddmu.git/blob/main/man/oddmu.service.5.txt): This
|
||||
man page documents how to setup a systemd unit and have it manage
|
||||
@@ -100,15 +116,28 @@ Oddmu. “Great configurability brings great burdens.”
|
||||
|
||||
## Building
|
||||
|
||||
To build the binary:
|
||||
|
||||
```sh
|
||||
go build
|
||||
```
|
||||
|
||||
The man pages are already built. If you want to rebuild them, you need
|
||||
to have [scdoc](https://git.sr.ht/~sircmpwn/scdoc) installed.
|
||||
|
||||
```sh
|
||||
make docs
|
||||
```
|
||||
|
||||
The `Makefile` in the `man` directory has targets to create Markdown
|
||||
and HTML files.
|
||||
|
||||
## Running
|
||||
|
||||
The working directory is where pages are saved and where templates are
|
||||
loaded from. You need a copy of the template files in this directory.
|
||||
Here's how to start it in the source directory:
|
||||
|
||||
Here's how to build and run straight from the source directory:
|
||||
|
||||
```sh
|
||||
go run .
|
||||
@@ -117,11 +146,43 @@ go run .
|
||||
The program serves the local directory as a wiki on port 8080. Point
|
||||
your browser to http://localhost:8080/ to use it.
|
||||
|
||||
Once the `oddmu` binary is built, you can run it instead:
|
||||
|
||||
```sh
|
||||
./oddmu
|
||||
```
|
||||
|
||||
To read the main man page witihout installing Oddmu:
|
||||
|
||||
```sh
|
||||
man -l man/oddmu.1
|
||||
```
|
||||
|
||||
## Installing
|
||||
|
||||
This installs `oddmu` into `$HOME/.local/bin` and the manual pages
|
||||
into `$HOME/.local/share/man/`.
|
||||
|
||||
```sh
|
||||
make install
|
||||
```
|
||||
|
||||
To install it elsewhere, here's an example using [GNU
|
||||
Stow](https://www.gnu.org/software/stow/) to install it into
|
||||
`/usr/local/stow` in a way that allows you to uninstall it later:
|
||||
|
||||
```sh
|
||||
sudo mkdir /usr/local/stow/oddmu
|
||||
sudo make install PREFIX=/usr/local/stow/oddmu/
|
||||
cd /usr/local/stow
|
||||
sudo stow oddmu
|
||||
```
|
||||
|
||||
## Bugs
|
||||
|
||||
If you spot any, [contact](https://alexschroeder.ch/wiki/Contact) me.
|
||||
|
||||
## Source
|
||||
## Hacking
|
||||
|
||||
If you're interested in making changes to the code, here's a
|
||||
high-level introduction to the various source files.
|
||||
@@ -134,6 +195,8 @@ high-level introduction to the various source files.
|
||||
account link destinations with the URI provided by webfinger
|
||||
- `add_append.go` implements the `/add` and `/append` handlers
|
||||
- `archive.go` implements the `/archive` handler
|
||||
- `changes.go` implements the "notifications": the automatic addition
|
||||
of links to index, changes and hashtag files when pages are edited
|
||||
- `diff.go` implements the `/diff` handler
|
||||
- `edit_save.go` implements the `/edit` and `/save` handlers
|
||||
- `feed.go` implements the feed for a page based on the links it lists
|
||||
@@ -153,6 +216,8 @@ high-level introduction to the various source files.
|
||||
- `watch.go` implements the filesystem notification watch
|
||||
- `wiki.go` implements the main function
|
||||
|
||||
### Changing the markup rules
|
||||
|
||||
If you want to change the markup rules, your starting point should be
|
||||
`parser.go`. Make sure you read the documentation of [Go
|
||||
Markdown](https://github.com/gomarkdown/markdown) and note that it
|
||||
@@ -161,6 +226,8 @@ that the MathJax Javascript gets loaded) and
|
||||
[MMark](https://mmark.miek.nl/post/syntax/) support, and it shows how
|
||||
extensions can be added.
|
||||
|
||||
### Filenames and URL path
|
||||
|
||||
One of the sad parts of the code is the distinction between path and
|
||||
filepath. On a Linux system, this doesn't matter. I suspect that it
|
||||
also doesn't matter on MacOS and Windows because the file systems
|
||||
@@ -178,6 +245,21 @@ If you need to access the page name in code that is used from a
|
||||
template, you have to decode the path. See the code in `diff.go` for
|
||||
an example.
|
||||
|
||||
### HTTP handlers
|
||||
|
||||
The URL paths all have the form `/action/directory/pagename` (with
|
||||
directory being optional and pagename sometimes being optional). If
|
||||
you need to limit access in Apache or nginx or some other web server
|
||||
acting as a [reverse
|
||||
proxy](https://en.wikipedia.org/wiki/Reverse_proxy), you can do that.
|
||||
See `man oddmu-apache` and `man oddmu-nginx` for some configuration
|
||||
examples.
|
||||
|
||||
This is how you can prevent some actions by simply not passing them on
|
||||
to Oddmu, or you can require authentication for certain actions.
|
||||
Furthermore, you can do the same for directories, allowing you to use
|
||||
subdirectories as separate sites, each with their own editors.
|
||||
|
||||
## References
|
||||
|
||||
[Writing Web Applications](https://golang.org/doc/articles/wiki/)
|
||||
|
||||
8
RELEASE
8
RELEASE
@@ -3,10 +3,12 @@ When preparing a new release
|
||||
|
||||
1. Run tests
|
||||
|
||||
2. Make docs
|
||||
2. Update man/oddmu-releases.7.txt
|
||||
|
||||
3. Make sure all files are checked in
|
||||
3. make docs
|
||||
|
||||
4. Update man/oddmu-releases.7.txt
|
||||
4. Make sure all files are checked in
|
||||
|
||||
5. Tag the release and push the tag to all remotes
|
||||
|
||||
6. cd man && make upload
|
||||
26
accounts.go
26
accounts.go
@@ -15,8 +15,8 @@ import (
|
||||
// environment variable ODDMU_WEBFINGER to "1".
|
||||
var useWebfinger = false
|
||||
|
||||
// Accounts contains the map used to set the usernames. Make sure to lock and unlock as appropriate.
|
||||
type Accounts struct {
|
||||
// accountStore controlls access to the usernames. Make sure to lock and unlock as appropriate.
|
||||
type accountStore struct {
|
||||
sync.RWMutex
|
||||
|
||||
// uris is a map, mapping account names likes "@alex@alexschroeder.ch" to URIs like
|
||||
@@ -25,7 +25,7 @@ type Accounts struct {
|
||||
}
|
||||
|
||||
// accounts holds the global mapping of accounts to profile URIs.
|
||||
var accounts Accounts
|
||||
var accounts accountStore
|
||||
|
||||
// This is called once at startup and therefore does not need to be locked. On every restart, this map starts empty and
|
||||
// is slowly repopulated as pages are visited.
|
||||
@@ -36,11 +36,11 @@ func init() {
|
||||
}
|
||||
}
|
||||
|
||||
// account links a social media account like @account@domain to a profile page like https://domain/user/account. Any
|
||||
// account seen for the first time uses a best guess profile URI. It is also looked up using webfinger, in parallel. See
|
||||
// accountLink links a social media accountLink like @accountLink@domain to a profile page like https://domain/user/accountLink. Any
|
||||
// accountLink seen for the first time uses a best guess profile URI. It is also looked up using webfinger, in parallel. See
|
||||
// lookUpAccountUri. If the lookup succeeds, the best guess is replaced with the new URI so on subsequent requests, the
|
||||
// URI is correct.
|
||||
func account(p *parser.Parser, data []byte, offset int) (int, ast.Node) {
|
||||
func accountLink(p *parser.Parser, data []byte, offset int) (int, ast.Node) {
|
||||
data = data[offset:]
|
||||
i := 1 // skip @ of username
|
||||
n := len(data)
|
||||
@@ -105,7 +105,7 @@ func lookUpAccountUri(account, domain string) {
|
||||
log.Printf("Failed to read from %s: %s", account, err)
|
||||
return
|
||||
}
|
||||
var wf WebFinger
|
||||
var wf webFinger
|
||||
err = json.Unmarshal([]byte(body), &wf)
|
||||
if err != nil {
|
||||
log.Printf("Failed to parse the JSON from %s: %s", account, err)
|
||||
@@ -121,24 +121,24 @@ func lookUpAccountUri(account, domain string) {
|
||||
accounts.uris[account] = uri
|
||||
}
|
||||
|
||||
// Link a link in the WebFinger JSON.
|
||||
type Link struct {
|
||||
// link a link in the WebFinger JSON.
|
||||
type link struct {
|
||||
Rel string `json:"rel"`
|
||||
Type string `json:"type"`
|
||||
Href string `json:"href"`
|
||||
}
|
||||
|
||||
// WebFinger is a structure used to unmarshall JSON.
|
||||
type WebFinger struct {
|
||||
// webFinger is a structure used to unmarshall JSON.
|
||||
type webFinger struct {
|
||||
Subject string `json:"subject"`
|
||||
Aliases []string `json:"aliases"`
|
||||
Links []Link `json:"links"`
|
||||
Links []link `json:"links"`
|
||||
}
|
||||
|
||||
// parseWebFinger parses the web finger JSON and returns the profile page URI. For unmarshalling the JSON, it uses the
|
||||
// Link and WebFinger structs.
|
||||
func parseWebFinger(body []byte) (string, error) {
|
||||
var wf WebFinger
|
||||
var wf webFinger
|
||||
err := json.Unmarshal(body, &wf)
|
||||
if err != nil {
|
||||
return "", err
|
||||
|
||||
17
feed.go
17
feed.go
@@ -11,16 +11,33 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Item is a Page plus a Date.
|
||||
type Item struct {
|
||||
|
||||
// Page is the page being used as the feed item.
|
||||
Page
|
||||
|
||||
// Date is the last modification date of the file storing the page. As the pages used by Oddmu are plain
|
||||
// Markdown files, they don't contain any metadata. Instead, the last modification date of the file is used.
|
||||
// This makes it work well with changes made to the files outside of Oddmu.
|
||||
Date string
|
||||
}
|
||||
|
||||
// Feed is an Item used for the feed itself, plus an array of items based on the linked pages.
|
||||
type Feed struct {
|
||||
|
||||
// Item is the page containing the list of links. It's title is used for the feed and it's last modified time is
|
||||
// used for the publication date. Thus, if linked pages change but the page with the links doesn't change, the
|
||||
// publication date remains unchanged.
|
||||
Item
|
||||
|
||||
// Items are based on the pages linked in list items starting with an asterisk ("*"). Links in
|
||||
// list items starting with a minus ("-") are ignored!
|
||||
Items []Item
|
||||
}
|
||||
|
||||
// feed returns a RSS 2.0 feed for any page. The feed items it contains are the pages linked from in list items starting
|
||||
// with an asterisk ("*").
|
||||
func feed(p *Page, ti time.Time) *Feed {
|
||||
feed := new(Feed)
|
||||
feed.Name = p.Name
|
||||
|
||||
31
index.go
31
index.go
@@ -16,9 +16,8 @@ import (
|
||||
|
||||
type docid uint
|
||||
|
||||
// Index contains the two maps used for search. Make sure to lock and
|
||||
// unlock as appropriate.
|
||||
type Index struct {
|
||||
// indexStore controls access to the maps used for search. Make sure to lock and unlock as appropriate.
|
||||
type indexStore struct {
|
||||
sync.RWMutex
|
||||
|
||||
// next_id is the number of the next document added to the index
|
||||
@@ -34,14 +33,14 @@ type Index struct {
|
||||
titles map[string]string
|
||||
}
|
||||
|
||||
var index Index
|
||||
var index indexStore
|
||||
|
||||
func init() {
|
||||
index.reset()
|
||||
}
|
||||
|
||||
// reset the index. This assumes that the index is locked. It's useful for tests.
|
||||
func (idx *Index) reset() {
|
||||
func (idx *indexStore) reset() {
|
||||
idx.next_id = 0
|
||||
idx.token = make(map[string][]docid)
|
||||
idx.documents = make(map[docid]string)
|
||||
@@ -49,7 +48,7 @@ func (idx *Index) reset() {
|
||||
}
|
||||
|
||||
// addDocument adds the text as a new document. This assumes that the index is locked!
|
||||
func (idx *Index) addDocument(text []byte) docid {
|
||||
func (idx *indexStore) addDocument(text []byte) docid {
|
||||
id := idx.next_id
|
||||
idx.next_id++
|
||||
for _, token := range hashtags(text) {
|
||||
@@ -66,7 +65,7 @@ func (idx *Index) addDocument(text []byte) docid {
|
||||
}
|
||||
|
||||
// deleteDocument deletes all references to the id. The id can no longer be used. This assumes that the index is locked.
|
||||
func (idx *Index) deleteDocument(id docid) {
|
||||
func (idx *indexStore) deleteDocument(id docid) {
|
||||
// Looping through all tokens makes sense if there are few tokens (like hashtags). It doesn't make sense if the
|
||||
// number of tokens is large (like for full-text search or a trigram index).
|
||||
for token, ids := range idx.token {
|
||||
@@ -87,7 +86,7 @@ func (idx *Index) deleteDocument(id docid) {
|
||||
|
||||
// deletePageName determines the document id based on the page name and calls deleteDocument to delete all references.
|
||||
// This assumes that the index is unlocked.
|
||||
func (idx *Index) deletePageName(name string) {
|
||||
func (idx *indexStore) deletePageName(name string) {
|
||||
idx.Lock()
|
||||
defer idx.Unlock()
|
||||
var id docid
|
||||
@@ -106,12 +105,12 @@ func (idx *Index) deletePageName(name string) {
|
||||
}
|
||||
|
||||
// remove the page from the index. Do this when deleting a page. This assumes that the index is unlocked.
|
||||
func (idx *Index) remove(p *Page) {
|
||||
func (idx *indexStore) remove(p *Page) {
|
||||
idx.deletePageName(p.Name)
|
||||
}
|
||||
|
||||
// load loads all the pages and indexes them. This takes a while. It returns the number of pages indexed.
|
||||
func (idx *Index) load() (int, error) {
|
||||
func (idx *indexStore) load() (int, error) {
|
||||
idx.Lock()
|
||||
defer idx.Unlock()
|
||||
err := filepath.Walk(".", idx.walk)
|
||||
@@ -123,7 +122,7 @@ func (idx *Index) load() (int, error) {
|
||||
}
|
||||
|
||||
// walk reads a file and adds it to the index. This assumes that the index is locked.
|
||||
func (idx *Index) walk(path string, info fs.FileInfo, err error) error {
|
||||
func (idx *indexStore) walk(path string, info fs.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -149,7 +148,7 @@ func (idx *Index) walk(path string, info fs.FileInfo, err error) error {
|
||||
}
|
||||
|
||||
// addPage adds a page to the index. This assumes that the index is locked.
|
||||
func (idx *Index) addPage(p *Page) {
|
||||
func (idx *indexStore) addPage(p *Page) {
|
||||
id := idx.addDocument(p.Body)
|
||||
idx.documents[id] = p.Name
|
||||
p.handleTitle(false)
|
||||
@@ -157,14 +156,14 @@ func (idx *Index) addPage(p *Page) {
|
||||
}
|
||||
|
||||
// add a page to the index. This assumes that the index is unlocked.
|
||||
func (idx *Index) add(p *Page) {
|
||||
func (idx *indexStore) add(p *Page) {
|
||||
idx.Lock()
|
||||
defer idx.Unlock()
|
||||
idx.addPage(p)
|
||||
}
|
||||
|
||||
// dump prints the index to the log for debugging.
|
||||
func (idx *Index) dump() {
|
||||
func (idx *indexStore) dump() {
|
||||
idx.RLock()
|
||||
defer idx.RUnlock()
|
||||
for token, ids := range idx.token {
|
||||
@@ -173,14 +172,14 @@ func (idx *Index) dump() {
|
||||
}
|
||||
|
||||
// updateIndex updates the index for a single page.
|
||||
func (idx *Index) update(p *Page) {
|
||||
func (idx *indexStore) update(p *Page) {
|
||||
idx.remove(p)
|
||||
idx.add(p)
|
||||
}
|
||||
|
||||
// search searches the index for a query string and returns page
|
||||
// names.
|
||||
func (idx *Index) search(q string) []string {
|
||||
func (idx *indexStore) search(q string) []string {
|
||||
idx.RLock()
|
||||
defer idx.RUnlock()
|
||||
names := make([]string, 0)
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
)
|
||||
|
||||
func TestIndexAdd(t *testing.T) {
|
||||
idx := &Index{}
|
||||
idx := &indexStore{}
|
||||
idx.reset()
|
||||
idx.Lock()
|
||||
defer idx.Unlock()
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
.nh
|
||||
.ad l
|
||||
.\" Begin generated content:
|
||||
.TH "ODDMU-HTML" "1" "2024-02-17"
|
||||
.TH "ODDMU-HTML" "1" "2024-02-26"
|
||||
.PP
|
||||
.SH NAME
|
||||
.PP
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
.nh
|
||||
.ad l
|
||||
.\" Begin generated content:
|
||||
.TH "ODDMU-LIST" "1" "2024-02-17"
|
||||
.TH "ODDMU-LIST" "1" "2024-02-24"
|
||||
.PP
|
||||
.SH NAME
|
||||
.PP
|
||||
@@ -13,7 +13,7 @@ oddmu-list - list page names and titles from the command-line
|
||||
.PP
|
||||
.SH SYNOPSIS
|
||||
.PP
|
||||
\fBoddmu list\fR [-dir string]
|
||||
\fBoddmu list\fR [-dir \fIstring\fR]
|
||||
.PP
|
||||
.SH DESCRIPTION
|
||||
.PP
|
||||
|
||||
@@ -6,7 +6,7 @@ oddmu-list - list page names and titles from the command-line
|
||||
|
||||
# SYNOPSIS
|
||||
|
||||
*oddmu list* [-dir string]
|
||||
*oddmu list* [-dir _string_]
|
||||
|
||||
# DESCRIPTION
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
.nh
|
||||
.ad l
|
||||
.\" Begin generated content:
|
||||
.TH "ODDMU-RELEASES" "7" "2024-02-20"
|
||||
.TH "ODDMU-RELEASES" "7" "2024-02-26"
|
||||
.PP
|
||||
.SH NAME
|
||||
.PP
|
||||
@@ -15,9 +15,13 @@ oddmu-releases - what'\&s new in this releases?\&
|
||||
.PP
|
||||
This page lists user-visible features and template changes to consider.\&
|
||||
.PP
|
||||
.SS 1.8 (2024)
|
||||
.PP
|
||||
No user-visible changes.\& Documentation and code comments got better.\&
|
||||
.PP
|
||||
.SS 1.7 (2024)
|
||||
.PP
|
||||
Allow upload of multiple files.\& This requires an update to the upload.\&html
|
||||
Allow upload of multiple files.\& This requires an update to the \fIupload.\&html\fR
|
||||
template: Add the \fImultiple\fR attribute to the file input element and change the
|
||||
label from "file" to "files".\&
|
||||
.PP
|
||||
|
||||
@@ -8,9 +8,13 @@ oddmu-releases - what's new in this releases?
|
||||
|
||||
This page lists user-visible features and template changes to consider.
|
||||
|
||||
## 1.8 (2024)
|
||||
|
||||
No user-visible changes. Documentation and code comments got better.
|
||||
|
||||
## 1.7 (2024)
|
||||
|
||||
Allow upload of multiple files. This requires an update to the upload.html
|
||||
Allow upload of multiple files. This requires an update to the _upload.html_
|
||||
template: Add the _multiple_ attribute to the file input element and change the
|
||||
label from "file" to "files".
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
.nh
|
||||
.ad l
|
||||
.\" Begin generated content:
|
||||
.TH "ODDMU-STATIC" "1" "2024-02-17"
|
||||
.TH "ODDMU-STATIC" "1" "2024-02-26"
|
||||
.PP
|
||||
.SH NAME
|
||||
.PP
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
.nh
|
||||
.ad l
|
||||
.\" Begin generated content:
|
||||
.TH "ODDMU-VERSION" "1" "2024-02-17"
|
||||
.TH "ODDMU-VERSION" "1" "2024-02-23"
|
||||
.PP
|
||||
.SH NAME
|
||||
.PP
|
||||
@@ -13,22 +13,22 @@ oddmu-version - print build info on the command-line
|
||||
.PP
|
||||
.SH SYNOPSIS
|
||||
.PP
|
||||
\fBoddmu version\fR
|
||||
\fBoddmu version\fR [-full]
|
||||
.PP
|
||||
.SH DESCRIPTION
|
||||
.PP
|
||||
The "version" subcommand prints a lot of stuff used to build the binary,
|
||||
including the git revision, git repository, versions of dependencies used and
|
||||
more.\&
|
||||
The "version" subcommand prints information related to the version control
|
||||
system state when it was built: what remote was used, what commit was checked
|
||||
out, whether there were any local changes were made.\&
|
||||
.PP
|
||||
It'\&s the equivalent of running this:
|
||||
.SH OPTIONS
|
||||
.PP
|
||||
.nf
|
||||
\fB-full\fR
|
||||
.RS 4
|
||||
go version -m oddmu
|
||||
.fi
|
||||
.RE
|
||||
Print a lot more information, including the versions of dependencies
|
||||
used.\& It'\&s the equivalent of running "go version -m oddmu".\&
|
||||
.PP
|
||||
.RE
|
||||
.SH SEE ALSO
|
||||
.PP
|
||||
\fIoddmu\fR(1)
|
||||
|
||||
@@ -6,19 +6,19 @@ oddmu-version - print build info on the command-line
|
||||
|
||||
# SYNOPSIS
|
||||
|
||||
*oddmu version*
|
||||
*oddmu version* [-full]
|
||||
|
||||
# DESCRIPTION
|
||||
|
||||
The "version" subcommand prints a lot of stuff used to build the binary,
|
||||
including the git revision, git repository, versions of dependencies used and
|
||||
more.
|
||||
The "version" subcommand prints information related to the version control
|
||||
system state when it was built: what remote was used, what commit was checked
|
||||
out, whether there were any local changes were made.
|
||||
|
||||
It's the equivalent of running this:
|
||||
# OPTIONS
|
||||
|
||||
```
|
||||
go version -m oddmu
|
||||
```
|
||||
*-full*
|
||||
Print a lot more information, including the versions of dependencies
|
||||
used. It's the equivalent of running "go version -m oddmu".
|
||||
|
||||
# SEE ALSO
|
||||
|
||||
|
||||
37
man/oddmu.1
37
man/oddmu.1
@@ -5,7 +5,7 @@
|
||||
.nh
|
||||
.ad l
|
||||
.\" Begin generated content:
|
||||
.TH "ODDMU" "1" "2024-02-19"
|
||||
.TH "ODDMU" "1" "2024-02-23"
|
||||
.PP
|
||||
.SH NAME
|
||||
.PP
|
||||
@@ -353,11 +353,30 @@ Note that some HTML file names are special: they act as templates.\& See
|
||||
\fIoddmu\fR(5), about the markup syntax and how feeds are generated based on link
|
||||
lists
|
||||
.IP \(bu 4
|
||||
\fIoddmu.\&service\fR(5), on how to run the service under systemd
|
||||
\fIoddmu-releases\fR(7), on what features are part of the latest release
|
||||
.IP \(bu 4
|
||||
\fIoddmu-filter\fR(7), on how to treat subdirectories as separate sites
|
||||
.IP \(bu 4
|
||||
\fIoddmu-search\fR(7), on how search works
|
||||
.IP \(bu 4
|
||||
\fIoddmu-templates\fR(5), on how to write the HTML templates
|
||||
.PD
|
||||
.PP
|
||||
If you run Oddmu as a web server:
|
||||
.PP
|
||||
.PD 0
|
||||
.IP \(bu 4
|
||||
\fIoddmu-apache\fR(5), on how to set up Apache as a reverse proxy
|
||||
.IP \(bu 4
|
||||
\fIoddmu-filter\fR(7), on how to treat subdirectories as separate sites
|
||||
\fIoddmu-nginx\fR(5), on how to set up freenginx as a reverse proxy
|
||||
.IP \(bu 4
|
||||
\fIoddmu.\&service\fR(5), on how to run the service under systemd
|
||||
.PD
|
||||
.PP
|
||||
If you run Oddmu as a static site generator or pages offline and sync them with
|
||||
Oddmu running as a webserver:
|
||||
.PP
|
||||
.PD 0
|
||||
.IP \(bu 4
|
||||
\fIoddmu-html\fR(1), on how to render a page from the command-line
|
||||
.IP \(bu 4
|
||||
@@ -365,23 +384,15 @@ lists
|
||||
.IP \(bu 4
|
||||
\fIoddmu-missing\fR(1), on how to find broken local links from the command-line
|
||||
.IP \(bu 4
|
||||
\fIoddmu-nginx\fR(5), on how to set up freenginx as a reverse proxy
|
||||
.IP \(bu 4
|
||||
\fIoddmu-releases\fR(7), on what features are part of the latest release
|
||||
\fIoddmu-notify\fR(1), on updating index, changes and hashtag pages from the
|
||||
command-line
|
||||
.IP \(bu 4
|
||||
\fIoddmu-replace\fR(1), on how to search and replace text from the command-line
|
||||
.IP \(bu 4
|
||||
\fIoddmu-search\fR(1), on how to run a search from the command-line
|
||||
.IP \(bu 4
|
||||
\fIoddmu-search\fR(7), on how search works
|
||||
.IP \(bu 4
|
||||
\fIoddmu-static\fR(1), on generating a static site from the command-line
|
||||
.IP \(bu 4
|
||||
\fIoddmu-notify\fR(1), on updating index, changes and hashtag pages from the
|
||||
command-line
|
||||
.IP \(bu 4
|
||||
\fIoddmu-templates\fR(5), on how to write the HTML templates
|
||||
.IP \(bu 4
|
||||
\fIoddmu-version\fR(1), on how to get all the build information from the binary
|
||||
.PD
|
||||
.PP
|
||||
|
||||
@@ -295,21 +295,28 @@ _oddmu-templates_(5) for their names and their use.
|
||||
|
||||
- _oddmu_(5), about the markup syntax and how feeds are generated based on link
|
||||
lists
|
||||
- _oddmu.service_(5), on how to run the service under systemd
|
||||
- _oddmu-apache_(5), on how to set up Apache as a reverse proxy
|
||||
- _oddmu-releases_(7), on what features are part of the latest release
|
||||
- _oddmu-filter_(7), on how to treat subdirectories as separate sites
|
||||
- _oddmu-search_(7), on how search works
|
||||
- _oddmu-templates_(5), on how to write the HTML templates
|
||||
|
||||
If you run Oddmu as a web server:
|
||||
|
||||
- _oddmu-apache_(5), on how to set up Apache as a reverse proxy
|
||||
- _oddmu-nginx_(5), on how to set up freenginx as a reverse proxy
|
||||
- _oddmu.service_(5), on how to run the service under systemd
|
||||
|
||||
If you run Oddmu as a static site generator or pages offline and sync them with
|
||||
Oddmu running as a webserver:
|
||||
|
||||
- _oddmu-html_(1), on how to render a page from the command-line
|
||||
- _oddmu-list_(1), on how to list pages and titles from the command-line
|
||||
- _oddmu-missing_(1), on how to find broken local links from the command-line
|
||||
- _oddmu-nginx_(5), on how to set up freenginx as a reverse proxy
|
||||
- _oddmu-releases_(7), on what features are part of the latest release
|
||||
- _oddmu-replace_(1), on how to search and replace text from the command-line
|
||||
- _oddmu-search_(1), on how to run a search from the command-line
|
||||
- _oddmu-search_(7), on how search works
|
||||
- _oddmu-static_(1), on generating a static site from the command-line
|
||||
- _oddmu-notify_(1), on updating index, changes and hashtag pages from the
|
||||
command-line
|
||||
- _oddmu-templates_(5), on how to write the HTML templates
|
||||
- _oddmu-replace_(1), on how to search and replace text from the command-line
|
||||
- _oddmu-search_(1), on how to run a search from the command-line
|
||||
- _oddmu-static_(1), on generating a static site from the command-line
|
||||
- _oddmu-version_(1), on how to get all the build information from the binary
|
||||
|
||||
# AUTHORS
|
||||
|
||||
32
man_test.go
32
man_test.go
@@ -28,3 +28,35 @@ func TestManPages(t *testing.T) {
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func TestReadme(t *testing.T) {
|
||||
b, err := os.ReadFile("README.md")
|
||||
main := string(b)
|
||||
assert.NoError(t, err)
|
||||
filepath.Walk("man", func(path string, info fs.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if strings.HasSuffix(path, ".txt") {
|
||||
s := strings.TrimPrefix(path, "man/")
|
||||
s = strings.TrimSuffix(s, ".txt")
|
||||
i := strings.LastIndex(s, ".")
|
||||
ref := "[" + s[:i] + "(" + s[i+1:] + ")]"
|
||||
assert.Contains(t, main, ref, ref)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
filepath.Walk(".", func(path string, info fs.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if strings.HasSuffix(path, ".go") &&
|
||||
!strings.HasSuffix(path, "_test.go") &&
|
||||
!strings.HasSuffix(path, "_cmd.go") {
|
||||
s := strings.TrimPrefix(path, "./")
|
||||
ref := "`" + s + "`"
|
||||
assert.Contains(t, main, ref, ref)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ func wikiParser() (*parser.Parser, *[]string) {
|
||||
fn, hashtags := hashtag()
|
||||
parser.RegisterInline('#', fn)
|
||||
if useWebfinger {
|
||||
parser.RegisterInline('@', account)
|
||||
parser.RegisterInline('@', accountLink)
|
||||
}
|
||||
return parser, hashtags
|
||||
}
|
||||
|
||||
14
templates.go
14
templates.go
@@ -17,15 +17,19 @@ import (
|
||||
var templateFiles = []string{"edit.html", "add.html", "view.html",
|
||||
"diff.html", "search.html", "static.html", "upload.html", "feed.html"}
|
||||
|
||||
// templates are the parsed HTML templates used. See renderTemplate and loadTemplates. Subdirectories may contain their
|
||||
// own templates which override the templates in the root directory. If so, they are not filepaths. Use
|
||||
// filepath.ToSlash() if necessary.
|
||||
type Template struct {
|
||||
// templateStore controls access to map of parsed HTML templates. Make sure to lock and unlock as appropriate. See
|
||||
// renderTemplate and loadTemplates.
|
||||
type templateStore struct {
|
||||
sync.RWMutex
|
||||
|
||||
// template is a map of parsed HTML templates. The key is their path name. By default, the map only contains
|
||||
// top-level templates like "view.html". Subdirectories may contain their own templates which override the
|
||||
// templates in the root directory. If so, they are paths like "dir/view.html", not filepaths. Use
|
||||
// filepath.ToSlash() if necessary.
|
||||
template map[string]*template.Template
|
||||
}
|
||||
|
||||
var templates Template
|
||||
var templates templateStore
|
||||
|
||||
// loadTemplates loads the templates. If templates have already been loaded, return immediately.
|
||||
func loadTemplates() {
|
||||
|
||||
@@ -19,7 +19,7 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Upload struct {
|
||||
type upload struct {
|
||||
Dir string
|
||||
Name string
|
||||
Last string
|
||||
@@ -34,7 +34,7 @@ var lastRe = regexp.MustCompile(`^(.*)([0-9]+)(.*)$`)
|
||||
// parameters are used to copy name, maxwidth and quality from the previous upload. If the previous name contains a
|
||||
// number, this is incremented by one.
|
||||
func uploadHandler(w http.ResponseWriter, r *http.Request, dir string) {
|
||||
data := &Upload{Dir: dir}
|
||||
data := &upload{Dir: dir}
|
||||
maxwidth := r.FormValue("maxwidth")
|
||||
if maxwidth != "" {
|
||||
data.MaxWidth = maxwidth
|
||||
|
||||
45
watch.go
45
watch.go
@@ -12,18 +12,33 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Watches holds a map and a mutex. The map contains the template names that have been requested and the exact time at
|
||||
// which they have been requested. Adding the same file multiple times, such as when the watch function sees multiple
|
||||
// Write events for the same file, the time keeps getting updated so that when the go routine runs, it only acts on
|
||||
// files that haven't been updated in the last second. The go routine is what forces us to use the RWMutex for the map.
|
||||
type Watches struct {
|
||||
// watchStore controls access to the maps used by the filesystem watches. Make sure to lock and unlock as appropriate.
|
||||
// The maps are used to control a sort of queue for files that need reloading (if a template) or reindexing (if a page).
|
||||
// File system notifications add files to the queue in order to handle changes made without Oddmu, while Oddmu is
|
||||
// running.
|
||||
type watchStore struct {
|
||||
sync.RWMutex
|
||||
ignores map[string]time.Time
|
||||
|
||||
// files contains the filenames that have been queued for reloading (if a template) or reindexing (if a page)
|
||||
// and the exact time at which they have been added. When the same file is added multiple times, such as when
|
||||
// the watchStore function sees multiple Write events for the same file, the time keeps getting updated so that
|
||||
// when the watchTimer runs, it only acts on files that haven't been updated in the last second.
|
||||
files map[string]time.Time
|
||||
|
||||
// ignores contains the files that some code intends to change, knowing that subsequent writes events would
|
||||
// result in file system notifications that would end up adding the filenames to the queue for reloading (if a
|
||||
// template) or reindexing (if a page). When Oddmu is making the changes, it can ignore the corresponding
|
||||
// notifications by the file system. Those notifications are consequences of Oddmu doing its job. In other
|
||||
// words, Oddmu does not rely on file system notification even it is Oddmu doing the changes. This avoids a 1s
|
||||
// when changing templates, for example.
|
||||
ignores map[string]time.Time
|
||||
|
||||
// watcher is the pointer to the actual watcher doing the file system watching. It watches a set of paths.
|
||||
// Whenever Oddmu creates a new subdirectory, it adds the path for this subdirectory to the watcher.
|
||||
watcher *fsnotify.Watcher
|
||||
}
|
||||
|
||||
var watches Watches
|
||||
var watches watchStore
|
||||
|
||||
func init() {
|
||||
watches.ignores = make(map[string]time.Time)
|
||||
@@ -31,7 +46,7 @@ func init() {
|
||||
}
|
||||
|
||||
// install initializes watches and installs watchers for all directories and subdirectories.
|
||||
func (w *Watches) install() (int, error) {
|
||||
func (w *watchStore) install() (int, error) {
|
||||
// create a watcher for the root directory and never close it
|
||||
var err error
|
||||
w.watcher, err = fsnotify.NewWatcher()
|
||||
@@ -48,7 +63,7 @@ func (w *Watches) install() (int, error) {
|
||||
}
|
||||
|
||||
// add installs a watch for every directory that isn't hidden. Note that the root directory (".") is not skipped.
|
||||
func (w *Watches) add(path string, info fs.FileInfo, err error) error {
|
||||
func (w *watchStore) add(path string, info fs.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -72,7 +87,7 @@ func (w *Watches) add(path string, info fs.FileInfo, err error) error {
|
||||
// adds an entry to the files map, or updates the file's time, and starts a go routine. Example: If a file gets three
|
||||
// consecutive Write events, the first two go routine invocations won't do anything, since the time kept getting
|
||||
// updated. Only the last invocation will act upon the event.
|
||||
func (w *Watches) watch() {
|
||||
func (w *watchStore) watch() {
|
||||
for {
|
||||
select {
|
||||
case err, ok := <-w.watcher.Errors:
|
||||
@@ -94,7 +109,7 @@ func (w *Watches) watch() {
|
||||
// Incidentally, this also prevents rsync updates from generating activity ("stat ./.index.md.tTfPFg: no such file or
|
||||
// directory"). Note the painful details: If moving a file into a watched directory, a Create event is received. If a
|
||||
// new file is created in a watched directory, a Create event and one or more Write events is received.
|
||||
func (w *Watches) watchHandle(e fsnotify.Event) {
|
||||
func (w *watchStore) watchHandle(e fsnotify.Event) {
|
||||
path := strings.TrimPrefix(e.Name, "./")
|
||||
if strings.HasPrefix(filepath.Base(path), ".") {
|
||||
return
|
||||
@@ -130,7 +145,7 @@ func (w *Watches) watchHandle(e fsnotify.Event) {
|
||||
|
||||
// watchTimer checks if the file hasn't been updated in 1s and if so, it calls watchDoUpdate. If another write has
|
||||
// updated the file, do nothing because another watchTimer will run at the appropriate time and check again.
|
||||
func (w *Watches) watchTimer(path string) {
|
||||
func (w *watchStore) watchTimer(path string) {
|
||||
t, ok := w.files[path]
|
||||
if ok && t.Add(time.Second).Before(time.Now().Add(time.Nanosecond)) {
|
||||
delete(w.files, path)
|
||||
@@ -141,7 +156,7 @@ func (w *Watches) watchTimer(path string) {
|
||||
// Do the right thing right now. For Create events such as directories being created or files being moved into a watched
|
||||
// directory, this is the right thing to do. When a file is being written to, watchHandle will have started a timer and
|
||||
// will call this function after 1s of no more writes. If, however, the path is in the ignores map, do nothing.
|
||||
func (w *Watches) watchDoUpdate(path string) {
|
||||
func (w *watchStore) watchDoUpdate(path string) {
|
||||
_, ignored := w.ignores[path]
|
||||
if ignored {
|
||||
return
|
||||
@@ -170,7 +185,7 @@ func (w *Watches) watchDoUpdate(path string) {
|
||||
|
||||
// watchDoRemove removes files from the index or discards templates. If the path in question is in the ignores map, do
|
||||
// nothing.
|
||||
func (w *Watches) watchDoRemove(path string) {
|
||||
func (w *watchStore) watchDoRemove(path string) {
|
||||
_, ignored := w.ignores[path]
|
||||
if ignored {
|
||||
return
|
||||
@@ -189,7 +204,7 @@ func (w *Watches) watchDoRemove(path string) {
|
||||
|
||||
// ignore is before code that is known suspected save files and trigger watchHandle eventhough the code already handles
|
||||
// this. This is achieved by adding the path to the ignores map for 1s.
|
||||
func (w *Watches) ignore(path string) {
|
||||
func (w *watchStore) ignore(path string) {
|
||||
w.Lock()
|
||||
defer w.Unlock()
|
||||
w.ignores[path] = time.Now()
|
||||
|
||||
23
wiki.go
23
wiki.go
@@ -1,3 +1,6 @@
|
||||
// Oddmu is a wiki web server and a static site generator.
|
||||
//
|
||||
// The types exported are the ones needed to write the templates.
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -139,6 +142,24 @@ func scheduleInstallWatcher() {
|
||||
}
|
||||
}
|
||||
|
||||
// serve starts the web server using [http.Serve]. The listener is determined via [getListener]. The various handlers
|
||||
// are created using [makeHandler] if their path starts with an action segment. For example, the URL path "/view/index"
|
||||
// is understood to contain the "view" action and so [viewHandler] is called with the argument "index". The one handler
|
||||
// that doesn't need this is [rootHandler].
|
||||
//
|
||||
// The handlers often come in pairs. One handler to show the user interface and one handler to make the change:
|
||||
// - [editHandler] shows the edit form and [saveHandler] saves changes to a page
|
||||
// - [addHandler] shows the add form and [appendHandler] appends the addition to a page
|
||||
// - [uploadHandler] shows the upload form and [dropHandler] saves the uploaded files
|
||||
//
|
||||
// Some handlers only do something and the links or forms to call them is expected to be part of the view template:
|
||||
// - [archiveHandler] zips up the current directory
|
||||
// - [diffHandler] shows the changes made in the last 60min to a page
|
||||
// - [searchHandler] shows search results
|
||||
//
|
||||
// At the same time as the server starts up, pages are indexed via [scheduleLoadIndex], languages are loaded via
|
||||
// [scheduleLoadLanguages] and the current directory and its subdirectories is watched for changes using watchers
|
||||
// installed via [scheduleInstallWatcher].
|
||||
func serve() {
|
||||
http.HandleFunc("/", rootHandler)
|
||||
http.HandleFunc("/archive/", makeHandler(archiveHandler, true))
|
||||
@@ -185,6 +206,8 @@ func commands() {
|
||||
os.Exit(int(subcommands.Execute(ctx)))
|
||||
}
|
||||
|
||||
// main runs [serve] if called without arguments and it runs [commands] if called with arguments.
|
||||
// The first argument is the subcommand.
|
||||
func main() {
|
||||
if len(os.Args) == 1 {
|
||||
serve()
|
||||
|
||||
Reference in New Issue
Block a user