From bd13650b2058af245e471eb273c12ef35f278194 Mon Sep 17 00:00:00 2001 From: Mohammad Al Zouabi Date: Sun, 11 Feb 2024 01:59:14 +0800 Subject: [PATCH] init: `go` server (#93) --- .husky/pre-commit | 12 +++++- .husky/pre-push | 15 +++++++- server/.air.toml | 46 +++++++++++++++++++++++ server/.env.example | 2 + server/.gitignore | 27 ++++++++++++++ server/Makefile | 83 +++++++++++++++++++++++++++++++++++++++++ server/config/config.go | 56 +++++++++++++++++++++++++++ server/go.mod | 12 ++++++ server/go.sum | 18 +++++++++ server/main.go | 45 ++++++++++++++++++++++ server/staticcheck.conf | 4 ++ vercel.json | 2 +- 12 files changed, 319 insertions(+), 3 deletions(-) create mode 100644 server/.air.toml create mode 100644 server/.env.example create mode 100644 server/.gitignore create mode 100644 server/Makefile create mode 100644 server/config/config.go create mode 100644 server/go.mod create mode 100644 server/go.sum create mode 100644 server/main.go create mode 100644 server/staticcheck.conf diff --git a/.husky/pre-commit b/.husky/pre-commit index 575c21f..0d584c0 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,4 +1,14 @@ #!/bin/sh . "$(dirname "$0")/_/husky.sh" -bun biome ci . +if (git diff --name-only --staged | grep -qvE '^(README\.md|(biome|vercel)\.json|(\.vscode|\.github|\.husky|scripts|server)/.*)$'); then + bun biome ci . +else + echo "Skipping 'biome ci'" +fi + +if (git diff --quiet --name-only --staged server); then + echo "Skipping server 'make tidy'" +else + cd server && make tidy +fi diff --git a/.husky/pre-push b/.husky/pre-push index b122a2e..f08f676 100755 --- a/.husky/pre-push +++ b/.husky/pre-push @@ -1,4 +1,17 @@ #!/bin/sh . "$(dirname "$0")/_/husky.sh" -bun biome ci . && pnpm build +BRANCH="$(git rev-parse --abbrev-ref HEAD)" + +if (git diff --name-only origin/"$BRANCH" HEAD | grep -qvE '^(README\.md|(biome|vercel)\.json|(\.vscode|\.github|\.husky|scripts|server)/.*)$'); then + bun biome ci . + bun run build +else + echo "Skipping 'bun run build'" +fi + +if (git diff --quiet --name-only origin/"$BRANCH" HEAD server); then + echo "Skipping server 'make audit'" +else + cd server && make audit +fi diff --git a/server/.air.toml b/server/.air.toml new file mode 100644 index 0000000..1b9d221 --- /dev/null +++ b/server/.air.toml @@ -0,0 +1,46 @@ +# root = "." +# testdata_dir = "testdata" +# tmp_dir = "tmp" + +[build] + # args_bin = [] + bin = "./tmp/app" + cmd = "make build" + delay = 100 + # exclude_dir = ["assets", "tmp", "vendor", "testdata"] + # exclude_file = [] + # exclude_regex = ["_test.go"] + # exclude_unchanged = false + # follow_symlink = false + # full_bin = "" + # include_dir = [] + # include_ext = ["go", "tpl", "tmpl", "html"] + include_file = [".env"] + # kill_delay = "0s" + # log = "build-errors.log" + # poll = false + # poll_interval = 0 + # post_cmd = [] + # pre_cmd = [] + # rerun = false + # rerun_delay = 500 + # send_interrupt = false + # stop_on_error = false + +[color] + # app = "" + # build = "yellow" + # main = "magenta" + # runner = "green" + # watcher = "cyan" + +[log] + # main_only = false + # time = false + +[misc] + clean_on_exit = true + +[screen] + # clear_on_rebuild = false + # keep_scroll = true diff --git a/server/.env.example b/server/.env.example new file mode 100644 index 0000000..4f88990 --- /dev/null +++ b/server/.env.example @@ -0,0 +1,2 @@ +PORT=8080 +LOG_LEVEL=debug diff --git a/server/.gitignore b/server/.gitignore new file mode 100644 index 0000000..3c4ba8b --- /dev/null +++ b/server/.gitignore @@ -0,0 +1,27 @@ +# Allowlisting gitignore template for GO projects prevents us +# from adding various unwanted local files, such as generated +# files, developer configurations or IDE-specific files etc. + +# Ignore everything +* + +# But not these files... +!/.gitignore + +!*.go +!go.sum +!go.mod + +!README.md +!LICENSE + +!Makefile + +!.env.example + +!.air.toml + +!staticcheck.conf + +# ...even if they are in subdirectories +!*/ diff --git a/server/Makefile b/server/Makefile new file mode 100644 index 0000000..370e80e --- /dev/null +++ b/server/Makefile @@ -0,0 +1,83 @@ +# Change these variables as necessary. +MAIN_PACKAGE_PATH := ./ +BINARY_NAME := app + +# ==================================================================================== # +# HELPERS +# ==================================================================================== # + +## help: print this help message +.PHONY: help +help: + @echo 'Usage:' + @sed -n 's/^##//p' ${MAKEFILE_LIST} | column -t -s ':' | sed -e 's/^/ /' + +.PHONY: confirm +confirm: + @echo -n 'Are you sure? [y/N] ' && read ans && [ $${ans:-N} = y ] + +.PHONY: no-dirty +no-dirty: + git diff --exit-code + +.PHONY: env +env: + @if [ ! -f .env ]; then \ + cp .env.example .env; \ + fi + + +# ==================================================================================== # +# QUALITY CONTROL +# ==================================================================================== # + +## tidy: format code and tidy modfile +.PHONY: tidy +tidy: + go fmt ./... + go mod tidy -v + +## audit: run quality control checks +.PHONY: audit +audit: tidy + go mod verify + go vet ./... + go run honnef.co/go/tools/cmd/staticcheck@latest ./... + go test -race -buildvcs -vet=off ./... + + +## vulncheck: check for known vulnerabilities +.PHONY: vulncheck +vulncheck: + go run golang.org/x/vuln/cmd/govulncheck@latest ./... + + +# ==================================================================================== # +# DEVELOPMENT +# ==================================================================================== # + +## test: run all tests +.PHONY: test +test: + go test -v -race -buildvcs ./... + +## test/cover: run all tests and display coverage +.PHONY: test/cover +test/cover: + go test -v -race -buildvcs -coverprofile=/tmp/coverage.out ./... + go tool cover -html=/tmp/coverage.out + +## build: build the application +.PHONY: build +build: + go build -o=./tmp/${BINARY_NAME} ${MAIN_PACKAGE_PATH} + +## run: run the application +.PHONY: run +run: env build + ./tmp/${BINARY_NAME} + +## run/live: run the application with reloading on file changes +.PHONY: run/live +run/live: env + go run github.com/cosmtrek/air@v1.49.0 diff --git a/server/config/config.go b/server/config/config.go new file mode 100644 index 0000000..2fc8324 --- /dev/null +++ b/server/config/config.go @@ -0,0 +1,56 @@ +package config + +import ( + "os" + "strconv" + + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" +) + +var logger = log.With().Str("pkg", "config").Logger() + +type Config struct { + Port uint16 + LogLevel zerolog.Level +} + +func Init() *Config { + return &Config{ + Port: parseEnv("PORT", 8080, parseUint16), + LogLevel: parseEnv("LOG_LEVEL", zerolog.InfoLevel, parseLogLevel), + } +} + +// UTILS + +func parseEnv[V any, P func(string) (V, error)](key string, defaultVal V, parser P) V { + if str, exists := os.LookupEnv(key); exists { + if value, err := parser(str); err == nil { + return value + } else { + logger.Warn().Err(err).Msgf("Failed to parse env var '%s'", key) + } + } + + return defaultVal +} + +// PARSERS + +func parseUint(s string, bits int) (uint64, error) { + return strconv.ParseUint(s, 10, bits) +} + +func parseUint16(s string) (uint16, error) { + i, e := parseUint(s, 16) + return uint16(i), e +} + +func parseLogLevel(s string) (zerolog.Level, error) { + l, err := zerolog.ParseLevel(s) + if err != nil { + return zerolog.NoLevel, err + } + return l, nil +} diff --git a/server/go.mod b/server/go.mod new file mode 100644 index 0000000..8dfc4aa --- /dev/null +++ b/server/go.mod @@ -0,0 +1,12 @@ +module github.com/aboqasem/portfolio/server + +go 1.21.7 + +require github.com/rs/zerolog v1.31.0 + +require ( + github.com/joho/godotenv v1.5.1 + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect + golang.org/x/sys v0.13.0 // indirect +) diff --git a/server/go.sum b/server/go.sum new file mode 100644 index 0000000..94f94a1 --- /dev/null +++ b/server/go.sum @@ -0,0 +1,18 @@ +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A= +github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/server/main.go b/server/main.go new file mode 100644 index 0000000..db49cd0 --- /dev/null +++ b/server/main.go @@ -0,0 +1,45 @@ +package main + +import ( + "fmt" + "html" + "net/http" + "time" + + "github.com/aboqasem/portfolio/server/config" + _ "github.com/joho/godotenv/autoload" + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" +) + +var logger = log.With().Str("pkg", "main").Logger() +var conf *config.Config + +func init() { + conf = config.Init() + + zerolog.SetGlobalLevel(conf.LogLevel) +} + +func main() { + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + logger.Debug().Any("method", r.Method).Str("path", r.URL.Path).Send() + + w.Write([]byte("

Hello, world!

"))
+		w.Write([]byte(r.Method))
+		w.Write([]byte{' '})
+		w.Write([]byte(html.EscapeString(r.URL.Path)))
+		w.Write([]byte("
")) + }) + + server := &http.Server{ + Addr: fmt.Sprintf(":%d", conf.Port), + ReadHeaderTimeout: 3 * time.Second, + } + + logger.Info().Uint16("port", conf.Port).Msg("Running server...") + err := server.ListenAndServe() + if err != nil { + logger.Fatal().Err(err).Msg("ListenAndServe") + } +} diff --git a/server/staticcheck.conf b/server/staticcheck.conf new file mode 100644 index 0000000..5d76215 --- /dev/null +++ b/server/staticcheck.conf @@ -0,0 +1,4 @@ +checks = ["all", "-ST1000", "-ST1020", "-ST1021", "-ST1022"] +initialisms = [] +dot_import_whitelist = [] +http_status_code_whitelist = [] diff --git a/vercel.json b/vercel.json index 074046e..5e3aab7 100644 --- a/vercel.json +++ b/vercel.json @@ -5,5 +5,5 @@ "outputDirectory": "dist", "devCommand": "bun dev", "framework": "vite", - "ignoreCommand": "! git diff --name-only HEAD^ HEAD | grep --quiet --invert-match --extended-regexp '^README\\.md|\\.biome\\.json|(\\.vscode|\\.github|\\.husky|scripts)/.*$'" + "ignoreCommand": "! git diff --name-only HEAD^ HEAD | grep --quiet --invert-match --extended-regexp '^README\\.md|biome\\.json|(\\.vscode|\\.github|\\.husky|scripts|server)/.*$'" }