Skip to content

Commit

Permalink
Revert "Remove the deprecated routes for PDF icons/thumbnails"
Browse files Browse the repository at this point in the history
This reverts commit 927a7f3.
  • Loading branch information
nono committed Mar 25, 2024
1 parent 35bef8d commit ab50d83
Show file tree
Hide file tree
Showing 7 changed files with 244 additions and 6 deletions.
12 changes: 12 additions & 0 deletions docs/files.md
Original file line number Diff line number Diff line change
Expand Up @@ -875,6 +875,18 @@ By default the `content-disposition` will be `inline`, but it will be
GET /files/download?Path=/Documents/hello.txt&Dl=1 HTTP/1.1
```

### GET /files/:file-id/icon/:secret

Get an image that shows the first page of a PDF in a small resolution (96x96).

**Note:** this route is deprecated, you should use thumbnails instead.

### GET /files/:file-id/preview/:secret

Get an image that shows the first page of a PDF (at most 1080x1920).

**Note:** this route is deprecated, you should use thumbnails instead.

### GET /files/:file-id/thumbnails/:secret/:format

Get a thumbnail of a file (for an image & pdf only). `:format` can be `tiny` (96x96)
Expand Down
6 changes: 0 additions & 6 deletions docs/important-changes.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,6 @@

This section will list important changes to the stack or its usage, and migration procedures if any is needed.

## Mars 2024: Routes for PDF

The deprecated routes for getting the icon or preview of a PDF file has been removed.

You should use the thumbnails instead.

## December 2023: Iterations for PBKDF2 increased

We have increased the number of PBKDF2 iterations for new users to 650_000, and removed the exception for Edge as it now supports PBKDF2 via the subtle crypto API.
Expand Down
178 changes: 178 additions & 0 deletions model/vfs/pdf.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
package vfs

import (
"bytes"
"fmt"
"net/http"
"os"
"os/exec"

"github.com/cozy/cozy-stack/pkg/config/config"
"github.com/cozy/cozy-stack/pkg/logger"
"github.com/cozy/cozy-stack/pkg/previewfs"
)

// ServePDFIcon will send the icon image for a PDF.
func ServePDFIcon(w http.ResponseWriter, req *http.Request, fs VFS, doc *FileDoc) error {
name := fmt.Sprintf("%s-icon.jpg", doc.ID())
modtime := doc.UpdatedAt
if doc.CozyMetadata != nil && doc.CozyMetadata.UploadedAt != nil {
modtime = *doc.CozyMetadata.UploadedAt
}
buf, err := icon(fs, doc)
if err != nil {
return err
}
http.ServeContent(w, req, name, modtime, bytes.NewReader(buf.Bytes()))
return nil
}

func icon(fs VFS, doc *FileDoc) (*bytes.Buffer, error) {
cache := previewfs.SystemCache()
if buf, err := cache.GetIcon(doc.MD5Sum); err == nil {
return buf, nil
}

buf, err := generateIcon(fs, doc)
if err != nil {
return nil, err
}
_ = cache.SetIcon(doc.MD5Sum, buf)
return buf, nil
}

func generateIcon(fs VFS, doc *FileDoc) (*bytes.Buffer, error) {
f, err := fs.OpenFile(doc)
if err != nil {
return nil, err
}
defer f.Close()

tempDir, err := os.MkdirTemp("", "magick")
if err != nil {
return nil, err
}
defer os.RemoveAll(tempDir)
envTempDir := fmt.Sprintf("MAGICK_TEMPORARY_PATH=%s", tempDir)
env := []string{envTempDir}

convertCmd := config.GetConfig().Jobs.ImageMagickConvertCmd
if convertCmd == "" {
convertCmd = "convert"
}
args := []string{
"-limit", "Memory", "1GB",
"-limit", "Map", "1GB",
"-[0]", // Takes the input from stdin
"-quality", "99", // At small resolution, we want a very good quality
"-interlace", "none", // Don't use progressive JPEGs, they are heavier
"-thumbnail", "96x96", // Makes a thumbnail that fits inside the given format
"-background", "white", // Use white for the background
"-alpha", "remove", // JPEGs don't have an alpha channel
"-colorspace", "sRGB", // Use the colorspace recommended for web, sRGB
"jpg:-", // Send the output on stdout, in JPEG format
}

var stdout, stderr bytes.Buffer
cmd := exec.Command(convertCmd, args...)
cmd.Env = env
cmd.Stdin = f
cmd.Stdout = &stdout
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
// Truncate very long messages
msg := stderr.String()
if len(msg) > 4000 {
msg = msg[:4000]
}
logger.WithNamespace("pdf_icon").
WithField("stderr", msg).
WithField("file_id", doc.ID()).
Errorf("imagemagick failed: %s", err)
return nil, err
}
return &stdout, nil
}

// ServePDFPreview will send the preview image for a PDF.
func ServePDFPreview(w http.ResponseWriter, req *http.Request, fs VFS, doc *FileDoc) error {
name := fmt.Sprintf("%s-preview.jpg", doc.ID())
modtime := doc.UpdatedAt
if doc.CozyMetadata != nil && doc.CozyMetadata.UploadedAt != nil {
modtime = *doc.CozyMetadata.UploadedAt
}
buf, err := preview(fs, doc)
if err != nil {
return err
}
http.ServeContent(w, req, name, modtime, bytes.NewReader(buf.Bytes()))
return nil
}

func preview(fs VFS, doc *FileDoc) (*bytes.Buffer, error) {
cache := previewfs.SystemCache()
if buf, err := cache.GetPreview(doc.MD5Sum); err == nil {
return buf, nil
}

buf, err := generatePreview(fs, doc)
if err != nil {
return nil, err
}
_ = cache.SetPreview(doc.MD5Sum, buf)
return buf, nil
}

func generatePreview(fs VFS, doc *FileDoc) (*bytes.Buffer, error) {
f, err := fs.OpenFile(doc)
if err != nil {
return nil, err
}
defer f.Close()

tempDir, err := os.MkdirTemp("", "magick")
if err != nil {
return nil, err
}
defer os.RemoveAll(tempDir)
envTempDir := fmt.Sprintf("MAGICK_TEMPORARY_PATH=%s", tempDir)
env := []string{envTempDir}

convertCmd := config.GetConfig().Jobs.ImageMagickConvertCmd
if convertCmd == "" {
convertCmd = "convert"
}
args := []string{
"-limit", "Memory", "2GB",
"-limit", "Map", "3GB",
"-density", "300", // We want a high resolution for PDFs
"-[0]", // Takes the input from stdin
"-quality", "82", // A good compromise between file size and quality
"-interlace", "none", // Don't use progressive JPEGs, they are heavier
"-thumbnail", "1080x1920>", // Makes a thumbnail that fits inside the given format
"-background", "white", // Use white for the background
"-alpha", "remove", // JPEGs don't have an alpha channel
"-colorspace", "sRGB", // Use the colorspace recommended for web, sRGB
"jpg:-", // Send the output on stdout, in JPEG format
}

var stdout, stderr bytes.Buffer
cmd := exec.Command(convertCmd, args...)
cmd.Env = env
cmd.Stdin = f
cmd.Stdout = &stdout
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
// Truncate very long messages
msg := stderr.String()
if len(msg) > 4000 {
msg = msg[:4000]
}
logger.WithNamespace("pdf_preview").
WithField("stderr", msg).
WithField("file_id", doc.ID()).
Errorf("imagemagick failed: %s", err)
return nil, err
}
return &stdout, nil
}
2 changes: 2 additions & 0 deletions pkg/jsonapi/data.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ type LinksList struct {
Small string `json:"small,omitempty"`
Medium string `json:"medium,omitempty"`
Large string `json:"large,omitempty"`
// Preview for PDF
Preview string `json:"preview,omitempty"`
}

// Relationship is a resource linkage, as described in JSON-API
Expand Down
44 changes: 44 additions & 0 deletions web/files/files.go
Original file line number Diff line number Diff line change
Expand Up @@ -977,6 +977,48 @@ func HeadDirOrFile(c echo.Context) error {
return nil
}

// IconHandler serves icon for the PDFs.
func IconHandler(c echo.Context) error {
instance := middlewares.GetInstance(c)

secret := c.Param("secret")
fileID, err := vfs.GetStore().GetThumb(instance, secret)
if err != nil {
return WrapVfsError(err)
}
if c.Param("file-id") != fileID {
return jsonapi.NewError(http.StatusBadRequest, "Wrong download token")
}

doc, err := instance.VFS().FileByID(fileID)
if err != nil {
return WrapVfsError(err)
}

return vfs.ServePDFIcon(c.Response(), c.Request(), instance.VFS(), doc)
}

// PreviewHandler serves preview images for the PDFs.
func PreviewHandler(c echo.Context) error {
instance := middlewares.GetInstance(c)

secret := c.Param("secret")
fileID, err := vfs.GetStore().GetThumb(instance, secret)
if err != nil {
return WrapVfsError(err)
}
if c.Param("file-id") != fileID {
return jsonapi.NewError(http.StatusBadRequest, "Wrong download token")
}

doc, err := instance.VFS().FileByID(fileID)
if err != nil {
return WrapVfsError(err)
}

return vfs.ServePDFPreview(c.Response(), c.Request(), instance.VFS(), doc)
}

// ThumbnailHandler serves thumbnails of the images/photos
func ThumbnailHandler(c echo.Context) error {
instance := middlewares.GetInstance(c)
Expand Down Expand Up @@ -1864,6 +1906,8 @@ func Routes(router *echo.Group) {
router.POST("/upload/metadata", UploadMetadataHandler)
router.POST("/:file-id/copy", FileCopyHandler)

router.GET("/:file-id/icon/:secret", IconHandler)
router.GET("/:file-id/preview/:secret", PreviewHandler)
router.GET("/:file-id/thumbnails/:secret/:format", ThumbnailHandler)

router.POST("/archive", ArchiveDownloadCreateHandler)
Expand Down
4 changes: 4 additions & 0 deletions web/files/files_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3614,6 +3614,10 @@ func TestFiles(t *testing.T) {
data.ValueEqual("id", parentID)
data.Value("attributes").Object().ValueEqual("size", "90")
})

t.Run("DeprecatePreviewAndIcon", func(t *testing.T) {
testutils.TODO(t, "2024-03-01", "Remove the deprecated preview and icon for PDF files")
})
}

func readFile(fs vfs.VFS, name string) ([]byte, error) {
Expand Down
4 changes: 4 additions & 0 deletions web/files/paginated.go
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,10 @@ func (f *file) Links() *jsonapi.LinksList {
links.Small = "/files/" + f.doc.DocID + "/thumbnails/" + f.thumbSecret + "/small"
links.Medium = "/files/" + f.doc.DocID + "/thumbnails/" + f.thumbSecret + "/medium"
links.Large = "/files/" + f.doc.DocID + "/thumbnails/" + f.thumbSecret + "/large"
if f.doc.Class == "pdf" {
links.Icon = "/files/" + f.doc.DocID + "/icon/" + f.thumbSecret
links.Preview = "/files/" + f.doc.DocID + "/preview/" + f.thumbSecret
}
}
}
return &links
Expand Down

0 comments on commit ab50d83

Please sign in to comment.