all repos — nirvash @ 6f663500cce5096b097cae74736e003c56b0c4e9

modular CMS using the quartzgun library

add basic file upload, delete
Iris Lightshard nilix@nilfm.cc
PGP Signature
-----BEGIN PGP SIGNATURE-----

iQIzBAABCAAdFiEEkFh6dA+k/6CXFXU4O3+8IhROY5gFAmKm2e4ACgkQO3+8IhRO
Y5iHPg/9Em3IN5LlpgEjTLHM8VBF1fVVCJHSVPOWUxqXWbNb00zMoKyXDWnORmDs
SLpH3hLh/huqQs6DxNJzrwR9ATFCapwl1dyukpE3z0/jgvzky2l6t3Xe3LRnyO2N
XD+xmvbQQK/erdPTOSt21umaZNg3rn79+HvAFb7L+jCLjGbuUHbh/EgUH4AJxmyH
TlkhEECt39lJe4TvxSW8XNEx9Eh1Xdd/mrTzIn1hnw6c4fUuz6n0hgMqUnhIHlZp
Nr0dqmCGk5v15mtST2ctP7euASRF8kokhQA7slrB+iKNOJHYmLjiIELTcN7tTXRx
vNlTDenOXmcbj4ObRzTdk7bc63oN9GjGbO56XhKc1N6UbT1Hzwb+jRWItbKwngh0
gkixarEDFTe3tMPUIteIFJOCWkZll7f0aRvLSMzV7IZyVR9B+vFw06aDySdyFRKG
kTCEtfCKas2tCoqC1oyAi9s5130pVJbXgXY3Vn2uT6tDART7U31KgRlQ9I9DL1/X
NsSvKr2Ssw5KOWNP0oUl8N6e+uIDnTZXAGLPXMu4lnV1oBJ5xRPA+zuqvdXyM0uR
DJQbOV8czsTLSi3tiL9lRH7IlFBOKj12Wl1S1sLEiDTTBouOV0wY2WwZNcguEh5A
71y2/+Yo+1VeKU2Pfd5yvvAeUwPRPqyVe7vYISFoJMHIHNvGiUM=
=sVOO
-----END PGP SIGNATURE-----
commit

6f663500cce5096b097cae74736e003c56b0c4e9

parent

b530a492ba83df0d63390991494bd8eb09a5e00c

M archetype/fileManager.goarchetype/fileManager.go

@@ -1,7 +1,9 @@

package archetype import ( + "errors" "io/ioutil" + "net/http" "os" "path/filepath" "strings"

@@ -33,9 +35,9 @@ Init(cfg *Config) error

// ListTree() FileListing ListSubTree(root string) FileListing GetFileData(slug string) FileData - // AddFile(path string, file multipart.FileHeader) error + AddFile(path string, req *http.Request) error // MkDir(path string) error - // Remove(path string) error + Remove(path string) error // Rename(old, new string) error }

@@ -120,3 +122,50 @@ Name: fileBase,

IsDir: fileInfo.IsDir(), } } + +func (self *SimpleFileManager) Remove(slug string) error { + fullPath := filepath.Join(self.Root, slug) + _, err := os.Stat(fullPath) + + if err != nil { + return err + } + if !strings.HasPrefix(fullPath, self.Root) { + return errors.New("You cannot escape!") + } + + return os.RemoveAll(fullPath) +} + +func (self *SimpleFileManager) AddFile(path string, req *http.Request) error { + fullPath := filepath.Join(self.Root, path) + _, err := os.Stat(fullPath) + if err != nil { + return err + } + + if !strings.HasPrefix(fullPath, filepath.Clean(self.Root)) { + return errors.New("You cannot escape!") + } + + req.ParseMultipartForm(250 << 20) + file, header, err := req.FormFile("file") + if err != nil { + return err + } + + fileData, err := ioutil.ReadAll(file) + if err != nil { + return err + } + + destPath := filepath.Join(fullPath, header.Filename) + dest, err := os.Create(destPath) + if err != nil { + return err + } + defer dest.Close() + + dest.Write(fileData) + return nil +}
M lfo/middleware.golfo/middleware.go

@@ -86,3 +86,12 @@ }

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) + } + + return http.HandlerFunc(handlerFunc) +}
M nirvash.gonirvash.go

@@ -244,14 +244,63 @@ fileManager),

http.MethodGet, udb, "/login"))) - // file upload GET contains form for file upload + + rtr.Post( + `/file-delete/(?P<Slug>.*)`, + Defend( + Protected( + WithFileManager( + renderer.Template( + pathConcat(templateRoot, "file_delete.html"), + pathConcat(templateRoot, "header.html"), + pathConcat(templateRoot, "footer.html")), + fileManager), + http.MethodGet, + udb, + "/login"), + udb, + "/")) + + rtr.Get( + `/upload/(?P<Slug>.*)`, + Fortify( + Protected( + WithFileManager( + renderer.Template( + pathConcat(templateRoot, "file_upload.html"), + pathConcat(templateRoot, "header.html"), + pathConcat(templateRoot, "footer.html")), + fileManager), + http.MethodGet, + udb, + "/login"))) + + rtr.Post( + `/upload-process/(?P<Slug>.*)`, + PrepareForUpload( + Defend( + Protected( + WithFileManager( + renderer.Template( + pathConcat(templateRoot, "file_upload_process.html"), + pathConcat(templateRoot, "header.html"), + pathConcat(templateRoot, "footer.html")), + fileManager), + http.MethodGet, + udb, + "/login"), + udb, + "/"))) + + // TODO: + // file upload GET contains form for file upload in current directory // 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 + // - 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 + http.ListenAndServe(":8080", rtr) }
D static/delete.svg

@@ -1,23 +0,0 @@

