mirror of
				https://github.com/superseriousbusiness/gotosocial.git
				synced 2025-10-31 01:12:24 -05:00 
			
		
		
		
	more refactoring, media handler => manager
This commit is contained in:
		
					parent
					
						
							
								6803c1682b
							
						
					
				
			
			
				commit
				
					
						c4d63d125b
					
				
			
		
					 29 changed files with 327 additions and 485 deletions
				
			
		|  | @ -105,10 +105,10 @@ var Start action.GTSAction = func(ctx context.Context) error { | |||
| 	} | ||||
| 
 | ||||
| 	// build backend handlers | ||||
| 	mediaHandler := media.New(dbService, storage) | ||||
| 	mediaManager := media.New(dbService, storage) | ||||
| 	oauthServer := oauth.New(ctx, dbService) | ||||
| 	transportController := transport.NewController(dbService, &federation.Clock{}, http.DefaultClient) | ||||
| 	federator := federation.NewFederator(dbService, federatingDB, transportController, typeConverter, mediaHandler) | ||||
| 	federator := federation.NewFederator(dbService, federatingDB, transportController, typeConverter, mediaManager) | ||||
| 
 | ||||
| 	// decide whether to create a noop email sender (won't send emails) or a real one | ||||
| 	var emailSender email.Sender | ||||
|  | @ -128,7 +128,7 @@ var Start action.GTSAction = func(ctx context.Context) error { | |||
| 	} | ||||
| 
 | ||||
