start refactor of media package

This commit is contained in:
tsmethurst 2021-12-27 18:03:36 +01:00
parent 14c472c1ab
commit 6803c1682b
6 changed files with 183 additions and 195 deletions

View file

@ -35,45 +35,16 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/uris" "github.com/superseriousbusiness/gotosocial/internal/uris"
) )
// EmojiMaxBytes is the maximum permitted bytes of an emoji upload (50kb)
const EmojiMaxBytes = 51200
type Size string
const ( type ProcessedCallback func(*gtsmodel.MediaAttachment) error
SizeSmall Size = "small" // SizeSmall is the key for small/thumbnail versions of media
SizeOriginal Size = "original" // SizeOriginal is the key for original/fullsize versions of media and emoji
SizeStatic Size = "static" // SizeStatic is the key for static (non-animated) versions of emoji
)
type Type string
const (
TypeAttachment Type = "attachment" // TypeAttachment is the key for media attachments
TypeHeader Type = "header" // TypeHeader is the key for profile header requests
TypeAvatar Type = "avatar" // TypeAvatar is the key for profile avatar requests
TypeEmoji Type = "emoji" // TypeEmoji is the key for emoji type requests
)
// Handler provides an interface for parsing, storing, and retrieving media objects like photos, videos, and gifs. // Handler provides an interface for parsing, storing, and retrieving media objects like photos, videos, and gifs.
type Handler interface { type Handler interface {
// ProcessHeaderOrAvatar takes a new header image for an account, checks it out, removes exif data from it, ProcessHeader(ctx context.Context, data []byte, accountID string, cb ProcessedCallback) (*gtsmodel.MediaAttachment, error)
// puts it in whatever storage backend we're using, sets the relevant fields in the database for the new image, ProcessAvatar(ctx context.Context, data []byte, accountID string, cb ProcessedCallback) (*gtsmodel.MediaAttachment, error)
// and then returns information to the caller about the new header. ProcessAttachment(ctx context.Context, data []byte, accountID string, cb ProcessedCallback) (*gtsmodel.MediaAttachment, error)
ProcessHeaderOrAvatar(ctx context.Context, attachment []byte, accountID string, mediaType Type, remoteURL string) (*gtsmodel.MediaAttachment, error) ProcessEmoji(ctx context.Context, data []byte, shortcode string) (*gtsmodel.Emoji, error)
// ProcessLocalAttachment takes a new attachment and the requesting account, checks it out, removes exif data from it,
// puts it in whatever storage backend we're using, sets the relevant fields in the database for the new media,
// and then returns information to the caller about the attachment. It's the caller's responsibility to put the returned struct
// in the database.
ProcessAttachment(ctx context.Context, attachmentBytes []byte, minAttachment *gtsmodel.MediaAttachment) (*gtsmodel.MediaAttachment, error)
// ProcessLocalEmoji takes a new emoji and a shortcode, cleans it up, puts it in storage, and creates a new
// *gts.Emoji for it, then returns it to the caller. It's the caller's responsibility to put the returned struct
// in the database.
ProcessLocalEmoji(ctx context.Context, emojiBytes []byte, shortcode string) (*gtsmodel.Emoji, error)
ProcessRemoteHeaderOrAvatar(ctx context.Context, t transport.Transport, currentAttachment *gtsmodel.MediaAttachment, accountID string) (*gtsmodel.MediaAttachment, error)
} }
type mediaHandler struct { type mediaHandler struct {
@ -108,7 +79,7 @@ func (mh *mediaHandler) ProcessHeaderOrAvatar(ctx context.Context, attachment []
if err != nil { if err != nil {
return nil, err return nil, err
} }
if !SupportedImageType(contentType) { if !supportedImage(contentType) {
return nil, fmt.Errorf("%s is not an accepted image type", contentType) return nil, fmt.Errorf("%s is not an accepted image type", contentType)
} }
@ -152,8 +123,8 @@ func (mh *mediaHandler) ProcessAttachment(ctx context.Context, attachmentBytes [
// return nil, errors.New("video was of size 0") // return nil, errors.New("video was of size 0")
// } // }
// return mh.processVideoAttachment(attachment, accountID, contentType, remoteURL) // return mh.processVideoAttachment(attachment, accountID, contentType, remoteURL)
case MIMEImage: case mimeImage:
if !SupportedImageType(contentType) { if !supportedImage(contentType) {
return nil, fmt.Errorf("image type %s not supported", contentType) return nil, fmt.Errorf("image type %s not supported", contentType)
} }
if len(attachmentBytes) == 0 { if len(attachmentBytes) == 0 {
@ -180,7 +151,7 @@ func (mh *mediaHandler) ProcessLocalEmoji(ctx context.Context, emojiBytes []byte
if err != nil { if err != nil {
return nil, err return nil, err
} }
if !supportedEmojiType(contentType) { if !supportedEmoji(contentType) {
return nil, fmt.Errorf("content type %s not supported for emojis", contentType) return nil, fmt.Errorf("content type %s not supported for emojis", contentType)
} }
@ -193,11 +164,11 @@ func (mh *mediaHandler) ProcessLocalEmoji(ctx context.Context, emojiBytes []byte
// clean any exif data from png but leave gifs alone // clean any exif data from png but leave gifs alone
switch contentType { switch contentType {
case MIMEPng: case mimePng:
if clean, err = purgeExif(emojiBytes); err != nil { if clean, err = purgeExif(emojiBytes); err != nil {
return nil, fmt.Errorf("error cleaning exif data: %s", err) return nil, fmt.Errorf("error cleaning exif data: %s", err)
} }
case MIMEGif: case mimeGif:
clean = emojiBytes clean = emojiBytes
default: default:
return nil, errors.New("media type unrecognized") return nil, errors.New("media type unrecognized")
@ -266,7 +237,7 @@ func (mh *mediaHandler) ProcessLocalEmoji(ctx context.Context, emojiBytes []byte
ImagePath: emojiPath, ImagePath: emojiPath,
ImageStaticPath: emojiStaticPath, ImageStaticPath: emojiStaticPath,
ImageContentType: contentType, ImageContentType: contentType,
ImageStaticContentType: MIMEPng, // static version will always be a png ImageStaticContentType: mimePng, // static version will always be a png
ImageFileSize: len(original.image), ImageFileSize: len(original.image),
ImageStaticFileSize: len(static.image), ImageStaticFileSize: len(static.image),
ImageUpdatedAt: time.Now(), ImageUpdatedAt: time.Now(),

View file

@ -29,7 +29,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/uris" "github.com/superseriousbusiness/gotosocial/internal/uris"
) )
func (mh *mediaHandler) processImageAttachment(data []byte, minAttachment *gtsmodel.MediaAttachment) (*gtsmodel.MediaAttachment, error) { func (mh *mediaHandler) processImage(data []byte, minAttachment *gtsmodel.MediaAttachment) (*gtsmodel.MediaAttachment, error) {
var clean []byte var clean []byte
var err error var err error
var original *imageAndMeta var original *imageAndMeta
@ -38,7 +38,7 @@ func (mh *mediaHandler) processImageAttachment(data []byte, minAttachment *gtsmo
contentType := minAttachment.File.ContentType contentType := minAttachment.File.ContentType
switch contentType { switch contentType {
case MIMEJpeg, MIMEPng: case mimeJpeg, mimePng:
if clean, err = purgeExif(data); err != nil { if clean, err = purgeExif(data); err != nil {
return nil, fmt.Errorf("error cleaning exif data: %s", err) return nil, fmt.Errorf("error cleaning exif data: %s", err)
} }
@ -46,7 +46,7 @@ func (mh *mediaHandler) processImageAttachment(data []byte, minAttachment *gtsmo
if err != nil { if err != nil {
return nil, fmt.Errorf("error parsing image: %s", err) return nil, fmt.Errorf("error parsing image: %s", err)
} }
case MIMEGif: case mimeGif:
clean = data clean = data
original, err = deriveGif(clean, contentType) original, err = deriveGif(clean, contentType)
if err != nil { if err != nil {
@ -119,7 +119,7 @@ func (mh *mediaHandler) processImageAttachment(data []byte, minAttachment *gtsmo
}, },
Thumbnail: gtsmodel.Thumbnail{ Thumbnail: gtsmodel.Thumbnail{
Path: smallPath, Path: smallPath,
ContentType: MIMEJpeg, // all thumbnails/smalls are encoded as jpeg ContentType: mimeJpeg, // all thumbnails/smalls are encoded as jpeg
FileSize: len(small.image), FileSize: len(small.image),
UpdatedAt: time.Now(), UpdatedAt: time.Now(),
URL: smallURL, URL: smallURL,

View file

@ -47,17 +47,17 @@ func (mh *mediaHandler) processHeaderOrAvi(imageBytes []byte, contentType string
var original *imageAndMeta var original *imageAndMeta
switch contentType { switch contentType {
case MIMEJpeg: case mimeJpeg:
if clean, err = purgeExif(imageBytes); err != nil { if clean, err = purgeExif(imageBytes); err != nil {
return nil, fmt.Errorf("error cleaning exif data: %s", err) return nil, fmt.Errorf("error cleaning exif data: %s", err)
} }
original, err = deriveImage(clean, contentType) original, err = deriveImage(clean, contentType)
case MIMEPng: case mimePng:
if clean, err = purgeExif(imageBytes); err != nil { if clean, err = purgeExif(imageBytes); err != nil {
return nil, fmt.Errorf("error cleaning exif data: %s", err) return nil, fmt.Errorf("error cleaning exif data: %s", err)
} }
original, err = deriveImage(clean, contentType) original, err = deriveImage(clean, contentType)
case MIMEGif: case mimeGif:
clean = imageBytes clean = imageBytes
original, err = deriveGif(clean, contentType) original, err = deriveGif(clean, contentType)
default: default:

View file

@ -1,21 +1,3 @@
/*
GoToSocial
Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package media package media
import ( import (
@ -28,112 +10,26 @@ import (
"image/png" "image/png"
"github.com/buckket/go-blurhash" "github.com/buckket/go-blurhash"
"github.com/h2non/filetype"
"github.com/nfnt/resize" "github.com/nfnt/resize"
"github.com/superseriousbusiness/exifremove/pkg/exifremove" "github.com/superseriousbusiness/exifremove/pkg/exifremove"
) )
const (
// MIMEImage is the mime type for image
MIMEImage = "image"
// MIMEJpeg is the jpeg image mime type
MIMEJpeg = "image/jpeg"
// MIMEGif is the gif image mime type
MIMEGif = "image/gif"
// MIMEPng is the png image mime type
MIMEPng = "image/png"
// MIMEVideo is the mime type for video
MIMEVideo = "video"
// MIMEMp4 is the mp4 video mime type
MIMEMp4 = "video/mp4"
// MIMEMpeg is the mpeg video mime type
MIMEMpeg = "video/mpeg"
// MIMEWebm is the webm video mime type
MIMEWebm = "video/webm"
)
// parseContentType parses the MIME content type from a file, returning it as a string in the form (eg., "image/jpeg").
// Returns an error if the content type is not something we can process.
func parseContentType(content []byte) (string, error) {
head := make([]byte, 261)
_, err := bytes.NewReader(content).Read(head)
if err != nil {
return "", fmt.Errorf("could not read first magic bytes of file: %s", err)
}
kind, err := filetype.Match(head)
if err != nil {
return "", err
}
if kind == filetype.Unknown {
return "", errors.New("filetype unknown")
}
return kind.MIME.Value, nil
}
// SupportedImageType checks mime type of an image against a slice of accepted types,
// and returns True if the mime type is accepted.
func SupportedImageType(mimeType string) bool {
acceptedImageTypes := []string{
MIMEJpeg,
MIMEGif,
MIMEPng,
}
for _, accepted := range acceptedImageTypes {
if mimeType == accepted {
return true
}
}
return false
}
// SupportedVideoType checks mime type of a video against a slice of accepted types,
// and returns True if the mime type is accepted.
func SupportedVideoType(mimeType string) bool {
acceptedVideoTypes := []string{
MIMEMp4,
MIMEMpeg,
MIMEWebm,
}
for _, accepted := range acceptedVideoTypes {
if mimeType == accepted {
return true
}
}
return false
}
// supportedEmojiType checks that the content type is image/png -- the only type supported for emoji.
func supportedEmojiType(mimeType string) bool {
acceptedEmojiTypes := []string{
MIMEGif,
MIMEPng,
}
for _, accepted := range acceptedEmojiTypes {
if mimeType == accepted {
return true
}
}
return false
}
// purgeExif is a little wrapper for the action of removing exif data from an image. // purgeExif is a little wrapper for the action of removing exif data from an image.
// Only pass pngs or jpegs to this function. // Only pass pngs or jpegs to this function.
func purgeExif(b []byte) ([]byte, error) { func purgeExif(data []byte) ([]byte, error) {
if len(b) == 0 { if len(data) == 0 {
return nil, errors.New("passed image was not valid") return nil, errors.New("passed image was not valid")
} }
clean, err := exifremove.Remove(b) clean, err := exifremove.Remove(data)
if err != nil { if err != nil {
return nil, fmt.Errorf("could not purge exif from image: %s", err) return nil, fmt.Errorf("could not purge exif from image: %s", err)
} }
if len(clean) == 0 { if len(clean) == 0 {
return nil, errors.New("purged image was not valid") return nil, errors.New("purged image was not valid")
} }
return clean, nil return clean, nil
} }
@ -141,7 +37,7 @@ func deriveGif(b []byte, extension string) (*imageAndMeta, error) {
var g *gif.GIF var g *gif.GIF
var err error var err error
switch extension { switch extension {
case MIMEGif: case mimeGif:
g, err = gif.DecodeAll(bytes.NewReader(b)) g, err = gif.DecodeAll(bytes.NewReader(b))
if err != nil { if err != nil {
return nil, err return nil, err
@ -170,12 +66,12 @@ func deriveImage(b []byte, contentType string) (*imageAndMeta, error) {
var err error var err error
switch contentType { switch contentType {
case MIMEJpeg: case mimeImageJpeg:
i, err = jpeg.Decode(bytes.NewReader(b)) i, err = jpeg.Decode(bytes.NewReader(b))
if err != nil { if err != nil {
return nil, err return nil, err
} }
case MIMEPng: case mimeImagePng:
i, err = png.Decode(bytes.NewReader(b)) i, err = png.Decode(bytes.NewReader(b))
if err != nil { if err != nil {
return nil, err return nil, err
@ -208,17 +104,17 @@ func deriveThumbnail(b []byte, contentType string, x uint, y uint) (*imageAndMet
var err error var err error
switch contentType { switch contentType {
case MIMEJpeg: case mimeImageJpeg:
i, err = jpeg.Decode(bytes.NewReader(b)) i, err = jpeg.Decode(bytes.NewReader(b))
if err != nil { if err != nil {
return nil, err return nil, err
} }
case MIMEPng: case mimeImagePng:
i, err = png.Decode(bytes.NewReader(b)) i, err = png.Decode(bytes.NewReader(b))
if err != nil { if err != nil {
return nil, err return nil, err
} }
case MIMEGif: case mimeImageGif:
i, err = gif.Decode(bytes.NewReader(b)) i, err = gif.Decode(bytes.NewReader(b))
if err != nil { if err != nil {
return nil, err return nil, err
@ -261,12 +157,12 @@ func deriveStaticEmoji(b []byte, contentType string) (*imageAndMeta, error) {
var err error var err error
switch contentType { switch contentType {
case MIMEPng: case mimeImagePng:
i, err = png.Decode(bytes.NewReader(b)) i, err = png.Decode(bytes.NewReader(b))
if err != nil { if err != nil {
return nil, err return nil, err
} }
case MIMEGif: case mimeImageGif:
i, err = gif.Decode(bytes.NewReader(b)) i, err = gif.Decode(bytes.NewReader(b))
if err != nil { if err != nil {
return nil, err return nil, err
@ -292,31 +188,3 @@ type imageAndMeta struct {
aspect float64 aspect float64
blurhash string blurhash string
} }
// ParseMediaType converts s to a recognized MediaType, or returns an error if unrecognized
func ParseMediaType(s string) (Type, error) {
switch s {
case string(TypeAttachment):
return TypeAttachment, nil
case string(TypeHeader):
return TypeHeader, nil
case string(TypeAvatar):
return TypeAvatar, nil
case string(TypeEmoji):
return TypeEmoji, nil
}
return "", fmt.Errorf("%s not a recognized MediaType", s)
}
// ParseMediaSize converts s to a recognized MediaSize, or returns an error if unrecognized
func ParseMediaSize(s string) (Size, error) {
switch s {
case string(SizeSmall):
return SizeSmall, nil
case string(SizeOriginal):
return SizeOriginal, nil
case string(SizeStatic):
return SizeStatic, nil
}
return "", fmt.Errorf("%s not a recognized MediaSize", s)
}

149
internal/media/types.go Normal file
View file

@ -0,0 +1,149 @@
/*
GoToSocial
Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package media
import (
"bytes"
"errors"
"fmt"
"github.com/h2non/filetype"
)
// mime consts
const (
mimeImage = "image"
mimeJpeg = "jpeg"
mimeImageJpeg = mimeImage + "/" + mimeJpeg
mimeGif = "gif"
mimeImageGif = mimeImage + "/" + mimeGif
mimePng = "png"
mimeImagePng = mimeImage + "/" + mimePng
)
// EmojiMaxBytes is the maximum permitted bytes of an emoji upload (50kb)
// const EmojiMaxBytes = 51200
// maxFileHeaderBytes represents the maximum amount of bytes we want
// to examine from the beginning of a file to determine its type.
//
// See: https://en.wikipedia.org/wiki/File_format#File_header
// and https://github.com/h2non/filetype
const maxFileHeaderBytes = 262
type Size string
const (
SizeSmall Size = "small" // SizeSmall is the key for small/thumbnail versions of media
SizeOriginal Size = "original" // SizeOriginal is the key for original/fullsize versions of media and emoji
SizeStatic Size = "static" // SizeStatic is the key for static (non-animated) versions of emoji
)
type Type string
const (
TypeAttachment Type = "attachment" // TypeAttachment is the key for media attachments
TypeHeader Type = "header" // TypeHeader is the key for profile header requests
TypeAvatar Type = "avatar" // TypeAvatar is the key for profile avatar requests
TypeEmoji Type = "emoji" // TypeEmoji is the key for emoji type requests
)
// parseContentType parses the MIME content type from a file, returning it as a string in the form (eg., "image/jpeg").
// Returns an error if the content type is not something we can process.
func parseContentType(content []byte) (string, error) {
// read in the first bytes of the file
fileHeader := make([]byte, maxFileHeaderBytes)
if _, err := bytes.NewReader(content).Read(fileHeader); err != nil {
return "", fmt.Errorf("could not read first magic bytes of file: %s", err)
}
kind, err := filetype.Match(fileHeader)
if err != nil {
return "", err
}
if kind == filetype.Unknown {
return "", errors.New("filetype unknown")
}
return kind.MIME.Value, nil
}
// supportedImage checks mime type of an image against a slice of accepted types,
// and returns True if the mime type is accepted.
func supportedImage(mimeType string) bool {
acceptedImageTypes := []string{
mimeImageJpeg,
mimeImageGif,
mimeImagePng,
}
for _, accepted := range acceptedImageTypes {
if mimeType == accepted {
return true
}
}
return false
}
// supportedEmoji checks that the content type is image/png -- the only type supported for emoji.
func supportedEmoji(mimeType string) bool {
acceptedEmojiTypes := []string{
mimeImageGif,
mimeImagePng,
}
for _, accepted := range acceptedEmojiTypes {
if mimeType == accepted {
return true
}
}
return false
}
// ParseMediaType converts s to a recognized MediaType, or returns an error if unrecognized
func ParseMediaType(s string) (Type, error) {
switch s {
case string(TypeAttachment):
return TypeAttachment, nil
case string(TypeHeader):
return TypeHeader, nil
case string(TypeAvatar):
return TypeAvatar, nil
case string(TypeEmoji):
return TypeEmoji, nil
}
return "", fmt.Errorf("%s not a recognized MediaType", s)
}
// ParseMediaSize converts s to a recognized MediaSize, or returns an error if unrecognized
func ParseMediaSize(s string) (Size, error) {
switch s {
case string(SizeSmall):
return SizeSmall, nil
case string(SizeOriginal):
return SizeOriginal, nil
case string(SizeStatic):
return SizeStatic, nil
}
return "", fmt.Errorf("%s not a recognized MediaSize", s)
}

View file

@ -138,10 +138,10 @@ func (suite *MediaUtilTestSuite) TestDeriveThumbnailFromJPEG() {
} }
func (suite *MediaUtilTestSuite) TestSupportedImageTypes() { func (suite *MediaUtilTestSuite) TestSupportedImageTypes() {
ok := SupportedImageType("image/jpeg") ok := supportedImage("image/jpeg")
suite.True(ok) suite.True(ok)
ok = SupportedImageType("image/bmp") ok = supportedImage("image/bmp")
suite.False(ok) suite.False(ok)
} }