~poptart/stagnant

d08879b03a840cc9f794c9e4eb1bc78c1c69a1e6 — terrorbyte 7 months ago 199fd52
Finally publish the project
M .gitignore => .gitignore +2 -0
@@ 1,6 1,8 @@
temp/
build/
util/src/
util/minify
util/mdtohtml
hmdtohtml
backup/
test/

M README.md => README.md +38 -10
@@ 1,6 1,9 @@
stagnant
========

!!! Experimental !!!

A pipeline based static "site" generator focused on allowing flexible composability using built in UNIX tools. Fundamentally, this project is simple and only enforces a simple set of rules, filesystem structures, and a small subset of exposed environment variables.
A pipeline based static "site" generator focused on allowing flexible generation. Fundamentally, this project is simple and only enforces a simple set of rules, filesystem structures, and a small subset of exposed environment variables.

Reasoning
---------


@@ 9,7 12,15 @@ I write my entire life in markdown, and things that aren't in markdown get gener

Additionally, my CI of choice is Laminar-CI which takes the opinionated stance of "just use job shell scripts". I found this rather freeing from implementation specific DSLs or janky non-reproducible web interface configuration management.

I wanted something that allowed me to write a simple step-by-step sychronous pipeline using POSIX.1-2008 shell scripts and allow me to write the pipeline steps in any language I desired. Use the tools you already have.
I wanted something that allowed me to write a simple step-by-step sychronous pipeline using POSIX.1-2008 shell scripts and allow me to write the pipeline steps in any language I felt like programming in.

Versions
--------

stagnant contains 2 supported versions:

* `stagnant.sh` - A POSIX.1-2008 shell script that contains all the logic for building a site
* `stagnant.go` - The Go implementation that is used for more programmatic logic, will eventually contain an extention to load Go plugins

Example Structure
-----------------


@@ 107,11 118,28 @@ Exposed Variables

The following variables are exposed to all running stage executables/scripts and to any generator scripts:

* `_FUNCDIR`
* `_SCRIPTDIR`
* `_BUILDROOT`
* `_UTIL`
* `_PROD`
* `_ID`
* `_BUILD`
* `_DEPLOY`
* `_ORIGIN` - The origin directory that is `pwd`
* `_SITEDIR` - Contains all the site files. In my case I use a bunch of .md files and assets that get built
* `_BUILDROOT` - The `build` directory that contains all the build runs and a symlink (`$_BUILDROOT/latest`) to the last build
* `_BUILDID` - The current running build ID, by default uses UNIX timestamps
* `_BUILDDIR` - The current running build and build ID in absolute path form
* `_STAGEDIR` - Directory containing the stage scripts or executables
* `_SCRIPTDIR` - Directory containing scripts that are used for "out-of-build" site management (for example, `clean.sh` cleans my build dirs)
* `_UTILDIR` - Utilities that are required for building are expected to be built into this directory
* `_PRODDIR` - The current "production" build. The idea is that this can contain the code that is committed to CI and all tests and deployments can be run without pulling in all the `stagnant` repo

Demo
----

The demo that is in the default repository requires two projects to either be in `$PATH` or to be in the `util` directory:

* https://github.com/gomarkdown/mdtohtml
* https://github.com/tdewolff/minify

Once those binaries are setup simply:

```
go run stagnant.go
```

and check `build/latest`

A site/about.md => site/about.md +8 -0
@@ 0,0 1,8 @@
//META:title Stagnant Project - Demo About 
//META:description About page 
About
=====

