mirror of
https://github.com/pldubouilh/gossa.git
synced 2026-08-28 12:56:35 -04:00
Compare commits
17
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
698f3ffaf8 | ||
|
|
dc1011bb5f | ||
|
|
5096f46ba4 | ||
|
|
f77b10d3de | ||
|
|
ccd6ae2bea | ||
|
|
858d30be9b | ||
|
|
0b586c5df9 | ||
|
|
d913176daa | ||
|
|
beac60f3d8 | ||
|
|
da7f60d5fc | ||
|
|
39d2d4b13f | ||
|
|
ec7168e718 | ||
|
|
95f01e47dd | ||
|
|
c9266d8849 | ||
|
|
e257d1145d | ||
|
|
698a418e5d | ||
|
|
e9eb3bf45b |
@@ -1 +0,0 @@
|
||||
ui/** linguist-vendored
|
||||
@@ -1,20 +0,0 @@
|
||||
name: always
|
||||
on: [push]
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.23.0
|
||||
id: go
|
||||
|
||||
- name: Run
|
||||
run: make ci
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
name: deploy
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set env
|
||||
run: echo "GIT_TAG=`echo $(git describe --tags --abbrev=0)`" >> $GITHUB_ENV
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USER }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Build and release on dockerhub
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
file: support/build.Dockerfile
|
||||
push: true
|
||||
platforms: linux/386,linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64
|
||||
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@v1
|
||||
with:
|
||||
allowUpdates: true
|
||||
artifacts: "builds/*"
|
||||
bodyFile: "builds/buildout"
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: "Release gh release latest"
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
tag: latest
|
||||
name: Latest
|
||||
allowUpdates: true
|
||||
artifacts: "builds/*"
|
||||
bodyFile: "builds/buildout"
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -0,0 +1,22 @@
|
||||
name: ci
|
||||
on: [push, pull_request]
|
||||
jobs:
|
||||
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
- name: Set up Go 1.12
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.12
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v1
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- name: Run
|
||||
run: make ci
|
||||
+1
-8
@@ -4,10 +4,7 @@ gossa-linux-arm
|
||||
gossa-linux-arm64
|
||||
gossa-mac
|
||||
gossa-windows.exe
|
||||
builds
|
||||
gossa.test
|
||||
|
||||
*.out
|
||||
.vscode
|
||||
test-fixture/*
|
||||
test-fixture/*/*
|
||||
@@ -41,8 +38,4 @@ test-fixture/*/*
|
||||
!test-fixture/fancy-path
|
||||
!test-fixture/fancy-path/*
|
||||
!test-fixture/ext/*
|
||||
!test-fixture/ext
|
||||
!test-fixture/.some-hidden-file
|
||||
!test-fixture/hols/.hidden-folder
|
||||
!test-fixture/hols/.hidden-folder/some-file
|
||||
!test-fixture/compress/hols-sym
|
||||
!test-fixture/ext
|
||||
@@ -0,0 +1,4 @@
|
||||
[submodule "gossa-ui"]
|
||||
path = gossa-ui
|
||||
url = https://github.com/pldubouilh/gossa-ui
|
||||
branch = master
|
||||
@@ -1,75 +1,56 @@
|
||||
FLAGS := -trimpath
|
||||
NOCGO := CGO_ENABLED=0
|
||||
|
||||
build::
|
||||
build:
|
||||
cp src/gossa.go gossa.go
|
||||
make -C gossa-ui/
|
||||
go vet && go fmt
|
||||
${NOCGO} go build ${FLAGS} -o gossa
|
||||
CGO_ENABLED=0 go build gossa.go
|
||||
rm gossa.go
|
||||
|
||||
install::
|
||||
sudo cp gossa /usr/local/bin
|
||||
|
||||
run::
|
||||
run:
|
||||
make build
|
||||
./gossa -verb=true test-fixture
|
||||
|
||||
run-ro::
|
||||
./gossa -verb=true -ro=true test-fixture
|
||||
|
||||
run-extra::
|
||||
run-extra:
|
||||
make build
|
||||
./gossa -verb=true -prefix="/fancy-path/" -k=false -symlinks=true test-fixture
|
||||
|
||||
ci:: build-all test
|
||||
echo "done"
|
||||
ci:
|
||||
-@cd test-fixture && ln -s ../support .
|
||||
make build
|
||||
timeout 15 make run &
|
||||
sleep 16 && timeout 15 make run-extra &
|
||||
cp src/gossa_test.go . && go test
|
||||
rm gossa_test.go
|
||||
|
||||
test::
|
||||
-@cd test-fixture && ln -s ../support .; true
|
||||
go test -cover -c -tags testrunmain
|
||||
watch:
|
||||
ls src/* gossa-ui/* | entr -rc make run
|
||||
|
||||
timeout -s SIGINT 3 ./gossa.test -test.coverprofile=normal.out -test.run '^TestRunMain' -verb=true test-fixture &
|
||||
sleep 2
|
||||
go test -run TestNormal
|
||||
sleep 1
|
||||
watch-extra:
|
||||
ls src/* gossa-ui/* | entr -rc make run-extra
|
||||
|
||||
timeout -s SIGINT 3 ./gossa.test -test.coverprofile=extra.out -test.run '^TestRunMain' -prefix='/fancy-path/' -k=false -symlinks=true test-fixture &
|
||||
sleep 2
|
||||
go test -run TestExtra
|
||||
sleep 1
|
||||
watch-ci:
|
||||
ls src/* gossa-ui/* | entr -rc make ci
|
||||
|
||||
timeout -s SIGINT 3 ./gossa.test -test.coverprofile=ro.out -test.run '^TestRunMain' -ro=true test-fixture &
|
||||
sleep 2
|
||||
go test -run TestRo
|
||||
sleep 1
|
||||
|
||||
# gocovmerge ro.out extra.out normal.out > all.out
|
||||
# go tool cover -html all.out
|
||||
# go tool cover -func=all.out | grep main | grep '9.\..\%'
|
||||
|
||||
watch::
|
||||
ls gossa.go gossa_test.go ui/* | entr -rc make build run
|
||||
|
||||
watch-extra::
|
||||
ls gossa.go gossa_test.go ui/* | entr -rc make build run-extra
|
||||
|
||||
watch-ro::
|
||||
ls gossa.go gossa_test.go ui/* | entr -rc make build run-ro
|
||||
|
||||
watch-test::
|
||||
ls gossa.go gossa_test.go ui/* | entr -rc make test
|
||||
|
||||
build-all:: build
|
||||
go version
|
||||
${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
|
||||
${NOCGO} GOOS=darwin GOARCH=amd64 go build ${FLAGS} -o builds/gossa-mac-x64
|
||||
${NOCGO} GOOS=darwin GOARCH=arm64 go build ${FLAGS} -o builds/gossa-mac-arm64
|
||||
${NOCGO} GOOS=windows GOARCH=amd64 go build ${FLAGS} -o builds/gossa-windows.exe
|
||||
sha256sum builds/* | tee builds/buildout
|
||||
|
||||
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
|
||||
build-all:
|
||||
cp src/gossa.go gossa.go
|
||||
make -C gossa-ui/
|
||||
env CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build gossa.go
|
||||
mv gossa gossa-linux64
|
||||
env GOOS=linux GOARCH=arm go build gossa.go
|
||||
mv gossa gossa-linux-arm
|
||||
env GOOS=linux GOARCH=arm64 go build gossa.go
|
||||
mv gossa gossa-linux-arm64
|
||||
env GOOS=darwin GOARCH=amd64 go build gossa.go
|
||||
mv gossa gossa-mac
|
||||
env GOOS=windows GOARCH=amd64 go build gossa.go
|
||||
mv gossa.exe gossa-windows.exe
|
||||
rm gossa.go
|
||||
|
||||
clean:
|
||||
-rm gossa.go
|
||||
-rm gossa_test.go
|
||||
-rm gossa
|
||||
-rm gossa-linux64
|
||||
-rm gossa-linux-arm
|
||||
-rm gossa-linux-arm64
|
||||
-rm gossa-mac
|
||||
-rm gossa-windows.exe
|
||||
|
||||
Submodule
+1
Submodule gossa-ui added at cc8fff1e26
@@ -1,291 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"compress/gzip"
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"html"
|
||||
"html/template"
|
||||
"io"
|
||||
"io/fs"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type rowTemplate struct {
|
||||
Name string
|
||||
Href template.URL
|
||||
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"`
|
||||
}
|
||||
|
||||
var rootPath = ""
|
||||
var handler http.Handler
|
||||
|
||||
func check(e error) {
|
||||
if e != nil {
|
||||
panic(e)
|
||||
}
|
||||
}
|
||||
|
||||
func exitPath(w http.ResponseWriter, s ...interface{}) {
|
||||
if r := recover(); r != nil {
|
||||
log.Println("error", s, r)
|
||||
w.WriteHeader(500)
|
||||
w.Write([]byte("error"))
|
||||
} else if *verb {
|
||||
log.Println(s...)
|
||||
}
|
||||
}
|
||||
|
||||
func humanize(bytes int64) string {
|
||||
b := float64(bytes)
|
||||
u := 0
|
||||
for {
|
||||
if b < 1024 {
|
||||
return strconv.FormatFloat(b, 'f', 1, 64) + [9]string{"B", "k", "M", "G", "T", "P", "E", "Z", "Y"}[u]
|
||||
}
|
||||
b = b / 1024
|
||||
u++
|
||||
}
|
||||
}
|
||||
|
||||
func replyList(w http.ResponseWriter, r *http.Request, fullPath string, path string) {
|
||||
files, err := os.ReadDir(fullPath)
|
||||
check(err)
|
||||
sort.Slice(files, func(i, j int) bool { return strings.ToLower(files[i].Name()) < strings.ToLower(files[j].Name()) })
|
||||
|
||||
if !strings.HasSuffix(path, "/") {
|
||||
path += "/"
|
||||
}
|
||||
|
||||
title := "/" + strings.TrimPrefix(path, *extraPath)
|
||||
p := pageTemplate{}
|
||||
if path != *extraPath {
|
||||
p.RowsFolders = append(p.RowsFolders, rowTemplate{"../", "../", "", "folder"})
|
||||
}
|
||||
p.ExtraPath = template.HTML(html.EscapeString(*extraPath))
|
||||
p.Ro = *ro
|
||||
p.Title = template.HTML(html.EscapeString(title))
|
||||
|
||||
for _, el := range files {
|
||||
info, errInfo := el.Info()
|
||||
el, err := os.Stat(fullPath + "/" + el.Name())
|
||||
if err != nil || errInfo != nil {
|
||||
log.Println("error - cant stat a file", err)
|
||||
continue
|
||||
}
|
||||
|
||||
if *skipHidden && strings.HasPrefix(el.Name(), ".") {
|
||||
continue // dont print hidden files if we're not allowed
|
||||
}
|
||||
if !*symlinks && info.Mode()&os.ModeSymlink != 0 {
|
||||
continue // dont follow symlinks if we're not allowed
|
||||
}
|
||||
|
||||
href := url.PathEscape(el.Name())
|
||||
name := el.Name()
|
||||
|
||||
if el.IsDir() && strings.HasPrefix(href, "/") {
|
||||
href = strings.Replace(href, "/", "", 1)
|
||||
}
|
||||
|
||||
if el.IsDir() {
|
||||
row := rowTemplate{name + "/", template.URL(href), "", "folder"}
|
||||
p.RowsFolders = append(p.RowsFolders, row)
|
||||
} else {
|
||||
sl := strings.Split(name, ".")
|
||||
ext := strings.ToLower(sl[len(sl)-1])
|
||||
row := rowTemplate{name, template.URL(href), humanize(el.Size()), ext}
|
||||
p.RowsFiles = append(p.RowsFiles, row)
|
||||
}
|
||||
}
|
||||
|
||||
if strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.Header().Add("Content-Encoding", "gzip")
|
||||
gz, err := gzip.NewWriterLevel(w, gzip.BestSpeed) // BestSpeed is Much Faster than default - base on a very unscientific local test, and only ~30% larger (compression remains still very effective, ~6x)
|
||||
check(err)
|
||||
defer gz.Close()
|
||||
tmpl.Execute(gz, p)
|
||||
} else {
|
||||
tmpl.Execute(w, p)
|
||||
}
|
||||
}
|
||||
|
||||
func doContent(w http.ResponseWriter, r *http.Request) {
|
||||
if !strings.HasPrefix(r.URL.Path, *extraPath) { // redir when were not hitting the supplementary path if one is set
|
||||
http.Redirect(w, r, *extraPath, http.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
path := html.UnescapeString(r.URL.Path)
|
||||
defer exitPath(w, "get content", path)
|
||||
fullPath := enforcePath(path)
|
||||
stat, errStat := os.Stat(fullPath)
|
||||
check(errStat)
|
||||
|
||||
if stat.IsDir() {
|
||||
replyList(w, r, fullPath, path)
|
||||
} else {
|
||||
handler.ServeHTTP(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
func upload(w http.ResponseWriter, r *http.Request) {
|
||||
path := r.Header.Get("gossa-path")
|
||||
defer exitPath(w, "upload", path)
|
||||
|
||||
path, err := url.PathUnescape(path)
|
||||
check(err)
|
||||
reader, err := r.MultipartReader()
|
||||
check(err)
|
||||
part, err := reader.NextPart()
|
||||
if err != nil && err != io.EOF { // errs EOF when no more parts to process
|
||||
check(err)
|
||||
}
|
||||
dst, err := os.Create(enforcePath(path))
|
||||
check(err)
|
||||
io.Copy(dst, part)
|
||||
w.Write([]byte("ok"))
|
||||
}
|
||||
|
||||
func zipRPC(w http.ResponseWriter, r *http.Request) {
|
||||
zipPath := r.URL.Query().Get("zipPath")
|
||||
zipName := r.URL.Query().Get("zipName")
|
||||
defer exitPath(w, "zip", zipPath)
|
||||
zipFullPath := enforcePath(zipPath)
|
||||
_, err := os.Lstat(zipFullPath)
|
||||
check(err)
|
||||
w.Header().Add("Content-Disposition", "attachment; filename=\""+zipName+".zip\"")
|
||||
zipWriter := zip.NewWriter(w)
|
||||
defer zipWriter.Close()
|
||||
|
||||
err = filepath.Walk(zipFullPath, func(path string, f fs.FileInfo, err error) error {
|
||||
check(err)
|
||||
if f.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
rel, err := filepath.Rel(zipFullPath, path)
|
||||
check(err)
|
||||
if *skipHidden && (strings.HasPrefix(rel, ".") || strings.HasPrefix(f.Name(), ".")) {
|
||||
return nil // hidden files not allowed
|
||||
}
|
||||
if f.Mode()&os.ModeSymlink != 0 {
|
||||
panic(errors.New("symlink not allowed in zip downloads")) // filepath.Walk doesnt support symlinks
|
||||
}
|
||||
|
||||
header, err := zip.FileInfoHeader(f)
|
||||
check(err)
|
||||
header.Name = filepath.ToSlash(rel) // make the paths consistent between OSes
|
||||
header.Method = zip.Store
|
||||
headerWriter, err := zipWriter.CreateHeader(header)
|
||||
check(err)
|
||||
file, err := os.Open(path)
|
||||
check(err)
|
||||
defer file.Close()
|
||||
_, err = io.Copy(headerWriter, file)
|
||||
check(err)
|
||||
return nil
|
||||
})
|
||||
|
||||
check(err)
|
||||
}
|
||||
|
||||
func rpc(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
var rpc rpcCall
|
||||
defer exitPath(w, "rpc", rpc)
|
||||
bodyBytes, err := io.ReadAll(r.Body)
|
||||
check(err)
|
||||
json.Unmarshal(bodyBytes, &rpc)
|
||||
|
||||
if rpc.Call == "mkdirp" {
|
||||
err = os.MkdirAll(enforcePath(rpc.Args[0]), os.ModePerm)
|
||||
} else if rpc.Call == "mv" {
|
||||
err = os.Rename(enforcePath(rpc.Args[0]), enforcePath(rpc.Args[1]))
|
||||
} else if rpc.Call == "rm" {
|
||||
err = os.RemoveAll(enforcePath(rpc.Args[0]))
|
||||
}
|
||||
|
||||
check(err)
|
||||
w.Write([]byte("ok"))
|
||||
}
|
||||
|
||||
func enforcePath(p string) string {
|
||||
joined := filepath.Join(rootPath, strings.TrimPrefix(p, *extraPath))
|
||||
fp, err := filepath.Abs(joined)
|
||||
sl, _ := filepath.EvalSymlinks(fp) // err skipped as it would error for unexistent files (RPC check). The actual behaviour is tested below
|
||||
|
||||
// panic if we had a error getting absolute path,
|
||||
// ... or if path doesnt contain the prefix path we expect,
|
||||
// ... or if we're skipping hidden folders, and one is requested,
|
||||
// ... or if we're skipping symlinks, path exists, and a symlink out of bound requested
|
||||
if err != nil || !strings.HasPrefix(fp, rootPath) || *skipHidden && strings.Contains(p, "/.") || !*symlinks && len(sl) > 0 && !strings.HasPrefix(sl, rootPath) {
|
||||
panic(errors.New("invalid path"))
|
||||
}
|
||||
|
||||
return fp
|
||||
}
|
||||
|
||||
func main() {
|
||||
if flag.Parse(); len(flag.Args()) == 1 {
|
||||
rootPath = flag.Args()[0]
|
||||
} else {
|
||||
fmt.Printf("\nusage: ./gossa [OPTIONS] ~/directory-to-share\n\n")
|
||||
flag.PrintDefaults()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
var err error
|
||||
rootPath, err = filepath.Abs(rootPath)
|
||||
check(err)
|
||||
server := &http.Server{Addr: *host + ":" + *port, Handler: handler}
|
||||
|
||||
if !*ro {
|
||||
http.HandleFunc(*extraPath+"rpc", rpc)
|
||||
http.HandleFunc(*extraPath+"post", upload)
|
||||
}
|
||||
http.HandleFunc(*extraPath+"zip", zipRPC)
|
||||
http.HandleFunc("/", doContent)
|
||||
handler = http.StripPrefix(*extraPath, http.FileServer(http.Dir(rootPath)))
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"encoding/base64"
|
||||
"html/template"
|
||||
"strings"
|
||||
)
|
||||
|
||||
//go:embed ui/script.js
|
||||
var scriptJs string
|
||||
|
||||
//go:embed ui/style.css
|
||||
var styleCss string
|
||||
|
||||
//go:embed ui/favicon.svg
|
||||
var faviconSvg []byte
|
||||
|
||||
//go:embed ui/ui.tmpl
|
||||
var uiTmpl string
|
||||
|
||||
var tmpl *template.Template
|
||||
|
||||
// fill in template
|
||||
func init() {
|
||||
var err error
|
||||
t := strings.Replace(uiTmpl, "css_will_be_here", styleCss, 1)
|
||||
t = strings.Replace(t, "js_will_be_here", scriptJs, 1)
|
||||
t = strings.Replace(t, "favicon_will_be_here", base64.StdEncoding.EncodeToString(faviconSvg), 2)
|
||||
tmpl, err = template.New("").Parse(t)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
@@ -1,39 +1,29 @@
|
||||
gossa
|
||||
=============
|
||||
|
||||

|
||||

|
||||
|
||||
[](https://github.com/pldubouilh/gossa/actions)
|
||||
[](https://hub.docker.com/r/pldubouilh/gossa)
|
||||
[](https://hub.docker.com/r/pldubouilh/gossa)
|
||||
[](https://github.com/pldubouilh/gossa/releases)
|
||||
|
||||
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 200 lines of code, easy to review.
|
||||
|
||||
a simple UI comes as default, featuring :
|
||||
a [simple UI](https://github.com/pldubouilh/gossa-ui) comes as default, featuring :
|
||||
|
||||
* 🔍 files/directories browser & handler
|
||||
* 📩 drag-and-drop uploader
|
||||
* 💾 90s web UI that prints in milliseconds
|
||||
* 📸 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-able
|
||||
* 🖥️ multi-platform support
|
||||
* 🔍 files/directories browser
|
||||
* 📩 drag-and-drop file/directory uploader
|
||||
* 🗺️ files handling - move/rename/delete
|
||||
* 📸 picture browser
|
||||
* 📽️ video streaming
|
||||
* ✍️ simple text editor
|
||||
* ⌨️ keyboard shortcuts
|
||||
* 🥂 speed - will easily fill available bandwidth
|
||||
* 🔒 safe - easy/secure multi account setup
|
||||
|
||||
### 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`
|
||||
|
||||
[mpr](https://mpr.makedeb.org/packages/gossa)
|
||||
|
||||
binaries are available on the [release page](https://github.com/pldubouilh/gossa/releases)
|
||||
|
||||
all builds are reproducible, checkout the hashes on the release page.
|
||||
### build
|
||||
built blobs are available on the [release page](https://github.com/pldubouilh/gossa/releases) - or simply `make build` this repo.
|
||||
|
||||
### usage
|
||||
```sh
|
||||
@@ -42,19 +32,16 @@ all builds are reproducible, checkout the hashes on the release page.
|
||||
% ./gossa -h 192.168.100.33 ~/storage
|
||||
```
|
||||
|
||||
### shortcuts
|
||||
press `Ctrl/Cmd + h` to see all the UI/keyboard shortcuts.
|
||||
|
||||
### fancier setups
|
||||
release images are pushed to [dockerhub](https://hub.docker.com/r/pldubouilh/gossa), e.g. :
|
||||
|
||||
```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.
|
||||
in a do-one-thing-well mindset, HTTPS and authentication has been left to middlewares and proxies. for instance [caddy](https://caddyserver.com/) handles this very well - have a look at this small [caddy config](https://github.com/pldubouilh/gossa/blob/master/support/Caddyfile) with authentication and option for HTTPS that works along with gossa.
|
||||
|
||||
automatic boot-time startup can be handled with a user systemd service - see [support](https://github.com/pldubouilh/gossa/tree/master/support)
|
||||
### shortcuts
|
||||
the default UI is fully usable by through keyboard/UI shortcuts - press `Ctrl/Cmd + h` to see them all.
|
||||
|
||||
|
||||
Executable
+198
@@ -0,0 +1,198 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"html"
|
||||
"html/template"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var host = flag.String("h", "127.0.0.1", "host to listen to")
|
||||
var port = flag.String("p", "8001", "port to listen to")
|
||||
var history = flag.Bool("history", true, "keep history for paths visited. default location is path/.gossa_history")
|
||||
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 initPath = "."
|
||||
|
||||
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
|
||||
ExtraPath template.HTML
|
||||
RowsFiles []rowTemplate
|
||||
RowsFolders []rowTemplate
|
||||
}
|
||||
|
||||
type rpcCall struct {
|
||||
Call string `json:"call"`
|
||||
Args []string `json:"args"`
|
||||
}
|
||||
|
||||
func check(e error) {
|
||||
if e != nil {
|
||||
panic(e)
|
||||
}
|
||||
}
|
||||
|
||||
func exitPath(w http.ResponseWriter, s ...interface{}) {
|
||||
if r := recover(); r != nil {
|
||||
log.Println("error", s, r)
|
||||
w.WriteHeader(500)
|
||||
w.Write([]byte("error"))
|
||||
} else if *verb {
|
||||
log.Println(s...)
|
||||
}
|
||||
}
|
||||
|
||||
func humanize(bytes int64) string {
|
||||
b := float64(bytes)
|
||||
u := 0
|
||||
for {
|
||||
if b < 1024 {
|
||||
return strconv.FormatFloat(b, 'f', 1, 64) + [9]string{"B", "k", "M", "G", "T", "P", "E", "Z", "Y"}[u]
|
||||
}
|
||||
b = b / 1024
|
||||
u++
|
||||
}
|
||||
}
|
||||
|
||||
func replyList(w http.ResponseWriter, r *http.Request, fullPath string, path string) {
|
||||
_files, err := ioutil.ReadDir(fullPath)
|
||||
check(err)
|
||||
if !strings.HasSuffix(path, "/") {
|
||||
path += "/"
|
||||
}
|
||||
|
||||
title := "/" + strings.TrimPrefix(path, *extraPath)
|
||||
p := pageTemplate{}
|
||||
if path != *extraPath {
|
||||
p.RowsFolders = append(p.RowsFolders, rowTemplate{"../", "../", "", "folder"})
|
||||
}
|
||||
p.ExtraPath = template.HTML(html.EscapeString(*extraPath))
|
||||
p.Title = template.HTML(html.EscapeString(title))
|
||||
|
||||
for _, el := range _files {
|
||||
if *skipHidden && strings.HasPrefix(el.Name(), ".") {
|
||||
continue
|
||||
}
|
||||
el, _ = os.Stat(fullPath + "/" + el.Name())
|
||||
href := url.PathEscape(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"})
|
||||
} else {
|
||||
sl := strings.Split(el.Name(), ".")
|
||||
p.RowsFiles = append(p.RowsFiles, rowTemplate{el.Name(), template.HTML(href), humanize(el.Size()), strings.ToLower(sl[len(sl)-1])})
|
||||
}
|
||||
}
|
||||
|
||||
page.Execute(w, p)
|
||||
}
|
||||
|
||||
func doContent(w http.ResponseWriter, r *http.Request) {
|
||||
if !strings.HasPrefix(r.URL.Path, *extraPath) {
|
||||
http.Redirect(w, r, *extraPath, 302)
|
||||
return
|
||||
}
|
||||
|
||||
path := html.UnescapeString(r.URL.Path)
|
||||
defer exitPath(w, "get content", path)
|
||||
fullPath := checkPath(path)
|
||||
stat, errStat := os.Stat(fullPath)
|
||||
check(errStat)
|
||||
|
||||
if stat.IsDir() {
|
||||
replyList(w, r, fullPath, path)
|
||||
} else {
|
||||
fs.ServeHTTP(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
func upload(w http.ResponseWriter, r *http.Request) {
|
||||
path, _ := url.PathUnescape(r.Header.Get("gossa-path"))
|
||||
defer exitPath(w, "upload", path)
|
||||
reader, _ := r.MultipartReader()
|
||||
part, _ := reader.NextPart()
|
||||
dst, _ := os.Create(checkPath(path))
|
||||
io.Copy(dst, part)
|
||||
w.Write([]byte("ok"))
|
||||
}
|
||||
|
||||
func rpc(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
var rpc rpcCall
|
||||
bodyBytes, _ := ioutil.ReadAll(r.Body)
|
||||
json.Unmarshal(bodyBytes, &rpc)
|
||||
defer exitPath(w, "rpc", rpc)
|
||||
ret := "ok"
|
||||
|
||||
if rpc.Call == "mkdirp" {
|
||||
err = os.MkdirAll(checkPath(rpc.Args[0]), os.ModePerm)
|
||||
} else if rpc.Call == "mv" {
|
||||
err = os.Rename(checkPath(rpc.Args[0]), checkPath(rpc.Args[1]))
|
||||
} else if rpc.Call == "rm" {
|
||||
err = os.RemoveAll(checkPath(rpc.Args[0]))
|
||||
}
|
||||
|
||||
check(err)
|
||||
w.Write([]byte(ret))
|
||||
}
|
||||
|
||||
func checkPath(p string) string {
|
||||
joined := filepath.Join(initPath, strings.TrimPrefix(p, *extraPath))
|
||||
fp, err := filepath.Abs(joined)
|
||||
sl, _ := filepath.EvalSymlinks(fp)
|
||||
|
||||
if err != nil || !strings.HasPrefix(fp, initPath) || *skipHidden && strings.Contains(p, "/.") || !*symlinks && len(sl) > 0 && !strings.HasPrefix(sl, initPath) {
|
||||
panic(errors.New("invalid path"))
|
||||
}
|
||||
|
||||
return fp
|
||||
}
|
||||
|
||||
func main() {
|
||||
var err error
|
||||
flag.Usage = func() {
|
||||
fmt.Printf("\nusage: ./gossa ~/directory-to-share\n\n")
|
||||
flag.PrintDefaults()
|
||||
}
|
||||
|
||||
flag.Parse()
|
||||
if len(flag.Args()) > 0 {
|
||||
initPath = flag.Args()[0]
|
||||
}
|
||||
|
||||
initPath, err = filepath.Abs(initPath)
|
||||
check(err)
|
||||
|
||||
http.HandleFunc(*extraPath+"rpc", rpc)
|
||||
http.HandleFunc(*extraPath+"post", upload)
|
||||
http.HandleFunc("/", doContent)
|
||||
fs = http.StripPrefix(*extraPath, http.FileServer(http.Dir(initPath)))
|
||||
fmt.Printf("Gossa startig on directory %s\nListening on http://%s:%s%s\n", initPath, *host, *port, *extraPath)
|
||||
err = http.ListenAndServe(*host+":"+*port, nil)
|
||||
check(err)
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
@@ -9,6 +8,7 @@ import (
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func dieMaybe(t *testing.T, err error) {
|
||||
@@ -22,29 +22,11 @@ func trimSpaces(str string) string {
|
||||
return space.ReplaceAllString(str, " ")
|
||||
}
|
||||
|
||||
func getRaw(t *testing.T, url string) []byte {
|
||||
func get(t *testing.T, url string) string {
|
||||
resp, err := http.Get(url)
|
||||
dieMaybe(t, err)
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
dieMaybe(t, err)
|
||||
return body
|
||||
}
|
||||
|
||||
func getZip(t *testing.T, needle string, dest string) (int, bool) {
|
||||
b := getRaw(t, dest)
|
||||
unzipped, err := zip.NewReader(bytes.NewReader(b), int64(len(b)))
|
||||
dieMaybe(t, err)
|
||||
length := len(unzipped.File)
|
||||
for _, file := range unzipped.File {
|
||||
if file.Name == needle {
|
||||
return length, true
|
||||
}
|
||||
}
|
||||
return length, false
|
||||
}
|
||||
|
||||
func get(t *testing.T, url string) string {
|
||||
body := getRaw(t, url)
|
||||
return trimSpaces(string(body))
|
||||
}
|
||||
|
||||
@@ -102,20 +84,13 @@ func fetchAndTestDefault(t *testing.T, url string) string {
|
||||
return body0
|
||||
}
|
||||
|
||||
func doTestRegular(t *testing.T, url string, testExtra bool) {
|
||||
func doTest(t *testing.T, url string, testExtra bool) {
|
||||
var payload, path, body0, body1, body2 string
|
||||
|
||||
// ~~~~~~~~~~~~~~~~~
|
||||
fmt.Println("\r\n~~~~~~~~~~ test fetching default path")
|
||||
fetchAndTestDefault(t, url)
|
||||
|
||||
// ~~~~~~~~~~~~~~~~~
|
||||
fmt.Println("\r\n~~~~~~~~~~ test fetching another page")
|
||||
body0 = get(t, url+"/hols")
|
||||
if !strings.Contains(body0, "glasgow.jpg") {
|
||||
t.Fatal("fetching a subfolder failed")
|
||||
}
|
||||
|
||||
// ~~~~~~~~~~~~~~~~~
|
||||
fmt.Println("\r\n~~~~~~~~~~ test fetching an invalid path - redirected to root")
|
||||
fetchAndTestDefault(t, url+"../../")
|
||||
@@ -138,30 +113,6 @@ func doTestRegular(t *testing.T, url string, testExtra bool) {
|
||||
fetchAndTestDefault(t, url+path) // extra path will just redirect to root dir
|
||||
}
|
||||
|
||||
// ~~~~~~~~~~~~~~~~~
|
||||
fmt.Println("\r\n~~~~~~~~~~ test zipping of folder 中文")
|
||||
len, foundFile := getZip(t, "檔案.html", url+"zip?zipPath=%2F%E4%B8%AD%E6%96%87%2F&zipName=%E4%B8%AD%E6%96%87")
|
||||
if len != 1 || !foundFile {
|
||||
t.Fatal("invalid zip generated")
|
||||
}
|
||||
|
||||
// ~~~~~~~~~~~~~~~~~
|
||||
fmt.Println("\r\n~~~~~~~~~~ test zipping of folder with hidden file")
|
||||
_, foundHidden := getZip(t, ".hidden-folder/some-file", url+"zip?zipPath=%2fhols%2f&zipName=hols")
|
||||
if foundHidden && !testExtra {
|
||||
t.Fatal("invalid zip generated - shouldnt contain hidden folder")
|
||||
} else if !foundHidden && testExtra {
|
||||
t.Fatal("invalid zip generated - should contain hidden folder")
|
||||
}
|
||||
|
||||
// ~~~~~~~~~~~~~~~~~
|
||||
fmt.Println("\r\n~~~~~~~~~~ test zip invalid path")
|
||||
body0 = get(t, url+"zip?zipPath=%2Ftmp&zipName=subdir")
|
||||
println(body0)
|
||||
if body0 != `error` {
|
||||
t.Fatal("zip passed for invalid path")
|
||||
}
|
||||
|
||||
// ~~~~~~~~~~~~~~~~~
|
||||
fmt.Println("\r\n~~~~~~~~~~ test mkdir rpc")
|
||||
body0 = postJSON(t, url+"rpc", `{"call":"mkdirp","args":["/AAA"]}`)
|
||||
@@ -204,17 +155,6 @@ func doTestRegular(t *testing.T, url string, testExtra bool) {
|
||||
t.Fatal("post file incorrect path didnt errored")
|
||||
}
|
||||
|
||||
// ~~~~~~~~~~~~~~~~~
|
||||
fmt.Println("\r\n~~~~~~~~~~ test post file")
|
||||
path = "2024-01-02-10:36:58.png"
|
||||
payload = "123123123123123123123123"
|
||||
body0 = postDummyFile(t, url, path, payload)
|
||||
body1 = get(t, url+path)
|
||||
body2 = fetchAndTestDefault(t, url)
|
||||
if body0 != `ok` || body1 != payload || !strings.Contains(body2, `href="2024-01-02-10:36:58.png"`) {
|
||||
t.Fatal("post file errored")
|
||||
}
|
||||
|
||||
// ~~~~~~~~~~~~~~~~~
|
||||
fmt.Println("\r\n~~~~~~~~~~ test mv rpc")
|
||||
body0 = postJSON(t, url+"rpc", `{"call":"mv","args":["/AAA", "/hols/AAA"]}`)
|
||||
@@ -234,27 +174,12 @@ func doTestRegular(t *testing.T, url string, testExtra bool) {
|
||||
|
||||
// ~~~~~~~~~~~~~~~~~
|
||||
fmt.Println("\r\n~~~~~~~~~~ test symlink, should succeed: ", testExtra)
|
||||
body0 = get(t, url+"/support/")
|
||||
hasListing := strings.Contains(body0, `readme.md`)
|
||||
body1 = get(t, url+"/support/readme.md")
|
||||
hasReadme := strings.Contains(body1, `the master branch is automatically built and pushed`)
|
||||
body2 = get(t, url)
|
||||
hasMainListing := strings.Contains(body2, `href="support">support/</a>`)
|
||||
|
||||
body0 = get(t, url+"/support/readme.md")
|
||||
hasReadme := strings.Contains(body0, `the master branch is automatically built and pushed`)
|
||||
if !testExtra && hasReadme {
|
||||
t.Fatal("error symlink file reached where illegal")
|
||||
t.Fatal("error symlink reached where illegal")
|
||||
} else if testExtra && !hasReadme {
|
||||
t.Fatal("error symlink file unreachable")
|
||||
}
|
||||
if !testExtra && hasListing {
|
||||
t.Fatal("error symlink folder reached where illegal")
|
||||
} else if testExtra && !hasListing {
|
||||
t.Fatal("error symlink folder unreachable")
|
||||
}
|
||||
if !testExtra && hasMainListing {
|
||||
t.Fatal("error symlink folder where illegal")
|
||||
} else if testExtra && !hasMainListing {
|
||||
t.Fatal("error symlink folder unreachable")
|
||||
t.Fatal("error symlink unreachable")
|
||||
}
|
||||
|
||||
if testExtra {
|
||||
@@ -304,87 +229,20 @@ func doTestRegular(t *testing.T, url string, testExtra bool) {
|
||||
if body0 != `ok` {
|
||||
t.Fatal("cleanup errored #2")
|
||||
}
|
||||
|
||||
fmt.Printf("\r\n=========\r\n")
|
||||
}
|
||||
|
||||
func doTestReadonly(t *testing.T, url string) {
|
||||
var payload, path, body0, body1 string
|
||||
|
||||
// ~~~~~~~~~~~~~~~~~
|
||||
fmt.Println("\r\n~~~~~~~~~~ test fetching default path")
|
||||
fetchAndTestDefault(t, url)
|
||||
|
||||
// ~~~~~~~~~~~~~~~~~
|
||||
fmt.Println("\r\n~~~~~~~~~~ test fetching an invalid path - redirected to root")
|
||||
fetchAndTestDefault(t, url+"../../")
|
||||
fetchAndTestDefault(t, url+"hols/../../")
|
||||
|
||||
// ~~~~~~~~~~~~~~~~~
|
||||
fmt.Println("\r\n~~~~~~~~~~ test fetching regular files")
|
||||
body0 = get(t, url+"subdir_with%20space/file_with%20space.html")
|
||||
body1 = get(t, url+"fancy-path/a")
|
||||
if body0 != `<b>spacious!!</b> ` || body1 != `fancy! ` {
|
||||
t.Fatal("fetching a regular file errored")
|
||||
}
|
||||
|
||||
// ~~~~~~~~~~~~~~~~~
|
||||
fmt.Println("\r\n~~~~~~~~~~ test fetching a invalid file")
|
||||
path = "../../../../../../../../../../etc/passwd"
|
||||
if get(t, url+path) != `error` {
|
||||
t.Fatal("fetching a invalid file didnt errored")
|
||||
}
|
||||
|
||||
// ~~~~~~~~~~~~~~~~~
|
||||
fmt.Println("\r\n~~~~~~~~~~ test mkdir rpc")
|
||||
body0 = postJSON(t, url+"rpc", `{"call":"mkdirp","args":["/AAA"]}`)
|
||||
if body0 == `ok` {
|
||||
t.Fatal("mkdir rpc passed - should not be allowed")
|
||||
}
|
||||
|
||||
// ~~~~~~~~~~~~~~~~~
|
||||
fmt.Println("\r\n~~~~~~~~~~ test post file")
|
||||
path = "%2F%E1%84%92%E1%85%A1%20%E1%84%92%E1%85%A1" // "하 하" encoded
|
||||
payload = "123 하"
|
||||
body0 = postDummyFile(t, url, path, payload)
|
||||
get(t, url+path)
|
||||
if body0 == `ok` {
|
||||
t.Fatal("post file passed - should not be allowed")
|
||||
}
|
||||
|
||||
// ~~~~~~~~~~~~~~~~~
|
||||
fmt.Println("\r\n~~~~~~~~~~ test mv rpc")
|
||||
body0 = postJSON(t, url+"rpc", `{"call":"mv","args":["/AAA", "/hols/AAA"]}`)
|
||||
fetchAndTestDefault(t, url)
|
||||
if body0 == `ok` {
|
||||
t.Fatal("mv rpc passed - should not be allowed")
|
||||
}
|
||||
|
||||
// ~~~~~~~~~~~~~~~~~
|
||||
fmt.Println("\r\n~~~~~~~~~~ test rm rpc & cleanup")
|
||||
body0 = postJSON(t, url+"rpc", `{"call":"rm","args":["/hols/AAA"]}`)
|
||||
if body0 == `ok` {
|
||||
t.Fatal("cleanup passed - should not be allowed")
|
||||
}
|
||||
|
||||
fmt.Printf("\r\n=========\r\n")
|
||||
}
|
||||
|
||||
func TestNormal(t *testing.T) {
|
||||
func TestGetFolder(t *testing.T) {
|
||||
time.Sleep(6 * time.Second)
|
||||
fmt.Println("========== testing normal path ============")
|
||||
doTestRegular(t, "http://127.0.0.1:8001/", false)
|
||||
}
|
||||
url := "http://127.0.0.1:8001/"
|
||||
doTest(t, url, false)
|
||||
|
||||
func TestExtra(t *testing.T) {
|
||||
fmt.Println("========== testing extras options ============")
|
||||
doTestRegular(t, "http://127.0.0.1:8001/fancy-path/", true)
|
||||
}
|
||||
fmt.Printf("\r\n=========\r\n")
|
||||
time.Sleep(10 * time.Second)
|
||||
|
||||
func TestRo(t *testing.T) {
|
||||
fmt.Println("========== testing read only ============")
|
||||
doTestReadonly(t, "http://127.0.0.1:8001/")
|
||||
}
|
||||
url = "http://127.0.0.1:8001/fancy-path/"
|
||||
fmt.Println("========== testing at fancy path ============")
|
||||
doTest(t, url, true)
|
||||
|
||||
func TestRunMain(t *testing.T) {
|
||||
main()
|
||||
fmt.Printf("\r\n=========\r\n")
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
# Caddy config
|
||||
# to enable https just set a valid domain (e.g. mydomain.com) instead of :8080 - how simple !
|
||||
# authentication has been setup with 2 users, alice and bob
|
||||
|
||||
:8080
|
||||
basicauth / alice paul
|
||||
basicauth / bob dylan
|
||||
proxy / 127.0.0.1:8001
|
||||
@@ -1,16 +1,11 @@
|
||||
FROM docker.io/library/golang:1.23.0-alpine AS builder
|
||||
RUN apk add --no-cache make
|
||||
WORKDIR /gossaSrc
|
||||
FROM golang:latest as builder
|
||||
COPY . /gossaSrc
|
||||
RUN make
|
||||
RUN cd /gossaSrc && make
|
||||
|
||||
FROM docker.io/library/alpine:3.20
|
||||
ENV UID="1000" GID="1000" HOST="0.0.0.0" PORT="8001" PREFIX="/" FOLLOW_SYMLINKS="false" SKIP_HIDDEN_FILES="true" DATADIR="/shared" READONLY="false" VERB="false"
|
||||
FROM alpine
|
||||
ENV UID="1000" GID="1000" HOST="0.0.0.0" PORT="8001" PREFIX="/" FOLLOW_SYMLINKS="false" SKIP_HIDDEN_FILES="true" DATADIR="/shared"
|
||||
EXPOSE 8001
|
||||
RUN apk add --no-cache su-exec
|
||||
COPY --from=builder /gossaSrc/gossa /gossa
|
||||
RUN addgroup -g ${GID} user \
|
||||
&& adduser -D -u ${UID} -G user user
|
||||
WORKDIR ${DATADIR}
|
||||
RUN chown ${UID}:${GID} ${DATADIR}
|
||||
USER ${UID}:${GID}
|
||||
ENTRYPOINT /gossa -h ${HOST} -p ${PORT} -k=${SKIP_HIDDEN_FILES} -ro=${READONLY} --symlinks=${FOLLOW_SYMLINKS} --prefix=${PREFIX} --verb=${VERB} ${DATADIR}
|
||||
HEALTHCHECK --timeout=5s --start-period=5s --retries=3 CMD wget --no-verbose --tries=1 --spider 127.0.0.1:8001 || exit 1
|
||||
RUN echo -e 'exec su-exec ${UID}:${GID} /gossa -h ${HOST} -p ${PORT} -k=${SKIP_HIDDEN_FILES} --symlinks=${FOLLOW_SYMLINKS} --prefix=${PREFIX} ${DATADIR}'>> /start.sh
|
||||
ENTRYPOINT [ "sh", "/start.sh" ]
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
FROM pldubouilh/gossa
|
||||
|
||||
RUN apk update && apk add curl ca-certificates caddy
|
||||
|
||||
ENV UID="1000" GID="1000" HOST="127.0.0.1" PORT="8001" PREFIX="/" FOLLOW_SYMLINKS="false" SKIP_HIDDEN_FILES="true" DATADIR="/shared"
|
||||
EXPOSE 443
|
||||
RUN echo -e 'exec su-exec ${UID}:${GID} /gossa -h ${HOST} -p ${PORT} -k=${SKIP_HIDDEN_FILES} --symlinks=${FOLLOW_SYMLINKS} --prefix=${PREFIX} ${DATADIR} & \n caddy' > /start.sh
|
||||
ENTRYPOINT [ "sh", "/start.sh" ]
|
||||
@@ -2,29 +2,9 @@ version: '2'
|
||||
|
||||
services:
|
||||
gossa-server:
|
||||
image: docker.io/pldubouilh/gossa:latest
|
||||
image: pldubouilh/gossa
|
||||
container_name: gossa
|
||||
restart: always
|
||||
read_only: true
|
||||
# uncomment to set the user
|
||||
# user: "1000:1000"
|
||||
# userns_mode: "keep-id" # uncomment if using rootless podman as well as the x-podman directive at the bottom
|
||||
# environment:
|
||||
#- READONLY=true # uncomment to set gossa as read only
|
||||
# - UID=1000 # this should match the user set above
|
||||
# - GID=1000 # this should match the user's group
|
||||
cap_drop:
|
||||
- ALL
|
||||
cap_add:
|
||||
- SETGID
|
||||
- SETUID
|
||||
# uncomment to set resource usage limits
|
||||
# deploy:
|
||||
# resources:
|
||||
# limits:
|
||||
# cpus: "2"
|
||||
# memory: 250m
|
||||
# pids: 1024
|
||||
ports:
|
||||
- 8001:8001
|
||||
volumes:
|
||||
@@ -34,6 +14,3 @@ services:
|
||||
# - "traefik.port=8001"
|
||||
# - "traefik.backend=gossa"
|
||||
# - "traefik.frontend.rule=Host:${GOSSA}.${DOMAIN}"
|
||||
|
||||
# x-podman: # uncomment if using rootless podman as well as the userns_mode directive at the top
|
||||
# in_pod: false
|
||||
+7
-175
@@ -1,195 +1,27 @@
|
||||
## run as a service
|
||||
|
||||
use the `service` file provided here to use as a systemd service.
|
||||
|
||||
```sh
|
||||
# edit service file
|
||||
|
||||
# copy over our service, and start. Notice no sudo with systemctl command !
|
||||
% mkdir -p ~/.config/systemd/user/
|
||||
% cp service ~/.config/systemd/user/gossa.service
|
||||
% systemctl --user start gossa
|
||||
|
||||
# enable at boot time
|
||||
% systemctl --user enable gossa
|
||||
```
|
||||
|
||||
## run with docker
|
||||
|
||||
the master branch is automatically built and pushed to [dockerhub](https://hub.docker.com/r/pldubouilh/gossa) under `pldubouilh/gossa`.
|
||||
|
||||
```sh
|
||||
# pull from dockerhub and run
|
||||
% mkdir ~/LocalDirToShare
|
||||
% sudo docker run -v ~/LocalDirToShare:/shared -p 8001:8001 pldubouilh/gossa
|
||||
|
||||
# options are settable through env. variabes. all the options are the build.Dockerfile
|
||||
% sudo docker run -e PREFIX="/gossa/" -v ~/LocalDirToShare:/shared -p 8001:8001 pldubouilh/gossa
|
||||
```
|
||||
|
||||
if you prefer building the image yourself :
|
||||
|
||||
```sh
|
||||
# build gossa within a build container, needs to be ran within the sources, ../ from here, and run
|
||||
% mkdir ~/LocalDirToShare
|
||||
% docker build -t gossa -f support/build.Dockerfile .
|
||||
% sudo docker run -v ~/LocalDirToShare:/shared -p 8001:8001 gossa
|
||||
```
|
||||
|
||||
a docker-compose example image is also provided. running docker compose should be straightforward : `docker-compose up .` have a look in `docker-compose.yml` for further configuration.
|
||||
the options are settable through environment variables that can be passed starting off the docker image.
|
||||
|
||||
## multi-account setup with Caddy
|
||||
|
||||
authentication / user routing has been left out of the design of gossa, as simple tools are already available for this purpose. [caddy](https://caddyserver.com) is used here as an example, but other proxy can be used in a similar fashion.
|
||||
|
||||
### example 1 root, multiple read-only users
|
||||
|
||||
This sample Caddyfile will
|
||||
+ enable https on the domain myserver.com (http will be automatically redirected to https)
|
||||
+ password protect the access
|
||||
+ route the root user requests to 1 gossa instance
|
||||
+ route user1 and user2 requests to a readonly gossa instance
|
||||
|
||||
<details>
|
||||
<summary>Legacy Caddy v1 Caddyfile</summary>
|
||||
|
||||
```sh
|
||||
myserver.com
|
||||
|
||||
# proxy regular and read only instance
|
||||
proxy / 127.0.0.1:8001
|
||||
proxy /ro 127.0.0.1:8002 { without /ro }
|
||||
|
||||
# reroute non-root user to read-only
|
||||
# cm9... is the output of `printf "root:password" | base64`
|
||||
rewrite {
|
||||
if {>Authorization} not "Basic cm9vdDpwYXNzd29yZA=="
|
||||
to /ro/{path}
|
||||
}
|
||||
|
||||
# gate access
|
||||
basicauth / root password
|
||||
basicauth / ro_user1 passworduser1
|
||||
basicauth / ro_user2 passworduser2
|
||||
```
|
||||
</details>
|
||||
|
||||
Caddy v2 Caddyfile
|
||||
a fancy docker image using [Caddy](https://caddyserver.com/) is also provided. have a look at the simple config file `Caddyfile`, it shows how to use http basic authentication, and automatic TLS for hands-free https 🎉
|
||||
|
||||
```sh
|
||||
myserver.com
|
||||
|
||||
# gate access
|
||||
basic_auth {
|
||||
root $2a$14$Zkx19XLiW6VYouLHR5NmfOFU0z2GTNmpkT/5qqR7hx4IjWJPDhjvG # password is "hiccup"
|
||||
ro_user1 $2a$14$Zkx19XLiW6VYouLHR5NmfOFU0z2GTNmpkT/5qqR7hx4IjWJPDhjvG # password is "hiccup"
|
||||
ro_user2 $2a$14$Zkx19XLiW6VYouLHR5NmfOFU0z2GTNmpkT/5qqR7hx4IjWJPDhjvG # password is "hiccup"
|
||||
}
|
||||
|
||||
# named matcher for root user
|
||||
@isroot {
|
||||
vars {http.auth.user.id} root
|
||||
}
|
||||
|
||||
# proxy regular and read only instance
|
||||
handle @isroot {
|
||||
reverse_proxy 127.0.0.1:8001
|
||||
}
|
||||
# route non-root user to read only instance
|
||||
handle {
|
||||
reverse_proxy 127.0.0.1:8002
|
||||
}
|
||||
# checkout the caddy config, build, and run docker image
|
||||
% vim caddy.Dockerfile
|
||||
% docker build -t gossa-caddy -f caddy.Dockerfile .
|
||||
% sudo docker run -v ~/LocalDirToShare:/shared -v `pwd`/Caddyfile:/Caddyfile --net=host gossa-caddy
|
||||
```
|
||||
|
||||
then simply start the 2 gossa instances, and caddy
|
||||
|
||||
```sh
|
||||
# start an instance in readonly
|
||||
% ./gossa -ro=true -p 8002 ~/folder &
|
||||
|
||||
# start an instance with access to hidden files
|
||||
% ./gossa -k=false -p 8001 ~/folder &
|
||||
|
||||
# start caddy
|
||||
% ./caddy
|
||||
```
|
||||
|
||||
### example 2 users on 2 different folders
|
||||
|
||||
This sample Caddyfile will
|
||||
+ enable https on the domain myserver.com (http will be automatically redirected to https)
|
||||
+ password protect the access
|
||||
+ route user1 to own folder
|
||||
+ route user2 to own folder
|
||||
+ share a folder between 2 users with a symlink
|
||||
|
||||
<details>
|
||||
<summary>Legacy Caddy v1 Caddyfile</summary>
|
||||
|
||||
```sh
|
||||
myserver.com
|
||||
|
||||
proxy /user1 127.0.0.1:8001 { without /user1 }
|
||||
proxy /user2 127.0.0.1:8002 { without /user2 }
|
||||
|
||||
basicauth / user1 passworduser1
|
||||
basicauth / user2 passworduser2
|
||||
|
||||
rewrite {
|
||||
if {>Authorization} is "Basic dXNlcjE6cGFzc3dvcmR1c2VyMQ=="
|
||||
to /user1/{path}
|
||||
}
|
||||
|
||||
rewrite {
|
||||
if {>Authorization} is "Basic dXNlcjI6cGFzc3dvcmR1c2VyMg=="
|
||||
to /user2/{path}
|
||||
}
|
||||
```
|
||||
</details>
|
||||
|
||||
Caddy v2 Caddyfile
|
||||
|
||||
```sh
|
||||
myserver.com
|
||||
|
||||
basic_auth {
|
||||
user1 $2a$14$Zkx19XLiW6VYouLHR5NmfOFU0z2GTNmpkT/5qqR7hx4IjWJPDhjvG # password is "hiccup"
|
||||
user2 $2a$14$Zkx19XLiW6VYouLHR5NmfOFU0z2GTNmpkT/5qqR7hx4IjWJPDhjvG # password is "hiccup"
|
||||
}
|
||||
|
||||
@user1auth {
|
||||
vars {http.auth.user.id} user1
|
||||
}
|
||||
handle @user1auth {
|
||||
reverse_proxy 127.0.0.1:8001
|
||||
}
|
||||
|
||||
@user2auth {
|
||||
vars {http.auth.user.id} user2
|
||||
}
|
||||
handle @user2auth {
|
||||
reverse_proxy 127.0.0.1:8002
|
||||
}
|
||||
```
|
||||
|
||||
start 2 gossa instances, and caddy
|
||||
|
||||
```sh
|
||||
# create symlink to share folder between 2 users
|
||||
% ln -s /path/shared test/user1
|
||||
% ln -s /path/shared test/user2
|
||||
|
||||
# start gossa & caddy
|
||||
% ./gossa -p 8001 -symlinks=true test/user1 &
|
||||
% ./gossa -p 8002 -symlinks=true test/user2 &
|
||||
% ./caddy
|
||||
```
|
||||
|
||||
## nginx setup
|
||||
|
||||
In order to allow for larger uploads, it's recommended to increase the maximum body size on your nginx config :
|
||||
|
||||
```
|
||||
# increase maximum request size
|
||||
client_max_body_size 100M;
|
||||
```
|
||||
a docker-compose example image is also provided. running docker compose should be straightforward : `docker-compose up .` have a look in `docker-compose.yml` for further configuration.
|
||||
@@ -1,13 +0,0 @@
|
||||
[Unit]
|
||||
Description=Gossa service for mytestfolder
|
||||
Wants=network-online.target
|
||||
After=network-online.target
|
||||
|
||||
# note: absolute paths are necessary here
|
||||
[Service]
|
||||
ExecStart=/usr/bin/gossa /home/joe/mytestfolder
|
||||
Restart=always
|
||||
RestartSec=30
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -1 +0,0 @@
|
||||
hidden !
|
||||
@@ -1 +0,0 @@
|
||||
../hols
|
||||
@@ -1 +0,0 @@
|
||||
I'm hidden by my directory!
|
||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 23 KiB |
@@ -1,4 +0,0 @@
|
||||
gossa-ui
|
||||
========
|
||||
|
||||
a ~~ugly~~ simple, dependency-free, plain js/css/html front-end for [gossa](https://github.com/pldubouilh/gossa)
|
||||
-861
@@ -1,861 +0,0 @@
|
||||
/* 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
|
||||
|
||||
// DOM elements
|
||||
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 helpMsg = document.getElementById('help_message')
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// check if we're at root path
|
||||
if (location.pathname === window.extraPath + '/') {
|
||||
helpMsg.style.display = 'block'
|
||||
} else {
|
||||
helpMsg.style.display = 'none'
|
||||
}
|
||||
}
|
||||
init()
|
||||
-483
File diff suppressed because one or more lines are too long
-89
@@ -1,89 +0,0 @@
|
||||
<!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>
|
||||
<p id="help_message">Help: Ctrl/Cmd + h<p>
|
||||
</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>
|
||||
Reference in New Issue
Block a user