tweak file manager UI and route, add file-move feature, add EurekaAdapter formatting help, and update README

This commit is contained in:
Iris Lightshard 2022-06-15 01:33:56 -06:00
parent 119d66cd27
commit 87333f9d87
Signed by: Iris Lightshard
GPG key ID: 3B7FBC22144E6398
17 changed files with 361 additions and 67 deletions

View file

@ -13,8 +13,11 @@ Clone this repository and run `go build` to build `nirvash`. Just running `./nir
``` ```
adapter=eureka // one of the supported adapters, currently just eureka adapter=eureka // one of the supported adapters, currently just eureka
root=/path/to/ssg/root // path to where your SSG content root is root=/path/to/ssg/root // path to where your SSG content root is
assetRoot=/path/to/asset/root // path to the Nirvash static assets (eg static/ directory in this repo) assetRoot=/path/to/asset/root // path to the parent folder containing Nirvash static/ assets and templates/ directory (eg base directory of this repo)
staticRoot=/path/to/static/root // path to static file storage on your webserver staticRoot=/path/to/static/root // path to static file storage on your webserver
staticShowHTML=false // true or false, show HTML files in the file manager interface
staticShowHidden=false // true or false, show hidden files in the file manager interface
staticMaxUploadMB=25 // integer, maximum size in MB of files uploaded in the file manager interface
plugins=none // list of plugins to use, currently none are implemented plugins=none // list of plugins to use, currently none are implemented
``` ```
@ -30,4 +33,31 @@ User management is done from the command line as well:
Running `nirvash` without any arguments starts the webserver on port 8080. Running `nirvash` without any arguments starts the webserver on port 8080.
MORE TO COME Initially the user will be presented with the login screen; upon successful login, the application presents the navbar with these options:
- `Pages`: the default page, shows a list of existing pages - clicking one enables editing that page; a button is also presented for adding a new page. Each `Adapter` will provide different formatting help and can allow editable slugs/URLs or not (eg, the `EurekaAdapter` builds slugs/URLs directly from the page title).
- `Files`: provides an interface for managing statically hosted files. Files and directories can be added, moved, and deleted.
- `Build`: a simple form to build the site - build options configurable by `Adapter` are present under an accordion.
- `Configuration`: interface to the configuration for the `Adapter`. Each `Adapter` provides its own configuration interface with associated data types (currently supported: `int`, `float`, `string`, and `multilinestring`)
- `Logout`: logs the user out and returns to the login screen
## adapter interface
`nirvash` is extensible by `Adapter`s that can interact with almost any static site generator under the hood.
The `Adapter` interface and associated data types can be found in the [adapter.go](https://nilfm.cc/git/nirvash/tree/archetype/adapter.go) file, but the basic interface looks like this:
- `Init(cfg *Config)`: set any initial settings that need to be handled - typically import SSG data root from the `nirvash` config file and read the SSG's own config file
- `Name() string`: the name of the adapter, used for the `nirvash.conf` config file
- `EditableSlugs() bool`: whether slugs can be edited independently or are calculated based on, eg, page titles
- `BuildOptions() []string`: a list of names of the build options to present on the `/build` page
- `GetConfig() map[ConfigOption]string`: retrieves the config to present on the `/config` page
- `SetConfig(map[ConfigOption]string) error`: takes the config from the `/config` page and applies it, typically by writing it to a file
- `ListPages() map[string]string`: list the pages in the site; keys are the slugs, values are the titles
- `GetPage(slug string) Page`: given a slug, return the page data
- `FormatPage(string) string`: given the raw page data as input by the user, return HTML suitable for preview (currently unimplemented and unused)
- `FormattingHelp() string`: return a string to be inserted into a `pre` tag on the `/fmt-help` page
- `CreatePage(slug, title, content string) error`: given all the proper arguments, create a new page in the backing store (eg filesystem, db)
- `SavePage(oldSlug, newSlug, title, content string) error`: given all the proper arguments, save a page to the backing store (eg filesystem, db)
- `DeletePage(slug string) error`: given a slug, delete the corresponding page source and possibly its generated HTML, depending on the `Adapter`
- `Build(buildOptions map[string][]string) BuildStatus`: takes a map of build option names to their values and builds the site, returning a `BuildStatus` object containing the success or failure as a boolean and the detailed status (eg, the console ouptut of the build process)

View file

@ -29,8 +29,8 @@ type Adapter interface {
GetConfig() map[ConfigOption]string GetConfig() map[ConfigOption]string
SetConfig(map[ConfigOption]string) error SetConfig(map[ConfigOption]string) error
ListPages() map[string]string ListPages() map[string]string
GetPage(string) Page GetPage(slug string) Page
FormatPage(string) string FormatPage(raw string) string
FormattingHelp() string FormattingHelp() string
CreatePage(slug, title, content string) error CreatePage(slug, title, content string) error
SavePage(oldSlug, newSlug, title, content string) error SavePage(oldSlug, newSlug, title, content string) error

View file

@ -5,6 +5,7 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"runtime" "runtime"
"strconv"
"strings" "strings"
) )
@ -12,8 +13,9 @@ type Config struct {
Adapter Adapter // adapter for this instance Adapter Adapter // adapter for this instance
Root string // root of the site data Root string // root of the site data
StaticRoot string // root of static files for StaticFileManager StaticRoot string // root of static files for StaticFileManager
StaticShowHidden bool // whether to show hidden files in the StaticFileManager StaticShowHidden bool // whether to show hidden files in the FileManager
StaticShowHtml bool // whether to show html files in the StaticFileManager StaticShowHTML bool // whether to show html files in the FileManager
StaticMaxUploadMB int64 // max size in MB of files uploaded via FileManager
AssetRoot string // root of Nirvash dist files (CSS, images) AssetRoot string // root of Nirvash dist files (CSS, images)
Plugins map[string]interface{} Plugins map[string]interface{}
} }
@ -63,6 +65,7 @@ func (self *Config) SetAdapter(adapter string) {
} }
func (self *Config) IsNull() bool { func (self *Config) IsNull() bool {
// zero-values for StaticShowHTML, StaticShowHidden, and StaticMaxUploadMB are valid
return self.Adapter == nil || len(self.Root) == 0 || len(self.StaticRoot) == 0 || len(self.AssetRoot) == 0 return self.Adapter == nil || len(self.Root) == 0 || len(self.StaticRoot) == 0 || len(self.AssetRoot) == 0
} }
@ -88,11 +91,22 @@ func (self *Config) RunWizard() {
self.Root = inputBuf self.Root = inputBuf
inputBuf = "" inputBuf = ""
fmt.Printf("static file root? ") fmt.Printf("static file root? ")
ensureNonEmptyOption(&inputBuf) ensureNonEmptyOption(&inputBuf)
self.StaticRoot = inputBuf self.StaticRoot = inputBuf
inputBuf = ""
fmt.Printf("show HTML files in file manager? ")
self.StaticShowHTML = ensureBooleanOption(&inputBuf)
inputBuf = ""
fmt.Printf("show hidden files in file manager? ")
self.StaticShowHidden = ensureBooleanOption(&inputBuf)
inputBuf = ""
fmt.Printf("max upload size (MB)? ")
self.StaticMaxUploadMB = ensureNumberOption(&inputBuf)
inputBuf = "" inputBuf = ""
fmt.Printf("nirvash asset root? ") fmt.Printf("nirvash asset root? ")
ensureNonEmptyOption(&inputBuf) ensureNonEmptyOption(&inputBuf)
@ -113,6 +127,31 @@ func ensureNonEmptyOption(buffer *string) {
if len(strings.TrimSpace(*buffer)) != 0 { if len(strings.TrimSpace(*buffer)) != 0 {
break break
} }
fmt.Println("Please enter a nonempty value")
}
}
func ensureBooleanOption(buffer *string) bool {
for {
fmt.Scanln(buffer)
trimmedBuf := strings.TrimSpace(*buffer)
v, err := strconv.ParseBool(trimmedBuf)
if err == nil {
return v
}
fmt.Println("Please enter a true or false value")
}
}
func ensureNumberOption(buffer *string) int64 {
for {
fmt.Scanln(buffer)
trimmedBuf := strings.TrimSpace(*buffer)
v, err := strconv.ParseInt(trimmedBuf, 10, 64)
if err == nil && v > 0 {
return v
}
fmt.Println("Please enter a positive integer")
} }
} }
@ -126,6 +165,9 @@ func writeConfig(cfg *Config, configFile string) error {
f.WriteString("root=" + cfg.Root + "\n") f.WriteString("root=" + cfg.Root + "\n")
f.WriteString("staticRoot=" + cfg.StaticRoot + "\n") f.WriteString("staticRoot=" + cfg.StaticRoot + "\n")
f.WriteString("staticShowHTML=" + strconv.FormatBool(cfg.StaticShowHTML) + "\n")
f.WriteString("staticShowHidden=" + strconv.FormatBool(cfg.StaticShowHidden) + "\n")
f.WriteString("staticMaxUploadMB=" + strconv.FormatInt(cfg.StaticMaxUploadMB, 10) + "\n")
f.WriteString("assetRoot=" + cfg.AssetRoot + "\n") f.WriteString("assetRoot=" + cfg.AssetRoot + "\n")
f.WriteString("adapter=" + cfg.Adapter.Name() + "\n") f.WriteString("adapter=" + cfg.Adapter.Name() + "\n")
f.WriteString("plugins=\n") f.WriteString("plugins=\n")
@ -159,6 +201,12 @@ func parseConfig(configFile string) *Config {
cfg.Root = v cfg.Root = v
case "staticRoot": case "staticRoot":
cfg.StaticRoot = v cfg.StaticRoot = v
case "staticShowHTML":
cfg.StaticShowHTML, _ = strconv.ParseBool(v)
case "staticShowHidden":
cfg.StaticShowHidden, _ = strconv.ParseBool(v)
case "staticMaxUploadMB":
cfg.StaticMaxUploadMB, _ = strconv.ParseInt(v, 10, 64)
case "assetRoot": case "assetRoot":
cfg.AssetRoot = v cfg.AssetRoot = v
case "plugins": case "plugins":

View file

@ -112,7 +112,70 @@ func (self *EurekaAdapter) FormatPage(raw string) string {
func (self *EurekaAdapter) FormattingHelp() string { func (self *EurekaAdapter) FormattingHelp() string {
// TODO: show Eureka formatting guide // TODO: show Eureka formatting guide
return "help!" return `// shorthand for linking other pages
{page name}
// shorthand for page transclusion
{/page name}
// shorthand for arbitary link
{*destination url|text}
// shorthand for an image you can click to see the full sized version
{:anchor-id|image url|alt text}
// shorthand for an image with arbitrary link destination
{?anchor-id|destination url|image url|alt text}
// shorthand for an audio player
{_/path/to/media}
// shorthand for paragraphs, can embed other markup inside it
{&paragraph text {with a link} {@and some bold text}}
// shorthand for ordered lists, can embed other markup inside it
{#
{-item one}
{-item two}
{-item three}
}
// shorthand for unordered lists, can embed other markup inside it
{,
{-item one}
{-item two}
{-item three}
}
// shorthand for bold
{@bold text}
// shorthand for italic
{~italic text}
// shorthand for code
{` + "`" + `short code}
// shorthand for pre
{$longer code}
// shorthand for quote
{'short quote}
// shorthand for blockquote
{>longer quote}
// shorthand for strikethrough
{\crossed-out text}
// shorthand for level 3 heading
{!heading text}
// shorthand for level 4 heading
{.heading text}
// shorthand for publish date (renders as <time class='publish-date'>)
{+2022-02-22}`
} }
func (self *EurekaAdapter) CreatePage(slug, title, content string) error { func (self *EurekaAdapter) CreatePage(slug, title, content string) error {

View file

@ -11,8 +11,9 @@ import (
type SimpleFileManager struct { type SimpleFileManager struct {
Root string Root string
ShowHtml bool ShowHTML bool
ShowHidden bool ShowHidden bool
maxUploadMB int64
} }
type FileData struct { type FileData struct {
@ -37,13 +38,15 @@ type FileManager interface {
AddFile(path string, req *http.Request) error AddFile(path string, req *http.Request) error
MkDir(path, newDir string) error MkDir(path, newDir string) error
Remove(path string) error Remove(path string) error
// Rename(old, new string) error Rename(oldFullPath, newPath, newName string) error
MaxUploadMB() int64
} }
func (self *SimpleFileManager) Init(cfg *Config) error { func (self *SimpleFileManager) Init(cfg *Config) error {
self.Root = filepath.Clean(cfg.StaticRoot) self.Root = filepath.Clean(cfg.StaticRoot)
self.ShowHtml = cfg.StaticShowHtml self.ShowHTML = cfg.StaticShowHTML
self.ShowHidden = cfg.StaticShowHidden self.ShowHidden = cfg.StaticShowHidden
self.maxUploadMB = cfg.StaticMaxUploadMB
return nil return nil
} }
@ -77,7 +80,10 @@ func (self *SimpleFileManager) ListSubTree(root string) FileListing {
list.Up = "/" list.Up = "/"
} }
if len(levels) >= 2 { if len(levels) >= 2 {
list.Up = "/" + strings.Join(levels[:len(levels)-1], "/") list.Up = strings.Join(levels[:len(levels)-1], "/")
if !strings.HasPrefix(list.Up, "/") {
list.Up = "/" + list.Up
}
} }
for _, file := range files { for _, file := range files {
@ -87,7 +93,7 @@ func (self *SimpleFileManager) ListSubTree(root string) FileListing {
if file.IsDir() { if file.IsDir() {
list.SubDirs = append(list.SubDirs, file.Name()) list.SubDirs = append(list.SubDirs, file.Name())
} else { } else {
if !self.ShowHtml && strings.HasSuffix(file.Name(), ".html") { if !self.ShowHTML && strings.HasSuffix(file.Name(), ".html") {
continue continue
} }
list.Files = append(list.Files, file.Name()) list.Files = append(list.Files, file.Name())
@ -147,7 +153,7 @@ func (self *SimpleFileManager) AddFile(path string, req *http.Request) error {
return errors.New("You cannot escape!") return errors.New("You cannot escape!")
} }
req.ParseMultipartForm(250 << 20) req.ParseMultipartForm(self.maxUploadMB << 20)
file, header, err := req.FormFile("file") file, header, err := req.FormFile("file")
if err != nil { if err != nil {
return err return err
@ -192,3 +198,28 @@ func (self *SimpleFileManager) MkDir(path, newDir string) error {
return os.Mkdir(newDirPath, 0755) return os.Mkdir(newDirPath, 0755)
} }
func (self *SimpleFileManager) Rename(oldFullPath, newPath, newName string) error {
fullPath := filepath.Join(self.Root, oldFullPath)
_, err := os.Stat(fullPath)
if err != nil {
return err
}
newParent := filepath.Join(self.Root, newPath)
_, err = os.Stat(newParent)
if err != nil {
return err
}
if newName == "" {
_, oldName := filepath.Split(oldFullPath)
newName = oldName
}
return os.Rename(fullPath, filepath.Join(newParent, newName))
}
func (self *SimpleFileManager) MaxUploadMB() int64 {
return self.maxUploadMB
}

View file

@ -75,21 +75,9 @@ func FormMapToAdapterConfig(next http.Handler, adapter core.Adapter) http.Handle
return http.HandlerFunc(handlerFunc) return http.HandlerFunc(handlerFunc)
} }
func WithFileData(next http.Handler, fileManager core.FileManager) http.Handler { func PrepareForUpload(next http.Handler, fileManager core.FileManager) http.Handler {
handlerFunc := func(w http.ResponseWriter, req *http.Request) { handlerFunc := func(w http.ResponseWriter, req *http.Request) {
ctx := req.Context() req.ParseMultipartForm(fileManager.MaxUploadMB() << 20)
fileSlug := ctx.Value("params").(map[string]string)["Slug"]
fileData := fileManager.GetFileData(fileSlug)
*req = *req.WithContext(context.WithValue(req.Context(), "file-data", fileData))
next.ServeHTTP(w, req)
}
return http.HandlerFunc(handlerFunc)
}
func PrepareForUpload(next http.Handler) http.Handler {
handlerFunc := func(w http.ResponseWriter, req *http.Request) {
req.ParseMultipartForm(250 << 20)
next.ServeHTTP(w, req) next.ServeHTTP(w, req)
} }

View file

@ -210,13 +210,13 @@ func main() {
"/")) "/"))
rtr.Get( rtr.Get(
`/static-mgr`, `/file-mgr`,
http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
http.Redirect(w, req, "/static-mgr/", http.StatusSeeOther) http.Redirect(w, req, "/file-mgr/", http.StatusSeeOther)
})) }))
rtr.Get( rtr.Get(
`/static-mgr/(?P<Slug>.*)`, `/file-mgr/(?P<Slug>.*)`,
Fortify( Fortify(
Protected( Protected(
WithFileManager( WithFileManager(
@ -234,17 +234,45 @@ func main() {
Fortify( Fortify(
Protected( Protected(
WithFileManager( WithFileManager(
WithFileData(
renderer.Template( renderer.Template(
pathConcat(templateRoot, "file_actions.html"), pathConcat(templateRoot, "file_actions.html"),
pathConcat(templateRoot, "header.html"), pathConcat(templateRoot, "header.html"),
pathConcat(templateRoot, "footer.html")), pathConcat(templateRoot, "footer.html")),
fileManager), fileManager),
http.MethodGet,
udb,
"/login")))
rtr.Get(
`/file-move/(?P<Slug>.*)`,
Fortify(
Protected(
WithFileManager(
renderer.Template(
pathConcat(templateRoot, "file_move.html"),
pathConcat(templateRoot, "header.html"),
pathConcat(templateRoot, "footer.html")),
fileManager), fileManager),
http.MethodGet, http.MethodGet,
udb, udb,
"/login"))) "/login")))
rtr.Post(
`/file-move-process/(?P<Slug>.*)`,
Defend(
Protected(
WithFileManager(
renderer.Template(
pathConcat(templateRoot, "file_move_process.html"),
pathConcat(templateRoot, "header.html"),
pathConcat(templateRoot, "footer.html")),
fileManager),
http.MethodGet,
udb,
"/login"),
udb,
"/"))
rtr.Post( rtr.Post(
`/file-delete/(?P<Slug>.*)`, `/file-delete/(?P<Slug>.*)`,
Defend( Defend(
@ -290,7 +318,8 @@ func main() {
udb, udb,
"/login"), "/login"),
udb, udb,
"/"))) "/"),
fileManager))
rtr.Get( rtr.Get(
`/mkdir/(?P<Slug>.*)`, `/mkdir/(?P<Slug>.*)`,
@ -321,12 +350,18 @@ func main() {
"/login"), "/login"),
udb, udb,
"/")) "/"))
// TODO:
// add directory POST performs the action of creating directory
// move-choose POST uses a form to navigate through the file tree
// - to use links to navigate, destination location uses the slug,
// - and file to move uses the POST parameters
// move-do POST moves the file when finalized in move-choose
rtr.Get(
`/fmt-help`,
Protected(
WithAdapter(
renderer.Template(
pathConcat(templateRoot, "format_help.html"),
pathConcat(templateRoot, "header.html"),
pathConcat(templateRoot, "footer.html")),
cfg.Adapter),
http.MethodGet,
udb,
"/login"))
http.ListenAndServe(":8080", rtr) http.ListenAndServe(":8080", rtr)
} }

View file

@ -149,7 +149,7 @@ a:hover {
overflow-y: visible; overflow-y: visible;
} }
a.new-page-button { .new-page-button {
position: relative; position: relative;
top: 1em; top: 1em;
text-decoration: none; text-decoration: none;
@ -164,7 +164,7 @@ a.new-page-button {
margin-bottom: 0.2em; margin-bottom: 0.2em;
} }
a.new-page-button:hover { .new-page-button:hover {
background: lightgray; background: lightgray;
color: black; color: black;
} }
@ -196,13 +196,13 @@ span.adapter-error {
border-bottom: 2px solid crimson; border-bottom: 2px solid crimson;
} }
form.editor label, form.build label, .danger-zone label, form.configurator label, form.file-move label, .mkdir label { form.editor label, form.build label, .danger-zone label, form.configurator label, form.file-move label, .mkdir label, .page-list label {
font-size: 80%; font-size: 80%;
color: lightgray; color: lightgray;
text-transform: uppercase; text-transform: uppercase;
} }
form.editor input, form.build input, form.editor textarea, form.configurator input, form.configurator textarea, .danger-zone input[type="submit"], .file-move input[type="submit"], .uploader label, .uploader input[type="submit"], .mkdir input { form.editor input, form.build input, form.editor textarea, form.configurator input, form.configurator textarea, .danger-zone input[type="submit"], .file-move input[type="submit"], .uploader label, .uploader input[type="submit"], .mkdir input, .page-list input[type="text"] {
display: block; display: block;
margin: 0; margin: 0;
margin-top: 0.2em; margin-top: 0.2em;
@ -247,7 +247,7 @@ form input:focus, form textarea:focus {
} }
form.editor, .wide { form.editor, .wide {
max-width: 80em; max-width: 80em !important;
} }
form.editor textarea { form.editor textarea {
@ -294,10 +294,18 @@ form.editor input[type="submit"]:hover,form.build input[type="submit"]:hover, .d
z-index: 1; z-index: 1;
} }
.page-list ul li a { .page-list ul li {
line-height: 2em; line-height: 2em;
} }
.page-list ul li span.file-nolink {
color: #7f7f7f;
}
.left-pad-uplink {
padding-left: 20px;
}
form input[hidden] { form input[hidden] {
display: none; display: none;
} }
@ -318,10 +326,15 @@ form input[readonly] {
.file-actions-icon { .file-actions-icon {
display: inline-block; display: inline-block;
opacity: 0;
max-height: 16px; max-height: 16px;
width: 16px; width: 16px;
} }
.file-list li:hover > .file-actions-icon, .file-list li:focus-within > .file-actions-icon {
opacity: 1;
}
input[type="file"] { input[type="file"] {
opacity: 0; opacity: 0;
position: absolute; position: absolute;
@ -331,7 +344,6 @@ input[type="file"] {
width: 110px; width: 110px;
} }
input[type="file"]:not(:valid) + .file-feedback::after { input[type="file"]:not(:valid) + .file-feedback::after {
content: "No file selected"; content: "No file selected";
height: 1em; height: 1em;

View file

@ -29,6 +29,7 @@
<span class="edited-time">last edited {{($page).Edited.Format "2006-01-02 15:04"}}</span><br/> <span class="edited-time">last edited {{($page).Edited.Format "2006-01-02 15:04"}}</span><br/>
<label for="content">Content</label><br/> <label for="content">Content</label><br/>
<textarea class="content-input" id="content" name="content" required>{{($page).Content}}</textarea><br/> <textarea class="content-input" id="content" name="content" required>{{($page).Content}}</textarea><br/>
<a target="_blank" class="fmt-help" href="/fmt-help">Formatting help</a>
<input type="submit" value="Save"/> <input type="submit" value="Save"/>
</form> </form>

View file

@ -19,6 +19,7 @@
{{ end }} {{ end }}
<label for="content">Content</label><br/> <label for="content">Content</label><br/>
<textarea class="content-input" id="content" name="content" required></textarea><br/> <textarea class="content-input" id="content" name="content" required></textarea><br/>
<a target="_blank" class="fmt-help" href="/fmt-help">Formatting help</a>
<input type="submit" value="Save"/> <input type="submit" value="Save"/>
</form> </form>

View file

@ -1,5 +1,5 @@
{{ $slug := ((.Context).Value "params").Slug }} {{ $slug := ((.Context).Value "params").Slug }}
{{ $file := (.Context).Value "file-data" }} {{ $file := ((.Context).Value "file-manager").GetFileData $slug }}
{{ $csrfToken := (.Context).Value "csrfToken" }} {{ $csrfToken := (.Context).Value "csrfToken" }}
{{ template "header" . }} {{ template "header" . }}
@ -17,9 +17,8 @@
{{end}} {{end}}
<div class="action-panel"> <div class="action-panel">
<form class="file-move" method="POST" action="/move-select/{{($file).Path}}"> <form class="file-move" method="GET" action="/file-move/{{($file).Path}}">
<span>/{{($file).Path}}</span> <span>/{{($file).Path}}</span>
<input hidden name="csrfToken" value="{{$csrfToken}}"/>
<input type="submit" value="Move/Rename"/> <input type="submit" value="Move/Rename"/>
</form> </form>
<details class="danger-zone"><summary>Danger Zone</summary> <details class="danger-zone"><summary>Danger Zone</summary>

View file

@ -19,12 +19,12 @@
<div class="page-list"> <div class="page-list">
<ul class="file-list"> <ul class="file-list">
{{ if ($fileList).Up }} {{ if ($fileList).Up }}
<li><a href="/static-mgr{{$fileList.Up}}">..</a></li> <li><a class="left-pad-uplink" href="/file-mgr{{$fileList.Up}}">..</a></li>
{{ end }} {{ end }}
{{ range $dir := ($fileList).SubDirs }} {{ range $dir := ($fileList).SubDirs }}
<li> <li>
<a class="file-actions-icon" href="/file-actions{{($fileList).Root}}{{$dir}}"><img src="/static/actions.png" width="16px" height="16px" alt="actions"/></a> <a class="file-actions-icon" href="/file-actions{{($fileList).Root}}{{$dir}}"><img src="/static/actions.png" width="16px" height="16px" alt="actions"/></a>
<a href="/static-mgr{{($fileList).Root}}{{$dir}}">{{$dir}}/</a> <a href="/file-mgr{{($fileList).Root}}{{$dir}}">{{$dir}}/</a>
</li> </li>
{{ end }} {{ end }}
{{ range $file := ($fileList).Files }} {{ range $file := ($fileList).Files }}

55
templates/file_move.html Normal file
View file

@ -0,0 +1,55 @@
{{ $slug := ((.Context).Value "params").Slug }}
{{ $dest := .FormValue "dest" }}
{{ $fileList := ((.Context).Value "file-manager").ListSubTree $dest }}
{{ $fileData := ((.Context).Value "file-manager").GetFileData $slug }}
{{ $csrfToken := (.Context).Value "csrfToken" }}
{{ template "header" .}}
{{ if ($fileList).Error}}
<h2>File Listing Error</h2>
<span class="adapter-error">{{($fileList).Error}}</span>
{{ else if ($fileData).Error }}
<h2>File Listing Error</h2>
<span class="adapter-error">{{($fileData).Error}}</span>
{{ else }}
<h2>Moving {{($fileData).Name}}: {{($fileList).Root}}</h2>
<form class="move-rename-file" method="POST" action="/file-move-process/{{($fileData).Path}}">
<input hidden type="text" name="csrfToken" value="{{$csrfToken}}"/>
<input hidden type="text" name="dest" value="{{($fileList).Root}}"/>
<div class="new-page-button-wrapper">
<input type="submit" class="new-page-button" value="Move here"/>
</div>
<div class="page-list">
<label>New file name
<input type="text" name="filename" value="{{($fileData).Name}}"/>
</label>
<ul class="file-list">
{{ if ($fileList).Up }}
<li><a href="/file-move/{{($fileData).Path}}?dest={{($fileList).Up}}">..</a></li>
{{ end }}
{{ range $dir := ($fileList).SubDirs }}
<li>
<a href="/file-move/{{($fileData).Path}}?dest={{($fileList).Root}}{{$dir}}">{{$dir}}/</a>
</li>
{{ end }}
{{ range $file := ($fileList).Files }}
<li>
<span class="file-nolink">{{$file}}</span>
</li>
{{ end }}
</ul>
</div>
</form>
{{ end }}
{{ template "footer" .}}

View file

@ -0,0 +1,22 @@
{{ $slug := ((.Context).Value "params").Slug }}
{{ $dest := .FormValue "dest" }}
{{ $name := .FormValue "filename" }}
{{ $moveError := ((.Context).Value "file-manager").Rename $slug $dest $name }}
{{ template "header" . }}
{{ if $moveError }}
<h2>File Move/Rename Error</h2>
<span class="adapter-error">{{$moveError}}</span>
{{ else }}
<h2>File Move/Rename Success</h2>
<span class="adapter-success">File moved from {{$slug}} to {{$dest}}{{$name}} </span>
{{ end }}
{{ template "footer.html" . }}

View file

@ -1,13 +1,13 @@
{{ $slug := ((.Context).Value "params").Slug }} {{ $slug := ((.Context).Value "params").Slug }}
{{ $csrfToken := (.Context).Value "csrfToken" }} {{ $csrfToken := (.Context).Value "csrfToken" }}
{{ $fileListing := ((.Context).Value "file-manager").ListSubtree $slug }} {{ $fileData := ((.Context).Value "file-manager").GetFileData $slug }}
{{ template "header" . }} {{ template "header" . }}
{{ if ($fileListing).Error }} {{ if ($fileData).Error }}
<h2>Error</h2> <h2>Error</h2>
<span class="adapter-error">{{($fileListing).Error}}</span> <span class="adapter-error">{{($fileData).Error}}</span>
{{ else }} {{ else }}

View file

@ -0,0 +1,9 @@
{{ $fmtHelp := ((.Context).Value "adapter").FormattingHelp }}
{{ template "header" . }}
<h2>Formatting Help</h2>
<span class="adapter-success wide"><pre>{{ $fmtHelp }}</pre></span>
{{ template "footer" . }}

View file

@ -13,7 +13,7 @@
<nav> <nav>
<ul> <ul>
<li><a href="/">Pages</a></li> <li><a href="/">Pages</a></li>
<li><a href="/static-mgr/">Static Files</a></li> <li><a href="/file-mgr">Files</a></li>
<li><a href="/build">Build</a></li> <li><a href="/build">Build</a></li>
<li><a href="/config">Configuration</a></li> <li><a href="/config">Configuration</a></li>
<li><a href="/logout">Logout</a></li> <li><a href="/logout">Logout</a></li>