filebrowser/backend/http/preview.go

171 lines
5.2 KiB
Go
Raw Permalink Normal View History

package http
import (
2020-07-27 17:01:02 +00:00
"bytes"
2020-07-23 00:41:19 +00:00
"context"
"fmt"
2020-07-23 00:41:19 +00:00
"io"
"net/http"
2024-08-24 22:02:33 +00:00
"os"
2024-11-21 00:15:30 +00:00
"path/filepath"
2024-12-17 00:01:55 +00:00
"strings"
2025-01-05 19:05:33 +00:00
"time"
2024-12-17 00:01:55 +00:00
"github.com/gtsteffaniak/filebrowser/backend/files"
"github.com/gtsteffaniak/filebrowser/backend/img"
2025-01-21 14:02:43 +00:00
"github.com/gtsteffaniak/filebrowser/backend/logger"
)
2020-07-23 00:41:19 +00:00
type ImgService interface {
FormatFromExtension(ext string) (img.Format, error)
2020-07-24 18:08:26 +00:00
Resize(ctx context.Context, in io.Reader, width, height int, out io.Writer, options ...img.Option) error
2020-07-23 00:41:19 +00:00
}
2020-07-27 17:01:02 +00:00
type FileCache interface {
Store(ctx context.Context, key string, value []byte) error
Load(ctx context.Context, key string) ([]byte, bool, error)
Delete(ctx context.Context, key string) error
2020-07-27 17:01:02 +00:00
}
2024-11-21 00:15:30 +00:00
// previewHandler handles the preview request for images.
// @Summary Get image preview
// @Description Returns a preview image based on the requested path and size.
// @Tags Resources
// @Accept json
// @Produce json
// @Param path query string true "File path of the image to preview"
// @Param size query string false "Preview size ('small' or 'large'). Default is based on server config."
// @Success 200 {file} file "Preview image content"
// @Failure 202 {object} map[string]string "Download permissions required"
// @Failure 400 {object} map[string]string "Invalid request path"
// @Failure 404 {object} map[string]string "File not found"
// @Failure 415 {object} map[string]string "Unsupported file type for preview"
// @Failure 500 {object} map[string]string "Internal server error"
// @Router /api/preview [get]
func previewHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
path := r.URL.Query().Get("path")
2025-01-05 19:05:33 +00:00
source := r.URL.Query().Get("source")
2024-11-21 00:15:30 +00:00
previewSize := r.URL.Query().Get("size")
if previewSize != "small" {
previewSize = "large"
}
if path == "" {
return http.StatusBadRequest, fmt.Errorf("invalid request path")
}
2024-11-26 17:21:41 +00:00
response, err := files.FileInfoFaster(files.FileOptions{
2025-01-27 00:21:12 +00:00
Path: filepath.Join(d.user.Scope, path),
Modify: d.user.Perm.Modify,
Source: source,
Expand: true,
Checker: d.user,
2020-07-23 00:41:19 +00:00
})
2024-11-26 17:21:41 +00:00
fileInfo := response.FileInfo
2024-11-21 00:15:30 +00:00
if err != nil {
return errToStatus(err), err
}
2024-11-26 17:21:41 +00:00
if fileInfo.Type == "directory" {
2024-11-21 00:15:30 +00:00
return http.StatusBadRequest, fmt.Errorf("can't create preview for directory")
}
2024-12-02 17:14:50 +00:00
setContentDisposition(w, r, fileInfo.Name)
2024-12-17 00:01:55 +00:00
if !strings.HasPrefix(fileInfo.Type, "image") {
2024-11-26 17:21:41 +00:00
return http.StatusNotImplemented, fmt.Errorf("can't create preview for %s type", fileInfo.Type)
2024-11-21 00:15:30 +00:00
}
2024-11-21 00:15:30 +00:00
if (previewSize == "large" && !config.Server.ResizePreview) ||
(previewSize == "small" && !config.Server.EnableThumbnails) {
if !d.user.Perm.Download {
return http.StatusAccepted, nil
}
2024-11-26 17:21:41 +00:00
return rawFileHandler(w, r, fileInfo)
}
2024-11-21 00:15:30 +00:00
2024-11-26 17:21:41 +00:00
format, err := imgSvc.FormatFromExtension(filepath.Ext(fileInfo.Name))
2021-04-19 13:16:48 +00:00
// Unsupported extensions directly return the raw data
if err == img.ErrUnsupportedFormat || format == img.FormatGif {
2024-11-21 00:15:30 +00:00
if !d.user.Perm.Download {
return http.StatusAccepted, nil
}
2024-11-26 17:21:41 +00:00
return rawFileHandler(w, r, fileInfo)
2021-04-19 13:16:48 +00:00
}
if err != nil {
return errToStatus(err), err
}
2025-01-05 19:05:33 +00:00
cacheKey := previewCacheKey(response.RealPath, previewSize, fileInfo.ModTime)
2021-08-06 12:31:39 +00:00
resizedImage, ok, err := fileCache.Load(r.Context(), cacheKey)
2020-07-27 17:01:02 +00:00
if err != nil {
return errToStatus(err), err
}
2024-11-21 00:15:30 +00:00
2021-08-06 12:31:39 +00:00
if !ok {
2025-01-05 19:05:33 +00:00
resizedImage, err = createPreview(imgSvc, fileCache, response, previewSize)
2021-08-06 12:31:39 +00:00
if err != nil {
return errToStatus(err), err
}
}
2021-08-06 12:31:39 +00:00
w.Header().Set("Cache-Control", "private")
2025-01-05 19:05:33 +00:00
http.ServeContent(w, r, response.RealPath, fileInfo.ModTime, bytes.NewReader(resizedImage))
2021-04-19 13:16:48 +00:00
return 0, nil
}
2025-01-05 19:05:33 +00:00
func createPreview(imgSvc ImgService, fileCache FileCache, file files.ExtendedFileInfo, previewSize string) ([]byte, error) {
fd, err := os.Open(file.RealPath)
2021-04-19 13:16:48 +00:00
if err != nil {
return nil, err
}
defer fd.Close()
2020-07-23 00:41:19 +00:00
var (
width int
height int
options []img.Option
)
switch {
2024-11-21 00:15:30 +00:00
case previewSize == "large":
2020-07-23 00:41:19 +00:00
width = 1080
height = 1080
options = append(options, img.WithMode(img.ResizeModeFit), img.WithQuality(img.QualityMedium))
2024-11-21 00:15:30 +00:00
case previewSize == "small":
2022-01-15 18:20:13 +00:00
width = 256
height = 256
options = append(options, img.WithMode(img.ResizeModeFill), img.WithQuality(img.QualityLow), img.WithFormat(img.FormatJpeg))
default:
2021-04-19 13:16:48 +00:00
return nil, img.ErrUnsupportedFormat
}
2020-07-27 17:01:02 +00:00
buf := &bytes.Buffer{}
if err := imgSvc.Resize(context.Background(), fd, width, height, buf, options...); err != nil {
2021-04-19 13:16:48 +00:00
return nil, err
}
2020-07-27 17:01:02 +00:00
go func() {
2025-01-05 19:05:33 +00:00
cacheKey := previewCacheKey(file.RealPath, previewSize, file.FileInfo.ModTime)
2020-07-27 17:01:02 +00:00
if err := fileCache.Store(context.Background(), cacheKey, buf.Bytes()); err != nil {
2025-01-21 14:02:43 +00:00
logger.Error(fmt.Sprintf("failed to cache resized image: %v", err))
2020-07-27 17:01:02 +00:00
}
}()
2021-08-06 12:31:39 +00:00
return buf.Bytes(), nil
}
2024-11-21 00:15:30 +00:00
// Generates a cache key for the preview image
2025-01-05 19:05:33 +00:00
func previewCacheKey(realPath, previewSize string, modTime time.Time) string {
return fmt.Sprintf("%x%x%x", realPath, modTime.Unix(), previewSize)
}
2024-12-02 17:14:50 +00:00
2025-02-16 14:07:38 +00:00
func rawFileHandler(w http.ResponseWriter, r *http.Request, file files.FileInfo) (int, error) {
2025-01-05 19:05:33 +00:00
idx := files.GetIndex("default")
realPath, _, _ := idx.GetRealPath(file.Path)
2024-12-02 17:14:50 +00:00
fd, err := os.Open(realPath)
if err != nil {
return http.StatusInternalServerError, err
}
defer fd.Close()
setContentDisposition(w, r, file.Name)
w.Header().Set("Cache-Control", "private")
http.ServeContent(w, r, file.Name, file.ModTime, fd)
return 0, nil
}