Various small nice fixes

This commit is contained in:
Noah 2018-04-10 19:07:25 -07:00
parent 7fa422e42f
commit 3b4bd4e978
8 changed files with 113 additions and 18 deletions

3
.gitignore vendored
View File

@ -1,3 +1,4 @@
bin/ bin/
dist/ dist/
root/.private user-root/
user-root/.private

View File

@ -21,7 +21,7 @@ build:
# `make run` to run it in debug mode. # `make run` to run it in debug mode.
.PHONY: run .PHONY: run
run: run:
./go-reload cmd/blog/main.go -debug root ./go-reload cmd/blog/main.go -debug user-root
# `make test` to run unit tests. # `make test` to run unit tests.
.PHONY: test .PHONY: test

View File

@ -33,12 +33,25 @@ func editorHandler(w http.ResponseWriter, r *http.Request) {
// Are they saving? // Are they saving?
if saving { if saving {
fp = filepath.Join(*render.UserRoot, file) fp = filepath.Join(*render.UserRoot, file)
body = []byte(r.FormValue("body")) body = []byte(strings.Replace(r.FormValue("body"), "\r\n", "\n", -1))
err := ioutil.WriteFile(fp, body, 0644)
// Ensure the folders exist.
dir, _ := filepath.Split(fp)
err := os.MkdirAll(dir, 0755)
if err != nil {
responses.Flash(w, r, "Error saving: can't create folder %s: %s", dir, err)
}
// Write the file.
err = ioutil.WriteFile(fp, body, 0644)
if err != nil { if err != nil {
responses.Flash(w, r, "Error saving: %s", err) responses.Flash(w, r, "Error saving: %s", err)
} else {
if render.HasHTMLSuffix(file) {
responses.FlashAndRedirect(w, r, render.URLFromPath(file), "Page saved successfully!")
} else { } else {
responses.FlashAndRedirect(w, r, "/admin/editor?file="+url.QueryEscape(file), "Page saved successfully!") responses.FlashAndRedirect(w, r, "/admin/editor?file="+url.QueryEscape(file), "Page saved successfully!")
}
return return
} }
} else if deleting { } else if deleting {
@ -62,8 +75,11 @@ func editorHandler(w http.ResponseWriter, r *http.Request) {
f, err := os.Stat(fp) f, err := os.Stat(fp)
if os.IsNotExist(err) { if os.IsNotExist(err) {
fp = filepath.Join(*render.DocumentRoot, file) fp = filepath.Join(*render.DocumentRoot, file)
fromCore = true
f, err = os.Stat(fp) f, err = os.Stat(fp)
if !os.IsNotExist(err) {
// The file was found in the core.
fromCore = true
}
} }
// If it exists, load it. // If it exists, load it.
@ -114,6 +130,11 @@ func editorFileList(w http.ResponseWriter, r *http.Request) {
return nil return nil
} }
// Hide vendored files.
if i == 1 && strings.HasPrefix(rel, "js/ace-editor") {
return nil
}
// Only text files. // Only text files.
ext := strings.ToLower(filepath.Ext(path)) ext := strings.ToLower(filepath.Ext(path))
okTypes := []string{ okTypes := []string{

View File

@ -9,10 +9,10 @@ import (
"github.com/kirsle/blog/internal/markdown" "github.com/kirsle/blog/internal/markdown"
"github.com/kirsle/blog/internal/middleware/auth" "github.com/kirsle/blog/internal/middleware/auth"
"github.com/kirsle/blog/models/posts"
"github.com/kirsle/blog/internal/render" "github.com/kirsle/blog/internal/render"
"github.com/kirsle/blog/internal/responses" "github.com/kirsle/blog/internal/responses"
"github.com/kirsle/blog/internal/types" "github.com/kirsle/blog/internal/types"
"github.com/kirsle/blog/models/posts"
) )
// editHandler is the blog writing and editing page. // editHandler is the blog writing and editing page.
@ -21,6 +21,7 @@ func editHandler(w http.ResponseWriter, r *http.Request) {
"preview": "", "preview": "",
} }
var post *posts.Post var post *posts.Post
var isNew bool
// Are we editing an existing post? // Are we editing an existing post?
if idStr := r.FormValue("id"); idStr != "" { if idStr := r.FormValue("id"); idStr != "" {
@ -30,10 +31,12 @@ func editHandler(w http.ResponseWriter, r *http.Request) {
if err != nil { if err != nil {
v["Error"] = errors.New("that post ID was not found") v["Error"] = errors.New("that post ID was not found")
post = posts.New() post = posts.New()
isNew = true
} }
} }
} else { } else {
post = posts.New() post = posts.New()
isNew = true
} }
if r.Method == http.MethodPost { if r.Method == http.MethodPost {
@ -55,8 +58,14 @@ func editHandler(w http.ResponseWriter, r *http.Request) {
author, _ := auth.CurrentUser(r) author, _ := auth.CurrentUser(r)
post.AuthorID = author.ID post.AuthorID = author.ID
// When editing, allow to not touch the last updated time.
if !isNew && r.FormValue("no-update") == "true" {
post.Updated = post.Created
} else {
post.Updated = time.Now().UTC() post.Updated = time.Now().UTC()
}
err = post.Save() err = post.Save()
if err != nil { if err != nil {
v["Error"] = err v["Error"] = err
} else { } else {

View File

@ -2,6 +2,7 @@ package render
import ( import (
"errors" "errors"
"fmt"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
@ -15,6 +16,17 @@ var (
DocumentRoot *string DocumentRoot *string
) )
// File extensions and URL suffixes that map to real files on disk, but which
// have suffixes hidden from the URL.
var hiddenSuffixes = []string{
".gohtml",
".html",
"/index.gohtml",
"/index.html",
".md",
"/index.md",
}
// Filepath represents a file discovered in the document roots, and maintains // Filepath represents a file discovered in the document roots, and maintains
// both its relative and absolute components. // both its relative and absolute components.
type Filepath struct { type Filepath struct {
@ -71,15 +83,7 @@ func ResolvePath(path string) (Filepath, error) {
} }
// Try some supported suffixes. // Try some supported suffixes.
suffixes := []string{ for _, suffix := range hiddenSuffixes {
".gohtml",
".html",
"/index.gohtml",
"/index.html",
".md",
"/index.md",
}
for _, suffix := range suffixes {
test := absPath + suffix test := absPath + suffix
if stat, err := os.Stat(test); !os.IsNotExist(err) && !stat.IsDir() { if stat, err := os.Stat(test); !os.IsNotExist(err) && !stat.IsDir() {
debug("Filepath found via suffix %s: %s", suffix, test) debug("Filepath found via suffix %s: %s", suffix, test)
@ -90,3 +94,36 @@ func ResolvePath(path string) (Filepath, error) {
return Filepath{}, errors.New("not found") return Filepath{}, errors.New("not found")
} }
// HasHTMLSuffix returns whether the file path will be renderable as HTML
// for the front-end. Basically, whether it ends with a .gohtml, .html or .md
// suffix and/or is an index page.
func HasHTMLSuffix(path string) bool {
for _, suffix := range hiddenSuffixes {
if strings.HasSuffix(path, suffix) {
return true
}
}
return false
}
// URLFromPath returns an HTTP path that matches the file path on disk.
//
// For example, given the file path "folder/page.md" it would return the string
// "/folder/page"
func URLFromPath(path string) string {
// Strip leading slashes.
if path[0] == '/' {
path = strings.TrimPrefix(path, "/")
}
// Hide-able suffixes.
for _, suffix := range hiddenSuffixes {
if strings.HasSuffix(path, suffix) {
path = strings.TrimSuffix(path, suffix)
break
}
}
return fmt.Sprintf("/%s", path)
}

View File

@ -79,7 +79,7 @@
{{ template "content" . }} {{ template "content" . }}
{{ if and .CurrentUser.Admin .Editable (ne .TemplatePath ".markdown") }} {{ if and .CurrentUser.Admin .Editable (ne .TemplatePath ".markdown.gohtml") }}
<p class="mt-4"> <p class="mt-4">
<strong>Admin:</strong> [<a href="/admin/editor?file={{ .TemplatePath }}">edit this page</a>] <strong>Admin:</strong> [<a href="/admin/editor?file={{ .TemplatePath }}">edit this page</a>]
</p> </p>

View File

@ -2,9 +2,23 @@
{{ define "content" }} {{ define "content" }}
<h1>Page Editor</h1> <h1>Page Editor</h1>
<form class="form-inline" method="GET" action="/admin/editor">
Create a new page:
<div class="input-group">
<input type="text"
name="file"
class="form-control ml-2"
placeholder="about.md">
<div class="input-group-append">
<button type="submit" class="btn btn-primary">Go</button>
</div>
</div>
</form>
{{ range .Data.FileTrees }} {{ range .Data.FileTrees }}
{{ if .UserRoot }} {{ if .UserRoot }}
<h2>User Root</h2> <h2>User Root</h2>
<p> <p>
These are your custom web files that override those in the CoreRoot. These are your custom web files that override those in the CoreRoot.
</p> </p>

View File

@ -8,6 +8,8 @@
Preview Preview
</div> </div>
<div class="card-body"> <div class="card-body">
<h1 class="blog-title">{{ .Data.post.Title }}</h1>
{{ .Data.preview }} {{ .Data.preview }}
</div> </div>
</div> </div>
@ -139,6 +141,17 @@
> Enable comments on this post > Enable comments on this post
</label> </label>
</div> </div>
{{ if .ID }}
<div class="form-check">
<label class="form-check-label">
<input type="checkbox"
class="form-check-label"
name="no-update"
value="true"
> <strong>Editing:</strong> do not show a "last updated" label.
</label>
</div>
{{ end }}
</div> </div>
<div class="form-group"> <div class="form-group">