10 Commits
v1.6 ... v1.7

Author SHA1 Message Date
Alex Schroeder
745500f09f Describe features of release 1.7 2024-02-20 21:54:14 +01:00
Alex Schroeder
f02491dda0 Add -full to version command 2024-02-20 21:47:59 +01:00
Alex Schroeder
0001583044 Go fmt 2024-02-20 21:47:50 +01:00
Alex Schroeder
4a5b7d52cd Use exiffix library to fix image orientation
If you upload an unedited image from an iPhone, the EXIF data
indicates how it should be oriented. Oddmu used to resize images strip
the EXIF data, resulting in resized images that were not oriented
correctly. If the EXIF data is stripped, the image has to be rotated.
In order to do this, the exiffix single-function library is used to
work around "image/jpeg: correct for EXIF orientation? #4341", opened
in 2012. See https://github.com/golang/go/issues/4341 for more
information and a link to https://github.com/edwvee/exiffix.
2024-02-20 21:45:51 +01:00
Alex Schroeder
c65f3ea386 Switch image manipulation library
Switch from github.com/anthonynsimon/bild/imgio
to github.com/disintegration/imaging. The goal is to get some sort of
auto-orientation going.

Image resizing now uses Lanczos instead of Linear.
2024-02-20 20:13:46 +01:00
Alex Schroeder
d2adffed6e Test HEIC upload
The bashdrew/goheif library is now used implicity and image decoding
figures out how to do it on its own.

The bashdrew/goheif only does decoding, not encoding, and so the HEIC
testfile is base64 encoded.
2024-02-20 13:09:22 +01:00
Alex Schroeder
d8e1d79127 Wording changes for the upload.html template 2024-02-19 22:44:33 +01:00
Alex Schroeder
d839219f96 Upload multiple files in one go
This requires an update to the upload.html template.
2024-02-19 22:27:56 +01:00
Alex Schroeder
103d1f4609 Add oddmu-nginx man page 2024-02-19 17:38:22 +01:00
Alex Schroeder
28a63e7479 Return 404 for feed requests of non-existing pages
Otherwise, there's a panic if requesting the RSS feed of a
non-existing page. This was caused by a recent rewrite of the
viewHandler.
2024-02-19 11:10:21 +01:00
27 changed files with 488 additions and 204 deletions

View File

