Compare commits
8 Commits
06b6c5b00c
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 0bc8fcb633 | |||
| 2f78a9d80b | |||
| 75140f716d | |||
| 5ae998f383 | |||
| 26e2a4e0e3 | |||
| c17377cf78 | |||
| 04aa748bc9 | |||
| 3c90a59ece |
@@ -0,0 +1 @@
|
|||||||
|
.development
|
||||||
@@ -13,6 +13,7 @@ Configure the service using environment variables:
|
|||||||
| Name | Default | Description
|
| Name | Default | Description
|
||||||
| HTTP_ADDRESS | 127.0.0.1:9000 | Address and Port to use for requests
|
| 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_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 ]
|
[ Credits ]
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"cmp"
|
||||||
"embed"
|
"embed"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@@ -16,13 +17,16 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"text/template"
|
"text/template"
|
||||||
"time"
|
"time"
|
||||||
|
"unicode"
|
||||||
|
"unicode/utf8"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
//go:embed private/*
|
//go:embed private/*
|
||||||
privateDirectory embed.FS
|
privateDirectory embed.FS
|
||||||
serveHash = fmt.Sprintf("%x", time.Now().Unix())
|
serveHash = fmt.Sprintf("%X", time.Now().Unix())
|
||||||
serveDirectory = os.Getenv("HTTP_DIRECTORY")
|
serveDirectory = os.Getenv("HTTP_DIRECTORY")
|
||||||
|
serveDotfiles = os.Getenv("HTTP_SHOW_DOTFILES")
|
||||||
serveAddress = os.Getenv("HTTP_ADDRESS")
|
serveAddress = os.Getenv("HTTP_ADDRESS")
|
||||||
tmpl = template.Must(
|
tmpl = template.Must(
|
||||||
template.
|
template.
|
||||||
@@ -83,35 +87,84 @@ func getEntrySizeHuman(b int64) string {
|
|||||||
|
|
||||||
func getEntryIcon(name string, isDir bool) string {
|
func getEntryIcon(name string, isDir bool) string {
|
||||||
if isDir {
|
if isDir {
|
||||||
return "icon_folder.png"
|
return "icon-directory"
|
||||||
}
|
}
|
||||||
switch strings.ToLower(path.Ext(name)) {
|
switch strings.ToLower(path.Ext(name)) {
|
||||||
// Custom Filetypes
|
// Custom Filetypes
|
||||||
case ".redir":
|
case ".redir":
|
||||||
return "icon_redirect.png"
|
return "icon-redirect"
|
||||||
|
|
||||||
// Default Filetypes
|
// Default Filetypes
|
||||||
case ".mp3", ".flac", ".ogg", ".wav", ".aac", ".opus", ".m4a":
|
case ".mp3", ".flac", ".ogg", ".wav", ".aac", ".opus", ".m4a":
|
||||||
return "icon_audio.png"
|
return "icon-audio"
|
||||||
case ".mp4", ".mkv", ".webm", ".avi", ".mov", ".m4v":
|
case ".mp4", ".mkv", ".webm", ".avi", ".mov", ".m4v":
|
||||||
return "icon_video.png"
|
return "icon-video"
|
||||||
case ".jpg", ".jpeg", ".png", ".gif", ".webp", ".avif", ".bmp", ".tiff":
|
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":
|
case ".zip", ".tar", ".gz", ".7z", ".rar", ".zst", ".xz", ".bz2":
|
||||||
return "icon_archive.png"
|
return "icon-archive"
|
||||||
case ".exe", ".elf", ".bin", ".out", ".appimage", ".deb", ".rpm":
|
case ".exe", ".elf", ".bin", ".out", ".appimage", ".deb", ".rpm":
|
||||||
return "icon_program.png"
|
return "icon-program"
|
||||||
case ".pdf", ".txt", ".md", ".srt", ".vtt", ".ass", ".log":
|
case ".pdf", ".txt", ".md", ".srt", ".vtt", ".ass", ".log":
|
||||||
return "icon_document.png"
|
return "icon-document"
|
||||||
case ".html", ".ts", ".js", ".go", ".rs", ".pug":
|
case ".html", ".ts", ".js", ".go", ".rs", ".pug":
|
||||||
return "icon_script.png"
|
return "icon-script"
|
||||||
case ".json", ".xml", ".yaml", ".toml", ".ini", ".cfg":
|
case ".json", ".xml", ".yaml", ".toml", ".ini", ".cfg":
|
||||||
return "icon_config.png"
|
return "icon-config"
|
||||||
|
|
||||||
default:
|
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() {
|
func init() {
|
||||||
if serveAddress == "" {
|
if serveAddress == "" {
|
||||||
serveAddress = "127.0.0.1:8080"
|
serveAddress = "127.0.0.1:8080"
|
||||||
@@ -150,6 +203,13 @@ func main() {
|
|||||||
return
|
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)
|
// Use Browser Cache (if possible)
|
||||||
modtime := filestat.ModTime().UTC()
|
modtime := filestat.ModTime().UTC()
|
||||||
modhash := fmt.Sprint(modtime.Unix())
|
modhash := fmt.Sprint(modtime.Unix())
|
||||||
@@ -193,6 +253,11 @@ func main() {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ignore Dotfiles
|
||||||
|
if serveDotfiles == "" && strings.HasPrefix(entry.Name(), ".") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
// Check Symbolic Link
|
// Check Symbolic Link
|
||||||
isDir := entry.IsDir()
|
isDir := entry.IsDir()
|
||||||
if entry.Type()&os.ModeSymlink != 0 {
|
if entry.Type()&os.ModeSymlink != 0 {
|
||||||
@@ -226,14 +291,14 @@ func main() {
|
|||||||
// Use
|
// Use
|
||||||
if sortOrder == "modified" {
|
if sortOrder == "modified" {
|
||||||
if a.ModTime.Equal(b.ModTime) {
|
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) {
|
if a.ModTime.After(b.ModTime) {
|
||||||
return -1
|
return -1
|
||||||
}
|
}
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
return strings.Compare(a.Name, b.Name)
|
return naturalCompare(a.Name, b.Name)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Directory Listing
|
// Directory Listing
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 12 KiB |
@@ -8,12 +8,16 @@
|
|||||||
<link rel="icon" href="/private/favicon.png?v={{ .Version }}">
|
<link rel="icon" href="/private/favicon.png?v={{ .Version }}">
|
||||||
<title>Browser: {{ .Path }}</title>
|
<title>Browser: {{ .Path }}</title>
|
||||||
<style>
|
<style>
|
||||||
a.entry[data-type="directory"] div.icon {
|
div.pattern {
|
||||||
background-image: url("/private/icon_folder.png?v={{ .Version }}");
|
background-image: url("/private/bg_pattern.png?v={{ .Version }}");
|
||||||
}
|
}
|
||||||
|
|
||||||
a.entry[data-type="directory"]:hover div.icon {
|
div.graphic {
|
||||||
background-image: url("/private/icon_folder_open.png?v={{ .Version }}");
|
background-image: url("/private/bg_graphic.png?v={{ .Version }}");
|
||||||
|
}
|
||||||
|
|
||||||
|
a.entry div.icon {
|
||||||
|
background-image: url("/private/icon_grid.png?v={{ .Version }}");
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
@@ -45,20 +49,20 @@
|
|||||||
|
|
||||||
{{ if .Parent }}
|
{{ if .Parent }}
|
||||||
<a class="entry" data-type="directory" data-ignore="true" href="{{ .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>
|
<span class="filename">../</span>
|
||||||
</a>
|
</a>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
||||||
{{ range .Item }}
|
{{ range .Item }}
|
||||||
{{ if .IsDir }}
|
{{ if .IsDir }}
|
||||||
<a class="entry" data-type="directory" href="{{ EncodeURI .Name }}/" title="{{ .Name }} Modified: {{ .ModTime.Format "2006-01-02 15:04" }}">
|
<a class="entry" data-type="directory" href="{{ EncodeURI .Name }}/" title="{{ .Name }} Modified: {{ .ModTime.Format " 2006-01-02 15:04" }}">
|
||||||
<div class="icon"></div>
|
<div class="icon icon-directory"></div>
|
||||||
<span class="filename">{{ .Name }}</span>
|
<span class="filename">{{ .Name }}</span>
|
||||||
</a>
|
</a>
|
||||||
{{ else }}
|
{{ else }}
|
||||||
<a class="entry" data-type="file" href="{{ EncodeURI .Name }}" title="Name: {{ .Name }} Size: {{ GetEntrySizeHuman .Size }} Modified: {{ .ModTime.Format "2006-01-02 15:04" }}">
|
<a class="entry" data-type="file" href="{{ EncodeURI .Name }}" title="Name: {{ .Name }} Size: {{ GetEntrySizeHuman .Size }} Modified: {{ .ModTime.Format " 2006-01-02 15:04" }}">
|
||||||
<img class="icon" src="/private/{{ GetEntryIcon .Name .IsDir }}?v={{ $.Version }}">
|
<div class="icon {{ GetEntryIcon .Name .IsDir }}"></div>
|
||||||
<span class="filename">{{ .Name }}</span>
|
<span class="filename">{{ .Name }}</span>
|
||||||
</a>
|
</a>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 653 B |
|
Before Width: | Height: | Size: 906 B |
|
Before Width: | Height: | Size: 486 B |
|
Before Width: | Height: | Size: 487 B |
|
Before Width: | Height: | Size: 571 B |
|
Before Width: | Height: | Size: 695 B |
|
Before Width: | Height: | Size: 496 B |
|
After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 554 B |
|
Before Width: | Height: | Size: 389 B |
|
Before Width: | Height: | Size: 793 B |
|
Before Width: | Height: | Size: 510 B |
|
Before Width: | Height: | Size: 511 B |
@@ -1,5 +1,8 @@
|
|||||||
|
/* Layout */
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
background-color: #f0faf9;
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
@@ -26,13 +29,11 @@ div.foreground {
|
|||||||
}
|
}
|
||||||
|
|
||||||
div.pattern {
|
div.pattern {
|
||||||
background: url("/private/bg_pattern.png?v={{ .Version }}");
|
|
||||||
background-repeat: repeat-x;
|
background-repeat: repeat-x;
|
||||||
height: 152px;
|
height: 152px;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.graphic {
|
div.graphic {
|
||||||
background: url("/private/bg_graphic.png?v={{ .Version }}");
|
|
||||||
width: 256px;
|
width: 256px;
|
||||||
height: 259px;
|
height: 259px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -56,12 +57,6 @@ div.directory {
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
a.entry[data-type="directory"] div.icon {
|
|
||||||
background-size: 32px 32px;
|
|
||||||
height: 32px;
|
|
||||||
width: 32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
a.entry {
|
a.entry {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -73,7 +68,7 @@ a.entry {
|
|||||||
color: inherit;
|
color: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
a.entry img.icon {
|
a.entry div.icon {
|
||||||
height: 32px;
|
height: 32px;
|
||||||
width: 32px;
|
width: 32px;
|
||||||
}
|
}
|
||||||
@@ -87,7 +82,6 @@ a.entry span.filename {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
|
|
||||||
a.entry {
|
a.entry {
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -96,14 +90,57 @@ a.entry span.filename {
|
|||||||
gap: 8px;
|
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 {
|
a.entry span.filename {
|
||||||
font-size: 1em;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/
|
||||||