Compare commits

...
14 Commits
Author SHA1 Message Date
Pierre Dubouilh b5505d4773 readme + ci 2023-07-08 15:46:47 +02:00
Pierre Dubouilh 58b6840c38 readme 2023-07-08 15:25:11 +02:00
Pierre Dubouilh 17e18cabab log options & stricter checks
closes #93
2023-07-08 15:25:11 +02:00
Pierre Dubouilh d832a760d2 docker: document mkdir prior to docker call
closes #92
2023-07-08 15:25:11 +02:00
hiranyey 929f30a9bb pdf viewer 2023-07-08 15:25:11 +02:00
Pierre Dubouilh 97c427fb87 lint js 2023-07-08 15:25:11 +02:00
Pierre Dubouilh 7b4987d503 small cleanup gossa.go 2023-07-08 15:25:11 +02:00
Pierre Dubouilh 9de04f2268 fix url with hashes
closes #76
2023-07-08 15:25:11 +02:00
Pierre Dubouilh a342c79aea let html be displayed naturally
closes #83
2023-07-08 15:25:11 +02:00
Pierre Dubouilh 7b899af6a8 download icon on video page
closes #90
2023-07-08 15:25:11 +02:00
Pierre Dubouilh c0ff5adaf9 nit ui 2023-07-08 15:25:11 +02:00
Pierre Dubouilh 0a77b08552 bring UI in this repo 2023-07-08 15:25:11 +02:00
Pierre Dubouilh 08ec485e78 enable dependabot 2023-07-08 15:25:11 +02:00
Ning Shang 3ce7703d60 Make make clean output cleaner
Use `rm -f` to suppress errors on removal of nonexistent files.

