Skip to content

Commit

Permalink
Implement server /photos routing, GET/POST functionality (#11)
Browse files Browse the repository at this point in the history
* Add thumbnail functionality (#12)

* Restructured server, added exif parsing, added fields to Photo

* Fix exif value stripping of null character

* Add IDs only response option

* Pull Hotshots data dir from environment if exists
  • Loading branch information
warrn authored and kochman committed Feb 21, 2018
1 parent 0f7f028 commit 3aca868
Show file tree
Hide file tree
Showing 7 changed files with 631 additions and 5 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
/vendor/*/
/hotshots
.idea
31 changes: 27 additions & 4 deletions config/config.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,34 @@
package config

import "os"
import "path"

type Config struct {
ListenURL string
ListenURL string
PhotosDirectory string
}

func New() (*Config, error) {
return &Config{
ListenURL: "127.0.0.1:8000",
}, nil
c := &Config{
ListenURL: "127.0.0.1:8000",
PhotosDirectory: "/var/hotshots",
}

dir, ok := os.LookupEnv("HOTSHOTS_DIR")
if ok {
c.PhotosDirectory = dir
}

return c, nil
}

func (c *Config) ImgFolder() string {
return path.Join(c.PhotosDirectory, "/img")
}
func (c *Config) ConfFolder() string {
return path.Join(c.PhotosDirectory, "/conf.d")
}

func (c *Config) StormFile() string {
return path.Join(c.ConfFolder(), "/hotshot.db")
}
36 changes: 36 additions & 0 deletions server/helpers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package server

import (
"encoding/json"
"math"
"net/http"
)

func round(num float64) int {
return int(num + math.Copysign(0.5, num))
}

func toFixed(num float64, precision int) float64 {
output := math.Pow(10, float64(precision))
return float64(round(num*output)) / output
}

func WriteError(s string, status int, w http.ResponseWriter) {
w.WriteHeader(status)
v := ErrorResponse{
Success: false,
Error: s,
}
WriteJsonResponse(v, w)
}

func WriteJsonResponse(v interface{}, w http.ResponseWriter) {
js, err := json.Marshal(v)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

w.Header().Set("Content-Type", "application/json")
w.Write(js)
}
199 changes: 199 additions & 0 deletions server/photo.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
package server

import (
"crypto/sha1"
"errors"
"fmt"
"image"
"image/jpeg"
"io"
"io/ioutil"
"os"
"strings"
"time"

"github.com/kochman/hotshots/log"
"github.com/nfnt/resize"
"github.com/rwcarlsen/goexif/exif"
"github.com/rwcarlsen/goexif/mknote"
)

/*
* Constants
*/

const MaxWidth = 256
const MaxHeight = 256

/*
* Data structs
*/

type Photo struct {
ID string `storm:"id"`
UploadedAt time.Time `storm:"index"`
TakenAt time.Time `storm:"index"`
Width int `storm:"index"`
Height int `storm:"index"`
Megapixels float64 `storm:"index"`
Lat float64
Long float64
CamSerial string `storm:"index"`
CamMake string `storm:"index"`
CamModel string `storm:"index"`
}

func NewPhoto(id string, r image.Rectangle, x exif.Exif) *Photo {
taken, err := x.DateTime()
if err != nil {
log.Error("Unable to find time for ", id)
}

lat, long, err := x.LatLong()
if err != nil {
log.Info("Unable to find gps for ", id)
}

cserial, err := x.Get(mknote.SerialNumber)
if err != nil {
log.Info("Unable to find serial for ", id)
}

cmake, err := x.Get(exif.Make)
if err != nil {
log.Info("Unable to find make for ", id)
}

cmodel, err := x.Get(exif.Model)
if err != nil {
log.Info("Unable to find model for ", id)
}

mp := toFixed(float64(r.Dx())*float64(r.Dy())/1000000.0, 1)

return &Photo{
ID: id,
UploadedAt: time.Now(),
TakenAt: taken,
Width: r.Dx(),
Height: r.Dy(),
Megapixels: mp,
Lat: lat,
Long: long,
CamSerial: strings.TrimSuffix(string(cserial.Val), "\u0000"),
CamMake: strings.TrimSuffix(string(cmake.Val), "\u0000"),
CamModel: strings.TrimSuffix(string(cmodel.Val), "\u0000"),
}
}

func ProcessPhoto(input io.Reader, id string, photoPath string, thumbPath string) (*exif.Exif, *image.Rectangle, error) {
saveErr := make(chan error)
defer close(saveErr)

photor, photow := io.Pipe()
thumbr, thumbw := io.Pipe()
exifr, exifw := io.Pipe()
defer photor.Close()
defer thumbr.Close()
defer exifr.Close()

go func() {
defer photow.Close()
defer thumbw.Close()
defer exifw.Close()

mw := io.MultiWriter(photow, thumbw, exifw)

if _, err := io.Copy(mw, input); err != nil {
log.Error(err)
return
}

}()

log.Info(fmt.Sprint("Saving image ", id))

var xif exif.Exif
var rect image.Rectangle
go SaveImage(photor, photoPath, saveErr)
go SaveThumb(&rect, thumbr, thumbPath, saveErr)
go GetExif(&xif, exifr, saveErr)

wasErrorSaving := false
for saveProcesses := 0; saveProcesses < 3; saveProcesses++ {
err := <-saveErr
if err != nil {
log.Error(err)
wasErrorSaving = true
}
}

if wasErrorSaving {
return nil, nil, errors.New("Unable to complete one of the photo processes")
}

return &xif, &rect, nil
}

func SaveImage(data io.Reader, path string, saveErr chan error) {
output, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE, 0660)
if err != nil {
log.Error(err)
saveErr <- err
return
}
defer output.Close()

if _, err := io.Copy(output, data); err != nil {
log.Error(err)
saveErr <- err
return
}

saveErr <- nil
}

func SaveThumb(r *image.Rectangle, data io.Reader, path string, saveErr chan error) {
img, err := jpeg.Decode(data)
io.Copy(ioutil.Discard, data)
if err != nil {
log.Error(err)
saveErr <- err
return
}
*r = img.Bounds()
resizedImg := resize.Thumbnail(MaxWidth, MaxHeight, img, resize.Bicubic)
output, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE, 0660)
if err != nil {
log.Error(err)
saveErr <- err
return
}
defer output.Close()

jpeg.Encode(output, resizedImg, nil)

saveErr <- nil
}

func GetExif(out *exif.Exif, data io.Reader, saveErr chan error) {
x, err := exif.Decode(data)
io.Copy(ioutil.Discard, data)
if err != nil {
log.Error(err)
saveErr <- err
return
}
*out = *x

saveErr <- nil
}

func GenPhotoID(f io.ReadSeeker) (string, error) {
digest := sha1.New()
if _, err := io.Copy(digest, f); err != nil {
return "", err
}
f.Seek(0, io.SeekStart)
return fmt.Sprintf("%x", digest.Sum(nil)), nil
}
Loading

0 comments on commit 3aca868

Please sign in to comment.