fix styles and build options, add error handling, start StaticFileManager implementation
This commit is contained in:
parent
3683e53c2a
commit
034d325ae3
9 changed files with 152 additions and 83 deletions
|
@ -1,10 +1,21 @@
|
|||
package archetype
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type BuildStatus struct {
|
||||
Success bool
|
||||
Message string
|
||||
}
|
||||
|
||||
type Page struct {
|
||||
Title string
|
||||
Content string
|
||||
Edited time.Time
|
||||
Error string
|
||||
}
|
||||
|
||||
type ConfigOption struct {
|
||||
Name string
|
||||
Type string
|
||||
|
@ -18,7 +29,7 @@ type Adapter interface {
|
|||
GetConfig() map[ConfigOption]string
|
||||
SetConfig(map[ConfigOption]string) error
|
||||
ListPages() map[string]string
|
||||
GetPage(string) (Page, error)
|
||||
GetPage(string) Page
|
||||
FormatPage(string) string
|
||||
FormattingHelp() string
|
||||
CreatePage(slug, title, content string) error
|
||||
|
|
|
@ -72,16 +72,25 @@ func (self *EurekaAdapter) ListPages() map[string]string {
|
|||
return pages
|
||||
}
|
||||
|
||||
func (self *EurekaAdapter) GetPage(filename string) (Page, error) {
|
||||
func (self *EurekaAdapter) GetPage(filename string) Page {
|
||||
if strings.Contains(filename, "../") || strings.Contains(filename, "..\\") {
|
||||
return Page{
|
||||
Error: "You cannot escape!",
|
||||
}
|
||||
}
|
||||
fullPath := filepath.Join(self.Root, "inc", filename)
|
||||
f, err := os.ReadFile(fullPath)
|
||||
|
||||
if err != nil {
|
||||
return Page{}, err
|
||||
return Page{
|
||||
Error: err.Error(),
|
||||
}
|
||||
}
|
||||
|
||||
if !strings.HasSuffix(filename, ".htm") {
|
||||
return Page{}, errors.New("Page file extension is not '.htm'")
|
||||
return Page{
|
||||
Error: "Page file extension is not '.htm'",
|
||||
}
|
||||
}
|
||||
|
||||
title := strings.Replace(
|
||||
|
@ -93,7 +102,7 @@ func (self *EurekaAdapter) GetPage(filename string) (Page, error) {
|
|||
Title: title,
|
||||
Content: content,
|
||||
Edited: fileInfo.ModTime(),
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (self *EurekaAdapter) FormatPage(raw string) string {
|
||||
|
@ -111,6 +120,10 @@ func (self *EurekaAdapter) CreatePage(slug, title, content string) error {
|
|||
slug = strings.ReplaceAll(title, " ", "_") + ".htm"
|
||||
path := filepath.Join(self.Root, "inc", slug)
|
||||
|
||||
if strings.Contains(slug, "../") || strings.Contains(slug, "..\\") {
|
||||
return errors.New("You cannot escape!")
|
||||
}
|
||||
|
||||
_, err := os.Stat(path)
|
||||
if err == nil || !os.IsNotExist(err) {
|
||||
return errors.New("File already exists")
|
||||
|
@ -127,6 +140,12 @@ func (self *EurekaAdapter) CreatePage(slug, title, content string) error {
|
|||
func (self *EurekaAdapter) SavePage(oldSlug, newSlug, title, content string) error {
|
||||
// eureka creates titles from slugs, so we transform the title into the slug
|
||||
newSlug = strings.ReplaceAll(title, " ", "_") + ".htm"
|
||||
|
||||
if strings.Contains(newSlug, "../") || strings.Contains(newSlug, "..\\") ||
|
||||
strings.Contains(oldSlug, "../") || strings.Contains(oldSlug, "..\\") {
|
||||
return errors.New("You cannot escape!")
|
||||
}
|
||||
|
||||
f, err := os.Create(filepath.Join(self.Root, "inc", newSlug))
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -151,6 +170,10 @@ func (self *EurekaAdapter) SavePage(oldSlug, newSlug, title, content string) err
|
|||
}
|
||||
|
||||
func (self *EurekaAdapter) DeletePage(slug string) error {
|
||||
if strings.Contains(slug, "../") || strings.Contains(slug, "..\\") {
|
||||
return errors.New("You cannot escape!")
|
||||
}
|
||||
|
||||
siteRoot := self.Config[ConfigOption{
|
||||
Name: "SITEROOT",
|
||||
Type: "string",
|
||||
|
@ -164,14 +187,14 @@ func (self *EurekaAdapter) DeletePage(slug string) error {
|
|||
}
|
||||
|
||||
func (self *EurekaAdapter) Build(buildOptions map[string][]string) BuildStatus {
|
||||
|
||||
twtxt := buildOptions["twtxt"][0]
|
||||
cmdArgs := ""
|
||||
twtxt := strings.Join(buildOptions["twtxt"], " ")
|
||||
cmdArgs := []string{}
|
||||
if twtxt != "" {
|
||||
cmdArgs += "-t " + twtxt
|
||||
cmdArgs = append(cmdArgs, "-t")
|
||||
cmdArgs = append(cmdArgs, twtxt)
|
||||
}
|
||||
|
||||
cmd := exec.Command("./build.sh", cmdArgs)
|
||||
cmd := exec.Command("./build.sh", cmdArgs...)
|
||||
cmd.Dir = self.Root
|
||||
out, err := cmd.CombinedOutput()
|
||||
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
package archetype
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type Page struct {
|
||||
Title string
|
||||
Content string
|
||||
Edited time.Time
|
||||
}
|
16
archetype/staticFileManager.go
Normal file
16
archetype/staticFileManager.go
Normal file
|
@ -0,0 +1,16 @@
|
|||
package archetype
|
||||
|
||||
type StaticFileManager struct {
|
||||
Root string
|
||||
ShowHtml bool
|
||||
ShowHidden bool
|
||||
}
|
||||
|
||||
type FileManager interface {
|
||||
Init(cfg Config) error
|
||||
ListTree() []string
|
||||
ListSubTree(root string) []string
|
||||
AddFile(path string, file interface{}) error
|
||||
MkDir(path string) error
|
||||
Remove(path string) error
|
||||
}
|
76
nirvash.go
76
nirvash.go
|
@ -3,9 +3,9 @@ package main
|
|||
import (
|
||||
"net/http"
|
||||
core "nilfm.cc/git/nirvash/archetype"
|
||||
shell "nilfm.cc/git/nirvash/lfo"
|
||||
. "nilfm.cc/git/nirvash/lfo"
|
||||
"nilfm.cc/git/quartzgun/indentalUserDB"
|
||||
"nilfm.cc/git/quartzgun/middleware"
|
||||
. "nilfm.cc/git/quartzgun/middleware"
|
||||
"nilfm.cc/git/quartzgun/renderer"
|
||||
"nilfm.cc/git/quartzgun/router"
|
||||
"os"
|
||||
|
@ -40,14 +40,14 @@ func main() {
|
|||
rtr.Get("/login", renderer.Template(
|
||||
pathConcat(templateRoot, "login.html")))
|
||||
|
||||
rtr.Post("/login", middleware.Authorize("/", udb, "/login?tryagain=1"))
|
||||
rtr.Post("/login", Authorize("/", udb, "/login?tryagain=1"))
|
||||
|
||||
rtr.Get("/logout", middleware.Bunt("/", udb, "/login?tryagain=1"))
|
||||
rtr.Get("/logout", Bunt("/", udb, "/login?tryagain=1"))
|
||||
|
||||
rtr.Get(
|
||||
"/",
|
||||
middleware.Protected(
|
||||
shell.WithAdapter(
|
||||
Protected(
|
||||
WithAdapter(
|
||||
renderer.Template(
|
||||
pathConcat(templateRoot, "cms_list.html"),
|
||||
pathConcat(templateRoot, "header.html"),
|
||||
|
@ -59,9 +59,9 @@ func main() {
|
|||
|
||||
rtr.Get(
|
||||
`/edit/(?P<Slug>\S+)`,
|
||||
middleware.Fortify(
|
||||
middleware.Protected(
|
||||
shell.WithAdapter(
|
||||
Fortify(
|
||||
Protected(
|
||||
WithAdapter(
|
||||
renderer.Template(
|
||||
pathConcat(templateRoot, "cms_edit.html"),
|
||||
pathConcat(templateRoot, "header.html"),
|
||||
|
@ -73,10 +73,10 @@ func main() {
|
|||
|
||||
rtr.Post(
|
||||
`/save/(?P<Slug>\S+)`,
|
||||
middleware.Defend(
|
||||
middleware.Protected(
|
||||
shell.WithAdapter(
|
||||
shell.EnsurePageData(
|
||||
Defend(
|
||||
Protected(
|
||||
WithAdapter(
|
||||
EnsurePageData(
|
||||
renderer.Template(
|
||||
pathConcat(templateRoot, "cms_save.html"),
|
||||
pathConcat(templateRoot, "header.html"),
|
||||
|
@ -91,9 +91,9 @@ func main() {
|
|||
|
||||
rtr.Get(
|
||||
`/new`,
|
||||
middleware.Fortify(
|
||||
middleware.Protected(
|
||||
shell.WithAdapter(
|
||||
Fortify(
|
||||
Protected(
|
||||
WithAdapter(
|
||||
renderer.Template(
|
||||
pathConcat(templateRoot, "cms_new.html"),
|
||||
pathConcat(templateRoot, "header.html"),
|
||||
|
@ -105,10 +105,10 @@ func main() {
|
|||
|
||||
rtr.Post(
|
||||
`/create`,
|
||||
middleware.Defend(
|
||||
middleware.Protected(
|
||||
shell.WithAdapter(
|
||||
shell.EnsurePageData(
|
||||
Defend(
|
||||
Protected(
|
||||
WithAdapter(
|
||||
EnsurePageData(
|
||||
renderer.Template(
|
||||
pathConcat(templateRoot, "cms_create.html"),
|
||||
pathConcat(templateRoot, "header.html"),
|
||||
|
@ -123,9 +123,9 @@ func main() {
|
|||
|
||||
rtr.Get(
|
||||
`/build`,
|
||||
middleware.Fortify(
|
||||
middleware.Protected(
|
||||
shell.WithAdapter(
|
||||
Fortify(
|
||||
Protected(
|
||||
WithAdapter(
|
||||
renderer.Template(
|
||||
pathConcat(templateRoot, "build.html"),
|
||||
pathConcat(templateRoot, "header.html"),
|
||||
|
@ -137,10 +137,10 @@ func main() {
|
|||
|
||||
rtr.Post(
|
||||
`/build-run`,
|
||||
middleware.Defend(
|
||||
middleware.Protected(
|
||||
shell.SanitizeFormMap(
|
||||
shell.WithAdapter(
|
||||
Defend(
|
||||
Protected(
|
||||
SanitizeFormMap(
|
||||
WithAdapter(
|
||||
renderer.Template(
|
||||
pathConcat(templateRoot, "build_run.html"),
|
||||
pathConcat(templateRoot, "header.html"),
|
||||
|
@ -154,9 +154,9 @@ func main() {
|
|||
|
||||
rtr.Post(
|
||||
`/delete/(?P<Slug>\S+)`,
|
||||
middleware.Defend(
|
||||
middleware.Protected(
|
||||
shell.WithAdapter(
|
||||
Defend(
|
||||
Protected(
|
||||
WithAdapter(
|
||||
renderer.Template(
|
||||
pathConcat(templateRoot, "delete.html"),
|
||||
pathConcat(templateRoot, "header.html"),
|
||||
|
@ -170,9 +170,9 @@ func main() {
|
|||
|
||||
rtr.Get(
|
||||
`/config`,
|
||||
middleware.Fortify(
|
||||
middleware.Protected(
|
||||
shell.WithAdapter(
|
||||
Fortify(
|
||||
Protected(
|
||||
WithAdapter(
|
||||
renderer.Template(
|
||||
pathConcat(templateRoot, "config.html"),
|
||||
pathConcat(templateRoot, "header.html"),
|
||||
|
@ -184,11 +184,11 @@ func main() {
|
|||
|
||||
rtr.Post(
|
||||
`/config-set`,
|
||||
middleware.Defend(
|
||||
middleware.Protected(
|
||||
shell.SanitizeFormMap(
|
||||
shell.FormMapToAdapterConfig(
|
||||
shell.WithAdapter(
|
||||
Defend(
|
||||
Protected(
|
||||
SanitizeFormMap(
|
||||
FormMapToAdapterConfig(
|
||||
WithAdapter(
|
||||
renderer.Template(
|
||||
pathConcat(templateRoot, "config_set.html"),
|
||||
pathConcat(templateRoot, "header.html"),
|
||||
|
|
|
@ -3,7 +3,6 @@ body {
|
|||
margin: 0;
|
||||
font-family: sans-serif;
|
||||
font-size: 16px;
|
||||
height: 100vh;
|
||||
background: black;
|
||||
color: white;
|
||||
background: url('/static/bg2.png');
|
||||
|
@ -16,6 +15,7 @@ body {
|
|||
background: url('/static/bg.png');
|
||||
background-size: cover;
|
||||
background-position: center center;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.login {
|
||||
|
@ -48,7 +48,7 @@ body {
|
|||
}
|
||||
|
||||
|
||||
.login form input, .login form input:-internal-autofill-selected {
|
||||
.login form input, #user-input, #password-input {
|
||||
display: block;
|
||||
margin: 1em;
|
||||
margin-left: auto;
|
||||
|
@ -60,7 +60,7 @@ body {
|
|||
padding: 0.2em;
|
||||
}
|
||||
|
||||
.login form input[type="text"], login form input[type="password"] {
|
||||
.login form input[type="text"], .login form input[type="password"] {
|
||||
transition: border 1s;
|
||||
outline: none;
|
||||
margin-bottom: -17px;
|
||||
|
@ -73,7 +73,6 @@ body {
|
|||
|
||||
|
||||
.login form input[type="submit"] {
|
||||
margin-top: -17px;
|
||||
text-transform: uppercase;
|
||||
transition: background 1s, color 1s;
|
||||
}
|
||||
|
@ -84,7 +83,7 @@ body {
|
|||
}
|
||||
|
||||
.login .error {
|
||||
positon: relative;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
|
@ -109,11 +108,12 @@ h1 {
|
|||
|
||||
nav {
|
||||
text-align: center;
|
||||
background: rgba(0,0,0,0.8);
|
||||
padding-top: 0.5em;
|
||||
background: rgba(0,0,0,0.8);
|
||||
padding-top: 0.5em;
|
||||
padding-bottom: 0.5em;
|
||||
position: sticky;
|
||||
top: 0px;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
nav ul {
|
||||
|
@ -136,9 +136,23 @@ a:hover {
|
|||
color: cyan;
|
||||
}
|
||||
|
||||
a.new-page-button {
|
||||
.new-page-button-wrapper {
|
||||
display: block;
|
||||
width: 80%;
|
||||
max-width: 500px;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
top: 5em;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
z-index: 2;
|
||||
text-align: right;
|
||||
height: 0;
|
||||
overflow-y: visible;
|
||||
}
|
||||
|
||||
a.new-page-button {
|
||||
position: relative;
|
||||
top: 1em;
|
||||
text-decoration: none;
|
||||
background: transparent;
|
||||
border: solid 2px lightgray;
|
||||
|
@ -147,8 +161,6 @@ a.new-page-button {
|
|||
padding: 0.2em;
|
||||
text-transform: uppercase;
|
||||
transition: background 1s, color 1s;
|
||||
float: right;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
a.new-page-button:hover {
|
||||
|
@ -164,8 +176,7 @@ h2 {
|
|||
display: block;
|
||||
border-left: 8px solid cyan;
|
||||
padding-left: 8px;
|
||||
position: sticky;
|
||||
top: 3em;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.page-list, form.editor, form.build, form.configurator, span.adapter-error, span.adapter-success, .danger-zone {
|
||||
|
@ -177,7 +188,6 @@ h2 {
|
|||
padding: 2em;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
max-height: calc(100vh - 20em);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
|
@ -210,6 +220,12 @@ form.editor input[type="text"], form.configurator input[type="text"], form.confi
|
|||
|
||||
}
|
||||
|
||||
|
||||
|
||||
form.editor input.slug-input {
|
||||
font-size: 100%;
|
||||
}
|
||||
|
||||
form.editor input.title-input {
|
||||
font-size: 150%;
|
||||
}
|
||||
|
@ -230,7 +246,7 @@ form.editor textarea {
|
|||
}
|
||||
|
||||
form.configurator textarea {
|
||||
marign: 0;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
height: 5em;
|
||||
}
|
||||
|
@ -272,3 +288,7 @@ form.editor input[type="submit"]:hover,form.build input[type="submit"]:hover, .d
|
|||
form input[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
form input[readonly] {
|
||||
border: none;
|
||||
}
|
|
@ -6,6 +6,12 @@
|
|||
|
||||
{{ template "header" . }}
|
||||
|
||||
{{ if ($page).Error }}
|
||||
<h2>Page Error</h2>
|
||||
|
||||
<span class="adapter-error">{{($page).Error}}</span>
|
||||
{{ else }}
|
||||
|
||||
<h2>Edit Page</h2>
|
||||
|
||||
<form class="editor" method="POST" action="/save/{{$slug}}">
|
||||
|
@ -14,12 +20,12 @@
|
|||
{{ end }}
|
||||
<input hidden name="csrfToken" value="{{$csrfToken}}"/>
|
||||
<input hidden name="oldSlug" value="{{$slug}}"/>
|
||||
{{ if $editableSlugs }}
|
||||
<label for="title">Title</label><br/>
|
||||
<input class="title-input" id="title" type="text" name="title" value="{{($page).Title}}" required/><br/>
|
||||
{{ if $editableSlugs }}
|
||||
<label for="slug">Slug</label><br/>
|
||||
<input class="slug-input" id="slug" type="text" name="slug" value="{{$slug}}" required/><br/>
|
||||
{{ end }}
|
||||
<label for="title">Title</label><br/>
|
||||
<input class="title-input" id="title" type="text" name="title" value="{{($page).Title}}" required/><br/>
|
||||
<span class="edited-time">last edited {{($page).Edited.Format "2006-01-02 15:04"}}</span><br/>
|
||||
<label for="content">Content</label><br/>
|
||||
<textarea class="content-input" id="content" name="content" required>{{($page).Content}}</textarea><br/>
|
||||
|
@ -38,4 +44,7 @@
|
|||
<input type="submit" value="DELETE"/>
|
||||
</form>
|
||||
</details>
|
||||
|
||||
{{ end }}
|
||||
|
||||
{{ template "footer" . }}
|
||||
|
|
|
@ -4,8 +4,11 @@
|
|||
|
||||
<h2>Pages</h2>
|
||||
|
||||
<div class="page-list">
|
||||
<div class="new-page-button-wrapper">
|
||||
<a class="new-page-button" href="/new">New Page</a>
|
||||
</div>
|
||||
|
||||
<div class="page-list">
|
||||
<ul>
|
||||
{{ range $slug, $title := $pages }}
|
||||
<li><a href="/edit/{{$slug}}">{{$title}}</a></li>
|
||||
|
@ -13,6 +16,4 @@
|
|||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="static-files-list">
|
||||
</div>
|
||||
{{ template "footer" .}}
|
||||
{{ template "footer" .}}
|
||||
|
|
|
@ -11,12 +11,12 @@
|
|||
<span class="edit-error">Empty fields are not allowed - please try again</span><br/>
|
||||
{{ end }}
|
||||
<input hidden name="csrfToken" value="{{$csrfToken}}"/>
|
||||
<label for="title">Title</label><br/>
|
||||
<input class="title-input" id="title" type="text" name="title" required/><br/>
|
||||
{{ if $editableSlugs }}
|
||||
<label for="slug">Slug</label><br/>
|
||||
<input class="slug-input" id="slug" type="text" name="slug" required/><br/>
|
||||
{{ end }}
|
||||
<label for="title">Title</label><br/>
|
||||
<input class="title-input" id="title" type="text" name="title" required/><br/>
|
||||
<label for="content">Content</label><br/>
|
||||
<textarea class="content-input" id="content" name="content" required></textarea><br/>
|
||||
<input type="submit" value="Save"/>
|
||||
|
|
Loading…
Reference in a new issue