Initial static file router

pull/4/head
Noah 2017-10-07 21:48:58 -07:00
parent 3d30a58ccb
commit 2966cb2c7f
11 changed files with 339 additions and 0 deletions

23
.editorconfig Normal file
View File

@ -0,0 +1,23 @@
# Top-most EditorConfig file
root = true
# Common settings for all files. I don't specify indent type here, because
# some EditorConfig implementations (notably Atom) will cause default settings
# and behaviors (like tab type auto-detection) to be overridden.
[*]
charset = utf-8
end_of_line = lf
trim_trailing_whitespace = true
insert_final_newline = true
[*.go]
indent_style = tab
indent_size = 4
[*.md]
indent_style = space
indent_size = 2
[*.yml]
indent_style = space
indent_size = 2

2
.gitignore vendored
View File

@ -0,0 +1,2 @@
bin/
dist/

34
Makefile Normal file
View File

@ -0,0 +1,34 @@
SHELL := /bin/bash
VERSION=$(shell grep -e 'Version' blog.go | head -n 1 | cut -d '"' -f 2)
BUILD=$(shell git describe --always)
CURDIR=$(shell curdir)
# Inject the build version (commit hash) into the executable.
LDFLAGS := -ldflags "-X main.Build=$(BUILD)"
# `make setup` to set up a new environment, pull dependencies, etc.
.PHONY: setup
setup: clean
go get -u ./...
# `make build` to build the binary.
.PHONY: build
build:
gofmt -w .
go build $(LDFLAGS) -i -o bin/blog main.go
# `make run` to run it in debug mode.
.PHONY: run
run:
./go-reload main.go -debug
# `make test` to run unit tests.
.PHONY: test
test:
go test ./...
# `make clean` cleans everything up.
.PHONY: clean
clean:
rm -rf bin dist

47
core/app.go Normal file
View File

@ -0,0 +1,47 @@
package core
import (
"net/http"
"github.com/gorilla/mux"
"github.com/urfave/negroni"
)
// Blog is the root application object that maintains the app configuration
// and helper objects.
type Blog struct {
// DocumentRoot is the core static files root; UserRoot masks over it.
DocumentRoot string
UserRoot string
// Web app objects.
n *negroni.Negroni // Negroni middleware manager
r *mux.Router // Router
}
// New initializes the Blog application.
func New(documentRoot, userRoot string) *Blog {
blog := &Blog{
DocumentRoot: documentRoot,
UserRoot: userRoot,
}
r := mux.NewRouter()
blog.r = r
r.HandleFunc("/", blog.PageHandler)
r.NotFoundHandler = http.HandlerFunc(blog.PageHandler)
n := negroni.New(
negroni.NewRecovery(),
negroni.NewLogger(),
)
blog.n = n
n.UseHandler(r)
return blog
}
// ListenAndServe begins listening on the given bind address.
func (b *Blog) ListenAndServe(address string) {
log.Info("Listening on %s", address)
http.ListenAndServe(address, b.n)
}

13
core/log.go Normal file
View File

@ -0,0 +1,13 @@
package core
import "github.com/kirsle/golog"
var log *golog.Logger
func init() {
log = golog.GetLogger("blog")
log.Configure(&golog.Config{
Colors: golog.ExtendedColor,
Theme: golog.DarkTheme,
})
}

56
core/pages.go Normal file
View File

@ -0,0 +1,56 @@
package core
import (
"net/http"
"os"
"path/filepath"
"strings"
)
// PageHandler is the catch-all route handler, for serving static web pages.
func (b *Blog) PageHandler(w http.ResponseWriter, r *http.Request) {
path := r.URL.Path
// Remove trailing slashes by redirecting them away.
if len(path) > 1 && path[len(path)-1] == '/' {
Redirect(w, strings.TrimRight(path, "/"))
return
}
// Search for a file that matches their URL.
log.Debug("Resolving filepath for URI: %s", path)
for _, root := range []string{b.DocumentRoot, b.UserRoot} {
relPath := filepath.Join(root, path)
absPath, err := filepath.Abs(relPath)
if err != nil {
log.Error("%v", err)
}
log.Debug("Expected filepath: %s", absPath)
// Found an exact hit?
if stat, err := os.Stat(absPath); !os.IsNotExist(err) && !stat.IsDir() {
log.Debug("Exact filepath found: %s", absPath)
http.ServeFile(w, r, absPath)
return
}
// Try some supported suffixes.
suffixes := []string{
".html",
"/index.html",
".md",
"/index.md",
}
for _, suffix := range suffixes {
if stat, err := os.Stat(absPath + suffix); !os.IsNotExist(err) && !stat.IsDir() {
log.Debug("Filepath found via suffix %s: %s", suffix, absPath+suffix)
http.ServeFile(w, r, absPath+suffix)
return
}
}
}
// No file, must be a 404.
http.NotFound(w, r)
}

