all repos — nirvash @ b530a492ba83df0d63390991494bd8eb09a5e00c

modular CMS using the quartzgun library

begin working out static file manager actions
Iris Lightshard nilix@nilfm.cc
PGP Signature
-----BEGIN PGP SIGNATURE-----

iQIzBAABCAAdFiEEkFh6dA+k/6CXFXU4O3+8IhROY5gFAmKlgQYACgkQO3+8IhRO
Y5jzbBAAjosaPQo3uCQAS+s2HncF61q9uj39vJBQTSJoZHvTrVI189bQ07gOuRv+
i7U+vjiq8CEV+pJr/sOVNVc240K9NRVGS6b0hvhUNnl0tZ2k8UvGoVQfvwH395Ov
CDqA5nYnnM6aS99MI95riTfDTs9ldmDEOb4mtTQhX262rpRyBC4EcLMSnmmO/Uo4
mlVpooh7mBrw4PkSoLsuc7moCQc6cQWGYTd1jrbXUoj9Hiu4f1Onu0b7CWluVNdS
jNOi/LeloqkDvKFBYEr6kcD7nxYfaMgoYrBCwdRjyT8WFR8ckFx7TwjGdmdLXDTH
yA/bZCMqtui9g14kvpGyYeIiaCQpf/knUhVniWmolamy76j3vNzIZX0cHFqpdZfw
K//UWtUXxN/uAKwdhEkivLeRSxEq3+Je3gCcnM+eNXL/YQ6pJMLtFwYmz+0Vyl0H
V06C8MMTMGjq2kPZpU3ZZPVspMCf7TIhvyBfg38Qdw3zcwyYhcmopmzoYe+qIRpm
IHVsXM3/SMNNsRXI+rr31y2EelAZHtFlqqogv9bHi3FVTOrgFskNz/wobOXJ5uZg
0BRIklT4/8EDzQNskWNzodWOgiz/VqaYwfq+3RLYq5xTFX0hggd54+R69lYwAtK/
Dh4Q9HO6sxyKdkWE/XoKFvLO4c8r/nkhzONAygN0uTom2JpIQ+w=
=qRy8
-----END PGP SIGNATURE-----
commit

b530a492ba83df0d63390991494bd8eb09a5e00c

parent

29583ab7d916e6589bbeefa6bb102de5cb11e58a

M archetype/fileManager.goarchetype/fileManager.go

@@ -2,6 +2,7 @@ package archetype

import ( "io/ioutil" + "os" "path/filepath" "strings" )

@@ -12,6 +13,13 @@ ShowHtml bool

ShowHidden bool } +type FileData struct { + Error string + Path string + Name string + IsDir bool +} + type FileListing struct { Error string Root string

@@ -24,6 +32,7 @@ type FileManager interface {

Init(cfg *Config) error // ListTree() FileListing ListSubTree(root string) FileListing + GetFileData(slug string) FileData // AddFile(path string, file multipart.FileHeader) error // MkDir(path string) error // Remove(path string) error

@@ -31,7 +40,7 @@ // Rename(old, new string) error

} func (self *SimpleFileManager) Init(cfg *Config) error { - self.Root = cfg.StaticRoot + self.Root = filepath.Clean(cfg.StaticRoot) self.ShowHtml = cfg.StaticShowHtml self.ShowHidden = cfg.StaticShowHidden return nil

@@ -86,3 +95,28 @@ }

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(), + } +}
M lfo/middleware.golfo/middleware.go

@@ -74,3 +74,15 @@ }

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) +}
M nirvash.gonirvash.go

@@ -1,6 +1,7 @@

package main import ( + "html/template" "net/http" core "nilfm.cc/git/nirvash/archetype" . "nilfm.cc/git/nirvash/lfo"

@@ -31,15 +32,18 @@ fileManager := &core.SimpleFileManager{}

fileManager.Init(cfg) pathConcat := filepath.Join + templateRoot := pathConcat(cfg.AssetRoot, "templates") rtr := &router.Router{ StaticPaths: map[string]string{ "/static/": filepath.Join(cfg.AssetRoot, "static"), "/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( pathConcat(templateRoot, "login.html")))

@@ -206,6 +210,12 @@ udb,

"/")) rtr.Get( + `/static-mgr`, + http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + http.Redirect(w, req, "/static-mgr/", http.StatusSeeOther) + })) + + rtr.Get( `/static-mgr/(?P<Slug>.*)`, Fortify( Protected(

@@ -219,5 +229,29 @@ http.MethodGet,

udb, "/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) }
M static/delete.svgstatic/delete.svg

@@ -13,11 +13,11 @@ <g

id="layer1"> <path 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" /> <path 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" /> </g> </svg>
M static/move.svgstatic/move.svg

@@ -1,6 +1,6 @@

<?xml version="1.0" encoding="UTF-8"?> <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" d="m249,30a220,220 0 1,0 2,0zm-10,75 140,145-140,145M110,250H350"/> </svg>
M static/style.cssstatic/style.css

@@ -160,6 +160,8 @@ color: lightgray;

padding: 0.2em; text-transform: uppercase; transition: background 1s, color 1s; + display: inline-block; + margin-bottom: 0.2em; } a.new-page-button:hover {

@@ -178,7 +180,7 @@ padding-left: 8px;

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; overflow-x: hidden; width: 80%;

@@ -194,13 +196,13 @@ span.adapter-error {

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%; color: lightgray; 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; margin: 0; margin-top: 0.2em;

@@ -234,7 +236,7 @@ form input:focus, form textarea:focus {

border: 2px solid cyan; } -form.editor, .danger-zone { +form.editor, form.editor.danger-zone { max-width: 80em; }

@@ -255,7 +257,7 @@ form.configurator input, form.configurator textarea {

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-right: 0; font-size: 150%;

@@ -263,7 +265,7 @@ text-transform: uppercase;

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; color: black; }

@@ -296,4 +298,9 @@

.edit-error { display: block; border-bottom: solid 2px crimson; +} + +.file-actions-icon { + display: inline-block; + max-height: 16px; }
M templates/error.htmltemplates/error.html

@@ -1,17 +1,8 @@

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

@@ -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" . }}
M templates/file_list.htmltemplates/file_list.html

@@ -12,7 +12,8 @@ {{ else }}

<h2>Files: {{($fileList).Root}}</h2> <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 class="page-list">

@@ -21,10 +22,16 @@ {{ if ($fileList).Up }}

<li><a href="/static-mgr{{$fileList.Up}}">..</a></li> {{ end }} {{ 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 }} {{ 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 }} </ul> </div>