diff options
Diffstat (limited to 'internal/media')
| -rw-r--r-- | internal/media/handler.go | 53 | ||||
| -rw-r--r-- | internal/media/image.go (renamed from internal/media/processimage.go) | 8 | ||||
| -rw-r--r-- | internal/media/processicon.go | 6 | ||||
| -rw-r--r-- | internal/media/processing.go (renamed from internal/media/util.go) | 158 | ||||
| -rw-r--r-- | internal/media/types.go | 149 | ||||
| -rw-r--r-- | internal/media/util_test.go | 4 | 
6 files changed, 183 insertions, 195 deletions
diff --git a/internal/media/handler.go b/internal/media/handler.go index e6c7369b6..b64e583b3 100644 --- a/internal/media/handler.go +++ b/internal/media/handler.go @@ -35,45 +35,16 @@ import (  	"github.com/superseriousbusiness/gotosocial/internal/uris"  ) -// EmojiMaxBytes is the maximum permitted bytes of an emoji upload (50kb) -const EmojiMaxBytes = 51200 -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 -) +type ProcessedCallback func(*gtsmodel.MediaAttachment) error  // Handler provides an interface for parsing, storing, and retrieving media objects like photos, videos, and gifs.  type Handler interface { -	// ProcessHeaderOrAvatar takes a new header image for an 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 image, -	// and then returns information to the caller about the new header. -	ProcessHeaderOrAvatar(ctx context.Context, attachment []byte, accountID string, mediaType Type, remoteURL string) (*gtsmodel.MediaAttachment, 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) +	ProcessHeader(ctx context.Context, data []byte, accountID string, cb ProcessedCallback) (*gtsmodel.MediaAttachment, error) +	ProcessAvatar(ctx context.Context, data []byte, accountID string, cb ProcessedCallback) (*gtsmodel.MediaAttachment, error) +	ProcessAttachment(ctx context.Context, data []byte, accountID string, cb ProcessedCallback) (*gtsmodel.MediaAttachment, error) +	ProcessEmoji(ctx context.Context, data []byte, shortcode string) (*gtsmodel.Emoji, error)  }  type mediaHandler struct { @@ -108,7 +79,7 @@ func (mh *mediaHandler) ProcessHeaderOrAvatar(ctx context.Context, attachment []  	if err != nil {  		return nil, err  	} -	if !SupportedImageType(contentType) { +	if !supportedImage(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 mh.processVideoAttachment(attachment, accountID, contentType, remoteURL) -	case MIMEImage: -		if !SupportedImageType(contentType) { +	case mimeImage: +		if !supportedImage(contentType) {  			return nil, fmt.Errorf("image type %s not supported", contentType)  		}  		if len(attachmentBytes) == 0 { @@ -180,7 +151,7 @@ func (mh *mediaHandler) ProcessLocalEmoji(ctx context.Context, emojiBytes []byte  	if err != nil {  		return nil, err  	} -	if !supportedEmojiType(contentType) { +	if !supportedEmoji(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  	switch contentType { -	case MIMEPng: +	case mimePng:  		if clean, err = purgeExif(emojiBytes); err != nil {  			return nil, fmt.Errorf("error cleaning exif data: %s", err)  		} -	case MIMEGif: +	case mimeGif:  		clean = emojiBytes  	default:  		return nil, errors.New("media type unrecognized") @@ -266,7 +237,7 @@ func (mh *mediaHandler) ProcessLocalEmoji(ctx context.Context, emojiBytes []byte  		ImagePath:              emojiPath,  		ImageStaticPath:        emojiStaticPath,  		ImageContentType:       contentType, -		ImageStaticContentType: MIMEPng, // static version will always be a png +		ImageStaticContentType: mimePng, // static version will always be a png  		ImageFileSize:          len(original.image),  		ImageStaticFileSize:    len(static.image),  		ImageUpdatedAt:         time.Now(), diff --git a/internal/media/processimage.go b/internal/media/image.go index ca92c0660..f1cc03bb6 100644 --- a/internal/media/processimage.go +++ b/internal/media/image.go @@ -29,7 +29,7 @@ import (  	"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 err error  	var original *imageAndMeta @@ -38,7 +38,7 @@ func (mh *mediaHandler) processImageAttachment(data []byte, minAttachment *gtsmo  	contentType := minAttachment.File.ContentType  	switch contentType { -	case MIMEJpeg, MIMEPng: +	case mimeJpeg, mimePng:  		if clean, err = purgeExif(data); err != nil {  			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 {  			return nil, fmt.Errorf("error parsing image: %s", err)  		} -	case MIMEGif: +	case mimeGif:  		clean = data  		original, err = deriveGif(clean, contentType)  		if err != nil { @@ -119,7 +119,7 @@ func (mh *mediaHandler) processImageAttachment(data []byte, minAttachment *gtsmo  		},  		Thumbnail: gtsmodel.Thumbnail{  			Path:        smallPath, -			ContentType: MIMEJpeg, // all thumbnails/smalls are encoded as jpeg +			ContentType: mimeJpeg, // all thumbnails/smalls are encoded as jpeg  			FileSize:    len(small.image),  			UpdatedAt:   time.Now(),  			URL:         smallURL, diff --git a/internal/media/processicon.go b/internal/media/processicon.go index 66cf1f999..faeae0ee6 100644 --- a/internal/media/processicon.go +++ b/internal/media/processicon.go @@ -47,17 +47,17 @@ func (mh *mediaHandler) processHeaderOrAvi(imageBytes []byte, contentType string  	var original *imageAndMeta  	switch contentType { -	case MIMEJpeg: +	case mimeJpeg:  		if clean, err = purgeExif(imageBytes); err != nil {  			return nil, fmt.Errorf("error cleaning exif data: %s", err)  		}  		original, err = deriveImage(clean, contentType) -	case MIMEPng: +	case mimePng:  		if clean, err = purgeExif(imageBytes); err != nil {  			return nil, fmt.Errorf("error cleaning exif data: %s", err)  		}  		original, err = deriveImage(clean, contentType) -	case MIMEGif: +	case mimeGif:  		clean = imageBytes  		original, err = deriveGif(clean, contentType)  	default: diff --git a/internal/media/util.go b/internal/media/processing.go index 348136c92..ccd9ebfdb 100644 --- a/internal/media/util.go +++ b/internal/media/processing.go @@ -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  import ( @@ -28,112 +10,26 @@ import (  	"image/png"  	"github.com/buckket/go-blurhash" -	"github.com/h2non/filetype"  	"github.com/nfnt/resize"  	"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.  // Only pass pngs or jpegs to this function. -func purgeExif(b []byte) ([]byte, error) { -	if len(b) == 0 { +func purgeExif(data []byte) ([]byte, error) { +	if len(data) == 0 {  		return nil, errors.New("passed image was not valid")  	} -	clean, err := exifremove.Remove(b) +	clean, err := exifremove.Remove(data)  	if err != nil {  		return nil, fmt.Errorf("could not purge exif from image: %s", err)  	} +  	if len(clean) == 0 {  		return nil, errors.New("purged image was not valid")  	} +	  	return clean, nil  } @@ -141,7 +37,7 @@ func deriveGif(b []byte, extension string) (*imageAndMeta, error) {  	var g *gif.GIF  	var err error  	switch extension { -	case MIMEGif: +	case mimeGif:  		g, err = gif.DecodeAll(bytes.NewReader(b))  		if err != nil {  			return nil, err @@ -170,12 +66,12 @@ func deriveImage(b []byte, contentType string) (*imageAndMeta, error) {  	var err error  	switch contentType { -	case MIMEJpeg: +	case mimeImageJpeg:  		i, err = jpeg.Decode(bytes.NewReader(b))  		if err != nil {  			return nil, err  		} -	case MIMEPng: +	case mimeImagePng:  		i, err = png.Decode(bytes.NewReader(b))  		if err != nil {  			return nil, err @@ -208,17 +104,17 @@ func deriveThumbnail(b []byte, contentType string, x uint, y uint) (*imageAndMet  	var err error  	switch contentType { -	case MIMEJpeg: +	case mimeImageJpeg:  		i, err = jpeg.Decode(bytes.NewReader(b))  		if err != nil {  			return nil, err  		} -	case MIMEPng: +	case mimeImagePng:  		i, err = png.Decode(bytes.NewReader(b))  		if err != nil {  			return nil, err  		} -	case MIMEGif: +	case mimeImageGif:  		i, err = gif.Decode(bytes.NewReader(b))  		if err != nil {  			return nil, err @@ -261,12 +157,12 @@ func deriveStaticEmoji(b []byte, contentType string) (*imageAndMeta, error) {  	var err error  	switch contentType { -	case MIMEPng: +	case mimeImagePng:  		i, err = png.Decode(bytes.NewReader(b))  		if err != nil {  			return nil, err  		} -	case MIMEGif: +	case mimeImageGif:  		i, err = gif.Decode(bytes.NewReader(b))  		if err != nil {  			return nil, err @@ -292,31 +188,3 @@ type imageAndMeta struct {  	aspect   float64  	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) -} diff --git a/internal/media/types.go b/internal/media/types.go new file mode 100644 index 000000000..f1608f880 --- /dev/null +++ b/internal/media/types.go @@ -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) +} diff --git a/internal/media/util_test.go b/internal/media/util_test.go index cb299d50e..817b597cb 100644 --- a/internal/media/util_test.go +++ b/internal/media/util_test.go @@ -138,10 +138,10 @@ func (suite *MediaUtilTestSuite) TestDeriveThumbnailFromJPEG() {  }  func (suite *MediaUtilTestSuite) TestSupportedImageTypes() { -	ok := SupportedImageType("image/jpeg") +	ok := supportedImage("image/jpeg")  	suite.True(ok) -	ok = SupportedImageType("image/bmp") +	ok = supportedImage("image/bmp")  	suite.False(ok)  }  | 