| 	// create and start the message processor using the other services we've created so far | ||||
| 	processor := processing.NewProcessor(typeConverter, federator, oauthServer, mediaHandler, storage, timelineManager, dbService, emailSender) | ||||
| 	processor := processing.NewProcessor(typeConverter, federator, oauthServer, mediaManager, storage, timelineManager, dbService, emailSender) | ||||
| 	if err := processor.Start(ctx); err != nil { | ||||
| 		return fmt.Errorf("error starting processor: %s", err) | ||||
| 	} | ||||
|  |  | |||
|  | @ -51,7 +51,7 @@ type ServeFileTestSuite struct { | |||
| 	federator    federation.Federator | ||||
| 	tc           typeutils.TypeConverter | ||||
| 	processor    processing.Processor | ||||
| 	mediaHandler media.Handler | ||||
| 	mediaManager media.Manager | ||||
| 	oauthServer  oauth.Server | ||||
| 	emailSender  email.Sender | ||||
| 
 | ||||
|  | @ -82,7 +82,7 @@ func (suite *ServeFileTestSuite) SetupSuite() { | |||
| 
 | ||||
| 	suite.processor = testrig.NewTestProcessor(suite.db, suite.storage, suite.federator, suite.emailSender) | ||||
| 	suite.tc = testrig.NewTestTypeConverter(suite.db) | ||||
| 	suite.mediaHandler = testrig.NewTestMediaHandler(suite.db, suite.storage) | ||||
| 	suite.mediaManager = testrig.NewTestMediaManager(suite.db, suite.storage) | ||||
| 	suite.oauthServer = testrig.NewTestOauthServer(suite.db) | ||||
| 
 | ||||
| 	// setup module being tested | ||||
|  |  | |||
|  | @ -53,7 +53,7 @@ type MediaCreateTestSuite struct { | |||
| 	storage      *kv.KVStore | ||||
| 	federator    federation.Federator | ||||
| 	tc           typeutils.TypeConverter | ||||
| 	mediaHandler media.Handler | ||||
| 	mediaManager media.Manager | ||||
| 	oauthServer  oauth.Server | ||||
| 	emailSender  email.Sender | ||||
| 	processor    processing.Processor | ||||
|  | @ -81,7 +81,7 @@ func (suite *MediaCreateTestSuite) SetupSuite() { | |||
| 	suite.db = testrig.NewTestDB() | ||||
| 	suite.storage = testrig.NewTestStorage() | ||||
| 	suite.tc = testrig.NewTestTypeConverter(suite.db) | ||||
| 	suite.mediaHandler = testrig.NewTestMediaHandler(suite.db, suite.storage) | ||||
| 	suite.mediaManager = testrig.NewTestMediaManager(suite.db, suite.storage) | ||||
| 	suite.oauthServer = testrig.NewTestOauthServer(suite.db) | ||||
| 	suite.federator = testrig.NewTestFederator(suite.db, testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil), suite.db), suite.storage) | ||||
| 	suite.emailSender = testrig.NewEmailSender("../../../../web/template/", nil) | ||||
|  |  | |||
|  | @ -69,7 +69,7 @@ func (suite *WebfingerGetTestSuite) TestFingerUser() { | |||
| func (suite *WebfingerGetTestSuite) TestFingerUserWithDifferentAccountDomainByHost() { | ||||
| 	viper.Set(config.Keys.Host, "gts.example.org") | ||||
| 	viper.Set(config.Keys.AccountDomain, "example.org") | ||||
| 	suite.processor = processing.NewProcessor(suite.tc, suite.federator, testrig.NewTestOauthServer(suite.db), testrig.NewTestMediaHandler(suite.db, suite.storage), suite.storage, testrig.NewTestTimelineManager(suite.db), suite.db, suite.emailSender) | ||||
| 	suite.processor = processing.NewProcessor(suite.tc, suite.federator, testrig.NewTestOauthServer(suite.db), testrig.NewTestMediaManager(suite.db, suite.storage), suite.storage, testrig.NewTestTimelineManager(suite.db), suite.db, suite.emailSender) | ||||
| 	suite.webfingerModule = webfinger.New(suite.processor).(*webfinger.Module) | ||||
| 
 | ||||
| 	targetAccount := accountDomainAccount() | ||||
|  | @ -103,7 +103,7 @@ func (suite *WebfingerGetTestSuite) TestFingerUserWithDifferentAccountDomainByHo | |||
| func (suite *WebfingerGetTestSuite) TestFingerUserWithDifferentAccountDomainByAccountDomain() { | ||||
| 	viper.Set(config.Keys.Host, "gts.example.org") | ||||
| 	viper.Set(config.Keys.AccountDomain, "example.org") | ||||
| 	suite.processor = processing.NewProcessor(suite.tc, suite.federator, testrig.NewTestOauthServer(suite.db), testrig.NewTestMediaHandler(suite.db, suite.storage), suite.storage, testrig.NewTestTimelineManager(suite.db), suite.db, suite.emailSender) | ||||
| 	suite.processor = processing.NewProcessor(suite.tc, suite.federator, testrig.NewTestOauthServer(suite.db), testrig.NewTestMediaManager(suite.db, suite.storage), suite.storage, testrig.NewTestTimelineManager(suite.db), suite.db, suite.emailSender) | ||||
| 	suite.webfingerModule = webfinger.New(suite.processor).(*webfinger.Module) | ||||
| 
 | ||||
| 	targetAccount := accountDomainAccount() | ||||
|  |  | |||
|  | @ -246,7 +246,7 @@ func (d *deref) fetchHeaderAndAviForAccount(ctx context.Context, targetAccount * | |||
| 	} | ||||
| 
 | ||||
| 	if targetAccount.AvatarRemoteURL != "" && (targetAccount.AvatarMediaAttachmentID == "" || refresh) { | ||||
| 		a, err := d.mediaHandler.ProcessRemoteHeaderOrAvatar(ctx, t, >smodel.MediaAttachment{ | ||||
| 		a, err := d.mediaManager.ProcessRemoteHeaderOrAvatar(ctx, t, >smodel.MediaAttachment{ | ||||
| 			RemoteURL: targetAccount.AvatarRemoteURL, | ||||
| 			Avatar:    true, | ||||
| 		}, targetAccount.ID) | ||||
|  | @ -257,7 +257,7 @@ func (d *deref) fetchHeaderAndAviForAccount(ctx context.Context, targetAccount * | |||
| 	} | ||||
| 
 | ||||
| 	if targetAccount.HeaderRemoteURL != "" && (targetAccount.HeaderMediaAttachmentID == "" || refresh) { | ||||
| 		a, err := d.mediaHandler.ProcessRemoteHeaderOrAvatar(ctx, t, >smodel.MediaAttachment{ | ||||
| 		a, err := d.mediaManager.ProcessRemoteHeaderOrAvatar(ctx, t, >smodel.MediaAttachment{ | ||||
| 			RemoteURL: targetAccount.HeaderRemoteURL, | ||||
| 			Header:    true, | ||||
| 		}, targetAccount.ID) | ||||
|  |  | |||
|  | @ -93,7 +93,7 @@ func (d *deref) RefreshAttachment(ctx context.Context, requestingUsername string | |||
| 		return nil, fmt.Errorf("RefreshAttachment: error dereferencing media: %s", err) | ||||
| 	} | ||||
| 
 | ||||
| 	a, err := d.mediaHandler.ProcessAttachment(ctx, attachmentBytes, minAttachment) | ||||
| 	a, err := d.mediaManager.ProcessAttachment(ctx, attachmentBytes, minAttachment) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("RefreshAttachment: error processing attachment: %s", err) | ||||
| 	} | ||||
|  |  | |||
|  | @ -80,18 +80,18 @@ type deref struct { | |||
| 	db                  db.DB | ||||
| 	typeConverter       typeutils.TypeConverter | ||||
| 	transportController transport.Controller | ||||
| 	mediaHandler        media.Handler | ||||
| 	mediaManager        media.Manager | ||||
| 	handshakes          map[string][]*url.URL | ||||
| 	handshakeSync       *sync.Mutex // mutex to lock/unlock when checking or updating the handshakes map | ||||
| } | ||||
| 
 | ||||
| // NewDereferencer returns a Dereferencer initialized with the given parameters. | ||||
| func NewDereferencer(db db.DB, typeConverter typeutils.TypeConverter, transportController transport.Controller, mediaHandler media.Handler) Dereferencer { | ||||
| func NewDereferencer(db db.DB, typeConverter typeutils.TypeConverter, transportController transport.Controller, mediaManager media.Manager) Dereferencer { | ||||
| 	return &deref{ | ||||
| 		db:                  db, | ||||
| 		typeConverter:       typeConverter, | ||||
| 		transportController: transportController, | ||||
| 		mediaHandler:        mediaHandler, | ||||
| 		mediaManager:        mediaManager, | ||||
| 		handshakeSync:       &sync.Mutex{}, | ||||
| 	} | ||||
| } | ||||
|  |  | |||
|  | @ -64,7 +64,7 @@ func (suite *DereferencerStandardTestSuite) SetupTest() { | |||
| 
 | ||||
| 	suite.db = testrig.NewTestDB() | ||||
| 	suite.storage = testrig.NewTestStorage() | ||||
| 	suite.dereferencer = dereferencing.NewDereferencer(suite.db, testrig.NewTestTypeConverter(suite.db), suite.mockTransportController(), testrig.NewTestMediaHandler(suite.db, suite.storage)) | ||||
| 	suite.dereferencer = dereferencing.NewDereferencer(suite.db, testrig.NewTestTypeConverter(suite.db), suite.mockTransportController(), testrig.NewTestMediaManager(suite.db, suite.storage)) | ||||
| 	testrig.StandardDBSetup(suite.db, nil) | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -78,13 +78,13 @@ type federator struct { | |||
| 	typeConverter       typeutils.TypeConverter | ||||
| 	transportController transport.Controller | ||||
| 	dereferencer        dereferencing.Dereferencer | ||||
| 	mediaHandler        media.Handler | ||||
| 	mediaManager        media.Manager | ||||
| 	actor               pub.FederatingActor | ||||
| } | ||||
| 
 | ||||
| // NewFederator returns a new federator | ||||
| func NewFederator(db db.DB, federatingDB federatingdb.DB, transportController transport.Controller, typeConverter typeutils.TypeConverter, mediaHandler media.Handler) Federator { | ||||
| 	dereferencer := dereferencing.NewDereferencer(db, typeConverter, transportController, mediaHandler) | ||||
| func NewFederator(db db.DB, federatingDB federatingdb.DB, transportController transport.Controller, typeConverter typeutils.TypeConverter, mediaManager media.Manager) Federator { | ||||
| 	dereferencer := dereferencing.NewDereferencer(db, typeConverter, transportController, mediaManager) | ||||
| 
 | ||||
| 	clock := &Clock{} | ||||
| 	f := &federator{ | ||||
|  | @ -94,7 +94,7 @@ func NewFederator(db db.DB, federatingDB federatingdb.DB, transportController tr | |||
| 		typeConverter:       typeConverter, | ||||
| 		transportController: transportController, | ||||
| 		dereferencer:        dereferencer, | ||||
| 		mediaHandler:        mediaHandler, | ||||
| 		mediaManager:        mediaManager, | ||||
| 	} | ||||
| 	actor := newFederatingActor(f, f, federatingDB, clock) | ||||
| 	f.actor = actor | ||||
|  |  | |||
|  | @ -78,7 +78,7 @@ func (suite *ProtocolTestSuite) TestPostInboxRequestBodyHook() { | |||
| 		return nil, nil | ||||
| 	}), suite.db) | ||||
| 	// setup module being tested | ||||
| 	federator := federation.NewFederator(suite.db, testrig.NewTestFederatingDB(suite.db), tc, suite.typeConverter, testrig.NewTestMediaHandler(suite.db, suite.storage)) | ||||
| 	federator := federation.NewFederator(suite.db, testrig.NewTestFederatingDB(suite.db), tc, suite.typeConverter, testrig.NewTestMediaManager(suite.db, suite.storage)) | ||||
| 
 | ||||
| 	// setup request | ||||
| 	ctx := context.Background() | ||||
|  | @ -107,7 +107,7 @@ func (suite *ProtocolTestSuite) TestAuthenticatePostInbox() { | |||
| 
 | ||||
| 	tc := testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil), suite.db) | ||||
| 	// now setup module being tested, with the mock transport controller | ||||
| 	federator := federation.NewFederator(suite.db, testrig.NewTestFederatingDB(suite.db), tc, suite.typeConverter, testrig.NewTestMediaHandler(suite.db, suite.storage)) | ||||
| 	federator := federation.NewFederator(suite.db, testrig.NewTestFederatingDB(suite.db), tc, suite.typeConverter, testrig.NewTestMediaManager(suite.db, suite.storage)) | ||||
| 
 | ||||
| 	request := httptest.NewRequest(http.MethodPost, "http://localhost:8080/users/the_mighty_zork/inbox", nil) | ||||
| 	// we need these headers for the request to be validated | ||||
|  |  | |||
|  | @ -19,67 +19,88 @@ | |||
| package media | ||||
| 
 | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"image" | ||||
| 	"image/gif" | ||||
| 	"image/jpeg" | ||||
| 	"image/png" | ||||
| 	"strings" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"github.com/buckket/go-blurhash" | ||||
| 	"github.com/nfnt/resize" | ||||
| 	"github.com/superseriousbusiness/exifremove/pkg/exifremove" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/id" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/uris" | ||||
| ) | ||||
| 
 | ||||
| func (mh *mediaHandler) processImage(data []byte, minAttachment *gtsmodel.MediaAttachment) (*gtsmodel.MediaAttachment, error) { | ||||
| const ( | ||||
| 	thumbnailMaxWidth  = 512 | ||||
| 	thumbnailMaxHeight = 512 | ||||
| ) | ||||
| 
 | ||||
| type imageAndMeta struct { | ||||
| 	image    []byte | ||||
| 	width    int | ||||
| 	height   int | ||||
| 	size     int | ||||
| 	aspect   float64 | ||||
| 	blurhash string | ||||
| } | ||||
| 
 | ||||
| func (m *manager) processImage(data []byte, contentType string) (*gtsmodel.MediaAttachment, error) { | ||||
| 	var clean []byte | ||||
| 	var err error | ||||
| 	var original *imageAndMeta | ||||
| 	var small *imageAndMeta | ||||
| 
 | ||||
| 	contentType := minAttachment.File.ContentType | ||||
| 
 | ||||
| 	switch contentType { | ||||
| 	case mimeJpeg, mimePng: | ||||
| 		if clean, err = purgeExif(data); err != nil { | ||||
| 			return nil, fmt.Errorf("error cleaning exif data: %s", err) | ||||
| 	case mimeImageJpeg, mimeImagePng: | ||||
| 		// first 'clean' image by purging exif data from it | ||||
| 		var exifErr error | ||||
| 		if clean, exifErr = purgeExif(data); exifErr != nil { | ||||
| 			return nil, fmt.Errorf("error cleaning exif data: %s", exifErr) | ||||
| 		} | ||||
| 		original, err = deriveImage(clean, contentType) | ||||
| 		if err != nil { | ||||
| 			return nil, fmt.Errorf("error parsing image: %s", err) | ||||
| 		} | ||||
| 	case mimeGif: | ||||
| 		original, err = decodeImage(clean, contentType) | ||||
| 	case mimeImageGif: | ||||
| 		// gifs are already clean - no exif data to remove | ||||
| 		clean = data | ||||
| 		original, err = deriveGif(clean, contentType) | ||||
| 		if err != nil { | ||||
| 			return nil, fmt.Errorf("error parsing gif: %s", err) | ||||
| 		} | ||||
| 		original, err = decodeGif(clean, contentType) | ||||
| 	default: | ||||
| 		return nil, errors.New("media type unrecognized") | ||||
| 		err = fmt.Errorf("content type %s not a recognized image type", contentType) | ||||
| 	} | ||||
| 
 | ||||
| 	small, err = deriveThumbnail(clean, contentType, 512, 512) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	small, err = deriveThumbnail(clean, contentType, thumbnailMaxWidth, thumbnailMaxHeight) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("error deriving thumbnail: %s", err) | ||||
| 	} | ||||
| 
 | ||||
| 	// now put it in storage, take a new id for the name of the file so we don't store any unnecessary info about it | ||||
| 	extension := strings.Split(contentType, "/")[1] | ||||
| 	newMediaID, err := id.NewRandomULID() | ||||
| 	attachmentID, err := id.NewRandomULID() | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	originalURL := uris.GenerateURIForAttachment(minAttachment.AccountID, string(TypeAttachment), string(SizeOriginal), newMediaID, extension) | ||||
| 	smallURL := uris.GenerateURIForAttachment(minAttachment.AccountID, string(TypeAttachment), string(SizeSmall), newMediaID, "jpeg") // all thumbnails/smalls are encoded as jpeg | ||||
| 	originalURL := uris.GenerateURIForAttachment(minAttachment.AccountID, string(TypeAttachment), string(SizeOriginal), attachmentID, extension) | ||||
| 	smallURL := uris.GenerateURIForAttachment(minAttachment.AccountID, string(TypeAttachment), string(SizeSmall), attachmentID, "jpeg") // all thumbnails/smalls are encoded as jpeg | ||||
| 
 | ||||
| 	// we store the original... | ||||
| 	originalPath := fmt.Sprintf("%s/%s/%s/%s.%s", minAttachment.AccountID, TypeAttachment, SizeOriginal, newMediaID, extension) | ||||
| 	if err := mh.storage.Put(originalPath, original.image); err != nil { | ||||
| 	originalPath := fmt.Sprintf("%s/%s/%s/%s.%s", minAttachment.AccountID, TypeAttachment, SizeOriginal, attachmentID, extension) | ||||
| 	if err := m.storage.Put(originalPath, original.image); err != nil { | ||||
| 		return nil, fmt.Errorf("storage error: %s", err) | ||||
| 	} | ||||
| 
 | ||||
| 	// and a thumbnail... | ||||
| 	smallPath := fmt.Sprintf("%s/%s/%s/%s.jpeg", minAttachment.AccountID, TypeAttachment, SizeSmall, newMediaID) // all thumbnails/smalls are encoded as jpeg | ||||
| 	if err := mh.storage.Put(smallPath, small.image); err != nil { | ||||
| 	smallPath := fmt.Sprintf("%s/%s/%s/%s.jpeg", minAttachment.AccountID, TypeAttachment, SizeSmall, attachmentID) // all thumbnails/smalls are encoded as jpeg | ||||
| 	if err := m.storage.Put(smallPath, small.image); err != nil { | ||||
| 		return nil, fmt.Errorf("storage error: %s", err) | ||||
| 	} | ||||
| 
 | ||||
|  | @ -98,7 +119,7 @@ func (mh *mediaHandler) processImage(data []byte, minAttachment *gtsmodel.MediaA | |||
| 	} | ||||
| 
 | ||||
| 	attachment := >smodel.MediaAttachment{ | ||||
| 		ID:                newMediaID, | ||||
| 		ID:                attachmentID, | ||||
| 		StatusID:          minAttachment.StatusID, | ||||
| 		URL:               originalURL, | ||||
| 		RemoteURL:         minAttachment.RemoteURL, | ||||
|  | @ -131,3 +152,173 @@ func (mh *mediaHandler) processImage(data []byte, minAttachment *gtsmodel.MediaA | |||
| 
 | ||||
| 	return attachment, nil | ||||
| } | ||||
| 
 | ||||
| func decodeGif(b []byte, extension string) (*imageAndMeta, error) { | ||||
| 	var g *gif.GIF | ||||
| 	var err error | ||||
| 
 | ||||
| 	switch extension { | ||||
| 	case mimeGif: | ||||
| 		g, err = gif.DecodeAll(bytes.NewReader(b)) | ||||
| 	default: | ||||
| 		err = fmt.Errorf("extension %s not recognised", extension) | ||||
| 	} | ||||
| 
 | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	// use the first frame to get the static characteristics | ||||
| 	width := g.Config.Width | ||||
| 	height := g.Config.Height | ||||
| 	size := width * height | ||||
| 	aspect := float64(width) / float64(height) | ||||
| 
 | ||||
| 	return &imageAndMeta{ | ||||
| 		image:  b, | ||||
| 		width:  width, | ||||
| 		height: height, | ||||
| 		size:   size, | ||||
| 		aspect: aspect, | ||||
| 	}, nil | ||||
| } | ||||
| 
 | ||||
| func decodeImage(b []byte, contentType string) (*imageAndMeta, error) { | ||||
| 	var i image.Image | ||||
| 	var err error | ||||
| 
 | ||||
| 	switch contentType { | ||||
| 	case mimeImageJpeg: | ||||
| 		i, err = jpeg.Decode(bytes.NewReader(b)) | ||||
| 	case mimeImagePng: | ||||
| 		i, err = png.Decode(bytes.NewReader(b)) | ||||
| 	default: | ||||
| 		err = fmt.Errorf("content type %s not recognised", contentType) | ||||
| 	} | ||||
| 
 | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	if i == nil { | ||||
| 		return nil, errors.New("processed image was nil") | ||||
| 	} | ||||
| 
 | ||||
| 	width := i.Bounds().Size().X | ||||
| 	height := i.Bounds().Size().Y | ||||
| 	size := width * height | ||||
| 	aspect := float64(width) / float64(height) | ||||
| 
 | ||||
| 	return &imageAndMeta{ | ||||
| 		image:  b, | ||||
| 		width:  width, | ||||
| 		height: height, | ||||
| 		size:   size, | ||||
| 		aspect: aspect, | ||||
| 	}, nil | ||||
| } | ||||
| 
 | ||||
| // deriveThumbnail returns a byte slice and metadata for a thumbnail of width x and height y, | ||||
| // of a given jpeg, png, or gif, or an error if something goes wrong. | ||||
| // | ||||
| // Note that the aspect ratio of the image will be retained, | ||||
| // so it will not necessarily be a square, even if x and y are set as the same value. | ||||
| func deriveThumbnail(b []byte, contentType string, x uint, y uint) (*imageAndMeta, error) { | ||||
| 	var i image.Image | ||||
| 	var err error | ||||
| 
 | ||||
| 	switch contentType { | ||||
| 	case mimeImageJpeg: | ||||
| 		i, err = jpeg.Decode(bytes.NewReader(b)) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 	case mimeImagePng: | ||||
| 		i, err = png.Decode(bytes.NewReader(b)) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 	case mimeImageGif: | ||||
| 		i, err = gif.Decode(bytes.NewReader(b)) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 	default: | ||||
| 		return nil, fmt.Errorf("content type %s not recognised", contentType) | ||||
| 	} | ||||
| 
 | ||||
| 	thumb := resize.Thumbnail(x, y, i, resize.NearestNeighbor) | ||||
| 	width := thumb.Bounds().Size().X | ||||
| 	height := thumb.Bounds().Size().Y | ||||
| 	size := width * height | ||||
| 	aspect := float64(width) / float64(height) | ||||
| 
 | ||||
| 	tiny := resize.Thumbnail(32, 32, thumb, resize.NearestNeighbor) | ||||
| 	bh, err := blurhash.Encode(4, 3, tiny) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	out := &bytes.Buffer{} | ||||
| 	if err := jpeg.Encode(out, thumb, &jpeg.Options{ | ||||
| 		Quality: 75, | ||||
| 	}); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return &imageAndMeta{ | ||||
| 		image:    out.Bytes(), | ||||
| 		width:    width, | ||||
| 		height:   height, | ||||
| 		size:     size, | ||||
| 		aspect:   aspect, | ||||
| 		blurhash: bh, | ||||
| 	}, nil | ||||
| } | ||||
| 
 | ||||
| // deriveStaticEmojji takes a given gif or png of an emoji, decodes it, and re-encodes it as a static png. | ||||
| func deriveStaticEmoji(b []byte, contentType string) (*imageAndMeta, error) { | ||||
| 	var i image.Image | ||||
| 	var err error | ||||
| 
 | ||||
| 	switch contentType { | ||||
| 	case mimeImagePng: | ||||
| 		i, err = png.Decode(bytes.NewReader(b)) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 	case mimeImageGif: | ||||
| 		i, err = gif.Decode(bytes.NewReader(b)) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 	default: | ||||
| 		return nil, fmt.Errorf("content type %s not allowed for emoji", contentType) | ||||
| 	} | ||||
| 
 | ||||
| 	out := &bytes.Buffer{} | ||||
| 	if err := png.Encode(out, i); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return &imageAndMeta{ | ||||
| 		image: out.Bytes(), | ||||
| 	}, nil | ||||
| } | ||||
| 
 | ||||
| // 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(data []byte) ([]byte, error) { | ||||
| 	if len(data) == 0 { | ||||
| 		return nil, errors.New("passed image was not valid") | ||||
| 	} | ||||
| 
 | ||||
| 	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 | ||||
| } | ||||
|  |  | |||
|  | @ -27,7 +27,6 @@ import ( | |||
| 	"time" | ||||
| 
 | ||||
| 	"codeberg.org/gruf/go-store/kv" | ||||
| 	"github.com/sirupsen/logrus" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/id" | ||||
|  | @ -35,26 +34,31 @@ import ( | |||
| 	"github.com/superseriousbusiness/gotosocial/internal/uris" | ||||
| ) | ||||
| 
 | ||||
| // ProcessCallback is triggered by the media manager when an attachment has finished undergoing | ||||
| // image processing (generation of a blurhash, thumbnail etc) but hasn't yet been inserted into | ||||
| // the database. It is provided to allow callers to a) access the processed media attachment and b) | ||||
| // make any last-minute changes to the media attachment before it enters the database. | ||||
| type ProcessCallback func(*gtsmodel.MediaAttachment) *gtsmodel.MediaAttachment | ||||
| 
 | ||||
| 
 | ||||
| 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 { | ||||
| 	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) | ||||
| // defaultCB will be used when a nil ProcessCallback is passed to one of the manager's interface functions. | ||||
| // It just returns the processed media attachment with no additional changes. | ||||
| var defaultCB ProcessCallback = func(a *gtsmodel.MediaAttachment) *gtsmodel.MediaAttachment { | ||||
| 	return a | ||||
| } | ||||
| 
 | ||||
| type mediaHandler struct { | ||||
| // Manager provides an interface for managing media: parsing, storing, and retrieving media objects like photos, videos, and gifs. | ||||
| type Manager interface { | ||||
| 	ProcessAttachment(ctx context.Context, data []byte, accountID string, cb ProcessCallback) (*gtsmodel.MediaAttachment, error) | ||||
| } | ||||
| 
 | ||||
| type manager struct { | ||||
| 	db      db.DB | ||||
| 	storage *kv.KVStore | ||||
| } | ||||
| 
 | ||||
| // New returns a new handler with the given db and storage | ||||
| func New(database db.DB, storage *kv.KVStore) Handler { | ||||
| 	return &mediaHandler{ | ||||
| // New returns a media manager with the given db and underlying storage. | ||||
| func New(database db.DB, storage *kv.KVStore) Manager { | ||||
| 	return &manager{ | ||||
| 		db:      database, | ||||
| 		storage: storage, | ||||
| 	} | ||||
|  | @ -64,83 +68,64 @@ func New(database db.DB, storage *kv.KVStore) Handler { | |||
| 	INTERFACE FUNCTIONS | ||||
| */ | ||||
| 
 | ||||
| // 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. | ||||
| func (mh *mediaHandler) ProcessHeaderOrAvatar(ctx context.Context, attachment []byte, accountID string, mediaType Type, remoteURL string) (*gtsmodel.MediaAttachment, error) { | ||||
| 	l := logrus.WithField("func", "SetHeaderForAccountID") | ||||
| 
 | ||||
| 	if mediaType != TypeHeader && mediaType != TypeAvatar { | ||||
| 		return nil, errors.New("header or avatar not selected") | ||||
| 	} | ||||
| 
 | ||||
| 	// make sure we have a type we can handle | ||||
| 	contentType, err := parseContentType(attachment) | ||||
| func (m *manager) ProcessAttachment(ctx context.Context, data []byte, accountID string, cb ProcessCallback) (*gtsmodel.MediaAttachment, error) { | ||||
| 	contentType, err := parseContentType(data) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	mainType := strings.Split(contentType, "/")[0] | ||||
| 	switch mainType { | ||||
| 	case mimeImage: | ||||
| 		if !supportedImage(contentType) { | ||||
| 			return nil, fmt.Errorf("image type %s not supported", contentType) | ||||
| 		} | ||||
| 		if len(data) == 0 { | ||||
| 			return nil, errors.New("image was of size 0") | ||||
| 		} | ||||
| 		return m.processImage(attachmentBytes, minAttachment) | ||||
| 	default: | ||||
| 		return nil, fmt.Errorf("content type %s not (yet) supported", contentType) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // 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. | ||||
| func (m *manager) ProcessHeader(ctx context.Context, data []byte, accountID string, cb ProcessCallback) (*gtsmodel.MediaAttachment, error) { | ||||
| 
 | ||||
| 	// make sure we have a type we can handle | ||||
| 	contentType, err := parseContentType(data) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	if !supportedImage(contentType) { | ||||
| 		return nil, fmt.Errorf("%s is not an accepted image type", contentType) | ||||
| 	} | ||||
| 
 | ||||
| 	if len(attachment) == 0 { | ||||
| 	if len(data) == 0 { | ||||
| 		return nil, fmt.Errorf("passed reader was of size 0") | ||||
| 	} | ||||
| 	l.Tracef("read %d bytes of file", len(attachment)) | ||||
| 
 | ||||
| 	// process it | ||||
| 	ma, err := mh.processHeaderOrAvi(attachment, contentType, mediaType, accountID, remoteURL) | ||||
| 	ma, err := m.processHeaderOrAvi(attachment, contentType, mediaType, accountID, remoteURL) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("error processing %s: %s", mediaType, err) | ||||
| 	} | ||||
| 
 | ||||
| 	// set it in the database | ||||
| 	if err := mh.db.SetAccountHeaderOrAvatar(ctx, ma, accountID); err != nil { | ||||
| 	if err := m.db.SetAccountHeaderOrAvatar(ctx, ma, accountID); err != nil { | ||||
| 		return nil, fmt.Errorf("error putting %s in database: %s", mediaType, err) | ||||
| 	} | ||||
| 
 | ||||
| 	return ma, nil | ||||
| } | ||||
| 
 | ||||
| // ProcessAttachment takes a new attachment and the owning 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. | ||||
| func (mh *mediaHandler) ProcessAttachment(ctx context.Context, attachmentBytes []byte, minAttachment *gtsmodel.MediaAttachment) (*gtsmodel.MediaAttachment, error) { | ||||
| 	contentType, err := parseContentType(attachmentBytes) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	minAttachment.File.ContentType = contentType | ||||
| 
 | ||||
| 	mainType := strings.Split(contentType, "/")[0] | ||||
| 	switch mainType { | ||||
| 	// case MIMEVideo: | ||||
| 	// 	if !SupportedVideoType(contentType) { | ||||
| 	// 		return nil, fmt.Errorf("video type %s not supported", contentType) | ||||
| 	// 	} | ||||
| 	// 	if len(attachment) == 0 { | ||||
| 	// 		return nil, errors.New("video was of size 0") | ||||
| 	// 	} | ||||
| 	// 	return mh.processVideoAttachment(attachment, accountID, contentType, remoteURL) | ||||
| 	case mimeImage: | ||||
| 		if !supportedImage(contentType) { | ||||
| 			return nil, fmt.Errorf("image type %s not supported", contentType) | ||||
| 		} | ||||
| 		if len(attachmentBytes) == 0 { | ||||
| 			return nil, errors.New("image was of size 0") | ||||
| 		} | ||||
| 		return mh.processImageAttachment(attachmentBytes, minAttachment) | ||||
| 	default: | ||||
| 		break | ||||
| 	} | ||||
| 	return nil, fmt.Errorf("content type %s not (yet) supported", contentType) | ||||
| } | ||||
| 
 | ||||
| // 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. | ||||
| func (mh *mediaHandler) ProcessLocalEmoji(ctx context.Context, emojiBytes []byte, shortcode string) (*gtsmodel.Emoji, error) { | ||||
| func (m *manager) ProcessLocalEmoji(ctx context.Context, emojiBytes []byte, shortcode string) (*gtsmodel.Emoji, error) { | ||||
| 	var clean []byte | ||||
| 	var err error | ||||
| 	var original *imageAndMeta | ||||
|  | @ -187,7 +172,7 @@ func (mh *mediaHandler) ProcessLocalEmoji(ctx context.Context, emojiBytes []byte | |||
| 	// since emoji aren't 'owned' by an account, but we still want to use the same pattern for serving them through the filserver, | ||||
| 	// (ie., fileserver/ACCOUNT_ID/etc etc) we need to fetch the INSTANCE ACCOUNT from the database. That is, the account that's created | ||||
| 	// with the same username as the instance hostname, which doesn't belong to any particular user. | ||||
| 	instanceAccount, err := mh.db.GetInstanceAccount(ctx, "") | ||||
| 	instanceAccount, err := m.db.GetInstanceAccount(ctx, "") | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("error fetching instance account: %s", err) | ||||
| 	} | ||||
|  | @ -214,12 +199,12 @@ func (mh *mediaHandler) ProcessLocalEmoji(ctx context.Context, emojiBytes []byte | |||
| 	emojiStaticPath := fmt.Sprintf("%s/%s/%s/%s.png", instanceAccount.ID, TypeEmoji, SizeStatic, newEmojiID) | ||||
| 
 | ||||
| 	// Store the original emoji | ||||
| 	if err := mh.storage.Put(emojiPath, original.image); err != nil { | ||||
| 	if err := m.storage.Put(emojiPath, original.image); err != nil { | ||||
| 		return nil, fmt.Errorf("storage error: %s", err) | ||||
| 	} | ||||
| 
 | ||||
| 	// Store the static emoji | ||||
| 	if err := mh.storage.Put(emojiStaticPath, static.image); err != nil { | ||||
| 	if err := m.storage.Put(emojiStaticPath, static.image); err != nil { | ||||
| 		return nil, fmt.Errorf("storage error: %s", err) | ||||
| 	} | ||||
| 
 | ||||
|  | @ -249,7 +234,7 @@ func (mh *mediaHandler) ProcessLocalEmoji(ctx context.Context, emojiBytes []byte | |||
| 	return e, nil | ||||
| } | ||||
| 
 | ||||
| func (mh *mediaHandler) ProcessRemoteHeaderOrAvatar(ctx context.Context, t transport.Transport, currentAttachment *gtsmodel.MediaAttachment, accountID string) (*gtsmodel.MediaAttachment, error) { | ||||
| func (m *manager) ProcessRemoteHeaderOrAvatar(ctx context.Context, t transport.Transport, currentAttachment *gtsmodel.MediaAttachment, accountID string) (*gtsmodel.MediaAttachment, error) { | ||||
| 	if !currentAttachment.Header && !currentAttachment.Avatar { | ||||
| 		return nil, errors.New("provided attachment was set to neither header nor avatar") | ||||
| 	} | ||||
|  | @ -285,5 +270,5 @@ func (mh *mediaHandler) ProcessRemoteHeaderOrAvatar(ctx context.Context, t trans | |||
| 		return nil, fmt.Errorf("dereferencing remote media with url %s: %s", remoteIRI.String(), err) | ||||
| 	} | ||||
| 
 | ||||
| 	return mh.ProcessHeaderOrAvatar(ctx, attachmentBytes, accountID, headerOrAvi, currentAttachment.RemoteURL) | ||||
| 	return m.ProcessHeaderOrAvatar(ctx, attachmentBytes, accountID, headerOrAvi, currentAttachment.RemoteURL) | ||||
| } | ||||
|  | @ -1,143 +0,0 @@ | |||
| /* | ||||
|    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 ( | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"strings" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/id" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/uris" | ||||
| ) | ||||
| 
 | ||||
| func (mh *mediaHandler) processHeaderOrAvi(imageBytes []byte, contentType string, mediaType Type, accountID string, remoteURL string) (*gtsmodel.MediaAttachment, error) { | ||||
| 	var isHeader bool | ||||
| 	var isAvatar bool | ||||
| 
 | ||||
| 	switch mediaType { | ||||
| 	case TypeHeader: | ||||
| 		isHeader = true | ||||
| 	case TypeAvatar: | ||||
| 		isAvatar = true | ||||
| 	default: | ||||
| 		return nil, errors.New("header or avatar not selected") | ||||
| 	} | ||||
| 
 | ||||
| 	var clean []byte | ||||
| 	var err error | ||||
| 
 | ||||
| 	var original *imageAndMeta | ||||
| 	switch contentType { | ||||
| 	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: | ||||
| 		if clean, err = purgeExif(imageBytes); err != nil { | ||||
| 			return nil, fmt.Errorf("error cleaning exif data: %s", err) | ||||
| 		} | ||||
| 		original, err = deriveImage(clean, contentType) | ||||
| 	case mimeGif: | ||||
| 		clean = imageBytes | ||||
| 		original, err = deriveGif(clean, contentType) | ||||
| 	default: | ||||
| 		return nil, errors.New("media type unrecognized") | ||||
| 	} | ||||
| 
 | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("error parsing image: %s", err) | ||||
| 	} | ||||
| 
 | ||||
| 	small, err := deriveThumbnail(clean, contentType, 256, 256) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("error deriving thumbnail: %s", err) | ||||
| 	} | ||||
| 
 | ||||
| 	// now put it in storage, take a new id for the name of the file so we don't store any unnecessary info about it | ||||
| 	extension := strings.Split(contentType, "/")[1] | ||||
| 	newMediaID, err := id.NewRandomULID() | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	originalURL := uris.GenerateURIForAttachment(accountID, string(mediaType), string(SizeOriginal), newMediaID, extension) | ||||
| 	smallURL := uris.GenerateURIForAttachment(accountID, string(mediaType), string(SizeSmall), newMediaID, extension) | ||||
| 	// we store the original... | ||||
| 	originalPath := fmt.Sprintf("%s/%s/%s/%s.%s", accountID, mediaType, SizeOriginal, newMediaID, extension) | ||||
| 	if err := mh.storage.Put(originalPath, original.image); err != nil { | ||||
| 		return nil, fmt.Errorf("storage error: %s", err) | ||||
| 	} | ||||
| 
 | ||||
| 	// and a thumbnail... | ||||
| 	smallPath := fmt.Sprintf("%s/%s/%s/%s.%s", accountID, mediaType, SizeSmall, newMediaID, extension) | ||||
| 	if err := mh.storage.Put(smallPath, small.image); err != nil { | ||||
| 		return nil, fmt.Errorf("storage error: %s", err) | ||||
| 	} | ||||
| 
 | ||||
| 	ma := >smodel.MediaAttachment{ | ||||
| 		ID:        newMediaID, | ||||
| 		StatusID:  "", | ||||
| 		URL:       originalURL, | ||||
| 		RemoteURL: remoteURL, | ||||
| 		CreatedAt: time.Now(), | ||||
| 		UpdatedAt: time.Now(), | ||||
| 		Type:      gtsmodel.FileTypeImage, | ||||
| 		FileMeta: gtsmodel.FileMeta{ | ||||
| 			Original: gtsmodel.Original{ | ||||
| 				Width:  original.width, | ||||
| 				Height: original.height, | ||||
| 				Size:   original.size, | ||||
| 				Aspect: original.aspect, | ||||
| 			}, | ||||
| 			Small: gtsmodel.Small{ | ||||
| 				Width:  small.width, | ||||
| 				Height: small.height, | ||||
| 				Size:   small.size, | ||||
| 				Aspect: small.aspect, | ||||
| 			}, | ||||
| 		}, | ||||
| 		AccountID:         accountID, | ||||
| 		Description:       "", | ||||
| 		ScheduledStatusID: "", | ||||
| 		Blurhash:          small.blurhash, | ||||
| 		Processing:        2, | ||||
| 		File: gtsmodel.File{ | ||||
| 			Path:        originalPath, | ||||
| 			ContentType: contentType, | ||||
| 			FileSize:    len(original.image), | ||||
| 			UpdatedAt:   time.Now(), | ||||
| 		}, | ||||
| 		Thumbnail: gtsmodel.Thumbnail{ | ||||
| 			Path:        smallPath, | ||||
| 			ContentType: contentType, | ||||
| 			FileSize:    len(small.image), | ||||
| 			UpdatedAt:   time.Now(), | ||||
| 			URL:         smallURL, | ||||
| 			RemoteURL:   "", | ||||
| 		}, | ||||
| 		Avatar: isAvatar, | ||||
| 		Header: isHeader, | ||||
| 	} | ||||
| 
 | ||||
| 	return ma, nil | ||||
| } | ||||
|  | @ -1,190 +0,0 @@ | |||
| package media | ||||
| 
 | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"image" | ||||
| 	"image/gif" | ||||
| 	"image/jpeg" | ||||
| 	"image/png" | ||||
| 
 | ||||
| 	"github.com/buckket/go-blurhash" | ||||
| 	"github.com/nfnt/resize" | ||||
| 	"github.com/superseriousbusiness/exifremove/pkg/exifremove" | ||||
| ) | ||||
| 
 | ||||
| // 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(data []byte) ([]byte, error) { | ||||
| 	if len(data) == 0 { | ||||
| 		return nil, errors.New("passed image was not valid") | ||||
| 	} | ||||
| 
 | ||||
| 	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 | ||||
| } | ||||
| 
 | ||||
| func deriveGif(b []byte, extension string) (*imageAndMeta, error) { | ||||
| 	var g *gif.GIF | ||||
| 	var err error | ||||
| 	switch extension { | ||||
| 	case mimeGif: | ||||
| 		g, err = gif.DecodeAll(bytes.NewReader(b)) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 	default: | ||||
| 		return nil, fmt.Errorf("extension %s not recognised", extension) | ||||
| 	} | ||||
| 
 | ||||
| 	// use the first frame to get the static characteristics | ||||
| 	width := g.Config.Width | ||||
| 	height := g.Config.Height | ||||
| 	size := width * height | ||||
| 	aspect := float64(width) / float64(height) | ||||
| 
 | ||||
| 	return &imageAndMeta{ | ||||
| 		image:  b, | ||||
| 		width:  width, | ||||
| 		height: height, | ||||
| 		size:   size, | ||||
| 		aspect: aspect, | ||||
| 	}, nil | ||||
| } | ||||
| 
 | ||||
| func deriveImage(b []byte, contentType string) (*imageAndMeta, error) { | ||||
| 	var i image.Image | ||||
| 	var err error | ||||
| 
 | ||||
| 	switch contentType { | ||||
| 	case mimeImageJpeg: | ||||
| 		i, err = jpeg.Decode(bytes.NewReader(b)) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 	case mimeImagePng: | ||||
| 		i, err = png.Decode(bytes.NewReader(b)) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 	default: | ||||
| 		return nil, fmt.Errorf("content type %s not recognised", contentType) | ||||
| 	} | ||||
| 
 | ||||
| 	width := i.Bounds().Size().X | ||||
| 	height := i.Bounds().Size().Y | ||||
| 	size := width * height | ||||
| 	aspect := float64(width) / float64(height) | ||||
| 
 | ||||
| 	return &imageAndMeta{ | ||||
| 		image:  b, | ||||
| 		width:  width, | ||||
| 		height: height, | ||||
| 		size:   size, | ||||
| 		aspect: aspect, | ||||
| 	}, nil | ||||
| } | ||||
| 
 | ||||
| // deriveThumbnail returns a byte slice and metadata for a thumbnail of width x and height y, | ||||
| // of a given jpeg, png, or gif, or an error if something goes wrong. | ||||
| // | ||||
| // Note that the aspect ratio of the image will be retained, | ||||
| // so it will not necessarily be a square, even if x and y are set as the same value. | ||||
| func deriveThumbnail(b []byte, contentType string, x uint, y uint) (*imageAndMeta, error) { | ||||
| 	var i image.Image | ||||
| 	var err error | ||||
| 
 | ||||
| 	switch contentType { | ||||
| 	case mimeImageJpeg: | ||||
| 		i, err = jpeg.Decode(bytes.NewReader(b)) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 	case mimeImagePng: | ||||
| 		i, err = png.Decode(bytes.NewReader(b)) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 	case mimeImageGif: | ||||
| 		i, err = gif.Decode(bytes.NewReader(b)) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 	default: | ||||
| 		return nil, fmt.Errorf("content type %s not recognised", contentType) | ||||
| 	} | ||||
| 
 | ||||
| 	thumb := resize.Thumbnail(x, y, i, resize.NearestNeighbor) | ||||
| 	width := thumb.Bounds().Size().X | ||||
| 	height := thumb.Bounds().Size().Y | ||||
| 	size := width * height | ||||
| 	aspect := float64(width) / float64(height) | ||||
| 
 | ||||
| 	tiny := resize.Thumbnail(32, 32, thumb, resize.NearestNeighbor) | ||||
| 	bh, err := blurhash.Encode(4, 3, tiny) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	out := &bytes.Buffer{} | ||||
| 	if err := jpeg.Encode(out, thumb, &jpeg.Options{ | ||||
| 		Quality: 75, | ||||
| 	}); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return &imageAndMeta{ | ||||
| 		image:    out.Bytes(), | ||||
| 		width:    width, | ||||
| 		height:   height, | ||||
| 		size:     size, | ||||
| 		aspect:   aspect, | ||||
| 		blurhash: bh, | ||||
| 	}, nil | ||||
| } | ||||
| 
 | ||||
| // deriveStaticEmojji takes a given gif or png of an emoji, decodes it, and re-encodes it as a static png. | ||||
| func deriveStaticEmoji(b []byte, contentType string) (*imageAndMeta, error) { | ||||
| 	var i image.Image | ||||
| 	var err error | ||||
| 
 | ||||
| 	switch contentType { | ||||
| 	case mimeImagePng: | ||||
| 		i, err = png.Decode(bytes.NewReader(b)) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 	case mimeImageGif: | ||||
| 		i, err = gif.Decode(bytes.NewReader(b)) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 	default: | ||||
| 		return nil, fmt.Errorf("content type %s not allowed for emoji", contentType) | ||||
| 	} | ||||
| 
 | ||||
| 	out := &bytes.Buffer{} | ||||
| 	if err := png.Encode(out, i); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return &imageAndMeta{ | ||||
| 		image: out.Bytes(), | ||||
| 	}, nil | ||||
| } | ||||
| 
 | ||||
| type imageAndMeta struct { | ||||
| 	image    []byte | ||||
| 	width    int | ||||
| 	height   int | ||||
| 	size     int | ||||
| 	aspect   float64 | ||||
| 	blurhash string | ||||
| } | ||||
|  | @ -18,6 +18,6 @@ | |||
| 
 | ||||
| package media | ||||
| 
 | ||||
| // func (mh *mediaHandler) processVideoAttachment(data []byte, accountID string, contentType string, remoteURL string) (*gtsmodel.MediaAttachment, error) { | ||||
| // func (mh *mediaManager) processVideoAttachment(data []byte, accountID string, contentType string, remoteURL string) (*gtsmodel.MediaAttachment, error) { | ||||
| // 	return nil, nil | ||||
| // } | ||||
|  |  | |||
|  | @ -40,7 +40,6 @@ const ( | |||
| 	mimeImagePng = mimeImage + "/" + mimePng | ||||
| ) | ||||
| 
 | ||||
| 
 | ||||
| // EmojiMaxBytes is the maximum permitted bytes of an emoji upload (50kb) | ||||
| // const EmojiMaxBytes = 51200 | ||||
| 
 | ||||
|  |  | |||
|  | @ -103,7 +103,7 @@ func (suite *MediaUtilTestSuite) TestDeriveImageFromJPEG() { | |||
| 	suite.NoError(err) | ||||
| 
 | ||||
| 	// clean it up and validate the clean version | ||||
| 	imageAndMeta, err := deriveImage(b, "image/jpeg") | ||||
| 	imageAndMeta, err := decodeImage(b, "image/jpeg") | ||||
| 	suite.NoError(err) | ||||
| 
 | ||||
| 	suite.Equal(1920, imageAndMeta.width) | ||||
|  |  | |||
|  | @ -77,7 +77,7 @@ type Processor interface { | |||
| 
 | ||||
| type processor struct { | ||||
| 	tc            typeutils.TypeConverter | ||||
| 	mediaHandler  media.Handler | ||||
| 	mediaManager  media.Manager | ||||
| 	fromClientAPI chan messages.FromClientAPI | ||||
| 	oauthServer   oauth.Server | ||||
| 	filter        visibility.Filter | ||||
|  | @ -87,10 +87,10 @@ type processor struct { | |||
| } | ||||
| 
 | ||||
| // New returns a new account processor. | ||||
| func New(db db.DB, tc typeutils.TypeConverter, mediaHandler media.Handler, oauthServer oauth.Server, fromClientAPI chan messages.FromClientAPI, federator federation.Federator) Processor { | ||||
| func New(db db.DB, tc typeutils.TypeConverter, mediaManager media.Manager, oauthServer oauth.Server, fromClientAPI chan messages.FromClientAPI, federator federation.Federator) Processor { | ||||
| 	return &processor{ | ||||
| 		tc:            tc, | ||||
| 		mediaHandler:  mediaHandler, | ||||
| 		mediaManager:  mediaManager, | ||||
| 		fromClientAPI: fromClientAPI, | ||||
| 		oauthServer:   oauthServer, | ||||
| 		filter:        visibility.NewFilter(db), | ||||
|  |  | |||
|  | @ -41,7 +41,7 @@ type AccountStandardTestSuite struct { | |||
| 	db                  db.DB | ||||
| 	tc                  typeutils.TypeConverter | ||||
| 	storage             *kv.KVStore | ||||
| 	mediaHandler        media.Handler | ||||
| 	mediaManager        media.Manager | ||||
| 	oauthServer         oauth.Server | ||||
| 	fromClientAPIChan   chan messages.FromClientAPI | ||||
| 	httpClient          pub.HttpClient | ||||
|  | @ -80,7 +80,7 @@ func (suite *AccountStandardTestSuite) SetupTest() { | |||
| 	suite.db = testrig.NewTestDB() | ||||
| 	suite.tc = testrig.NewTestTypeConverter(suite.db) | ||||
| 	suite.storage = testrig.NewTestStorage() | ||||
| 	suite.mediaHandler = testrig.NewTestMediaHandler(suite.db, suite.storage) | ||||
| 	suite.mediaManager = testrig.NewTestMediaManager(suite.db, suite.storage) | ||||
| 	suite.oauthServer = testrig.NewTestOauthServer(suite.db) | ||||
| 	suite.fromClientAPIChan = make(chan messages.FromClientAPI, 100) | ||||
| 	suite.httpClient = testrig.NewMockHTTPClient(nil) | ||||
|  | @ -88,7 +88,7 @@ func (suite *AccountStandardTestSuite) SetupTest() { | |||
| 	suite.federator = testrig.NewTestFederator(suite.db, suite.transportController, suite.storage) | ||||
| 	suite.sentEmails = make(map[string]string) | ||||
| 	suite.emailSender = testrig.NewEmailSender("../../../web/template/", suite.sentEmails) | ||||
| 	suite.accountProcessor = account.New(suite.db, suite.tc, suite.mediaHandler, suite.oauthServer, suite.fromClientAPIChan, suite.federator) | ||||
| 	suite.accountProcessor = account.New(suite.db, suite.tc, suite.mediaManager, suite.oauthServer, suite.fromClientAPIChan, suite.federator) | ||||
| 	testrig.StandardDBSetup(suite.db, nil) | ||||
| 	testrig.StandardStorageSetup(suite.storage, "../../../testrig/media") | ||||
| } | ||||
|  |  | |||
|  | @ -159,7 +159,7 @@ func (p *processor) UpdateAvatar(ctx context.Context, avatar *multipart.FileHead | |||
| 	} | ||||
| 
 | ||||
| 	// do the setting | ||||
| 	avatarInfo, err := p.mediaHandler.ProcessHeaderOrAvatar(ctx, buf.Bytes(), accountID, media.TypeAvatar, "") | ||||
| 	avatarInfo, err := p.mediaManager.ProcessHeaderOrAvatar(ctx, buf.Bytes(), accountID, media.TypeAvatar, "") | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("error processing avatar: %s", err) | ||||
| 	} | ||||
|  | @ -193,7 +193,7 @@ func (p *processor) UpdateHeader(ctx context.Context, header *multipart.FileHead | |||
| 	} | ||||
| 
 | ||||
| 	// do the setting | ||||
| 	headerInfo, err := p.mediaHandler.ProcessHeaderOrAvatar(ctx, buf.Bytes(), accountID, media.TypeHeader, "") | ||||
| 	headerInfo, err := p.mediaManager.ProcessHeaderOrAvatar(ctx, buf.Bytes(), accountID, media.TypeHeader, "") | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("error processing header: %s", err) | ||||
| 	} | ||||
|  |  | |||
|  | @ -43,16 +43,16 @@ type Processor interface { | |||
| 
 | ||||
| type processor struct { | ||||
| 	tc            typeutils.TypeConverter | ||||
| 	mediaHandler  media.Handler | ||||
| 	mediaManager  media.Manager | ||||
| 	fromClientAPI chan messages.FromClientAPI | ||||
| 	db            db.DB | ||||
| } | ||||
| 
 | ||||
| // New returns a new admin processor. | ||||
| func New(db db.DB, tc typeutils.TypeConverter, mediaHandler media.Handler, fromClientAPI chan messages.FromClientAPI) Processor { | ||||
| func New(db db.DB, tc typeutils.TypeConverter, mediaManager media.Manager, fromClientAPI chan messages.FromClientAPI) Processor { | ||||
| 	return &processor{ | ||||
| 		tc:            tc, | ||||
| 		mediaHandler:  mediaHandler, | ||||
| 		mediaManager:  mediaManager, | ||||
| 		fromClientAPI: fromClientAPI, | ||||
| 		db:            db, | ||||
| 	} | ||||
|  |  | |||
|  | @ -49,8 +49,8 @@ func (p *processor) EmojiCreate(ctx context.Context, account *gtsmodel.Account, | |||
| 		return nil, errors.New("could not read provided emoji: size 0 bytes") | ||||
| 	} | ||||
| 
 | ||||
| 	// allow the mediaHandler to work its magic of processing the emoji bytes, and putting them in whatever storage backend we're using | ||||
| 	emoji, err := p.mediaHandler.ProcessLocalEmoji(ctx, buf.Bytes(), form.Shortcode) | ||||
| 	// allow the mediaManager to work its magic of processing the emoji bytes, and putting them in whatever storage backend we're using | ||||
| 	emoji, err := p.mediaManager.ProcessLocalEmoji(ctx, buf.Bytes(), form.Shortcode) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("error reading emoji: %s", err) | ||||
| 	} | ||||
|  |  | |||
|  | @ -65,8 +65,8 @@ func (p *processor) Create(ctx context.Context, account *gtsmodel.Account, form | |||
| 		}, | ||||
| 	} | ||||
| 
 | ||||
| 	// allow the mediaHandler to work its magic of processing the attachment bytes, and putting them in whatever storage backend we're using | ||||
| 	attachment, err := p.mediaHandler.ProcessAttachment(ctx, buf.Bytes(), minAttachment) | ||||
| 	// allow the mediaManager to work its magic of processing the attachment bytes, and putting them in whatever storage backend we're using | ||||
| 	attachment, err := p.mediaManager.ProcessAttachment(ctx, buf.Bytes(), minAttachment) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("error reading attachment: %s", err) | ||||
| 	} | ||||
|  |  | |||
|  | @ -43,16 +43,16 @@ type Processor interface { | |||
| 
 | ||||
| type processor struct { | ||||
| 	tc           typeutils.TypeConverter | ||||
| 	mediaHandler media.Handler | ||||
| 	mediaManager media.Manager | ||||
| 	storage      *kv.KVStore | ||||
| 	db           db.DB | ||||
| } | ||||
| 
 | ||||
| // New returns a new media processor. | ||||
| func New(db db.DB, tc typeutils.TypeConverter, mediaHandler media.Handler, storage *kv.KVStore) Processor { | ||||
| func New(db db.DB, tc typeutils.TypeConverter, mediaManager media.Manager, storage *kv.KVStore) Processor { | ||||
| 	return &processor{ | ||||
| 		tc:           tc, | ||||
| 		mediaHandler: mediaHandler, | ||||
| 		mediaManager: mediaManager, | ||||
| 		storage:      storage, | ||||
| 		db:           db, | ||||
| 	} | ||||
|  |  | |||
|  | @ -235,7 +235,7 @@ type processor struct { | |||
| 	stop            chan interface{} | ||||
| 	tc              typeutils.TypeConverter | ||||
| 	oauthServer     oauth.Server | ||||
| 	mediaHandler    media.Handler | ||||
| 	mediaManager    media.Manager | ||||
| 	storage         *kv.KVStore | ||||
| 	timelineManager timeline.Manager | ||||
| 	db              db.DB | ||||
|  | @ -259,7 +259,7 @@ func NewProcessor( | |||
| 	tc typeutils.TypeConverter, | ||||
| 	federator federation.Federator, | ||||
| 	oauthServer oauth.Server, | ||||
| 	mediaHandler media.Handler, | ||||
| 	mediaManager media.Manager, | ||||
| 	storage *kv.KVStore, | ||||
| 	timelineManager timeline.Manager, | ||||
| 	db db.DB, | ||||
|  | @ -269,9 +269,9 @@ func NewProcessor( | |||
| 
 | ||||
| 	statusProcessor := status.New(db, tc, fromClientAPI) | ||||
| 	streamingProcessor := streaming.New(db, oauthServer) | ||||
| 	accountProcessor := account.New(db, tc, mediaHandler, oauthServer, fromClientAPI, federator) | ||||
| 	adminProcessor := admin.New(db, tc, mediaHandler, fromClientAPI) | ||||
| 	mediaProcessor := mediaProcessor.New(db, tc, mediaHandler, storage) | ||||
| 	accountProcessor := account.New(db, tc, mediaManager, oauthServer, fromClientAPI, federator) | ||||
| 	adminProcessor := admin.New(db, tc, mediaManager, fromClientAPI) | ||||
| 	mediaProcessor := mediaProcessor.New(db, tc, mediaManager, storage) | ||||
| 	userProcessor := user.New(db, emailSender) | ||||
| 	federationProcessor := federationProcessor.New(db, tc, federator, fromFederator) | ||||
| 
 | ||||
|  | @ -282,7 +282,7 @@ func NewProcessor( | |||
| 		stop:            make(chan interface{}), | ||||
| 		tc:              tc, | ||||
| 		oauthServer:     oauthServer, | ||||
| 		mediaHandler:    mediaHandler, | ||||
| 		mediaManager:    mediaManager, | ||||
| 		storage:         storage, | ||||
| 		timelineManager: timelineManager, | ||||
| 		db:              db, | ||||
|  |  | |||
|  | @ -51,7 +51,7 @@ type ProcessingStandardTestSuite struct { | |||
| 	transportController transport.Controller | ||||
| 	federator           federation.Federator | ||||
| 	oauthServer         oauth.Server | ||||
| 	mediaHandler        media.Handler | ||||
| 	mediaManager        media.Manager | ||||
| 	timelineManager     timeline.Manager | ||||
| 	emailSender         email.Sender | ||||
| 
 | ||||
|  | @ -218,11 +218,11 @@ func (suite *ProcessingStandardTestSuite) SetupTest() { | |||
| 	suite.transportController = testrig.NewTestTransportController(httpClient, suite.db) | ||||
| 	suite.federator = testrig.NewTestFederator(suite.db, suite.transportController, suite.storage) | ||||
| 	suite.oauthServer = testrig.NewTestOauthServer(suite.db) | ||||
| 	suite.mediaHandler = testrig.NewTestMediaHandler(suite.db, suite.storage) | ||||
| 	suite.mediaManager = testrig.NewTestMediaManager(suite.db, suite.storage) | ||||
| 	suite.timelineManager = testrig.NewTestTimelineManager(suite.db) | ||||
| 	suite.emailSender = testrig.NewEmailSender("../../web/template/", nil) | ||||
| 
 | ||||
| 	suite.processor = processing.NewProcessor(suite.typeconverter, suite.federator, suite.oauthServer, suite.mediaHandler, suite.storage, suite.timelineManager, suite.db, suite.emailSender) | ||||
| 	suite.processor = processing.NewProcessor(suite.typeconverter, suite.federator, suite.oauthServer, suite.mediaManager, suite.storage, suite.timelineManager, suite.db, suite.emailSender) | ||||
| 
 | ||||
| 	testrig.StandardDBSetup(suite.db, suite.testAccounts) | ||||
| 	testrig.StandardStorageSetup(suite.storage, "../../testrig/media") | ||||
|  |  | |||
|  | @ -27,5 +27,5 @@ import ( | |||
| 
 | ||||
| // NewTestFederator returns a federator with the given database and (mock!!) transport controller. | ||||
| func NewTestFederator(db db.DB, tc transport.Controller, storage *kv.KVStore) federation.Federator { | ||||
| 	return federation.NewFederator(db, NewTestFederatingDB(db), tc, NewTestTypeConverter(db), NewTestMediaHandler(db, storage)) | ||||
| 	return federation.NewFederator(db, NewTestFederatingDB(db), tc, NewTestTypeConverter(db), NewTestMediaManager(db, storage)) | ||||
| } | ||||
|  |  | |||
|  | @ -24,7 +24,7 @@ import ( | |||
| 	"github.com/superseriousbusiness/gotosocial/internal/media" | ||||
| ) | ||||
| 
 | ||||
| // NewTestMediaHandler returns a media handler with the default test config, and the given db and storage. | ||||
| func NewTestMediaHandler(db db.DB, storage *kv.KVStore) media.Handler { | ||||
| // NewTestMediaManager returns a media handler with the default test config, and the given db and storage. | ||||
| func NewTestMediaManager(db db.DB, storage *kv.KVStore) media.Manager { | ||||
| 	return media.New(db, storage) | ||||
| } | ||||
|  |  | |||
|  | @ -28,5 +28,5 @@ import ( | |||
| 
 | ||||
| // NewTestProcessor returns a Processor suitable for testing purposes | ||||
| func NewTestProcessor(db db.DB, storage *kv.KVStore, federator federation.Federator, emailSender email.Sender) processing.Processor { | ||||
| 	return processing.NewProcessor(NewTestTypeConverter(db), federator, NewTestOauthServer(db), NewTestMediaHandler(db, storage), storage, NewTestTimelineManager(db), db, emailSender) | ||||
| 	return processing.NewProcessor(NewTestTypeConverter(db), federator, NewTestOauthServer(db), NewTestMediaManager(db, storage), storage, NewTestTimelineManager(db), db, emailSender) | ||||
| } | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue