better version of the base system
This is a complete redesign of the blog system. This time, we just make it as simple as possible without layer on layer of abstraction.
This commit is contained in:
parent
fce1df3f21
commit
e8fcff2cbb
25
boot.go
25
boot.go
|
@ -1,25 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"github.com/gibheer/zero-blog/lib"
|
||||
)
|
||||
|
||||
func boot_system() (*lib.Environment, error) {
|
||||
env := &lib.Environment{}
|
||||
settings, err := lib.LoadConfiguration()
|
||||
if err != nil {
|
||||
return env, err
|
||||
}
|
||||
env.Config = settings
|
||||
|
||||
env.DB, err = lib.NewDatabase(settings.Connection)
|
||||
if err != nil {
|
||||
return env, err
|
||||
}
|
||||
env.Template, err = lib.LoadTemplates(settings.Templates)
|
||||
if err != nil {
|
||||
return env, err
|
||||
}
|
||||
|
||||
return env, nil
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
package admin
|
||||
|
||||
import (
|
||||
"log"
|
||||
"github.com/gibheer/zero-blog/lib"
|
||||
)
|
||||
|
||||
func LoginGet(c *lib.Context) error {
|
||||
c.Env.Template.Lookup("login/index").Execute(c.Response, struct {}{})
|
||||
return nil
|
||||
}
|
||||
|
||||
func LoginPost(c *lib.Context) error {
|
||||
username := c.Request.PostFormValue("username")
|
||||
password := c.Request.PostFormValue("password")
|
||||
if username == "" && password == "" {
|
||||
log.Print("There was nothing at all!")
|
||||
} else {
|
||||
log.Print(username, password)
|
||||
}
|
||||
c.Env.Template.Lookup("login/index").Execute(c.Response, struct {}{})
|
||||
return nil
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
package controller
|
||||
|
||||
import (
|
||||
"github.com/gibheer/zero-blog/lib"
|
||||
"github.com/gibheer/zero-blog/middleware"
|
||||
"github.com/gibheer/zero-blog/controller/admin"
|
||||
"github.com/gibheer/zero-blog/controller/welcome"
|
||||
)
|
||||
|
||||
func DefineRoutes(router *lib.Router) {
|
||||
router.Get("/", welcome.Welcome)
|
||||
router.Get( "/login", admin.LoginGet)
|
||||
router.Post("/login", admin.LoginPost)
|
||||
authentication := router.NewGroup("/admin")
|
||||
authentication.Use(middleware.Authentication)
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
package welcome
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/gibheer/zero-blog/lib"
|
||||
)
|
||||
|
||||
func Welcome(c *lib.Context) error {
|
||||
fmt.Fprint(c.Response, "Hello World!")
|
||||
return nil
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
package lib
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"database/sql"
|
||||
pq "github.com/lib/pq"
|
||||
)
|
||||
|
||||
type Database struct {
|
||||
Conn *sql.DB
|
||||
}
|
||||
|
||||
func (d *Database) Query(query string) (*sql.Rows, error) {
|
||||
res, err := d.Conn.Query(query)
|
||||
if err != nil {
|
||||
err := err.(*pq.Error)
|
||||
return nil, errors.New(err.Code.Name())
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func NewDatabase(connection string) (*Database, error) {
|
||||
if connection == "" {
|
||||
return nil, errors.New("No connection string given! Check the config.yml file!")
|
||||
}
|
||||
DB, err := sql.Open("postgres", connection)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Database{DB}, nil
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
package lib
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"text/template"
|
||||
"gopkg.in/yaml.v1"
|
||||
)
|
||||
|
||||
type Settings struct {
|
||||
Connection string
|
||||
Templates string
|
||||
}
|
||||
|
||||
type Environment struct {
|
||||
// settings from the config file
|
||||
Config *Settings
|
||||
// the database connection pool
|
||||
DB *Database
|
||||
// the base template system
|
||||
Template *template.Template
|
||||
}
|
||||
|
||||
func LoadConfiguration() (*Settings, error) {
|
||||
configfile, err := ioutil.ReadFile("config.yml")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
settings := Settings{}
|
||||
err = yaml.Unmarshal(configfile, &settings)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &settings, nil
|
||||
}
|
135
lib/router.go
135
lib/router.go
|
@ -1,135 +0,0 @@
|
|||
package lib
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"github.com/julienschmidt/httprouter"
|
||||
"github.com/gibheer/zero-blog/repository"
|
||||
)
|
||||
|
||||
// the following is inspired by the gin framework
|
||||
|
||||
// this router defines each action to take for an URI including the middlewares
|
||||
type Router struct {
|
||||
// the environment of the application with settings and database connection
|
||||
env *Environment
|
||||
// path that this engine takes care of relative to the parent
|
||||
path string
|
||||
// the list of functions to run on a request
|
||||
funcList []ContextFunc
|
||||
// the router to use as the main entity
|
||||
router *httprouter.Router
|
||||
// the parent router, if any
|
||||
parent *Router
|
||||
}
|
||||
|
||||
// create a new router with the specific environment
|
||||
func NewRouter(env *Environment) *Router {
|
||||
return &Router{env, "", make([]ContextFunc, 0), httprouter.New(), nil}
|
||||
}
|
||||
|
||||
// Bundle all parameters into the context to make it easier to push important
|
||||
// data into functions.
|
||||
type Context struct {
|
||||
// the current request
|
||||
Request *http.Request
|
||||
// the response
|
||||
Response http.ResponseWriter
|
||||
// parameters provided by the router
|
||||
Params httprouter.Params
|
||||
// current session
|
||||
Session *repository.Session
|
||||
|
||||
// the list of functions to run
|
||||
funcList []ContextFunc
|
||||
// the current position in the function list
|
||||
current int
|
||||
// a direct link to the environment
|
||||
Env *Environment
|
||||
}
|
||||
|
||||
// the func type for internal routes
|
||||
type ContextFunc func(*Context) error
|
||||
|
||||
func (r *Router) fullpath(path string) string {
|
||||
if r.parent != nil {
|
||||
return r.parent.fullpath(r.path + path)
|
||||
}
|
||||
return r.path + path
|
||||
}
|
||||
|
||||
func (r *Router) fullFuncList() []ContextFunc {
|
||||
if r.parent != nil {
|
||||
return append(r.parent.fullFuncList(), r.funcList...)
|
||||
}
|
||||
return r.funcList
|
||||
}
|
||||
|
||||
func (r *Router) Get(path string, target ContextFunc) {
|
||||
r.addRoute("GET", path, target)
|
||||
}
|
||||
|
||||
func (r *Router) Post(path string, target ContextFunc) {
|
||||
r.addRoute("POST", path, target)
|
||||
}
|
||||
|
||||
func (r *Router) Put(path string, target ContextFunc) {
|
||||
r.addRoute("PUT", path, target)
|
||||
}
|
||||
|
||||
func (r *Router) Delete(path string, target ContextFunc) {
|
||||
r.addRoute("DELETE", path, target)
|
||||
}
|
||||
|
||||
func (router *Router) addRoute(method, path string, target ContextFunc) {
|
||||
router.router.Handle(
|
||||
method,
|
||||
router.fullpath(path),
|
||||
router.createHandleFunction(target),
|
||||
)
|
||||
}
|
||||
|
||||
func (r *Router) createHandleFunction(target ContextFunc) httprouter.Handle {
|
||||
return func(
|
||||
response http.ResponseWriter,
|
||||
request *http.Request,
|
||||
params httprouter.Params) {
|
||||
|
||||
ctx := &Context{
|
||||
Request: request,
|
||||
Response: response,
|
||||
Params: params,
|
||||
funcList: append(r.fullFuncList(), target),
|
||||
current: 0,
|
||||
Env: r.env,
|
||||
}
|
||||
for !ctx.Aborted() && ctx.current < len(ctx.funcList) {
|
||||
ctx.funcList[ctx.current](ctx)
|
||||
ctx.current++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Router) Use(middleware ContextFunc) {
|
||||
r.funcList = append(r.funcList, middleware)
|
||||
}
|
||||
|
||||
func (r *Router) NewGroup(path string) *Router {
|
||||
return &Router{r.env, path, make([]ContextFunc, 0), r.router, r}
|
||||
}
|
||||
|
||||
func (r *Router) Start() {
|
||||
log.Print("Starting to listen for incoming requests ...")
|
||||
log.Fatal(http.ListenAndServe(":9292", r.router))
|
||||
}
|
||||
|
||||
func (c *Context) Aborted() bool {
|
||||
if c.current < 0 {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *Context) Abort() {
|
||||
c.current = -1
|
||||
}
|
|
@ -1,50 +0,0 @@
|
|||
package lib
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
type fileList struct {
|
||||
len_base int
|
||||
t *template.Template
|
||||
}
|
||||
|
||||
// load all templates found as childs of the path
|
||||
func LoadTemplates(path string) (*template.Template, error) {
|
||||
// TODO add better check for template directory
|
||||
if path == "" { return nil, errors.New("template path empty") }
|
||||
f := &fileList{len(path), &template.Template{}}
|
||||
|
||||
err := filepath.Walk(path, f.scanFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return f.t, nil
|
||||
}
|
||||
|
||||
func (f *fileList) scanFile(path string, info os.FileInfo, err error) error {
|
||||
if err != nil { log.Println(`Error with file:`, path, `-`, err) }
|
||||
|
||||
if info.IsDir() {
|
||||
log.Print(`Scanning '`, path, `' for templates`)
|
||||
}
|
||||
if !info.IsDir() && path[len(path) - 5:] == `.tmpl` {
|
||||
name := path[f.len_base + 1 : len(path) - 5]
|
||||
log.Println(`Adding:`, name)
|
||||
f.t.New(name).Parse(read_file_content(path))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func read_file_content(path string) string {
|
||||
content, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return string(content)
|
||||
}
|
71
main.go
71
main.go
|
@ -1,20 +1,77 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"log"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"github.com/gibheer/zero-blog/lib"
|
||||
"github.com/gibheer/zero-blog/controller"
|
||||
"time"
|
||||
|
||||
"github.com/julienschmidt/httprouter"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
type (
|
||||
Config struct {
|
||||
Address string // which address should be listened on
|
||||
Port int // the port to listen for the admin panel
|
||||
Connection string // the URL to the database
|
||||
TemplatePath string // path to the templates
|
||||
}
|
||||
Logger struct {
|
||||
router *httprouter.Router
|
||||
}
|
||||
)
|
||||
|
||||
// logger works as a middleware here
|
||||
func (l Logger) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
start := time.Now()
|
||||
w.Header()["Content-Type"] = []string{"text/html"}
|
||||
l.router.ServeHTTP(w, r)
|
||||
log.Print(r.URL.Path, ";", time.Since(start))
|
||||
}
|
||||
|
||||
func main() {
|
||||
environment, err := boot_system()
|
||||
var config_path string
|
||||
flag.StringVar(&config_path, "config", "config.yml", "path to the config file")
|
||||
flag.Parse()
|
||||
|
||||
config, err := readConfig(config_path)
|
||||
if err != nil {
|
||||
log.Fatal("Boot crashed with message: ", err)
|
||||
log.Println("Error with config file: ", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if err := templates_load(config.TemplatePath); err != nil {
|
||||
log.Printf("Error when loading the templates: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
router := lib.NewRouter(environment)
|
||||
controller.DefineRoutes(router)
|
||||
router.Start()
|
||||
router := httprouter.New()
|
||||
define_routes(router)
|
||||
|
||||
start_server(config, router)
|
||||
}
|
||||
|
||||
// Reads the config from config_path and returns the settings.
|
||||
func readConfig(config_path string) (*Config, error) {
|
||||
if config_path == "" {
|
||||
return nil, errors.New("No config path given!")
|
||||
}
|
||||
file, err := ioutil.ReadFile(config_path)
|
||||
if err != nil { return nil, err }
|
||||
|
||||
config := Config{}
|
||||
if err := yaml.Unmarshal(file, &config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
// start the server with access log
|
||||
func start_server(config *Config, router *httprouter.Router) {
|
||||
logger := Logger{router}
|
||||
log.Print(http.ListenAndServe(fmt.Sprintf("%v:%v", config.Address, config.Port), logger))
|
||||
}
|
||||
|
|
|
@ -1,27 +0,0 @@
|
|||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"github.com/gibheer/zero-blog/lib"
|
||||
"github.com/gibheer/zero-blog/repository"
|
||||
)
|
||||
|
||||
func Authentication(c *lib.Context) error {
|
||||
session_id, err := c.Request.Cookie("session")
|
||||
if err != nil {
|
||||
redirectToLogin(c)
|
||||
return err
|
||||
}
|
||||
session := repository.GetSession(c.Env.DB.Conn, session_id.Value)
|
||||
if session == nil {
|
||||
redirectToLogin(c)
|
||||
return err
|
||||
}
|
||||
c.Session = session
|
||||
return nil
|
||||
}
|
||||
|
||||
func redirectToLogin(c *lib.Context) {
|
||||
c.Abort()
|
||||
http.Redirect(c.Response, c.Request, "/login", 307)
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
type Account struct {
|
||||
Id int
|
||||
Username string
|
||||
Email string
|
||||
Password string
|
||||
Role string
|
||||
}
|
||||
|
||||
func GetAccount(db *sql.DB, account_id string) *Account {
|
||||
account := &Account{}
|
||||
err := db.QueryRow(
|
||||
`select id, username, email from accounts where id = ?`,
|
||||
account_id).Scan(
|
||||
&account.Id,
|
||||
&account.Username,
|
||||
&account.Email,
|
||||
)
|
||||
// TODO do something with the error
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return account
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
type Session struct {
|
||||
Id string
|
||||
Account *Account
|
||||
}
|
||||
|
||||
func GetSession(db *sql.DB, session_id string) *Session {
|
||||
session := &Session{Account: &Account{}}
|
||||
|
||||
err := db.QueryRow(
|
||||
`select session_id, id, username
|
||||
from sessions s
|
||||
join accounts a
|
||||
on s.account_id = a.id
|
||||
where session_id = ?
|
||||
and last_change > now() - interval '1 hour'`,
|
||||
session_id).Scan(&session.Id, &session.Account.Id, &session.Account.Username)
|
||||
// TODO do something with the error
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return session
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"github.com/julienschmidt/httprouter"
|
||||
)
|
||||
|
||||
// map URLs to their functions
|
||||
func define_routes(router *httprouter.Router) {
|
||||
router.HandlerFunc("GET", "/", Index)
|
||||
|
||||
router.GET("/post", Posts)
|
||||
}
|
||||
|
||||
// the function for route /
|
||||
func Index(w http.ResponseWriter, r *http.Request) {
|
||||
Templates.ExecuteTemplate(w, "welcome", nil)
|
||||
}
|
||||
|
||||
func Posts(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
||||
posts := []int {0, 1, 2, 3, 4}
|
||||
Templates.ExecuteTemplate(w, "posts/index", posts)
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"html/template"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"path/filepath"
|
||||
"os"
|
||||
)
|
||||
|
||||
var (
|
||||
Templates *template.Template // all templates
|
||||
base_path_len int // the base path len to generate a template name
|
||||
)
|
||||
|
||||
// load all templates in the path
|
||||
func templates_load(path string) error {
|
||||
if base_path_len > 0 { return errors.New("Templates already loaded!") }
|
||||
if len(path) == 0 { return errors.New("Path is empty!") }
|
||||
base_path_len = len(path)
|
||||
|
||||
Templates = template.New("dummy")
|
||||
if err := filepath.Walk(path, templates_scan_file); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// scan the file for a useable name and add to template collection
|
||||
func templates_scan_file(path string, info os.FileInfo, err error) error {
|
||||
if err != nil { log.Println(`Error with file:`, path, `-`, err) }
|
||||
|
||||
if !info.IsDir() && path[len(path) - 5:] == `.tmpl` {
|
||||
name := path[base_path_len + 1 : len(path) - 5]
|
||||
template.Must(Templates.New(name).Parse(template_parse(path)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// parse the template or return an error
|
||||
func template_parse(path string) string {
|
||||
content, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return string(content)
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
</body>
|
||||
</html>
|
|
@ -0,0 +1,7 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>foobar</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>This is working!</h1>
|
|
@ -0,0 +1,7 @@
|
|||
{{ template "header" . }}
|
||||
<ul>
|
||||
{{ range . }}
|
||||
<li>{{ . }}
|
||||
{{ end }}
|
||||
</ul>
|
||||
{{ template "footer" . }}
|
|
@ -0,0 +1,3 @@
|
|||
{{ template "header" . }}
|
||||
From me too!
|
||||
{{ template "footer" . }}
|
Loading…
Reference in New Issue