tweak file manager UI and route, add file-move feature, add EurekaAdapter formatting help, and update README
This commit is contained in:
parent
119d66cd27
commit
87333f9d87
17 changed files with 361 additions and 67 deletions
34
README.md
34
README.md
|
@ -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)
|
|
@ -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
|
||||||
|
|
|
@ -5,17 +5,19 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
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
|
||||||
AssetRoot string // root of Nirvash dist files (CSS, images)
|
StaticMaxUploadMB int64 // max size in MB of files uploaded via FileManager
|
||||||
Plugins map[string]interface{}
|
AssetRoot string // root of Nirvash dist files (CSS, images)
|
||||||
|
Plugins map[string]interface{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetConfigLocation() string {
|
func GetConfigLocation() string {
|
||||||
|
@ -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":
|
||||||
|
|
|
@ -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
|
||||||
|
{¶graph 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 {
|
||||||
|
|
|
@ -10,9 +10,10 @@ 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
|
||||||
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
67
nirvash.go
67
nirvash.go
|
@ -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),
|
fileManager),
|
||||||
http.MethodGet,
|
http.MethodGet,
|
||||||
udb,
|
udb,
|
||||||
"/login")))
|
"/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),
|
||||||
|
http.MethodGet,
|
||||||
|
udb,
|
||||||
|
"/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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
55
templates/file_move.html
Normal 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" .}}
|
22
templates/file_move_process.html
Normal file
22
templates/file_move_process.html
Normal 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" . }}
|
|
@ -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 }}
|
||||||
|
|
||||||
|
|
9
templates/format_help.html
Normal file
9
templates/format_help.html
Normal 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" . }}
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in a new issue