mirror of
https://github.com/Cernobor/oko-server.git
synced 2025-02-24 08:27:17 +00:00
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:
parent
7b1c3cfc28
commit
a5d98c2eb7
1
go.mod
1
go.mod
@ -24,6 +24,7 @@ require (
|
|||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
github.com/ugorji/go/codec v1.2.6 // indirect
|
github.com/ugorji/go/codec v1.2.6 // indirect
|
||||||
golang.org/x/crypto v0.0.0-20220112180741-5e0467b6c7ce // 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/sys v0.0.0-20220114195835-da31bd327af9 // indirect
|
||||||
golang.org/x/text v0.3.7 // indirect
|
golang.org/x/text v0.3.7 // indirect
|
||||||
google.golang.org/protobuf v1.27.1 // indirect
|
google.golang.org/protobuf v1.27.1 // indirect
|
||||||
|
2
go.sum
2
go.sum
@ -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/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-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-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-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-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||||
|
11
main.go
11
main.go
@ -28,6 +28,9 @@ func main() {
|
|||||||
minZoomArg := flag.Int("min-zoom", 1, "Minimum zoom that will be sent to clients.")
|
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.")
|
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.")
|
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()
|
flag.Parse()
|
||||||
|
|
||||||
@ -42,6 +45,11 @@ func main() {
|
|||||||
t = time.Now()
|
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{
|
s := server.New(server.ServerConfig{
|
||||||
VersionHash: sha1ver,
|
VersionHash: sha1ver,
|
||||||
BuildTime: &t,
|
BuildTime: &t,
|
||||||
@ -55,6 +63,9 @@ func main() {
|
|||||||
Lat: *defaultCenterLatArg,
|
Lat: *defaultCenterLatArg,
|
||||||
Lng: *defaultCenterLngArg,
|
Lng: *defaultCenterLngArg,
|
||||||
},
|
},
|
||||||
|
MaxPhotoX: *maxPhotoXArg,
|
||||||
|
MaxPhotoY: *maxPhotoYArg,
|
||||||
|
PhotoQuality: *photoQualityArg,
|
||||||
})
|
})
|
||||||
|
|
||||||
sigs := make(chan os.Signal, 1)
|
sigs := make(chan os.Signal, 1)
|
||||||
|
@ -4,7 +4,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io"
|
||||||
"math"
|
"math"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
@ -257,7 +257,7 @@ func (s *Server) handlePOSTDataMultipart(gc *gin.Context) {
|
|||||||
internalError(gc, fmt.Errorf("failed to open 'data' 'file': %w", err))
|
internalError(gc, fmt.Errorf("failed to open 'data' 'file': %w", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
dataBytes, err := ioutil.ReadAll(df)
|
dataBytes, err := io.ReadAll(df)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
internalError(gc, fmt.Errorf("failed to open 'data' 'file': %w", err))
|
internalError(gc, fmt.Errorf("failed to open 'data' 'file': %w", err))
|
||||||
return
|
return
|
||||||
|
@ -2,6 +2,7 @@ package server
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"archive/zip"
|
"archive/zip"
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
@ -48,6 +49,9 @@ type ServerConfig struct {
|
|||||||
ReinitDB bool
|
ReinitDB bool
|
||||||
MinZoom int
|
MinZoom int
|
||||||
DefaultCenter models.Coords
|
DefaultCenter models.Coords
|
||||||
|
MaxPhotoX int
|
||||||
|
MaxPhotoY int
|
||||||
|
PhotoQuality int
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(config ServerConfig) *Server {
|
func New(config ServerConfig) *Server {
|
||||||
@ -62,6 +66,8 @@ func (s *Server) Run(ctx context.Context) {
|
|||||||
|
|
||||||
s.ctx = ctx
|
s.ctx = ctx
|
||||||
s.setupDB()
|
s.setupDB()
|
||||||
|
defer s.cleanupDb()
|
||||||
|
|
||||||
s.setupTiles()
|
s.setupTiles()
|
||||||
|
|
||||||
router := s.setupRouter()
|
router := s.setupRouter()
|
||||||
@ -86,6 +92,9 @@ func (s *Server) Run(ctx context.Context) {
|
|||||||
if err := server.Shutdown(ctx); err != nil {
|
if err := server.Shutdown(ctx); err != nil {
|
||||||
s.log.WithError(err).Fatal("Server forced to shutdown.")
|
s.log.WithError(err).Fatal("Server forced to shutdown.")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) cleanupDb() {
|
||||||
close(s.checkpointNotice)
|
close(s.checkpointNotice)
|
||||||
s.log.Info("Closing db connection pool...")
|
s.log.Info("Closing db connection pool...")
|
||||||
s.dbpool.Close()
|
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.")
|
s.log.WithError(err).Error("Failed to open connection for final checkpoint.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
err = sqlitex.Exec(conn, "vacuum", nil)
|
||||||
|
if err != nil {
|
||||||
|
s.log.WithError(err).Error("Failed to vacuum db.")
|
||||||
|
}
|
||||||
s.checkpointDb(conn, true)
|
s.checkpointDb(conn, true)
|
||||||
conn.Close()
|
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)
|
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.BindInt64(1, int64(featureID))
|
||||||
stmt.BindText(2, thumbnail.ContentType)
|
stmt.BindText(2, thumbnail.ContentType)
|
||||||
stmt.BindText(3, photo.ContentType)
|
stmt.BindText(3, "image/jpeg")
|
||||||
stmt.BindZeroBlob(4, thumbnail.Size)
|
stmt.BindZeroBlob(4, thumbnail.Size)
|
||||||
stmt.BindZeroBlob(5, photo.Size)
|
stmt.BindZeroBlob(5, int64(len(resizedPhoto)))
|
||||||
|
|
||||||
_, err = stmt.Step()
|
_, err = stmt.Step()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -742,9 +766,8 @@ func (s *Server) addPhotos(conn *sqlite.Conn, createdFeatureMapping, addedFeatur
|
|||||||
}
|
}
|
||||||
err = func() error {
|
err = func() error {
|
||||||
defer blob.Close()
|
defer blob.Close()
|
||||||
defer photo.File.Close()
|
|
||||||
|
|
||||||
_, err := io.Copy(blob, photo.File)
|
_, err = io.Copy(blob, bytes.NewBuffer(resizedPhoto))
|
||||||
return err
|
return err
|
||||||
}()
|
}()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -1,6 +1,15 @@
|
|||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
"image/jpeg"
|
||||||
|
"io"
|
||||||
|
"math"
|
||||||
|
|
||||||
|
"golang.org/x/image/draw"
|
||||||
|
|
||||||
"cernobor.cz/oko-server/models"
|
"cernobor.cz/oko-server/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -32,3 +41,57 @@ func isUniqueFeatureID(features []models.Feature) bool {
|
|||||||
}
|
}
|
||||||
return true
|
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
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user