Newer
Older
// Imported for side effects to register format handlers
_ "image/jpeg"
_ "image/png"
var errNotFound = errors.New("not found")
var UnknownCountry = ISO2CountryCode{'X', 'X'}
func ISO2CountryCodeFromString(s string) (ISO2CountryCode, error) {
if len(s) != 2 {
return UnknownCountry, errors.New("invalid length")
func (i ISO2CountryCode) String() string {
if i[0] == 0 || i[1] == 0 {
return "XX"
}
func (i *ISO2CountryCode) UnmarshalBinary(d []byte) error {
func (i ISO2CountryCode) Value() (driver.Value, error) {
return i.String(), nil
}
func (i *ISO2CountryCode) Scan(data interface{}) error {
var raw string
switch data := data.(type) {
case string:
raw = data
case []byte:
raw = string(data)
default:
return fmt.Errorf("can't convert from %T", data)
}
c, err := ISO2CountryCodeFromString(raw)
if err != nil {
return err
}
*i = c
return nil
}
ID int `db:"id"`
Name string `db:"name"`
Rating int `db:"rating"`
Country ISO2CountryCode `db:"country"`
Picture image.Image `db:"-"`
HasPicture bool `db:"has_picture"` // Set to true if there's picture data for this vino
Comments []Comment `db:"-"`
func DeleteVino(ctx context.Context, db *sqlx.DB, id int) (err error) {
tx, err := db.Begin()
if err != nil {
return err
}
defer func() {
if err != nil {
rerr := tx.Rollback()
if rerr != nil {
Error("can't roll back transaction")
}
}()
_, err = tx.ExecContext(ctx, `DELETE FROM comments WHERE wine = ?1`, id)
if err != nil {
return fmt.Errorf("deleting comments for %d: %w", id, err)
}
_, err = tx.ExecContext(ctx, `DELETE FROM wines WHERE id() = ?1`, id)
if err != nil {
return fmt.Errorf("deleting wine %d: %w", id, err)
}
return nil
}
func (v *Vino) loadComments(ctx context.Context, tx *sqlx.Tx) error {
err := tx.SelectContext(ctx, &v.Comments, `
SELECT id() as id, content
FROM comments
WHERE wine = ?1`, v.ID)
if err != nil {
return err
}
return nil
// loadVino uses the given read-only bolt transaction to load the data for the wine with the given id. When
// there are no wines at all, or there is no wine with the given ID, loadVino returns errNotFound.
func loadVino(ctx context.Context, tx *sqlx.Tx, id int) (Vino, error) {
var v Vino
err := tx.GetContext(ctx, &v, `
SELECT id() as id, name, rating, country, picture IS NOT NULL AS has_picture
FROM wines
WHERE id() = ?1`, id)
if err != nil {
return v, err
}
err = v.loadComments(ctx, tx)
if err != nil {
return v, err
}
return v, nil
}
func LoadVino(ctx context.Context, db *sqlx.DB, id int) (Vino, error) {
tx, err := db.Beginx()
if err != nil {
return Vino{}, err
}
defer tx.Rollback() //nolint:errcheck // No change in tx intended, it's only used for read consistency
if err != nil {
return v, err
}
return v, nil
}
func ListWines(ctx context.Context, db *sqlx.DB) ([]Vino, error) {
tx, err := db.Beginx()
if err != nil {
return nil, err
}
defer tx.Rollback() //nolint:errcheck // No write intended in this tx, it's only for read consistency
err = tx.SelectContext(ctx, &wines, `
SELECT id() as id, name, rating, country, picture IS NOT NULL AS has_picture
FROM wines`)
// Load comments
for _, v := range wines {
err = v.loadComments(ctx, tx)
if err != nil {
return nil, err
}
}
sort.Slice(wines, func(i, j int) bool {
return wines[i].Rating > wines[j].Rating
})
func LoadPictureData(ctx context.Context, db *sqlx.DB, id int) ([]byte, error) {
err := db.GetContext(ctx, &data, `SELECT picture FROM wines WHERE id() = ?1`, id)
if errors.Is(err, sql.ErrNoRows) {
return nil, errNotFound
}
if err != nil {
return nil, err
}
if len(data) == 0 {
log.GetContext(ctx).
Info("zero length image data")
return nil, errNotFound
}
return data, nil
// AddPicture loads picture data (PNG or JPEG) from fh and sets v's picture to it.
// If something goes wrong during loading, or the image is neither PNG nor JPEG, an error
// is returned. If contentType is not the empty string, it is validated to be either
func (v *Vino) AddPicture(fh io.Reader, contentType string) error {
switch contentType {
case "", "image/jpeg", "image/png":
default:
return fmt.Errorf("unexpected content type for image: %q", contentType)
}
img, _, err := image.Decode(fh)
if err != nil {
return err
}
v.Picture = img
return nil
}
func (v Vino) String() string {
return fmt.Sprintf("{Name: %q, Rating: %d}", v.Name, v.Rating)
}
func (v *Vino) Store(ctx context.Context, db *sqlx.DB) (err error) {
// Encode scaled image as PNG, will contain image data if there is any
values := map[string]interface{}{
"name": v.Name,
"rating": v.Rating,
"country": v.Country,
// Scale image down and encode as PNG
// Get aspect ratio of incoming picture
bounds := v.Picture.Bounds()
aspect := float64(bounds.Max.X) / float64(bounds.Max.Y)
const destHeight = 800
rect := image.Rect(0, 0, int(destHeight*aspect), destHeight)
log.GetContext(ctx).
WithFields(logrus.Fields{
"bounds": bounds,
"aspect": aspect,
"rect": rect,
}).
Info("resizing image")
scaled := image.NewRGBA(rect)
draw.ApproxBiLinear.Scale(scaled, rect, v.Picture, v.Picture.Bounds(), draw.Over, nil)
err := png.Encode(&img, scaled)
if err != nil {
return err
}
var (
query string
args []interface{}
)
if v.ID != 0 {
query, args, err = squirrel.Update("wines").
Where(squirrel.Eq{"id()": v.ID}).
PlaceholderFormat(squirrel.Dollar).
SetMap(values).
ToSql()
} else {
query, args, err = squirrel.Insert("wines").
PlaceholderFormat(squirrel.Dollar).
SetMap(values).
ToSql()
}
if err != nil {
return err
}
tx, err := db.Beginx()
if err != nil {
return err
}
rerr := tx.Rollback()
if rerr != nil {
Error("can't roll back transaction")
}
}()
res, err := tx.ExecContext(ctx, query, args...)
if err != nil {
return err
}
if v.ID == 0 {
id, err := res.LastInsertId()
func (v *Vino) StoreComment(ctx context.Context, db *sqlx.DB, text string) (err error) {
tx, err := db.Begin()
if err != nil {
return err
}
_, err = tx.ExecContext(ctx, `INSERT INTO comments (wine, content) VALUES (?1, ?2)`, v.ID, text)
if err != nil {
rerr := tx.Rollback()
if rerr != nil {
Error("can't roll back transaction")
}