Compare commits

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

Also make `clean` a phony target.
2022-10-20 08:51:14 +02:00
Pierre Dubouilh 382b2e3734 bump go & auto-release from CI 2022-09-17 13:13:02 +02:00
Pierre Dubouilh a1613a08e8 bump go to 1.18 (#79) 2022-04-01 14:14:25 +02:00
Pierre DubouilhandDennis 99ac521aa8 fixup paths in service file (#78)
systemd needs paths absolutely

Co-authored-by: Dennis <me@dennis.is>
2022-04-01 14:01:58 +02:00
Tim Eichholz f1dc8280f5 Dockerfile updates (#77)
* Added VERB environment variable for enabling verbosity in the Docker container
* pin the versions for golang and alpine at their current latest
* move the entrypoint script to a separate file, removed EXPOSE command from Dockerfile
2022-04-01 13:51:26 +02:00
Pierre Dubouilh cd255a21f6 fix docker release tag 2021-12-22 14:46:05 +01:00
Pierre Dubouilh bf4be929f5 rephrase service file
and move RPC structure back to gossa.go
2021-12-22 14:29:46 +01:00
Pierre DubouilhandVictor d99d975f70 multiplatform docker
cross platform build for docker images

Co-authored-by:     Victor <ViRb3@users.noreply.github.com>
2021-12-22 14:29:46 +01:00
Pierre Dubouilh 177edf4791 reproducible build 2021-12-22 14:29:46 +01:00
Pierre Dubouilh 16b31a5047 split embedder 2021-12-22 14:29:46 +01:00
Pierre Dubouilh 65a6087c60 add coverage test 2021-12-22 14:29:46 +01:00
Pierre Dubouilh 5b55ab0a0a refactor 2021-12-22 14:29:46 +01:00
Pierre Dubouilh cb4631c086 autodeploy dockerhub 2021-12-22 14:29:46 +01:00
Pierre Dubouilh de632f9ab7 add service example 2021-12-22 14:29:46 +01:00
Pierre DubouilhandVictor 4385249c94 better zip #2
stream content to clients

Co-authored-by:     Victor <ViRb3@users.noreply.github.com>
2021-12-22 14:29:46 +01:00
Pierre Dubouilh 5440b4c5a2 more test-fixture 2021-12-22 14:29:46 +01:00
Pierre Dubouilh 2f73fd9df8 better zip 2021-12-22 14:29:46 +01:00
Pierre Dubouilh 688be87923 better error handling 2021-12-22 14:29:46 +01:00
Pierre Dubouilh 166869fc04 support arm based macs 2021-12-22 14:29:46 +01:00
Pierre DubouilhandVictor a2754c616d use go static assets embedder
shift to use new static go assets embedded

Co-authored-by:     Victor <ViRb3@users.noreply.github.com>
2021-12-22 14:29:46 +01:00
Pierre Dubouilh e2aa4f149e shuffle makefile & add empty go.mod
go 1.16 requires go.mod in git folders now apparently
2021-04-17 16:06:57 +02:00
Pierre Dubouilh 8a7a542b22 bump ui 2021-04-17 16:06:57 +02:00
Pierre Dubouilh e490853baa add compression on page
and strip build blobs
2021-04-17 16:06:57 +02:00
Pierre Dubouilh 557b3fd50d fix dockerbuild 2020-10-25 15:48:22 +01:00
Pierre DubouilhandSharad Bhat 1992815b41 Fixup typo
Co-authored-by: Sharad Bhat <sharad.mbhat@gmail.com>
2020-10-04 16:57:03 +02:00
Pierre Dubouilh 97e24cc513 fixup tests 2020-10-04 16:57:03 +02:00
Pierre Dubouilh c1eeb9820a archive download 2020-10-04 16:57:03 +02:00
Pierre Dubouilh dd6cb90e9d bump ui 2020-08-08 13:52:03 +02:00
Pierre Dubouilh 682e76da1f bump ui 2020-07-11 12:08:13 +02:00
Pierre Dubouilh 43e8e36ea2 Merge pull request #58 from pldubouilh/pld/bump-ui
bump ui
2020-06-28 13:11:48 +02:00
Pierre Dubouilh 56f418bbff reword readme 2020-06-28 13:10:44 +02:00
Pierre Dubouilh c7e92cb730 add install instructions to makefile 2020-06-28 12:49:03 +02:00
Pierre Dubouilh 5ea4838d19 bump ui 2020-06-28 12:48:34 +02:00
Pierre Dubouilh 50b9bc77ba Merge pull request #56 from shrmnk/patch-1
Add option for read-only mode in dockerfile
2020-06-12 10:40:05 +02:00
Sherman K 2cd2753480 Add option for read-only mode in dockerfile 2020-06-12 15:49:17 +08:00
Pierre Dubouilh a39424159d Merge pull request #54 from pldubouilh/readmes
push caddy examples
2020-06-07 17:49:55 +02:00
Pierre Dubouilh 32b64da0e6 mkdir folder before docker run 2020-06-07 17:42:16 +02:00
Pierre Dubouilh 2565dcca13 push caddy examples 2020-06-07 17:35:45 +02:00
28 changed files with 2146 additions and 342 deletions
+1
View File
@@ -0,0 +1 @@
ui/** linguist-vendored
@@ -1,22 +1,25 @@
name: ci
on: [push, pull_request]
name: always
on: [push]
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Set up Go 1.13
- name: Set up Go
uses: actions/setup-go@v1
with:
go-version: 1.13
go-version: 1.19.1
id: go
- name: deps
run: npm i -g standard
- name: Check out code into the Go module directory
uses: actions/checkout@v1
with:
submodules: true
- name: Run
run: make test
run: make ci
+63
View File
@@ -0,0 +1,63 @@
name: deploy
on:
push:
tags:
- '*'
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v1
with:
submodules: true
- name: Set up Go
uses: actions/setup-go@v1
with:
go-version: 1.19.1
id: go
- name: Set env
run: echo "GIT_TAG=`echo $(git describe --tags --abbrev=0)`" >> $GITHUB_ENV
- name: Set up QEMU
uses: docker/setup-qemu-action@v1.2.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1.6.0
- name: Login to DockerHub
uses: docker/login-action@v1.10.0
with:
username: ${{ secrets.DOCKER_USER }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and release on dockerhub
uses: docker/build-push-action@v2.7.0
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@58ae73b360456532aafd58ee170c045abbeaee37
with:
allowUpdates: true
artifacts: "builds/*"
token: ${{ secrets.GITHUB_TOKEN }}
- name: "Release gh release latest"
uses: ncipollo/release-action@58ae73b360456532aafd58ee170c045abbeaee37
with:
tag: latest
name: Latest
allowUpdates: true
artifacts: "builds/*"
token: ${{ secrets.GITHUB_TOKEN }}
+39
View File
@@ -0,0 +1,39 @@
name: rc
on:
push:
branches:
- 'rc/**'
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Set up Go
uses: actions/setup-go@v1
with:
go-version: 1.19.1
id: go
- name: Check out code into the Go module directory
uses: actions/checkout@v1
with:
submodules: true
- name: Run
run: make ci
- name: Build all artifacts
run: make build-all
- name: "Release gh release prerelease"
uses: ncipollo/release-action@58ae73b360456532aafd58ee170c045abbeaee37
with:
allowUpdates: true
prerelease: true
tag: "rc"
artifacts: "builds/*"
token: ${{ secrets.GITHUB_TOKEN }}
+8 -2
View File
@@ -4,8 +4,10 @@ gossa-linux-arm
gossa-linux-arm64
gossa-mac
gossa-windows.exe
gossa_test.go
builds
gossa.test
*.out
.vscode
test-fixture/*
test-fixture/*/*
@@ -39,4 +41,8 @@ test-fixture/*/*
!test-fixture/fancy-path
!test-fixture/fancy-path/*
!test-fixture/ext/*
!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
-4
View File
@@ -1,4 +0,0 @@
[submodule "gossa-ui"]
path = gossa-ui
url = https://github.com/pldubouilh/gossa-ui
branch = master
+64 -46
View File
@@ -1,59 +1,77 @@
build:
cp src/gossa.go gossa.go
make -C gossa-ui/
go vet && go fmt
CGO_ENABLED=0 go build gossa.go
rm gossa.go
FLAGS := -ldflags "-s -w" -trimpath
NOCGO := CGO_ENABLED=0
run:
build::
go vet && go fmt
${NOCGO} go build ${FLAGS} -o gossa
install::
sudo cp gossa /usr/local/bin
run::
./gossa -verb=true test-fixture
run-ro:
run-ro::
./gossa -verb=true -ro=true test-fixture
run-extra:
run-extra::
./gossa -verb=true -prefix="/fancy-path/" -k=false -symlinks=true test-fixture
test:
make build
-rm gossa_test.go
-@cd test-fixture && ln -s ../support .
cp src/gossa_test.go .
go test
lint-js::
standard
watch:
ls src/* gossa-ui/* | entr -rc make build run
ci:: build-all test lint-js
echo "done"
watch-extra:
ls src/* gossa-ui/* | entr -rc make build run-extra
test::
-@cd test-fixture && ln -s ../support .; true
go test -cover -c -tags testrunmain
watch-ro:
ls src/* gossa-ui/* | entr -rc make build run-ro
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-test:
ls src/* gossa-ui/* | entr -rc make test
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
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
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
${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/*
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
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
+3
View File
@@ -0,0 +1,3 @@
module github.com/pldubouilh/gossa
go 1.16
Submodule gossa-ui deleted from db5c18c923
Executable
+289
View File
@@ -0,0 +1,289 @@
package main
import (
"archive/zip"
"compress/gzip"
_ "embed"
"encoding/json"
"errors"
"flag"
"fmt"
"html"
"html/template"
"io"
"io/fs"
"io/ioutil"
"log"
"net/http"
"net/url"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
)
type rowTemplate struct {
Name string
Href template.HTML
Size string
Ext string
}
type pageTemplate struct {
Title template.HTML
ExtraPath template.HTML
Ro bool
RowsFiles []rowTemplate
RowsFolders []rowTemplate
}
var host = flag.String("h", "127.0.0.1", "host to listen to")
var port = flag.String("p", "8001", "port to listen to")
var extraPath = flag.String("prefix", "/", "url prefix at which gossa can be reached, e.g. /gossa/ (slashes of importance)")
var symlinks = flag.Bool("symlinks", false, "follow symlinks \033[4mWARNING\033[0m: symlinks will by nature allow to escape the defined path (default: false)")
var verb = flag.Bool("verb", false, "verbosity")
var skipHidden = flag.Bool("k", true, "\nskip hidden files")
var ro = flag.Bool("ro", false, "read only mode (no upload, rename, move, etc...)")
type rpcCall struct {
Call string `json:"call"`
Args []string `json:"args"`
}
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 := ioutil.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 {
if *skipHidden && strings.HasPrefix(el.Name(), ".") {
continue // dont print hidden files if we're not allowed
}
if !*symlinks && el.Mode()&os.ModeSymlink != 0 {
continue // dont print symlinks if were not allowed
}
el, err := os.Stat(fullPath + "/" + el.Name())
if err != nil {
log.Println("error - cant stat a file", err)
continue
}
href := url.PathEscape(el.Name())
name := el.Name()
if el.IsDir() && strings.HasPrefix(href, "/") {
href = strings.Replace(href, "/", "", 1)
}
if el.IsDir() {
p.RowsFolders = append(p.RowsFolders, rowTemplate{name + "/", template.HTML(href), "", "folder"})
} else {
sl := strings.Split(name, ".")
ext := strings.ToLower(sl[len(sl)-1])
p.RowsFiles = append(p.RowsFiles, rowTemplate{name, template.HTML(href), humanize(el.Size()), ext})
}
}
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()
templateParsed.Execute(gz, p)
} else {
templateParsed.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 := ioutil.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)
}
}
Executable
+26
View File
@@ -0,0 +1,26 @@
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 template0 string
// fill in template
var template1 = strings.Replace(template0, "css_will_be_here", styleCss, 1)
var template2 = strings.Replace(template1, "js_will_be_here", scriptJs, 1)
var template3 = strings.Replace(template2, "favicon_will_be_here", base64.StdEncoding.EncodeToString(faviconSvg), 2)
var templateParsed, _ = template.New("").Parse(template3)
+74 -31
View File
@@ -1,16 +1,14 @@
package main
import (
"archive/zip"
"bytes"
"fmt"
"io/ioutil"
"net/http"
"os"
"os/exec"
"regexp"
"strings"
"testing"
"time"
)
func dieMaybe(t *testing.T, err error) {
@@ -24,11 +22,29 @@ func trimSpaces(str string) string {
return space.ReplaceAllString(str, " ")
}
func get(t *testing.T, url string) string {
func getRaw(t *testing.T, url string) []byte {
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))
}
@@ -93,6 +109,13 @@ func doTestRegular(t *testing.T, url string, testExtra bool) {
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+"../../")
@@ -115,6 +138,30 @@ 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"]}`)
@@ -176,12 +223,20 @@ func doTestRegular(t *testing.T, url string, testExtra bool) {
// ~~~~~~~~~~~~~~~~~
fmt.Println("\r\n~~~~~~~~~~ test symlink, should succeed: ", testExtra)
body0 = get(t, url+"/support/readme.md")
hasReadme := strings.Contains(body0, `the master branch is automatically built and pushed`)
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`)
if !testExtra && hasReadme {
t.Fatal("error symlink reached where illegal")
t.Fatal("error symlink file reached where illegal")
} else if testExtra && !hasReadme {
t.Fatal("error symlink unreachable")
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 {
@@ -274,7 +329,7 @@ func doTestReadonly(t *testing.T, url string) {
path = "%2F%E1%84%92%E1%85%A1%20%E1%84%92%E1%85%A1" // "하 하" encoded
payload = "123 하"
body0 = postDummyFile(t, url, path, payload)
body1 = get(t, url+path)
get(t, url+path)
if body0 == `ok` {
t.Fatal("post file passed - should not be allowed")
}
@@ -282,7 +337,7 @@ func doTestReadonly(t *testing.T, url string) {
// ~~~~~~~~~~~~~~~~~
fmt.Println("\r\n~~~~~~~~~~ test mv rpc")
body0 = postJSON(t, url+"rpc", `{"call":"mv","args":["/AAA", "/hols/AAA"]}`)
body1 = fetchAndTestDefault(t, url)
fetchAndTestDefault(t, url)
if body0 == `ok` {
t.Fatal("mv rpc passed - should not be allowed")
}
@@ -297,33 +352,21 @@ func doTestReadonly(t *testing.T, url string) {
fmt.Printf("\r\n=========\r\n")
}
func startGossa(what string) int {
cmd := exec.Command("/usr/bin/make", what)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stdout
cmd.Start()
time.Sleep(3 * time.Second)
return cmd.Process.Pid
}
func killallgossa() {
cmd := exec.Command("/usr/bin/killall", "gossa")
cmd.Start()
}
func TestGossa(t *testing.T) {
startGossa("run")
func TestNormal(t *testing.T) {
fmt.Println("========== testing normal path ============")
doTestRegular(t, "http://127.0.0.1:8001/", false)
killallgossa()
}
startGossa("run-extra")
func TestExtra(t *testing.T) {
fmt.Println("========== testing extras options ============")
doTestRegular(t, "http://127.0.0.1:8001/fancy-path/", true)
killallgossa()
}
startGossa("run-ro")
func TestRo(t *testing.T) {
fmt.Println("========== testing read only ============")
doTestReadonly(t, "http://127.0.0.1:8001/")
killallgossa()
}
func TestRunMain(t *testing.T) {
main()
}
+28 -18
View File
@@ -4,27 +4,34 @@ gossa
![e](https://user-images.githubusercontent.com/760637/71449335-790a4200-274a-11ea-80be-4c536fbad8a1.gif)
[![build status](https://github.com/pldubouilh/gossa/workflows/ci/badge.svg)](https://github.com/pldubouilh/gossa/actions)
[![docker build status](https://img.shields.io/docker/cloud/build/pldubouilh/gossa.svg?logo=docker)](https://hub.docker.com/r/pldubouilh/gossa)
[![docker pulls](https://img.shields.io/docker/pulls/pldubouilh/gossa.svg?logo=docker)](https://hub.docker.com/r/pldubouilh/gossa)
[![github downloads](https://img.shields.io/github/downloads/pldubouilh/gossa/total.svg?logo=github)](https://github.com/pldubouilh/gossa/releases)
a fast and simple webserver for your files, that's dependency-free and with under 200 lines of code, easy to review.
a fast and simple webserver for your files, that's dependency-free and with under 250 lines of code, easy to review.
a [simple UI](https://github.com/pldubouilh/gossa-ui) comes as default, featuring :
a simple UI comes as default, featuring :
* 🔍 files/directories browser
* 📩 drag-and-drop file/directory uploader
* 🚀 lightweight, default ui weights 110kB and prints in ms
* 🗺️ files handling - move/rename/delete
* 📸 picture browser
* 📽 video streaming
* ✍️ simple text editor
* ⌨️ keyboard shortcuts
* 🥂 fast golang static server, easily fills available bandwidth
* 🔒 easy/secure multi account setup
* 🔍 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
### build
built blobs are available on the [release page](https://github.com/pldubouilh/gossa/releases) - or simply `make build` this repo.
### install / build
[arch linux (AUR)](https://aur.archlinux.org/packages/gossa/) - e.g. `yay -S gossa`
[nix](https://search.nixos.org/packages?channel=unstable&show=gossa&from=0&size=50&sort=relevance&type=packages&query=gossa) - e.g. `nix-shell -p gossa`
binaries are available on the [release page](https://github.com/pldubouilh/gossa/releases) - or simply `make build` this repo.
all builds are reproducible, checkout the hashes on the release page.
### usage
```sh
@@ -33,16 +40,19 @@ built blobs are available on the [release page](https://github.com/pldubouilh/go
% ./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. 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.
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.
### shortcuts
the default UI is fully usable by through keyboard/UI shortcuts - press `Ctrl/Cmd + h` to see them all.
automatic boot-time startup can be handled with a user systemd service - see [support](https://github.com/pldubouilh/gossa/tree/master/support)
-205
View File
@@ -1,205 +0,0 @@
package main
import (
"encoding/json"
"errors"
"flag"
"fmt"
"html"
"html/template"
"io"
"io/ioutil"
"log"
"net/http"
"net/url"
"os"
"path/filepath"
"sort"
"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 extraPath = flag.String("prefix", "/", "url prefix at which gossa can be reached, e.g. /gossa/ (slashes of importance)")
var symlinks = flag.Bool("symlinks", false, "follow symlinks \033[4mWARNING\033[0m: symlinks will by nature allow to escape the defined path (default: false)")
var verb = flag.Bool("verb", false, "verbosity")
var skipHidden = flag.Bool("k", true, "\nskip hidden files")
var ro = flag.Bool("ro", false, "read only mode (no upload, rename, move, etc...)")
var 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
Ro bool
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, fullPath string, path string) {
_files, err := ioutil.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 {
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(), ".")
ext := strings.ToLower(sl[len(sl)-1])
p.RowsFiles = append(p.RowsFiles, rowTemplate{el.Name(), template.HTML(href), humanize(el.Size()), ext})
}
}
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, 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)
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("ok"))
}
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)
if !*ro {
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)
}
-8
View File
@@ -1,8 +0,0 @@
# 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
+5 -6
View File
@@ -1,11 +1,10 @@
FROM golang:latest as builder
FROM golang:1.18 as builder
COPY . /gossaSrc
RUN cd /gossaSrc && make
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
FROM alpine:3.15
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"
RUN apk add --no-cache su-exec
COPY ./support/entrypoint.sh /entrypoint.sh
COPY --from=builder /gossaSrc/gossa /gossa
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" ]
ENTRYPOINT "/entrypoint.sh"
-8
View File
@@ -1,8 +0,0 @@
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
View File
@@ -0,0 +1,2 @@
#!/bin/sh
exec su-exec ${UID}:${GID} /gossa -h ${HOST} -p ${PORT} -k=${SKIP_HIDDEN_FILES} -ro=${READONLY} --symlinks=${FOLLOW_SYMLINKS} --prefix=${PREFIX} --verb=${VERB} ${DATADIR}
+106 -7
View File
@@ -1,27 +1,126 @@
## 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
```
the options are settable through environment variables that can be passed starting off the docker image.
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.
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 🎉
## multi-account setup
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/v1/) 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 caddy config will
+ enable https on the domain myserver.com
+ password protect the access
+ route the root user requests to 1 gossa instance
+ route user1 and user2 requests to a readonly gossa instance
```sh
# 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
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
```
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.
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 caddy config will
+ enable https on the domain myserver.com
+ password protect the access
+ route user1 to own folder
+ route user2 to own folder
+ share a folder between 2 users with a symlink
```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}
}
```
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
```
+13
View File
@@ -0,0 +1,13 @@
[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
View File
@@ -0,0 +1 @@
hidden !
+1
View File
@@ -0,0 +1 @@
../hols
@@ -0,0 +1 @@
I'm hidden by my directory!
+1
View File
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 23 KiB

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