mirror of
https://github.com/Kugelschieber/marvinblum.git
synced 2026-01-18 14:50:27 +00:00
@@ -19,8 +19,9 @@ WORKDIR /app
|
||||
# default config
|
||||
ENV MB_LOGLEVEL=info
|
||||
ENV MB_ALLOWED_ORIGINS=*
|
||||
ENV MB_HOST=0.0.0.0:80
|
||||
ENV MB_HOST=0.0.0.0:8888
|
||||
ENV MB_DB_PORT=5432
|
||||
ENV MB_DB_SSLMODE=disable
|
||||
|
||||
EXPOSE 80
|
||||
EXPOSE 443
|
||||
EXPOSE 8888
|
||||
CMD ["/app/main"]
|
||||
|
||||
11
blog/blog.go
11
blog/blog.go
@@ -2,6 +2,7 @@ package blog
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/Kugelschieber/marvinblum.de/tpl"
|
||||
emvi "github.com/emvi/api-go"
|
||||
"github.com/emvi/logbuch"
|
||||
"io/ioutil"
|
||||
@@ -9,6 +10,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -29,11 +31,13 @@ type Blog struct {
|
||||
articles map[string]emvi.Article // id -> article
|
||||
articlesYear map[int][]emvi.Article // year -> articles
|
||||
nextUpdate time.Time
|
||||
cache *tpl.Cache
|
||||
m sync.Mutex
|
||||
}
|
||||
|
||||
func NewBlog() *Blog {
|
||||
func NewBlog(cache *tpl.Cache) *Blog {
|
||||
logbuch.Info("Initializing blog")
|
||||
b := new(Blog)
|
||||
b := &Blog{cache: cache}
|
||||
b.client = emvi.NewClient(os.Getenv("MB_EMVI_CLIENT_ID"),
|
||||
os.Getenv("MB_EMVI_CLIENT_SECRET"),
|
||||
os.Getenv("MB_EMVI_ORGA"),
|
||||
@@ -76,6 +80,8 @@ func (blog *Blog) GetLatestArticles() []emvi.Article {
|
||||
}
|
||||
|
||||
func (blog *Blog) loadArticles() {
|
||||
blog.m.Lock()
|
||||
defer blog.m.Unlock()
|
||||
logbuch.Info("Refreshing blog articles...")
|
||||
articles, offset, count := make(map[string]emvi.Article), 0, 1
|
||||
var err error
|
||||
@@ -188,6 +194,7 @@ func (blog *Blog) setArticles(articles map[string]emvi.Article) {
|
||||
|
||||
func (blog *Blog) refreshIfRequired() {
|
||||
if blog.nextUpdate.Before(time.Now()) {
|
||||
blog.cache.Clear()
|
||||
blog.loadArticles()
|
||||
}
|
||||
}
|
||||
|
||||
9
dev.sh
9
dev.sh
@@ -10,5 +10,14 @@ export MB_HOT_RELOAD=true
|
||||
export MB_EMVI_CLIENT_ID=3fBBn144yvSF9R3dPC8l
|
||||
export MB_EMVI_CLIENT_SECRET=dw3FeshelTgdf1Gj13J7uF5FfdPDi40sQvvwqeFVKTTyIDuCdlAHhRY72csFL6yg
|
||||
export MB_EMVI_ORGA=marvin
|
||||
export MB_DB_HOST=localhost
|
||||
export MB_DB_PORT=5432
|
||||
export MB_DB_USER=postgres
|
||||
export MB_DB_PASSWORD=postgres
|
||||
export MB_DB_SCHEMA=marvinblum
|
||||
export MB_DB_SSLMODE=disable
|
||||
export MB_DB_SSLCERT=
|
||||
export MB_DB_SSLKEY=
|
||||
export MB_DB_SSLROOTCERT=
|
||||
|
||||
go run main.go
|
||||
|
||||
@@ -5,11 +5,14 @@ services:
|
||||
image: "traefik:v2.2"
|
||||
container_name: traefik
|
||||
restart: always
|
||||
networks:
|
||||
- traefik-internal
|
||||
command:
|
||||
# - "--log.level=DEBUG"
|
||||
# - "--api.insecure=true"
|
||||
- "--providers.docker=true"
|
||||
- "--providers.docker.exposedbydefault=false"
|
||||
- "--providers.docker.network=marvinblum_traefik-internal"
|
||||
- "--entrypoints.web.address=:80"
|
||||
- "--entrypoints.websecure.address=:443"
|
||||
- "--certificatesresolvers.tls-resolver.acme.httpchallenge=true"
|
||||
@@ -24,18 +27,31 @@ services:
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
- /root/marvinblum/letsencrypt:/letsencrypt
|
||||
# labels:
|
||||
# - "traefik.enable=true"
|
||||
# - "traefik.port=8080"
|
||||
# - "traefik.http.routers.traefik.entrypoints=web"
|
||||
# - "traefik.http.routers.traefik.service=api@internal"
|
||||
marvinblum:
|
||||
image: kugel/marvinblum
|
||||
container_name: marvinblum
|
||||
restart: always
|
||||
depends_on:
|
||||
- traefik
|
||||
networks:
|
||||
- postgres_db-internal
|
||||
- traefik-internal
|
||||
environment:
|
||||
- MB_EMVI_CLIENT_ID="3fBBn144yvSF9R3dPC8l"
|
||||
- MB_EMVI_CLIENT_SECRET="dw3FeshelTgdf1Gj13J7uF5FfdPDi40sQvvwqeFVKTTyIDuCdlAHhRY72csFL6yg"
|
||||
- MB_EMVI_ORGA=marvin
|
||||
MB_EMVI_CLIENT_ID: 3fBBn144yvSF9R3dPC8l
|
||||
MB_EMVI_CLIENT_SECRET: dw3FeshelTgdf1Gj13J7uF5FfdPDi40sQvvwqeFVKTTyIDuCdlAHhRY72csFL6yg
|
||||
MB_EMVI_ORGA: marvin
|
||||
MB_DB_HOST: postgres
|
||||
MB_DB_USER:
|
||||
MB_DB_PASSWORD:
|
||||
MB_DB_SCHEMA:
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.port=8888"
|
||||
- "traefik.http.routers.marvinblum.entrypoints=web"
|
||||
- "traefik.http.routers.marvinblum.rule=Host(`marvinblum.de`)"
|
||||
- "traefik.http.routers.marvinblum.middlewares=http-redirect"
|
||||
@@ -44,3 +60,9 @@ services:
|
||||
- "traefik.http.routers.marvinblum-secure.tls.certresolver=tls-resolver"
|
||||
- "traefik.http.middlewares.http-redirect.redirectscheme.scheme=https"
|
||||
- "traefik.http.middlewares.http-redirect.redirectscheme.permanent=true"
|
||||
|
||||
networks:
|
||||
traefik-internal:
|
||||
driver: bridge
|
||||
postgres_db-internal:
|
||||
external: true
|
||||
|
||||
15
go.mod
15
go.mod
@@ -4,10 +4,21 @@ go 1.14
|
||||
|
||||
require (
|
||||
github.com/NYTimes/gziphandler v1.1.1
|
||||
github.com/caddyserver/certmagic v0.10.13
|
||||
github.com/caddyserver/certmagic v0.11.2
|
||||
github.com/cenkalti/backoff/v4 v4.0.2 // indirect
|
||||
github.com/emvi/api-go v0.0.0-20191210194347-0a945446f6a8
|
||||
github.com/emvi/logbuch v0.0.0-20200214115750-61de9b6d5934
|
||||
github.com/emvi/logbuch v1.1.1
|
||||
github.com/emvi/pirsch v0.0.0-20200624123353-86381b017755
|
||||
github.com/gorilla/mux v1.7.4
|
||||
github.com/gosimple/slug v1.9.0
|
||||
github.com/jmoiron/sqlx v1.2.0
|
||||
github.com/klauspost/cpuid v1.3.0 // indirect
|
||||
github.com/lib/pq v1.7.0
|
||||
github.com/miekg/dns v1.1.29 // indirect
|
||||
github.com/rs/cors v1.7.0
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 // indirect
|
||||
golang.org/x/net v0.0.0-20200602114024-627f9648deb9 // indirect
|
||||
golang.org/x/sys v0.0.0-20200622214017-ed371f2e16b4 // indirect
|
||||
golang.org/x/text v0.3.3 // indirect
|
||||
gopkg.in/square/go-jose.v2 v2.5.1 // indirect
|
||||
)
|
||||
|
||||
43
go.sum
43
go.sum
@@ -51,10 +51,12 @@ github.com/aws/aws-sdk-go v1.30.20/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZve
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/caddyserver/certmagic v0.10.13 h1:wfyYpXVXSSYMS1ZFpSr7HptwsC+j7elda5PUERrHtRc=
|
||||
github.com/caddyserver/certmagic v0.10.13/go.mod h1:Yz6cSRUdddGy6Ut5JfrvcqBwrm1BqXxJRqJq2TwjPnA=
|
||||
github.com/caddyserver/certmagic v0.11.2 h1:nPBqyuFNHJEf2FwC1ixJjArtTKWyPqpaH6k4jl7gxYI=
|
||||
github.com/caddyserver/certmagic v0.11.2/go.mod h1:fqY1IZk5iqhsj5FU3Vw20Sjq66tEKaanTFYNZ74soMY=
|
||||
github.com/cenkalti/backoff/v4 v4.0.0 h1:6VeaLF9aI+MAUQ95106HwWzYZgJJpZ4stumjj6RFYAU=
|
||||
github.com/cenkalti/backoff/v4 v4.0.0/go.mod h1:eEew/i+1Q6OrCDZh3WiXYv3+nJwBASZ8Bog/87DQnVg=
|
||||
github.com/cenkalti/backoff/v4 v4.0.2 h1:JIufpQLbh4DkbQoii76ItQIUFzevQSqOLZca4eamEDs=
|
||||
github.com/cenkalti/backoff/v4 v4.0.2/go.mod h1:eEew/i+1Q6OrCDZh3WiXYv3+nJwBASZ8Bog/87DQnVg=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
@@ -75,8 +77,18 @@ github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1
|
||||
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
|
||||
github.com/emvi/api-go v0.0.0-20191210194347-0a945446f6a8 h1:DZJfRdvwlybLvHkyeN8HRU4QnBFqVZg21ki4to1AVPo=
|
||||
github.com/emvi/api-go v0.0.0-20191210194347-0a945446f6a8/go.mod h1:g9RdDC3s5ebCknAHQQ5PjoM2vRFSyyGoOUX3QkDKU+o=
|
||||
github.com/emvi/logbuch v0.0.0-20200214115750-61de9b6d5934 h1:+G10WRp72llJuaW89QezNK8QU9SsOmd5Ja7ocrrUaI0=
|
||||
github.com/emvi/logbuch v0.0.0-20200214115750-61de9b6d5934/go.mod h1:J2Wgbr3BuSc1JO+D2MBVh6q3WPVSK5GzktwWz8pvkKw=
|
||||
github.com/emvi/logbuch v1.1.1 h1:poBGNbHy/nB95oNoqLKAaJoBrcKxTO0W9DhMijKEkkU=
|
||||
github.com/emvi/logbuch v1.1.1/go.mod h1:J2Wgbr3BuSc1JO+D2MBVh6q3WPVSK5GzktwWz8pvkKw=
|
||||
github.com/emvi/pirsch v0.0.0-20200623183712-76dda933f960 h1:7q8uRCOILenzPP7au4BsylEvkJeIkqJcKN5z/49Wpm8=
|
||||
github.com/emvi/pirsch v0.0.0-20200623183712-76dda933f960/go.mod h1:+YmBbltJ3feZz9L/QQyqwywltYvQKBfzrGD51TPKl5g=
|
||||
github.com/emvi/pirsch v0.0.0-20200623190422-b0f4c9980175 h1:rj+o6eyEkYy9bYd/pupAiUnLZxo9U1qRfdHJ9nzQSLY=
|
||||
github.com/emvi/pirsch v0.0.0-20200623190422-b0f4c9980175/go.mod h1:+YmBbltJ3feZz9L/QQyqwywltYvQKBfzrGD51TPKl5g=
|
||||
github.com/emvi/pirsch v0.0.0-20200623192632-5c5fcd1d78cc h1:G05tiq3JKZwjnPSObRBwypzWplKTUWG2XFWhgFnqD/g=
|
||||
github.com/emvi/pirsch v0.0.0-20200623192632-5c5fcd1d78cc/go.mod h1:+YmBbltJ3feZz9L/QQyqwywltYvQKBfzrGD51TPKl5g=
|
||||
github.com/emvi/pirsch v0.0.0-20200623193552-b3a3d4a6434d h1:+HRIYgA9AaXlGFQfhXpUShXxJ1knG2stwNIai1M6mzs=
|
||||
github.com/emvi/pirsch v0.0.0-20200623193552-b3a3d4a6434d/go.mod h1:+YmBbltJ3feZz9L/QQyqwywltYvQKBfzrGD51TPKl5g=
|
||||
github.com/emvi/pirsch v0.0.0-20200624123353-86381b017755 h1:TdiDC7+IfV6giMtFNvbYKFlSPJJZewr9jfh3KGcr8QQ=
|
||||
github.com/emvi/pirsch v0.0.0-20200624123353-86381b017755/go.mod h1:+YmBbltJ3feZz9L/QQyqwywltYvQKBfzrGD51TPKl5g=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/exoscale/egoscale v0.18.1/go.mod h1:Z7OOdzzTOz1Q1PjQXumlz9Wn/CddH0zSYdCF3rnBKXE=
|
||||
@@ -93,6 +105,7 @@ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
@@ -150,6 +163,8 @@ github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:
|
||||
github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df/go.mod h1:QMZY7/J/KSQEhKWFeDesPjMj+wCHReeknARU3wqlyN4=
|
||||
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
|
||||
github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik=
|
||||
github.com/jmoiron/sqlx v1.2.0 h1:41Ip0zITnmWNR/vHV+S4m+VoUivnWY5E4OJfLZjCJMA=
|
||||
github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks=
|
||||
github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
@@ -160,6 +175,8 @@ github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7V
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/cpuid v1.2.3 h1:CCtW0xUnWGVINKvE/WWOYKdsPV6mawAtvQuSl8guwQs=
|
||||
github.com/klauspost/cpuid v1.2.3/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
||||
github.com/klauspost/cpuid v1.3.0 h1:2JqaNE1hGdABW2YbA3TenkO7RiPFRvSWnEnGqWh9sHE=
|
||||
github.com/klauspost/cpuid v1.3.0/go.mod h1:bYW4mA6ZgKPob1/Dlai2LviZJO7KGI3uoWLd42rAQw4=
|
||||
github.com/kolo/xmlrpc v0.0.0-20190717152603-07c4ee3fd181/go.mod h1:o03bZfuBwAXHetKXuInt4S7omeXUu62/A845kiycsSQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
@@ -169,15 +186,22 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/labbsr0x/bindman-dns-webhook v1.0.2/go.mod h1:p6b+VCXIR8NYKpDr8/dg1HKfQoRHCdcsROXKvmoehKA=
|
||||
github.com/labbsr0x/goh v1.0.1/go.mod h1:8K2UhVoaWXcCU7Lxoa2omWnC8gyW8px7/lmO61c027w=
|
||||
github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A=
|
||||
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.7.0 h1:h93mCPfUSkaul3Ka/VG8uZdmW1uMHDGxzu0NWHuJmHY=
|
||||
github.com/lib/pq v1.7.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/linode/linodego v0.10.0/go.mod h1:cziNP7pbvE3mXIPneHj0oRY8L1WtGEIKlZ8LANE4eXA=
|
||||
github.com/liquidweb/liquidweb-go v1.6.0/go.mod h1:UDcVnAMDkZxpw4Y7NOHkqoeiGacVLEIG/i5J9cyixzQ=
|
||||
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||
github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||
github.com/mattn/go-tty v0.0.0-20180219170247-931426f7535a/go.mod h1:XPvLUNfbS4fJH25nqRHfWLMa1ONC8Amw+mIA639KxkE=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/miekg/dns v1.1.27 h1:aEH/kqUzUxGJ/UHcEKdJY+ugH6WEzsEBBSPa8zuy1aM=
|
||||
github.com/miekg/dns v1.1.27/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
|
||||
github.com/miekg/dns v1.1.29 h1:xHBEhR+t5RzcFJjBLJlax2daXOrTYtr9z4WdKEfWFzg=
|
||||
github.com/miekg/dns v1.1.29/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/go-vnc v0.0.0-20150629162542-723ed9867aed/go.mod h1:3rdaFaCv4AyBgu5ALFM0+tSuHrBh6v692nyQe3ikrq0=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
@@ -268,6 +292,8 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8U
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073 h1:xMPOj6Pz6UipU1wXLkrtqpHbR0AVFnyPEQq/wRWz9lM=
|
||||
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
@@ -323,6 +349,8 @@ golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLL
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200301022130-244492dfa37a h1:GuSPYbZzB5/dcLNCwLQLsg3obCJtX9IJhpXkvY7kzk0=
|
||||
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200602114024-627f9648deb9 h1:pNX+40auqi2JqRfOP1akLGtYcn15TUbkhwuCO3foqqM=
|
||||
golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
@@ -364,11 +392,16 @@ golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527 h1:uYVVQ9WP/Ds2ROhcaGPeIdVq0RIXVLwsHlnvJ+cT1So=
|
||||
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200622214017-ed371f2e16b4 h1:5/PjkGUjvEU5Gl6BxmvKRPpqo2uNMv4rcHBMwzk/st8=
|
||||
golang.org/x/sys v0.0.0-20200622214017-ed371f2e16b4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190921001708-c4c64cad1fd0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
@@ -465,6 +498,8 @@ gopkg.in/resty.v1 v1.9.1/go.mod h1:vo52Hzryw9PnPHcJfPsBiFW62XhNx5OczbV9y+IMpgc=
|
||||
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
|
||||
gopkg.in/square/go-jose.v2 v2.3.1 h1:SK5KegNXmKmqE342YYN2qPHEnUYeoMiXXl1poUlI+o4=
|
||||
gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
|
||||
gopkg.in/square/go-jose.v2 v2.5.1 h1:7odma5RETjNHWJnR32wx8t+Io4djHE1PqxCFx3iiZ2w=
|
||||
gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
|
||||
111
main.go
111
main.go
@@ -1,17 +1,22 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/Kugelschieber/marvinblum.de/blog"
|
||||
"github.com/Kugelschieber/marvinblum.de/tpl"
|
||||
"github.com/Kugelschieber/marvinblum.de/tracking"
|
||||
"github.com/NYTimes/gziphandler"
|
||||
"github.com/caddyserver/certmagic"
|
||||
emvi "github.com/emvi/api-go"
|
||||
"github.com/emvi/logbuch"
|
||||
"github.com/emvi/pirsch"
|
||||
"github.com/gorilla/mux"
|
||||
_ "github.com/lib/pq"
|
||||
"github.com/rs/cors"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
@@ -21,9 +26,12 @@ const (
|
||||
staticDirPrefix = "/static/"
|
||||
logTimeFormat = "2006-01-02_15:04:05"
|
||||
envPrefix = "MB_"
|
||||
shutdownTimeout = time.Second * 30
|
||||
)
|
||||
|
||||
var (
|
||||
tracker *pirsch.Tracker
|
||||
tplCache *tpl.Cache
|
||||
blogInstance *blog.Blog
|
||||
)
|
||||
|
||||
@@ -52,36 +60,36 @@ func logEnvConfig() {
|
||||
|
||||
func serveAbout() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
data := struct {
|
||||
tracker.Hit(r)
|
||||
tplCache.Render(w, "about.html", struct {
|
||||
Articles []emvi.Article
|
||||
}{
|
||||
blogInstance.GetLatestArticles(),
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if err := tpl.Get().ExecuteTemplate(w, "about.html", data); err != nil {
|
||||
logbuch.Error("Error executing blog template", logbuch.Fields{"err": err})
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
func serveLegal() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
tracker.Hit(r)
|
||||
tplCache.Render(w, "legal.html", nil)
|
||||
}
|
||||
}
|
||||
|
||||
func serveBlogPage() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
data := struct {
|
||||
tracker.Hit(r)
|
||||
tplCache.Render(w, "blog.html", struct {
|
||||
Articles map[int][]emvi.Article
|
||||
}{
|
||||
blogInstance.GetArticles(),
|
||||
}
|
||||
|
||||
if err := tpl.Get().ExecuteTemplate(w, "blog.html", data); err != nil {
|
||||
logbuch.Error("Error executing blog template", logbuch.Fields{"err": err})
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func serveBlogArticle() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
tracker.Hit(r)
|
||||
vars := mux.Vars(r)
|
||||
slug := strings.Split(vars["slug"], "-")
|
||||
|
||||
@@ -97,7 +105,7 @@ func serveBlogArticle() http.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
data := struct {
|
||||
tplCache.Render(w, "article.html", struct {
|
||||
Title string
|
||||
Content template.HTML
|
||||
Published time.Time
|
||||
@@ -105,12 +113,38 @@ func serveBlogArticle() http.HandlerFunc {
|
||||
article.LatestArticleContent.Title,
|
||||
template.HTML(article.LatestArticleContent.Content),
|
||||
article.Published,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func serveTracking() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
start, _ := strconv.Atoi(r.URL.Query().Get("start"))
|
||||
|
||||
if start > 365 {
|
||||
start = 365
|
||||
} else if start < 7 {
|
||||
start = 7
|
||||
}
|
||||
|
||||
if err := tpl.Get().ExecuteTemplate(w, "article.html", data); err != nil {
|
||||
logbuch.Error("Error executing blog article template", logbuch.Fields{"err": err})
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
totalVisitorsLabels, totalVisitorsDps := tracking.GetTotalVisitors(start)
|
||||
tplCache.RenderWithoutCache(w, "tracking.html", struct {
|
||||
Start int
|
||||
TotalVisitorsLabels template.JS
|
||||
TotalVisitorsDps template.JS
|
||||
PageVisits []tracking.PageVisits
|
||||
}{
|
||||
start,
|
||||
totalVisitorsLabels,
|
||||
totalVisitorsDps,
|
||||
tracking.GetPageVisits(start),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func serveNotFound() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
tplCache.Render(w, "notfound.html", nil)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,9 +153,10 @@ func setupRouter() *mux.Router {
|
||||
router.PathPrefix(staticDirPrefix).Handler(http.StripPrefix(staticDirPrefix, gziphandler.GzipHandler(http.FileServer(http.Dir(staticDir)))))
|
||||
router.Handle("/blog/{slug}", serveBlogArticle())
|
||||
router.Handle("/blog", serveBlogPage())
|
||||
router.Handle("/legal", tpl.ServeTemplate("legal.html"))
|
||||
router.Handle("/legal", serveLegal())
|
||||
router.Handle("/tracking", serveTracking())
|
||||
router.Handle("/", serveAbout())
|
||||
router.NotFoundHandler = tpl.ServeTemplate("notfound.html")
|
||||
router.NotFoundHandler = serveNotFound()
|
||||
return router
|
||||
}
|
||||
|
||||
@@ -141,28 +176,36 @@ func configureCors(router *mux.Router) http.Handler {
|
||||
|
||||
func start(handler http.Handler) {
|
||||
logbuch.Info("Starting server...")
|
||||
var server http.Server
|
||||
server.Handler = handler
|
||||
server.Addr = os.Getenv("MB_HOST")
|
||||
|
||||
if strings.ToLower(os.Getenv("MB_TLS")) == "true" {
|
||||
logbuch.Info("TLS enabled")
|
||||
certmagic.DefaultACME.Agreed = true
|
||||
certmagic.DefaultACME.Email = os.Getenv("MB_TLS_EMAIL")
|
||||
certmagic.DefaultACME.CA = certmagic.LetsEncryptProductionCA
|
||||
go func() {
|
||||
sigint := make(chan os.Signal)
|
||||
signal.Notify(sigint, os.Interrupt)
|
||||
<-sigint
|
||||
logbuch.Info("Shutting down server...")
|
||||
tracker.Stop()
|
||||
ctx, _ := context.WithTimeout(context.Background(), shutdownTimeout)
|
||||
|
||||
if err := certmagic.HTTPS(strings.Split(os.Getenv("MB_DOMAIN"), ","), handler); err != nil {
|
||||
logbuch.Fatal("Error starting server", logbuch.Fields{"err": err})
|
||||
}
|
||||
} else {
|
||||
if err := http.ListenAndServe(os.Getenv("MB_HOST"), handler); err != nil {
|
||||
logbuch.Fatal("Error starting server", logbuch.Fields{"err": err})
|
||||
if err := server.Shutdown(ctx); err != nil {
|
||||
logbuch.Fatal("Error shutting down server gracefully", logbuch.Fields{"err": err})
|
||||
}
|
||||
}()
|
||||
|
||||
if err := server.ListenAndServe(); err != http.ErrServerClosed {
|
||||
logbuch.Fatal("Error starting server", logbuch.Fields{"err": err})
|
||||
}
|
||||
|
||||
logbuch.Info("Server shut down")
|
||||
}
|
||||
|
||||
func main() {
|
||||
configureLog()
|
||||
logEnvConfig()
|
||||
tpl.LoadTemplate()
|
||||
blogInstance = blog.NewBlog()
|
||||
tracker = tracking.NewTracker()
|
||||
tplCache = tpl.NewCache()
|
||||
blogInstance = blog.NewBlog(tplCache)
|
||||
router := setupRouter()
|
||||
corsConfig := configureCors(router)
|
||||
start(corsConfig)
|
||||
|
||||
4
postgres/clear_logs.sh
Normal file
4
postgres/clear_logs.sh
Normal file
@@ -0,0 +1,4 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "Clearing docker logs..."
|
||||
echo "" > $(docker inspect --format='{{.LogPath}}' postgres)
|
||||
22
postgres/docker-compose.yml
Normal file
22
postgres/docker-compose.yml
Normal file
@@ -0,0 +1,22 @@
|
||||
version: "3"
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:12-alpine
|
||||
container_name: postgres
|
||||
restart: always
|
||||
command: -c ssl=on -c ssl_cert_file=/var/lib/postgresql/server.crt -c ssl_key_file=/var/lib/postgresql/server.key
|
||||
networks:
|
||||
- db-internal
|
||||
environment:
|
||||
POSTGRES_PASSWORD:
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- /root/postgres/data:/var/lib/postgresql/data
|
||||
- /root/postgres/cert/server.crt:/var/lib/postgresql/server.crt
|
||||
- /root/postgres/cert/server.key:/var/lib/postgresql/server.key
|
||||
|
||||
networks:
|
||||
db-internal:
|
||||
driver: bridge
|
||||
10
postgres/gen_cert.sh
Executable file
10
postgres/gen_cert.sh
Executable file
@@ -0,0 +1,10 @@
|
||||
#!/bin/bash
|
||||
|
||||
mkdir cert
|
||||
cd cert
|
||||
openssl req -new -text -passout pass:$1 -subj /CN=localhost -out server.req -keyout privkey.pem
|
||||
openssl rsa -in privkey.pem -passin pass:$1 -out server.key
|
||||
openssl req -x509 -in server.req -text -key server.key -out server.crt
|
||||
chown 0:70 server.key
|
||||
chmod 640 server.key
|
||||
echo "done"
|
||||
7
static/js/Chart-v2.9.3.bundle.min.js
vendored
Normal file
7
static/js/Chart-v2.9.3.bundle.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -47,6 +47,11 @@ body {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.tracking {
|
||||
width: 100%;
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
section {
|
||||
margin: 80px 0;
|
||||
}
|
||||
|
||||
@@ -11,14 +11,14 @@
|
||||
{{template "menu.html"}}
|
||||
|
||||
<section>
|
||||
<h2>who am I?</h2>
|
||||
<h2>Who Am I?</h2>
|
||||
<p>
|
||||
I'm a full stack software engineer from Germany, open source and Linux enthusiast and co-founder of <a href="https://emvi.com/" target="_blank">Emvi</a>.
|
||||
In love with Go, but fluent in a lot of programming languages.
|
||||
</p>
|
||||
</section>
|
||||
<section>
|
||||
<h2>latest blog posts</h2>
|
||||
<h2>Latest Blog Posts</h2>
|
||||
{{range $article := .Articles}}
|
||||
<p>
|
||||
<a href="/blog/{{slug $article.LatestArticleContent.Title}}-{{$article.Id}}">{{$article.LatestArticleContent.Title}}</a>
|
||||
@@ -32,11 +32,14 @@
|
||||
</p>
|
||||
</section>
|
||||
<section>
|
||||
<h2>projects</h2>
|
||||
<h2>Projects</h2>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="https://emvi.com/" target="_blank">Emvi</a> a note taking, collaboration and knowledge management tool for personal use and teams of any size
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://github.com/emvi/pirsch" target="_blank">pirsch</a> a server side, no-cookie and privacy focused tracking library for Golang
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://github.com/emvi/logbuch" target="_blank">logbuch</a> a simple Golang logging library
|
||||
</li>
|
||||
@@ -52,7 +55,7 @@
|
||||
</ul>
|
||||
</section>
|
||||
<section>
|
||||
<h2>skills</h2>
|
||||
<h2>Skills</h2>
|
||||
<ul>
|
||||
<li>Go (Golang)</li>
|
||||
<li>JavaScript (Vue, Node)</li>
|
||||
@@ -66,7 +69,7 @@
|
||||
</ul>
|
||||
</section>
|
||||
<section>
|
||||
<h2>work</h2>
|
||||
<h2>Work</h2>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="https://emvi.com/" target="_blank">Emvi</a> co-founder of a note taking, collaboration and knowledge management SaaS startup
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
|
||||
<section>
|
||||
<h1>Blog</h1>
|
||||
{{range $year, $articles := .Articles}}
|
||||
</section>
|
||||
{{range $year, $articles := .Articles}}
|
||||
<section>
|
||||
<h2>{{$year}}</h2>
|
||||
|
||||
{{range $article := $articles}}
|
||||
@@ -12,9 +14,11 @@
|
||||
<small>{{format $article.Published "2. January 2006"}}</small>
|
||||
</p>
|
||||
{{end}}
|
||||
{{else}}
|
||||
</section>
|
||||
{{else}}
|
||||
<section>
|
||||
<p>There are no blog posts yet...</p>
|
||||
{{end}}
|
||||
</section>
|
||||
</section>
|
||||
{{end}}
|
||||
|
||||
{{template "end.html"}}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<hr />
|
||||
<section>
|
||||
<p>
|
||||
Like to see more? Read my blog articles on <a href="https://emvi.com/blog" target="_blank">Emvi</a>, my project page on <a href="https://github.com/Kugelschieber" target="_blank">GitHub</a> or send me a <a href="mailto:marvin@marvinblum.de">mail</a>.
|
||||
Would you like to see more? Read my blog articles on <a href="https://emvi.com/blog" target="_blank">Emvi</a>, my project page on <a href="https://github.com/Kugelschieber" target="_blank">GitHub</a> or send me a <a href="mailto:marvin@marvinblum.de">mail</a>.
|
||||
</p>
|
||||
<p>
|
||||
This page uses <a href="https://concrete.style/" target="_blank">concrete</a> for styling. Check it out!
|
||||
</p>
|
||||
<p>
|
||||
<a href="/legal">Legal</a>
|
||||
This page does not use cookies. <a href="/legal">Legal</a>
|
||||
</p>
|
||||
</section>
|
||||
</body>
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
{{template "head.html"}}
|
||||
{{template "menu.html"}}
|
||||
|
||||
<section>
|
||||
<h1>Legal</h1>
|
||||
</section>
|
||||
<section>
|
||||
<h2>According to §5 TMG</h2>
|
||||
<p>
|
||||
@@ -10,5 +13,11 @@
|
||||
<a href="mailto:marvin@marvinblum.de">marvin@marvinblum.de</a>
|
||||
</p>
|
||||
</section>
|
||||
<section>
|
||||
<h2>Cookie Policy</h2>
|
||||
<p>
|
||||
This page does not use cookies.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
{{template "end.html"}}
|
||||
|
||||
74
template/tracking.html
Normal file
74
template/tracking.html
Normal file
@@ -0,0 +1,74 @@
|
||||
{{template "head.html"}}
|
||||
{{template "menu.html"}}
|
||||
|
||||
<section>
|
||||
<h1>Tracking</h1>
|
||||
<p>
|
||||
This page shows tracking statistics for my website using <a href="https://github.com/emvi/pirsch" target="_blank">Pirsch</a> and <a href="https://www.chartjs.org/" target="_blank">Chart.Js</a>. The data shows unique visitors.
|
||||
</p>
|
||||
<p>
|
||||
<a href="/tracking?start=7" class="button {{if eq .Start 7}}filled{{end}}">Week</a>
|
||||
<a href="/tracking?start=30" class="button {{if eq .Start 30}}filled{{end}}">Month</a>
|
||||
<a href="/tracking?start=90" class="button {{if eq .Start 90}}filled{{end}}">Quarter</a>
|
||||
<a href="/tracking?start=182" class="button {{if eq .Start 182}}filled{{end}}">Half Year</a>
|
||||
<a href="/tracking?start=365" class="button {{if eq .Start 365}}filled{{end}}">Year</a>
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<!-- TODO real time -->
|
||||
|
||||
<section>
|
||||
<h2>Total Visitors</h2>
|
||||
<canvas id="totalVisitors" class="tracking"></canvas>
|
||||
</section>
|
||||
<section>
|
||||
<h2>Pages Visits</h2>
|
||||
</section>
|
||||
|
||||
{{range $i, $data := .PageVisits}}
|
||||
<section>
|
||||
<h3>{{$data.Path}}</h3>
|
||||
<canvas id="pageVisits{{$i}}" class="tracking"></canvas>
|
||||
</section>
|
||||
{{end}}
|
||||
|
||||
<!-- TODO -->
|
||||
<!--<section>
|
||||
<h2>Languages</h2>
|
||||
</section>
|
||||
<section>
|
||||
<h2>Visitors Per Hour</h2>
|
||||
</section>-->
|
||||
|
||||
<script type="text/javascript" src="/static/js/Chart-v2.9.3.bundle.min.js"></script>
|
||||
<script type="text/javascript">
|
||||
new Chart(document.getElementById('totalVisitors').getContext('2d'), {
|
||||
type: "line",
|
||||
data: {
|
||||
labels: [{{.TotalVisitorsLabels}}],
|
||||
datasets: [{
|
||||
backgroundColor: "rgba(0, 0, 0, 0.05)",
|
||||
borderColor: "#000",
|
||||
label: "Total Visitors",
|
||||
data: [{{.TotalVisitorsDps}}]
|
||||
}]
|
||||
}
|
||||
});
|
||||
|
||||
{{range $i, $data := .PageVisits}}
|
||||
new Chart(document.getElementById('pageVisits{{$i}}').getContext('2d'), {
|
||||
type: "line",
|
||||
data: {
|
||||
labels: [{{$data.Labels}}],
|
||||
datasets: [{
|
||||
backgroundColor: "rgba(0, 0, 0, 0.05)",
|
||||
borderColor: "#000",
|
||||
label: "Page Visits",
|
||||
data: [{{$data.Data}}]
|
||||
}]
|
||||
}
|
||||
});
|
||||
{{end}}
|
||||
</script>
|
||||
|
||||
{{template "end.html"}}
|
||||
101
tpl/template.go
101
tpl/template.go
@@ -7,6 +7,7 @@ import (
|
||||
"html/template"
|
||||
"net/http"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -14,59 +15,87 @@ const (
|
||||
templateDir = "template/*"
|
||||
)
|
||||
|
||||
var (
|
||||
type Cache struct {
|
||||
tpl *template.Template
|
||||
tplCache = make(map[string][]byte)
|
||||
cache map[string][]byte
|
||||
hotReload bool
|
||||
)
|
||||
|
||||
var funcMap = template.FuncMap{
|
||||
"slug": slug.Make,
|
||||
"format": func(t time.Time, layout string) string {
|
||||
return t.Format(layout)
|
||||
},
|
||||
m sync.RWMutex
|
||||
}
|
||||
|
||||
func LoadTemplate() {
|
||||
func NewCache() *Cache {
|
||||
cache := &Cache{
|
||||
cache: make(map[string][]byte),
|
||||
hotReload: os.Getenv("MB_HOT_RELOAD") == "true",
|
||||
}
|
||||
logbuch.Debug("Template cache hot reload", logbuch.Fields{"hot_reload": cache.hotReload})
|
||||
cache.load()
|
||||
return cache
|
||||
}
|
||||
|
||||
func (cache *Cache) load() {
|
||||
logbuch.Debug("Loading templates")
|
||||
funcMap := template.FuncMap{
|
||||
"slug": slug.Make,
|
||||
"format": func(t time.Time, layout string) string {
|
||||
return t.Format(layout)
|
||||
},
|
||||
}
|
||||
var err error
|
||||
tpl, err = template.New("").Funcs(funcMap).ParseGlob(templateDir)
|
||||
cache.tpl, err = template.New("").Funcs(funcMap).ParseGlob(templateDir)
|
||||
|
||||
if err != nil {
|
||||
logbuch.Fatal("Error loading template", logbuch.Fields{"err": err})
|
||||
}
|
||||
|
||||
hotReload = os.Getenv("MB_HOT_RELOAD") == "true"
|
||||
logbuch.Debug("Templates loaded", logbuch.Fields{"hot_reload": hotReload})
|
||||
logbuch.Debug("Templates loaded", logbuch.Fields{"hot_reload": cache.hotReload})
|
||||
}
|
||||
|
||||
func renderTemplate(name string) {
|
||||
logbuch.Debug("Rendering template", logbuch.Fields{"name": name})
|
||||
var buffer bytes.Buffer
|
||||
func (cache *Cache) Render(w http.ResponseWriter, name string, data interface{}) {
|
||||
cache.m.RLock()
|
||||
|
||||
if err := tpl.ExecuteTemplate(&buffer, name, nil); err != nil {
|
||||
logbuch.Fatal("Error executing template", logbuch.Fields{"err": err, "name": name})
|
||||
}
|
||||
if cache.cache[name] == nil || cache.hotReload {
|
||||
cache.m.RUnlock()
|
||||
cache.m.Lock()
|
||||
defer cache.m.Unlock()
|
||||
logbuch.Debug("Rendering template", logbuch.Fields{"name": name})
|
||||
|
||||
tplCache[name] = buffer.Bytes()
|
||||
}
|
||||
|
||||
func Get() *template.Template {
|
||||
return tpl
|
||||
}
|
||||
|
||||
func ServeTemplate(name string) http.HandlerFunc {
|
||||
// render once so we have it in cache
|
||||
renderTemplate(name)
|
||||
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if hotReload {
|
||||
LoadTemplate()
|
||||
renderTemplate(name)
|
||||
if cache.hotReload {
|
||||
logbuch.Debug("Reloading templates")
|
||||
cache.load()
|
||||
}
|
||||
|
||||
if _, err := w.Write(tplCache[name]); err != nil {
|
||||
logbuch.Error("Error returning page to client", logbuch.Fields{"err": err, "name": name})
|
||||
var buffer bytes.Buffer
|
||||
|
||||
if err := cache.tpl.ExecuteTemplate(&buffer, name, data); err != nil {
|
||||
logbuch.Error("Error executing template", logbuch.Fields{"err": err, "name": name})
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
cache.cache[name] = buffer.Bytes()
|
||||
} else {
|
||||
cache.m.RUnlock()
|
||||
}
|
||||
|
||||
if _, err := w.Write(cache.cache[name]); err != nil {
|
||||
logbuch.Error("Error sending response to client", logbuch.Fields{"err": err, "template": name})
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
func (cache *Cache) RenderWithoutCache(w http.ResponseWriter, name string, data interface{}) {
|
||||
if cache.hotReload {
|
||||
logbuch.Debug("Reloading templates")
|
||||
cache.load()
|
||||
}
|
||||
|
||||
if err := cache.tpl.ExecuteTemplate(w, name, data); err != nil {
|
||||
logbuch.Error("Error executing template", logbuch.Fields{"err": err, "name": name})
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
func (cache *Cache) Clear() {
|
||||
cache.m.Lock()
|
||||
defer cache.m.Unlock()
|
||||
cache.cache = make(map[string][]byte)
|
||||
}
|
||||
|
||||
102
tracking/statistics.go
Normal file
102
tracking/statistics.go
Normal file
@@ -0,0 +1,102 @@
|
||||
package tracking
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/emvi/logbuch"
|
||||
"github.com/emvi/pirsch"
|
||||
"html/template"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
statisticsDateFormat = "2006-01-02"
|
||||
)
|
||||
|
||||
type PageVisits struct {
|
||||
Path string
|
||||
Labels template.JS
|
||||
Data template.JS
|
||||
}
|
||||
|
||||
func GetTotalVisitors(start int) (template.JS, template.JS) {
|
||||
query := `SELECT "date" "day",
|
||||
CASE WHEN "visitors_per_day".visitors IS NULL THEN 0 ELSE "visitors_per_day".visitors END
|
||||
FROM (SELECT * FROM generate_series(date($1), date(now()), interval '1 day') "date") AS date_series
|
||||
LEFT JOIN "visitors_per_day" ON date("visitors_per_day"."day") = date("date")
|
||||
ORDER BY "date" ASC`
|
||||
startTime := today()
|
||||
startTime = startTime.Add(-time.Hour * 24 * time.Duration(start-1))
|
||||
logbuch.Debug("Reading total visitors since", logbuch.Fields{"since": startTime})
|
||||
var visitors []pirsch.VisitorsPerDay
|
||||
|
||||
if err := db.Select(&visitors, query, startTime); err != nil {
|
||||
logbuch.Error("Error reading total visitors", logbuch.Fields{"err": err, "since": startTime})
|
||||
return "", ""
|
||||
}
|
||||
|
||||
today := today()
|
||||
visitorsToday, err := store.VisitorsPerDay(today)
|
||||
|
||||
if err != nil {
|
||||
logbuch.Error("Error reading total visitors for today", logbuch.Fields{"err": err, "since": startTime})
|
||||
return "", ""
|
||||
}
|
||||
|
||||
visitors[len(visitors)-1].Visitors = visitorsToday
|
||||
return getLabelsAndData(visitors)
|
||||
}
|
||||
|
||||
func GetPageVisits(start int) []PageVisits {
|
||||
pathsQuery := `SELECT * FROM (SELECT DISTINCT "path" FROM "visitors_per_page" WHERE day >= $1) AS paths ORDER BY length("path") ASC`
|
||||
query := `SELECT "date" "day",
|
||||
CASE WHEN "visitors_per_page".visitors IS NULL THEN 0 ELSE "visitors_per_page".visitors END
|
||||
FROM (SELECT * FROM generate_series(date($1), date(now() - INTERVAL '1 day'), interval '1 day') "date") AS date_series
|
||||
LEFT JOIN "visitors_per_page" ON date("visitors_per_page"."day") = date("date") AND "visitors_per_page"."path" = $2
|
||||
ORDER BY "date" ASC, length("visitors_per_page"."path") ASC`
|
||||
startTime := today()
|
||||
startTime = startTime.Add(-time.Hour * 24 * time.Duration(start-1))
|
||||
logbuch.Debug("Reading page visits", logbuch.Fields{"since": startTime})
|
||||
var paths []string
|
||||
|
||||
if err := db.Select(&paths, pathsQuery, startTime); err != nil {
|
||||
logbuch.Error("Error reading distinct paths", logbuch.Fields{"err": err})
|
||||
return nil
|
||||
}
|
||||
|
||||
// TODO add visitors for today
|
||||
pageVisits := make([]PageVisits, len(paths))
|
||||
|
||||
for i, path := range paths {
|
||||
var visitors []pirsch.VisitorsPerDay
|
||||
|
||||
if err := db.Select(&visitors, query, startTime, path); err != nil {
|
||||
logbuch.Error("Error reading visitor for path", logbuch.Fields{"err": err, "since": startTime})
|
||||
return nil
|
||||
}
|
||||
|
||||
labels, data := getLabelsAndData(visitors)
|
||||
pageVisits[i] = PageVisits{path, labels, data}
|
||||
}
|
||||
|
||||
return pageVisits
|
||||
}
|
||||
|
||||
func getLabelsAndData(visitors []pirsch.VisitorsPerDay) (template.JS, template.JS) {
|
||||
var labels strings.Builder
|
||||
var dp strings.Builder
|
||||
|
||||
for _, point := range visitors {
|
||||
labels.WriteString(fmt.Sprintf("'%s',", point.Day.Format(statisticsDateFormat)))
|
||||
dp.WriteString(fmt.Sprintf("%d,", point.Visitors))
|
||||
}
|
||||
|
||||
labelsStr := labels.String()
|
||||
dataStr := dp.String()
|
||||
return template.JS(labelsStr[:len(labelsStr)-1]), template.JS(dataStr[:len(dataStr)-1])
|
||||
}
|
||||
|
||||
func today() time.Time {
|
||||
now := time.Now()
|
||||
return time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
|
||||
}
|
||||
70
tracking/tracking.go
Normal file
70
tracking/tracking.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package tracking
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"github.com/emvi/logbuch"
|
||||
"github.com/emvi/pirsch"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
connectionString = `host=%s port=%s user=%s password=%s dbname=%s sslmode=%s sslcert=%s sslkey=%s sslrootcert=%s connectTimeout=%s timezone=%s`
|
||||
)
|
||||
|
||||
var db *sqlx.DB
|
||||
var store pirsch.Store
|
||||
|
||||
func NewTracker() *pirsch.Tracker {
|
||||
logbuch.Info("Connecting to database...")
|
||||
host := os.Getenv("MB_DB_HOST")
|
||||
port := os.Getenv("MB_DB_PORT")
|
||||
user := os.Getenv("MB_DB_USER")
|
||||
password := os.Getenv("MB_DB_PASSWORD")
|
||||
schema := os.Getenv("MB_DB_SCHEMA")
|
||||
sslMode := os.Getenv("MB_DB_SSLMODE")
|
||||
sslCert := os.Getenv("MB_DB_SSLCERT")
|
||||
sslKey := os.Getenv("MB_DB_SSLKEY")
|
||||
sslRootCert := os.Getenv("MB_DB_SSLROOTCERT")
|
||||
zone, offset := time.Now().Zone()
|
||||
timezone := zone + strconv.Itoa(-offset/3600)
|
||||
logbuch.Info("Setting time zone", logbuch.Fields{"timezone": timezone})
|
||||
connectionStr := fmt.Sprintf(connectionString, host, port, user, password, schema, sslMode, sslCert, sslKey, sslRootCert, "30", timezone)
|
||||
conn, err := sql.Open("postgres", connectionStr)
|
||||
|
||||
if err != nil {
|
||||
logbuch.Fatal("Error connecting to database", logbuch.Fields{"err": err})
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := conn.Ping(); err != nil {
|
||||
logbuch.Fatal("Error pinging database", logbuch.Fields{"err": err})
|
||||
return nil
|
||||
}
|
||||
|
||||
db = sqlx.NewDb(conn, "postgres")
|
||||
store = pirsch.NewPostgresStore(conn)
|
||||
tracker := pirsch.NewTracker(store, nil)
|
||||
processor := pirsch.NewProcessor(store)
|
||||
processTrackingData(processor)
|
||||
pirsch.RunAtMidnight(func() {
|
||||
processTrackingData(processor)
|
||||
})
|
||||
return tracker
|
||||
}
|
||||
|
||||
func processTrackingData(processor *pirsch.Processor) {
|
||||
logbuch.Info("Processing tracking data...")
|
||||
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
logbuch.Error("Error processing tracking data", logbuch.Fields{"err": err})
|
||||
}
|
||||
}()
|
||||
|
||||
processor.Process()
|
||||
logbuch.Info("Done processing tracking data")
|
||||
}
|
||||
Reference in New Issue
Block a user