230 lines
		
	
	
	
		
			6 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			230 lines
		
	
	
	
		
			6 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| 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{"-resize", "512x512", "-quality", "80", "webp:-"}
 | |
| 	jpgArgs  = []string{"-resize", "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+".webp", minio.RemoveObjectOptions{})
 | |
| 	if err != nil {
 | |
| 		return errors.Wrap(err, "deleting jpeg avatar")
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 |