Compare commits
20 Commits
Author | SHA1 | Date | |
---|---|---|---|
d7dff35533 | |||
bf92e6ec7b | |||
23bfb9ad4d | |||
1d956a8430 | |||
883835bf24 | |||
4b188da546 | |||
6669337d3a | |||
aa387e2021 | |||
3bb26f397c | |||
28488a75c2 | |||
73a3b16f29 | |||
dac60a6010 | |||
034216efd4 | |||
dc2729ff91 | |||
2c8a9e3e61 | |||
c63d59f0a4 | |||
edd1d9f16d | |||
cec25f6c6a | |||
fdec858f48 | |||
aeccc9d629 |
6
Makefile
6
Makefile
@ -6,7 +6,7 @@ SYSDDIR=${SYSDDIR_:/%=%}
|
|||||||
DESTDIR:=
|
DESTDIR:=
|
||||||
PREFIX:=/usr/local
|
PREFIX:=/usr/local
|
||||||
|
|
||||||
VERSION=24.50.0
|
VERSION=24.53.0
|
||||||
|
|
||||||
FLAGS=-buildmode=pie -modcacherw -mod=readonly -trimpath
|
FLAGS=-buildmode=pie -modcacherw -mod=readonly -trimpath
|
||||||
LDFLAGS=-ldflags "-s -w -X main.version=${VERSION}" -tags osusergo,netgo
|
LDFLAGS=-ldflags "-s -w -X main.version=${VERSION}" -tags osusergo,netgo
|
||||||
@ -22,10 +22,10 @@ ifeq (,$(wildcard $(shell go env GOPATH)/bin/templ))
|
|||||||
endif
|
endif
|
||||||
$(shell go env GOPATH)/bin/templ generate
|
$(shell go env GOPATH)/bin/templ generate
|
||||||
|
|
||||||
run:
|
run: | ${TARGET}
|
||||||
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: | ${TARGET}
|
||||||
install -Dm 0755 bin/${TARGET} ${DESTDIR}${PREFIX}/bin/${TARGET}
|
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
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
# Maintainer: Alexander "Arav" Andreev <me@arav.su>
|
# Maintainer: Alexander "Arav" Andreev <me@arav.su>
|
||||||
pkgname=dwelling-files
|
pkgname=dwelling-files
|
||||||
pkgver=24.50.0
|
pkgver=24.53.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')
|
||||||
|
@ -21,7 +21,7 @@ var version string
|
|||||||
|
|
||||||
var listenAddress *string = flag.String("listen", "/var/run/dwelling-files/sock", "listen address (ip:port|unix_path)")
|
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 directoryPath *string = flag.String("path", "/srv/ftp", "path to file share")
|
||||||
var enableFileHandler *bool = flag.Bool("file-handling", false, "enable 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 not handled by something else")
|
||||||
var showVersion *bool = flag.Bool("v", false, "show version")
|
var showVersion *bool = flag.Bool("v", false, "show version")
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@ -43,6 +43,10 @@ func main() {
|
|||||||
r.Handler(http.MethodGet, "/*filepath", Index)
|
r.Handler(http.MethodGet, "/*filepath", Index)
|
||||||
r.ServeStatic("/assets/*filepath", web.Assets())
|
r.ServeStatic("/assets/*filepath", web.Assets())
|
||||||
|
|
||||||
|
if (*directoryPath)[len(*directoryPath)-1] == '/' {
|
||||||
|
*directoryPath = (*directoryPath)[:len(*directoryPath)-1]
|
||||||
|
}
|
||||||
|
|
||||||
var fileServer http.Handler
|
var fileServer http.Handler
|
||||||
if *enableFileHandler {
|
if *enableFileHandler {
|
||||||
fileServer = http.FileServer(http.Dir(*directoryPath))
|
fileServer = http.FileServer(http.Dir(*directoryPath))
|
||||||
@ -100,12 +104,12 @@ func Index(w http.ResponseWriter, r *http.Request) {
|
|||||||
path += "/"
|
path += "/"
|
||||||
}
|
}
|
||||||
|
|
||||||
entries, stats, err := files.ScanDirectory(*directoryPath+path, path)
|
entries, stats, err := files.ScanDirectory(*directoryPath, path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("Error directory scan:", err)
|
log.Println("Error directory scan:", err)
|
||||||
http.Error(w, "Not found", http.StatusNotFound)
|
http.Error(w, "Not found", http.StatusNotFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
web.Index(files.CurrentPath(path), version, &stats, &entries, r).Render(r.Context(), w)
|
web.Index(path, version, &stats, &entries, r).Render(r.Context(), w)
|
||||||
}
|
}
|
||||||
|
2
go.mod
2
go.mod
@ -2,8 +2,6 @@ module dwelling-files
|
|||||||
|
|
||||||
go 1.21
|
go 1.21
|
||||||
|
|
||||||
toolchain go1.23.4
|
|
||||||
|
|
||||||
require (
|
require (
|
||||||
git.arav.su/Arav/httpr v0.3.2
|
git.arav.su/Arav/httpr v0.3.2
|
||||||
github.com/a-h/templ v0.2.793
|
github.com/a-h/templ v0.2.793
|
||||||
|
@ -1,20 +0,0 @@
|
|||||||
package files
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
func CurrentPath(path string) (curPath string) {
|
|
||||||
parts := strings.Split(path, "/")[1:]
|
|
||||||
curPath = "<a href=\"/\">root</a>"
|
|
||||||
for i, part := range parts {
|
|
||||||
var sb strings.Builder
|
|
||||||
sb.WriteString("/<a href=\"/")
|
|
||||||
sb.WriteString(strings.Join(parts[:i+1], "/"))
|
|
||||||
sb.WriteString("/\">")
|
|
||||||
sb.WriteString(part)
|
|
||||||
sb.WriteString("</a>")
|
|
||||||
curPath += sb.String()
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
@ -10,9 +10,10 @@ import (
|
|||||||
const FileDateFormat = "2006-01-02 15:04:05 MST"
|
const FileDateFormat = "2006-01-02 15:04:05 MST"
|
||||||
|
|
||||||
type DirStat struct {
|
type DirStat struct {
|
||||||
Files int64
|
Files int64
|
||||||
FilesSize string
|
FilesSize string
|
||||||
Directories int64
|
FilesSizeUnit string
|
||||||
|
Directories int64
|
||||||
}
|
}
|
||||||
|
|
||||||
type DirEntry struct {
|
type DirEntry struct {
|
||||||
@ -20,10 +21,17 @@ type DirEntry struct {
|
|||||||
Link string
|
Link string
|
||||||
Datetime time.Time
|
Datetime time.Time
|
||||||
Size string
|
Size string
|
||||||
|
SizeUnit string
|
||||||
}
|
}
|
||||||
|
|
||||||
func ScanDirectory(path, urlBase string) (entries []DirEntry, stats DirStat, err error) {
|
// ScanDirectory returns entries of directory which is located by its relative
|
||||||
dir, err := os.ReadDir(path)
|
// path within a base directory.
|
||||||
|
//
|
||||||
|
// rel path should start/end with a / symbol.
|
||||||
|
func ScanDirectory(base, rel string) (entries []DirEntry, stats DirStat, err error) {
|
||||||
|
abs := base + rel
|
||||||
|
|
||||||
|
dir, err := os.ReadDir(abs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -37,7 +45,7 @@ func ScanDirectory(path, urlBase string) (entries []DirEntry, stats DirStat, err
|
|||||||
|
|
||||||
var isDirLink bool
|
var isDirLink bool
|
||||||
if entry.Mode().Type()&os.ModeSymlink != 0 {
|
if entry.Mode().Type()&os.ModeSymlink != 0 {
|
||||||
if slp, err := filepath.EvalSymlinks(filepath.Join(path, entry.Name())); err == nil {
|
if slp, err := filepath.EvalSymlinks(filepath.Join(abs, entry.Name())); err == nil {
|
||||||
lStat, _ := os.Lstat(slp)
|
lStat, _ := os.Lstat(slp)
|
||||||
isDirLink = lStat.IsDir()
|
isDirLink = lStat.IsDir()
|
||||||
}
|
}
|
||||||
@ -52,11 +60,13 @@ func ScanDirectory(path, urlBase string) (entries []DirEntry, stats DirStat, err
|
|||||||
})
|
})
|
||||||
stats.Directories++
|
stats.Directories++
|
||||||
} else {
|
} else {
|
||||||
|
sz, ui := convertFileSize(entry.Size())
|
||||||
fileEntries = append(fileEntries, DirEntry{
|
fileEntries = append(fileEntries, DirEntry{
|
||||||
Name: entry.Name(),
|
Name: entry.Name(),
|
||||||
Link: "/file" + urlBase + url.PathEscape(entry.Name()),
|
Link: "/file" + rel + url.PathEscape(entry.Name()),
|
||||||
Datetime: entry.ModTime(),
|
Datetime: entry.ModTime(),
|
||||||
Size: convertFileSize(entry.Size()),
|
Size: sz,
|
||||||
|
SizeUnit: ui,
|
||||||
})
|
})
|
||||||
|
|
||||||
totalFilesSize += entry.Size()
|
totalFilesSize += entry.Size()
|
||||||
@ -64,7 +74,7 @@ func ScanDirectory(path, urlBase string) (entries []DirEntry, stats DirStat, err
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
stats.FilesSize = convertFileSize(totalFilesSize)
|
stats.FilesSize, stats.FilesSizeUnit = convertFileSize(totalFilesSize)
|
||||||
|
|
||||||
entries = append(entries, dirEntries...)
|
entries = append(entries, dirEntries...)
|
||||||
entries = append(entries, fileEntries...)
|
entries = append(entries, fileEntries...)
|
||||||
|
@ -2,12 +2,12 @@ package files
|
|||||||
|
|
||||||
import "testing"
|
import "testing"
|
||||||
|
|
||||||
const path = "/mnt/data/music/Various"
|
const base = "/srv/ftp"
|
||||||
const urlBase = "/srv/ftp/"
|
const path = "/music/Various"
|
||||||
|
|
||||||
func BenchmarkScanDirectory(b *testing.B) {
|
func BenchmarkScanDirectory(b *testing.B) {
|
||||||
for i := 0; i < b.N; i++ {
|
for i := 0; i < b.N; i++ {
|
||||||
/*e, _, _ :=*/ ScanDirectory(path, urlBase)
|
/*e, _, _ :=*/ ScanDirectory(base, path)
|
||||||
// b.Log(e[len(e)-1], len(e))
|
// b.Log(e[len(e)-1], len(e))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,9 +8,9 @@ import (
|
|||||||
var sizeSuffixes = [...]string{"B", "KiB", "MiB", "GiB", "TiB"}
|
var sizeSuffixes = [...]string{"B", "KiB", "MiB", "GiB", "TiB"}
|
||||||
|
|
||||||
// convertFileSize converts size in bytes down to biggest units it represents.
|
// convertFileSize converts size in bytes down to biggest units it represents.
|
||||||
// Returns a concatenation of a converted size and a unit
|
// Returns a converted size string and a unit idx
|
||||||
func convertFileSize(size int64) string {
|
func convertFileSize(size int64) (string, string) {
|
||||||
var idx int
|
var idx uint8
|
||||||
var fSize float64 = float64(size)
|
var fSize float64 = float64(size)
|
||||||
for idx = 0; fSize >= 1024; fSize /= 1024 {
|
for idx = 0; fSize >= 1024; fSize /= 1024 {
|
||||||
idx++
|
idx++
|
||||||
@ -18,5 +18,5 @@ func convertFileSize(size int64) string {
|
|||||||
fSizeStr := strconv.FormatFloat(fSize, 'f', 3, 64)
|
fSizeStr := strconv.FormatFloat(fSize, 'f', 3, 64)
|
||||||
fSizeStr = strings.TrimRight(fSizeStr, "0")
|
fSizeStr = strings.TrimRight(fSizeStr, "0")
|
||||||
fSizeStr = strings.TrimSuffix(fSizeStr, ".")
|
fSizeStr = strings.TrimSuffix(fSizeStr, ".")
|
||||||
return fSizeStr + " " + sizeSuffixes[idx]
|
return fSizeStr, sizeSuffixes[idx]
|
||||||
}
|
}
|
||||||
|
@ -198,8 +198,7 @@ thead tr th.clickable.sort-down::after { content: '↓'; }
|
|||||||
#overlay :is(video, audio, img) {
|
#overlay :is(video, audio, img) {
|
||||||
margin: auto;
|
margin: auto;
|
||||||
max-height: 100%;
|
max-height: 100%;
|
||||||
max-width: 100%;
|
max-width: 100%; }
|
||||||
width: auto; }
|
|
||||||
|
|
||||||
#overlay button {
|
#overlay button {
|
||||||
background: none;
|
background: none;
|
||||||
|
@ -6,7 +6,7 @@ const overlay = document.getElementById("overlay");
|
|||||||
const overlay_content = overlay.children[1];
|
const overlay_content = overlay.children[1];
|
||||||
const overlay_label = overlay.children[2];
|
const overlay_label = overlay.children[2];
|
||||||
|
|
||||||
const g_tbody = document.getElementsByTagName('tbody')[0];
|
const g_tbody = document.getElementsByTagName("tbody")[0];
|
||||||
let g_first_row = g_tbody.firstChild;
|
let g_first_row = g_tbody.firstChild;
|
||||||
let g_last_row = g_tbody.lastChild;
|
let g_last_row = g_tbody.lastChild;
|
||||||
const g_back_row = document.getElementsByTagName("tr")[1];
|
const g_back_row = document.getElementsByTagName("tr")[1];
|
||||||
@ -17,8 +17,8 @@ let g_oc_translate = [0, 0];
|
|||||||
let g_current_row = g_back_row;
|
let g_current_row = g_back_row;
|
||||||
|
|
||||||
|
|
||||||
if (localStorage.getItem('audio_volume') == null)
|
if (localStorage.getItem("audio_volume") == null)
|
||||||
localStorage['audio_volume'] = 0.5;
|
localStorage.setItem("audio_volume", 0.5);
|
||||||
|
|
||||||
|
|
||||||
function overlayClose() {
|
function overlayClose() {
|
||||||
@ -80,8 +80,8 @@ function overlaySet(pathname, media_type_element) {
|
|||||||
if (media_type_element !== "img") {
|
if (media_type_element !== "img") {
|
||||||
media_element.autoplay = media_element.controls = true;
|
media_element.autoplay = media_element.controls = true;
|
||||||
media_element.addEventListener("volumechange", e => {
|
media_element.addEventListener("volumechange", e => {
|
||||||
localStorage['audio_volume'] = e.target.volume; });
|
localStorage.setItem("audio_volume", e.target.volume); });
|
||||||
media_element.volume = localStorage["audio_volume"];
|
media_element.volume = localStorage.getItem("audio_volume");
|
||||||
media_element.addEventListener("ended", e => { if (overlay_autoplay.checked) b_next.click(); });
|
media_element.addEventListener("ended", e => { if (overlay_autoplay.checked) b_next.click(); });
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -143,13 +143,13 @@ Array.from(g_tbody.children)
|
|||||||
|
|
||||||
overlay_autoplay = document.getElementsByName("autoplay")[0];
|
overlay_autoplay = document.getElementsByName("autoplay")[0];
|
||||||
|
|
||||||
if (localStorage.getItem('autoplay') == null)
|
if (localStorage.getItem("autoplay") == null) {
|
||||||
localStorage['autoplay'] = overlay_autoplay.checked = false;
|
localStorage.setItem("autoplay", overlay_autoplay.checked = false);
|
||||||
else
|
} else
|
||||||
overlay_autoplay.checked = localStorage['autoplay'];
|
overlay_autoplay.checked = localStorage.getItem("autoplay");
|
||||||
|
|
||||||
overlay_autoplay.addEventListener("change", e => {
|
overlay_autoplay.addEventListener("change", e => {
|
||||||
localStorage['autoplay'] = e.target.checked;
|
localStorage.setItem("autoplay", overlay_autoplay.checked);
|
||||||
const media_element = overlay_content.firstChild;
|
const media_element = overlay_content.firstChild;
|
||||||
if (e.target.checked && media_element !== undefined)
|
if (e.target.checked && media_element !== undefined)
|
||||||
if (media_element.tagName === "AUDIO" || media_element.tagName === "VIDEO")
|
if (media_element.tagName === "AUDIO" || media_element.tagName === "VIDEO")
|
||||||
@ -220,60 +220,11 @@ document.getElementsByName("filter")[0].addEventListener("input", e => filter(e.
|
|||||||
|
|
||||||
//// SORT BY COLUMN
|
//// 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]
|
||||||
const [thead_name, thead_date, thead_size] = document.getElementsByTagName('thead')[0]
|
|
||||||
.children[0].children;
|
.children[0].children;
|
||||||
|
|
||||||
let g_sort_reverse = false;
|
if (localStorage.getItem("sort_reverse") == null)
|
||||||
|
localStorage.setItem("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]);
|
|
||||||
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]);
|
|
||||||
g_first_row = g_tbody.firstChild;
|
|
||||||
g_last_row = g_tbody.lastChild;
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
function sizeToBytes(size, unit) {
|
|
||||||
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]);
|
|
||||||
g_first_row = g_tbody.firstChild;
|
|
||||||
g_last_row = g_tbody.lastChild;
|
|
||||||
});
|
|
||||||
|
|
||||||
function sortTable(compareFn, filterFn, filterNegFn, target, other) {
|
function sortTable(compareFn, filterFn, filterNegFn, target, other) {
|
||||||
let records = Array.from(g_tbody.children);
|
let records = Array.from(g_tbody.children);
|
||||||
@ -296,7 +247,7 @@ function sortTable(compareFn, filterFn, filterNegFn, target, other) {
|
|||||||
if (filterFn != null)
|
if (filterFn != null)
|
||||||
g_tbody.append(...dirs);
|
g_tbody.append(...dirs);
|
||||||
|
|
||||||
if (g_sort_reverse) {
|
if (localStorage.getItem("sort_reverse") == "true") {
|
||||||
g_tbody.append(...records.reverse());
|
g_tbody.append(...records.reverse());
|
||||||
target.classList.add("sort-up");
|
target.classList.add("sort-up");
|
||||||
target.classList.remove("sort-down");
|
target.classList.remove("sort-down");
|
||||||
@ -305,6 +256,68 @@ function sortTable(compareFn, filterFn, filterNegFn, target, other) {
|
|||||||
target.classList.add("sort-down");
|
target.classList.add("sort-down");
|
||||||
target.classList.remove("sort-up");
|
target.classList.remove("sort-up");
|
||||||
}
|
}
|
||||||
|
|
||||||
g_sort_reverse = !g_sort_reverse;
|
localStorage.setItem("sort_reverse", !(localStorage.getItem("sort_reverse") == "true"));
|
||||||
|
}
|
||||||
|
|
||||||
|
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]);
|
||||||
|
g_first_row = g_tbody.firstChild;
|
||||||
|
g_last_row = g_tbody.lastChild;
|
||||||
|
localStorage.setItem("sort_column", "name");
|
||||||
|
});
|
||||||
|
|
||||||
|
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]);
|
||||||
|
g_first_row = g_tbody.firstChild;
|
||||||
|
g_last_row = g_tbody.lastChild;
|
||||||
|
localStorage.setItem("sort_column", "date");
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
const size_units = document.getElementsByTagName("html")[0].lang == "ru" ?
|
||||||
|
{"Б": 0, "КиБ": 1, "МиБ": 2, "ГиБ": 3, "ТиБ": 4} :
|
||||||
|
{"B": 0, "KiB": 1, "MiB": 2, "GiB": 3, "TiB": 4};
|
||||||
|
function sizeToBytes(size, unit) {
|
||||||
|
if (size_units[unit] == 0) return size;
|
||||||
|
for (let i = 0; i <= size_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]);
|
||||||
|
g_first_row = g_tbody.firstChild;
|
||||||
|
g_last_row = g_tbody.lastChild;
|
||||||
|
localStorage.setItem("sort_column", "size");
|
||||||
|
});
|
||||||
|
|
||||||
|
localStorage.setItem("sort_reverse", !(localStorage.getItem("sort_reverse") == "true"));
|
||||||
|
switch (localStorage.getItem("sort_column")) {
|
||||||
|
case "name": thead_name.click(); break;
|
||||||
|
case "date": thead_date.click(); break;
|
||||||
|
case "size": thead_size.click(); break;
|
||||||
}
|
}
|
182
web/index.templ
182
web/index.templ
@ -5,95 +5,113 @@ import "net/http"
|
|||||||
import "dwelling-files/pkg/files"
|
import "dwelling-files/pkg/files"
|
||||||
import "dwelling-files/pkg/utils"
|
import "dwelling-files/pkg/utils"
|
||||||
import "strconv"
|
import "strconv"
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
func currentPathToLink(path string) (curPath string) {
|
||||||
|
parts := strings.Split(path, "/")[1:]
|
||||||
|
for i, part := range parts {
|
||||||
|
var sb strings.Builder
|
||||||
|
sb.WriteString("/<a href=\"/")
|
||||||
|
sb.WriteString(strings.Join(parts[:i+1], "/"))
|
||||||
|
sb.WriteString("/\">")
|
||||||
|
sb.WriteString(part)
|
||||||
|
sb.WriteString("</a>")
|
||||||
|
curPath += sb.String()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
templ Index(currentPath, progVer string, stat *files.DirStat, entries *[]files.DirEntry, r *http.Request) {
|
templ Index(currentPath, progVer string, stat *files.DirStat, entries *[]files.DirEntry, r *http.Request) {
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang={ i18n.GetLocale(ctx).Code().String() }>
|
<html lang={ i18n.GetLocale(ctx).Code().String() }>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<meta name="theme-color" content="#cd2682" />
|
<meta name="theme-color" content="#cd2682" />
|
||||||
<meta name="color-scheme" content="light dark" />
|
<meta name="color-scheme" content="light dark" />
|
||||||
|
|
||||||
<title>Arav's dwelling / { i18n.T(ctx, "title") }</title>
|
<title>Arav's dwelling / { i18n.T(ctx, "title") }</title>
|
||||||
|
|
||||||
<meta name="description" content={ i18n.T(ctx, "description") } />
|
<meta name="description" content={ i18n.T(ctx, "description") } />
|
||||||
<meta name="keywords" content={ i18n.T(ctx, "keywords") } />
|
<meta name="keywords" content={ i18n.T(ctx, "keywords") } />
|
||||||
|
|
||||||
<link rel="icon" href="/assets/img/favicon.svg" sizes="any" type="image/svg+xml"/>
|
<link rel="icon" href="/assets/img/favicon.svg" sizes="any" type="image/svg+xml"/>
|
||||||
<link rel="stylesheet" href="/assets/css/main.css" />
|
<link rel="stylesheet" href="/assets/css/main.css" />
|
||||||
<script src="/assets/js/main.js" defer></script>
|
<script src="/assets/js/main.js" defer></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header>
|
<header>
|
||||||
<svg width="360" viewBox="0 -36 360 66">
|
<svg width="360" viewBox="0 -36 360 66">
|
||||||
<text y="7" textLength="360" lengthAdjust="spacingAndGlyphs">Arav's dwelling</text>
|
<text y="7" textLength="360" lengthAdjust="spacingAndGlyphs">Arav's dwelling</text>
|
||||||
<text y="25" textLength="360" lengthAdjust="spacingAndGlyphs">Welcome to my sacred place, wanderer</text>
|
<text y="25" textLength="360" lengthAdjust="spacingAndGlyphs">Welcome to my sacred place, wanderer</text>
|
||||||
</svg>
|
</svg>
|
||||||
<nav>
|
<nav>
|
||||||
<a href={ templ.SafeURL(utils.MainSite(r.Host)) }>{ i18n.T(ctx, "back-home") }</a>
|
<a href={ templ.SafeURL(utils.MainSite(r.Host)) }>{ i18n.T(ctx, "back-home") }</a>
|
||||||
<h1>{ i18n.T(ctx, "title") }</h1>
|
<h1>{ i18n.T(ctx, "title") }</h1>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
<main>
|
<main>
|
||||||
<section>
|
<section>
|
||||||
<span>@templ.Raw(currentPath)</span>
|
<span><a href="/">{ i18n.T(ctx, "curpath-root") }</a>@templ.Raw(currentPathToLink(currentPath))</span>
|
||||||
<p>{ i18n.T(ctx, "stats.files") }: { strconv.FormatInt(stat.Files, 10) } ({ stat.FilesSize }); { i18n.T(ctx, "stats.directories") }: { strconv.FormatInt(stat.Directories, 10) }.</p>
|
<p>{ i18n.T(ctx, "stats.files") }: { strconv.FormatInt(stat.Files, 10) } ({ stat.FilesSize }); { i18n.T(ctx, "stats.directories") }: { strconv.FormatInt(stat.Directories, 10) }.</p>
|
||||||
<input type="text" name="filter" placeholder={ i18n.T(ctx, "stats.filter") } class="hidden">
|
<input type="text" name="filter" placeholder={ i18n.T(ctx, "stats.filter") } class="hidden">
|
||||||
</section>
|
</section>
|
||||||
<section>
|
<section>
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>{ i18n.T(ctx, "table.name") }</th>
|
<th>{ i18n.T(ctx, "table.name") }</th>
|
||||||
<th>{ i18n.T(ctx, "table.date") }</th>
|
<th>{ i18n.T(ctx, "table.date") }</th>
|
||||||
<th>{ i18n.T(ctx, "table.size") }</th>
|
<th>{ i18n.T(ctx, "table.size") }</th>
|
||||||
|
</tr>
|
||||||
|
<tr tabindex="0">
|
||||||
|
<td><a href="../">../</a></td>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
for i, entry := range *entries {
|
||||||
|
<tr tabindex={ strconv.FormatInt(int64(i)+1, 10) }>
|
||||||
|
<td><a href={ templ.SafeURL(entry.Link) }>{ entry.Name }</a></td>
|
||||||
|
<td>{ utils.ToClientTimezone(entry.Datetime, r).Format(files.FileDateFormat) }</td>
|
||||||
|
if entry.Size == "DIR" {
|
||||||
|
<td>DIR</td>
|
||||||
|
} else {
|
||||||
|
<td>{ entry.Size + " " + i18n.T(ctx, "size-unit."+entry.SizeUnit) }</td>
|
||||||
|
}
|
||||||
</tr>
|
</tr>
|
||||||
<tr tabindex="0">
|
}
|
||||||
<td><a href="../">../</a></td>
|
</tbody>
|
||||||
</tr>
|
</table>
|
||||||
</thead>
|
</section>
|
||||||
<tbody>
|
<section>
|
||||||
for i, entry := range *entries {
|
<span><a href="/">{ i18n.T(ctx, "curpath-root") }</a>@templ.Raw(currentPathToLink(currentPath))</span>
|
||||||
<tr tabindex={ strconv.FormatInt(int64(i)+1, 10) }>
|
</section>
|
||||||
<td><a href={ templ.SafeURL(entry.Link) }>{ entry.Name }</a></td>
|
<noscript>
|
||||||
<td>{ utils.ToClientTimezone(entry.Datetime, r).Format(files.FileDateFormat) }</td>
|
|
||||||
<td>{ entry.Size }</td>
|
|
||||||
</tr>
|
|
||||||
}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</section>
|
|
||||||
<section>
|
<section>
|
||||||
<span>@templ.Raw(currentPath)</span>
|
<p>{ i18n.T(ctx, "no-script-explain") }</p>
|
||||||
</section>
|
</section>
|
||||||
<noscript>
|
</noscript>
|
||||||
<section>
|
<section id="instruction" class="hidden">
|
||||||
<p>{ i18n.T(ctx, "no-script-explain") }</p>
|
<p>{ i18n.T(ctx, "instruction") }</p>
|
||||||
</section>
|
</section>
|
||||||
</noscript>
|
</main>
|
||||||
<section id="instruction" class="hidden">
|
<footer>
|
||||||
<p>{ i18n.T(ctx, "instruction") }</p>
|
<a href="?lang=ru">рус</a>
|
||||||
</section>
|
<a href="?lang=en">eng</a>
|
||||||
</main>
|
<br/>
|
||||||
<footer>
|
v{ progVer } 2017—2024 { i18n.T(ctx, "footer.author") } <<a href="mailto:me@arav.su">me@arav.su</a>> <a href={ templ.SafeURL(utils.MainSite(r.Host) + "/privacy") }>{ i18n.T(ctx, "footer.privacy") }</a>
|
||||||
<a href="?lang=ru">рус</a>
|
</footer>
|
||||||
<a href="?lang=en">eng</a>
|
<div id="overlay">
|
||||||
<br/>
|
<button name="prev">❰</button>
|
||||||
v{ progVer } 2017—2024 { i18n.T(ctx, "footer.author") } <<a href="mailto:me@arav.su">me@arav.su</a>> <a href={ templ.SafeURL(utils.MainSite(r.Host) + "/privacy") }>{ i18n.T(ctx, "footer.privacy") }</a>
|
<div></div>
|
||||||
</footer>
|
<span></span>
|
||||||
<div id="overlay">
|
<span class="c-autoplay">
|
||||||
<button name="prev">❰</button>
|
<input id="c-autoplay" type="checkbox" name="autoplay">
|
||||||
<div></div>
|
<label for="c-autoplay">{ i18n.T(ctx, "autoplay") }</label>
|
||||||
<span></span>
|
</span>
|
||||||
<span class="c-autoplay">
|
<button name="next">❱</button>
|
||||||
<input id="c-autoplay" type="checkbox" name="autoplay">
|
</div>
|
||||||
<label for="c-autoplay">{ i18n.T(ctx, "autoplay") }</label>
|
</body>
|
||||||
</span>
|
</html>
|
||||||
<button name="next">❱</button>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
}
|
}
|
@ -3,6 +3,7 @@ en:
|
|||||||
description: My file share
|
description: My file share
|
||||||
keywords: files ftp share self-host
|
keywords: files ftp share self-host
|
||||||
back-home: Back home
|
back-home: Back home
|
||||||
|
curpath-root: root
|
||||||
stats:
|
stats:
|
||||||
files: Files
|
files: Files
|
||||||
directories: Directories
|
directories: Directories
|
||||||
@ -16,4 +17,10 @@ en:
|
|||||||
autoplay: Autoplay
|
autoplay: Autoplay
|
||||||
footer:
|
footer:
|
||||||
author: Alexander ❝Arav❞ Andreev
|
author: Alexander ❝Arav❞ Andreev
|
||||||
privacy: Privacy statements
|
privacy: Privacy statements
|
||||||
|
size-unit:
|
||||||
|
B: "B"
|
||||||
|
KiB: "KiB"
|
||||||
|
MiB: "MiB"
|
||||||
|
GiB: "GiB"
|
||||||
|
TiB: "TiB"
|
@ -3,6 +3,7 @@ ru:
|
|||||||
description: Моя файловая шара
|
description: Моя файловая шара
|
||||||
keywords: файлы шара ftp селф-хост само-хост self-host
|
keywords: файлы шара ftp селф-хост само-хост self-host
|
||||||
back-home: Назад домой
|
back-home: Назад домой
|
||||||
|
curpath-root: корень
|
||||||
stats:
|
stats:
|
||||||
files: Файлы
|
files: Файлы
|
||||||
directories: Директории
|
directories: Директории
|
||||||
@ -16,4 +17,10 @@ ru:
|
|||||||
autoplay: Автовоспроизведение
|
autoplay: Автовоспроизведение
|
||||||
footer:
|
footer:
|
||||||
author: Александр «Arav» Андреев
|
author: Александр «Arav» Андреев
|
||||||
privacy: О приватности
|
privacy: О приватности
|
||||||
|
size-unit:
|
||||||
|
B: "Б"
|
||||||
|
KiB: "КиБ"
|
||||||
|
MiB: "МиБ"
|
||||||
|
GiB: "ГиБ"
|
||||||
|
TiB: "ТиБ"
|
@ -13,7 +13,3 @@ func Assets() http.FileSystem {
|
|||||||
f, _ := fs.Sub(assetsDir, "assets")
|
f, _ := fs.Sub(assetsDir, "assets")
|
||||||
return http.FS(f)
|
return http.FS(f)
|
||||||
}
|
}
|
||||||
|
|
||||||
func AssetsGetFile(path string) ([]byte, error) {
|
|
||||||
return assetsDir.ReadFile("assets/" + path)
|
|
||||||
}
|
|
||||||
|
Loading…
Reference in New Issue
Block a user