This is the stagnant project demo page, the project can be found here: [https://git.sr.ht/~poptart/stagnant](https://git.sr.ht/~poptart/stagnant).

Bring your own tools.

A site/i/logo-small.png => site/i/logo-small.png +0 -0

M site/index.md => site/index.md +135 -19
@@ 1,20 1,136 @@
//META:title Hosaka Corp - Shell Distributors
//META:description Index of Research and Blog Posts
//META:title Stagnant Project Index Page 
//META:description A fun side project 
//META:style /style.css
<div id=post>0x0B <a href=/p/ssh-pki.html>creating a PKI for OpenSSH</a></div>
<div id=post>0x0A <a href=/p/slides.html>hosakahashi: takahashi in pure css</a></div>
<div id=post>0x09 <a href=/p/voidmap-pentest.html>using voidmap for penetration test project management</a></div>
<div id=post>0x08 <a href=/p/ldpreload-hashcat.html>LD_PRELOAD, hashcat, and bad ideas</a></div>
<div id=post>0x07 <a href=/p/riscv-asm.html>RISC-V assembly and shellcode creation series</a>
<ul>
 <li>Part 1: <a href="/p/riscv-asm-1.html">the basics</a></li>
 <li>Part 2: <a href="/p/riscv-asm-2.html">some complexity</a></li>
 <li>...</li>
</div>
<div id=post>0x06 <a href=/p/systemd-user-msf.html>systemd user persistence in metasploit</a></div>
<div id="post">0x05 <a href="/t/ipv6.html">talk: ipv6 for pentesters</a></div>
<div id="post">0x04 <a href="/p/go-spark.html">Go+SPARK</a></div>
<div id="post">0x03 <a href="/p/wireguard-basic.html">wireguard quickstart</a></div>
<div id="post">0x02 <a href="/p/systemd-user.html">abusing systemd user services</a></div>
<div id="post">0x01 <a href="/p/unicode-rot.html">unicode rotation cipher</a></div>
<div id="post">0x00 <a href="/p/tinc-1.1-overview.html">tinc 1.1 overview</a></div>
Stagnant
========

!!! Experimental !!!

A pipeline based static "site" generator focused on allowing flexible generation. Fundamentally, this project is simple and only enforces a simple set of rules, filesystem structures, and a small subset of exposed environment variables.

## Demo Pages

* [About](/about.html)

Reasoning
---------

I write my entire life in markdown, and things that aren't in markdown get generators to turn them into markdown for my own consumption. While some static site generators have a common markdown backend I found that often the generated code was hard to pipeline into other outputs, such as Gopher. 

Additionally, my CI of choice is Laminar-CI which takes the opinionated stance of "just use job shell scripts". I found this rather freeing from implementation specific DSLs or janky non-reproducible web interface configuration management.

I wanted something that allowed me to write a simple step-by-step sychronous pipeline using POSIX.1-2008 shell scripts and allow me to write the pipeline steps in any language I felt like programming in.

Versions
--------

stagnant contains 2 supported versions:

* `stagnant.sh` - A POSIX.1-2008 shell script that contains all the logic for building a site
* `stagnant.go` - The Go implementation that is used for more programmatic logic, will eventually contain an extention to load Go plugins

Example Structure
-----------------

The only directories that are fundamental to the creation of a "site" is the `site` directory which contains the initial format of the site and the functions (`stages`) directory that contains the staged pipeline pieces of code. It is also highly suggested to create a `util` directory that contains executables or scripts used during pipelining.

My personal project root looks like this (directories marked with `<` are required for the generator to work by default):

```
.
|-- README.md
|-- build/
|-- stages/	<
|-- gen.sh*
|-- prod/
|-- site/	<
|-- tmpl/
`-- util/	<
```

```
.
|-- build/
|   |-- 1581996075/
|   |-- 1581996127/
|   |-- 1581996133/
|   `-- latest@ -> 1581996133
|-- stages/
|   |-- 00-depends.sh*
|   |-- 01-slides.sh*
|   |-- 02-http.sh*
|   |-- 03-removemd.sh*
|   |-- 04-minify.sh*
|   `-- scripts/
|       |-- clean.sh*
|       `-- deploy.sh*
|-- gen.sh*
|-- prod/
|   |-- 403.html
|   |-- 404.html
|   |-- 50x.html
|   |-- a/
|   |-- d/
|   |-- e/
|   |-- f/
|   |-- favicon.ico
|   |-- favicon.png
|   |-- i/
|   |-- id.html
|   |-- index.html
|   |-- keybase.txt
|   |-- keys.txt
|   |-- n/
|   |-- p/
|   |-- pub.key
|   |-- resume.pdf
|   |-- rss.xml
|   |-- style.css
|   |-- t/
|   `-- talks.html
|-- site/
|   |-- 403.md
|   |-- 404.md
|   |-- 50x.md
|   |-- f/
|   |-- favicon.ico
|   |-- favicon.png
|   |-- i/
|   |-- id.md
|   |-- index.md
|   |-- keys.txt
|   |-- p/
|   |-- pub.key
|   |-- rss.xml
|   |-- t/
|   `-- talks.md
|-- tmpl/
|   |-- footer.html
|   |-- header.html
|   |-- meta.html
|   |-- rss.xml
|   |-- rss_item.xml
|   |-- style.css
|   `-- template.html
`-- util/
    |-- hdev*
    |-- hmdtohtml*
    |-- hosakahashi*
    `-- minify*

``` 

Exposed Variables
-----------------

The following variables are exposed to all running stage executables/scripts and to any generator scripts:

* `_ORIGIN` - The origin directory that is `pwd`
* `_SITEDIR` - Contains all the site files. In my case I use a bunch of .md files and assets that get built
* `_BUILDROOT` - The `build` directory that contains all the build runs and a symlink (`$_BUILDROOT/latest`) to the last build
* `_BUILDID` - The current running build ID, by default uses UNIX timestamps
* `_BUILDDIR` - The current running build and build ID in absolute path form
* `_STAGEDIR` - Directory containing the stage scripts or executables
* `_SCRIPTDIR` - Directory containing scripts that are used for "out-of-build" site management (for example, `clean.sh` cleans my build dirs)
* `_UTILDIR` - Utilities that are required for building are expected to be built into this directory
* `_PRODDIR` - The current "production" build. The idea is that this can contain the code that is committed to CI and all tests and deployments can be run without pulling in all the `stagnant` repo

M stages/00-depends.sh => stages/00-depends.sh +4 -6
@@ 1,7 1,5 @@
#!/bin/sh -e
[ ! -z "$_ROOT" ] || exit 1
[ -f "$_ROOT/util/minify" ] || exit 2
[ -f "$_ROOT/util/smu" ] || exit 2
[ -f "$_ROOT/util/hmdtohtml" ] || exit 2
[ -f "$_ROOT/util/hosakahashi" ] || exit 2
[ -d "$_ROOT/site/" ] || exit 2
[ ! -z "$_UTILDIR" ] || exit 1
[ -f "$_UTILDIR/minify" ] || command -v minify || exit 2
[ -f "$_UTILDIR/mdtohtml" ] || command -v mdtohtml || exit 2
[ -d "$_ORIGIN/site/" ] || exit 2

M stages/01-http.sh => stages/01-http.sh +7 -5
@@ 1,15 1,17 @@
#!/bin/sh
META="$(cat "$_ROOT"/tmpl/meta.html)"
HEADER="$(cat "$_ROOT"/tmpl/header.html)"
FOOTER="$(cat "$_ROOT"/tmpl/footer.html)"
META="$(cat "$_ORIGIN"/tmpl/meta.html)"
HEADER="$(cat "$_ORIGIN"/tmpl/header.html)"
FOOTER="$(cat "$_ORIGIN"/tmpl/footer.html)"
CSS="$(cat "$_ORIGIN"/tmpl/style.css)"
export TMPL_TITLE="⊕ hosaka corp"
export TMPL_DESCR="Digital voodoo, cyber-witchcraft, and shell conjuring."
export TMPL_STYLE="/style.css"
find "${_BUILD}" -name '*.md' | while IFS= read -r file; do
printf "%s\\n" "$CSS" > "$_SITEDIR/style.css"
find "${_BUILDDIR}" -name '*.md' | while IFS= read -r file; do
        # Search for template meta tags
        [ -n "$(grep '//META:style' "$file")" ] && TMPL_STYLE="$(grep '//META:style' "$file" | sed -e 's%//META:style %%')"
        [ -n "$(grep '//META:title' "$file")" ] && TMPL_TITLE="$(grep '//META:title' "$file" | sed -e 's%//META:title %%')"
        [ -n "$(grep '//META:description' "$file")" ] && TMPL_DESCR="$(grep '//META:description' "$file" | sed -e 's%//META:description %%')"
        BODY="$(grep -v '//META:' "$file" | "$_UTIL"/hmdtohtml | "$_UTIL"/minify --type html)"
        BODY="$(grep -v '//META:' "$file" | "$_UTILDIR"/mdtohtml | "$_UTILDIR"/minify --type html)"
        echo "$META$HEADER$BODY$FOOTER" | sed -e "s/{{title}}/$TMPL_TITLE/g" -e "s/{{description}}/$TMPL_DESCR/g" -e "s%{{style}}%$TMPL_STYLE%g" > "$(echo "$file" | sed 's/\.md/\.html/')"
done

M stages/02-removemd.sh => stages/02-removemd.sh +1 -1
@@ 1,2 1,2 @@
#!/bin/sh
find "$_BUILD" -iname '*.md' -exec rm {} \;
find "$_BUILDDIR" -iname '*.md' -exec rm {} \;

M stages/03-minify.sh => stages/03-minify.sh +2 -2
@@ 1,3 1,3 @@
#!/bin/sh
"$_UTIL"/minify --type css < "$_ROOT"/tmpl/style.css > "$_BUILD"/style.css
find "$_BUILD" -type f -name '*.html' -exec sh -c 'cat "$1" | $_UTIL/minify --type html -o "$1"' -- {} \;
"$_UTILDIR"/minify --type css < "$_ROOT"/tmpl/style.css > "$_BUILDDIR"/style.css
find "$_BUILDDIR" -type f -name '*.html' -exec sh -c 'cat "$1" | $_UTILDIR/minify --type html -o "$1"' -- {} \;

A stagnant.go => stagnant.go +126 -0
@@ 0,0 1,126 @@
package main

import (
	util "./util"
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"sort"
	"strconv"
	"time"
)

var builddir = "build"
var stagedir = "stages"
var scriptdir = stagedir + "/scripts"
var utildir = "util"
var proddir = "prod"
var sitedir = "site"
var origin string
var buildid string

type DirectoryEnv struct {
	Path     string
	Variable string
}

type StagnantBuild struct {
	BuildEnv []DirectoryEnv
	BuildRoot,
	BuildDir,
	StageDir,
	ScriptsDir,
	ProdDir,
	OriginDir,
	UtilDir,
	SiteDir,
	BuildId DirectoryEnv
}

func main() {
	origin, err := os.Getwd()
	if err != nil {
		fmt.Println(err)
	}
	var build = StagnantBuild{
		OriginDir:  DirectoryEnv{Path: origin, Variable: "_ORIGIN"},
		BuildRoot:  DirectoryEnv{Path: origin + "/" + builddir, Variable: "_BUILDROOT"},
		BuildId:    DirectoryEnv{Path: strconv.FormatInt(int64(time.Now().Unix()), 10), Variable: "_BUILDID"},
		StageDir:   DirectoryEnv{Path: origin + "/" + stagedir, Variable: "_STAGEDIR"},
		ScriptsDir: DirectoryEnv{Path: origin + "/" + scriptdir, Variable: "_SCRIPTDIR"},
		UtilDir:    DirectoryEnv{Path: origin + "/" + utildir, Variable: "_UTILDIR"},
		ProdDir:    DirectoryEnv{Path: origin + "/" + proddir, Variable: "_PRODDIR"},
		SiteDir:    DirectoryEnv{Path: origin + "/" + sitedir, Variable: "_SITEDIR"},
	}
	build.BuildDir = DirectoryEnv{Path: build.BuildRoot.Path + "/" + build.BuildId.Path, Variable: "_BUILDDIR"}
	//TODO I should make function types for StagnanBuild and create a Add() and Remove() so that these can be tracked instead of this crap
	build.BuildEnv = append(build.BuildEnv, build.OriginDir, build.BuildRoot, build.BuildId, build.StageDir, build.ScriptsDir, build.UtilDir, build.ProdDir, build.SiteDir, build.BuildDir)

	if os.MkdirAll(build.BuildRoot.Path, 0755) != nil {
		fmt.Println(err)
		os.Exit(1)
	}
	if os.MkdirAll(build.ProdDir.Path, 0755) != nil {
		fmt.Println(err)
		os.Exit(1)
	}
	fmt.Println(build.BuildDir.Path)
	//TODO check for required directories
	if util.CopyDir(build.SiteDir.Path, build.BuildDir.Path) != nil {
		fmt.Println(err)
		os.Exit(1)
	}
	fi, err := os.Lstat(build.BuildRoot.Path + "/latest")
	if err != nil && !os.IsNotExist(err) {
		fmt.Println(err)
		os.Exit(3)
	}
	if os.IsNotExist(err) {
		os.Symlink(build.BuildId.Path, build.BuildRoot.Path+"/latest")

	} else {
		switch mode := fi.Mode(); {
		case mode&os.ModeSymlink != 0:
			err = os.Remove(build.BuildRoot.Path + "/latest")
			if err != nil {
				os.Exit(2)
			}
		}
		os.Symlink(build.BuildId.Path, build.BuildRoot.Path+"/latest")
	}
	bds, err := os.Open(build.StageDir.Path)
	if err != nil {
		//TODO error
		os.Exit(3)
	}
	defer bds.Close()
	dirs, err := bds.Readdirnames(0)
	if err != nil {
		os.Exit(3)
	}
	//Quick hack, this needs a cleaned up and remove the useless var
	var a []string
	for _, m := range dirs {
		//Ignore filepath error since it is static here
		matched, _ := filepath.Match("[0-9]*-*", m)
		if matched {
			a = append(a, m)
		}
	}
	dirs = a
	sort.Strings(dirs)
	for _, stage := range dirs {
		cmd := exec.Command(build.StageDir.Path + "/" + stage)
		cmd.Env = append(os.Environ())
		for _, e := range build.BuildEnv {
			//TODO should I be quoting these variables? Almost certainly
			cmd.Env = append(cmd.Env, e.Variable+"="+e.Path)
		}
		fmt.Println(cmd.Path)
		if err := cmd.Run(); err != nil {
			fmt.Println(err)
			os.Exit(4)
		}
	}
}

R gen.sh => stagnant.sh +20 -21
@@ 5,16 5,17 @@ export _ROOT
_ORIGIN="$(pwd)"
_ROOT=$(dirname "$(readlink -f "$0")")

export _FUNCDIR="$_ROOT/stages"
export _STAGEDIR="$_ROOT/stages"
export _SCRIPTDIR="$_ROOT/stages/scripts"
export _BUILDROOT="$_ROOT/build"
export _UTIL="$_ROOT/util"
export _PROD="$_ROOT/prod"
export _ID=""
export _BUILD=""
export _UTILDIR="$_ROOT/util"
export _PRODDIR="$_ROOT/prod"
export _SITEDIR="$_ROOT/site"
export _BUILDID=""
export _BUILDDIR=""
export _DEPLOY=0
_ID="$(date +%s)"
_BUILD="$_BUILDROOT/$_ID"
_BUILDID="$(date +%s)"
_BUILDDIR="$_BUILDROOT/$_BUILDID"

HELPDIAG="[-p]\\n   -p: tag and stage for deployment\\n   -s [script]: list scripts with no arguments, or run a script"



@@ 57,24 58,22 @@ while :; do
done

cd "$_ROOT" || exit 3
mkdir -p "$_BUILD"
printf "%s\\n" "$_ID"
cp -r "${_ROOT}"/site/* "$_BUILD"
mkdir -p "$_BUILDDIR"
printf "%s\\n" "$_BUILDDIR"
cp -r "${_ROOT}"/site/* "$_BUILDDIR"
[ -h "$_BUILDROOT/latest" ] && rm "$_BUILDROOT/latest"
ln -s "$_BUILD" "$_BUILDROOT/latest"
cd "$_BUILD" || exit 3
find "$_FUNCDIR/" -type f -name '[0-9]*-*.sh' -print0 | sort -n -z | xargs -0 -r sh -c '"$@" || exit 255' 
ln -s "$_BUILDDIR" "$_BUILDROOT/latest"
cd "$_BUILDDIR" || exit 3

# This is the old order rendering function that was based around staged execution. In reality just making the "stages" numeric makes more sense
#export _ORDER="pre:render:post"
#for f in $(printf "%s\\n" "$_ORDER" | tr ':' '\n'); do 
#	find "$_FUNCDIR/$f" -type f -print0 -name '[0-9]*-*.sh' | xargs -0 -I {} sh -c "{}"
#done 
# we should prefer a single find and args pipeline with null termination... but unfortunately the xargs sh -c "{}" doesn't seem to exit properly
for sc in $(find "$_STAGEDIR/" -type f -name '[0-9]*-*.sh' -print | sort -n); do
	$sc || (printf "%s stage failed\\n" "$(basename "$sc")" >&2 ; exit 2)
done

if [ "$_DEPLOY" -ne 0 ]; then
	[ -d "$_PROD" ] && rm -rf "$_PROD"
	mkdir -p "$_PROD"
	cp -r "$_BUILDROOT/latest/"* "$_PROD"
	[ -d "$_PRODDIR" ] && rm -rf "$_PRODDIR"
	mkdir -p "$_PRODDIDIRR"
	cp -r "$_BUILDROOT/latest/"* "$_PRODDIR"
fi

cd "$_ORIGIN" || exit 3

A tmpl/footer.html => tmpl/footer.html +5 -0
@@ 0,0 1,5 @@
</div>
<footer>
<a href='https://hosakacorp.net'>hosaka</a>
</footer>
</body>

A tmpl/header.html => tmpl/header.html +8 -0
@@ 0,0 1,8 @@
<header><h1><a href="/"><img id="logo" src="/i/logo-small.png"/></a></h1><br><h2>a hosaka corp project</h2></header>
<div id='wrapper'>
<ul class="nav nav-tabs">
<li class=active><a href=/>/</a>
<li><a href=/about.html>about</a>
<li><a href=https://git.sr.ht/~poptart/stagnant>code</a>
</ul>


A tmpl/meta.html => tmpl/meta.html +10 -0
@@ 0,0 1,10 @@
<!DOCTYPE html>
<meta charset=utf-8>
<meta name=viewport content="width=device-width, initial-scale=1">
<meta name=description content='{{description}}'>
<meta name=theme-color content="#cee318"> 
<head>
	<link rel="stylesheet" href="{{style}}">
	<link rel="icon" type="image/png" href="/i/favicon.png">
<head>
<title>{{title}}</title>

A tmpl/style.css => tmpl/style.css +238 -0
@@ 0,0 1,238 @@
body {
  padding: 0;
  margin: 0;
  font-family: Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, Courier New, monospace, serif;
  font-size: 100%;
  background-color: #222;
  overflow-y: scroll;
}

table {
  border-collapse: collapse;
}

tr, td {
  padding: 10px;
  border: 1px #93a1a1 solid;
  text-align: center;
}
th {
  color: #222;
  background-color: #93a1a1;
}

#wrapper {
  width: 90%;
  max-width: 720px;
  margin: auto;
}


#post {
  padding: 5px;
}

#post:hover {
  background-color: #111;
}

#rsslink {
  float: right;
}

footer {
  text-align: center;
  margin: auto;
  width: 70%;
  max-width: 720px;
  border-top: 1px solid #333;
  padding: 10px;
}

#blank {
  height: 100%;
  padding: 10px;
}

h1 {
  font-size: 40px;
}

h1,h2 {
  display: block;
}

h2 {
  font-weight: 400;
  color: #666;
  font-size: 100%;
}

h3 {
  text-decoration: underline;
}

a {
  color: #666;
  text-decoration: none;
  border-bottom: 1px dotted grey;
}

a:hover {
  color: #cee318;
}

h1 a,h2 a,h3 a {
  font-style: normal;
}

header {
  text-align: center;
}

header h1 a {
  color: #93a1a1;
  border: 0;
  text-shadow: 0 0 8px #000;
}

header h1 a:hover {
  color: #fff;
  text-shadow: 0 0 8px #cee318;
}

code,pre {
  color: inherit;
  font-family: monospace;
  font-size: 95%;
  padding: 2px;
  background-color: #93a1a1;
  color: #222;
  border-radius: 4px;
  word-wrap: break-word;
  scrollbar-color: #cee318 #1a1a1a;
  scrollbar-width: thin;
}

pre code:before,pre code:after {
  content: none;
}

pre {
  padding: 10px;
  overflow-x: auto;
  overflow-y: hidden;
}

pre code {
  padding: 0;
  border: 0;
}

blockquote,q {
  color: #666;
  font-style: italic;
  border-left: 8px solid #aaa;
  padding-left: 8px;
  margin-left: -8px;
}

.strike {
  text-decoration: line-through;
}

img {
  display: block;
  padding: 8px;
  border-radius: 4px;
  margin: auto;
  max-width: 100%;
}

#logo {
  display: block;
  padding: 8px;
  border-radius: 4px;
  margin: auto;
  max-width: 5em;
  max-height: 5em;
}

video {
  width: 100%;
}

.caption {
  display: block;
  font-style: italic;
  margin: auto;
  text-align: center;
}

html {
  font-family: monospace;
  color: #aaa;
  text-shadow: 0 0 1px rgba(0,0,0,.3);
  scroll-behavior: smooth;
}

.nav {
  padding-left: 0;
  margin-bottom: 0;
  list-style: outside none none;
}

.nav::after {
  display: table;
  content: " ";
  clear: both;
}

.nav>li {
  position: relative;
  display: block;
}

.nav>li>a {
  position: relative;
  display: block;
  padding: 10px 15px;
  border-bottom: 1px solid #333;
}

.nav>li>a:focus,.nav>li>a:hover {
  text-decoration: none;
  background-color: #171717;
  border-bottom: 1px solid #cee318;
}

.nav-tabs {
  border-bottom: 1px solid #333;
}

.nav-tabs>li {
  float: left;
  margin-bottom: -1px;
}

.nav-tabs>li>a {
  margin-right: 2px;
  line-height: 1.42857;
}

.nav-tabs>li.active>a {
  cursor: default;
  border-bottom: 1px solid #cee318;
}


::-webkit-scrollbar {
  width: 5px;
  height: 8px;
  background-color: #1a1a1a;
}

::-webkit-scrollbar-thumb {
    background: #cee318; 
}


A util/file.go => util/file.go +134 -0
@@ 0,0 1,134 @@
package util

import (
	"fmt"
	"io"
	"io/ioutil"
	"os"
	"path/filepath"
)

/* MIT License
 *
 * Copyright (c) 2017 Roland Singer [roland.singer@desertbit.com]
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

// CopyFile copies the contents of the file named src to the file named
// by dst. The file will be created if it does not already exist. If the
// destination file exists, all it's contents will be replaced by the contents
// of the source file. The file mode will be copied from the source and
// the copied data is synced/flushed to stable storage.
func CopyFile(src, dst string) (err error) {
	in, err := os.Open(src)
	if err != nil {
		return
	}
	defer in.Close()

	out, err := os.Create(dst)
	if err != nil {
		return
	}
	defer func() {
		if e := out.Close(); e != nil {
			err = e
		}
	}()

	_, err = io.Copy(out, in)
	if err != nil {
		return
	}

	err = out.Sync()
	if err != nil {
		return
	}

	si, err := os.Stat(src)
	if err != nil {
		return
	}
	err = os.Chmod(dst, si.Mode())
	if err != nil {
		return
	}

	return
}

// CopyDir recursively copies a directory tree, attempting to preserve permissions.
// Source directory must exist, destination directory must *not* exist.
// Symlinks are ignored and skipped.
func CopyDir(src string, dst string) (err error) {
	src = filepath.Clean(src)
	dst = filepath.Clean(dst)

	si, err := os.Stat(src)
	if err != nil {
		return err
	}
	if !si.IsDir() {
		return fmt.Errorf("source is not a directory")
	}

	_, err = os.Stat(dst)
	if err != nil && !os.IsNotExist(err) {
		return
	}
	if err == nil {
		return fmt.Errorf("destination already exists")
	}

	err = os.MkdirAll(dst, si.Mode())
	if err != nil {
		return
	}

	entries, err := ioutil.ReadDir(src)
	if err != nil {
		return
	}

	for _, entry := range entries {
		srcPath := filepath.Join(src, entry.Name())
		dstPath := filepath.Join(dst, entry.Name())

		if entry.IsDir() {
			err = CopyDir(srcPath, dstPath)
			if err != nil {
				return
			}
		} else {
			// Skip symlinks.
			if entry.Mode()&os.ModeSymlink != 0 {
				continue
			}

			err = CopyFile(srcPath, dstPath)
			if err != nil {
				return
			}
		}
	}

	return
}