processingemoji.go (9786B)
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 "bytes" 22 "context" 23 "fmt" 24 "io" 25 26 "codeberg.org/gruf/go-bytesize" 27 "codeberg.org/gruf/go-errors/v2" 28 "codeberg.org/gruf/go-runners" 29 "github.com/h2non/filetype" 30 "github.com/superseriousbusiness/gotosocial/internal/config" 31 "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" 32 "github.com/superseriousbusiness/gotosocial/internal/log" 33 "github.com/superseriousbusiness/gotosocial/internal/uris" 34 ) 35 36 // ProcessingEmoji represents an emoji currently processing. It exposes 37 // various functions for retrieving data from the process. 38 type ProcessingEmoji struct { 39 instAccID string // instance account ID 40 emoji *gtsmodel.Emoji // processing emoji details 41 refresh bool // whether this is an existing emoji being refreshed 42 newPathID string // new emoji path ID to use if refreshed 43 dataFn DataFunc // load-data function, returns media stream 44 done bool // done is set when process finishes with non ctx canceled type error 45 proc runners.Processor // proc helps synchronize only a singular running processing instance 46 err error // error stores permanent error value when done 47 mgr *Manager // mgr instance (access to db / storage) 48 } 49 50 // EmojiID returns the ID of the underlying emoji without blocking processing. 51 func (p *ProcessingEmoji) EmojiID() string { 52 return p.emoji.ID // immutable, safe outside mutex. 53 } 54 55 // LoadEmoji blocks until the static and fullsize image has been processed, and then returns the completed emoji. 56 func (p *ProcessingEmoji) LoadEmoji(ctx context.Context) (*gtsmodel.Emoji, error) { 57 // Attempt to load synchronously. 58 emoji, done, err := p.load(ctx) 59 60 if err == nil { 61 // No issue, return media. 62 return emoji, nil 63 } 64 65 if !done { 66 // Provided context was cancelled, e.g. request cancelled 67 // early. Queue this item for asynchronous processing. 68 log.Warnf(ctx, "reprocessing emoji %s after canceled ctx", p.emoji.ID) 69 go p.mgr.state.Workers.Media.Enqueue(p.Process) 70 } 71 72 return nil, err 73 } 74 75 // Process allows the receiving object to fit the runners.WorkerFunc signature. It performs a (blocking) load and logs on error. 76 func (p *ProcessingEmoji) Process(ctx context.Context) { 77 if _, _, err := p.load(ctx); err != nil { 78 log.Errorf(ctx, "error processing emoji: %v", err) 79 } 80 } 81 82 // load performs a concurrency-safe load of ProcessingEmoji, only marking itself as complete when returned error is NOT a context cancel. 83 func (p *ProcessingEmoji) load(ctx context.Context) (*gtsmodel.Emoji, bool, error) { 84 var ( 85 done bool 86 err error 87 ) 88 89 err = p.proc.Process(func() error { 90 if p.done { 91 // Already proc'd. 92 return p.err 93 } 94 95 defer func() { 96 // This is only done when ctx NOT cancelled. 97 done = err == nil || !errors.Comparable(err, 98 context.Canceled, 99 context.DeadlineExceeded, 100 ) 101 102 if !done { 103 return 104 } 105 106 // Store final values. 107 p.done = true 108 p.err = err 109 }() 110 111 // Attempt to store media and calculate 112 // full-size media attachment details. 113 if err = p.store(ctx); err != nil { 114 return err 115 } 116 117 // Finish processing by reloading media into 118 // memory to get dimension and generate a thumb. 119 if err = p.finish(ctx); err != nil { 120 return err 121 } 122 123 if p.refresh { 124 columns := []string{ 125 "image_remote_url", 126 "image_static_remote_url", 127 "image_url", 128 "image_static_url", 129 "image_path", 130 "image_static_path", 131 "image_content_type", 132 "image_file_size", 133 "image_static_file_size", 134 "image_updated_at", 135 "shortcode", 136 "uri", 137 } 138 139 // Existing emoji we're refreshing, so only need to update. 140 _, err = p.mgr.state.DB.UpdateEmoji(ctx, p.emoji, columns...) 141 return err 142 } 143 144 // New emoji media, first time caching. 145 err = p.mgr.state.DB.PutEmoji(ctx, p.emoji) 146 return err 147 }) 148 149 if err != nil { 150 return nil, done, err 151 } 152 153 return p.emoji, done, nil 154 } 155 156 // store calls the data function attached to p if it hasn't been called yet, 157 // and updates the underlying attachment fields as necessary. It will then stream 158 // bytes from p's reader directly into storage so that it can be retrieved later. 159 func (p *ProcessingEmoji) store(ctx context.Context) error { 160 // Load media from provided data fn. 161 rc, sz, err := p.dataFn(ctx) 162 if err != nil { 163 return fmt.Errorf("error executing data function: %w", err) 164 } 165 166 defer func() { 167 // Ensure data reader gets closed on return. 168 if err := rc.Close(); err != nil { 169 log.Errorf(ctx, "error closing data reader: %v", err) 170 } 171 }() 172 173 // Byte buffer to read file header into. 174 // See: https://en.wikipedia.org/wiki/File_format#File_header 175 // and https://github.com/h2non/filetype 176 hdrBuf := make([]byte, 261) 177 178 // Read the first 261 header bytes into buffer. 179 if _, err := io.ReadFull(rc, hdrBuf); err != nil { 180 return fmt.Errorf("error reading incoming media: %w", err) 181 } 182 183 // Parse file type info from header buffer. 184 info, err := filetype.Match(hdrBuf) 185 if err != nil { 186 return fmt.Errorf("error parsing file type: %w", err) 187 } 188 189 switch info.Extension { 190 // only supported emoji types 191 case "gif", "png": 192 193 // unhandled 194 default: 195 return fmt.Errorf("unsupported emoji filetype: %s", info.Extension) 196 } 197 198 // Recombine header bytes with remaining stream 199 r := io.MultiReader(bytes.NewReader(hdrBuf), rc) 200 201 var maxSize bytesize.Size 202 203 if p.emoji.Domain == "" { 204 // this is a local emoji upload 205 maxSize = config.GetMediaEmojiLocalMaxSize() 206 } else { 207 // this is a remote incoming emoji 208 maxSize = config.GetMediaEmojiRemoteMaxSize() 209 } 210 211 // Check that provided size isn't beyond max. We check beforehand 212 // so that we don't attempt to stream the emoji into storage if not needed. 213 if size := bytesize.Size(sz); sz > 0 && size > maxSize { 214 return fmt.Errorf("given emoji size %s greater than max allowed %s", size, maxSize) 215 } 216 217 var pathID string 218 219 if p.refresh { 220 // This is a refreshed emoji with a new 221 // path ID that this will be stored under. 222 pathID = p.newPathID 223 } else { 224 // This is a new emoji, simply use provided ID. 225 pathID = p.emoji.ID 226 } 227 228 // Calculate emoji file path. 229 p.emoji.ImagePath = fmt.Sprintf( 230 "%s/%s/%s/%s.%s", 231 p.instAccID, 232 TypeEmoji, 233 SizeOriginal, 234 pathID, 235 info.Extension, 236 ) 237 238 // This shouldn't already exist, but we do a check as it's worth logging. 239 if have, _ := p.mgr.state.Storage.Has(ctx, p.emoji.ImagePath); have { 240 log.Warnf(ctx, "emoji already exists at storage path: %s", p.emoji.ImagePath) 241 242 // Attempt to remove existing emoji at storage path (might be broken / out-of-date) 243 if err := p.mgr.state.Storage.Delete(ctx, p.emoji.ImagePath); err != nil { 244 return fmt.Errorf("error removing emoji from storage: %v", err) 245 } 246 } 247 248 // Write the final image reader stream to our storage. 249 sz, err = p.mgr.state.Storage.PutStream(ctx, p.emoji.ImagePath, r) 250 if err != nil { 251 return fmt.Errorf("error writing emoji to storage: %w", err) 252 } 253 254 // Once again check size in case none was provided previously. 255 if size := bytesize.Size(sz); size > maxSize { 256 257 if err := p.mgr.state.Storage.Delete(ctx, p.emoji.ImagePath); err != nil { 258 log.Errorf(ctx, "error removing too-large-emoji from storage: %v", err) 259 } 260 return fmt.Errorf("calculated emoji size %s greater than max allowed %s", size, maxSize) 261 } 262 263 // Fill in remaining attachment data now it's stored. 264 p.emoji.ImageURL = uris.GenerateURIForAttachment( 265 p.instAccID, 266 string(TypeEmoji), 267 string(SizeOriginal), 268 pathID, 269 info.Extension, 270 ) 271 p.emoji.ImageContentType = info.MIME.Value 272 p.emoji.ImageFileSize = int(sz) 273 274 return nil 275 } 276 277 func (p *ProcessingEmoji) finish(ctx context.Context) error { 278 // Fetch a stream to the original file in storage. 279 rc, err := p.mgr.state.Storage.GetStream(ctx, p.emoji.ImagePath) 280 if err != nil { 281 return fmt.Errorf("error loading file from storage: %w", err) 282 } 283 defer rc.Close() 284 285 // Decode the image from storage. 286 staticImg, err := decodeImage(rc) 287 if err != nil { 288 return fmt.Errorf("error decoding image: %w", err) 289 } 290 291 // The image should be in-memory by now. 292 if err := rc.Close(); err != nil { 293 return fmt.Errorf("error closing file: %w", err) 294 } 295 296 // This shouldn't already exist, but we do a check as it's worth logging. 297 if have, _ := p.mgr.state.Storage.Has(ctx, p.emoji.ImageStaticPath); have { 298 log.Warnf(ctx, "static emoji already exists at storage path: %s", p.emoji.ImagePath) 299 // Attempt to remove static existing emoji at storage path (might be broken / out-of-date) 300 if err := p.mgr.state.Storage.Delete(ctx, p.emoji.ImageStaticPath); err != nil { 301 return fmt.Errorf("error removing static emoji from storage: %v", err) 302 } 303 } 304 305 // Create an emoji PNG encoder stream. 306 enc := staticImg.ToPNG() 307 308 // Stream-encode the PNG static image into storage. 309 sz, err := p.mgr.state.Storage.PutStream(ctx, p.emoji.ImageStaticPath, enc) 310 if err != nil { 311 return fmt.Errorf("error stream-encoding static emoji to storage: %w", err) 312 } 313 314 // Set written image size. 315 p.emoji.ImageStaticFileSize = int(sz) 316 317 return nil 318 }