Media: Log underlying error when MIME type detection fails #5149

Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
Michael Mayer
2025-08-06 09:59:38 +02:00
parent 0d24ec5abb
commit a6d840056c
8 changed files with 139 additions and 81 deletions

View File

@@ -43,6 +43,7 @@ type MediaFile struct {
fileNameResolved string
fileRoot string
statErr error
mimeErr error
modTime time.Time
fileSize int64
fileType fs.Type
@@ -518,24 +519,36 @@ func (m *MediaFile) Root() string {
// since media types have become used in contexts unrelated to email, such as HTTP:
// https://en.wikipedia.org/wiki/Media_type#Structure
func (m *MediaFile) MimeType() string {
if m.mimeType != "" {
// Do not detect the MIME type again if it is already known,
// or if the detection failed.
if m.mimeType != "" || m.mimeErr != nil {
return m.mimeType
}
var err error
fileName := m.FileName()
// Resolve symlinks.
// Get the filename and resolve symbolic links, if necessary.
fileName := m.FileName()
if fileName, err = fs.Resolve(fileName); err != nil {
return m.mimeType
}
m.mimeType = fs.MimeType(fileName)
// Detect the file's MIME type based on its content and file extension.
m.mimeType, err = fs.DetectMimeType(fileName)
// Log and remember the error if the MIME type detection has failed.
if err != nil {
log.Errorf("media: failed to detect mime type of %s (%s)", clean.Log(m.RootRelName()), clean.Error(err))
m.mimeErr = err
return m.mimeType
}
// Adjust the MIME type for MP4 files containing MPEG-2 transport streams.
if m.mimeType == header.ContentTypeMp4 && m.MetaData().Codec == video.CodecM2TS {
m.mimeType = header.ContentTypeM2TS
}
// Return MIME type.
return m.mimeType
}
@@ -930,9 +943,9 @@ func (m *MediaFile) CheckType() error {
return nil
}
// Exclude mime type from the error message if it could not be detected.
// If the MIME type is empty, it is usually because the file could not be read.
if mimeType == fs.MimeTypeUnknown {
return fmt.Errorf("has an invalid extension (unknown media type)")
return fmt.Errorf("could not be identified")
}
return fmt.Errorf("has an invalid extension for media type %s", clean.LogQuote(mimeType))

View File

@@ -149,7 +149,7 @@ func (m *MediaFile) GenerateThumbnails(thumbPath string, force bool) (err error)
msg := imgErr.Error()
// Non-repairable file error?
if !(strings.Contains(msg, "EOF") ||
if !(strings.Contains(msg, fs.EOF.Error()) ||
strings.HasPrefix(msg, "invalid JPEG")) {
log.Debugf("media: %s in %s", msg, clean.Log(m.RootRelName()))
return imgErr

View File

@@ -32,7 +32,7 @@ func TestCollage(t *testing.T) {
err = imaging.Save(preview, saveName)
assert.NoError(t, err)
mimeType := fs.MimeType(saveName)
mimeType, _ := fs.DetectMimeType(saveName)
assert.Equal(t, header.ContentTypeJpeg, mimeType)
_ = os.Remove(saveName)
@@ -56,7 +56,7 @@ func TestCollage(t *testing.T) {
err = imaging.Save(preview, saveName)
assert.NoError(t, err)
mimeType := fs.MimeType(saveName)
mimeType, _ := fs.DetectMimeType(saveName)
assert.Equal(t, header.ContentTypeJpeg, mimeType)
_ = os.Remove(saveName)
@@ -73,7 +73,7 @@ func TestCollage(t *testing.T) {
err = imaging.Save(preview, saveName)
assert.NoError(t, err)
mimeType := fs.MimeType(saveName)
mimeType, _ := fs.DetectMimeType(saveName)
assert.Equal(t, header.ContentTypeJpeg, mimeType)
_ = os.Remove(saveName)
@@ -100,7 +100,7 @@ func TestCollage(t *testing.T) {
assert.NoError(t, err)
mimeType := fs.MimeType(saveName)
mimeType, _ := fs.DetectMimeType(saveName)
assert.Equal(t, header.ContentTypeJpeg, mimeType)
_ = os.Remove(saveName)

View File

@@ -26,7 +26,7 @@ func TestImage(t *testing.T) {
err = imaging.Save(out, saveName)
assert.NoError(t, err)
mimeType := fs.MimeType(saveName)
mimeType, _ := fs.DetectMimeType(saveName)
assert.Equal(t, header.ContentTypePng, mimeType)
_ = os.Remove(saveName)
@@ -46,7 +46,7 @@ func TestImage(t *testing.T) {
err = imaging.Save(out, saveName)
assert.NoError(t, err)
mimeType := fs.MimeType(saveName)
mimeType, _ := fs.DetectMimeType(saveName)
assert.Equal(t, header.ContentTypePng, mimeType)
_ = os.Remove(saveName)

View File

@@ -26,7 +26,7 @@ func TestPolaroid(t *testing.T) {
err = imaging.Save(out, saveName)
assert.NoError(t, err)
mimeType := fs.MimeType(saveName)
mimeType, _ := fs.DetectMimeType(saveName)
assert.Equal(t, header.ContentTypePng, mimeType)
_ = os.Remove(saveName)

17
pkg/fs/errors.go Normal file
View File

@@ -0,0 +1,17 @@
package fs
import (
"errors"
"io"
)
// Generic errors that may occur when accessing files and folders:
var (
EOF = io.EOF
ErrUnexpectedEOF = io.ErrUnexpectedEOF
ErrShortWrite = io.ErrShortWrite
ErrShortBuffer = io.ErrShortBuffer
ErrNoProgress = io.ErrNoProgress
ErrInvalidWrite = errors.New("invalid write result")
ErrPermissionDenied = errors.New("permission denied")
)

View File

@@ -1,6 +1,7 @@
package fs
import (
"errors"
"path/filepath"
"strings"
@@ -13,14 +14,16 @@ const (
MimeTypeUnknown = ""
)
// MimeType returns the mimetype of a file, or an empty string if it could not be determined.
// DetectMimeType returns the MIME type of the specified file,
// or an error if the type could not be detected.
//
// The IANA and IETF use the term "media type", and consider the term "MIME type" to be obsolete,
// since media types have become used in contexts unrelated to email, such as HTTP:
// https://en.wikipedia.org/wiki/Media_type#Structure
func MimeType(filename string) (mimeType string) {
func DetectMimeType(filename string) (mimeType string, err error) {
// Abort if no filename was specified.
if filename == "" {
return MimeTypeUnknown
return MimeTypeUnknown, errors.New("missing filename")
}
// Detect file type based on the filename extension.
@@ -31,44 +34,51 @@ func MimeType(filename string) (mimeType string) {
switch fileType {
// MPEG-2 Transport Stream
case VideoM2TS, VideoAVCHD:
return header.ContentTypeM2TS
return header.ContentTypeM2TS, nil
// Apple QuickTime Container
case VideoMov:
return header.ContentTypeMov
return header.ContentTypeMov, nil
// MPEG-4 AVC Video
case VideoAvc:
return header.ContentTypeMp4Avc
return header.ContentTypeMp4Avc, nil
// MPEG-4 HEVC Video
case VideoHvc:
return header.ContentTypeMp4Hvc
return header.ContentTypeMp4Hvc, nil
// MPEG-4 HEVC Bitstream
case VideoHev:
return header.ContentTypeMp4Hev
return header.ContentTypeMp4Hev, nil
// Adobe Digital Negative
case ImageDng:
return header.ContentTypeDng
return header.ContentTypeDng, nil
// Adobe Illustrator
case VectorAI:
return header.ContentTypeAI
return header.ContentTypeAI, nil
// Adobe PostScript
case VectorPS:
return header.ContentTypePS
return header.ContentTypePS, nil
// Adobe Embedded PostScript
case VectorEPS:
return header.ContentTypeEPS
return header.ContentTypeEPS, nil
// Adobe PDF
case DocumentPDF:
return header.ContentTypePDF
return header.ContentTypePDF, nil
// Scalable Vector Graphics
case VectorSVG:
return header.ContentTypeSVG
return header.ContentTypeSVG, nil
}
// Detect mime type based on the file content.
// Use "gabriel-vasile/mimetype" to automatically detect the MIME type.
detectedType, err := mimetype.DetectFile(filename)
if detectedType != nil && err == nil {
mimeType = detectedType.String()
// Check if type could be successfully detected.
if err == nil {
if detectedType != nil {
mimeType = detectedType.String()
}
} else if e := err.Error(); strings.HasSuffix(e, ErrPermissionDenied.Error()) {
return MimeTypeUnknown, ErrPermissionDenied
} else if strings.Contains(e, EOF.Error()) {
return MimeTypeUnknown, ErrUnexpectedEOF
}
// Treat "application/octet-stream" as unknown.
@@ -81,25 +91,32 @@ func MimeType(filename string) (mimeType string) {
switch fileType {
// MPEG-4 Multimedia Container
case VideoMp4:
return header.ContentTypeMp4
return header.ContentTypeMp4, nil
// AV1 Image File
case ImageAvif:
return header.ContentTypeAvif
return header.ContentTypeAvif, nil
// AV1 Image File Sequence
case ImageAvifS:
return header.ContentTypeAvifS
return header.ContentTypeAvifS, nil
// High Efficiency Image Container
case ImageHeic, ImageHeif:
return header.ContentTypeHeic
return header.ContentTypeHeic, nil
// High Efficiency Image Container Sequence
case ImageHeicS:
return header.ContentTypeHeicS
return header.ContentTypeHeicS, nil
// ZIP Archive File:
case ArchiveZip:
return header.ContentTypeZip
return header.ContentTypeZip, nil
}
}
return mimeType, err
}
// MimeType returns the MIME type of the specified file,
// or an empty string if the type could not be detected.
func MimeType(filename string) (mimeType string) {
mimeType, _ = DetectMimeType(filename)
return mimeType
}

View File

@@ -8,147 +8,158 @@ import (
"github.com/photoprism/photoprism/pkg/media/http/header"
)
func TestMimeType(t *testing.T) {
t.Run("Mp4", func(t *testing.T) {
func TestDetectMimeType(t *testing.T) {
t.Run("MP4", func(t *testing.T) {
filename := Abs("./testdata/test.mp4")
mimeType := MimeType(filename)
mimeType, _ := DetectMimeType(filename)
assert.Equal(t, "video/mp4", mimeType)
})
t.Run("MOV", func(t *testing.T) {
filename := Abs("./testdata/test.mov")
mimeType := MimeType(filename)
mimeType, _ := DetectMimeType(filename)
assert.Equal(t, "video/quicktime", mimeType)
assert.Equal(t, "video/quicktime", MimeType(filename))
})
t.Run("JPEG", func(t *testing.T) {
filename := Abs("./testdata/test.jpg")
mimeType := MimeType(filename)
mimeType, _ := DetectMimeType(filename)
assert.Equal(t, "image/jpeg", mimeType)
assert.Equal(t, "image/jpeg", MimeType(filename))
})
t.Run("InvalidFilename", func(t *testing.T) {
filename := Abs("./testdata/xxx.jpg")
mimeType := MimeType(filename)
mimeType, _ := DetectMimeType(filename)
assert.Equal(t, "", mimeType)
assert.Equal(t, "", MimeType(filename))
})
t.Run("EmptyFilename", func(t *testing.T) {
mimeType := MimeType("")
mimeType, _ := DetectMimeType("")
assert.Equal(t, "", mimeType)
assert.Equal(t, "", MimeType(""))
})
t.Run("AVIF", func(t *testing.T) {
filename := Abs("./testdata/test.avif")
mimeType := MimeType(filename)
mimeType, _ := DetectMimeType(filename)
assert.Equal(t, "image/avif", mimeType)
assert.Equal(t, "image/avif", MimeType(filename))
})
t.Run("AVIFS", func(t *testing.T) {
filename := Abs("./testdata/test.avifs")
mimeType := MimeType(filename)
mimeType, _ := DetectMimeType(filename)
assert.Equal(t, "image/avif-sequence", mimeType)
assert.Equal(t, "image/avif-sequence", MimeType(filename))
})
t.Run("HEIC", func(t *testing.T) {
filename := Abs("./testdata/test.heic")
mimeType := MimeType(filename)
mimeType, _ := DetectMimeType(filename)
assert.Equal(t, "image/heic", mimeType)
assert.Equal(t, "image/heic", MimeType(filename))
})
t.Run("HEICS", func(t *testing.T) {
filename := Abs("./testdata/test.heics")
mimeType := MimeType(filename)
mimeType, _ := DetectMimeType(filename)
assert.Equal(t, "image/heic-sequence", mimeType)
})
t.Run("DNG", func(t *testing.T) {
filename := Abs("./testdata/test.dng")
mimeType := MimeType(filename)
mimeType, _ := DetectMimeType(filename)
assert.Equal(t, "image/dng", mimeType)
})
t.Run("SVG", func(t *testing.T) {
filename := Abs("./testdata/test.svg")
mimeType := MimeType(filename)
mimeType, _ := DetectMimeType(filename)
assert.Equal(t, "image/svg+xml", mimeType)
assert.Equal(t, "image/svg+xml", MimeType(filename))
})
t.Run("AI", func(t *testing.T) {
filename := Abs("./testdata/test.ai")
mimeType := MimeType(filename)
mimeType, _ := DetectMimeType(filename)
assert.Equal(t, "application/vnd.adobe.illustrator", mimeType)
assert.Equal(t, "application/vnd.adobe.illustrator", MimeType(filename))
})
t.Run("PS", func(t *testing.T) {
filename := Abs("./testdata/test.ps")
mimeType := MimeType(filename)
mimeType, _ := DetectMimeType(filename)
assert.Equal(t, "application/postscript", mimeType)
assert.Equal(t, "application/postscript", MimeType(filename))
})
t.Run("EPS", func(t *testing.T) {
filename := Abs("./testdata/test.eps")
mimeType := MimeType(filename)
mimeType, _ := DetectMimeType(filename)
assert.Equal(t, "image/eps", mimeType)
assert.Equal(t, "image/eps", MimeType(filename))
})
}
func TestBaseType(t *testing.T) {
t.Run("Mp4", func(t *testing.T) {
t.Run("MP4", func(t *testing.T) {
filename := Abs("./testdata/test.mp4")
mimeType := BaseType(MimeType(filename))
assert.Equal(t, "video/mp4", mimeType)
result := BaseType(MimeType(filename))
assert.Equal(t, "video/mp4", result)
})
t.Run("MOV", func(t *testing.T) {
filename := Abs("./testdata/test.mov")
mimeType := BaseType(MimeType(filename))
assert.Equal(t, "video/quicktime", mimeType)
result := BaseType(MimeType(filename))
assert.Equal(t, "video/quicktime", result)
})
t.Run("JPEG", func(t *testing.T) {
filename := Abs("./testdata/test.jpg")
mimeType := BaseType(MimeType(filename))
assert.Equal(t, "image/jpeg", mimeType)
result := BaseType(MimeType(filename))
assert.Equal(t, "image/jpeg", result)
})
t.Run("InvalidFilename", func(t *testing.T) {
filename := Abs("./testdata/xxx.jpg")
mimeType := BaseType(MimeType(filename))
assert.Equal(t, "", mimeType)
result := BaseType(MimeType(filename))
assert.Equal(t, "", result)
})
t.Run("EmptyFilename", func(t *testing.T) {
mimeType := BaseType("")
assert.Equal(t, "", mimeType)
assert.Equal(t, "", BaseType(""))
})
t.Run("AVIF", func(t *testing.T) {
filename := Abs("./testdata/test.avif")
mimeType := BaseType(MimeType(filename))
assert.Equal(t, "image/avif", mimeType)
result := BaseType(MimeType(filename))
assert.Equal(t, "image/avif", result)
})
t.Run("AVIFS", func(t *testing.T) {
filename := Abs("./testdata/test.avifs")
mimeType := MimeType(filename)
mimeType, _ := DetectMimeType(filename)
assert.Equal(t, "image/avif-sequence", mimeType)
assert.Equal(t, "image/avif-sequence", BaseType(mimeType))
})
t.Run("HEIC", func(t *testing.T) {
filename := Abs("./testdata/test.heic")
mimeType := BaseType(MimeType(filename))
assert.Equal(t, "image/heic", mimeType)
result := BaseType(MimeType(filename))
assert.Equal(t, "image/heic", result)
})
t.Run("HEICS", func(t *testing.T) {
filename := Abs("./testdata/test.heics")
mimeType := BaseType(MimeType(filename))
assert.Equal(t, "image/heic-sequence", mimeType)
result := BaseType(MimeType(filename))
assert.Equal(t, "image/heic-sequence", result)
})
t.Run("DNG", func(t *testing.T) {
filename := Abs("./testdata/test.dng")
mimeType := BaseType(MimeType(filename))
assert.Equal(t, "image/dng", mimeType)
result := BaseType(MimeType(filename))
assert.Equal(t, "image/dng", result)
})
t.Run("SVG", func(t *testing.T) {
filename := Abs("./testdata/test.svg")
mimeType := BaseType(MimeType(filename))
assert.Equal(t, "image/svg+xml", mimeType)
result := BaseType(MimeType(filename))
assert.Equal(t, "image/svg+xml", result)
})
t.Run("AI", func(t *testing.T) {
filename := Abs("./testdata/test.ai")
mimeType := BaseType(MimeType(filename))
assert.Equal(t, "application/vnd.adobe.illustrator", mimeType)
result := BaseType(MimeType(filename))
assert.Equal(t, "application/vnd.adobe.illustrator", result)
})
t.Run("PS", func(t *testing.T) {
filename := Abs("./testdata/test.ps")
mimeType := BaseType(MimeType(filename))
assert.Equal(t, "application/postscript", mimeType)
result := BaseType(MimeType(filename))
assert.Equal(t, "application/postscript", result)
})
t.Run("EPS", func(t *testing.T) {
filename := Abs("./testdata/test.eps")
mimeType := BaseType(MimeType(filename))
assert.Equal(t, "image/eps", mimeType)
result := BaseType(MimeType(filename))
assert.Equal(t, "image/eps", result)
})
}