simpleserve/main.go

270 lines
6.4 KiB
Go
Executable File

package main
import (
"bytes"
"errors"
"fmt"
"io"
"io/ioutil"
"local/args"
"local/gziphttp"
"local/notes-server/notes/md"
"local/simpleserve/simpleserve"
"log"
"net/http"
"net/http/httptest"
"os"
"path"
"regexp"
"strings"
)
const (
ENDPOINT_UPLOAD = "__upload__"
ENDPOINT_DELETE = "__delete__"
)
var (
fs *args.ArgSet
)
func main() {
fs = args.NewArgSet()
fs.Append(args.STRING, "p", "port to serve", "8100")
fs.Append(args.BOOL, "md", "whether to render markdown as html", true)
fs.Append(args.STRING, "md-css", "css to load for md", "./public/bundle.css")
fs.Append(args.STRING, "md-class", "class to wrap md", "phb")
fs.Append(args.STRING, "d", "static path to serve", "./public")
if err := fs.Parse(); err != nil {
panic(err)
}
d := fs.Get("d").GetString()
md := fs.Get("md").GetBool()
mdCss := fs.Get("md-css").GetString()
mdClass := fs.Get("md-class").GetString()
if mdCss != "" {
b, err := ioutil.ReadFile(mdCss)
if err != nil {
panic(err)
}
mdCss = fmt.Sprintf(`
<style>
body > div {
margin: auto;
overflow: auto;
}
.%s {
width: auto !important;
/*height: auto !important;*/
/*height: 100%% !important;
column-count: auto !important;
-webkit-column-count: auto !important;*/
overflow: auto !important;
background-repeat: repeat-x;
}
.%s h1 {
column-span: none !important;
-webkit-column-span: none !important;
}
.%s hr+table {
margin-top: 40px !important;
}
.%s nav {
background: none !important;
}
%s
</style>`,
mdClass,
mdClass,
mdClass,
mdClass,
b,
)
}
p := strings.TrimPrefix(fs.Get("p").GetString(), ":")
http.Handle("/", http.HandlerFunc(handler(d, md, mdCss, mdClass)))
log.Printf("Serving %s on HTTP port: %s\n", d, p)
log.Fatal(http.ListenAndServe(":"+p, nil))
}
func handler(d string, md bool, mdCss, mdClass string) http.HandlerFunc {
return gzip(endpoints(withDel(withMD(d, md, mdCss, mdClass, fserve(d)))))
}
func writeMeta(w http.ResponseWriter) {
fmt.Fprintf(w, `
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
</head>
`)
}
func writeForm(w http.ResponseWriter) {
fmt.Fprintf(w, `
<form enctype="multipart/form-data" action="./%s" method="post">
<input type="file" name="file" required/>
<input type="submit" value="upload"/>
</form>
`, ENDPOINT_UPLOAD)
}
func gzip(foo http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if gziphttp.Can(r) {
gz := gziphttp.New(w)
defer gz.Close()
w = gz
}
foo(w, r)
}
}
func endpoints(foo http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if isDir(r) {
writeMeta(w)
writeForm(w)
}
if isUploaded(r) {
if err := upload(w, r); err != nil {
fmt.Fprintln(w, err.Error())
}
} else if isDeleted(r) {
if err := del(w, r); err != nil {
fmt.Fprintln(w, err.Error())
}
} else {
simpleserve.SetContentTypeIfMedia(w, r)
foo(w, r)
}
}
}
func isUploaded(r *http.Request) bool {
return path.Base(r.URL.Path) == ENDPOINT_UPLOAD
}
func isDeleted(r *http.Request) bool {
return path.Base(r.URL.Path) == ENDPOINT_DELETE
}
func isDir(r *http.Request) bool {
d := toRealPath(r.URL.Path)
fi, err := os.Stat(d)
if err != nil {
return false
}
if !fi.IsDir() {
return false
}
if _, err := os.Stat(path.Join(d, "index.html")); err == nil {
return false
}
return true
}
func withMD(dir string, enabled bool, mdCss, mdClass string, foo http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
realpath := toRealPath(r.URL.Path)
if enabled && !isDir(r) && path.Ext(realpath) == ".md" {
b, err := ioutil.ReadFile(realpath)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
s, err := md.Gomarkdown(b, nil)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
fmt.Fprintln(w, mdCss)
fmt.Fprintf(w, "<div class=%q>", mdClass)
s = strings.ReplaceAll(s, "<nav>", `<nav class="toc">`)
fmt.Fprintln(w, s)
fmt.Fprintf(w, "</div>")
} else {
foo(w, r)
}
}
}
func withDel(foo http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if !isDir(r) {
foo(w, r)
return
}
fmt.Fprintln(w, `<a href=".."><input type="button" style="padding: .15em 4em .35em 4em" value=".."/></a>`+"\n")
w2 := httptest.NewRecorder()
foo(w2, r)
b := bytes.Split(w2.Body.Bytes(), []byte("\n"))
buff := bytes.NewBuffer(nil)
for i := range b {
if bytes.Contains(b[i], []byte("<a href=")) {
re := regexp.MustCompile(`href="[^"]*"`)
match := re.Find(b[i])
if len(match) > 0 {
match = bytes.Split(match, []byte(`href="`))[1]
match = match[:len(match)-1]
b[i] = []byte(fmt.Sprintf(`<a href="%s/%s"><input type="button" value="&#10060;" style="padding: .40em 1em .10em 1em; margin-right: .5em" onclick='return confirm("Delete "+%q+"?");'></input></a> %s`, match, ENDPOINT_DELETE, match, b[i]))
}
}
buff.Write(b[i])
buff.Write([]byte("\n"))
}
io.Copy(w, buff)
}
}
func fserve(d string) http.HandlerFunc {
h := http.FileServer(http.Dir(d))
return h.ServeHTTP
}
func upload(w http.ResponseWriter, r *http.Request) error {
r.ParseMultipartForm(100 << 20)
file, handler, err := r.FormFile("file")
if err != nil {
return err
}
defer file.Close()
p := toRealPath(path.Join(path.Dir(r.URL.Path), handler.Filename))
if fi, err := os.Stat(p); err == nil && !fi.IsDir() {
return errors.New("already exists")
}
f, err := os.Create(p)
if err != nil {
return err
}
defer f.Close()
if _, err := io.Copy(f, file); err != nil {
return err
}
http.Redirect(w, r, path.Dir(r.URL.Path)+"/", http.StatusSeeOther)
return nil
}
func del(w http.ResponseWriter, r *http.Request) error {
p := toRealPath(path.Dir(r.URL.Path))
_, err := os.Stat(p)
if err != nil {
return err
}
err = os.RemoveAll(p)
if err != nil {
return err
}
http.Redirect(w, r, path.Dir(path.Dir(r.URL.Path))+"/", http.StatusSeeOther)
return nil
}
func toRealPath(p string) string {
d := path.Join(fs.Get("d").GetString())
return path.Join(d, p)
}