diff --git a/internal/media/image.go b/internal/media/image.go index 4ad68db5a..157ae0f4a 100644 --- a/internal/media/image.go +++ b/internal/media/image.go @@ -54,6 +54,14 @@ type ImageMeta struct { } func (m *manager) preProcessImage(ctx context.Context, data []byte, contentType string, accountID string) (*Media, error) { + if !supportedImage(contentType) { + return nil, fmt.Errorf("image type %s not supported", contentType) + } + + if len(data) == 0 { + return nil, errors.New("image was of size 0") + } + id, err := id.NewRandomULID() if err != nil { return nil, err @@ -93,21 +101,7 @@ func (m *manager) preProcessImage(ctx context.Context, data []byte, contentType var original *ImageMeta var small *ImageMeta - switch contentType { - case mimeImageJpeg, mimeImagePng: - // first 'clean' image by purging exif data from it - var exifErr error - if clean, exifErr = purgeExif(data); exifErr != nil { - return nil, fmt.Errorf("error cleaning exif data: %s", exifErr) - } - original, err = decodeImage(clean, contentType) - case mimeImageGif: - // gifs are already clean - no exif data to remove - clean = data - original, err = decodeGif(clean) - default: - err = fmt.Errorf("content type %s not a processible image type", contentType) - } + if err != nil { return nil, err diff --git a/internal/media/manager.go b/internal/media/manager.go index 54b964564..074ebdb58 100644 --- a/internal/media/manager.go +++ b/internal/media/manager.go @@ -81,23 +81,22 @@ func (m *manager) ProcessMedia(ctx context.Context, data []byte, accountID strin switch mainType { case mimeImage: - if !supportedImage(contentType) { - return nil, fmt.Errorf("image type %s not supported", contentType) - } - if len(data) == 0 { - return nil, errors.New("image was of size 0") - } - media, err := m.preProcessImage(ctx, data, contentType, accountID) if err != nil { return nil, err } m.pool.Enqueue(func(innerCtx context.Context) { - + select { + case <-innerCtx.Done(): + // if the inner context is done that means the worker pool is closing, so we should just return + return + default: + media.PreLoad(innerCtx) + } }) - return nil, nil + return media, nil default: return nil, fmt.Errorf("content type %s not (yet) supported", contentType) } diff --git a/internal/media/media.go b/internal/media/media.go index 0bd196b27..aa11787b2 100644 --- a/internal/media/media.go +++ b/internal/media/media.go @@ -1,29 +1,151 @@ package media import ( + "context" "fmt" "sync" + "codeberg.org/gruf/go-store/kv" + "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" ) +type processState int + +const ( + received processState = iota // processing order has been received but not done yet + complete // processing order has been completed successfully + errored // processing order has been completed with an error +) + type Media struct { - mu sync.Mutex + mu sync.Mutex + + /* + below fields should be set on newly created media; + attachment will be updated incrementally as media goes through processing + */ + attachment *gtsmodel.MediaAttachment rawData []byte + + /* + below fields represent the processing state of the media thumbnail + */ + + thumbing processState + thumb *ImageMeta + + /* + below fields represent the processing state of the full-sized media + */ + + processing processState + processed *ImageMeta + + /* + below pointers to database and storage are maintained so that + the media can store and update itself during processing steps + */ + + database db.DB + storage *kv.KVStore + + err error // error created during processing, if any } -func (m *Media) Thumb() (*ImageMeta, error) { +func (m *Media) Thumb(ctx context.Context) (*ImageMeta, error) { m.mu.Lock() - thumb, err := deriveThumbnail(m.rawData, m.attachment.File.ContentType) - if err != nil { - return nil, fmt.Errorf("error deriving thumbnail: %s", err) + defer m.mu.Unlock() + + switch m.thumbing { + case received: + // we haven't processed a thumbnail for this media yet so do it now + thumb, err := deriveThumbnail(m.rawData, m.attachment.File.ContentType) + if err != nil { + m.err = fmt.Errorf("error deriving thumbnail: %s", err) + m.thumbing = errored + return nil, m.err + } + + // put the thumbnail in storage + if err := m.storage.Put(m.attachment.Thumbnail.Path, thumb.image); err != nil { + m.err = fmt.Errorf("error storing thumbnail: %s", err) + m.thumbing = errored + return nil, m.err + } + + // set appropriate fields on the attachment based on the thumbnail we derived + m.attachment.Blurhash = thumb.blurhash + m.attachment.FileMeta.Small = gtsmodel.Small{ + Width: thumb.width, + Height: thumb.height, + Size: thumb.size, + Aspect: thumb.aspect, + } + m.attachment.Thumbnail.FileSize = thumb.size + + // put or update the attachment in the database + if err := m.database.Put(ctx, m.attachment); err != nil { + if err != db.ErrAlreadyExists { + m.err = fmt.Errorf("error putting attachment: %s", err) + m.thumbing = errored + return nil, m.err + } + if err := m.database.UpdateByPrimaryKey(ctx, m.attachment); err != nil { + m.err = fmt.Errorf("error updating attachment: %s", err) + m.thumbing = errored + return nil, m.err + } + } + + // set the thumbnail of this media + m.thumb = thumb + + // we're done processing the thumbnail! + m.thumbing = complete + fallthrough + case complete: + return m.thumb, nil + case errored: + return nil, m.err } - m.attachment.Blurhash = thumb.blurhash - aaaaaaaaaaaaaaaa + + return nil, fmt.Errorf("thumbnail processing status %d unknown", m.thumbing) } -func (m *Media) PreLoad() { +func (m *Media) Full(ctx context.Context) (*ImageMeta, error) { + var clean []byte + var err error + var original *ImageMeta + + ct := m.attachment.File.ContentType +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + switch ct { + case mimeImageJpeg, mimeImagePng: + // first 'clean' image by purging exif data from it + var exifErr error + if clean, exifErr = purgeExif(m.rawData); exifErr != nil { + return nil, fmt.Errorf("error cleaning exif data: %s", exifErr) + } + original, err = decodeImage(clean, ct) + case mimeImageGif: + // gifs are already clean - no exif data to remove + clean = m.rawData + original, err = decodeGif(clean) + default: + err = fmt.Errorf("content type %s not a processible image type", ct) + } + + if err != nil { + return nil, err + } + + return original, nil +} + +func (m *Media) PreLoad(ctx context.Context) { + go m.Thumb(ctx) m.mu.Lock() defer m.mu.Unlock() }