mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2025-12-07 04:38:08 -06:00
[chore] media and emoji refactoring (#3000)
* start updating media manager interface ready for storing attachments / emoji right away
* store emoji and media as uncached immediately, then (re-)cache on Processing{}.Load()
* remove now unused media workers
* fix tests and issues
* fix another test!
* fix emoji activitypub uri setting behaviour, fix remainder of test compilation issues
* fix more tests
* fix (most of) remaining tests, add debouncing to repeatedly failing media / emojis
* whoops, rebase issue
* remove kim's whacky experiments
* do some reshuffling, ensure emoji uri gets set
* ensure marked as not cached on cleanup
* tweaks to media / emoji processing to handle context canceled better
* ensure newly fetched emojis actually get set in returned slice
* use different varnames to be a bit more obvious
* move emoji refresh rate limiting to dereferencer
* add exported dereferencer functions for remote media, use these for recaching in processor
* add check for nil attachment in updateAttachment()
* remove unused emoji and media fields + columns
* see previous commit
* fix old migrations expecting image_updated_at to exists (from copies of old models)
* remove freshness checking code (seems to be broken...)
* fix error arg causing nil ptr exception
* finish documentating functions with comments, slight tweaks to media / emoji deref error logic
* remove some extra unneeded boolean checking
* finish writing documentation (code comments) for exported media manager methods
* undo changes to migration snapshot gtsmodels, updated failing migration to have its own snapshot
* move doesColumnExist() to util.go in migrations package
This commit is contained in:
parent
fa710057c8
commit
21bb324156
48 changed files with 2578 additions and 1926 deletions
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,176 +56,172 @@ 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 {
|
||||
data DataFunc,
|
||||
info AdditionalMediaInfo,
|
||||
) (
|
||||
*ProcessingMedia,
|
||||
error,
|
||||
) {
|
||||
now := time.Now()
|
||||
|
||||
// Generate new ID.
|
||||
id := id.NewULID()
|
||||
|
||||
// Placeholder URL for attachment.
|
||||
url := uris.URIForAttachment(
|
||||
accountID,
|
||||
string(TypeAttachment),
|
||||
string(SizeOriginal),
|
||||
id,
|
||||
"unknown",
|
||||
)
|
||||
|
||||
// Placeholder storage path for attachment.
|
||||
path := uris.StoragePathForAttachment(
|
||||
accountID,
|
||||
string(TypeAttachment),
|
||||
string(SizeOriginal),
|
||||
id,
|
||||
"unknown",
|
||||
)
|
||||
|
||||
// Calculate attachment thumbnail file path
|
||||
thumbPath := uris.StoragePathForAttachment(
|
||||
accountID,
|
||||
string(TypeAttachment),
|
||||
string(SizeSmall),
|
||||
id,
|
||||
|
||||
// Always encode attachment
|
||||
// thumbnails as jpg.
|
||||
"jpg",
|
||||
)
|
||||
|
||||
// Calculate attachment thumbnail URL.
|
||||
thumbURL := uris.URIForAttachment(
|
||||
accountID,
|
||||
string(TypeAttachment),
|
||||
string(SizeSmall),
|
||||
id,
|
||||
|
||||
// Always encode attachment
|
||||
// thumbnails as jpg.
|
||||
"jpg",
|
||||
)
|
||||
|
||||
// Populate initial fields on the new media,
|
||||
// leaving out fields with values we don't know
|
||||
// yet. These will be overwritten as we go.
|
||||
now := time.Now()
|
||||
attachment := >smodel.MediaAttachment{
|
||||
ID: id.NewULID(),
|
||||
ID: id,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
URL: url,
|
||||
Type: gtsmodel.FileTypeUnknown,
|
||||
FileMeta: gtsmodel.FileMeta{},
|
||||
AccountID: accountID,
|
||||
Processing: gtsmodel.ProcessingStatusReceived,
|
||||
File: gtsmodel.File{
|
||||
UpdatedAt: now,
|
||||
ContentType: "application/octet-stream",
|
||||
Path: path,
|
||||
},
|
||||
Thumbnail: gtsmodel.Thumbnail{UpdatedAt: now},
|
||||
Avatar: util.Ptr(false),
|
||||
Header: util.Ptr(false),
|
||||
Cached: util.Ptr(false),
|
||||
Thumbnail: gtsmodel.Thumbnail{
|
||||
ContentType: mimeImageJpeg, // thumbs always jpg.
|
||||
Path: thumbPath,
|
||||
URL: thumbURL,
|
||||
},
|
||||
Avatar: util.Ptr(false),
|
||||
Header: util.Ptr(false),
|
||||
Cached: util.Ptr(false),
|
||||
}
|
||||
|
||||
attachment.URL = uris.URIForAttachment(
|
||||
accountID,
|
||||
string(TypeAttachment),
|
||||
string(SizeOriginal),
|
||||
attachment.ID,
|
||||
"unknown",
|
||||
)
|
||||
|
||||
attachment.File.Path = uris.StoragePathForAttachment(
|
||||
accountID,
|
||||
string(TypeAttachment),
|
||||
string(SizeOriginal),
|
||||
attachment.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
|
||||
}
|
||||
|
||||
if ai.ScheduledStatusID != nil {
|
||||
attachment.ScheduledStatusID = *ai.ScheduledStatusID
|
||||
}
|
||||
|
||||
if ai.Blurhash != nil {
|
||||
attachment.Blurhash = *ai.Blurhash
|
||||
}
|
||||
|
||||
if ai.Avatar != nil {
|
||||
attachment.Avatar = ai.Avatar
|
||||
}
|
||||
|
||||
if ai.Header != nil {
|
||||
attachment.Header = ai.Header
|
||||
}
|
||||
|
||||
if ai.FocusX != nil {
|
||||
attachment.FileMeta.Focus.X = *ai.FocusX
|
||||
}
|
||||
|
||||
if ai.FocusY != nil {
|
||||
attachment.FileMeta.Focus.Y = *ai.FocusY
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
processingMedia := &ProcessingMedia{
|
||||
media: attachment,
|
||||
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(
|
||||
ctx context.Context,
|
||||
data DataFunc,
|
||||
attachmentID string,
|
||||
) (*ProcessingMedia, error) {
|
||||
// Get the existing attachment from database.
|
||||
attachment, err := m.state.DB.GetAttachmentByID(ctx, attachmentID)
|
||||
// Store attachment in database in initial form.
|
||||
err := m.state.DB.PutAttachment(ctx, attachment)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
processingMedia := &ProcessingMedia{
|
||||
media: attachment,
|
||||
dataFn: data,
|
||||
recache: true, // Indicate it's a recache.
|
||||
mgr: m,
|
||||
}
|
||||
|
||||
return processingMedia, nil
|
||||
// Pass prepared media as ready to be cached.
|
||||
return m.RecacheMedia(attachment, data), nil
|
||||
}
|
||||
|
||||
// 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(
|
||||
ctx context.Context,
|
||||
// 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,
|
||||
}
|
||||
}
|
||||
|
||||
// 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,
|
||||
emojiID string,
|
||||
uri string,
|
||||
ai *AdditionalEmojiInfo,
|
||||
refresh bool,
|
||||
) (*ProcessingEmoji, error) {
|
||||
var (
|
||||
newPathID string
|
||||
emoji *gtsmodel.Emoji
|
||||
now = time.Now()
|
||||
)
|
||||
domain string,
|
||||
data DataFunc,
|
||||
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, "")
|
||||
|
|
@ -233,206 +229,240 @@ func (m *Manager) PreProcessEmoji(
|
|||
return nil, gtserror.Newf("error fetching instance account: %w", err)
|
||||
}
|
||||
|
||||
if refresh {
|
||||
// Existing emoji!
|
||||
if domain == "" && info.URI == nil {
|
||||
// Generate URI for local emoji.
|
||||
uri := uris.URIForEmoji(id)
|
||||
info.URI = &uri
|
||||
}
|
||||
|
||||
emoji, err = m.state.DB.GetEmojiByID(ctx, emojiID)
|
||||
// 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,
|
||||
}
|
||||
|
||||
// Finally, create new emoji.
|
||||
return m.createEmoji(ctx,
|
||||
m.state.DB.PutEmoji,
|
||||
data,
|
||||
emoji,
|
||||
info,
|
||||
)
|
||||
}
|
||||
|
||||
// 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,
|
||||
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)
|
||||
}
|
||||
|
||||
// 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) {
|
||||
|
||||
// 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
|
||||
// Wrap closer to cleanup old data.
|
||||
c := iotools.CloserFunc(func() error {
|
||||
|
||||
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
|
||||
// First try close original.
|
||||
if rc.Close(); err != nil {
|
||||
return 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)
|
||||
}
|
||||
// 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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
})
|
||||
// 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)
|
||||
}
|
||||
|
||||
// Return newly wrapped readcloser and size.
|
||||
return iotools.ReadCloser(rc, c), sz, nil
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
// Reuse existing shortcode and URI -
|
||||
// these don't change when we refresh.
|
||||
emoji.Shortcode = shortcode
|
||||
emoji.URI = uri
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// Generate new static URL for emoji.
|
||||
emoji.ImageStaticURL = uris.URIForAttachment(
|
||||
instanceAcc.ID,
|
||||
string(TypeEmoji),
|
||||
string(SizeStatic),
|
||||
newPathID,
|
||||
|
||||
// All static emojis
|
||||
// are encoded as png.
|
||||
mimePng,
|
||||
)
|
||||
|
||||
// Generate new static image storage path for emoji.
|
||||
emoji.ImageStaticPath = uris.StoragePathForAttachment(
|
||||
instanceAcc.ID,
|
||||
string(TypeEmoji),
|
||||
string(SizeStatic),
|
||||
newPathID,
|
||||
|
||||
// All static emojis
|
||||
// are encoded as png.
|
||||
mimePng,
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// Set the refreshed path ID used.
|
||||
processingEmoji.newPathID = newPathID
|
||||
|
||||
return processingEmoji, nil
|
||||
}
|
||||
|
||||
func (m *Manager) createEmoji(
|
||||
ctx context.Context,
|
||||
putDB func(context.Context, *gtsmodel.Emoji) error,
|
||||
data DataFunc,
|
||||
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 ai != nil {
|
||||
if ai.CreatedAt != nil {
|
||||
emoji.CreatedAt = *ai.CreatedAt
|
||||
}
|
||||
|
||||
if ai.Domain != nil {
|
||||
emoji.Domain = *ai.Domain
|
||||
}
|
||||
|
||||
if ai.ImageRemoteURL != nil {
|
||||
emoji.ImageRemoteURL = *ai.ImageRemoteURL
|
||||
}
|
||||
|
||||
if ai.ImageStaticRemoteURL != nil {
|
||||
emoji.ImageStaticRemoteURL = *ai.ImageStaticRemoteURL
|
||||
}
|
||||
|
||||
if ai.Disabled != nil {
|
||||
emoji.Disabled = ai.Disabled
|
||||
}
|
||||
|
||||
if ai.VisibleInPicker != nil {
|
||||
emoji.VisibleInPicker = ai.VisibleInPicker
|
||||
}
|
||||
|
||||
if ai.CategoryID != nil {
|
||||
emoji.CategoryID = *ai.CategoryID
|
||||
}
|
||||
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,
|
||||
existing: refresh,
|
||||
newPathID: newPathID,
|
||||
dataFn: data,
|
||||
mgr: m,
|
||||
emoji: emoji,
|
||||
dataFn: data,
|
||||
mgr: m,
|
||||
}
|
||||
|
||||
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(
|
||||
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,
|
||||
emojiID string,
|
||||
) (*ProcessingEmoji, error) {
|
||||
// Get the existing emoji from the database.
|
||||
emoji, err := m.state.DB.GetEmojiByID(ctx, emojiID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
) *ProcessingEmoji {
|
||||
return &ProcessingEmoji{
|
||||
emoji: emoji,
|
||||
dataFn: data,
|
||||
mgr: m,
|
||||
}
|
||||
|
||||
processingEmoji := &ProcessingEmoji{
|
||||
emoji: emoji,
|
||||
dataFn: data,
|
||||
existing: true, // Indicate recache.
|
||||
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,
|
||||
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
|
||||
}
|
||||
|
||||
// Attempt to add emoji item to the worker queue.
|
||||
m.state.Workers.Media.Queue.Push(emoji.Process)
|
||||
|
||||
return emoji, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 := >smodel.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())
|
||||
}
|
||||
processing, err := suite.manager.CreateMedia(ctx,
|
||||
accountID,
|
||||
data,
|
||||
media.AdditionalMediaInfo{},
|
||||
)
|
||||
suite.NoError(err)
|
||||
suite.NotNil(processing)
|
||||
|
||||
attachmentID := processingMedia.AttachmentID()
|
||||
// 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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
// 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 nil, err
|
||||
return emoji, 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)
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
return err //nolint:revive
|
||||
}
|
||||
|
||||
if p.existing {
|
||||
// Existing emoji we're updating, so only update.
|
||||
err = p.mgr.state.DB.UpdateEmoji(ctx, p.emoji)
|
||||
return err
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 err = p.finish(ctx); err != nil {
|
||||
return err //nolint:revive
|
||||
}
|
||||
|
||||
// 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 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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -37,6 +37,5 @@ func newHdrBuf(fileSize int) []byte {
|
|||
if fileSize > 0 && fileSize < bufSize {
|
||||
bufSize = fileSize
|
||||
}
|
||||
|
||||
return make([]byte, bufSize)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue