Compare commits
76 Commits
Author | SHA1 | Date |
---|---|---|
Alexander Andreev | df3ea75ca9 | |
Alexander Andreev | cbe0292e4c | |
Alexander Andreev | d6463e81e8 | |
Alexander Andreev | 99e39fc6c1 | |
Alexander Andreev | 5cc9b12030 | |
Alexander Andreev | 8b39e60a01 | |
Alexander Andreev | c06eaecbfc | |
Alexander Andreev | f23e4d713b | |
Alexander Andreev | 1a5d32f1b9 | |
Alexander Andreev | 775eef657e | |
Alexander Andreev | 06961fc7c4 | |
Alexander Andreev | 391f589a90 | |
Alexander Andreev | 404f88c099 | |
Alexander Andreev | 54bc8d744d | |
Alexander Andreev | 6cf74599cc | |
Alexander Andreev | 4657319d52 | |
Alexander Andreev | c5ffe37c52 | |
Alexander Andreev | 0e54693b4a | |
Alexander Andreev | cc8634fdb5 | |
Alexander Andreev | 8038a0d551 | |
Alexander Andreev | aa64903161 | |
Alexander Andreev | 15af164462 | |
Alexander Andreev | ed62b37dbc | |
Alexander Andreev | 80647144b5 | |
Alexander Andreev | 7d1da65f38 | |
Alexander Andreev | eaa49744af | |
Alexander Andreev | 24e46c79e7 | |
Alexander Andreev | 56399e85bb | |
Alexander Andreev | def915607b | |
Alexander Andreev | 85e5095120 | |
Alexander Andreev | e1107e94eb | |
Alexander Andreev | 2b5c26b2db | |
Alexander Andreev | 277333b1cb | |
Alexander Andreev | bdcdecb612 | |
Alexander Andreev | 3d8ee35053 | |
Alexander Andreev | d430b09178 | |
Alexander Andreev | 920f9f08ee | |
Alexander Andreev | 0798ba6602 | |
Alexander Andreev | a567a27c69 | |
Alexander Andreev | 71e7765ba8 | |
Alexander Andreev | e03a0e320e | |
Alexander Andreev | 007ab59c81 | |
Alexander Andreev | b793da40d4 | |
Alexander Andreev | 760d08dcde | |
Alexander Andreev | 2c583201d7 | |
Alexander Andreev | 38b5e6ae6e | |
Alexander Andreev | 17e346f3b6 | |
Alexander Andreev | 5678d3e65a | |
Alexander Andreev | f04c6424a1 | |
Alexander Andreev | 50d2c6a8bd | |
Alexander Andreev | 182726f3e3 | |
Alexander Andreev | b63e3ecbf5 | |
Alexander Andreev | 714d0816b5 | |
Alexander Andreev | 1adda7d437 | |
Alexander Andreev | 3fa12d8950 | |
Alexander Andreev | 7da359ddda | |
Alexander Andreev | 23737319d2 | |
Alexander Andreev | 1021945108 | |
Alexander Andreev | d29eb84c3f | |
Alexander Andreev | ed17b83aae | |
Alexander Andreev | 4ef2a77c25 | |
Alexander Andreev | f6dd6b76d0 | |
Alexander Andreev | 3830588748 | |
Alexander Andreev | ca35aa6019 | |
Alexander Andreev | 151cb86df0 | |
Alexander Andreev | 2667dfd07e | |
Alexander Andreev | 9827c5f731 | |
Alexander Andreev | 5a47a0a5e1 | |
Alexander Andreev | fa8f439dec | |
Alexander Andreev | 71733ec429 | |
Alexander Andreev | f52ffc74ce | |
Alexander Andreev | 8f60fa1d77 | |
Alexander Andreev | 3aa9186f9e | |
Alexander Andreev | f15cf88ad9 | |
Alexander Andreev | 7fc6daa50f | |
Alexander Andreev | b6aa765183 |
|
@ -1,3 +1,5 @@
|
|||
bin/*
|
||||
!bin/.keep
|
||||
.vscode
|
||||
.vscode
|
||||
web/*.jade.go
|
||||
web/jade.go
|
2
LICENSE
2
LICENSE
|
@ -1,4 +1,4 @@
|
|||
Copyright (c) 2022 Alexander "Arav" Andreev <me@arav.top>
|
||||
Copyright (c) 2022,2023 Alexander "Arav" Andreev <me@arav.su>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
|
|
38
Makefile
38
Makefile
|
@ -1,30 +1,40 @@
|
|||
TARGET=dwelling-files
|
||||
|
||||
SYSCTL=${shell which systemctl}
|
||||
SYSDDIR_=${shell pkg-config systemd --variable=systemdsystemunitdir}
|
||||
SYSDDIR=${SYSDDIR_:/%=%}
|
||||
DESTDIR=/
|
||||
|
||||
LDFLAGS=-ldflags "-s -w" -tags osusergo,netgo
|
||||
DESTDIR:=
|
||||
PREFIX:=/usr/local
|
||||
|
||||
all: ${TARGET}
|
||||
VERSION=23.32.0
|
||||
|
||||
.PHONY: ${TARGET}
|
||||
FLAGS=-buildmode=pie -modcacherw -mod=readonly -trimpath
|
||||
LDFLAGS=-ldflags "-s -w -X main.version=${VERSION}" -tags osusergo,netgo
|
||||
|
||||
${TARGET}:
|
||||
go build -o bin/$@ ${LDFLAGS} cmd/$@/main.go
|
||||
.PHONY: run install uninstall clean
|
||||
|
||||
${TARGET}: web/*.jade.go
|
||||
go build -o bin/$@ ${LDFLAGS} ${FLAGS} cmd/$@/main.go
|
||||
|
||||
web/*.jade.go: web/templates/*.jade
|
||||
ifeq (,$(wildcard $(shell go env GOPATH)/bin/jade))
|
||||
go install github.com/Joker/jade/cmd/jade@latest
|
||||
endif
|
||||
go generate web/web.go
|
||||
|
||||
run:
|
||||
bin/${TARGET} -path ~ -listen 127.0.0.1:19135
|
||||
bin/${TARGET} -file-handling -path /mnt/data -listen 127.0.0.1:19135
|
||||
|
||||
install:
|
||||
install -Dm 0755 bin/${TARGET} ${DESTDIR}usr/bin/${TARGET}
|
||||
install -Dm 0644 LICENSE ${DESTDIR}usr/share/licenses/${TARGET}/LICENSE
|
||||
install -Dm 0755 bin/${TARGET} ${DESTDIR}${PREFIX}/bin/${TARGET}
|
||||
|
||||
install -Dm 0644 init/systemd/${TARGET}.service ${DESTDIR}${SYSDDIR}/${TARGET}.service
|
||||
install -Dm 0644 init/systemd/${TARGET}.service ${DESTDIR}/${SYSDDIR}/${TARGET}.service
|
||||
|
||||
uninstall:
|
||||
rm ${DESTDIR}usr/bin/${TARGET}
|
||||
rm ${DESTDIR}usr/share/licenses/${TARGET}/LICENSE
|
||||
rm ${DESTDIR}${PREFIX}/bin/${TARGET}
|
||||
|
||||
rm ${DESTDIR}${SYSDDIR}/${TARGET}.service
|
||||
rm ${DESTDIR}/${SYSDDIR}/${TARGET}.service
|
||||
|
||||
clean:
|
||||
rm -f web/*.jade.go
|
||||
go clean
|
|
@ -1,30 +1,24 @@
|
|||
# Maintainer: Alexander "Arav" Andreev <me@arav.top>
|
||||
pkgname=dwelling-files-git
|
||||
pkgver=22.27.0
|
||||
# Maintainer: Alexander "Arav" Andreev <me@arav.su>
|
||||
pkgname=dwelling-files
|
||||
pkgver=23.32.0
|
||||
pkgrel=1
|
||||
pkgdesc="Arav's dwelling / Files"
|
||||
arch=('i686' 'x86_64' 'arm' 'armv6h' 'armv7h' 'aarch64')
|
||||
url="https://git.arav.top/Arav/dwelling-files"
|
||||
url="https://git.arav.su/Arav/dwelling-files"
|
||||
license=('MIT')
|
||||
groups=()
|
||||
depends=()
|
||||
makedepends=('git' 'go')
|
||||
makedepends=('go>=1.16')
|
||||
provides=('dwelling-files')
|
||||
conflicts=('dwelling-files')
|
||||
replaces=()
|
||||
backup=()
|
||||
options=()
|
||||
install=
|
||||
source=('dwelling-files-git::git+https://git.arav.top/Arav/dwelling-files.git')
|
||||
noextract=()
|
||||
source=("${pkgver}.tar.gz::https://git.arav.su/Arav/dwelling-files/archive/v${pkgver}.tar.gz")
|
||||
md5sums=('SKIP')
|
||||
|
||||
build() {
|
||||
cd "$srcdir/$pkgname"
|
||||
make DESTDIR="$pkgdir/"
|
||||
export GOPATH="$srcdir"/gopath
|
||||
make VERSION=$pkgver DESTDIR="$pkgdir" PREFIX="/usr"
|
||||
}
|
||||
|
||||
package() {
|
||||
cd "$srcdir/$pkgname"
|
||||
make DESTDIR="$pkgdir/" install
|
||||
make DESTDIR="$pkgdir" PREFIX="/usr" install
|
||||
}
|
||||
|
|
|
@ -1,22 +1,24 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"dwelling-files/internal/handlers"
|
||||
"dwelling-files/pkg/server"
|
||||
dwhttp "dwelling-files/internal/http"
|
||||
"dwelling-files/web"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/netip"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"git.arav.su/Arav/httpr"
|
||||
)
|
||||
|
||||
var listenAddress *string = flag.String("listen", "/var/run/dwelling-files/f.sock", "listen address (ip:port|unix_path)")
|
||||
var version string
|
||||
|
||||
var listenAddress *string = flag.String("listen", "/var/run/dwelling-files/sock", "listen address (ip:port|unix_path)")
|
||||
var directoryPath *string = flag.String("path", "/srv/ftp", "path to file share")
|
||||
var disableFileHandler *bool = flag.Bool("no-file-handling", false, "disable file handling if it is handled by something else (e.g. NGiNX)")
|
||||
var enableFileHandler *bool = flag.Bool("file-handling", false, "enable file handling if it is handled by something else (e.g. NGiNX)")
|
||||
var showVersion *bool = flag.Bool("v", false, "show version")
|
||||
|
||||
func main() {
|
||||
|
@ -24,33 +26,20 @@ func main() {
|
|||
log.SetFlags(0)
|
||||
|
||||
if *showVersion {
|
||||
fmt.Println("dwelling-files ver. 22.27.0\nCopyright (c) 2022 Alexander \"Arav\" Andreev <me@arav.top>")
|
||||
fmt.Println("dwelling-files ver.", version, "\nCopyright (c) 2023 Alexander \"Arav\" Andreev <me@arav.su>")
|
||||
return
|
||||
}
|
||||
|
||||
hand := handlers.New(directoryPath, web.Assets(), *disableFileHandler)
|
||||
srv := server.NewHttpServer()
|
||||
hand := dwhttp.New(directoryPath, !*enableFileHandler)
|
||||
r := httpr.New()
|
||||
|
||||
srv.GET("/*filepath", hand.Index)
|
||||
r.ServeStatic("/assets/*filepath", web.Assets())
|
||||
r.Handler(http.MethodGet, "/file/*filepath", hand.File)
|
||||
r.Handler(http.MethodGet, "/*filepath", hand.Index)
|
||||
r.Handler(http.MethodGet, "/", hand.Index)
|
||||
|
||||
var network string
|
||||
if !strings.ContainsRune(*listenAddress, ':') {
|
||||
network = "unix"
|
||||
defer os.Remove(*listenAddress)
|
||||
} else {
|
||||
ap, err := netip.ParseAddrPort(*listenAddress)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
if ap.Addr().Is4() {
|
||||
network = "tcp4"
|
||||
} else if ap.Addr().Is6() {
|
||||
network = "tcp6"
|
||||
}
|
||||
}
|
||||
|
||||
if err := srv.Start(network, *listenAddress); err != nil {
|
||||
srv := dwhttp.NewHttpServer(r)
|
||||
if err := srv.Start(*listenAddress); err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
server {
|
||||
listen 443 ssl http2;
|
||||
listen 8092; # Tor
|
||||
listen 443 http2;
|
||||
listen 127.0.0.1:8112; # I2P
|
||||
listen [300:a98d:d6d0:8a08::d]:80; # Yggdrasil
|
||||
|
||||
|
||||
server_name files.arav.top files.arav.i2p qf5e43nlhvnrutmikuvbdfj3cmtthokpbaxtkm6mjlslttzvtgm4fxid.onion;
|
||||
server_name files.arav.su files.arav.i2p qf5e43nlhvnrutmikuvbdfj3cmtthokpbaxtkm6mjlslttzvtgm4fxid.onion;
|
||||
|
||||
|
||||
access_log /var/log/nginx/dwelling/files.log main if=$nolog;
|
||||
|
||||
|
||||
ssl_certificate /etc/letsencrypt/live/arav.top/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/arav.top/privkey.pem;
|
||||
ssl_certificate /etc/letsencrypt/live/arav.su/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/arav.su/privkey.pem;
|
||||
|
||||
|
||||
add_header Content-Security-Policy "default-src 'none'; script-src 'self'; style-src 'self'; img-src 'self'; media-src 'self'; object-src 'none'; frame-src 'none'; frame-ancestors 'none'; font-src 'self'; form-action 'self'";
|
||||
|
@ -23,11 +23,10 @@ server {
|
|||
|
||||
|
||||
location / {
|
||||
proxy_pass http://unix:/var/run/dwelling-files/f.sock;
|
||||
proxy_pass http://unix:/var/run/dwelling-files/sock;
|
||||
|
||||
proxy_set_header X-Client-Timezone "$gi2_location_tz";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header Schema "https";
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
|
||||
|
|
4
go.mod
4
go.mod
|
@ -1,5 +1,5 @@
|
|||
module dwelling-files
|
||||
|
||||
go 1.17
|
||||
go 1.16
|
||||
|
||||
require github.com/julienschmidt/httprouter v1.3.0
|
||||
require git.arav.su/Arav/httpr v0.3.2
|
||||
|
|
4
go.sum
4
go.sum
|
@ -1,2 +1,2 @@
|
|||
github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
|
||||
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
|
||||
git.arav.su/Arav/httpr v0.3.2 h1:a+ifu+9+FnQe6p/Kd4kgTDKAFN6zBOJjBTMjbAuHxVk=
|
||||
git.arav.su/Arav/httpr v0.3.2/go.mod h1:z0SVYwe5dBReeVuFU9QH2PmBxICJwchxqY5OfZbeVzU=
|
||||
|
|
|
@ -6,7 +6,7 @@ After=network-online.target
|
|||
Type=simple
|
||||
Restart=on-failure
|
||||
DynamicUser=yes
|
||||
ExecStart=/usr/bin/dwelling-files -path /srv/ftp -listen /var/run/dwelling-files/f.sock
|
||||
ExecStart=/usr/bin/dwelling-files -path /srv/ftp -listen /var/run/dwelling-files/sock
|
||||
|
||||
ReadOnlyPaths=/
|
||||
NoExecPaths=/
|
||||
|
|
|
@ -1,72 +0,0 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"dwelling-files/pkg/files"
|
||||
"dwelling-files/pkg/server"
|
||||
"dwelling-files/pkg/utils"
|
||||
"dwelling-files/web"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/julienschmidt/httprouter"
|
||||
)
|
||||
|
||||
type FilesHandlers struct {
|
||||
directoryPath string
|
||||
assetsServer http.Handler
|
||||
fileServer http.Handler
|
||||
noFileHandling bool
|
||||
}
|
||||
|
||||
func New(directoryPath *string, assetsFS http.FileSystem, noFileHandling bool) *FilesHandlers {
|
||||
var fSrv http.Handler
|
||||
if noFileHandling {
|
||||
fSrv = nil
|
||||
} else {
|
||||
fSrv = http.FileServer(http.Dir(*directoryPath))
|
||||
}
|
||||
return &FilesHandlers{
|
||||
directoryPath: *directoryPath,
|
||||
assetsServer: http.FileServer(assetsFS),
|
||||
fileServer: fSrv,
|
||||
noFileHandling: noFileHandling}
|
||||
}
|
||||
|
||||
func (FilesHandlers) AssetsFS() http.FileSystem {
|
||||
return web.Assets()
|
||||
}
|
||||
|
||||
func (FilesHandlers) Robots(w http.ResponseWriter, r *http.Request) {
|
||||
fc, _ := web.AssetsGetFile("robots.txt")
|
||||
w.Write(fc)
|
||||
}
|
||||
|
||||
func (h *FilesHandlers) Index(w http.ResponseWriter, r *http.Request) {
|
||||
path := httprouter.CleanPath(server.GetURLParam(r, "filepath"))
|
||||
|
||||
if strings.HasPrefix(path, "/assets") {
|
||||
h.assetsServer.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
if strings.HasPrefix(path, "/file") {
|
||||
if h.noFileHandling {
|
||||
w.WriteHeader(http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
r.URL.Path = path[5:]
|
||||
h.fileServer.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
currentPath := files.CurrentPath(path)
|
||||
|
||||
entries, stats, err := files.ScanDirectory(h.directoryPath+path, path)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
web.Index(utils.MainSite(r.Host), currentPath, &stats, &entries, r, w)
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
package http
|
||||
|
||||
import (
|
||||
"dwelling-files/pkg/files"
|
||||
"dwelling-files/pkg/utils"
|
||||
"dwelling-files/web"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"git.arav.su/Arav/httpr"
|
||||
)
|
||||
|
||||
type FilesHandlers struct {
|
||||
directoryPath string
|
||||
fileServer http.Handler
|
||||
noFileHandling bool
|
||||
}
|
||||
|
||||
func New(directoryPath *string, noFileHandling bool) *FilesHandlers {
|
||||
var fSrv http.Handler
|
||||
if noFileHandling {
|
||||
fSrv = nil
|
||||
} else {
|
||||
fSrv = http.FileServer(http.Dir(*directoryPath))
|
||||
}
|
||||
return &FilesHandlers{
|
||||
directoryPath: *directoryPath,
|
||||
fileServer: fSrv,
|
||||
noFileHandling: noFileHandling}
|
||||
}
|
||||
|
||||
func (h *FilesHandlers) Index(w http.ResponseWriter, r *http.Request) {
|
||||
path := "/" + httpr.Param(r, "filepath")
|
||||
if !strings.HasSuffix(path, "/") {
|
||||
path += "/"
|
||||
}
|
||||
|
||||
currentPath := files.CurrentPath(path)
|
||||
|
||||
entries, stats, err := files.ScanDirectory(h.directoryPath+path, path)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
Error(w, http.StatusNotFound, "", "", r.Referer())
|
||||
return
|
||||
}
|
||||
|
||||
web.Index("Files", utils.MainSite(r.Host), currentPath, &stats, &entries, r, w)
|
||||
}
|
||||
|
||||
func (h *FilesHandlers) File(w http.ResponseWriter, r *http.Request) {
|
||||
if h.noFileHandling {
|
||||
Error(w, http.StatusServiceUnavailable, "File handling is turned off.", "", r.Referer())
|
||||
return
|
||||
}
|
||||
r.URL.Path = httpr.Param(r, "filepath")
|
||||
h.fileServer.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func RobotsTxt(w http.ResponseWriter, r *http.Request) {
|
||||
fc, _ := web.AssetsGetFile("robots.txt")
|
||||
w.Write(fc)
|
||||
}
|
||||
|
||||
func Error(w http.ResponseWriter, code int, reason, message, referer string) {
|
||||
w.WriteHeader(code)
|
||||
web.ErrorXXX("/ "+http.StatusText(code), reason, message, referer, code, w)
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type HttpServer struct {
|
||||
s http.Server
|
||||
addr net.Addr
|
||||
}
|
||||
|
||||
func NewHttpServer(r http.Handler) *HttpServer {
|
||||
return &HttpServer{s: http.Server{
|
||||
ReadTimeout: 3 * time.Second,
|
||||
WriteTimeout: 3 * time.Second,
|
||||
Handler: r}}
|
||||
}
|
||||
|
||||
func (s *HttpServer) Start(address string) error {
|
||||
var network string
|
||||
if !strings.ContainsRune(address, ':') {
|
||||
network = "unix"
|
||||
} else {
|
||||
ap, err := netip.ParseAddrPort(address)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if ap.Addr().Is4() {
|
||||
network = "tcp4"
|
||||
} else if ap.Addr().Is6() {
|
||||
network = "tcp6"
|
||||
}
|
||||
}
|
||||
|
||||
listener, err := net.Listen(network, address)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if listener.Addr().Network() == "unix" {
|
||||
os.Chmod(address, 0777)
|
||||
}
|
||||
|
||||
s.addr = listener.Addr()
|
||||
|
||||
go func() {
|
||||
if err = s.s.Serve(listener); err != nil && err != http.ErrServerClosed {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *HttpServer) Stop() error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if s.addr.Network() == "unix" {
|
||||
defer os.Remove(s.addr.String())
|
||||
}
|
||||
|
||||
if err := s.s.Shutdown(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -1,7 +1,6 @@
|
|||
package files
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
@ -10,7 +9,7 @@ import (
|
|||
|
||||
const FileDateFormat = "2006-01-02 15:04:05 MST"
|
||||
|
||||
type DirStats struct {
|
||||
type DirStat struct {
|
||||
Files int64
|
||||
FilesSize string
|
||||
Directories int64
|
||||
|
@ -23,16 +22,21 @@ type DirEntry struct {
|
|||
Size string
|
||||
}
|
||||
|
||||
func ScanDirectory(path, urlBase string) (entries []DirEntry, stats DirStats, err error) {
|
||||
func ScanDirectory(path, urlBase string) (entries []DirEntry, stats DirStat, err error) {
|
||||
dir, err := os.ReadDir(path)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var dirEntries []DirEntry = make([]DirEntry, 0)
|
||||
var fileEntries []DirEntry = make([]DirEntry, 0)
|
||||
var totalFilesSize int64 = 0
|
||||
|
||||
dir, err := ioutil.ReadDir(path)
|
||||
for _, entry := range dir {
|
||||
for _, ent := range dir {
|
||||
entry, _ := ent.Info()
|
||||
|
||||
var isDirLink bool
|
||||
if entry.Mode().Type().String()[0] == 'L' {
|
||||
if entry.Mode().Type()&os.ModeSymlink != 0 {
|
||||
if slp, err := filepath.EvalSymlinks(filepath.Join(path, entry.Name())); err == nil {
|
||||
lStat, _ := os.Lstat(slp)
|
||||
isDirLink = lStat.IsDir()
|
||||
|
@ -48,12 +52,11 @@ func ScanDirectory(path, urlBase string) (entries []DirEntry, stats DirStats, er
|
|||
})
|
||||
stats.Directories++
|
||||
} else {
|
||||
_, _, sz := convertFileSize(entry.Size())
|
||||
fileEntries = append(fileEntries, DirEntry{
|
||||
Name: entry.Name(),
|
||||
Link: "/file" + urlBase + url.PathEscape(entry.Name()),
|
||||
Datetime: entry.ModTime(),
|
||||
Size: sz,
|
||||
Size: convertFileSize(entry.Size()),
|
||||
})
|
||||
|
||||
totalFilesSize += entry.Size()
|
||||
|
@ -61,8 +64,7 @@ func ScanDirectory(path, urlBase string) (entries []DirEntry, stats DirStats, er
|
|||
}
|
||||
}
|
||||
|
||||
_, _, sz := convertFileSize(totalFilesSize)
|
||||
stats.FilesSize = sz
|
||||
stats.FilesSize = convertFileSize(totalFilesSize)
|
||||
|
||||
entries = append(entries, dirEntries...)
|
||||
entries = append(entries, fileEntries...)
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
package files
|
||||
|
||||
import "testing"
|
||||
|
||||
const path = "/mnt/data/music/Various"
|
||||
const urlBase = "/srv/ftp/"
|
||||
|
||||
func BenchmarkScanDirectory(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
/*e, _, _ :=*/ ScanDirectory(path, urlBase)
|
||||
// b.Log(e[len(e)-1], len(e))
|
||||
}
|
||||
}
|
|
@ -8,8 +8,8 @@ import (
|
|||
var sizeSuffixes = [...]string{"B", "KiB", "MiB", "GiB", "TiB"}
|
||||
|
||||
// convertFileSize converts size in bytes down to biggest units it represents.
|
||||
// Returns converted size, unit and, a concatenation of size and unit
|
||||
func convertFileSize(size int64) (float64, string, string) {
|
||||
// Returns a concatenation of a converted size and a unit
|
||||
func convertFileSize(size int64) string {
|
||||
var idx int
|
||||
var fSize float64 = float64(size)
|
||||
for idx = 0; fSize >= 1024; fSize /= 1024 {
|
||||
|
@ -18,5 +18,5 @@ func convertFileSize(size int64) (float64, string, string) {
|
|||
fSizeStr := strconv.FormatFloat(fSize, 'f', 3, 64)
|
||||
fSizeStr = strings.TrimRight(fSizeStr, "0")
|
||||
fSizeStr = strings.TrimSuffix(fSizeStr, ".")
|
||||
return fSize, sizeSuffixes[idx], fSizeStr + " " + sizeSuffixes[idx]
|
||||
return fSizeStr + " " + sizeSuffixes[idx]
|
||||
}
|
||||
|
|
|
@ -1,80 +0,0 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/julienschmidt/httprouter"
|
||||
)
|
||||
|
||||
type HttpServer struct {
|
||||
server *http.Server
|
||||
router *httprouter.Router
|
||||
}
|
||||
|
||||
func NewHttpServer() *HttpServer {
|
||||
r := httprouter.New()
|
||||
return &HttpServer{
|
||||
server: &http.Server{
|
||||
ReadTimeout: 3 * time.Second,
|
||||
WriteTimeout: 3 * time.Second,
|
||||
Handler: r,
|
||||
},
|
||||
router: r,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *HttpServer) GET(path string, handler http.HandlerFunc) {
|
||||
s.router.Handler(http.MethodGet, path, handler)
|
||||
}
|
||||
|
||||
func (s *HttpServer) POST(path string, handler http.HandlerFunc) {
|
||||
s.router.Handler(http.MethodPost, path, handler)
|
||||
}
|
||||
|
||||
func (s *HttpServer) ServeStatic(path string, fsys http.FileSystem) {
|
||||
s.router.ServeFiles(path, fsys)
|
||||
}
|
||||
|
||||
func (s *HttpServer) SetNotFoundHandler(handler http.HandlerFunc) {
|
||||
s.router.NotFound = handler
|
||||
}
|
||||
|
||||
// GetURLParam wrapper around underlying router for getting URL parameters.
|
||||
func GetURLParam(r *http.Request, param string) string {
|
||||
return httprouter.ParamsFromContext(r.Context()).ByName(param)
|
||||
}
|
||||
|
||||
func (s *HttpServer) Start(network, address string) error {
|
||||
listener, err := net.Listen(network, address)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if listener.Addr().Network() == "unix" {
|
||||
os.Chmod(address, 0777)
|
||||
}
|
||||
|
||||
go func() {
|
||||
if err = s.server.Serve(listener); err != nil && err != http.ErrServerClosed {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *HttpServer) Stop() error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if err := s.server.Shutdown(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -12,9 +12,11 @@ func MainSite(host string) string {
|
|||
return "http://arav.i2p"
|
||||
} else if strings.Contains(host, "onion") {
|
||||
return "http://moq7aejnf4xk5k2bkaltli3ftkhusy2mbrd3pj23nrca343ku2mgk4yd.onion"
|
||||
} else if strings.HasPrefix(host, "[300") {
|
||||
return "http://[300:a98d:d6d0:8a08::f]"
|
||||
}
|
||||
|
||||
return "https://arav.top"
|
||||
return "https://arav.su"
|
||||
}
|
||||
|
||||
// ToClientTimezone converts given time to timezone set in a
|
||||
|
|
|
@ -28,6 +28,10 @@
|
|||
background-color: var(--secondary-color);
|
||||
color: var(--background-color); }
|
||||
|
||||
::placeholder { color: var(--primary-color); }
|
||||
|
||||
.hidden { display: none; }
|
||||
|
||||
a {
|
||||
color: var(--primary-color);
|
||||
text-decoration: none; }
|
||||
|
@ -65,6 +69,7 @@ body {
|
|||
font-size: 1.1rem;
|
||||
margin: 0 auto;
|
||||
max-width: 960px;
|
||||
min-height: 100vh;
|
||||
width: 98%; }
|
||||
|
||||
header {
|
||||
|
@ -72,24 +77,22 @@ header {
|
|||
flex-wrap: wrap;
|
||||
justify-content: space-between; }
|
||||
|
||||
#logo {
|
||||
display: block;
|
||||
width: 360px; }
|
||||
header svg { width: 360px; }
|
||||
|
||||
#logo text { fill: var(--text-color); }
|
||||
header svg text { fill: var(--text-color); }
|
||||
|
||||
#logo .logo {
|
||||
header svg text:first-child {
|
||||
font-size: 2rem;
|
||||
font-variant-caps: small-caps;
|
||||
font-weight: bold; }
|
||||
|
||||
header svg text:last-child { font-size: .88rem; }
|
||||
|
||||
@media screen and (-webkit-min-device-pixel-ratio:0) {
|
||||
#logo .logo { font-size: 2.082rem; } }
|
||||
header svg text:first-child { font-size: 2.082rem; } }
|
||||
|
||||
@-moz-document url-prefix() {
|
||||
#logo .logo { font-size: 2rem; } }
|
||||
|
||||
#logo .under { font-size: .88rem; }
|
||||
header svg text:first-child { font-size: 2rem; } }
|
||||
|
||||
nav { margin-top: .5rem; }
|
||||
|
||||
|
@ -101,44 +104,34 @@ nav h1 {
|
|||
|
||||
section { margin-top: 1rem; }
|
||||
|
||||
#overlay {
|
||||
align-items: center;
|
||||
background-color: var(--overlay-background-color);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
max-height: 100%;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
visibility: hidden;
|
||||
input[name="filter"] {
|
||||
background-color: var(--background-color);
|
||||
border: none;
|
||||
border-bottom: 1px solid var(--primary-color);
|
||||
color: var(--text-color);
|
||||
font: inherit;
|
||||
width: 100%; }
|
||||
|
||||
#overlay video,
|
||||
#overlay audio,
|
||||
#overlay img {
|
||||
margin: auto;
|
||||
max-height: 100%;
|
||||
max-width: 86%;
|
||||
}
|
||||
|
||||
#overlay span {
|
||||
color: var(--background-color);
|
||||
text-shadow: 0 0 .3rem var(--secondary-color);
|
||||
}
|
||||
|
||||
table { overflow-y: scroll; width: 100%; }
|
||||
table {
|
||||
overflow-y: scroll;
|
||||
width: 100%; }
|
||||
|
||||
tr { vertical-align: top; }
|
||||
|
||||
tr:hover { background-color: var(--primary-color); color: white; }
|
||||
tr:hover,
|
||||
tr:focus-within {
|
||||
background-color: var(--primary-color);
|
||||
color: white; }
|
||||
|
||||
tr:hover a { color: white; }
|
||||
tr:hover a,
|
||||
tr:focus-within a { color: white; }
|
||||
|
||||
th { text-align: left; }
|
||||
|
||||
th:nth-child(2),
|
||||
th:last-child { width: 1%; white-space: nowrap; }
|
||||
th:last-child {
|
||||
width: 1%;
|
||||
white-space: nowrap; }
|
||||
|
||||
th,
|
||||
td { line-break: strict; }
|
||||
|
@ -146,32 +139,79 @@ td { line-break: strict; }
|
|||
th:nth-child(2),
|
||||
td:nth-child(2) { padding: 0 1rem; }
|
||||
|
||||
td a { display: block; width: 100%; }
|
||||
td a {
|
||||
display: block;
|
||||
width: 100%; }
|
||||
|
||||
td a:hover { transition: none; }
|
||||
|
||||
td:nth-child(2),
|
||||
td:last-child { white-space: nowrap; }
|
||||
|
||||
thead tr th.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
thead tr th.clickable { cursor: pointer; }
|
||||
|
||||
thead tr th.clickable:hover {
|
||||
color: var(--secondary-color);
|
||||
}
|
||||
thead tr th.clickable:hover { color: var(--secondary-color); }
|
||||
|
||||
thead tr th.clickable:not(.sort-up):not(.sort-down)::after {
|
||||
content: '⇅';
|
||||
}
|
||||
thead tr th.clickable:not(.sort-up):not(.sort-down)::after { content: '⇅'; }
|
||||
|
||||
thead tr th.clickable.sort-up::after {
|
||||
content: '↑';
|
||||
}
|
||||
thead tr th.clickable.sort-up::after { content: '↑'; }
|
||||
|
||||
thead tr th.clickable.sort-down::after {
|
||||
content: '↓';
|
||||
}
|
||||
thead tr th.clickable.sort-down::after { content: '↓'; }
|
||||
|
||||
#overlay {
|
||||
align-items: center;
|
||||
background-color: var(--overlay-background-color);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 100%;
|
||||
justify-content: space-between;
|
||||
left: 0;
|
||||
max-height: 100%;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
visibility: hidden;
|
||||
width: 100%; }
|
||||
|
||||
#overlay div {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
width: 100%; }
|
||||
|
||||
#overlay :is(video, audio, img) {
|
||||
margin: auto;
|
||||
max-height: 100%;
|
||||
max-width: 86%; }
|
||||
|
||||
#overlay span {
|
||||
bottom: 0;
|
||||
color: var(--background-color);
|
||||
left: 0;
|
||||
position: fixed;
|
||||
text-shadow: 0 0 .3rem var(--secondary-color);
|
||||
z-index: 999; }
|
||||
|
||||
#overlay button {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--background-color);
|
||||
font: inherit;
|
||||
font-size: 2rem;
|
||||
padding: 0 1rem;
|
||||
height: 100%;
|
||||
z-index: 999; }
|
||||
|
||||
#overlay button:hover {
|
||||
background-color: var(--background-color);
|
||||
color: var(--primary-color); }
|
||||
|
||||
#error {
|
||||
font-size: 3.5rem;
|
||||
line-height: 5rem;
|
||||
text-align: center;
|
||||
margin: 6rem 0; }
|
||||
|
||||
#error h1 { font-size: 8rem; }
|
||||
|
||||
footer {
|
||||
font-size: .8rem;
|
||||
|
@ -181,7 +221,7 @@ footer {
|
|||
@media screen and (max-width: 640px) {
|
||||
header { display: block; }
|
||||
|
||||
#logo {
|
||||
header svg {
|
||||
margin: 0 auto;
|
||||
width: 100%; }
|
||||
|
||||
|
|
|
@ -1,147 +1,260 @@
|
|||
//// OVERLAY FOR VIEWING MEDIA FILES
|
||||
|
||||
const video_formats = ["webm", "mp4"];
|
||||
const video_formats = ["webm", "mp4", "mov"];
|
||||
const audio_formats = ["mp3", "flac", "opus", "ogg", "m4a"];
|
||||
const image_formats = ["jpg", "jpeg", "gif", "png", "bmp", "webp"];
|
||||
|
||||
const overlay = document.getElementById("overlay");
|
||||
let g_scale = 1;
|
||||
const overlay_content = overlay.children[1].firstChild;
|
||||
const overlay_label = overlay.children[1].lastChild;
|
||||
|
||||
const g_tbody = document.getElementsByTagName('tbody')[0];
|
||||
let g_first_row = g_tbody.firstChild;
|
||||
let g_last_row = g_tbody.lastChild;
|
||||
const g_back_row = document.getElementsByTagName("tr")[1];
|
||||
|
||||
let g_scale = 1;
|
||||
let g_current_row = g_back_row;
|
||||
|
||||
|
||||
const file_links = Array.from(g_tbody.children)
|
||||
.filter(e => e.lastChild.innerHTML != "DIR").map(l => l.firstChild.firstChild);
|
||||
|
||||
|
||||
if (localStorage.getItem('audio_volume') == null)
|
||||
localStorage['audio_volume'] = 0.5;
|
||||
localStorage['audio_volume'] = 0.5;
|
||||
|
||||
function mousescroll(e) {
|
||||
e.preventDefault();
|
||||
g_scale = Math.min(Math.max(0.25, g_scale + (e.deltaY * -0.001)), 4);
|
||||
e.target.style.transform = `scale(${g_scale})`;
|
||||
function overlay_close() {
|
||||
overlay_content.children[0].remove();
|
||||
overlay.style.visibility = "hidden";
|
||||
g_scale = 1;
|
||||
}
|
||||
|
||||
function onvolumechange(e) {
|
||||
localStorage['audio_volume'] = e.target.volume;
|
||||
overlay.addEventListener("mouseup", e => {
|
||||
if (e.target.tagName !== "DIV") return;
|
||||
if (e.button === 0) overlay_close(); });
|
||||
|
||||
|
||||
function determine_media_element(path) {
|
||||
path = path.toLowerCase();
|
||||
if (video_formats.some(ext => path.endsWith(ext)))
|
||||
return "video";
|
||||
else if (audio_formats.some(ext => path.endsWith(ext)))
|
||||
return "audio";
|
||||
else if (image_formats.some(ext => path.endsWith(ext)))
|
||||
return "img";
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const ext_filter = (ext, pathname) => pathname.toLowerCase().endsWith(ext);
|
||||
function send_to_overlay(pathname, media_type_element) {
|
||||
if (media_type_element === undefined)
|
||||
return false;
|
||||
if (overlay_content.children.length != 0)
|
||||
overlay_content.children[0].remove();
|
||||
|
||||
function to_overlay(eltyp, pathname) {
|
||||
const el = document.createElement(eltyp);
|
||||
const el_label = document.createElement("span");
|
||||
el_label.textContent = decodeURI(pathname.substr(pathname.lastIndexOf("/") + 1));
|
||||
if (eltyp !== "audio") el.addEventListener('wheel', mousescroll);
|
||||
if (eltyp !== "img") {
|
||||
el.autoplay = el.controls = true;
|
||||
el.addEventListener("volumechange", onvolumechange);
|
||||
el.volume = localStorage['audio_volume'];
|
||||
}
|
||||
el.src = pathname;
|
||||
overlay.appendChild(el);
|
||||
overlay.appendChild(el_label);
|
||||
overlay.style.visibility = "visible";
|
||||
const media_element = document.createElement(media_type_element);
|
||||
media_element.src = pathname;
|
||||
|
||||
overlay_label.textContent = decodeURI(pathname.substr(pathname.lastIndexOf("/") + 1));
|
||||
|
||||
if (media_type_element !== "audio") {
|
||||
media_element.addEventListener("wheel", e => {
|
||||
e.preventDefault();
|
||||
g_scale = Math.min(Math.max(0.25, g_scale + (e.deltaY * -0.001)), 4);
|
||||
e.target.style.transform = `scale(${g_scale})`; });
|
||||
}
|
||||
if (media_type_element !== "img") {
|
||||
media_element.autoplay = media_element.controls = true;
|
||||
media_element.addEventListener("volumechange", e => {
|
||||
localStorage['audio_volume'] = e.target.volume; });
|
||||
media_element.volume = localStorage["audio_volume"];
|
||||
}
|
||||
|
||||
overlay_content.appendChild(media_element);
|
||||
overlay.style.visibility = "visible";
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
document.getElementById("overlay").addEventListener("click", e => {
|
||||
e.target.firstChild.remove();
|
||||
e.target.firstChild.remove();
|
||||
e.target.style.visibility = "hidden";
|
||||
g_scale = 1;
|
||||
function getSibling(isNext = true, upDown = false) {
|
||||
if (upDown && g_current_row == g_back_row)
|
||||
g_current_row = isNext ? g_first_row : g_last_row;
|
||||
else
|
||||
g_current_row = isNext ?
|
||||
( (g_current_row.nextSibling === null) ?
|
||||
(upDown ? g_back_row : g_first_row) : g_current_row.nextSibling )
|
||||
: ( (g_current_row.previousSibling === null) ?
|
||||
(upDown ? g_back_row : g_last_row) : g_current_row.previousSibling );
|
||||
return g_current_row;
|
||||
}
|
||||
|
||||
const [b_prev, b_next] = overlay.getElementsByTagName("button");
|
||||
|
||||
b_prev.addEventListener("click", e => {
|
||||
do {
|
||||
getSibling(false, false);
|
||||
} while (g_current_row.classList.contains("hidden")
|
||||
|| !send_to_overlay(g_current_row.firstChild.firstChild.pathname,
|
||||
determine_media_element(g_current_row.firstChild.firstChild.pathname)));
|
||||
g_current_row.firstChild.firstChild.focus();
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
const file_links = Array.from(document.getElementsByTagName('tr')).slice(2)
|
||||
.filter(e => e.lastChild.innerHTML != "DIR").map(l => l.firstChild.firstChild);
|
||||
b_next.addEventListener("click", e => {
|
||||
do {
|
||||
getSibling(true, false);
|
||||
} while (g_current_row.classList.contains("hidden")
|
||||
|| !send_to_overlay(g_current_row.firstChild.firstChild.pathname,
|
||||
determine_media_element(g_current_row.firstChild.firstChild.pathname)));
|
||||
g_current_row.firstChild.firstChild.focus();
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
file_links.forEach(f => f.addEventListener('click', e => {
|
||||
const pathname = e.target.pathname;
|
||||
if (video_formats.some(ext => ext_filter(ext, pathname)))
|
||||
to_overlay("video", pathname);
|
||||
else if (audio_formats.some(ext => ext_filter(ext, pathname)))
|
||||
to_overlay("audio", pathname);
|
||||
else if (image_formats.some(ext => ext_filter(ext, pathname)))
|
||||
to_overlay("img", pathname);
|
||||
if (overlay.firstChild != null)
|
||||
e.preventDefault();
|
||||
}));
|
||||
|
||||
for (let i = 0; i < file_links.length; ++i)
|
||||
file_links[i].addEventListener("click", e => {
|
||||
g_current_row = e.target.parentNode.parentNode;
|
||||
if (send_to_overlay(e.target.pathname, determine_media_element(e.target.pathname)))
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
//// KEYBOARD HANDLING
|
||||
|
||||
window.addEventListener("keydown", e => {
|
||||
if (e.isComposing)
|
||||
return;
|
||||
|
||||
if (overlay.style.visibility === "hidden" || overlay.style.visibility === "") {
|
||||
switch (e.code) {
|
||||
case "Backspace": if (e.ctrlKey) window.location = "../"; break;
|
||||
case "Home": g_current_row = g_back_row; g_back_row.firstChild.firstChild.focus(); break;
|
||||
case "End": g_current_row = g_last_row; g_last_row.firstChild.firstChild.focus(); break;
|
||||
case "ArrowUp":
|
||||
e.preventDefault();
|
||||
getSibling(false, true);
|
||||
g_current_row.firstChild.firstChild.focus();
|
||||
break;
|
||||
case "ArrowDown":
|
||||
e.preventDefault();
|
||||
getSibling(true, true);
|
||||
g_current_row.firstChild.firstChild.focus();
|
||||
break;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
switch (e.code) {
|
||||
case "ArrowLeft": b_prev.click(); break;
|
||||
case "ArrowRight": b_next.click(); break;
|
||||
case "Escape": overlay_close(); break;
|
||||
case "Space":
|
||||
e.preventDefault();
|
||||
const el = overlay_content.firstChild;
|
||||
if (el.paused !== undefined)
|
||||
el.paused ? el.play() : el.pause();
|
||||
}
|
||||
});
|
||||
|
||||
//// FILTERING
|
||||
|
||||
|
||||
document.getElementsByName("filter")[0].classList.remove("hidden");
|
||||
function filter(sub) {
|
||||
const table = g_tbody.children;
|
||||
for (let j = 0; j < table.length; ++j)
|
||||
table[j].classList.toggle("hidden",
|
||||
!(sub === "" || table[j].firstChild.firstChild.innerText.toLowerCase().indexOf(sub) != -1));
|
||||
}
|
||||
|
||||
document.getElementsByName("filter")[0].addEventListener("input", e => filter(e.target.value.toLowerCase()));
|
||||
|
||||
//// SORT BY COLUMN
|
||||
|
||||
const units = {"B": 0, "KiB": 1, "MiB": 2, "GiB": 3, "TiB": 4};
|
||||
const [thead_name, thead_date, thead_size] = document.getElementsByTagName('thead')[0]
|
||||
.children[0].children;
|
||||
const tbody = document.getElementsByTagName('tbody')[0];
|
||||
.children[0].children;
|
||||
|
||||
let g_sort_reverse = false;
|
||||
|
||||
thead_name.classList.toggle("clickable");
|
||||
thead_name.addEventListener('click', e => {
|
||||
e.preventDefault();
|
||||
sortTable((a,b) => {
|
||||
const a_name = a.children[0].textContent.toLowerCase();
|
||||
const b_name = b.children[0].textContent.toLowerCase();
|
||||
return a_name < b_name ? -1 : a_name > b_name ? 1 : 0;
|
||||
}, null, null, thead_name, [thead_date, thead_size]);
|
||||
e.preventDefault();
|
||||
sortTable((a,b) => {
|
||||
const a_name = a.children[0].textContent.toLowerCase();
|
||||
const b_name = b.children[0].textContent.toLowerCase();
|
||||
return a_name < b_name ? -1 : a_name > b_name ? 1 : 0;
|
||||
}, null, null, thead_name, [thead_date, thead_size]);
|
||||
g_first_row = g_tbody.firstChild;
|
||||
g_last_row = g_tbody.lastChild;
|
||||
});
|
||||
|
||||
thead_date.classList.toggle("clickable");
|
||||
thead_date.addEventListener('click', e => {
|
||||
e.preventDefault();
|
||||
sortTable((a,b) => {
|
||||
const a_date = new Date(a.children[1].textContent.slice(0, -4));
|
||||
const b_date = new Date(b.children[1].textContent.slice(0, -4));
|
||||
return a_date - b_date;
|
||||
}, null, null, thead_date, [thead_name, thead_size]);
|
||||
e.preventDefault();
|
||||
sortTable((a,b) => {
|
||||
const a_date = new Date(a.children[1].textContent.slice(0, -4));
|
||||
const b_date = new Date(b.children[1].textContent.slice(0, -4));
|
||||
return a_date - b_date;
|
||||
}, null, null, thead_date, [thead_name, thead_size]);
|
||||
g_first_row = g_tbody.firstChild;
|
||||
g_last_row = g_tbody.lastChild;
|
||||
});
|
||||
|
||||
const units = {"B": 0, "KiB": 1, "MiB": 2, "GiB": 3, "TiB": 4};
|
||||
|
||||
function sizeToBytes(size, unit) {
|
||||
if (units[unit] == 0) return size;
|
||||
for (let i = 0; i <= units[unit]; ++i) size *= 1024;
|
||||
return size;
|
||||
if (units[unit] == 0) return size;
|
||||
for (let i = 0; i <= units[unit]; ++i) size *= 1024;
|
||||
return size;
|
||||
}
|
||||
|
||||
thead_size.classList.toggle("clickable");
|
||||
thead_size.addEventListener('click', e => {
|
||||
e.preventDefault();
|
||||
sortTable(
|
||||
(a,b) => {
|
||||
if (a.textContent == "DIR")
|
||||
return 1;
|
||||
let [a_size, a_unit] = a.children[2].textContent.split(" ");
|
||||
let [b_size, b_unit] = b.children[2].textContent.split(" ");
|
||||
return sizeToBytes(+a_size, a_unit) - sizeToBytes(+b_size, b_unit);
|
||||
},
|
||||
e => e.children[2].textContent == "DIR",
|
||||
e => e.children[2].textContent != "DIR",
|
||||
thead_size, [thead_name, thead_date]);
|
||||
e.preventDefault();
|
||||
sortTable(
|
||||
(a,b) => {
|
||||
if (a.textContent == "DIR")
|
||||
return 1;
|
||||
let [a_size, a_unit] = a.children[2].textContent.split(" ");
|
||||
let [b_size, b_unit] = b.children[2].textContent.split(" ");
|
||||
return sizeToBytes(+a_size, a_unit) - sizeToBytes(+b_size, b_unit);
|
||||
},
|
||||
e => e.children[2].textContent == "DIR",
|
||||
e => e.children[2].textContent != "DIR",
|
||||
thead_size, [thead_name, thead_date]);
|
||||
g_first_row = g_tbody.firstChild;
|
||||
g_last_row = g_tbody.lastChild;
|
||||
});
|
||||
|
||||
function sortTable(compareFn, filterFn, filterNegFn, target, other) {
|
||||
let records = Array.from(document.getElementsByTagName('tbody')[0].children);
|
||||
let records = Array.from(g_tbody.children);
|
||||
|
||||
let dirs = [];
|
||||
if (filterFn != null) {
|
||||
dirs = records.filter(filterFn);
|
||||
records = records.filter(filterNegFn);
|
||||
}
|
||||
let dirs = [];
|
||||
if (filterFn != null) {
|
||||
dirs = records.filter(filterFn);
|
||||
records = records.filter(filterNegFn);
|
||||
}
|
||||
|
||||
records.sort(compareFn);
|
||||
records.sort(compareFn);
|
||||
|
||||
tbody.textContent = "";
|
||||
g_tbody.textContent = "";
|
||||
|
||||
other.forEach(v => {
|
||||
v.classList.remove("sort-up");
|
||||
v.classList.remove("sort-down");
|
||||
});
|
||||
other.forEach(v => {
|
||||
v.classList.remove("sort-up");
|
||||
v.classList.remove("sort-down");
|
||||
});
|
||||
|
||||
if (filterFn != null)
|
||||
tbody.append(...dirs);
|
||||
if (filterFn != null)
|
||||
g_tbody.append(...dirs);
|
||||
|
||||
if (g_sort_reverse) {
|
||||
tbody.append(...records.reverse());
|
||||
target.classList.add("sort-up");
|
||||
target.classList.remove("sort-down");
|
||||
} else {
|
||||
tbody.append(...records);
|
||||
target.classList.add("sort-down");
|
||||
target.classList.remove("sort-up");
|
||||
}
|
||||
|
||||
g_sort_reverse = !g_sort_reverse;
|
||||
if (g_sort_reverse) {
|
||||
g_tbody.append(...records.reverse());
|
||||
target.classList.add("sort-up");
|
||||
target.classList.remove("sort-down");
|
||||
} else {
|
||||
g_tbody.append(...records);
|
||||
target.classList.add("sort-down");
|
||||
target.classList.remove("sort-up");
|
||||
}
|
||||
|
||||
g_sort_reverse = !g_sort_reverse;
|
||||
}
|
|
@ -1,61 +0,0 @@
|
|||
// Code generated by "jade.go"; DO NOT EDIT.
|
||||
|
||||
package web
|
||||
|
||||
import (
|
||||
"dwelling-files/pkg/files"
|
||||
"dwelling-files/pkg/utils"
|
||||
"fmt"
|
||||
"html"
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
const (
|
||||
index__0 = `<!DOCTYPE html><html lang="en"><head><title>Arav's dwelling / Files</title><meta charset="utf-8"/><meta http-equiv="X-UA-Compatible" content="IE=edge"/><meta name="viewport" content="width=device-width, initial-scale=1.0"/><meta name="theme-color" content="#cd2682"/><meta name="description" content="My file share."/><link rel="icon" type="image/svg+xml" href="/assets/img/favicon.svg" sizes="any"/><link href="/assets/css/main.css" rel="stylesheet"/><script src="/assets/js/main.js" defer=""></script></head><body><header><svg id="logo" viewBox="0 -25 216 40"><text class="logo">Arav's dwelling</text><text class="under" y="11">Welcome to my sacred place, wanderer</text></svg><nav><a href="`
|
||||
index__1 = `">Back to main website</a><h1>Files</h1></nav></header><section id="files"><span>`
|
||||
index__2 = `</span><p>Files: `
|
||||
index__3 = ` (`
|
||||
index__4 = `); Directories: `
|
||||
index__5 = `.</p><table><thead><tr><th>Name</th><th>Date</th><th>Size</th></tr><tr><td><a href="../">../</a></td></tr></thead><tbody>`
|
||||
index__6 = `</tbody></table></section><section><span>`
|
||||
index__7 = `</span></section><section id="usage"><p>In an overlay you can use your mouse wheel to change scale of a video or a picture. An audio volume is being kept across site using LocalStorage API.</p></section><section id="privacy"><h2>Privacy statements</h2><p>I collect access logs that include access date and time, IP-address, User-Agent, referer URL that tells me where have you came from, request that you sent to me. In addition there are GeoIP information added based on your IP-address that includes country, region, and city for my convenience.</p><p>This site makes use of JavaScript purely for convenient functionality, like being able to watch video, listen to music, and look images in an overlay without the need to open a file in a new tab or return back.</p></section><footer>2017—2022 Arav <<a href="mailto:me@arav.top">me@arav.top</a>></footer><div id="overlay"></div></body></html>`
|
||||
index__8 = `<tr><td><a href="`
|
||||
index__9 = `">`
|
||||
index__10 = `</a></td><td>`
|
||||
index__11 = `</td><td>`
|
||||
index__12 = `</td></tr>`
|
||||
)
|
||||
|
||||
func Index(mainSite, currentPath string, stats *files.DirStats, items *[]files.DirEntry, r *http.Request, wr io.Writer) {
|
||||
buffer := &WriterAsBuffer{wr}
|
||||
|
||||
buffer.WriteString(index__0)
|
||||
buffer.WriteString(html.EscapeString(mainSite))
|
||||
buffer.WriteString(index__1)
|
||||
buffer.WriteString(currentPath)
|
||||
buffer.WriteString(index__2)
|
||||
buffer.WriteString(html.EscapeString(fmt.Sprintf("%v", stats.Files)))
|
||||
buffer.WriteString(index__3)
|
||||
buffer.WriteString(html.EscapeString(fmt.Sprintf("%v", stats.FilesSize)))
|
||||
buffer.WriteString(index__4)
|
||||
buffer.WriteString(html.EscapeString(fmt.Sprintf("%v", stats.Directories)))
|
||||
buffer.WriteString(index__5)
|
||||
|
||||
for _, item := range *items {
|
||||
buffer.WriteString(index__8)
|
||||
buffer.WriteString(html.EscapeString(fmt.Sprintf("%v", item.Link)))
|
||||
buffer.WriteString(index__9)
|
||||
buffer.WriteString(html.EscapeString(fmt.Sprintf("%v", item.Name)))
|
||||
buffer.WriteString(index__10)
|
||||
buffer.WriteString(utils.ToClientTimezone(item.Datetime, r).Format(files.FileDateFormat))
|
||||
buffer.WriteString(index__11)
|
||||
buffer.WriteString(html.EscapeString(fmt.Sprintf("%v", item.Size)))
|
||||
buffer.WriteString(index__12)
|
||||
|
||||
}
|
||||
buffer.WriteString(index__6)
|
||||
buffer.WriteString(currentPath)
|
||||
buffer.WriteString(index__7)
|
||||
|
||||
}
|
141
web/jade.go
141
web/jade.go
|
@ -1,141 +0,0 @@
|
|||
// Code generated by "jade.go"; DO NOT EDIT.
|
||||
package web
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
var (
|
||||
escaped = []byte{'<', '>', '"', '\'', '&'}
|
||||
replacing = []string{"<", ">", """, "'", "&"}
|
||||
)
|
||||
|
||||
func WriteEscString(st string, buffer *WriterAsBuffer) {
|
||||
for i := 0; i < len(st); i++ {
|
||||
if n := bytes.IndexByte(escaped, st[i]); n >= 0 {
|
||||
buffer.WriteString(replacing[n])
|
||||
} else {
|
||||
buffer.WriteByte(st[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type WriterAsBuffer struct {
|
||||
io.Writer
|
||||
}
|
||||
|
||||
func (w *WriterAsBuffer) WriteString(s string) (n int, err error) {
|
||||
n, err = w.Write([]byte(s))
|
||||
return
|
||||
}
|
||||
|
||||
func (w *WriterAsBuffer) WriteByte(b byte) (err error) {
|
||||
_, err = w.Write([]byte{b})
|
||||
return
|
||||
}
|
||||
|
||||
type stringer interface {
|
||||
String() string
|
||||
}
|
||||
|
||||
func WriteAll(a interface{}, escape bool, buffer *WriterAsBuffer) {
|
||||
switch v := a.(type) {
|
||||
case string:
|
||||
if escape {
|
||||
WriteEscString(v, buffer)
|
||||
} else {
|
||||
buffer.WriteString(v)
|
||||
}
|
||||
case int:
|
||||
WriteInt(int64(v), buffer)
|
||||
case int8:
|
||||
WriteInt(int64(v), buffer)
|
||||
case int16:
|
||||
WriteInt(int64(v), buffer)
|
||||
case int32:
|
||||
WriteInt(int64(v), buffer)
|
||||
case int64:
|
||||
WriteInt(v, buffer)
|
||||
case uint:
|
||||
WriteUint(uint64(v), buffer)
|
||||
case uint8:
|
||||
WriteUint(uint64(v), buffer)
|
||||
case uint16:
|
||||
WriteUint(uint64(v), buffer)
|
||||
case uint32:
|
||||
WriteUint(uint64(v), buffer)
|
||||
case uint64:
|
||||
WriteUint(v, buffer)
|
||||
case float32:
|
||||
buffer.WriteString(strconv.FormatFloat(float64(v), 'f', -1, 64))
|
||||
case float64:
|
||||
buffer.WriteString(strconv.FormatFloat(v, 'f', -1, 64))
|
||||
case bool:
|
||||
WriteBool(v, buffer)
|
||||
case stringer:
|
||||
if escape {
|
||||
WriteEscString(v.String(), buffer)
|
||||
} else {
|
||||
buffer.WriteString(v.String())
|
||||
}
|
||||
default:
|
||||
buffer.WriteString("\n<<< unprinted type, fmt.Stringer implementation needed >>>\n")
|
||||
}
|
||||
}
|
||||
|
||||
func ternary(condition bool, iftrue, iffalse interface{}) interface{} {
|
||||
if condition {
|
||||
return iftrue
|
||||
} else {
|
||||
return iffalse
|
||||
}
|
||||
}
|
||||
|
||||
// Used part of go source:
|
||||
// https://github.com/golang/go/blob/master/src/strconv/itoa.go
|
||||
func WriteUint(u uint64, buffer *WriterAsBuffer) {
|
||||
var a [64 + 1]byte
|
||||
i := len(a)
|
||||
|
||||
if ^uintptr(0)>>32 == 0 {
|
||||
for u > uint64(^uintptr(0)) {
|
||||
q := u / 1e9
|
||||
us := uintptr(u - q*1e9)
|
||||
for j := 9; j > 0; j-- {
|
||||
i--
|
||||
qs := us / 10
|
||||
a[i] = byte(us - qs*10 + '0')
|
||||
us = qs
|
||||
}
|
||||
u = q
|
||||
}
|
||||
}
|
||||
|
||||
us := uintptr(u)
|
||||
for us >= 10 {
|
||||
i--
|
||||
q := us / 10
|
||||
a[i] = byte(us - q*10 + '0')
|
||||
us = q
|
||||
}
|
||||
|
||||
i--
|
||||
a[i] = byte(us + '0')
|
||||
buffer.Write(a[i:])
|
||||
}
|
||||
func WriteInt(i int64, buffer *WriterAsBuffer) {
|
||||
if i < 0 {
|
||||
buffer.WriteByte('-')
|
||||
i = -i
|
||||
}
|
||||
WriteUint(uint64(i), buffer)
|
||||
}
|
||||
func WriteBool(b bool, buffer *WriterAsBuffer) {
|
||||
if b {
|
||||
buffer.WriteString("true")
|
||||
return
|
||||
}
|
||||
buffer.WriteString("false")
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
doctype html
|
||||
html(lang='en')
|
||||
head
|
||||
block head
|
||||
title Arav's dwelling / #{title}
|
||||
meta(charset='utf-8')
|
||||
meta(http-equiv='X-UA-Compatible' content='IE=edge')
|
||||
meta(name='viewport' content='width=device-width, initial-scale=1.0')
|
||||
meta(name='theme-color' content='#cd2682')
|
||||
meta(name='description' content='My file share.')
|
||||
link(rel='icon' type='image/svg+xml' href='/assets/img/favicon.svg' sizes='any')
|
||||
link(href='/assets/css/main.css' rel='stylesheet')
|
||||
script(src='/assets/js/main.js' defer='')
|
||||
body
|
||||
header
|
||||
svg(viewBox='0 -25 216 40')
|
||||
text Arav's dwelling
|
||||
text(y='11') Welcome to my sacred place, wanderer
|
||||
nav
|
||||
block nav
|
||||
block content
|
||||
footer
|
||||
| 2017—2023 Alexander "Arav" Andreev <#[a(href='mailto:me@arav.su') me@arav.su]> #[a(href='/privacy') Privacy statements]
|
|
@ -0,0 +1,21 @@
|
|||
extends base.jade
|
||||
|
||||
block meta_description
|
||||
meta(name='description' content=http.StatusText(code))
|
||||
|
||||
block nav
|
||||
a(href='/') Back to index page
|
||||
h1 #{http.StatusText(code)}
|
||||
|
||||
block content
|
||||
:go:func ErrorXXX(title, reason, message, referer string, code int)
|
||||
section#error
|
||||
h1 #{code}
|
||||
| #{http.StatusText(code)}
|
||||
if reason != ""
|
||||
p #{reason}
|
||||
if message != ""
|
||||
p #{message}
|
||||
if referer != ""
|
||||
section
|
||||
h2 #[a(href=referer) Go back]
|
|
@ -1,53 +1,39 @@
|
|||
:go:func Index(mainSite, currentPath string, stats *files.DirStats, items *[]files.DirEntry, r *http.Request)
|
||||
extends base.jade
|
||||
|
||||
:go:import "dwelling-files/pkg/files"
|
||||
:go:import "dwelling-files/pkg/utils"
|
||||
block nav
|
||||
a(href=mainSite) Back to main website
|
||||
h1 Files
|
||||
|
||||
doctype html
|
||||
html(lang='en')
|
||||
head
|
||||
title Arav's dwelling / Files
|
||||
meta(charset='utf-8')
|
||||
meta(http-equiv='X-UA-Compatible' content='IE=edge')
|
||||
meta(name='viewport' content='width=device-width, initial-scale=1.0')
|
||||
meta(name='theme-color' content='#cd2682')
|
||||
meta(name='description' content='My file share.')
|
||||
link(rel='icon' type='image/svg+xml' href='/assets/img/favicon.svg' sizes='any')
|
||||
link(href='/assets/css/main.css' rel='stylesheet')
|
||||
script(src='/assets/js/main.js' defer='')
|
||||
body
|
||||
header
|
||||
svg#logo(viewBox='0 -25 216 40')
|
||||
text.logo Arav's dwelling
|
||||
text.under(y='11') Welcome to my sacred place, wanderer
|
||||
nav
|
||||
a(href=mainSite) Back to main website
|
||||
h1 Files
|
||||
section#files
|
||||
span!= currentPath
|
||||
p Files: #{stats.Files} (#{stats.FilesSize}); Directories: #{stats.Directories}.
|
||||
table
|
||||
thead
|
||||
tr
|
||||
th Name
|
||||
th Date
|
||||
th Size
|
||||
tr
|
||||
td #[a(href="../") ../]
|
||||
tbody
|
||||
each item in *items
|
||||
tr
|
||||
td #[a(href=item.Link) #{item.Name}]
|
||||
td!= utils.ToClientTimezone(item.Datetime, r).Format(files.FileDateFormat)
|
||||
td= item.Size
|
||||
section
|
||||
span!= currentPath
|
||||
section#usage
|
||||
p In an overlay you can use your mouse wheel to change scale of a video or a picture. An audio volume is being kept across site using LocalStorage API.
|
||||
section#privacy
|
||||
h2 Privacy statements
|
||||
p I collect access logs that include access date and time, IP-address, User-Agent, referer URL that tells me where have you came from, request that you sent to me. In addition there are GeoIP information added based on your IP-address that includes country, region, and city for my convenience.
|
||||
p This site makes use of JavaScript purely for convenient functionality, like being able to watch video, listen to music, and look images in an overlay without the need to open a file in a new tab or return back.
|
||||
footer
|
||||
| 2017—2022 Arav <#[a(href='mailto:me@arav.top') me@arav.top]>
|
||||
div#overlay
|
||||
block content
|
||||
:go:func Index(title, mainSite, currentPath string, stats *files.DirStat, items *[]files.DirEntry, r *http.Request)
|
||||
:go:import "dwelling-files/pkg/files"
|
||||
:go:import "dwelling-files/pkg/utils"
|
||||
section
|
||||
span!= currentPath
|
||||
p Files: #{stats.Files} (#{stats.FilesSize}); Directories: #{stats.Directories}.
|
||||
input.hidden(type="text", name="filter" placeholder="Type in to filter this directory (case insensitive)")
|
||||
table
|
||||
thead
|
||||
tr
|
||||
th Name
|
||||
th Date
|
||||
th Size
|
||||
tr(tabindex=0)
|
||||
td #[a(href="../") ../]
|
||||
tbody
|
||||
each item, i in *items
|
||||
tr(tabindex=i+1)
|
||||
td #[a(href=item.Link) #{item.Name}]
|
||||
td!= utils.ToClientTimezone(item.Datetime, r).Format(files.FileDateFormat)
|
||||
td= item.Size
|
||||
section
|
||||
span!= currentPath
|
||||
section
|
||||
p On a page use up and down arrow keys to navigate through list. Use home and end keys to go to the start and end of a list. Use Ctrl+Backspace to return to a parent directory.
|
||||
p In an overlay use a mouse wheel to change a scale of a video or a picture. Use left and right arrow keys to go through media. Use space key to toggle pause. Use escape key to close an overlay, or click outside a media. An audio volume is being kept across site using LocalStorage API.
|
||||
div#overlay
|
||||
button(name='prev') ❰
|
||||
div
|
||||
div
|
||||
span
|
||||
button(name='next') ❱
|
|
@ -2,16 +2,19 @@ package web
|
|||
|
||||
import (
|
||||
"embed"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
//go:generate $GOPATH/bin/jade -pkg=web -stdlib -stdbuf -writer templates/index.jade
|
||||
//go:generate $GOPATH/bin/jade -pkg=web -stdbuf -writer templates/index.jade
|
||||
//go:generate $GOPATH/bin/jade -pkg=web -stdbuf -writer templates/errorXXX.jade
|
||||
|
||||
//go:embed assets
|
||||
var assetsDir embed.FS
|
||||
|
||||
func Assets() http.FileSystem {
|
||||
return http.FS(assetsDir)
|
||||
f, _ := fs.Sub(assetsDir, "assets")
|
||||
return http.FS(f)
|
||||
}
|
||||
|
||||
func AssetsGetFile(path string) ([]byte, error) {
|
||||
|
|
Loading…
Reference in New Issue