mirror of
				https://github.com/superseriousbusiness/gotosocial.git
				synced 2025-10-30 21:12:24 -05:00 
			
		
		
		
	rewrite file serving system
This commit is contained in:
		
					parent
					
						
							
								e47ee2b883
							
						
					
				
			
			
				commit
				
					
						cc424df169
					
				
			
		
					 12 changed files with 355 additions and 226 deletions
				
			
		|  | @ -21,12 +21,11 @@ package fileserver | ||||||
| import ( | import ( | ||||||
| 	"bytes" | 	"bytes" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"strings" |  | ||||||
| 
 | 
 | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/sirupsen/logrus" | 	"github.com/sirupsen/logrus" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | 	"github.com/superseriousbusiness/gotosocial/internal/api/model" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/media" | 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // ServeFile is for serving attachments, headers, and avatars to the requester from instance storage. | // ServeFile is for serving attachments, headers, and avatars to the requester from instance storage. | ||||||
|  | @ -42,6 +41,12 @@ func (m *FileServer) ServeFile(c *gin.Context) { | ||||||
| 	}) | 	}) | ||||||
| 	l.Trace("received request") | 	l.Trace("received request") | ||||||
| 
 | 
 | ||||||
|  | 	authed, err := oauth.Authed(c, false, false, false, false) | ||||||
|  | 	if err != nil { | ||||||
|  | 		c.String(http.StatusNotFound, "404 page not found") | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	// We use request params to check what to pull out of the database/storage so check everything. A request URL should be formatted as follows: | 	// We use request params to check what to pull out of the database/storage so check everything. A request URL should be formatted as follows: | ||||||
| 	// "https://example.org/fileserver/[ACCOUNT_ID]/[MEDIA_TYPE]/[MEDIA_SIZE]/[FILE_NAME]" | 	// "https://example.org/fileserver/[ACCOUNT_ID]/[MEDIA_TYPE]/[MEDIA_SIZE]/[FILE_NAME]" | ||||||
| 	// "FILE_NAME" consists of two parts, the attachment's database id, a period, and the file extension. | 	// "FILE_NAME" consists of two parts, the attachment's database id, a period, and the file extension. | ||||||
|  | @ -73,171 +78,16 @@ func (m *FileServer) ServeFile(c *gin.Context) { | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Only serve media types that are defined in our internal media module | 	content, err := m.processor.MediaGet(authed, &model.GetContentRequestForm{ | ||||||
| 	switch mediaType { | 		AccountID: accountID, | ||||||
| 	case media.MediaHeader, media.MediaAvatar, media.MediaAttachment: | 		MediaType: mediaType, | ||||||
| 		m.serveAttachment(c, accountID, mediaType, mediaSize, fileName) | 		MediaSize: mediaSize, | ||||||
| 		return | 		FileName:  fileName, | ||||||
| 	case media.MediaEmoji: |  | ||||||
| 		m.serveEmoji(c, accountID, mediaType, mediaSize, fileName) |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
| 	l.Debugf("mediatype %s not recognized", mediaType) |  | ||||||
| 	c.String(http.StatusNotFound, "404 page not found") |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (m *FileServer) serveAttachment(c *gin.Context, accountID string, mediaType string, mediaSize string, fileName string) { |  | ||||||
| 	l := m.log.WithFields(logrus.Fields{ |  | ||||||
| 		"func":        "serveAttachment", |  | ||||||
| 		"request_uri": c.Request.RequestURI, |  | ||||||
| 		"user_agent":  c.Request.UserAgent(), |  | ||||||
| 		"origin_ip":   c.ClientIP(), |  | ||||||
| 	}) | 	}) | ||||||
| 
 |  | ||||||
| 	// This corresponds to original-sized image as it was uploaded, small (which is the thumbnail), or static |  | ||||||
| 	switch mediaSize { |  | ||||||
| 	case media.MediaOriginal, media.MediaSmall, media.MediaStatic: |  | ||||||
| 	default: |  | ||||||
| 		l.Debugf("mediasize %s not recognized", mediaSize) |  | ||||||
| 		c.String(http.StatusNotFound, "404 page not found") |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// derive the media id and the file extension from the last part of the request |  | ||||||
| 	spl := strings.Split(fileName, ".") |  | ||||||
| 	if len(spl) != 2 { |  | ||||||
| 		l.Debugf("filename %s not parseable", fileName) |  | ||||||
| 		c.String(http.StatusNotFound, "404 page not found") |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
| 	wantedMediaID := spl[0] |  | ||||||
| 	fileExtension := spl[1] |  | ||||||
| 	if wantedMediaID == "" || fileExtension == "" { |  | ||||||
| 		l.Debugf("filename %s not parseable", fileName) |  | ||||||
| 		c.String(http.StatusNotFound, "404 page not found") |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// now we know the attachment ID that the caller is asking for we can use it to pull the attachment out of the db |  | ||||||
| 	attachment := >smodel.MediaAttachment{} |  | ||||||
| 	if err := m.db.GetByID(wantedMediaID, attachment); err != nil { |  | ||||||
| 		l.Debugf("attachment with id %s not retrievable: %s", wantedMediaID, err) |  | ||||||
| 		c.String(http.StatusNotFound, "404 page not found") |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// make sure the given account id owns the requested attachment |  | ||||||
| 	if accountID != attachment.AccountID { |  | ||||||
| 		l.Debugf("account %s does not own attachment with id %s", accountID, wantedMediaID) |  | ||||||
| 		c.String(http.StatusNotFound, "404 page not found") |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// now we can start preparing the response depending on whether we're serving a thumbnail or a larger attachment |  | ||||||
| 	var storagePath string |  | ||||||
| 	var contentType string |  | ||||||
| 	var contentLength int |  | ||||||
| 	switch mediaSize { |  | ||||||
| 	case media.MediaOriginal: |  | ||||||
| 		storagePath = attachment.File.Path |  | ||||||
| 		contentType = attachment.File.ContentType |  | ||||||
| 		contentLength = attachment.File.FileSize |  | ||||||
| 	case media.MediaSmall: |  | ||||||
| 		storagePath = attachment.Thumbnail.Path |  | ||||||
| 		contentType = attachment.Thumbnail.ContentType |  | ||||||
| 		contentLength = attachment.Thumbnail.FileSize |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// use the path listed on the attachment we pulled out of the database to retrieve the object from storage |  | ||||||
| 	attachmentBytes, err := m.storage.RetrieveFileFrom(storagePath) |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		l.Debugf("error retrieving from storage: %s", err) |  | ||||||
| 		c.String(http.StatusNotFound, "404 page not found") | 		c.String(http.StatusNotFound, "404 page not found") | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	l.Errorf("about to serve content length: %d attachment bytes is: %d", int64(contentLength), int64(len(attachmentBytes))) | 	c.DataFromReader(http.StatusOK, content.ContentLength, content.ContentType, bytes.NewReader(content.Content), nil) | ||||||
| 
 |  | ||||||
| 	// finally we can return with all the information we derived above |  | ||||||
| 	c.DataFromReader(http.StatusOK, int64(contentLength), contentType, bytes.NewReader(attachmentBytes), map[string]string{}) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (m *FileServer) serveEmoji(c *gin.Context, accountID string, mediaType string, mediaSize string, fileName string) { |  | ||||||
| 	l := m.log.WithFields(logrus.Fields{ |  | ||||||
| 		"func":        "serveEmoji", |  | ||||||
| 		"request_uri": c.Request.RequestURI, |  | ||||||
| 		"user_agent":  c.Request.UserAgent(), |  | ||||||
| 		"origin_ip":   c.ClientIP(), |  | ||||||
| 	}) |  | ||||||
| 
 |  | ||||||
| 	// This corresponds to original-sized emoji as it was uploaded, or static |  | ||||||
| 	switch mediaSize { |  | ||||||
| 	case media.MediaOriginal, media.MediaStatic: |  | ||||||
| 	default: |  | ||||||
| 		l.Debugf("mediasize %s not recognized", mediaSize) |  | ||||||
| 		c.String(http.StatusNotFound, "404 page not found") |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// derive the media id and the file extension from the last part of the request |  | ||||||
| 	spl := strings.Split(fileName, ".") |  | ||||||
| 	if len(spl) != 2 { |  | ||||||
| 		l.Debugf("filename %s not parseable", fileName) |  | ||||||
| 		c.String(http.StatusNotFound, "404 page not found") |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
| 	wantedEmojiID := spl[0] |  | ||||||
| 	fileExtension := spl[1] |  | ||||||
| 	if wantedEmojiID == "" || fileExtension == "" { |  | ||||||
| 		l.Debugf("filename %s not parseable", fileName) |  | ||||||
| 		c.String(http.StatusNotFound, "404 page not found") |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// now we know the attachment ID that the caller is asking for we can use it to pull the attachment out of the db |  | ||||||
| 	emoji := >smodel.Emoji{} |  | ||||||
| 	if err := m.db.GetByID(wantedEmojiID, emoji); err != nil { |  | ||||||
| 		l.Debugf("emoji with id %s not retrievable: %s", wantedEmojiID, err) |  | ||||||
| 		c.String(http.StatusNotFound, "404 page not found") |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// make sure the instance account id owns the requested emoji |  | ||||||
| 	instanceAccount := >smodel.Account{} |  | ||||||
| 	if err := m.db.GetLocalAccountByUsername(m.config.Host, instanceAccount); err != nil { |  | ||||||
| 		l.Debugf("error fetching instance account: %s", err) |  | ||||||
| 		c.String(http.StatusNotFound, "404 page not found") |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
| 	if accountID != instanceAccount.ID { |  | ||||||
| 		l.Debugf("account %s does not own emoji with id %s", accountID, wantedEmojiID) |  | ||||||
| 		c.String(http.StatusNotFound, "404 page not found") |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// now we can start preparing the response depending on whether we're serving a thumbnail or a larger attachment |  | ||||||
| 	var storagePath string |  | ||||||
| 	var contentType string |  | ||||||
| 	var contentLength int |  | ||||||
| 	switch mediaSize { |  | ||||||
| 	case media.MediaOriginal: |  | ||||||
| 		storagePath = emoji.ImagePath |  | ||||||
| 		contentType = emoji.ImageContentType |  | ||||||
| 		contentLength = emoji.ImageFileSize |  | ||||||
| 	case media.MediaStatic: |  | ||||||
| 		storagePath = emoji.ImageStaticPath |  | ||||||
| 		contentType = "image/png" |  | ||||||
| 		contentLength = emoji.ImageStaticFileSize |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// use the path listed on the emoji we pulled out of the database to retrieve the object from storage |  | ||||||
| 	emojiBytes, err := m.storage.RetrieveFileFrom(storagePath) |  | ||||||
| 	if err != nil { |  | ||||||
| 		l.Debugf("error retrieving emoji from storage: %s", err) |  | ||||||
| 		c.String(http.StatusNotFound, "404 page not found") |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// finally we can return with all the information we derived above |  | ||||||
| 	c.DataFromReader(http.StatusOK, int64(contentLength), contentType, bytes.NewReader(emojiBytes), map[string]string{}) |  | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -129,11 +129,11 @@ func (suite *ServeFileTestSuite) TestServeOriginalFileSuccessful() { | ||||||
| 		}, | 		}, | ||||||
| 		gin.Param{ | 		gin.Param{ | ||||||
| 			Key:   fileserver.MediaTypeKey, | 			Key:   fileserver.MediaTypeKey, | ||||||
| 			Value: media.MediaAttachment, | 			Value: string(media.Attachment), | ||||||
| 		}, | 		}, | ||||||
| 		gin.Param{ | 		gin.Param{ | ||||||
| 			Key:   fileserver.MediaSizeKey, | 			Key:   fileserver.MediaSizeKey, | ||||||
| 			Value: media.MediaOriginal, | 			Value: string(media.Original), | ||||||
| 		}, | 		}, | ||||||
| 		gin.Param{ | 		gin.Param{ | ||||||
| 			Key:   fileserver.FileNameKey, | 			Key:   fileserver.FileNameKey, | ||||||
|  |  | ||||||
							
								
								
									
										41
									
								
								internal/api/model/content.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								internal/api/model/content.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,41 @@ | ||||||
|  | /* | ||||||
|  |    GoToSocial | ||||||
|  |    Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org | ||||||
|  | 
 | ||||||
|  |    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 <http://www.gnu.org/licenses/>. | ||||||
|  | */ | ||||||
|  | 
 | ||||||
|  | package model | ||||||
|  | 
 | ||||||
|  | // Content wraps everything needed to serve a blob of content (some kind of media) through the API. | ||||||
|  | type Content struct { | ||||||
|  | 	// MIME content type | ||||||
|  | 	ContentType string | ||||||
|  | 	// ContentLength in bytes | ||||||
|  | 	ContentLength int64 | ||||||
|  | 	// Actual content blob | ||||||
|  | 	Content []byte | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // GetContentRequestForm describes a piece of content desired by the caller of the fileserver API. | ||||||
|  | type GetContentRequestForm struct { | ||||||
|  | 	// AccountID of the content owner | ||||||
|  | 	AccountID string | ||||||
|  | 	// MediaType of the content (should be convertible to a media.MediaType) | ||||||
|  | 	MediaType string | ||||||
|  | 	// MediaSize of the content (should be convertible to a media.MediaSize) | ||||||
|  | 	MediaSize string | ||||||
|  | 	// Filename of the content | ||||||
|  | 	FileName string | ||||||
|  | } | ||||||
|  | @ -72,7 +72,7 @@ var Run action.GTSAction = func(ctx context.Context, c *config.Config, log *logr | ||||||
| 	// build backend handlers | 	// build backend handlers | ||||||
| 	mediaHandler := media.New(c, dbService, storageBackend, log) | 	mediaHandler := media.New(c, dbService, storageBackend, log) | ||||||
| 	oauthServer := oauth.New(dbService, log) | 	oauthServer := oauth.New(dbService, log) | ||||||
| 	processor := message.NewProcessor(c, typeConverter, oauthServer, mediaHandler, dbService, log) | 	processor := message.NewProcessor(c, typeConverter, oauthServer, mediaHandler, storageBackend, dbService, log) | ||||||
| 	if err := processor.Start(); err != nil { | 	if err := processor.Start(); err != nil { | ||||||
| 		return fmt.Errorf("error starting processor: %s", err) | 		return fmt.Errorf("error starting processor: %s", err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | @ -58,6 +58,8 @@ type Emoji struct { | ||||||
| 	// MIME content type of the emoji image | 	// MIME content type of the emoji image | ||||||
| 	// Probably "image/png" | 	// Probably "image/png" | ||||||
| 	ImageContentType string `pg:",notnull"` | 	ImageContentType string `pg:",notnull"` | ||||||
|  | 	// MIME content type of the static version of the emoji image. | ||||||
|  | 	ImageStaticContentType string `pg:",notnull"` | ||||||
| 	// Size of the emoji image file in bytes, for serving purposes. | 	// Size of the emoji image file in bytes, for serving purposes. | ||||||
| 	ImageFileSize int `pg:",notnull"` | 	ImageFileSize int `pg:",notnull"` | ||||||
| 	// Size of the static version of the emoji image file in bytes, for serving purposes. | 	// Size of the static version of the emoji image file in bytes, for serving purposes. | ||||||
|  |  | ||||||
|  | @ -32,21 +32,28 @@ import ( | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/storage" | 	"github.com/superseriousbusiness/gotosocial/internal/storage" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | // MediaSize describes the *size* of a piece of media | ||||||
|  | type MediaSize string | ||||||
|  | 
 | ||||||
|  | // MediaType describes the *type* of a piece of media | ||||||
|  | type MediaType string | ||||||
|  | 
 | ||||||
| const ( | const ( | ||||||
| 	// MediaSmall is the key for small/thumbnail versions of media | 	// Small is the key for small/thumbnail versions of media | ||||||
| 	MediaSmall = "small" | 	Small MediaSize = "small" | ||||||
| 	// MediaOriginal is the key for original/fullsize versions of media and emoji | 	// Original is the key for original/fullsize versions of media and emoji | ||||||
| 	MediaOriginal = "original" | 	Original MediaSize = "original" | ||||||
| 	// MediaStatic is the key for static (non-animated) versions of emoji | 	// Static is the key for static (non-animated) versions of emoji | ||||||
| 	MediaStatic = "static" | 	Static MediaSize = "static" | ||||||
| 	// MediaAttachment is the key for media attachments | 
 | ||||||
| 	MediaAttachment = "attachment" | 	// Attachment is the key for media attachments | ||||||
| 	// MediaHeader is the key for profile header requests | 	Attachment MediaType = "attachment" | ||||||
| 	MediaHeader = "header" | 	// Header is the key for profile header requests | ||||||
| 	// MediaAvatar is the key for profile avatar requests | 	Header MediaType = "header" | ||||||
| 	MediaAvatar = "avatar" | 	// Avatar is the key for profile avatar requests | ||||||
| 	// MediaEmoji is the key for emoji type requests | 	Avatar MediaType = "avatar" | ||||||
| 	MediaEmoji = "emoji" | 	// Emoji is the key for emoji type requests | ||||||
|  | 	Emoji MediaType = "emoji" | ||||||
| 
 | 
 | ||||||
| 	// EmojiMaxBytes is the maximum permitted bytes of an emoji upload (50kb) | 	// EmojiMaxBytes is the maximum permitted bytes of an emoji upload (50kb) | ||||||
| 	EmojiMaxBytes = 51200 | 	EmojiMaxBytes = 51200 | ||||||
|  | @ -57,7 +64,7 @@ type Handler interface { | ||||||
| 	// ProcessHeaderOrAvatar takes a new header image for an account, checks it out, removes exif data from it, | 	// ProcessHeaderOrAvatar takes a new header image for an account, checks it out, removes exif data from it, | ||||||
| 	// puts it in whatever storage backend we're using, sets the relevant fields in the database for the new image, | 	// puts it in whatever storage backend we're using, sets the relevant fields in the database for the new image, | ||||||
| 	// and then returns information to the caller about the new header. | 	// and then returns information to the caller about the new header. | ||||||
| 	ProcessHeaderOrAvatar(img []byte, accountID string, headerOrAvi string) (*gtsmodel.MediaAttachment, error) | 	ProcessHeaderOrAvatar(img []byte, accountID string, mediaType MediaType) (*gtsmodel.MediaAttachment, error) | ||||||
| 
 | 
 | ||||||
| 	// ProcessLocalAttachment takes a new attachment and the requesting account, checks it out, removes exif data from it, | 	// ProcessLocalAttachment takes a new attachment and the requesting account, checks it out, removes exif data from it, | ||||||
| 	// puts it in whatever storage backend we're using, sets the relevant fields in the database for the new media, | 	// puts it in whatever storage backend we're using, sets the relevant fields in the database for the new media, | ||||||
|  | @ -94,10 +101,10 @@ func New(config *config.Config, database db.DB, storage storage.Storage, log *lo | ||||||
| // ProcessHeaderOrAvatar takes a new header image for an account, checks it out, removes exif data from it, | // ProcessHeaderOrAvatar takes a new header image for an account, checks it out, removes exif data from it, | ||||||
| // puts it in whatever storage backend we're using, sets the relevant fields in the database for the new image, | // puts it in whatever storage backend we're using, sets the relevant fields in the database for the new image, | ||||||
| // and then returns information to the caller about the new header. | // and then returns information to the caller about the new header. | ||||||
| func (mh *mediaHandler) ProcessHeaderOrAvatar(attachment []byte, accountID string, headerOrAvi string) (*gtsmodel.MediaAttachment, error) { | func (mh *mediaHandler) ProcessHeaderOrAvatar(attachment []byte, accountID string, mediaType MediaType) (*gtsmodel.MediaAttachment, error) { | ||||||
| 	l := mh.log.WithField("func", "SetHeaderForAccountID") | 	l := mh.log.WithField("func", "SetHeaderForAccountID") | ||||||
| 
 | 
 | ||||||
| 	if headerOrAvi != MediaHeader && headerOrAvi != MediaAvatar { | 	if mediaType != Header && mediaType != Avatar { | ||||||
| 		return nil, errors.New("header or avatar not selected") | 		return nil, errors.New("header or avatar not selected") | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -116,14 +123,14 @@ func (mh *mediaHandler) ProcessHeaderOrAvatar(attachment []byte, accountID strin | ||||||
| 	l.Tracef("read %d bytes of file", len(attachment)) | 	l.Tracef("read %d bytes of file", len(attachment)) | ||||||
| 
 | 
 | ||||||
| 	// process it | 	// process it | ||||||
| 	ma, err := mh.processHeaderOrAvi(attachment, contentType, headerOrAvi, accountID) | 	ma, err := mh.processHeaderOrAvi(attachment, contentType, mediaType, accountID) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, fmt.Errorf("error processing %s: %s", headerOrAvi, err) | 		return nil, fmt.Errorf("error processing %s: %s", mediaType, err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// set it in the database | 	// set it in the database | ||||||
| 	if err := mh.db.SetHeaderOrAvatarForAccountID(ma, accountID); err != nil { | 	if err := mh.db.SetHeaderOrAvatarForAccountID(ma, accountID); err != nil { | ||||||
| 		return nil, fmt.Errorf("error putting %s in database: %s", headerOrAvi, err) | 		return nil, fmt.Errorf("error putting %s in database: %s", mediaType, err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	return ma, nil | 	return ma, nil | ||||||
|  | @ -234,15 +241,15 @@ func (mh *mediaHandler) ProcessLocalEmoji(emojiBytes []byte, shortcode string) ( | ||||||
| 
 | 
 | ||||||
| 	// webfinger uri for the emoji -- unrelated to actually serving the image | 	// webfinger uri for the emoji -- unrelated to actually serving the image | ||||||
| 	// will be something like https://example.org/emoji/70a7f3d7-7e35-4098-8ce3-9b5e8203bb9c | 	// will be something like https://example.org/emoji/70a7f3d7-7e35-4098-8ce3-9b5e8203bb9c | ||||||
| 	emojiURI := fmt.Sprintf("%s://%s/%s/%s", mh.config.Protocol, mh.config.Host, MediaEmoji, newEmojiID) | 	emojiURI := fmt.Sprintf("%s://%s/%s/%s", mh.config.Protocol, mh.config.Host, Emoji, newEmojiID) | ||||||
| 
 | 
 | ||||||
| 	// serve url and storage path for the original emoji -- can be png or gif | 	// serve url and storage path for the original emoji -- can be png or gif | ||||||
| 	emojiURL := fmt.Sprintf("%s/%s/%s/%s/%s.%s", URLbase, instanceAccount.ID, MediaEmoji, MediaOriginal, newEmojiID, extension) | 	emojiURL := fmt.Sprintf("%s/%s/%s/%s/%s.%s", URLbase, instanceAccount.ID, Emoji, Original, newEmojiID, extension) | ||||||
| 	emojiPath := fmt.Sprintf("%s/%s/%s/%s/%s.%s", mh.config.StorageConfig.BasePath, instanceAccount.ID, MediaEmoji, MediaOriginal, newEmojiID, extension) | 	emojiPath := fmt.Sprintf("%s/%s/%s/%s/%s.%s", mh.config.StorageConfig.BasePath, instanceAccount.ID, Emoji, Original, newEmojiID, extension) | ||||||
| 
 | 
 | ||||||
| 	// serve url and storage path for the static version -- will always be png | 	// serve url and storage path for the static version -- will always be png | ||||||
| 	emojiStaticURL := fmt.Sprintf("%s/%s/%s/%s/%s.png", URLbase, instanceAccount.ID, MediaEmoji, MediaStatic, newEmojiID) | 	emojiStaticURL := fmt.Sprintf("%s/%s/%s/%s/%s.png", URLbase, instanceAccount.ID, Emoji, Static, newEmojiID) | ||||||
| 	emojiStaticPath := fmt.Sprintf("%s/%s/%s/%s/%s.png", mh.config.StorageConfig.BasePath, instanceAccount.ID, MediaEmoji, MediaStatic, newEmojiID) | 	emojiStaticPath := fmt.Sprintf("%s/%s/%s/%s/%s.png", mh.config.StorageConfig.BasePath, instanceAccount.ID, Emoji, Static, newEmojiID) | ||||||
| 
 | 
 | ||||||
| 	// store the original | 	// store the original | ||||||
| 	if err := mh.storage.StoreFileAt(emojiPath, original.image); err != nil { | 	if err := mh.storage.StoreFileAt(emojiPath, original.image); err != nil { | ||||||
|  | @ -268,6 +275,7 @@ func (mh *mediaHandler) ProcessLocalEmoji(emojiBytes []byte, shortcode string) ( | ||||||
| 		ImagePath:              emojiPath, | 		ImagePath:              emojiPath, | ||||||
| 		ImageStaticPath:        emojiStaticPath, | 		ImageStaticPath:        emojiStaticPath, | ||||||
| 		ImageContentType:       contentType, | 		ImageContentType:       contentType, | ||||||
|  | 		ImageStaticContentType: "image/png", // static version will always be a png | ||||||
| 		ImageFileSize:          len(original.image), | 		ImageFileSize:          len(original.image), | ||||||
| 		ImageStaticFileSize:    len(static.image), | 		ImageStaticFileSize:    len(static.image), | ||||||
| 		ImageUpdatedAt:         time.Now(), | 		ImageUpdatedAt:         time.Now(), | ||||||
|  | @ -326,13 +334,13 @@ func (mh *mediaHandler) processImageAttachment(data []byte, accountID string, co | ||||||
| 	smallURL := fmt.Sprintf("%s/%s/attachment/small/%s.jpeg", URLbase, accountID, newMediaID) // all thumbnails/smalls are encoded as jpeg | 	smallURL := fmt.Sprintf("%s/%s/attachment/small/%s.jpeg", URLbase, accountID, newMediaID) // all thumbnails/smalls are encoded as jpeg | ||||||
| 
 | 
 | ||||||
| 	// we store the original... | 	// we store the original... | ||||||
| 	originalPath := fmt.Sprintf("%s/%s/%s/%s/%s.%s", mh.config.StorageConfig.BasePath, accountID, MediaAttachment, MediaOriginal, newMediaID, extension) | 	originalPath := fmt.Sprintf("%s/%s/%s/%s/%s.%s", mh.config.StorageConfig.BasePath, accountID, Attachment, Original, newMediaID, extension) | ||||||
| 	if err := mh.storage.StoreFileAt(originalPath, original.image); err != nil { | 	if err := mh.storage.StoreFileAt(originalPath, original.image); err != nil { | ||||||
| 		return nil, fmt.Errorf("storage error: %s", err) | 		return nil, fmt.Errorf("storage error: %s", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// and a thumbnail... | 	// and a thumbnail... | ||||||
| 	smallPath := fmt.Sprintf("%s/%s/%s/%s/%s.jpeg", mh.config.StorageConfig.BasePath, accountID, MediaAttachment, MediaSmall, newMediaID) // all thumbnails/smalls are encoded as jpeg | 	smallPath := fmt.Sprintf("%s/%s/%s/%s/%s.jpeg", mh.config.StorageConfig.BasePath, accountID, Attachment, Small, newMediaID) // all thumbnails/smalls are encoded as jpeg | ||||||
| 	if err := mh.storage.StoreFileAt(smallPath, small.image); err != nil { | 	if err := mh.storage.StoreFileAt(smallPath, small.image); err != nil { | ||||||
| 		return nil, fmt.Errorf("storage error: %s", err) | 		return nil, fmt.Errorf("storage error: %s", err) | ||||||
| 	} | 	} | ||||||
|  | @ -386,14 +394,14 @@ func (mh *mediaHandler) processImageAttachment(data []byte, accountID string, co | ||||||
| 
 | 
 | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (mh *mediaHandler) processHeaderOrAvi(imageBytes []byte, contentType string, headerOrAvi string, accountID string) (*gtsmodel.MediaAttachment, error) { | func (mh *mediaHandler) processHeaderOrAvi(imageBytes []byte, contentType string, mediaType MediaType, accountID string) (*gtsmodel.MediaAttachment, error) { | ||||||
| 	var isHeader bool | 	var isHeader bool | ||||||
| 	var isAvatar bool | 	var isAvatar bool | ||||||
| 
 | 
 | ||||||
| 	switch headerOrAvi { | 	switch mediaType { | ||||||
| 	case MediaHeader: | 	case Header: | ||||||
| 		isHeader = true | 		isHeader = true | ||||||
| 	case MediaAvatar: | 	case Avatar: | ||||||
| 		isAvatar = true | 		isAvatar = true | ||||||
| 	default: | 	default: | ||||||
| 		return nil, errors.New("header or avatar not selected") | 		return nil, errors.New("header or avatar not selected") | ||||||
|  | @ -432,17 +440,17 @@ func (mh *mediaHandler) processHeaderOrAvi(imageBytes []byte, contentType string | ||||||
| 	newMediaID := uuid.NewString() | 	newMediaID := uuid.NewString() | ||||||
| 
 | 
 | ||||||
| 	URLbase := fmt.Sprintf("%s://%s%s", mh.config.StorageConfig.ServeProtocol, mh.config.StorageConfig.ServeHost, mh.config.StorageConfig.ServeBasePath) | 	URLbase := fmt.Sprintf("%s://%s%s", mh.config.StorageConfig.ServeProtocol, mh.config.StorageConfig.ServeHost, mh.config.StorageConfig.ServeBasePath) | ||||||
| 	originalURL := fmt.Sprintf("%s/%s/%s/original/%s.%s", URLbase, accountID, headerOrAvi, newMediaID, extension) | 	originalURL := fmt.Sprintf("%s/%s/%s/original/%s.%s", URLbase, accountID, mediaType, newMediaID, extension) | ||||||
| 	smallURL := fmt.Sprintf("%s/%s/%s/small/%s.%s", URLbase, accountID, headerOrAvi, newMediaID, extension) | 	smallURL := fmt.Sprintf("%s/%s/%s/small/%s.%s", URLbase, accountID, mediaType, newMediaID, extension) | ||||||
| 
 | 
 | ||||||
| 	// we store the original... | 	// we store the original... | ||||||
| 	originalPath := fmt.Sprintf("%s/%s/%s/%s/%s.%s", mh.config.StorageConfig.BasePath, accountID, headerOrAvi, MediaOriginal, newMediaID, extension) | 	originalPath := fmt.Sprintf("%s/%s/%s/%s/%s.%s", mh.config.StorageConfig.BasePath, accountID, mediaType, Original, newMediaID, extension) | ||||||
| 	if err := mh.storage.StoreFileAt(originalPath, original.image); err != nil { | 	if err := mh.storage.StoreFileAt(originalPath, original.image); err != nil { | ||||||
| 		return nil, fmt.Errorf("storage error: %s", err) | 		return nil, fmt.Errorf("storage error: %s", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// and a thumbnail... | 	// and a thumbnail... | ||||||
| 	smallPath := fmt.Sprintf("%s/%s/%s/%s/%s.%s", mh.config.StorageConfig.BasePath, accountID, headerOrAvi, MediaSmall, newMediaID, extension) | 	smallPath := fmt.Sprintf("%s/%s/%s/%s/%s.%s", mh.config.StorageConfig.BasePath, accountID, mediaType, Small, newMediaID, extension) | ||||||
| 	if err := mh.storage.StoreFileAt(smallPath, small.image); err != nil { | 	if err := mh.storage.StoreFileAt(smallPath, small.image); err != nil { | ||||||
| 		return nil, fmt.Errorf("storage error: %s", err) | 		return nil, fmt.Errorf("storage error: %s", err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | @ -285,3 +285,31 @@ type imageAndMeta struct { | ||||||
| 	aspect   float64 | 	aspect   float64 | ||||||
| 	blurhash string | 	blurhash string | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | // ParseMediaType converts s to a recognized MediaType, or returns an error if unrecognized | ||||||
|  | func ParseMediaType(s string) (MediaType, error) { | ||||||
|  | 	switch MediaType(s) { | ||||||
|  | 	case Attachment: | ||||||
|  | 		return Attachment, nil | ||||||
|  | 	case Header: | ||||||
|  | 		return Header, nil | ||||||
|  | 	case Avatar: | ||||||
|  | 		return Avatar, nil | ||||||
|  | 	case Emoji: | ||||||
|  | 		return Emoji, nil | ||||||
|  | 	} | ||||||
|  | 	return "", fmt.Errorf("%s not a recognized MediaType", s) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // ParseMediaSize converts s to a recognized MediaSize, or returns an error if unrecognized | ||||||
|  | func ParseMediaSize(s string) (MediaSize, error) { | ||||||
|  | 	switch MediaSize(s) { | ||||||
|  | 	case Small: | ||||||
|  | 		return Small, nil | ||||||
|  | 	case Original: | ||||||
|  | 		return Original, nil | ||||||
|  | 	case Static: | ||||||
|  | 		return Static, nil | ||||||
|  | 	} | ||||||
|  | 	return "", fmt.Errorf("%s not a recognized MediaSize", s) | ||||||
|  | } | ||||||
|  |  | ||||||
							
								
								
									
										106
									
								
								internal/message/error.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										106
									
								
								internal/message/error.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,106 @@ | ||||||
|  | package message | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"errors" | ||||||
|  | 	"net/http" | ||||||
|  | 	"strings" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // ErrorWithCode wraps an internal error with an http code, and a 'safe' version of | ||||||
|  | // the error that can be served to clients without revealing internal business logic. | ||||||
|  | // | ||||||
|  | // A typical use of this error would be to first log the Original error, then return | ||||||
|  | // the Safe error and the StatusCode to an API caller. | ||||||
|  | type ErrorWithCode interface { | ||||||
|  | 	// Error returns the original internal error for debugging within the GoToSocial logs. | ||||||
|  | 	// This should *NEVER* be returned to a client as it may contain sensitive information. | ||||||
|  | 	Error() string | ||||||
|  | 	// Safe returns the API-safe version of the error for serialization towards a client. | ||||||
|  | 	// There's not much point logging this internally because it won't contain much helpful information. | ||||||
|  | 	Safe() string | ||||||
|  | 	//  Code returns the status code for serving to a client. | ||||||
|  | 	Code() int | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type errorWithCode struct { | ||||||
|  | 	original error | ||||||
|  | 	safe     error | ||||||
|  | 	code     int | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (e errorWithCode) Error() string { | ||||||
|  | 	return e.original.Error() | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (e errorWithCode) Safe() string { | ||||||
|  | 	return e.safe.Error() | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (e errorWithCode) Code() int { | ||||||
|  | 	return e.code | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // NewErrorBadRequest returns an ErrorWithCode 400 with the given original error and optional help text. | ||||||
|  | func NewErrorBadRequest(original error, helpText ...string) ErrorWithCode { | ||||||
|  | 	safe := "bad request" | ||||||
|  | 	if helpText != nil { | ||||||
|  | 		safe = safe + ": " + strings.Join(helpText, ": ") | ||||||
|  | 	} | ||||||
|  | 	return errorWithCode{ | ||||||
|  | 		original: original, | ||||||
|  | 		safe:     errors.New(safe), | ||||||
|  | 		code:     http.StatusBadRequest, | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // NewErrorNotAuthorized returns an ErrorWithCode 401 with the given original error and optional help text. | ||||||
|  | func NewErrorNotAuthorized(original error, helpText ...string) ErrorWithCode { | ||||||
|  | 	safe := "not authorized" | ||||||
|  | 	if helpText != nil { | ||||||
|  | 		safe = safe + ": " + strings.Join(helpText, ": ") | ||||||
|  | 	} | ||||||
|  | 	return errorWithCode{ | ||||||
|  | 		original: original, | ||||||
|  | 		safe:     errors.New(safe), | ||||||
|  | 		code:     http.StatusUnauthorized, | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // NewErrorForbidden returns an ErrorWithCode 403 with the given original error and optional help text. | ||||||
|  | func NewErrorForbidden(original error, helpText ...string) ErrorWithCode { | ||||||
|  | 	safe := "forbidden" | ||||||
|  | 	if helpText != nil { | ||||||
|  | 		safe = safe + ": " + strings.Join(helpText, ": ") | ||||||
|  | 	} | ||||||
|  | 	return errorWithCode{ | ||||||
|  | 		original: original, | ||||||
|  | 		safe:     errors.New(safe), | ||||||
|  | 		code:     http.StatusForbidden, | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // NewErrorNotFound returns an ErrorWithCode 404 with the given original error and optional help text. | ||||||
|  | func NewErrorNotFound(original error, helpText ...string) ErrorWithCode { | ||||||
|  | 	safe := "404 not found" | ||||||
|  | 	if helpText != nil { | ||||||
|  | 		safe = safe + ": " + strings.Join(helpText, ": ") | ||||||
|  | 	} | ||||||
|  | 	return errorWithCode{ | ||||||
|  | 		original: original, | ||||||
|  | 		safe:     errors.New(safe), | ||||||
|  | 		code:     http.StatusNotFound, | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // NewErrorInternalError returns an ErrorWithCode 500 with the given original error and optional help text. | ||||||
|  | func NewErrorInternalError(original error, helpText ...string) ErrorWithCode { | ||||||
|  | 	safe := "internal server error" | ||||||
|  | 	if helpText != nil { | ||||||
|  | 		safe = safe + ": " + strings.Join(helpText, ": ") | ||||||
|  | 	} | ||||||
|  | 	return errorWithCode{ | ||||||
|  | 		original: original, | ||||||
|  | 		safe:     errors.New(safe), | ||||||
|  | 		code:     http.StatusInternalServerError, | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | @ -9,6 +9,8 @@ import ( | ||||||
| 	"strings" | 	"strings" | ||||||
| 
 | 
 | ||||||
| 	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" | 	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/media" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | @ -93,3 +95,92 @@ func (p *processor) MediaCreate(authed *oauth.Auth, form *apimodel.AttachmentReq | ||||||
| 
 | 
 | ||||||
| 	return &mastoAttachment, nil | 	return &mastoAttachment, nil | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | func (p *processor) MediaGet(authed *oauth.Auth, form *apimodel.GetContentRequestForm) (*apimodel.Content, error) { | ||||||
|  | 	// parse the form fields | ||||||
|  | 	mediaSize, err := media.ParseMediaSize(form.MediaSize) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, NewErrorNotFound(fmt.Errorf("media size %s not valid", form.MediaSize)) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	mediaType, err := media.ParseMediaType(form.MediaType) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, NewErrorNotFound(fmt.Errorf("media type %s not valid", form.MediaType)) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	spl := strings.Split(form.FileName, ".") | ||||||
|  | 	if len(spl) != 2 || spl[0] == "" || spl[1] == "" { | ||||||
|  | 		return nil, NewErrorNotFound(fmt.Errorf("file name %s not parseable", form.FileName)) | ||||||
|  | 	} | ||||||
|  | 	wantedMediaID := spl[0] | ||||||
|  | 
 | ||||||
|  | 	// get the account that owns the media and make sure it's not suspended | ||||||
|  | 	acct := >smodel.Account{} | ||||||
|  | 	if err := p.db.GetByID(form.AccountID, acct); err != nil { | ||||||
|  | 		return nil, NewErrorNotFound(fmt.Errorf("account with id %s could not be selected from the db: %s", form.AccountID, err)) | ||||||
|  | 	} | ||||||
|  | 	if !acct.SuspendedAt.IsZero() { | ||||||
|  | 		return nil, NewErrorNotFound(fmt.Errorf("account with id %s is suspended", form.AccountID)) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// make sure the requesting account and the media account don't block each other | ||||||
|  | 	if authed.Account != nil { | ||||||
|  | 		blocked, err := p.db.Blocked(authed.Account.ID, form.AccountID) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return nil, NewErrorNotFound(fmt.Errorf("block status could not be established between accounts %s and %s: %s", form.AccountID, authed.Account.ID, err)) | ||||||
|  | 		} | ||||||
|  | 		if blocked { | ||||||
|  | 			return nil, NewErrorNotFound(fmt.Errorf("block exists between accounts %s and %s: %s", form.AccountID, authed.Account.ID)) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	content := &apimodel.Content{} | ||||||
|  | 	var storagePath string | ||||||
|  | 	switch mediaType { | ||||||
|  | 	case media.Emoji: | ||||||
|  | 		e := >smodel.Emoji{} | ||||||
|  | 		if err := p.db.GetByID(wantedMediaID, e); err != nil { | ||||||
|  | 			return nil, NewErrorNotFound(fmt.Errorf("emoji %s could not be taken from the db: %s", wantedMediaID, err)) | ||||||
|  | 		} | ||||||
|  | 		if e.Disabled { | ||||||
|  | 			return nil, NewErrorNotFound(fmt.Errorf("emoji %s has been disabled", wantedMediaID)) | ||||||
|  | 		} | ||||||
|  | 		switch mediaSize { | ||||||
|  | 		case media.Original: | ||||||
|  | 			content.ContentType = e.ImageContentType | ||||||
|  | 			storagePath = e.ImagePath | ||||||
|  | 		case media.Static: | ||||||
|  | 			content.ContentType = e.ImageStaticContentType | ||||||
|  | 			storagePath = e.ImageStaticPath | ||||||
|  | 		default: | ||||||
|  | 			return nil, NewErrorNotFound(fmt.Errorf("media size %s not recognized for emoji", mediaSize)) | ||||||
|  | 		} | ||||||
|  | 	case media.Attachment: | ||||||
|  | 		a := >smodel.MediaAttachment{} | ||||||
|  | 		if err := p.db.GetByID(wantedMediaID, a); err != nil { | ||||||
|  | 			return nil, NewErrorNotFound(fmt.Errorf("attachment %s could not be taken from the db: %s", wantedMediaID, err)) | ||||||
|  | 		} | ||||||
|  | 		if a.AccountID != form.AccountID { | ||||||
|  | 			return nil, NewErrorNotFound(fmt.Errorf("attachment %s is not owned by %s", wantedMediaID, form.AccountID)) | ||||||
|  | 		} | ||||||
|  | 		switch mediaSize { | ||||||
|  | 		case media.Original: | ||||||
|  | 			content.ContentType = a.File.ContentType | ||||||
|  | 			storagePath = a.File.Path | ||||||
|  | 		case media.Small: | ||||||
|  | 			content.ContentType = a.Thumbnail.ContentType | ||||||
|  | 			storagePath = a.Thumbnail.Path | ||||||
|  | 		default: | ||||||
|  | 			return nil, NewErrorNotFound(fmt.Errorf("media size %s not recognized for attachment", mediaSize)) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	bytes, err := p.storage.RetrieveFileFrom(storagePath) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, NewErrorNotFound(fmt.Errorf("error retrieving from storage: %s", err)) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	content.ContentLength = int64(len(bytes)) | ||||||
|  | 	content.Content = bytes | ||||||
|  | 	return content, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -26,6 +26,7 @@ import ( | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/media" | 	"github.com/superseriousbusiness/gotosocial/internal/media" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/storage" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/typeutils" | 	"github.com/superseriousbusiness/gotosocial/internal/typeutils" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | @ -72,7 +73,7 @@ type Processor interface { | ||||||
| 
 | 
 | ||||||
| 	// MediaCreate handles the creation of a media attachment, using the given form. | 	// MediaCreate handles the creation of a media attachment, using the given form. | ||||||
| 	MediaCreate(authed *oauth.Auth, form *apimodel.AttachmentRequest) (*apimodel.Attachment, error) | 	MediaCreate(authed *oauth.Auth, form *apimodel.AttachmentRequest) (*apimodel.Attachment, error) | ||||||
| 
 | 	MediaGet(authed *oauth.Auth, form *apimodel.GetContentRequestForm) (*apimodel.Content, error) | ||||||
| 	// AdminEmojiCreate handles the creation of a new instance emoji by an admin, using the given form. | 	// AdminEmojiCreate handles the creation of a new instance emoji by an admin, using the given form. | ||||||
| 	AdminEmojiCreate(authed *oauth.Auth, form *apimodel.EmojiCreateRequest) (*apimodel.Emoji, error) | 	AdminEmojiCreate(authed *oauth.Auth, form *apimodel.EmojiCreateRequest) (*apimodel.Emoji, error) | ||||||
| 
 | 
 | ||||||
|  | @ -93,11 +94,12 @@ type processor struct { | ||||||
| 	tc           typeutils.TypeConverter | 	tc           typeutils.TypeConverter | ||||||
| 	oauthServer  oauth.Server | 	oauthServer  oauth.Server | ||||||
| 	mediaHandler media.Handler | 	mediaHandler media.Handler | ||||||
|  | 	storage      storage.Storage | ||||||
| 	db           db.DB | 	db           db.DB | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // NewProcessor returns a new Processor that uses the given federator and logger | // NewProcessor returns a new Processor that uses the given federator and logger | ||||||
| func NewProcessor(config *config.Config, tc typeutils.TypeConverter, oauthServer oauth.Server, mediaHandler media.Handler, db db.DB, log *logrus.Logger) Processor { | func NewProcessor(config *config.Config, tc typeutils.TypeConverter, oauthServer oauth.Server, mediaHandler media.Handler, storage storage.Storage, db db.DB, log *logrus.Logger) Processor { | ||||||
| 	return &processor{ | 	return &processor{ | ||||||
| 		toClientAPI:  make(chan ToClientAPI, 100), | 		toClientAPI:  make(chan ToClientAPI, 100), | ||||||
| 		toFederator:  make(chan ToFederator, 100), | 		toFederator:  make(chan ToFederator, 100), | ||||||
|  | @ -107,6 +109,7 @@ func NewProcessor(config *config.Config, tc typeutils.TypeConverter, oauthServer | ||||||
| 		tc:           tc, | 		tc:           tc, | ||||||
| 		oauthServer:  oauthServer, | 		oauthServer:  oauthServer, | ||||||
| 		mediaHandler: mediaHandler, | 		mediaHandler: mediaHandler, | ||||||
|  | 		storage:      storage, | ||||||
| 		db:           db, | 		db:           db, | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -263,7 +263,7 @@ func (p *processor) updateAccountAvatar(avatar *multipart.FileHeader, accountID | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// do the setting | 	// do the setting | ||||||
| 	avatarInfo, err := p.mediaHandler.ProcessHeaderOrAvatar(buf.Bytes(), accountID, media.MediaAvatar) | 	avatarInfo, err := p.mediaHandler.ProcessHeaderOrAvatar(buf.Bytes(), accountID, media.Avatar) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, fmt.Errorf("error processing avatar: %s", err) | 		return nil, fmt.Errorf("error processing avatar: %s", err) | ||||||
| 	} | 	} | ||||||
|  | @ -296,7 +296,7 @@ func (p *processor) updateAccountHeader(header *multipart.FileHeader, accountID | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// do the setting | 	// do the setting | ||||||
| 	headerInfo, err := p.mediaHandler.ProcessHeaderOrAvatar(buf.Bytes(), accountID, media.MediaHeader) | 	headerInfo, err := p.mediaHandler.ProcessHeaderOrAvatar(buf.Bytes(), accountID, media.Header) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, fmt.Errorf("error processing header: %s", err) | 		return nil, fmt.Errorf("error processing header: %s", err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | @ -26,5 +26,5 @@ import ( | ||||||
| 
 | 
 | ||||||
| // NewTestProcessor returns a Processor suitable for testing purposes | // NewTestProcessor returns a Processor suitable for testing purposes | ||||||
| func NewTestProcessor(db db.DB, storage storage.Storage) message.Processor { | func NewTestProcessor(db db.DB, storage storage.Storage) message.Processor { | ||||||
| 	return message.NewProcessor(NewTestConfig(), NewTestTypeConverter(db), NewTestOauthServer(db), NewTestMediaHandler(db, storage), db, NewTestLog()) | 	return message.NewProcessor(NewTestConfig(), NewTestTypeConverter(db), NewTestOauthServer(db), NewTestMediaHandler(db, storage), storage, db, NewTestLog()) | ||||||
| } | } | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue