Compare commits

...
15 Commits
Author SHA1 Message Date
Pierre Dubouilh 18d44184c5 readme + ci 2023-07-08 15:43:08 +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
Pierre Dubouilh 382b2e3734 bump go & auto-release from CI 2022-09-17 13:13:02 +02:00
15 changed files with 1567 additions and 91 deletions
+1
View File
@@ -0,0 +1 @@
ui/** linguist-vendored
@@ -1,4 +1,4 @@
name: ci name: always
on: [push] on: [push]
jobs: jobs:
build: build:
@@ -6,12 +6,15 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Set up Go 1.18 - name: Set up Go
uses: actions/setup-go@v1 uses: actions/setup-go@v1
with: with:
go-version: 1.18 go-version: 1.19.1
id: go id: go
- name: deps
run: npm i -g standard
- name: Check out code into the Go module directory - name: Check out code into the Go module directory
uses: actions/checkout@v1 uses: actions/checkout@v1
with: with:
+25 -1
View File
@@ -14,6 +14,12 @@ jobs:
with: with:
submodules: true submodules: true
- name: Set up Go
uses: actions/setup-go@v1
with:
go-version: 1.19.1
id: go
- name: Set env - name: Set env
run: echo "GIT_TAG=`echo $(git describe --tags --abbrev=0)`" >> $GITHUB_ENV run: echo "GIT_TAG=`echo $(git describe --tags --abbrev=0)`" >> $GITHUB_ENV
@@ -29,7 +35,7 @@ jobs:
username: ${{ secrets.DOCKER_USER }} username: ${{ secrets.DOCKER_USER }}
password: ${{ secrets.DOCKER_PASSWORD }} password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push - name: Build and release on dockerhub
uses: docker/build-push-action@v2.7.0 uses: docker/build-push-action@v2.7.0
with: with:
file: support/build.Dockerfile file: support/build.Dockerfile
@@ -37,3 +43,21 @@ jobs:
platforms: linux/386,linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64 platforms: linux/386,linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64
tags: pldubouilh/gossa:latest,pldubouilh/gossa:${{github.ref_name}} tags: pldubouilh/gossa:latest,pldubouilh/gossa:${{github.ref_name}}
- name: Build all artifacts
run: make build-all
- name: "Release gh release versioned"
uses: ncipollo/release-action@58ae73b360456532aafd58ee170c045abbeaee37
with:
allowUpdates: true
artifacts: "builds/*"
token: ${{ secrets.GITHUB_TOKEN }}
- name: "Release gh release latest"
uses: ncipollo/release-action@58ae73b360456532aafd58ee170c045abbeaee37
with:
tag: latest
name: Latest
allowUpdates: true
artifacts: "builds/*"
token: ${{ secrets.GITHUB_TOKEN }}
+39
View File
@@ -0,0 +1,39 @@
name: rc
on:
push:
branches:
- 'rc/**'
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Set up Go
uses: actions/setup-go@v1
with:
go-version: 1.19.1
id: go
- name: Check out code into the Go module directory
uses: actions/checkout@v1
with:
submodules: true
- name: Run
run: make ci
- name: Build all artifacts
run: make build-all
- name: "Release gh release prerelease"
uses: ncipollo/release-action@58ae73b360456532aafd58ee170c045abbeaee37
with:
allowUpdates: true
prerelease: true
tag: "rc"
artifacts: "builds/*"
token: ${{ secrets.GITHUB_TOKEN }}
-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 FLAGS := -ldflags "-s -w" -trimpath
NOCGO := CGO_ENABLED=0 NOCGO := CGO_ENABLED=0
build: build::
go vet && go fmt go vet && go fmt
${NOCGO} go build ${FLAGS} -o gossa ${NOCGO} go build ${FLAGS} -o gossa
install: install::
sudo cp gossa /usr/local/bin sudo cp gossa /usr/local/bin
run: run::
./gossa -verb=true test-fixture ./gossa -verb=true test-fixture
run-ro: run-ro::
./gossa -verb=true -ro=true test-fixture ./gossa -verb=true -ro=true test-fixture
run-extra: run-extra::
./gossa -verb=true -prefix="/fancy-path/" -k=false -symlinks=true test-fixture ./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" echo "done"
test: test::
-@cd test-fixture && ln -s ../support .; true -@cd test-fixture && ln -s ../support .; true
go test -cover -c -tags testrunmain go test -cover -c -tags testrunmain
@@ -43,19 +46,19 @@ test:
# go tool cover -html all.out # go tool cover -html all.out
# go tool cover -func=all.out | grep main | grep '9.\..\%' # go tool cover -func=all.out | grep main | grep '9.\..\%'
watch: watch::
ls gossa.go gossa_test.go gossa-ui/* | entr -rc make build run ls gossa.go gossa_test.go ui/* | entr -rc make build run
watch-extra: watch-extra::
ls gossa.go gossa_test.go gossa-ui/* | entr -rc make build run-extra ls gossa.go gossa_test.go ui/* | entr -rc make build run-extra
watch-ro: watch-ro::
ls gossa.go gossa_test.go gossa-ui/* | entr -rc make build run-ro ls gossa.go gossa_test.go ui/* | entr -rc make build run-ro
watch-test: watch-test::
ls gossa.go gossa_test.go gossa-ui/* | entr -rc make 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=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=arm go build ${FLAGS} -o builds/gossa-linux-arm
${NOCGO} GOOS=linux GOARCH=arm64 go build ${FLAGS} -o builds/gossa-linux-arm64 ${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 ${NOCGO} GOOS=windows GOARCH=amd64 go build ${FLAGS} -o builds/gossa-windows.exe
sha256sum builds/* sha256sum builds/*
clean: clean::
-rm gossa rm -f gossa
-rm gossa-linux64 rm -f gossa-linux64
-rm gossa-linux-arm rm -f gossa-linux-arm
-rm gossa-linux-arm64 rm -f gossa-linux-arm64
-rm gossa-mac rm -f gossa-mac
-rm gossa-windows.exe rm -f gossa-windows.exe
Submodule gossa-ui deleted from 5ec0a804c9
+34 -16
View File
@@ -23,6 +23,29 @@ import (
"strings" "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 { type rpcCall struct {
Call string `json:"call"` Call string `json:"call"`
Args []string `json:"args"` 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()) href := url.PathEscape(el.Name())
name := el.Name()
if el.IsDir() && strings.HasPrefix(href, "/") { if el.IsDir() && strings.HasPrefix(href, "/") {
href = strings.Replace(href, "/", "", 1) href = strings.Replace(href, "/", "", 1)
} }
if el.IsDir() { 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 { } else {
sl := strings.Split(el.Name(), ".") sl := strings.Split(name, ".")
ext := strings.ToLower(sl[len(sl)-1]) 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() { func main() {
if flag.Parse(); len(flag.Args()) > 0 { if flag.Parse(); len(flag.Args()) == 1 {
rootPath = flag.Args()[0] rootPath = flag.Args()[0]
} else { } else {
fmt.Printf("\nusage: ./gossa ~/directory-to-share\n\n") fmt.Printf("\nusage: ./gossa [OPTIONS] ~/directory-to-share\n\n")
flag.PrintDefaults() flag.PrintDefaults()
os.Exit(1) os.Exit(1)
} }
@@ -246,16 +272,6 @@ func main() {
check(err) check(err)
server := &http.Server{Addr: *host + ":" + *port, Handler: handler} 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 { if !*ro {
http.HandleFunc(*extraPath+"rpc", rpc) http.HandleFunc(*extraPath+"rpc", rpc)
http.HandleFunc(*extraPath+"post", upload) http.HandleFunc(*extraPath+"post", upload)
@@ -264,7 +280,9 @@ func main() {
http.HandleFunc("/", doContent) http.HandleFunc("/", doContent)
handler = http.StripPrefix(*extraPath, http.FileServer(http.Dir(rootPath))) 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 { if err = server.ListenAndServe(); err != http.ErrServerClosed {
check(err) check(err)
} }
+9 -33
View File
@@ -3,48 +3,24 @@ package main
import ( import (
_ "embed" _ "embed"
"encoding/base64" "encoding/base64"
"flag"
"html/template" "html/template"
"strings" "strings"
) )
//go:embed gossa-ui/script.js //go:embed ui/script.js
var scriptJs string var scriptJs string
//go:embed gossa-ui/style.css //go:embed ui/style.css
var styleCss string var styleCss string
//go:embed gossa-ui/favicon.svg //go:embed ui/favicon.svg
var faviconSvg []byte var faviconSvg []byte
//go:embed gossa-ui/ui.tmpl //go:embed ui/ui.tmpl
var templateStr string var template0 string
// fill in template // fill in template
var templateCss = strings.Replace(templateStr, "css_will_be_here", styleCss, 1) var template1 = strings.Replace(template0, "css_will_be_here", styleCss, 1)
var templateCssJs = strings.Replace(templateCss, "js_will_be_here", scriptJs, 1) var template2 = strings.Replace(template1, "js_will_be_here", scriptJs, 1)
var templateCssJssIcon = strings.Replace(templateCssJs, "favicon_will_be_here", base64.StdEncoding.EncodeToString(faviconSvg), 2) var template3 = strings.Replace(template2, "favicon_will_be_here", base64.StdEncoding.EncodeToString(faviconSvg), 2)
var templateParsed, _ = template.New("").Parse(templateCssJssIcon) var templateParsed, _ = template.New("").Parse(template3)
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...)")
+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 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 * 🔍 files/directories browser & handler
* 📩 drag-and-drop uploader * 📩 drag-and-drop uploader
* 🥂 fast golang static server
* 💾 90s web UI that prints in milliseconds * 💾 90s web UI that prints in milliseconds
* 📸 video streaming & picture browser * 📸 video streaming, picture browser, pdf viewer
* ✍️ simple note editor * ✍️ simple note editor
* ⌨️ keyboard navigation * ⌨️ keyboard navigation
* 🚀 lightweight and dependency free codebase * 🚀 lightweight and dependency free codebase
* 🔒 >95% test coverage and reproducible builds * 🔒 >95% test coverage and reproducible builds
* 🥂 fast golang static server
* 💑 easy multi account setup, read-only mode * 💑 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. 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 ### usage
```sh ```sh
% ./gossa --help % ./gossa --help
@@ -46,9 +48,11 @@ release images are pushed to [dockerhub](https://hub.docker.com/r/pldubouilh/gos
```sh ```sh
# pull from dockerhub and run # pull from dockerhub and run
% mkdir ~/LocalDirToShare
% sudo docker run -v ~/LocalDirToShare:/shared -p 8001:8001 pldubouilh/gossa % 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. 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>