fup: add basic view and paste UIs

This commit is contained in:
Luke Granger-Brown 2021-03-22 00:25:32 +00:00
parent d05aa3ace1
commit bb31335319
9 changed files with 275 additions and 4 deletions

View file

@ -63,7 +63,12 @@ type Config struct {
type Application struct {
indexTmpl *template.Template
pasteTmpl *template.Template
notFoundTmpl *template.Template
viewTextTmpl *template.Template
viewRenderedTmpl *template.Template
viewBinaryTmpl *template.Template
storageBackend *blob.Bucket
appRoot string
@ -95,9 +100,11 @@ func (a *Application) Handler() http.Handler {
r.NotFoundHandler = http.HandlerFunc(a.notFound)
r.HandleFunc("/", renderTemplate(a.indexTmpl))
r.HandleFunc("/paste", renderTemplate(a.pasteTmpl))
r.HandleFunc("/raw/{filename}", a.rawDownload)
r.HandleFunc("/upload", a.upload).Methods("POST")
r.HandleFunc("/upload/{filename}", a.upload).Methods("PUT")
r.HandleFunc("/{filename}", a.view)
return r
}
@ -157,6 +164,7 @@ func New(ctx context.Context, cfg *Config) (*Application, error) {
filenameGenerator: cfg.FilenameGenerator,
useDirectDownload: cfg.UseDirectDownload,
appRoot: cfg.AppRoot,
highlighter: cfg.Highlighter,
}
if a.redirectExpiry == 0 {
a.redirectExpiry = defaultRedirectExpiry
@ -182,7 +190,11 @@ func New(ctx context.Context, cfg *Config) (*Application, error) {
name string
}{
{&a.indexTmpl, "index"},
{&a.pasteTmpl, "paste"},
{&a.notFoundTmpl, "404"},
{&a.viewTextTmpl, "text"},
{&a.viewRenderedTmpl, "rendered"},
{&a.viewBinaryTmpl, "binary"},
}
funcMap := template.FuncMap{

View file

@ -153,7 +153,8 @@ func (a *Application) upload(rw http.ResponseWriter, r *http.Request) {
return
}
origExt := r.PostFormValue("extension")
origContentType = ""
origExt = r.PostFormValue("extension")
if origExt == "" {
origExt = ".txt"
} else if origExt[0] != '.' {

136
web/fup/fuphttp/httpview.go Normal file
View 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)
}
}

View file

@ -90,6 +90,7 @@ h1 {
display: flex;
flex-direction: column;
justify-content: center;
margin-bottom: 10px;
}
.upload-list {
@ -135,3 +136,42 @@ h1 {
.uploaded a, .uploaded a:visited {
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;
}

View file

@ -5,7 +5,7 @@ SPDX-License-Identifier: Apache-2.0
-->
<!DOCTYPE html>
<html>
<html lang="en">
<head>
<meta charset="utf-8">
<link rel="stylesheet" type="text/css" href="{{static "css/reset.min.css"}}">

View 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}}

View 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}}

View 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}}

View 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}}