@@ -27,34 +27,34 @@ func archiveHandler(w http.ResponseWriter, r *http.Request, path string) {
matches := re.MatchString(path)
dir := filepath.Dir(filepath.FromSlash(path))
z := zip.NewWriter(w)
err = filepath.Walk(dir, func (path string, info fs.FileInfo, err error) error {
err = filepath.Walk(dir, func(path string, info fs.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() {
if path != "." && strings.HasPrefix(filepath.Base(path), ".") {
return filepath.SkipDir
}
} else if !strings.HasPrefix(filepath.Base(path), ".") &&
(matches || !re.MatchString(path)) {
zf, err := z.Create(path)
if err != nil {
log.Println(err)
return err
}
if info.IsDir() {
if path != "." && strings.HasPrefix(filepath.Base(path), ".") {
return filepath.SkipDir
}
} else if !strings.HasPrefix(filepath.Base(path), ".") &&
(matches || !re.MatchString(path)) {
zf, err := z.Create(path)
if err != nil {
log.Println(err)
return err
}
file, err := os.Open(path)
if err != nil {
log.Println(err)
return err
}
_, err = io.Copy(zf, file)
if err != nil {
log.Println(err)
return err
}
file, err := os.Open(path)
if err != nil {
log.Println(err)
return err
}
return nil
})
_, err = io.Copy(zf, file)
if err != nil {
log.Println(err)
return err
}
}
return nil
})
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)

View File

@@ -4,8 +4,8 @@ import (
"archive/zip"
"github.com/stretchr/testify/assert"
"os"
"testing"
"strings"
"testing"
)
func TestArchive(t *testing.T) {

View File

@@ -2,6 +2,7 @@ package main
import (
"github.com/stretchr/testify/assert"
"net/http"
"testing"
)
@@ -11,6 +12,11 @@ func TestFeed(t *testing.T) {
"Welcome to Oddµ")
}
func TestNoFeed(t *testing.T) {
assert.HTTPStatusCode(t,
makeHandler(viewHandler, true), "GET", "/view/no-feed.rss", nil, http.StatusNotFound)
}
func TestFeedItems(t *testing.T) {
cleanup(t, "testdata/feed")
index.load()

5
go.mod
View File

@@ -3,9 +3,11 @@ module alexschroeder.ch/cgit/oddmu
go 1.21.0
require (
github.com/anthonynsimon/bild v0.13.0
github.com/bashdrew/goheif v0.0.0-20230406184952-7a08ca9c9bdd
github.com/charmbracelet/lipgloss v0.9.1
github.com/disintegration/imaging v1.6.2
github.com/edwvee/exiffix v0.0.0-20210922235313-0f6cbda5e58f
github.com/fsnotify/fsnotify v1.7.0
github.com/gomarkdown/markdown v0.0.0-20231222211730-1d6d20845b47
github.com/google/subcommands v1.2.0
github.com/hexops/gotextdiff v1.0.3
@@ -20,7 +22,6 @@ require (
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/gorilla/css v1.0.1 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect

33
go.sum
View File

@@ -1,7 +1,3 @@
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/anthonynsimon/bild v0.13.0 h1:mN3tMaNds1wBWi1BrJq0ipDBhpkooYfu7ZFSMhXt1C8=
github.com/anthonynsimon/bild v0.13.0/go.mod h1:tpzzp0aYkAsMi1zmfhimaDyX1xjn2OUc1AJZK/TF0AE=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
@@ -10,14 +6,13 @@ github.com/bashdrew/goheif v0.0.0-20230406184952-7a08ca9c9bdd h1:SxkQeH4jjXT0zMg
github.com/bashdrew/goheif v0.0.0-20230406184952-7a08ca9c9bdd/go.mod h1:p1sbxRy+MY71fEWHcfRmerC8WUYXDFCExF9A7aXwp98=
github.com/charmbracelet/lipgloss v0.9.1 h1:PNyd3jvaJbg4jRHKWXnCj1akQm4rh8dbEzN1p/u1KWg=
github.com/charmbracelet/lipgloss v0.9.1/go.mod h1:1mPmG4cxScwUQALAAnacHaigiiHB9Pmr+v1VEawJl6I=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
github.com/edwvee/exiffix v0.0.0-20210922235313-0f6cbda5e58f h1:RMnUwTnNR070mFAEIoqMYjNirHj8i0h79VXTYyBCyVA=
github.com/edwvee/exiffix v0.0.0-20210922235313-0f6cbda5e58f/go.mod h1:KoE3Ti1qbQXCb3s/XGj0yApHnbnNnn1bXTtB5Auq/Vc=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/gomarkdown/markdown v0.0.0-20231222211730-1d6d20845b47 h1:k4Tw0nt6lwro3Uin8eqoET7MDA4JnT8YgbCjc/g5E3k=
@@ -28,10 +23,8 @@ github.com/google/subcommands v1.2.0 h1:vWQspBTo2nEqTUFita5/KeEWlUL8kQObDFbub/EN
github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
@@ -39,7 +32,6 @@ github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
@@ -47,13 +39,10 @@ github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZ
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58=
github.com/microcosm-cc/bluemonday v1.0.26/go.mod h1:JyzOCs9gkyQyjs+6h10UEVSe02CGwkhd72Xdqh78TWs=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pemistahl/lingua-go v1.4.0 h1:ifYhthrlW7iO4icdubwlduYnmwU37V1sbNrwhKBR4rM=
github.com/pemistahl/lingua-go v1.4.0/go.mod h1:ECuM1Hp/3hvyh7k8aWSqNCPlTxLemFZsRjocUf3KgME=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -62,35 +51,23 @@ github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJ
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.6 h1:Sovz9sDSwbOz9tgUy8JpT+KgCkPYJEN/oYzlJiYTNLg=
github.com/rivo/uniseg v0.4.6/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd h1:CmH9+J6ZSsIjUK3dcGsnCnO41eRBOnY12zwkn5qVwgc=
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk=
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a h1:Q8/wZp0KX97QFTc2ywcOE0YRjZPVIx+MXInMzdvQqcA=
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08=
golang.org/x/image v0.0.0-20190703141733-d6a02ce849c9/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8=
golang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=
golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=

View File

@@ -5,7 +5,7 @@
.nh
.ad l
.\" Begin generated content:
.TH "ODDMU-APACHE" "5" "2024-02-17"
.TH "ODDMU-APACHE" "5" "2024-02-19"
.PP
.SH NAME
.PP
@@ -390,7 +390,7 @@ such that ever domain acts as a reverse proxy to a different Oddmu instance.\&
.PP
.SH SEE ALSO
.PP
\fIoddmu\fR(1), \fIoddmu-filter\fR(7)
\fIoddmu\fR(1), \fIoddmu-filter\fR(7), \fIoddmu-nginx\fR(5)
.PP
"Apache Core Features".\&
https://httpd.\&apache.\&org/docs/current/mod/core.\&html

View File

@@ -337,7 +337,7 @@ such that ever domain acts as a reverse proxy to a different Oddmu instance.
# SEE ALSO
_oddmu_(1), _oddmu-filter_(7)
_oddmu_(1), _oddmu-filter_(7), _oddmu-nginx_(5)
"Apache Core Features".
https://httpd.apache.org/docs/current/mod/core.html

View File

@@ -5,7 +5,7 @@
.nh
.ad l
.\" Begin generated content:
.TH "ODDMU-FILTER" "7" "2024-02-17"
.TH "ODDMU-FILTER" "7" "2024-02-19"
.PP
.SH NAME
.PP
@@ -52,11 +52,11 @@ always does: search is limited to "project/" and its subdirectories.\&
If the subdirectory is a private site, then you need to use ODDMU_FILTER to
exclude it from directory tree actions in the main site, and you need to
configure your web server such that it doesn'\&t allow visitors access to the
directory tree without authentication.\& See \fIoddmu-apache\fR(5).\&
directory tree without authentication.\&
.PP
.SH SEE ALSO
.PP
\fIoddmu\fR(1), \fIoddmu-search\fR(7), \fIoddmu-apache\fR(5)
\fIoddmu\fR(1), \fIoddmu-search\fR(7), \fIoddmu-apache\fR(5), \fIoddmu-nginx\fR(5)
.PP
.SH AUTHORS
.PP

View File

@@ -45,11 +45,11 @@ always does: search is limited to "project/" and its subdirectories.
If the subdirectory is a private site, then you need to use ODDMU_FILTER to
exclude it from directory tree actions in the main site, and you need to
configure your web server such that it doesn't allow visitors access to the
directory tree without authentication. See _oddmu-apache_(5).
directory tree without authentication.
# SEE ALSO
_oddmu_(1), _oddmu-search_(7), _oddmu-apache_(5)
_oddmu_(1), _oddmu-search_(7), _oddmu-apache_(5), _oddmu-nginx_(5)
# AUTHORS

91
man/oddmu-nginx.5 Normal file
View File

@@ -0,0 +1,91 @@
.\" Generated by scdoc 1.11.3
.\" Complete documentation for this program is not available as a GNU info page
.ie \n(.g .ds Aq \(aq
.el .ds Aq '
.nh
.ad l
.\" Begin generated content:
.TH "ODDMU-NGINX" "5" "2024-02-19"
.PP
.SH NAME
.PP
oddmu-nginx - how to setup Nginx as a reverse proxy for Oddmu
.PP
.SS DESCRIPTION
.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.\&
.PP
This page explains how to setup NGINX on Debian to act as a reverse proxy for
Oddmu.\& Once this is done, you can use NGINX to provide HTTPS, request users to
authenticate themselves, and so on.\&
.PP
.SS CONFIGURATION
.PP
The site is defined in "/etc/nginx/sites-available/default", in the \fIserver\fR
section.\& Add a new \fIlocation\fR section after the existing \fIlocation\fR section:
.PP
.nf
.RS 4
location ~ ^/(view|diff|edit|save|add|append|upload|drop|search|archive)/ {
proxy_pass http://localhost:8080;
}
.fi
.RE
.PP
If you remove an action from the regular expression, those requests no longer
get passed on to Oddmu.\& They are essentially disabled.\& Somebody on the same
machine pointing their browser at http://localhost:8080/ directly would still
have access to all the actions, of course.\&
.PP
To restrict access to some actions, use two different \fIlocation\fR sections:
.PP
.nf
.RS 4
# public
location ~ ^/(view|diff|search)/ {
proxy_pass http://localhost:8080;
}
# password required
location ~ ^/(edit|save|add|append|upload|drop|archive)/ {
auth_basic "Oddmu author";
auth_basic_user_file /etc/nginx/conf\&.d/htpasswd;
proxy_pass http://localhost:8080;
}
.fi
.RE
.PP
The passwords in "/etc/nginx/conf.\&d/htpasswd" are generated using \fIopenssl\fR(1).\&
Assuming the password is "CPTk&qO[Y@?\&M~L>qKOkd", this is how you encrypt it:
.PP
.nf
.RS 4
openssl passwd \&'CPTk&qO[Y@?M~L>qKOkd\&'
.fi
.RE
.PP
The output gets used in "/etc/nginx/conf.\&d/htpasswd".\& Here'\&s the user "alex"
using this password:
.PP
.nf
.RS 4
alex:$1$DOwphABk$W4VmR9p8t2\&.htxF6ctXHX\&.
.fi
.RE
.PP
.SH SEE ALSO
.PP
\fIoddmu\fR(1), \fIoddmu-apache\fR(5)
.PP
"freenginx"
http://freenginx.\&org/
.PP
"freenginx ngx_http_proxy_module", proxy_pass
http://freenginx.\&org/en/docs/http/ngx_http_proxy_module.\&html#proxy_pass
.PP
"freenginx ngx_http_auth_basic_module"
http://freenginx.\&org/en/docs/http/ngx_http_auth_basic_module.\&html
.PP
.SH AUTHORS
.PP
Maintained by Alex Schroeder <alex@gnu.\&org>.\&

76
man/oddmu-nginx.5.txt Normal file
View File

@@ -0,0 +1,76 @@
ODDMU-NGINX(5)
# NAME
oddmu-nginx - how to setup Nginx as a reverse proxy for Oddmu
## DESCRIPTION
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.
This page explains how to setup NGINX on Debian to act as a reverse proxy for
Oddmu. Once this is done, you can use NGINX to provide HTTPS, request users to
authenticate themselves, and so on.
## CONFIGURATION
The site is defined in "/etc/nginx/sites-available/default", in the _server_
section. Add a new _location_ section after the existing _location_ section:
```
location ~ ^/(view|diff|edit|save|add|append|upload|drop|search|archive)/ {
proxy_pass http://localhost:8080;
}
```
If you remove an action from the regular expression, those requests no longer
get passed on to Oddmu. They are essentially disabled. Somebody on the same
machine pointing their browser at http://localhost:8080/ directly would still
have access to all the actions, of course.
To restrict access to some actions, use two different _location_ sections:
```
# public
location ~ ^/(view|diff|search)/ {
proxy_pass http://localhost:8080;
}
# password required
location ~ ^/(edit|save|add|append|upload|drop|archive)/ {
auth_basic "Oddmu author";
auth_basic_user_file /etc/nginx/conf.d/htpasswd;
proxy_pass http://localhost:8080;
}
```
The passwords in "/etc/nginx/conf.d/htpasswd" are generated using _openssl_(1).
Assuming the password is "CPTk&qO[Y@?M~L>qKOkd", this is how you encrypt it:
```
openssl passwd 'CPTk&qO[Y@?M~L>qKOkd'
```
The output gets used in "/etc/nginx/conf.d/htpasswd". Here's the user "alex"
using this password:
```
alex:$1$DOwphABk$W4VmR9p8t2.htxF6ctXHX.
```
# SEE ALSO
_oddmu_(1), _oddmu-apache_(5)
"freenginx"
http://freenginx.org/
"freenginx ngx_http_proxy_module", proxy_pass
http://freenginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_pass
"freenginx ngx_http_auth_basic_module"
http://freenginx.org/en/docs/http/ngx_http_auth_basic_module.html
# AUTHORS
Maintained by Alex Schroeder <alex@gnu.org>.

View File

@@ -5,7 +5,7 @@
.nh
.ad l
.\" Begin generated content:
.TH "ODDMU-RELEASES" "7" "2024-02-17"
.TH "ODDMU-RELEASES" "7" "2024-02-20"
.PP
.SH NAME
.PP
@@ -13,9 +13,22 @@ oddmu-releases - what'\&s new in this releases?\&
.PP
.SH DESCRIPTION
.PP
This page lists user-visible features.\&
This page lists user-visible features and template changes to consider.\&
.PP
.SS 1.6 (unrelease)
.SS 1.7 (2024)
.PP
Allow upload of multiple files.\& This requires an update to the upload.\&html
template: Add the \fImultiple\fR attribute to the file input element and change the
label from "file" to "files".\&
.PP
Fix orientation of uploaded images.\& JPG and HEIC images have EXIF data telling a
viewer how to orient the image.\& Oddmu now uses this information to rotate the
image correctly before stripping it.\&
.PP
The version command now displays much less information unless given the -full
argument.\&
.PP
.SS 1.6 (2024)
.PP
Add \fIarchive\fR action to serve a zip file.\&
.PP

View File

@@ -6,11 +6,20 @@ oddmu-releases - what's new in this releases?
# DESCRIPTION
This page lists user-visible features.
This page lists user-visible features and template changes to consider.
## Next
## 1.7 (2024)
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".
Fix orientation of uploaded images. JPG and HEIC images have EXIF data telling a
viewer how to orient the image. Oddmu now uses this information to rotate the
image correctly before stripping it.
The version command now displays much less information unless given the -full
argument.
## 1.6 (2024)

View File

@@ -5,7 +5,7 @@
.nh
.ad l
.\" Begin generated content:
.TH "ODDMU-SEARCH" "7" "2024-02-17"
.TH "ODDMU-SEARCH" "7" "2024-02-19"
.PP
.SH NAME
.PP
@@ -95,12 +95,12 @@ directory that doesn'\&t match, subdirectories that do match are skipped.\& See
\fIoddmu-filter\fR(7).\&
.PP
To prevent access to a private directory tree, you must configure the web server
in addition to setting the ODDMU_FILTER environment variable.\& See
\fIoddmu-apache\fR(5) for more.\&
in addition to setting the ODDMU_FILTER environment variable.\&
.PP
.SH SEE ALSO
.PP
\fIoddmu\fR(1), \fIoddmu-search\fR(1), \fIoddmu-filter\fR(7), \fIoddmu-apache\fR(5)
\fIoddmu\fR(1), \fIoddmu-search\fR(1), \fIoddmu-filter\fR(7), \fIoddmu-apache\fR(5),
\fIoddmu-nginx\fR(5)
.PP
.SH AUTHORS
.PP

View File

@@ -75,12 +75,12 @@ directory that doesn't match, subdirectories that do match are skipped. See
_oddmu-filter_(7).
To prevent access to a private directory tree, you must configure the web server
in addition to setting the ODDMU_FILTER environment variable. See
_oddmu-apache_(5) for more.
in addition to setting the ODDMU_FILTER environment variable.
# SEE ALSO
_oddmu_(1), _oddmu-search_(1), _oddmu-filter_(7), _oddmu-apache_(5)
_oddmu_(1), _oddmu-search_(1), _oddmu-filter_(7), _oddmu-apache_(5),
_oddmu-nginx_(5)
# AUTHORS

View File

@@ -5,7 +5,7 @@
.nh
.ad l
.\" Begin generated content:
.TH "ODDMU" "1" "2024-02-17"
.TH "ODDMU" "1" "2024-02-19"
.PP
.SH NAME
.PP
@@ -350,11 +350,12 @@ Note that some HTML file names are special: they act as templates.\& See
.PP
.PD 0
.IP \(bu 4
\fIoddmu\fR(5), about the markup syntax and how feeds are generated based on link lists
\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
.IP \(bu 4
\fIoddmu-apache\fR(5), on how to set up a web server such as Apache
\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
.IP \(bu 4
@@ -364,6 +365,8 @@ Note that some HTML file names are special: they act as templates.\& See
.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
.IP \(bu 4
\fIoddmu-replace\fR(1), on how to search and replace text from the command-line

View File

@@ -293,13 +293,15 @@ _oddmu-templates_(5) for their names and their use.
# SEE ALSO
- _oddmu_(5), about the markup syntax and how feeds are generated based on link lists
- _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 a web server such as Apache
- _oddmu-apache_(5), on how to set up Apache as a reverse proxy
- _oddmu-filter_(7), on how to treat subdirectories as separate sites
- _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

View File

@@ -3,17 +3,17 @@ package main
import (
"github.com/stretchr/testify/assert"
"io/fs"
"path/filepath"
"os"
"path/filepath"
"strings"
"testing"
)
func TestManPages(t *testing.T) {
b, err := os.ReadFile("man/oddmu.1.txt");
b, err := os.ReadFile("man/oddmu.1.txt")
main := string(b)
assert.NoError(t, err)
filepath.Walk("man", func (path string, info fs.FileInfo, err error) error {
filepath.Walk("man", func(path string, info fs.FileInfo, err error) error {
if err != nil {
return err
}
@@ -28,4 +28,3 @@ func TestManPages(t *testing.T) {
return nil
})
}

View File

@@ -88,7 +88,7 @@ func (p *Page) save() error {
// to it ("~"). This is true even if the file refers to a binary file like "image.png" and most applications don't know
// what to do with a file called "image.png~". This expects a file path. Use filepath.FromSlash(path) if necessary.
func backup(fp string) error {
_, err := os.Stat(fp)
_, err := os.Stat(fp)
if err != nil {
return nil
}
@@ -105,7 +105,7 @@ func backup(fp string) error {
// undefined (there is no caching).
func loadPage(path string) (*Page, error) {
path = strings.TrimPrefix(path, "./") // result of a filepath.TreeWalk starting with "."
body, err := os.ReadFile(filepath.FromSlash(path+".md"))
body, err := os.ReadFile(filepath.FromSlash(path + ".md"))
if err != nil {
return nil, err
}

View File

@@ -14,7 +14,7 @@ label { display: inline-block; width: 20ch }
</style>
</head>
<body lang="en">
<h1>Upload File</h1>
<h1>Upload Files</h1>
{{if ne .Last ""}}
<p>Previous upload: <a href="/view/{{.Last}}">{{.Last}}</a></p>
{{if .Image}}
@@ -23,23 +23,24 @@ label { display: inline-block; width: 20ch }
{{end}}
<form action="/drop/{{.Dir}}" method="POST" enctype="multipart/form-data">
<p>When uploading pictures from a phone, its filename is going to be something cryptic like IMG_1234.JPG.
Please provide your own filename. End the filename with "-1" to auto-increment.
Please provide your own filename. End the base name with "-1" to auto-increment.
Use <tt>.jpg</tt> or <tt>.png</tt> as the extension.
<p><label for="text">Filename to use:</label>
<input id="text" name="name" value="{{.Name}}" type="text" placeholder="image-1.jpg" autofocus required>
<p>If the uploaded file is a picture from a phone, it is going to be too big for your site.
Sadly, resizing only works for JPG and PNG files. Luckily, most pictures from a phone camera are JPG images.
Sadly, resizing only works when uploading <tt>.jpeg</tt>, <tt>.heic</tt> and <tt>.png</tt> files.
Feel free to specify a max width of 1200 pixels, for example.
<p><label for="maxwidth">Max width:</label>
<input id="maxwidth" name="maxwidth" value="{{.MaxWidth}}" type="number" min="10" placeholder="1200">
<p>If the uploaded file is a JPEG-encoded picture, like most pictures from a phone, you can specify a quality.
<p>If the filename you provided above ends in <tt>.jpg</tt>, you can specify a quality.
Typically, a quality of 60 is not too bad and a quality of 90 is more than enough.
<p><label for="quality">Quality:</label>
<input id="quality" name="quality" value="{{.Quality}}" type="number" min="1" max="99" placeholder="75">
<p>Finally, pick the file or photo to upload.
Picture metadata is only removed if the picture gets resized.
<p>Finally, pick the files or photos to upload.
Picture metadata is only removed if the pictures gets resized.
Providing a new max width is recommended for all pictures.
<p><label for="file">Pick file to upload:</label>
<input type="file" name="file" required>
<p><label for="file">Pick files to upload:</label>
<input type="file" name="file" required multiple>
<p><input type="submit" value="Save">
<a href="/view/index"><button type="button">Cancel</button></a></p>
</form>

View File

@@ -1,11 +1,12 @@
package main
// The imaging library uses image.Decode internally. This function can use all image decoders available at that time.
// This is why we import goheif for side effects: HEIC files are read correctly.
import (
"github.com/anthonynsimon/bild/imgio"
"github.com/anthonynsimon/bild/transform"
"github.com/bashdrew/goheif"
"image/jpeg"
"image/png"
_ "github.com/bashdrew/goheif"
"github.com/disintegration/imaging"
"github.com/edwvee/exiffix"
"io"
"log"
"net/http"
@@ -52,17 +53,24 @@ func uploadHandler(w http.ResponseWriter, r *http.Request, dir string) {
data.Image = true
}
data.Last = path.Join(dir, last)
m := lastRe.FindStringSubmatch(last)
if m != nil {
n, err := strconv.Atoi(m[2])
if err == nil {
data.Name = m[1] + strconv.Itoa(n+1) + m[3]
}
}
data.Name, _ = next(last)
}
renderTemplate(w, dir, "upload", data)
}
// next returns the next name for a string matching lastRe. The last number in the given string is incremented by one
// ("a2b" → "a3b"). The second return value indicates whether such a replacement was made or not.
func next(s string) (string, bool) {
m := lastRe.FindStringSubmatch(s)
if m != nil {
n, err := strconv.Atoi(m[2])
if err == nil {
return m[1] + strconv.Itoa(n+1) + m[3], true
}
}
return s, false
}
// dropHandler takes the "name" form field and the "file" form file and saves the file under the given name. The browser
// is redirected to the view of that file. Some errors are for the users and some are for users and the admins. Those
// later errors are printed, too.
@@ -80,116 +88,125 @@ func dropHandler(w http.ResponseWriter, r *http.Request, dir string) {
}
data := url.Values{}
name := r.FormValue("name")
data.Set("last", name)
filename := filepath.Base(name)
// no overwriting of hidden files or adding subdirectories
if strings.HasPrefix(filename, ".") || filepath.Dir(name) != "." {
http.Error(w, "no filename", http.StatusForbidden)
return
}
file, _, err := r.FormFile("file")
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
defer file.Close()
path := filepath.Join(d, filename)
watches.ignore(path)
err = backup(path)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
dst, err := os.Create(path)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer dst.Close()
// if a resize was requested
// prepare for image encoding (saving) with the encoder based on the desired file name extensions
var format imaging.Format
quality := 75
maxwidth := r.FormValue("maxwidth")
mw := 0
if len(maxwidth) > 0 {
mw, err := strconv.Atoi(maxwidth)
mw, err = strconv.Atoi(maxwidth)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
data.Add("maxwidth", maxwidth)
// determine how the file will be written
ext := strings.ToLower(filepath.Ext(path))
var encoder imgio.Encoder
ext := strings.ToLower(filepath.Ext(filename))
switch ext {
case ".png":
encoder = imgio.PNGEncoder()
format = imaging.PNG
case ".jpg", ".jpeg":
q := jpeg.DefaultQuality
quality := r.FormValue("quality")
if len(quality) > 0 {
q, err = strconv.Atoi(quality)
q := r.FormValue("quality")
if len(q) > 0 {
quality, err = strconv.Atoi(q)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
data.Add("quality", quality)
data.Add("quality", q)
}
encoder = imgio.JPEGEncoder(q)
format = imaging.JPEG
default:
http.Error(w, "Resizing images requires a .png, .jpg or .jpeg extension for the filename", http.StatusBadRequest)
return
}
// try and decode the data in various formats
img, err := jpeg.Decode(file)
}
first := true
for _, fhs := range r.MultipartForm.File["file"] {
file, err := fhs.Open()
if err != nil {
img, err = png.Decode(file)
}
if err != nil {
img, err = goheif.Decode(file)
}
if err != nil {
http.Error(w, "The image could not be decoded (only PNG, JPG and HEIC formats are supported for resizing)", http.StatusBadRequest)
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
rect := img.Bounds()
width := rect.Max.X - rect.Min.X
if width > mw {
height := (rect.Max.Y - rect.Min.Y) * mw / width
img = transform.Resize(img, mw, height, transform.Linear)
if err := imgio.Save(path, img, encoder); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
defer file.Close()
if !first {
s, ok := next(filename)
if ok {
filename = s
} else {
ext := filepath.Ext(s)
filename = s[:len(s)-len(ext)] + "-1" + ext
}
} else {
http.Error(w, "The file is too small for this", http.StatusBadRequest)
return
}
} else {
// just copy the bytes
n, err := io.Copy(dst, file)
first = false
path := filepath.Join(d, filename)
watches.ignore(path)
err = backup(path)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// if zero bytes were copied, delete the file instead
if n == 0 {
err := os.Remove(path)
dst, err := os.Create(path)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer dst.Close()
if mw > 0 {
// do not use imaging.Decode(file, imaging.AutoOrientation(true)) because that only works for JPEG files
img, fmt, err := exiffix.Decode(file)
if err != nil {
http.Error(w, "The image could not be decoded (only PNG, JPG and HEIC formats are supported for resizing)", http.StatusBadRequest)
return
}
log.Println("Decoded", fmt, "file")
res := imaging.Resize(img, mw, 0, imaging.Lanczos) // preserve aspect ratio
// imaging functions don't return errors but empty images…
if !res.Rect.Empty() {
img = res
}
// images are always reencoded, so image quality goes down
err = imaging.Encode(dst, img, format, imaging.JPEGQuality(quality))
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
log.Println("Delete", path)
} else {
// just copy the bytes
n, err := io.Copy(dst, file)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// if zero bytes were copied, delete the file instead
if n == 0 {
err := os.Remove(path)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
log.Println("Delete", path)
}
}
username, _, ok := r.BasicAuth()
if ok {
log.Println("Save", path, "by", username)
} else {
log.Println("Save", path)
}
updateTemplate(path)
}
username, _, ok := r.BasicAuth()
if ok {
log.Println("Save", path, "by", username)
} else {
log.Println("Save", path)
}
updateTemplate(path)
data.Set("last", filename) // has no slashes
http.Redirect(w, r, "/upload/"+dir+"?"+data.Encode(), http.StatusFound)
}

View File

@@ -2,6 +2,7 @@ package main
import (
"bytes"
"encoding/base64"
"github.com/stretchr/testify/assert"
"image"
"image/jpeg"
@@ -68,6 +69,34 @@ func TestUploadJpg(t *testing.T) {
writer.FormDataContentType(), form, "/upload/testdata/jpg/?last=ok.jpg")
}
func TestUploadHeic(t *testing.T) {
cleanup(t, "testdata/heic")
// for uploads, the directory is not created automatically
os.MkdirAll("testdata/heic", 0755)
form := new(bytes.Buffer)
writer := multipart.NewWriter(form)
field, _ := writer.CreateFormField("name")
field.Write([]byte("ok.jpg")) // target
file, _ := writer.CreateFormFile("file", "ok.heic") // source
// convert -size 1x1 canvas: heic:- | base64
imgBase64 := `
AAAAGGZ0eXBoZWljAAAAAG1pZjFoZWljAAABqm1ldGEAAAAAAAAAIWhkbHIAAAAAAAAAAHBpY3QA
AAAAAAAAAAAAAAAAAAAADnBpdG0AAAAAAAIAAAAQaWRhdAAAAAAAAQABAAAAOGlsb2MBAAAAREAA
AgABAAAAAAAAAcoAAQAAAAAAAAAtAAIAAQAAAAAAAAABAAAAAAAAAAgAAAA4aWluZgAAAAAAAgAA
ABVpbmZlAgAAAQABAABodmMxAAAAABVpbmZlAgAAAAACAABncmlkAAAAANVpcHJwAAAAs2lwY28A
AABzaHZjQwEDcAAAAAAAAAAAAB7wAPz9+PgAAA8DIAABABhAAQwB//8DcAAAAwCQAAADAAADAB66
AkAhAAEAJ0IBAQNwAAADAJAAAAMAAAMAHqAggQWW6q6a5sCAAAADAIAAAAMAhCIAAQAGRAHBc8GJ
AAAAFGlzcGUAAAAAAAAAQAAAAEAAAAAUaXNwZQAAAAAAAAABAAAAAQAAABBwaXhpAAAAAAMICAgA
AAAaaXBtYQAAAAAAAAACAAECgQIAAgIDhAAAABppcmVmAAAAAAAAAA5kaW1nAAIAAQABAAAANW1k
YXQAAAApKAGvEyE1mvXho5qH3STtzcWnOxedwNIXAKNDaJNqz3uONoCHeUhi/HA=`
img, err := base64.StdEncoding.DecodeString(imgBase64)
assert.NoError(t, err)
file.Write(img)
writer.Close()
HTTPUploadAndRedirectTo(t, makeHandler(dropHandler, false), "/drop/testdata/heic/",
writer.FormDataContentType(), form, "/upload/testdata/heic/?last=ok.jpg")
}
func TestDeleteFile(t *testing.T) {
cleanup(t, "testdata/delete")
os.MkdirAll("testdata/delete", 0755)
@@ -176,3 +205,52 @@ There is no answer`)}
body = assert.HTTPBody(makeHandler(uploadHandler, false), "GET", url.Path, values)
assert.Contains(t, body, `src="/view/testdata/dir/test.jpg"`)
}
func TestUploadTwoInOne(t *testing.T) {
cleanup(t, "testdata/two")
os.MkdirAll("testdata/two", 0755)
form := new(bytes.Buffer)
writer := multipart.NewWriter(form)
field, _ := writer.CreateFormField("name")
field.Write([]byte("2024-02-19-hike-1.jpg"))
file1, _ := writer.CreateFormFile("file", "one.jpg")
img1 := image.NewRGBA(image.Rect(0, 0, 10, 10))
jpeg.Encode(file1, img1, &jpeg.Options{Quality: 90})
file2, _ := writer.CreateFormFile("file", "two.jpg")
img2 := image.NewRGBA(image.Rect(0, 0, 20, 20))
jpeg.Encode(file2, img2, &jpeg.Options{Quality: 90})
writer.Close()
location := HTTPUploadLocation(t, makeHandler(dropHandler, false), "/drop/testdata/two/",
writer.FormDataContentType(), form)
url, _ := url.Parse(location)
assert.Equal(t, "/upload/testdata/two/", url.Path, "Redirect to upload location")
values := url.Query()
assert.Equal(t, "2024-02-19-hike-2.jpg", values.Get("last"))
// check the files
assert.FileExists(t, "testdata/two/2024-02-19-hike-1.jpg")
assert.FileExists(t, "testdata/two/2024-02-19-hike-2.jpg")
}
func TestUploadTwoInOneAgain(t *testing.T) {
cleanup(t, "testdata/zwei")
os.MkdirAll("testdata/zwei", 0755)
form := new(bytes.Buffer)
writer := multipart.NewWriter(form)
field, _ := writer.CreateFormField("name")
field.Write([]byte("image.jpg")) // cannot be incremented!
file1, _ := writer.CreateFormFile("file", "one.jpg")
img1 := image.NewRGBA(image.Rect(0, 0, 10, 10))
jpeg.Encode(file1, img1, &jpeg.Options{Quality: 90})
file2, _ := writer.CreateFormFile("file", "two.jpg")
img2 := image.NewRGBA(image.Rect(0, 0, 20, 20))
jpeg.Encode(file2, img2, &jpeg.Options{Quality: 90})
writer.Close()
location := HTTPUploadLocation(t, makeHandler(dropHandler, false), "/drop/testdata/zwei/",
writer.FormDataContentType(), form)
url, _ := url.Parse(location)
assert.Equal(t, "/upload/testdata/zwei/", url.Path, "Redirect to upload location")
values := url.Query()
assert.Equal(t, "image-1.jpg", values.Get("last"))
// check the files
assert.FileExists(t, "testdata/zwei/image.jpg")
assert.FileExists(t, "testdata/zwei/image-1.jpg")
}

View File

@@ -8,32 +8,43 @@ import (
"io"
"os"
"runtime/debug"
"strings"
)
type versionCmd struct {
full bool
}
func (cmd *versionCmd) SetFlags(f *flag.FlagSet) {
f.BoolVar(&cmd.full, "full", false, "show all the debug information")
}
func (*versionCmd) Name() string { return "version" }
func (*versionCmd) Synopsis() string { return "report build information" }
func (*versionCmd) Usage() string {
return `version:
Report all the debug information about this build.
return `version [-full]:
Report the exact version control commit this is built from, or the
full debug information about this build.
`
}
func (cmd *versionCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
return versionCli(os.Stdout, f.Args())
return versionCli(os.Stdout, cmd.full, f.Args())
}
func versionCli(w io.Writer, args []string) subcommands.ExitStatus {
if len(args) > 0 {
fmt.Fprintln(os.Stderr, "Version takes no arguments.")
return subcommands.ExitFailure
func versionCli(w io.Writer, full bool, args []string) subcommands.ExitStatus {
info, ok := debug.ReadBuildInfo()
if !ok {
fmt.Println("This binary contains no debug info.")
} else if full {
fmt.Println(info)
} else {
fmt.Println(info.Path)
for _, setting := range info.Settings {
if strings.HasPrefix(setting.Key, "vcs") {
fmt.Printf("%s=%s\n", setting.Key, setting.Value)
}
}
}
info, _ := debug.ReadBuildInfo()
fmt.Println(info)
return subcommands.ExitSuccess
}

View File

@@ -9,7 +9,7 @@ import (
func TestVersionCmd(t *testing.T) {
b := new(bytes.Buffer)
s := versionCli(b, nil)
s := versionCli(b, false, nil)
assert.Equal(t, subcommands.ExitSuccess, s)
assert.Contains(t, "vcs.revision", b.String())
}

10
view.go
View File

@@ -46,7 +46,7 @@ func viewHandler(w http.ResponseWriter, r *http.Request, path string) {
t = rss
}
fp := filepath.FromSlash(path)
fi, err := os.Stat(fp+".md")
fi, err := os.Stat(fp + ".md")
if err == nil {
if fi.IsDir() {
t = dir // directory ending in ".md"
@@ -55,6 +55,10 @@ func viewHandler(w http.ResponseWriter, r *http.Request, path string) {
}
// otherwise t == rss
} else {
if t == rss {
http.Error(w, err.Error(), http.StatusNotFound)
return
}
if fp == "" {
fp = "." // make sure Stat works
}
@@ -107,10 +111,6 @@ func viewHandler(w http.ResponseWriter, r *http.Request, path string) {
}
p, err := loadPage(path)
if err != nil {
if t == rss {
http.Error(w, err.Error(), http.StatusNotFound)
return
}
http.Redirect(w, r, "/edit/"+path, http.StatusFound)
return
}

View File

@@ -1,15 +1,15 @@
package main
import (
"github.com/fsnotify/fsnotify"
"github.com/fsnotify/fsnotify"
"io/fs"
"log"
"log"
"os"
"path/filepath"
"slices"
"strings"
"sync"
"time"
"slices"
"strings"
"sync"
"time"
)
// Watches holds a map and a mutex. The map contains the template names that have been requested and the exact time at
@@ -17,9 +17,9 @@ import (
// 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 {
sync.RWMutex
sync.RWMutex
ignores map[string]time.Time
files map[string]time.Time
files map[string]time.Time
watcher *fsnotify.Watcher
}
@@ -97,12 +97,12 @@ func (w *Watches) watch() {
func (w *Watches) watchHandle(e fsnotify.Event) {
path := strings.TrimPrefix(e.Name, "./")
if strings.HasPrefix(filepath.Base(path), ".") {
return;
return
}
// log.Println(e)
w.Lock()
defer w.Unlock()
if e.Op.Has(fsnotify.Create | fsnotify.Write) &&
if e.Op.Has(fsnotify.Create|fsnotify.Write) &&
(strings.HasSuffix(path, ".html") &&
slices.Contains(templateFiles, filepath.Base(path)) ||
strings.HasSuffix(path, ".md")) {

View File

@@ -4,7 +4,7 @@ import (
"github.com/stretchr/testify/assert"
"os"
"testing"
"time"
"time"
)
func TestWatchedPageUpdate(t *testing.T) {
@@ -64,7 +64,7 @@ the smell is everywhere
assert.Contains(t,
assert.HTTPBody(makeHandler(viewHandler, true), "GET", "/view/testdata/watched-template/raclette", nil),
"Skip navigation")
// save a new view handler directly
assert.NoError(t,
os.WriteFile(path,
@@ -82,8 +82,8 @@ the smell is everywhere
watches.Unlock()
watches.watchTimer(path)
body := assert.HTTPBody(makeHandler(viewHandler, true), "GET", "/view/" + name, nil)
body := assert.HTTPBody(makeHandler(viewHandler, true), "GET", "/view/"+name, nil)
assert.Contains(t, body, "<h1>Raclette</h1>") // page text is still there
assert.NotContains(t, body, "Skip") // but the header is not
assert.NotContains(t, body, "Skip") // but the header is not
}