begin working out static file manager actions

This commit is contained in:
Iris Lightshard 2022-06-12 00:00:38 -06:00
parent 29583ab7d9
commit b530a492ba
Signed by: Iris Lightshard
GPG key ID: 3B7FBC22144E6398
10 changed files with 155 additions and 29 deletions

View file

@ -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(),
}
}

View file

@ -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)
}

View file

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 612 B

View file

@ -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

View file

@ -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

View file

@ -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;
}

View file

@ -1,17 +1,8 @@
{{ $params := (.Context).Value "params" }} {{ $params := (.Context).Value "params" }}
<!DOCTYPE html> {{ template "header" . }}
<html lang='en'>
<head>
<meta charset='utf-8'>
<meta name='viewport' content='width=device-width,initial-scale=1'> <h2>Error</h2>
<link rel='shortcut icon' href='/favicon.ico'> <span class="adapter-error">{{$params.ErrorCode}}: {{$params.ErrorMessage}}</span>
<title>test &mdash; error</title>
</head> {{ template "footer" . }}
<body>
<header><h1>{{ $params.ErrorCode }}</h1></header>
<main>
{{ $params.ErrorMessage }}
</main>
{{ template "footer" .}}

View 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" . }}

View file

@ -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>