Compare commits

..

8 Commits

Author SHA1 Message Date
bakonpancakz 0bc8fcb633 bug: proper background colors
never noticed ig
2026-07-18 04:35:56 -07:00
bakonpancakz 2f78a9d80b feat: natural sorting
so like numbers are in order and stuff idk black magic
2026-07-18 04:22:03 -07:00
bakonpancakz 75140f716d bug: trailing slash issue
manually writing a dir caused path traversal issues in the html so we force a trailing slash to prevent this
2026-07-18 04:03:57 -07:00
bakonpancakz 5ae998f383 bug: actually hide dotfiles
disappointed in myself
2026-07-18 03:37:20 -07:00
bakonpancakz 26e2a4e0e3 feature: Hide Dotfiles
And Dotdirs if thats a thing. Enabled by default, use envvars to disable this feature.
2026-06-18 20:51:42 -07:00
bakonpancakz c17377cf78 Add .gitignore 2026-05-28 21:52:40 -07:00
bakonpancakz 04aa748bc9 Swap to icon grid instead of individual icons
saves like 3kb :p
2026-05-28 21:52:26 -07:00
bakonpancakz 3c90a59ece Better example directory
Display all the icons!
2026-05-28 21:51:47 -07:00
31 changed files with 155 additions and 43 deletions
+1
View File
@@ -0,0 +1 @@
.development
+1
View File
@@ -13,6 +13,7 @@ Configure the service using environment variables:
| Name | Default | Description
| HTTP_ADDRESS | 127.0.0.1:9000 | Address and Port to use for requests
| HTTP_DIRECTORY | <empty> | Starting directory to list and serve files from
| HTTP_SHOW_DOTFILES | <empty> | Set this variable to list directories and files prefixed with a dot (e.g. .DS_Store)
[ Credits ]
+79 -14
View File
@@ -2,6 +2,7 @@ package main
import (
"bytes"
"cmp"
"embed"
"fmt"
"io"
@@ -16,13 +17,16 @@ import (
"strings"
"text/template"
"time"
"unicode"
"unicode/utf8"
)
var (
//go:embed private/*
privateDirectory embed.FS
serveHash = fmt.Sprintf("%x", time.Now().Unix())
serveHash = fmt.Sprintf("%X", time.Now().Unix())
serveDirectory = os.Getenv("HTTP_DIRECTORY")
serveDotfiles = os.Getenv("HTTP_SHOW_DOTFILES")
serveAddress = os.Getenv("HTTP_ADDRESS")
tmpl = template.Must(
template.
@@ -83,35 +87,84 @@ func getEntrySizeHuman(b int64) string {
func getEntryIcon(name string, isDir bool) string {
if isDir {
return "icon_folder.png"
return "icon-directory"
}
switch strings.ToLower(path.Ext(name)) {
// Custom Filetypes
case ".redir":
return "icon_redirect.png"
return "icon-redirect"
// Default Filetypes
case ".mp3", ".flac", ".ogg", ".wav", ".aac", ".opus", ".m4a":
return "icon_audio.png"
return "icon-audio"
case ".mp4", ".mkv", ".webm", ".avi", ".mov", ".m4v":
return "icon_video.png"
return "icon-video"
case ".jpg", ".jpeg", ".png", ".gif", ".webp", ".avif", ".bmp", ".tiff":
return "icon_photo.png"
return "icon-photo"
case ".zip", ".tar", ".gz", ".7z", ".rar", ".zst", ".xz", ".bz2":
return "icon_archive.png"
return "icon-archive"
case ".exe", ".elf", ".bin", ".out", ".appimage", ".deb", ".rpm":
return "icon_program.png"
return "icon-program"
case ".pdf", ".txt", ".md", ".srt", ".vtt", ".ass", ".log":
return "icon_document.png"
return "icon-document"
case ".html", ".ts", ".js", ".go", ".rs", ".pug":
return "icon_script.png"
return "icon-script"
case ".json", ".xml", ".yaml", ".toml", ".ini", ".cfg":
return "icon_config.png"
return "icon-config"
default:
return "icon_generic.png"
return "icon-generic"
}
}
func parseNumber(s string) (num int, chars int) {
for chars < len(s) && s[chars] >= '0' && s[chars] <= '9' {
num = num*10 + int(s[chars]-'0')
chars++
}
return
}
func naturalCompare(a, b string) int {
for len(a) > 0 && len(b) > 0 {
if unicode.IsDigit(rune(a[0])) && unicode.IsDigit(rune(b[0])) {
aValue, aSize := parseNumber(a)
bValue, bSize := parseNumber(b)
if aValue != bValue {
if aValue < bValue {
return -1
}
return 1
}
a = a[aSize:]
b = b[bSize:]
continue
}
aRune, aSize := utf8.DecodeRuneInString(a)
bRune, bSize := utf8.DecodeRuneInString(b)
aRuneLower := unicode.ToLower(aRune)
bRuneLower := unicode.ToLower(bRune)
if aRuneLower != bRuneLower {
if aRuneLower < bRuneLower {
return -1
}
return 1
}
a = a[aSize:]
b = b[bSize:]
}
return cmp.Compare(len(a), len(b))
}
func init() {
if serveAddress == "" {
serveAddress = "127.0.0.1:8080"
@@ -150,6 +203,13 @@ func main() {
return
}
// Append trailing slash to directories so rendered HTML has proper paths
if filestat.IsDir() && !strings.HasSuffix(r.URL.Path, "/") {
r.URL.Path += "/"
http.Redirect(w, r, r.URL.String(), http.StatusMovedPermanently)
return
}
// Use Browser Cache (if possible)
modtime := filestat.ModTime().UTC()
modhash := fmt.Sprint(modtime.Unix())
@@ -193,6 +253,11 @@ func main() {
continue
}
// Ignore Dotfiles
if serveDotfiles == "" && strings.HasPrefix(entry.Name(), ".") {
continue
}
// Check Symbolic Link
isDir := entry.IsDir()
if entry.Type()&os.ModeSymlink != 0 {
@@ -226,14 +291,14 @@ func main() {
// Use
if sortOrder == "modified" {
if a.ModTime.Equal(b.ModTime) {
return strings.Compare(a.Name, b.Name)
return naturalCompare(a.Name, b.Name)
}
if a.ModTime.After(b.ModTime) {
return -1
}
return 1
}
return strings.Compare(a.Name, b.Name)
return naturalCompare(a.Name, b.Name)
})
// Directory Listing
Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 12 KiB

+13 -9
View File
@@ -8,12 +8,16 @@
<link rel="icon" href="/private/favicon.png?v={{ .Version }}">
<title>Browser: {{ .Path }}</title>
<style>
a.entry[data-type="directory"] div.icon {
background-image: url("/private/icon_folder.png?v={{ .Version }}");
div.pattern {
background-image: url("/private/bg_pattern.png?v={{ .Version }}");
}
a.entry[data-type="directory"]:hover div.icon {
background-image: url("/private/icon_folder_open.png?v={{ .Version }}");
div.graphic {
background-image: url("/private/bg_graphic.png?v={{ .Version }}");
}
a.entry div.icon {
background-image: url("/private/icon_grid.png?v={{ .Version }}");
}
</style>
</head>
@@ -45,20 +49,20 @@
{{ if .Parent }}
<a class="entry" data-type="directory" data-ignore="true" href="{{ .Parent }}">
<div class="icon"></div>
<div class="icon icon-directory"></div>
<span class="filename">../</span>
</a>
{{ end }}
{{ range .Item }}
{{ if .IsDir }}
<a class="entry" data-type="directory" href="{{ EncodeURI .Name }}/" title="{{ .Name }}&#10;Modified: {{ .ModTime.Format "2006-01-02 15:04" }}">
<div class="icon"></div>
<a class="entry" data-type="directory" href="{{ EncodeURI .Name }}/" title="{{ .Name }}&#10;Modified: {{ .ModTime.Format " 2006-01-02 15:04" }}">
<div class="icon icon-directory"></div>
<span class="filename">{{ .Name }}</span>
</a>
{{ else }}
<a class="entry" data-type="file" href="{{ EncodeURI .Name }}" title="Name: {{ .Name }}&#10;Size: {{ GetEntrySizeHuman .Size }}&#10;Modified: {{ .ModTime.Format "2006-01-02 15:04" }}">
<img class="icon" src="/private/{{ GetEntryIcon .Name .IsDir }}?v={{ $.Version }}">
<a class="entry" data-type="file" href="{{ EncodeURI .Name }}" title="Name: {{ .Name }}&#10;Size: {{ GetEntrySizeHuman .Size }}&#10;Modified: {{ .ModTime.Format " 2006-01-02 15:04" }}">
<div class="icon {{ GetEntryIcon .Name .IsDir }}"></div>
<span class="filename">{{ .Name }}</span>
</a>
{{ end }}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 653 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 906 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 486 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 487 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 571 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 695 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 496 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 554 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 389 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 793 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 510 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 511 B

+54 -17
View File
@@ -1,5 +1,8 @@
/* Layout */
body {
margin: 0;
background-color: #f0faf9;
}
::-webkit-scrollbar {
@@ -26,13 +29,11 @@ div.foreground {
}
div.pattern {
background: url("/private/bg_pattern.png?v={{ .Version }}");
background-repeat: repeat-x;
height: 152px;
}
div.graphic {
background: url("/private/bg_graphic.png?v={{ .Version }}");
width: 256px;
height: 259px;
position: absolute;
@@ -56,12 +57,6 @@ div.directory {
flex-wrap: wrap;
}
a.entry[data-type="directory"] div.icon {
background-size: 32px 32px;
height: 32px;
width: 32px;
}
a.entry {
display: inline-flex;
flex-direction: column;
@@ -73,7 +68,7 @@ a.entry {
color: inherit;
}
a.entry img.icon {
a.entry div.icon {
height: 32px;
width: 32px;
}
@@ -87,7 +82,6 @@ a.entry span.filename {
}
@media (max-width: 600px) {
a.entry {
padding: 4px;
width: 100%;
@@ -96,14 +90,57 @@ a.entry span.filename {
gap: 8px;
}
a.entry img.icon,
a.entry[data-type="directory"] div.icon {
background-size: 1em 1em;
height: 1em;
width: 1em;
}
a.entry span.filename {
font-size: 1em;
}
}
/* Icons */
.icon-archive {
background-position: 0 0;
}
.icon-audio {
background-position: -32px 0;
}
.icon-config {
background-position: -64px 0;
}
.icon-document {
background-position: -96px 0;
}
a.entry[data-type="directory"] .icon-directory {
background-position: 0 -32px;
}
a.entry[data-type="directory"]:hover .icon-directory {
background-position: -32px -32px;
}
.icon-generic {
background-position: -64px -32px;
}
.icon-photo {
background-position: -96px -32px;
}
.icon-program {
background-position: 0 -64px;
}
.icon-redirect {
background-position: -32px -64px;
}
.icon-script {
background-position: -64px -64px;
}
.icon-video {
background-position: -96px -64px;
}
+3
View File
@@ -0,0 +1,3 @@
I'M CALLING OUT YOUR NAME RECEIVING NO REPLY
[ REFLECTED OFF THE SIDE OF THE SKYLINE ]
CALLING OUT IN VAIN, LIKE YOU WOULD EVEN TRY
View File
View File
View File
View File
View File
+1
View File
@@ -0,0 +1 @@
/
View File
View File
View File
View File