repos / pico

pico services - prose.sh, pastes.sh, imgs.sh, feeds.sh, pgs.sh
git clone https://github.com/picosh/pico.git

commit
8e0b2c1
parent
af76934
author
Eric Bower
date
2022-08-30 03:29:02 +0000 UTC
feat(imgs): web optimized images
12 files changed,  +380, -66
M go.mod
M go.sum
M .gitignore
+1, -0
1@@ -11,3 +11,4 @@ data/*
2 ssh_data
3 .storage
4 __debug_bin
5+.bin
M cmd/scripts/dates/dates.go
+3, -1
 1@@ -9,6 +9,7 @@ import (
 2 
 3 	"git.sr.ht/~erock/pico/db"
 4 	"git.sr.ht/~erock/pico/db/postgres"
 5+	"git.sr.ht/~erock/pico/imgs"
 6 	"git.sr.ht/~erock/pico/lists"
 7 	"git.sr.ht/~erock/pico/shared"
 8 	"git.sr.ht/~erock/pico/wish/cms/config"
 9@@ -97,7 +98,8 @@ func main() {
10 	logger.Info("updating dates")
11 	for _, post := range posts {
12 		if post.Space == "prose" {
13-			parsed, err := shared.ParseText(post.Text, "")
14+			linkify := imgs.NewImgsLinkify(post.Username, true, false)
15+			parsed, err := shared.ParseText(post.Text, linkify)
16 			if err != nil {
17 				logger.Error(err)
18 				continue
M cmd/scripts/tags/tags.go
+3, -1
 1@@ -7,6 +7,7 @@ import (
 2 
 3 	"git.sr.ht/~erock/pico/db"
 4 	"git.sr.ht/~erock/pico/db/postgres"
 5+	"git.sr.ht/~erock/pico/imgs"
 6 	"git.sr.ht/~erock/pico/shared"
 7 	"git.sr.ht/~erock/pico/wish/cms/config"
 8 	"go.uber.org/zap"
 9@@ -75,7 +76,8 @@ func main() {
10 
11 	logger.Info("replacing tags")
12 	for _, post := range posts {
13-		parsed, err := shared.ParseText(post.Text, "")
14+		linkify := imgs.NewImgsLinkify(post.Username, true, false)
15+		parsed, err := shared.ParseText(post.Text, linkify)
16 		if err != nil {
17 			continue
18 		}
M go.mod
+12, -0
 1@@ -21,6 +21,8 @@ require (
 2 	github.com/microcosm-cc/bluemonday v1.0.19
 3 	github.com/minio/minio-go/v7 v7.0.34
 4 	github.com/muesli/reflow v0.3.0
 5+	github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646
 6+	github.com/nickalie/go-webpbin v0.0.0-20220110095747-f10016bf2dc1
 7 	github.com/pkg/sftp v1.13.5
 8 	github.com/yuin/goldmark v1.4.13
 9 	github.com/yuin/goldmark-highlighting v0.0.0-20220208100518-594be1970594
10@@ -41,8 +43,11 @@ require (
11 	github.com/charmbracelet/keygen v0.3.0 // indirect
12 	github.com/containerd/console v1.0.3 // indirect
13 	github.com/dlclark/regexp2 v1.7.0 // indirect
14+	github.com/dsnet/compress v0.0.1 // indirect
15 	github.com/dustin/go-humanize v1.0.0 // indirect
16+	github.com/frankban/quicktest v1.14.3 // indirect
17 	github.com/golang/protobuf v1.5.2 // indirect
18+	github.com/golang/snappy v0.0.4 // indirect
19 	github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
20 	github.com/google/uuid v1.3.0 // indirect
21 	github.com/gorilla/css v1.0.0 // indirect
22@@ -50,11 +55,13 @@ require (
23 	github.com/klauspost/compress v1.15.9 // indirect
24 	github.com/klauspost/cpuid/v2 v2.1.0 // indirect
25 	github.com/kr/fs v0.1.0 // indirect
26+	github.com/kr/pretty v0.3.0 // indirect
27 	github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
28 	github.com/mattn/go-isatty v0.0.16 // indirect
29 	github.com/mattn/go-localereader v0.0.1 // indirect
30 	github.com/mattn/go-runewidth v0.0.13 // indirect
31 	github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
32+	github.com/mholt/archiver v3.1.1+incompatible // indirect
33 	github.com/minio/md5-simd v1.1.2 // indirect
34 	github.com/minio/sha256-simd v1.0.0 // indirect
35 	github.com/mitchellh/go-homedir v1.1.0 // indirect
36@@ -64,6 +71,9 @@ require (
37 	github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70 // indirect
38 	github.com/muesli/cancelreader v0.2.2 // indirect
39 	github.com/muesli/termenv v0.12.0 // indirect
40+	github.com/nickalie/go-binwrapper v0.0.0-20190114141239-525121d43c84 // indirect
41+	github.com/nwaples/rardecode v1.1.0 // indirect
42+	github.com/pierrec/lz4 v2.6.1+incompatible // indirect
43 	github.com/prometheus/client_golang v1.13.0 // indirect
44 	github.com/prometheus/client_model v0.2.0 // indirect
45 	github.com/prometheus/common v0.37.0 // indirect
46@@ -71,6 +81,8 @@ require (
47 	github.com/rivo/uniseg v0.3.4 // indirect
48 	github.com/rs/xid v1.4.0 // indirect
49 	github.com/sirupsen/logrus v1.9.0 // indirect
50+	github.com/ulikunitz/xz v0.5.10 // indirect
51+	github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
52 	go.uber.org/atomic v1.10.0 // indirect
53 	go.uber.org/multierr v1.8.0 // indirect
54 	golang.org/x/net v0.0.0-20220822230855-b0a4917ee28c // indirect
M go.sum
+36, -2
  1@@ -84,18 +84,24 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk
  2 github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
  3 github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw=
  4 github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
  5+github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
  6 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
  7 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
  8 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
  9 github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
 10 github.com/dlclark/regexp2 v1.7.0 h1:7lJfhqlPssTb1WQx4yvTHN0uElPEv52sbaECrAQxjAo=
 11 github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
 12+github.com/dsnet/compress v0.0.1 h1:PlZu0n3Tuv04TzpfPbrnI0HW/YwodEXDS+oPKahKF0Q=
 13+github.com/dsnet/compress v0.0.1/go.mod h1:Aw8dCMJ7RioblQeTqt88akK31OvO8Dhf5JflhBbQEHo=
 14+github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
 15 github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
 16 github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
 17 github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
 18 github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
 19 github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
 20 github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
 21+github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=
 22+github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps=
 23 github.com/gliderlabs/ssh v0.3.4 h1:+AXBtim7MTKaLVPgvE+3mhewYRawNLTd+jEEz/wExZw=
 24 github.com/gliderlabs/ssh v0.3.4/go.mod h1:ZSS+CUoKHDrqVakTfTWUlKSr9MtMFkC4UvtQKD7O914=
 25 github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
 26@@ -139,6 +145,8 @@ github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw
 27 github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
 28 github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
 29 github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
 30+github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
 31+github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
 32 github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
 33 github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
 34 github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
 35@@ -150,6 +158,7 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
 36 github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 37 github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 38 github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 39+github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
 40 github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
 41 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
 42 github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
 43@@ -186,8 +195,10 @@ github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/X
 44 github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
 45 github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
 46 github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
 47+github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
 48 github.com/klauspost/compress v1.15.9 h1:wKRjX6JRtDdrE9qwa4b/Cip7ACOshUI4smpCQanqjSY=
 49 github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU=
 50+github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
 51 github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
 52 github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
 53 github.com/klauspost/cpuid/v2 v2.1.0 h1:eyi1Ad2aNJMW95zcSbmGg7Cg6cq3ADwLpMAP96d8rF0=
 54@@ -197,11 +208,13 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxv
 55 github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
 56 github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
 57 github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
 58-github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
 59 github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
 60+github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
 61+github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
 62 github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 63-github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
 64 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 65+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 66+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
 67 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
 68 github.com/lib/pq v1.10.6 h1:jbk+ZieJ0D7EVGJYpL9QTz7/YW6UHbmdnZWYyK5cdBs=
 69 github.com/lib/pq v1.10.6/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
 70@@ -220,6 +233,8 @@ github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4
 71 github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
 72 github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
 73 github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
 74+github.com/mholt/archiver v3.1.1+incompatible h1:1dCVxuqs0dJseYEhi5pl7MYPH9zDa1wBi7mF09cbNkU=
 75+github.com/mholt/archiver v3.1.1+incompatible/go.mod h1:Dh2dOXnSdiLxRiPoVfIr/fI1TwETms9B8CTWfeh7ROU=
 76 github.com/microcosm-cc/bluemonday v1.0.19 h1:OI7hoF5FY4pFz2VA//RN8TfM0YJ2dJcl4P4APrCWy6c=
 77 github.com/microcosm-cc/bluemonday v1.0.19/go.mod h1:QNzV2UbLK2/53oIIwTOyLUSABMkjZ4tqiyC1g/DyqxE=
 78 github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
 79@@ -254,6 +269,16 @@ github.com/muesli/termenv v0.12.0 h1:KuQRUE3PgxRFWhq4gHvZtPSLCGDqM5q/cYr1pZ39ytc
 80 github.com/muesli/termenv v0.12.0/go.mod h1:WCCv32tusQ/EEZ5S8oUIIrC/nIuBcxCVqlN4Xfkv+7A=
 81 github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
 82 github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
 83+github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
 84+github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
 85+github.com/nickalie/go-binwrapper v0.0.0-20190114141239-525121d43c84 h1:/6MoQlTdk1eAi0J9O89ypO8umkp+H7mpnSF2ggSL62Q=
 86+github.com/nickalie/go-binwrapper v0.0.0-20190114141239-525121d43c84/go.mod h1:Eeech2fhQ/E4bS8cdc3+SGABQ+weQYGyWBvZ/mNr5uY=
 87+github.com/nickalie/go-webpbin v0.0.0-20220110095747-f10016bf2dc1 h1:9awJsNP+gYOGCr3pQu9i217bCNsVwoQCmD3h7CYwxOw=
 88+github.com/nickalie/go-webpbin v0.0.0-20220110095747-f10016bf2dc1/go.mod h1:m5oz0fmp+uyRBxxFkvciIpe1wd2JZ3pDVJ3x/D8/EGw=
 89+github.com/nwaples/rardecode v1.1.0 h1:vSxaY8vQhOcVr4mm5e8XllHWTiM4JF507A0Katqw7MQ=
 90+github.com/nwaples/rardecode v1.1.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
 91+github.com/pierrec/lz4 v2.6.1+incompatible h1:9UY3+iC23yxF0UfGaYrGplQ+79Rg+h/q9FV9ix19jjM=
 92+github.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
 93 github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 94 github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 95 github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
 96@@ -292,6 +317,8 @@ github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJ
 97 github.com/rivo/uniseg v0.3.4 h1:3Z3Eu6FGHZWSfNKJTOUiPatWwfc7DzJRU04jFUqJODw=
 98 github.com/rivo/uniseg v0.3.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
 99 github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
100+github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k=
101+github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
102 github.com/rs/xid v1.4.0 h1:qd7wPTDkN6KQx2VmMBLrpHkiyQwgFXRnkOLacUiaSNY=
103 github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
104 github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
105@@ -308,6 +335,11 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
106 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
107 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
108 github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
109+github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8=
110+github.com/ulikunitz/xz v0.5.10 h1:t92gobL9l3HE202wg3rlk19F6X+JOxl9BBrCCMYEYd8=
111+github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
112+github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo=
113+github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
114 github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
115 github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
116 github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
117@@ -355,6 +387,8 @@ golang.org/x/exp v0.0.0-20220823124025-807a23277127 h1:S4NrSKDfihhl3+4jSTgwoIevK
118 golang.org/x/exp v0.0.0-20220823124025-807a23277127/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
119 golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
120 golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
121+golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d h1:RNPAfi2nHY7C2srAV8A49jpsYr0ADedCk1wq6fTMTvs=
122+golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
123 golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
124 golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
125 golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
M imgs/api.go
+249, -43
  1@@ -1,11 +1,17 @@
  2 package imgs
  3 
  4 import (
  5+	"bufio"
  6 	"bytes"
  7 	"fmt"
  8 	"html/template"
  9+	"image"
 10+	gif "image/gif"
 11+	jpeg "image/jpeg"
 12+	png "image/png"
 13 	"net/http"
 14 	"net/url"
 15+	"strconv"
 16 	"strings"
 17 	"time"
 18 
 19@@ -14,6 +20,9 @@ import (
 20 	"git.sr.ht/~erock/pico/imgs/storage"
 21 	"git.sr.ht/~erock/pico/shared"
 22 	"github.com/gorilla/feeds"
 23+	"github.com/nfnt/resize"
 24+	"github.com/nickalie/go-webpbin"
 25+	"go.uber.org/zap"
 26 	"golang.org/x/exp/slices"
 27 )
 28 
 29@@ -96,10 +105,6 @@ func GetBlogName(username string) string {
 30 	return username
 31 }
 32 
 33-func isRequestTrackable(r *http.Request) bool {
 34-	return true
 35-}
 36-
 37 func blogHandler(w http.ResponseWriter, r *http.Request) {
 38 	username := shared.GetUsernameFromRequest(r)
 39 	dbpool := shared.GetDB(r)
 40@@ -154,9 +159,13 @@ func blogHandler(w http.ResponseWriter, r *http.Request) {
 41 
 42 	postCollection := make([]*PostItemData, 0, len(posts))
 43 	for _, post := range posts {
 44+		url := fmt.Sprintf(
 45+			"%s/300x",
 46+			cfg.ImgURL(post.Username, post.Slug, onSubdomain, withUserName),
 47+		)
 48 		postCollection = append(postCollection, &PostItemData{
 49-			ImgURL:       template.URL(cfg.ImgURL(post.Username, post.Filename, onSubdomain, withUserName)),
 50-			URL:          template.URL(cfg.ImgURL(post.Username, post.Slug, onSubdomain, withUserName)),
 51+			ImgURL:       template.URL(url),
 52+			URL:          template.URL(cfg.ImgPostURL(post.Username, post.Slug, onSubdomain, withUserName)),
 53 			Caption:      post.Title,
 54 			PublishAt:    post.PublishAt.Format("02 Jan, 2006"),
 55 			PublishAtISO: post.PublishAt.Format(time.RFC3339),
 56@@ -182,62 +191,255 @@ func blogHandler(w http.ResponseWriter, r *http.Request) {
 57 	}
 58 }
 59 
 60-func imgHandler(w http.ResponseWriter, r *http.Request) {
 61-	username := shared.GetUsernameFromRequest(r)
 62-	subdomain := shared.GetSubdomain(r)
 63-	cfg := shared.GetCfg(r)
 64+type deviceType int
 65 
 66-	var filename string
 67-	if !cfg.IsSubdomains() || subdomain == "" {
 68-		filename, _ = url.PathUnescape(shared.GetField(r, 1))
 69-	} else {
 70-		filename, _ = url.PathUnescape(shared.GetField(r, 0))
 71+const (
 72+	desktopDevice deviceType = iota
 73+)
 74+
 75+type ImgOptimizer struct {
 76+	// Specify the compression factor for RGB channels between 0 and 100. The default is 75.
 77+	// A small factor produces a smaller file with lower quality.
 78+	// Best quality is achieved by using a value of 100.
 79+	Quality    uint
 80+	Optimized  bool
 81+	Width      uint
 82+	Height     uint
 83+	DeviceType deviceType
 84+	Output     []byte
 85+	Dimes      string
 86+}
 87+
 88+func (h *ImgOptimizer) GetImage(contents []byte, mimeType string) (image.Image, error) {
 89+	r := bytes.NewReader(contents)
 90+	switch mimeType {
 91+	case "image/png":
 92+		return png.Decode(r)
 93+	case "image/jpeg":
 94+		return jpeg.Decode(r)
 95+	case "image/jpg":
 96+		return jpeg.Decode(r)
 97+	case "image/gif":
 98+		return gif.Decode(r)
 99 	}
100 
101-	dbpool := shared.GetDB(r)
102-	st := shared.GetStorage(r)
103-	logger := shared.GetLogger(r)
104+	return nil, fmt.Errorf("(%s) not supported optimization", mimeType)
105+}
106 
107-	user, err := dbpool.FindUserForName(username)
108+func (h *ImgOptimizer) GetRatio() error {
109+	// dimes = x250 -- width is auto scaled and height is 250
110+	if strings.HasPrefix(h.Dimes, "x") {
111+		height, err := strconv.ParseUint(h.Dimes[1:], 10, 64)
112+		if err != nil {
113+			return err
114+		}
115+		h.Height = uint(height)
116+	}
117+
118+	// dimes = 250x -- width is 250 and height is auto scaled
119+	if strings.HasSuffix(h.Dimes, "x") {
120+		width, err := strconv.ParseUint(h.Dimes[:len(h.Dimes)-1], 10, 64)
121+		if err != nil {
122+			return err
123+		}
124+		h.Width = uint(width)
125+	}
126+
127+	res := strings.Split(h.Dimes, "x")
128+	if len(res) != 2 {
129+		return fmt.Errorf("(%s) must be in format (x200, 200x, or 200x200)", h.Dimes)
130+	}
131+
132+	width, err := strconv.ParseUint(res[0], 10, 64)
133 	if err != nil {
134-		logger.Infof("blog not found: %s", username)
135+		return err
136+	}
137+	h.Width = uint(width)
138+
139+	height, err := strconv.ParseUint(res[1], 10, 64)
140+	if err != nil {
141+		return err
142+	}
143+	h.Height = uint(height)
144+
145+	return nil
146+}
147+
148+type SubImager interface {
149+	SubImage(r image.Rectangle) image.Image
150+}
151+
152+func (h *ImgOptimizer) Process(contents []byte, mimeType string) ([]byte, error) {
153+	if !h.Optimized {
154+		return contents, nil
155+	}
156+
157+	img, err := h.GetImage(contents, mimeType)
158+	if err != nil {
159+		return []byte{}, err
160+	}
161+
162+	nextImg := img
163+	if h.Height > 0 || h.Width > 0 {
164+		nextImg = resize.Resize(h.Width, h.Height, img, resize.NearestNeighbor)
165+	}
166+
167+	encoder := webpbin.Encoder{
168+		Quality: h.Quality,
169+	}
170+
171+	var output bytes.Buffer
172+	w := bufio.NewWriter(&output)
173+
174+	err = encoder.Encode(w, nextImg)
175+	if err != nil {
176+		return []byte{}, err
177+	}
178+
179+	return output.Bytes(), nil
180+}
181+
182+func NewImgOptimizer(logger *zap.SugaredLogger, optimized bool, dimes string) *ImgOptimizer {
183+	opt := &ImgOptimizer{
184+		Optimized:  optimized,
185+		DeviceType: desktopDevice,
186+		Quality:    75,
187+		Dimes:      dimes,
188+	}
189+
190+	err := opt.GetRatio()
191+	if err != nil {
192+		logger.Error(err)
193+	}
194+	return opt
195+}
196+
197+type ImgHandler struct {
198+	Username  string
199+	Subdomain string
200+	Slug      string
201+	Cfg       *shared.ConfigSite
202+	Dbpool    db.DB
203+	Storage   storage.ObjectStorage
204+	Logger    *zap.SugaredLogger
205+	Img       *ImgOptimizer
206+}
207+
208+func imgHandler(w http.ResponseWriter, h *ImgHandler) {
209+	user, err := h.Dbpool.FindUserForName(h.Username)
210+	if err != nil {
211+		h.Logger.Infof("blog not found: %s", h.Username)
212 		http.Error(w, "blog not found", http.StatusNotFound)
213 		return
214 	}
215 
216-	post, err := dbpool.FindPostWithFilename(filename, user.ID, cfg.Space)
217+	post, err := h.Dbpool.FindPostWithSlug(h.Slug, user.ID, h.Cfg.Space)
218 	if err != nil {
219-		logger.Infof("image not found %s/%s", username, filename)
220+		h.Logger.Infof("image not found %s/%s", h.Username, h.Slug)
221 		http.Error(w, err.Error(), http.StatusInternalServerError)
222 		return
223 	}
224 
225-	// validate and fire off analytic event
226-	if isRequestTrackable(r) {
227-		_, err := dbpool.AddViewCount(post.ID)
228-		if err != nil {
229-			logger.Error(err)
230-		}
231+	_, err = h.Dbpool.AddViewCount(post.ID)
232+	if err != nil {
233+		h.Logger.Error(err)
234 	}
235 
236-	bucket, err := st.GetBucket(user.ID)
237+	bucket, err := h.Storage.GetBucket(user.ID)
238 	if err != nil {
239-		logger.Infof("bucket not found %s/%s", username, filename)
240+		h.Logger.Infof("bucket not found %s/%s", h.Username, post.Filename)
241 		http.Error(w, err.Error(), http.StatusInternalServerError)
242 		return
243 	}
244-	contents, err := st.GetFile(bucket, post.Filename)
245+	contents, err := h.Storage.GetFile(bucket, post.Filename)
246 	if err != nil {
247-		logger.Infof("file not found %s/%s", username, post.Filename)
248+		h.Logger.Infof("file not found %s/%s", h.Username, post.Filename)
249 		http.Error(w, err.Error(), http.StatusInternalServerError)
250 		return
251 	}
252 
253-	w.Header().Add("Content-Type", "image/png")
254-	_, err = w.Write(contents)
255+	if h.Img.Optimized {
256+		w.Header().Add("Content-Type", "image/webp")
257+	} else {
258+		w.Header().Add("Content-Type", post.MimeType)
259+	}
260+
261+	contentsProc, err := h.Img.Process(contents, strings.TrimSpace(post.MimeType))
262 	if err != nil {
263-		logger.Error(err)
264+		h.Logger.Error(err)
265+	}
266+
267+	_, err = w.Write(contentsProc)
268+	if err != nil {
269+		h.Logger.Error(err)
270+	}
271+}
272+
273+func imgRequestOriginal(w http.ResponseWriter, r *http.Request) {
274+	username := shared.GetUsernameFromRequest(r)
275+	subdomain := shared.GetSubdomain(r)
276+	cfg := shared.GetCfg(r)
277+
278+	var slug string
279+	if !cfg.IsSubdomains() || subdomain == "" {
280+		slug, _ = url.PathUnescape(shared.GetField(r, 1))
281+	} else {
282+		slug, _ = url.PathUnescape(shared.GetField(r, 0))
283 	}
284+
285+	// users might add the file extension when requesting an image
286+	// but we want to remove that
287+	slug = shared.SanitizeFileExt(slug)
288+
289+	dbpool := shared.GetDB(r)
290+	st := shared.GetStorage(r)
291+	logger := shared.GetLogger(r)
292+
293+	imgHandler(w, &ImgHandler{
294+		Username:  username,
295+		Subdomain: subdomain,
296+		Slug:      slug,
297+		Cfg:       cfg,
298+		Dbpool:    dbpool,
299+		Storage:   st,
300+		Logger:    logger,
301+		Img:       NewImgOptimizer(logger, false, ""),
302+	})
303+}
304+
305+func imgRequest(w http.ResponseWriter, r *http.Request) {
306+	username := shared.GetUsernameFromRequest(r)
307+	subdomain := shared.GetSubdomain(r)
308+	cfg := shared.GetCfg(r)
309+
310+	var dimes string
311+	var slug string
312+	if !cfg.IsSubdomains() || subdomain == "" {
313+		slug, _ = url.PathUnescape(shared.GetField(r, 1))
314+		dimes, _ = url.PathUnescape(shared.GetField(r, 2))
315+	} else {
316+		slug, _ = url.PathUnescape(shared.GetField(r, 0))
317+		dimes, _ = url.PathUnescape(shared.GetField(r, 1))
318+	}
319+
320+	// users might add the file extension when requesting an image
321+	// but we want to remove that
322+	slug = shared.SanitizeFileExt(slug)
323+
324+	dbpool := shared.GetDB(r)
325+	st := shared.GetStorage(r)
326+	logger := shared.GetLogger(r)
327+
328+	imgHandler(w, &ImgHandler{
329+		Username:  username,
330+		Subdomain: subdomain,
331+		Slug:      slug,
332+		Cfg:       cfg,
333+		Dbpool:    dbpool,
334+		Storage:   st,
335+		Logger:    logger,
336+		Img:       NewImgOptimizer(logger, true, dimes),
337+	})
338 }
339 
340 func postHandler(w http.ResponseWriter, r *http.Request) {
341@@ -272,7 +474,8 @@ func postHandler(w http.ResponseWriter, r *http.Request) {
342 	var data PostPageData
343 	post, err := dbpool.FindPostWithSlug(slug, user.ID, cfg.Space)
344 	if err == nil {
345-		parsed, err := shared.ParseText(post.Text, cfg.ImgURL(username, "", true, false))
346+		linkify := NewImgsLinkify(username, onSubdomain, withUserName)
347+		parsed, err := shared.ParseText(post.Text, linkify)
348 		if err != nil {
349 			logger.Error(err)
350 		}
351@@ -302,7 +505,7 @@ func postHandler(w http.ResponseWriter, r *http.Request) {
352 			Username:     username,
353 			BlogName:     blogName,
354 			Contents:     template.HTML(text),
355-			ImgURL:       template.URL(cfg.ImgURL(username, post.Filename, onSubdomain, withUserName)),
356+			ImgURL:       template.URL(cfg.ImgURL(username, post.Slug, onSubdomain, withUserName)),
357 			Tags:         tagLinks,
358 		}
359 	} else {
360@@ -431,7 +634,7 @@ func rssBlogHandler(w http.ResponseWriter, r *http.Request) {
361 		}
362 		var tpl bytes.Buffer
363 		data := &PostPageData{
364-			ImgURL: template.URL(cfg.ImgURL(username, post.Filename, onSubdomain, withUserName)),
365+			ImgURL: template.URL(cfg.ImgURL(username, post.Slug, onSubdomain, withUserName)),
366 		}
367 		if err := ts.Execute(&tpl, data); err != nil {
368 			continue
369@@ -510,7 +713,7 @@ func rssHandler(w http.ResponseWriter, r *http.Request) {
370 	for _, post := range pager.Data {
371 		var tpl bytes.Buffer
372 		data := &PostPageData{
373-			ImgURL: template.URL(cfg.ImgURL(post.Username, post.Filename, onSubdomain, withUserName)),
374+			ImgURL: template.URL(cfg.ImgURL(post.Username, post.Slug, onSubdomain, withUserName)),
375 		}
376 		if err := ts.Execute(&tpl, data); err != nil {
377 			continue
378@@ -593,8 +796,9 @@ func createMainRoutes(staticRoutes []shared.Route) []shared.Route {
379 		shared.NewRoute("GET", "/([^/]+)/rss.xml", rssBlogHandler),
380 		shared.NewRoute("GET", "/([^/]+)/atom.xml", rssBlogHandler),
381 		shared.NewRoute("GET", "/([^/]+)/feed.xml", rssBlogHandler),
382-		shared.NewRoute("GET", "/([^/]+)/([^/]+\\..+)", imgHandler),
383-		shared.NewRoute("GET", "/([^/]+)/([^/]+)", postHandler),
384+		shared.NewRoute("GET", "/([^/]+)/([^/]+)", imgRequest),
385+		shared.NewRoute("GET", "/([^/]+)/o/([^/]+)", imgRequestOriginal),
386+		shared.NewRoute("GET", "/([^/]+)/p/([^/]+)", postHandler),
387 	)
388 
389 	return routes
390@@ -613,8 +817,10 @@ func createSubdomainRoutes(staticRoutes []shared.Route) []shared.Route {
391 
392 	routes = append(
393 		routes,
394-		shared.NewRoute("GET", "/([^/]+\\..+)", imgHandler),
395-		shared.NewRoute("GET", "/([^/]+)", postHandler),
396+		shared.NewRoute("GET", "/([^/]+)", imgRequest),
397+		shared.NewRoute("GET", "/([^/]+)/([a-z0-9]+)", imgRequest),
398+		shared.NewRoute("GET", "/o/([^/]+)", imgRequestOriginal),
399+		shared.NewRoute("GET", "/p/([^/]+)", postHandler),
400 	)
401 
402 	return routes
M imgs/config.go
+16, -4
 1@@ -7,13 +7,25 @@ import (
 2 	"git.sr.ht/~erock/pico/wish/cms/config"
 3 )
 4 
 5-func ImgBaseURL(username string) string {
 6+type ImgsLinkify struct {
 7+	Cfg          *shared.ConfigSite
 8+	Username     string
 9+	OnSubdomain  bool
10+	WithUsername bool
11+}
12+
13+func NewImgsLinkify(username string, onSubdomain, withUsername bool) *ImgsLinkify {
14 	cfg := NewConfigSite()
15-	if cfg.IsSubdomains() {
16-		return fmt.Sprintf("%s://%s.%s", cfg.Protocol, username, cfg.Domain)
17+	return &ImgsLinkify{
18+		Cfg:          cfg,
19+		Username:     username,
20+		OnSubdomain:  onSubdomain,
21+		WithUsername: withUsername,
22 	}
23+}
24 
25-	return "/"
26+func (i *ImgsLinkify) Create(fname string) string {
27+	return i.Cfg.ImgURL(i.Username, fname, i.OnSubdomain, i.WithUsername)
28 }
29 
30 func NewConfigSite() *shared.ConfigSite {
M prose/api.go
+11, -6
 1@@ -203,7 +203,8 @@ func blogHandler(w http.ResponseWriter, r *http.Request) {
 2 
 3 	readme, err := dbpool.FindPostWithFilename("_readme.md", user.ID, cfg.Space)
 4 	if err == nil {
 5-		parsedText, err := shared.ParseText(readme.Text, imgs.ImgBaseURL(readme.Username))
 6+		linkify := imgs.NewImgsLinkify(readme.Username, onSubdomain, withUserName)
 7+		parsedText, err := shared.ParseText(readme.Text, linkify)
 8 		if err != nil {
 9 			logger.Error(err)
10 		}
11@@ -352,8 +353,9 @@ func postHandler(w http.ResponseWriter, r *http.Request) {
12 	hasCSS := false
13 	var data PostPageData
14 	post, err := dbpool.FindPostWithSlug(slug, user.ID, cfg.Space)
15+	linkify := imgs.NewImgsLinkify(username, onSubdomain, withUserName)
16 	if err == nil {
17-		parsedText, err := shared.ParseText(post.Text, imgs.ImgBaseURL(username))
18+		parsedText, err := shared.ParseText(post.Text, linkify)
19 		if err != nil {
20 			logger.Error(err)
21 		}
22@@ -361,7 +363,7 @@ func postHandler(w http.ResponseWriter, r *http.Request) {
23 		// we need the blog name from the readme unfortunately
24 		readme, err := dbpool.FindPostWithFilename("_readme.md", user.ID, cfg.Space)
25 		if err == nil {
26-			readmeParsed, err := shared.ParseText(readme.Text, imgs.ImgBaseURL(username))
27+			readmeParsed, err := shared.ParseText(readme.Text, linkify)
28 			if err != nil {
29 				logger.Error(err)
30 			}
31@@ -604,7 +606,8 @@ func rssBlogHandler(w http.ResponseWriter, r *http.Request) {
32 
33 	readme, err := dbpool.FindPostWithFilename("_readme.md", user.ID, cfg.Space)
34 	if err == nil {
35-		parsedText, err := shared.ParseText(readme.Text, imgs.ImgBaseURL(readme.Username))
36+		linkify := imgs.NewImgsLinkify(readme.Username, true, false)
37+		parsedText, err := shared.ParseText(readme.Text, linkify)
38 		if err != nil {
39 			logger.Error(err)
40 		}
41@@ -636,7 +639,8 @@ func rssBlogHandler(w http.ResponseWriter, r *http.Request) {
42 		if slices.Contains(cfg.HiddenPosts, post.Filename) {
43 			continue
44 		}
45-		parsed, err := shared.ParseText(post.Text, imgs.ImgBaseURL(post.Username))
46+		linkify := imgs.NewImgsLinkify(post.Username, true, false)
47+		parsed, err := shared.ParseText(post.Text, linkify)
48 		if err != nil {
49 			logger.Error(err)
50 		}
51@@ -719,7 +723,8 @@ func rssHandler(w http.ResponseWriter, r *http.Request) {
52 
53 	var feedItems []*feeds.Item
54 	for _, post := range pager.Data {
55-		parsed, err := shared.ParseText(post.Text, imgs.ImgBaseURL(post.Username))
56+		linkify := imgs.NewImgsLinkify(post.Username, onSubdomain, withUserName)
57+		parsed, err := shared.ParseText(post.Text, linkify)
58 		if err != nil {
59 			logger.Error(err)
60 		}
M prose/scp_hooks.go
+3, -1
 1@@ -6,6 +6,7 @@ import (
 2 
 3 	"git.sr.ht/~erock/pico/db"
 4 	"git.sr.ht/~erock/pico/filehandlers"
 5+	"git.sr.ht/~erock/pico/imgs"
 6 	"git.sr.ht/~erock/pico/shared"
 7 	"golang.org/x/exp/slices"
 8 )
 9@@ -45,7 +46,8 @@ func (p *MarkdownHooks) FileValidate(data *filehandlers.PostMetaData) (bool, err
10 }
11 
12 func (p *MarkdownHooks) FileMeta(data *filehandlers.PostMetaData) error {
13-	parsedText, err := shared.ParseText(data.Text, "")
14+	linkify := imgs.NewImgsLinkify("", true, false)
15+	parsedText, err := shared.ParseText(data.Text, linkify)
16 	// we return nil here because we don't want the file upload to fail
17 	if err != nil {
18 		return nil
M shared/config.go
+26, -0
 1@@ -155,6 +155,32 @@ func (c *ConfigSite) ImgURL(username string, slug string, onSubdomain bool, with
 2 	return fmt.Sprintf("/%s", fname)
 3 }
 4 
 5+func (c *ConfigSite) ImgPostURL(username string, slug string, onSubdomain bool, withUserName bool) string {
 6+	fname := url.PathEscape(strings.TrimLeft(slug, "/"))
 7+	if c.IsSubdomains() && onSubdomain {
 8+		return fmt.Sprintf("%s://%s.%s/p/%s", c.Protocol, username, c.Domain, fname)
 9+	}
10+
11+	if withUserName {
12+		return fmt.Sprintf("/%s/p/%s", username, fname)
13+	}
14+
15+	return fmt.Sprintf("/p/%s", fname)
16+}
17+
18+func (c *ConfigSite) ImgOrigURL(username string, slug string, onSubdomain bool, withUserName bool) string {
19+	fname := url.PathEscape(strings.TrimLeft(slug, "/"))
20+	if c.IsSubdomains() && onSubdomain {
21+		return fmt.Sprintf("%s://%s.%s/o/%s", c.Protocol, username, c.Domain, fname)
22+	}
23+
24+	if withUserName {
25+		return fmt.Sprintf("/%s/o/%s", username, fname)
26+	}
27+
28+	return fmt.Sprintf("/o/%s", fname)
29+}
30+
31 func (c *ConfigSite) TagURL(username, tag string, onSubdomain, withUserName bool) string {
32 	tg := url.PathEscape(tag)
33 	return fmt.Sprintf("%s?tag=%s", c.FullBlogURL(username, onSubdomain, withUserName), tg)
M shared/mdparser.go
+17, -8
 1@@ -158,21 +158,28 @@ func (r *ImgRender) renderImage(w util.BufWriter, source []byte, node ast.Node,
 2 	return ast.WalkSkipChildren, nil
 3 }
 4 
 5-func createImgURL(absURL string) func([]byte) []byte {
 6+type Linkify interface {
 7+	Create(fname string) string
 8+}
 9+
10+func createImgURL(linkify Linkify) func([]byte) []byte {
11 	return func(url []byte) []byte {
12 		if url[0] == '/' {
13-			nextURL := fmt.Sprintf("%s%s", absURL, string(url))
14+			name := SanitizeFileExt(string(url))
15+			//  fmt.Sprintf("%s%s", absURL, name)
16+			nextURL := linkify.Create(name)
17 			return []byte(nextURL)
18 		} else if bytes.HasPrefix(url, []byte{'.', '/'}) {
19-			fname := url[1:]
20-			nextURL := fmt.Sprintf("%s%s", absURL, string(fname))
21+			name := SanitizeFileExt(string(url[1:]))
22+			// nextURL := fmt.Sprintf("%s%s", absURL, name)
23+			nextURL := linkify.Create(name)
24 			return []byte(nextURL)
25 		}
26 		return url
27 	}
28 }
29 
30-func ParseText(text string, absURL string) (*ParsedText, error) {
31+func ParseText(text string, linkify Linkify) (*ParsedText, error) {
32 	parsed := ParsedText{
33 		MetaData: &MetaData{
34 			Tags: []string{},
35@@ -198,7 +205,7 @@ func ParseText(text string, absURL string) (*ParsedText, error) {
36 		goldmark.WithRendererOptions(
37 			ghtml.WithUnsafe(),
38 			renderer.WithNodeRenderers(
39-				util.Prioritized(NewImgsRenderer(createImgURL(absURL)), 0),
40+				util.Prioritized(NewImgsRenderer(createImgURL(linkify)), 0),
41 			),
42 		),
43 	)
44@@ -214,9 +221,11 @@ func ParseText(text string, absURL string) (*ParsedText, error) {
45 	parsed.MetaData.Layout = toString(metaData["layout"])
46 	parsed.MetaData.Image = toString(metaData["image"])
47 	if strings.HasPrefix(parsed.Image, "/") {
48-		parsed.MetaData.Image = fmt.Sprintf("%s%s", absURL, parsed.Image)
49+		parsed.Image = linkify.Create(parsed.Image)
50+		// parsed.MetaData.Image = fmt.Sprintf("%s%s", absURL, parsed.Image)
51 	} else if strings.HasPrefix(parsed.Image, "./") {
52-		parsed.MetaData.Image = fmt.Sprintf("%s%s", absURL, parsed.Image[1:])
53+		parsed.Image = linkify.Create(parsed.Image[1:])
54+		// parsed.MetaData.Image = fmt.Sprintf("%s%s", absURL, parsed.Image[1:])
55 	}
56 	parsed.MetaData.ImageCard = toString(metaData["card"])
57 
M shared/router.go
+3, -0
 1@@ -105,6 +105,9 @@ func GetStorage(r *http.Request) storage.ObjectStorage {
 2 
 3 func GetField(r *http.Request, index int) string {
 4 	fields := r.Context().Value(ctxKey{}).([]string)
 5+	if index >= len(fields) {
 6+		return ""
 7+	}
 8 	return fields[index]
 9 }
10