fup: add basic view and paste UIs
This commit is contained in:
parent
d05aa3ace1
commit
bb31335319
9 changed files with 275 additions and 4 deletions
|
@ -63,7 +63,12 @@ type Config struct {
|
||||||
|
|
||||||
type Application struct {
|
type Application struct {
|
||||||
indexTmpl *template.Template
|
indexTmpl *template.Template
|
||||||
|
pasteTmpl *template.Template
|
||||||
notFoundTmpl *template.Template
|
notFoundTmpl *template.Template
|
||||||
|
viewTextTmpl *template.Template
|
||||||
|
viewRenderedTmpl *template.Template
|
||||||
|
viewBinaryTmpl *template.Template
|
||||||
|
|
||||||
storageBackend *blob.Bucket
|
storageBackend *blob.Bucket
|
||||||
|
|
||||||
appRoot string
|
appRoot string
|
||||||
|
@ -95,9 +100,11 @@ func (a *Application) Handler() http.Handler {
|
||||||
|
|
||||||
r.NotFoundHandler = http.HandlerFunc(a.notFound)
|
r.NotFoundHandler = http.HandlerFunc(a.notFound)
|
||||||
r.HandleFunc("/", renderTemplate(a.indexTmpl))
|
r.HandleFunc("/", renderTemplate(a.indexTmpl))
|
||||||
|
r.HandleFunc("/paste", renderTemplate(a.pasteTmpl))
|
||||||
r.HandleFunc("/raw/{filename}", a.rawDownload)
|
r.HandleFunc("/raw/{filename}", a.rawDownload)
|
||||||
r.HandleFunc("/upload", a.upload).Methods("POST")
|
r.HandleFunc("/upload", a.upload).Methods("POST")
|
||||||
r.HandleFunc("/upload/{filename}", a.upload).Methods("PUT")
|
r.HandleFunc("/upload/{filename}", a.upload).Methods("PUT")
|
||||||
|
r.HandleFunc("/{filename}", a.view)
|
||||||
|
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
@ -157,6 +164,7 @@ func New(ctx context.Context, cfg *Config) (*Application, error) {
|
||||||
filenameGenerator: cfg.FilenameGenerator,
|
filenameGenerator: cfg.FilenameGenerator,
|
||||||
useDirectDownload: cfg.UseDirectDownload,
|
useDirectDownload: cfg.UseDirectDownload,
|
||||||
appRoot: cfg.AppRoot,
|
appRoot: cfg.AppRoot,
|
||||||
|
highlighter: cfg.Highlighter,
|
||||||
}
|
}
|
||||||
if a.redirectExpiry == 0 {
|
if a.redirectExpiry == 0 {
|
||||||
a.redirectExpiry = defaultRedirectExpiry
|
a.redirectExpiry = defaultRedirectExpiry
|
||||||
|
@ -182,7 +190,11 @@ func New(ctx context.Context, cfg *Config) (*Application, error) {
|
||||||
name string
|
name string
|
||||||
}{
|
}{
|
||||||
{&a.indexTmpl, "index"},
|
{&a.indexTmpl, "index"},
|
||||||
|
{&a.pasteTmpl, "paste"},
|
||||||
{&a.notFoundTmpl, "404"},
|
{&a.notFoundTmpl, "404"},
|
||||||
|
{&a.viewTextTmpl, "text"},
|
||||||
|
{&a.viewRenderedTmpl, "rendered"},
|
||||||
|
{&a.viewBinaryTmpl, "binary"},
|
||||||
}
|
}
|
||||||
|
|
||||||
funcMap := template.FuncMap{
|
funcMap := template.FuncMap{
|
||||||
|
|
|
@ -153,7 +153,8 @@ func (a *Application) upload(rw http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
origExt := r.PostFormValue("extension")
|
origContentType = ""
|
||||||
|
origExt = r.PostFormValue("extension")
|
||||||
if origExt == "" {
|
if origExt == "" {
|
||||||
origExt = ".txt"
|
origExt = ".txt"
|
||||||
} else if origExt[0] != '.' {
|
} else if origExt[0] != '.' {
|
||||||
|
|
136
web/fup/fuphttp/httpview.go
Normal file
136
web/fup/fuphttp/httpview.go
Normal file
|
@ -0,0 +1,136 @@
|
||||||
|
// SPDX-FileCopyrightText: 2021 Luke Granger-Brown <depot@lukegb.com>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package fuphttp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"mime"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/google/safehtml"
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
"gocloud.dev/gcerrors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type viewTemplateData struct {
|
||||||
|
Text string
|
||||||
|
Rendered safehtml.HTML
|
||||||
|
RawURL safehtml.TrustedResourceURL
|
||||||
|
|
||||||
|
Filename string
|
||||||
|
Meta *Metadata
|
||||||
|
|
||||||
|
ExpandBorder bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderAsCode(mimeType string) bool {
|
||||||
|
if strings.HasPrefix(mimeType, "text/") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
switch mimeType {
|
||||||
|
case "application/json", "application/xml":
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Application) view(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
filename := vars["filename"]
|
||||||
|
|
||||||
|
meta, err := metadata(ctx, a.storageBackend, filename)
|
||||||
|
switch errorCode(err) {
|
||||||
|
case gcerrors.NotFound:
|
||||||
|
a.notFound(rw, r)
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
log.Printf("view(%q) metadata: %v", filename, err)
|
||||||
|
a.internalError(rw, r)
|
||||||
|
return
|
||||||
|
case gcerrors.OK:
|
||||||
|
// OK
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(lukegb): Range header and conditionals?.
|
||||||
|
rdr, err := a.storageBackend.NewReader(ctx, filename, nil)
|
||||||
|
switch errorCode(err) {
|
||||||
|
case gcerrors.NotFound:
|
||||||
|
a.notFound(rw, r)
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
a.internalError(rw, r)
|
||||||
|
return
|
||||||
|
case gcerrors.OK:
|
||||||
|
// OK
|
||||||
|
}
|
||||||
|
defer rdr.Close()
|
||||||
|
|
||||||
|
content, err := io.ReadAll(rdr)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("view(%q): ReadAll: %v", filename, err)
|
||||||
|
a.internalError(rw, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if v := meta.Attributes.ModTime; !v.IsZero() {
|
||||||
|
rw.Header().Set("Last-Modified", v.UTC().Format(http.TimeFormat))
|
||||||
|
}
|
||||||
|
|
||||||
|
data := viewTemplateData{
|
||||||
|
Filename: filename,
|
||||||
|
Text: string(content),
|
||||||
|
Meta: meta,
|
||||||
|
}
|
||||||
|
data.RawURL, err = safehtml.TrustedResourceURLAppend(
|
||||||
|
safehtml.TrustedResourceURLFromConstant("/raw/"),
|
||||||
|
filename)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("view(%q): TrustedResourceURLAppend: %v", filename, err)
|
||||||
|
a.internalError(rw, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tmpl := a.viewTextTmpl
|
||||||
|
|
||||||
|
parsedContentType, _, err := mime.ParseMediaType(meta.Attributes.ContentType)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("view(%q): parsing content type %v: %v", filename, meta.Attributes.ContentType, err)
|
||||||
|
a.internalError(rw, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Is highlighting available?
|
||||||
|
if bytes.Contains(content, []byte{0}) {
|
||||||
|
// Probably binary.
|
||||||
|
tmpl = a.viewBinaryTmpl
|
||||||
|
} else if a.highlighter != nil {
|
||||||
|
log.Printf("view(%q): content type %v", filename, parsedContentType)
|
||||||
|
if parsedContentType == "text/markdown" {
|
||||||
|
highlighted, err := a.highlighter.Markdown(ctx, data.Text)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("view(%q): rendering markdown: %v", filename, err)
|
||||||
|
} else {
|
||||||
|
data.Rendered = highlighted
|
||||||
|
tmpl = a.viewRenderedTmpl
|
||||||
|
}
|
||||||
|
} else if renderAsCode(parsedContentType) {
|
||||||
|
highlighted, err := a.highlighter.Code(ctx, filename, "Dark", data.Text)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("view(%q): rendering markdown: %v", filename, err)
|
||||||
|
} else {
|
||||||
|
data.Rendered = highlighted
|
||||||
|
data.ExpandBorder = true
|
||||||
|
tmpl = a.viewRenderedTmpl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tmpl.Execute(rw, data); err != nil {
|
||||||
|
log.Printf("view(%q): Execute: %v", filename, err)
|
||||||
|
}
|
||||||
|
}
|
|
@ -90,6 +90,7 @@ h1 {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.upload-list {
|
.upload-list {
|
||||||
|
@ -135,3 +136,42 @@ h1 {
|
||||||
.uploaded a, .uploaded a:visited {
|
.uploaded a, .uploaded a:visited {
|
||||||
color: var(--colour-text-primary-dark);
|
color: var(--colour-text-primary-dark);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta {
|
||||||
|
text-align: right;
|
||||||
|
padding: 5px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container > pre {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expanded-border .container {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.expanded-border .container > pre {
|
||||||
|
padding: 10px 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.binary-page .container h2 {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.paste-header {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.paste-header-left {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.paste-content {
|
||||||
|
margin-top: 20px;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 300px;
|
||||||
|
font-family: "Roboto Mono", monospace;
|
||||||
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@ SPDX-License-Identifier: Apache-2.0
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<link rel="stylesheet" type="text/css" href="{{static "css/reset.min.css"}}">
|
<link rel="stylesheet" type="text/css" href="{{static "css/reset.min.css"}}">
|
||||||
|
|
16
web/fup/fupstatic/tmpl/binary.html
Normal file
16
web/fup/fupstatic/tmpl/binary.html
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
{{/*
|
||||||
|
SPDX-FileCopyrightText: 2021 Luke Granger-Brown <depot@lukegb.com>
|
||||||
|
|
||||||
|
SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/}}
|
||||||
|
|
||||||
|
{{define "main"}}
|
||||||
|
<div class="meta">
|
||||||
|
<a href="{{.RawURL}}">get</a>
|
||||||
|
</div>
|
||||||
|
<h2><em>{{.Filename}}</em> appears to be a binary file.</h2>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{define "extra_body_classes" -}}
|
||||||
|
view-page binary-page
|
||||||
|
{{- end}}
|
34
web/fup/fupstatic/tmpl/paste.html
Normal file
34
web/fup/fupstatic/tmpl/paste.html
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
{{/*
|
||||||
|
SPDX-FileCopyrightText: 2021 Luke Granger-Brown <depot@lukegb.com>
|
||||||
|
|
||||||
|
SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/}}
|
||||||
|
|
||||||
|
{{define "main"}}
|
||||||
|
<form action="{{app "upload"}}" method="POST">
|
||||||
|
|
||||||
|
<div class="paste-header">
|
||||||
|
<div class="paste-header-left">
|
||||||
|
.<input type="text" name="extension" placeholder="txt">
|
||||||
|
</div>
|
||||||
|
<select name="expiry" id="expiry">
|
||||||
|
<option value="" selected>never</option>
|
||||||
|
<option value="1m">1 minute</option>
|
||||||
|
<option value="5m">5 minutes</option>
|
||||||
|
<option value="1h">1 hour</option>
|
||||||
|
<option value="24h">1 day</option>
|
||||||
|
<option value="168h">1 week</option>
|
||||||
|
<option value="672h">4 weeks</option>
|
||||||
|
<option value="8760h">1 year</option>
|
||||||
|
</select>
|
||||||
|
<input type="submit" value="paste">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<textarea class="paste-content" name="content"></textarea>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{define "extra_body_classes" -}}
|
||||||
|
paste-page
|
||||||
|
{{- end}}
|
16
web/fup/fupstatic/tmpl/rendered.html
Normal file
16
web/fup/fupstatic/tmpl/rendered.html
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
{{/*
|
||||||
|
SPDX-FileCopyrightText: 2021 Luke Granger-Brown <depot@lukegb.com>
|
||||||
|
|
||||||
|
SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/}}
|
||||||
|
|
||||||
|
{{define "main"}}
|
||||||
|
<div class="meta">
|
||||||
|
<a href="{{.RawURL}}">get</a>
|
||||||
|
</div>
|
||||||
|
{{.Rendered}}
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{define "extra_body_classes" -}}
|
||||||
|
view-page rendered-page {{if .ExpandBorder}}expanded-border{{end}}
|
||||||
|
{{- end}}
|
16
web/fup/fupstatic/tmpl/text.html
Normal file
16
web/fup/fupstatic/tmpl/text.html
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
{{/*
|
||||||
|
SPDX-FileCopyrightText: 2021 Luke Granger-Brown <depot@lukegb.com>
|
||||||
|
|
||||||
|
SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/}}
|
||||||
|
|
||||||
|
{{define "main"}}
|
||||||
|
<div class="meta">
|
||||||
|
<a href="{{.RawURL}}">get</a>
|
||||||
|
</div>
|
||||||
|
<pre class="pre-content">{{.Text}}</pre>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{define "extra_body_classes" -}}
|
||||||
|
view-page text-page
|
||||||
|
{{- end}}
|
Loading…
Reference in a new issue