package db import ( "bytes" "context" "crypto/sha256" "encoding/base64" "encoding/hex" "io" "os/exec" "strings" "emperror.dev/errors" "github.com/minio/minio-go/v7" "github.com/rs/xid" ) 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) ( webp *bytes.Buffer, jpg *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, ",") rest, b64 := split[0], split[1] rest = strings.Split(rest, ":")[1] contentType := strings.Split(rest, ";")[0] var contentArg []string switch contentType { case "image/png": contentArg = []string{"png:-"} case "image/jpeg": contentArg = []string{"jpg:-"} case "image/gif": contentArg = []string{"gif:-"} case "image/webp": contentArg = []string{"webp:-"} default: return nil, nil, ErrInvalidContentType } rawData, err := base64.StdEncoding.DecodeString(b64) if err != nil { return nil, nil, errors.Wrap(err, "invalid base64 data") } // create webp convert command and get its pipes webpConvert := exec.Command("convert", append(contentArg, webpArgs...)...) stdIn, err := webpConvert.StdinPipe() if err != nil { return nil, nil, errors.Wrap(err, "getting webp stdin") } stdOut, err := webpConvert.StdoutPipe() if err != nil { return nil, nil, errors.Wrap(err, "getting webp stdout") } // start webp command err = webpConvert.Start() if err != nil { return nil, nil, errors.Wrap(err, "starting webp command") } // write data _, err = stdIn.Write(rawData) if err != nil { return nil, nil, errors.Wrap(err, "writing webp data") } err = stdIn.Close() if err != nil { return nil, nil, errors.Wrap(err, "closing webp stdin") } // read webp output webpBuffer := new(bytes.Buffer) _, err = io.Copy(webpBuffer, stdOut) if err != nil { return nil, nil, errors.Wrap(err, "reading webp data") } webp = webpBuffer // finish webp command err = webpConvert.Wait() if err != nil { return nil, nil, errors.Wrap(err, "running webp command") } // create jpg convert command and get its pipes jpgConvert := exec.Command("convert", append(contentArg, jpgArgs...)...) stdIn, err = jpgConvert.StdinPipe() if err != nil { return nil, nil, errors.Wrap(err, "getting jpg stdin") } stdOut, err = jpgConvert.StdoutPipe() if err != nil { return nil, nil, errors.Wrap(err, "getting jpg stdout") } // start jpg command err = jpgConvert.Start() if err != nil { return nil, nil, errors.Wrap(err, "starting jpg command") } // write data _, err = stdIn.Write(rawData) if err != nil { return nil, nil, errors.Wrap(err, "writing jpg data") } err = stdIn.Close() if err != nil { return nil, nil, errors.Wrap(err, "closing jpg stdin") } // read jpg output jpgBuffer := new(bytes.Buffer) _, err = io.Copy(jpgBuffer, stdOut) if err != nil { return nil, nil, errors.Wrap(err, "reading jpg data") } jpg = jpgBuffer // finish jpg command err = jpgConvert.Wait() if err != nil { return nil, nil, errors.Wrap(err, "running jpg command") } return webp, jpg, 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 }