2020-06-25 07:37:13 +00:00
|
|
|
package http
|
|
|
|
|
|
|
|
import (
|
2020-07-27 17:01:02 +00:00
|
|
|
"bytes"
|
2020-07-23 00:41:19 +00:00
|
|
|
"context"
|
2020-06-25 07:37:13 +00:00
|
|
|
"fmt"
|
2020-07-23 00:41:19 +00:00
|
|
|
"io"
|
2020-06-25 07:37:13 +00:00
|
|
|
"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"
|
2020-06-25 07:37:13 +00:00
|
|
|
|
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-06-25 07:37:13 +00:00
|
|
|
)
|
|
|
|
|
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-06-25 07:37:13 +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)
|
2020-07-28 09:57:26 +00:00
|
|
|
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{
|
2024-11-21 00:15:30 +00:00
|
|
|
Path: filepath.Join(d.user.Scope, path),
|
|
|
|
Modify: d.user.Perm.Modify,
|
2025-01-05 19:05:33 +00:00
|
|
|
Source: source,
|
2024-11-21 00:15:30 +00:00
|
|
|
Expand: true,
|
|
|
|
ReadHeader: config.Server.TypeDetectionByHeader,
|
|
|
|
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
|
|
|
}
|
2020-06-25 07:37:13 +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)
|
2021-08-23 08:03:11 +00:00
|
|
|
}
|
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
|
|
|
}
|
2020-06-25 07:37:13 +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
|
|
|
|
}
|
2020-07-07 14:47:11 +00:00
|
|
|
}
|
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
|
|
|
|
}
|
2020-07-07 14:47:11 +00:00
|
|
|
defer fd.Close()
|
|
|
|
|
2020-07-23 00:41:19 +00:00
|
|
|
var (
|
|
|
|
width int
|
|
|
|
height int
|
|
|
|
options []img.Option
|
|
|
|
)
|
|
|
|
|
2020-07-23 10:38:03 +00:00
|
|
|
switch {
|
2024-11-21 00:15:30 +00:00
|
|
|
case previewSize == "large":
|
2020-07-23 00:41:19 +00:00
|
|
|
width = 1080
|
|
|
|
height = 1080
|
2020-07-23 10:38:03 +00:00
|
|
|
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
|
2020-07-23 10:38:03 +00:00
|
|
|
options = append(options, img.WithMode(img.ResizeModeFill), img.WithQuality(img.QualityLow), img.WithFormat(img.FormatJpeg))
|
2020-06-25 07:37:13 +00:00
|
|
|
default:
|
2021-04-19 13:16:48 +00:00
|
|
|
return nil, img.ErrUnsupportedFormat
|
2020-06-25 07:37:13 +00:00
|
|
|
}
|
|
|
|
|
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-06-25 07:37:13 +00:00
|
|
|
}
|
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
|
2020-06-25 07:37:13 +00:00
|
|
|
}
|
2020-07-28 09:57:26 +00:00
|
|
|
|
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)
|
2020-07-28 09:57:26 +00:00
|
|
|
}
|
2024-12-02 17:14:50 +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
|
|
|
|
}
|