begin working out static file manager actions
This commit is contained in:
parent
29583ab7d9
commit
b530a492ba
10 changed files with 155 additions and 29 deletions
|
@ -2,6 +2,7 @@ package archetype
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
@ -12,6 +13,13 @@ type SimpleFileManager struct {
|
||||||
ShowHidden bool
|
ShowHidden bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type FileData struct {
|
||||||
|
Error string
|
||||||
|
Path string
|
||||||
|
Name string
|
||||||
|
IsDir bool
|
||||||
|
}
|
||||||
|
|
||||||
type FileListing struct {
|
type FileListing struct {
|
||||||
Error string
|
Error string
|
||||||
Root string
|
Root string
|
||||||
|
@ -24,6 +32,7 @@ type FileManager interface {
|
||||||
Init(cfg *Config) error
|
Init(cfg *Config) error
|
||||||
// ListTree() FileListing
|
// ListTree() FileListing
|
||||||
ListSubTree(root string) FileListing
|
ListSubTree(root string) FileListing
|
||||||
|
GetFileData(slug string) FileData
|
||||||
// AddFile(path string, file multipart.FileHeader) error
|
// AddFile(path string, file multipart.FileHeader) error
|
||||||
// MkDir(path string) error
|
// MkDir(path string) error
|
||||||
// Remove(path string) error
|
// Remove(path string) error
|
||||||
|
@ -31,7 +40,7 @@ type FileManager interface {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *SimpleFileManager) Init(cfg *Config) error {
|
func (self *SimpleFileManager) Init(cfg *Config) error {
|
||||||
self.Root = cfg.StaticRoot
|
self.Root = filepath.Clean(cfg.StaticRoot)
|
||||||
self.ShowHtml = cfg.StaticShowHtml
|
self.ShowHtml = cfg.StaticShowHtml
|
||||||
self.ShowHidden = cfg.StaticShowHidden
|
self.ShowHidden = cfg.StaticShowHidden
|
||||||
return nil
|
return nil
|
||||||
|
@ -86,3 +95,28 @@ func (self *SimpleFileManager) ListSubTree(root string) FileListing {
|
||||||
|
|
||||||
return list
|
return list
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (self *SimpleFileManager) GetFileData(slug string) FileData {
|
||||||
|
fullPath := filepath.Join(self.Root, slug)
|
||||||
|
fileInfo, err := os.Stat(fullPath)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return FileData{
|
||||||
|
Error: err.Error(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(fullPath, self.Root) {
|
||||||
|
return FileData{
|
||||||
|
Error: "You cannot escape!",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanedSlug := filepath.Clean(slug)
|
||||||
|
fileBase := filepath.Base(cleanedSlug)
|
||||||
|
|
||||||
|
return FileData{
|
||||||
|
Path: filepath.Clean(slug),
|
||||||
|
Name: fileBase,
|
||||||
|
IsDir: fileInfo.IsDir(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -74,3 +74,15 @@ 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 {
|
||||||
|
handlerFunc := func(w http.ResponseWriter, req *http.Request) {
|
||||||
|
ctx := req.Context()
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
38
nirvash.go
38
nirvash.go
|
@ -1,6 +1,7 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"html/template"
|
||||||
"net/http"
|
"net/http"
|
||||||
core "nilfm.cc/git/nirvash/archetype"
|
core "nilfm.cc/git/nirvash/archetype"
|
||||||
. "nilfm.cc/git/nirvash/lfo"
|
. "nilfm.cc/git/nirvash/lfo"
|
||||||
|
@ -31,16 +32,19 @@ func main() {
|
||||||
fileManager.Init(cfg)
|
fileManager.Init(cfg)
|
||||||
|
|
||||||
pathConcat := filepath.Join
|
pathConcat := filepath.Join
|
||||||
|
templateRoot := pathConcat(cfg.AssetRoot, "templates")
|
||||||
|
|
||||||
rtr := &router.Router{
|
rtr := &router.Router{
|
||||||
StaticPaths: map[string]string{
|
StaticPaths: map[string]string{
|
||||||
"/static/": filepath.Join(cfg.AssetRoot, "static"),
|
"/static/": filepath.Join(cfg.AssetRoot, "static"),
|
||||||
"/files/": cfg.StaticRoot,
|
"/files/": cfg.StaticRoot,
|
||||||
},
|
},
|
||||||
|
Fallback: *template.Must(template.ParseFiles(
|
||||||
|
pathConcat(templateRoot, "error.html"),
|
||||||
|
pathConcat(templateRoot, "header.html"),
|
||||||
|
pathConcat(templateRoot, "footer.html"))),
|
||||||
}
|
}
|
||||||
|
|
||||||
templateRoot := pathConcat(cfg.AssetRoot, "templates")
|
|
||||||
|
|
||||||
rtr.Get("/login", renderer.Template(
|
rtr.Get("/login", renderer.Template(
|
||||||
pathConcat(templateRoot, "login.html")))
|
pathConcat(templateRoot, "login.html")))
|
||||||
|
|
||||||
|
@ -205,6 +209,12 @@ func main() {
|
||||||
udb,
|
udb,
|
||||||
"/"))
|
"/"))
|
||||||
|
|
||||||
|
rtr.Get(
|
||||||
|
`/static-mgr`,
|
||||||
|
http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||||
|
http.Redirect(w, req, "/static-mgr/", http.StatusSeeOther)
|
||||||
|
}))
|
||||||
|
|
||||||
rtr.Get(
|
rtr.Get(
|
||||||
`/static-mgr/(?P<Slug>.*)`,
|
`/static-mgr/(?P<Slug>.*)`,
|
||||||
Fortify(
|
Fortify(
|
||||||
|
@ -219,5 +229,29 @@ func main() {
|
||||||
udb,
|
udb,
|
||||||
"/login")))
|
"/login")))
|
||||||
|
|
||||||
|
rtr.Get(
|
||||||
|
`/file-actions/(?P<Slug>.*)`,
|
||||||
|
Fortify(
|
||||||
|
Protected(
|
||||||
|
WithFileManager(
|
||||||
|
WithFileData(
|
||||||
|
renderer.Template(
|
||||||
|
pathConcat(templateRoot, "file_actions.html"),
|
||||||
|
pathConcat(templateRoot, "header.html"),
|
||||||
|
pathConcat(templateRoot, "footer.html")),
|
||||||
|
fileManager),
|
||||||
|
fileManager),
|
||||||
|
http.MethodGet,
|
||||||
|
udb,
|
||||||
|
"/login")))
|
||||||
|
// file upload GET contains form for file upload
|
||||||
|
// file upload POST performs the action of creating/overwriting
|
||||||
|
// add directory GET contains the form for directory creation
|
||||||
|
// add directory POST performs the action of creating directory
|
||||||
|
// delete GET contains the form for confirming deletion
|
||||||
|
// delete POST performs the action of deleting
|
||||||
|
// move GET (not required?)
|
||||||
|
// move-choose POST uses a form to navigate through the file tree
|
||||||
|
// move-do POST moves the file when finalized in move-choose
|
||||||
http.ListenAndServe(":8080", rtr)
|
http.ListenAndServe(":8080", rtr)
|
||||||
}
|
}
|
||||||
|
|
BIN
static/actions.png
Normal file
BIN
static/actions.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 612 B |
|
@ -13,11 +13,11 @@
|
||||||
id="layer1">
|
id="layer1">
|
||||||
<path
|
<path
|
||||||
d="M 6.3895625,6.4195626 C 93.580437,93.610437 93.580437,93.610437 93.580437,93.610437"
|
d="M 6.3895625,6.4195626 C 93.580437,93.610437 93.580437,93.610437 93.580437,93.610437"
|
||||||
style="fill:none;fill-rule:evenodd;stroke:#ff0000;stroke-width:18.05195999;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
style="fill:none;fill-rule:evenodd;stroke:#D80F0F;stroke-width:18.05195999;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
id="path8986" />
|
id="path8986" />
|
||||||
<path
|
<path
|
||||||
d="M 6.3894001,93.6106 C 93.830213,6.4194003 93.830213,6.4194003 93.830213,6.4194003"
|
d="M 6.3894001,93.6106 C 93.830213,6.4194003 93.830213,6.4194003 93.830213,6.4194003"
|
||||||
style="fill:none;fill-rule:evenodd;stroke:#ff0000;stroke-width:17.80202103;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
style="fill:none;fill-rule:evenodd;stroke:#D80F0F;stroke-width:17.80202103;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
id="path8988" />
|
id="path8988" />
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
Before Width: | Height: | Size: 954 B After Width: | Height: | Size: 954 B |
|
@ -1,6 +1,6 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="500" height="500">
|
<svg xmlns="http://www.w3.org/2000/svg" width="500" height="500">
|
||||||
<path stroke="#000" stroke-width="55" fill="none"
|
<path stroke="#177355" stroke-width="55" fill="none"
|
||||||
stroke-linecap="round" stroke-linejoin="round"
|
stroke-linecap="round" stroke-linejoin="round"
|
||||||
d="m249,30a220,220 0 1,0 2,0zm-10,75 140,145-140,145M110,250H350"/>
|
d="m249,30a220,220 0 1,0 2,0zm-10,75 140,145-140,145M110,250H350"/>
|
||||||
</svg>
|
</svg>
|
Before Width: | Height: | Size: 276 B After Width: | Height: | Size: 279 B |
|
@ -160,6 +160,8 @@ a.new-page-button {
|
||||||
padding: 0.2em;
|
padding: 0.2em;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
transition: background 1s, color 1s;
|
transition: background 1s, color 1s;
|
||||||
|
display: inline-block;
|
||||||
|
margin-bottom: 0.2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
a.new-page-button:hover {
|
a.new-page-button:hover {
|
||||||
|
@ -178,7 +180,7 @@ h2 {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-list, form.editor, form.build, form.configurator, span.adapter-error, span.adapter-success, .danger-zone {
|
.page-list, form.editor, form.build, form.configurator, span.adapter-error, span.adapter-success, .file-move, .danger-zone {
|
||||||
display: block;
|
display: block;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
width: 80%;
|
width: 80%;
|
||||||
|
@ -194,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.editor label, form.build label, .danger-zone label, form.configurator label, form.file-move 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"] {
|
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"] {
|
||||||
display: block;
|
display: block;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
margin-top: 0.2em;
|
margin-top: 0.2em;
|
||||||
|
@ -234,7 +236,7 @@ form input:focus, form textarea:focus {
|
||||||
border: 2px solid cyan;
|
border: 2px solid cyan;
|
||||||
}
|
}
|
||||||
|
|
||||||
form.editor, .danger-zone {
|
form.editor, form.editor.danger-zone {
|
||||||
max-width: 80em;
|
max-width: 80em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -255,7 +257,7 @@ form.configurator input, form.configurator textarea {
|
||||||
font-size: 125%;
|
font-size: 125%;
|
||||||
}
|
}
|
||||||
|
|
||||||
form.editor input[type="submit"], form.build input[type="submit"], .danger-zone input[type="submit"], form.configurator input[type="submit"] {
|
form.editor input[type="submit"], form.build input[type="submit"], .danger-zone input[type="submit"], form.configurator input[type="submit"], .file-move input[type="submit"] {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
font-size: 150%;
|
font-size: 150%;
|
||||||
|
@ -263,7 +265,7 @@ form.editor input[type="submit"], form.build input[type="submit"], .danger-zone
|
||||||
transition: background 1s, color 1s;
|
transition: background 1s, color 1s;
|
||||||
}
|
}
|
||||||
|
|
||||||
form.editor input[type="submit"]:hover,form.build input[type="submit"]:hover, .danger-zone input[type="submit"]:hover, form.configurator input[type="submit"]:hover {
|
form.editor input[type="submit"]:hover,form.build input[type="submit"]:hover, .danger-zone input[type="submit"]:hover, form.configurator input[type="submit"]:hover, .file-move input[type="submit"]:hover {
|
||||||
background: lightgray;
|
background: lightgray;
|
||||||
color: black;
|
color: black;
|
||||||
}
|
}
|
||||||
|
@ -297,3 +299,8 @@ form input[readonly] {
|
||||||
display: block;
|
display: block;
|
||||||
border-bottom: solid 2px crimson;
|
border-bottom: solid 2px crimson;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.file-actions-icon {
|
||||||
|
display: inline-block;
|
||||||
|
max-height: 16px;
|
||||||
|
}
|
|
@ -1,17 +1,8 @@
|
||||||
{{ $params := (.Context).Value "params" }}
|
{{ $params := (.Context).Value "params" }}
|
||||||
|
|
||||||
<!DOCTYPE html>
|
{{ template "header" . }}
|
||||||
<html lang='en'>
|
|
||||||
<head>
|
<h2>Error</h2>
|
||||||
<meta charset='utf-8'>
|
<span class="adapter-error">{{$params.ErrorCode}}: {{$params.ErrorMessage}}</span>
|
||||||
|
|
||||||
<meta name='viewport' content='width=device-width,initial-scale=1'>
|
|
||||||
<link rel='shortcut icon' href='/favicon.ico'>
|
|
||||||
<title>test — error</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<header><h1>{{ $params.ErrorCode }}</h1></header>
|
|
||||||
<main>
|
|
||||||
{{ $params.ErrorMessage }}
|
|
||||||
</main>
|
|
||||||
{{ template "footer" . }}
|
{{ template "footer" . }}
|
||||||
|
|
41
templates/file_actions.html
Normal file
41
templates/file_actions.html
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
{{ $slug := ((.Context).Value "params").Slug }}
|
||||||
|
{{ $file := (.Context).Value "file-data" }}
|
||||||
|
{{ $csrfToken := (.Context).Value "csrfToken" }}
|
||||||
|
|
||||||
|
{{ template "header" . }}
|
||||||
|
|
||||||
|
{{ if ($file).Error }}
|
||||||
|
<h2>File Error</h2>
|
||||||
|
|
||||||
|
<span class="adapter-error">{{($file).Error}}</span>
|
||||||
|
|
||||||
|
{{ else }}
|
||||||
|
{{ if ($file).IsDir }}
|
||||||
|
<h2>Directory: {{($file).Name}}</h2>
|
||||||
|
{{ else }}
|
||||||
|
<h2>File: {{($file).Name}}</h2>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
<div class="action-panel">
|
||||||
|
<form class="file-move" method="POST" action="/move-select{{($file).Path}}">
|
||||||
|
<span>/{{($file).Path}}</span>
|
||||||
|
<input hidden name="csrfToken" value="{{$csrfToken}}"/>
|
||||||
|
<input type="submit" value="Move/Rename"/>
|
||||||
|
</form>
|
||||||
|
<details class="danger-zone"><summary>Danger Zone</summary>
|
||||||
|
<form class="file-delete" method="POST" action="/file-delete{{($file).Path}}">
|
||||||
|
<input hidden name="csrfToken" value="{{$csrfToken}}"/>
|
||||||
|
<label>I want to delete this file
|
||||||
|
<input type="checkbox" required/><br/>
|
||||||
|
</label>
|
||||||
|
<label>Yes, I'm sure!
|
||||||
|
<input type="checkbox" required/><br/>
|
||||||
|
</label>
|
||||||
|
<input type="submit" value="Delete"/>
|
||||||
|
</form>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{ template "footer" . }}
|
|
@ -12,7 +12,8 @@
|
||||||
<h2>Files: {{($fileList).Root}}</h2>
|
<h2>Files: {{($fileList).Root}}</h2>
|
||||||
|
|
||||||
<div class="new-page-button-wrapper">
|
<div class="new-page-button-wrapper">
|
||||||
<a class="new-page-button" href="/upload{{($fileList).Root}}">Upload File</a>
|
<a class="new-page-button" href="/upload{{($fileList).Root}}">Upload File</a><br/>
|
||||||
|
<a class="new-page-button" href="/mkdir{{($fileList).Root}}">New Directory</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="page-list">
|
<div class="page-list">
|
||||||
|
@ -21,10 +22,16 @@
|
||||||
<li><a href="/static-mgr{{$fileList.Up}}">..</a></li>
|
<li><a href="/static-mgr{{$fileList.Up}}">..</a></li>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
{{ range $dir := ($fileList).SubDirs }}
|
{{ range $dir := ($fileList).SubDirs }}
|
||||||
<li><a href="/static-mgr{{($fileList).Root}}{{$dir}}">{{$dir}}/</a></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 href="/static-mgr{{($fileList).Root}}{{$dir}}">{{$dir}}/</a>
|
||||||
|
</li>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
{{ range $file := ($fileList).Files }}
|
{{ range $file := ($fileList).Files }}
|
||||||
<li><a href="/files{{($fileList).Root}}{{$file}}">{{$file}}</a></li>
|
<li>
|
||||||
|
<a class="file-actions-icon" href="/file-actions{{($fileList).Root}}{{$file}}"><img src="/static/actions.png" width="16px" height="16px" alt="actions"/></a>
|
||||||
|
<a href="/files{{($fileList).Root}}{{$file}}">{{$file}}</a>
|
||||||
|
</li>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Reference in a new issue