pronounsfu/backend/db/avatars.go

176 lines
5.0 KiB
Go

package db
import (
"bytes"
"context"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"image"
_ "image/gif"
"image/jpeg"
_ "image/png"
"io"
"strings"
"emperror.dev/errors"
"github.com/disintegration/imaging"
"github.com/minio/minio-go/v7"
"github.com/rs/xid"
"github.com/chai2010/webp"
)
var (
webpArgs = []string{"-thumbnail", "512x512^", "-gravity", "center", "-background", "none", "-extent", "512x512", "-quality", "90", "webp:-"}
jpgArgs = []string{"-thumbnail", "512x512^", "-gravity", "center", "-extent", "512x512", "-quality", "80", "jpg:-"}
)
const ErrInvalidDataURI = errors.Sentinel("invalid data URI")
const ErrInvalidContentType = errors.Sentinel("invalid avatar content type")
// ConvertAvatar parses an avatar from a data URI, converts it to WebP and JPEG, and returns the results.
func (db *DB) ConvertAvatar(data string) (
webpOut *bytes.Buffer,
jpgOut *bytes.Buffer,
err error,
) {
data = strings.TrimSpace(data)
if !strings.Contains(data, ",") || !strings.Contains(data, ":") || !strings.Contains(data, ";") {
return nil, nil, ErrInvalidDataURI
}
split := strings.Split(data, ",")
rawData, err := base64.StdEncoding.DecodeString(split[1])
if err != nil {
return nil, nil, errors.Wrap(err, "invalid base64 data")
}
img, _, err := image.Decode(bytes.NewReader(rawData))
if err != nil {
return nil, nil, errors.Wrap(err, "decodign image")
}
resized := imaging.Fill(img, 512, 512, imaging.Center, imaging.Linear)
webpOut = new(bytes.Buffer)
err = webp.Encode(webpOut, resized, &webp.Options{
Quality: 90,
})
if err != nil {
return nil, nil, errors.Wrap(err, "encoding WebP image")
}
jpgOut = new(bytes.Buffer)
err = jpeg.Encode(jpgOut, resized, &jpeg.Options{
Quality: 80,
})
if err != nil {
return nil, nil, errors.Wrap(err, "encoding JPEG image")
}
return webpOut, jpgOut, nil
}
func (db *DB) WriteUserAvatar(ctx context.Context,
userID xid.ID, webp *bytes.Buffer, jpeg *bytes.Buffer,
) (
hash string, err error,
) {
hasher := sha256.New()
_, err = hasher.Write(webp.Bytes())
if err != nil {
return "", errors.Wrap(err, "hashing webp avatar")
}
hash = hex.EncodeToString(hasher.Sum(nil))
_, err = db.minio.PutObject(ctx, db.minioBucket, "/users/"+userID.String()+"/"+hash+".webp", webp, -1, minio.PutObjectOptions{
ContentType: "image/webp",
})
if err != nil {
return "", errors.Wrap(err, "uploading webp avatar")
}
_, err = db.minio.PutObject(ctx, db.minioBucket, "/users/"+userID.String()+"/"+hash+".jpg", jpeg, -1, minio.PutObjectOptions{
ContentType: "image/jpeg",
})
if err != nil {
return "", errors.Wrap(err, "uploading jpeg avatar")
}
return hash, nil
}
func (db *DB) WriteMemberAvatar(ctx context.Context,
memberID xid.ID, webp *bytes.Buffer, jpeg *bytes.Buffer,
) (
hash string, err error,
) {
hasher := sha256.New()
_, err = hasher.Write(webp.Bytes())
if err != nil {
return "", errors.Wrap(err, "hashing webp avatar")
}
hash = hex.EncodeToString(hasher.Sum(nil))
_, err = db.minio.PutObject(ctx, db.minioBucket, "/members/"+memberID.String()+"/"+hash+".webp", webp, -1, minio.PutObjectOptions{
ContentType: "image/webp",
})
if err != nil {
return "", errors.Wrap(err, "uploading webp avatar")
}
_, err = db.minio.PutObject(ctx, db.minioBucket, "/members/"+memberID.String()+"/"+hash+".jpg", jpeg, -1, minio.PutObjectOptions{
ContentType: "image/jpeg",
})
if err != nil {
return "", errors.Wrap(err, "uploading jpeg avatar")
}
return hash, nil
}
func (db *DB) DeleteUserAvatar(ctx context.Context, userID xid.ID, hash string) error {
err := db.minio.RemoveObject(ctx, db.minioBucket, "/users/"+userID.String()+"/"+hash+".webp", minio.RemoveObjectOptions{})
if err != nil {
return errors.Wrap(err, "deleting webp avatar")
}
err = db.minio.RemoveObject(ctx, db.minioBucket, "/users/"+userID.String()+"/"+hash+".jpg", minio.RemoveObjectOptions{})
if err != nil {
return errors.Wrap(err, "deleting jpeg avatar")
}
return nil
}
func (db *DB) DeleteMemberAvatar(ctx context.Context, memberID xid.ID, hash string) error {
err := db.minio.RemoveObject(ctx, db.minioBucket, "/members/"+memberID.String()+"/"+hash+".webp", minio.RemoveObjectOptions{})
if err != nil {
return errors.Wrap(err, "deleting webp avatar")
}
err = db.minio.RemoveObject(ctx, db.minioBucket, "/members/"+memberID.String()+"/"+hash+".jpg", minio.RemoveObjectOptions{})
if err != nil {
return errors.Wrap(err, "deleting jpeg avatar")
}
return nil
}
func (db *DB) UserAvatar(ctx context.Context, userID xid.ID, hash string) (io.ReadCloser, error) {
obj, err := db.minio.GetObject(ctx, db.minioBucket, "/users/"+userID.String()+"/"+hash+".webp", minio.GetObjectOptions{})
if err != nil {
return nil, errors.Wrap(err, "getting object")
}
return obj, nil
}
func (db *DB) MemberAvatar(ctx context.Context, memberID xid.ID, hash string) (io.ReadCloser, error) {
obj, err := db.minio.GetObject(ctx, db.minioBucket, "/members/"+memberID.String()+"/"+hash+".webp", minio.GetObjectOptions{})
if err != nil {
return nil, errors.Wrap(err, "getting object")
}
return obj, nil
}