mirror of
https://github.com/pldubouilh/gossa.git
synced 2026-08-28 12:56:35 -04:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c517f7b7e1 | ||
|
|
3ff1c03d5f | ||
|
|
52188c1ae1 | ||
|
|
64c69716b9 | ||
|
|
83e610bbf4 | ||
|
|
1d0d7c3b8c | ||
|
|
989449f3c3 | ||
|
|
e192cf373a |
+1
-1
@@ -2,7 +2,7 @@ debug
|
||||
test
|
||||
gossa.go
|
||||
gossa
|
||||
gossa-linux
|
||||
gossa-linux64
|
||||
gossa-linux-arm
|
||||
gossa-linux-arm64
|
||||
gossa-mac
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
FROM alpine
|
||||
EXPOSE 8001
|
||||
COPY gossa /gossa
|
||||
ENTRYPOINT [ "/gossa", "-h", "0.0.0.0", "/shared" ]
|
||||
@@ -1,11 +1,12 @@
|
||||
build:
|
||||
make embed
|
||||
go build gossa.go
|
||||
CGO_ENABLED=0 go build gossa.go
|
||||
rm gossa.go
|
||||
|
||||
embed:
|
||||
echo "embedding css and js into binary"
|
||||
cp src/main.go gossa.go
|
||||
perl -pe 's/template_will_be_here/`cat src\/page.tmpl`/ge' -i gossa.go
|
||||
perl -pe 's/css_will_be_here/`cat src\/style.css`/ge' -i gossa.go
|
||||
perl -pe 's/js_will_be_here/`cat src\/script.js`/ge' -i gossa.go
|
||||
perl -pe 's/favicon_will_be_here/`base64 -w0 src\/favicon.png`/ge' -i gossa.go
|
||||
@@ -16,11 +17,11 @@ run:
|
||||
|
||||
ci:
|
||||
cd src && go vet && go fmt
|
||||
timeout 5 make run &
|
||||
cd src && sleep 1 && go test
|
||||
timeout 10 make run &
|
||||
cd src && sleep 5 && go test
|
||||
|
||||
ci-watch:
|
||||
ls src/* | entr -rc make ci
|
||||
ls src/* | entr -rc make ci
|
||||
|
||||
watch:
|
||||
ls src/* | entr -rc make run
|
||||
|
||||
@@ -5,17 +5,19 @@ gossa
|
||||
|
||||
[](https://travis-ci.org/pldubouilh/gossa)
|
||||
|
||||
🎶 A fast and simple webserver for your files. It's dependency-free and with under 250 lines for the server code, easily code-reviewable.
|
||||
🎶 A fast and simple webserver for your files, that's dependency-free and with under 240 lines for the server code, easily code-reviewable.
|
||||
|
||||
### features
|
||||
* browse through files/directories
|
||||
* upload with drag-and-drop
|
||||
* create new folders
|
||||
* move files with drag-and-drop and keyboard
|
||||
* move/rename/delete files
|
||||
* browse through pictures with a full-screen carousel
|
||||
* simple keyboard navigation/shortcuts
|
||||
* fast ; fills my 80MB/s AC wifi link
|
||||
|
||||
### built blobs
|
||||
built blobs are available on the [release page](https://github.com/pldubouilh/gossa/releases).
|
||||
|
||||
### run
|
||||
```sh
|
||||
# build
|
||||
@@ -26,12 +28,35 @@ make
|
||||
```
|
||||
|
||||
### keyboard shortcuts
|
||||
* Arrows/Enter browse throughout the files/directories and pictures
|
||||
* Ctrl/Meta + C copy URL to clipboard
|
||||
* Ctrl/Meta + D create a new directory
|
||||
* Ctrl/Meta + X cut selected path
|
||||
* Ctrl/Meta + V paste previously selected paths to directory
|
||||
* \<any letter\> search
|
||||
|shortcut | action|
|
||||
|-------------|-------------|
|
||||
|Arrows/Enter | browse through files/directories and pictures|
|
||||
|Ctrl/Meta + C | copy URL to clipboard|
|
||||
|Ctrl/Meta + E | rename file/folder|
|
||||
|Ctrl/Meta + Del | delete file/folder|
|
||||
|Ctrl/Meta + D | create a new directory|
|
||||
|Ctrl/Meta + X | cut selected path|
|
||||
|Ctrl/Meta + V | paste previously selected paths to directory|
|
||||
|\<any letter\> | search|
|
||||
|
||||
### built blobs
|
||||
built blobs are available on the [release page](https://github.com/pldubouilh/gossa/releases).
|
||||
### ui shortcuts
|
||||
|shortcut | action|
|
||||
| ------------- |-------------|
|
||||
|click new folder icon | create new folder|
|
||||
|click images icon | toggle image carousel|
|
||||
|click file icon | rename item|
|
||||
|double click file icon | delete item|
|
||||
|drag-and-drop item on UI | move item|
|
||||
|drag-and-drop external item | upload file/folders|
|
||||
|
||||
### using with docker
|
||||
a pretty short docker file is provided
|
||||
|
||||
```sh
|
||||
# build docker image
|
||||
make
|
||||
docker build -t gossa .
|
||||
|
||||
# run
|
||||
sudo docker run -v ~/ToShare:/shared -p 8001:8001 gossa
|
||||
```
|
||||
+40
-61
@@ -6,6 +6,7 @@ import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"html"
|
||||
"html/template"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
@@ -21,14 +22,23 @@ var host = flag.String("h", "127.0.0.1", "host to listen to")
|
||||
var port = flag.String("p", "8001", "port to listen to")
|
||||
var verb = flag.Bool("verb", true, "verbosity")
|
||||
var skipHidden = flag.Bool("k", true, "skip hidden files")
|
||||
|
||||
var initPath = ""
|
||||
var css = `css_will_be_here` // js will be embedded here
|
||||
var js = `js_will_be_here` // id. css
|
||||
var favicon = "data:image/png;base64,favicon_will_be_here" // id. b64 favicon
|
||||
var units = [8]string{"k", "M", "G", "T", "P", "E", "Z", "Y"}
|
||||
|
||||
var fs http.Handler
|
||||
var page, _ = template.New("pageTemplate").Parse(`template_will_be_here`)
|
||||
|
||||
type rowTemplate struct {
|
||||
Name string
|
||||
Href template.HTML
|
||||
Size string
|
||||
Ext string
|
||||
}
|
||||
|
||||
type pageTemplate struct {
|
||||
Title template.HTML
|
||||
RowsFiles []rowTemplate
|
||||
RowsFolders []rowTemplate
|
||||
}
|
||||
|
||||
type rpcCall struct {
|
||||
Call string `json:"call"`
|
||||
@@ -47,84 +57,52 @@ func logVerb(s ...interface{}) {
|
||||
}
|
||||
}
|
||||
|
||||
func sizeToString(bytes float64) string {
|
||||
if bytes == 0 {
|
||||
return "0"
|
||||
}
|
||||
var u = -1
|
||||
func sizeToString(bytes int64) string {
|
||||
units := [9]string{"B", "k", "M", "G", "T", "P", "E", "Z", "Y"}
|
||||
b := float64(bytes)
|
||||
u := 0
|
||||
for {
|
||||
bytes = bytes / 1024
|
||||
u++
|
||||
if bytes < 1024 {
|
||||
return strconv.FormatFloat(bytes, 'f', 1, 64) + units[u]
|
||||
if b < 1024 {
|
||||
return strconv.FormatFloat(b, 'f', 1, 64) + units[u]
|
||||
}
|
||||
b = b / 1024
|
||||
u++
|
||||
}
|
||||
}
|
||||
|
||||
func row(name string, href string, size float64, ext string) string {
|
||||
if strings.HasPrefix(href, "/") {
|
||||
href = strings.Replace(href, "/", "", 1)
|
||||
}
|
||||
|
||||
return `<tr>
|
||||
<td><i class="btn icon icon-` + strings.ToLower(ext) + ` icon-blank"></i></td>
|
||||
<td class="file-size"><code>` + sizeToString(size) + `</code></td>
|
||||
<td class="arrow"><i class="arrow-icon"></i></td>
|
||||
<td class="display-name"><a class="list-links" onclick="return onClickLink(event)" href="` + url.PathEscape(href) + `">` + name + `</a></td>
|
||||
</tr>`
|
||||
}
|
||||
|
||||
func replyList(w http.ResponseWriter, path string) {
|
||||
if !strings.HasSuffix(path, "/") {
|
||||
path += "/"
|
||||
}
|
||||
|
||||
var head = `<!doctype html><html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<title>` + html.EscapeString(path) + `</title>
|
||||
<link href="` + favicon + `" rel="icon" type="image/png"/>
|
||||
<script>window.onload = function(){` + js + `}</script>
|
||||
<style type="text/css">` + css + `</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="drop-grid"> Drop here to upload </div>
|
||||
<h1>.` + html.EscapeString(path) + `</h1>
|
||||
<div class="icHolder"><div style="display:none;" class="ic icon-large-images" onclick="window.picsToggle()"></div>
|
||||
<div class="ic icon-large-folder" onclick="window.mkdirBtn()"></div></div>
|
||||
<div id="pics" style="display:none;"> <div onclick="window.picsToggle()" id="picsToggleCinema"></div> <img onclick="window.picsNav()" id="picsHolder"/> <span id="picsLabel"></span> </div>
|
||||
<table>`
|
||||
|
||||
_files, err := ioutil.ReadDir(initPath + path)
|
||||
check(err)
|
||||
|
||||
p := pageTemplate{}
|
||||
if path != "/" {
|
||||
head += row("../", "../", 0, "folder")
|
||||
p.RowsFolders = append(p.RowsFolders, rowTemplate{"../", "../", "", "folder"})
|
||||
}
|
||||
|
||||
var dirs = ""
|
||||
var files = ""
|
||||
|
||||
for _, el := range _files {
|
||||
var name = el.Name()
|
||||
name := el.Name()
|
||||
href := url.PathEscape(name)
|
||||
if *skipHidden && strings.HasPrefix(name, ".") {
|
||||
continue
|
||||
}
|
||||
|
||||
if el.IsDir() && strings.HasPrefix(href, "/") {
|
||||
href = strings.Replace(href, "/", "", 1)
|
||||
}
|
||||
if el.IsDir() {
|
||||
dirs += row(name+"/", name, 0, "folder")
|
||||
p.RowsFolders = append(p.RowsFolders, rowTemplate{name + "/", template.HTML(href), "", "folder"})
|
||||
} else {
|
||||
var sl = strings.Split(name, ".")
|
||||
var ext = sl[len(sl)-1]
|
||||
files += row(name, name, float64(el.Size()), ext)
|
||||
sl := strings.Split(name, ".")
|
||||
ext := strings.ToLower(sl[len(sl)-1])
|
||||
p.RowsFiles = append(p.RowsFiles, rowTemplate{name, template.HTML(href), sizeToString(el.Size()), ext})
|
||||
}
|
||||
}
|
||||
|
||||
w.Write([]byte(head + dirs + files + `</table>
|
||||
<br><address><a href="https://github.com/pldubouilh/gossa">Gossa 🎶</a></address>
|
||||
<div id="progress" style="display:none;"><span id="dlBarName"></span><div id="dlBarPc">1%</div></div>
|
||||
</body></html>`))
|
||||
p.Title = template.HTML(html.EscapeString(path))
|
||||
page.Execute(w, p)
|
||||
}
|
||||
|
||||
func doContent(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -185,6 +163,8 @@ func rpc(w http.ResponseWriter, r *http.Request) {
|
||||
err = os.MkdirAll(payload.Args[0], os.ModePerm)
|
||||
} else if payload.Call == "mv" {
|
||||
err = os.Rename(payload.Args[0], payload.Args[1])
|
||||
} else if payload.Call == "rm" {
|
||||
err = os.Remove(payload.Args[0])
|
||||
}
|
||||
|
||||
logVerb("RPC", err, payload)
|
||||
@@ -204,7 +184,6 @@ func checkPath(p string) (string, error) {
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
if len(flag.Args()) == 0 {
|
||||
initPath = "."
|
||||
} else {
|
||||
@@ -215,11 +194,11 @@ func main() {
|
||||
initPath, err = filepath.Abs(initPath)
|
||||
check(err)
|
||||
|
||||
var hostString = *host + ":" + *port
|
||||
hostString := *host + ":" + *port
|
||||
fmt.Println("Gossa startig on directory " + initPath)
|
||||
fmt.Println("Listening on http://" + hostString)
|
||||
|
||||
var root = http.Dir(initPath)
|
||||
root := http.Dir(initPath)
|
||||
fs = http.StripPrefix("/", http.FileServer(root))
|
||||
|
||||
http.HandleFunc("/rpc", rpc)
|
||||
|
||||
+21
-4
@@ -77,7 +77,7 @@ func testDefaults(t *testing.T, url string) string {
|
||||
t.Fatal("error 中文 folder")
|
||||
}
|
||||
|
||||
if !strings.Contains(bodyStr, `<tr> <td><i class="btn icon icon-types icon-blank"></i></td> <td class="file-size"><code>0.2k</code></td> <td class="arrow"><i class="arrow-icon"></i></td> <td class="display-name"><a class="list-links" onclick="return onClickLink(event)" href="custom_mime_type.types">custom_mime_type.types</a></td> </tr>`) {
|
||||
if !strings.Contains(bodyStr, `<tr> <td><i ondblclick="return rm(event)" onclick="return rename(event)" class="btn icon icon-types icon-blank"></i></td> <td class="file-size"><code>0.2k</code></td> <td class="arrow"><i class="arrow-icon"></i></td> <td class="display-name"><a class="list-links" onclick="return onClickLink(event)" href="custom_mime_type.types">custom_mime_type.types</a></td> </tr>`) {
|
||||
t.Fatal("error row custom_mime_type")
|
||||
}
|
||||
|
||||
@@ -116,7 +116,7 @@ func TestGetFolder(t *testing.T) {
|
||||
}
|
||||
|
||||
bodyStr = testDefaults(t, "http://127.0.0.1:8001/")
|
||||
if !strings.Contains(bodyStr, `<tr> <td><i class="btn icon icon-folder icon-blank"></i></td> <td class="file-size"><code>0</code></td> <td class="arrow"><i class="arrow-icon"></i></td> <td class="display-name"><a class="list-links" onclick="return onClickLink(event)" href="AAA">AAA/</a></td> </tr>`) {
|
||||
if !strings.Contains(bodyStr, `<tr> <td><i ondblclick="return rm(event)" onclick="return rename(event)" class="btn icon icon-folder icon-blank"></i></td> <td class="file-size"><code>0</code></td> <td class="arrow"><i class="arrow-icon"></i></td> <td class="display-name"><a class="list-links" onclick="return onClickLink(event)" href="AAA">AAA/</a></td> </tr>`) {
|
||||
t.Fatal("error new folder created")
|
||||
}
|
||||
|
||||
@@ -140,7 +140,7 @@ func TestGetFolder(t *testing.T) {
|
||||
}
|
||||
|
||||
bodyStr = testDefaults(t, "http://127.0.0.1:8001/")
|
||||
if strings.Contains(bodyStr, `<tr> <td><i class="btn icon icon-folder icon-blank"></i></td> <td class="file-size"><code>0</code></td> <td class="arrow"><i class="arrow-icon"></i></td> <td class="display-name"><a class="list-links" onclick="return onClickLink(event)" href="AAA">AAA/</a></td> </tr>`) {
|
||||
if strings.Contains(bodyStr, `<tr> <td><i ondblclick="return rm(event)" onclick="return rename(event)" class="btn icon icon-folder icon-blank"></i></td> <td class="file-size"><code>0</code></td> <td class="arrow"><i class="arrow-icon"></i></td> <td class="display-name"><a class="list-links" onclick="return onClickLink(event)" href="AAA">AAA/</a></td> </tr>`) {
|
||||
t.Fatal("error folder moved")
|
||||
}
|
||||
|
||||
@@ -157,7 +157,7 @@ func TestGetFolder(t *testing.T) {
|
||||
}
|
||||
|
||||
bodyStr = testDefaults(t, "http://127.0.0.1:8001/")
|
||||
if !strings.Contains(bodyStr, `<tr> <td><i class="btn icon icon-하 하 icon-blank"></i></td> <td class="file-size"><code>0.0k</code></td> <td class="arrow"><i class="arrow-icon"></i></td> <td class="display-name"><a class="list-links" onclick="return onClickLink(event)" href="%E1%84%92%E1%85%A1%20%E1%84%92%E1%85%A1">하 하</a></td> </tr>`) {
|
||||
if !strings.Contains(bodyStr, `<tr> <td><i ondblclick="return rm(event)" onclick="return rename(event)" class="btn icon icon-하 하 icon-blank"></i></td> <td class="file-size"><code>0.0k</code></td> <td class="arrow"><i class="arrow-icon"></i></td> <td class="display-name"><a class="list-links" onclick="return onClickLink(event)" href="%E1%84%92%E1%85%A1%20%E1%84%92%E1%85%A1">하 하</a></td> </tr>`) {
|
||||
t.Fatal("error checking new file row")
|
||||
}
|
||||
|
||||
@@ -167,4 +167,21 @@ func TestGetFolder(t *testing.T) {
|
||||
if !strings.Contains(bodyStr, `err`) {
|
||||
t.Fatal("error not returned")
|
||||
}
|
||||
|
||||
// ~~~~~~~~~~~~~~~~~
|
||||
fmt.Println("\r\n~~~~~~~~~~ test rm rpc & cleanup")
|
||||
bodyStr = postJSON(t, "http://127.0.0.1:8001/rpc", `{"call":"rm","args":["/hols/AAA"]}`)
|
||||
if !strings.Contains(bodyStr, `ok`) {
|
||||
t.Fatal("error returned value")
|
||||
}
|
||||
|
||||
bodyStr = get(t, "http://127.0.0.1:8001/hols/AAA")
|
||||
if !strings.Contains(bodyStr, `error`) {
|
||||
t.Fatal("error not returned")
|
||||
}
|
||||
|
||||
bodyStr = postJSON(t, "http://127.0.0.1:8001/rpc", `{"call":"rm","args":["/하 하"]}`)
|
||||
if !strings.Contains(bodyStr, `ok`) {
|
||||
t.Fatal("error returned value")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<title>{{.Title}}</title>
|
||||
<link href="data:image/png;base64,favicon_will_be_here" rel="icon" type="image/png" />
|
||||
<script>window.onload = function () { js_will_be_here }</script>
|
||||
<style type="text/css">css_will_be_here</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="drop-grid"> Drop here to upload </div>
|
||||
|
||||
<h1>.{{.Title}}</h1>
|
||||
|
||||
<div class="icHolder">
|
||||
<div style="display:none;" class="ic icon-large-images" onclick="window.picsToggle()"></div>
|
||||
<div class="ic icon-large-folder" onclick="window.mkdirBtn()"></div>
|
||||
</div>
|
||||
|
||||
<div id="pics" style="display:none;">
|
||||
<div onclick="window.picsToggle()" id="picsToggleCinema"></div> <img onclick="window.picsNav()" id="picsHolder" />
|
||||
<span id="picsLabel"></span>
|
||||
</div>
|
||||
|
||||
<table>
|
||||
{{range .RowsFolders}}
|
||||
<tr>
|
||||
<td><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"><i class="arrow-icon"></i></td>
|
||||
<td class="display-name"><a class="list-links" onclick="return onClickLink(event)" href="{{.Href}}">{{.Name}}</a></td>
|
||||
</tr>
|
||||
{{end}}
|
||||
{{range .RowsFiles}}
|
||||
<tr>
|
||||
<td><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"><i class="arrow-icon"></i></td>
|
||||
<td class="display-name"><a class="list-links" onclick="return onClickLink(event)" href="{{.Href}}">{{.Name}}</a></td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</table>
|
||||
|
||||
<div id="progress" style="display:none;">
|
||||
<span id="dlBarName"></span>
|
||||
<div id="dlBarPc">1%</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
+183
-174
@@ -6,39 +6,83 @@ function cancelDefault (e) {
|
||||
e.stopPropagation()
|
||||
}
|
||||
|
||||
const warningMsg = () => 'Leaving will interrupt transfer?\n'
|
||||
const rmMsg = () => !confirm('Remove file?\n')
|
||||
|
||||
const barName = document.getElementById('dlBarName')
|
||||
const barPc = document.getElementById('dlBarPc')
|
||||
const barDiv = document.getElementById('progress')
|
||||
const upGrid = document.getElementById('drop-grid')
|
||||
const pics = document.getElementById('pics')
|
||||
const picsHolder = document.getElementById('picsHolder')
|
||||
const picsLabel = document.getElementById('picsLabel')
|
||||
|
||||
// helpers
|
||||
let allA
|
||||
let imgsIndex
|
||||
let allImgs
|
||||
const decode = a => decodeURIComponent(a).replace(location.origin, '')
|
||||
const getArrowSelected = () => document.querySelectorAll('i.arrow-selected')[0]
|
||||
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()
|
||||
|
||||
// Soft nav
|
||||
function browseTo (href) {
|
||||
fetch(href).then(r => r.text().then(t => {
|
||||
const parsed = new DOMParser().parseFromString(t, 'text/html')
|
||||
const table = parsed.querySelectorAll('table')[0].innerHTML
|
||||
document.body.querySelectorAll('table')[0].innerHTML = table
|
||||
|
||||
const title = parsed.head.querySelectorAll('title')[0].innerText
|
||||
// check if is current path - if so skip following
|
||||
if (document.head.querySelectorAll('title')[0].innerText !== title) {
|
||||
document.head.querySelectorAll('title')[0].innerText = title
|
||||
document.body.querySelectorAll('h1')[0].innerText = '.' + title
|
||||
history.pushState({}, '', encodeURI(title))
|
||||
}
|
||||
|
||||
init()
|
||||
}))
|
||||
}
|
||||
|
||||
window.onClickLink = e => {
|
||||
if (e.target.innerText.endsWith('/')) {
|
||||
storeLastArrowSrc(e.target.href)
|
||||
browseTo(e.target.href)
|
||||
return false
|
||||
} else if (picsOn(true, e.target.href)) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
const refresh = () => browseTo(location.href)
|
||||
const prevPage = () => browseTo(location.href + '../')
|
||||
window.onpopstate = prevPage
|
||||
|
||||
// RPC
|
||||
function rpcFs (call, args, cb) {
|
||||
console.log('RPC', call, args)
|
||||
const xhr = new window.XMLHttpRequest()
|
||||
const xhr = new XMLHttpRequest()
|
||||
xhr.open('POST', location.origin + '/rpc')
|
||||
xhr.setRequestHeader('Content-Type', 'application/json;charset=UTF-8')
|
||||
xhr.send(JSON.stringify({ call, args }))
|
||||
xhr.onload = cb
|
||||
}
|
||||
|
||||
const prependPath = (a) => a.startsWith('/') ? a : decodeURI(location.pathname) + a
|
||||
|
||||
// RPC Handlers
|
||||
const mkdirCall = (path, cb) => rpcFs('mkdirp', [prependPath(path)], cb)
|
||||
|
||||
const rmCall = (path1, cb) => rpcFs('rm', [prependPath(path1)], cb)
|
||||
const mvCall = (path1, path2, cb) => rpcFs('mv', [path1, path2], cb)
|
||||
|
||||
// Mkdir switch
|
||||
window.mkdirBtn = function () {
|
||||
const folder = window.prompt('New folder name', '')
|
||||
// File upload
|
||||
let totalDone = 0
|
||||
let totalUploads = 0
|
||||
let totalUploadsSize = 0
|
||||
let totalUploadedSize = []
|
||||
|
||||
if (!folder) {
|
||||
return
|
||||
} else if (checkDupes(folder)) {
|
||||
return window.alert('Name already already exists')
|
||||
}
|
||||
|
||||
mkdirCall(folder, refresh)
|
||||
}
|
||||
|
||||
function warning (e) {
|
||||
return 'Leaving will interrupt transfer\nAre you sure you want to leave?'
|
||||
}
|
||||
const dupe = test => allA.find(a => a.innerText.replace('/', '') === test)
|
||||
const isDupe = t => dupe(t) ? alert(t + ' already already exists') || true : false
|
||||
|
||||
function shouldRefresh () {
|
||||
totalDone += 1
|
||||
@@ -54,19 +98,6 @@ function shouldRefresh () {
|
||||
}
|
||||
}
|
||||
|
||||
const checkDupes = test => allA.find(a => a.innerText.replace('/', '') === test)
|
||||
|
||||
const barName = document.getElementById('dlBarName')
|
||||
|
||||
const barPc = document.getElementById('dlBarPc')
|
||||
|
||||
const barDiv = document.getElementById('progress')
|
||||
|
||||
let totalDone = 0
|
||||
let totalUploads = 0
|
||||
let totalUploadsSize = 0
|
||||
let totalUploadedSize = []
|
||||
|
||||
function updatePercent (ev) {
|
||||
totalUploadedSize[ev.target.id] = ev.loaded
|
||||
const ttlDone = totalUploadedSize.reduce((s, x) => s + x)
|
||||
@@ -77,17 +108,17 @@ function updatePercent (ev) {
|
||||
|
||||
function postFile (file, path) {
|
||||
path = decodeURI(location.pathname).slice(0, -1) + path
|
||||
window.onbeforeunload = warning
|
||||
window.onbeforeunload = warningMsg
|
||||
|
||||
barDiv.style.display = 'block'
|
||||
totalUploads += 1
|
||||
totalUploadsSize += file.size
|
||||
barName.innerText = totalUploads > 1 ? totalUploads + ' files' : file.name
|
||||
|
||||
const formData = new window.FormData()
|
||||
const formData = new FormData()
|
||||
formData.append(file.name, file)
|
||||
|
||||
const xhr = new window.XMLHttpRequest()
|
||||
const xhr = new XMLHttpRequest()
|
||||
xhr.open('POST', location.origin + '/post')
|
||||
xhr.setRequestHeader('gossa-path', encodeURIComponent(path))
|
||||
xhr.upload.addEventListener('load', shouldRefresh)
|
||||
@@ -96,15 +127,12 @@ function postFile (file, path) {
|
||||
xhr.send(formData)
|
||||
}
|
||||
|
||||
const parseDomFolder = f => {
|
||||
f.createReader().readEntries(e => e.forEach(i => parseDomItem(i)))
|
||||
}
|
||||
const parseDomFolder = f => f.createReader().readEntries(e => e.forEach(i => parseDomItem(i)))
|
||||
|
||||
function parseDomItem (domFile, shoudCheckDupes) {
|
||||
if (shoudCheckDupes && checkDupes(domFile.name)) {
|
||||
return window.alert(domFile.name + ' already exists !')
|
||||
if (shoudCheckDupes && isDupe(domFile.name)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (domFile.isFile) {
|
||||
domFile.file(f => postFile(f, domFile.fullPath))
|
||||
} else {
|
||||
@@ -116,7 +144,7 @@ function parseDomItem (domFile, shoudCheckDupes) {
|
||||
|
||||
function pushEntry (entry) {
|
||||
if (!entry.webkitGetAsEntry && !entry.getAsEntry) {
|
||||
return window.alert('Unsupported browser ! Please update to chrome/firefox.')
|
||||
return alert('Unsupported browser ! Please update to chrome/firefox.')
|
||||
} else {
|
||||
entry = entry.webkitGetAsEntry() || entry.getAsEntry()
|
||||
}
|
||||
@@ -135,9 +163,12 @@ const setBackgroundLinks = t => { t.style.backgroundColor = 'rgba(123, 123, 123,
|
||||
|
||||
const getLink = e => e.target.parentElement.querySelectorAll('a.list-links')[0]
|
||||
|
||||
const upGrid = document.getElementById('drop-grid')
|
||||
upGrid.ondragleave = e => {
|
||||
cancelDefault(e)
|
||||
upGrid.style.display = 'none'
|
||||
}
|
||||
|
||||
document.ondragenter = (e) => {
|
||||
document.ondragenter = e => {
|
||||
if (isPicMode()) { return }
|
||||
cancelDefault(e)
|
||||
|
||||
@@ -155,18 +186,15 @@ document.ondragenter = (e) => {
|
||||
}
|
||||
}
|
||||
|
||||
upGrid.ondragleave = (e) => {
|
||||
cancelDefault(e)
|
||||
upGrid.style.display = 'none'
|
||||
}
|
||||
document.ondragend = e => resetBackgroundLinks()
|
||||
|
||||
document.ondragover = (e) => {
|
||||
document.ondragover = e => {
|
||||
cancelDefault(e)
|
||||
return false
|
||||
}
|
||||
|
||||
// Handle drop - upload or move
|
||||
document.ondrop = (e) => {
|
||||
document.ondrop = e => {
|
||||
cancelDefault(e)
|
||||
upGrid.style.display = 'none'
|
||||
resetBackgroundLinks()
|
||||
@@ -186,16 +214,46 @@ document.ondrop = (e) => {
|
||||
return false
|
||||
}
|
||||
|
||||
const getArrowSelected = () => document.querySelectorAll('i.arrow-selected')[0]
|
||||
|
||||
function getASelected () {
|
||||
const dest = getArrowSelected()
|
||||
return !dest ? false : dest.parentElement.parentElement.querySelectorAll('a')[0]
|
||||
// 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.parentElement.parentElement.querySelector('a')
|
||||
|
||||
window.rm = e => {
|
||||
clearTimeout(window.clickToken)
|
||||
const path = e.key ? getASelected().href : getBtnA(e).pathname
|
||||
rmMsg() || rmCall(decode(path), refresh)
|
||||
}
|
||||
|
||||
window.rename = (e, commit) => {
|
||||
clearTimeout(window.clickToken)
|
||||
|
||||
if (!commit) {
|
||||
window.clickToken = setTimeout(window.rename, 300, e, true)
|
||||
return
|
||||
}
|
||||
|
||||
const orig = e.key ? getASelected().innerText : getBtnA(e).innerText
|
||||
const chg = prompt('rename to', orig)
|
||||
if (chg && !isDupe(chg)) {
|
||||
mvCall(prependPath(orig), prependPath(chg), refresh)
|
||||
}
|
||||
}
|
||||
|
||||
// Keyboard Arrow
|
||||
const storeLastArrowSrc = src => localStorage.setItem('last-selected' + location.href, src)
|
||||
|
||||
function scrollToArrow () {
|
||||
const pos = getArrowSelected().getBoundingClientRect()
|
||||
window.scrollTo(0, pos.y)
|
||||
if (pos.top < 0 || pos.bottom > window.innerHeight) {
|
||||
setTimeout(scrollTo, 50, 0, pos.y)
|
||||
}
|
||||
}
|
||||
|
||||
function clearArrowSelected () {
|
||||
@@ -222,8 +280,6 @@ function restoreCursorPos () {
|
||||
scrollToArrow()
|
||||
}
|
||||
|
||||
const storeLastArrowSrc = src => localStorage.setItem('last-selected' + location.href, src)
|
||||
|
||||
function moveArrow (down) {
|
||||
const all = Array.from(document.querySelectorAll('i.arrow-icon'))
|
||||
let i = all.findIndex(el => el.classList.contains('arrow-selected'))
|
||||
@@ -242,96 +298,49 @@ function moveArrow (down) {
|
||||
const itemPos = all[i].getBoundingClientRect()
|
||||
|
||||
if (i === 0) {
|
||||
window.scrollTo(0, 0)
|
||||
scrollTo(0, 0)
|
||||
} else if (i === all.length - 1) {
|
||||
window.scrollTo(0, document.documentElement.scrollHeight)
|
||||
scrollTo(0, document.documentElement.scrollHeight)
|
||||
} else if (itemPos.top < 0) {
|
||||
window.scrollBy(0, -200)
|
||||
scrollBy(0, -200)
|
||||
} else if (itemPos.bottom > window.innerHeight) {
|
||||
window.scrollBy(0, 200)
|
||||
scrollBy(0, 200)
|
||||
}
|
||||
}
|
||||
|
||||
const refresh = () => browseTo(location.href)
|
||||
|
||||
const prevPage = () => browseTo(location.href + '../')
|
||||
|
||||
window.onpopstate = prevPage
|
||||
|
||||
function browseTo (href) {
|
||||
window.fetch(href).then(r => r.text().then(t => {
|
||||
const parsed = new window.DOMParser().parseFromString(t, 'text/html')
|
||||
const table = parsed.querySelectorAll('table')[0].innerHTML
|
||||
document.body.querySelectorAll('table')[0].innerHTML = table
|
||||
|
||||
const title = parsed.head.querySelectorAll('title')[0].innerText
|
||||
// check if is current path - if so skip following
|
||||
if (document.head.querySelectorAll('title')[0].innerText !== title) {
|
||||
document.head.querySelectorAll('title')[0].innerText = title
|
||||
document.body.querySelectorAll('h1')[0].innerText = '.' + title
|
||||
window.history.pushState({}, '', window.encodeURI(title))
|
||||
}
|
||||
|
||||
init()
|
||||
}))
|
||||
}
|
||||
|
||||
function cpPath () {
|
||||
var t = document.createElement('textarea')
|
||||
t.value = getASelected().href
|
||||
document.body.appendChild(t)
|
||||
t.select()
|
||||
document.execCommand('copy')
|
||||
document.body.removeChild(t)
|
||||
}
|
||||
|
||||
const pics = document.getElementById('pics')
|
||||
const picsHolder = document.getElementById('picsHolder')
|
||||
const picsLabel = document.getElementById('picsLabel')
|
||||
|
||||
// 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)
|
||||
|
||||
let imgsIndex
|
||||
let allImgs
|
||||
|
||||
function setImage (src) {
|
||||
src = src || allImgs[imgsIndex]
|
||||
picsLabel.innerText = src.split('/').pop()
|
||||
function setImage () {
|
||||
const src = allImgs[imgsIndex]
|
||||
picsHolder.src = src
|
||||
picsLabel.innerText = src.split('/').pop()
|
||||
storeLastArrowSrc(src)
|
||||
restoreCursorPos()
|
||||
}
|
||||
|
||||
function picsOn (ifImgSelected, href) {
|
||||
href = href || getASelected().href
|
||||
|
||||
if (isPicMode()) {
|
||||
return false
|
||||
} else if (ifImgSelected && !isPic(href)) {
|
||||
if (isPicMode() || (ifImgSelected && !isPic(href))) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (isPic(href)) {
|
||||
imgsIndex = allImgs.findIndex(el => el.includes(href))
|
||||
setImage()
|
||||
} else {
|
||||
setImage(picsHolder.src)
|
||||
}
|
||||
|
||||
setImage()
|
||||
pics.style.display = 'flex'
|
||||
return true
|
||||
}
|
||||
|
||||
function picsToggle () {
|
||||
if (!isPicMode()) {
|
||||
picsOn()
|
||||
} else {
|
||||
pics.style.display = 'none'
|
||||
restoreCursorPos()
|
||||
}
|
||||
}
|
||||
const picsOff = () => { pics.style.display = 'none' }
|
||||
|
||||
window.picsToggle = () => isPicMode() ? picsOff() : picsOn()
|
||||
|
||||
function picsNav (down) {
|
||||
if (!isPicMode()) { return false }
|
||||
@@ -346,19 +355,8 @@ function picsNav (down) {
|
||||
return true
|
||||
}
|
||||
|
||||
let allA
|
||||
let typedPath = ''
|
||||
let typedToken = null
|
||||
|
||||
function setCursorToClosestTyped () {
|
||||
const a = allA.find(el => el.innerText.toLocaleLowerCase().startsWith(typedPath))
|
||||
if (!a) { return }
|
||||
storeLastArrowSrc(a.href)
|
||||
restoreCursorPos()
|
||||
}
|
||||
|
||||
// Paste handler
|
||||
let cuts = []
|
||||
|
||||
function onPaste () {
|
||||
if (!cuts.length) { return refresh() }
|
||||
const root = cuts.pop()
|
||||
@@ -369,77 +367,92 @@ function onPaste () {
|
||||
mvCall(root, dest + filename, onPaste)
|
||||
}
|
||||
|
||||
// dark mode
|
||||
const darkMode = 'html, a { background-color: #2d3436; color: #dfe6e9; } .arrow, .icon-large-images { filter: invert(100%) !important; }'
|
||||
const extraCss = document.createElement('style')
|
||||
document.body.appendChild(extraCss);
|
||||
|
||||
const currentMode = () => localStorage.getItem('colorMode')
|
||||
const setMode = () => { extraCss.innerHTML = currentMode() === 'dark' ? darkMode : '' }
|
||||
const toggleBWMode = () => localStorage.setItem('colorMode', currentMode() === 'dark' ? 'white' : 'dark') || setMode()
|
||||
setMode()
|
||||
|
||||
// Kb handler
|
||||
let typedPath = ''
|
||||
let typedToken = null
|
||||
|
||||
function cpPath () {
|
||||
var t = document.createElement('textarea')
|
||||
t.value = getASelected().href
|
||||
document.body.appendChild(t)
|
||||
t.select()
|
||||
document.execCommand('copy')
|
||||
document.body.removeChild(t)
|
||||
}
|
||||
|
||||
function setCursorToClosestTyped () {
|
||||
const a = allA.find(el => el.innerText.toLocaleLowerCase().startsWith(typedPath))
|
||||
if (!a) { return }
|
||||
storeLastArrowSrc(a.href)
|
||||
restoreCursorPos()
|
||||
}
|
||||
|
||||
document.body.addEventListener('keydown', e => {
|
||||
switch (e.code) {
|
||||
case 'Tab':
|
||||
case 'ArrowDown':
|
||||
e.preventDefault()
|
||||
return picsNav(true) || moveArrow(true)
|
||||
return prevent(e) || picsNav(true) || moveArrow(true)
|
||||
|
||||
case 'ArrowUp':
|
||||
e.preventDefault()
|
||||
return picsNav(false) || moveArrow(false)
|
||||
return prevent(e) || picsNav(false) || moveArrow(false)
|
||||
|
||||
case 'Enter':
|
||||
case 'ArrowRight':
|
||||
e.preventDefault()
|
||||
return picsOn(true) || picsNav(true) || getASelected().click()
|
||||
return prevent(e) || picsOn(true) || picsNav(true) || getASelected().click()
|
||||
|
||||
case 'ArrowLeft':
|
||||
e.preventDefault()
|
||||
return picsNav(false) || prevPage()
|
||||
return prevent(e) || picsNav(false) || prevPage()
|
||||
|
||||
case 'Escape':
|
||||
if (isPicMode()) {
|
||||
e.preventDefault()
|
||||
return picsToggle()
|
||||
}
|
||||
return prevent(e) || resetBackgroundLinks() || picsOff()
|
||||
}
|
||||
|
||||
// Ctrl keys
|
||||
if (e.ctrlKey || e.metaKey) {
|
||||
switch (e.code) {
|
||||
case 'KeyD':
|
||||
e.preventDefault()
|
||||
return isPicMode() || window.mkdirBtn()
|
||||
|
||||
case 'KeyC':
|
||||
e.preventDefault()
|
||||
return isPicMode() || cpPath()
|
||||
return prevent(e) || isPicMode() || cpPath()
|
||||
|
||||
case 'KeyX':
|
||||
e.preventDefault()
|
||||
const x = decodeURIComponent(getASelected().href).replace(location.origin, '')
|
||||
cuts.push(prependPath(x))
|
||||
return false
|
||||
cuts.push(prependPath(decode(getASelected().href)))
|
||||
return prevent(e) || false
|
||||
|
||||
case 'KeyV':
|
||||
e.preventDefault()
|
||||
return onPaste()
|
||||
return prevent(e) || onPaste()
|
||||
|
||||
case 'Backspace':
|
||||
return prevent(e) || isPicMode() || window.rm(e)
|
||||
|
||||
case 'KeyE':
|
||||
return prevent(e) || isPicMode() || window.rename(e)
|
||||
|
||||
case 'KeyD':
|
||||
return prevent(e) || isPicMode() || window.mkdirBtn()
|
||||
|
||||
case 'KeyB':
|
||||
return prevent(e) || toggleBWMode()
|
||||
}
|
||||
}
|
||||
|
||||
// Any other key, for text search
|
||||
// text search
|
||||
if (e.code.includes('Key')) {
|
||||
typedPath += e.code.replace('Key', '').toLocaleLowerCase()
|
||||
window.clearTimeout(typedToken)
|
||||
clearTimeout(typedToken)
|
||||
typedToken = setTimeout(() => { typedPath = '' }, 1000)
|
||||
setCursorToClosestTyped()
|
||||
}
|
||||
}, false)
|
||||
|
||||
window.onClickLink = e => {
|
||||
if (e.target.innerText.endsWith('/')) {
|
||||
storeLastArrowSrc(e.target.href)
|
||||
browseTo(e.target.href)
|
||||
return false
|
||||
} else if (picsOn(true, e.target.href)) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
function init () {
|
||||
allA = Array.from(document.querySelectorAll('a.list-links'))
|
||||
allImgs = allA.map(el => el.href).filter(isPic)
|
||||
@@ -449,8 +462,4 @@ function init () {
|
||||
restoreCursorPos()
|
||||
console.log('Browsed to ' + location.href)
|
||||
}
|
||||
|
||||
init()
|
||||
|
||||
window.picsToggle = picsToggle
|
||||
window.picsNav = () => picsNav(true)
|
||||
|
||||
+8
-4
@@ -1,9 +1,16 @@
|
||||
a {
|
||||
text-decoration: none;
|
||||
background-color: transparent !important;
|
||||
/* border-bottom: .01em solid #dfe6e9; */
|
||||
}
|
||||
|
||||
.icon {
|
||||
display: block;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
zoom: 1.2;
|
||||
margin: 1px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.arrow-icon {
|
||||
@@ -74,7 +81,6 @@ h1 {
|
||||
}
|
||||
|
||||
#progress {
|
||||
background-color: white;
|
||||
width: 99%;
|
||||
left: 0.5%;
|
||||
right: 0.5%;
|
||||
@@ -82,7 +88,6 @@ h1 {
|
||||
bottom: 0px;
|
||||
padding-bottom: 10px;
|
||||
max-height: 50%;
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
@@ -117,10 +122,9 @@ h1 {
|
||||
text-align: center;
|
||||
font-size: 4em;
|
||||
font-family: Helvetica;
|
||||
background-color: white;
|
||||
color: green;
|
||||
opacity: 0.8;
|
||||
backdrop-filter: grayscale(100%);
|
||||
background-color: rgba(123, 123, 123, 0.2)
|
||||
}
|
||||
|
||||
#pics {
|
||||
|
||||
Reference in New Issue
Block a user