Browse Source

Reorganize the project. Remove the random handler. Add a health logger.

eventually
Quentin Barrand 10 months ago
parent
commit
14117030bc
7 changed files with 274 additions and 229 deletions
  1. +1
    -1
      go.mod
  2. +3
    -1
      main.go
  3. +145
    -0
      pkg/handlers/handlers.go
  4. +83
    -0
      pkg/handlers/image.go
  5. +3
    -3
      pkg/img/img.go
  6. +39
    -0
      pkg/server.go
  7. +0
    -224
      server.go

+ 1
- 1
go.mod View File

@@ -1,4 +1,4 @@
module git.quba.fr/qbarrand/img-mw
module git.quba.fr/qbarrand/quba.fr-server

go 1.13



+ 3
- 1
main.go View File

@@ -5,6 +5,8 @@ import (
"os"

"github.com/urfave/cli"

"git.quba.fr/qbarrand/quba.fr-server/pkg"
)

func main() {
@@ -47,7 +49,7 @@ func main() {
log.Print("Serving contents from " + dir)
log.Print("Starting the server on " + addr)

return startServer(addr, dir, quality)
return pkg.StartServer(addr, dir, quality)
}

if err := app.Run(os.Args); err != nil {


+ 145
- 0
pkg/handlers/handlers.go View File

@@ -0,0 +1,145 @@
package handlers

import (
"fmt"
"log"
"net"
"net/http"
"os/exec"
"path/filepath"
"strings"
"sync"
"text/template"
"time"

"gopkg.in/gographics/imagick.v2/imagick"
)

type healthCache struct {
lastCheck time.Time
value bool

m sync.Mutex
}

func Health() http.Handler {
const secondsBetweenChecks = 120

c := &healthCache{}

return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
const fqdn = "ping.quba.fr"

log.Printf("Last DNS lookup: %v", c.lastCheck.String())

now := time.Now()

if now.Sub(c.lastCheck) <= secondsBetweenChecks*time.Second {
log.Print("Using cache")
} else {
log.Printf("Older than %d seconds; cache invalidated", secondsBetweenChecks)

records, err := net.LookupTXT(fqdn)
if err != nil {
log.Print(err)
w.WriteHeader(http.StatusInternalServerError)
return
}

if len(records) != 1 {
log.Printf("%s/TXT: not enough records", fqdn)
w.WriteHeader(http.StatusInternalServerError)
return
}

const expected = "quentin@quba.fr"
got := records[0]

c.m.Lock()

if got != expected {
log.Printf("Expected %s, got %s", expected, got)
c.value = false
} else {
c.value = true
}

c.lastCheck = now

c.m.Unlock()
}

if !c.value {
w.WriteHeader(http.StatusInternalServerError)
} else {
w.WriteHeader(http.StatusOK)
}
})
}

func Image(baseDir string, quality uint, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
handledExtensions := map[string]bool{
".jpg": true,
".png": true,
}

if !handledExtensions[filepath.Ext(r.URL.Path)] {
// Not an image
next.ServeHTTP(w, r)
return
}

mw := imagick.NewMagickWand()

if err := mw.ReadImage(filepath.Join(baseDir, r.URL.Path)); err != nil {
log.Print(err)
http.NotFound(w, r)
return
}

ih{mw: mw, quality: quality}.ServeHTTP(w, r)
})
}

func Logger(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Printf("%s %s %s", r.RemoteAddr, r.Method, r.RequestURI)
next.ServeHTTP(w, r)
})
}

