summaryrefslogtreecommitdiff
path: root/internal/media
diff options
context:
space:
mode:
Diffstat (limited to 'internal/media')
-rw-r--r--internal/media/image.go37
-rw-r--r--internal/media/manager.go628
-rw-r--r--internal/media/manager_test.go411
-rw-r--r--internal/media/processingemoji.go207
-rw-r--r--internal/media/processingmedia.go287
-rw-r--r--internal/media/refetch.go6
-rw-r--r--internal/media/types.go76
-rw-r--r--internal/media/util.go1
8 files changed, 903 insertions, 750 deletions
diff --git a/internal/media/image.go b/internal/media/image.go
index 29527c085..8a34e5062 100644
--- a/internal/media/image.go
+++ b/internal/media/image.go
@@ -43,12 +43,9 @@ var (
BufferPool: &pngEncoderBufferPool{},
}
- // jpegBufferPool is a memory pool of byte buffers for JPEG encoding.
- jpegBufferPool = sync.Pool{
- New: func() any {
- return bufio.NewWriter(nil)
- },
- }
+ // jpegBufferPool is a memory pool
+ // of byte buffers for JPEG encoding.
+ jpegBufferPool sync.Pool
)
// gtsImage is a thin wrapper around the standard library image
@@ -80,25 +77,29 @@ func decodeImage(r io.Reader, opts ...imaging.DecodeOption) (*gtsImage, error) {
}
// Width returns the image width in pixels.
-func (m *gtsImage) Width() uint32 {
- return uint32(m.image.Bounds().Size().X)
+func (m *gtsImage) Width() int {
+ return m.image.Bounds().Size().X
}
// Height returns the image height in pixels.
-func (m *gtsImage) Height() uint32 {
- return uint32(m.image.Bounds().Size().Y)
+func (m *gtsImage) Height() int {
+ return m.image.Bounds().Size().Y
}
// Size returns the total number of image pixels.
-func (m *gtsImage) Size() uint64 {
- return uint64(m.image.Bounds().Size().X) *
- uint64(m.image.Bounds().Size().Y)
+func (m *gtsImage) Size() int {
+ return m.image.Bounds().Size().X *
+ m.image.Bounds().Size().Y
}
// AspectRatio returns the image ratio of width:height.
func (m *gtsImage) AspectRatio() float32 {
- return float32(m.image.Bounds().Size().X) /
- float32(m.image.Bounds().Size().Y)
+
+ // note: we cast bounds to float64 to prevent truncation
+ // and only at the end aspect ratio do we cast to float32
+ // (as the sizes are likely to be much larger than ratio).
+ return float32(float64(m.image.Bounds().Size().X) /
+ float64(m.image.Bounds().Size().Y))
}
// Thumbnail returns a small sized copy of gtsImage{}, limited to 512x512 if not small enough.
@@ -160,7 +161,11 @@ func (m *gtsImage) ToPNG() io.Reader {
// getJPEGBuffer fetches a reset JPEG encoding buffer from global JPEG buffer pool.
func getJPEGBuffer(w io.Writer) *bufio.Writer {
- buf, _ := jpegBufferPool.Get().(*bufio.Writer)
+ v := jpegBufferPool.Get()
+ if v == nil {
+ v = bufio.NewWriter(nil)
+ }
+ buf := v.(*bufio.Writer)
buf.Reset(w)
return buf
}
diff --git a/internal/media/manager.go b/internal/media/manager.go
index be428aa3b..90a2923b5 100644
--- a/internal/media/manager.go
+++ b/internal/media/manager.go
@@ -56,383 +56,413 @@ func NewManager(state *state.State) *Manager {
return &Manager{state: state}
}
-// PreProcessMedia begins the process of decoding
-// and storing the given data as an attachment.
-// It will return a pointer to a ProcessingMedia
-// struct upon which further actions can be performed,
-// such as getting the finished media, thumbnail,
-// attachment, etc.
-//
-// - data: a function that the media manager can call
-// to return a reader containing the media data.
-// - accountID: the account that the media belongs to.
-// - ai: optional and can be nil. Any additional information
-// about the attachment provided will be put in the database.
-//
-// Note: unlike ProcessMedia, this will NOT
-// queue the media to be asynchronously processed.
-func (m *Manager) PreProcessMedia(
- data DataFunc,
+// CreateMedia creates a new media attachment entry
+// in the database for given owning account ID and
+// extra information, and prepares a new processing
+// media entry to dereference it using the given
+// data function, decode the media and finish filling
+// out remaining media fields (e.g. type, path, etc).
+func (m *Manager) CreateMedia(
+ ctx context.Context,
accountID string,
- ai *AdditionalMediaInfo,
-) *ProcessingMedia {
- // Populate initial fields on the new media,
- // leaving out fields with values we don't know
- // yet. These will be overwritten as we go.
+ data DataFunc,
+ info AdditionalMediaInfo,
+) (
+ *ProcessingMedia,
+ error,
+) {
now := time.Now()
- attachment := &gtsmodel.MediaAttachment{
- ID: id.NewULID(),
- CreatedAt: now,
- UpdatedAt: now,
- Type: gtsmodel.FileTypeUnknown,
- FileMeta: gtsmodel.FileMeta{},
- AccountID: accountID,
- Processing: gtsmodel.ProcessingStatusReceived,
- File: gtsmodel.File{
- UpdatedAt: now,
- ContentType: "application/octet-stream",
- },
- Thumbnail: gtsmodel.Thumbnail{UpdatedAt: now},
- Avatar: util.Ptr(false),
- Header: util.Ptr(false),
- Cached: util.Ptr(false),
- }
- attachment.URL = uris.URIForAttachment(
+ // Generate new ID.
+ id := id.NewULID()
+
+ // Placeholder URL for attachment.
+ url := uris.URIForAttachment(
accountID,
string(TypeAttachment),
string(SizeOriginal),
- attachment.ID,
+ id,
"unknown",
)
- attachment.File.Path = uris.StoragePathForAttachment(
+ // Placeholder storage path for attachment.
+ path := uris.StoragePathForAttachment(
accountID,
string(TypeAttachment),
string(SizeOriginal),
- attachment.ID,
+ id,
"unknown",
)
- // Check if we were provided additional info
- // to add to the attachment, and overwrite
- // some of the attachment fields if so.
- if ai != nil {
- if ai.CreatedAt != nil {
- attachment.CreatedAt = *ai.CreatedAt
- }
-
- if ai.StatusID != nil {
- attachment.StatusID = *ai.StatusID
- }
-
- if ai.RemoteURL != nil {
- attachment.RemoteURL = *ai.RemoteURL
- }
-
- if ai.Description != nil {
- attachment.Description = *ai.Description
- }
+ // Calculate attachment thumbnail file path
+ thumbPath := uris.StoragePathForAttachment(
+ accountID,
+ string(TypeAttachment),
+ string(SizeSmall),
+ id,
- if ai.ScheduledStatusID != nil {
- attachment.ScheduledStatusID = *ai.ScheduledStatusID
- }
+ // Always encode attachment
+ // thumbnails as jpg.
+ "jpg",
+ )
- if ai.Blurhash != nil {
- attachment.Blurhash = *ai.Blurhash
- }
+ // Calculate attachment thumbnail URL.
+ thumbURL := uris.URIForAttachment(
+ accountID,
+ string(TypeAttachment),
+ string(SizeSmall),
+ id,
- if ai.Avatar != nil {
- attachment.Avatar = ai.Avatar
- }
+ // Always encode attachment
+ // thumbnails as jpg.
+ "jpg",
+ )
- if ai.Header != nil {
- attachment.Header = ai.Header
- }
+ // Populate initial fields on the new media,
+ // leaving out fields with values we don't know
+ // yet. These will be overwritten as we go.
+ attachment := &gtsmodel.MediaAttachment{
+ ID: id,
+ CreatedAt: now,
+ UpdatedAt: now,
+ URL: url,
+ Type: gtsmodel.FileTypeUnknown,
+ AccountID: accountID,
+ Processing: gtsmodel.ProcessingStatusReceived,
+ File: gtsmodel.File{
+ ContentType: "application/octet-stream",
+ Path: path,
+ },
+ Thumbnail: gtsmodel.Thumbnail{
+ ContentType: mimeImageJpeg, // thumbs always jpg.
+ Path: thumbPath,
+ URL: thumbURL,
+ },
+ Avatar: util.Ptr(false),
+ Header: util.Ptr(false),
+ Cached: util.Ptr(false),
+ }
- if ai.FocusX != nil {
- attachment.FileMeta.Focus.X = *ai.FocusX
- }
+ // Check if we were provided additional info
+ // to add to the attachment, and overwrite
+ // some of the attachment fields if so.
+ if info.CreatedAt != nil {
+ attachment.CreatedAt = *info.CreatedAt
+ }
+ if info.StatusID != nil {
+ attachment.StatusID = *info.StatusID
+ }
+ if info.RemoteURL != nil {
+ attachment.RemoteURL = *info.RemoteURL
+ }
+ if info.Description != nil {
+ attachment.Description = *info.Description
+ }
+ if info.ScheduledStatusID != nil {
+ attachment.ScheduledStatusID = *info.ScheduledStatusID
+ }
+ if info.Blurhash != nil {
+ attachment.Blurhash = *info.Blurhash
+ }
+ if info.Avatar != nil {
+ attachment.Avatar = info.Avatar
+ }
+ if info.Header != nil {
+ attachment.Header = info.Header
+ }
+ if info.FocusX != nil {
+ attachment.FileMeta.Focus.X = *info.FocusX
+ }
+ if info.FocusY != nil {
+ attachment.FileMeta.Focus.Y = *info.FocusY
+ }
- if ai.FocusY != nil {
- attachment.FileMeta.Focus.Y = *ai.FocusY
- }
+ // Store attachment in database in initial form.
+ err := m.state.DB.PutAttachment(ctx, attachment)
+ if err != nil {
+ return nil, err
}
- processingMedia := &ProcessingMedia{
- media: attachment,
+ // Pass prepared media as ready to be cached.
+ return m.RecacheMedia(attachment, data), nil
+}
+
+// RecacheMedia wraps a media model (assumed already
+// inserted in the database!) with given data function
+// to perform a blocking dereference / decode operation
+// from the data stream returned.
+func (m *Manager) RecacheMedia(
+ media *gtsmodel.MediaAttachment,
+ data DataFunc,
+) *ProcessingMedia {
+ return &ProcessingMedia{
+ media: media,
dataFn: data,
mgr: m,
}
-
- return processingMedia
}
-// PreProcessMediaRecache refetches, reprocesses,
-// and recaches an existing attachment that has
-// been uncached via cleaner pruning.
-//
-// Note: unlike ProcessMedia, this will NOT queue
-// the media to be asychronously processed.
-func (m *Manager) PreProcessMediaRecache(
+// CreateEmoji creates a new emoji entry in the
+// database for given shortcode, domain and extra
+// information, and prepares a new processing emoji
+// entry to dereference it using the given data
+// function, decode the media and finish filling
+// out remaining fields (e.g. type, path, etc).
+func (m *Manager) CreateEmoji(
ctx context.Context,
+ shortcode string,
+ domain string,
data DataFunc,
- attachmentID string,
-) (*ProcessingMedia, error) {
- // Get the existing attachment from database.
- attachment, err := m.state.DB.GetAttachmentByID(ctx, attachmentID)
+ info AdditionalEmojiInfo,
+) (
+ *ProcessingEmoji,
+ error,
+) {
+ now := time.Now()
+
+ // Generate new ID.
+ id := id.NewULID()
+
+ // Fetch the local instance account for emoji path generation.
+ instanceAcc, err := m.state.DB.GetInstanceAccount(ctx, "")
if err != nil {
- return nil, err
+ return nil, gtserror.Newf("error fetching instance account: %w", err)
+ }
+
+ if domain == "" && info.URI == nil {
+ // Generate URI for local emoji.
+ uri := uris.URIForEmoji(id)
+ info.URI = &uri
}
- processingMedia := &ProcessingMedia{
- media: attachment,
- dataFn: data,
- recache: true, // Indicate it's a recache.
- mgr: m,
+ // Generate static URL for attachment.
+ staticURL := uris.URIForAttachment(
+ instanceAcc.ID,
+ string(TypeEmoji),
+ string(SizeStatic),
+ id,
+
+ // All static emojis
+ // are encoded as png.
+ mimePng,
+ )
+
+ // Generate static image path for attachment.
+ staticPath := uris.StoragePathForAttachment(
+ instanceAcc.ID,
+ string(TypeEmoji),
+ string(SizeStatic),
+ id,
+
+ // All static emojis
+ // are encoded as png.
+ mimePng,
+ )
+
+ // Populate initial fields on the new emoji,
+ // leaving out fields with values we don't know
+ // yet. These will be overwritten as we go.
+ emoji := &gtsmodel.Emoji{
+ ID: id,
+ Shortcode: shortcode,
+ Domain: domain,
+ ImageStaticURL: staticURL,
+ ImageStaticPath: staticPath,
+ ImageStaticContentType: mimeImagePng,
+ Disabled: util.Ptr(false),
+ VisibleInPicker: util.Ptr(true),
+ CreatedAt: now,
+ UpdatedAt: now,
}
- return processingMedia, nil
+ // Finally, create new emoji.
+ return m.createEmoji(ctx,
+ m.state.DB.PutEmoji,
+ data,
+ emoji,
+ info,
+ )
}
-// PreProcessEmoji begins the process of decoding and storing
-// the given data as an emoji. It will return a pointer to a
-// ProcessingEmoji struct upon which further actions can be
-// performed, such as getting the finished media, thumbnail,
-// attachment, etc.
-//
-// - data: function that the media manager can call
-// to return a reader containing the emoji data.
-// - shortcode: the emoji shortcode without the ':'s around it.
-// - emojiID: database ID that should be used to store the emoji.
-// - uri: ActivityPub URI/ID of the emoji.
-// - ai: optional and can be nil. Any additional information
-// about the emoji provided will be put in the database.
-// - refresh: refetch/refresh the emoji.
-//
-// Note: unlike ProcessEmoji, this will NOT queue
-// the emoji to be asynchronously processed.
-func (m *Manager) PreProcessEmoji(
+// RefreshEmoji will prepare a recache operation
+// for the given emoji, updating it with extra
+// information, and in particular using new storage
+// paths for the dereferenced media files to skirt
+// around browser caching of the old files.
+func (m *Manager) RefreshEmoji(
ctx context.Context,
+ emoji *gtsmodel.Emoji,
data DataFunc,
- shortcode string,
- emojiID string,
- uri string,
- ai *AdditionalEmojiInfo,
- refresh bool,
-) (*ProcessingEmoji, error) {
- var (
- newPathID string
- emoji *gtsmodel.Emoji
- now = time.Now()
- )
-
+ info AdditionalEmojiInfo,
+) (
+ *ProcessingEmoji,
+ error,
+) {
// Fetch the local instance account for emoji path generation.
instanceAcc, err := m.state.DB.GetInstanceAccount(ctx, "")
if err != nil {
return nil, gtserror.Newf("error fetching instance account: %w", err)
}
- if refresh {
- // Existing emoji!
+ // Create references to old emoji image
+ // paths before they get updated with new
+ // path ID. These are required for later
+ // deleting the old image files on refresh.
+ shortcodeDomain := util.ShortcodeDomain(emoji)
+ oldStaticPath := emoji.ImageStaticPath
+ oldPath := emoji.ImagePath
+
+ // Since this is a refresh we will end up storing new images at new
+ // paths, so we should wrap closer to delete old paths at completion.
+ wrapped := func(ctx context.Context) (io.ReadCloser, int64, error) {
- emoji, err = m.state.DB.GetEmojiByID(ctx, emojiID)
+ // Call original data func.
+ rc, sz, err := data(ctx)
if err != nil {
- err = gtserror.Newf("error fetching emoji to refresh from the db: %w", err)
- return nil, err
+ return nil, 0, err
}
- // Since this is a refresh, we will end up with
- // new images stored for this emoji, so we should
- // use an io.Closer callback to perform clean up
- // of the original images from storage.
- originalData := data
- originalImagePath := emoji.ImagePath
- originalImageStaticPath := emoji.ImageStaticPath
-
- data = func(ctx context.Context) (io.ReadCloser, int64, error) {
- // Call original data func.
- rc, sz, err := originalData(ctx)
- if err != nil {
- return nil, 0, err
- }
-
- // Wrap closer to cleanup old data.
- c := iotools.CloserCallback(rc, func() {
- if err := m.state.Storage.Delete(ctx, originalImagePath); err != nil && !storage.IsNotFound(err) {
- log.Errorf(ctx, "error removing old emoji %s@%s from storage: %v", emoji.Shortcode, emoji.Domain, err)
- }
+ // Wrap closer to cleanup old data.
+ c := iotools.CloserFunc(func() error {
- if err := m.state.Storage.Delete(ctx, originalImageStaticPath); err != nil && !storage.IsNotFound(err) {
- log.Errorf(ctx, "error removing old static emoji %s@%s from storage: %v", emoji.Shortcode, emoji.Domain, err)
- }
- })
+ // First try close original.
+ if rc.Close(); err != nil {
+ return err
+ }
- // Return newly wrapped readcloser and size.
- return iotools.ReadCloser(rc, c), sz, nil
- }
+ // Remove any *old* emoji image file path now stream is closed.
+ if err := m.state.Storage.Delete(ctx, oldPath); err != nil &&
+ !storage.IsNotFound(err) {
+ log.Errorf(ctx, "error deleting old emoji %s from storage: %v", shortcodeDomain, err)
+ }
- // Reuse existing shortcode and URI -
- // these don't change when we refresh.
- emoji.Shortcode = shortcode
- emoji.URI = uri
+ // Remove any *old* emoji static image file path now stream is closed.
+ if err := m.state.Storage.Delete(ctx, oldStaticPath); err != nil &&
+ !storage.IsNotFound(err) {
+ log.Errorf(ctx, "error deleting old static emoji %s from storage: %v", shortcodeDomain, err)
+ }
- // Use a new ID to create a new path
- // for the new images, to get around
- // needing to do cache invalidation.
- newPathID, err = id.NewRandomULID()
- if err != nil {
- return nil, gtserror.Newf("error generating alternateID for emoji refresh: %s", err)
- }
+ return nil
+ })
- emoji.ImageStaticURL = uris.URIForAttachment(
- instanceAcc.ID,
- string(TypeEmoji),
- string(SizeStatic),
- newPathID,
- // All static emojis
- // are encoded as png.
- mimePng,
- )
-
- emoji.ImageStaticPath = uris.StoragePathForAttachment(
- instanceAcc.ID,
- string(TypeEmoji),
- string(SizeStatic),
- newPathID,
- // All static emojis
- // are encoded as png.
- mimePng,
- )
- } else {
- // New emoji!
-
- imageStaticURL := uris.URIForAttachment(
- instanceAcc.ID,
- string(TypeEmoji),
- string(SizeStatic),
- emojiID,
- // All static emojis
- // are encoded as png.
- mimePng,
- )
-
- imageStaticPath := uris.StoragePathForAttachment(
- instanceAcc.ID,
- string(TypeEmoji),
- string(SizeStatic),
- emojiID,
- // All static emojis
- // are encoded as png.
- mimePng,
- )
-
- // Populate initial fields on the new emoji,
- // leaving out fields with values we don't know
- // yet. These will be overwritten as we go.
- emoji = &gtsmodel.Emoji{
- ID: emojiID,
- CreatedAt: now,
- UpdatedAt: now,
- Shortcode: shortcode,
- ImageStaticURL: imageStaticURL,
- ImageStaticPath: imageStaticPath,
- ImageStaticContentType: mimeImagePng,
- ImageUpdatedAt: now,
- Disabled: util.Ptr(false),
- URI: uri,
- VisibleInPicker: util.Ptr(true),
- }
+ // Return newly wrapped readcloser and size.
+ return iotools.ReadCloser(rc, c), sz, nil
}
- // Check if we have additional info to add to the emoji,
- // and overwrite some of the emoji fields if so.
- if ai != nil {
- if ai.CreatedAt != nil {
- emoji.CreatedAt = *ai.CreatedAt
- }
-
- if ai.Domain != nil {
- emoji.Domain = *ai.Domain
- }
+ // Use a new ID to create a new path
+ // for the new images, to get around
+ // needing to do cache invalidation.
+ newPathID, err := id.NewRandomULID()
+ if err != nil {
+ return nil, gtserror.Newf("error generating newPathID for emoji refresh: %s", err)
+ }
- if ai.ImageRemoteURL != nil {
- emoji.ImageRemoteURL = *ai.ImageRemoteURL
- }
+ // Generate new static URL for emoji.
+ emoji.ImageStaticURL = uris.URIForAttachment(
+ instanceAcc.ID,
+ string(TypeEmoji),
+ string(SizeStatic),
+ newPathID,
- if ai.ImageStaticRemoteURL != nil {
- emoji.ImageStaticRemoteURL = *ai.ImageStaticRemoteURL
- }
+ // All static emojis
+ // are encoded as png.
+ mimePng,
+ )
- if ai.Disabled != nil {
- emoji.Disabled = ai.Disabled
- }
+ // Generate new static image storage path for emoji.
+ emoji.ImageStaticPath = uris.StoragePathForAttachment(
+ instanceAcc.ID,
+ string(TypeEmoji),
+ string(SizeStatic),
+ newPathID,
- if ai.VisibleInPicker != nil {
- emoji.VisibleInPicker = ai.VisibleInPicker
- }
+ // All static emojis
+ // are encoded as png.
+ mimePng,
+ )
- if ai.CategoryID != nil {
- emoji.CategoryID = *ai.CategoryID
- }
+ // Finally, create new emoji in database.
+ processingEmoji, err := m.createEmoji(ctx,
+ func(ctx context.Context, emoji *gtsmodel.Emoji) error {
+ return m.state.DB.UpdateEmoji(ctx, emoji)
+ },
+ wrapped,
+ emoji,
+ info,
+ )
+ if err != nil {
+ return nil, err
}
- processingEmoji := &ProcessingEmoji{
- emoji: emoji,
- existing: refresh,
- newPathID: newPathID,
- dataFn: data,
- mgr: m,
- }
+ // Set the refreshed path ID used.
+ processingEmoji.newPathID = newPathID
return processingEmoji, nil
}
-// PreProcessEmojiRecache refetches, reprocesses, and recaches
-// an existing emoji that has been uncached via cleaner pruning.
-//
-// Note: unlike ProcessEmoji, this will NOT queue the emoji to
-// be asychronously processed.
-func (m *Manager) PreProcessEmojiRecache(
+func (m *Manager) createEmoji(
ctx context.Context,
+ putDB func(context.Context, *gtsmodel.Emoji) error,
data DataFunc,
- emojiID string,
-) (*ProcessingEmoji, error) {
- // Get the existing emoji from the database.
- emoji, err := m.state.DB.GetEmojiByID(ctx, emojiID)
- if err != nil {
+ emoji *gtsmodel.Emoji,
+ info AdditionalEmojiInfo,
+) (
+ *ProcessingEmoji,
+ error,
+) {
+ // Check if we have additional info to add to the emoji,
+ // and overwrite some of the emoji fields if so.
+ if info.URI != nil {
+ emoji.URI = *info.URI
+ }
+ if info.CreatedAt != nil {
+ emoji.CreatedAt = *info.CreatedAt
+ }
+ if info.Domain != nil {
+ emoji.Domain = *info.Domain
+ }
+ if info.ImageRemoteURL != nil {
+ emoji.ImageRemoteURL = *info.ImageRemoteURL
+ }
+ if info.ImageStaticRemoteURL != nil {
+ emoji.ImageStaticRemoteURL = *info.ImageStaticRemoteURL
+ }
+ if info.Disabled != nil {
+ emoji.Disabled = info.Disabled
+ }
+ if info.VisibleInPicker != nil {
+ emoji.VisibleInPicker = info.VisibleInPicker
+ }
+ if info.CategoryID != nil {
+ emoji.CategoryID = *info.CategoryID
+ }
+
+ // Store emoji in database in initial form.
+ if err := putDB(ctx, emoji); err != nil {
return nil, err
}
+ // Return wrapped emoji for later processing.
processingEmoji := &ProcessingEmoji{
- emoji: emoji,
- dataFn: data,
- existing: true, // Indicate recache.
- mgr: m,
+ emoji: emoji,
+ dataFn: data,
+ mgr: m,
}
return processingEmoji, nil
}
-// ProcessEmoji will call PreProcessEmoji, followed
-// by queuing the emoji in the emoji worker queue.
-func (m *Manager) ProcessEmoji(
- ctx context.Context,
+// RecacheEmoji wraps an emoji model (assumed already
+// inserted in the database!) with given data function
+// to perform a blocking dereference / decode operation
+// from the data stream returned.
+func (m *Manager) RecacheEmoji(
+ emoji *gtsmodel.Emoji,
data DataFunc,
- shortcode string,
- id string,
- uri string,
- ai *AdditionalEmojiInfo,
- refresh bool,
-) (*ProcessingEmoji, error) {
- // Create a new processing emoji object for this emoji request.
- emoji, err := m.PreProcessEmoji(ctx, data, shortcode, id, uri, ai, refresh)
- if err != nil {
- return nil, err
+) *ProcessingEmoji {
+ return &ProcessingEmoji{
+ emoji: emoji,
+ dataFn: data,
+ mgr: m,
}
-
- // Attempt to add emoji item to the worker queue.
- m.state.Workers.Media.Queue.Push(emoji.Process)
-
- return emoji, nil
}
diff --git a/internal/media/manager_test.go b/internal/media/manager_test.go
index d184e4605..53c08eed8 100644
--- a/internal/media/manager_test.go
+++ b/internal/media/manager_test.go
@@ -40,7 +40,7 @@ type ManagerTestSuite struct {
MediaStandardTestSuite
}
-func (suite *ManagerTestSuite) TestEmojiProcessBlocking() {
+func (suite *ManagerTestSuite) TestEmojiProcess() {
ctx := context.Background()
data := func(_ context.Context) (io.ReadCloser, int64, error) {
@@ -52,27 +52,26 @@ func (suite *ManagerTestSuite) TestEmojiProcessBlocking() {
return io.NopCloser(bytes.NewBuffer(b)), int64(len(b)), nil
}
- emojiID := "01GDQ9G782X42BAMFASKP64343"
- emojiURI := "http://localhost:8080/emoji/01GDQ9G782X42BAMFASKP64343"
-
- processingEmoji, err := suite.manager.ProcessEmoji(ctx, data, "rainbow_test", emojiID, emojiURI, nil, false)
+ processing, err := suite.manager.CreateEmoji(ctx,
+ "rainbow_test",
+ "",
+ data,
+ media.AdditionalEmojiInfo{},
+ )
suite.NoError(err)
// do a blocking call to fetch the emoji
- emoji, err := processingEmoji.LoadEmoji(ctx)
+ emoji, err := processing.Load(ctx)
suite.NoError(err)
suite.NotNil(emoji)
- // make sure it's got the stuff set on it that we expect
- suite.Equal(emojiID, emoji.ID)
-
// file meta should be correctly derived from the image
suite.Equal("image/png", emoji.ImageContentType)
suite.Equal("image/png", emoji.ImageStaticContentType)
suite.Equal(36702, emoji.ImageFileSize)
// now make sure the emoji is in the database
- dbEmoji, err := suite.db.GetEmojiByID(ctx, emojiID)
+ dbEmoji, err := suite.db.GetEmojiByID(ctx, emoji.ID)
suite.NoError(err)
suite.NotNil(dbEmoji)
@@ -101,14 +100,15 @@ func (suite *ManagerTestSuite) TestEmojiProcessBlocking() {
suite.Equal(processedStaticBytesExpected, processedStaticBytes)
}
-func (suite *ManagerTestSuite) TestEmojiProcessBlockingRefresh() {
+func (suite *ManagerTestSuite) TestEmojiProcessRefresh() {
ctx := context.Background()
// we're going to 'refresh' the remote 'yell' emoji by changing the image url to the pixellated gts logo
originalEmoji := suite.testEmojis["yell"]
- emojiToUpdate := &gtsmodel.Emoji{}
- *emojiToUpdate = *originalEmoji
+ emojiToUpdate, err := suite.db.GetEmojiByID(ctx, originalEmoji.ID)
+ suite.NoError(err)
+
newImageRemoteURL := "http://fossbros-anonymous.io/some/image/path.png"
oldEmojiImagePath := emojiToUpdate.ImagePath
@@ -122,23 +122,24 @@ func (suite *ManagerTestSuite) TestEmojiProcessBlockingRefresh() {
return io.NopCloser(bytes.NewBuffer(b)), int64(len(b)), nil
}
- emojiID := emojiToUpdate.ID
- emojiURI := emojiToUpdate.URI
-
- processingEmoji, err := suite.manager.ProcessEmoji(ctx, data, "yell", emojiID, emojiURI, &media.AdditionalEmojiInfo{
- CreatedAt: &emojiToUpdate.CreatedAt,
- Domain: &emojiToUpdate.Domain,
- ImageRemoteURL: &newImageRemoteURL,
- }, true)
+ processing, err := suite.manager.RefreshEmoji(ctx,
+ emojiToUpdate,
+ data,
+ media.AdditionalEmojiInfo{
+ CreatedAt: &emojiToUpdate.CreatedAt,
+ Domain: &emojiToUpdate.Domain,
+ ImageRemoteURL: &newImageRemoteURL,
+ },
+ )
suite.NoError(err)
// do a blocking call to fetch the emoji
- emoji, err := processingEmoji.LoadEmoji(ctx)
+ emoji, err := processing.Load(ctx)
suite.NoError(err)
suite.NotNil(emoji)
// make sure it's got the stuff set on it that we expect
- suite.Equal(emojiID, emoji.ID)
+ suite.Equal(originalEmoji.ID, emoji.ID)
// file meta should be correctly derived from the image
suite.Equal("image/png", emoji.ImageContentType)
@@ -146,7 +147,7 @@ func (suite *ManagerTestSuite) TestEmojiProcessBlockingRefresh() {
suite.Equal(10296, emoji.ImageFileSize)
// now make sure the emoji is in the database
- dbEmoji, err := suite.db.GetEmojiByID(ctx, emojiID)
+ dbEmoji, err := suite.db.GetEmojiByID(ctx, emoji.ID)
suite.NoError(err)
suite.NotNil(dbEmoji)
@@ -185,7 +186,6 @@ func (suite *ManagerTestSuite) TestEmojiProcessBlockingRefresh() {
suite.NotEqual(originalEmoji.ImageStaticPath, dbEmoji.ImageStaticPath)
suite.NotEqual(originalEmoji.ImageStaticPath, dbEmoji.ImageStaticPath)
suite.NotEqual(originalEmoji.UpdatedAt, dbEmoji.UpdatedAt)
- suite.NotEqual(originalEmoji.ImageUpdatedAt, dbEmoji.ImageUpdatedAt)
// the old image files should no longer be in storage
_, err = suite.storage.Get(ctx, oldEmojiImagePath)
@@ -194,7 +194,7 @@ func (suite *ManagerTestSuite) TestEmojiProcessBlockingRefresh() {
suite.True(storage.IsNotFound(err))
}
-func (suite *ManagerTestSuite) TestEmojiProcessBlockingTooLarge() {
+func (suite *ManagerTestSuite) TestEmojiProcessTooLarge() {
ctx := context.Background()
data := func(_ context.Context) (io.ReadCloser, int64, error) {
@@ -206,19 +206,20 @@ func (suite *ManagerTestSuite) TestEmojiProcessBlockingTooLarge() {
return io.NopCloser(bytes.NewBuffer(b)), int64(len(b)), nil
}
- emojiID := "01GDQ9G782X42BAMFASKP64343"
- emojiURI := "http://localhost:8080/emoji/01GDQ9G782X42BAMFASKP64343"
-
- processingEmoji, err := suite.manager.ProcessEmoji(ctx, data, "big_panda", emojiID, emojiURI, nil, false)
+ processing, err := suite.manager.CreateEmoji(ctx,
+ "big_panda",
+ "",
+ data,
+ media.AdditionalEmojiInfo{},
+ )
suite.NoError(err)
// do a blocking call to fetch the emoji
- emoji, err := processingEmoji.LoadEmoji(ctx)
+ _, err = processing.Load(ctx)
suite.EqualError(err, "store: given emoji size 630kiB greater than max allowed 50.0kiB")
- suite.Nil(emoji)
}
-func (suite *ManagerTestSuite) TestEmojiProcessBlockingTooLargeNoSizeGiven() {
+func (suite *ManagerTestSuite) TestEmojiProcessTooLargeNoSizeGiven() {
ctx := context.Background()
data := func(_ context.Context) (io.ReadCloser, int64, error) {
@@ -230,19 +231,20 @@ func (suite *ManagerTestSuite) TestEmojiProcessBlockingTooLargeNoSizeGiven() {
return io.NopCloser(bytes.NewBuffer(b)), -1, nil
}
- emojiID := "01GDQ9G782X42BAMFASKP64343"
- emojiURI := "http://localhost:8080/emoji/01GDQ9G782X42BAMFASKP64343"
-
- processingEmoji, err := suite.manager.ProcessEmoji(ctx, data, "big_panda", emojiID, emojiURI, nil, false)
+ processing, err := suite.manager.CreateEmoji(ctx,
+ "big_panda",
+ "",
+ data,
+ media.AdditionalEmojiInfo{},
+ )
suite.NoError(err)
// do a blocking call to fetch the emoji
- emoji, err := processingEmoji.LoadEmoji(ctx)
- suite.EqualError(err, "store: calculated emoji size 630kiB greater than max allowed 50.0kiB")
- suite.Nil(emoji)
+ _, err = processing.Load(ctx)
+ suite.EqualError(err, "store: written emoji size 630kiB greater than max allowed 50.0kiB")
}
-func (suite *ManagerTestSuite) TestEmojiProcessBlockingNoFileSizeGiven() {
+func (suite *ManagerTestSuite) TestEmojiProcessNoFileSizeGiven() {
ctx := context.Background()
data := func(_ context.Context) (io.ReadCloser, int64, error) {
@@ -254,28 +256,27 @@ func (suite *ManagerTestSuite) TestEmojiProcessBlockingNoFileSizeGiven() {
return io.NopCloser(bytes.NewBuffer(b)), -1, nil
}
- emojiID := "01GDQ9G782X42BAMFASKP64343"
- emojiURI := "http://localhost:8080/emoji/01GDQ9G782X42BAMFASKP64343"
-
// process the media with no additional info provided
- processingEmoji, err := suite.manager.ProcessEmoji(ctx, data, "rainbow_test", emojiID, emojiURI, nil, false)
+ processing, err := suite.manager.CreateEmoji(ctx,
+ "rainbow_test",
+ "",
+ data,
+ media.AdditionalEmojiInfo{},
+ )
suite.NoError(err)
// do a blocking call to fetch the emoji
- emoji, err := processingEmoji.LoadEmoji(ctx)
+ emoji, err := processing.Load(ctx)
suite.NoError(err)
suite.NotNil(emoji)
- // make sure it's got the stuff set on it that we expect
- suite.Equal(emojiID, emoji.ID)
-
// file meta should be correctly derived from the image
suite.Equal("image/png", emoji.ImageContentType)
suite.Equal("image/png", emoji.ImageStaticContentType)
suite.Equal(36702, emoji.ImageFileSize)
// now make sure the emoji is in the database
- dbEmoji, err := suite.db.GetEmojiByID(ctx, emojiID)
+ dbEmoji, err := suite.db.GetEmojiByID(ctx, emoji.ID)
suite.NoError(err)
suite.NotNil(dbEmoji)
@@ -316,27 +317,27 @@ func (suite *ManagerTestSuite) TestEmojiWebpProcess() {
return io.NopCloser(bytes.NewBuffer(b)), int64(len(b)), nil
}
- emojiID := "01GDQ9G782X42BAMFASKP64343"
- emojiURI := "http://localhost:8080/emoji/01GDQ9G782X42BAMFASKP64343"
-
- processingEmoji, err := suite.manager.ProcessEmoji(ctx, data, "nb-flag", emojiID, emojiURI, nil, false)
+ // process the media with no additional info provided
+ processing, err := suite.manager.CreateEmoji(ctx,
+ "nb-flag",
+ "",
+ data,
+ media.AdditionalEmojiInfo{},
+ )
suite.NoError(err)
// do a blocking call to fetch the emoji
- emoji, err := processingEmoji.LoadEmoji(ctx)
+ emoji, err := processing.Load(ctx)
suite.NoError(err)
suite.NotNil(emoji)
- // make sure it's got the stuff set on it that we expect
- suite.Equal(emojiID, emoji.ID)
-
// file meta should be correctly derived from the image
suite.Equal("image/webp", emoji.ImageContentType)
suite.Equal("image/png", emoji.ImageStaticContentType)
suite.Equal(294, emoji.ImageFileSize)
// now make sure the emoji is in the database
- dbEmoji, err := suite.db.GetEmojiByID(ctx, emojiID)
+ dbEmoji, err := suite.db.GetEmojiByID(ctx, emoji.ID)
suite.NoError(err)
suite.NotNil(dbEmoji)
@@ -365,7 +366,7 @@ func (suite *ManagerTestSuite) TestEmojiWebpProcess() {
suite.Equal(processedStaticBytesExpected, processedStaticBytes)
}
-func (suite *ManagerTestSuite) TestSimpleJpegProcessBlocking() {
+func (suite *ManagerTestSuite) TestSimpleJpegProcess() {
ctx := context.Background()
data := func(_ context.Context) (io.ReadCloser, int64, error) {
@@ -380,18 +381,22 @@ func (suite *ManagerTestSuite) TestSimpleJpegProcessBlocking() {
accountID := "01FS1X72SK9ZPW0J1QQ68BD264"
// process the media with no additional info provided
- processingMedia := suite.manager.PreProcessMedia(data, accountID, nil)
- // fetch the attachment id from the processing media
- attachmentID := processingMedia.AttachmentID()
+ processing, err := suite.manager.CreateMedia(ctx,
+ accountID,
+ data,
+ media.AdditionalMediaInfo{},
+ )
+ suite.NoError(err)
+ suite.NotNil(processing)
// do a blocking call to fetch the attachment
- attachment, err := processingMedia.LoadAttachment(ctx)
+ attachment, err := processing.Load(ctx)
suite.NoError(err)
suite.NotNil(attachment)
// make sure it's got the stuff set on it that we expect
// the attachment ID and accountID we expect
- suite.Equal(attachmentID, attachment.ID)
+ suite.Equal(processing.ID(), attachment.ID)
suite.Equal(accountID, attachment.AccountID)
// file meta should be correctly derived from the image
@@ -407,7 +412,7 @@ func (suite *ManagerTestSuite) TestSimpleJpegProcessBlocking() {
suite.Equal("LiBzRk#6V[WF_NvzV@WY_3rqV@a$", attachment.Blurhash)
// now make sure the attachment is in the database
- dbAttachment, err := suite.db.GetAttachmentByID(ctx, attachmentID)
+ dbAttachment, err := suite.db.GetAttachmentByID(ctx, attachment.ID)
suite.NoError(err)
suite.NotNil(dbAttachment)
@@ -456,13 +461,16 @@ func (suite *ManagerTestSuite) TestSimpleJpegProcessPartial() {
accountID := "01FS1X72SK9ZPW0J1QQ68BD264"
// process the media with no additional info provided
- processingMedia := suite.manager.PreProcessMedia(data, accountID, nil)
-
- // fetch the attachment id from the processing media
- attachmentID := processingMedia.AttachmentID()
+ processing, err := suite.manager.CreateMedia(ctx,
+ accountID,
+ data,
+ media.AdditionalMediaInfo{},
+ )
+ suite.NoError(err)
+ suite.NotNil(processing)
// do a blocking call to fetch the attachment
- attachment, err := processingMedia.LoadAttachment(ctx)
+ attachment, err := processing.Load(ctx)
// Since we're cutting off the byte stream
// halfway through, we should get an error here.
@@ -471,17 +479,16 @@ func (suite *ManagerTestSuite) TestSimpleJpegProcessPartial() {
// make sure it's got the stuff set on it that we expect
// the attachment ID and accountID we expect
- suite.Equal(attachmentID, attachment.ID)
+ suite.Equal(processing.ID(), attachment.ID)
suite.Equal(accountID, attachment.AccountID)
// file meta should be correctly derived from the image
suite.Zero(attachment.FileMeta)
suite.Equal("image/jpeg", attachment.File.ContentType)
- suite.Equal("image/jpeg", attachment.Thumbnail.ContentType)
suite.Empty(attachment.Blurhash)
// now make sure the attachment is in the database
- dbAttachment, err := suite.db.GetAttachmentByID(ctx, attachmentID)
+ dbAttachment, err := suite.db.GetAttachmentByID(ctx, attachment.ID)
suite.NoError(err)
suite.NotNil(dbAttachment)
@@ -518,19 +525,22 @@ func (suite *ManagerTestSuite) TestPDFProcess() {
accountID := "01FS1X72SK9ZPW0J1QQ68BD264"
// process the media with no additional info provided
- processingMedia := suite.manager.PreProcessMedia(data, accountID, nil)
-
- // fetch the attachment id from the processing media
- attachmentID := processingMedia.AttachmentID()
+ processing, err := suite.manager.CreateMedia(ctx,
+ accountID,
+ data,
+ media.AdditionalMediaInfo{},
+ )
+ suite.NoError(err)
+ suite.NotNil(processing)
// do a blocking call to fetch the attachment
- attachment, err := processingMedia.LoadAttachment(ctx)
+ attachment, err := processing.Load(ctx)
suite.NoError(err)
suite.NotNil(attachment)
// make sure it's got the stuff set on it that we expect
// the attachment ID and accountID we expect
- suite.Equal(attachmentID, attachment.ID)
+ suite.Equal(processing.ID(), attachment.ID)
suite.Equal(accountID, attachment.AccountID)
// file meta should be correctly derived from the image
@@ -540,7 +550,7 @@ func (suite *ManagerTestSuite) TestPDFProcess() {
suite.Empty(attachment.Blurhash)
// now make sure the attachment is in the database
- dbAttachment, err := suite.db.GetAttachmentByID(ctx, attachmentID)
+ dbAttachment, err := suite.db.GetAttachmentByID(ctx, attachment.ID)
suite.NoError(err)
suite.NotNil(dbAttachment)
@@ -561,7 +571,7 @@ func (suite *ManagerTestSuite) TestPDFProcess() {
suite.False(stored)
}
-func (suite *ManagerTestSuite) TestSlothVineProcessBlocking() {
+func (suite *ManagerTestSuite) TestSlothVineProcess() {
ctx := context.Background()
data := func(_ context.Context) (io.ReadCloser, int64, error) {
@@ -576,18 +586,22 @@ func (suite *ManagerTestSuite) TestSlothVineProcessBlocking() {
accountID := "01FS1X72SK9ZPW0J1QQ68BD264"
// process the media with no additional info provided
- processingMedia := suite.manager.PreProcessMedia(data, accountID, nil)
- // fetch the attachment id from the processing media
- attachmentID := processingMedia.AttachmentID()
+ processing, err := suite.manager.CreateMedia(ctx,
+ accountID,
+ data,
+ media.AdditionalMediaInfo{},
+ )
+ suite.NoError(err)
+ suite.NotNil(processing)
// do a blocking call to fetch the attachment
- attachment, err := processingMedia.LoadAttachment(ctx)
+ attachment, err := processing.Load(ctx)
suite.NoError(err)
suite.NotNil(attachment)
// make sure it's got the stuff set on it that we expect
// the attachment ID and accountID we expect
- suite.Equal(attachmentID, attachment.ID)
+ suite.Equal(processing.ID(), attachment.ID)
suite.Equal(accountID, attachment.AccountID)
// file meta should be correctly derived from the video
@@ -607,7 +621,7 @@ func (suite *ManagerTestSuite) TestSlothVineProcessBlocking() {
suite.Equal("L00000fQfQfQfQfQfQfQfQfQfQfQ", attachment.Blurhash)
// now make sure the attachment is in the database
- dbAttachment, err := suite.db.GetAttachmentByID(ctx, attachmentID)
+ dbAttachment, err := suite.db.GetAttachmentByID(ctx, attachment.ID)
suite.NoError(err)
suite.NotNil(dbAttachment)
@@ -636,7 +650,7 @@ func (suite *ManagerTestSuite) TestSlothVineProcessBlocking() {
suite.Equal(processedThumbnailBytesExpected, processedThumbnailBytes)
}
-func (suite *ManagerTestSuite) TestLongerMp4ProcessBlocking() {
+func (suite *ManagerTestSuite) TestLongerMp4Process() {
ctx := context.Background()
data := func(_ context.Context) (io.ReadCloser, int64, error) {
@@ -651,18 +665,22 @@ func (suite *ManagerTestSuite) TestLongerMp4ProcessBlocking() {
accountID := "01FS1X72SK9ZPW0J1QQ68BD264"
// process the media with no additional info provided
- processingMedia := suite.manager.PreProcessMedia(data, accountID, nil)
- // fetch the attachment id from the processing media
- attachmentID := processingMedia.AttachmentID()
+ processing, err := suite.manager.CreateMedia(ctx,
+ accountID,
+ data,
+ media.AdditionalMediaInfo{},
+ )
+ suite.NoError(err)
+ suite.NotNil(processing)
// do a blocking call to fetch the attachment
- attachment, err := processingMedia.LoadAttachment(ctx)
+ attachment, err := processing.Load(ctx)
suite.NoError(err)
suite.NotNil(attachment)
// make sure it's got the stuff set on it that we expect
// the attachment ID and accountID we expect
- suite.Equal(attachmentID, attachment.ID)
+ suite.Equal(processing.ID(), attachment.ID)
suite.Equal(accountID, attachment.AccountID)
// file meta should be correctly derived from the video
@@ -682,7 +700,7 @@ func (suite *ManagerTestSuite) TestLongerMp4ProcessBlocking() {
suite.Equal("L00000fQfQfQfQfQfQfQfQfQfQfQ", attachment.Blurhash)
// now make sure the attachment is in the database
- dbAttachment, err := suite.db.GetAttachmentByID(ctx, attachmentID)
+ dbAttachment, err := suite.db.GetAttachmentByID(ctx, attachment.ID)
suite.NoError(err)
suite.NotNil(dbAttachment)
@@ -711,7 +729,7 @@ func (suite *ManagerTestSuite) TestLongerMp4ProcessBlocking() {
suite.Equal(processedThumbnailBytesExpected, processedThumbnailBytes)
}
-func (suite *ManagerTestSuite) TestBirdnestMp4ProcessBlocking() {
+func (suite *ManagerTestSuite) TestBirdnestMp4Process() {
ctx := context.Background()
data := func(_ context.Context) (io.ReadCloser, int64, error) {
@@ -726,18 +744,22 @@ func (suite *ManagerTestSuite) TestBirdnestMp4ProcessBlocking() {
accountID := "01FS1X72SK9ZPW0J1QQ68BD264"
// process the media with no additional info provided
- processingMedia := suite.manager.PreProcessMedia(data, accountID, nil)
- // fetch the attachment id from the processing media
- attachmentID := processingMedia.AttachmentID()
+ processing, err := suite.manager.CreateMedia(ctx,
+ accountID,
+ data,
+ media.AdditionalMediaInfo{},
+ )
+ suite.NoError(err)
+ suite.NotNil(processing)
// do a blocking call to fetch the attachment
- attachment, err := processingMedia.LoadAttachment(ctx)
+ attachment, err := processing.Load(ctx)
suite.NoError(err)
suite.NotNil(attachment)
// make sure it's got the stuff set on it that we expect
// the attachment ID and accountID we expect
- suite.Equal(attachmentID, attachment.ID)
+ suite.Equal(processing.ID(), attachment.ID)
suite.Equal(accountID, attachment.AccountID)
// file meta should be correctly derived from the video
@@ -757,7 +779,7 @@ func (suite *ManagerTestSuite) TestBirdnestMp4ProcessBlocking() {
suite.Equal("L00000fQfQfQfQfQfQfQfQfQfQfQ", attachment.Blurhash)
// now make sure the attachment is in the database
- dbAttachment, err := suite.db.GetAttachmentByID(ctx, attachmentID)
+ dbAttachment, err := suite.db.GetAttachmentByID(ctx, attachment.ID)
suite.NoError(err)
suite.NotNil(dbAttachment)
@@ -786,7 +808,7 @@ func (suite *ManagerTestSuite) TestBirdnestMp4ProcessBlocking() {
suite.Equal(processedThumbnailBytesExpected, processedThumbnailBytes)
}
-func (suite *ManagerTestSuite) TestNotAnMp4ProcessBlocking() {
+func (suite *ManagerTestSuite) TestNotAnMp4Process() {
// try to load an 'mp4' that's actually an mkv in disguise
ctx := context.Background()
@@ -803,10 +825,16 @@ func (suite *ManagerTestSuite) TestNotAnMp4ProcessBlocking() {
accountID := "01FS1X72SK9ZPW0J1QQ68BD264"
// pre processing should go fine but...
- processingMedia := suite.manager.PreProcessMedia(data, accountID, nil)
+ processing, err := suite.manager.CreateMedia(ctx,
+ accountID,
+ data,
+ media.AdditionalMediaInfo{},
+ )
+ suite.NoError(err)
+ suite.NotNil(processing)
// we should get an error while loading
- attachment, err := processingMedia.LoadAttachment(ctx)
+ attachment, err := processing.Load(ctx)
suite.EqualError(err, "finish: error decoding video: error determining video metadata: [width height framerate]")
// partial attachment should be
@@ -815,7 +843,7 @@ func (suite *ManagerTestSuite) TestNotAnMp4ProcessBlocking() {
suite.Equal(gtsmodel.FileTypeUnknown, attachment.Type)
}
-func (suite *ManagerTestSuite) TestSimpleJpegProcessBlockingNoContentLengthGiven() {
+func (suite *ManagerTestSuite) TestSimpleJpegProcessNoContentLengthGiven() {
ctx := context.Background()
data := func(_ context.Context) (io.ReadCloser, int64, error) {
@@ -831,18 +859,22 @@ func (suite *ManagerTestSuite) TestSimpleJpegProcessBlockingNoContentLengthGiven
accountID := "01FS1X72SK9ZPW0J1QQ68BD264"
// process the media with no additional info provided
- processingMedia := suite.manager.PreProcessMedia(data, accountID, nil)
- // fetch the attachment id from the processing media
- attachmentID := processingMedia.AttachmentID()
+ processing, err := suite.manager.CreateMedia(ctx,
+ accountID,
+ data,
+ media.AdditionalMediaInfo{},
+ )
+ suite.NoError(err)
+ suite.NotNil(processing)
// do a blocking call to fetch the attachment
- attachment, err := processingMedia.LoadAttachment(ctx)
+ attachment, err := processing.Load(ctx)
suite.NoError(err)
suite.NotNil(attachment)
// make sure it's got the stuff set on it that we expect
// the attachment ID and accountID we expect
- suite.Equal(attachmentID, attachment.ID)
+ suite.Equal(processing.ID(), attachment.ID)
suite.Equal(accountID, attachment.AccountID)
// file meta should be correctly derived from the image
@@ -858,7 +890,7 @@ func (suite *ManagerTestSuite) TestSimpleJpegProcessBlockingNoContentLengthGiven
suite.Equal("LiBzRk#6V[WF_NvzV@WY_3rqV@a$", attachment.Blurhash)
// now make sure the attachment is in the database
- dbAttachment, err := suite.db.GetAttachmentByID(ctx, attachmentID)
+ dbAttachment, err := suite.db.GetAttachmentByID(ctx, attachment.ID)
suite.NoError(err)
suite.NotNil(dbAttachment)
@@ -887,7 +919,7 @@ func (suite *ManagerTestSuite) TestSimpleJpegProcessBlockingNoContentLengthGiven
suite.Equal(processedThumbnailBytesExpected, processedThumbnailBytes)
}
-func (suite *ManagerTestSuite) TestSimpleJpegProcessBlockingReadCloser() {
+func (suite *ManagerTestSuite) TestSimpleJpegProcessReadCloser() {
ctx := context.Background()
data := func(_ context.Context) (io.ReadCloser, int64, error) {
@@ -903,18 +935,22 @@ func (suite *ManagerTestSuite) TestSimpleJpegProcessBlockingReadCloser() {
accountID := "01FS1X72SK9ZPW0J1QQ68BD264"
// process the media with no additional info provided
- processingMedia := suite.manager.PreProcessMedia(data, accountID, nil)
- // fetch the attachment id from the processing media
- attachmentID := processingMedia.AttachmentID()
+ processing, err := suite.manager.CreateMedia(ctx,
+ accountID,
+ data,
+ media.AdditionalMediaInfo{},
+ )
+ suite.NoError(err)
+ suite.NotNil(processing)
// do a blocking call to fetch the attachment
- attachment, err := processingMedia.LoadAttachment(ctx)
+ attachment, err := processing.Load(ctx)
suite.NoError(err)
suite.NotNil(attachment)
// make sure it's got the stuff set on it that we expect
// the attachment ID and accountID we expect
- suite.Equal(attachmentID, attachment.ID)
+ suite.Equal(processing.ID(), attachment.ID)
suite.Equal(accountID, attachment.AccountID)
// file meta should be correctly derived from the image
@@ -930,7 +966,7 @@ func (suite *ManagerTestSuite) TestSimpleJpegProcessBlockingReadCloser() {
suite.Equal("LiBzRk#6V[WF_NvzV@WY_3rqV@a$", attachment.Blurhash)
// now make sure the attachment is in the database
- dbAttachment, err := suite.db.GetAttachmentByID(ctx, attachmentID)
+ dbAttachment, err := suite.db.GetAttachmentByID(ctx, attachment.ID)
suite.NoError(err)
suite.NotNil(dbAttachment)
@@ -959,7 +995,7 @@ func (suite *ManagerTestSuite) TestSimpleJpegProcessBlockingReadCloser() {
suite.Equal(processedThumbnailBytesExpected, processedThumbnailBytes)
}
-func (suite *ManagerTestSuite) TestPngNoAlphaChannelProcessBlocking() {
+func (suite *ManagerTestSuite) TestPngNoAlphaChannelProcess() {
ctx := context.Background()
data := func(_ context.Context) (io.ReadCloser, int64, error) {
@@ -974,18 +1010,22 @@ func (suite *ManagerTestSuite) TestPngNoAlphaChannelProcessBlocking() {
accountID := "01FS1X72SK9ZPW0J1QQ68BD264"
// process the media with no additional info provided
- processingMedia := suite.manager.PreProcessMedia(data, accountID, nil)
- // fetch the attachment id from the processing media
- attachmentID := processingMedia.AttachmentID()
+ processing, err := suite.manager.CreateMedia(ctx,
+ accountID,
+ data,
+ media.AdditionalMediaInfo{},
+ )
+ suite.NoError(err)
+ suite.NotNil(processing)
// do a blocking call to fetch the attachment
- attachment, err := processingMedia.LoadAttachment(ctx)
+ attachment, err := processing.Load(ctx)
suite.NoError(err)
suite.NotNil(attachment)
// make sure it's got the stuff set on it that we expect
// the attachment ID and accountID we expect
- suite.Equal(attachmentID, attachment.ID)
+ suite.Equal(processing.ID(), attachment.ID)
suite.Equal(accountID, attachment.AccountID)
// file meta should be correctly derived from the image
@@ -1001,7 +1041,7 @@ func (suite *ManagerTestSuite) TestPngNoAlphaChannelProcessBlocking() {
suite.Equal("LFQT7e.A%O%4?co$M}M{_1W9~TxV", attachment.Blurhash)
// now make sure the attachment is in the database
- dbAttachment, err := suite.db.GetAttachmentByID(ctx, attachmentID)
+ dbAttachment, err := suite.db.GetAttachmentByID(ctx, attachment.ID)
suite.NoError(err)
suite.NotNil(dbAttachment)
@@ -1030,7 +1070,7 @@ func (suite *ManagerTestSuite) TestPngNoAlphaChannelProcessBlocking() {
suite.Equal(processedThumbnailBytesExpected, processedThumbnailBytes)
}
-func (suite *ManagerTestSuite) TestPngAlphaChannelProcessBlocking() {
+func (suite *ManagerTestSuite) TestPngAlphaChannelProcess() {
ctx := context.Background()
data := func(_ context.Context) (io.ReadCloser, int64, error) {
@@ -1045,18 +1085,22 @@ func (suite *ManagerTestSuite) TestPngAlphaChannelProcessBlocking() {
accountID := "01FS1X72SK9ZPW0J1QQ68BD264"
// process the media with no additional info provided
- processingMedia := suite.manager.PreProcessMedia(data, accountID, nil)
- // fetch the attachment id from the processing media
- attachmentID := processingMedia.AttachmentID()
+ processing, err := suite.manager.CreateMedia(ctx,
+ accountID,
+ data,
+ media.AdditionalMediaInfo{},
+ )
+ suite.NoError(err)
+ suite.NotNil(processing)
// do a blocking call to fetch the attachment
- attachment, err := processingMedia.LoadAttachment(ctx)
+ attachment, err := processing.Load(ctx)
suite.NoError(err)
suite.NotNil(attachment)
// make sure it's got the stuff set on it that we expect
// the attachment ID and accountID we expect
- suite.Equal(attachmentID, attachment.ID)
+ suite.Equal(processing.ID(), attachment.ID)
suite.Equal(accountID, attachment.AccountID)
// file meta should be correctly derived from the image
@@ -1072,7 +1116,7 @@ func (suite *ManagerTestSuite) TestPngAlphaChannelProcessBlocking() {
suite.Equal("LFQT7e.A%O%4?co$M}M{_1W9~TxV", attachment.Blurhash)
// now make sure the attachment is in the database
- dbAttachment, err := suite.db.GetAttachmentByID(ctx, attachmentID)
+ dbAttachment, err := suite.db.GetAttachmentByID(ctx, attachment.ID)
suite.NoError(err)
suite.NotNil(dbAttachment)
@@ -1101,7 +1145,7 @@ func (suite *ManagerTestSuite) TestPngAlphaChannelProcessBlocking() {
suite.Equal(processedThumbnailBytesExpected, processedThumbnailBytes)
}
-func (suite *ManagerTestSuite) TestSimpleJpegProcessBlockingWithCallback() {
+func (suite *ManagerTestSuite) TestSimpleJpegProcessWithCallback() {
ctx := context.Background()
data := func(_ context.Context) (io.ReadCloser, int64, error) {
@@ -1116,18 +1160,22 @@ func (suite *ManagerTestSuite) TestSimpleJpegProcessBlockingWithCallback() {
accountID := "01FS1X72SK9ZPW0J1QQ68BD264"
// process the media with no additional info provided
- processingMedia := suite.manager.PreProcessMedia(data, accountID, nil)
- // fetch the attachment id from the processing media
- attachmentID := processingMedia.AttachmentID()
+ processing, err := suite.manager.CreateMedia(ctx,
+ accountID,
+ data,
+ media.AdditionalMediaInfo{},
+ )
+ suite.NoError(err)
+ suite.NotNil(processing)
// do a blocking call to fetch the attachment
- attachment, err := processingMedia.LoadAttachment(ctx)
+ attachment, err := processing.Load(ctx)
suite.NoError(err)
suite.NotNil(attachment)
// make sure it's got the stuff set on it that we expect
// the attachment ID and accountID we expect
- suite.Equal(attachmentID, attachment.ID)
+ suite.Equal(processing.ID(), attachment.ID)
suite.Equal(accountID, attachment.AccountID)
// file meta should be correctly derived from the image
@@ -1143,7 +1191,7 @@ func (suite *ManagerTestSuite) TestSimpleJpegProcessBlockingWithCallback() {
suite.Equal("LiBzRk#6V[WF_NvzV@WY_3rqV@a$", attachment.Blurhash)
// now make sure the attachment is in the database
- dbAttachment, err := suite.db.GetAttachmentByID(ctx, attachmentID)
+ dbAttachment, err := suite.db.GetAttachmentByID(ctx, attachment.ID)
suite.NoError(err)
suite.NotNil(dbAttachment)
@@ -1172,7 +1220,7 @@ func (suite *ManagerTestSuite) TestSimpleJpegProcessBlockingWithCallback() {
suite.Equal(processedThumbnailBytesExpected, processedThumbnailBytes)
}
-func (suite *ManagerTestSuite) TestSimpleJpegProcessBlockingWithDiskStorage() {
+func (suite *ManagerTestSuite) TestSimpleJpegProcessWithDiskStorage() {
ctx := context.Background()
data := func(_ context.Context) (io.ReadCloser, int64, error) {
@@ -1209,18 +1257,22 @@ func (suite *ManagerTestSuite) TestSimpleJpegProcessBlockingWithDiskStorage() {
suite.manager = diskManager
// process the media with no additional info provided
- processingMedia := diskManager.PreProcessMedia(data, accountID, nil)
- // fetch the attachment id from the processing media
- attachmentID := processingMedia.AttachmentID()
+ processing, err := suite.manager.CreateMedia(ctx,
+ accountID,
+ data,
+ media.AdditionalMediaInfo{},
+ )
+ suite.NoError(err)
+ suite.NotNil(processing)
// do a blocking call to fetch the attachment
- attachment, err := processingMedia.LoadAttachment(ctx)
+ attachment, err := processing.Load(ctx)
suite.NoError(err)
suite.NotNil(attachment)
// make sure it's got the stuff set on it that we expect
// the attachment ID and accountID we expect
- suite.Equal(attachmentID, attachment.ID)
+ suite.Equal(processing.ID(), attachment.ID)
suite.Equal(accountID, attachment.AccountID)
// file meta should be correctly derived from the image
@@ -1236,7 +1288,7 @@ func (suite *ManagerTestSuite) TestSimpleJpegProcessBlockingWithDiskStorage() {
suite.Equal("LiBzRk#6V[WF_NvzV@WY_3rqV@a$", attachment.Blurhash)
// now make sure the attachment is in the database
- dbAttachment, err := suite.db.GetAttachmentByID(ctx, attachmentID)
+ dbAttachment, err := suite.db.GetAttachmentByID(ctx, attachment.ID)
suite.NoError(err)
suite.NotNil(dbAttachment)
@@ -1307,22 +1359,27 @@ func (suite *ManagerTestSuite) TestSmallSizedMediaTypeDetection_issue2263() {
accountID := "01FS1X72SK9ZPW0J1QQ68BD264"
// process the media with no additional info provided
- processingMedia := suite.manager.PreProcessMedia(data, accountID, nil)
- if _, err := processingMedia.LoadAttachment(ctx); err != nil {
- suite.FailNow(err.Error())
- }
-
- attachmentID := processingMedia.AttachmentID()
+ processing, err := suite.manager.CreateMedia(ctx,
+ accountID,
+ data,
+ media.AdditionalMediaInfo{},
+ )
+ suite.NoError(err)
+ suite.NotNil(processing)
+
+ // Load the attachment (but ignore return).
+ _, err = processing.Load(ctx)
+ suite.NoError(err)
// fetch the attachment id from the processing media
- attachment, err := suite.db.GetAttachmentByID(ctx, attachmentID)
+ attachment, err := suite.db.GetAttachmentByID(ctx, processing.ID())
if err != nil {
suite.FailNow(err.Error())
}
// make sure it's got the stuff set on it that we expect
// the attachment ID and accountID we expect
- suite.Equal(attachmentID, attachment.ID)
+ suite.Equal(processing.ID(), attachment.ID)
suite.Equal(accountID, attachment.AccountID)
actual := attachment.File.ContentType
@@ -1350,13 +1407,21 @@ func (suite *ManagerTestSuite) TestMisreportedSmallMedia() {
return io.NopCloser(bytes.NewBuffer(b)), int64(2 * actualSize), nil
}
- // Process the media with no additional info provided.
- attachment, err := suite.manager.
- PreProcessMedia(data, accountID, nil).
- LoadAttachment(context.Background())
- if err != nil {
- suite.FailNow(err.Error())
- }
+ ctx := context.Background()
+
+ // process the media with no additional info provided
+ processing, err := suite.manager.CreateMedia(ctx,
+ accountID,
+ data,
+ media.AdditionalMediaInfo{},
+ )
+ suite.NoError(err)
+ suite.NotNil(processing)
+
+ // do a blocking call to fetch the attachment
+ attachment, err := processing.Load(ctx)
+ suite.NoError(err)
+ suite.NotNil(attachment)
suite.Equal(actualSize, attachment.File.FileSize)
}
@@ -1378,13 +1443,21 @@ func (suite *ManagerTestSuite) TestNoReportedSizeSmallMedia() {
return io.NopCloser(bytes.NewBuffer(b)), 0, nil
}
- // Process the media with no additional info provided.
- attachment, err := suite.manager.
- PreProcessMedia(data, accountID, nil).
- LoadAttachment(context.Background())
- if err != nil {
- suite.FailNow(err.Error())
- }
+ ctx := context.Background()
+
+ // process the media with no additional info provided
+ processing, err := suite.manager.CreateMedia(ctx,
+ accountID,
+ data,
+ media.AdditionalMediaInfo{},
+ )
+ suite.NoError(err)
+ suite.NotNil(processing)
+
+ // do a blocking call to fetch the attachment
+ attachment, err := processing.Load(ctx)
+ suite.NoError(err)
+ suite.NotNil(attachment)
suite.Equal(actualSize, attachment.File.FileSize)
}
diff --git a/internal/media/processingemoji.go b/internal/media/processingemoji.go
index b62c4f76e..d61043523 100644
--- a/internal/media/processingemoji.go
+++ b/internal/media/processingemoji.go
@@ -24,14 +24,16 @@ import (
"slices"
"codeberg.org/gruf/go-bytesize"
- "codeberg.org/gruf/go-errors/v2"
+ errorsv2 "codeberg.org/gruf/go-errors/v2"
"codeberg.org/gruf/go-runners"
"github.com/h2non/filetype"
"github.com/superseriousbusiness/gotosocial/internal/config"
+ "github.com/superseriousbusiness/gotosocial/internal/gtscontext"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/log"
"github.com/superseriousbusiness/gotosocial/internal/regexes"
+ "github.com/superseriousbusiness/gotosocial/internal/storage"
"github.com/superseriousbusiness/gotosocial/internal/uris"
"github.com/superseriousbusiness/gotosocial/internal/util"
)
@@ -40,7 +42,6 @@ import (
// various functions for retrieving data from the process.
type ProcessingEmoji struct {
emoji *gtsmodel.Emoji // processing emoji details
- existing bool // indicates whether this is an existing emoji ID being refreshed / recached
newPathID string // new emoji path ID to use when being refreshed
dataFn DataFunc // load-data function, returns media stream
done bool // done is set when process finishes with non ctx canceled type error
@@ -49,61 +50,72 @@ type ProcessingEmoji struct {
mgr *Manager // mgr instance (access to db / storage)
}
-// EmojiID returns the ID of the underlying emoji without blocking processing.
-func (p *ProcessingEmoji) EmojiID() string {
+// ID returns the ID of the underlying emoji.
+func (p *ProcessingEmoji) ID() string {
return p.emoji.ID // immutable, safe outside mutex.
}
// LoadEmoji blocks until the static and fullsize image has been processed, and then returns the completed emoji.
-func (p *ProcessingEmoji) LoadEmoji(ctx context.Context) (*gtsmodel.Emoji, error) {
- // Attempt to load synchronously.
+func (p *ProcessingEmoji) Load(ctx context.Context) (*gtsmodel.Emoji, error) {
emoji, done, err := p.load(ctx)
- if err == nil {
- // No issue, return media.
- return emoji, nil
- }
-
if !done {
- // Provided context was cancelled, e.g. request cancelled
- // early. Queue this item for asynchronous processing.
- log.Warnf(ctx, "reprocessing emoji %s after canceled ctx", p.emoji.ID)
- p.mgr.state.Workers.Media.Queue.Push(p.Process)
- }
-
- return nil, err
-}
-
-// Process allows the receiving object to fit the runners.WorkerFunc signature. It performs a (blocking) load and logs on error.
-func (p *ProcessingEmoji) Process(ctx context.Context) {
- if _, _, err := p.load(ctx); err != nil {
- log.Errorf(ctx, "error processing emoji: %v", err)
+ // On a context-canceled error (marked as !done), requeue for loading.
+ p.mgr.state.Workers.Dereference.Queue.Push(func(ctx context.Context) {
+ if _, _, err := p.load(ctx); err != nil {
+ log.Errorf(ctx, "error loading emoji: %v", err)
+ }
+ })
}
+ return emoji, err
}
-// load performs a concurrency-safe load of ProcessingEmoji, only marking itself as complete when returned error is NOT a context cancel.
-func (p *ProcessingEmoji) load(ctx context.Context) (*gtsmodel.Emoji, bool, error) {
- var (
- done bool
- err error
- )
-
+// load is the package private form of load() that is wrapped to catch context canceled.
+func (p *ProcessingEmoji) load(ctx context.Context) (
+ emoji *gtsmodel.Emoji,
+ done bool,
+ err error,
+) {
err = p.proc.Process(func() error {
- if p.done {
+ if done = p.done; done {
// Already proc'd.
return p.err
}
defer func() {
// This is only done when ctx NOT cancelled.
- done = err == nil || !errors.IsV2(err,
+ done = (err == nil || !errorsv2.IsV2(err,
context.Canceled,
context.DeadlineExceeded,
+ ))
+
+ if !done {
+ return
+ }
+
+ // Anything from here, we
+ // need to ensure happens
+ // (i.e. no ctx canceled).
+ ctx = gtscontext.WithValues(
+ context.Background(),
+ ctx, // values
)
+ // On error, clean
+ // downloaded files.
+ if err != nil {
+ p.cleanup(ctx)
+ }
+
if !done {
return
}
+ // Update with latest details, whatever happened.
+ e := p.mgr.state.DB.UpdateEmoji(ctx, p.emoji)
+ if e != nil {
+ log.Errorf(ctx, "error updating emoji in db: %v", e)
+ }
+
// Store final values.
p.done = true
p.err = err
@@ -111,39 +123,31 @@ func (p *ProcessingEmoji) load(ctx context.Context) (*gtsmodel.Emoji, bool, erro
// Attempt to store media and calculate
// full-size media attachment details.
+ //
+ // This will update p.emoji as it goes.
if err = p.store(ctx); err != nil {
return err
}
// Finish processing by reloading media into
// memory to get dimension and generate a thumb.
+ //
+ // This will update p.emoji as it goes.
if err = p.finish(ctx); err != nil {
- return err
- }
-
- if p.existing {
- // Existing emoji we're updating, so only update.
- err = p.mgr.state.DB.UpdateEmoji(ctx, p.emoji)
- return err
+ return err //nolint:revive
}
- // New emoji media, first time caching.
- err = p.mgr.state.DB.PutEmoji(ctx, p.emoji)
- return err
+ return nil
})
-
- if err != nil {
- return nil, done, err
- }
-
- return p.emoji, done, nil
+ emoji = p.emoji
+ return
}
// store calls the data function attached to p if it hasn't been called yet,
// and updates the underlying attachment fields as necessary. It will then stream
// bytes from p's reader directly into storage so that it can be retrieved later.
func (p *ProcessingEmoji) store(ctx context.Context) error {
- // Load media from provided data fn.
+ // Load media from provided data fun
rc, sz, err := p.dataFn(ctx)
if err != nil {
return gtserror.Newf("error executing data function: %w", err)
@@ -168,8 +172,9 @@ func (p *ProcessingEmoji) store(ctx context.Context) error {
// Check that provided size isn't beyond max. We check beforehand
// so that we don't attempt to stream the emoji into storage if not needed.
- if size := bytesize.Size(sz); sz > 0 && size > maxSize {
- return gtserror.Newf("given emoji size %s greater than max allowed %s", size, maxSize)
+ if sz > 0 && sz > int64(maxSize) {
+ sz := bytesize.Size(sz) // improves log readability
+ return gtserror.Newf("given emoji size %s greater than max allowed %s", sz, maxSize)
}
// Prepare to read bytes from
@@ -196,14 +201,14 @@ func (p *ProcessingEmoji) store(ctx context.Context) error {
// Initial file size was misreported, so we didn't read
// fully into hdrBuf. Reslice it to the size we did read.
- log.Warnf(ctx,
- "recovered from misreported file size; reported %d; read %d",
- fileSize, n,
- )
hdrBuf = hdrBuf[:n]
+ fileSize = n
+ p.emoji.ImageFileSize = fileSize
}
// Parse file type info from header buffer.
+ // This should only ever error if the buffer
+ // is empty (ie., the attachment is 0 bytes).
info, err := filetype.Match(hdrBuf)
if err != nil {
return gtserror.Newf("error parsing file type: %w", err)
@@ -227,10 +232,13 @@ func (p *ProcessingEmoji) store(ctx context.Context) error {
pathID = p.emoji.ID
}
- // Determine instance account ID from already generated image static path.
- instanceAccID := regexes.FilePath.FindStringSubmatch(p.emoji.ImageStaticPath)[1]
+ // Determine instance account ID from generated image static path.
+ instanceAccID, ok := getInstanceAccountID(p.emoji.ImageStaticPath)
+ if !ok {
+ return gtserror.Newf("invalid emoji static path; no instance account id: %s", p.emoji.ImageStaticPath)
+ }
- // Calculate emoji file path.
+ // Calculate final media attachment file path.
p.emoji.ImagePath = uris.StoragePathForAttachment(
instanceAccID,
string(TypeEmoji),
@@ -239,32 +247,32 @@ func (p *ProcessingEmoji) store(ctx context.Context) error {
info.Extension,
)
- // This shouldn't already exist, but we do a check as it's worth logging.
+ // File shouldn't already exist in storage at this point,
+ // but we do a check as it's worth logging / cleaning up.
if have, _ := p.mgr.state.Storage.Has(ctx, p.emoji.ImagePath); have {
- log.Warnf(ctx, "emoji already exists at storage path: %s", p.emoji.ImagePath)
+ log.Warnf(ctx, "emoji already exists at: %s", p.emoji.ImagePath)
// Attempt to remove existing emoji at storage path (might be broken / out-of-date)
if err := p.mgr.state.Storage.Delete(ctx, p.emoji.ImagePath); err != nil {
- return gtserror.Newf("error removing emoji from storage: %v", err)
+ return gtserror.Newf("error removing emoji %s from storage: %v", p.emoji.ImagePath, err)
}
}
// Write the final image reader stream to our storage.
- wroteSize, err := p.mgr.state.Storage.PutStream(ctx, p.emoji.ImagePath, r)
+ sz, err = p.mgr.state.Storage.PutStream(ctx, p.emoji.ImagePath, r)
if err != nil {
return gtserror.Newf("error writing emoji to storage: %w", err)
}
- // Once again check size in case none was provided previously.
- if size := bytesize.Size(wroteSize); size > maxSize {
- if err := p.mgr.state.Storage.Delete(ctx, p.emoji.ImagePath); err != nil {
- log.Errorf(ctx, "error removing too-large-emoji from storage: %v", err)
- }
-
- return gtserror.Newf("calculated emoji size %s greater than max allowed %s", size, maxSize)
+ // Perform final size check in case none was
+ // given previously, or size was mis-reported.
+ // (error here will later perform p.cleanup()).
+ if sz > int64(maxSize) {
+ sz := bytesize.Size(sz) // improves log readability
+ return gtserror.Newf("written emoji size %s greater than max allowed %s", sz, maxSize)
}
- // Fill in remaining attachment data now it's stored.
+ // Fill in remaining emoji data now it's stored.
p.emoji.ImageURL = uris.URIForAttachment(
instanceAccID,
string(TypeEmoji),
@@ -273,14 +281,14 @@ func (p *ProcessingEmoji) store(ctx context.Context) error {
info.Extension,
)
p.emoji.ImageContentType = info.MIME.Value
- p.emoji.ImageFileSize = int(wroteSize)
+ p.emoji.ImageFileSize = int(sz)
p.emoji.Cached = util.Ptr(true)
return nil
}
func (p *ProcessingEmoji) finish(ctx context.Context) error {
- // Fetch a stream to the original file in storage.
+ // Get a stream to the original file for further processing.
rc, err := p.mgr.state.Storage.GetStream(ctx, p.emoji.ImagePath)
if err != nil {
return gtserror.Newf("error loading file from storage: %w", err)
@@ -293,32 +301,69 @@ func (p *ProcessingEmoji) finish(ctx context.Context) error {
return gtserror.Newf("error decoding image: %w", err)
}
- // The image should be in-memory by now.
+ // staticImg should be in-memory by
+ // now so we're done with storage.
if err := rc.Close(); err != nil {
return gtserror.Newf("error closing file: %w", err)
}
- // This shouldn't already exist, but we do a check as it's worth logging.
+ // Static img shouldn't exist in storage at this point,
+ // but we do a check as it's worth logging / cleaning up.
if have, _ := p.mgr.state.Storage.Has(ctx, p.emoji.ImageStaticPath); have {
- log.Warnf(ctx, "static emoji already exists at storage path: %s", p.emoji.ImagePath)
+ log.Warnf(ctx, "static emoji already exists at: %s", p.emoji.ImageStaticPath)
- // Attempt to remove static existing emoji at storage path (might be broken / out-of-date)
+ // Attempt to remove existing thumbnail (might be broken / out-of-date).
if err := p.mgr.state.Storage.Delete(ctx, p.emoji.ImageStaticPath); err != nil {
- return gtserror.Newf("error removing static emoji from storage: %v", err)
+ return gtserror.Newf("error removing static emoji %s from storage: %v", p.emoji.ImageStaticPath, err)
}
}
- // Create an emoji PNG encoder stream.
+ // Create emoji PNG encoder stream.
enc := staticImg.ToPNG()
- // Stream-encode the PNG static image into storage.
+ // Stream-encode the PNG static emoji image into our storage driver.
sz, err := p.mgr.state.Storage.PutStream(ctx, p.emoji.ImageStaticPath, enc)
if err != nil {
return gtserror.Newf("error stream-encoding static emoji to storage: %w", err)
}
- // Set written image size.
+ // Set final written thumb size.
p.emoji.ImageStaticFileSize = int(sz)
return nil
}
+
+// cleanup will remove any traces of processing emoji from storage,
+// and perform any other necessary cleanup steps after failure.
+func (p *ProcessingEmoji) cleanup(ctx context.Context) {
+ var err error
+
+ if p.emoji.ImagePath != "" {
+ // Ensure emoji file at path is deleted from storage.
+ err = p.mgr.state.Storage.Delete(ctx, p.emoji.ImagePath)
+ if err != nil && !storage.IsNotFound(err) {
+ log.Errorf(ctx, "error deleting %s: %v", p.emoji.ImagePath, err)
+ }
+ }
+
+ if p.emoji.ImageStaticPath != "" {
+ // Ensure emoji static file at path is deleted from storage.
+ err = p.mgr.state.Storage.Delete(ctx, p.emoji.ImageStaticPath)
+ if err != nil && !storage.IsNotFound(err) {
+ log.Errorf(ctx, "error deleting %s: %v", p.emoji.ImageStaticPath, err)
+ }
+ }
+
+ // Ensure marked as not cached.
+ p.emoji.Cached = util.Ptr(false)
+}
+
+// getInstanceAccountID determines the instance account ID from
+// emoji static image storage path. returns false on failure.
+func getInstanceAccountID(staticPath string) (string, bool) {
+ matches := regexes.FilePath.FindStringSubmatch(staticPath)
+ if len(matches) < 2 {
+ return "", false
+ }
+ return matches[1], true
+}
diff --git a/internal/media/processingmedia.go b/internal/media/processingmedia.go
index b65e3cd48..466c3443f 100644
--- a/internal/media/processingmedia.go
+++ b/internal/media/processingmedia.go
@@ -19,6 +19,7 @@ package media
import (
"bytes"
+ "cmp"
"context"
"image/jpeg"
"io"
@@ -29,6 +30,7 @@ import (
terminator "codeberg.org/superseriousbusiness/exif-terminator"
"github.com/disintegration/imaging"
"github.com/h2non/filetype"
+ "github.com/superseriousbusiness/gotosocial/internal/gtscontext"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/log"
@@ -41,18 +43,16 @@ import (
// currently being processed. It exposes functions
// for retrieving data from the process.
type ProcessingMedia struct {
- media *gtsmodel.MediaAttachment // processing media attachment details
- dataFn DataFunc // load-data function, returns media stream
- recache bool // recaching existing (uncached) media
- done bool // done is set when process finishes with non ctx canceled type error
- proc runners.Processor // proc helps synchronize only a singular running processing instance
- err error // error stores permanent error value when done
- mgr *Manager // mgr instance (access to db / storage)
+ media *gtsmodel.MediaAttachment // processing media attachment details
+ dataFn DataFunc // load-data function, returns media stream
+ done bool // done is set when process finishes with non ctx canceled type error
+ proc runners.Processor // proc helps synchronize only a singular running processing instance
+ err error // error stores permanent error value when done
+ mgr *Manager // mgr instance (access to db / storage)
}
-// AttachmentID returns the ID of the underlying
-// media attachment without blocking processing.
-func (p *ProcessingMedia) AttachmentID() string {
+// ID returns the ID of the underlying media.
+func (p *ProcessingMedia) ID() string {
return p.media.ID // immutable, safe outside mutex.
}
@@ -65,124 +65,102 @@ func (p *ProcessingMedia) AttachmentID() string {
// will still be returned in that case, but it will
// only be partially complete and should be treated
// as a placeholder.
-func (p *ProcessingMedia) LoadAttachment(ctx context.Context) (*gtsmodel.MediaAttachment, error) {
- // Attempt to load synchronously.
+func (p *ProcessingMedia) Load(ctx context.Context) (*gtsmodel.MediaAttachment, error) {
media, done, err := p.load(ctx)
- if err == nil {
- // No issue, return media.
- return media, nil
- }
-
if !done {
- // Provided context was cancelled,
- // e.g. request aborted early before
- // its context could be used to finish
- // loading the attachment. Enqueue for
- // asynchronous processing, which will
- // use a background context.
- log.Warnf(ctx, "reprocessing media %s after canceled ctx", p.media.ID)
- p.mgr.state.Workers.Media.Queue.Push(p.Process)
+ // On a context-canceled error (marked as !done), requeue for loading.
+ p.mgr.state.Workers.Dereference.Queue.Push(func(ctx context.Context) {
+ if _, _, err := p.load(ctx); err != nil {
+ log.Errorf(ctx, "error loading media: %v", err)
+ }
+ })
}
-
- // Media could not be retrieved FULLY,
- // but partial attachment should be present.
return media, err
}
-// Process allows the receiving object to fit the
-// runners.WorkerFunc signature. It performs a
-// (blocking) load and logs on error.
-func (p *ProcessingMedia) Process(ctx context.Context) {
- if _, _, err := p.load(ctx); err != nil {
- log.Errorf(ctx, "error(s) processing media: %v", err)
- }
-}
-
-// load performs a concurrency-safe load of ProcessingMedia, only
-// marking itself as complete when returned error is NOT a context cancel.
-func (p *ProcessingMedia) load(ctx context.Context) (*gtsmodel.MediaAttachment, bool, error) {
- var (
- done bool
- err error
- )
-
+// load is the package private form of load() that is wrapped to catch context canceled.
+func (p *ProcessingMedia) load(ctx context.Context) (
+ media *gtsmodel.MediaAttachment,
+ done bool,
+ err error,
+) {
err = p.proc.Process(func() error {
- if p.done {
+ if done = p.done; done {
// Already proc'd.
return p.err
}
defer func() {
// This is only done when ctx NOT cancelled.
- done = err == nil || !errorsv2.IsV2(err,
+ done = (err == nil || !errorsv2.IsV2(err,
context.Canceled,
context.DeadlineExceeded,
- )
+ ))
if !done {
return
}
+ // Anything from here, we
+ // need to ensure happens
+ // (i.e. no ctx canceled).
+ ctx = gtscontext.WithValues(
+ context.Background(),
+ ctx, // values
+ )
+
+ // On error or unknown media types, perform error cleanup.
+ if err != nil || p.media.Type == gtsmodel.FileTypeUnknown {
+ p.cleanup(ctx)
+ }
+
+ // Update with latest details, whatever happened.
+ e := p.mgr.state.DB.UpdateAttachment(ctx, p.media)
+ if e != nil {
+ log.Errorf(ctx, "error updating media in db: %v", e)
+ }
+
// Store final values.
p.done = true
p.err = err
}()
- // Gather errors as we proceed.
- var errs = gtserror.NewMultiError(4)
+ // TODO: in time update this
+ // to perhaps follow a similar
+ // freshness window to statuses
+ // / accounts? But that's a big
+ // maybe, media don't change in
+ // the same way so this is largely
+ // just to slow down fail retries.
+ const maxfreq = 6 * time.Hour
+
+ // Check whether media is uncached but repeatedly failing,
+ // specifically limit the frequency at which we allow this.
+ if !p.media.UpdatedAt.Equal(p.media.CreatedAt) && // i.e. not new
+ p.media.UpdatedAt.Add(maxfreq).Before(time.Now()) {
+ return nil
+ }
// Attempt to store media and calculate
// full-size media attachment details.
//
// This will update p.media as it goes.
- storeErr := p.store(ctx)
- if storeErr != nil {
- errs.Append(storeErr)
+ if err = p.store(ctx); err != nil {
+ return err
}
// Finish processing by reloading media into
// memory to get dimension and generate a thumb.
//
// This will update p.media as it goes.
- if finishErr := p.finish(ctx); finishErr != nil {
- errs.Append(finishErr)
- }
-
- // If this isn't a file we were able to process,
- // we may have partially stored it (eg., it's a
- // jpeg, which is fine, but streaming it to storage
- // was interrupted halfway through and so it was
- // never decoded). Try to clean up in this case.
- if p.media.Type == gtsmodel.FileTypeUnknown {
- deleteErr := p.mgr.state.Storage.Delete(ctx, p.media.File.Path)
- if deleteErr != nil && !storage.IsNotFound(deleteErr) {
- errs.Append(deleteErr)
- }
- }
-
- var dbErr error
- switch {
- case !p.recache:
- // First time caching this attachment, insert it.
- dbErr = p.mgr.state.DB.PutAttachment(ctx, p.media)
-
- case p.recache && len(errs) == 0:
- // Existing attachment we're recaching, update it.
- //
- // (We only want to update if everything went OK so far,
- // otherwise we'd better leave previous version alone.)
- dbErr = p.mgr.state.DB.UpdateAttachment(ctx, p.media)
+ if err = p.finish(ctx); err != nil {
+ return err //nolint:revive
}
- if dbErr != nil {
- errs.Append(dbErr)
- }
-
- err = errs.Combine()
- return err
+ return nil
})
-
- return p.media, done, err
+ media = p.media
+ return
}
// store calls the data function attached to p if it hasn't been called yet,
@@ -231,10 +209,6 @@ func (p *ProcessingMedia) store(ctx context.Context) error {
// Initial file size was misreported, so we didn't read
// fully into hdrBuf. Reslice it to the size we did read.
- log.Warnf(ctx,
- "recovered from misreported file size; reported %d; read %d",
- fileSize, n,
- )
hdrBuf = hdrBuf[:n]
fileSize = n
p.media.File.FileSize = fileSize
@@ -273,20 +247,13 @@ func (p *ProcessingMedia) store(ctx context.Context) error {
}
default:
- // The file is not a supported format that
- // we can process, so we can't do much with it.
- log.Warnf(ctx,
- "media extension '%s' not officially supported, will be processed as "+
- "type '%s' with minimal metadata, and will not be cached locally",
- info.Extension, gtsmodel.FileTypeUnknown,
- )
-
- // Don't bother storing this.
+ // The file is not a supported format that we can process, so we can't do much with it.
+ log.Warnf(ctx, "unsupported media extension '%s'; not caching locally", info.Extension)
store = false
}
// Fill in correct attachment
- // data now we're parsed it.
+ // data now we've parsed it.
p.media.URL = uris.URIForAttachment(
p.media.AccountID,
string(TypeAttachment),
@@ -295,15 +262,11 @@ func (p *ProcessingMedia) store(ctx context.Context) error {
info.Extension,
)
- // Prefer discovered mime type, fall back to
- // generic "this contains some bytes" type.
- mime := info.MIME.Value
- if mime == "" {
- mime = "application/octet-stream"
- }
+ // Prefer discovered MIME, fallback to generic data stream.
+ mime := cmp.Or(info.MIME.Value, "application/octet-stream")
p.media.File.ContentType = mime
- // Calculate attachment file path.
+ // Calculate final media attachment file path.
p.media.File.Path = uris.StoragePathForAttachment(
p.media.AccountID,
string(TypeAttachment),
@@ -323,23 +286,23 @@ func (p *ProcessingMedia) store(ctx context.Context) error {
// File shouldn't already exist in storage at this point,
// but we do a check as it's worth logging / cleaning up.
if have, _ := p.mgr.state.Storage.Has(ctx, p.media.File.Path); have {
- log.Warnf(ctx, "media already exists at storage path: %s", p.media.File.Path)
+ log.Warnf(ctx, "media already exists at: %s", p.media.File.Path)
// Attempt to remove existing media at storage path (might be broken / out-of-date)
if err := p.mgr.state.Storage.Delete(ctx, p.media.File.Path); err != nil {
- return gtserror.Newf("error removing media from storage: %v", err)
+ return gtserror.Newf("error removing media %s from storage: %v", p.media.File.Path, err)
}
}
- // Write the final reader stream to our storage.
- wroteSize, err := p.mgr.state.Storage.PutStream(ctx, p.media.File.Path, r)
+ // Write the final reader stream to our storage driver.
+ sz, err = p.mgr.state.Storage.PutStream(ctx, p.media.File.Path, r)
if err != nil {
return gtserror.Newf("error writing media to storage: %w", err)
}
// Set actual written size
// as authoritative file size.
- p.media.File.FileSize = int(wroteSize)
+ p.media.File.FileSize = int(sz)
// We can now consider this cached.
p.media.Cached = util.Ptr(true)
@@ -348,36 +311,9 @@ func (p *ProcessingMedia) store(ctx context.Context) error {
}
func (p *ProcessingMedia) finish(ctx context.Context) error {
- // Make a jolly assumption about thumbnail type.
- p.media.Thumbnail.ContentType = mimeImageJpeg
-
- // Calculate attachment thumbnail file path
- p.media.Thumbnail.Path = uris.StoragePathForAttachment(
- p.media.AccountID,
- string(TypeAttachment),
- string(SizeSmall),
- p.media.ID,
- // Always encode attachment
- // thumbnails as jpg.
- "jpg",
- )
-
- // Calculate attachment thumbnail serve path.
- p.media.Thumbnail.URL = uris.URIForAttachment(
- p.media.AccountID,
- string(TypeAttachment),
- string(SizeSmall),
- p.media.ID,
- // Always encode attachment
- // thumbnails as jpg.
- "jpg",
- )
-
- // If original file hasn't been stored, there's
- // likely something wrong with the data, or we
- // don't want to store it. Skip everything else.
+ // Nothing else to do if
+ // media was not cached.
if !*p.media.Cached {
- p.media.Processing = gtsmodel.ProcessingStatusProcessed
return nil
}
@@ -398,8 +334,7 @@ func (p *ProcessingMedia) finish(ctx context.Context) error {
// .jpeg, .gif, .webp image type
case mimeImageJpeg, mimeImageGif, mimeImageWebp:
- fullImg, err = decodeImage(
- rc,
+ fullImg, err = decodeImage(rc,
imaging.AutoOrientation(true),
)
if err != nil {
@@ -451,9 +386,9 @@ func (p *ProcessingMedia) finish(ctx context.Context) error {
}
// Set full-size dimensions in attachment info.
- p.media.FileMeta.Original.Width = int(fullImg.Width())
- p.media.FileMeta.Original.Height = int(fullImg.Height())
- p.media.FileMeta.Original.Size = int(fullImg.Size())
+ p.media.FileMeta.Original.Width = fullImg.Width()
+ p.media.FileMeta.Original.Height = fullImg.Height()
+ p.media.FileMeta.Original.Size = fullImg.Size()
p.media.FileMeta.Original.Aspect = fullImg.AspectRatio()
// Get smaller thumbnail image
@@ -475,44 +410,72 @@ func (p *ProcessingMedia) finish(ctx context.Context) error {
p.media.Blurhash = hash
}
- // Thumbnail shouldn't already exist in storage at this point,
+ // Thumbnail shouldn't exist in storage at this point,
// but we do a check as it's worth logging / cleaning up.
if have, _ := p.mgr.state.Storage.Has(ctx, p.media.Thumbnail.Path); have {
- log.Warnf(ctx, "thumbnail already exists at storage path: %s", p.media.Thumbnail.Path)
+ log.Warnf(ctx, "thumbnail already exists at: %s", p.media.Thumbnail.Path)
- // Attempt to remove existing thumbnail at storage path (might be broken / out-of-date)
+ // Attempt to remove existing thumbnail (might be broken / out-of-date).
if err := p.mgr.state.Storage.Delete(ctx, p.media.Thumbnail.Path); err != nil {
- return gtserror.Newf("error removing thumbnail from storage: %v", err)
+ return gtserror.Newf("error removing thumbnail %s from storage: %v", p.media.Thumbnail.Path, err)
}
}
// Create a thumbnail JPEG encoder stream.
enc := thumbImg.ToJPEG(&jpeg.Options{
+
// Good enough for
// a thumbnail.
Quality: 70,
})
- // Stream-encode the JPEG thumbnail image into storage.
+ // Stream-encode the JPEG thumbnail image into our storage driver.
sz, err := p.mgr.state.Storage.PutStream(ctx, p.media.Thumbnail.Path, enc)
if err != nil {
return gtserror.Newf("error stream-encoding thumbnail to storage: %w", err)
}
+ // Set final written thumb size.
+ p.media.Thumbnail.FileSize = int(sz)
+
// Set thumbnail dimensions in attachment info.
p.media.FileMeta.Small = gtsmodel.Small{
- Width: int(thumbImg.Width()),
- Height: int(thumbImg.Height()),
- Size: int(thumbImg.Size()),
+ Width: thumbImg.Width(),
+ Height: thumbImg.Height(),
+ Size: thumbImg.Size(),
Aspect: thumbImg.AspectRatio(),
}
- // Set written image size.
- p.media.Thumbnail.FileSize = int(sz)
-
- // Finally set the attachment as processed and update time.
+ // Finally set the attachment as processed.
p.media.Processing = gtsmodel.ProcessingStatusProcessed
- p.media.File.UpdatedAt = time.Now()
return nil
}
+
+// cleanup will remove any traces of processing media from storage.
+// and perform any other necessary cleanup steps after failure.
+func (p *ProcessingMedia) cleanup(ctx context.Context) {
+ var err error
+
+ if p.media.File.Path != "" {
+ // Ensure media file at path is deleted from storage.
+ err = p.mgr.state.Storage.Delete(ctx, p.media.File.Path)
+ if err != nil && !storage.IsNotFound(err) {
+ log.Errorf(ctx, "error deleting %s: %v", p.media.File.Path, err)
+ }
+ }
+
+ if p.media.Thumbnail.Path != "" {
+ // Ensure media thumbnail at path is deleted from storage.
+ err = p.mgr.state.Storage.Delete(ctx, p.media.Thumbnail.Path)
+ if err != nil && !storage.IsNotFound(err) {
+ log.Errorf(ctx, "error deleting %s: %v", p.media.Thumbnail.Path, err)
+ }
+ }
+
+ // Also ensure marked as unknown and finished
+ // processing so gets inserted as placeholder URL.
+ p.media.Processing = gtsmodel.ProcessingStatusProcessed
+ p.media.Type = gtsmodel.FileTypeUnknown
+ p.media.Cached = util.Ptr(false)
+}
diff --git a/internal/media/refetch.go b/internal/media/refetch.go
index a1483ccd4..c239655d2 100644
--- a/internal/media/refetch.go
+++ b/internal/media/refetch.go
@@ -112,19 +112,19 @@ func (m *Manager) RefetchEmojis(ctx context.Context, domain string, dereferenceM
return dereferenceMedia(ctx, emojiImageIRI)
}
- processingEmoji, err := m.PreProcessEmoji(ctx, dataFunc, emoji.Shortcode, emoji.ID, emoji.URI, &AdditionalEmojiInfo{
+ processingEmoji, err := m.RefreshEmoji(ctx, emoji, dataFunc, AdditionalEmojiInfo{
Domain: &emoji.Domain,
ImageRemoteURL: &emoji.ImageRemoteURL,
ImageStaticRemoteURL: &emoji.ImageStaticRemoteURL,
Disabled: emoji.Disabled,
VisibleInPicker: emoji.VisibleInPicker,
- }, true)
+ })
if err != nil {
log.Errorf(ctx, "emoji %s could not be refreshed because of an error during processing: %s", shortcodeDomain, err)
continue
}
- if _, err := processingEmoji.LoadEmoji(ctx); err != nil {
+ if _, err := processingEmoji.Load(ctx); err != nil {
log.Errorf(ctx, "emoji %s could not be refreshed because of an error during loading: %s", shortcodeDomain, err)
continue
}
diff --git a/internal/media/types.go b/internal/media/types.go
index 6e7727cd5..cea026b98 100644
--- a/internal/media/types.go
+++ b/internal/media/types.go
@@ -61,47 +61,85 @@ const (
TypeEmoji Type = "emoji" // TypeEmoji is the key for emoji type requests
)
-// AdditionalMediaInfo represents additional information that should be added to an attachment
-// when processing a piece of media.
+// AdditionalMediaInfo represents additional information that
+// should be added to attachment when processing a piece of media.
type AdditionalMediaInfo struct {
- // Time that this media was created; defaults to time.Now().
+
+ // Time that this media was
+ // created; defaults to time.Now().
CreatedAt *time.Time
- // ID of the status to which this media is attached; defaults to "".
+
+ // ID of the status to which this
+ // media is attached; defaults to "".
StatusID *string
- // URL of the media on a remote instance; defaults to "".
+
+ // URL of the media on a
+ // remote instance; defaults to "".
RemoteURL *string
- // Image description of this media; defaults to "".
+
+ // Image description of
+ // this media; defaults to "".
Description *string
- // Blurhash of this media; defaults to "".
+
+ // Blurhash of this
+ // media; defaults to "".
Blurhash *string
- // ID of the scheduled status to which this media is attached; defaults to "".
+
+ // ID of the scheduled status to which
+ // this media is attached; defaults to "".
ScheduledStatusID *string
- // Mark this media as in-use as an avatar; defaults to false.
+
+ // Mark this media as in-use
+ // as an avatar; defaults to false.
Avatar *bool
- // Mark this media as in-use as a header; defaults to false.
+
+ // Mark this media as in-use
+ // as a header; defaults to false.
Header *bool
- // X focus coordinate for this media; defaults to 0.
+
+ // X focus coordinate for
+ // this media; defaults to 0.
FocusX *float32
- // Y focus coordinate for this media; defaults to 0.
+
+ // Y focus coordinate for
+ // this media; defaults to 0.
FocusY *float32
}
// AdditionalEmojiInfo represents additional information
// that should be taken into account when processing an emoji.
type AdditionalEmojiInfo struct {
- // Time that this emoji was created; defaults to time.Now().
+
+ // ActivityPub URI of
+ // this remote emoji.
+ URI *string
+
+ // Time that this emoji was
+ // created; defaults to time.Now().
CreatedAt *time.Time
- // Domain the emoji originated from. Blank for this instance's domain. Defaults to "".
+
+ // Domain the emoji originated from. Blank
+ // for this instance's domain. Defaults to "".
Domain *string
- // URL of this emoji on a remote instance; defaults to "".
+
+ // URL of this emoji on a
+ // remote instance; defaults to "".
ImageRemoteURL *string
- // URL of the static version of this emoji on a remote instance; defaults to "".
+
+ // URL of the static version of this emoji
+ // on a remote instance; defaults to "".
ImageStaticRemoteURL *string
- // Whether this emoji should be disabled (not shown) on this instance; defaults to false.
+
+ // Whether this emoji should be disabled (not
+ // shown) on this instance; defaults to false.
Disabled *bool
- // Whether this emoji should be visible in the instance's emoji picker; defaults to true.
+
+ // Whether this emoji should be visible in
+ // the instance's emoji picker; defaults to true.
VisibleInPicker *bool
- // ID of the category this emoji should be placed in; defaults to "".
+
+ // ID of the category this emoji
+ // should be placed in; defaults to "".
CategoryID *string
}
diff --git a/internal/media/util.go b/internal/media/util.go
index 1595da6d7..296bdb883 100644
--- a/internal/media/util.go
+++ b/internal/media/util.go
@@ -37,6 +37,5 @@ func newHdrBuf(fileSize int) []byte {
if fileSize > 0 && fileSize < bufSize {
bufSize = fileSize
}
-
return make([]byte, bufSize)
}