1
0

Compare commits

...

14 Commits

13 changed files with 147 additions and 115 deletions

View File

@ -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

View File

@ -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')

View File

@ -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
View File

@ -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

View File

@ -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
}

View File

@ -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...)

View File

@ -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))
}
}

View File

@ -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]
}

View File

@ -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;
}

View File

@ -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>

View File

@ -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"

View File

@ -17,4 +17,10 @@ ru:
autoplay: Автовоспроизведение
footer:
author: Александр «Arav» Андреев
privacy: О приватности
privacy: О приватности
size-unit:
B: "Б"
KiB: "КиБ"
MiB: "МиБ"
GiB: "ГиБ"
TiB: "ТиБ"

View File

@ -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)
}