From 1d89e6856690f439b202fafaf8240196328fa8df Mon Sep 17 00:00:00 2001 From: 2mal3 <56305732+2mal3@users.noreply.github.com> Date: Sun, 26 Oct 2025 11:17:39 +0100 Subject: [PATCH] chore(display/filePreview): support video and pdf --- display/pkg/file_preview.go | 84 +++++++++++++++++++++++++++++++++++++ display/pkg/image.go | 19 --------- display/pkg/main.go | 29 ------------- display/web/main.go | 18 ++++---- 4 files changed, 93 insertions(+), 57 deletions(-) create mode 100644 display/pkg/file_preview.go delete mode 100644 display/pkg/image.go diff --git a/display/pkg/file_preview.go b/display/pkg/file_preview.go new file mode 100644 index 0000000..4fe1b21 --- /dev/null +++ b/display/pkg/file_preview.go @@ -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 +} \ No newline at end of file diff --git a/display/pkg/image.go b/display/pkg/image.go deleted file mode 100644 index bd685ad..0000000 --- a/display/pkg/image.go +++ /dev/null @@ -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 -} diff --git a/display/pkg/main.go b/display/pkg/main.go index d19e013..e01ebe2 100644 --- a/display/pkg/main.go +++ b/display/pkg/main.go @@ -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 -} diff --git a/display/web/main.go b/display/web/main.go index 2e4b10e..289cc45 100644 --- a/display/web/main.go +++ b/display/web/main.go @@ -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