getfile.go (9724B)
1 // GoToSocial 2 // Copyright (C) GoToSocial Authors admin@gotosocial.org 3 // SPDX-License-Identifier: AGPL-3.0-or-later 4 // 5 // This program is free software: you can redistribute it and/or modify 6 // it under the terms of the GNU Affero General Public License as published by 7 // the Free Software Foundation, either version 3 of the License, or 8 // (at your option) any later version. 9 // 10 // This program is distributed in the hope that it will be useful, 11 // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 // GNU Affero General Public License for more details. 14 // 15 // You should have received a copy of the GNU Affero General Public License 16 // along with this program. If not, see <http://www.gnu.org/licenses/>. 17 18 package media 19 20 import ( 21 "context" 22 "fmt" 23 "io" 24 "net/url" 25 "strings" 26 27 apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" 28 "github.com/superseriousbusiness/gotosocial/internal/gtscontext" 29 "github.com/superseriousbusiness/gotosocial/internal/gtserror" 30 "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" 31 "github.com/superseriousbusiness/gotosocial/internal/media" 32 "github.com/superseriousbusiness/gotosocial/internal/uris" 33 ) 34 35 // GetFile retrieves a file from storage and streams it back to the caller via an io.reader embedded in *apimodel.Content. 36 func (p *Processor) GetFile(ctx context.Context, requestingAccount *gtsmodel.Account, form *apimodel.GetContentRequestForm) (*apimodel.Content, gtserror.WithCode) { 37 // parse the form fields 38 mediaSize, err := parseSize(form.MediaSize) 39 if err != nil { 40 return nil, gtserror.NewErrorNotFound(fmt.Errorf("media size %s not valid", form.MediaSize)) 41 } 42 43 mediaType, err := parseType(form.MediaType) 44 if err != nil { 45 return nil, gtserror.NewErrorNotFound(fmt.Errorf("media type %s not valid", form.MediaType)) 46 } 47 48 spl := strings.Split(form.FileName, ".") 49 if len(spl) != 2 || spl[0] == "" || spl[1] == "" { 50 return nil, gtserror.NewErrorNotFound(fmt.Errorf("file name %s not parseable", form.FileName)) 51 } 52 wantedMediaID := spl[0] 53 owningAccountID := form.AccountID 54 55 // get the account that owns the media and make sure it's not suspended 56 owningAccount, err := p.state.DB.GetAccountByID(ctx, owningAccountID) 57 if err != nil { 58 return nil, gtserror.NewErrorNotFound(fmt.Errorf("account with id %s could not be selected from the db: %s", owningAccountID, err)) 59 } 60 if !owningAccount.SuspendedAt.IsZero() { 61 return nil, gtserror.NewErrorNotFound(fmt.Errorf("account with id %s is suspended", owningAccountID)) 62 } 63 64 // make sure the requesting account and the media account don't block each other 65 if requestingAccount != nil { 66 blocked, err := p.state.DB.IsEitherBlocked(ctx, requestingAccount.ID, owningAccountID) 67 if err != nil { 68 return nil, gtserror.NewErrorNotFound(fmt.Errorf("block status could not be established between accounts %s and %s: %s", owningAccountID, requestingAccount.ID, err)) 69 } 70 if blocked { 71 return nil, gtserror.NewErrorNotFound(fmt.Errorf("block exists between accounts %s and %s", owningAccountID, requestingAccount.ID)) 72 } 73 } 74 75 // the way we store emojis is a little different from the way we store other attachments, 76 // so we need to take different steps depending on the media type being requested 77 switch mediaType { 78 case media.TypeEmoji: 79 return p.getEmojiContent(ctx, wantedMediaID, owningAccountID, mediaSize) 80 case media.TypeAttachment, media.TypeHeader, media.TypeAvatar: 81 return p.getAttachmentContent(ctx, requestingAccount, wantedMediaID, owningAccountID, mediaSize) 82 default: 83 return nil, gtserror.NewErrorNotFound(fmt.Errorf("media type %s not recognized", mediaType)) 84 } 85 } 86 87 /* 88 UTIL FUNCTIONS 89 */ 90 91 func parseType(s string) (media.Type, error) { 92 switch s { 93 case string(media.TypeAttachment): 94 return media.TypeAttachment, nil 95 case string(media.TypeHeader): 96 return media.TypeHeader, nil 97 case string(media.TypeAvatar): 98 return media.TypeAvatar, nil 99 case string(media.TypeEmoji): 100 return media.TypeEmoji, nil 101 } 102 return "", fmt.Errorf("%s not a recognized media.Type", s) 103 } 104 105 func parseSize(s string) (media.Size, error) { 106 switch s { 107 case string(media.SizeSmall): 108 return media.SizeSmall, nil 109 case string(media.SizeOriginal): 110 return media.SizeOriginal, nil 111 case string(media.SizeStatic): 112 return media.SizeStatic, nil 113 } 114 return "", fmt.Errorf("%s not a recognized media.Size", s) 115 } 116 117 func (p *Processor) getAttachmentContent(ctx context.Context, requestingAccount *gtsmodel.Account, wantedMediaID string, owningAccountID string, mediaSize media.Size) (*apimodel.Content, gtserror.WithCode) { 118 // retrieve attachment from the database and do basic checks on it 119 a, err := p.state.DB.GetAttachmentByID(ctx, wantedMediaID) 120 if err != nil { 121 return nil, gtserror.NewErrorNotFound(fmt.Errorf("attachment %s could not be taken from the db: %s", wantedMediaID, err)) 122 } 123 124 if a.AccountID != owningAccountID { 125 return nil, gtserror.NewErrorNotFound(fmt.Errorf("attachment %s is not owned by %s", wantedMediaID, owningAccountID)) 126 } 127 128 if !*a.Cached { 129 // if we don't have it cached, then we can assume two things: 130 // 1. this is remote media, since local media should never be uncached 131 // 2. we need to fetch it again using a transport and the media manager 132 remoteMediaIRI, err := url.Parse(a.RemoteURL) 133 if err != nil { 134 return nil, gtserror.NewErrorNotFound(fmt.Errorf("error parsing remote media iri %s: %s", a.RemoteURL, err)) 135 } 136 137 // use an empty string as requestingUsername to use the instance account, unless the request for this 138 // media has been http signed, then use the requesting account to make the request to remote server 139 var requestingUsername string 140 if requestingAccount != nil { 141 requestingUsername = requestingAccount.Username 142 } 143 144 // Pour one out for tobi's original streamed recache 145 // (streaming data both to the client and storage). 146 // Gone and forever missed <3 147 // 148 // [ 149 // the reason it was removed was because a slow 150 // client connection could hold open a storage 151 // recache operation -> holding open a media worker. 152 // ] 153 154 dataFn := func(innerCtx context.Context) (io.ReadCloser, int64, error) { 155 t, err := p.transportController.NewTransportForUsername(innerCtx, requestingUsername) 156 if err != nil { 157 return nil, 0, err 158 } 159 return t.DereferenceMedia(gtscontext.SetFastFail(innerCtx), remoteMediaIRI) 160 } 161 162 // Start recaching this media with the prepared data function. 163 processingMedia, err := p.mediaManager.PreProcessMediaRecache(ctx, dataFn, wantedMediaID) 164 if err != nil { 165 return nil, gtserror.NewErrorNotFound(fmt.Errorf("error recaching media: %s", err)) 166 } 167 168 // Load attachment and block until complete 169 a, err = processingMedia.LoadAttachment(ctx) 170 if err != nil { 171 return nil, gtserror.NewErrorNotFound(fmt.Errorf("error loading recached attachment: %s", err)) 172 } 173 } 174 175 var ( 176 storagePath string 177 attachmentContent = &apimodel.Content{ 178 ContentUpdated: a.UpdatedAt, 179 } 180 ) 181 182 // get file information from the attachment depending on the requested media size 183 switch mediaSize { 184 case media.SizeOriginal: 185 attachmentContent.ContentType = a.File.ContentType 186 attachmentContent.ContentLength = int64(a.File.FileSize) 187 storagePath = a.File.Path 188 case media.SizeSmall: 189 attachmentContent.ContentType = a.Thumbnail.ContentType 190 attachmentContent.ContentLength = int64(a.Thumbnail.FileSize) 191 storagePath = a.Thumbnail.Path 192 default: 193 return nil, gtserror.NewErrorNotFound(fmt.Errorf("media size %s not recognized for attachment", mediaSize)) 194 } 195 196 // ... so now we can safely return it 197 return p.retrieveFromStorage(ctx, storagePath, attachmentContent) 198 } 199 200 func (p *Processor) getEmojiContent(ctx context.Context, fileName string, owningAccountID string, emojiSize media.Size) (*apimodel.Content, gtserror.WithCode) { 201 emojiContent := &apimodel.Content{} 202 var storagePath string 203 204 // reconstruct the static emoji image url -- reason 205 // for using the static URL rather than full size url 206 // is that static emojis are always encoded as png, 207 // so this is more reliable than using full size url 208 imageStaticURL := uris.GenerateURIForAttachment(owningAccountID, string(media.TypeEmoji), string(media.SizeStatic), fileName, "png") 209 210 e, err := p.state.DB.GetEmojiByStaticURL(ctx, imageStaticURL) 211 if err != nil { 212 return nil, gtserror.NewErrorNotFound(fmt.Errorf("emoji %s could not be taken from the db: %s", fileName, err)) 213 } 214 215 if *e.Disabled { 216 return nil, gtserror.NewErrorNotFound(fmt.Errorf("emoji %s has been disabled", fileName)) 217 } 218 219 switch emojiSize { 220 case media.SizeOriginal: 221 emojiContent.ContentType = e.ImageContentType 222 emojiContent.ContentLength = int64(e.ImageFileSize) 223 storagePath = e.ImagePath 224 case media.SizeStatic: 225 emojiContent.ContentType = e.ImageStaticContentType 226 emojiContent.ContentLength = int64(e.ImageStaticFileSize) 227 storagePath = e.ImageStaticPath 228 default: 229 return nil, gtserror.NewErrorNotFound(fmt.Errorf("media size %s not recognized for emoji", emojiSize)) 230 } 231 232 return p.retrieveFromStorage(ctx, storagePath, emojiContent) 233 } 234 235 func (p *Processor) retrieveFromStorage(ctx context.Context, storagePath string, content *apimodel.Content) (*apimodel.Content, gtserror.WithCode) { 236 // If running on S3 storage with proxying disabled then 237 // just fetch a pre-signed URL instead of serving the content. 238 if url := p.state.Storage.URL(ctx, storagePath); url != nil { 239 content.URL = url 240 return content, nil 241 } 242 243 reader, err := p.state.Storage.GetStream(ctx, storagePath) 244 if err != nil { 245 return nil, gtserror.NewErrorNotFound(fmt.Errorf("error retrieving from storage: %s", err)) 246 } 247 248 content.Content = reader 249 return content, nil 250 }