func Sitemap(dir string) (http.Handler, error) {
const sitemapTemplateStr = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://quba.fr/</loc>
<lastmod>{{ .LastMod }}</lastmod>
<changefreq>monthly</changefreq>
<priority>1.0</priority>
</url>
</urlset>`

sitemapTempate, err := template.New("sitemap").Parse(sitemapTemplateStr)
if err != nil {
return nil, fmt.Errorf("Could not parse the sitemap template: %v", err)
}

handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
cmd := exec.Command("git", "log", "-1", "--format=%ad", "--date=iso-strict")
cmd.Dir = dir

out, err := cmd.Output()
if err != nil {
log.Printf("Error while running git: %v", err)
w.WriteHeader(http.StatusInternalServerError)
return
}

date := strings.TrimSuffix(string(out), "\n")

sitemapTempate.Execute(w, struct{ LastMod string }{date})
})

return handler, nil
}

+ 83
- 0
pkg/handlers/image.go View File

@@ -0,0 +1,83 @@
package handlers

import (
"fmt"
"log"
"net/http"
"strconv"

"git.quba.fr/qbarrand/quba.fr-server/pkg/img"

"gopkg.in/gographics/imagick.v2/imagick"
)

func parseDimensions(r *http.Request) (uint, uint, error) {
var (
height uint
width uint
)

const (
base = 10
bits = 64
)

if heightStr := r.FormValue("height"); heightStr != "" {
if height64, err := strconv.ParseUint(heightStr, base, bits); err != nil {
return 0, 0, fmt.Errorf("%q: invalid height: %v", heightStr, err)
} else {
height = uint(height64)
}
}

if widthStr := r.FormValue("width"); widthStr != "" {
if width64, err := strconv.ParseUint(widthStr, base, bits); err != nil {
return 0, 0, fmt.Errorf("%q: invalid width: %v", widthStr, err)
} else {
width = uint(width64)
}
}

if height != 0 && width != 0 {
return 0, 0, fmt.Errorf("height and width both set (%dx%d)", height, width)
}

return height, width, nil
}

type ih struct {
mw *imagick.MagickWand
quality uint
}

func (i ih) ServeHTTP(w http.ResponseWriter, r *http.Request) {
height, width, err := parseDimensions(r)
if err != nil {
log.Print(err)
w.WriteHeader(http.StatusBadRequest)
return
}

if err := img.Resize(i.mw, height, width, i.quality, r.FormValue("format")); err != nil {
log.Printf("Could not resize the image: %v", err)
w.WriteHeader(http.StatusInternalServerError)
return
}

cr, cg, cb, err := img.GetMainColor(i.mw)

if err != nil {
log.Printf("Could not get the main color: %v", err)
} else {
hexRGB := fmt.Sprintf("#%02X%02X%02X", cr, cg, cb)
w.Header().Set("X-Quba-MainColor", hexRGB)
}

if n, err := w.Write(i.mw.GetImageBlob()); err != nil {
log.Printf("Could not write the bytes: %v", err)
w.WriteHeader(http.StatusInternalServerError)
return
} else {
log.Printf("Wrote %d bytes", n)
}
}

img.go → pkg/img/img.go View File

@@ -1,4 +1,4 @@
package main
package img

import (
"fmt"
@@ -7,7 +7,7 @@ import (
"gopkg.in/gographics/imagick.v2/imagick"
)

func resize(mw *imagick.MagickWand, height, width, quality uint, format string) error {
func Resize(mw *imagick.MagickWand, height, width, quality uint, format string) error {
//
// Sampling factor
//
@@ -93,7 +93,7 @@ func resize(mw *imagick.MagickWand, height, width, quality uint, format string)
return nil
}

func getMainColor(mw *imagick.MagickWand) (uint, uint, uint, error) {
func GetMainColor(mw *imagick.MagickWand) (uint, uint, uint, error) {
c := mw.Clone()

if err := c.SetDepth(8); err != nil {

+ 39
- 0
pkg/server.go View File

@@ -0,0 +1,39 @@
package pkg

import (
"net/http"

"gopkg.in/gographics/imagick.v2/imagick"

"git.quba.fr/qbarrand/quba.fr-server/pkg/handlers"
)

func StartServer(addr, dir string, quality uint) error {
imagick.Initialize()
defer imagick.Terminate()

s, err := handlers.Sitemap(dir)
if err != nil {
return err
}

http.Handle("/sitemap.xml", handlers.Logger(s))

http.Handle("/",
handlers.Logger(
handlers.Image(
dir,
quality,
http.FileServer(http.Dir(dir)),
),
),
)

http.Handle("/health",
handlers.Logger(
handlers.Health(),
),
)

return http.ListenAndServe(addr, nil)
}

+ 0
- 224
server.go View File

@@ -1,224 +0,0 @@
package main

import (
"encoding/json"
"fmt"
"log"
"math/rand"
"net/http"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"text/template"

"gopkg.in/gographics/imagick.v2/imagick"
)

type bgData struct {
File string
Location string
Date string
}

type ih struct {
mw *imagick.MagickWand
quality uint
}

func (i ih) ServeHTTP(w http.ResponseWriter, r *http.Request) {
height, width, err := parseDimensions(r)
if err != nil {
log.Print(err)
w.WriteHeader(http.StatusBadRequest)
return
}

if err := resize(i.mw, height, width, i.quality, r.FormValue("format")); err != nil {
log.Printf("Could not resize the image: %v", err)
w.WriteHeader(http.StatusInternalServerError)
return
}

if n, err := w.Write(i.mw.GetImageBlob()); err != nil {
log.Printf("Could not write the bytes: %v", err)
w.WriteHeader(http.StatusInternalServerError)
return
} else {
log.Printf("Wrote %d bytes", n)
}
}

func loggerHandler(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Printf("%s %s %s", r.RemoteAddr, r.Method, r.RequestURI)
next.ServeHTTP(w, r)
})
}

func parseDimensions(r *http.Request) (uint, uint, error) {
var (
height uint
width uint
)

const (
base = 10
bits = 64
)

if heightStr := r.FormValue("height"); heightStr != "" {
if height64, err := strconv.ParseUint(heightStr, base, bits); err != nil {
return 0, 0, fmt.Errorf("%q: invalid height: %v", heightStr, err)
} else {
height = uint(height64)
}
}

if widthStr := r.FormValue("width"); widthStr != "" {
if width64, err := strconv.ParseUint(widthStr, base, bits); err != nil {
return 0, 0, fmt.Errorf("%q: invalid width: %v", widthStr, err)
} else {
width = uint(width64)
}
}

if height != 0 && width != 0 {
return 0, 0, fmt.Errorf("height and width both set (%dx%d)", height, width)
}

return height, width, nil
}

func imageHandler(baseDir string, quality uint, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
handledExtensions := map[string]bool{
".jpg": true,
".png": true,
}

if !handledExtensions[filepath.Ext(r.URL.Path)] {
// Not an image
next.ServeHTTP(w, r)
return
}

mw := imagick.NewMagickWand()

if err := mw.ReadImage(filepath.Join(baseDir, r.URL.Path)); err != nil {
log.Print(err)
http.NotFound(w, r)
return
}

ih{mw: mw, quality: quality}.ServeHTTP(w, r)
})
}

func randomHandler(dir string, quality uint) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fd, err := os.Open(filepath.Join(dir, "db.json"))
if err != nil {
log.Printf("Could not open the background database: %v", err)
w.WriteHeader(http.StatusInternalServerError)
return
}

d := make([]bgData, 0)

if err := json.NewDecoder(fd).Decode(&d); err != nil {
log.Printf("Could not decode bg.json: %v", err)
w.WriteHeader(http.StatusInternalServerError)
return
}

selected := d[rand.Intn(len(d))]

mw := imagick.NewMagickWand()

if err := mw.ReadImage(filepath.Join(dir, selected.File)); err != nil {
log.Print(err)
w.WriteHeader(http.StatusInternalServerError)
return
}

cr, cg, cb, err := getMainColor(mw)
if err != nil {
log.Printf("Could not get the image's main color: %v", err)
w.WriteHeader(http.StatusInternalServerError)
return
}

hexRGB := fmt.Sprintf("#%02X%02X%02X", cr, cg, cb)

w.Header().Set("X-Quba-Date", selected.Date)
w.Header().Set("X-Quba-Location", selected.Location)
w.Header().Set("X-Quba-MainColor", hexRGB)

ih{mw: mw, quality: quality}.ServeHTTP(w, r)
})
}

func sitemapHandler(dir string) (http.Handler, error) {
const sitemapTemplateStr = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://quba.fr/</loc>
<lastmod>{{ .LastMod }}</lastmod>
<changefreq>monthly</changefreq>
<priority>1.0</priority>
</url>
</urlset>`

sitemapTempate, err := template.New("sitemap").Parse(sitemapTemplateStr)
if err != nil {
return nil, fmt.Errorf("Could not parse the sitemap template: %v", err)
}

handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
cmd := exec.Command("git", "log", "-1", "--format=%ad", "--date=iso-strict")
cmd.Dir = dir

out, err := cmd.Output()
if err != nil {
log.Printf("Error while running git: %v", err)
w.WriteHeader(http.StatusInternalServerError)
return
}

date := strings.TrimSuffix(string(out), "\n")

sitemapTempate.Execute(w, struct{ LastMod string }{date})
})

return handler, nil
}

func startServer(addr, dir string, quality uint) error {
imagick.Initialize()
defer imagick.Terminate()

s, err := sitemapHandler(dir)
if err != nil {
return err
}

http.Handle("/sitemap.xml", loggerHandler(s))

http.Handle("/",
loggerHandler(
imageHandler(
dir,
quality,
http.FileServer(http.Dir(dir)),
),
),
)

imgDir := filepath.Join(dir, "images", "bg")

http.Handle("/images/bg/random", loggerHandler(randomHandler(imgDir, quality)))

return http.ListenAndServe(addr, nil)
}

Loading…
Cancel
Save