Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
theredrad committed Sep 24, 2021
0 parents commit 24a3696
Show file tree
Hide file tree
Showing 10 changed files with 996 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.idea
.DS_Store
images
113 changes: 113 additions & 0 deletions api/api.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package api

import (
"bytes"
"encoding/json"
"github.com/gorilla/mux"
"image/jpeg"
"log"
"lorem/image"
"lorem/manager"
"net/http"
"strconv"
)

type Options struct {
MinWidth int
MaxWidth int
MinHeight int
MaxHeight int
}

const (
minSize = 8
maxSize = 2000
)

type API struct {
mngr *manager.Manager
pr image.Processor
opt *Options
}

func New(manager *manager.Manager, imageProcessor image.Processor, options *Options) *API {
if options == nil {
options = &Options{
MinWidth: minSize,
MinHeight: minSize,
MaxWidth: maxSize,
MaxHeight: maxSize,
}
}

return &API{
mngr: manager,
pr: imageProcessor,
}
}

func (a *API) SizeHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")

vars := mux.Vars(r)

file := a.mngr.Pick(vars["category"])
if file == "" {
handleError(w, http.StatusNotFound, "category not found")
return
}

width, err := strconv.ParseInt(vars["width"], 10, 32)
if err != nil || width < 8 {
handleError(w, http.StatusBadRequest, "invalid width size")
return
}

height, err := strconv.ParseInt(vars["height"], 10, 32)
if err != nil || height < 8 {
handleError(w, http.StatusBadRequest, "invalid height size")
return
}

img, err := image.Decode(file)
if err != nil {
handleError(w, http.StatusInternalServerError, "internal error")
return
}

img = a.pr.CropCenter(*img, int(width), int(height))


buffer := new(bytes.Buffer)
if err := jpeg.Encode(buffer, *img, nil); err != nil {
log.Println("unable to encode image.")
return
}

w.Header().Set("Content-Type", "image/jpeg")
w.Header().Set("Content-Length", strconv.Itoa(len(buffer.Bytes())))
if _, err := w.Write(buffer.Bytes()); err != nil {
log.Println("unable to write image.")
return
}
}

func (a *API) NotFound(w http.ResponseWriter, r *http.Request) {
handleError(w, http.StatusNotFound, "not found")
}

func handleError(w http.ResponseWriter, code int, msg string) {
b, err := json.Marshal(&Error{
Message: msg,
})
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
log.Printf("error while marshaling response: %s", err)
return
}

w.WriteHeader(code)
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.Write(b)
return
}
5 changes: 5 additions & 0 deletions api/types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package api

type Error struct {
Message string `json:"message"`
}
11 changes: 11 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
module lorem

go 1.16

require (
github.com/disintegration/imaging v1.6.2
github.com/gorilla/handlers v1.5.1
github.com/gorilla/mux v1.8.0
github.com/spf13/pflag v1.0.5
github.com/spf13/viper v1.9.0
)
663 changes: 663 additions & 0 deletions go.sum

Large diffs are not rendered by default.

25 changes: 25 additions & 0 deletions image/image.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package image

import (
"image"
"os"
)

type Processor interface {
CropCenter(img image.Image, width, height int) *image.Image
}

func Decode(name string) (*image.Image, error) {
imagePath, err := os.Open(name)
if err != nil {
return nil, err
}
defer imagePath.Close()

img, _, err := image.Decode(imagePath)
if err != nil {
return nil, err
}

return &img, nil
}
13 changes: 13 additions & 0 deletions image/imaging.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package image

import (
"github.com/disintegration/imaging"
"image"
)

type Imaging struct {}

func (i *Imaging) CropCenter(img image.Image, width, height int) *image.Image {
img = imaging.Fill(img, width, height, imaging.Center, imaging.Lanczos)
return &img
}
59 changes: 59 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package main

import (
"log"
"lorem/api"
"lorem/image"
"lorem/manager"
"net/http"
"os"
"path"

"github.com/gorilla/handlers"
"github.com/gorilla/mux"
"github.com/spf13/pflag"
"github.com/spf13/viper"
)

func init() {
flags := pflag.NewFlagSet(path.Base(os.Args[0]), pflag.ContinueOnError)

flags.String("host", "127.0.0.1:8080", "host:port for the HTTP server")
flags.String("dir", "./images", "directory of images")

err := flags.Parse(os.Args[1:])
if err != nil {
log.Panic("failed to parse arguments")
}

err = viper.BindPFlags(flags)
if err != nil {
log.Panic("failed to bind flags")
}
}

func main() {
m, err := manager.New(viper.GetString("dir"))
if err != nil {
log.Panic(err)
}

log.Printf("%d items loaded", m.Total())

a := api.New(m, &image.Imaging{}, nil)

r := mux.NewRouter()
r.HandleFunc("/{category}/{width:[0-9]+}/{height:[0-9]+}", a.SizeHandler).Methods(http.MethodGet)
r.HandleFunc("/{width:[0-9]+}/{height:[0-9]+}", a.SizeHandler).Methods(http.MethodGet)
r.PathPrefix("/").HandlerFunc(a.NotFound)

log.Printf("listening on %s", viper.GetString("host"))
err = http.ListenAndServe(viper.GetString("host"), handlers.CORS(
handlers.AllowedOrigins([]string{"*"}),
handlers.AllowedHeaders([]string{"Content-Type", "X-Requested-With"}),
)(r))
if err != nil {
log.Panic("HTTP server failed to start")
}
}

71 changes: 71 additions & 0 deletions manager/manager.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package manager

import (
"math/rand"
"strings"
"time"
)

const (
defaultDirName = "__root"
)

type Manager struct {
base string
ds map[string][]string
mc []string // index of categories
}

func New(dir string) (*Manager, error) {
ds, err := scan(dir)
if err != nil {
return nil, err
}

var mc []string
for c, _ := range ds {
mc = append(mc, c)
}

return &Manager{
base: dir,
ds: ds,
mc: mc,
}, nil
}


func (m *Manager) Pick(cat string) string {
if cat == "" {
cat = m.randomCategory()
}
return m.randomEntity(cat)
}

func (m *Manager) Total() int {
var t int
for _, d := range m.ds {
t += len(d)
}
return t
}

func (m *Manager) randomCategory() string {
rand.Seed(time.Now().UnixNano())
return m.mc[rand.Intn(len(m.mc))]
}

func (m *Manager) randomEntity(c string) string {
rand.Seed(time.Now().UnixNano())
if _, ok := m.ds[c]; !ok {
return ""
}

names := []string{m.base}
if c != defaultDirName {
names = append(names, c)
}
names = append(names, m.ds[c][rand.Intn(len(m.ds[c]))])

return strings.Join(names, "/")
}
33 changes: 33 additions & 0 deletions manager/scan.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package manager

import (
"fmt"
"io/ioutil"
)

func scan(d string) (map[string][]string, error) {
var ds = make(map[string][]string)
files, err := ioutil.ReadDir(d)
if err != nil {
return nil, err
}

for _, f := range files {
if f.IsDir() {
dFiles, err := ioutil.ReadDir(fmt.Sprintf("%s/%s", d, f.Name()))
if err != nil {
return nil, err
}

for _, df := range dFiles {
if !df.IsDir() { // ignore nested directories
ds[f.Name()] = append(ds[f.Name()], df.Name())
}
}
} else {
ds[defaultDirName] = append(ds[defaultDirName], f.Name())
}
}

return ds, err
}

0 comments on commit 24a3696

Please sign in to comment.