mirror of
https://github.com/Cernobor/oko-server.git
synced 2025-02-24 08:27:17 +00:00
refining photo handling
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
* photo metedata provided with downloaded data * data download recognizes application/json and application/zip accepted types and serves bare json and zip with photos respectively * thumbnail handling and storage
This commit is contained in:
parent
f8b42f35f6
commit
7deb7e3f39
@ -34,6 +34,22 @@ func (e *ErrPhotoNotProvided) Error() string {
|
|||||||
return fmt.Sprintf("referenced photo %s which was not provided", e.Reference)
|
return fmt.Sprintf("referenced photo %s which was not provided", e.Reference)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type EErrPhotoThumbnailNotProvided *ErrPhotoThumbnailNotProvided
|
||||||
|
|
||||||
|
type ErrPhotoThumbnailNotProvided struct {
|
||||||
|
Reference string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewErrPhotoThumbnailNotProvided(reference string) *ErrPhotoThumbnailNotProvided {
|
||||||
|
return &ErrPhotoThumbnailNotProvided{
|
||||||
|
Reference: reference,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ErrPhotoThumbnailNotProvided) Error() string {
|
||||||
|
return fmt.Sprintf("referenced photo %s the thumbnail of which was not provided", e.Reference)
|
||||||
|
}
|
||||||
|
|
||||||
type ErrFeatureForPhotoNotExists struct {
|
type ErrFeatureForPhotoNotExists struct {
|
||||||
PhotoFeatureReference int64
|
PhotoFeatureReference int64
|
||||||
}
|
}
|
||||||
|
@ -71,6 +71,7 @@ type HandshakeResponse struct {
|
|||||||
type Data struct {
|
type Data struct {
|
||||||
Users []User `json:"users"`
|
Users []User `json:"users"`
|
||||||
Features []Feature `json:"features"`
|
Features []Feature `json:"features"`
|
||||||
|
PhotoMetadata map[string]PhotoMetadata `json:"photo_metadata,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Photo struct {
|
type Photo struct {
|
||||||
@ -78,3 +79,10 @@ type Photo struct {
|
|||||||
File io.ReadCloser
|
File io.ReadCloser
|
||||||
Size int64
|
Size int64
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PhotoMetadata struct {
|
||||||
|
ContentType string `json:"content_type"`
|
||||||
|
Size int64 `json:"size"`
|
||||||
|
ID FeaturePhotoID `json:"id"`
|
||||||
|
ThumbnailFilename string `json:"thumbnail_filename"`
|
||||||
|
}
|
||||||
|
@ -155,12 +155,40 @@ func (s *Server) handlePOSTHandshake(gc *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) handleGETData(gc *gin.Context) {
|
func (s *Server) handleGETData(gc *gin.Context) {
|
||||||
data, err := s.getData()
|
accept := gc.GetHeader("Accept")
|
||||||
|
if accept == "application/json" {
|
||||||
|
data, err := s.getDataOnly()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
internalError(gc, err)
|
internalError(gc, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
gc.JSON(http.StatusOK, data)
|
gc.JSON(http.StatusOK, data)
|
||||||
|
return
|
||||||
|
} else if accept == "application/zip" {
|
||||||
|
file, err := s.getDataWithPhotos()
|
||||||
|
defer func() {
|
||||||
|
file.Close()
|
||||||
|
os.Remove(file.Name())
|
||||||
|
}()
|
||||||
|
if err != nil {
|
||||||
|
internalError(gc, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fi, err := file.Stat()
|
||||||
|
if err != nil {
|
||||||
|
internalError(gc, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
size := fi.Size()
|
||||||
|
_, err = file.Seek(0, 0)
|
||||||
|
if err != nil {
|
||||||
|
internalError(gc, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
gc.DataFromReader(http.StatusOK, size, "application/zip", file, nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
gc.String(http.StatusNotAcceptable, "%s is not acceptable", accept)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) handlePOSTData(gc *gin.Context) {
|
func (s *Server) handlePOSTData(gc *gin.Context) {
|
||||||
|
@ -20,5 +20,6 @@ CREATE TABLE IF NOT EXISTS feature_photos (
|
|||||||
id integer PRIMARY KEY AUTOINCREMENT,
|
id integer PRIMARY KEY AUTOINCREMENT,
|
||||||
feature_id integer NOT NULL REFERENCES features(id) ON DELETE CASCADE,
|
feature_id integer NOT NULL REFERENCES features(id) ON DELETE CASCADE,
|
||||||
content_type text NOT NULL,
|
content_type text NOT NULL,
|
||||||
|
thumbnail_contents blob NOT NULL,
|
||||||
file_contents blob NOT NULL
|
file_contents blob NOT NULL
|
||||||
);
|
);
|
154
server/server.go
154
server/server.go
@ -1,6 +1,7 @@
|
|||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"archive/zip"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
@ -187,13 +188,7 @@ func (s *Server) handshake(hc models.HandshakeChallenge) (models.UserID, error)
|
|||||||
return models.UserID(userID), nil
|
return models.UserID(userID), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) getData() (models.Data, error) {
|
func (s *Server) getData(conn *sqlite.Conn) (data models.Data, err error) {
|
||||||
conn := s.getDbConn()
|
|
||||||
defer s.dbpool.Put(conn)
|
|
||||||
|
|
||||||
return func() (data models.Data, err error) {
|
|
||||||
defer sqlitex.Save(conn)(&err)
|
|
||||||
|
|
||||||
err = s.deleteExpiredFeatures(conn)
|
err = s.deleteExpiredFeatures(conn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return models.Data{}, fmt.Errorf("failed to delete expired featured: %w", err)
|
return models.Data{}, fmt.Errorf("failed to delete expired featured: %w", err)
|
||||||
@ -211,7 +206,126 @@ func (s *Server) getData() (models.Data, error) {
|
|||||||
Users: people,
|
Users: people,
|
||||||
Features: features,
|
Features: features,
|
||||||
}, nil
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) getDataOnly() (data models.Data, err error) {
|
||||||
|
conn := s.getDbConn()
|
||||||
|
defer s.dbpool.Put(conn)
|
||||||
|
defer sqlitex.Save(conn)(&err)
|
||||||
|
|
||||||
|
data, err = s.getData(conn)
|
||||||
|
if err != nil {
|
||||||
|
return models.Data{}, fmt.Errorf("failed to get json data: %w", err)
|
||||||
|
}
|
||||||
|
return data, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) getDataWithPhotos() (file *os.File, err error) {
|
||||||
|
conn := s.getDbConn()
|
||||||
|
defer s.dbpool.Put(conn)
|
||||||
|
|
||||||
|
defer sqlitex.Save(conn)(&err)
|
||||||
|
|
||||||
|
makePhotoFilename := func(id models.FeaturePhotoID) string {
|
||||||
|
return fmt.Sprintf("img%d", id)
|
||||||
|
}
|
||||||
|
makeThumbnailFilename := func(id models.FeaturePhotoID) string {
|
||||||
|
return fmt.Sprintf("thumb_%s", makePhotoFilename(id))
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := s.getData(conn)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get json data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
data.PhotoMetadata = make(map[string]models.PhotoMetadata, 100)
|
||||||
|
err = sqlitex.Exec(conn, "select id, content_type, length(file_contents) from feature_photos", func(stmt *sqlite.Stmt) error {
|
||||||
|
id := models.FeaturePhotoID(stmt.ColumnInt64(0))
|
||||||
|
contentType := stmt.ColumnText(1)
|
||||||
|
fileSize := stmt.ColumnInt64(2)
|
||||||
|
data.PhotoMetadata[makePhotoFilename(id)] = models.PhotoMetadata{
|
||||||
|
ContentType: contentType,
|
||||||
|
Size: fileSize,
|
||||||
|
ID: id,
|
||||||
|
ThumbnailFilename: makeThumbnailFilename(id),
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to collect photo metadata: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
dataBytes, err := json.MarshalIndent(&data, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to marshal json data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := os.CreateTemp("", "")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create temporary file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
zw := zip.NewWriter(f)
|
||||||
|
defer zw.Close()
|
||||||
|
w, err := zw.Create("data.json")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create data zip entry: %w", err)
|
||||||
|
}
|
||||||
|
_, err = w.Write(dataBytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to write data zip entry: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = sqlitex.Exec(conn, "select id from feature_photos", func(stmt *sqlite.Stmt) error {
|
||||||
|
id := stmt.ColumnInt64(0)
|
||||||
|
|
||||||
|
blob, err := conn.OpenBlob("", "feature_photos", "thumbnail_contents", id, false)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to open photo ID %d thumbnail content blob: %w", id, err)
|
||||||
|
}
|
||||||
|
err = func() error {
|
||||||
|
defer blob.Close()
|
||||||
|
w, err := zw.Create(makeThumbnailFilename(models.FeaturePhotoID(id)))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create zip entry: %w", err)
|
||||||
|
}
|
||||||
|
_, err = io.Copy(w, blob)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to write zip entry: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}()
|
}()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to write photo ID %d thumbnail: %w", id, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
blob, err = conn.OpenBlob("", "feature_photos", "file_contents", id, false)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to open photo ID %d photo content blob: %w", id, err)
|
||||||
|
}
|
||||||
|
err = func() error {
|
||||||
|
defer blob.Close()
|
||||||
|
w, err := zw.Create(makePhotoFilename(models.FeaturePhotoID(id)))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create zip entry: %w", err)
|
||||||
|
}
|
||||||
|
_, err = io.Copy(w, blob)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to write zip entry: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to write photo ID %d photo: %w", id, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to collect photo files: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return f, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) update(data models.Update, photos map[string]models.Photo) error {
|
func (s *Server) update(data models.Update, photos map[string]models.Photo) error {
|
||||||
@ -424,7 +538,7 @@ func (s *Server) addFeatures(conn *sqlite.Conn, features []models.Feature) (map[
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) addPhotos(conn *sqlite.Conn, createdFeatureMapping, addedFeatureMapping map[models.FeatureID][]string, createdIDMapping map[models.FeatureID]models.FeatureID, photos map[string]models.Photo) error {
|
func (s *Server) addPhotos(conn *sqlite.Conn, createdFeatureMapping, addedFeatureMapping map[models.FeatureID][]string, createdIDMapping map[models.FeatureID]models.FeatureID, photos map[string]models.Photo) error {
|
||||||
stmt, err := conn.Prepare("insert into feature_photos(feature_id, content_type, file_contents) values(?, ?, ?)")
|
stmt, err := conn.Prepare("insert into feature_photos(feature_id, content_type, thumbnail_contents, file_contents) values(?, ?, ?, ?)")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to prepare statement: %w", err)
|
return fmt.Errorf("failed to prepare statement: %w", err)
|
||||||
}
|
}
|
||||||
@ -436,6 +550,10 @@ func (s *Server) addPhotos(conn *sqlite.Conn, createdFeatureMapping, addedFeatur
|
|||||||
if !ok {
|
if !ok {
|
||||||
return errs.NewErrPhotoNotProvided(photoName)
|
return errs.NewErrPhotoNotProvided(photoName)
|
||||||
}
|
}
|
||||||
|
thumbnail, ok := photos[fmt.Sprintf("thumb_%s", photoName)]
|
||||||
|
if !ok {
|
||||||
|
return errs.NewErrPhotoThumbnailNotProvided(photoName)
|
||||||
|
}
|
||||||
if !checkImageContentType(photo.ContentType) {
|
if !checkImageContentType(photo.ContentType) {
|
||||||
return errs.NewErrUnsupportedContentType(photoName)
|
return errs.NewErrUnsupportedContentType(photoName)
|
||||||
}
|
}
|
||||||
@ -451,14 +569,30 @@ func (s *Server) addPhotos(conn *sqlite.Conn, createdFeatureMapping, addedFeatur
|
|||||||
|
|
||||||
stmt.BindInt64(1, int64(featureID))
|
stmt.BindInt64(1, int64(featureID))
|
||||||
stmt.BindText(2, photo.ContentType)
|
stmt.BindText(2, photo.ContentType)
|
||||||
stmt.BindZeroBlob(3, photo.Size)
|
stmt.BindZeroBlob(3, thumbnail.Size)
|
||||||
|
stmt.BindZeroBlob(4, photo.Size)
|
||||||
|
|
||||||
_, err = stmt.Step()
|
_, err = stmt.Step()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to evaluate prepared statement: %w", err)
|
return fmt.Errorf("failed to evaluate prepared statement: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
blob, err := conn.OpenBlob("", "feature_photos", "file_contents", conn.LastInsertRowID(), true)
|
blob, err := conn.OpenBlob("", "feature_photos", "thumbnail_contents", conn.LastInsertRowID(), true)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to open thumbnail content blob: %w", err)
|
||||||
|
}
|
||||||
|
err = func() error {
|
||||||
|
defer blob.Close()
|
||||||
|
defer thumbnail.File.Close()
|
||||||
|
|
||||||
|
_, err := io.Copy(blob, thumbnail.File)
|
||||||
|
return err
|
||||||
|
}()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to write to thumbnail content blob: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
blob, err = conn.OpenBlob("", "feature_photos", "file_contents", conn.LastInsertRowID(), true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to open photo content blob: %w", err)
|
return fmt.Errorf("failed to open photo content blob: %w", err)
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user