Compare commits
14 Commits
Author | SHA1 | Date | |
---|---|---|---|
d7dff35533 | |||
bf92e6ec7b | |||
23bfb9ad4d | |||
1d956a8430 | |||
883835bf24 | |||
4b188da546 | |||
6669337d3a | |||
aa387e2021 | |||
3bb26f397c | |||
28488a75c2 | |||
73a3b16f29 | |||
dac60a6010 | |||
034216efd4 | |||
dc2729ff91 |
2
Makefile
2
Makefile
@ -6,7 +6,7 @@ SYSDDIR=${SYSDDIR_:/%=%}
|
||||
DESTDIR:=
|
||||
PREFIX:=/usr/local
|
||||
|
||||
VERSION=24.52.0
|
||||
VERSION=24.53.0
|
||||
|
||||
FLAGS=-buildmode=pie -modcacherw -mod=readonly -trimpath
|
||||
LDFLAGS=-ldflags "-s -w -X main.version=${VERSION}" -tags osusergo,netgo
|
||||
|
@ -1,6 +1,6 @@
|
||||
# Maintainer: Alexander "Arav" Andreev <me@arav.su>
|
||||
pkgname=dwelling-files
|
||||
pkgver=24.52.0
|
||||
pkgver=24.53.0
|
||||
pkgrel=1
|
||||
pkgdesc="Arav's dwelling / Files"
|
||||
arch=('i686' 'x86_64' 'arm' 'armv6h' 'armv7h' 'aarch64')
|
||||
|
@ -43,6 +43,10 @@ func main() {
|
||||
r.Handler(http.MethodGet, "/*filepath", Index)
|
||||
r.ServeStatic("/assets/*filepath", web.Assets())
|
||||
|
||||
if (*directoryPath)[len(*directoryPath)-1] == '/' {
|
||||
*directoryPath = (*directoryPath)[:len(*directoryPath)-1]
|
||||
}
|
||||
|
||||
var fileServer http.Handler
|
||||
if *enableFileHandler {
|
||||
fileServer = http.FileServer(http.Dir(*directoryPath))
|
||||
@ -100,12 +104,12 @@ func Index(w http.ResponseWriter, r *http.Request) {
|
||||
path += "/"
|
||||
}
|
||||
|
||||
entries, stats, err := files.ScanDirectory(*directoryPath+path, path)
|
||||
entries, stats, err := files.ScanDirectory(*directoryPath, path)
|
||||
if err != nil {
|
||||
log.Println("Error directory scan:", err)
|
||||
http.Error(w, "Not found", http.StatusNotFound)
|
||||
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
|
||||
|
||||
toolchain go1.23.4
|
||||
|
||||
require (
|
||||
git.arav.su/Arav/httpr v0.3.2
|
||||
github.com/a-h/templ v0.2.793
|
||||
|
@ -1,19 +0,0 @@
|
||||
package files
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
func CurrentPath(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
|
||||
}
|
@ -10,9 +10,10 @@ import (
|
||||
const FileDateFormat = "2006-01-02 15:04:05 MST"
|
||||
|
||||
type DirStat struct {
|
||||
Files int64
|
||||
FilesSize string
|
||||
Directories int64
|
||||
Files int64
|
||||
FilesSize string
|
||||
FilesSizeUnit string
|
||||
Directories int64
|
||||
}
|
||||
|
||||
type DirEntry struct {
|
||||
@ -20,10 +21,17 @@ type DirEntry struct {
|
||||
Link string
|
||||
Datetime time.Time
|
||||
Size string
|
||||
SizeUnit string
|
||||
}
|
||||
|
||||
func ScanDirectory(path, urlBase string) (entries []DirEntry, stats DirStat, err error) {
|
||||
dir, err := os.ReadDir(path)
|
||||
// ScanDirectory returns entries of directory which is located by its relative
|
||||
// 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 {
|
||||
return
|
||||
}
|
||||
@ -37,7 +45,7 @@ func ScanDirectory(path, urlBase string) (entries []DirEntry, stats DirStat, err
|
||||
|
||||
var isDirLink bool
|
||||
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)
|
||||
isDirLink = lStat.IsDir()
|
||||
}
|
||||
@ -52,11 +60,13 @@ func ScanDirectory(path, urlBase string) (entries []DirEntry, stats DirStat, err
|
||||
})
|
||||
stats.Directories++
|
||||
} else {
|
||||
sz, ui := convertFileSize(entry.Size())
|
||||
fileEntries = append(fileEntries, DirEntry{
|
||||
Name: entry.Name(),
|
||||
Link: "/file" + urlBase + url.PathEscape(entry.Name()),
|
||||
Link: "/file" + rel + url.PathEscape(entry.Name()),
|
||||
Datetime: entry.ModTime(),
|
||||
Size: convertFileSize(entry.Size()),
|
||||
Size: sz,
|
||||
SizeUnit: ui,
|
||||
})
|
||||
|
||||
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, fileEntries...)
|
||||
|
@ -2,12 +2,12 @@ package files
|
||||
|
||||
import "testing"
|
||||
|
||||
const path = "/mnt/data/music/Various"
|
||||
const urlBase = "/srv/ftp/"
|
||||
const base = "/srv/ftp"
|
||||
const path = "/music/Various"
|
||||
|
||||
func BenchmarkScanDirectory(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
/*e, _, _ :=*/ ScanDirectory(path, urlBase)
|
||||
/*e, _, _ :=*/ ScanDirectory(base, path)
|
||||
// b.Log(e[len(e)-1], len(e))
|
||||
}
|
||||
}
|
||||
|
@ -8,9 +8,9 @@ import (
|
||||
var sizeSuffixes = [...]string{"B", "KiB", "MiB", "GiB", "TiB"}
|
||||
|
||||
// convertFileSize converts size in bytes down to biggest units it represents.
|
||||
// Returns a concatenation of a converted size and a unit
|
||||
func convertFileSize(size int64) string {
|
||||
var idx int
|
||||
// Returns a converted size string and a unit idx
|
||||
func convertFileSize(size int64) (string, string) {
|
||||
var idx uint8
|
||||
var fSize float64 = float64(size)
|
||||
for idx = 0; fSize >= 1024; fSize /= 1024 {
|
||||
idx++
|
||||
@ -18,5 +18,5 @@ func convertFileSize(size int64) string {
|
||||
fSizeStr := strconv.FormatFloat(fSize, 'f', 3, 64)
|
||||
fSizeStr = strings.TrimRight(fSizeStr, "0")
|
||||
fSizeStr = strings.TrimSuffix(fSizeStr, ".")
|
||||
return fSizeStr + " " + sizeSuffixes[idx]
|
||||
return fSizeStr, sizeSuffixes[idx]
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ const overlay = document.getElementById("overlay");
|
||||
const overlay_content = overlay.children[1];
|
||||
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_last_row = g_tbody.lastChild;
|
||||
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;
|
||||
|
||||
|
||||
if (localStorage.getItem('audio_volume') == null)
|
||||
localStorage['audio_volume'] = 0.5;
|
||||
if (localStorage.getItem("audio_volume") == null)
|
||||
localStorage.setItem("audio_volume", 0.5);
|
||||
|
||||
|
||||
function overlayClose() {
|
||||
@ -80,8 +80,8 @@ function overlaySet(pathname, media_type_element) {
|
||||
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"];
|
||||
localStorage.setItem("audio_volume", e.target.volume); });
|
||||
media_element.volume = localStorage.getItem("audio_volume");
|
||||
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];
|
||||
|
||||
if (localStorage.getItem('autoplay') == null)
|
||||
localStorage['autoplay'] = overlay_autoplay.checked = false;
|
||||
else
|
||||
overlay_autoplay.checked = localStorage['autoplay'];
|
||||
if (localStorage.getItem("autoplay") == null) {
|
||||
localStorage.setItem("autoplay", overlay_autoplay.checked = false);
|
||||
} else
|
||||
overlay_autoplay.checked = localStorage.getItem("autoplay");
|
||||
|
||||
overlay_autoplay.addEventListener("change", e => {
|
||||
localStorage['autoplay'] = e.target.checked;
|
||||
localStorage.setItem("autoplay", overlay_autoplay.checked);
|
||||
const media_element = overlay_content.firstChild;
|
||||
if (e.target.checked && media_element !== undefined)
|
||||
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
|
||||
|
||||
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;
|
||||
|
||||
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]);
|
||||
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;
|
||||
});
|
||||
if (localStorage.getItem("sort_reverse") == null)
|
||||
localStorage.setItem("sort_reverse", false);
|
||||
|
||||
function sortTable(compareFn, filterFn, filterNegFn, target, other) {
|
||||
let records = Array.from(g_tbody.children);
|
||||
@ -296,7 +247,7 @@ function sortTable(compareFn, filterFn, filterNegFn, target, other) {
|
||||
if (filterFn != null)
|
||||
g_tbody.append(...dirs);
|
||||
|
||||
if (g_sort_reverse) {
|
||||
if (localStorage.getItem("sort_reverse") == "true") {
|
||||
g_tbody.append(...records.reverse());
|
||||
target.classList.add("sort-up");
|
||||
target.classList.remove("sort-down");
|
||||
@ -305,6 +256,68 @@ function sortTable(compareFn, filterFn, filterNegFn, target, other) {
|
||||
target.classList.add("sort-down");
|
||||
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;
|
||||
}
|
@ -5,7 +5,21 @@ import "net/http"
|
||||
import "dwelling-files/pkg/files"
|
||||
import "dwelling-files/pkg/utils"
|
||||
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) {
|
||||
<!doctype html>
|
||||
@ -39,7 +53,7 @@ templ Index(currentPath, progVer string, stat *files.DirStat, entries *[]files.D
|
||||
</header>
|
||||
<main>
|
||||
<section>
|
||||
<span><a href="/">{ i18n.T(ctx, "curpath-root") }</a>@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>
|
||||
<input type="text" name="filter" placeholder={ i18n.T(ctx, "stats.filter") } class="hidden">
|
||||
</section>
|
||||
@ -60,14 +74,18 @@ templ Index(currentPath, progVer string, stat *files.DirStat, entries *[]files.D
|
||||
<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>
|
||||
<td>{ entry.Size }</td>
|
||||
if entry.Size == "DIR" {
|
||||
<td>DIR</td>
|
||||
} else {
|
||||
<td>{ entry.Size + " " + i18n.T(ctx, "size-unit."+entry.SizeUnit) }</td>
|
||||
}
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
<section>
|
||||
<span>@templ.Raw(currentPath)</span>
|
||||
<span><a href="/">{ i18n.T(ctx, "curpath-root") }</a>@templ.Raw(currentPathToLink(currentPath))</span>
|
||||
</section>
|
||||
<noscript>
|
||||
<section>
|
||||
|
@ -17,4 +17,10 @@ en:
|
||||
autoplay: Autoplay
|
||||
footer:
|
||||
author: Alexander ❝Arav❞ Andreev
|
||||
privacy: Privacy statements
|
||||
privacy: Privacy statements
|
||||
size-unit:
|
||||
B: "B"
|
||||
KiB: "KiB"
|
||||
MiB: "MiB"
|
||||
GiB: "GiB"
|
||||
TiB: "TiB"
|
@ -17,4 +17,10 @@ ru:
|
||||
autoplay: Автовоспроизведение
|
||||
footer:
|
||||
author: Александр «Arav» Андреев
|
||||
privacy: О приватности
|
||||
privacy: О приватности
|
||||
size-unit:
|
||||
B: "Б"
|
||||
KiB: "КиБ"
|
||||
MiB: "МиБ"
|
||||
GiB: "ГиБ"
|
||||
TiB: "ТиБ"
|
@ -13,7 +13,3 @@ func Assets() http.FileSystem {
|
||||
f, _ := fs.Sub(assetsDir, "assets")
|
||||
return http.FS(f)
|
||||
}
|
||||
|
||||
func AssetsGetFile(path string) ([]byte, error) {
|
||||
return assetsDir.ReadFile("assets/" + path)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user