From 21bb324156f582e918a097ea744e52fc21b2ddf4 Mon Sep 17 00:00:00 2001
From: kim <89579420+NyaaaWhatsUpDoc@users.noreply.github.com>
Date: Wed, 26 Jun 2024 15:01:16 +0000
Subject: [PATCH] [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
---
.../api/activitypub/users/inboxpost_test.go | 12 +-
internal/api/client/admin/emojicreate_test.go | 2 +-
internal/api/client/admin/emojiupdate_test.go | 9 +-
internal/api/fileserver/servefile_test.go | 8 +-
internal/cache/size.go | 3 -
internal/cleaner/media_test.go | 5 +-
.../20230521105850_emoji_empty_domain_fix.go | 2 +-
.../emoji.go | 53 ++
...0240613091853_drop_unused_media_columns.go | 68 ++
internal/db/bundb/migrations/util.go | 40 ++
internal/federation/dereferencing/account.go | 184 ++---
internal/federation/dereferencing/emoji.go | 389 ++++++----
.../federation/dereferencing/emoji_test.go | 45 +-
internal/federation/dereferencing/media.go | 215 ++++++
internal/federation/dereferencing/status.go | 241 ++++---
internal/federation/dereferencing/util.go | 124 +---
internal/gtsmodel/emoji.go | 15 +-
internal/gtsmodel/mediaattachment.go | 32 +-
internal/media/image.go | 37 +-
internal/media/manager.go | 670 +++++++++---------
internal/media/manager_test.go | 409 ++++++-----
internal/media/processingemoji.go | 209 +++---
internal/media/processingmedia.go | 287 ++++----
internal/media/refetch.go | 6 +-
internal/media/types.go | 76 +-
internal/media/util.go | 1 -
internal/processing/account/account_test.go | 2 +-
internal/processing/account/update.go | 102 +--
internal/processing/admin/admin.go | 35 +-
internal/processing/admin/debug_apurl.go | 2 +-
internal/processing/admin/email.go | 2 +-
internal/processing/admin/emoji.go | 533 ++++++--------
internal/processing/admin/media.go | 4 +-
internal/processing/common/common.go | 4 +
internal/processing/common/media.go | 98 +++
internal/processing/instance.go | 20 +-
internal/processing/media/create.go | 27 +-
internal/processing/media/getfile.go | 456 ++++++------
internal/processing/media/media.go | 17 +-
internal/processing/media/media_test.go | 9 +-
internal/processing/polls/poll_test.go | 2 +-
internal/processing/processor.go | 6 +-
internal/processing/status/status_test.go | 2 +-
internal/storage/storage.go | 2 +-
internal/typeutils/internaltoas.go | 2 +-
internal/workers/workers.go | 11 -
testrig/testmodels.go | 24 -
testrig/util.go | 2 -
48 files changed, 2578 insertions(+), 1926 deletions(-)
create mode 100644 internal/db/bundb/migrations/20230521105850_emoji_empty_domain_fix/emoji.go
create mode 100644 internal/db/bundb/migrations/20240613091853_drop_unused_media_columns.go
create mode 100644 internal/db/bundb/migrations/util.go
create mode 100644 internal/federation/dereferencing/media.go
create mode 100644 internal/processing/common/media.go
diff --git a/internal/api/activitypub/users/inboxpost_test.go b/internal/api/activitypub/users/inboxpost_test.go
index 9d863f234..64c9f7e6c 100644
--- a/internal/api/activitypub/users/inboxpost_test.go
+++ b/internal/api/activitypub/users/inboxpost_test.go
@@ -376,7 +376,17 @@ func (suite *InboxPostTestSuite) TestPostUpdate() {
}
// emojis should be updated
- suite.Contains(dbUpdatedAccount.EmojiIDs, testEmoji.ID)
+ var haveUpdatedEmoji bool
+ for _, emoji := range dbUpdatedAccount.Emojis {
+ if emoji.Shortcode == testEmoji.Shortcode &&
+ emoji.Domain == testEmoji.Domain &&
+ emoji.ImageRemoteURL == emoji.ImageRemoteURL &&
+ emoji.ImageStaticRemoteURL == emoji.ImageStaticRemoteURL {
+ haveUpdatedEmoji = true
+ break
+ }
+ }
+ suite.True(haveUpdatedEmoji)
// account should be freshly fetched
suite.WithinDuration(time.Now(), dbUpdatedAccount.FetchedAt, 10*time.Second)
diff --git a/internal/api/client/admin/emojicreate_test.go b/internal/api/client/admin/emojicreate_test.go
index 46139df47..be39ebdf5 100644
--- a/internal/api/client/admin/emojicreate_test.go
+++ b/internal/api/client/admin/emojicreate_test.go
@@ -281,7 +281,7 @@ func (suite *EmojiCreateTestSuite) TestEmojiCreateAlreadyExists() {
suite.NoError(err)
suite.NotEmpty(b)
- suite.Equal(`{"error":"Conflict: emoji with shortcode rainbow already exists"}`, string(b))
+ suite.Equal(`{"error":"Conflict: emoji with shortcode already exists"}`, string(b))
}
func TestEmojiCreateTestSuite(t *testing.T) {
diff --git a/internal/api/client/admin/emojiupdate_test.go b/internal/api/client/admin/emojiupdate_test.go
index 676363e39..11beaeaa9 100644
--- a/internal/api/client/admin/emojiupdate_test.go
+++ b/internal/api/client/admin/emojiupdate_test.go
@@ -20,6 +20,7 @@ package admin_test
import (
"context"
"encoding/json"
+ "io"
"io/ioutil"
"net/http"
"net/http/httptest"
@@ -370,10 +371,10 @@ func (suite *EmojiUpdateTestSuite) TestEmojiUpdateModifyRemoteEmoji() {
defer result.Body.Close()
// check the response
- b, err := ioutil.ReadAll(result.Body)
+ b, err := io.ReadAll(result.Body)
suite.NoError(err)
- suite.Equal(`{"error":"Bad Request: emoji 01GD5KP5CQEE1R3X43Y1EHS2CW is not a local emoji, cannot update it via this endpoint"}`, string(b))
+ suite.Equal(`{"error":"Bad Request: cannot modify remote emoji"}`, string(b))
}
func (suite *EmojiUpdateTestSuite) TestEmojiUpdateModifyNoParams() {
@@ -440,7 +441,7 @@ func (suite *EmojiUpdateTestSuite) TestEmojiUpdateCopyLocalToLocal() {
b, err := ioutil.ReadAll(result.Body)
suite.NoError(err)
- suite.Equal(`{"error":"Bad Request: emoji 01F8MH9H8E4VG3KDYJR9EGPXCQ is not a remote emoji, cannot copy it to local"}`, string(b))
+ suite.Equal(`{"error":"Bad Request: target emoji is not remote; cannot copy to local"}`, string(b))
}
func (suite *EmojiUpdateTestSuite) TestEmojiUpdateCopyEmptyShortcode() {
@@ -541,7 +542,7 @@ func (suite *EmojiUpdateTestSuite) TestEmojiUpdateCopyShortcodeAlreadyInUse() {
b, err := ioutil.ReadAll(result.Body)
suite.NoError(err)
- suite.Equal(`{"error":"Conflict: emoji with shortcode rainbow already exists on this instance"}`, string(b))
+ suite.Equal(`{"error":"Conflict: emoji with shortcode already exists"}`, string(b))
}
func TestEmojiUpdateTestSuite(t *testing.T) {
diff --git a/internal/api/fileserver/servefile_test.go b/internal/api/fileserver/servefile_test.go
index c840d232f..cb3a35e45 100644
--- a/internal/api/fileserver/servefile_test.go
+++ b/internal/api/fileserver/servefile_test.go
@@ -19,7 +19,7 @@ package fileserver_test
import (
"context"
- "io/ioutil"
+ "io"
"net/http"
"net/http/httptest"
"testing"
@@ -28,6 +28,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/api/fileserver"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/media"
+ "github.com/superseriousbusiness/gotosocial/internal/middleware"
"github.com/superseriousbusiness/gotosocial/testrig"
)
@@ -54,12 +55,15 @@ func (suite *ServeFileTestSuite) GetFile(
ctx.AddParam(fileserver.MediaSizeKey, string(mediaSize))
ctx.AddParam(fileserver.FileNameKey, filename)
+ logger := middleware.Logger(false)
suite.fileServer.ServeFile(ctx)
+ logger(ctx)
+
code = recorder.Code
headers = recorder.Result().Header
var err error
- body, err = ioutil.ReadAll(recorder.Body)
+ body, err = io.ReadAll(recorder.Body)
if err != nil {
suite.FailNow(err.Error())
}
diff --git a/internal/cache/size.go b/internal/cache/size.go
index e1529f741..fb1f165c2 100644
--- a/internal/cache/size.go
+++ b/internal/cache/size.go
@@ -334,7 +334,6 @@ func sizeofEmoji() uintptr {
ImageStaticPath: exampleURI,
ImageContentType: "image/png",
ImageStaticContentType: "image/png",
- ImageUpdatedAt: exampleTime,
Disabled: func() *bool { ok := false; return &ok }(),
URI: "http://localhost:8080/emoji/01F8MH9H8E4VG3KDYJR9EGPXCQ",
VisibleInPicker: func() *bool { ok := true; return &ok }(),
@@ -473,12 +472,10 @@ func sizeofMedia() uintptr {
File: gtsmodel.File{
Path: exampleURI,
ContentType: "image/jpeg",
- UpdatedAt: exampleTime,
},
Thumbnail: gtsmodel.Thumbnail{
Path: exampleURI,
ContentType: "image/jpeg",
- UpdatedAt: exampleTime,
URL: exampleURI,
RemoteURL: exampleURI,
},
diff --git a/internal/cleaner/media_test.go b/internal/cleaner/media_test.go
index b33ae4b4f..acb5416f7 100644
--- a/internal/cleaner/media_test.go
+++ b/internal/cleaner/media_test.go
@@ -386,11 +386,10 @@ func (suite *MediaTestSuite) TestUncacheAndRecache() {
testStatusAttachment,
testHeader,
} {
- processingRecache, err := suite.manager.PreProcessMediaRecache(ctx, data, original.ID)
- suite.NoError(err)
+ processing := suite.manager.RecacheMedia(original, data)
// synchronously load the recached attachment
- recachedAttachment, err := processingRecache.LoadAttachment(ctx)
+ recachedAttachment, err := processing.Load(ctx)
suite.NoError(err)
suite.NotNil(recachedAttachment)
diff --git a/internal/db/bundb/migrations/20230521105850_emoji_empty_domain_fix.go b/internal/db/bundb/migrations/20230521105850_emoji_empty_domain_fix.go
index b6cd2ffe5..efb9f6ce6 100644
--- a/internal/db/bundb/migrations/20230521105850_emoji_empty_domain_fix.go
+++ b/internal/db/bundb/migrations/20230521105850_emoji_empty_domain_fix.go
@@ -20,7 +20,7 @@ package migrations
import (
"context"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ gtsmodel "github.com/superseriousbusiness/gotosocial/internal/db/bundb/migrations/20230521105850_emoji_empty_domain_fix"
"github.com/uptrace/bun"
"github.com/uptrace/bun/dialect"
)
diff --git a/internal/db/bundb/migrations/20230521105850_emoji_empty_domain_fix/emoji.go b/internal/db/bundb/migrations/20230521105850_emoji_empty_domain_fix/emoji.go
new file mode 100644
index 000000000..2c00cd765
--- /dev/null
+++ b/internal/db/bundb/migrations/20230521105850_emoji_empty_domain_fix/emoji.go
@@ -0,0 +1,53 @@
+// GoToSocial
+// Copyright (C) GoToSocial Authors admin@gotosocial.org
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package gtsmodel
+
+import "time"
+
+// Emoji represents a custom emoji that's been uploaded through the admin UI or downloaded from a remote instance.
+type Emoji struct {
+ ID string `validate:"required,ulid" bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // id of this item in the database
+ CreatedAt time.Time `validate:"-" bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item created
+ UpdatedAt time.Time `validate:"-" bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item last updated
+ Shortcode string `validate:"required" bun:",nullzero,notnull,unique:domainshortcode"` // String shortcode for this emoji -- the part that's between colons. This should be lowercase a-z_ eg., 'blob_hug' 'purple_heart' Must be unique with domain.
+ Domain string `validate:"omitempty,fqdn" bun:",nullzero,unique:domainshortcode"` // Origin domain of this emoji, eg 'example.org', 'queer.party'. empty string for local emojis.
+ ImageRemoteURL string `validate:"required_without=ImageURL,omitempty,url" bun:",nullzero"` // Where can this emoji be retrieved remotely? Null for local emojis.
+ ImageStaticRemoteURL string `validate:"required_without=ImageStaticURL,omitempty,url" bun:",nullzero"` // Where can a static / non-animated version of this emoji be retrieved remotely? Null for local emojis.
+ ImageURL string `validate:"required_without=ImageRemoteURL,required_without=Domain,omitempty,url" bun:",nullzero"` // Where can this emoji be retrieved from the local server? Null for remote emojis.
+ ImageStaticURL string `validate:"required_without=ImageStaticRemoteURL,required_without=Domain,omitempty,url" bun:",nullzero"` // Where can a static version of this emoji be retrieved from the local server? Null for remote emojis.
+ ImagePath string `validate:"required,file" bun:",nullzero,notnull"` // Path of the emoji image in the server storage system.
+ ImageStaticPath string `validate:"required,file" bun:",nullzero,notnull"` // Path of a static version of the emoji image in the server storage system
+ ImageContentType string `validate:"required" bun:",nullzero,notnull"` // MIME content type of the emoji image
+ ImageStaticContentType string `validate:"required" bun:",nullzero,notnull"` // MIME content type of the static version of the emoji image.
+ ImageFileSize int `validate:"required,min=1" bun:",nullzero,notnull"` // Size of the emoji image file in bytes, for serving purposes.
+ ImageStaticFileSize int `validate:"required,min=1" bun:",nullzero,notnull"` // Size of the static version of the emoji image file in bytes, for serving purposes.
+ ImageUpdatedAt time.Time `validate:"-" bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // When was the emoji image last updated?
+ Disabled *bool `validate:"-" bun:",nullzero,notnull,default:false"` // Has a moderation action disabled this emoji from being shown?
+ URI string `validate:"url" bun:",nullzero,notnull,unique"` // ActivityPub uri of this emoji. Something like 'https://example.org/emojis/1234'
+ VisibleInPicker *bool `validate:"-" bun:",nullzero,notnull,default:true"` // Is this emoji visible in the admin emoji picker?
+ Category *EmojiCategory `validate:"-" bun:"rel:belongs-to"` // In which emoji category is this emoji visible?
+ CategoryID string `validate:"omitempty,ulid" bun:"type:CHAR(26),nullzero"` // ID of the category this emoji belongs to.
+}
+
+// EmojiCategory represents a grouping of custom emojis.
+type EmojiCategory struct {
+ ID string `validate:"required,ulid" bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // id of this item in the database
+ CreatedAt time.Time `validate:"-" bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item created
+ UpdatedAt time.Time `validate:"-" bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item last updated
+ Name string `validate:"required" bun:",nullzero,notnull,unique"` // name of this category
+}
diff --git a/internal/db/bundb/migrations/20240613091853_drop_unused_media_columns.go b/internal/db/bundb/migrations/20240613091853_drop_unused_media_columns.go
new file mode 100644
index 000000000..7c0cea99e
--- /dev/null
+++ b/internal/db/bundb/migrations/20240613091853_drop_unused_media_columns.go
@@ -0,0 +1,68 @@
+// GoToSocial
+// Copyright (C) GoToSocial Authors admin@gotosocial.org
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package migrations
+
+import (
+ "context"
+
+ "github.com/uptrace/bun"
+)
+
+func init() {
+ up := func(ctx context.Context, db *bun.DB) error {
+ return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
+
+ for _, dropcase := range []struct {
+ table string
+ col string
+ }{
+ {table: "media_attachments", col: "file_updated_at"},
+ {table: "media_attachments", col: "thumbnail_updated_at"},
+ {table: "emojis", col: "thumbnail_updated_at"},
+ } {
+ // For each case check the column actually exists on database.
+ exists, err := doesColumnExist(ctx, tx, dropcase.table, dropcase.col)
+ if err != nil {
+ return err
+ }
+
+ if exists {
+ // Now actually drop the column.
+ if _, err := tx.NewDropColumn().
+ Table(dropcase.table).
+ Column(dropcase.col).
+ Exec(ctx); err != nil {
+ return err
+ }
+ }
+ }
+
+ return nil
+ })
+ }
+
+ down := func(ctx context.Context, db *bun.DB) error {
+ return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
+ return nil
+ })
+ }
+
+ if err := Migrations.Register(up, down); err != nil {
+ panic(err)
+ }
+}
diff --git a/internal/db/bundb/migrations/util.go b/internal/db/bundb/migrations/util.go
new file mode 100644
index 000000000..47de09e23
--- /dev/null
+++ b/internal/db/bundb/migrations/util.go
@@ -0,0 +1,40 @@
+// GoToSocial
+// Copyright (C) GoToSocial Authors admin@gotosocial.org
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package migrations
+
+import (
+ "context"
+
+ "github.com/uptrace/bun"
+ "github.com/uptrace/bun/dialect"
+)
+
+// doesColumnExist safely checks whether given column exists on table, handling both SQLite and PostgreSQL appropriately.
+func doesColumnExist(ctx context.Context, tx bun.Tx, table, col string) (bool, error) {
+ var n int
+ var err error
+ switch tx.Dialect().Name() {
+ case dialect.SQLite:
+ err = tx.NewRaw("SELECT COUNT(*) FROM pragma_table_info(?) WHERE name=?", table, col).Scan(ctx, &n)
+ case dialect.PG:
+ err = tx.NewRaw("SELECT COUNT(*) FROM information_schema.columns WHERE table_name=? and column_name=?", table, col).Scan(ctx, &n)
+ default:
+ panic("unexpected dialect")
+ }
+ return (n > 0), err
+}
diff --git a/internal/federation/dereferencing/account.go b/internal/federation/dereferencing/account.go
index 069fca1bc..e48507124 100644
--- a/internal/federation/dereferencing/account.go
+++ b/internal/federation/dereferencing/account.go
@@ -33,7 +33,6 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/id"
"github.com/superseriousbusiness/gotosocial/internal/log"
"github.com/superseriousbusiness/gotosocial/internal/media"
- "github.com/superseriousbusiness/gotosocial/internal/transport"
"github.com/superseriousbusiness/gotosocial/internal/util"
)
@@ -730,18 +729,18 @@ func (d *Dereferencer) enrichAccount(
latestAcc.ID = account.ID
latestAcc.FetchedAt = time.Now()
- // Ensure the account's avatar media is populated, passing in existing to check for changes.
- if err := d.fetchRemoteAccountAvatar(ctx, tsport, account, latestAcc); err != nil {
+ // Ensure the account's avatar media is populated, passing in existing to check for chages.
+ if err := d.fetchAccountAvatar(ctx, requestUser, account, latestAcc); err != nil {
log.Errorf(ctx, "error fetching remote avatar for account %s: %v", uri, err)
}
- // Ensure the account's avatar media is populated, passing in existing to check for changes.
- if err := d.fetchRemoteAccountHeader(ctx, tsport, account, latestAcc); err != nil {
+ // Ensure the account's avatar media is populated, passing in existing to check for chages.
+ if err := d.fetchAccountHeader(ctx, requestUser, account, latestAcc); err != nil {
log.Errorf(ctx, "error fetching remote header for account %s: %v", uri, err)
}
// Fetch the latest remote account emoji IDs used in account display name/bio.
- if _, err = d.fetchRemoteAccountEmojis(ctx, latestAcc, requestUser); err != nil {
+ if err = d.fetchAccountEmojis(ctx, account, latestAcc); err != nil {
log.Errorf(ctx, "error fetching remote emojis for account %s: %v", uri, err)
}
@@ -779,9 +778,9 @@ func (d *Dereferencer) enrichAccount(
return latestAcc, apubAcc, nil
}
-func (d *Dereferencer) fetchRemoteAccountAvatar(
+func (d *Dereferencer) fetchAccountAvatar(
ctx context.Context,
- tsport transport.Transport,
+ requestUser string,
existingAcc *gtsmodel.Account,
latestAcc *gtsmodel.Account,
) error {
@@ -808,7 +807,7 @@ func (d *Dereferencer) fetchRemoteAccountAvatar(
// Ensuring existing attachment is up-to-date
// and any recaching is performed if required.
existing, err := d.updateAttachment(ctx,
- tsport,
+ requestUser,
existing,
nil,
)
@@ -830,18 +829,23 @@ func (d *Dereferencer) fetchRemoteAccountAvatar(
}
}
- // Fetch newly changed avatar from remote.
- attachment, err := d.loadAttachment(ctx,
- tsport,
+ // Fetch newly changed avatar.
+ attachment, err := d.GetMedia(ctx,
+ requestUser,
latestAcc.ID,
latestAcc.AvatarRemoteURL,
- &media.AdditionalMediaInfo{
+ media.AdditionalMediaInfo{
Avatar: util.Ptr(true),
RemoteURL: &latestAcc.AvatarRemoteURL,
},
)
if err != nil {
- return gtserror.Newf("error loading attachment %s: %w", latestAcc.AvatarRemoteURL, err)
+ if attachment == nil {
+ return gtserror.Newf("error loading attachment %s: %w", latestAcc.AvatarRemoteURL, err)
+ }
+
+ // non-fatal error occurred during loading, still use it.
+ log.Warnf(ctx, "partially loaded attachment: %v", err)
}
// Set the avatar attachment on account model.
@@ -851,9 +855,9 @@ func (d *Dereferencer) fetchRemoteAccountAvatar(
return nil
}
-func (d *Dereferencer) fetchRemoteAccountHeader(
+func (d *Dereferencer) fetchAccountHeader(
ctx context.Context,
- tsport transport.Transport,
+ requestUser string,
existingAcc *gtsmodel.Account,
latestAcc *gtsmodel.Account,
) error {
@@ -880,7 +884,7 @@ func (d *Dereferencer) fetchRemoteAccountHeader(
// Ensuring existing attachment is up-to-date
// and any recaching is performed if required.
existing, err := d.updateAttachment(ctx,
- tsport,
+ requestUser,
existing,
nil,
)
@@ -902,18 +906,23 @@ func (d *Dereferencer) fetchRemoteAccountHeader(
}
}
- // Fetch newly changed header from remote.
- attachment, err := d.loadAttachment(ctx,
- tsport,
+ // Fetch newly changed header.
+ attachment, err := d.GetMedia(ctx,
+ requestUser,
latestAcc.ID,
latestAcc.HeaderRemoteURL,
- &media.AdditionalMediaInfo{
+ media.AdditionalMediaInfo{
Header: util.Ptr(true),
RemoteURL: &latestAcc.HeaderRemoteURL,
},
)
if err != nil {
- return gtserror.Newf("error loading attachment %s: %w", latestAcc.HeaderRemoteURL, err)
+ if attachment == nil {
+ return gtserror.Newf("error loading attachment %s: %w", latestAcc.HeaderRemoteURL, err)
+ }
+
+ // non-fatal error occurred during loading, still use it.
+ log.Warnf(ctx, "partially loaded attachment: %v", err)
}
// Set the header attachment on account model.
@@ -923,119 +932,44 @@ func (d *Dereferencer) fetchRemoteAccountHeader(
return nil
}
-func (d *Dereferencer) fetchRemoteAccountEmojis(ctx context.Context, targetAccount *gtsmodel.Account, requestingUsername string) (bool, error) {
- maybeEmojis := targetAccount.Emojis
- maybeEmojiIDs := targetAccount.EmojiIDs
-
- // It's possible that the account had emoji IDs set on it, but not Emojis
- // themselves, depending on how it was fetched before being passed to us.
- //
- // If we only have IDs, fetch the emojis from the db. We know they're in
- // there or else they wouldn't have IDs.
- if len(maybeEmojiIDs) > len(maybeEmojis) {
- maybeEmojis = make([]*gtsmodel.Emoji, 0, len(maybeEmojiIDs))
- for _, emojiID := range maybeEmojiIDs {
- maybeEmoji, err := d.state.DB.GetEmojiByID(ctx, emojiID)
- if err != nil {
- return false, err
- }
- maybeEmojis = append(maybeEmojis, maybeEmoji)
- }
- }
-
- // For all the maybe emojis we have, we either fetch them from the database
- // (if we haven't already), or dereference them from the remote instance.
- gotEmojis, err := d.populateEmojis(ctx, maybeEmojis, requestingUsername)
- if err != nil {
- return false, err
- }
-
- // Extract the ID of each fetched or dereferenced emoji, so we can attach
- // this to the account if necessary.
- gotEmojiIDs := make([]string, 0, len(gotEmojis))
- for _, e := range gotEmojis {
- gotEmojiIDs = append(gotEmojiIDs, e.ID)
- }
-
- var (
- changed = false // have the emojis for this account changed?
- maybeLen = len(maybeEmojis)
- gotLen = len(gotEmojis)
+func (d *Dereferencer) fetchAccountEmojis(
+ ctx context.Context,
+ existing *gtsmodel.Account,
+ account *gtsmodel.Account,
+) error {
+ // Fetch the updated emojis for our account.
+ emojis, changed, err := d.fetchEmojis(ctx,
+ existing.Emojis,
+ account.Emojis,
)
-
- // if the length of everything is zero, this is simple:
- // nothing has changed and there's nothing to do
- if maybeLen == 0 && gotLen == 0 {
- return changed, nil
+ if err != nil {
+ return gtserror.Newf("error fetching emojis: %w", err)
}
- // if the *amount* of emojis on the account has changed, then the got emojis
- // are definitely different from the previous ones (if there were any) --
- // the account has either more or fewer emojis set on it now, so take the
- // discovered emojis as the new correct ones.
- if maybeLen != gotLen {
- changed = true
- targetAccount.Emojis = gotEmojis
- targetAccount.EmojiIDs = gotEmojiIDs
- return changed, nil
+ if !changed {
+ // Use existing account emoji objects.
+ account.EmojiIDs = existing.EmojiIDs
+ account.Emojis = existing.Emojis
+ return nil
}
- // if the lengths are the same but not all of the slices are
- // zero, something *might* have changed, so we have to check
+ // Set latest emojis.
+ account.Emojis = emojis
- // 1. did we have emojis before that we don't have now?
- for _, maybeEmoji := range maybeEmojis {
- var stillPresent bool
-
- for _, gotEmoji := range gotEmojis {
- if maybeEmoji.URI == gotEmoji.URI {
- // the emoji we maybe had is still present now,
- // so we can stop checking gotEmojis
- stillPresent = true
- break
- }
- }
-
- if !stillPresent {
- // at least one maybeEmoji is no longer present in
- // the got emojis, so we can stop checking now
- changed = true
- targetAccount.Emojis = gotEmojis
- targetAccount.EmojiIDs = gotEmojiIDs
- return changed, nil
- }
+ // Iterate over and set changed emoji IDs.
+ account.EmojiIDs = make([]string, len(emojis))
+ for i, emoji := range emojis {
+ account.EmojiIDs[i] = emoji.ID
}
- // 2. do we have emojis now that we didn't have before?
- for _, gotEmoji := range gotEmojis {
- var wasPresent bool
-
- for _, maybeEmoji := range maybeEmojis {
- // check emoji IDs here as well, because unreferenced
- // maybe emojis we didn't already have would not have
- // had IDs set on them yet
- if gotEmoji.URI == maybeEmoji.URI && gotEmoji.ID == maybeEmoji.ID {
- // this got emoji was present already in the maybeEmoji,
- // so we can stop checking through maybeEmojis
- wasPresent = true
- break
- }
- }
-
- if !wasPresent {
- // at least one gotEmojis was not present in
- // the maybeEmojis, so we can stop checking now
- changed = true
- targetAccount.Emojis = gotEmojis
- targetAccount.EmojiIDs = gotEmojiIDs
- return changed, nil
- }
- }
-
- return changed, nil
+ return nil
}
-func (d *Dereferencer) dereferenceAccountStats(ctx context.Context, requestUser string, account *gtsmodel.Account) error {
+func (d *Dereferencer) dereferenceAccountStats(
+ ctx context.Context,
+ requestUser string,
+ account *gtsmodel.Account,
+) error {
// Ensure we have a stats model for this account.
if account.Stats == nil {
if err := d.state.DB.PopulateAccountStats(ctx, account); err != nil {
diff --git a/internal/federation/dereferencing/emoji.go b/internal/federation/dereferencing/emoji.go
index e81737d04..16f5acf25 100644
--- a/internal/federation/dereferencing/emoji.go
+++ b/internal/federation/dereferencing/emoji.go
@@ -19,29 +19,190 @@ package dereferencing
import (
"context"
- "fmt"
+ "errors"
"io"
"net/url"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/id"
"github.com/superseriousbusiness/gotosocial/internal/log"
"github.com/superseriousbusiness/gotosocial/internal/media"
"github.com/superseriousbusiness/gotosocial/internal/util"
)
-func (d *Dereferencer) GetRemoteEmoji(ctx context.Context, requestUser string, remoteURL string, shortcode string, domain string, id string, emojiURI string, ai *media.AdditionalEmojiInfo, refresh bool) (*media.ProcessingEmoji, error) {
- var shortcodeDomain = shortcode + "@" + domain
-
- // Ensure we have been passed a valid URL.
- derefURI, err := url.Parse(remoteURL)
- if err != nil {
- return nil, fmt.Errorf("GetRemoteEmoji: error parsing url for emoji %s: %s", shortcodeDomain, err)
+// GetEmoji fetches the emoji with given shortcode,
+// domain and remote URL to dereference it by. This
+// handles the case of existing emojis by passing them
+// to RefreshEmoji(), which in the case of a local
+// emoji will be a no-op. If the emoji does not yet
+// exist it will be newly inserted into the database
+// followed by dereferencing the actual media file.
+//
+// Please note that even if an error is returned,
+// an emoji model may still be returned if the error
+// was only encountered during actual dereferencing.
+// In this case, it will act as a placeholder.
+func (d *Dereferencer) GetEmoji(
+ ctx context.Context,
+ shortcode string,
+ domain string,
+ remoteURL string,
+ info media.AdditionalEmojiInfo,
+ refresh bool,
+) (
+ *gtsmodel.Emoji,
+ error,
+) {
+ // Look for an existing emoji with shortcode domain.
+ emoji, err := d.state.DB.GetEmojiByShortcodeDomain(ctx,
+ shortcode,
+ domain,
+ )
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ return nil, gtserror.Newf("error fetching emoji from db: %w", err)
}
- // Acquire derefs lock.
+ if emoji != nil {
+ // This was an existing emoji, pass to refresh func.
+ return d.RefreshEmoji(ctx, emoji, info, refresh)
+ }
+
+ if domain == "" {
+ // failed local lookup, will be db.ErrNoEntries.
+ return nil, gtserror.SetUnretrievable(err)
+ }
+
+ // Generate shortcode domain for locks + logging.
+ shortcodeDomain := shortcode + "@" + domain
+
+ // Ensure we have a valid remote URL.
+ url, err := url.Parse(remoteURL)
+ if err != nil {
+ err := gtserror.Newf("invalid image remote url %s for emoji %s: %w", remoteURL, shortcodeDomain, err)
+ return nil, err
+ }
+
+ // Acquire new instance account transport for emoji dereferencing.
+ tsport, err := d.transportController.NewTransportForUsername(ctx, "")
+ if err != nil {
+ err := gtserror.Newf("error getting instance transport: %w", err)
+ return nil, err
+ }
+
+ // Prepare data function to dereference remote emoji media.
+ data := func(context.Context) (io.ReadCloser, int64, error) {
+ return tsport.DereferenceMedia(ctx, url)
+ }
+
+ // Pass along for safe processing.
+ return d.processEmojiSafely(ctx,
+ shortcodeDomain,
+ func() (*media.ProcessingEmoji, error) {
+ return d.mediaManager.CreateEmoji(ctx,
+ shortcode,
+ domain,
+ data,
+ info,
+ )
+ },
+ )
+}
+
+// RefreshEmoji ensures that the given emoji is
+// up-to-date, both in terms of being cached in
+// in local instance storage, and compared to extra
+// information provided in media.AdditionEmojiInfo{}.
+// (note that is a no-op to pass in a local emoji).
+//
+// Please note that even if an error is returned,
+// an emoji model may still be returned if the error
+// was only encountered during actual dereferencing.
+// In this case, it will act as a placeholder.
+func (d *Dereferencer) RefreshEmoji(
+ ctx context.Context,
+ emoji *gtsmodel.Emoji,
+ info media.AdditionalEmojiInfo,
+ force bool,
+) (
+ *gtsmodel.Emoji,
+ error,
+) {
+ // Can't refresh local.
+ if emoji.IsLocal() {
+ return emoji, nil
+ }
+
+ // Check emoji is up-to-date
+ // with provided extra info.
+ switch {
+ case info.URI != nil &&
+ *info.URI != emoji.URI:
+ force = true
+ case info.ImageRemoteURL != nil &&
+ *info.ImageRemoteURL != emoji.ImageRemoteURL:
+ force = true
+ case info.ImageStaticRemoteURL != nil &&
+ *info.ImageStaticRemoteURL != emoji.ImageStaticRemoteURL:
+ force = true
+ }
+
+ // Check if needs updating.
+ if !force && *emoji.Cached {
+ return emoji, nil
+ }
+
+ // TODO: more finegrained freshness checks.
+
+ // Generate shortcode domain for locks + logging.
+ shortcodeDomain := emoji.Shortcode + "@" + emoji.Domain
+
+ // Ensure we have a valid image remote URL.
+ url, err := url.Parse(emoji.ImageRemoteURL)
+ if err != nil {
+ err := gtserror.Newf("invalid image remote url %s for emoji %s: %w", emoji.ImageRemoteURL, shortcodeDomain, err)
+ return nil, err
+ }
+
+ // Acquire new instance account transport for emoji dereferencing.
+ tsport, err := d.transportController.NewTransportForUsername(ctx, "")
+ if err != nil {
+ err := gtserror.Newf("error getting instance transport: %w", err)
+ return nil, err
+ }
+
+ // Prepare data function to dereference remote emoji media.
+ data := func(context.Context) (io.ReadCloser, int64, error) {
+ return tsport.DereferenceMedia(ctx, url)
+ }
+
+ // Pass along for safe processing.
+ return d.processEmojiSafely(ctx,
+ shortcodeDomain,
+ func() (*media.ProcessingEmoji, error) {
+ return d.mediaManager.RefreshEmoji(ctx,
+ emoji,
+ data,
+ info,
+ )
+ },
+ )
+}
+
+// processingEmojiSafely provides concurrency-safe processing of
+// an emoji with given shortcode+domain. if a copy of the emoji is
+// not already being processed, the given 'process' callback will
+// be used to generate new *media.ProcessingEmoji{} instance.
+func (d *Dereferencer) processEmojiSafely(
+ ctx context.Context,
+ shortcodeDomain string,
+ process func() (*media.ProcessingEmoji, error),
+) (
+ emoji *gtsmodel.Emoji,
+ err error,
+) {
+
+ // Acquire map lock.
d.derefEmojisMu.Lock()
// Ensure unlock only done once.
@@ -53,146 +214,118 @@ func (d *Dereferencer) GetRemoteEmoji(ctx context.Context, requestUser string, r
processing, ok := d.derefEmojis[shortcodeDomain]
if !ok {
- // Fetch a transport for current request user in order to perform request.
- tsport, err := d.transportController.NewTransportForUsername(ctx, requestUser)
+ // Start new processing emoji.
+ processing, err = process()
if err != nil {
- return nil, gtserror.Newf("couldn't create transport: %w", err)
+ return nil, err
}
-
- // Set the media data function to dereference emoji from URI.
- data := func(ctx context.Context) (io.ReadCloser, int64, error) {
- return tsport.DereferenceMedia(ctx, derefURI)
- }
-
- // Create new emoji processing request from the media manager.
- processing, err = d.mediaManager.PreProcessEmoji(ctx, data,
- shortcode,
- id,
- emojiURI,
- ai,
- refresh,
- )
- if err != nil {
- return nil, gtserror.Newf("error preprocessing emoji %s: %s", shortcodeDomain, err)
- }
-
- // Store media in map to mark as processing.
- d.derefEmojis[shortcodeDomain] = processing
-
- defer func() {
- // On exit safely remove emoji from map.
- d.derefEmojisMu.Lock()
- delete(d.derefEmojis, shortcodeDomain)
- d.derefEmojisMu.Unlock()
- }()
}
// Unlock map.
unlock()
- // Start emoji attachment loading (blocking call).
- if _, err := processing.LoadEmoji(ctx); err != nil {
- return nil, err
+ // Perform emoji load operation.
+ emoji, err = processing.Load(ctx)
+ if err != nil {
+ err = gtserror.Newf("error loading emoji %s: %w", shortcodeDomain, err)
+
+ // TODO: in time we should return checkable flags by gtserror.Is___()
+ // which can determine if loading error should allow remaining placeholder.
}
- return processing, nil
+ // Return a COPY of emoji.
+ emoji2 := new(gtsmodel.Emoji)
+ *emoji2 = *emoji
+ return emoji2, err
}
-func (d *Dereferencer) populateEmojis(ctx context.Context, rawEmojis []*gtsmodel.Emoji, requestingUsername string) ([]*gtsmodel.Emoji, error) {
- // At this point we should know:
- // * the AP uri of the emoji
- // * the domain of the emoji
- // * the shortcode of the emoji
- // * the remote URL of the image
- // This should be enough to dereference the emoji
- gotEmojis := make([]*gtsmodel.Emoji, 0, len(rawEmojis))
+func (d *Dereferencer) fetchEmojis(
+ ctx context.Context,
+ existing []*gtsmodel.Emoji,
+ emojis []*gtsmodel.Emoji, // newly dereferenced
+) (
+ []*gtsmodel.Emoji,
+ bool, // any changes?
+ error,
+) {
+ // Track any changes.
+ changed := false
- for _, e := range rawEmojis {
- var gotEmoji *gtsmodel.Emoji
- var err error
- shortcodeDomain := e.Shortcode + "@" + e.Domain
+ for i, placeholder := range emojis {
+ // Look for an existing emoji with shortcode + domain.
+ existing, ok := getEmojiByShortcodeDomain(existing,
+ placeholder.Shortcode,
+ placeholder.Domain,
+ )
+ if ok && existing.ID != "" {
- // check if we already know this emoji
- if e.ID != "" {
- // we had an ID for this emoji already, which means
- // it should be fleshed out already and we won't
- // have to get it from the database again
- gotEmoji = e
- } else if gotEmoji, err = d.state.DB.GetEmojiByShortcodeDomain(ctx, e.Shortcode, e.Domain); err != nil && err != db.ErrNoEntries {
- log.Errorf(ctx, "error checking database for emoji %s: %s", shortcodeDomain, err)
+ // Check for any emoji changes that
+ // indicate we should force a refresh.
+ force := emojiChanged(existing, placeholder)
+
+ // Ensure that the existing emoji model is up-to-date and cached.
+ existing, err := d.RefreshEmoji(ctx, existing, media.AdditionalEmojiInfo{
+
+ // Set latest values from placeholder.
+ URI: &placeholder.URI,
+ ImageRemoteURL: &placeholder.ImageRemoteURL,
+ ImageStaticRemoteURL: &placeholder.ImageStaticRemoteURL,
+ }, force)
+ if err != nil {
+ log.Errorf(ctx, "error refreshing emoji: %v", err)
+
+ // specifically do NOT continue here,
+ // we already have a model, we don't
+ // want to drop it from the slice, just
+ // log that an update for it failed.
+ }
+
+ // Set existing emoji.
+ emojis[i] = existing
continue
}
- var refresh bool
+ // Emojis changed!
+ changed = true
- if gotEmoji != nil {
- // we had the emoji already, but refresh it if necessary
- if e.UpdatedAt.Unix() > gotEmoji.ImageUpdatedAt.Unix() {
- log.Tracef(ctx, "emoji %s was updated since we last saw it, will refresh", shortcodeDomain)
- refresh = true
- }
-
- if !refresh && (e.URI != gotEmoji.URI) {
- log.Tracef(ctx, "emoji %s changed URI since we last saw it, will refresh", shortcodeDomain)
- refresh = true
- }
-
- if !refresh && (e.ImageRemoteURL != gotEmoji.ImageRemoteURL) {
- log.Tracef(ctx, "emoji %s changed image URL since we last saw it, will refresh", shortcodeDomain)
- refresh = true
- }
-
- if !refresh {
- log.Tracef(ctx, "emoji %s is up to date, will not refresh", shortcodeDomain)
- } else {
- log.Tracef(ctx, "refreshing emoji %s", shortcodeDomain)
- emojiID := gotEmoji.ID // use existing ID
- processingEmoji, err := d.GetRemoteEmoji(ctx, requestingUsername, e.ImageRemoteURL, e.Shortcode, e.Domain, emojiID, e.URI, &media.AdditionalEmojiInfo{
- Domain: &e.Domain,
- ImageRemoteURL: &e.ImageRemoteURL,
- ImageStaticRemoteURL: &e.ImageStaticRemoteURL,
- Disabled: gotEmoji.Disabled,
- VisibleInPicker: gotEmoji.VisibleInPicker,
- }, refresh)
- if err != nil {
- log.Errorf(ctx, "couldn't refresh remote emoji %s: %s", shortcodeDomain, err)
- continue
- }
-
- if gotEmoji, err = processingEmoji.LoadEmoji(ctx); err != nil {
- log.Errorf(ctx, "couldn't load refreshed remote emoji %s: %s", shortcodeDomain, err)
- continue
- }
- }
- } else {
- // it's new! go get it!
- newEmojiID, err := id.NewRandomULID()
- if err != nil {
- log.Errorf(ctx, "error generating id for remote emoji %s: %s", shortcodeDomain, err)
+ // Fetch this newly added emoji,
+ // this function handles the case
+ // of existing cached emojis and
+ // new ones requiring dereference.
+ emoji, err := d.GetEmoji(ctx,
+ placeholder.Shortcode,
+ placeholder.Domain,
+ placeholder.ImageRemoteURL,
+ media.AdditionalEmojiInfo{
+ URI: &placeholder.URI,
+ ImageRemoteURL: &placeholder.ImageRemoteURL,
+ ImageStaticRemoteURL: &placeholder.ImageStaticRemoteURL,
+ },
+ false,
+ )
+ if err != nil {
+ if emoji == nil {
+ log.Errorf(ctx, "error loading emoji %s: %v", placeholder.ImageRemoteURL, err)
continue
}
- processingEmoji, err := d.GetRemoteEmoji(ctx, requestingUsername, e.ImageRemoteURL, e.Shortcode, e.Domain, newEmojiID, e.URI, &media.AdditionalEmojiInfo{
- Domain: &e.Domain,
- ImageRemoteURL: &e.ImageRemoteURL,
- ImageStaticRemoteURL: &e.ImageStaticRemoteURL,
- Disabled: e.Disabled,
- VisibleInPicker: e.VisibleInPicker,
- }, refresh)
- if err != nil {
- log.Errorf(ctx, "couldn't get remote emoji %s: %s", shortcodeDomain, err)
- continue
- }
-
- if gotEmoji, err = processingEmoji.LoadEmoji(ctx); err != nil {
- log.Errorf(ctx, "couldn't load remote emoji %s: %s", shortcodeDomain, err)
- continue
- }
+ // non-fatal error occurred during loading, still use it.
+ log.Warnf(ctx, "partially loaded emoji: %v", err)
}
- // if we get here, we either had the emoji already or we successfully fetched it
- gotEmojis = append(gotEmojis, gotEmoji)
+ // Set updated emoji.
+ emojis[i] = emoji
}
- return gotEmojis, nil
+ for i := 0; i < len(emojis); {
+ if emojis[i].ID == "" {
+ // Remove failed emoji populations.
+ copy(emojis[i:], emojis[i+1:])
+ emojis = emojis[:len(emojis)-1]
+ continue
+ }
+ i++
+ }
+
+ return emojis, changed, nil
}
diff --git a/internal/federation/dereferencing/emoji_test.go b/internal/federation/dereferencing/emoji_test.go
index 08365741f..fdb815762 100644
--- a/internal/federation/dereferencing/emoji_test.go
+++ b/internal/federation/dereferencing/emoji_test.go
@@ -19,6 +19,7 @@ package dereferencing_test
import (
"context"
+ "fmt"
"testing"
"time"
@@ -32,48 +33,50 @@ type EmojiTestSuite struct {
func (suite *EmojiTestSuite) TestDereferenceEmojiBlocking() {
ctx := context.Background()
- fetchingAccount := suite.testAccounts["local_account_1"]
emojiImageRemoteURL := "http://example.org/media/emojis/1781772.gif"
emojiImageStaticRemoteURL := "http://example.org/media/emojis/1781772.gif"
emojiURI := "http://example.org/emojis/1781772"
emojiShortcode := "peglin"
- emojiID := "01GCBMGNZBKMEE1KTZ6PMJEW5D"
emojiDomain := "example.org"
emojiDisabled := false
emojiVisibleInPicker := false
- ai := &media.AdditionalEmojiInfo{
- Domain: &emojiDomain,
- ImageRemoteURL: &emojiImageRemoteURL,
- ImageStaticRemoteURL: &emojiImageStaticRemoteURL,
- Disabled: &emojiDisabled,
- VisibleInPicker: &emojiVisibleInPicker,
- }
-
- processingEmoji, err := suite.dereferencer.GetRemoteEmoji(ctx, fetchingAccount.Username, emojiImageRemoteURL, emojiShortcode, emojiDomain, emojiID, emojiURI, ai, false)
- suite.NoError(err)
-
- // make a blocking call to load the emoji from the in-process media
- emoji, err := processingEmoji.LoadEmoji(ctx)
+ emoji, err := suite.dereferencer.GetEmoji(
+ ctx,
+ emojiShortcode,
+ emojiDomain,
+ emojiImageRemoteURL,
+ media.AdditionalEmojiInfo{
+ URI: &emojiURI,
+ Domain: &emojiDomain,
+ ImageRemoteURL: &emojiImageRemoteURL,
+ ImageStaticRemoteURL: &emojiImageStaticRemoteURL,
+ Disabled: &emojiDisabled,
+ VisibleInPicker: &emojiVisibleInPicker,
+ },
+ false,
+ )
suite.NoError(err)
suite.NotNil(emoji)
- suite.Equal(emojiID, emoji.ID)
+ expectPath := fmt.Sprintf("/emoji/original/%s.gif", emoji.ID)
+ expectStaticPath := fmt.Sprintf("/emoji/static/%s.png", emoji.ID)
+
suite.WithinDuration(time.Now(), emoji.CreatedAt, 10*time.Second)
suite.WithinDuration(time.Now(), emoji.UpdatedAt, 10*time.Second)
suite.Equal(emojiShortcode, emoji.Shortcode)
suite.Equal(emojiDomain, emoji.Domain)
suite.Equal(emojiImageRemoteURL, emoji.ImageRemoteURL)
suite.Equal(emojiImageStaticRemoteURL, emoji.ImageStaticRemoteURL)
- suite.Contains(emoji.ImageURL, "/emoji/original/01GCBMGNZBKMEE1KTZ6PMJEW5D.gif")
- suite.Contains(emoji.ImageStaticURL, "emoji/static/01GCBMGNZBKMEE1KTZ6PMJEW5D.png")
- suite.Contains(emoji.ImagePath, "/emoji/original/01GCBMGNZBKMEE1KTZ6PMJEW5D.gif")
- suite.Contains(emoji.ImageStaticPath, "/emoji/static/01GCBMGNZBKMEE1KTZ6PMJEW5D.png")
+ suite.Contains(emoji.ImageURL, expectPath)
+ suite.Contains(emoji.ImageStaticURL, expectStaticPath)
+ suite.Contains(emoji.ImagePath, expectPath)
+ suite.Contains(emoji.ImageStaticPath, expectStaticPath)
suite.Equal("image/gif", emoji.ImageContentType)
suite.Equal("image/png", emoji.ImageStaticContentType)
suite.Equal(37796, emoji.ImageFileSize)
suite.Equal(7951, emoji.ImageStaticFileSize)
- suite.WithinDuration(time.Now(), emoji.ImageUpdatedAt, 10*time.Second)
+ suite.WithinDuration(time.Now(), emoji.UpdatedAt, 10*time.Second)
suite.False(*emoji.Disabled)
suite.Equal(emojiURI, emoji.URI)
suite.False(*emoji.VisibleInPicker)
diff --git a/internal/federation/dereferencing/media.go b/internal/federation/dereferencing/media.go
new file mode 100644
index 000000000..874107b13
--- /dev/null
+++ b/internal/federation/dereferencing/media.go
@@ -0,0 +1,215 @@
+// GoToSocial
+// Copyright (C) GoToSocial Authors admin@gotosocial.org
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package dereferencing
+
+import (
+ "context"
+ "io"
+ "net/url"
+
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "github.com/superseriousbusiness/gotosocial/internal/media"
+)
+
+// GetMedia fetches the media at given remote URL by
+// dereferencing it. The passed accountID is used to
+// store it as being owned by that account. Additional
+// information to set on the media attachment may also
+// be provided.
+//
+// Please note that even if an error is returned,
+// a media model may still be returned if the error
+// was only encountered during actual dereferencing.
+// In this case, it will act as a placeholder.
+//
+// Also note that since account / status dereferencing is
+// already protected by per-uri locks, and that fediverse
+// media is generally not shared between accounts (etc),
+// there aren't any concurrency protections against multiple
+// insertion / dereferencing of media at remoteURL. Worst
+// case scenario, an extra media entry will be inserted
+// and the scheduled cleaner.Cleaner{} will catch it!
+func (d *Dereferencer) GetMedia(
+ ctx context.Context,
+ requestUser string,
+ accountID string, // media account owner
+ remoteURL string,
+ info media.AdditionalMediaInfo,
+) (
+ *gtsmodel.MediaAttachment,
+ error,
+) {
+ // Parse str as valid URL object.
+ url, err := url.Parse(remoteURL)
+ if err != nil {
+ return nil, gtserror.Newf("invalid remote media url %q: %v", remoteURL, err)
+ }
+
+ // Fetch transport for the provided request user from controller.
+ tsport, err := d.transportController.NewTransportForUsername(ctx,
+ requestUser,
+ )
+ if err != nil {
+ return nil, gtserror.Newf("failed getting transport for %s: %w", requestUser, err)
+ }
+
+ // Start processing remote attachment at URL.
+ processing, err := d.mediaManager.CreateMedia(
+ ctx,
+ accountID,
+ func(ctx context.Context) (io.ReadCloser, int64, error) {
+ return tsport.DereferenceMedia(ctx, url)
+ },
+ info,
+ )
+ if err != nil {
+ return nil, err
+ }
+
+ // Perform media load operation.
+ media, err := processing.Load(ctx)
+ if err != nil {
+ err = gtserror.Newf("error loading media %s: %w", media.RemoteURL, err)
+
+ // TODO: in time we should return checkable flags by gtserror.Is___()
+ // which can determine if loading error should allow remaining placeholder.
+ }
+
+ return media, err
+}
+
+// RefreshMedia ensures that given media is up-to-date,
+// both in terms of being cached in local instance,
+// storage and compared to extra info in information
+// in given gtsmodel.AdditionMediaInfo{}. This handles
+// the case of local emoji by returning early.
+//
+// Please note that even if an error is returned,
+// a media model may still be returned if the error
+// was only encountered during actual dereferencing.
+// In this case, it will act as a placeholder.
+//
+// Also note that since account / status dereferencing is
+// already protected by per-uri locks, and that fediverse
+// media is generally not shared between accounts (etc),
+// there aren't any concurrency protections against multiple
+// insertion / dereferencing of media at remoteURL. Worst
+// case scenario, an extra media entry will be inserted
+// and the scheduled cleaner.Cleaner{} will catch it!
+func (d *Dereferencer) RefreshMedia(
+ ctx context.Context,
+ requestUser string,
+ media *gtsmodel.MediaAttachment,
+ info media.AdditionalMediaInfo,
+ force bool,
+) (
+ *gtsmodel.MediaAttachment,
+ error,
+) {
+ // Can't refresh local.
+ if media.IsLocal() {
+ return media, nil
+ }
+
+ // Check emoji is up-to-date
+ // with provided extra info.
+ switch {
+ case info.Blurhash != nil &&
+ *info.Blurhash != media.Blurhash:
+ force = true
+ case info.Description != nil &&
+ *info.Description != media.Description:
+ force = true
+ case info.RemoteURL != nil &&
+ *info.RemoteURL != media.RemoteURL:
+ force = true
+ }
+
+ // Check if needs updating.
+ if !force && *media.Cached {
+ return media, nil
+ }
+
+ // TODO: more finegrained freshness checks.
+
+ // Ensure we have a valid remote URL.
+ url, err := url.Parse(media.RemoteURL)
+ if err != nil {
+ err := gtserror.Newf("invalid media remote url %s: %w", media.RemoteURL, err)
+ return nil, err
+ }
+
+ // Fetch transport for the provided request user from controller.
+ tsport, err := d.transportController.NewTransportForUsername(ctx,
+ requestUser,
+ )
+ if err != nil {
+ return nil, gtserror.Newf("failed getting transport for %s: %w", requestUser, err)
+ }
+
+ // Start processing remote attachment recache.
+ processing := d.mediaManager.RecacheMedia(
+ media,
+ func(ctx context.Context) (io.ReadCloser, int64, error) {
+ return tsport.DereferenceMedia(ctx, url)
+ },
+ )
+
+ // Perform media load operation.
+ media, err = processing.Load(ctx)
+ if err != nil {
+ err = gtserror.Newf("error loading media %s: %w", media.RemoteURL, err)
+
+ // TODO: in time we should return checkable flags by gtserror.Is___()
+ // which can determine if loading error should allow remaining placeholder.
+ }
+
+ return media, err
+}
+
+// updateAttachment handles the case of an existing media attachment
+// that *may* have changes or need recaching. it checks for changed
+// fields, updating in the database if so, and recaches uncached media.
+func (d *Dereferencer) updateAttachment(
+ ctx context.Context,
+ requestUser string,
+ existing *gtsmodel.MediaAttachment, // existing attachment
+ attach *gtsmodel.MediaAttachment, // (optional) changed media
+) (
+ *gtsmodel.MediaAttachment, // always set
+ error,
+) {
+ var info media.AdditionalMediaInfo
+
+ if attach != nil {
+ // Set optional extra information,
+ // (will later check for changes).
+ info.Description = &attach.Description
+ info.Blurhash = &attach.Blurhash
+ info.RemoteURL = &attach.RemoteURL
+ }
+
+ // Ensure media is cached.
+ return d.RefreshMedia(ctx,
+ requestUser,
+ existing,
+ info,
+ false,
+ )
+}
diff --git a/internal/federation/dereferencing/status.go b/internal/federation/dereferencing/status.go
index add12c31f..406534457 100644
--- a/internal/federation/dereferencing/status.go
+++ b/internal/federation/dereferencing/status.go
@@ -33,7 +33,6 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/id"
"github.com/superseriousbusiness/gotosocial/internal/log"
"github.com/superseriousbusiness/gotosocial/internal/media"
- "github.com/superseriousbusiness/gotosocial/internal/transport"
"github.com/superseriousbusiness/gotosocial/internal/util"
)
@@ -536,12 +535,12 @@ func (d *Dereferencer) enrichStatus(
}
// Ensure the status' media attachments are populated, passing in existing to check for changes.
- if err := d.fetchStatusAttachments(ctx, tsport, status, latestStatus); err != nil {
+ if err := d.fetchStatusAttachments(ctx, requestUser, status, latestStatus); err != nil {
return nil, nil, gtserror.Newf("error populating attachments for status %s: %w", uri, err)
}
- // Ensure the status' emoji attachments are populated, (changes are expected / okay).
- if err := d.fetchStatusEmojis(ctx, requestUser, latestStatus); err != nil {
+ // Ensure the status' emoji attachments are populated, passing in existing to check for changes.
+ if err := d.fetchStatusEmojis(ctx, status, latestStatus); err != nil {
return nil, nil, gtserror.Newf("error populating emojis for status %s: %w", uri, err)
}
@@ -643,79 +642,12 @@ func (d *Dereferencer) isPermittedStatus(
return onFail()
}
-// populateMentionTarget tries to populate the given
-// mention with the correct TargetAccount and (if not
-// yet set) TargetAccountURI, returning the populated
-// mention.
-//
-// Will check on the existing status if the mention
-// is already there and populated; if so, existing
-// mention will be returned along with `true`.
-//
-// Otherwise, this function will try to parse first
-// the Href of the mention, and then the namestring,
-// to see who it targets, and go fetch that account.
-func (d *Dereferencer) populateMentionTarget(
+func (d *Dereferencer) fetchStatusMentions(
ctx context.Context,
- mention *gtsmodel.Mention,
requestUser string,
- existing, status *gtsmodel.Status,
-) (
- *gtsmodel.Mention,
- bool, // True if mention already exists in the DB.
- error,
-) {
- // Mentions can be created using Name or Href.
- // Prefer Href (TargetAccountURI), fall back to Name.
- if mention.TargetAccountURI != "" {
- // Look for existing mention with this URI.
- // If we already have it we can return early.
- existingMention, ok := existing.GetMentionByTargetURI(mention.TargetAccountURI)
- if ok && existingMention.ID != "" {
- return existingMention, true, nil
- }
-
- // Ensure that mention account URI is parseable.
- accountURI, err := url.Parse(mention.TargetAccountURI)
- if err != nil {
- err = gtserror.Newf("invalid account uri %q: %w", mention.TargetAccountURI, err)
- return nil, false, err
- }
-
- // Ensure we have the account of the mention target dereferenced.
- mention.TargetAccount, _, err = d.getAccountByURI(ctx, requestUser, accountURI)
- if err != nil {
- err = gtserror.Newf("failed to dereference account %s: %w", accountURI, err)
- return nil, false, err
- }
- } else {
- // Href wasn't set. Find the target account using namestring.
- username, domain, err := util.ExtractNamestringParts(mention.NameString)
- if err != nil {
- err = gtserror.Newf("failed to parse namestring %s: %w", mention.NameString, err)
- return nil, false, err
- }
-
- mention.TargetAccount, _, err = d.getAccountByUsernameDomain(ctx, requestUser, username, domain)
- if err != nil {
- err = gtserror.Newf("failed to dereference account %s: %w", mention.NameString, err)
- return nil, false, err
- }
-
- // Look for existing mention with this URI.
- mention.TargetAccountURI = mention.TargetAccount.URI
- existingMention, ok := existing.GetMentionByTargetURI(mention.TargetAccountURI)
- if ok && existingMention.ID != "" {
- return existingMention, true, nil
- }
- }
-
- // At this point, mention.TargetAccountURI
- // and mention.TargetAccount must be set.
- return mention, false, nil
-}
-
-func (d *Dereferencer) fetchStatusMentions(ctx context.Context, requestUser string, existing, status *gtsmodel.Status) error {
+ existing *gtsmodel.Status,
+ status *gtsmodel.Status,
+) error {
// Allocate new slice to take the yet-to-be created mention IDs.
status.MentionIDs = make([]string, len(status.Mentions))
@@ -728,10 +660,10 @@ func (d *Dereferencer) fetchStatusMentions(ctx context.Context, requestUser stri
mention, alreadyExists, err = d.populateMentionTarget(
ctx,
- mention,
requestUser,
existing,
status,
+ mention,
)
if err != nil {
log.Errorf(ctx, "failed to derive mention: %v", err)
@@ -845,7 +777,11 @@ func (d *Dereferencer) threadStatus(ctx context.Context, status *gtsmodel.Status
return nil
}
-func (d *Dereferencer) fetchStatusTags(ctx context.Context, existing, status *gtsmodel.Status) error {
+func (d *Dereferencer) fetchStatusTags(
+ ctx context.Context,
+ existing *gtsmodel.Status,
+ status *gtsmodel.Status,
+) error {
// Allocate new slice to take the yet-to-be determined tag IDs.
status.TagIDs = make([]string, len(status.Tags))
@@ -900,7 +836,11 @@ func (d *Dereferencer) fetchStatusTags(ctx context.Context, existing, status *gt
return nil
}
-func (d *Dereferencer) fetchStatusPoll(ctx context.Context, existing, status *gtsmodel.Status) error {
+func (d *Dereferencer) fetchStatusPoll(
+ ctx context.Context,
+ existing *gtsmodel.Status,
+ status *gtsmodel.Status,
+) error {
var (
// insertStatusPoll generates ID and inserts the poll attached to status into the database.
insertStatusPoll = func(ctx context.Context, status *gtsmodel.Status) error {
@@ -990,19 +930,24 @@ func (d *Dereferencer) fetchStatusPoll(ctx context.Context, existing, status *gt
}
}
-func (d *Dereferencer) fetchStatusAttachments(ctx context.Context, tsport transport.Transport, existing, status *gtsmodel.Status) error {
+func (d *Dereferencer) fetchStatusAttachments(
+ ctx context.Context,
+ requestUser string,
+ existing *gtsmodel.Status,
+ status *gtsmodel.Status,
+) error {
// Allocate new slice to take the yet-to-be fetched attachment IDs.
status.AttachmentIDs = make([]string, len(status.Attachments))
for i := range status.Attachments {
- attachment := status.Attachments[i]
+ placeholder := status.Attachments[i]
// Look for existing media attachment with remote URL first.
- existing, ok := existing.GetAttachmentByRemoteURL(attachment.RemoteURL)
+ existing, ok := existing.GetAttachmentByRemoteURL(placeholder.RemoteURL)
if ok && existing.ID != "" {
// Ensure the existing media attachment is up-to-date and cached.
- existing, err := d.updateAttachment(ctx, tsport, existing, attachment)
+ existing, err := d.updateAttachment(ctx, requestUser, existing, placeholder)
if err != nil {
log.Errorf(ctx, "error updating existing attachment: %v", err)
@@ -1019,25 +964,25 @@ func (d *Dereferencer) fetchStatusAttachments(ctx context.Context, tsport transp
}
// Load this new media attachment.
- attachment, err := d.loadAttachment(
+ attachment, err := d.GetMedia(
ctx,
- tsport,
+ requestUser,
status.AccountID,
- attachment.RemoteURL,
- &media.AdditionalMediaInfo{
+ placeholder.RemoteURL,
+ media.AdditionalMediaInfo{
StatusID: &status.ID,
- RemoteURL: &attachment.RemoteURL,
- Description: &attachment.Description,
- Blurhash: &attachment.Blurhash,
+ RemoteURL: &placeholder.RemoteURL,
+ Description: &placeholder.Description,
+ Blurhash: &placeholder.Blurhash,
},
)
- if err != nil && attachment == nil {
- log.Errorf(ctx, "error loading attachment: %v", err)
- continue
- }
-
if err != nil {
- // A non-fatal error occurred during loading.
+ if attachment == nil {
+ log.Errorf(ctx, "error loading attachment %s: %v", placeholder.RemoteURL, err)
+ continue
+ }
+
+ // non-fatal error occurred during loading, still use it.
log.Warnf(ctx, "partially loaded attachment: %v", err)
}
@@ -1061,22 +1006,108 @@ func (d *Dereferencer) fetchStatusAttachments(ctx context.Context, tsport transp
return nil
}
-func (d *Dereferencer) fetchStatusEmojis(ctx context.Context, requestUser string, status *gtsmodel.Status) error {
- // Fetch the full-fleshed-out emoji objects for our status.
- emojis, err := d.populateEmojis(ctx, status.Emojis, requestUser)
+func (d *Dereferencer) fetchStatusEmojis(
+ ctx context.Context,
+ existing *gtsmodel.Status,
+ status *gtsmodel.Status,
+) error {
+ // Fetch the updated emojis for our status.
+ emojis, changed, err := d.fetchEmojis(ctx,
+ existing.Emojis,
+ status.Emojis,
+ )
if err != nil {
- return gtserror.Newf("failed to populate emojis: %w", err)
+ return gtserror.Newf("error fetching emojis: %w", err)
}
- // Iterate over and get their IDs.
- emojiIDs := make([]string, 0, len(emojis))
- for _, e := range emojis {
- emojiIDs = append(emojiIDs, e.ID)
+ if !changed {
+ // Use existing status emoji objects.
+ status.EmojiIDs = existing.EmojiIDs
+ status.Emojis = existing.Emojis
+ return nil
}
- // Set known emoji details.
+ // Set latest emojis.
status.Emojis = emojis
- status.EmojiIDs = emojiIDs
+
+ // Iterate over and set changed emoji IDs.
+ status.EmojiIDs = make([]string, len(emojis))
+ for i, emoji := range emojis {
+ status.EmojiIDs[i] = emoji.ID
+ }
return nil
}
+
+// populateMentionTarget tries to populate the given
+// mention with the correct TargetAccount and (if not
+// yet set) TargetAccountURI, returning the populated
+// mention.
+//
+// Will check on the existing status if the mention
+// is already there and populated; if so, existing
+// mention will be returned along with `true`.
+//
+// Otherwise, this function will try to parse first
+// the Href of the mention, and then the namestring,
+// to see who it targets, and go fetch that account.
+func (d *Dereferencer) populateMentionTarget(
+ ctx context.Context,
+ requestUser string,
+ existing *gtsmodel.Status,
+ status *gtsmodel.Status,
+ mention *gtsmodel.Mention,
+) (
+ *gtsmodel.Mention,
+ bool, // True if mention already exists in the DB.
+ error,
+) {
+ // Mentions can be created using Name or Href.
+ // Prefer Href (TargetAccountURI), fall back to Name.
+ if mention.TargetAccountURI != "" {
+ // Look for existing mention with this URI.
+ // If we already have it we can return early.
+ existingMention, ok := existing.GetMentionByTargetURI(mention.TargetAccountURI)
+ if ok && existingMention.ID != "" {
+ return existingMention, true, nil
+ }
+
+ // Ensure that mention account URI is parseable.
+ accountURI, err := url.Parse(mention.TargetAccountURI)
+ if err != nil {
+ err = gtserror.Newf("invalid account uri %q: %w", mention.TargetAccountURI, err)
+ return nil, false, err
+ }
+
+ // Ensure we have the account of the mention target dereferenced.
+ mention.TargetAccount, _, err = d.getAccountByURI(ctx, requestUser, accountURI)
+ if err != nil {
+ err = gtserror.Newf("failed to dereference account %s: %w", accountURI, err)
+ return nil, false, err
+ }
+ } else {
+ // Href wasn't set. Find the target account using namestring.
+ username, domain, err := util.ExtractNamestringParts(mention.NameString)
+ if err != nil {
+ err = gtserror.Newf("failed to parse namestring %s: %w", mention.NameString, err)
+ return nil, false, err
+ }
+
+ mention.TargetAccount, _, err = d.getAccountByUsernameDomain(ctx, requestUser, username, domain)
+ if err != nil {
+ err = gtserror.Newf("failed to dereference account %s: %w", mention.NameString, err)
+ return nil, false, err
+ }
+
+ // Look for existing mention with this URI.
+ mention.TargetAccountURI = mention.TargetAccount.URI
+ existingMention, ok := existing.GetMentionByTargetURI(mention.TargetAccountURI)
+ if ok && existingMention.ID != "" {
+ return existingMention, true, nil
+ }
+ }
+
+ // At this point, mention.TargetAccountURI
+ // and mention.TargetAccount must be set.
+ return mention, false, nil
+}
diff --git a/internal/federation/dereferencing/util.go b/internal/federation/dereferencing/util.go
index 5cb7a0106..297e90adc 100644
--- a/internal/federation/dereferencing/util.go
+++ b/internal/federation/dereferencing/util.go
@@ -18,120 +18,36 @@
package dereferencing
import (
- "context"
- "io"
- "net/url"
"slices"
- "github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/media"
- "github.com/superseriousbusiness/gotosocial/internal/transport"
- "github.com/superseriousbusiness/gotosocial/internal/util"
)
-// loadAttachment handles the case of a new media attachment
-// that requires loading. it stores and caches from given data.
-func (d *Dereferencer) loadAttachment(
- ctx context.Context,
- tsport transport.Transport,
- accountID string, // media account owner
- remoteURL string,
- info *media.AdditionalMediaInfo,
+// getEmojiByShortcodeDomain searches input slice
+// for emoji with given shortcode and domain.
+func getEmojiByShortcodeDomain(
+ emojis []*gtsmodel.Emoji,
+ shortcode string,
+ domain string,
) (
- *gtsmodel.MediaAttachment,
- error,
+ *gtsmodel.Emoji,
+ bool,
) {
- // Parse str as valid URL object.
- url, err := url.Parse(remoteURL)
- if err != nil {
- return nil, gtserror.Newf("invalid remote media url %q: %v", remoteURL, err)
+ for _, emoji := range emojis {
+ if emoji.Shortcode == shortcode &&
+ emoji.Domain == domain {
+ return emoji, true
+ }
}
-
- // Start pre-processing remote media at remote URL.
- processing := d.mediaManager.PreProcessMedia(
- func(ctx context.Context) (io.ReadCloser, int64, error) {
- return tsport.DereferenceMedia(ctx, url)
- },
- accountID,
- info,
- )
-
- // Force attachment loading *right now*.
- return processing.LoadAttachment(ctx)
+ return nil, false
}
-// updateAttachment handles the case of an existing media attachment
-// that *may* have changes or need recaching. it checks for changed
-// fields, updating in the database if so, and recaches uncached media.
-func (d *Dereferencer) updateAttachment(
- ctx context.Context,
- tsport transport.Transport,
- existing *gtsmodel.MediaAttachment, // existing attachment
- media *gtsmodel.MediaAttachment, // (optional) changed media
-) (
- *gtsmodel.MediaAttachment, // always set
- error,
-) {
- if media != nil {
- // Possible changed media columns.
- changed := make([]string, 0, 3)
-
- // Check if attachment description has changed.
- if existing.Description != media.Description {
- changed = append(changed, "description")
- existing.Description = media.Description
- }
-
- // Check if attachment blurhash has changed (i.e. content change).
- if existing.Blurhash != media.Blurhash && media.Blurhash != "" {
- changed = append(changed, "blurhash", "cached")
- existing.Blurhash = media.Blurhash
- existing.Cached = util.Ptr(false)
- }
-
- if len(changed) > 0 {
- // Update the existing attachment model in the database.
- err := d.state.DB.UpdateAttachment(ctx, existing, changed...)
- if err != nil {
- return media, gtserror.Newf("error updating media: %w", err)
- }
- }
- }
-
- // Check if cached.
- if *existing.Cached {
- return existing, nil
- }
-
- // Parse str as valid URL object.
- url, err := url.Parse(existing.RemoteURL)
- if err != nil {
- return nil, gtserror.Newf("invalid remote media url %q: %v", media.RemoteURL, err)
- }
-
- // Start pre-processing remote media recaching from remote.
- processing, err := d.mediaManager.PreProcessMediaRecache(
- ctx,
- func(ctx context.Context) (io.ReadCloser, int64, error) {
- return tsport.DereferenceMedia(ctx, url)
- },
- existing.ID,
- )
- if err != nil {
- return nil, gtserror.Newf("error processing recache: %w", err)
- }
-
- // Force load attachment recache *right now*.
- recached, err := processing.LoadAttachment(ctx)
-
- // Always return the error we
- // receive, but ensure we return
- // most up-to-date media file.
- if recached != nil {
- return recached, err
- }
- return existing, err
+// emojiChanged returns whether an emoji has changed in a way
+// that indicates that it should be refetched and refreshed.
+func emojiChanged(existing, latest *gtsmodel.Emoji) bool {
+ return existing.URI != latest.URI ||
+ existing.ImageRemoteURL != latest.ImageRemoteURL ||
+ existing.ImageStaticRemoteURL != latest.ImageStaticRemoteURL
}
// pollChanged returns whether a poll has changed in way that
diff --git a/internal/gtsmodel/emoji.go b/internal/gtsmodel/emoji.go
index b377b1440..c80e98ecb 100644
--- a/internal/gtsmodel/emoji.go
+++ b/internal/gtsmodel/emoji.go
@@ -30,19 +30,18 @@ type Emoji struct {
ImageStaticRemoteURL string `bun:",nullzero"` // Where can a static / non-animated version of this emoji be retrieved remotely? Null for local emojis.
ImageURL string `bun:",nullzero"` // Where can this emoji be retrieved from the local server? Null for remote emojis.
ImageStaticURL string `bun:",nullzero"` // Where can a static version of this emoji be retrieved from the local server? Null for remote emojis.
- ImagePath string `bun:",nullzero,notnull"` // Path of the emoji image in the server storage system.
- ImageStaticPath string `bun:",nullzero,notnull"` // Path of a static version of the emoji image in the server storage system
- ImageContentType string `bun:",nullzero,notnull"` // MIME content type of the emoji image
- ImageStaticContentType string `bun:",nullzero,notnull"` // MIME content type of the static version of the emoji image.
- ImageFileSize int `bun:",nullzero,notnull"` // Size of the emoji image file in bytes, for serving purposes.
- ImageStaticFileSize int `bun:",nullzero,notnull"` // Size of the static version of the emoji image file in bytes, for serving purposes.
- ImageUpdatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // When was the emoji image last updated?
+ ImagePath string `bun:",notnull"` // Path of the emoji image in the server storage system.
+ ImageStaticPath string `bun:",notnull"` // Path of a static version of the emoji image in the server storage system
+ ImageContentType string `bun:",notnull"` // MIME content type of the emoji image
+ ImageStaticContentType string `bun:",notnull"` // MIME content type of the static version of the emoji image.
+ ImageFileSize int `bun:",notnull"` // Size of the emoji image file in bytes, for serving purposes.
+ ImageStaticFileSize int `bun:",notnull"` // Size of the static version of the emoji image file in bytes, for serving purposes.
Disabled *bool `bun:",nullzero,notnull,default:false"` // Has a moderation action disabled this emoji from being shown?
URI string `bun:",nullzero,notnull,unique"` // ActivityPub uri of this emoji. Something like 'https://example.org/emojis/1234'
VisibleInPicker *bool `bun:",nullzero,notnull,default:true"` // Is this emoji visible in the admin emoji picker?
Category *EmojiCategory `bun:"rel:belongs-to"` // In which emoji category is this emoji visible?
CategoryID string `bun:"type:CHAR(26),nullzero"` // ID of the category this emoji belongs to.
- Cached *bool `bun:",nullzero,notnull,default:false"`
+ Cached *bool `bun:",nullzero,notnull,default:false"` // whether emoji is cached in locally in gotosocial storage.
}
// IsLocal returns true if the emoji is
diff --git a/internal/gtsmodel/mediaattachment.go b/internal/gtsmodel/mediaattachment.go
index f18589f85..471a5abd1 100644
--- a/internal/gtsmodel/mediaattachment.go
+++ b/internal/gtsmodel/mediaattachment.go
@@ -30,8 +30,8 @@ type MediaAttachment struct {
StatusID string `bun:"type:CHAR(26),nullzero"` // ID of the status to which this is attached
URL string `bun:",nullzero"` // Where can the attachment be retrieved on *this* server
RemoteURL string `bun:",nullzero"` // Where can the attachment be retrieved on a remote server (empty for local media)
- Type FileType `bun:",nullzero,notnull"` // Type of file (image/gifv/audio/video/unknown)
- FileMeta FileMeta `bun:",embed:,nullzero,notnull"` // Metadata about the file
+ Type FileType `bun:",notnull"` // Type of file (image/gifv/audio/video/unknown)
+ FileMeta FileMeta `bun:",embed:,notnull"` // Metadata about the file
AccountID string `bun:"type:CHAR(26),nullzero,notnull"` // To which account does this attachment belong
Description string `bun:""` // Description of the attachment (for screenreaders)
ScheduledStatusID string `bun:"type:CHAR(26),nullzero"` // To which scheduled status does this attachment belong
@@ -44,22 +44,30 @@ type MediaAttachment struct {
Cached *bool `bun:",nullzero,notnull,default:false"` // Is this attachment currently cached by our instance?
}
+// IsLocal returns whether media attachment is local.
+func (m *MediaAttachment) IsLocal() bool {
+ return m.RemoteURL == ""
+}
+
+// IsRemote returns whether media attachment is remote.
+func (m *MediaAttachment) IsRemote() bool {
+ return m.RemoteURL != ""
+}
+
// File refers to the metadata for the whole file
type File struct {
- Path string `bun:",nullzero,notnull"` // Path of the file in storage.
- ContentType string `bun:",nullzero,notnull"` // MIME content type of the file.
- FileSize int `bun:",notnull"` // File size in bytes
- UpdatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // When was the file last updated.
+ Path string `bun:",notnull"` // Path of the file in storage.
+ ContentType string `bun:",notnull"` // MIME content type of the file.
+ FileSize int `bun:",notnull"` // File size in bytes
}
// Thumbnail refers to a small image thumbnail derived from a larger image, video, or audio file.
type Thumbnail struct {
- Path string `bun:",nullzero,notnull"` // Path of the file in storage.
- ContentType string `bun:",nullzero,notnull"` // MIME content type of the file.
- FileSize int `bun:",notnull"` // File size in bytes
- UpdatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // When was the file last updated.
- URL string `bun:",nullzero"` // What is the URL of the thumbnail on the local server
- RemoteURL string `bun:",nullzero"` // What is the remote URL of the thumbnail (empty for local media)
+ Path string `bun:",notnull"` // Path of the file in storage.
+ ContentType string `bun:",notnull"` // MIME content type of the file.
+ FileSize int `bun:",notnull"` // File size in bytes
+ URL string `bun:",nullzero"` // What is the URL of the thumbnail on the local server
+ RemoteURL string `bun:",nullzero"` // What is the remote URL of the thumbnail (empty for local media)
}
// ProcessingStatus refers to how far along in the processing stage the attachment is.
diff --git a/internal/media/image.go b/internal/media/image.go
index 29527c085..8a34e5062 100644
--- a/internal/media/image.go
+++ b/internal/media/image.go
@@ -43,12 +43,9 @@ var (
BufferPool: &pngEncoderBufferPool{},
}
- // jpegBufferPool is a memory pool of byte buffers for JPEG encoding.
- jpegBufferPool = sync.Pool{
- New: func() any {
- return bufio.NewWriter(nil)
- },
- }
+ // jpegBufferPool is a memory pool
+ // of byte buffers for JPEG encoding.
+ jpegBufferPool sync.Pool
)
// gtsImage is a thin wrapper around the standard library image
@@ -80,25 +77,29 @@ func decodeImage(r io.Reader, opts ...imaging.DecodeOption) (*gtsImage, error) {
}
// Width returns the image width in pixels.
-func (m *gtsImage) Width() uint32 {
- return uint32(m.image.Bounds().Size().X)
+func (m *gtsImage) Width() int {
+ return m.image.Bounds().Size().X
}
// Height returns the image height in pixels.
-func (m *gtsImage) Height() uint32 {
- return uint32(m.image.Bounds().Size().Y)
+func (m *gtsImage) Height() int {
+ return m.image.Bounds().Size().Y
}
// Size returns the total number of image pixels.
-func (m *gtsImage) Size() uint64 {
- return uint64(m.image.Bounds().Size().X) *
- uint64(m.image.Bounds().Size().Y)
+func (m *gtsImage) Size() int {
+ return m.image.Bounds().Size().X *
+ m.image.Bounds().Size().Y
}
// AspectRatio returns the image ratio of width:height.
func (m *gtsImage) AspectRatio() float32 {
- return float32(m.image.Bounds().Size().X) /
- float32(m.image.Bounds().Size().Y)
+
+ // note: we cast bounds to float64 to prevent truncation
+ // and only at the end aspect ratio do we cast to float32
+ // (as the sizes are likely to be much larger than ratio).
+ return float32(float64(m.image.Bounds().Size().X) /
+ float64(m.image.Bounds().Size().Y))
}
// Thumbnail returns a small sized copy of gtsImage{}, limited to 512x512 if not small enough.
@@ -160,7 +161,11 @@ func (m *gtsImage) ToPNG() io.Reader {
// getJPEGBuffer fetches a reset JPEG encoding buffer from global JPEG buffer pool.
func getJPEGBuffer(w io.Writer) *bufio.Writer {
- buf, _ := jpegBufferPool.Get().(*bufio.Writer)
+ v := jpegBufferPool.Get()
+ if v == nil {
+ v = bufio.NewWriter(nil)
+ }
+ buf := v.(*bufio.Writer)
buf.Reset(w)
return buf
}
diff --git a/internal/media/manager.go b/internal/media/manager.go
index be428aa3b..90a2923b5 100644
--- a/internal/media/manager.go
+++ b/internal/media/manager.go
@@ -56,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
}
diff --git a/internal/media/manager_test.go b/internal/media/manager_test.go
index d184e4605..53c08eed8 100644
--- a/internal/media/manager_test.go
+++ b/internal/media/manager_test.go
@@ -40,7 +40,7 @@ type ManagerTestSuite struct {
MediaStandardTestSuite
}
-func (suite *ManagerTestSuite) TestEmojiProcessBlocking() {
+func (suite *ManagerTestSuite) TestEmojiProcess() {
ctx := context.Background()
data := func(_ context.Context) (io.ReadCloser, int64, error) {
@@ -52,27 +52,26 @@ func (suite *ManagerTestSuite) TestEmojiProcessBlocking() {
return io.NopCloser(bytes.NewBuffer(b)), int64(len(b)), nil
}
- emojiID := "01GDQ9G782X42BAMFASKP64343"
- emojiURI := "http://localhost:8080/emoji/01GDQ9G782X42BAMFASKP64343"
-
- processingEmoji, err := suite.manager.ProcessEmoji(ctx, data, "rainbow_test", emojiID, emojiURI, nil, false)
+ processing, err := suite.manager.CreateEmoji(ctx,
+ "rainbow_test",
+ "",
+ data,
+ media.AdditionalEmojiInfo{},
+ )
suite.NoError(err)
// do a blocking call to fetch the emoji
- emoji, err := processingEmoji.LoadEmoji(ctx)
+ emoji, err := processing.Load(ctx)
suite.NoError(err)
suite.NotNil(emoji)
- // make sure it's got the stuff set on it that we expect
- suite.Equal(emojiID, emoji.ID)
-
// file meta should be correctly derived from the image
suite.Equal("image/png", emoji.ImageContentType)
suite.Equal("image/png", emoji.ImageStaticContentType)
suite.Equal(36702, emoji.ImageFileSize)
// now make sure the emoji is in the database
- dbEmoji, err := suite.db.GetEmojiByID(ctx, emojiID)
+ dbEmoji, err := suite.db.GetEmojiByID(ctx, emoji.ID)
suite.NoError(err)
suite.NotNil(dbEmoji)
@@ -101,14 +100,15 @@ func (suite *ManagerTestSuite) TestEmojiProcessBlocking() {
suite.Equal(processedStaticBytesExpected, processedStaticBytes)
}
-func (suite *ManagerTestSuite) TestEmojiProcessBlockingRefresh() {
+func (suite *ManagerTestSuite) TestEmojiProcessRefresh() {
ctx := context.Background()
// we're going to 'refresh' the remote 'yell' emoji by changing the image url to the pixellated gts logo
originalEmoji := suite.testEmojis["yell"]
- emojiToUpdate := >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)
}
diff --git a/internal/media/processingemoji.go b/internal/media/processingemoji.go
index b62c4f76e..d61043523 100644
--- a/internal/media/processingemoji.go
+++ b/internal/media/processingemoji.go
@@ -24,14 +24,16 @@ import (
"slices"
"codeberg.org/gruf/go-bytesize"
- "codeberg.org/gruf/go-errors/v2"
+ errorsv2 "codeberg.org/gruf/go-errors/v2"
"codeberg.org/gruf/go-runners"
"github.com/h2non/filetype"
"github.com/superseriousbusiness/gotosocial/internal/config"
+ "github.com/superseriousbusiness/gotosocial/internal/gtscontext"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/log"
"github.com/superseriousbusiness/gotosocial/internal/regexes"
+ "github.com/superseriousbusiness/gotosocial/internal/storage"
"github.com/superseriousbusiness/gotosocial/internal/uris"
"github.com/superseriousbusiness/gotosocial/internal/util"
)
@@ -40,7 +42,6 @@ import (
// various functions for retrieving data from the process.
type ProcessingEmoji struct {
emoji *gtsmodel.Emoji // processing emoji details
- existing bool // indicates whether this is an existing emoji ID being refreshed / recached
newPathID string // new emoji path ID to use when being refreshed
dataFn DataFunc // load-data function, returns media stream
done bool // done is set when process finishes with non ctx canceled type error
@@ -49,61 +50,72 @@ type ProcessingEmoji struct {
mgr *Manager // mgr instance (access to db / storage)
}
-// EmojiID returns the ID of the underlying emoji without blocking processing.
-func (p *ProcessingEmoji) EmojiID() string {
+// ID returns the ID of the underlying emoji.
+func (p *ProcessingEmoji) ID() string {
return p.emoji.ID // immutable, safe outside mutex.
}
// LoadEmoji blocks until the static and fullsize image has been processed, and then returns the completed emoji.
-func (p *ProcessingEmoji) LoadEmoji(ctx context.Context) (*gtsmodel.Emoji, error) {
- // Attempt to load synchronously.
+func (p *ProcessingEmoji) Load(ctx context.Context) (*gtsmodel.Emoji, error) {
emoji, done, err := p.load(ctx)
- if err == nil {
- // No issue, return media.
- return emoji, nil
- }
-
if !done {
- // Provided context was cancelled, e.g. request cancelled
- // early. Queue this item for asynchronous processing.
- log.Warnf(ctx, "reprocessing emoji %s after canceled ctx", p.emoji.ID)
- p.mgr.state.Workers.Media.Queue.Push(p.Process)
+ // 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
+}
diff --git a/internal/media/processingmedia.go b/internal/media/processingmedia.go
index b65e3cd48..466c3443f 100644
--- a/internal/media/processingmedia.go
+++ b/internal/media/processingmedia.go
@@ -19,6 +19,7 @@ package media
import (
"bytes"
+ "cmp"
"context"
"image/jpeg"
"io"
@@ -29,6 +30,7 @@ import (
terminator "codeberg.org/superseriousbusiness/exif-terminator"
"github.com/disintegration/imaging"
"github.com/h2non/filetype"
+ "github.com/superseriousbusiness/gotosocial/internal/gtscontext"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/log"
@@ -41,18 +43,16 @@ import (
// currently being processed. It exposes functions
// for retrieving data from the process.
type ProcessingMedia struct {
- media *gtsmodel.MediaAttachment // processing media attachment details
- dataFn DataFunc // load-data function, returns media stream
- recache bool // recaching existing (uncached) media
- done bool // done is set when process finishes with non ctx canceled type error
- proc runners.Processor // proc helps synchronize only a singular running processing instance
- err error // error stores permanent error value when done
- mgr *Manager // mgr instance (access to db / storage)
+ media *gtsmodel.MediaAttachment // processing media attachment details
+ dataFn DataFunc // load-data function, returns media stream
+ done bool // done is set when process finishes with non ctx canceled type error
+ proc runners.Processor // proc helps synchronize only a singular running processing instance
+ err error // error stores permanent error value when done
+ mgr *Manager // mgr instance (access to db / storage)
}
-// AttachmentID returns the ID of the underlying
-// media attachment without blocking processing.
-func (p *ProcessingMedia) AttachmentID() string {
+// ID returns the ID of the underlying media.
+func (p *ProcessingMedia) ID() string {
return p.media.ID // immutable, safe outside mutex.
}
@@ -65,124 +65,102 @@ func (p *ProcessingMedia) AttachmentID() string {
// will still be returned in that case, but it will
// only be partially complete and should be treated
// as a placeholder.
-func (p *ProcessingMedia) LoadAttachment(ctx context.Context) (*gtsmodel.MediaAttachment, error) {
- // Attempt to load synchronously.
+func (p *ProcessingMedia) Load(ctx context.Context) (*gtsmodel.MediaAttachment, error) {
media, done, err := p.load(ctx)
- if err == nil {
- // No issue, return media.
- return media, nil
- }
-
if !done {
- // Provided context was cancelled,
- // e.g. request aborted early before
- // its context could be used to finish
- // loading the attachment. Enqueue for
- // asynchronous processing, which will
- // use a background context.
- log.Warnf(ctx, "reprocessing media %s after canceled ctx", p.media.ID)
- p.mgr.state.Workers.Media.Queue.Push(p.Process)
+ // On a context-canceled error (marked as !done), requeue for loading.
+ p.mgr.state.Workers.Dereference.Queue.Push(func(ctx context.Context) {
+ if _, _, err := p.load(ctx); err != nil {
+ log.Errorf(ctx, "error loading media: %v", err)
+ }
+ })
}
-
- // Media could not be retrieved FULLY,
- // but partial attachment should be present.
return media, err
}
-// Process allows the receiving object to fit the
-// runners.WorkerFunc signature. It performs a
-// (blocking) load and logs on error.
-func (p *ProcessingMedia) Process(ctx context.Context) {
- if _, _, err := p.load(ctx); err != nil {
- log.Errorf(ctx, "error(s) processing media: %v", err)
- }
-}
-
-// load performs a concurrency-safe load of ProcessingMedia, only
-// marking itself as complete when returned error is NOT a context cancel.
-func (p *ProcessingMedia) load(ctx context.Context) (*gtsmodel.MediaAttachment, bool, error) {
- var (
- done bool
- err error
- )
-
+// load is the package private form of load() that is wrapped to catch context canceled.
+func (p *ProcessingMedia) load(ctx context.Context) (
+ media *gtsmodel.MediaAttachment,
+ done bool,
+ err error,
+) {
err = p.proc.Process(func() error {
- if p.done {
+ if done = p.done; done {
// Already proc'd.
return p.err
}
defer func() {
// This is only done when ctx NOT cancelled.
- done = err == nil || !errorsv2.IsV2(err,
+ done = (err == nil || !errorsv2.IsV2(err,
context.Canceled,
context.DeadlineExceeded,
- )
+ ))
if !done {
return
}
+ // Anything from here, we
+ // need to ensure happens
+ // (i.e. no ctx canceled).
+ ctx = gtscontext.WithValues(
+ context.Background(),
+ ctx, // values
+ )
+
+ // On error or unknown media types, perform error cleanup.
+ if err != nil || p.media.Type == gtsmodel.FileTypeUnknown {
+ p.cleanup(ctx)
+ }
+
+ // Update with latest details, whatever happened.
+ e := p.mgr.state.DB.UpdateAttachment(ctx, p.media)
+ if e != nil {
+ log.Errorf(ctx, "error updating media in db: %v", e)
+ }
+
// Store final values.
p.done = true
p.err = err
}()
- // Gather errors as we proceed.
- var errs = gtserror.NewMultiError(4)
+ // TODO: in time update this
+ // to perhaps follow a similar
+ // freshness window to statuses
+ // / accounts? But that's a big
+ // maybe, media don't change in
+ // the same way so this is largely
+ // just to slow down fail retries.
+ const maxfreq = 6 * time.Hour
+
+ // Check whether media is uncached but repeatedly failing,
+ // specifically limit the frequency at which we allow this.
+ if !p.media.UpdatedAt.Equal(p.media.CreatedAt) && // i.e. not new
+ p.media.UpdatedAt.Add(maxfreq).Before(time.Now()) {
+ return nil
+ }
// Attempt to store media and calculate
// full-size media attachment details.
//
// This will update p.media as it goes.
- storeErr := p.store(ctx)
- if storeErr != nil {
- errs.Append(storeErr)
+ if err = p.store(ctx); err != nil {
+ return err
}
// Finish processing by reloading media into
// memory to get dimension and generate a thumb.
//
// This will update p.media as it goes.
- if finishErr := p.finish(ctx); finishErr != nil {
- errs.Append(finishErr)
+ if 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)
+}
diff --git a/internal/media/refetch.go b/internal/media/refetch.go
index a1483ccd4..c239655d2 100644
--- a/internal/media/refetch.go
+++ b/internal/media/refetch.go
@@ -112,19 +112,19 @@ func (m *Manager) RefetchEmojis(ctx context.Context, domain string, dereferenceM
return dereferenceMedia(ctx, emojiImageIRI)
}
- processingEmoji, err := m.PreProcessEmoji(ctx, dataFunc, emoji.Shortcode, emoji.ID, emoji.URI, &AdditionalEmojiInfo{
+ processingEmoji, err := m.RefreshEmoji(ctx, emoji, dataFunc, AdditionalEmojiInfo{
Domain: &emoji.Domain,
ImageRemoteURL: &emoji.ImageRemoteURL,
ImageStaticRemoteURL: &emoji.ImageStaticRemoteURL,
Disabled: emoji.Disabled,
VisibleInPicker: emoji.VisibleInPicker,
- }, true)
+ })
if err != nil {
log.Errorf(ctx, "emoji %s could not be refreshed because of an error during processing: %s", shortcodeDomain, err)
continue
}
- if _, err := processingEmoji.LoadEmoji(ctx); err != nil {
+ if _, err := processingEmoji.Load(ctx); err != nil {
log.Errorf(ctx, "emoji %s could not be refreshed because of an error during loading: %s", shortcodeDomain, err)
continue
}
diff --git a/internal/media/types.go b/internal/media/types.go
index 6e7727cd5..cea026b98 100644
--- a/internal/media/types.go
+++ b/internal/media/types.go
@@ -61,47 +61,85 @@ const (
TypeEmoji Type = "emoji" // TypeEmoji is the key for emoji type requests
)
-// AdditionalMediaInfo represents additional information that should be added to an attachment
-// when processing a piece of media.
+// AdditionalMediaInfo represents additional information that
+// should be added to attachment when processing a piece of media.
type AdditionalMediaInfo struct {
- // Time that this media was created; defaults to time.Now().
+
+ // Time that this media was
+ // created; defaults to time.Now().
CreatedAt *time.Time
- // ID of the status to which this media is attached; defaults to "".
+
+ // ID of the status to which this
+ // media is attached; defaults to "".
StatusID *string
- // URL of the media on a remote instance; defaults to "".
+
+ // URL of the media on a
+ // remote instance; defaults to "".
RemoteURL *string
- // Image description of this media; defaults to "".
+
+ // Image description of
+ // this media; defaults to "".
Description *string
- // Blurhash of this media; defaults to "".
+
+ // Blurhash of this
+ // media; defaults to "".
Blurhash *string
- // ID of the scheduled status to which this media is attached; defaults to "".
+
+ // ID of the scheduled status to which
+ // this media is attached; defaults to "".
ScheduledStatusID *string
- // Mark this media as in-use as an avatar; defaults to false.
+
+ // Mark this media as in-use
+ // as an avatar; defaults to false.
Avatar *bool
- // Mark this media as in-use as a header; defaults to false.
+
+ // Mark this media as in-use
+ // as a header; defaults to false.
Header *bool
- // X focus coordinate for this media; defaults to 0.
+
+ // X focus coordinate for
+ // this media; defaults to 0.
FocusX *float32
- // Y focus coordinate for this media; defaults to 0.
+
+ // Y focus coordinate for
+ // this media; defaults to 0.
FocusY *float32
}
// AdditionalEmojiInfo represents additional information
// that should be taken into account when processing an emoji.
type AdditionalEmojiInfo struct {
- // Time that this emoji was created; defaults to time.Now().
+
+ // ActivityPub URI of
+ // this remote emoji.
+ URI *string
+
+ // Time that this emoji was
+ // created; defaults to time.Now().
CreatedAt *time.Time
- // Domain the emoji originated from. Blank for this instance's domain. Defaults to "".
+
+ // Domain the emoji originated from. Blank
+ // for this instance's domain. Defaults to "".
Domain *string
- // URL of this emoji on a remote instance; defaults to "".
+
+ // URL of this emoji on a
+ // remote instance; defaults to "".
ImageRemoteURL *string
- // URL of the static version of this emoji on a remote instance; defaults to "".
+
+ // URL of the static version of this emoji
+ // on a remote instance; defaults to "".
ImageStaticRemoteURL *string
- // Whether this emoji should be disabled (not shown) on this instance; defaults to false.
+
+ // Whether this emoji should be disabled (not
+ // shown) on this instance; defaults to false.
Disabled *bool
- // Whether this emoji should be visible in the instance's emoji picker; defaults to true.
+
+ // Whether this emoji should be visible in
+ // the instance's emoji picker; defaults to true.
VisibleInPicker *bool
- // ID of the category this emoji should be placed in; defaults to "".
+
+ // ID of the category this emoji
+ // should be placed in; defaults to "".
CategoryID *string
}
diff --git a/internal/media/util.go b/internal/media/util.go
index 1595da6d7..296bdb883 100644
--- a/internal/media/util.go
+++ b/internal/media/util.go
@@ -37,6 +37,5 @@ func newHdrBuf(fileSize int) []byte {
if fileSize > 0 && fileSize < bufSize {
bufSize = fileSize
}
-
return make([]byte, bufSize)
}
diff --git a/internal/processing/account/account_test.go b/internal/processing/account/account_test.go
index 556f4d91f..8eec1f9dd 100644
--- a/internal/processing/account/account_test.go
+++ b/internal/processing/account/account_test.go
@@ -111,7 +111,7 @@ func (suite *AccountStandardTestSuite) SetupTest() {
suite.emailSender = testrig.NewEmailSender("../../../web/template/", suite.sentEmails)
filter := visibility.NewFilter(&suite.state)
- common := common.New(&suite.state, suite.tc, suite.federator, filter)
+ common := common.New(&suite.state, suite.mediaManager, suite.tc, suite.federator, filter)
suite.accountProcessor = account.New(&common, &suite.state, suite.tc, suite.mediaManager, suite.federator, filter, processing.GetParseMentionFunc(&suite.state, suite.federator))
testrig.StandardDBSetup(suite.db, nil)
testrig.StandardStorageSetup(suite.storage, "../../../testrig/media")
diff --git a/internal/processing/account/update.go b/internal/processing/account/update.go
index ea6abed6e..61e88501f 100644
--- a/internal/processing/account/update.go
+++ b/internal/processing/account/update.go
@@ -19,10 +19,12 @@ package account
import (
"context"
+ "errors"
"fmt"
"io"
"mime/multipart"
+ "codeberg.org/gruf/go-bytesize"
"github.com/superseriousbusiness/gotosocial/internal/ap"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/config"
@@ -203,9 +205,13 @@ func (p *Processor) Update(ctx context.Context, account *gtsmodel.Account, form
}
if form.Avatar != nil && form.Avatar.Size != 0 {
- avatarInfo, err := p.UpdateAvatar(ctx, form.Avatar, nil, account.ID)
- if err != nil {
- return nil, gtserror.NewErrorBadRequest(err)
+ avatarInfo, errWithCode := p.UpdateAvatar(ctx,
+ account,
+ form.Avatar,
+ nil,
+ )
+ if errWithCode != nil {
+ return nil, errWithCode
}
account.AvatarMediaAttachmentID = avatarInfo.ID
account.AvatarMediaAttachment = avatarInfo
@@ -213,9 +219,13 @@ func (p *Processor) Update(ctx context.Context, account *gtsmodel.Account, form
}
if form.Header != nil && form.Header.Size != 0 {
- headerInfo, err := p.UpdateHeader(ctx, form.Header, nil, account.ID)
- if err != nil {
- return nil, gtserror.NewErrorBadRequest(err)
+ headerInfo, errWithCode := p.UpdateHeader(ctx,
+ account,
+ form.Header,
+ nil,
+ )
+ if errWithCode != nil {
+ return nil, errWithCode
}
account.HeaderMediaAttachmentID = headerInfo.ID
account.HeaderMediaAttachment = headerInfo
@@ -316,35 +326,33 @@ func (p *Processor) Update(ctx context.Context, account *gtsmodel.Account, form
// for this to become the account's new avatar.
func (p *Processor) UpdateAvatar(
ctx context.Context,
+ account *gtsmodel.Account,
avatar *multipart.FileHeader,
description *string,
- accountID string,
-) (*gtsmodel.MediaAttachment, error) {
- maxImageSize := config.GetMediaImageMaxSize()
- if avatar.Size > int64(maxImageSize) {
- return nil, gtserror.Newf("size %d exceeded max media size of %d bytes", avatar.Size, maxImageSize)
+) (
+ *gtsmodel.MediaAttachment,
+ gtserror.WithCode,
+) {
+ max := config.GetMediaImageMaxSize()
+ if sz := bytesize.Size(avatar.Size); sz > max {
+ text := fmt.Sprintf("size %s exceeds max media size %s", sz, max)
+ return nil, gtserror.NewErrorBadRequest(errors.New(text), text)
}
- data := func(innerCtx context.Context) (io.ReadCloser, int64, error) {
+ data := func(_ context.Context) (io.ReadCloser, int64, error) {
f, err := avatar.Open()
return f, avatar.Size, err
}
- // Process the media attachment and load it immediately.
- media := p.mediaManager.PreProcessMedia(data, accountID, &media.AdditionalMediaInfo{
- Avatar: util.Ptr(true),
- Description: description,
- })
-
- attachment, err := media.LoadAttachment(ctx)
- if err != nil {
- return nil, gtserror.NewErrorUnprocessableEntity(err, err.Error())
- } else if attachment.Type == gtsmodel.FileTypeUnknown {
- err = gtserror.Newf("could not process uploaded file with extension %s", attachment.File.ContentType)
- return nil, gtserror.NewErrorUnprocessableEntity(err, err.Error())
- }
-
- return attachment, nil
+ // Write to instance storage.
+ return p.c.StoreLocalMedia(ctx,
+ account.ID,
+ data,
+ media.AdditionalMediaInfo{
+ Avatar: util.Ptr(true),
+ Description: description,
+ },
+ )
}
// UpdateHeader does the dirty work of checking the header
@@ -353,33 +361,31 @@ func (p *Processor) UpdateAvatar(
// for this to become the account's new header.
func (p *Processor) UpdateHeader(
ctx context.Context,
+ account *gtsmodel.Account,
header *multipart.FileHeader,
description *string,
- accountID string,
-) (*gtsmodel.MediaAttachment, error) {
- maxImageSize := config.GetMediaImageMaxSize()
- if header.Size > int64(maxImageSize) {
- return nil, gtserror.Newf("size %d exceeded max media size of %d bytes", header.Size, maxImageSize)
+) (
+ *gtsmodel.MediaAttachment,
+ gtserror.WithCode,
+) {
+ max := config.GetMediaImageMaxSize()
+ if sz := bytesize.Size(header.Size); sz > max {
+ text := fmt.Sprintf("size %s exceeds max media size %s", sz, max)
+ return nil, gtserror.NewErrorBadRequest(errors.New(text), text)
}
- data := func(innerCtx context.Context) (io.ReadCloser, int64, error) {
+ data := func(_ context.Context) (io.ReadCloser, int64, error) {
f, err := header.Open()
return f, header.Size, err
}
- // Process the media attachment and load it immediately.
- media := p.mediaManager.PreProcessMedia(data, accountID, &media.AdditionalMediaInfo{
- Header: util.Ptr(true),
- Description: description,
- })
-
- attachment, err := media.LoadAttachment(ctx)
- if err != nil {
- return nil, gtserror.NewErrorUnprocessableEntity(err, err.Error())
- } else if attachment.Type == gtsmodel.FileTypeUnknown {
- err = gtserror.Newf("could not process uploaded file with extension %s", attachment.File.ContentType)
- return nil, gtserror.NewErrorUnprocessableEntity(err, err.Error())
- }
-
- return attachment, nil
+ // Write to instance storage.
+ return p.c.StoreLocalMedia(ctx,
+ account.ID,
+ data,
+ media.AdditionalMediaInfo{
+ Header: util.Ptr(true),
+ Description: description,
+ },
+ )
}
diff --git a/internal/processing/admin/admin.go b/internal/processing/admin/admin.go
index 3093b3e36..170298ca5 100644
--- a/internal/processing/admin/admin.go
+++ b/internal/processing/admin/admin.go
@@ -20,20 +20,26 @@ package admin
import (
"github.com/superseriousbusiness/gotosocial/internal/cleaner"
"github.com/superseriousbusiness/gotosocial/internal/email"
+ "github.com/superseriousbusiness/gotosocial/internal/federation"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/media"
+ "github.com/superseriousbusiness/gotosocial/internal/processing/common"
"github.com/superseriousbusiness/gotosocial/internal/state"
"github.com/superseriousbusiness/gotosocial/internal/transport"
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
)
type Processor struct {
- state *state.State
- cleaner *cleaner.Cleaner
- converter *typeutils.Converter
- mediaManager *media.Manager
- transportController transport.Controller
- emailSender email.Sender
+ // common processor logic
+ c *common.Processor
+
+ state *state.State
+ cleaner *cleaner.Cleaner
+ converter *typeutils.Converter
+ federator *federation.Federator
+ media *media.Manager
+ transport transport.Controller
+ email email.Sender
// admin Actions currently
// undergoing processing
@@ -46,21 +52,24 @@ func (p *Processor) Actions() *Actions {
// New returns a new admin processor.
func New(
+ common *common.Processor,
state *state.State,
cleaner *cleaner.Cleaner,
+ federator *federation.Federator,
converter *typeutils.Converter,
mediaManager *media.Manager,
transportController transport.Controller,
emailSender email.Sender,
) Processor {
return Processor{
- state: state,
- cleaner: cleaner,
- converter: converter,
- mediaManager: mediaManager,
- transportController: transportController,
- emailSender: emailSender,
-
+ c: common,
+ state: state,
+ cleaner: cleaner,
+ converter: converter,
+ federator: federator,
+ media: mediaManager,
+ transport: transportController,
+ email: emailSender,
actions: &Actions{
r: make(map[string]*gtsmodel.AdminAction),
state: state,
diff --git a/internal/processing/admin/debug_apurl.go b/internal/processing/admin/debug_apurl.go
index db3c60d0c..dbf337dc3 100644
--- a/internal/processing/admin/debug_apurl.go
+++ b/internal/processing/admin/debug_apurl.go
@@ -78,7 +78,7 @@ func (p *Processor) DebugAPUrl(
}
// All looks fine. Prepare the transport and (signed) GET request.
- tsport, err := p.transportController.NewTransportForUsername(ctx, adminAcct.Username)
+ tsport, err := p.transport.NewTransportForUsername(ctx, adminAcct.Username)
if err != nil {
err = gtserror.Newf("error creating transport: %w", err)
return nil, gtserror.NewErrorInternalError(err, err.Error())
diff --git a/internal/processing/admin/email.go b/internal/processing/admin/email.go
index fda60754c..949be6e4b 100644
--- a/internal/processing/admin/email.go
+++ b/internal/processing/admin/email.go
@@ -55,7 +55,7 @@ func (p *Processor) EmailTest(
InstanceName: instance.Title,
}
- if err := p.emailSender.SendTestEmail(toAddress, testData); err != nil {
+ if err := p.email.SendTestEmail(toAddress, testData); err != nil {
if gtserror.IsSMTP(err) {
// An error occurred during the SMTP part.
// We should indicate this to the caller, as
diff --git a/internal/processing/admin/emoji.go b/internal/processing/admin/emoji.go
index dcdf77642..4d1b464d3 100644
--- a/internal/processing/admin/emoji.go
+++ b/internal/processing/admin/emoji.go
@@ -31,7 +31,6 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/id"
"github.com/superseriousbusiness/gotosocial/internal/media"
- "github.com/superseriousbusiness/gotosocial/internal/uris"
"github.com/superseriousbusiness/gotosocial/internal/util"
)
@@ -41,64 +40,21 @@ func (p *Processor) EmojiCreate(
account *gtsmodel.Account,
form *apimodel.EmojiCreateRequest,
) (*apimodel.Emoji, gtserror.WithCode) {
- // Ensure emoji with this shortcode
- // doesn't already exist on the instance.
- maybeExisting, err := p.state.DB.GetEmojiByShortcodeDomain(ctx, form.Shortcode, "")
- if err != nil && !errors.Is(err, db.ErrNoEntries) {
- err := gtserror.Newf("error checking existence of emoji with shortcode %s: %w", form.Shortcode, err)
- return nil, gtserror.NewErrorInternalError(err)
- }
- if maybeExisting != nil {
- err := fmt.Errorf("emoji with shortcode %s already exists", form.Shortcode)
- return nil, gtserror.NewErrorConflict(err, err.Error())
- }
-
- // Prepare data function for emoji processing
- // (just read data from the submitted form).
- data := func(innerCtx context.Context) (io.ReadCloser, int64, error) {
+ // Simply read provided form data for emoji data source.
+ data := func(_ context.Context) (io.ReadCloser, int64, error) {
f, err := form.Image.Open()
return f, form.Image.Size, err
}
- // If category was supplied on the form,
- // ensure the category exists and provide
- // it as additional info to emoji processing.
- var ai *media.AdditionalEmojiInfo
- if form.CategoryName != "" {
- category, err := p.getOrCreateEmojiCategory(ctx, form.CategoryName)
- if err != nil {
- return nil, gtserror.NewErrorInternalError(err)
- }
-
- ai = &media.AdditionalEmojiInfo{
- CategoryID: &category.ID,
- }
- }
-
- // Generate new emoji ID and URI.
- emojiID, err := id.NewRandomULID()
- if err != nil {
- err := gtserror.Newf("error creating id for new emoji: %w", err)
- return nil, gtserror.NewErrorInternalError(err)
- }
-
- emojiURI := uris.URIForEmoji(emojiID)
-
- // Begin media processing.
- processingEmoji, err := p.mediaManager.PreProcessEmoji(ctx,
- data, form.Shortcode, emojiID, emojiURI, ai, false,
+ // Attempt to create the new local emoji.
+ emoji, errWithCode := p.createEmoji(ctx,
+ form.Shortcode,
+ form.CategoryName,
+ data,
)
- if err != nil {
- err := gtserror.Newf("error processing emoji: %w", err)
- return nil, gtserror.NewErrorInternalError(err)
- }
-
- // Complete processing immediately.
- emoji, err := processingEmoji.LoadEmoji(ctx)
- if err != nil {
- err := gtserror.Newf("error loading emoji: %w", err)
- return nil, gtserror.NewErrorInternalError(err)
+ if errWithCode != nil {
+ return nil, errWithCode
}
apiEmoji, err := p.converter.EmojiToAPIEmoji(ctx, emoji)
@@ -110,53 +66,6 @@ func (p *Processor) EmojiCreate(
return &apiEmoji, nil
}
-// emojisGetFilterParams builds extra
-// query parameters to return as part
-// of an Emojis pageable response.
-//
-// The returned string will look like:
-//
-// "filter=domain:all,enabled,shortcode:example"
-func emojisGetFilterParams(
- shortcode string,
- domain string,
- includeDisabled bool,
- includeEnabled bool,
-) string {
- var filterBuilder strings.Builder
- filterBuilder.WriteString("filter=")
-
- switch domain {
- case "", "local":
- // Local emojis only.
- filterBuilder.WriteString("domain:local")
-
- case db.EmojiAllDomains:
- // Local or remote.
- filterBuilder.WriteString("domain:all")
-
- default:
- // Specific domain only.
- filterBuilder.WriteString("domain:" + domain)
- }
-
- if includeDisabled != includeEnabled {
- if includeDisabled {
- filterBuilder.WriteString(",disabled")
- }
- if includeEnabled {
- filterBuilder.WriteString(",enabled")
- }
- }
-
- if shortcode != "" {
- // Specific shortcode only.
- filterBuilder.WriteString(",shortcode:" + shortcode)
- }
-
- return filterBuilder.String()
-}
-
// EmojisGet returns an admin view of custom
// emojis, filtered with the given parameters.
func (p *Processor) EmojisGet(
@@ -287,21 +196,24 @@ func (p *Processor) EmojiDelete(
// given id, using the provided form parameters.
func (p *Processor) EmojiUpdate(
ctx context.Context,
- id string,
+ emojiID string,
form *apimodel.EmojiUpdateRequest,
) (*apimodel.AdminEmoji, gtserror.WithCode) {
- emoji, err := p.state.DB.GetEmojiByID(ctx, id)
+
+ // Get the emoji with given ID from the database.
+ emoji, err := p.state.DB.GetEmojiByID(ctx, emojiID)
if err != nil && !errors.Is(err, db.ErrNoEntries) {
- err := gtserror.Newf("db error: %w", err)
+ err := gtserror.Newf("error fetching emoji from db: %w", err)
return nil, gtserror.NewErrorInternalError(err)
}
+ // Check found.
if emoji == nil {
- err := gtserror.Newf("no emoji with id %s found in the db", id)
- return nil, gtserror.NewErrorNotFound(err)
+ const text = "emoji not found"
+ return nil, gtserror.NewErrorNotFound(errors.New(text), text)
}
- switch t := form.Type; t {
+ switch form.Type {
case apimodel.EmojiUpdateCopy:
return p.emojiUpdateCopy(ctx, emoji, form.Shortcode, form.CategoryName)
@@ -313,8 +225,8 @@ func (p *Processor) EmojiUpdate(
return p.emojiUpdateModify(ctx, emoji, form.Image, form.CategoryName)
default:
- err := fmt.Errorf("unrecognized emoji action type %s", t)
- return nil, gtserror.NewErrorBadRequest(err, err.Error())
+ const text = "unrecognized emoji update action type"
+ return nil, gtserror.NewErrorBadRequest(errors.New(text), text)
}
}
@@ -342,56 +254,6 @@ func (p *Processor) EmojiCategoriesGet(
return apiCategories, nil
}
-/*
- UTIL FUNCTIONS
-*/
-
-// getOrCreateEmojiCategory either gets an existing
-// category with the given name from the database,
-// or, if the category doesn't yet exist, it creates
-// the category and then returns it.
-func (p *Processor) getOrCreateEmojiCategory(
- ctx context.Context,
- name string,
-) (*gtsmodel.EmojiCategory, error) {
- category, err := p.state.DB.GetEmojiCategoryByName(ctx, name)
- if err != nil && !errors.Is(err, db.ErrNoEntries) {
- return nil, gtserror.Newf(
- "database error trying get emoji category %s: %w",
- name, err,
- )
- }
-
- if category != nil {
- // We had it already.
- return category, nil
- }
-
- // We don't have the category yet,
- // create it with the given name.
- categoryID, err := id.NewRandomULID()
- if err != nil {
- return nil, gtserror.Newf(
- "error generating id for new emoji category %s: %w",
- name, err,
- )
- }
-
- category = >smodel.EmojiCategory{
- ID: categoryID,
- Name: name,
- }
-
- if err := p.state.DB.PutEmojiCategory(ctx, category); err != nil {
- return nil, gtserror.Newf(
- "db error putting new emoji category %s: %w",
- name, err,
- )
- }
-
- return category, nil
-}
-
// emojiUpdateCopy copies and stores the given
// *remote* emoji as a *local* emoji, preserving
// the same image, and using the provided shortcode.
@@ -400,99 +262,56 @@ func (p *Processor) getOrCreateEmojiCategory(
// emoji already stored in the database + storage.
func (p *Processor) emojiUpdateCopy(
ctx context.Context,
- targetEmoji *gtsmodel.Emoji,
+ target *gtsmodel.Emoji,
shortcode *string,
- category *string,
+ categoryName *string,
) (*apimodel.AdminEmoji, gtserror.WithCode) {
- if targetEmoji.IsLocal() {
- err := fmt.Errorf("emoji %s is not a remote emoji, cannot copy it to local", targetEmoji.ID)
- return nil, gtserror.NewErrorBadRequest(err, err.Error())
+ if target.IsLocal() {
+ const text = "target emoji is not remote; cannot copy to local"
+ return nil, gtserror.NewErrorBadRequest(errors.New(text), text)
}
- if shortcode == nil {
- err := errors.New("no shortcode provided")
- return nil, gtserror.NewErrorBadRequest(err, err.Error())
- }
+ // Ensure target emoji is locally cached.
+ target, err := p.federator.RefreshEmoji(
+ ctx,
+ target,
- sc := *shortcode
- if sc == "" {
- err := errors.New("empty shortcode provided")
- return nil, gtserror.NewErrorBadRequest(err, err.Error())
+ // no changes we want to make.
+ media.AdditionalEmojiInfo{},
+ false,
+ )
+ if err != nil {
+ err := gtserror.Newf("error recaching emoji %s: %w", target.ImageRemoteURL, err)
+ return nil, gtserror.NewErrorNotFound(err)
}
- // Ensure we don't already have an emoji
- // stored locally with this shortcode.
- maybeExisting, err := p.state.DB.GetEmojiByShortcodeDomain(ctx, sc, "")
- if err != nil && !errors.Is(err, db.ErrNoEntries) {
- err := gtserror.Newf("db error checking for emoji with shortcode %s: %w", sc, err)
- return nil, gtserror.NewErrorInternalError(err)
- }
-
- if maybeExisting != nil {
- err := fmt.Errorf("emoji with shortcode %s already exists on this instance", sc)
- return nil, gtserror.NewErrorConflict(err, err.Error())
- }
-
- // We don't have an emoji with this
- // shortcode yet! Prepare to create it.
-
// Data function for copying just streams media
// out of storage into an additional location.
//
// This means that data for the copy persists even
// if the remote copied emoji gets deleted at some point.
data := func(ctx context.Context) (io.ReadCloser, int64, error) {
- rc, err := p.state.Storage.GetStream(ctx, targetEmoji.ImagePath)
- return rc, int64(targetEmoji.ImageFileSize), err
+ rc, err := p.state.Storage.GetStream(ctx, target.ImagePath)
+ return rc, int64(target.ImageFileSize), err
}
- // Generate new emoji ID and URI.
- emojiID, err := id.NewRandomULID()
- if err != nil {
- err := gtserror.Newf("error creating id for new emoji: %w", err)
- return nil, gtserror.NewErrorInternalError(err)
- }
-
- emojiURI := uris.URIForEmoji(emojiID)
-
- // If category was supplied, ensure the
- // category exists and provide it as
- // additional info to emoji processing.
- var ai *media.AdditionalEmojiInfo
- if category != nil && *category != "" {
- category, err := p.getOrCreateEmojiCategory(ctx, *category)
- if err != nil {
- return nil, gtserror.NewErrorInternalError(err)
- }
-
- ai = &media.AdditionalEmojiInfo{
- CategoryID: &category.ID,
- }
- }
-
- // Begin media processing.
- processingEmoji, err := p.mediaManager.PreProcessEmoji(ctx,
- data, sc, emojiID, emojiURI, ai, false,
+ // Attempt to create the new local emoji.
+ emoji, errWithCode := p.createEmoji(ctx,
+ util.PtrValueOr(shortcode, ""),
+ util.PtrValueOr(categoryName, ""),
+ data,
)
+ if errWithCode != nil {
+ return nil, errWithCode
+ }
+
+ apiEmoji, err := p.converter.EmojiToAdminAPIEmoji(ctx, emoji)
if err != nil {
- err := gtserror.Newf("error processing emoji: %w", err)
+ err := gtserror.Newf("error converting emoji: %w", err)
return nil, gtserror.NewErrorInternalError(err)
}
- // Complete processing immediately.
- newEmoji, err := processingEmoji.LoadEmoji(ctx)
- if err != nil {
- err := gtserror.Newf("error loading emoji: %w", err)
- return nil, gtserror.NewErrorInternalError(err)
- }
-
- adminEmoji, err := p.converter.EmojiToAdminAPIEmoji(ctx, newEmoji)
- if err != nil {
- err := gtserror.Newf("error converting emoji %s to admin emoji: %w", newEmoji.ID, err)
- return nil, gtserror.NewErrorInternalError(err)
- }
-
- return adminEmoji, nil
+ return apiEmoji, nil
}
// emojiUpdateDisable marks the given *remote*
@@ -521,7 +340,7 @@ func (p *Processor) emojiUpdateDisable(
adminEmoji, err := p.converter.EmojiToAdminAPIEmoji(ctx, emoji)
if err != nil {
- err := gtserror.Newf("error converting emoji %s to admin emoji: %w", emoji.ID, err)
+ err := gtserror.Newf("error converting emoji: %w", err)
return nil, gtserror.NewErrorInternalError(err)
}
@@ -541,104 +360,222 @@ func (p *Processor) emojiUpdateModify(
ctx context.Context,
emoji *gtsmodel.Emoji,
image *multipart.FileHeader,
- category *string,
+ categoryName *string,
) (*apimodel.AdminEmoji, gtserror.WithCode) {
if !emoji.IsLocal() {
- err := fmt.Errorf("emoji %s is not a local emoji, cannot update it via this endpoint", emoji.ID)
- return nil, gtserror.NewErrorBadRequest(err, err.Error())
+ const text = "cannot modify remote emoji"
+ return nil, gtserror.NewErrorBadRequest(errors.New(text), text)
}
// Ensure there's actually something to update.
- if image == nil && category == nil {
- err := errors.New("neither new category nor new image set, cannot update")
- return nil, gtserror.NewErrorBadRequest(err, err.Error())
+ if image == nil && categoryName == nil {
+ const text = "no changes were provided"
+ return nil, gtserror.NewErrorBadRequest(errors.New(text), text)
}
- // Only update category
- // if it's changed.
- var (
- newCategory *gtsmodel.EmojiCategory
- newCategoryID string
- updateCategoryID bool
- )
-
- if category != nil {
- catName := *category
- if catName != "" {
- // Set new category.
- var err error
- newCategory, err = p.getOrCreateEmojiCategory(ctx, catName)
- if err != nil {
- err := gtserror.Newf("error getting or creating category: %w", err)
- return nil, gtserror.NewErrorInternalError(err)
+ if categoryName != nil {
+ if *categoryName != "" {
+ // A category was provided, get / create relevant emoji category.
+ category, errWithCode := p.mustGetEmojiCategory(ctx, *categoryName)
+ if errWithCode != nil {
+ return nil, errWithCode
}
- newCategoryID = newCategory.ID
+ if category.ID == emoji.CategoryID {
+ // There was no change,
+ // indicate this by unsetting
+ // the category name pointer.
+ categoryName = nil
+ } else {
+ // Update emoji category.
+ emoji.CategoryID = category.ID
+ emoji.Category = category
+ }
} else {
- // Clear existing category.
- newCategoryID = ""
+ // Emoji category was unset.
+ emoji.CategoryID = ""
+ emoji.Category = nil
}
-
- updateCategoryID = emoji.CategoryID != newCategoryID
}
- // Only update image
- // if one is provided.
- var updateImage bool
- if image != nil && image.Size != 0 {
- updateImage = true
- }
+ // Check whether any image changes were requested.
+ imageUpdated := (image != nil && image.Size > 0)
- if updateCategoryID && !updateImage {
- // Only updating category; we only
- // need to do a db update for this.
- emoji.CategoryID = newCategoryID
- emoji.Category = newCategory
+ if !imageUpdated && categoryName != nil {
+ // Only updating category; only a single database update required.
if err := p.state.DB.UpdateEmoji(ctx, emoji, "category_id"); err != nil {
- err := gtserror.Newf("db error updating emoji %s: %w", emoji.ID, err)
+ err := gtserror.Newf("error updating emoji in db: %w", err)
return nil, gtserror.NewErrorInternalError(err)
}
- } else if updateImage {
+ } else if imageUpdated {
+ var err error
+
// Updating image and maybe categoryID.
// We can do both at the same time :)
- // Set data function to provided image.
- data := func(ctx context.Context) (io.ReadCloser, int64, error) {
- i, err := image.Open()
- return i, image.Size, err
+ // Simply read provided form data for emoji data source.
+ data := func(_ context.Context) (io.ReadCloser, int64, error) {
+ f, err := image.Open()
+ return f, image.Size, err
}
- // If necessary, include
- // update to categoryID too.
- var ai *media.AdditionalEmojiInfo
- if updateCategoryID {
- ai = &media.AdditionalEmojiInfo{
- CategoryID: &newCategoryID,
- }
- }
+ // Prepare emoji model for recache from new data.
+ processing := p.media.RecacheEmoji(emoji, data)
- // Begin media processing.
- processingEmoji, err := p.mediaManager.PreProcessEmoji(ctx,
- data, emoji.Shortcode, emoji.ID, emoji.URI, ai, false,
- )
+ // Load to trigger update + write.
+ emoji, err = processing.Load(ctx)
if err != nil {
- err := gtserror.Newf("error processing emoji: %w", err)
- return nil, gtserror.NewErrorInternalError(err)
- }
-
- // Replace emoji ptr with newly-processed version.
- emoji, err = processingEmoji.LoadEmoji(ctx)
- if err != nil {
- err := gtserror.Newf("error loading emoji: %w", err)
+ err := gtserror.Newf("error processing emoji %s: %w", emoji.Shortcode, err)
return nil, gtserror.NewErrorInternalError(err)
}
}
adminEmoji, err := p.converter.EmojiToAdminAPIEmoji(ctx, emoji)
if err != nil {
- err := gtserror.Newf("error converting emoji %s to admin emoji: %w", emoji.ID, err)
+ err := gtserror.Newf("error converting emoji: %w", err)
return nil, gtserror.NewErrorInternalError(err)
}
return adminEmoji, nil
}
+
+// createEmoji will create a new local emoji
+// with the given shortcode, attached category
+// name (if any) and data source function.
+func (p *Processor) createEmoji(
+ ctx context.Context,
+ shortcode string,
+ categoryName string,
+ data media.DataFunc,
+) (
+ *gtsmodel.Emoji,
+ gtserror.WithCode,
+) {
+ // Validate shortcode.
+ if shortcode == "" {
+ const text = "empty shortcode name"
+ return nil, gtserror.NewErrorBadRequest(errors.New(text), text)
+ }
+
+ // Look for an existing local emoji with shortcode to ensure this is new.
+ existing, err := p.state.DB.GetEmojiByShortcodeDomain(ctx, shortcode, "")
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ err := gtserror.Newf("error fetching emoji from db: %w", err)
+ return nil, gtserror.NewErrorInternalError(err)
+ } else if existing != nil {
+ const text = "emoji with shortcode already exists"
+ return nil, gtserror.NewErrorConflict(errors.New(text), text)
+ }
+
+ var categoryID *string
+
+ if categoryName != "" {
+ // A category was provided, get / create relevant emoji category.
+ category, errWithCode := p.mustGetEmojiCategory(ctx, categoryName)
+ if errWithCode != nil {
+ return nil, errWithCode
+ }
+
+ // Set category ID for emoji.
+ categoryID = &category.ID
+ }
+
+ // Store to instance storage.
+ return p.c.StoreLocalEmoji(
+ ctx,
+ shortcode,
+ data,
+ media.AdditionalEmojiInfo{
+ CategoryID: categoryID,
+ },
+ )
+}
+
+// mustGetEmojiCategory either gets an existing
+// category with the given name from the database,
+// or, if the category doesn't yet exist, it creates
+// the category and then returns it.
+func (p *Processor) mustGetEmojiCategory(
+ ctx context.Context,
+ name string,
+) (
+ *gtsmodel.EmojiCategory,
+ gtserror.WithCode,
+) {
+ // Look for an existing emoji category with name.
+ category, err := p.state.DB.GetEmojiCategoryByName(ctx, name)
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ err := gtserror.Newf("error fetching emoji category from db: %w", err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ if category != nil {
+ // We had it already.
+ return category, nil
+ }
+
+ // Create new ID.
+ id := id.NewULID()
+
+ // Prepare new category for insertion.
+ category = >smodel.EmojiCategory{
+ ID: id,
+ Name: name,
+ }
+
+ // Insert new category into the database.
+ err = p.state.DB.PutEmojiCategory(ctx, category)
+ if err != nil {
+ err := gtserror.Newf("error inserting emoji category into db: %w", err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ return category, nil
+}
+
+// emojisGetFilterParams builds extra
+// query parameters to return as part
+// of an Emojis pageable response.
+//
+// The returned string will look like:
+//
+// "filter=domain:all,enabled,shortcode:example"
+func emojisGetFilterParams(
+ shortcode string,
+ domain string,
+ includeDisabled bool,
+ includeEnabled bool,
+) string {
+ var filterBuilder strings.Builder
+ filterBuilder.WriteString("filter=")
+
+ switch domain {
+ case "", "local":
+ // Local emojis only.
+ filterBuilder.WriteString("domain:local")
+
+ case db.EmojiAllDomains:
+ // Local or remote.
+ filterBuilder.WriteString("domain:all")
+
+ default:
+ // Specific domain only.
+ filterBuilder.WriteString("domain:" + domain)
+ }
+
+ if includeDisabled != includeEnabled {
+ if includeDisabled {
+ filterBuilder.WriteString(",disabled")
+ }
+ if includeEnabled {
+ filterBuilder.WriteString(",enabled")
+ }
+ }
+
+ if shortcode != "" {
+ // Specific shortcode only.
+ filterBuilder.WriteString(",shortcode:" + shortcode)
+ }
+
+ return filterBuilder.String()
+}
diff --git a/internal/processing/admin/media.go b/internal/processing/admin/media.go
index 13dcb7d28..edbcbe349 100644
--- a/internal/processing/admin/media.go
+++ b/internal/processing/admin/media.go
@@ -28,7 +28,7 @@ import (
// MediaRefetch forces a refetch of remote emojis.
func (p *Processor) MediaRefetch(ctx context.Context, requestingAccount *gtsmodel.Account, domain string) gtserror.WithCode {
- transport, err := p.transportController.NewTransportForUsername(ctx, requestingAccount.Username)
+ transport, err := p.transport.NewTransportForUsername(ctx, requestingAccount.Username)
if err != nil {
err = fmt.Errorf("error getting transport for user %s during media refetch request: %w", requestingAccount.Username, err)
return gtserror.NewErrorInternalError(err)
@@ -36,7 +36,7 @@ func (p *Processor) MediaRefetch(ctx context.Context, requestingAccount *gtsmode
go func() {
log.Info(ctx, "starting emoji refetch")
- refetched, err := p.mediaManager.RefetchEmojis(context.Background(), domain, transport.DereferenceMedia)
+ refetched, err := p.media.RefetchEmojis(context.Background(), domain, transport.DereferenceMedia)
if err != nil {
log.Errorf(ctx, "error refetching emojis: %s", err)
} else {
diff --git a/internal/processing/common/common.go b/internal/processing/common/common.go
index e4a49cc45..942cecc59 100644
--- a/internal/processing/common/common.go
+++ b/internal/processing/common/common.go
@@ -20,6 +20,7 @@ package common
import (
"github.com/superseriousbusiness/gotosocial/internal/federation"
"github.com/superseriousbusiness/gotosocial/internal/filter/visibility"
+ "github.com/superseriousbusiness/gotosocial/internal/media"
"github.com/superseriousbusiness/gotosocial/internal/state"
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
)
@@ -29,6 +30,7 @@ import (
// processing subsection of the codebase.
type Processor struct {
state *state.State
+ media *media.Manager
converter *typeutils.Converter
federator *federation.Federator
filter *visibility.Filter
@@ -37,12 +39,14 @@ type Processor struct {
// New returns a new Processor instance.
func New(
state *state.State,
+ media *media.Manager,
converter *typeutils.Converter,
federator *federation.Federator,
filter *visibility.Filter,
) Processor {
return Processor{
state: state,
+ media: media,
converter: converter,
federator: federator,
filter: filter,
diff --git a/internal/processing/common/media.go b/internal/processing/common/media.go
new file mode 100644
index 000000000..7baf30345
--- /dev/null
+++ b/internal/processing/common/media.go
@@ -0,0 +1,98 @@
+// GoToSocial
+// Copyright (C) GoToSocial Authors admin@gotosocial.org
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package common
+
+import (
+ "context"
+ "errors"
+ "fmt"
+
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "github.com/superseriousbusiness/gotosocial/internal/media"
+)
+
+// StoreLocalMedia is a wrapper around CreateMedia() and
+// ProcessingMedia{}.Load() with appropriate error responses.
+func (p *Processor) StoreLocalMedia(
+ ctx context.Context,
+ accountID string,
+ data media.DataFunc,
+ info media.AdditionalMediaInfo,
+) (
+ *gtsmodel.MediaAttachment,
+ gtserror.WithCode,
+) {
+ // Create a new processing media attachment.
+ processing, err := p.media.CreateMedia(ctx,
+ accountID,
+ data,
+ info,
+ )
+ if err != nil {
+ err := gtserror.Newf("error creating media: %w", err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ // Immediately trigger write to storage.
+ attachment, err := processing.Load(ctx)
+ if err != nil {
+ const text = "error processing emoji"
+ err := gtserror.Newf("error processing media: %w", err)
+ return nil, gtserror.NewErrorUnprocessableEntity(err, text)
+ } else if attachment.Type == gtsmodel.FileTypeUnknown {
+ text := fmt.Sprintf("could not process %s type media", attachment.File.ContentType)
+ return nil, gtserror.NewErrorUnprocessableEntity(errors.New(text), text)
+ }
+
+ return attachment, nil
+}
+
+// StoreLocalMedia is a wrapper around CreateMedia() and
+// ProcessingMedia{}.Load() with appropriate error responses.
+func (p *Processor) StoreLocalEmoji(
+ ctx context.Context,
+ shortcode string,
+ data media.DataFunc,
+ info media.AdditionalEmojiInfo,
+) (
+ *gtsmodel.Emoji,
+ gtserror.WithCode,
+) {
+ // Create a new processing emoji media.
+ processing, err := p.media.CreateEmoji(ctx,
+ shortcode,
+ "", // domain = "" -> local
+ data,
+ info,
+ )
+ if err != nil {
+ err := gtserror.Newf("error creating emoji: %w", err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ // Immediately write to storage.
+ emoji, err := processing.Load(ctx)
+ if err != nil {
+ const text = "error processing emoji"
+ err := gtserror.Newf("error processing emoji %s: %w", shortcode, err)
+ return nil, gtserror.NewErrorUnprocessableEntity(err, text)
+ }
+
+ return emoji, nil
+}
diff --git a/internal/processing/instance.go b/internal/processing/instance.go
index a93936425..a9be6db1d 100644
--- a/internal/processing/instance.go
+++ b/internal/processing/instance.go
@@ -246,9 +246,13 @@ func (p *Processor) InstancePatch(ctx context.Context, form *apimodel.InstanceSe
if form.Avatar != nil && form.Avatar.Size != 0 {
// Process instance avatar image + description.
- avatarInfo, err := p.account.UpdateAvatar(ctx, form.Avatar, form.AvatarDescription, instanceAcc.ID)
- if err != nil {
- return nil, gtserror.NewErrorBadRequest(err, "error processing avatar")
+ avatarInfo, errWithCode := p.account.UpdateAvatar(ctx,
+ instanceAcc,
+ form.Avatar,
+ form.AvatarDescription,
+ )
+ if errWithCode != nil {
+ return nil, errWithCode
}
instanceAcc.AvatarMediaAttachmentID = avatarInfo.ID
instanceAcc.AvatarMediaAttachment = avatarInfo
@@ -264,9 +268,13 @@ func (p *Processor) InstancePatch(ctx context.Context, form *apimodel.InstanceSe
if form.Header != nil && form.Header.Size != 0 {
// process instance header image
- headerInfo, err := p.account.UpdateHeader(ctx, form.Header, nil, instanceAcc.ID)
- if err != nil {
- return nil, gtserror.NewErrorBadRequest(err, "error processing header")
+ headerInfo, errWithCode := p.account.UpdateHeader(ctx,
+ instanceAcc,
+ form.Header,
+ nil,
+ )
+ if errWithCode != nil {
+ return nil, errWithCode
}
instanceAcc.HeaderMediaAttachmentID = headerInfo.ID
instanceAcc.HeaderMediaAttachment = headerInfo
diff --git a/internal/processing/media/create.go b/internal/processing/media/create.go
index fe20457b4..0dbe997de 100644
--- a/internal/processing/media/create.go
+++ b/internal/processing/media/create.go
@@ -30,7 +30,7 @@ import (
// Create creates a new media attachment belonging to the given account, using the request form.
func (p *Processor) Create(ctx context.Context, account *gtsmodel.Account, form *apimodel.AttachmentRequest) (*apimodel.Attachment, gtserror.WithCode) {
- data := func(innerCtx context.Context) (io.ReadCloser, int64, error) {
+ data := func(_ context.Context) (io.ReadCloser, int64, error) {
f, err := form.File.Open()
return f, form.File.Size, err
}
@@ -41,19 +41,18 @@ func (p *Processor) Create(ctx context.Context, account *gtsmodel.Account, form
return nil, gtserror.NewErrorBadRequest(err, err.Error())
}
- // process the media attachment and load it immediately
- media := p.mediaManager.PreProcessMedia(data, account.ID, &media.AdditionalMediaInfo{
- Description: &form.Description,
- FocusX: &focusX,
- FocusY: &focusY,
- })
-
- attachment, err := media.LoadAttachment(ctx)
- if err != nil {
- return nil, gtserror.NewErrorUnprocessableEntity(err, err.Error())
- } else if attachment.Type == gtsmodel.FileTypeUnknown {
- err = gtserror.Newf("could not process uploaded file with extension %s", attachment.File.ContentType)
- return nil, gtserror.NewErrorUnprocessableEntity(err, err.Error())
+ // Create local media and write to instance storage.
+ attachment, errWithCode := p.c.StoreLocalMedia(ctx,
+ account.ID,
+ data,
+ media.AdditionalMediaInfo{
+ Description: &form.Description,
+ FocusX: &focusX,
+ FocusY: &focusY,
+ },
+ )
+ if errWithCode != nil {
+ return nil, errWithCode
}
apiAttachment, err := p.converter.AttachmentToAPIAttachment(ctx, attachment)
diff --git a/internal/processing/media/getfile.go b/internal/processing/media/getfile.go
index 28f5e6464..7ba549029 100644
--- a/internal/processing/media/getfile.go
+++ b/internal/processing/media/getfile.go
@@ -19,14 +19,14 @@ package media
import (
"context"
+ "errors"
"fmt"
- "io"
"net/url"
"strings"
"time"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
- "github.com/superseriousbusiness/gotosocial/internal/gtscontext"
+ "github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/media"
@@ -38,7 +38,7 @@ import (
// to the caller via an io.reader embedded in *apimodel.Content.
func (p *Processor) GetFile(
ctx context.Context,
- requestingAccount *gtsmodel.Account,
+ requester *gtsmodel.Account,
form *apimodel.GetContentRequestForm,
) (*apimodel.Content, gtserror.WithCode) {
// parse the form fields
@@ -69,13 +69,13 @@ func (p *Processor) GetFile(
}
// make sure the requesting account and the media account don't block each other
- if requestingAccount != nil {
- blocked, err := p.state.DB.IsEitherBlocked(ctx, requestingAccount.ID, owningAccountID)
+ if requester != nil {
+ blocked, err := p.state.DB.IsEitherBlocked(ctx, requester.ID, owningAccountID)
if err != nil {
- return nil, gtserror.NewErrorNotFound(fmt.Errorf("block status could not be established between accounts %s and %s: %s", owningAccountID, requestingAccount.ID, err))
+ return nil, gtserror.NewErrorNotFound(fmt.Errorf("block status could not be established between accounts %s and %s: %s", owningAccountID, requester.ID, err))
}
if blocked {
- return nil, gtserror.NewErrorNotFound(fmt.Errorf("block exists between accounts %s and %s", owningAccountID, requestingAccount.ID))
+ return nil, gtserror.NewErrorNotFound(fmt.Errorf("block exists between accounts %s and %s", owningAccountID, requester.ID))
}
}
@@ -83,17 +83,254 @@ func (p *Processor) GetFile(
// so we need to take different steps depending on the media type being requested
switch mediaType {
case media.TypeEmoji:
- return p.getEmojiContent(ctx, wantedMediaID, owningAccountID, mediaSize)
+ return p.getEmojiContent(ctx,
+ owningAccountID,
+ wantedMediaID,
+ mediaSize,
+ )
case media.TypeAttachment, media.TypeHeader, media.TypeAvatar:
- return p.getAttachmentContent(ctx, requestingAccount, wantedMediaID, owningAccountID, mediaSize)
+ return p.getAttachmentContent(ctx,
+ requester,
+ owningAccountID,
+ wantedMediaID,
+ mediaSize,
+ )
default:
return nil, gtserror.NewErrorNotFound(fmt.Errorf("media type %s not recognized", mediaType))
}
}
-/*
- UTIL FUNCTIONS
-*/
+func (p *Processor) getAttachmentContent(
+ ctx context.Context,
+ requester *gtsmodel.Account,
+ ownerID string,
+ mediaID string,
+ sizeStr media.Size,
+) (
+ *apimodel.Content,
+ gtserror.WithCode,
+) {
+ // Search for media with given ID in the database.
+ attach, err := p.state.DB.GetAttachmentByID(ctx, mediaID)
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ err := gtserror.Newf("error fetching media from database: %w", err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ if attach == nil {
+ const text = "media not found"
+ return nil, gtserror.NewErrorNotFound(errors.New(text), text)
+ }
+
+ // Ensure the 'owner' owns media.
+ if attach.AccountID != ownerID {
+ const text = "media was not owned by passed account id"
+ return nil, gtserror.NewErrorNotFound(errors.New(text) /* no help text! */)
+ }
+
+ var remoteURL *url.URL
+ if attach.RemoteURL != "" {
+
+ // Parse media remote URL to valid URL object.
+ remoteURL, err = url.Parse(attach.RemoteURL)
+ if err != nil {
+ err := gtserror.Newf("invalid media remote url %s: %w", attach.RemoteURL, err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+ }
+
+ // Uknown file types indicate no *locally*
+ // stored data we can serve. Handle separately.
+ if attach.Type == gtsmodel.FileTypeUnknown {
+ if remoteURL == nil {
+ err := gtserror.Newf("missing remote url for unknown type media %s: %w", attach.ID, err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ // If this is an "Unknown" file type, ie., one we
+ // tried to process and couldn't, or one we refused
+ // to process because it wasn't supported, then we
+ // can skip a lot of steps here by simply forwarding
+ // the request to the remote URL.
+ url := &storage.PresignedURL{
+ URL: remoteURL,
+
+ // We might manage to cache the media
+ // at some point, so set a low-ish expiry.
+ Expiry: time.Now().Add(2 * time.Hour),
+ }
+
+ return &apimodel.Content{URL: url}, nil
+ }
+
+ var requestUser string
+
+ if requester != nil {
+ // Set requesting acc username.
+ requestUser = requester.Username
+ }
+
+ // Ensure that stored media is cached.
+ // (this handles local media / recaches).
+ attach, err = p.federator.RefreshMedia(
+ ctx,
+ requestUser,
+ attach,
+ media.AdditionalMediaInfo{},
+ false,
+ )
+ if err != nil {
+ err := gtserror.Newf("error recaching media: %w", err)
+ return nil, gtserror.NewErrorNotFound(err)
+ }
+
+ // Start preparing API content model.
+ apiContent := &apimodel.Content{
+ ContentUpdated: attach.UpdatedAt,
+ }
+
+ // Retrieve appropriate
+ // size file from storage.
+ switch sizeStr {
+
+ case media.SizeOriginal:
+ apiContent.ContentType = attach.File.ContentType
+ apiContent.ContentLength = int64(attach.File.FileSize)
+ return p.getContent(ctx,
+ attach.File.Path,
+ apiContent,
+ )
+
+ case media.SizeSmall:
+ apiContent.ContentType = attach.Thumbnail.ContentType
+ apiContent.ContentLength = int64(attach.Thumbnail.FileSize)
+ return p.getContent(ctx,
+ attach.Thumbnail.Path,
+ apiContent,
+ )
+
+ default:
+ const text = "invalid media attachment size"
+ return nil, gtserror.NewErrorBadRequest(errors.New(text), text)
+ }
+}
+
+func (p *Processor) getEmojiContent(
+ ctx context.Context,
+
+ ownerID string,
+ emojiID string,
+ sizeStr media.Size,
+) (
+ *apimodel.Content,
+ gtserror.WithCode,
+) {
+ // Reconstruct static emoji image URL to search for it.
+ // As refreshed emojis use a newly generated path ID to
+ // differentiate them (cache-wise) from the original.
+ staticURL := uris.URIForAttachment(
+ ownerID,
+ string(media.TypeEmoji),
+ string(media.SizeStatic),
+ emojiID,
+ "png",
+ )
+
+ // Search for emoji with given static URL in the database.
+ emoji, err := p.state.DB.GetEmojiByStaticURL(ctx, staticURL)
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ err := gtserror.Newf("error fetching emoji from database: %w", err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ if emoji == nil {
+ const text = "emoji not found"
+ return nil, gtserror.NewErrorNotFound(errors.New(text), text)
+ }
+
+ if *emoji.Disabled {
+ const text = "emoji has been disabled"
+ return nil, gtserror.NewErrorNotFound(errors.New(text), text)
+ }
+
+ // Ensure that stored emoji is cached.
+ // (this handles local emoji / recaches).
+ emoji, err = p.federator.RefreshEmoji(
+ ctx,
+ emoji,
+ media.AdditionalEmojiInfo{},
+ false,
+ )
+ if err != nil {
+ err := gtserror.Newf("error recaching emoji: %w", err)
+ return nil, gtserror.NewErrorNotFound(err)
+ }
+
+ // Start preparing API content model.
+ apiContent := &apimodel.Content{}
+
+ // Retrieve appropriate
+ // size file from storage.
+ switch sizeStr {
+
+ case media.SizeOriginal:
+ apiContent.ContentType = emoji.ImageContentType
+ apiContent.ContentLength = int64(emoji.ImageFileSize)
+ return p.getContent(ctx,
+ emoji.ImagePath,
+ apiContent,
+ )
+
+ case media.SizeStatic:
+ apiContent.ContentType = emoji.ImageStaticContentType
+ apiContent.ContentLength = int64(emoji.ImageStaticFileSize)
+ return p.getContent(ctx,
+ emoji.ImageStaticPath,
+ apiContent,
+ )
+
+ default:
+ const text = "invalid media attachment size"
+ return nil, gtserror.NewErrorBadRequest(errors.New(text), text)
+ }
+}
+
+// getContent performs the final file fetching of
+// stored content at path in storage. This is
+// populated in the apimodel.Content{} and returned.
+// (note: this also handles un-proxied S3 storage).
+func (p *Processor) getContent(
+ ctx context.Context,
+ path string,
+ content *apimodel.Content,
+) (
+ *apimodel.Content,
+ gtserror.WithCode,
+) {
+ // If running on S3 storage with proxying disabled then
+ // just fetch pre-signed URL instead of the content.
+ if url := p.state.Storage.URL(ctx, path); url != nil {
+ content.URL = url
+ return content, nil
+ }
+
+ // Fetch file stream for the stored media at path.
+ rc, err := p.state.Storage.GetStream(ctx, path)
+ if err != nil && !storage.IsNotFound(err) {
+ err := gtserror.Newf("error getting file %s from storage: %w", path, err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ // Ensure found.
+ if rc == nil {
+ const text = "file not found"
+ return nil, gtserror.NewErrorNotFound(errors.New(text), text)
+ }
+
+ // Return with stream.
+ content.Content = rc
+ return content, nil
+}
func parseType(s string) (media.Type, error) {
switch s {
@@ -120,198 +357,3 @@ func parseSize(s string) (media.Size, error) {
}
return "", fmt.Errorf("%s not a recognized media.Size", s)
}
-
-func (p *Processor) getAttachmentContent(ctx context.Context, requestingAccount *gtsmodel.Account, wantedMediaID string, owningAccountID string, mediaSize media.Size) (*apimodel.Content, gtserror.WithCode) {
- // retrieve attachment from the database and do basic checks on it
- a, err := p.state.DB.GetAttachmentByID(ctx, wantedMediaID)
- if err != nil {
- err = gtserror.Newf("attachment %s could not be taken from the db: %w", wantedMediaID, err)
- return nil, gtserror.NewErrorNotFound(err)
- }
-
- if a.AccountID != owningAccountID {
- err = gtserror.Newf("attachment %s is not owned by %s", wantedMediaID, owningAccountID)
- return nil, gtserror.NewErrorNotFound(err)
- }
-
- // If this is an "Unknown" file type, ie., one we
- // tried to process and couldn't, or one we refused
- // to process because it wasn't supported, then we
- // can skip a lot of steps here by simply forwarding
- // the request to the remote URL.
- if a.Type == gtsmodel.FileTypeUnknown {
- remoteURL, err := url.Parse(a.RemoteURL)
- if err != nil {
- err = gtserror.Newf("error parsing remote URL of 'Unknown'-type attachment for redirection: %w", err)
- return nil, gtserror.NewErrorInternalError(err)
- }
-
- url := &storage.PresignedURL{
- URL: remoteURL,
- // We might manage to cache the media
- // at some point, so set a low-ish expiry.
- Expiry: time.Now().Add(2 * time.Hour),
- }
-
- return &apimodel.Content{URL: url}, nil
- }
-
- if !*a.Cached {
- // if we don't have it cached, then we can assume two things:
- // 1. this is remote media, since local media should never be uncached
- // 2. we need to fetch it again using a transport and the media manager
- remoteMediaIRI, err := url.Parse(a.RemoteURL)
- if err != nil {
- return nil, gtserror.NewErrorNotFound(fmt.Errorf("error parsing remote media iri %s: %w", a.RemoteURL, err))
- }
-
- // use an empty string as requestingUsername to use the instance account, unless the request for this
- // media has been http signed, then use the requesting account to make the request to remote server
- var requestingUsername string
- if requestingAccount != nil {
- requestingUsername = requestingAccount.Username
- }
-
- // Pour one out for tobi's original streamed recache
- // (streaming data both to the client and storage).
- // Gone and forever missed <3
- //
- // [
- // the reason it was removed was because a slow
- // client connection could hold open a storage
- // recache operation -> holding open a media worker.
- // ]
-
- dataFn := func(ctx context.Context) (io.ReadCloser, int64, error) {
- t, err := p.transportController.NewTransportForUsername(ctx, requestingUsername)
- if err != nil {
- return nil, 0, err
- }
- return t.DereferenceMedia(gtscontext.SetFastFail(ctx), remoteMediaIRI)
- }
-
- // Start recaching this media with the prepared data function.
- processingMedia, err := p.mediaManager.PreProcessMediaRecache(ctx, dataFn, wantedMediaID)
- if err != nil {
- return nil, gtserror.NewErrorNotFound(fmt.Errorf("error recaching media: %w", err))
- }
-
- // Load attachment and block until complete
- a, err = processingMedia.LoadAttachment(ctx)
- if err != nil {
- return nil, gtserror.NewErrorNotFound(fmt.Errorf("error loading recached attachment: %w", err))
- }
- }
-
- var (
- storagePath string
- attachmentContent = &apimodel.Content{
- ContentUpdated: a.UpdatedAt,
- }
- )
-
- // get file information from the attachment depending on the requested media size
- switch mediaSize {
- case media.SizeOriginal:
- attachmentContent.ContentType = a.File.ContentType
- attachmentContent.ContentLength = int64(a.File.FileSize)
- storagePath = a.File.Path
- case media.SizeSmall:
- attachmentContent.ContentType = a.Thumbnail.ContentType
- attachmentContent.ContentLength = int64(a.Thumbnail.FileSize)
- storagePath = a.Thumbnail.Path
- default:
- return nil, gtserror.NewErrorNotFound(fmt.Errorf("media size %s not recognized for attachment", mediaSize))
- }
-
- // ... so now we can safely return it
- return p.retrieveFromStorage(ctx, storagePath, attachmentContent)
-}
-
-func (p *Processor) getEmojiContent(ctx context.Context, fileName string, owningAccountID string, emojiSize media.Size) (*apimodel.Content, gtserror.WithCode) {
- emojiContent := &apimodel.Content{}
- var storagePath string
-
- // reconstruct the static emoji image url -- reason
- // for using the static URL rather than full size url
- // is that static emojis are always encoded as png,
- // so this is more reliable than using full size url
- imageStaticURL := uris.URIForAttachment(
- owningAccountID,
- string(media.TypeEmoji),
- string(media.SizeStatic),
- fileName,
- "png",
- )
-
- e, err := p.state.DB.GetEmojiByStaticURL(ctx, imageStaticURL)
- if err != nil {
- return nil, gtserror.NewErrorNotFound(fmt.Errorf("emoji %s could not be taken from the db: %w", fileName, err))
- }
-
- if *e.Disabled {
- return nil, gtserror.NewErrorNotFound(fmt.Errorf("emoji %s has been disabled", fileName))
- }
-
- if !*e.Cached {
- // if we don't have it cached, then we can assume two things:
- // 1. this is remote emoji, since local emoji should never be uncached
- // 2. we need to fetch it again using a transport and the media manager
- remoteURL, err := url.Parse(e.ImageRemoteURL)
- if err != nil {
- return nil, gtserror.NewErrorNotFound(fmt.Errorf("error parsing remote emoji iri %s: %w", e.ImageRemoteURL, err))
- }
-
- dataFn := func(ctx context.Context) (io.ReadCloser, int64, error) {
- t, err := p.transportController.NewTransportForUsername(ctx, "")
- if err != nil {
- return nil, 0, err
- }
- return t.DereferenceMedia(gtscontext.SetFastFail(ctx), remoteURL)
- }
-
- // Start recaching this emoji with the prepared data function.
- processingEmoji, err := p.mediaManager.PreProcessEmojiRecache(ctx, dataFn, e.ID)
- if err != nil {
- return nil, gtserror.NewErrorNotFound(fmt.Errorf("error recaching emoji: %w", err))
- }
-
- // Load attachment and block until complete
- e, err = processingEmoji.LoadEmoji(ctx)
- if err != nil {
- return nil, gtserror.NewErrorNotFound(fmt.Errorf("error loading recached emoji: %w", err))
- }
- }
-
- switch emojiSize {
- case media.SizeOriginal:
- emojiContent.ContentType = e.ImageContentType
- emojiContent.ContentLength = int64(e.ImageFileSize)
- storagePath = e.ImagePath
- case media.SizeStatic:
- emojiContent.ContentType = e.ImageStaticContentType
- emojiContent.ContentLength = int64(e.ImageStaticFileSize)
- storagePath = e.ImageStaticPath
- default:
- return nil, gtserror.NewErrorNotFound(fmt.Errorf("media size %s not recognized for emoji", emojiSize))
- }
-
- return p.retrieveFromStorage(ctx, storagePath, emojiContent)
-}
-
-func (p *Processor) retrieveFromStorage(ctx context.Context, storagePath string, content *apimodel.Content) (*apimodel.Content, gtserror.WithCode) {
- // If running on S3 storage with proxying disabled then
- // just fetch a pre-signed URL instead of serving the content.
- if url := p.state.Storage.URL(ctx, storagePath); url != nil {
- content.URL = url
- return content, nil
- }
-
- reader, err := p.state.Storage.GetStream(ctx, storagePath)
- if err != nil {
- return nil, gtserror.NewErrorNotFound(fmt.Errorf("error retrieving from storage: %s", err))
- }
-
- content.Content = reader
- return content, nil
-}
diff --git a/internal/processing/media/media.go b/internal/processing/media/media.go
index 22c455920..76ed68f5a 100644
--- a/internal/processing/media/media.go
+++ b/internal/processing/media/media.go
@@ -18,24 +18,39 @@
package media
import (
+ "github.com/superseriousbusiness/gotosocial/internal/federation"
"github.com/superseriousbusiness/gotosocial/internal/media"
+ "github.com/superseriousbusiness/gotosocial/internal/processing/common"
"github.com/superseriousbusiness/gotosocial/internal/state"
"github.com/superseriousbusiness/gotosocial/internal/transport"
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
)
type Processor struct {
+ // common processor logic
+ c *common.Processor
+
state *state.State
converter *typeutils.Converter
+ federator *federation.Federator
mediaManager *media.Manager
transportController transport.Controller
}
// New returns a new media processor.
-func New(state *state.State, converter *typeutils.Converter, mediaManager *media.Manager, transportController transport.Controller) Processor {
+func New(
+ common *common.Processor,
+ state *state.State,
+ converter *typeutils.Converter,
+ federator *federation.Federator,
+ mediaManager *media.Manager,
+ transportController transport.Controller,
+) Processor {
return Processor{
+ c: common,
state: state,
converter: converter,
+ federator: federator,
mediaManager: mediaManager,
transportController: transportController,
}
diff --git a/internal/processing/media/media_test.go b/internal/processing/media/media_test.go
index 523428140..80f1a7be7 100644
--- a/internal/processing/media/media_test.go
+++ b/internal/processing/media/media_test.go
@@ -20,8 +20,10 @@ package media_test
import (
"github.com/stretchr/testify/suite"
"github.com/superseriousbusiness/gotosocial/internal/db"
+ "github.com/superseriousbusiness/gotosocial/internal/filter/visibility"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/media"
+ "github.com/superseriousbusiness/gotosocial/internal/processing/common"
mediaprocessing "github.com/superseriousbusiness/gotosocial/internal/processing/media"
"github.com/superseriousbusiness/gotosocial/internal/state"
"github.com/superseriousbusiness/gotosocial/internal/storage"
@@ -78,7 +80,12 @@ func (suite *MediaStandardTestSuite) SetupTest() {
suite.state.Storage = suite.storage
suite.mediaManager = testrig.NewTestMediaManager(&suite.state)
suite.transportController = testrig.NewTestTransportController(&suite.state, testrig.NewMockHTTPClient(nil, "../../../testrig/media"))
- suite.mediaProcessor = mediaprocessing.New(&suite.state, suite.tc, suite.mediaManager, suite.transportController)
+
+ federator := testrig.NewTestFederator(&suite.state, suite.transportController, suite.mediaManager)
+ filter := visibility.NewFilter(&suite.state)
+ common := common.New(&suite.state, suite.mediaManager, suite.tc, federator, filter)
+
+ suite.mediaProcessor = mediaprocessing.New(&common, &suite.state, suite.tc, federator, suite.mediaManager, suite.transportController)
testrig.StandardDBSetup(suite.db, nil)
testrig.StandardStorageSetup(suite.storage, "../../../testrig/media")
}
diff --git a/internal/processing/polls/poll_test.go b/internal/processing/polls/poll_test.go
index 847612503..bf6ae4aad 100644
--- a/internal/processing/polls/poll_test.go
+++ b/internal/processing/polls/poll_test.go
@@ -57,7 +57,7 @@ func (suite *PollTestSuite) SetupTest() {
mediaMgr := media.NewManager(&suite.state)
federator := testrig.NewTestFederator(&suite.state, controller, mediaMgr)
suite.filter = visibility.NewFilter(&suite.state)
- common := common.New(&suite.state, converter, federator, suite.filter)
+ common := common.New(&suite.state, mediaMgr, converter, federator, suite.filter)
suite.polls = polls.New(&common, &suite.state, converter)
}
diff --git a/internal/processing/processor.go b/internal/processing/processor.go
index 8765819d3..fb6b05d80 100644
--- a/internal/processing/processor.go
+++ b/internal/processing/processor.go
@@ -179,15 +179,15 @@ func NewProcessor(
//
// Start with sub processors that will
// be required by the workers processor.
- common := common.New(state, converter, federator, filter)
+ common := common.New(state, mediaManager, converter, federator, filter)
processor.account = account.New(&common, state, converter, mediaManager, federator, filter, parseMentionFunc)
- processor.media = media.New(state, converter, mediaManager, federator.TransportController())
+ processor.media = media.New(&common, state, converter, federator, mediaManager, federator.TransportController())
processor.stream = stream.New(state, oauthServer)
// Instantiate the rest of the sub
// processors + pin them to this struct.
processor.account = account.New(&common, state, converter, mediaManager, federator, filter, parseMentionFunc)
- processor.admin = admin.New(state, cleaner, converter, mediaManager, federator.TransportController(), emailSender)
+ processor.admin = admin.New(&common, state, cleaner, federator, converter, mediaManager, federator.TransportController(), emailSender)
processor.fedi = fedi.New(state, &common, converter, federator, filter)
processor.filtersv1 = filtersv1.New(state, converter, &processor.stream)
processor.filtersv2 = filtersv2.New(state, converter, &processor.stream)
diff --git a/internal/processing/status/status_test.go b/internal/processing/status/status_test.go
index 171e4b488..9eba78ec6 100644
--- a/internal/processing/status/status_test.go
+++ b/internal/processing/status/status_test.go
@@ -96,7 +96,7 @@ func (suite *StatusStandardTestSuite) SetupTest() {
suite.typeConverter,
)
- common := common.New(&suite.state, suite.typeConverter, suite.federator, filter)
+ common := common.New(&suite.state, suite.mediaManager, suite.typeConverter, suite.federator, filter)
polls := polls.New(&common, &suite.state, suite.typeConverter)
suite.status = status.New(&suite.state, &common, &polls, suite.federator, suite.typeConverter, filter, processing.GetParseMentionFunc(&suite.state, suite.federator))
diff --git a/internal/storage/storage.go b/internal/storage/storage.go
index 872ea1210..55ec0d167 100644
--- a/internal/storage/storage.go
+++ b/internal/storage/storage.go
@@ -95,7 +95,7 @@ func (d *Driver) PutStream(ctx context.Context, key string, r io.Reader) (int64,
return d.Storage.WriteStream(ctx, key, r)
}
-// Remove attempts to remove the supplied key (and corresponding value) from storage.
+// Delete attempts to remove the supplied key (and corresponding value) from storage.
func (d *Driver) Delete(ctx context.Context, key string) error {
return d.Storage.Remove(ctx, key)
}
diff --git a/internal/typeutils/internaltoas.go b/internal/typeutils/internaltoas.go
index 2fb782029..567493673 100644
--- a/internal/typeutils/internaltoas.go
+++ b/internal/typeutils/internaltoas.go
@@ -1051,7 +1051,7 @@ func (c *Converter) EmojiToAS(ctx context.Context, e *gtsmodel.Emoji) (vocab.Too
emoji.SetActivityStreamsIcon(iconProperty)
updatedProp := streams.NewActivityStreamsUpdatedProperty()
- updatedProp.Set(e.ImageUpdatedAt)
+ updatedProp.Set(e.UpdatedAt)
emoji.SetActivityStreamsUpdated(updatedProp)
return emoji, nil
diff --git a/internal/workers/workers.go b/internal/workers/workers.go
index 306d9e635..4d2b146b6 100644
--- a/internal/workers/workers.go
+++ b/internal/workers/workers.go
@@ -49,10 +49,6 @@ type Workers struct {
// for asynchronous dereferencer jobs.
Dereference FnWorkerPool
- // Media provides a worker pool for
- // asynchronous media processing jobs.
- Media FnWorkerPool
-
// prevent pass-by-value.
_ nocopy
}
@@ -84,10 +80,6 @@ func (w *Workers) Start() {
n = 4 * maxprocs
w.Dereference.Start(n)
log.Infof(nil, "started %d dereference workers", n)
-
- n = 8 * maxprocs
- w.Media.Start(n)
- log.Infof(nil, "started %d media workers", n)
}
// Stop will stop all of the contained worker pools (and global scheduler).
@@ -105,9 +97,6 @@ func (w *Workers) Stop() {
w.Dereference.Stop()
log.Info(nil, "stopped dereference workers")
-
- w.Media.Stop()
- log.Info(nil, "stopped media workers")
}
// nocopy when embedded will signal linter to
diff --git a/testrig/testmodels.go b/testrig/testmodels.go
index e3d31b7d2..3db8ef62f 100644
--- a/testrig/testmodels.go
+++ b/testrig/testmodels.go
@@ -739,13 +739,11 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment {
Path: "01F8MH17FWEB39HZJ76B6VXSKF/attachment/original/01F8MH6NEM8D7527KZAECTCR76.jpg",
ContentType: "image/jpeg",
FileSize: 62529,
- UpdatedAt: TimeMustParse("2022-06-04T13:12:00Z"),
},
Thumbnail: gtsmodel.Thumbnail{
Path: "01F8MH17FWEB39HZJ76B6VXSKF/attachment/small/01F8MH6NEM8D7527KZAECTCR76.jpg",
ContentType: "image/jpeg",
FileSize: 6872,
- UpdatedAt: TimeMustParse("2022-06-04T13:12:00Z"),
URL: "http://localhost:8080/fileserver/01F8MH17FWEB39HZJ76B6VXSKF/attachment/small/01F8MH6NEM8D7527KZAECTCR76.jpg",
RemoteURL: "",
},
@@ -788,13 +786,11 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment {
Path: "01F8MH1H7YV1Z7D2C8K2730QBF/attachment/original/01F8MH7TDVANYKWVE8VVKFPJTJ.gif",
ContentType: "image/gif",
FileSize: 1109138,
- UpdatedAt: TimeMustParse("2022-06-09T13:12:00Z"),
},
Thumbnail: gtsmodel.Thumbnail{
Path: "01F8MH1H7YV1Z7D2C8K2730QBF/attachment/small/01F8MH7TDVANYKWVE8VVKFPJTJ.jpg",
ContentType: "image/jpeg",
FileSize: 8803,
- UpdatedAt: TimeMustParse("2022-06-09T13:12:00Z"),
URL: "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/attachment/small/01F8MH7TDVANYKWVE8VVKFPJTJ.jpg",
RemoteURL: "",
},
@@ -840,13 +836,11 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment {
Path: "01F8MH1H7YV1Z7D2C8K2730QBF/attachment/original/01CDR64G398ADCHXK08WWTHEZ5.gif",
ContentType: "video/mp4",
FileSize: 2273532,
- UpdatedAt: TimeMustParse("2022-06-09T13:12:00Z"),
},
Thumbnail: gtsmodel.Thumbnail{
Path: "01F8MH1H7YV1Z7D2C8K2730QBF/attachment/small/01CDR64G398ADCHXK08WWTHEZ5.jpg",
ContentType: "image/jpeg",
FileSize: 5272,
- UpdatedAt: TimeMustParse("2022-06-09T13:12:00Z"),
URL: "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/attachment/small/01CDR64G398ADCHXK08WWTHEZ5.jpg",
RemoteURL: "",
},
@@ -889,13 +883,11 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment {
Path: "01F8MH1H7YV1Z7D2C8K2730QBF/attachment/original/01F8MH8RMYQ6MSNY3JM2XT1CQ5.jpg",
ContentType: "image/jpeg",
FileSize: 27759,
- UpdatedAt: TimeMustParse("2022-06-09T13:12:00Z"),
},
Thumbnail: gtsmodel.Thumbnail{
Path: "01F8MH1H7YV1Z7D2C8K2730QBF/attachment/small/01F8MH8RMYQ6MSNY3JM2XT1CQ5.jpg",
ContentType: "image/jpeg",
FileSize: 6177,
- UpdatedAt: TimeMustParse("2022-06-09T13:12:00Z"),
URL: "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/attachment/small/01F8MH8RMYQ6MSNY3JM2XT1CQ5.jpg",
RemoteURL: "",
},
@@ -938,13 +930,11 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment {
Path: "01F8MH1H7YV1Z7D2C8K2730QBF/avatar/original/01F8MH58A357CV5K7R7TJMSH6S.jpg",
ContentType: "image/jpeg",
FileSize: 457680,
- UpdatedAt: TimeMustParse("2022-06-09T13:12:00Z"),
},
Thumbnail: gtsmodel.Thumbnail{
Path: "01F8MH1H7YV1Z7D2C8K2730QBF/avatar/small/01F8MH58A357CV5K7R7TJMSH6S.jpg",
ContentType: "image/jpeg",
FileSize: 15374,
- UpdatedAt: TimeMustParse("2022-06-09T13:12:00Z"),
URL: "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/small/01F8MH58A357CV5K7R7TJMSH6S.jpg",
RemoteURL: "",
},
@@ -987,13 +977,11 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment {
Path: "01F8MH1H7YV1Z7D2C8K2730QBF/header/original/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg",
ContentType: "image/jpeg",
FileSize: 517226,
- UpdatedAt: TimeMustParse("2022-06-09T13:12:00Z"),
},
Thumbnail: gtsmodel.Thumbnail{
Path: "01F8MH1H7YV1Z7D2C8K2730QBF/header/small/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg",
ContentType: "image/jpeg",
FileSize: 42308,
- UpdatedAt: TimeMustParse("2022-06-09T13:12:00Z"),
URL: "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/small/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg",
RemoteURL: "",
},
@@ -1036,13 +1024,11 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment {
Path: "01F8MH5ZK5VRH73AKHQM6Y9VNX/attachment/original/01FVW7RXPQ8YJHTEXYPE7Q8ZY0.jpg",
ContentType: "image/jpeg",
FileSize: 19310,
- UpdatedAt: TimeMustParse("2021-09-20T12:40:37+02:00"),
},
Thumbnail: gtsmodel.Thumbnail{
Path: "01F8MH5ZK5VRH73AKHQM6Y9VNX/attachment/small/01FVW7RXPQ8YJHTEXYPE7Q8ZY0.jpg",
ContentType: "image/jpeg",
FileSize: 19312,
- UpdatedAt: TimeMustParse("2021-09-20T12:40:37+02:00"),
URL: "http://localhost:8080/fileserver/01F8MH5ZK5VRH73AKHQM6Y9VNX/attachment/small/01FVW7RXPQ8YJHTEXYPE7Q8ZY0.jpg",
RemoteURL: "http://fossbros-anonymous.io/attachments/small/a499f55b-2d1e-4acd-98d2-1ac2ba6d79b9.jpg",
},
@@ -1085,13 +1071,11 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment {
Path: "062G5WYKY35KKD12EMSM3F8PJ8/attachment/original/01PFPMWK2FF0D9WMHEJHR07C3R.jpg",
ContentType: "image/jpeg",
FileSize: 19310,
- UpdatedAt: TimeMustParse("2022-06-09T13:12:00Z"),
},
Thumbnail: gtsmodel.Thumbnail{
Path: "062G5WYKY35KKD12EMSM3F8PJ8/attachment/small/01PFPMWK2FF0D9WMHEJHR07C3R.jpg",
ContentType: "image/jpeg",
FileSize: 20395,
- UpdatedAt: TimeMustParse("2022-06-09T13:12:00Z"),
URL: "http://localhost:8080/fileserver/062G5WYKY35KKD12EMSM3F8PJ8/header/small/01PFPMWK2FF0D9WMHEJHR07C3R.jpg",
RemoteURL: "http://fossbros-anonymous.io/attachments/small/a499f55b-2d1e-4acd-98d2-1ac2ba6d79b9.jpg",
},
@@ -1133,13 +1117,11 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment {
Path: "01FHMQX3GAABWSM0S2VZEC2SWC/attachment/original/01HE7Y3C432WRSNS10EZM86SA5.jpg",
ContentType: "image/jpg",
FileSize: 5450054,
- UpdatedAt: TimeMustParse("2023-11-02T12:44:25+02:00"),
},
Thumbnail: gtsmodel.Thumbnail{
Path: "01FHMQX3GAABWSM0S2VZEC2SWC/attachment/small/01HE7Y3C432WRSNS10EZM86SA5.jpg",
ContentType: "image/jpeg",
FileSize: 50820,
- UpdatedAt: TimeMustParse("2023-11-02T12:44:25+02:00"),
URL: "http://localhost:8080/fileserver/01FHMQX3GAABWSM0S2VZEC2SWC/attachment/small/01HE7Y3C432WRSNS10EZM86SA5.jpg",
},
Avatar: util.Ptr(false),
@@ -1163,13 +1145,11 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment {
Path: "01FHMQX3GAABWSM0S2VZEC2SWC/attachment/original/01HE7ZFX9GKA5ZZVD4FACABSS9.svg",
ContentType: "image/svg",
FileSize: 147819,
- UpdatedAt: TimeMustParse("2023-11-02T12:44:25+02:00"),
},
Thumbnail: gtsmodel.Thumbnail{
Path: "01FHMQX3GAABWSM0S2VZEC2SWC/attachment/small/01HE7ZFX9GKA5ZZVD4FACABSS9.jpg",
ContentType: "image/jpeg",
FileSize: 0,
- UpdatedAt: TimeMustParse("2023-11-02T12:44:25+02:00"),
URL: "http://localhost:8080/fileserver/01FHMQX3GAABWSM0S2VZEC2SWC/attachment/small/01HE7ZFX9GKA5ZZVD4FACABSS9.jpg",
},
Avatar: util.Ptr(false),
@@ -1193,13 +1173,11 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment {
Path: "01FHMQX3GAABWSM0S2VZEC2SWC/attachment/original/01HE88YG74PVAB81PX2XA9F3FG.mp3",
ContentType: "audio/mpeg",
FileSize: 147819,
- UpdatedAt: TimeMustParse("2023-11-02T12:44:25+02:00"),
},
Thumbnail: gtsmodel.Thumbnail{
Path: "01FHMQX3GAABWSM0S2VZEC2SWC/attachment/small/01HE88YG74PVAB81PX2XA9F3FG.jpg",
ContentType: "image/jpeg",
FileSize: 0,
- UpdatedAt: TimeMustParse("2023-11-02T12:44:25+02:00"),
URL: "http://localhost:8080/fileserver/01FHMQX3GAABWSM0S2VZEC2SWC/attachment/small/01HE88YG74PVAB81PX2XA9F3FG.jpg",
},
Avatar: util.Ptr(false),
@@ -1228,7 +1206,6 @@ func NewTestEmojis() map[string]*gtsmodel.Emoji {
ImageStaticContentType: "image/png",
ImageFileSize: 36702,
ImageStaticFileSize: 10413,
- ImageUpdatedAt: TimeMustParse("2021-09-20T12:40:37+02:00"),
Disabled: util.Ptr(false),
URI: "http://localhost:8080/emoji/01F8MH9H8E4VG3KDYJR9EGPXCQ",
VisibleInPicker: util.Ptr(true),
@@ -1251,7 +1228,6 @@ func NewTestEmojis() map[string]*gtsmodel.Emoji {
ImageStaticContentType: "image/png",
ImageFileSize: 10889,
ImageStaticFileSize: 10808,
- ImageUpdatedAt: TimeMustParse("2020-03-18T13:12:00+01:00"),
Disabled: util.Ptr(false),
URI: "http://fossbros-anonymous.io/emoji/01GD5KP5CQEE1R3X43Y1EHS2CW",
VisibleInPicker: util.Ptr(false),
diff --git a/testrig/util.go b/testrig/util.go
index d5eaedcd5..abc94bf02 100644
--- a/testrig/util.go
+++ b/testrig/util.go
@@ -82,7 +82,6 @@ func StartWorkers(state *state.State, processor *workers.Processor) {
state.Workers.Client.Start(1)
state.Workers.Federator.Start(1)
state.Workers.Dereference.Start(1)
- state.Workers.Media.Start(1)
}
func StopWorkers(state *state.State) {
@@ -90,7 +89,6 @@ func StopWorkers(state *state.State) {
state.Workers.Client.Stop()
state.Workers.Federator.Stop()
state.Workers.Dereference.Stop()
- state.Workers.Media.Stop()
}
func StartTimelines(state *state.State, filter *visibility.Filter, converter *typeutils.Converter) {