-<?xml version="1.0" encoding="UTF-8" standalone="no"?> -<!-- Created with Inkscape (http://www.inkscape.org/) --> -<svg - xmlns:svg="http://www.w3.org/2000/svg" - xmlns="http://www.w3.org/2000/svg" - version="1.0" - width="100" - height="100" - id="svg8978"> - <defs - id="defs8980" /> - <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:#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:#D80F0F;stroke-width:17.80202103;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" - id="path8988" /> - </g> -</svg>
D static/move.svg

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

-<?xml version="1.0" encoding="UTF-8"?> -<svg xmlns="http://www.w3.org/2000/svg" width="500" height="500"> -<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

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

position: relative; } -.page-list, form.editor, form.build, form.configurator, span.adapter-error, span.adapter-success, .file-move, .danger-zone { +.page-list, form.editor, form.build, form.configurator, span.adapter-error, span.adapter-success, .file-move, .danger-zone, .uploader { display: block; overflow-x: hidden; width: 80%;

@@ -202,7 +202,7 @@ 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"], .file-move 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"], .uploader label, .uploader input[type="submit"] { display: block; margin: 0; margin-top: 0.2em;

@@ -216,13 +216,21 @@ outline: none;

box-sizing: border-box; } +.uploader label { + text-transform: uppercase; + display: inline-block; + transition: background 1s, color 1s; +} + +.upload-warning { + border-bottom: 2px solid crimson; +} + form.editor input[type="text"], form.configurator input[type="text"], form.configurator input[type="number"], form.build input[type="text"] { margin: 0; width: 100%; } - - form.editor input.slug-input { font-size: 100%;

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

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

@@ -257,7 +265,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"], .file-move 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"], .uploader input[type="submit"] { margin-left: auto; margin-right: 0; font-size: 150%;

@@ -265,7 +273,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, .file-move 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, .uploader input[type="submit"]:hover, .uploader label:hover { background: lightgray; color: black; }

@@ -300,7 +308,17 @@ display: block;

border-bottom: solid 2px crimson; } +.file-list li a { + display: inline-block; + max-width: calc(100% - 32px); +} + .file-actions-icon { display: inline-block; max-height: 16px; + width: 16px; +} + +input[type="file"] { + display: none; }
M templates/cms_edit.htmltemplates/cms_edit.html

@@ -32,7 +32,7 @@ <textarea class="content-input" id="content" name="content" required>{{($page).Content}}</textarea><br/>

<input type="submit" value="Save"/> </form> -<details class="danger-zone"><summary>Danger Zone</summary> +<details class="danger-zone wide"><summary>Danger Zone</summary> <form class="eraser" method="POST" action="/delete/{{$slug}}"> <input hidden name="csrfToken" value="{{$csrfToken}}"/> <label>I want to delete this page
M templates/file_actions.htmltemplates/file_actions.html

@@ -17,15 +17,15 @@ <h2>File: {{($file).Name}}</h2>

{{end}} <div class="action-panel"> - <form class="file-move" method="POST" action="/move-select{{($file).Path}}"> + <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}}"> + <form class="file-delete" method="POST" action="/file-delete/{{($file).Path}}"> <input hidden name="csrfToken" value="{{$csrfToken}}"/> - <label>I want to delete this file + <label>I want to delete this {{if ($file).IsDir }} diretory and everything under it {{ else }} file {{ end }} <input type="checkbox" required/><br/> </label> <label>Yes, I'm sure!
A templates/file_delete.html

@@ -0,0 +1,14 @@

+{{ $slug := ((.Context).Value "params").Slug }} +{{ $deleteErr := ((.Context).Value "file-manager").Remove $slug }} + +{{ template "header" . }} + +{{ if $deleteErr }} + <h2>File Deletion Error</h2> + <span class="adapter-error">There was an error deleting the file: {{ ($deleteErr).Error }}</span> +{{ else }} + <h2>File Deleted</h2> + <span class="adapter-success">Static file '{{ $slug }}' was deleted</span> +{{ end }} + +{{ template "footer" . }}
M templates/file_list.htmltemplates/file_list.html

@@ -17,7 +17,7 @@ <a class="new-page-button" href="/mkdir{{($fileList).Root}}">New Directory</a>

</div> <div class="page-list"> - <ul> + <ul class="file-list"> {{ if ($fileList).Up }} <li><a href="/static-mgr{{$fileList.Up}}">..</a></li> {{ end }}
A templates/file_upload.html

@@ -0,0 +1,20 @@

+{{ $slug := ((.Context).Value "params").Slug }} +{{ $csrfToken := (.Context).Value "csrfToken" }} + +{{ template "header" . }} + +<h2>File Upload</h2> + +<form class="uploader" enctype="multipart/form-data" method="POST" action="/upload-process/{{$slug}}"> +<input hidden type="text" name="csrfToken" value="{{$csrfToken}}"/> + + <span>Uploading file to /{{$slug}}</span><br/> + <span class="upload-warning">(file with the same name as your upload will be overwritten!)</span><br/> + + <label>Select File<br/> + <input required type="file" name="file"/> + </label> + <input type="submit" value="Upload"/> +</form> + +{{ template "footer" . }}
A templates/file_upload_process.html

@@ -0,0 +1,14 @@

+{{ $slug := ((.Context).Value "params").Slug }} +{{ $uploadError := ((.Context).Value "file-manager").AddFile $slug . }} + +{{ template "header" . }} + +{{ if $uploadError }} + <h2>Upload Error</h2> + <span class="adapter-error">{{($uploadError).Error}}</span> +{{ else }} + <h2>Upload Successful</h2> + <span class="adapter-success">The file has been uploaded successfuly</span> +{{ end }} + +{{ template "footer" . }}