Make photo library offline-first and fix video thumbnails
Server / deployment-config (push) Successful in 59s
Android / test-and-build (push) Failing after 1m14s
Server / vulnerability-scan (push) Failing after 8m54s

This commit is contained in:
Курнат Андрей
2026-07-19 21:01:35 +03:00
parent 54556b76e9
commit 7135199f23
52 changed files with 2424 additions and 464 deletions
+1 -1
View File
@@ -36,7 +36,7 @@ RUN make release-linux-docker-${TARGETARCH} ENABLE_VIPS=true DIST=/dist
FROM alpine:3.23
ARG TARGETARCH=arm64
RUN apk add --no-cache attr ca-certificates curl mailcap tree vips && \
RUN apk add --no-cache attr ca-certificates curl ffmpeg mailcap tree vips && \
echo 'hosts: files dns' >| /etc/nsswitch.conf
LABEL maintainer="QSfera" \
@@ -3,7 +3,7 @@ FROM amd64/alpine:edge
ARG VERSION=""
ARG REVISION=""
RUN apk add --no-cache attr bash ca-certificates curl delve inotify-tools libc6-compat mailcap tree vips patch && \
RUN apk add --no-cache attr bash ca-certificates curl delve ffmpeg inotify-tools libc6-compat mailcap tree vips patch && \
echo 'hosts: files dns' >| /etc/nsswitch.conf
LABEL maintainer="QSfera" \
@@ -3,7 +3,7 @@ FROM arm64v8/alpine:edge
ARG VERSION=""
ARG REVISION=""
RUN apk add --no-cache attr bash ca-certificates curl delve inotify-tools libc6-compat mailcap tree vips patch && \
RUN apk add --no-cache attr bash ca-certificates curl delve ffmpeg inotify-tools libc6-compat mailcap tree vips patch && \
echo 'hosts: files dns' >| /etc/nsswitch.conf
LABEL maintainer="QSfera" \
+1 -1
View File
@@ -20,7 +20,7 @@ ARG REVISION
ARG TARGETOS
ARG TARGETARCH
RUN apk add --no-cache attr bash ca-certificates curl imagemagick \
RUN apk add --no-cache attr bash ca-certificates curl ffmpeg imagemagick \
inotify-tools libc6-compat mailcap tree vips \
vips-magick patch && \
echo 'hosts: files dns' >| /etc/nsswitch.conf
@@ -78,6 +78,10 @@ func (b *Backend) Search(_ context.Context, sir *searchService.SearchIndexReques
bleveReq := bleve.NewSearchRequest(q)
bleveReq.Highlight = bleve.NewHighlight()
// Keep relevance as the primary order, but make equal-score media searches stable and
// newest-first. Without explicit tie breakers, increasing PageSize can reshuffle results
// that all have the same mediatype score and makes paged photo grids jump.
bleveReq.SortBy([]string{"-_score", "-Mtime", "_id"})
switch {
case sir.PageSize == -1:
@@ -111,6 +111,11 @@ func (b *Backend) Search(ctx context.Context, sir *searchService.SearchIndexRequ
},
boolQuery,
osu.SearchBodyParams{
Sort: []any{
map[string]any{"_score": map[string]string{"order": "desc"}},
map[string]any{"Mtime": map[string]string{"order": "desc"}},
map[string]any{"_id": map[string]string{"order": "asc"}},
},
Highlight: &osu.BodyParamHighlight{
HighlightOptions: osu.HighlightOptions{
NumberOfFragments: 2,
@@ -95,6 +95,7 @@ func BuildSearchReq(req *opensearchgoAPI.SearchReq, q Builder, p ...SearchBodyPa
type SearchBodyParams struct {
Highlight *BodyParamHighlight `json:"highlight,omitempty"`
Sort []any `json:"sort,omitempty"`
}
//----------------------------------------------------------------------------//
+12 -1
View File
@@ -96,7 +96,18 @@ func (ma matchArray) Swap(i, j int) {
ma[i], ma[j] = ma[j], ma[i]
}
func (ma matchArray) Less(i, j int) bool {
return ma[i].GetScore() > ma[j].GetScore()
if ma[i].GetScore() != ma[j].GetScore() {
return ma[i].GetScore() > ma[j].GetScore()
}
leftTime := ma[i].GetEntity().GetLastModifiedTime()
rightTime := ma[j].GetEntity().GetLastModifiedTime()
if leftTime.GetSeconds() != rightTime.GetSeconds() {
return leftTime.GetSeconds() > rightTime.GetSeconds()
}
if leftTime.GetNanos() != rightTime.GetNanos() {
return leftTime.GetNanos() > rightTime.GetNanos()
}
return ma[i].GetEntity().GetId().GetOpaqueId() < ma[j].GetEntity().GetId().GetOpaqueId()
}
func logDocCount(engine Engine, logger log.Logger) {
@@ -46,4 +46,5 @@ type Thumbnail struct {
MaxInputWidth int `yaml:"max_input_width" env:"THUMBNAILS_MAX_INPUT_WIDTH" desc:"The maximum width of an input image which is being processed." introductionVersion:"1.0.0"`
MaxInputHeight int `yaml:"max_input_height" env:"THUMBNAILS_MAX_INPUT_HEIGHT" desc:"The maximum height of an input image which is being processed." introductionVersion:"1.0.0"`
MaxInputImageFileSize string `yaml:"max_input_image_file_size" env:"THUMBNAILS_MAX_INPUT_IMAGE_FILE_SIZE" desc:"The maximum file size of an input image which is being processed. Usable common abbreviations: [KB, KiB, MB, MiB, GB, GiB, TB, TiB, PB, PiB, EB, EiB], example: 2GB." introductionVersion:"1.0.0"`
MaxInputVideoFileSize string `yaml:"max_input_video_file_size" env:"THUMBNAILS_MAX_INPUT_VIDEO_FILE_SIZE" desc:"The maximum file size of an input video used to generate a preview frame." introductionVersion:"1.0.0"`
}
@@ -30,7 +30,7 @@ func DefaultConfig() *config.Config {
GRPC: config.GRPCConfig{
Addr: "127.0.0.1:9185",
Namespace: "qsfera.api",
MaxConcurrentRequests: 0,
MaxConcurrentRequests: 2,
},
HTTP: config.HTTP{
Addr: "127.0.0.1:9186",
@@ -47,7 +47,7 @@ func DefaultConfig() *config.Config {
Name: "thumbnails",
},
Thumbnail: config.Thumbnail{
Resolutions: []string{"16x16", "32x32", "64x64", "128x128", "1080x1920", "1920x1080", "2160x3840", "3840x2160", "4320x7680", "7680x4320"},
Resolutions: []string{"16x16", "32x32", "64x64", "128x128", "256x256", "512x512", "1024x1024", "1080x1920", "1920x1080", "2160x3840", "3840x2160", "4320x7680", "7680x4320"},
FileSystemStorage: config.FileSystemStorage{
RootDirectory: path.Join(defaults.BaseDataPath(), "thumbnails"),
},
@@ -58,6 +58,7 @@ func DefaultConfig() *config.Config {
MaxInputWidth: 7680,
MaxInputHeight: 7680,
MaxInputImageFileSize: "50MB",
MaxInputVideoFileSize: "2GB",
},
}
}
@@ -4,15 +4,20 @@ import (
"archive/zip"
"bufio"
"bytes"
"context"
"encoding/base64"
"encoding/json"
"image"
"image/draw"
"image/gif"
_ "image/jpeg"
"io"
"math"
"mime"
"os"
"os/exec"
"strings"
"time"
"github.com/pkg/errors"
"golang.org/x/image/font"
@@ -41,6 +46,59 @@ func (i GifDecoder) Convert(r io.Reader) (any, error) {
return img, nil
}
// VideoDecoder extracts a bounded preview frame with ffmpeg. The input is first persisted to a
// temporary file so ffmpeg can seek to MP4/MOV metadata stored at the end of large camera files.
// The file lives on disk and is never retained in process memory.
type VideoDecoder struct{}
func (VideoDecoder) Convert(r io.Reader) (any, error) {
input, err := os.CreateTemp("", "qsfera-video-*")
if err != nil {
return nil, err
}
inputPath := input.Name()
defer os.Remove(inputPath)
if _, err = io.Copy(input, r); err != nil {
input.Close()
return nil, errors.Wrap(err, "could not persist video for preview extraction")
}
if err = input.Sync(); err != nil {
input.Close()
return nil, err
}
if err = input.Close(); err != nil {
return nil, err
}
ctx, cancel := context.WithTimeout(context.Background(), videoFrameExtractionTimeout)
defer cancel()
frame, err := extractVideoFrame(ctx, inputPath, nil)
if err != nil {
return nil, errors.Wrap(err, "could not extract video preview frame")
}
decoded, _, err := image.Decode(bytes.NewReader(frame))
if err != nil {
return nil, errors.Wrap(err, "could not decode video preview frame")
}
return decoded, nil
}
const videoFrameExtractionTimeout = 90 * time.Second
func extractVideoFrame(ctx context.Context, input string, stdin io.Reader) ([]byte, error) {
cmd := exec.CommandContext(
ctx,
"ffmpeg",
"-hide_banner", "-loglevel", "error", "-i", input,
"-map", "0:v:0", "-an", "-sn", "-frames:v", "1",
"-vf", "thumbnail=30,scale=1920:-2:force_original_aspect_ratio=decrease",
"-f", "image2pipe", "-vcodec", "png", "pipe:1",
)
cmd.Stdin = stdin
return cmd.Output()
}
// GgsDecoder is a converter for the geogebra slides file
type GgsDecoder struct{ thumbnailpath string }
@@ -303,6 +361,9 @@ func ForType(mimeType string, opts map[string]any) FileConverter {
// return the service call. So we should only get here when the mimeType parses fine.
mimeType, _, _ = mime.ParseMediaType(mimeType)
switch mimeType {
case "video/mp4", "video/quicktime", "video/webm", "video/x-matroska", "video/x-msvideo", "video/mpeg", "video/3gpp",
"video/x-m4v", "video/mp2t", "video/ogg", "video/x-ms-wmv", "video/x-flv", "video/hevc":
return VideoDecoder{}
case "text/plain":
fontFileMap := ""
fontFaceOpts := &opentype.FaceOptions{
@@ -173,6 +173,13 @@ var _ = Describe("ImageDecoder", func() {
Expect(decoder).To(BeAssignableToTypeOf(GifDecoder{}))
})
It("should return a VideoDecoder for supported video types", func() {
for _, mimeType := range []string{"video/mp4", "video/quicktime", "video/webm", "video/x-matroska"} {
decoder := ForType(mimeType, nil)
Expect(decoder).To(BeAssignableToTypeOf(VideoDecoder{}))
}
})
It("should return an GgsDecoder for ggs types", func() {
decoder := ForType("application/vnd.geogebra.ggs", nil)
// This will not return the expected ggsDecoder, but an ImageDecoder since ggs contains an embedded png.
@@ -1,6 +1,8 @@
package grpc
import (
"github.com/opencloud-eu/reva/v2/pkg/bytesize"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"github.com/qsfera/server/pkg/registry"
"github.com/qsfera/server/pkg/service/grpc"
"github.com/qsfera/server/pkg/service/grpc/handler/ratelimiter"
@@ -10,8 +12,6 @@ import (
"github.com/qsfera/server/services/thumbnails/pkg/service/grpc/v0/decorators"
"github.com/qsfera/server/services/thumbnails/pkg/thumbnail/imgsource"
"github.com/qsfera/server/services/thumbnails/pkg/thumbnail/storage"
"github.com/opencloud-eu/reva/v2/pkg/bytesize"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
)
// NewService initializes the grpc service and server.
@@ -62,20 +62,25 @@ func NewService(opts ...Option) grpc.Service {
options.Logger.Error().Err(err).Msg("could not parse MaxInputImageFileSize")
return grpc.Service{}
}
videoLimit, err := bytesize.Parse(tconf.MaxInputVideoFileSize)
if err != nil {
options.Logger.Error().Err(err).Msg("could not parse MaxInputVideoFileSize")
return grpc.Service{}
}
var thumbnail decorators.DecoratedService
{
thumbnail = svc.NewService(
svc.Config(options.Config),
svc.Logger(options.Logger),
svc.ThumbnailSource(imgsource.NewWebDavSource(tconf, b)),
svc.ThumbnailSource(imgsource.NewWebDavSource(tconf, b, videoLimit)),
svc.ThumbnailStorage(
storage.NewFileSystemStorage(
tconf.FileSystemStorage,
options.Logger,
),
),
svc.CS3Source(imgsource.NewCS3Source(tconf, gatewaySelector, b)),
svc.CS3Source(imgsource.NewCS3Source(tconf, gatewaySelector, b, videoLimit)),
svc.GatewaySelector(gatewaySelector),
)
thumbnail = decorators.NewInstrument(thumbnail, options.Metrics)
@@ -156,6 +156,7 @@ func (g Thumbnail) handleCS3Source(ctx context.Context, req *thumbnailssvc.GetTh
}
ctx = imgsource.ContextSetAuthorization(ctx, src.GetAuthorization())
ctx = imgsource.ContextSetVideoSource(ctx, strings.HasPrefix(sRes.GetInfo().GetMimeType(), "video/"))
r, err := g.cs3Source.Get(ctx, src.GetPath())
switch {
case errors.Is(err, terrors.ErrImageTooLarge):
@@ -245,6 +246,7 @@ func (g Thumbnail) handleWebdavSource(ctx context.Context, req *thumbnailssvc.Ge
if src.GetWebdavAuthorization() != "" {
ctx = imgsource.ContextSetAuthorization(ctx, src.GetWebdavAuthorization())
}
ctx = imgsource.ContextSetVideoSource(ctx, strings.HasPrefix(sRes.GetInfo().GetMimeType(), "video/"))
// add signature and expiration to webdav url
signature, expiration := imgURL.Query().Get("signature"), imgURL.Query().Get("expiration")
@@ -5,11 +5,11 @@ package thumbnail
import (
"bytes"
"image"
"image/png"
"strings"
"github.com/davidbyttow/govips/v2/vips"
"github.com/qsfera/server/services/thumbnails/pkg/errors"
"golang.org/x/image/bmp"
)
// SimpleGenerator is the default image generator and is used for all image types expect gif.
@@ -41,11 +41,13 @@ func (g SimpleGenerator) ProcessorID() string {
func (g SimpleGenerator) Generate(size image.Rectangle, img interface{}) (interface{}, error) {
var m *vips.ImageRef
var err error
switch img.(type) {
case *image.RGBA:
// This comes from the txt preprocessor
switch typed := img.(type) {
case image.Image:
// Preprocessors for text and video return standard-library image types. Convert
// them to a lossless stream before handing them to libvips; decoded PNG video
// frames are usually *image.NRGBA and were previously rejected as invalid.
var buf bytes.Buffer
if err = bmp.Encode(&buf, img.(*image.RGBA)); err != nil {
if err = png.Encode(&buf, typed); err != nil {
return nil, err
}
m, err = vips.NewImageFromReader(&buf)
@@ -71,13 +73,11 @@ func (g SimpleGenerator) Generate(size image.Rectangle, img interface{}) (interf
}
func (g SimpleGenerator) Dimensions(img interface{}) (image.Rectangle, error) {
switch img.(type) {
case *image.RGBA:
m := img.(*image.RGBA)
return m.Bounds(), nil
switch typed := img.(type) {
case image.Image:
return typed.Bounds(), nil
case *vips.ImageRef:
m := img.(*vips.ImageRef)
return image.Rect(0, 0, m.Width(), m.Height()), nil
return image.Rect(0, 0, typed.Width(), typed.Height()), nil
default:
return image.Rectangle{}, errors.ErrInvalidType
}
@@ -0,0 +1,24 @@
//go:build enable_vips
package thumbnail
import (
"image"
"testing"
)
func TestVipsGeneratorAcceptsDecodedVideoFrameDimensions(t *testing.T) {
frame := image.NewNRGBA(image.Rect(0, 0, 1920, 1080))
generator, err := NewSimpleGenerator(typePng, "fit")
if err != nil {
t.Fatal(err)
}
dimensions, err := generator.Dimensions(frame)
if err != nil {
t.Fatalf("decoded video frame must be accepted: %v", err)
}
if dimensions.Dx() != 1920 || dimensions.Dy() != 1080 {
t.Fatalf("unexpected dimensions: %v", dimensions)
}
}
@@ -6,17 +6,18 @@ import (
"fmt"
"io"
"net/http"
"time"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/qsfera/server/services/thumbnails/pkg/config"
"github.com/qsfera/server/services/thumbnails/pkg/errors"
"github.com/opencloud-eu/reva/v2/pkg/bytesize"
revactx "github.com/opencloud-eu/reva/v2/pkg/ctx"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"github.com/opencloud-eu/reva/v2/pkg/rhttp"
"github.com/opencloud-eu/reva/v2/pkg/storagespace"
"github.com/qsfera/server/services/thumbnails/pkg/config"
"github.com/qsfera/server/services/thumbnails/pkg/errors"
"google.golang.org/grpc/metadata"
)
@@ -31,14 +32,20 @@ type CS3 struct {
gatewaySelector pool.Selectable[gateway.GatewayAPIClient]
insecure bool
maxImageFileSize uint64
maxVideoFileSize uint64
}
// NewCS3Source configures a new CS3 image source
func NewCS3Source(cfg config.Thumbnail, gatewaySelector pool.Selectable[gateway.GatewayAPIClient], b bytesize.ByteSize) CS3 {
func NewCS3Source(
cfg config.Thumbnail,
gatewaySelector pool.Selectable[gateway.GatewayAPIClient],
imageLimit, videoLimit bytesize.ByteSize,
) CS3 {
return CS3{
gatewaySelector: gatewaySelector,
insecure: cfg.CS3AllowInsecure,
maxImageFileSize: b.Bytes(),
maxImageFileSize: imageLimit.Bytes(),
maxVideoFileSize: videoLimit.Bytes(),
}
}
@@ -58,7 +65,10 @@ func (s CS3) Get(ctx context.Context, path string) (io.ReadCloser, error) {
}
}
ctx = metadata.AppendToOutgoingContext(context.Background(), revactx.TokenHeader, auth)
// Preserve source metadata already attached to the request context. In particular, the
// video marker selects the separate video size limit; replacing the context here made
// every video larger than the image limit fail before ffmpeg was reached.
ctx = withCS3Authorization(ctx, auth)
err = s.checkImageFileSize(ctx, ref)
if err != nil {
return nil, err
@@ -88,31 +98,65 @@ func (s CS3) Get(ctx context.Context, path string) (io.ReadCloser, error) {
ep, tk = rsp.GetProtocols()[0].GetDownloadEndpoint(), rsp.GetProtocols()[0].GetToken()
}
httpReq, err := rhttp.NewRequest(ctx, "GET", ep, nil)
downloadCtx := ctx
cancelDownload := func() {}
if contextIsVideoSource(ctx) {
downloadCtx, cancelDownload = detachedVideoDownloadContext(ctx)
}
httpReq, err := rhttp.NewRequest(downloadCtx, "GET", ep, nil)
if err != nil {
cancelDownload()
return nil, err
}
httpReq.Header.Set(revactx.TokenHeader, auth)
httpReq.Header.Set(TokenTransportHeader, tk)
http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{
transport := http.DefaultTransport.(*http.Transport).Clone()
transport.TLSClientConfig = &tls.Config{
MinVersion: tls.VersionTLS12,
InsecureSkipVerify: s.insecure, //nolint:gosec
}
client := &http.Client{}
client := &http.Client{Transport: transport}
resp, err := client.Do(httpReq)
if err != nil {
cancelDownload()
return nil, err
}
if resp.StatusCode != http.StatusOK {
resp.Body.Close()
cancelDownload()
return nil, fmt.Errorf("could not get the image \"%s\". Request returned with statuscode %d ", path, resp.StatusCode)
}
if contextIsVideoSource(ctx) {
return cancelOnCloseReadCloser{ReadCloser: resp.Body, cancel: cancelDownload}, nil
}
return resp.Body, nil
}
type cancelOnCloseReadCloser struct {
io.ReadCloser
cancel context.CancelFunc
}
func (r cancelOnCloseReadCloser) Close() error {
err := r.ReadCloser.Close()
r.cancel()
return err
}
func detachedVideoDownloadContext(ctx context.Context) (context.Context, context.CancelFunc) {
return context.WithTimeout(context.WithoutCancel(ctx), videoDownloadTimeout)
}
const videoDownloadTimeout = 15 * time.Minute
func withCS3Authorization(ctx context.Context, auth string) context.Context {
return metadata.AppendToOutgoingContext(ctx, revactx.TokenHeader, auth)
}
func (s CS3) checkImageFileSize(ctx context.Context, ref provider.Reference) error {
gwc, err := s.gatewaySelector.Next()
if err != nil {
@@ -125,7 +169,11 @@ func (s CS3) checkImageFileSize(ctx context.Context, ref provider.Reference) err
if stat.GetStatus().GetCode() != rpc.Code_CODE_OK {
return fmt.Errorf("could not stat image: %s", stat.GetStatus().GetMessage())
}
if stat.GetInfo().GetSize() > s.maxImageFileSize {
limit := s.maxImageFileSize
if contextIsVideoSource(ctx) {
limit = s.maxVideoFileSize
}
if stat.GetInfo().GetSize() > limit {
return errors.ErrImageTooLarge
}
return nil
@@ -0,0 +1,29 @@
package imgsource
import (
"context"
"testing"
)
func TestCS3AuthorizationPreservesVideoSourceMarker(t *testing.T) {
ctx := ContextSetVideoSource(context.Background(), true)
ctx = withCS3Authorization(ctx, "token")
if !contextIsVideoSource(ctx) {
t.Fatal("CS3 authorization must preserve the video source marker")
}
}
func TestDetachedVideoDownloadSurvivesCallerCancellation(t *testing.T) {
parent, cancelParent := context.WithCancel(context.Background())
download, cancelDownload := detachedVideoDownloadContext(parent)
defer cancelDownload()
cancelParent()
select {
case <-download.Done():
t.Fatal("video cache warming must continue after the requesting client disconnects")
default:
}
}
@@ -9,6 +9,7 @@ type key int
const (
auth key = iota
video
)
// Source defines the interface for image sources
@@ -16,6 +17,16 @@ type Source interface {
Get(ctx context.Context, path string) (io.ReadCloser, error)
}
// ContextSetVideoSource selects the separately bounded video input limit.
func ContextSetVideoSource(parent context.Context, isVideo bool) context.Context {
return context.WithValue(parent, video, isVideo)
}
func contextIsVideoSource(ctx context.Context) bool {
value, _ := ctx.Value(video).(bool)
return value
}
// ContextSetAuthorization puts the authorization in the context.
func ContextSetAuthorization(parent context.Context, authorization string) context.Context {
return context.WithValue(parent, auth, authorization)
@@ -11,17 +11,18 @@ import (
"net/http"
"strconv"
"github.com/qsfera/server/services/thumbnails/pkg/config"
thumbnailerErrors "github.com/qsfera/server/services/thumbnails/pkg/errors"
"github.com/opencloud-eu/reva/v2/pkg/bytesize"
"github.com/pkg/errors"
"github.com/qsfera/server/services/thumbnails/pkg/config"
thumbnailerErrors "github.com/qsfera/server/services/thumbnails/pkg/errors"
)
// NewWebDavSource creates a new webdav instance.
func NewWebDavSource(cfg config.Thumbnail, b bytesize.ByteSize) WebDav {
func NewWebDavSource(cfg config.Thumbnail, imageLimit, videoLimit bytesize.ByteSize) WebDav {
return WebDav{
insecure: cfg.WebdavAllowInsecure,
maxImageFileSize: b.Bytes(),
maxImageFileSize: imageLimit.Bytes(),
maxVideoFileSize: videoLimit.Bytes(),
}
}
@@ -29,6 +30,7 @@ func NewWebDavSource(cfg config.Thumbnail, b bytesize.ByteSize) WebDav {
type WebDav struct {
insecure bool
maxImageFileSize uint64
maxVideoFileSize uint64
}
// Get downloads the file from a webdav service
@@ -67,7 +69,11 @@ func (s WebDav) Get(ctx context.Context, url string) (io.ReadCloser, error) {
if err != nil {
return nil, errors.Wrapf(err, `could not parse content length of webdav response "%s"`, url)
}
if c > s.maxImageFileSize {
limit := s.maxImageFileSize
if contextIsVideoSource(ctx) {
limit = s.maxVideoFileSize
}
if c > limit {
return nil, thumbnailerErrors.ErrImageTooLarge
}
@@ -18,5 +18,18 @@ var (
"audio/ogg": {},
"application/vnd.geogebra.slides": {},
"application/vnd.geogebra.pinboard": {},
"video/mp4": {},
"video/quicktime": {},
"video/webm": {},
"video/x-matroska": {},
"video/x-msvideo": {},
"video/mpeg": {},
"video/3gpp": {},
"video/x-m4v": {},
"video/mp2t": {},
"video/ogg": {},
"video/x-ms-wmv": {},
"video/x-flv": {},
"video/hevc": {},
}
)
@@ -27,5 +27,18 @@ var (
"application/vnd.geogebra.slides": {},
"application/vnd.geogebra.pinboard": {},
"image/webp": {},
"video/mp4": {},
"video/quicktime": {},
"video/webm": {},
"video/x-matroska": {},
"video/x-msvideo": {},
"video/mpeg": {},
"video/3gpp": {},
"video/x-m4v": {},
"video/mp2t": {},
"video/ogg": {},
"video/x-ms-wmv": {},
"video/x-flv": {},
"video/hevc": {},
}
)