chore(display/filePreview): support video and pdf

This commit is contained in:
2025-10-26 11:17:39 +01:00
parent 7b0631d6c4
commit 1d89e68566
4 changed files with 93 additions and 57 deletions
+84
View File
@@ -0,0 +1,84 @@
package pkg
import (
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
)
var ErrFileTypePreviewNotSupported = errors.New("file type not supported for preview");
var ErrFilePreviewToolsMissing = errors.New("required tools for file preview are missing");
func GenerateFilePreview(inputPath string) (string, error) {
var err error
ext := strings.ToLower(filepath.Ext(inputPath))
tempFilePath := filepath.Join(os.TempDir(), fmt.Sprintf("preview_%d.webp", time.Now().Unix()))
switch ext {
case ".png", ".jpg", ".jpeg", ".bmp", ".gif", ".tiff", ".webp":
err = generateImagePreview(inputPath, tempFilePath)
case ".pdf":
err = generatePDFPreview(inputPath, tempFilePath)
case ".mp4", ".mov":
err = generateVideoPreview(inputPath, tempFilePath)
default:
return "", ErrFileTypePreviewNotSupported
}
if err != nil {
return "", err
}
return tempFilePath, nil
}
func generateImagePreview(inputPath string, outputPath string) error {
cmd := exec.Command("magick", inputPath, "-thumbnail", "100x100", "-quality", "50", outputPath)
result := RunShellCommand(cmd)
if result.ExitCode != 0 {
if result.ExitCode == 127 {
return ErrFilePreviewToolsMissing
}
return errors.New(result.Stderr)
}
return nil
}
func generatePDFPreview(inputPath string, outputPath string) error {
testCmd := exec.Command("which", "gs")
if result := RunShellCommand(testCmd); result.ExitCode != 0 {
return ErrFilePreviewToolsMissing
}
cmd := exec.Command("magick", fmt.Sprintf("%s[0]", inputPath), "-thumbnail", "100x100", "-quality", "50", outputPath)
result := RunShellCommand(cmd)
if result.ExitCode != 0 {
if result.ExitCode == 127 {
return ErrFilePreviewToolsMissing
}
return errors.New(result.Stderr)
}
return nil
}
func generateVideoPreview(inputPath string, outputPath string) error {
tempFilePath := filepath.Join(os.TempDir(), fmt.Sprintf("preview_temp_%d.webp", time.Now().Unix()))
ffmpegCmd := exec.Command("ffmpeg", "-i", inputPath, "-ss", "00:00:01.000", "-vframes", "1", tempFilePath)
result := RunShellCommand(ffmpegCmd)
if result.ExitCode != 0 {
if result.ExitCode == 127 {
return ErrFilePreviewToolsMissing
}
return errors.New(result.Stderr)
}
err := generateImagePreview(tempFilePath, outputPath)
if err != nil {
return err
}
return nil
}
-19
View File
@@ -1,19 +0,0 @@
package pkg
import "image"
func ResizeImage(img image.Image, newWidth, newHeight int) image.Image {
srcBounds := img.Bounds()
srcWidth := srcBounds.Dx()
srcHeight := srcBounds.Dy()
dst := image.NewRGBA(image.Rect(0, 0, newWidth, newHeight))
for y := range newHeight {
for x := range newWidth {
srcX := x * srcWidth / newWidth
srcY := y * srcHeight / newHeight
c := img.At(srcBounds.Min.X+srcX, srcBounds.Min.Y+srcY)
dst.Set(x, y, c)
}
}
return dst
}
-29
View File
@@ -4,9 +4,6 @@ import (
"bytes"
"errors"
"fmt"
"image"
"image/jpeg"
"image/png"
"log/slog"
"net"
"os"
@@ -182,29 +179,3 @@ func ResolveStorageFilePath(pathParam string) (string, bool, error) {
return fullPath, true, nil
}
func PreviewFile(fullPath string) (image.Image, error) {
var err error
f, err := os.Open(fullPath)
if err != nil {
return nil, err
}
defer f.Close()
ext := strings.ToLower(filepath.Ext(fullPath))
var img image.Image
switch ext {
case ".jpg", ".jpeg":
img, err = jpeg.Decode(f)
case ".png":
img, err = png.Decode(f)
default:
return nil, fmt.Errorf("unsupported file type for preview: %s", ext)
}
if err != nil {
return nil, err
}
preview := resizeImage(img, 200, 200)
return preview, nil
}
+9 -9
View File
@@ -3,8 +3,8 @@ package web
import (
"bytes"
"context"
"errors"
"fmt"
"image/png"
"io"
"log/slog"
"net/http"
@@ -325,19 +325,19 @@ func previewRoute(ctx echo.Context) error {
return ctx.JSON(http.StatusNotFound, ErrorResponse{Error: "File not found"})
}
resized, err := pkg.PreviewFile(fullPath)
outputFilePath, err := pkg.GenerateFilePreview(fullPath)
if err != nil {
slog.Error("Failed to generate preview", "file", fullPath, "error", err)
if errors.Is(err, pkg.ErrFileTypePreviewNotSupported) {
return ctx.JSON(http.StatusBadRequest, ErrorResponse{Error: "File type not supported for preview"})
}
if errors.Is(err, pkg.ErrFilePreviewToolsMissing) {
return ctx.JSON(http.StatusInternalServerError, ErrorResponse{Error: "Required tools for file preview are missing"})
}
return ctx.JSON(http.StatusInternalServerError, ErrorResponse{Error: "Failed to generate preview"})
}
var buf bytes.Buffer
if err := png.Encode(&buf, resized); err != nil {
slog.Error("Failed to encode image", "error", err)
return ctx.JSON(http.StatusInternalServerError, ErrorResponse{Error: "Failed to encode image"})
}
return ctx.Blob(http.StatusOK, "image/png", buf.Bytes())
return ctx.File(outputFilePath)
}
// Reset previous file views so they dont collide with the new one