Resizing photos, db vacuuming.

* Incoming feature photos are resized and compressed to JPEG to the given dimensions and quality.
* Database is vacuumed during cleanup.
This commit is contained in:
zegkljan 2022-09-11 19:40:52 +02:00
parent 7b1c3cfc28
commit a5d98c2eb7
6 changed files with 106 additions and 6 deletions

1
go.mod
View File

@ -24,6 +24,7 @@ require (
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/ugorji/go/codec v1.2.6 // indirect
golang.org/x/crypto v0.0.0-20220112180741-5e0467b6c7ce // indirect
golang.org/x/image v0.0.0-20220722155232-062f8c9fd539
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 // indirect
golang.org/x/text v0.3.7 // indirect
google.golang.org/protobuf v1.27.1 // indirect

2
go.sum
View File

@ -429,6 +429,8 @@ golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EH
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20220722155232-062f8c9fd539 h1:/eM0PCrQI2xd471rI+snWuu251/+/jpBpZqir2mPdnU=
golang.org/x/image v0.0.0-20220722155232-062f8c9fd539/go.mod h1:doUCurBvlfPMKfmIpRIywoHmhN3VyhnoFDbvIEWF4hY=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=

11
main.go
View File

@ -28,6 +28,9 @@ func main() {
minZoomArg := flag.Int("min-zoom", 1, "Minimum zoom that will be sent to clients.")
defaultCenterLatArg := flag.Float64("default-center-lat", 0, "Latitude of the default map center.")
defaultCenterLngArg := flag.Float64("default-center-lng", 0, "Longitude of the default map center.")
maxPhotoXArg := flag.Int("max-photo-width", 0, "Maximum width of photos. 0 means no limit.")
maxPhotoYArg := flag.Int("max-photo-height", 0, "Maximum height of photos. 0 means no limit.")
photoQualityArg := flag.Int("photo-quality", 90, "Photo JPEG quality.")
flag.Parse()
@ -42,6 +45,11 @@ func main() {
t = time.Now()
}
if *maxPhotoXArg < 0 || *maxPhotoYArg < 0 {
fmt.Fprintln(os.Stderr, "Max photo width and height cannot be less than 0.")
os.Exit(1)
}
s := server.New(server.ServerConfig{
VersionHash: sha1ver,
BuildTime: &t,
@ -55,6 +63,9 @@ func main() {
Lat: *defaultCenterLatArg,
Lng: *defaultCenterLngArg,
},
MaxPhotoX: *maxPhotoXArg,
MaxPhotoY: *maxPhotoYArg,
PhotoQuality: *photoQualityArg,
})
sigs := make(chan os.Signal, 1)

View File

@ -4,7 +4,7 @@ import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"io"
"math"
"net/http"
"os"
@ -257,7 +257,7 @@ func (s *Server) handlePOSTDataMultipart(gc *gin.Context) {
internalError(gc, fmt.Errorf("failed to open 'data' 'file': %w", err))
return
}
dataBytes, err := ioutil.ReadAll(df)
dataBytes, err := io.ReadAll(df)
if err != nil {
internalError(gc, fmt.Errorf("failed to open 'data' 'file': %w", err))
return

View File

@ -2,6 +2,7 @@ package server
import (
"archive/zip"
"bytes"
"context"
"encoding/json"
"fmt"
@ -48,6 +49,9 @@ type ServerConfig struct {
ReinitDB bool
MinZoom int
DefaultCenter models.Coords
MaxPhotoX int
MaxPhotoY int
PhotoQuality int
}
func New(config ServerConfig) *Server {
@ -62,6 +66,8 @@ func (s *Server) Run(ctx context.Context) {
s.ctx = ctx
s.setupDB()
defer s.cleanupDb()
s.setupTiles()
router := s.setupRouter()
@ -86,6 +92,9 @@ func (s *Server) Run(ctx context.Context) {
if err := server.Shutdown(ctx); err != nil {
s.log.WithError(err).Fatal("Server forced to shutdown.")
}
}
func (s *Server) cleanupDb() {
close(s.checkpointNotice)
s.log.Info("Closing db connection pool...")
s.dbpool.Close()
@ -96,6 +105,10 @@ func (s *Server) Run(ctx context.Context) {
s.log.WithError(err).Error("Failed to open connection for final checkpoint.")
return
}
err = sqlitex.Exec(conn, "vacuum", nil)
if err != nil {
s.log.WithError(err).Error("Failed to vacuum db.")
}
s.checkpointDb(conn, true)
conn.Close()
}
@ -710,11 +723,22 @@ func (s *Server) addPhotos(conn *sqlite.Conn, createdFeatureMapping, addedFeatur
return fmt.Errorf("failed to clear bindings of prepared statement: %w", err)
}
resizedPhoto, err := func() ([]byte, error) {
defer photo.File.Close()
resized, err := resizePhoto(s.config.MaxPhotoX, s.config.MaxPhotoY, s.config.PhotoQuality, photo.File)
if err != nil {
return nil, err
}
return resized, nil
}()
stmt.BindInt64(1, int64(featureID))
stmt.BindText(2, thumbnail.ContentType)
stmt.BindText(3, photo.ContentType)
stmt.BindText(3, "image/jpeg")
stmt.BindZeroBlob(4, thumbnail.Size)
stmt.BindZeroBlob(5, photo.Size)
stmt.BindZeroBlob(5, int64(len(resizedPhoto)))
_, err = stmt.Step()
if err != nil {
@ -742,9 +766,8 @@ func (s *Server) addPhotos(conn *sqlite.Conn, createdFeatureMapping, addedFeatur
}
err = func() error {
defer blob.Close()
defer photo.File.Close()
_, err := io.Copy(blob, photo.File)
_, err = io.Copy(blob, bytes.NewBuffer(resizedPhoto))
return err
}()
if err != nil {

View File

@ -1,6 +1,15 @@
package server
import (
"bytes"
"fmt"
"image"
"image/jpeg"
"io"
"math"
"golang.org/x/image/draw"
"cernobor.cz/oko-server/models"
)
@ -32,3 +41,57 @@ func isUniqueFeatureID(features []models.Feature) bool {
}
return true
}
func resizePhoto(maxX, maxY, quality int, data io.Reader) ([]byte, error) {
src, _, err := image.Decode(data)
if err != nil {
return nil, fmt.Errorf("failed to decode image: %w", err)
}
if maxX == 0 && maxY == 0 {
output := &bytes.Buffer{}
jpeg.Encode(output, src, &jpeg.Options{
Quality: 90,
})
return output.Bytes(), nil
}
var dst draw.Image
srcX := src.Bounds().Max.X
srcY := src.Bounds().Max.Y
srcRatio := float64(srcX) / float64(srcY)
if maxX == 0 && srcY > maxY {
newX := int(math.Round(float64(maxY) * srcRatio))
newY := maxY
dst = image.NewRGBA(image.Rect(0, 0, newX, newY))
draw.CatmullRom.Scale(dst, dst.Bounds(), src, src.Bounds(), draw.Over, nil)
} else if maxY == 0 && srcX > maxX {
newX := maxX
newY := int(math.Round(float64(maxX) / srcRatio))
dst = image.NewRGBA(image.Rect(0, 0, newX, newY))
draw.CatmullRom.Scale(dst, dst.Bounds(), src, src.Bounds(), draw.Over, nil)
} else if srcX > maxX || srcY > maxY {
tgtRatio := float64(maxX) / float64(maxY)
var newX, newY int
if srcRatio > tgtRatio {
newX = maxX
newY = int(math.Round(float64(maxX) / srcRatio))
} else {
newX = int(math.Round(float64(maxY) * srcRatio))
newY = maxY
}
dst = image.NewRGBA(image.Rect(0, 0, newX, newY))
draw.CatmullRom.Scale(dst, dst.Bounds(), src, src.Bounds(), draw.Over, nil)
} else {
dst = image.NewRGBA(image.Rect(0, 0, srcX, srcY))
draw.Copy(dst, image.Point{X: 0, Y: 0}, src, src.Bounds(), draw.Over, nil)
}
output := &bytes.Buffer{}
jpeg.Encode(output, dst, &jpeg.Options{
Quality: quality,
})
return output.Bytes(), nil
}