package contented import ( "context" "errors" "fmt" "io" "log" "net/http" "os" "path/filepath" "code.ivysaur.me/thumbnail" ) func getThumbnailerConfig(t byte) (*thumbnail.Config, error) { // Modelled on what imgur.com offers // @ref https://api.imgur.com/models/image#thumbs opts := thumbnail.Config{ Aspect: thumbnail.FitOutside, Output: thumbnail.Jpeg, Scale: thumbnail.Bicubic, } switch t { case 's': opts.Width = 90 opts.Height = 90 case 'b': opts.Width = 160 opts.Height = 160 case 't': opts.Width = 160 opts.Height = 160 // thumbnail.ASPECT_RESPECT_MAX_DIMENSION_ONLY case 'm': opts.Width = 340 opts.Height = 340 // thumbnail.ASPECT_RESPECT_MAX_DIMENSION_ONLY case 'l': opts.Width = 640 opts.Height = 640 // thumbnail.ASPECT_RESPECT_MAX_DIMENSION_ONLY case 'h': opts.Width = 1024 opts.Height = 1024 // thumbnail.ASPECT_RESPECT_MAX_DIMENSION_ONLY default: return nil, errors.New("Unsupported thumbnail type (should be s/b/t/m/l/h)") } return &opts, nil } func (this *Server) handleThumb(w http.ResponseWriter, r *http.Request, thumbnailType byte, fileId string) { ctx := r.Context() opts, err := getThumbnailerConfig(thumbnailType) if err != nil { log.Printf("%s Thumbnail failed: %s\n", this.remoteIP(r), err.Error()) http.Error(w, err.Error(), 400) return } // Only a limited number of thumbnails can be generated concurrently select { case <-this.thumbnailSem: case <-r.Context().Done(): http.Error(w, r.Context().Err().Error(), 400) // probably won't be delivered anyway return } defer func() { this.thumbnailSem <- struct{}{} }() if ctx.Err() != nil { // The request was already cancelled return } t := thumbnail.NewThumbnailerEx(opts) err = this.handleThumbInternal(ctx, w, t, fileId) if err != nil { log.Printf("%s Thumbnail failed: %s\n", this.remoteIP(r), err.Error()) w.Header().Set(`Location`, fmt.Sprintf(`/nothumb_%d.png`, opts.Height)) w.WriteHeader(302) } } func (this *Server) handleThumbInternal(ctx context.Context, w http.ResponseWriter, t thumbnail.Thumbnailer, fileId string) error { // Load metadata m, err := this.Metadata(fileId) if err != nil { return err } if m.FileSize > this.opts.MaxThumbSizeBytes { return errors.New("Don't want to thumbnail very large files, sorry") } var filePath string if this.opts.StorageType == STORAGE_LOCAL { filePath = filepath.Join(this.opts.DataDirectory, m.FileHash) } else if this.opts.StorageType == STORAGE_S3 { // Need to temporarily download it for thumbnailing (slow and costs money) destFh, err := os.CreateTemp("", "contented-thumbcache-*") defer os.Remove(destFh.Name()) srcFh, err := this.ReadFile(ctx, m.FileHash) if err != nil { return err } _, err = io.CopyN(destFh, srcFh, m.FileSize) srcFh.Close() if err != nil { return err } destFh.Seek(0, io.SeekStart) filePath = destFh.Name() } else { panic("bad StorageType") } thumb, err := t.RenderFileAs(filePath, m.MimeType) if err != nil { return err } w.Header().Set(`Cache-Control`, `max-age=31536000, immutable`) w.Header().Set(`Content-Length`, fmt.Sprintf("%d", len(thumb))) w.Header().Set(`Content-Type`, `image/jpeg`) w.WriteHeader(200) w.Write(thumb) return nil }