add basic file upload, delete

This commit is contained in:
Iris Lightshard 2022-06-13 00:32:14 -06:00
parent b530a492ba
commit 6f663500cc
Signed by: Iris Lightshard
GPG key ID: 3B7FBC22144E6398
12 changed files with 191 additions and 47 deletions

View file

@ -1,7 +1,9 @@
package archetype
import (
"errors"
"io/ioutil"
"net/http"
"os"
"path/filepath"
"strings"
@ -33,9 +35,9 @@ type FileManager interface {
// 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 @@ func (self *SimpleFileManager) GetFileData(slug string) FileData {
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
}

View file

@ -86,3 +86,12 @@ func WithFileData(next http.Handler, fileManager core.FileManager) http.Handler
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)
}

View file

@ -244,14 +244,63 @@ func main() {
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)
}

View file

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

Before

Width:  |  Height:  |  Size: 954 B

View file

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

Before

Width:  |  Height:  |  Size: 279 B

View file

@ -180,7 +180,7 @@ h2 {
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 @@ form.editor label, form.build label, .danger-zone label, form.configurator label
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,14 +216,22 @@ form.editor input, form.build input, form.editor textarea, form.configurator inp
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 @@ form.editor input[type="submit"], form.build input[type="submit"], .danger-zone
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 @@ form input[readonly] {
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;
}

View file

@ -32,7 +32,7 @@
<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

View file

@ -17,15 +17,15 @@
{{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!

View file

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

View file

@ -17,7 +17,7 @@
</div>
<div class="page-list">
<ul>
<ul class="file-list">
{{ if ($fileList).Up }}
<li><a href="/static-mgr{{$fileList.Up}}">..</a></li>
{{ end }}

View file

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

View file

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