forked from mirror/oddmu
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
745500f09f | ||
|
|
f02491dda0 | ||
|
|
0001583044 | ||
|
|
4a5b7d52cd | ||
|
|
c65f3ea386 | ||
|
|
d2adffed6e | ||
|
|
d8e1d79127 | ||
|
|
d839219f96 | ||
|
|
103d1f4609 | ||
|
|
28a63e7479 |
48
archive.go
48
archive.go
@@ -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)
|
||||
|
||||
@@ -4,8 +4,8 @@ import (
|
||||
"archive/zip"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"os"
|
||||
"testing"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestArchive(t *testing.T) {
|
||||
|
||||
@@ -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
5
go.mod
@@ -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
33
go.sum
@@ -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=
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
91
man/oddmu-nginx.5
Normal 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
76
man/oddmu-nginx.5.txt
Normal 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>.
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
4
page.go
4
page.go
@@ -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
|
||||
}
|
||||
|
||||
17
upload.html
17
upload.html
@@ -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>
|
||||
|
||||
177
upload_drop.go
177
upload_drop.go
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
10
view.go
@@ -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
|
||||
}
|
||||
|
||||
20
watch.go
20
watch.go
@@ -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")) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user