Skip to content

Commit

Permalink
one grid to rule them all
Browse files Browse the repository at this point in the history
  • Loading branch information
takenX10 committed May 18, 2022
1 parent de4ffb2 commit 15436e7
Show file tree
Hide file tree
Showing 10 changed files with 338 additions and 5 deletions.
9 changes: 9 additions & 0 deletions site/README
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
Simple tool to generate a static listing of a directory, similarly to what web
servers like Apache and lighthttpd offer. Can be used to host directories on
static hosting sites such as Github Pages or Netlify.

You can obtain the binary version of this software via:
$ curl -sf https://goblin.reaper.im/github.com/lucat1/statik | sh
Or any other compliant service such as https://gobinaries.com/.

A list of customization opitons can be found by looking at the program's help.
8 changes: 8 additions & 0 deletions site/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
module github.com/lucat1/statik

go 1.17

require (
github.com/tdewolff/minify/v2 v2.11.2 // indirect
github.com/tdewolff/parse/v2 v2.5.29 // indirect
)
12 changes: 12 additions & 0 deletions site/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
github.com/cheekybits/is v0.0.0-20150225183255-68e9c0620927/go.mod h1:h/aW8ynjgkuj+NQRlZcDbAbM1ORAbXjXX77sX7T289U=
github.com/djherbis/atime v1.1.0/go.mod h1:28OF6Y8s3NQWwacXc5eZTsEsiMzp7LF8MbXE+XJPdBE=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/fsnotify/fsnotify v1.5.3/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU=
github.com/matryer/try v0.0.0-20161228173917-9ac251b645a2/go.mod h1:0KeJpeMD6o+O4hW7qJOT7vyQPKrWmj26uf5wMc/IiIs=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/tdewolff/minify/v2 v2.11.2 h1:PpaPWhNlMVjkAKaOj0bbPv6KCVnrm8jbVwG7OtSdAqw=
github.com/tdewolff/minify/v2 v2.11.2/go.mod h1:NxozhBtgUVypPLzQdV96wkIu9J9vAiVmBcKhfC2zMfg=
github.com/tdewolff/parse/v2 v2.5.29 h1:Uf0OtZL9YaUXTuHEOitdo9lD90P0XTwCjZi+KbGChuM=
github.com/tdewolff/parse/v2 v2.5.29/go.mod h1:WzaJpRSbwq++EIQHYIRTpbYKNA3gn9it1Ik++q4zyho=
github.com/tdewolff/test v1.0.6/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
1 change: 1 addition & 0 deletions site/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<html><head><meta name='viewport' content='width=device-width'><style>:root{--b:#fbf1c7;--f:#282828;--d:#af3a03}@media(prefers-color-scheme:dark){:root{--b:#282828;--f:#fbf1c7;--d:#fe8019}}*{color:var(--f);background:var(--b)}body{font-size:16px;font-family:monospace;margin:0;padding:1.5rem;line-height:1.8}a{word-wrap:break-word;min-width:0;white-space:pre-wrap;text-underline-position:under}.d{color:var(--d)}div{display:grid;align-items:center;width:100%;grid-template:1fr/7fr 3fr 2fr}p{min-width:0}@media(max-width:880px){div{grid-template:1fr/7fr 3fr}.t{display:none}}</style><title>Index of /</title></head><body><h1>Index of /</h1><hr><pre><div><a href="http://localhost/README" >README</a><p class="t">17 May 22 13:32 CEST</p><p>477 B</p></div><div><a href="http://localhost/go.mod" >go.mod</a><p class="t">17 May 22 14:46 CEST</p><p>155 B</p></div><div><a href="http://localhost/go.sum" >go.sum</a><p class="t">17 May 22 14:46 CEST</p><p>1.1 kB</p></div><div><a href="http://localhost/statik" >statik</a><p class="t">17 May 22 16:56 CEST</p><p>4.5 MB</p></div><div><a href="http://localhost/statik.go" >statik.go</a><p class="t">17 May 22 16:46 CEST</p><p>7.1 kB</p></div><div><a href="http://localhost/style.css" >style.css</a><p class="t">17 May 22 16:45 CEST</p><p>759 B</p></div></pre><hr><p>Generated by <a href="https://github.com/lucat1/statik">statik</a> on 17 May 22 16:56 CEST</p></body></html>
Binary file added site/statik
Binary file not shown.
251 changes: 251 additions & 0 deletions site/statik.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
package main

import (
"flag"
"fmt"
"io/fs"
"io/ioutil"
"log"
"net/url"
"os"
"path"
"path/filepath"
"regexp"
"sort"
"strings"
"time"
_"embed"
"github.com/tdewolff/minify/v2"
"github.com/tdewolff/minify/v2/css"
)
//go:embed "style.css"
var style string

const (
formatLayout = time.RFC822
linkSuffix = ".link"
)

var (
baseDir, outDir string
baseUrl *url.URL = nil

include, exclude *regexp.Regexp = nil, nil
empty, recursive, sortEntries, converLinks bool
)

func bytes(b int64) string {
const unit = 1000
if b < unit {
return fmt.Sprintf("%d B", b)
}
div, exp := int64(unit), 0
for n := b / unit; n >= unit; n /= unit {
div *= unit
exp++
}
return fmt.Sprintf("%.1f %cB", float64(b)/float64(div), "kMGTPE"[exp])
}

// joins the baseUrl path with the given relative path and returns the url as a string
func join(rel string) string {
cpy := baseUrl.Path
baseUrl.Path = path.Join(baseUrl.Path, rel)
res := baseUrl.String()
baseUrl.Path = cpy
return res
}

func header(rel string, css string) string {
path := path.Join(baseUrl.Path + rel)
str := "<html><head><meta name='viewport' content='width=device-width'><style>" + css + "</style><title>Index of " + path + "</title></head><body><h1>Index of " + path + "</h1><hr><pre>"
if rel != "/" {
str += "<a href=\"" + join(rel+"/..") + "\" class=\"d\">..</a>\n"
}
return str
}

func line(name string, path string, modTime time.Time, isDir bool, size int64, link bool) string {
url := path
if !link {
url = join(path)
}
extra := ""
if isDir {
extra = "class=\"d\""
}
return fmt.Sprintf("<div><a href=\"%s\" %s>%s</a><p class=\"t\">%s</p><p>%s</p></div>", url, extra, name, modTime.Format(formatLayout), bytes(size))
}

func footer(date time.Time) string {
return "</pre><hr><p>Generated by <a href=\"https://github.com/lucat1/statik\">statik</a> on " + date.Format(formatLayout) + "</p></body></html>"
}

func copy(src, dest string) {
input, err := ioutil.ReadFile(src)
if err != nil {
log.Fatalf("Could not open source file for copying: %s\n%s\n", src, err)
}
err = ioutil.WriteFile(dest, input, 0644)
if err != nil {
log.Fatalf("Could not write to destination file for copying: %s\n%s\n", dest, err)
}
}

func filter(entries []fs.FileInfo) []fs.FileInfo {
filtered := []fs.FileInfo{}
for _, entry := range entries {
if entry.IsDir() && !exclude.MatchString(entry.Name()) || (!entry.IsDir() && include.MatchString(entry.Name()) && !exclude.MatchString(entry.Name())) {
filtered = append(filtered, entry)
}
}
return filtered
}

func generate(dir string, css string) bool {
entries, err := ioutil.ReadDir(dir)
if err != nil {
log.Fatalf("Could not read input directory: %s\n%s\n", dir, err)
}
entries = filter(entries)
if len(entries) == 0 {
return empty
}
if sortEntries {
sort.Slice(entries, func(i, j int) bool {
isFirstEntryDir := entries[i].IsDir()
isSecondEntryDir := entries[j].IsDir()
return isFirstEntryDir && !isSecondEntryDir ||
(isFirstEntryDir || !isSecondEntryDir) &&
entries[i].Name() < entries[j].Name()
})
}

if !strings.HasSuffix(dir, "/") {
dir += "/"
}
rel := strings.Replace(dir, baseDir, "", 1)
out := path.Join(outDir, rel)
if err := os.Mkdir(out, os.ModePerm); err != nil {
log.Fatalf("Could not create output *sub*directory: %s\n%s\n", out, err)
}
htmlPath := path.Join(out, "index.html")
html, err := os.OpenFile(htmlPath, os.O_RDWR|os.O_CREATE, 0666)
if err != nil {
log.Fatalf("Could not create output index.html: %s\n%s\n", htmlPath, err)
}

content := header(rel, css)
for _, entry := range entries {
pth := path.Join(dir, entry.Name())
// Avoid recursive infinite loop
if pth == outDir {
continue
}

if strings.HasSuffix(pth, linkSuffix) {
url, err := ioutil.ReadFile(pth)
if err != nil {
log.Fatalf("Could not read link file: %s\n%s\n", pth, err)
}
content += line(entry.Name()[:len(entry.Name())-len(linkSuffix)], string(url), entry.ModTime(), entry.IsDir(), 0, true)
continue
}

// Only list directories when recursing and only those which are not empty
if !entry.IsDir() || recursive && generate(pth, css) {
content += line(entry.Name(), path.Join(rel, entry.Name()), entry.ModTime(), entry.IsDir(), entry.Size(), false)
}

// Copy all files over to the web root
if !entry.IsDir() {
copy(pth, path.Join(out, entry.Name()))
}
}
content += footer(time.Now())
if n, err := html.Write([]byte(content)); err != nil || n != len(content) {
log.Fatalf("Could not write to index.html: %s\n%s\n", htmlPath, err)
}
if err := html.Close(); err != nil {
log.Fatalf("Could not write to close index.html: %s\n%s\n", htmlPath, err)
}
log.Printf("Generated data for directory: %s\n", dir)

return !empty
}

func main() {
i := flag.String("i", ".*", "A regex pattern to include files into the listing")
e := flag.String("e", "\\.git(hub)?", "A regex pattern to exclude files from the listing")
r := flag.Bool("r", true, "Recursively scan the file tree")
emp := flag.Bool("empty", false, "Whether to list empty directories")
s := flag.Bool("sort", true, "Sort files A-z and by type")
b := flag.String("b", "http://localhost", "The base URL")
l := flag.Bool("l", false, "Convert .link files to anchor tags")
flag.Parse()

args := flag.Args()
src, dest := ".", "site"
if len(args) > 2 {
log.Fatal("Invalid number of aruments, expected two at max")
}
if len(args) == 1 {
dest = args[0]
} else if len(args) == 2 {
src = args[0]
dest = args[1]
}

log.Println("Running with parameters:")
log.Println("\tInclude:\t", *i)
log.Println("\tExclude:\t", *e)
log.Println("\tRecursive:\t", *r)
log.Println("\tEmpty:\t\t", *emp)
log.Println("\tConvert links:\t\t", *l)
log.Println("\tSource:\t\t", src)
log.Println("\tDestination:\t", dest)
log.Println("\tBase URL:\t", *b)

var err error
if include, err = regexp.Compile(*i); err != nil {
log.Fatal("Invalid regexp for include matching", err)
}
if exclude, err = regexp.Compile(*e); err != nil {
log.Fatal("Invalid regexp for exclude matching", err)
}
recursive = *r
empty = *emp
sortEntries = *s
converLinks = *l

var wd string
if !filepath.IsAbs(src) || !filepath.IsAbs(dest) {
wd, err = os.Getwd()
if err != nil {
log.Fatal("Could not get currently working directory", err)
}
}
if baseDir = src; !filepath.IsAbs(src) {
baseDir = path.Join(wd, src)
}
if outDir = dest; !filepath.IsAbs(dest) {
outDir = path.Join(wd, dest)
}
if _, err := os.Stat(outDir); err == nil {
if err = os.RemoveAll(outDir); err != nil {
log.Fatalf("Could not remove output directory previous contents: %s\n%s\n", outDir, err)
}
}
if baseUrl, err = url.Parse(*b); err != nil {
log.Fatalf("Could not parse base URL: %s\n%s\n", *b, err)
}
m := minify.New()
m.AddFunc("text/css", css.Minify)
out, err := m.String("text/css", style)
if err != nil {
log.Fatalf("Could not minify css")
panic(err)
}
log.Printf("css code minified correctly")
generate(baseDir, out)
}
52 changes: 52 additions & 0 deletions site/style.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/* NOTE: this stylesheet is compressed and directly embedded into the go code */
:root {
--b: #fbf1c7;
--f: #282828;
--d: #af3a03;
}

@media (prefers-color-scheme: dark) {
:root {
--b: #282828;
--f: #fbf1c7;
--d: #fe8019;
}
}

* {
color: var(--f);
background: var(--b);
}
body {
font-size: 16px;
font-family: monospace;
margin: 0;
padding: 1.5rem;
line-height: 1.8;
}
a {
word-wrap: break-word;
min-width: 0;
white-space: pre-wrap;
text-underline-position: under;
}
.d {
color: var(--d);
}
div {
display: grid;
align-items: center;
width: 100%;
grid-template: 1fr / 7fr 3fr 2fr;
}
p {
min-width: 0;
}
@media (max-width:880px) {
div{
grid-template: 1fr / 7fr 3fr;
}
.t{
display: none;
}
}
Binary file added statik
Binary file not shown.
6 changes: 3 additions & 3 deletions statik.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ func join(rel string) string {

func header(rel string, css string) string {
path := path.Join(baseUrl.Path + rel)
str := "<html><head><meta name='viewport' content='width=device-width'><style>" + css + "</style><title>Index of " + path + "</title></head><body><h1>Index of " + path + "</h1><hr><pre>"
str := "<html><head><meta name='viewport' content='width=device-width'><style>" + css + "</style><title>Index of " + path + "</title></head><body><h1>Index of " + path + "</h1><hr><pre><div>"
if rel != "/" {
str += "<a href=\"" + join(rel+"/..") + "\" class=\"d\">..</a>\n"
}
Expand All @@ -74,11 +74,11 @@ func line(name string, path string, modTime time.Time, isDir bool, size int64, l
if isDir {
extra = "class=\"d\""
}
return fmt.Sprintf("<div><a href=\"%s\" %s>%s</a><p class=\"t\">%s</p><p>%s</p></div>", url, extra, name, modTime.Format(formatLayout), bytes(size))
return fmt.Sprintf("<a href=\"%s\" %s>%s</a><p class=\"t\">%s</p><p>%s</p>", url, extra, name, modTime.Format(formatLayout), bytes(size))
}

func footer(date time.Time) string {
return "</pre><hr><p>Generated by <a href=\"https://github.com/lucat1/statik\">statik</a> on " + date.Format(formatLayout) + "</p></body></html>"
return "</div></pre><hr><p>Generated by <a href=\"https://github.com/lucat1/statik\">statik</a> on " + date.Format(formatLayout) + "</p></body></html>"
}

func copy(src, dest string) {
Expand Down
4 changes: 2 additions & 2 deletions style.css
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,14 @@ div {
display: grid;
align-items: center;
width: 100%;
grid-template: 1fr / 7fr 3fr 2fr;
grid-template-columns: 7fr 3fr 2fr;
}
p {
min-width: 0;
}
@media (max-width:880px) {
div{
grid-template: 1fr / 7fr 3fr;
grid-template-columns: 7fr 3fr;
}
.t{
display: none;
Expand Down

0 comments on commit 15436e7

Please sign in to comment.