Compare commits

..
Author SHA1 Message Date
Pierre Dubouilh eaf1342559 more conservative text editor 2024-09-28 18:02:16 +02:00
6 changed files with 100 additions and 182 deletions
+39 -55
View File
@@ -3,17 +3,11 @@ package main
import ( import (
"archive/zip" "archive/zip"
"compress/gzip" "compress/gzip"
"crypto/md5"
"crypto/sha1"
"crypto/sha256"
"crypto/sha512"
_ "embed" _ "embed"
"encoding/hex"
"encoding/json" "encoding/json"
"errors" "errors"
"flag" "flag"
"fmt" "fmt"
"hash"
"html" "html"
"html/template" "html/template"
"io" "io"
@@ -51,11 +45,6 @@ var verb = flag.Bool("verb", false, "verbosity")
var skipHidden = flag.Bool("k", true, "\nskip hidden files") var skipHidden = flag.Bool("k", true, "\nskip hidden files")
var ro = flag.Bool("ro", false, "read only mode (no upload, rename, move, etc...)") var ro = flag.Bool("ro", false, "read only mode (no upload, rename, move, etc...)")
type rpcCall struct {
Call string `json:"call"`
Args []string `json:"args"`
}
var rootPath = "" var rootPath = ""
var handler http.Handler var handler http.Handler
@@ -106,9 +95,8 @@ func replyList(w http.ResponseWriter, r *http.Request, fullPath string, path str
p.Title = template.HTML(html.EscapeString(title)) p.Title = template.HTML(html.EscapeString(title))
for _, el := range files { for _, el := range files {
info, errInfo := el.Info() info, err := el.Info()
el, err := os.Stat(fullPath + "/" + el.Name()) if err != nil {
if err != nil || errInfo != nil {
log.Println("error - cant stat a file", err) log.Println("error - cant stat a file", err)
continue continue
} }
@@ -116,7 +104,7 @@ func replyList(w http.ResponseWriter, r *http.Request, fullPath string, path str
if *skipHidden && strings.HasPrefix(el.Name(), ".") { if *skipHidden && strings.HasPrefix(el.Name(), ".") {
continue // dont print hidden files if we're not allowed continue // dont print hidden files if we're not allowed
} }
if !*symlinks && info.Mode()&os.ModeSymlink != 0 { if *symlinks && info.Mode()&os.ModeSymlink != 0 {
continue // dont follow symlinks if we're not allowed continue // dont follow symlinks if we're not allowed
} }
@@ -133,7 +121,7 @@ func replyList(w http.ResponseWriter, r *http.Request, fullPath string, path str
} else { } else {
sl := strings.Split(name, ".") sl := strings.Split(name, ".")
ext := strings.ToLower(sl[len(sl)-1]) ext := strings.ToLower(sl[len(sl)-1])
row := rowTemplate{name, template.URL(href), humanize(el.Size()), ext} row := rowTemplate{name, template.URL(href), humanize(info.Size()), ext}
p.RowsFiles = append(p.RowsFiles, row) p.RowsFiles = append(p.RowsFiles, row)
} }
} }
@@ -141,7 +129,7 @@ func replyList(w http.ResponseWriter, r *http.Request, fullPath string, path str
if strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") { if strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
w.Header().Set("Content-Type", "text/html") w.Header().Set("Content-Type", "text/html")
w.Header().Add("Content-Encoding", "gzip") w.Header().Add("Content-Encoding", "gzip")
gz, err := gzip.NewWriterLevel(w, gzip.BestSpeed) // BestSpeed is Much Faster than default - base on a very unscientific local test, and only ~30% larger (compression remains still very effective, ~6x) gz, err := gzip.NewWriterLevel(w, gzip.BestSpeed) // BestSpeed is Much Faster than default - base on a very unscientific local test
check(err) check(err)
defer gz.Close() defer gz.Close()
tmpl.Execute(gz, p) tmpl.Execute(gz, p)
@@ -158,7 +146,8 @@ func doContent(w http.ResponseWriter, r *http.Request) {
path := html.UnescapeString(r.URL.Path) path := html.UnescapeString(r.URL.Path)
defer exitPath(w, "get content", path) defer exitPath(w, "get content", path)
fullPath := enforcePath(path) fullPath, err := enforcePath(path)
check(err)
stat, errStat := os.Stat(fullPath) stat, errStat := os.Stat(fullPath)
check(errStat) check(errStat)
@@ -181,7 +170,9 @@ func upload(w http.ResponseWriter, r *http.Request) {
if err != nil && err != io.EOF { // errs EOF when no more parts to process if err != nil && err != io.EOF { // errs EOF when no more parts to process
check(err) check(err)
} }
dst, err := os.Create(enforcePath(path)) path, err = enforcePath(path)
check(err)
dst, err := os.Create(path)
check(err) check(err)
io.Copy(dst, part) io.Copy(dst, part)
w.Write([]byte("ok")) w.Write([]byte("ok"))
@@ -191,8 +182,9 @@ func zipRPC(w http.ResponseWriter, r *http.Request) {
zipPath := r.URL.Query().Get("zipPath") zipPath := r.URL.Query().Get("zipPath")
zipName := r.URL.Query().Get("zipName") zipName := r.URL.Query().Get("zipName")
defer exitPath(w, "zip", zipPath) defer exitPath(w, "zip", zipPath)
zipFullPath := enforcePath(zipPath) zipFullPath, err := enforcePath(zipPath)
_, err := os.Lstat(zipFullPath) check(err)
_, err = os.Lstat(zipFullPath)
check(err) check(err)
w.Header().Add("Content-Disposition", "attachment; filename=\""+zipName+".zip\"") w.Header().Add("Content-Disposition", "attachment; filename=\""+zipName+".zip\"")
zipWriter := zip.NewWriter(w) zipWriter := zip.NewWriter(w)
@@ -210,7 +202,7 @@ func zipRPC(w http.ResponseWriter, r *http.Request) {
return nil // hidden files not allowed return nil // hidden files not allowed
} }
if f.Mode()&os.ModeSymlink != 0 { if f.Mode()&os.ModeSymlink != 0 {
panic(errors.New("symlink not allowed in zip downloads")) // filepath.Walk doesnt support symlinks check(errors.New("symlink not allowed in zip downloads")) // filepath.Walk doesnt support symlinks
} }
header, err := zip.FileInfoHeader(f) header, err := zip.FileInfoHeader(f)
@@ -231,60 +223,52 @@ func zipRPC(w http.ResponseWriter, r *http.Request) {
} }
func rpc(w http.ResponseWriter, r *http.Request) { func rpc(w http.ResponseWriter, r *http.Request) {
var err error type rpcCall struct {
Call string `json:"call"`
Args []string `json:"args"`
}
var rpc rpcCall var rpc rpcCall
defer exitPath(w, "rpc", &rpc) defer exitPath(w, "rpc", rpc)
bodyBytes, err := io.ReadAll(r.Body) bodyBytes, err := io.ReadAll(r.Body)
check(err) check(err)
json.Unmarshal(bodyBytes, &rpc) json.Unmarshal(bodyBytes, &rpc)
ret := []byte("ok")
switch rpc.Call { path0, err := enforcePath(rpc.Args[0])
case "mkdirp": path1 := ""
err = os.MkdirAll(enforcePath(rpc.Args[0]), os.ModePerm) check(err)
case "mv": if len(rpc.Args) > 1 {
err = os.Rename(enforcePath(rpc.Args[0]), enforcePath(rpc.Args[1])) path1, err = enforcePath(rpc.Args[1])
case "rm":
err = os.RemoveAll(enforcePath(rpc.Args[0]))
case "sum":
file, err := os.Open(enforcePath(rpc.Args[0]))
check(err) check(err)
var hash hash.Hash }
switch rpc.Args[1] {
case "md5": if rpc.Call == "mkdirp" {
hash = md5.New() err = os.MkdirAll(path0, os.ModePerm)
case "sha1": } else if rpc.Call == "mv" && len(rpc.Args) == 2 {
hash = sha1.New() err = os.Rename(path0, path1)
case "sha256": } else if rpc.Call == "rm" {
hash = sha256.New() err = os.RemoveAll(path0)
case "sha512": } else {
hash = sha512.New() err = errors.New("invalid rpc call")
}
_, err = io.Copy(hash, file)
check(err)
checksum := hash.Sum(nil)
ret = make([]byte, hex.EncodedLen(len(checksum)))
hex.Encode(ret, checksum)
} }
check(err) check(err)
w.Write(ret) w.Write([]byte("ok"))
} }
func enforcePath(p string) string { func enforcePath(p string) (string, error) {
joined := filepath.Join(rootPath, strings.TrimPrefix(p, *extraPath)) joined := filepath.Join(rootPath, strings.TrimPrefix(p, *extraPath))
fp, err := filepath.Abs(joined) fp, err := filepath.Abs(joined)
sl, _ := filepath.EvalSymlinks(fp) // err skipped as it would error for unexistent files (RPC check). The actual behaviour is tested below sl, _ := filepath.EvalSymlinks(fp) // err skipped as it would error for inexistent files (RPC check). The actual behaviour is tested below
// panic if we had a error getting absolute path, // panic if we had a error getting absolute path,
// ... or if path doesnt contain the prefix path we expect, // ... or if path doesnt contain the prefix path we expect,
// ... or if we're skipping hidden folders, and one is requested, // ... or if we're skipping hidden folders, and one is requested,
// ... or if we're skipping symlinks, path exists, and a symlink out of bound requested // ... or if we're skipping symlinks, path exists, and a symlink out of bound requested
if err != nil || !strings.HasPrefix(fp, rootPath) || *skipHidden && strings.Contains(p, "/.") || !*symlinks && len(sl) > 0 && !strings.HasPrefix(sl, rootPath) { if err != nil || !strings.HasPrefix(fp, rootPath) || *skipHidden && strings.Contains(p, "/.") || !*symlinks && len(sl) > 0 && !strings.HasPrefix(sl, rootPath) {
panic(errors.New("invalid path")) return "", errors.New("invalid path")
} }
return fp return fp, nil
} }
func main() { func main() {
-7
View File
@@ -238,8 +238,6 @@ func doTestRegular(t *testing.T, url string, testExtra bool) {
hasListing := strings.Contains(body0, `readme.md`) hasListing := strings.Contains(body0, `readme.md`)
body1 = get(t, url+"/support/readme.md") body1 = get(t, url+"/support/readme.md")
hasReadme := strings.Contains(body1, `the master branch is automatically built and pushed`) hasReadme := strings.Contains(body1, `the master branch is automatically built and pushed`)
body2 = get(t, url)
hasMainListing := strings.Contains(body2, `href="support">support/</a>`)
if !testExtra && hasReadme { if !testExtra && hasReadme {
t.Fatal("error symlink file reached where illegal") t.Fatal("error symlink file reached where illegal")
@@ -251,11 +249,6 @@ func doTestRegular(t *testing.T, url string, testExtra bool) {
} else if testExtra && !hasListing { } else if testExtra && !hasListing {
t.Fatal("error symlink folder unreachable") t.Fatal("error symlink folder unreachable")
} }
if !testExtra && hasMainListing {
t.Fatal("error symlink folder where illegal")
} else if testExtra && !hasMainListing {
t.Fatal("error symlink folder unreachable")
}
if testExtra { if testExtra {
fmt.Println("\r\n~~~~~~~~~~ test symlink mkdir & cleanup") fmt.Println("\r\n~~~~~~~~~~ test symlink mkdir & cleanup")
+1
View File
@@ -1 +1,2 @@
B!!! B!!!
test
+48 -101
View File
@@ -153,14 +153,16 @@ function rpc (call, args, cb) {
xhr.open('POST', location.origin + window.extraPath + '/rpc') xhr.open('POST', location.origin + window.extraPath + '/rpc')
xhr.setRequestHeader('Content-Type', 'application/json;charset=UTF-8') xhr.setRequestHeader('Content-Type', 'application/json;charset=UTF-8')
xhr.send(JSON.stringify({ call, args })) xhr.send(JSON.stringify({ call, args }))
xhr.onload = cb xhr.onload = () => cb(false)
xhr.onerror = () => flicker(sadBadge) xhr.onerror = () => {
flicker(sadBadge)
cb(true)
}
} }
const mkdirCall = (path, cb) => rpc('mkdirp', [prependPath(path)], cb) const mkdirCall = (path, cb) => rpc('mkdirp', [prependPath(path)], cb)
const rmCall = (path1, cb) => rpc('rm', [prependPath(path1)], cb) const rmCall = (path1, cb) => rpc('rm', [prependPath(path1)], cb)
const mvCall = (path1, path2, cb) => rpc('mv', [path1, path2], cb) const mvCall = (path1, path2, cb) => rpc('mv', [path1, path2], cb)
const sumCall = (path, type, cb) => rpc('sum', [prependPath(path), type], cb)
// File upload // File upload
let totalDone = 0 let totalDone = 0
@@ -316,28 +318,36 @@ const textTypes = ['.txt', '.rtf', '.md', '.markdown', '.log', '.yaml', '.yml']
const isTextFile = src => src && textTypes.find(type => src.toLocaleLowerCase().includes(type)) const isTextFile = src => src && textTypes.find(type => src.toLocaleLowerCase().includes(type))
let fileEdited let fileEdited
function saveText (quitting) { function saveText (cb) {
const formData = new FormData() const formData = new FormData()
formData.append(fileEdited, editor.value) formData.append(fileEdited, editor.value)
const path = encodeURIComponent(decodeURI(location.pathname) + fileEdited) const fname = fileEdited + ".swp"
const path = encodeURIComponent(decodeURI(location.pathname) + fname)
upload(0, formData, path, () => { upload(0, formData, path, () => {
toast.style.display = 'none' toast.style.display = 'none'
if (!quitting) return cb()
clearInterval(window.padTimer)
window.onbeforeunload = null
resetView()
softPrev()
refresh()
}, () => { }, () => {
toast.style.display = 'block' toast.style.display = 'block'
if (!quitting) return
alert('cant save!\r\nleave window open to resume saving\r\nwhen connection back up') alert('cant save!\r\nleave window open to resume saving\r\nwhen connection back up')
}) })
} }
function padOff () { function padOff () {
if (!isEditorMode()) { return } if (!isEditorMode()) { return }
saveText(true) const swapfile = fileEdited + ".swp"
saveText(() => {
mvCall(prependPath(swapfile), prependPath(fileEdited), err => {
if (err) {
alert('cant save!\r\nleave window open to resume saving\r\nwhen connection back up')
return
}
clearInterval(window.padTimer)
window.onbeforeunload = null
resetView()
softPrev()
refresh()
})
})
return true return true
} }
@@ -384,7 +394,7 @@ function resetView () {
scrollToArrow() scrollToArrow()
} }
window.quitAll = () => helpOff() || sumsOff() || picsOff() || videosOff() || padOff() || pdfOff() window.quitAll = () => helpOff() || picsOff() || videosOff() || padOff() || pdfOff()
// Mkdir icon // Mkdir icon
window.mkdirBtn = function () { window.mkdirBtn = function () {
@@ -660,40 +670,6 @@ function helpOff () {
return true return true
} }
// checksums
function getSum (type) {
upBarPc.style.display = 'block'
upBarPc.innerText = 'computing checksum...'
upBarPc.style.width = '100%'
sumsOff()
sumCall(getASelected().innerText, type, loaded => {
navigator.clipboard.writeText(loaded.target.responseText)
upBarPc.style.display = 'none'
flicker(okBadge)
})
}
const isSumsMode = () => sums.style.display === 'block'
const sumsToggle = () => isSumsMode() ? sumsOff() : sumsOn()
function sumsOn () {
if (isFolder(getASelected())) {
alert('cannot checksum a directory')
return
}
sums.style.display = 'block'
table.style.display = 'none'
}
window.sumsOff = sumsOff
function sumsOff () {
if (!isSumsMode()) return
sums.style.display = 'none'
table.style.display = 'table'
return true
}
// Paste handler // Paste handler
const cuts = [] const cuts = []
function onPaste () { function onPaste () {
@@ -784,69 +760,40 @@ document.body.addEventListener('keydown', e => {
return return
} }
// Modifier keys // Ctrl keys
if (!e.shiftKey) { if ((e.ctrlKey || e.metaKey) && !e.shiftKey) {
if (e.ctrlKey || e.metaKey) { switch (e.code) {
switch (e.code) { case 'KeyC':
case 'KeyC': return prevent(e) || isRo() || cpPath()
return prevent(e) || isRo() || cpPath()
case 'KeyH': case 'KeyH':
return prevent(e) || isRo() || helpToggle() return prevent(e) || isRo() || helpToggle()
case 'KeyZ': case 'KeyX':
return prevent(e) || isRo() || sumsToggle() return prevent(e) || isRo() || onCut()
case 'KeyX': case 'KeyR':
return prevent(e) || isRo() || onCut() return prevent(e) || refresh()
case 'KeyR': case 'KeyV':
return prevent(e) || refresh() return prevent(e) || isRo() || ensureMove() || onPaste()
case 'KeyV': case 'Backspace':
return prevent(e) || isRo() || ensureMove() || onPaste() return prevent(e) || isRo() || window.rm(e)
case 'Backspace': case 'KeyE':
return prevent(e) || isRo() || window.rm(e) return prevent(e) || isRo() || window.rename(e)
case 'KeyE': case 'KeyM':
return prevent(e) || isRo() || window.rename(e) return prevent(e) || isRo() || window.mkdirBtn()
case 'KeyM': case 'KeyU':
return prevent(e) || isRo() || window.mkdirBtn() return prevent(e) || isRo() || manualUpload.click()
case 'KeyU': case 'Enter':
return prevent(e) || isRo() || manualUpload.click() case 'ArrowRight':
return prevent(e) || dl(getASelected())
case 'Enter':
case 'ArrowRight':
return prevent(e) || dl(getASelected())
}
} else if (isSumsMode()) {
switch (e.code) {
case 'Digit1':
return prevent(e) || isRo() || getSum('sha1')
case 'Digit2':
return prevent(e) || isRo() || getSum('sha256')
case 'Digit3':
return prevent(e) || isRo() || getSum('sha512')
case 'Digit5':
return prevent(e) || isRo() || getSum('md5')
}
} }
} else {
// Workaround Firefox requirement for transient activation
// https://developer.mozilla.org/en-US/docs/Web/Security/User_activation
// Firefox requires user interaction (that is not reserved by the user agent)
// before a file picker can be displayed. This means that ctrl/meta are not
// usable as modifiers until the user clicks the page or presses another
// non-modifier key. To work around this, the shift key can be used, instead.
if (e.code == 'KeyU') {
return prevent(e) || isRo() || manualUpload.click()
}
} }
switch (e.code) { switch (e.code) {
+8 -6
View File
@@ -344,8 +344,12 @@ h1 > span:hover {
right: 30px; right: 30px;
} }
#helpTable, #helpHead {
#sumsTable { margin-top: 60px;
text-align: center;
}
#helpTable {
border-collapse: collapse; border-collapse: collapse;
width: 70%; width: 70%;
max-width: 790px; max-width: 790px;
@@ -356,8 +360,7 @@ h1 > span:hover {
overflow-y: auto; overflow-y: auto;
} }
#helpTable td, #helpTable td {
#sumsTable td {
width: 200px; width: 200px;
padding: 9px; padding: 9px;
border: 1px solid #fff; border: 1px solid #fff;
@@ -366,8 +369,7 @@ h1 > span:hover {
margin: 0; margin: 0;
} }
#help, #help {
#sums {
background-color: black; background-color: black;
position: absolute; position: absolute;
top: 0px; top: 0px;
Vendored
+3 -12
View File
@@ -4,7 +4,7 @@
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="theme-color" content="rgb(45,52,54)"> <meta name="theme-color" content="rgb(45,52,54)">
<meta name="msapplication-navbutton-color" content="rgb(45,52,54)"> <meta name="msapplication-navbutton-color" content="rgb(45,52,54)">
<meta name="mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-capable" content="yes">
<meta name="viewport" content="width=device-width"> <meta name="viewport" content="width=device-width">
<link rel="manifest" href='data:application/manifest+json,{"name":"{{.Title}}","short_name":"{{.Title}}","description":" ","icons":[{"src":"data:image/svg+xml;base64,favicon_will_be_here","sizes":"150x150","type":"image/svg+xml"}],"background":"rgb(45,52,54)","theme_color":"rgb(45,52,54)","display":"standalone"}' /> <link rel="manifest" href='data:application/manifest+json,{"name":"{{.Title}}","short_name":"{{.Title}}","description":" ","icons":[{"src":"data:image/svg+xml;base64,favicon_will_be_here","sizes":"150x150","type":"image/svg+xml"}],"background":"rgb(45,52,54)","theme_color":"rgb(45,52,54)","display":"standalone"}' />
@@ -26,11 +26,10 @@
<tr><td>Ctrl/Meta + C</td><td>copy URL to clipboard</td></tr> <tr><td>Ctrl/Meta + C</td><td>copy URL to clipboard</td></tr>
<tr><td>Ctrl/Meta + E</td><td>rename item</td></tr> <tr><td>Ctrl/Meta + E</td><td>rename item</td></tr>
<tr><td>Ctrl/Meta + Backspace</td><td>delete item</td></tr> <tr><td>Ctrl/Meta + Backspace</td><td>delete item</td></tr>
<tr><td>Ctrl/Meta/Shift + U</td><td>upload new file/folder</td></tr> <tr><td>Ctrl/Meta + U</td><td>upload new file/folder</td></tr>
<tr><td>Ctrl/Meta + M</td><td>create a new directory</td></tr> <tr><td>Ctrl/Meta + M</td><td>create a new directory</td></tr>
<tr><td>Ctrl/Meta + X</td><td>cut selected path</td></tr> <tr><td>Ctrl/Meta + X</td><td>cut selected path</td></tr>
<tr><td>Ctrl/Meta + V</td><td>paste previously selected paths to directory</td></tr> <tr><td>Ctrl/Meta + V</td><td>paste previously selected paths to directory</td></tr>
<tr><td>Ctrl/Meta + Z</td><td>copy checksums of selected file</td></tr>
<tr><td>Ctrl + click</td><td>download selected item as archive</td></tr> <tr><td>Ctrl + click</td><td>download selected item as archive</td></tr>
<tr><td>click file icon </td><td>rename item</td></tr> <tr><td>click file icon </td><td>rename item</td></tr>
<tr><td>double click file icon</td><td>delete item</td></tr> <tr><td>double click file icon</td><td>delete item</td></tr>
@@ -39,18 +38,10 @@
<tr><td>any other letter</td><td>fuzzy search</td></tr> <tr><td>any other letter</td><td>fuzzy search</td></tr>
</tbody></table></div> </tbody></table></div>
<div onclick="window.sumsOff()" style="display: none;" id="sums"><table id="sumsTable"><tbody>
<tr><td>Key</td><td>Hash Algorithm</td></tr>
<tr><td>1</td><td>copy sha1 sum</td></tr>
<tr><td>2</td><td>copy sha256 sum</td></tr>
<tr><td>3</td><td>copy sha512 sum</td></tr>
<tr><td>5</td><td>copy md5 sum</td></tr>
</tbody></table></div>
<div style="display: none;" onclick="window.quitAll()" id="quitAll"><i style="display: none;" id="toast">cant reach server</i></div> <div style="display: none;" onclick="window.quitAll()" id="quitAll"><i style="display: none;" id="toast">cant reach server</i></div>
<textarea style="display: none;" id="text-editor"></textarea> <textarea style="display: none;" id="text-editor"></textarea>
<div id="drop-grid"></div> <div id="drop-grid"></div>
<input type="file" id="clickupload" multiple style="display:none"/> <input type="file" id="clickupload" style="display:none"/>
<h1 onclick="return titleClick(event)">.{{.Title}}</h1> <h1 onclick="return titleClick(event)">.{{.Title}}</h1>