9
core/responses.go Normal file
View File

@ -0,0 +1,9 @@
package core
import "net/http"
// Redirect sends an HTTP redirect response.
func Redirect(w http.ResponseWriter, location string) {
w.Header().Set("Location", location)
w.WriteHeader(http.StatusFound)
}

95
go-reload Executable file
View File

@ -0,0 +1,95 @@
#!/bin/bash
# Credit from: https://github.com/alexedwards/go-reload/tree/aabe19d0a9935d1238763a4a35e71787854cd5f5
####################################################
# @kirsle's custom changes from upstream are below #
####################################################
function die() {
echo >&2 $1
exit 1
}
# Before we crash and burn, make sure necessary programs are installed!
command -v inotifywait || die "I need the inotifywait command (apt install inotify-tools)"
################################
# end @kirsle's custom changes #
################################
function monitor() {
if [ "$2" = "true" ]; then
# Watch all files in the specified directory
# Call the restart function when they are saved
inotifywait -q -m -r -e close_write -e moved_to $1 |
while read line; do
restart
done
else
# Watch all *.go files in the specified directory
# Call the restart function when they are saved
inotifywait -q -m -r -e close_write -e moved_to --exclude '[^g][^o]$' $1 |
while read line; do
restart
done
fi
}
# Terminate and rerun the main Go program
function restart {
if [ "$(pidof $PROCESS_NAME)" ]; then
killall -q -w -9 $PROCESS_NAME
fi
echo ">> Reloading..."
eval "go run $ARGS &"
}
# Make sure all background processes get terminated
function close {
killall -q -w -9 inotifywait
exit 0
}
trap close INT
echo "== Go-reload"
WATCH_ALL=false
while getopts ":a" opt; do
case $opt in
a)
WATCH_ALL=true
;;
\?)
echo "Invalid option: -$OPTARG" >&2
exit 0
;;
esac
done
shift "$((OPTIND - 1))"
FILE_NAME=$(basename $1)
PROCESS_NAME=${FILE_NAME%%.*}
ARGS=$@
# Start the main Go program
echo ">> Watching directories, CTRL+C to stop"
eval "go run $ARGS &"
# Monitor all /src directories on the GOPATH
OIFS="$IFS"
IFS=':'
for path in $GOPATH
do
monitor $path/src $WATCH_ALL &
done
IFS="$OIFS"
# If the current working directory isn't on the GOPATH, monitor it too
if [[ $PWD != "$GOPATH/"* ]]
then
monitor $PWD $WATCH_ALL
fi
wait

38
main.go Normal file
View File

@ -0,0 +1,38 @@
// Package blog is a web application which lets you host your own web blog,
// photo albums, wiki, etc.
//
// It is currently under early development and is not yet stable.
package main
import (
"flag"
"github.com/kirsle/blog/core"
)
// Build-time config constants.
var (
Version = "0.0.1"
Build = "live"
DocumentRoot = "root"
)
// Command line args.
var (
fDebug bool
fAddress string
)
func init() {
flag.BoolVar(&fDebug, "debug", false, "Debug mode")
flag.BoolVar(&fDebug, "d", false, "Debug mode (alias)")
flag.StringVar(&fAddress, "address", ":8000", "Bind address")
flag.StringVar(&fAddress, "a", ":8000", "Bind address (alias)")
}
func main() {
flag.Parse()
app := core.New(DocumentRoot, "")
app.ListenAndServe(fAddress)
}

11
root/index.html Normal file
View File

@ -0,0 +1,11 @@
<!DOCTYPE html>
<html>
<head>
<title>Blog</title>
</head>
<body>
<h1>Index</h1>
</body>
</html>

11
root/test.html Normal file
View File

@ -0,0 +1,11 @@
<!DOCTYPE html>
<html>
<head>
<title>Blog</title>
</head>
<body>
<h1>Test</h1>
</body>
</html>