mirror of
				https://github.com/superseriousbusiness/gotosocial.git
				synced 2025-10-30 23:52:26 -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 ( | ||||
| 	"bytes" | ||||
| 	"net/http" | ||||
| 	"strings" | ||||
| 
 | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"github.com/sirupsen/logrus" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/media" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api/model" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||
| ) | ||||
| 
 | ||||
| // 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") | ||||
| 
 | ||||
| 	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: | ||||
| 	// "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. | ||||
|  | @ -73,171 +78,16 @@ func (m *FileServer) ServeFile(c *gin.Context) { | |||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	// Only serve media types that are defined in our internal media module | ||||
| 	switch mediaType { | ||||
| 	case media.MediaHeader, media.MediaAvatar, media.MediaAttachment: | ||||
| 		m.serveAttachment(c, accountID, mediaType, mediaSize, fileName) | ||||
| 		return | ||||
| 	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(), | ||||
| 	content, err := m.processor.MediaGet(authed, &model.GetContentRequestForm{ | ||||
| 		AccountID: accountID, | ||||
| 		MediaType: mediaType, | ||||
| 		MediaSize: mediaSize, | ||||
| 		FileName:  fileName, | ||||
| 	}) | ||||
| 
 | ||||
| 	// 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 { | ||||
| 		l.Debugf("error retrieving from storage: %s", err) | ||||
| 		c.String(http.StatusNotFound, "404 page not found") | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	l.Errorf("about to serve content length: %d attachment bytes is: %d", int64(contentLength), int64(len(attachmentBytes))) | ||||
| 
 | ||||
| 	// 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{}) | ||||
| 	c.DataFromReader(http.StatusOK, content.ContentLength, content.ContentType, bytes.NewReader(content.Content), nil) | ||||
| } | ||||
|  |  | |||
|  | @ -129,11 +129,11 @@ func (suite *ServeFileTestSuite) TestServeOriginalFileSuccessful() { | |||
| 		}, | ||||
| 		gin.Param{ | ||||
| 			Key:   fileserver.MediaTypeKey, | ||||
| 			Value: media.MediaAttachment, | ||||
| 			Value: string(media.Attachment), | ||||
| 		}, | ||||
| 		gin.Param{ | ||||
| 			Key:   fileserver.MediaSizeKey, | ||||
| 			Value: media.MediaOriginal, | ||||
| 			Value: string(media.Original), | ||||
| 		}, | ||||
| 		gin.Param{ | ||||
| 			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 | ||||
| 	mediaHandler := media.New(c, dbService, storageBackend, 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 { | ||||
| 		return fmt.Errorf("error starting processor: %s", err) | ||||
| 	} | ||||
|  |  | |||
|  | @ -58,6 +58,8 @@ type Emoji struct { | |||
| 	// MIME content type of the emoji image | ||||
| 	// Probably "image/png" | ||||
| 	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. | ||||
| 	ImageFileSize int `pg:",notnull"` | ||||
| 	// 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" | ||||
| ) | ||||
| 
 | ||||
| // 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 ( | ||||
| 	// MediaSmall is the key for small/thumbnail versions of media | ||||
| 	MediaSmall = "small" | ||||
| 	// MediaOriginal is the key for original/fullsize versions of media and emoji | ||||
| 	MediaOriginal = "original" | ||||
| 	// MediaStatic is the key for static (non-animated) versions of emoji | ||||
| 	MediaStatic = "static" | ||||
| 	// MediaAttachment is the key for media attachments | ||||
| 	MediaAttachment = "attachment" | ||||
| 	// MediaHeader is the key for profile header requests | ||||
| 	MediaHeader = "header" | ||||
| 	// MediaAvatar is the key for profile avatar requests | ||||
| 	MediaAvatar = "avatar" | ||||
| 	// MediaEmoji is the key for emoji type requests | ||||
| 	MediaEmoji = "emoji" | ||||
| 	// Small is the key for small/thumbnail versions of media | ||||
| 	Small MediaSize = "small" | ||||
| 	// Original is the key for original/fullsize versions of media and emoji | ||||
| 	Original MediaSize = "original" | ||||
| 	// Static is the key for static (non-animated) versions of emoji | ||||
| 	Static MediaSize = "static" | ||||
| 
 | ||||
| 	// Attachment is the key for media attachments | ||||
| 	Attachment MediaType = "attachment" | ||||
| 	// Header is the key for profile header requests | ||||
| 	Header MediaType = "header" | ||||
| 	// Avatar is the key for profile avatar requests | ||||
| 	Avatar MediaType = "avatar" | ||||
| 	// Emoji is the key for emoji type requests | ||||
| 	Emoji MediaType = "emoji" | ||||
| 
 | ||||
| 	// EmojiMaxBytes is the maximum permitted bytes of an emoji upload (50kb) | ||||
| 	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, | ||||
| 	// 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. | ||||
| 	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, | ||||
| 	// 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, | ||||
| // 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. | ||||
| 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") | ||||
| 
 | ||||
| 	if headerOrAvi != MediaHeader && headerOrAvi != MediaAvatar { | ||||
| 	if mediaType != Header && mediaType != Avatar { | ||||
| 		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)) | ||||
| 
 | ||||
| 	// process it | ||||
| 	ma, err := mh.processHeaderOrAvi(attachment, contentType, headerOrAvi, accountID) | ||||
| 	ma, err := mh.processHeaderOrAvi(attachment, contentType, mediaType, accountID) | ||||
| 	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 | ||||
| 	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 | ||||
|  | @ -234,15 +241,15 @@ func (mh *mediaHandler) ProcessLocalEmoji(emojiBytes []byte, shortcode string) ( | |||
| 
 | ||||
| 	// webfinger uri for the emoji -- unrelated to actually serving the image | ||||
| 	// 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 | ||||
| 	emojiURL := fmt.Sprintf("%s/%s/%s/%s/%s.%s", URLbase, instanceAccount.ID, MediaEmoji, MediaOriginal, newEmojiID, extension) | ||||
| 	emojiPath := fmt.Sprintf("%s/%s/%s/%s/%s.%s", mh.config.StorageConfig.BasePath, 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, Emoji, Original, newEmojiID, extension) | ||||
| 
 | ||||
| 	// 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) | ||||
| 	emojiStaticPath := fmt.Sprintf("%s/%s/%s/%s/%s.png", mh.config.StorageConfig.BasePath, 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, Emoji, Static, newEmojiID) | ||||
| 
 | ||||
| 	// store the original | ||||
| 	if err := mh.storage.StoreFileAt(emojiPath, original.image); err != nil { | ||||
|  | @ -268,6 +275,7 @@ func (mh *mediaHandler) ProcessLocalEmoji(emojiBytes []byte, shortcode string) ( | |||
| 		ImagePath:              emojiPath, | ||||
| 		ImageStaticPath:        emojiStaticPath, | ||||
| 		ImageContentType:       contentType, | ||||
| 		ImageStaticContentType: "image/png", // static version will always be a png | ||||
| 		ImageFileSize:          len(original.image), | ||||
| 		ImageStaticFileSize:    len(static.image), | ||||
| 		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 | ||||
| 
 | ||||
| 	// 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 { | ||||
| 		return nil, fmt.Errorf("storage error: %s", err) | ||||
| 	} | ||||
| 
 | ||||
| 	// 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 { | ||||
| 		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 isAvatar bool | ||||
| 
 | ||||
| 	switch headerOrAvi { | ||||
| 	case MediaHeader: | ||||
| 	switch mediaType { | ||||
| 	case Header: | ||||
| 		isHeader = true | ||||
| 	case MediaAvatar: | ||||
| 	case Avatar: | ||||
| 		isAvatar = true | ||||
| 	default: | ||||
| 		return nil, errors.New("header or avatar not selected") | ||||
|  | @ -432,17 +440,17 @@ func (mh *mediaHandler) processHeaderOrAvi(imageBytes []byte, contentType string | |||
| 	newMediaID := uuid.NewString() | ||||
| 
 | ||||
| 	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) | ||||
| 	smallURL := fmt.Sprintf("%s/%s/%s/small/%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, mediaType, newMediaID, extension) | ||||
| 
 | ||||
| 	// 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 { | ||||
| 		return nil, fmt.Errorf("storage error: %s", err) | ||||
| 	} | ||||
| 
 | ||||
| 	// 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 { | ||||
| 		return nil, fmt.Errorf("storage error: %s", err) | ||||
| 	} | ||||
|  |  | |||
|  | @ -285,3 +285,31 @@ type imageAndMeta struct { | |||
| 	aspect   float64 | ||||
| 	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" | ||||
| 
 | ||||
| 	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" | ||||
| ) | ||||
| 
 | ||||
|  | @ -93,3 +95,92 @@ func (p *processor) MediaCreate(authed *oauth.Auth, form *apimodel.AttachmentReq | |||
| 
 | ||||
| 	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/media" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/storage" | ||||
| 	"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(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(authed *oauth.Auth, form *apimodel.EmojiCreateRequest) (*apimodel.Emoji, error) | ||||
| 
 | ||||
|  | @ -93,11 +94,12 @@ type processor struct { | |||
| 	tc           typeutils.TypeConverter | ||||
| 	oauthServer  oauth.Server | ||||
| 	mediaHandler media.Handler | ||||
| 	storage      storage.Storage | ||||
| 	db           db.DB | ||||
| } | ||||
| 
 | ||||
| // 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{ | ||||
| 		toClientAPI:  make(chan ToClientAPI, 100), | ||||
| 		toFederator:  make(chan ToFederator, 100), | ||||
|  | @ -107,6 +109,7 @@ func NewProcessor(config *config.Config, tc typeutils.TypeConverter, oauthServer | |||
| 		tc:           tc, | ||||
| 		oauthServer:  oauthServer, | ||||
| 		mediaHandler: mediaHandler, | ||||
| 		storage:      storage, | ||||
| 		db:           db, | ||||
| 	} | ||||
| } | ||||
|  |  | |||
|  | @ -263,7 +263,7 @@ func (p *processor) updateAccountAvatar(avatar *multipart.FileHeader, accountID | |||
| 	} | ||||
| 
 | ||||
| 	// 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 { | ||||
| 		return nil, fmt.Errorf("error processing avatar: %s", err) | ||||
| 	} | ||||
|  | @ -296,7 +296,7 @@ func (p *processor) updateAccountHeader(header *multipart.FileHeader, accountID | |||
| 	} | ||||
| 
 | ||||
| 	// 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 { | ||||
| 		return nil, fmt.Errorf("error processing header: %s", err) | ||||
| 	} | ||||
|  |  | |||
|  | @ -26,5 +26,5 @@ import ( | |||
| 
 | ||||
| // NewTestProcessor returns a Processor suitable for testing purposes | ||||
| 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