Compare commits
30 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 |
|
@ -1,3 +1,5 @@
|
||||||
bin/*
|
bin/*
|
||||||
!bin/.keep
|
!bin/.keep
|
||||||
.vscode
|
.vscode
|
||||||
|
web/*.jade.go
|
||||||
|
web/jade.go
|
34
Makefile
34
Makefile
|
@ -2,29 +2,39 @@ TARGET=dwelling-files
|
||||||
|
|
||||||
SYSDDIR_=${shell pkg-config systemd --variable=systemdsystemunitdir}
|
SYSDDIR_=${shell pkg-config systemd --variable=systemdsystemunitdir}
|
||||||
SYSDDIR=${SYSDDIR_:/%=%}
|
SYSDDIR=${SYSDDIR_:/%=%}
|
||||||
DESTDIR=/
|
|
||||||
|
|
||||||
LDFLAGS=-ldflags "-s -w -X main.version=23.8.0" -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}:
|
.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
|
go generate web/web.go
|
||||||
go build -o bin/$@ ${LDFLAGS} cmd/$@/main.go
|
|
||||||
|
|
||||||
run:
|
run:
|
||||||
bin/${TARGET} -file-handling -path /mnt/data -listen 127.0.0.1:19135
|
bin/${TARGET} -file-handling -path /mnt/data -listen 127.0.0.1:19135
|
||||||
|
|
||||||
install:
|
install:
|
||||||
install -Dm 0755 bin/${TARGET} ${DESTDIR}usr/bin/${TARGET}
|
install -Dm 0755 bin/${TARGET} ${DESTDIR}${PREFIX}/bin/${TARGET}
|
||||||
install -Dm 0644 LICENSE ${DESTDIR}usr/share/licenses/${TARGET}/LICENSE
|
|
||||||
|
|
||||||
install -Dm 0644 init/systemd/${TARGET}.service ${DESTDIR}${SYSDDIR}/${TARGET}.service
|
install -Dm 0644 init/systemd/${TARGET}.service ${DESTDIR}/${SYSDDIR}/${TARGET}.service
|
||||||
|
|
||||||
uninstall:
|
uninstall:
|
||||||
rm ${DESTDIR}usr/bin/${TARGET}
|
rm ${DESTDIR}${PREFIX}/bin/${TARGET}
|
||||||
rm ${DESTDIR}usr/share/licenses/${TARGET}/LICENSE
|
|
||||||
|
|
||||||
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.su>
|
# Maintainer: Alexander "Arav" Andreev <me@arav.su>
|
||||||
pkgname=dwelling-files
|
pkgname=dwelling-files
|
||||||
pkgver=23.8.0
|
pkgver=23.32.0
|
||||||
pkgrel=1
|
pkgrel=1
|
||||||
pkgdesc="Arav's dwelling / Files"
|
pkgdesc="Arav's dwelling / Files"
|
||||||
arch=('i686' 'x86_64' 'arm' 'armv6h' 'armv7h' 'aarch64')
|
arch=('i686' 'x86_64' 'arm' 'armv6h' 'armv7h' 'aarch64')
|
||||||
url="https://git.arav.su/Arav/dwelling-files"
|
url="https://git.arav.su/Arav/dwelling-files"
|
||||||
license=('MIT')
|
license=('MIT')
|
||||||
groups=()
|
makedepends=('go>=1.16')
|
||||||
depends=()
|
|
||||||
makedepends=('go')
|
|
||||||
provides=('dwelling-files')
|
provides=('dwelling-files')
|
||||||
conflicts=('dwelling-files')
|
conflicts=('dwelling-files')
|
||||||
replaces=()
|
source=("${pkgver}.tar.gz::https://git.arav.su/Arav/dwelling-files/archive/v${pkgver}.tar.gz")
|
||||||
backup=()
|
|
||||||
options=()
|
|
||||||
install=
|
|
||||||
source=('https://git.arav.su/Arav/dwelling-files/archive/23.8.0.tar.gz')
|
|
||||||
noextract=()
|
|
||||||
md5sums=('SKIP')
|
md5sums=('SKIP')
|
||||||
|
|
||||||
build() {
|
build() {
|
||||||
cd "$srcdir/$pkgname"
|
cd "$srcdir/$pkgname"
|
||||||
make DESTDIR="$pkgdir/"
|
export GOPATH="$srcdir"/gopath
|
||||||
|
make VERSION=$pkgver DESTDIR="$pkgdir" PREFIX="/usr"
|
||||||
}
|
}
|
||||||
|
|
||||||
package() {
|
package() {
|
||||||
cd "$srcdir/$pkgname"
|
cd "$srcdir/$pkgname"
|
||||||
make DESTDIR="$pkgdir/" install
|
make DESTDIR="$pkgdir" PREFIX="/usr" install
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,17 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"dwelling-files/internal/http"
|
dwhttp "dwelling-files/internal/http"
|
||||||
"dwelling-files/web"
|
"dwelling-files/web"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net/netip"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"strings"
|
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
|
"git.arav.su/Arav/httpr"
|
||||||
)
|
)
|
||||||
|
|
||||||
var version string
|
var version string
|
||||||
|
@ -25,33 +26,20 @@ func main() {
|
||||||
log.SetFlags(0)
|
log.SetFlags(0)
|
||||||
|
|
||||||
if *showVersion {
|
if *showVersion {
|
||||||
fmt.Println("dwelling-files ver.", version, "\nCopyright (c) 2022 Alexander \"Arav\" Andreev <me@arav.su>")
|
fmt.Println("dwelling-files ver.", version, "\nCopyright (c) 2023 Alexander \"Arav\" Andreev <me@arav.su>")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var network string
|
hand := dwhttp.New(directoryPath, !*enableFileHandler)
|
||||||
if !strings.ContainsRune(*listenAddress, ':') {
|
r := httpr.New()
|
||||||
network = "unix"
|
|
||||||
defer os.Remove(*listenAddress)
|
|
||||||
} else {
|
|
||||||
ap, err := netip.ParseAddrPort(*listenAddress)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalln(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if ap.Addr().Is4() {
|
r.ServeStatic("/assets/*filepath", web.Assets())
|
||||||
network = "tcp4"
|
r.Handler(http.MethodGet, "/file/*filepath", hand.File)
|
||||||
} else if ap.Addr().Is6() {
|
r.Handler(http.MethodGet, "/*filepath", hand.Index)
|
||||||
network = "tcp6"
|
r.Handler(http.MethodGet, "/", hand.Index)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
hand := http.New(directoryPath, web.Assets(), !*enableFileHandler)
|
srv := dwhttp.NewHttpServer(r)
|
||||||
srv := http.NewHttpServer()
|
if err := srv.Start(*listenAddress); err != nil {
|
||||||
|
|
||||||
srv.GET("/*filepath", hand.Index)
|
|
||||||
|
|
||||||
if err := srv.Start(network, *listenAddress); err != nil {
|
|
||||||
log.Fatalln(err)
|
log.Fatalln(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
server {
|
server {
|
||||||
listen 443 ssl http2;
|
listen 443 http2;
|
||||||
listen 127.0.0.1:8112; # I2P
|
listen 127.0.0.1:8112; # I2P
|
||||||
listen [300:a98d:d6d0:8a08::d]:80; # Yggdrasil
|
listen [300:a98d:d6d0:8a08::d]:80; # Yggdrasil
|
||||||
|
|
||||||
|
|
4
go.mod
4
go.mod
|
@ -1,5 +1,5 @@
|
||||||
module dwelling-files
|
module dwelling-files
|
||||||
|
|
||||||
go 1.19
|
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=
|
git.arav.su/Arav/httpr v0.3.2 h1:a+ifu+9+FnQe6p/Kd4kgTDKAFN6zBOJjBTMjbAuHxVk=
|
||||||
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
|
git.arav.su/Arav/httpr v0.3.2/go.mod h1:z0SVYwe5dBReeVuFU9QH2PmBxICJwchxqY5OfZbeVzU=
|
||||||
|
|
|
@ -8,17 +8,16 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/julienschmidt/httprouter"
|
"git.arav.su/Arav/httpr"
|
||||||
)
|
)
|
||||||
|
|
||||||
type FilesHandlers struct {
|
type FilesHandlers struct {
|
||||||
directoryPath string
|
directoryPath string
|
||||||
assetsServer http.Handler
|
|
||||||
fileServer http.Handler
|
fileServer http.Handler
|
||||||
noFileHandling bool
|
noFileHandling bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(directoryPath *string, assetsFS http.FileSystem, noFileHandling bool) *FilesHandlers {
|
func New(directoryPath *string, noFileHandling bool) *FilesHandlers {
|
||||||
var fSrv http.Handler
|
var fSrv http.Handler
|
||||||
if noFileHandling {
|
if noFileHandling {
|
||||||
fSrv = nil
|
fSrv = nil
|
||||||
|
@ -27,37 +26,14 @@ func New(directoryPath *string, assetsFS http.FileSystem, noFileHandling bool) *
|
||||||
}
|
}
|
||||||
return &FilesHandlers{
|
return &FilesHandlers{
|
||||||
directoryPath: *directoryPath,
|
directoryPath: *directoryPath,
|
||||||
assetsServer: http.FileServer(assetsFS),
|
|
||||||
fileServer: fSrv,
|
fileServer: fSrv,
|
||||||
noFileHandling: noFileHandling}
|
noFileHandling: noFileHandling}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (FilesHandlers) AssetsFS() http.FileSystem {
|
|
||||||
return web.Assets()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *FilesHandlers) Index(w http.ResponseWriter, r *http.Request) {
|
func (h *FilesHandlers) Index(w http.ResponseWriter, r *http.Request) {
|
||||||
path := httprouter.CleanPath(GetURLParam(r, "filepath"))
|
path := "/" + httpr.Param(r, "filepath")
|
||||||
|
if !strings.HasSuffix(path, "/") {
|
||||||
if strings.HasPrefix(path, "/assets") {
|
path += "/"
|
||||||
h.assetsServer.ServeHTTP(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.HasPrefix(path, "/robots.txt") {
|
|
||||||
fc, _ := web.AssetsGetFile("robots.txt")
|
|
||||||
w.Write(fc)
|
|
||||||
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)
|
currentPath := files.CurrentPath(path)
|
||||||
|
@ -65,8 +41,28 @@ func (h *FilesHandlers) Index(w http.ResponseWriter, r *http.Request) {
|
||||||
entries, stats, err := files.ScanDirectory(h.directoryPath+path, path)
|
entries, stats, err := files.ScanDirectory(h.directoryPath+path, path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println(err)
|
log.Println(err)
|
||||||
|
Error(w, http.StatusNotFound, "", "", r.Referer())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
web.Index(utils.MainSite(r.Host), currentPath, &stats, &entries, r, w)
|
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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,39 +5,41 @@ import (
|
||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/julienschmidt/httprouter"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type HttpServer struct {
|
type HttpServer struct {
|
||||||
server *http.Server
|
s http.Server
|
||||||
router *httprouter.Router
|
addr net.Addr
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewHttpServer() *HttpServer {
|
func NewHttpServer(r http.Handler) *HttpServer {
|
||||||
r := httprouter.New()
|
return &HttpServer{s: http.Server{
|
||||||
return &HttpServer{
|
ReadTimeout: 3 * time.Second,
|
||||||
server: &http.Server{
|
WriteTimeout: 3 * time.Second,
|
||||||
ReadTimeout: 3 * time.Second,
|
Handler: r}}
|
||||||
WriteTimeout: 3 * time.Second,
|
}
|
||||||
Handler: r,
|
|
||||||
},
|
func (s *HttpServer) Start(address string) error {
|
||||||
router: r,
|
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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
func (s *HttpServer) GET(path string, handler http.HandlerFunc) {
|
|
||||||
s.router.Handler(http.MethodGet, path, 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)
|
listener, err := net.Listen(network, address)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -47,8 +49,10 @@ func (s *HttpServer) Start(network, address string) error {
|
||||||
os.Chmod(address, 0777)
|
os.Chmod(address, 0777)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
s.addr = listener.Addr()
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
if err = s.server.Serve(listener); err != nil && err != http.ErrServerClosed {
|
if err = s.s.Serve(listener); err != nil && err != http.ErrServerClosed {
|
||||||
log.Fatalln(err)
|
log.Fatalln(err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
@ -60,7 +64,11 @@ func (s *HttpServer) Stop() error {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
if err := s.server.Shutdown(ctx); err != nil {
|
if s.addr.Network() == "unix" {
|
||||||
|
defer os.Remove(s.addr.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.s.Shutdown(ctx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,7 @@ import (
|
||||||
|
|
||||||
const FileDateFormat = "2006-01-02 15:04:05 MST"
|
const FileDateFormat = "2006-01-02 15:04:05 MST"
|
||||||
|
|
||||||
type DirStats struct {
|
type DirStat struct {
|
||||||
Files int64
|
Files int64
|
||||||
FilesSize string
|
FilesSize string
|
||||||
Directories int64
|
Directories int64
|
||||||
|
@ -22,16 +22,16 @@ type DirEntry struct {
|
||||||
Size string
|
Size string
|
||||||
}
|
}
|
||||||
|
|
||||||
func ScanDirectory(path, urlBase string) (entries []DirEntry, stats DirStats, err error) {
|
func ScanDirectory(path, urlBase string) (entries []DirEntry, stats DirStat, err error) {
|
||||||
var dirEntries []DirEntry = make([]DirEntry, 0)
|
|
||||||
var fileEntries []DirEntry = make([]DirEntry, 0)
|
|
||||||
var totalFilesSize int64 = 0
|
|
||||||
|
|
||||||
dir, err := os.ReadDir(path)
|
dir, err := os.ReadDir(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var dirEntries []DirEntry = make([]DirEntry, 0)
|
||||||
|
var fileEntries []DirEntry = make([]DirEntry, 0)
|
||||||
|
var totalFilesSize int64 = 0
|
||||||
|
|
||||||
for _, ent := range dir {
|
for _, ent := range dir {
|
||||||
entry, _ := ent.Info()
|
entry, _ := ent.Info()
|
||||||
|
|
||||||
|
|
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
|
@ -30,6 +30,8 @@
|
||||||
|
|
||||||
::placeholder { color: var(--primary-color); }
|
::placeholder { color: var(--primary-color); }
|
||||||
|
|
||||||
|
.hidden { display: none; }
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: var(--primary-color);
|
color: var(--primary-color);
|
||||||
text-decoration: none; }
|
text-decoration: none; }
|
||||||
|
@ -58,8 +60,6 @@ h2 {
|
||||||
font-size: 1.4rem;
|
font-size: 1.4rem;
|
||||||
margin: 1rem 0; }
|
margin: 1rem 0; }
|
||||||
|
|
||||||
.hidden { display: none; }
|
|
||||||
|
|
||||||
html { margin-left: calc(100vw - 100%); }
|
html { margin-left: calc(100vw - 100%); }
|
||||||
|
|
||||||
body {
|
body {
|
||||||
|
@ -69,6 +69,7 @@ body {
|
||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
max-width: 960px;
|
max-width: 960px;
|
||||||
|
min-height: 100vh;
|
||||||
width: 98%; }
|
width: 98%; }
|
||||||
|
|
||||||
header {
|
header {
|
||||||
|
@ -76,24 +77,22 @@ header {
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
justify-content: space-between; }
|
justify-content: space-between; }
|
||||||
|
|
||||||
#logo {
|
header svg { width: 360px; }
|
||||||
display: block;
|
|
||||||
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-size: 2rem;
|
||||||
font-variant-caps: small-caps;
|
font-variant-caps: small-caps;
|
||||||
font-weight: bold; }
|
font-weight: bold; }
|
||||||
|
|
||||||
|
header svg text:last-child { font-size: .88rem; }
|
||||||
|
|
||||||
@media screen and (-webkit-min-device-pixel-ratio:0) {
|
@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() {
|
@-moz-document url-prefix() {
|
||||||
#logo .logo { font-size: 2rem; } }
|
header svg text:first-child { font-size: 2rem; } }
|
||||||
|
|
||||||
#logo .under { font-size: .88rem; }
|
|
||||||
|
|
||||||
nav { margin-top: .5rem; }
|
nav { margin-top: .5rem; }
|
||||||
|
|
||||||
|
@ -105,6 +104,60 @@ nav h1 {
|
||||||
|
|
||||||
section { margin-top: 1rem; }
|
section { margin-top: 1rem; }
|
||||||
|
|
||||||
|
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%; }
|
||||||
|
|
||||||
|
table {
|
||||||
|
overflow-y: scroll;
|
||||||
|
width: 100%; }
|
||||||
|
|
||||||
|
tr { vertical-align: top; }
|
||||||
|
|
||||||
|
tr:hover,
|
||||||
|
tr:focus-within {
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
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,
|
||||||
|
td { line-break: strict; }
|
||||||
|
|
||||||
|
th:nth-child(2),
|
||||||
|
td:nth-child(2) { padding: 0 1rem; }
|
||||||
|
|
||||||
|
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:hover { color: var(--secondary-color); }
|
||||||
|
|
||||||
|
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-down::after { content: '↓'; }
|
||||||
|
|
||||||
#overlay {
|
#overlay {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background-color: var(--overlay-background-color);
|
background-color: var(--overlay-background-color);
|
||||||
|
@ -152,51 +205,13 @@ section { margin-top: 1rem; }
|
||||||
background-color: var(--background-color);
|
background-color: var(--background-color);
|
||||||
color: var(--primary-color); }
|
color: var(--primary-color); }
|
||||||
|
|
||||||
input[name="filter"] {
|
#error {
|
||||||
background-color: var(--background-color);
|
font-size: 3.5rem;
|
||||||
border: none;
|
line-height: 5rem;
|
||||||
border-bottom: 1px solid var(--primary-color);
|
text-align: center;
|
||||||
color: var(--text-color);
|
margin: 6rem 0; }
|
||||||
font: inherit;
|
|
||||||
width: 100%; }
|
|
||||||
|
|
||||||
table { overflow-y: scroll; width: 100%; }
|
#error h1 { font-size: 8rem; }
|
||||||
|
|
||||||
tr { vertical-align: top; }
|
|
||||||
|
|
||||||
tr:hover,
|
|
||||||
tr:focus-within { background-color: var(--primary-color); 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,
|
|
||||||
td { line-break: strict; }
|
|
||||||
|
|
||||||
th:nth-child(2),
|
|
||||||
td:nth-child(2) { padding: 0 1rem; }
|
|
||||||
|
|
||||||
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:hover { color: var(--secondary-color); }
|
|
||||||
|
|
||||||
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-down::after { content: '↓'; }
|
|
||||||
|
|
||||||
footer {
|
footer {
|
||||||
font-size: .8rem;
|
font-size: .8rem;
|
||||||
|
@ -206,7 +221,7 @@ footer {
|
||||||
@media screen and (max-width: 640px) {
|
@media screen and (max-width: 640px) {
|
||||||
header { display: block; }
|
header { display: block; }
|
||||||
|
|
||||||
#logo {
|
header svg {
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
width: 100%; }
|
width: 100%; }
|
||||||
|
|
||||||
|
|
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,60 +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"
|
block nav
|
||||||
:go:import "dwelling-files/pkg/utils"
|
a(href=mainSite) Back to main website
|
||||||
|
h1 Files
|
||||||
|
|
||||||
doctype html
|
block content
|
||||||
html(lang='en')
|
:go:func Index(title, mainSite, currentPath string, stats *files.DirStat, items *[]files.DirEntry, r *http.Request)
|
||||||
head
|
:go:import "dwelling-files/pkg/files"
|
||||||
title Arav's dwelling / Files
|
:go:import "dwelling-files/pkg/utils"
|
||||||
meta(charset='utf-8')
|
section
|
||||||
meta(http-equiv='X-UA-Compatible' content='IE=edge')
|
span!= currentPath
|
||||||
meta(name='viewport' content='width=device-width, initial-scale=1.0')
|
p Files: #{stats.Files} (#{stats.FilesSize}); Directories: #{stats.Directories}.
|
||||||
meta(name='theme-color' content='#cd2682')
|
input.hidden(type="text", name="filter" placeholder="Type in to filter this directory (case insensitive)")
|
||||||
meta(name='description' content='My file share.')
|
table
|
||||||
link(rel='icon' type='image/svg+xml' href='/assets/img/favicon.svg' sizes='any')
|
thead
|
||||||
link(href='/assets/css/main.css' rel='stylesheet')
|
tr
|
||||||
script(src='/assets/js/main.js' defer='')
|
th Name
|
||||||
body
|
th Date
|
||||||
header
|
th Size
|
||||||
svg#logo(viewBox='0 -25 216 40')
|
tr(tabindex=0)
|
||||||
text.logo Arav's dwelling
|
td #[a(href="../") ../]
|
||||||
text.under(y='11') Welcome to my sacred place, wanderer
|
tbody
|
||||||
nav
|
each item, i in *items
|
||||||
a(href=mainSite) Back to main website
|
tr(tabindex=i+1)
|
||||||
h1 Files
|
td #[a(href=item.Link) #{item.Name}]
|
||||||
section#files
|
td!= utils.ToClientTimezone(item.Datetime, r).Format(files.FileDateFormat)
|
||||||
span!= currentPath
|
td= item.Size
|
||||||
p Files: #{stats.Files} (#{stats.FilesSize}); Directories: #{stats.Directories}.
|
section
|
||||||
input.hidden(type="text", name="filter" placeholder="Type in to filter this directory (case insensitive)")
|
span!= currentPath
|
||||||
table
|
section
|
||||||
thead
|
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.
|
||||||
tr
|
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.
|
||||||
th Name
|
div#overlay
|
||||||
th Date
|
button(name='prev') ❰
|
||||||
th Size
|
div
|
||||||
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#usage
|
|
||||||
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.
|
|
||||||
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.su') me@arav.su]>
|
|
||||||
div#overlay
|
|
||||||
button(name='prev') ❰
|
|
||||||
div
|
div
|
||||||
div
|
span
|
||||||
span
|
button(name='next') ❱
|
||||||
button(name='next') ❱
|
|
|
@ -2,16 +2,19 @@ package web
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"embed"
|
"embed"
|
||||||
|
"io/fs"
|
||||||
"net/http"
|
"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
|
//go:embed assets
|
||||||
var assetsDir embed.FS
|
var assetsDir embed.FS
|
||||||
|
|
||||||
func Assets() http.FileSystem {
|
func Assets() http.FileSystem {
|
||||||
return http.FS(assetsDir)
|
f, _ := fs.Sub(assetsDir, "assets")
|
||||||
|
return http.FS(f)
|
||||||
}
|
}
|
||||||
|
|
||||||
func AssetsGetFile(path string) ([]byte, error) {
|
func AssetsGetFile(path string) ([]byte, error) {
|
||||||
|
|
Loading…
Reference in New Issue