Add asset file upload form, plus related necessary refactors
This commit is contained in:
parent
e406ad6e7c
commit
69de76cb32
@ -11,6 +11,7 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"github.com/mediocregopher/blog.mediocregopher.com/srv/api/apiutil"
|
||||||
"github.com/mediocregopher/blog.mediocregopher.com/srv/cfg"
|
"github.com/mediocregopher/blog.mediocregopher.com/srv/cfg"
|
||||||
"github.com/mediocregopher/blog.mediocregopher.com/srv/chat"
|
"github.com/mediocregopher/blog.mediocregopher.com/srv/chat"
|
||||||
"github.com/mediocregopher/blog.mediocregopher.com/srv/mailinglist"
|
"github.com/mediocregopher/blog.mediocregopher.com/srv/mailinglist"
|
||||||
@ -158,9 +159,9 @@ func (a *api) handler() http.Handler {
|
|||||||
return a.requirePowMiddleware(h)
|
return a.requirePowMiddleware(h)
|
||||||
}
|
}
|
||||||
|
|
||||||
postFormMiddleware := func(h http.Handler) http.Handler {
|
formMiddleware := func(h http.Handler) http.Handler {
|
||||||
h = checkCSRFMiddleware(h)
|
h = checkCSRFMiddleware(h)
|
||||||
h = postOnlyMiddleware(h)
|
h = disallowGetMiddleware(h)
|
||||||
h = logReqMiddleware(h)
|
h = logReqMiddleware(h)
|
||||||
h = addResponseHeaders(map[string]string{
|
h = addResponseHeaders(map[string]string{
|
||||||
"Cache-Control": "no-store, max-age=0",
|
"Cache-Control": "no-store, max-age=0",
|
||||||
@ -193,14 +194,17 @@ func (a *api) handler() http.Handler {
|
|||||||
a.requirePowMiddleware,
|
a.requirePowMiddleware,
|
||||||
)))
|
)))
|
||||||
|
|
||||||
mux.Handle("/api/", http.StripPrefix("/api", postFormMiddleware(apiMux)))
|
mux.Handle("/api/", http.StripPrefix("/api", formMiddleware(apiMux)))
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
v2Mux := http.NewServeMux()
|
v2Mux := http.NewServeMux()
|
||||||
v2Mux.Handle("/follow.html", a.renderDumbHandler("follow.html"))
|
v2Mux.Handle("/follow.html", a.renderDumbHandler("follow.html"))
|
||||||
v2Mux.Handle("/posts/", a.renderPostHandler())
|
v2Mux.Handle("/posts/", a.renderPostHandler())
|
||||||
v2Mux.Handle("/assets", a.renderPostAssetsIndexHandler())
|
v2Mux.Handle("/assets", apiutil.MethodMux(map[string]http.Handler{
|
||||||
|
"GET": a.renderPostAssetsIndexHandler(),
|
||||||
|
"POST": formMiddleware(a.uploadPostAssetHandler()),
|
||||||
|
}))
|
||||||
v2Mux.Handle("/assets/", a.servePostAssetHandler())
|
v2Mux.Handle("/assets/", a.servePostAssetHandler())
|
||||||
v2Mux.Handle("/", a.renderIndexHandler())
|
v2Mux.Handle("/", a.renderIndexHandler())
|
||||||
|
|
||||||
|
@ -11,6 +11,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/mediocregopher/mediocre-go-lib/v2/mlog"
|
"github.com/mediocregopher/mediocre-go-lib/v2/mlog"
|
||||||
)
|
)
|
||||||
@ -110,3 +111,23 @@ func RandStr(numBytes int) string {
|
|||||||
}
|
}
|
||||||
return hex.EncodeToString(b)
|
return hex.EncodeToString(b)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MethodMux will take the request method (GET, POST, etc...) and handle the
|
||||||
|
// request using the corresponding Handler in the given map.
|
||||||
|
//
|
||||||
|
// If no Handler is defined for a method then a 405 Method Not Allowed error is
|
||||||
|
// returned.
|
||||||
|
func MethodMux(handlers map[string]http.Handler) http.Handler {
|
||||||
|
|
||||||
|
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
|
handler, ok := handlers[strings.ToUpper(r.Method)]
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
handler.ServeHTTP(rw, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
@ -111,3 +111,31 @@ func (a *api) servePostAssetHandler() http.Handler {
|
|||||||
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *api) uploadPostAssetHandler() http.Handler {
|
||||||
|
|
||||||
|
renderIndex := a.renderPostAssetsIndexHandler()
|
||||||
|
|
||||||
|
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
|
id := r.PostFormValue("id")
|
||||||
|
if id == "" {
|
||||||
|
apiutil.BadRequest(rw, r, errors.New("id is required"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
file, _, err := r.FormFile("file")
|
||||||
|
if err != nil {
|
||||||
|
apiutil.BadRequest(rw, r, fmt.Errorf("reading multipart file: %w", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
if err := a.params.PostAssetStore.Set(id, file); err != nil {
|
||||||
|
apiutil.InternalServerError(rw, r, fmt.Errorf("storing file: %w", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
renderIndex.ServeHTTP(rw, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
@ -10,6 +10,7 @@ import (
|
|||||||
const (
|
const (
|
||||||
csrfTokenCookieName = "csrf_token"
|
csrfTokenCookieName = "csrf_token"
|
||||||
csrfTokenHeaderName = "X-CSRF-Token"
|
csrfTokenHeaderName = "X-CSRF-Token"
|
||||||
|
csrfTokenFormName = "csrfToken"
|
||||||
)
|
)
|
||||||
|
|
||||||
func setCSRFMiddleware(h http.Handler) http.Handler {
|
func setCSRFMiddleware(h http.Handler) http.Handler {
|
||||||
@ -45,7 +46,7 @@ func checkCSRFMiddleware(h http.Handler) http.Handler {
|
|||||||
|
|
||||||
givenCSRFTok := r.Header.Get(csrfTokenHeaderName)
|
givenCSRFTok := r.Header.Get(csrfTokenHeaderName)
|
||||||
if givenCSRFTok == "" {
|
if givenCSRFTok == "" {
|
||||||
givenCSRFTok = r.FormValue("csrfToken")
|
givenCSRFTok = r.FormValue(csrfTokenFormName)
|
||||||
}
|
}
|
||||||
|
|
||||||
if csrfTok == "" || givenCSRFTok != csrfTok {
|
if csrfTok == "" || givenCSRFTok != csrfTok {
|
||||||
|
@ -80,11 +80,11 @@ func logReqMiddleware(h http.Handler) http.Handler {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func postOnlyMiddleware(h http.Handler) http.Handler {
|
func disallowGetMiddleware(h http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
// we allow websockets to not be POSTs because, well, they can't be
|
// we allow websockets to be GETs because, well, they must be
|
||||||
if r.Method == "POST" || r.Header.Get("Upgrade") == "websocket" {
|
if r.Method != "GET" || r.Header.Get("Upgrade") == "websocket" {
|
||||||
h.ServeHTTP(rw, r)
|
h.ServeHTTP(rw, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -51,6 +51,39 @@ func (a *api) mustParseTpl(name string) *template.Template {
|
|||||||
return tpl
|
return tpl
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type tplData struct {
|
||||||
|
Payload interface{}
|
||||||
|
CSRFToken string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t tplData) CSRFFormInput() template.HTML {
|
||||||
|
return template.HTML(fmt.Sprintf(
|
||||||
|
`<input type="hidden" name="%s" value="%s" />`,
|
||||||
|
csrfTokenFormName, t.CSRFToken,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
// executeTemplate expects to be the final action in an http.Handler
|
||||||
|
func executeTemplate(
|
||||||
|
rw http.ResponseWriter, r *http.Request,
|
||||||
|
tpl *template.Template, payload interface{},
|
||||||
|
) {
|
||||||
|
|
||||||
|
csrfToken, _ := apiutil.GetCookie(r, csrfTokenCookieName, "")
|
||||||
|
|
||||||
|
tplData := tplData{
|
||||||
|
Payload: payload,
|
||||||
|
CSRFToken: csrfToken,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tpl.Execute(rw, tplData); err != nil {
|
||||||
|
apiutil.InternalServerError(
|
||||||
|
rw, r, fmt.Errorf("rendering template: %w", err),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (a *api) renderIndexHandler() http.Handler {
|
func (a *api) renderIndexHandler() http.Handler {
|
||||||
|
|
||||||
tpl := a.mustParseTpl("index.html")
|
tpl := a.mustParseTpl("index.html")
|
||||||
@ -79,7 +112,7 @@ func (a *api) renderIndexHandler() http.Handler {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
tplData := struct {
|
tplPayload := struct {
|
||||||
Posts []post.StoredPost
|
Posts []post.StoredPost
|
||||||
PrevPage, NextPage int
|
PrevPage, NextPage int
|
||||||
}{
|
}{
|
||||||
@ -89,19 +122,14 @@ func (a *api) renderIndexHandler() http.Handler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if page > 0 {
|
if page > 0 {
|
||||||
tplData.PrevPage = page - 1
|
tplPayload.PrevPage = page - 1
|
||||||
}
|
}
|
||||||
|
|
||||||
if hasMore {
|
if hasMore {
|
||||||
tplData.NextPage = page + 1
|
tplPayload.NextPage = page + 1
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := tpl.Execute(rw, tplData); err != nil {
|
executeTemplate(rw, r, tpl, tplPayload)
|
||||||
apiutil.InternalServerError(
|
|
||||||
rw, r, fmt.Errorf("rendering index: %w", err),
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -133,7 +161,7 @@ func (a *api) renderPostHandler() http.Handler {
|
|||||||
|
|
||||||
renderedBody := markdown.ToHTML([]byte(storedPost.Body), parser, htmlRenderer)
|
renderedBody := markdown.ToHTML([]byte(storedPost.Body), parser, htmlRenderer)
|
||||||
|
|
||||||
tplData := struct {
|
tplPayload := struct {
|
||||||
post.StoredPost
|
post.StoredPost
|
||||||
SeriesPrevious, SeriesNext *post.StoredPost
|
SeriesPrevious, SeriesNext *post.StoredPost
|
||||||
Body template.HTML
|
Body template.HTML
|
||||||
@ -165,21 +193,16 @@ func (a *api) renderPostHandler() http.Handler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !foundThis {
|
if !foundThis {
|
||||||
tplData.SeriesPrevious = &seriesPost
|
tplPayload.SeriesPrevious = &seriesPost
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
tplData.SeriesNext = &seriesPost
|
tplPayload.SeriesNext = &seriesPost
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := tpl.Execute(rw, tplData); err != nil {
|
executeTemplate(rw, r, tpl, tplPayload)
|
||||||
apiutil.InternalServerError(
|
|
||||||
rw, r, fmt.Errorf("rendering post with id %q: %w", id, err),
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -212,17 +235,12 @@ func (a *api) renderPostAssetsIndexHandler() http.Handler {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
tplData := struct {
|
tplPayload := struct {
|
||||||
IDs []string
|
IDs []string
|
||||||
}{
|
}{
|
||||||
IDs: ids,
|
IDs: ids,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := tpl.Execute(rw, tplData); err != nil {
|
executeTemplate(rw, r, tpl, tplPayload)
|
||||||
apiutil.InternalServerError(
|
|
||||||
rw, r, fmt.Errorf("rendering: %w", err),
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,32 @@
|
|||||||
{{ define "body" }}
|
{{ define "body" }}
|
||||||
|
|
||||||
|
<h2>Upload Asset</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
If the given ID is the same as an existing asset's ID, then that asset will be
|
||||||
|
overwritten.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<form action={{ BlogURL "assets" }} method="POST" enctype="multipart/form-data">
|
||||||
|
{{ .CSRFFormInput }}
|
||||||
|
<div class="row">
|
||||||
|
<div class="four columns">
|
||||||
|
<input type="text" placeholder="Unique ID" name="id" />
|
||||||
|
</div>
|
||||||
|
<div class="four columns">
|
||||||
|
<input type="file" name="file" /><br/>
|
||||||
|
</div>
|
||||||
|
<div class="four columns">
|
||||||
|
<input type="submit" value="Upload" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<h2>Existing Assets</h2>
|
||||||
|
|
||||||
<table>
|
<table>
|
||||||
|
|
||||||
{{ range .IDs }}
|
{{ range .Payload.IDs }}
|
||||||
<tr>
|
<tr>
|
||||||
<td><a href="{{ AssetURL . }}" target="_blank">{{ . }}</a></td>
|
<td><a href="{{ AssetURL . }}" target="_blank">{{ . }}</a></td>
|
||||||
<td>
|
<td>
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
<ul id="posts-list">
|
<ul id="posts-list">
|
||||||
|
|
||||||
{{ range .Posts }}
|
{{ range .Payload.Posts }}
|
||||||
<li>
|
<li>
|
||||||
<h2>
|
<h2>
|
||||||
<a href="posts/{{ .HTTPPath }}">{{ .Title }}</a>
|
<a href="posts/{{ .HTTPPath }}">{{ .Title }}</a>
|
||||||
@ -17,15 +17,15 @@
|
|||||||
|
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
{{ if or (ge .PrevPage 0) (ge .NextPage 0) }}
|
{{ if or (ge .Payload.PrevPage 0) (ge .Payload.NextPage 0) }}
|
||||||
<div id="page-turner">
|
<div id="page-turner">
|
||||||
|
|
||||||
{{ if ge .PrevPage 0 }}
|
{{ if ge .Payload.PrevPage 0 }}
|
||||||
<a style="float: left;" href="?p={{ .PrevPage}}">Newer</a>
|
<a style="float: left;" href="?p={{ .Payload.PrevPage}}">Newer</a>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
||||||
{{ if ge .NextPage 0 }}
|
{{ if ge .Payload.NextPage 0 }}
|
||||||
<a style="float:right;" href="?p={{ .NextPage}}">Older</a>
|
<a style="float:right;" href="?p={{ .Payload.NextPage}}">Older</a>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
@ -2,43 +2,43 @@
|
|||||||
|
|
||||||
<header id="post-header">
|
<header id="post-header">
|
||||||
<h1 id="post-headline">
|
<h1 id="post-headline">
|
||||||
{{ .Title }}
|
{{ .Payload.Title }}
|
||||||
</h1>
|
</h1>
|
||||||
<div class="light">
|
<div class="light">
|
||||||
{{ .PublishedAt.Format "2006-01-02" }}
|
{{ .Payload.PublishedAt.Format "2006-01-02" }}
|
||||||
•
|
•
|
||||||
{{ if not .LastUpdatedAt.IsZero }}
|
{{ if not .Payload.LastUpdatedAt.IsZero }}
|
||||||
(Updated {{ .LastUpdatedAt.Format "2006-01-02" }})
|
(Updated {{ .Payload.LastUpdatedAt.Format "2006-01-02" }})
|
||||||
•
|
•
|
||||||
{{ end }}
|
{{ end }}
|
||||||
<em>{{ .Description }}</em>
|
<em>{{ .Payload.Description }}</em>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{{ if (or .SeriesPrevious .SeriesNext) }}
|
{{ if (or .Payload.SeriesPrevious .Payload.SeriesNext) }}
|
||||||
<p class="light"><em>
|
<p class="light"><em>
|
||||||
This post is part of a series:<br/>
|
This post is part of a series:<br/>
|
||||||
{{ if .SeriesPrevious }}
|
{{ if .Payload.SeriesPrevious }}
|
||||||
Previously: <a href="{{ .SeriesPrevious.HTTPPath }}">{{ .SeriesPrevious.Title }}</a></br>
|
Previously: <a href="{{ .Payload.SeriesPrevious.HTTPPath }}">{{ .Payload.SeriesPrevious.Title }}</a></br>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
{{ if .SeriesNext }}
|
{{ if .Payload.SeriesNext }}
|
||||||
Next: <a href="{{ .SeriesNext.HTTPPath }}">{{ .SeriesNext.Title }}</a></br>
|
Next: <a href="{{ .Payload.SeriesNext.HTTPPath }}">{{ .Payload.SeriesNext.Title }}</a></br>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</em></p>
|
</em></p>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
||||||
<div id="post-content">
|
<div id="post-content">
|
||||||
{{ .Body }}
|
{{ .Payload.Body }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{ if (or .SeriesPrevious .SeriesNext) }}
|
{{ if (or .Payload.SeriesPrevious .Payload.SeriesNext) }}
|
||||||
<p class="light"><em>
|
<p class="light"><em>
|
||||||
If you liked this post, consider checking out other posts in the series:<br/>
|
If you liked this post, consider checking out other posts in the series:<br/>
|
||||||
{{ if .SeriesPrevious }}
|
{{ if .Payload.SeriesPrevious }}
|
||||||
Previously: <a href="{{ .SeriesPrevious.HTTPPath }}">{{ .SeriesPrevious.Title }}</a></br>
|
Previously: <a href="{{ .Payload.SeriesPrevious.HTTPPath }}">{{ .Payload.SeriesPrevious.Title }}</a></br>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
{{ if .SeriesNext }}
|
{{ if .Payload.SeriesNext }}
|
||||||
Next: <a href="{{ .SeriesNext.HTTPPath }}">{{ .SeriesNext.Title }}</a></br>
|
Next: <a href="{{ .Payload.SeriesNext.HTTPPath }}">{{ .Payload.SeriesNext.Title }}</a></br>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</em></p>
|
</em></p>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
Loading…
Reference in New Issue
Block a user