Also make `clean` a phony target.
2022-10-20 08:51:14 +02:00
13 changed files with 1500 additions and 87 deletions
+1
View File
@@ -0,0 +1 @@
ui/** linguist-vendored
+3
View File
@@ -12,6 +12,9 @@ jobs:
go-version: 1.19.1
id: go
- name: deps
run: npm i -g standard
- name: Check out code into the Go module directory
uses: actions/checkout@v1
with:
-4
View File
@@ -1,4 +0,0 @@
[submodule "gossa-ui"]
path = gossa-ui
url = https://github.com/pldubouilh/gossa-ui
branch = master
+26 -23
View File
@@ -1,26 +1,29 @@
FLAGS := -ldflags "-s -w" -trimpath
NOCGO := CGO_ENABLED=0
build:
build::
go vet && go fmt
${NOCGO} go build ${FLAGS} -o gossa
install:
install::
sudo cp gossa /usr/local/bin
run:
run::
./gossa -verb=true test-fixture
run-ro:
run-ro::
./gossa -verb=true -ro=true test-fixture
run-extra:
run-extra::
./gossa -verb=true -prefix="/fancy-path/" -k=false -symlinks=true test-fixture
ci: build-all test
lint-js::
standard
ci:: build-all test lint-js
echo "done"
test:
test::
-@cd test-fixture && ln -s ../support .; true
go test -cover -c -tags testrunmain
@@ -43,19 +46,19 @@ test:
# go tool cover -html all.out
# go tool cover -func=all.out | grep main | grep '9.\..\%'
watch:
ls gossa.go gossa_test.go gossa-ui/* | entr -rc make build run
watch::
ls gossa.go gossa_test.go ui/* | entr -rc make build run
watch-extra:
ls gossa.go gossa_test.go gossa-ui/* | entr -rc make build run-extra
watch-extra::
ls gossa.go gossa_test.go ui/* | entr -rc make build run-extra
watch-ro:
ls gossa.go gossa_test.go gossa-ui/* | entr -rc make build run-ro
watch-ro::
ls gossa.go gossa_test.go ui/* | entr -rc make build run-ro
watch-test:
ls gossa.go gossa_test.go gossa-ui/* | entr -rc make test
watch-test::
ls gossa.go gossa_test.go ui/* | entr -rc make test
build-all: build
build-all:: build
${NOCGO} GOOS=linux GOARCH=amd64 go build ${FLAGS} -o builds/gossa-linux-x64
${NOCGO} GOOS=linux GOARCH=arm go build ${FLAGS} -o builds/gossa-linux-arm
${NOCGO} GOOS=linux GOARCH=arm64 go build ${FLAGS} -o builds/gossa-linux-arm64
@@ -64,11 +67,11 @@ build-all: build
${NOCGO} GOOS=windows GOARCH=amd64 go build ${FLAGS} -o builds/gossa-windows.exe
sha256sum builds/*
clean:
-rm gossa
-rm gossa-linux64
-rm gossa-linux-arm
-rm gossa-linux-arm64
-rm gossa-mac
-rm gossa-windows.exe
clean::
rm -f gossa
rm -f gossa-linux64
rm -f gossa-linux-arm
rm -f gossa-linux-arm64
rm -f gossa-mac
rm -f gossa-windows.exe
Submodule gossa-ui deleted from 5ec0a804c9
+34 -16
View File
@@ -23,6 +23,29 @@ import (
"strings"
)
type rowTemplate struct {
Name string
Href template.HTML
Size string
Ext string
}
type pageTemplate struct {
Title template.HTML
ExtraPath template.HTML
Ro bool
RowsFiles []rowTemplate
RowsFolders []rowTemplate
}
var host = flag.String("h", "127.0.0.1", "host to listen to")
var port = flag.String("p", "8001", "port to listen to")
var extraPath = flag.String("prefix", "/", "url prefix at which gossa can be reached, e.g. /gossa/ (slashes of importance)")
var symlinks = flag.Bool("symlinks", false, "follow symlinks \033[4mWARNING\033[0m: symlinks will by nature allow to escape the defined path (default: false)")
var verb = flag.Bool("verb", false, "verbosity")
var skipHidden = flag.Bool("k", true, "\nskip hidden files")
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"`
@@ -92,15 +115,18 @@ func replyList(w http.ResponseWriter, r *http.Request, fullPath string, path str
}
href := url.PathEscape(el.Name())
name := el.Name()
if el.IsDir() && strings.HasPrefix(href, "/") {
href = strings.Replace(href, "/", "", 1)
}
if el.IsDir() {
p.RowsFolders = append(p.RowsFolders, rowTemplate{el.Name() + "/", template.HTML(href), "", "folder"})
p.RowsFolders = append(p.RowsFolders, rowTemplate{name + "/", template.HTML(href), "", "folder"})
} else {
sl := strings.Split(el.Name(), ".")
sl := strings.Split(name, ".")
ext := strings.ToLower(sl[len(sl)-1])
p.RowsFiles = append(p.RowsFiles, rowTemplate{el.Name(), template.HTML(href), humanize(el.Size()), ext})
p.RowsFiles = append(p.RowsFiles, rowTemplate{name, template.HTML(href), humanize(el.Size()), ext})
}
}
@@ -233,10 +259,10 @@ func enforcePath(p string) string {
}
func main() {
if flag.Parse(); len(flag.Args()) > 0 {
if flag.Parse(); len(flag.Args()) == 1 {
rootPath = flag.Args()[0]
} else {
fmt.Printf("\nusage: ./gossa ~/directory-to-share\n\n")
fmt.Printf("\nusage: ./gossa [OPTIONS] ~/directory-to-share\n\n")
flag.PrintDefaults()
os.Exit(1)
}
@@ -246,16 +272,6 @@ func main() {
check(err)
server := &http.Server{Addr: *host + ":" + *port, Handler: handler}
// clean shutdown - used only for coverage test
// go func() {
// sigchan := make(chan os.Signal, 1)
// signal.Notify(sigchan, os.Interrupt)
// <-sigchan
// ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
// defer cancel()
// server.Shutdown(ctx)
// }()
if !*ro {
http.HandleFunc(*extraPath+"rpc", rpc)
http.HandleFunc(*extraPath+"post", upload)
@@ -264,7 +280,9 @@ func main() {
http.HandleFunc("/", doContent)
handler = http.StripPrefix(*extraPath, http.FileServer(http.Dir(rootPath)))
fmt.Printf("Gossa starting on directory %s\nListening on http://%s:%s%s\n", rootPath, *host, *port, *extraPath)
fmt.Printf("Gossa starting on directory %s\n", rootPath)
fmt.Printf("Verbose: %t, Symlinks: %t, Read-Only: %t, Hidden-Files Skipped: %t\n", *verb, *symlinks, *ro, *skipHidden)
fmt.Printf("Listening on http://%s:%s%s\n", *host, *port, *extraPath)
if err = server.ListenAndServe(); err != http.ErrServerClosed {
check(err)
}
+9 -33
View File
@@ -3,48 +3,24 @@ package main
import (
_ "embed"
"encoding/base64"
"flag"
"html/template"
"strings"
)
//go:embed gossa-ui/script.js
//go:embed ui/script.js
var scriptJs string
//go:embed gossa-ui/style.css
//go:embed ui/style.css
var styleCss string
//go:embed gossa-ui/favicon.svg
//go:embed ui/favicon.svg
var faviconSvg []byte
//go:embed gossa-ui/ui.tmpl
var templateStr string
//go:embed ui/ui.tmpl
var template0 string
// fill in template
var templateCss = strings.Replace(templateStr, "css_will_be_here", styleCss, 1)
var templateCssJs = strings.Replace(templateCss, "js_will_be_here", scriptJs, 1)
var templateCssJssIcon = strings.Replace(templateCssJs, "favicon_will_be_here", base64.StdEncoding.EncodeToString(faviconSvg), 2)
var templateParsed, _ = template.New("").Parse(templateCssJssIcon)
type rowTemplate struct {
Name string
Href template.HTML
Size string
Ext string
}
type pageTemplate struct {
Title template.HTML
ExtraPath template.HTML
Ro bool
RowsFiles []rowTemplate
RowsFolders []rowTemplate
}
var host = flag.String("h", "127.0.0.1", "host to listen to")
var port = flag.String("p", "8001", "port to listen to")
var extraPath = flag.String("prefix", "/", "url prefix at which gossa can be reached, e.g. /gossa/ (slashes of importance)")
var symlinks = flag.Bool("symlinks", false, "follow symlinks \033[4mWARNING\033[0m: symlinks will by nature allow to escape the defined path (default: false)")
var verb = flag.Bool("verb", false, "verbosity")
var skipHidden = flag.Bool("k", true, "\nskip hidden files")
var ro = flag.Bool("ro", false, "read only mode (no upload, rename, move, etc...)")
var template1 = strings.Replace(template0, "css_will_be_here", styleCss, 1)
var template2 = strings.Replace(template1, "js_will_be_here", scriptJs, 1)
var template3 = strings.Replace(template2, "favicon_will_be_here", base64.StdEncoding.EncodeToString(faviconSvg), 2)
var templateParsed, _ = template.New("").Parse(template3)
+14 -10
View File
@@ -9,28 +9,30 @@ gossa
a fast and simple webserver for your files, that's dependency-free and with under 250 lines of code, easy to review.
a [simple UI](https://github.com/pldubouilh/gossa-ui) comes as default, featuring :
a simple UI comes as default, featuring :
* 🔍 files/directories browser & handler
* 📩 drag-and-drop uploader
* 🥂 fast golang static server
* 💾 90s web UI that prints in milliseconds
* 📸 video streaming & picture browser
* 📸 video streaming, picture browser, pdf viewer
* ✍️ simple note editor
* ⌨️ keyboard navigation
* 🚀 lightweight and dependency free codebase
* 🔒 >95% test coverage and reproducible builds
* 🥂 fast golang static server
* 💑 easy multi account setup, read-only mode
* ✨ PWA enabled
* ✨ PWA-able
* 🖥️ multi-platform support
### install / build
[arch linux (AUR)](https://aur.archlinux.org/packages/gossa/) - e.g. `yay -S gossa`
[nix](https://search.nixos.org/packages?channel=unstable&show=gossa&from=0&size=50&sort=relevance&type=packages&query=gossa) - e.g. `nix-shell -p gossa`
binaries are available on the [release page](https://github.com/pldubouilh/gossa/releases) - or simply `make build` this repo.
### build
built blobs are available on the [release page](https://github.com/pldubouilh/gossa/releases) - or simply `make build` this repo.
all builds are reproducible, checkout the hashes on the release page.
arch linux users can also install through the [user repos](https://aur.archlinux.org/packages/gossa/) - e.g. `yay -S gossa`
automatic boot-time startup can be handled with a user systemd service - see [support](https://github.com/pldubouilh/gossa/tree/master/support)
### usage
```sh
% ./gossa --help
@@ -46,9 +48,11 @@ release images are pushed to [dockerhub](https://hub.docker.com/r/pldubouilh/gos
```sh
# pull from dockerhub and run
% mkdir ~/LocalDirToShare
% sudo docker run -v ~/LocalDirToShare:/shared -p 8001:8001 pldubouilh/gossa
```
in a do-one-thing-well mindset, HTTPS and authentication has been left to middlewares and proxies. [sample caddy configs](https://github.com/pldubouilh/gossa/blob/master/support/) are available to quickly setup multi users setups along with https.
automatic boot-time startup can be handled with a user systemd service - see [support](https://github.com/pldubouilh/gossa/tree/master/support)
+1
View File
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 23 KiB

+4
View File
@@ -0,0 +1,4 @@
gossa-ui
========
a ~~ugly~~ simple, dependency-free, plain js/css/html front-end for [gossa](https://github.com/pldubouilh/gossa)
Vendored Executable
+851
View File
@@ -0,0 +1,851 @@
/* eslint-env browser */
/* eslint-disable no-multi-str */
function cancelDefault (e) {
e.preventDefault()
e.stopPropagation()
}
const warningMsg = () => 'Leaving will interrupt transfer?\n'
const rmMsg = () => !confirm('Remove file?\n')
const ensureMove = () => !confirm('move items?')
const isRo = () => window.ro
const upBarName = document.getElementById('upBarName')
const upBarPc = document.getElementById('upBarPc')
const upGrid = document.getElementById('drop-grid')
const pics = document.getElementById('pics')
const picsHolder = document.getElementById('picsHolder')
const video = document.getElementById('video')
const videoHolder = document.getElementById('videoHolder')
const manualUpload = document.getElementById('clickupload')
const pdf = document.getElementById('pdf')
const help = document.getElementById('help')
const okBadge = document.getElementById('ok')
const sadBadge = document.getElementById('sad')
const pageTitle = document.head.querySelector('title')
const pageH1 = document.body.querySelector('h1')
const editor = document.getElementById('text-editor')
const crossIcon = document.getElementById('quitAll')
const toast = document.getElementById('toast')
const table = document.getElementById('linkTable')
const transparentPixel = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII='
// helpers
let allA
let imgsIndex
let allImgs
const decode = a => decodeURIComponent(a).replace(location.origin, '')
const getArrowSelected = () => document.querySelector('.arrow-selected')
const getASelected = () => !getArrowSelected() ? false : getArrowSelected().parentElement.parentElement.querySelectorAll('a')[0]
const prependPath = a => a.startsWith('/') ? a : decodeURI(location.pathname) + a
const prevent = e => e.preventDefault()
const flicker = w => w.classList.remove('runFade') || void w.offsetWidth || w.classList.add('runFade') // eslint-disable-line
const encodeURIHash = e => encodeURI(e).replaceAll('#', '%23')
// Manual upload
manualUpload.addEventListener('change', () => Array.from(manualUpload.files).forEach(f => isDupe(f.name) || postFile(f, '/' + f.name)), false)
// Soft nav
async function browseTo (href, flickerDone, skipHistory) {
try {
const r = await fetch(href, { credentials: 'include' })
const t = await r.text()
const parsed = new DOMParser().parseFromString(t, 'text/html')
table.innerHTML = parsed.getElementById('linkTable').innerHTML
const title = parsed.head.querySelector('title').innerText
// check if is current path - if so skip following
if (pageTitle.innerText !== title) {
if (!skipHistory) {
const escaped = encodeURIHash(window.extraPath + title)
history.pushState({}, '', escaped)
}
pageTitle.innerText = title
pageH1.innerText = '.' + title
setTitle()
}
init()
if (flickerDone) flicker(okBadge)
} catch (error) {
flicker(sadBadge)
}
}
window.onClickLink = e => {
const a = e ? e.target : getASelected()
if (e) {
setCursorTo(e.target.innerText)
}
// always force download if ctrl pressed (also covers zipping folders)
if (e && e.ctrlKey) {
dl(a)
return false
}
// follow dirs
if (isFolder(a)) {
browseTo(a.href)
return false
// enable notepad if relevant
} else if (!window.ro && isTextFile(a.innerText) && !isEditorMode()) {
padOn(a)
return false
// toggle picture carousel
} else if (isPic(a.href) && !isPicMode()) {
picsOn(a.href)
return false
// toggle videos mode
} else if (isVideo(a.href) && !isVideoMode()) {
videoOn(a.href)
return false
// let html be displayed naturally
} else if (a.innerText.endsWith('.html')) {
return true
} else if (isPdf(a.href)) {
openPDF(a.href)
return false
}
// else just force download
dl(a)
return false
}
let softStatePushed
function pushSoftState (d) {
if (softStatePushed) { return }
softStatePushed = true
history.pushState({}, '', encodeURIHash(d))
}
const refresh = () => browseTo(location.href, true)
const softPrev = () => history.replaceState({}, '', location.href.split('/').slice(0, -1).join('/') + '/')
const isAtExtraPath = url => location.origin + window.extraPath + '/../' === url
const prevPage = (url, skipHistory) => window.quitAll() || isAtExtraPath(url) || browseTo(url, false, skipHistory)
window.onpopstate = () => prevPage(location.href, true)
// RPC
function upload (id, what, path, cbDone, cbErr, cbUpdate) {
const xhr = new XMLHttpRequest()
xhr.open('POST', location.origin + window.extraPath + '/post')
xhr.setRequestHeader('gossa-path', path)
xhr.upload.addEventListener('load', cbDone)
xhr.upload.addEventListener('progress', cbUpdate)
xhr.upload.addEventListener('error', cbErr)
xhr.upload.id = id
xhr.send(what)
}
function rpc (call, args, cb) {
console.log('RPC', call, args)
const xhr = new XMLHttpRequest()
xhr.open('POST', location.origin + window.extraPath + '/rpc')
xhr.setRequestHeader('Content-Type', 'application/json;charset=UTF-8')
xhr.send(JSON.stringify({ call, args }))
xhr.onload = cb
xhr.onerror = () => flicker(sadBadge)
}
const mkdirCall = (path, cb) => rpc('mkdirp', [prependPath(path)], cb)
const rmCall = (path1, cb) => rpc('rm', [prependPath(path1)], cb)
const mvCall = (path1, path2, cb) => rpc('mv', [path1, path2], cb)
// File upload
let totalDone = 0
let totalUploads = 0
let totalUploadsSize = 0
let totalUploadedSize = []
const dupe = test => allA.find(a => a.innerHTML.replace('/', '') === test)
const isDupe = t => dupe(t) ? alert(t + ' already already exists') || true : false
function shouldRefresh () {
totalDone += 1
if (totalUploads === totalDone) {
window.onbeforeunload = null
console.log('done uploading ' + totalDone + ' files')
totalDone = 0
totalUploads = 0
totalUploadsSize = 0
totalUploadedSize = []
upBarPc.style.display = upBarName.style.display = 'none'
table.classList.remove('uploading-table')
setTimeout(refresh, 200)
}
}
function updatePercent (ev) {
totalUploadedSize[ev.target.id] = ev.loaded
const ttlDone = totalUploadedSize.reduce((s, x) => s + x)
const pc = Math.floor(100 * ttlDone / totalUploadsSize) + '%'
upBarPc.innerText = pc
upBarPc.style.width = pc
}
function postFile (file, path) {
if (window.ro) return
path = decodeURI(location.pathname).slice(0, -1) + path
window.onbeforeunload = warningMsg
table.classList.add('uploading-table')
upBarPc.style.display = upBarName.style.display = 'block'
totalUploads += 1
totalUploadsSize += file.size
upBarName.innerText = totalUploads > 1 ? totalUploads + ' files' : file.name
const formData = new FormData()
formData.append(file.name, file)
upload(totalUploads, formData, encodeURIComponent(path), shouldRefresh, null, updatePercent)
}
const parseDomFolder = f => f.createReader().readEntries(e => e.forEach(i => parseDomItem(i)))
function parseDomItem (domFile, shoudCheckDupes) {
if (shoudCheckDupes && isDupe(domFile.name)) {
return
}
if (domFile.isFile) {
domFile.file(f => postFile(f, domFile.fullPath))
} else {
// remove absolute path
const f = domFile.fullPath.startsWith('/') ? domFile.fullPath.slice(1) : domFile.fullPath
mkdirCall(f, () => parseDomFolder(domFile))
}
}
function pushEntry (entry) {
if (!entry.webkitGetAsEntry && !entry.getAsEntry) {
return alert('Unsupported browser ! Please update to chrome/firefox.')
} else {
entry = entry.webkitGetAsEntry() || entry.getAsEntry()
}
parseDomItem(entry, true)
}
window.titleClick = function (e) {
const p = Array.from(document.querySelector('h1').childNodes).map(k => k.innerText)
const i = p.findIndex(s => s === e.target.innerText)
const dst = p.slice(0, i + 1).join('').slice(1)
const target = location.origin + window.extraPath + encodeURIHash(dst)
browseTo(target, false)
}
// Move files and folders
const isFolder = e => e && e.href && e.innerText.endsWith('/')
const setBackgroundLinks = t => { t.classList.add('highlight') }
const getLink = () => document.querySelector('.highlight') || {}
const resetBackgroundLinks = () => { try { getLink().classList.remove('highlight') } catch(e) { /* */ } } // eslint-disable-line
// Not the nicest - sometimes, upon hover, firefox reports nodeName === '#text', and chrome reports nodeName === 'A'...
const getClosestRow = t => t.nodeName === '#text' ? t.parentElement.parentElement : t.nodeName === 'A' ? t.parentElement : t
let draggingSrc
upGrid.ondragend = upGrid.ondragexit = upGrid.ondragleave = e => {
cancelDefault(e)
upGrid.style.display = 'none'
}
// Handle hover
document.ondragenter = e => {
if (isEditorMode() || isPicMode() || window.ro) { return }
cancelDefault(e)
resetBackgroundLinks()
// Display upload grid when uploading new elements
if (!draggingSrc) {
upGrid.style.display = 'flex'
e.dataTransfer.dropEffect = 'copy'
// Or highlight entry if drag and drop
} else if (draggingSrc) {
const t = getClosestRow(e.target)
isFolder(t.firstChild) && setBackgroundLinks(t)
}
}
document.ondragstart = e => { draggingSrc = e.target.innerHTML }
document.ondragend = e => resetBackgroundLinks()
document.ondragover = e => {
cancelDefault(e)
return false
}
// Handle drop
document.ondrop = e => {
if (window.ro) return
cancelDefault(e)
upGrid.style.display = 'none'
const t = getLink().firstChild
// move to a folder
if (draggingSrc && t) {
const dest = t.innerHTML + draggingSrc
ensureMove() || mvCall(prependPath(draggingSrc), prependPath(dest), refresh)
// ... or upload
} else if (e.dataTransfer.items.length) {
Array.from(e.dataTransfer.items).forEach(pushEntry)
}
resetBackgroundLinks()
draggingSrc = null
return false
}
// Notepad
const isEditorMode = () => editor.style.display === 'block'
const textTypes = ['.txt', '.rtf', '.md', '.markdown', '.log', '.yaml', '.yml']
const isTextFile = src => src && textTypes.find(type => src.toLocaleLowerCase().includes(type))
let fileEdited
function saveText (quitting) {
const formData = new FormData()
formData.append(fileEdited, editor.value)
const path = encodeURIComponent(decodeURI(location.pathname) + fileEdited)
upload(0, formData, path, () => {
toast.style.display = 'none'
if (!quitting) return
clearInterval(window.padTimer)
window.onbeforeunload = null
resetView()
softPrev()
refresh()
}, () => {
toast.style.display = 'block'
if (!quitting) return
alert('cant save!\r\nleave window open to resume saving\r\nwhen connection back up')
})
}
function padOff () {
if (!isEditorMode()) { return }
saveText(true)
return true
}
async function padOn (a) {
if (a) {
try {
fileEdited = a.innerHTML
const f = await fetch(a.href, {
credentials: 'include',
headers: new Headers({ pragma: 'no-cache', 'cache-control': 'no-cache' })
})
editor.value = await f.text()
} catch (error) {
return alert('cant read file')
}
} else {
fileEdited = prompt('new filename', '')
if (!fileEdited) { return }
fileEdited = isTextFile(fileEdited) ? fileEdited : fileEdited + '.txt'
if (isDupe(fileEdited)) { return }
editor.value = ''
}
console.log('editing file', fileEdited)
setCursorTo(fileEdited)
editor.style.display = crossIcon.style.display = 'block'
table.style.display = 'none'
editor.focus()
window.onbeforeunload = warningMsg
window.padTimer = setInterval(saveText, 5000)
pushSoftState('?editor=' + fileEdited)
}
window.displayPad = padOn
// quit pictures or editor
function resetView () {
softStatePushed = false
table.style.display = 'table'
picsHolder.src = transparentPixel
videoHolder.src = ''
pdf.innerHTML = ''
editor.style.display = pics.style.display = video.style.display = pdf.style.display = crossIcon.style.display = 'none'
scrollToArrow()
}
window.quitAll = () => helpOff() || picsOff() || videosOff() || padOff() || pdfOff()
// Mkdir icon
window.mkdirBtn = function () {
const folder = prompt('new folder name', '')
if (folder && !isDupe(folder)) {
mkdirCall(folder, refresh)
}
}
// Icon click handler
const getBtnA = e => e.target.closest('tr').querySelector('a')
window.rm = e => {
if (window.ro) return true
clearTimeout(window.clickToken)
const target = e.key ? getASelected() : getBtnA(e)
if (target.innerText === '../') return
if (rmMsg()) return
moveArrow()
rmCall(decode(target.href), refresh)
}
window.rename = (e, commit) => {
if (window.ro) return true
clearTimeout(window.clickToken)
if (!commit) {
window.clickToken = setTimeout(window.rename, 300, e, true)
return
}
const target = e.key ? getASelected() : getBtnA(e)
if (target.innerText === '../') return
const chg = prompt('rename to', target.innerText)
if (chg && !isDupe(chg)) {
mvCall(prependPath(target.innerText), prependPath(chg), refresh)
}
}
function aboveBelowRightin (el) {
const itemPos = el.getBoundingClientRect()
return itemPos.top < 0 ? -1 : itemPos.bottom > window.innerHeight ? 1 : 0
}
function scrollToArrow () {
const el = getASelected()
while (1) {
const pos = aboveBelowRightin(el)
if (pos === -1) {
scrollBy(0, -300)
} else if (pos === 1) {
scrollBy(0, 300)
} else {
break
}
}
}
function clearArrowSelected () {
const arr = getArrowSelected()
if (!arr) { return }
arr.classList.remove('arrow-selected')
}
window.setCursorTo = setCursorTo
function setCursorTo (where) {
if (!where) return false
clearArrowSelected()
let a = allA.find(el => el.innerText === where || el.innerText === where + '/')
if (!a) {
if (allA[0].innerText === '../') {
a = allA[1] || allA[0]
} else {
a = allA[0]
}
}
const icon = a.parentElement.parentElement.querySelectorAll('.arrow-icon')[0]
icon.classList.add('arrow-selected')
scrollToArrow()
storeArrow(where)
return true
}
function moveArrow (down) {
const all = Array.from(document.querySelectorAll('.arrow-icon'))
let i = all.findIndex(el => el.classList.contains('arrow-selected'))
clearArrowSelected()
if (down) {
i = all[i + 1] ? i + 1 : 0
} else {
i = all[i - 1] ? i - 1 : all.length - 1
}
all[i].classList.add('arrow-selected')
storeArrow(getASelected().innerText)
scrollToArrow()
}
const storeArrow = src => localStorage.setItem('last-selected' + window.extraPath + location.pathname, src)
const isTop = () => window.scrollY === 0
const isBottom = () => (window.innerHeight + window.scrollY) >= document.body.offsetHeight
const hasScroll = () => table.clientHeight > window.innerHeight
function movePage (up) {
const current = getASelected().href
if (!hasScroll()) return
if (!up) {
const i = allA.findIndex(e => aboveBelowRightin(e) === 1)
if (isTop() && current !== allA[i - 1].href) {
return setCursorTo(allA[i - 1].innerText)
} else if (isBottom() && current !== allA[allA.length - 1].href) {
return setCursorTo(allA[allA.length - 1].innerText)
}
if (!allA[i - 1]) return
setCursorTo(allA[i - 1].innerText)
scrollBy(0, window.innerHeight - 100)
} else {
const i = allA.findIndex(e => aboveBelowRightin(e) === 0)
if (isTop() && current !== allA[0].href) {
return setCursorTo(allA[0].innerText)
} else if (isBottom() && current !== allA[i].href) {
return setCursorTo(allA[i].innerText)
}
scrollBy(0, -(window.innerHeight - 100))
setCursorTo(allA[i].innerText)
}
}
// Pictures carousel
const picTypes = ['.jpg', '.jpeg', '.png', '.gif']
const isPic = src => src && picTypes.find(type => src.toLocaleLowerCase().includes(type))
const isPicMode = () => pics.style.display === 'flex'
window.picsNav = () => picsNav(true)
function setImage () {
const src = allImgs[imgsIndex]
picsHolder.src = src
const name = src.split('/').pop()
setCursorTo(decodeURI(name))
history.replaceState({}, '', encodeURIHash(name))
}
function picsOn (href) {
imgsIndex = allImgs.findIndex(el => el.includes(href))
setImage()
table.style.display = 'none'
crossIcon.style.display = 'block'
pics.style.display = 'flex'
const name = href.split('/').pop()
pushSoftState(name)
return true
}
function picsOff () {
if (!isPicMode()) { return }
resetView()
softPrev()
return true
}
function picsNav (down) {
if (!isPicMode()) { return false }
if (down) {
imgsIndex = allImgs[imgsIndex + 1] ? imgsIndex + 1 : 0
} else {
imgsIndex = allImgs[imgsIndex - 1] ? imgsIndex - 1 : allImgs.length - 1
}
setImage()
return true
}
let picsTouchStart = 0
picsHolder.addEventListener('touchstart', e => {
picsTouchStart = e.changedTouches[0].screenX
}, false)
picsHolder.addEventListener('touchend', e => {
if (e.changedTouches[0].screenX < picsTouchStart) {
picsNav(true)
} else if (e.changedTouches[0].screenX > picsTouchStart) {
picsNav(false)
}
}, false)
// Video player
const videosTypes = ['.mp4', '.webm', '.ogv', '.ogg', '.mp3', '.flac', '.wav']
const isVideo = src => src && videosTypes.find(type => src.toLocaleLowerCase().includes(type))
const isVideoMode = () => video.style.display === 'flex'
const videoFs = () => video.requestFullscreen()
const videoFf = future => { videoHolder.currentTime += future ? 10 : -10 }
const videoSound = up => { videoHolder.volume += up ? 0.1 : -0.1 }
videoHolder.oncanplay = () => videoHolder.play()
async function videoOn (src) {
const name = src.split('/').pop()
table.style.display = 'none'
crossIcon.style.display = 'block'
video.style.display = 'flex'
videoHolder.pause()
const time = localStorage.getItem('video-time' + src)
videoHolder.currentTime = parseInt(time) || 0
videoHolder.src = src
pushSoftState(decodeURI(name))
return true
}
function videosOff () {
if (!isVideoMode()) { return }
localStorage.setItem('video-time' + videoHolder.src, videoHolder.currentTime)
resetView()
softPrev()
return true
}
window.videodl = function () {
dl(getASelected())
}
// PDF Viewer
const pdfTypes = ['.pdf']
const isPdf = src => src && pdfTypes.find(type => src.toLocaleLowerCase().includes(type))
const isPdfMode = () => pdf.style.display === 'flex'
function openPDF (src) {
const name = src.split('/').pop()
table.style.display = 'none'
crossIcon.style.display = 'block'
pdf.style.display = 'flex'
const pdfEmbed = document.createElement('embed')
pdfEmbed.setAttribute('src', src + '#toolbar=0')
pdfEmbed.setAttribute('type', 'application/pdf')
pdf.appendChild(pdfEmbed)
pushSoftState(decodeURI(name))
return false
}
function pdfOff () {
if (!isPdfMode()) { return }
resetView()
softPrev()
return true
}
// help
const isHelpMode = () => help.style.display === 'block'
const helpToggle = () => isHelpMode() ? helpOff() : helpOn()
function helpOn () {
help.style.display = 'block'
table.style.display = 'none'
}
window.helpOff = helpOff
function helpOff () {
if (!isHelpMode()) return
help.style.display = 'none'
table.style.display = 'table'
return true
}
// Paste handler
const cuts = []
function onPaste () {
if (!cuts.length) { return refresh() }
const a = getASelected()
const root = cuts.pop()
const filename = root.split('/').pop()
const pwd = decodeURIComponent(location.pathname)
const dest = isFolder(a) ? pwd + a.innerHTML : pwd
mvCall(root, dest + filename, onPaste)
}
function onCut () {
const a = getASelected()
a.classList.add('linkSelected')
cuts.push(prependPath(decode(a.href)))
}
function dl (a) {
const orig = a.onclick
a.onclick = ''
// download as zip if folder
if (isFolder(a)) {
const loc = a.href
a.href = window.extraPath + '/zip?zipPath=' + encodeURIComponent(prependPath(a.innerText)) + '&zipName=' + encodeURIComponent(a.innerText.slice(0, -1))
a.click()
a.href = loc
} else {
a.download = a.innerText
a.click()
a.download = ''
}
a.onclick = orig
}
// Kb handler
let typedPath = ''
let typedToken = null
function cpPath () {
const t = document.createElement('textarea')
t.value = getASelected().href
document.body.appendChild(t)
t.select()
document.execCommand('copy')
document.body.removeChild(t)
}
document.body.addEventListener('keydown', e => {
if (e.code === 'Escape') {
return resetBackgroundLinks() || window.quitAll()
}
if (isHelpMode()) { return prevent(e) || window.quitAll() }
if (isEditorMode()) { return }
if (isPicMode()) {
switch (e.code) {
case 'ArrowLeft':
case 'ArrowUp':
return prevent(e) || picsNav(false)
case 'Enter':
case 'Tab':
case 'ArrowRight':
case 'ArrowDown':
return prevent(e) || picsNav(true)
}
return
}
if (isVideoMode()) {
switch (e.code) {
case 'ArrowDown':
case 'ArrowUp':
return prevent(e) || videoSound(e.code === 'ArrowUp')
case 'ArrowLeft':
case 'ArrowRight':
return prevent(e) || videoFf(e.code === 'ArrowRight')
case 'KeyF':
return prevent(e) || videoFs()
}
return
}
// Ctrl keys
if ((e.ctrlKey || e.metaKey) && !e.shiftKey) {
switch (e.code) {
case 'KeyC':
return prevent(e) || isRo() || cpPath()
case 'KeyH':
return prevent(e) || isRo() || helpToggle()
case 'KeyX':
return prevent(e) || isRo() || onCut()
case 'KeyR':
return prevent(e) || refresh()
case 'KeyV':
return prevent(e) || isRo() || ensureMove() || onPaste()
case 'Backspace':
return prevent(e) || isRo() || window.rm(e)
case 'KeyE':
return prevent(e) || isRo() || window.rename(e)
case 'KeyM':
return prevent(e) || isRo() || window.mkdirBtn()
case 'KeyU':
return prevent(e) || isRo() || manualUpload.click()
case 'Enter':
case 'ArrowRight':
return prevent(e) || dl(getASelected())
}
}
switch (e.code) {
case 'Tab':
case 'ArrowDown':
return prevent(e) || moveArrow(true)
case 'ArrowUp':
return prevent(e) || moveArrow(false)
case 'Enter':
case 'ArrowRight':
return prevent(e) || window.onClickLink()
case 'ArrowLeft':
return prevent(e) || prevPage(location.href + '../')
case 'PageDown':
case 'PageUp':
return prevent(e) || movePage(e.key === 'PageUp')
case 'Space':
return prevent(e) || movePage(e.shiftKey)
}
// text search
if (e.code.includes('Key') && !e.ctrlKey && !e.metaKey) {
typedPath += e.code.replace('Key', '').toLocaleLowerCase()
clearTimeout(typedToken)
typedToken = setTimeout(() => { typedPath = '' }, 1000)
const a = allA.find(el => el.innerText.toLocaleLowerCase().startsWith(typedPath)) || allA.find(el => el.innerText.toLocaleLowerCase().includes(typedPath))
if (!a) { return }
setCursorTo(a.innerText)
}
}, false)
function setTitle () {
pageH1.innerHTML = '<span>' + pageH1.innerText.split('/').join('/</span><span>') + '</span>'
}
function init () {
allA = Array.from(document.querySelectorAll('a.list-links'))
allImgs = allA.map(el => el.href).filter(isPic)
imgsIndex = softStatePushed = 0
const successRestore = setCursorTo(localStorage.getItem('last-selected' + window.extraPath + location.pathname))
if (!successRestore) {
const entries = table.querySelectorAll('.arrow-icon')
entries.length === 1 ? entries[0].classList.add('arrow-selected') : entries[1].classList.add('arrow-selected')
}
setTitle()
scrollToArrow()
console.log('browsed to ' + location.href)
if (cuts.length) {
const match = allA.filter(a => cuts.find(c => c === decode(a.href)))
match.forEach(m => m.classList.add('linkSelected'))
}
// restore editor if was queried
if (location.search.includes('?editor=')) {
const cleanURL = location.href.replace('?editor=', '')
const matchingA = allA.find(a => a.href === cleanURL)
padOn(matchingA)
}
}
init()
+469
View File
File diff suppressed because one or more lines are too long
Vendored
+88
View File
@@ -0,0 +1,88 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="theme-color" content="rgb(45,52,54)">
<meta name="msapplication-navbutton-color" content="rgb(45,52,54)">
<meta name="apple-mobile-web-app-capable" content="yes">
<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"}' />
<title>{{.Title}}</title>
<link href="data:image/svg+xml;base64,favicon_will_be_here" rel="icon" type="image/svg+xml" />
<style type="text/css">css_will_be_here</style>
<script>
window.ro = {{.Ro}}
window.extraPath = {{.ExtraPath}}.slice(0, -1)
window.onload = function () { js_will_be_here }
</script>
</head>
<body>
<div onclick="window.helpOff()" style="display: none;" id="help"><table id="helpTable"><tbody>
<tr><td>Arrows/Enter</td><td>browse files/folders and pictures</td></tr>
<tr><td>Ctrl/Meta + Enter</td><td>download selected item as archive</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 + Backspace</td><td>delete item</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 + 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 + click</td><td>download selected item as archive</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>drag-and-drop item on UI</td><td>move item</td></tr>
<tr><td>drag-and-drop external item</td><td>upload file/folders</td></tr>
<tr><td>any other letter</td><td>fuzzy search</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>
<textarea style="display: none;" id="text-editor"></textarea>
<div id="drop-grid"></div>
<input type="file" id="clickupload" style="display:none"/>
<h1 onclick="return titleClick(event)">.{{.Title}}</h1>
<div id="icHolder">
{{if not .Ro}}
<div style="display:none;" onclick="document.getElementById('clickupload').click()" class="ic icon-large-upload manualUp"></div>
<div onclick="window.displayPad()" class="ic icon-large-pad" title="Create TXT file"></div>
<div class="ic icon-large-folder" onclick="window.mkdirBtn()" title="Create Folder"></div>
{{end}}
</div>
<div id="pics" style="display:none;"> <img draggable="false" onclick="window.picsNav()" id="picsHolder" /></div>
<div id="video" style="display:none;">
<div onclick="window.videodl()" class="icon-dl" id="video-dl"></div>
<video controls id="videoHolder"> </video>
</div>
<div id="pdf" style="display:none;"> </div>
<table id="linkTable">
{{range .RowsFolders}}
<tr>
<td class="iconRow"><i ondblclick="return rm(event)" onclick="return rename(event)" class="btn icon icon-{{.Ext}} icon-blank"></i></td>
<td class="file-size"><code>{{.Size}}</code></td>
<td class="arrow"><div class="arrow-icon"></div></td>
<td class="display-name"><a class="list-links" oncontextmenu="return setCursorTo(event.target.innerText)" onclick="return onClickLink(event)" href="{{.Href}}">{{.Name}}</a></td>
</tr>
{{end}}
{{range .RowsFiles}}
<tr>
<td class="iconRow"><i ondblclick="return rm(event)" onclick="return rename(event)" class="btn icon icon-{{.Ext}} icon-blank"></i></td>
<td class="file-size"><code>{{.Size}}</code></td>
<td class="arrow"><div class="arrow-icon"></div></td>
<td class="display-name"><a class="list-links" oncontextmenu="return setCursorTo(event.target.innerText)" onclick="return onClickLink(event)" href="{{.Href}}">{{.Name}}</a></td>
</tr>
{{end}}
</table>
</body>
<div id="upBar" class="bar">
<span style="display: none;" class="barName" id="upBarName"></span>
<div style="display: none; background-color: green;" class="barPc" id="upBarPc">1%</div>
</div>
<div id="ok" class="notif icon-large-ok"></div>
<div id="sad" class="notif icon-large-sad-server"></div>
</html>