diff options
Diffstat (limited to 'internal/media/manager.go')
-rw-r--r-- | internal/media/manager.go | 628 |
1 files changed, 329 insertions, 299 deletions
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 := >smodel.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 := >smodel.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 := >smodel.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 = >smodel.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 } |