diff --git a/web/fup/fuphttp/fuphttp.go b/web/fup/fuphttp/fuphttp.go index 311691c41b..41a82b741c 100644 --- a/web/fup/fuphttp/fuphttp.go +++ b/web/fup/fuphttp/fuphttp.go @@ -62,8 +62,13 @@ type Config struct { } type Application struct { - indexTmpl *template.Template - notFoundTmpl *template.Template + 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{ diff --git a/web/fup/fuphttp/httpupload.go b/web/fup/fuphttp/httpupload.go index d9fc561ed7..febdb3ddce 100644 --- a/web/fup/fuphttp/httpupload.go +++ b/web/fup/fuphttp/httpupload.go @@ -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] != '.' { diff --git a/web/fup/fuphttp/httpview.go b/web/fup/fuphttp/httpview.go new file mode 100644 index 0000000000..daeff64bc5 --- /dev/null +++ b/web/fup/fuphttp/httpview.go @@ -0,0 +1,136 @@ +// SPDX-FileCopyrightText: 2021 Luke Granger-Brown +// +// 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) + } +} diff --git a/web/fup/fupstatic/css/base.css b/web/fup/fupstatic/css/base.css index fcf4a022f0..81917fc7a4 100644 --- a/web/fup/fupstatic/css/base.css +++ b/web/fup/fupstatic/css/base.css @@ -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; +} diff --git a/web/fup/fupstatic/tmpl/base.html b/web/fup/fupstatic/tmpl/base.html index 8930294078..29643172ea 100644 --- a/web/fup/fupstatic/tmpl/base.html +++ b/web/fup/fupstatic/tmpl/base.html @@ -5,7 +5,7 @@ SPDX-License-Identifier: Apache-2.0 --> - + diff --git a/web/fup/fupstatic/tmpl/binary.html b/web/fup/fupstatic/tmpl/binary.html new file mode 100644 index 0000000000..90f84e1764 --- /dev/null +++ b/web/fup/fupstatic/tmpl/binary.html @@ -0,0 +1,16 @@ +{{/* +SPDX-FileCopyrightText: 2021 Luke Granger-Brown + +SPDX-License-Identifier: Apache-2.0 +*/}} + +{{define "main"}} +
+ get +
+

{{.Filename}} appears to be a binary file.

+{{end}} + +{{define "extra_body_classes" -}} +view-page binary-page +{{- end}} diff --git a/web/fup/fupstatic/tmpl/paste.html b/web/fup/fupstatic/tmpl/paste.html new file mode 100644 index 0000000000..7d49f89d76 --- /dev/null +++ b/web/fup/fupstatic/tmpl/paste.html @@ -0,0 +1,34 @@ +{{/* +SPDX-FileCopyrightText: 2021 Luke Granger-Brown + +SPDX-License-Identifier: Apache-2.0 +*/}} + +{{define "main"}} +
+ +
+
+ . +
+ + +
+ + + +
+{{end}} + +{{define "extra_body_classes" -}} +paste-page +{{- end}} diff --git a/web/fup/fupstatic/tmpl/rendered.html b/web/fup/fupstatic/tmpl/rendered.html new file mode 100644 index 0000000000..f3afc27b8a --- /dev/null +++ b/web/fup/fupstatic/tmpl/rendered.html @@ -0,0 +1,16 @@ +{{/* +SPDX-FileCopyrightText: 2021 Luke Granger-Brown + +SPDX-License-Identifier: Apache-2.0 +*/}} + +{{define "main"}} +
+ get +
+{{.Rendered}} +{{end}} + +{{define "extra_body_classes" -}} +view-page rendered-page {{if .ExpandBorder}}expanded-border{{end}} +{{- end}} diff --git a/web/fup/fupstatic/tmpl/text.html b/web/fup/fupstatic/tmpl/text.html new file mode 100644 index 0000000000..807890ac48 --- /dev/null +++ b/web/fup/fupstatic/tmpl/text.html @@ -0,0 +1,16 @@ +{{/* +SPDX-FileCopyrightText: 2021 Luke Granger-Brown + +SPDX-License-Identifier: Apache-2.0 +*/}} + +{{define "main"}} +
+ get +
+
{{.Text}}
+{{end}} + +{{define "extra_body_classes" -}} +view-page text-page +{{- end}}