Compare commits

..
Author SHA1 Message Date
Pierre Dubouilh 418af92512 bump go & auto-release from CI 2022-09-15 00:44:24 +02:00
21 changed files with 218 additions and 1828 deletions
-1
View File
@@ -1 +0,0 @@
ui/** linguist-vendored
+7 -5
View File
@@ -6,15 +6,17 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v5 uses: actions/setup-go@v1
with: with:
go-version: 1.23.0 go-version: 1.19.1
id: go id: go
- name: Check out code into the Go module directory
uses: actions/checkout@v1
with:
submodules: true
- name: Run - name: Run
run: make ci run: make ci
+17 -11
View File
@@ -10,25 +10,33 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 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 - name: Set env
run: echo "GIT_TAG=`echo $(git describe --tags --abbrev=0)`" >> $GITHUB_ENV run: echo "GIT_TAG=`echo $(git describe --tags --abbrev=0)`" >> $GITHUB_ENV
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v3 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 - name: Login to DockerHub
uses: docker/login-action@v3 uses: docker/login-action@v1.10.0
with: with:
username: ${{ secrets.DOCKER_USER }} username: ${{ secrets.DOCKER_USER }}
password: ${{ secrets.DOCKER_PASSWORD }} password: ${{ secrets.DOCKER_PASSWORD }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and release on dockerhub - name: Build and release on dockerhub
uses: docker/build-push-action@v6 uses: docker/build-push-action@v2.7.0
with: with:
file: support/build.Dockerfile file: support/build.Dockerfile
push: true push: true
@@ -39,19 +47,17 @@ jobs:
run: make build-all run: make build-all
- name: "Release gh release versioned" - name: "Release gh release versioned"
uses: ncipollo/release-action@v1 uses: ncipollo/release-action@58ae73b360456532aafd58ee170c045abbeaee37
with: with:
allowUpdates: true allowUpdates: true
artifacts: "builds/*" artifacts: "builds/*"
bodyFile: "builds/buildout"
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
- name: "Release gh release latest" - name: "Release gh release latest"
uses: ncipollo/release-action@v1 uses: ncipollo/release-action@58ae73b360456532aafd58ee170c045abbeaee37
with: with:
tag: latest tag: latest
name: Latest name: Latest
allowUpdates: true allowUpdates: true
artifacts: "builds/*" artifacts: "builds/*"
bodyFile: "builds/buildout"
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
+39
View File
@@ -0,0 +1,39 @@
name: rc
on:
push:
branches:
- 'rc/**'
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Set up Go
uses: actions/setup-go@v1
with:
go-version: 1.19.1
id: go
- name: Check out code into the Go module directory
uses: actions/checkout@v1
with:
submodules: true
- name: Run
run: make ci
- name: Build all artifacts
run: make build-all
- name: "Release gh release prerelease"
uses: ncipollo/release-action@58ae73b360456532aafd58ee170c045abbeaee37
with:
allowUpdates: true
prerelease: true
tag: "rc"
artifacts: "builds/*"
token: ${{ secrets.GITHUB_TOKEN }}
+4
View File
@@ -0,0 +1,4 @@
[submodule "gossa-ui"]
path = gossa-ui
url = https://github.com/pldubouilh/gossa-ui
branch = master
Regular → Executable
+31 -32
View File
@@ -1,26 +1,26 @@
FLAGS := -trimpath FLAGS := -ldflags "-s -w" -trimpath
NOCGO := CGO_ENABLED=0 NOCGO := CGO_ENABLED=0
build:: build:
go vet && go fmt go vet && go fmt
${NOCGO} go build ${FLAGS} -o gossa ${NOCGO} go build ${FLAGS} -o gossa
install:: install:
sudo cp gossa /usr/local/bin sudo cp gossa /usr/local/bin
run:: run:
./gossa -verb=true test-fixture ./gossa -verb=true test-fixture
run-ro:: run-ro:
./gossa -verb=true -ro=true test-fixture ./gossa -verb=true -ro=true test-fixture
run-extra:: run-extra:
./gossa -verb=true -prefix="/fancy-path/" -k=false -symlinks=true test-fixture ./gossa -verb=true -prefix="/fancy-path/" -k=false -symlinks=true test-fixture
ci:: build-all test ci: build-all test
echo "done" echo "done"
test:: test:
-@cd test-fixture && ln -s ../support .; true -@cd test-fixture && ln -s ../support .; true
go test -cover -c -tags testrunmain go test -cover -c -tags testrunmain
@@ -43,33 +43,32 @@ test::
# go tool cover -html all.out # go tool cover -html all.out
# go tool cover -func=all.out | grep main | grep '9.\..\%' # go tool cover -func=all.out | grep main | grep '9.\..\%'
watch:: watch:
ls gossa.go gossa_test.go ui/* | entr -rc make build run ls gossa.go gossa_test.go gossa-ui/* | entr -rc make build run
watch-extra:: watch-extra:
ls gossa.go gossa_test.go ui/* | entr -rc make build run-extra ls gossa.go gossa_test.go gossa-ui/* | entr -rc make build run-extra
watch-ro:: watch-ro:
ls gossa.go gossa_test.go ui/* | entr -rc make build run-ro ls gossa.go gossa_test.go gossa-ui/* | entr -rc make build run-ro
watch-test:: watch-test:
ls gossa.go gossa_test.go ui/* | entr -rc make test ls gossa.go gossa_test.go gossa-ui/* | entr -rc make test
build-all:: build build-all: build
go version ${NOCGO} GOOS=linux GOARCH=amd64 go build ${FLAGS} -o builds/gossa-linux-x64
${NOCGO} GOOS=linux GOARCH=amd64 go build ${FLAGS} -o builds/gossa-linux-x64 ${NOCGO} GOOS=linux GOARCH=arm go build ${FLAGS} -o builds/gossa-linux-arm
${NOCGO} GOOS=linux GOARCH=arm go build ${FLAGS} -o builds/gossa-linux-arm ${NOCGO} GOOS=linux GOARCH=arm64 go build ${FLAGS} -o builds/gossa-linux-arm64
${NOCGO} GOOS=linux GOARCH=arm64 go build ${FLAGS} -o builds/gossa-linux-arm64 ${NOCGO} GOOS=darwin GOARCH=amd64 go build ${FLAGS} -o builds/gossa-mac-x64
${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=darwin GOARCH=arm64 go build ${FLAGS} -o builds/gossa-mac-arm64 ${NOCGO} GOOS=windows GOARCH=amd64 go build ${FLAGS} -o builds/gossa-windows.exe
${NOCGO} GOOS=windows GOARCH=amd64 go build ${FLAGS} -o builds/gossa-windows.exe sha256sum builds/*
sha256sum builds/* | tee builds/buildout
clean:: clean:
rm -f gossa -rm gossa
rm -f gossa-linux64 -rm gossa-linux64
rm -f gossa-linux-arm -rm gossa-linux-arm
rm -f gossa-linux-arm64 -rm gossa-linux-arm64
rm -f gossa-mac -rm gossa-mac
rm -f gossa-windows.exe -rm gossa-windows.exe
+1 -1
View File
@@ -1,3 +1,3 @@
module github.com/pldubouilh/gossa module github.com/pldubouilh/gossa
go 1.23.0 go 1.16
Submodule
+1
Submodule gossa-ui added at 5ec0a804c9
Regular → Executable
+36 -83
View File
@@ -3,21 +3,16 @@ package main
import ( import (
"archive/zip" "archive/zip"
"compress/gzip" "compress/gzip"
"crypto/md5"
"crypto/sha1"
"crypto/sha256"
"crypto/sha512"
_ "embed" _ "embed"
"encoding/hex"
"encoding/json" "encoding/json"
"errors" "errors"
"flag" "flag"
"fmt" "fmt"
"hash"
"html" "html"
"html/template" "html/template"
"io" "io"
"io/fs" "io/fs"
"io/ioutil"
"log" "log"
"net/http" "net/http"
"net/url" "net/url"
@@ -28,29 +23,6 @@ import (
"strings" "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 { type rpcCall struct {
Call string `json:"call"` Call string `json:"call"`
Args []string `json:"args"` Args []string `json:"args"`
@@ -88,9 +60,9 @@ func humanize(bytes int64) string {
} }
func replyList(w http.ResponseWriter, r *http.Request, fullPath string, path string) { func replyList(w http.ResponseWriter, r *http.Request, fullPath string, path string) {
files, err := os.ReadDir(fullPath) _files, err := ioutil.ReadDir(fullPath)
check(err) check(err)
sort.Slice(files, func(i, j int) bool { return strings.ToLower(files[i].Name()) < strings.ToLower(files[j].Name()) }) sort.Slice(_files, func(i, j int) bool { return strings.ToLower(_files[i].Name()) < strings.ToLower(_files[j].Name()) })
if !strings.HasSuffix(path, "/") { if !strings.HasSuffix(path, "/") {
path += "/" path += "/"
@@ -105,36 +77,30 @@ func replyList(w http.ResponseWriter, r *http.Request, fullPath string, path str
p.Ro = *ro p.Ro = *ro
p.Title = template.HTML(html.EscapeString(title)) p.Title = template.HTML(html.EscapeString(title))
for _, el := range files { for _, el := range _files {
info, errInfo := el.Info() 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()) el, err := os.Stat(fullPath + "/" + el.Name())
if err != nil || errInfo != nil { if err != nil {
log.Println("error - cant stat a file", err) log.Println("error - cant stat a file", err)
continue 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()) href := url.PathEscape(el.Name())
name := el.Name()
if el.IsDir() && strings.HasPrefix(href, "/") { if el.IsDir() && strings.HasPrefix(href, "/") {
href = strings.Replace(href, "/", "", 1) href = strings.Replace(href, "/", "", 1)
} }
if el.IsDir() { if el.IsDir() {
row := rowTemplate{name + "/", template.URL(href), "", "folder"} p.RowsFolders = append(p.RowsFolders, rowTemplate{el.Name() + "/", template.HTML(href), "", "folder"})
p.RowsFolders = append(p.RowsFolders, row)
} else { } else {
sl := strings.Split(name, ".") sl := strings.Split(el.Name(), ".")
ext := strings.ToLower(sl[len(sl)-1]) ext := strings.ToLower(sl[len(sl)-1])
row := rowTemplate{name, template.URL(href), humanize(el.Size()), ext} p.RowsFiles = append(p.RowsFiles, rowTemplate{el.Name(), template.HTML(href), humanize(el.Size()), ext})
p.RowsFiles = append(p.RowsFiles, row)
} }
} }
@@ -144,9 +110,9 @@ func replyList(w http.ResponseWriter, r *http.Request, fullPath string, path str
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) 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) check(err)
defer gz.Close() defer gz.Close()
tmpl.Execute(gz, p) templateParsed.Execute(gz, p)
} else { } else {
tmpl.Execute(w, p) templateParsed.Execute(w, p)
} }
} }
@@ -233,42 +199,21 @@ func zipRPC(w http.ResponseWriter, r *http.Request) {
func rpc(w http.ResponseWriter, r *http.Request) { func rpc(w http.ResponseWriter, r *http.Request) {
var err error var err error
var rpc rpcCall var rpc rpcCall
defer exitPath(w, "rpc", &rpc) defer exitPath(w, "rpc", rpc)
bodyBytes, err := io.ReadAll(r.Body) bodyBytes, err := ioutil.ReadAll(r.Body)
check(err) check(err)
json.Unmarshal(bodyBytes, &rpc) json.Unmarshal(bodyBytes, &rpc)
ret := []byte("ok")
switch rpc.Call { if rpc.Call == "mkdirp" {
case "mkdirp":
err = os.MkdirAll(enforcePath(rpc.Args[0]), os.ModePerm) err = os.MkdirAll(enforcePath(rpc.Args[0]), os.ModePerm)
case "mv": } else if rpc.Call == "mv" {
err = os.Rename(enforcePath(rpc.Args[0]), enforcePath(rpc.Args[1])) err = os.Rename(enforcePath(rpc.Args[0]), enforcePath(rpc.Args[1]))
case "rm": } else if rpc.Call == "rm" {
err = os.RemoveAll(enforcePath(rpc.Args[0])) err = os.RemoveAll(enforcePath(rpc.Args[0]))
case "sum":
file, err := os.Open(enforcePath(rpc.Args[0]))
check(err)
var hash hash.Hash
switch rpc.Args[1] {
case "md5":
hash = md5.New()
case "sha1":
hash = sha1.New()
case "sha256":
hash = sha256.New()
case "sha512":
hash = sha512.New()
}
_, err = io.Copy(hash, file)
check(err)
checksum := hash.Sum(nil)
ret = make([]byte, hex.EncodedLen(len(checksum)))
hex.Encode(ret, checksum)
} }
check(err) check(err)
w.Write(ret) w.Write([]byte("ok"))
} }
func enforcePath(p string) string { func enforcePath(p string) string {
@@ -288,10 +233,10 @@ func enforcePath(p string) string {
} }
func main() { func main() {
if flag.Parse(); len(flag.Args()) == 1 { if flag.Parse(); len(flag.Args()) > 0 {
rootPath = flag.Args()[0] rootPath = flag.Args()[0]
} else { } else {
fmt.Printf("\nusage: ./gossa [OPTIONS] ~/directory-to-share\n\n") fmt.Printf("\nusage: ./gossa ~/directory-to-share\n\n")
flag.PrintDefaults() flag.PrintDefaults()
os.Exit(1) os.Exit(1)
} }
@@ -301,6 +246,16 @@ func main() {
check(err) check(err)
server := &http.Server{Addr: *host + ":" + *port, Handler: handler} server := &http.Server{Addr: *host + ":" + *port, Handler: handler}
// clean shutdown - used only for coverage test
// go func() {
// sigchan := make(chan os.Signal, 1)
// signal.Notify(sigchan, os.Interrupt)
// <-sigchan
// ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
// defer cancel()
// server.Shutdown(ctx)
// }()
if !*ro { if !*ro {
http.HandleFunc(*extraPath+"rpc", rpc) http.HandleFunc(*extraPath+"rpc", rpc)
http.HandleFunc(*extraPath+"post", upload) http.HandleFunc(*extraPath+"post", upload)
@@ -309,9 +264,7 @@ func main() {
http.HandleFunc("/", doContent) http.HandleFunc("/", doContent)
handler = http.StripPrefix(*extraPath, http.FileServer(http.Dir(rootPath))) handler = http.StripPrefix(*extraPath, http.FileServer(http.Dir(rootPath)))
fmt.Printf("Gossa starting on directory %s\n", rootPath) fmt.Printf("Gossa starting on directory %s\nListening on http://%s:%s%s\n", rootPath, *host, *port, *extraPath)
fmt.Printf("Verbose: %t, Symlinks: %t, Read-Only: %t, Hidden-Files Skipped: %t\n", *verb, *symlinks, *ro, *skipHidden)
fmt.Printf("Listening on http://%s:%s%s\n", *host, *port, *extraPath)
if err = server.ListenAndServe(); err != http.ErrServerClosed { if err = server.ListenAndServe(); err != http.ErrServerClosed {
check(err) check(err)
} }
Regular → Executable
+32 -16
View File
@@ -3,32 +3,48 @@ package main
import ( import (
_ "embed" _ "embed"
"encoding/base64" "encoding/base64"
"flag"
"html/template" "html/template"
"strings" "strings"
) )
//go:embed ui/script.js //go:embed gossa-ui/script.js
var scriptJs string var scriptJs string
//go:embed ui/style.css //go:embed gossa-ui/style.css
var styleCss string var styleCss string
//go:embed ui/favicon.svg //go:embed gossa-ui/favicon.svg
var faviconSvg []byte var faviconSvg []byte
//go:embed ui/ui.tmpl //go:embed gossa-ui/ui.tmpl
var uiTmpl string var templateStr string
var tmpl *template.Template
// fill in template // fill in template
func init() { var templateCss = strings.Replace(templateStr, "css_will_be_here", styleCss, 1)
var err error var templateCssJs = strings.Replace(templateCss, "js_will_be_here", scriptJs, 1)
t := strings.Replace(uiTmpl, "css_will_be_here", styleCss, 1) var templateCssJssIcon = strings.Replace(templateCssJs, "favicon_will_be_here", base64.StdEncoding.EncodeToString(faviconSvg), 2)
t = strings.Replace(t, "js_will_be_here", scriptJs, 1) var templateParsed, _ = template.New("").Parse(templateCssJssIcon)
t = strings.Replace(t, "favicon_will_be_here", base64.StdEncoding.EncodeToString(faviconSvg), 2)
tmpl, err = template.New("").Parse(t) type rowTemplate struct {
if err != nil { Name string
panic(err) 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...)")
-18
View File
@@ -204,17 +204,6 @@ func doTestRegular(t *testing.T, url string, testExtra bool) {
t.Fatal("post file incorrect path didnt errored") 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") fmt.Println("\r\n~~~~~~~~~~ test mv rpc")
body0 = postJSON(t, url+"rpc", `{"call":"mv","args":["/AAA", "/hols/AAA"]}`) body0 = postJSON(t, url+"rpc", `{"call":"mv","args":["/AAA", "/hols/AAA"]}`)
@@ -238,8 +227,6 @@ func doTestRegular(t *testing.T, url string, testExtra bool) {
hasListing := strings.Contains(body0, `readme.md`) hasListing := strings.Contains(body0, `readme.md`)
body1 = get(t, url+"/support/readme.md") body1 = get(t, url+"/support/readme.md")
hasReadme := strings.Contains(body1, `the master branch is automatically built and pushed`) hasReadme := strings.Contains(body1, `the master branch is automatically built and pushed`)
body2 = get(t, url)
hasMainListing := strings.Contains(body2, `href="support">support/</a>`)
if !testExtra && hasReadme { if !testExtra && hasReadme {
t.Fatal("error symlink file reached where illegal") t.Fatal("error symlink file reached where illegal")
@@ -251,11 +238,6 @@ func doTestRegular(t *testing.T, url string, testExtra bool) {
} else if testExtra && !hasListing { } else if testExtra && !hasListing {
t.Fatal("error symlink folder unreachable") 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")
}
if testExtra { if testExtra {
fmt.Println("\r\n~~~~~~~~~~ test symlink mkdir & cleanup") fmt.Println("\r\n~~~~~~~~~~ test symlink mkdir & cleanup")
+10 -16
View File
@@ -9,32 +9,28 @@ gossa
a fast and simple webserver for your files, that's dependency-free and with under 250 lines of code, easy to review. a fast and simple webserver for your files, that's dependency-free and with under 250 lines of code, easy to review.
a simple UI comes as default, featuring : a [simple UI](https://github.com/pldubouilh/gossa-ui) comes as default, featuring :
* 🔍 files/directories browser & handler * 🔍 files/directories browser & handler
* 📩 drag-and-drop uploader * 📩 drag-and-drop uploader
* 🥂 fast golang static server
* 💾 90s web UI that prints in milliseconds * 💾 90s web UI that prints in milliseconds
* 📸 video streaming, picture browser, pdf viewer * 📸 video streaming & picture browser
* ✍️ simple note editor * ✍️ simple note editor
* ⌨️ keyboard navigation * ⌨️ keyboard navigation
* 🚀 lightweight and dependency free codebase * 🚀 lightweight and dependency free codebase
* 🔒 >95% test coverage and reproducible builds * 🔒 >95% test coverage and reproducible builds
* 🥂 fast golang static server
* 💑 easy multi account setup, read-only mode * 💑 easy multi account setup, read-only mode
* ✨ PWA-able * ✨ PWA enabled
* 🖥️ multi-platform support
### install / build
[arch linux (AUR)](https://aur.archlinux.org/packages/gossa/) - e.g. `yay -S gossa`
[nix](https://search.nixos.org/packages?channel=unstable&show=gossa&from=0&size=50&sort=relevance&type=packages&query=gossa) - e.g. `nix-shell -p gossa`
[mpr](https://mpr.makedeb.org/packages/gossa)
binaries are available on the [release page](https://github.com/pldubouilh/gossa/releases)
### build
built blobs are available on the [release page](https://github.com/pldubouilh/gossa/releases) - or simply `make build` this repo.
all builds are reproducible, checkout the hashes on the release page. all builds are reproducible, checkout the hashes on the release page.
arch linux users can also install through the [user repos](https://aur.archlinux.org/packages/gossa/) - e.g. `yay -S gossa`
automatic boot-time startup can be handled with a user systemd service - see [support](https://github.com/pldubouilh/gossa/tree/master/support)
### usage ### usage
```sh ```sh
% ./gossa --help % ./gossa --help
@@ -50,11 +46,9 @@ release images are pushed to [dockerhub](https://hub.docker.com/r/pldubouilh/gos
```sh ```sh
# pull from dockerhub and run # pull from dockerhub and run
% mkdir ~/LocalDirToShare
% sudo docker run -v ~/LocalDirToShare:/shared -p 8001:8001 pldubouilh/gossa % sudo docker run -v ~/LocalDirToShare:/shared -p 8001:8001 pldubouilh/gossa
``` ```
in a do-one-thing-well mindset, HTTPS and authentication has been left to middlewares and proxies. [sample caddy configs](https://github.com/pldubouilh/gossa/blob/master/support/) are available to quickly setup multi users setups along with https. in a do-one-thing-well mindset, HTTPS and authentication has been left to middlewares and proxies. [sample caddy configs](https://github.com/pldubouilh/gossa/blob/master/support/) are available to quickly setup multi users setups along with https.
automatic boot-time startup can be handled with a user systemd service - see [support](https://github.com/pldubouilh/gossa/tree/master/support)
+6 -12
View File
@@ -1,16 +1,10 @@
FROM docker.io/library/golang:1.23.0-alpine AS builder FROM golang:1.18 as builder
RUN apk add --no-cache make
WORKDIR /gossaSrc
COPY . /gossaSrc COPY . /gossaSrc
RUN make RUN cd /gossaSrc && make
FROM docker.io/library/alpine:3.20 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" 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 COPY --from=builder /gossaSrc/gossa /gossa
RUN addgroup -g ${GID} user \ ENTRYPOINT "/entrypoint.sh"
&& 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
+1 -24
View File
@@ -2,29 +2,9 @@ version: '2'
services: services:
gossa-server: gossa-server:
image: docker.io/pldubouilh/gossa:latest image: pldubouilh/gossa
container_name: gossa container_name: gossa
restart: always 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: ports:
- 8001:8001 - 8001:8001
volumes: volumes:
@@ -34,6 +14,3 @@ services:
# - "traefik.port=8001" # - "traefik.port=8001"
# - "traefik.backend=gossa" # - "traefik.backend=gossa"
# - "traefik.frontend.rule=Host:${GOSSA}.${DOMAIN}" # - "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
+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}
+31 -100
View File
@@ -38,67 +38,36 @@ if you prefer building the image yourself :
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 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.
## multi-account setup with Caddy ## 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) is used here as an example, but other proxy can be used in a similar fashion. 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 ### example 1 root, multiple read-only users
This sample Caddyfile will this sample caddy config will
+ enable https on the domain myserver.com (http will be automatically redirected to https) + enable https on the domain myserver.com
+ password protect the access + password protect the access
+ route the root user requests to 1 gossa instance + route the root user requests to 1 gossa instance
+ route user1 and user2 requests to a readonly 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
```sh ```sh
myserver.com 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 # proxy regular and read only instance
handle @isroot { proxy / 127.0.0.1:8001
reverse_proxy 127.0.0.1:8001 proxy /ro 127.0.0.1:8002 { without /ro }
}
# route non-root user to read only instance # reroute non-root user to read-only
handle { # cm9... is the output of `printf "root:password" | base64`
reverse_proxy 127.0.0.1:8002 rewrite {
if {>Authorization} not "Basic cm9vdDpwYXNzd29yZA=="
to /ro/{path}
} }
# gate access
basicauth / root password
basicauth / ro_user1 passworduser1
basicauth / ro_user2 passworduser2
``` ```
then simply start the 2 gossa instances, and caddy then simply start the 2 gossa instances, and caddy
@@ -116,59 +85,30 @@ then simply start the 2 gossa instances, and caddy
### example 2 users on 2 different folders ### example 2 users on 2 different folders
This sample Caddyfile will this sample caddy config will
+ enable https on the domain myserver.com (http will be automatically redirected to https) + enable https on the domain myserver.com
+ password protect the access + password protect the access
+ route user1 to own folder + route user1 to own folder
+ route user2 to own folder + route user2 to own folder
+ share a folder between 2 users with a symlink + 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 ```sh
myserver.com myserver.com
basic_auth { proxy /user1 127.0.0.1:8001 { without /user1 }
user1 $2a$14$Zkx19XLiW6VYouLHR5NmfOFU0z2GTNmpkT/5qqR7hx4IjWJPDhjvG # password is "hiccup" proxy /user2 127.0.0.1:8002 { without /user2 }
user2 $2a$14$Zkx19XLiW6VYouLHR5NmfOFU0z2GTNmpkT/5qqR7hx4IjWJPDhjvG # password is "hiccup"
basicauth / user1 passworduser1
basicauth / user2 passworduser2
rewrite {
if {>Authorization} is "Basic dXNlcjE6cGFzc3dvcmR1c2VyMQ=="
to /user1/{path}
} }
@user1auth { rewrite {
vars {http.auth.user.id} user1 if {>Authorization} is "Basic dXNlcjI6cGFzc3dvcmR1c2VyMg=="
} to /user2/{path}
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
} }
``` ```
@@ -184,12 +124,3 @@ start 2 gossa instances, and caddy
% ./gossa -p 8002 -symlinks=true test/user2 & % ./gossa -p 8002 -symlinks=true test/user2 &
% ./caddy % ./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;
```
-1
View File
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 23 KiB

-4
View File
@@ -1,4 +0,0 @@
gossa-ui
========
a ~~ugly~~ simple, dependency-free, plain js/css/html front-end for [gossa](https://github.com/pldubouilh/gossa)
-925
View File
@@ -1,925 +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)
const sumCall = (path, type, cb) => rpc('sum', [prependPath(path), type], 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() || sumsOff() || 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
}
// checksums
function getSum (type) {
upBarPc.style.display = 'block'
upBarPc.innerText = 'computing checksum...'
upBarPc.style.width = '100%'
sumsOff()
sumCall(getASelected().innerText, type, loaded => {
navigator.clipboard.writeText(loaded.target.responseText)
upBarPc.style.display = 'none'
flicker(okBadge)
})
}
const isSumsMode = () => sums.style.display === 'block'
const sumsToggle = () => isSumsMode() ? sumsOff() : sumsOn()
function sumsOn () {
if (isFolder(getASelected())) {
alert('cannot checksum a directory')
return
}
sums.style.display = 'block'
table.style.display = 'none'
}
window.sumsOff = sumsOff
function sumsOff () {
if (!isSumsMode()) return
sums.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
}
// Modifier keys
if (!e.shiftKey) {
if (e.ctrlKey || e.metaKey) {
switch (e.code) {
case 'KeyC':
return prevent(e) || isRo() || cpPath()
case 'KeyH':
return prevent(e) || isRo() || helpToggle()
case 'KeyZ':
return prevent(e) || isRo() || sumsToggle()
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())
}
} else if (isSumsMode()) {
switch (e.code) {
case 'Digit1':
return prevent(e) || isRo() || getSum('sha1')
case 'Digit2':
return prevent(e) || isRo() || getSum('sha256')
case 'Digit3':
return prevent(e) || isRo() || getSum('sha512')
case 'Digit5':
return prevent(e) || isRo() || getSum('md5')
}
}
} else {
// Workaround Firefox requirement for transient activation
// https://developer.mozilla.org/en-US/docs/Web/Security/User_activation
// Firefox requires user interaction (that is not reserved by the user agent)
// before a file picker can be displayed. This means that ctrl/meta are not
// usable as modifiers until the user clicks the page or presses another
// non-modifier key. To work around this, the shift key can be used, instead.
if (e.code == 'KeyU') {
return prevent(e) || isRo() || manualUpload.click()
}
}
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()
-481
View File
File diff suppressed because one or more lines are too long
-98
View File
@@ -1,98 +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="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/Shift + 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/Meta + Z</td><td>copy checksums of selected file</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 onclick="window.sumsOff()" style="display: none;" id="sums"><table id="sumsTable"><tbody>
<tr><td>Key</td><td>Hash Algorithm</td></tr>
<tr><td>1</td><td>copy sha1 sum</td></tr>
<tr><td>2</td><td>copy sha256 sum</td></tr>
<tr><td>3</td><td>copy sha512 sum</td></tr>
<tr><td>5</td><td>copy md5 sum</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" multiple 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>