mirror of
https://git.vectorsigma.ru/public/photoview.git
synced 2026-08-03 21:19:18 +00:00
Use libmagic to identify files type. (#1124)
* Use libmagic to identify file types. * Fix mime array for database querying. * go mod tidy * Fix typo * Another fix for the scheduled image build workflow for tag case (#1137) * Try to fix the incorrect tag processing * more debug code * try with context=git * hopefully, final debug * the final commit with no debug code --------- Co-authored-by: Konstantin Koval * Add benchmark. * rollback workflows. * Fix AI comments. * Build image. * Fix AI comments. * Fix AI comments. * Rollback build workflow. --------- Co-authored-by: Kostiantyn <32730812+kkovaletp@users.noreply.github.com>
This commit is contained in:
@@ -1,8 +1,6 @@
|
|||||||
module github.com/photoview/photoview/api
|
module github.com/photoview/photoview/api
|
||||||
|
|
||||||
go 1.22.5
|
go 1.23.1
|
||||||
|
|
||||||
toolchain go1.23.1
|
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/99designs/gqlgen v0.17.57
|
github.com/99designs/gqlgen v0.17.57
|
||||||
@@ -14,7 +12,7 @@ require (
|
|||||||
github.com/gorilla/handlers v1.5.2
|
github.com/gorilla/handlers v1.5.2
|
||||||
github.com/gorilla/mux v1.8.1
|
github.com/gorilla/mux v1.8.1
|
||||||
github.com/gorilla/websocket v1.5.3
|
github.com/gorilla/websocket v1.5.3
|
||||||
github.com/h2non/filetype v1.1.3
|
github.com/hosom/gomagic v0.0.0-20160718182707-cbc00aac97a4
|
||||||
github.com/joho/godotenv v1.5.1
|
github.com/joho/godotenv v1.5.1
|
||||||
github.com/otiai10/copy v1.14.0
|
github.com/otiai10/copy v1.14.0
|
||||||
github.com/pkg/errors v0.9.1
|
github.com/pkg/errors v0.9.1
|
||||||
|
|||||||
@@ -42,10 +42,10 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
|||||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
github.com/h2non/filetype v1.1.3 h1:FKkx9QbD7HR/zjK1Ia5XiBsq9zdLi5Kf3zGyFTAFkGg=
|
|
||||||
github.com/h2non/filetype v1.1.3/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY=
|
|
||||||
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||||
|
github.com/hosom/gomagic v0.0.0-20160718182707-cbc00aac97a4 h1:HWFAOHTnol5SkBTgR+A52tWHyH0SscmDjPJWOVWlwys=
|
||||||
|
github.com/hosom/gomagic v0.0.0-20160718182707-cbc00aac97a4/go.mod h1:2Oan8DZ6RtFP1c8gJGsw1sshEGsuCRzHFCkssb1PPyA=
|
||||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package media_encoding
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"image"
|
"image"
|
||||||
"image/jpeg"
|
"image/jpeg"
|
||||||
"os"
|
"os"
|
||||||
@@ -12,7 +13,6 @@ import (
|
|||||||
"github.com/photoview/photoview/api/scanner/media_encoding/executable_worker"
|
"github.com/photoview/photoview/api/scanner/media_encoding/executable_worker"
|
||||||
"github.com/photoview/photoview/api/scanner/media_encoding/media_utils"
|
"github.com/photoview/photoview/api/scanner/media_encoding/media_utils"
|
||||||
"github.com/photoview/photoview/api/scanner/media_type"
|
"github.com/photoview/photoview/api/scanner/media_type"
|
||||||
"github.com/photoview/photoview/api/utils"
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"gopkg.in/vansante/go-ffprobe.v2"
|
"gopkg.in/vansante/go-ffprobe.v2"
|
||||||
|
|
||||||
@@ -73,25 +73,28 @@ type EncodeMediaData struct {
|
|||||||
Media *models.Media
|
Media *models.Media
|
||||||
CounterpartPath *string
|
CounterpartPath *string
|
||||||
_photoImage image.Image
|
_photoImage image.Image
|
||||||
_contentType *media_type.MediaType
|
_contentType media_type.MediaType
|
||||||
_videoMetadata *ffprobe.ProbeData
|
_videoMetadata *ffprobe.ProbeData
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewEncodeMediaData(media *models.Media) EncodeMediaData {
|
func NewEncodeMediaData(media *models.Media) EncodeMediaData {
|
||||||
|
fileType := media_type.GetMediaType(media.Path)
|
||||||
|
|
||||||
return EncodeMediaData{
|
return EncodeMediaData{
|
||||||
Media: media,
|
Media: media,
|
||||||
|
_contentType: fileType,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ContentType reads the image to determine its content type
|
// ContentType reads the image to determine its content type
|
||||||
func (img *EncodeMediaData) ContentType() (*media_type.MediaType, error) {
|
func (img *EncodeMediaData) ContentType() (media_type.MediaType, error) {
|
||||||
if img._contentType != nil {
|
if img._contentType != media_type.TypeUnknown {
|
||||||
return img._contentType, nil
|
return img._contentType, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
imgType, err := media_type.GetMediaType(img.Media.Path)
|
imgType := media_type.GetMediaType(img.Media.Path)
|
||||||
if err != nil {
|
if imgType == media_type.TypeUnknown {
|
||||||
return nil, err
|
return imgType, fmt.Errorf("unknown type of %q", img.Media.Path)
|
||||||
}
|
}
|
||||||
|
|
||||||
img._contentType = imgType
|
img._contentType = imgType
|
||||||
@@ -109,78 +112,21 @@ func (img *EncodeMediaData) EncodeHighRes(outputPath string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Use magick if there is no counterpart JPEG file to use instead
|
// Use magick if there is no counterpart JPEG file to use instead
|
||||||
if contentType.IsRaw() && img.CounterpartPath == nil {
|
if contentType.IsImage() && !contentType.IsWebCompatible() {
|
||||||
if executable_worker.Magick.IsInstalled() {
|
imgPath := img.Media.Path
|
||||||
err := executable_worker.Magick.EncodeJpeg(img.Media.Path, outputPath, 70)
|
if img.CounterpartPath != nil {
|
||||||
if err != nil {
|
imgPath = *img.CounterpartPath
|
||||||
return err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return errors.New("could not convert photo as no RAW converter was found")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
image, err := img.photoImage()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
encodeImageJPEG(image, outputPath, 70)
|
err := executable_worker.Magick.EncodeJpeg(imgPath, outputPath, 70)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to convert RAW photo %q to JPEG: %w", imgPath, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// photoImage reads and decodes the image file and saves it in a cache so the photo in only decoded once
|
|
||||||
func (img *EncodeMediaData) photoImage() (image.Image, error) {
|
|
||||||
if img._photoImage != nil {
|
|
||||||
return img._photoImage, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var photoPath string
|
|
||||||
if img.CounterpartPath != nil {
|
|
||||||
photoPath = *img.CounterpartPath
|
|
||||||
} else {
|
|
||||||
photoPath = img.Media.Path
|
|
||||||
}
|
|
||||||
|
|
||||||
photoImg, err := img.decodeImage(photoPath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, utils.HandleError("image decoding", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
img._photoImage = photoImg
|
|
||||||
return img._photoImage, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (img *EncodeMediaData) decodeImage(imagePath string) (image.Image, error) {
|
|
||||||
file, err := os.Open(imagePath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrapf(err, "failed to open file to decode image (%s)", imagePath)
|
|
||||||
}
|
|
||||||
defer file.Close()
|
|
||||||
|
|
||||||
mediaType, err := img.ContentType()
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrapf(err, "failed to get media content type needed to decode it (%s)", imagePath)
|
|
||||||
}
|
|
||||||
|
|
||||||
var decodedImage image.Image
|
|
||||||
|
|
||||||
if *mediaType == media_type.TypeHeic {
|
|
||||||
decodedImage, _, err = image.Decode(file)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrapf(err, "failed to decode HEIF image (%s)", imagePath)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
decodedImage, err = imaging.Decode(file, imaging.AutoOrientation(true))
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrapf(err, "failed to decode image (%s)", imagePath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return decodedImage, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (enc *EncodeMediaData) VideoMetadata() (*ffprobe.ProbeData, error) {
|
func (enc *EncodeMediaData) VideoMetadata() (*ffprobe.ProbeData, error) {
|
||||||
|
|
||||||
if enc._videoMetadata != nil {
|
if enc._videoMetadata != nil {
|
||||||
|
|||||||
45
api/scanner/media_type/counterpart.go
Normal file
45
api/scanner/media_type/counterpart.go
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
package media_type
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FindWebCounterpart returns the filename if the file `imagePath` has a counterpart file competible with the browser.
|
||||||
|
func FindWebCounterpart(imagePath string) (string, bool) {
|
||||||
|
return findCounterpart(imagePath, func(filename string) bool {
|
||||||
|
mt := GetMediaType(filename)
|
||||||
|
return mt.IsImage() && mt.IsWebCompatible()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindRawCounterpart returns the filename if the file `imagePath` has a counterpart file which needs to be processed before showing in the browser.
|
||||||
|
func FindRawCounterpart(imagePath string) (string, bool) {
|
||||||
|
return findCounterpart(imagePath, func(filename string) bool {
|
||||||
|
mt := GetMediaType(filename)
|
||||||
|
return mt.IsImage() && !mt.IsWebCompatible()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func findCounterpart(filename string, acceptFn func(filepath string) bool) (string, bool) {
|
||||||
|
ext := path.Ext(filename)
|
||||||
|
filenamePattern := strings.TrimSuffix(filename, ext) + ".*"
|
||||||
|
|
||||||
|
filenames, err := filepath.Glob(filenamePattern)
|
||||||
|
if err != nil {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, f := range filenames {
|
||||||
|
if f == filename {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if acceptFn(f) {
|
||||||
|
return f, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
66
api/scanner/media_type/counterpart_test.go
Normal file
66
api/scanner/media_type/counterpart_test.go
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
package media_type
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/photoview/photoview/api/test_utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFindWebCounterpart(t *testing.T) {
|
||||||
|
mediaPath := test_utils.PathFromAPIRoot("./scanner/test_media/real_media")
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
input string
|
||||||
|
wantFile string
|
||||||
|
wantOk bool
|
||||||
|
}{
|
||||||
|
{"raw_with_jpg.tiff", "raw_with_jpg.jpg", true},
|
||||||
|
{"raw_with_file.tiff", "", false},
|
||||||
|
{"standalone_raw.tiff", "", false},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
input := filepath.Join(mediaPath, tc.input)
|
||||||
|
if _, err := os.Stat(input); err != nil {
|
||||||
|
t.Fatalf("input %q doesn't exist: %v", input, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
file, ok := FindWebCounterpart(input)
|
||||||
|
got := strings.TrimLeft(strings.TrimPrefix(file, mediaPath), "/")
|
||||||
|
|
||||||
|
if got != tc.wantFile || ok != tc.wantOk {
|
||||||
|
t.Errorf("FindWebCounterpart(%q) = (%q, %v), want: (%q, %v)", tc.input, got, ok, tc.wantFile, tc.wantOk)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFindRawCounterpart(t *testing.T) {
|
||||||
|
mediaPath := test_utils.PathFromAPIRoot("./scanner/test_media/real_media")
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
input string
|
||||||
|
wantFile string
|
||||||
|
wantOk bool
|
||||||
|
}{
|
||||||
|
{"raw_with_jpg.jpg", "raw_with_jpg.tiff", true},
|
||||||
|
{"jpg_with_file.jpg", "", false},
|
||||||
|
{"standalone_jpg.jpg", "", false},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
input := filepath.Join(mediaPath, tc.input)
|
||||||
|
if _, err := os.Stat(input); err != nil {
|
||||||
|
t.Fatalf("input %q doesn't exist: %v", input, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
file, ok := FindRawCounterpart(input)
|
||||||
|
got := strings.TrimLeft(strings.TrimPrefix(file, mediaPath), "/")
|
||||||
|
|
||||||
|
if got != tc.wantFile || ok != tc.wantOk {
|
||||||
|
t.Errorf("FindRawCounterpart(%q) = (%q, %v), want: (%q, %v)", tc.input, got, ok, tc.wantFile, tc.wantOk)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
78
api/scanner/media_type/magic.go
Normal file
78
api/scanner/media_type/magic.go
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
package media_type
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
magic "github.com/hosom/gomagic"
|
||||||
|
"github.com/photoview/photoview/api/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
var libmagic struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
libmagic *libMagic
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
libmagic.libmagic, libmagic.err = newLibMagic()
|
||||||
|
if libmagic.err != nil {
|
||||||
|
libmagic.libmagic = nil
|
||||||
|
log.Error("Init libmagic error.", "error", libmagic.err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMediaType returns a media type of file `f`.
|
||||||
|
// This function is thread-safe.
|
||||||
|
func GetMediaType(f string) MediaType {
|
||||||
|
libmagic.mu.Lock()
|
||||||
|
defer libmagic.mu.Unlock()
|
||||||
|
|
||||||
|
if libmagic.err != nil {
|
||||||
|
log.Warn("GetMediaType() error.", "error", libmagic.err, "file", f)
|
||||||
|
return TypeUnknown
|
||||||
|
}
|
||||||
|
|
||||||
|
mime, err := libmagic.libmagic.Type(f)
|
||||||
|
if err != nil {
|
||||||
|
log.Warn("GetMediaType() error.", "error", err, "file", f)
|
||||||
|
return TypeUnknown
|
||||||
|
}
|
||||||
|
|
||||||
|
mime = strings.SplitN(mime, ";", 2)[0]
|
||||||
|
|
||||||
|
return mediaType(mime)
|
||||||
|
}
|
||||||
|
|
||||||
|
// libMagic parses the magic code in a file and returns the media type.
|
||||||
|
type libMagic struct {
|
||||||
|
magic *magic.Magic
|
||||||
|
}
|
||||||
|
|
||||||
|
// newLibMagic creates an instance of Magic.
|
||||||
|
func newLibMagic() (*libMagic, error) {
|
||||||
|
// MAGIC_SYMLINK: Follow symlinks
|
||||||
|
// MAGIC_MIME: Return MIME type string
|
||||||
|
// MAGIC_ERROR: Handle errors in magic database
|
||||||
|
// MAGIC_NO_CHECK_COMPRESS: Don't check for compressed files
|
||||||
|
// MAGIC_NO_CHECK_ENCODING: Don't check for text encodings
|
||||||
|
m, err := magic.Open(magic.MAGIC_SYMLINK | magic.MAGIC_MIME | magic.MAGIC_ERROR | magic.MAGIC_NO_CHECK_COMPRESS | magic.MAGIC_NO_CHECK_ENCODING)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &libMagic{
|
||||||
|
magic: m,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes the library.
|
||||||
|
func (m *libMagic) Close() error {
|
||||||
|
return m.magic.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type returns the media type of the given file `f`. It returns TypeUnknown if the file is not a image nor a video.
|
||||||
|
// This function is not thread-safe.
|
||||||
|
func (m *libMagic) Type(f string) (string, error) {
|
||||||
|
return m.magic.File(f)
|
||||||
|
}
|
||||||
146
api/scanner/media_type/magic_test.go
Normal file
146
api/scanner/media_type/magic_test.go
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
package media_type
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
magic "github.com/hosom/gomagic"
|
||||||
|
"github.com/photoview/photoview/api/test_utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMagic(t *testing.T) {
|
||||||
|
mediaPath := test_utils.PathFromAPIRoot("./scanner/test_media/real_media")
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
filepath string
|
||||||
|
filetype MediaType
|
||||||
|
}{
|
||||||
|
{"file.pdf", mediaType("application/pdf")},
|
||||||
|
|
||||||
|
{"bmp.bmp", TypeBMP},
|
||||||
|
{"gif.gif", TypeGIF},
|
||||||
|
{"jpeg.jpg", TypeJPEG},
|
||||||
|
{"png.png", TypePNG},
|
||||||
|
{"webp.webp", TypeWebP},
|
||||||
|
|
||||||
|
{"heif.heif", mediaType("image/heic")},
|
||||||
|
{"jpeg2000.jp2", mediaType("image/jp2")},
|
||||||
|
{"tiff.tiff", mediaType("image/tiff")},
|
||||||
|
|
||||||
|
{"mp4.mp4", TypeMP4},
|
||||||
|
{"ogg.ogg", TypeOGG},
|
||||||
|
{"mpeg.mpg", TypeMPEG},
|
||||||
|
{"webm.webm", TypeWEBM},
|
||||||
|
|
||||||
|
{"avi.avi", mediaType("video/x-msvideo")},
|
||||||
|
{"mkv.mkv", mediaType("video/x-matroska")},
|
||||||
|
{"quicktime.mov", mediaType("video/quicktime")},
|
||||||
|
{"wmv.wmv", mediaType("video/x-ms-asf")},
|
||||||
|
}
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
defer wg.Wait()
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
wg.Add(1)
|
||||||
|
input, want := tc.filepath, tc.filetype
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
path := filepath.Join(mediaPath, input)
|
||||||
|
|
||||||
|
got := GetMediaType(path)
|
||||||
|
if got != want {
|
||||||
|
t.Errorf("magic.Type(%q) = %v, want: %v", path, got, want)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMagicNoInit(t *testing.T) {
|
||||||
|
org := libmagic.libmagic
|
||||||
|
libmagic.libmagic = nil
|
||||||
|
libmagic.err = errors.New("error")
|
||||||
|
t.Cleanup(func() {
|
||||||
|
libmagic.libmagic = org
|
||||||
|
libmagic.err = nil
|
||||||
|
})
|
||||||
|
|
||||||
|
mediaPath := test_utils.PathFromAPIRoot("./scanner/test_media/real_media")
|
||||||
|
file := filepath.Join(mediaPath, "file.pdf")
|
||||||
|
|
||||||
|
got := GetMediaType(file)
|
||||||
|
if want := TypeUnknown; got != want {
|
||||||
|
t.Errorf("GetMediaType() = %v, want: %v", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getMediaFiles() []string {
|
||||||
|
mediaPath := test_utils.PathFromAPIRoot("./scanner/test_media/real_media")
|
||||||
|
var files []string
|
||||||
|
for _, f := range []string{
|
||||||
|
"file.pdf",
|
||||||
|
|
||||||
|
"bmp.bmp",
|
||||||
|
"gif.gif",
|
||||||
|
"jpeg.jpg",
|
||||||
|
"png.png",
|
||||||
|
"webp.webp",
|
||||||
|
|
||||||
|
"heif.heif",
|
||||||
|
"jpeg2000.jp2",
|
||||||
|
"tiff.tiff",
|
||||||
|
|
||||||
|
"mp4.mp4",
|
||||||
|
"ogg.ogg",
|
||||||
|
"mpeg.mpg",
|
||||||
|
"webm.webm",
|
||||||
|
|
||||||
|
"avi.avi",
|
||||||
|
"mkv.mkv",
|
||||||
|
"quicktime.mov",
|
||||||
|
"wmv.wmv",
|
||||||
|
} {
|
||||||
|
files = append(files, filepath.Join(mediaPath, f))
|
||||||
|
}
|
||||||
|
|
||||||
|
return files
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkMagicAll(b *testing.B) {
|
||||||
|
files := getMediaFiles()
|
||||||
|
b.ResetTimer()
|
||||||
|
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
m, err := magic.Open(magic.MAGIC_SYMLINK | magic.MAGIC_MIME | magic.MAGIC_ERROR | magic.MAGIC_NO_CHECK_COMPRESS | magic.MAGIC_NO_CHECK_ENCODING)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatalf("magic.Open() error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _ = m.File(files[i%len(files)])
|
||||||
|
m.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkMagicType(b *testing.B) {
|
||||||
|
files := getMediaFiles()
|
||||||
|
|
||||||
|
m, err := magic.Open(magic.MAGIC_SYMLINK | magic.MAGIC_MIME | magic.MAGIC_ERROR | magic.MAGIC_NO_CHECK_COMPRESS | magic.MAGIC_NO_CHECK_ENCODING)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatalf("magic.Open() error: %v", err)
|
||||||
|
}
|
||||||
|
defer m.Close()
|
||||||
|
|
||||||
|
var mu sync.Mutex
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
mu.Lock()
|
||||||
|
_, _ = m.File(files[i%len(files)])
|
||||||
|
mu.Unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,347 +1,121 @@
|
|||||||
package media_type
|
package media_type
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
"unique"
|
||||||
"github.com/h2non/filetype"
|
|
||||||
"github.com/photoview/photoview/api/scanner/media_encoding/executable_worker"
|
|
||||||
"github.com/photoview/photoview/api/scanner/scanner_utils"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type MediaType string
|
type MediaType unique.Handle[string]
|
||||||
|
|
||||||
const (
|
func mediaType(mime string) MediaType {
|
||||||
TypeJpeg MediaType = "image/jpeg"
|
return MediaType(unique.Make(mime))
|
||||||
TypePng MediaType = "image/png"
|
}
|
||||||
TypeTiff MediaType = "image/tiff"
|
|
||||||
TypeWebp MediaType = "image/webp"
|
|
||||||
TypeBmp MediaType = "image/bmp"
|
|
||||||
TypeHeic MediaType = "image/heic"
|
|
||||||
TypeGif MediaType = "image/gif"
|
|
||||||
|
|
||||||
// Raw formats
|
var (
|
||||||
TypeDNG MediaType = "image/x-adobe-dng"
|
TypeUnknown MediaType
|
||||||
TypeARW MediaType = "image/x-sony-arw"
|
|
||||||
TypeSR2 MediaType = "image/x-sony-sr2"
|
|
||||||
TypeSRF MediaType = "image/x-sony-srf"
|
|
||||||
TypeCR2 MediaType = "image/x-canon-cr2"
|
|
||||||
TypeCR3 MediaType = "image/x-canon-cr3"
|
|
||||||
TypeCRW MediaType = "image/x-canon-crw"
|
|
||||||
TypeERF MediaType = "image/x-epson-erf"
|
|
||||||
TypeDCS MediaType = "image/x-kodak-dcs"
|
|
||||||
TypeDRF MediaType = "image/x-kodak-drf"
|
|
||||||
TypeDCR MediaType = "image/x-kodak-dcr"
|
|
||||||
TypeK25 MediaType = "image/x-kodak-k25"
|
|
||||||
TypeKDC MediaType = "image/x-kodak-kdc"
|
|
||||||
TypeMRW MediaType = "image/x-minolta-mrw"
|
|
||||||
TypeMDC MediaType = "image/x-minolta-mdc"
|
|
||||||
TypeNEF MediaType = "image/x-nikon-nef"
|
|
||||||
TypeNRW MediaType = "image/x-nikon-nrw"
|
|
||||||
TypeORF MediaType = "image/x-olympus-orf"
|
|
||||||
TypePEF MediaType = "image/x-pentax-pef"
|
|
||||||
TypeRAF MediaType = "image/x-fuji-raf"
|
|
||||||
TypeRAW MediaType = "image/x-panasonic-raw"
|
|
||||||
TypeRW2 MediaType = "image/x-panasonic-rw2"
|
|
||||||
TypeGPR MediaType = "image/x-gopro-gpr"
|
|
||||||
Type3FR MediaType = "image/x-hasselblad-3fr"
|
|
||||||
TypeFFF MediaType = "image/x-hasselblad-fff"
|
|
||||||
TypeMEF MediaType = "image/x-mamiya-mef"
|
|
||||||
TypeCap MediaType = "image/x-phaseone-cap"
|
|
||||||
TypeIIQ MediaType = "image/x-phaseone-iiq"
|
|
||||||
TypeMOS MediaType = "image/x-leaf-mos"
|
|
||||||
TypeRWL MediaType = "image/x-leica-rwl"
|
|
||||||
TypeSRW MediaType = "image/x-samsung-srw"
|
|
||||||
|
|
||||||
// Video formats
|
TypeImage = mediaType("image/")
|
||||||
TypeMP4 MediaType = "video/mp4"
|
TypeVideo = mediaType("video/")
|
||||||
TypeMPEG MediaType = "video/mpeg"
|
|
||||||
Type3GP MediaType = "video/3gpp"
|
// Web Image formats
|
||||||
Type3G2 MediaType = "video/3gpp2"
|
TypeJPEG = mediaType("image/jpeg")
|
||||||
TypeOGG MediaType = "video/ogg"
|
TypePNG = mediaType("image/png")
|
||||||
TypeWMV MediaType = "video/x-ms-wmv"
|
TypeWebP = mediaType("image/webp")
|
||||||
TypeAVI MediaType = "video/x-msvideo"
|
TypeBMP = mediaType("image/bmp")
|
||||||
TypeWEBM MediaType = "video/webm"
|
TypeGIF = mediaType("image/gif")
|
||||||
TypeMOV MediaType = "video/quicktime"
|
|
||||||
TypeTS MediaType = "video/mp2t"
|
// Web Video formats
|
||||||
TypeMTS MediaType = "video/MP2T"
|
TypeMP4 = mediaType("video/mp4")
|
||||||
TypeMKV MediaType = "video/x-matroska"
|
TypeMPEG = mediaType("video/mpeg")
|
||||||
|
TypeOGG = mediaType("video/ogg")
|
||||||
|
TypeWEBM = mediaType("video/webm")
|
||||||
)
|
)
|
||||||
|
|
||||||
var SupportedMimetypes = [...]MediaType{
|
var webImageMimetypes = arrayToSet([]MediaType{
|
||||||
TypeJpeg,
|
TypeJPEG,
|
||||||
TypePng,
|
TypePNG,
|
||||||
TypeTiff,
|
TypeWebP,
|
||||||
TypeWebp,
|
TypeBMP,
|
||||||
TypeBmp,
|
TypeGIF,
|
||||||
TypeHeic,
|
})
|
||||||
TypeGif,
|
|
||||||
}
|
|
||||||
|
|
||||||
var WebMimetypes = [...]MediaType{
|
var webVideoMimetypes = arrayToSet([]MediaType{
|
||||||
TypeJpeg,
|
|
||||||
TypePng,
|
|
||||||
TypeWebp,
|
|
||||||
TypeBmp,
|
|
||||||
TypeGif,
|
|
||||||
}
|
|
||||||
|
|
||||||
var RawMimeTypes = [...]MediaType{
|
|
||||||
TypeDNG,
|
|
||||||
TypeARW,
|
|
||||||
TypeSR2,
|
|
||||||
TypeSRF,
|
|
||||||
TypeCR2,
|
|
||||||
TypeCR3,
|
|
||||||
TypeCRW,
|
|
||||||
TypeERF,
|
|
||||||
TypeDCS,
|
|
||||||
TypeDRF,
|
|
||||||
TypeDCR,
|
|
||||||
TypeK25,
|
|
||||||
TypeKDC,
|
|
||||||
TypeMRW,
|
|
||||||
TypeMDC,
|
|
||||||
TypeNEF,
|
|
||||||
TypeNRW,
|
|
||||||
TypeORF,
|
|
||||||
TypePEF,
|
|
||||||
TypeRAF,
|
|
||||||
TypeRAW,
|
|
||||||
TypeRW2,
|
|
||||||
TypeGPR,
|
|
||||||
Type3FR,
|
|
||||||
TypeFFF,
|
|
||||||
TypeMEF,
|
|
||||||
TypeCap,
|
|
||||||
TypeIIQ,
|
|
||||||
TypeMOS,
|
|
||||||
TypeRWL,
|
|
||||||
TypeSRW,
|
|
||||||
}
|
|
||||||
|
|
||||||
var VideoMimetypes = [...]MediaType{
|
|
||||||
TypeMP4,
|
|
||||||
TypeMPEG,
|
|
||||||
Type3GP,
|
|
||||||
Type3G2,
|
|
||||||
TypeOGG,
|
|
||||||
TypeWMV,
|
|
||||||
TypeAVI,
|
|
||||||
TypeWEBM,
|
|
||||||
TypeMOV,
|
|
||||||
TypeTS,
|
|
||||||
TypeMTS,
|
|
||||||
TypeMKV,
|
|
||||||
}
|
|
||||||
|
|
||||||
// WebVideoMimetypes are video types that can be played directly in the browser without transcoding
|
|
||||||
var WebVideoMimetypes = [...]MediaType{
|
|
||||||
TypeMP4,
|
TypeMP4,
|
||||||
TypeMPEG,
|
TypeMPEG,
|
||||||
TypeWEBM,
|
TypeWEBM,
|
||||||
TypeOGG,
|
TypeOGG,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Legacy function. Should be removed.
|
||||||
|
var WebMimetypes = []string{
|
||||||
|
TypeJPEG.String(),
|
||||||
|
TypePNG.String(),
|
||||||
|
TypeWebP.String(),
|
||||||
|
TypeBMP.String(),
|
||||||
|
TypeGIF.String(),
|
||||||
|
|
||||||
|
TypeMP4.String(),
|
||||||
|
TypeMPEG.String(),
|
||||||
|
TypeWEBM.String(),
|
||||||
|
TypeOGG.String(),
|
||||||
}
|
}
|
||||||
|
|
||||||
var fileExtensions = map[string]MediaType{
|
func arrayToSet[T comparable](array []T) map[T]struct{} {
|
||||||
".jpg": TypeJpeg,
|
ret := make(map[T]struct{})
|
||||||
".jpeg": TypeJpeg,
|
for _, item := range array {
|
||||||
".png": TypePng,
|
ret[item] = struct{}{}
|
||||||
".tif": TypeTiff,
|
}
|
||||||
".tiff": TypeTiff,
|
return ret
|
||||||
".bmp": TypeBmp,
|
|
||||||
".heic": TypeHeic,
|
|
||||||
".gif": TypeGif,
|
|
||||||
|
|
||||||
// RAW formats
|
|
||||||
".dng": TypeDNG,
|
|
||||||
".arw": TypeARW,
|
|
||||||
".sr2": TypeSR2,
|
|
||||||
".srf": TypeSRF,
|
|
||||||
".srw": TypeSRW,
|
|
||||||
".cr2": TypeCR2,
|
|
||||||
".cr3": TypeCR3,
|
|
||||||
".crw": TypeCRW,
|
|
||||||
".erf": TypeERF,
|
|
||||||
".dcr": TypeDCR,
|
|
||||||
".k25": TypeK25,
|
|
||||||
".kdc": TypeKDC,
|
|
||||||
".mrw": TypeMRW,
|
|
||||||
".nef": TypeNEF,
|
|
||||||
".nrw": TypeNRW,
|
|
||||||
".mdc": TypeMDC,
|
|
||||||
".mef": TypeMEF,
|
|
||||||
".orf": TypeORF,
|
|
||||||
".pef": TypePEF,
|
|
||||||
".raf": TypeRAF,
|
|
||||||
".raw": TypeRAW,
|
|
||||||
".rw2": TypeRW2,
|
|
||||||
".dcs": TypeDCS,
|
|
||||||
".drf": TypeDRF,
|
|
||||||
".gpr": TypeGPR,
|
|
||||||
".3fr": Type3FR,
|
|
||||||
".fff": TypeFFF,
|
|
||||||
".cap": TypeCap,
|
|
||||||
".iiq": TypeIIQ,
|
|
||||||
".mos": TypeMOS,
|
|
||||||
".rwl": TypeRWL,
|
|
||||||
|
|
||||||
// Video formats
|
|
||||||
".mp4": TypeMP4,
|
|
||||||
".m4v": TypeMP4,
|
|
||||||
".mpeg": TypeMPEG,
|
|
||||||
".3gp": Type3GP,
|
|
||||||
".3g2": Type3G2,
|
|
||||||
".ogv": TypeOGG,
|
|
||||||
".wmv": TypeWMV,
|
|
||||||
".avi": TypeAVI,
|
|
||||||
".webm": TypeWEBM,
|
|
||||||
".mov": TypeMOV,
|
|
||||||
".qt": TypeMOV,
|
|
||||||
".ts": TypeTS,
|
|
||||||
".m2ts": TypeMTS,
|
|
||||||
".mts": TypeMTS,
|
|
||||||
".mkv": TypeMKV,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (imgType *MediaType) IsRaw() bool {
|
// IsWebCompatible returns true if the media type is compatible with the browser.
|
||||||
for _, raw_mime := range RawMimeTypes {
|
func (t MediaType) IsWebCompatible() bool {
|
||||||
if raw_mime == *imgType {
|
if t == TypeUnknown {
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
|
||||||
|
|
||||||
func (imgType *MediaType) IsWebCompatible() bool {
|
|
||||||
for _, web_mime := range WebMimetypes {
|
|
||||||
if web_mime == *imgType {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, web_mime := range WebVideoMimetypes {
|
if _, ok := webImageMimetypes[t]; ok {
|
||||||
if web_mime == *imgType {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (imgType *MediaType) IsVideo() bool {
|
|
||||||
for _, video_mime := range VideoMimetypes {
|
|
||||||
if video_mime == *imgType {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (imgType *MediaType) IsBasicTypeSupported() bool {
|
|
||||||
for _, img_mime := range SupportedMimetypes {
|
|
||||||
if img_mime == *imgType {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsSupported determines if the given type can be processed
|
|
||||||
func (imgType *MediaType) IsSupported() bool {
|
|
||||||
if imgType.IsBasicTypeSupported() {
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
if executable_worker.Magick.IsInstalled() && imgType.IsRaw() {
|
if _, ok := webVideoMimetypes[t]; ok {
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if executable_worker.Ffmpeg.IsInstalled() && imgType.IsVideo() {
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetExtensionMediaType(ext string) (MediaType, bool) {
|
// IsImage returns true if the media type is image type.
|
||||||
result, found := fileExtensions[strings.ToLower(ext)]
|
func (t MediaType) IsImage() bool {
|
||||||
return result, found
|
if t == TypeUnknown {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.HasPrefix(t.String(), TypeImage.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetMediaType(path string) (*MediaType, error) {
|
// IsVideo returns true if the media type is video type.
|
||||||
ext := filepath.Ext(path)
|
func (t MediaType) IsVideo() bool {
|
||||||
|
if t == TypeUnknown {
|
||||||
fileExtType, found := GetExtensionMediaType(ext)
|
return false
|
||||||
|
|
||||||
if found {
|
|
||||||
if fileExtType.IsSupported() {
|
|
||||||
return &fileExtType, nil
|
|
||||||
} else {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If extension was not recognized try to read file header
|
return strings.HasPrefix(t.String(), TypeVideo.String())
|
||||||
file, err := os.Open(path)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrapf(err, "could not open file to determine content-type %s", path)
|
|
||||||
}
|
|
||||||
defer file.Close()
|
|
||||||
|
|
||||||
head := make([]byte, 261)
|
|
||||||
if _, err := file.Read(head); err != nil {
|
|
||||||
if err == io.EOF {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, errors.Wrapf(err, "could not read file to determine content-type: %s", path)
|
|
||||||
}
|
|
||||||
|
|
||||||
_imgType, err := filetype.Image(head)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
imgType := MediaType(_imgType.MIME.Value)
|
|
||||||
if imgType.IsSupported() {
|
|
||||||
return &imgType, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (mediaType MediaType) FileExtensions() []string {
|
// IsSupported returns true if the media type can be processed.
|
||||||
var extensions []string
|
func (t MediaType) IsSupported() bool {
|
||||||
|
if t == TypeUnknown {
|
||||||
for ext, extType := range fileExtensions {
|
return false
|
||||||
if extType == mediaType {
|
|
||||||
extensions = append(extensions, ext)
|
|
||||||
extensions = append(extensions, strings.ToUpper(ext))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return extensions
|
return t.IsImage() || t.IsVideo()
|
||||||
}
|
}
|
||||||
|
|
||||||
func RawCounterpart(imagePath string) *string {
|
func (t MediaType) String() string {
|
||||||
pathWithoutExt := strings.TrimSuffix(imagePath, path.Ext(imagePath))
|
if t == TypeUnknown {
|
||||||
|
return "unknown"
|
||||||
for _, rawType := range RawMimeTypes {
|
|
||||||
for _, ext := range rawType.FileExtensions() {
|
|
||||||
testPath := pathWithoutExt + ext
|
|
||||||
if scanner_utils.FileExists(testPath) {
|
|
||||||
return &testPath
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return unique.Handle[string](t).Value()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,42 +1,85 @@
|
|||||||
package media_type_test
|
package media_type
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/photoview/photoview/api/scanner/media_type"
|
|
||||||
"github.com/photoview/photoview/api/test_utils"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestMain(m *testing.M) {
|
type boolImage bool
|
||||||
os.Exit(test_utils.UnitTestRun(m))
|
|
||||||
|
const isImage boolImage = true
|
||||||
|
|
||||||
|
type boolVideo bool
|
||||||
|
|
||||||
|
const isVideo boolVideo = true
|
||||||
|
|
||||||
|
type boolWebCompatible bool
|
||||||
|
|
||||||
|
const isWebCompatible boolWebCompatible = true
|
||||||
|
|
||||||
|
type boolSupport bool
|
||||||
|
|
||||||
|
const isSupport boolSupport = true
|
||||||
|
|
||||||
|
func TestMediaTypeNoDeps(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
mtype MediaType
|
||||||
|
wantIsImage boolImage
|
||||||
|
wantIsVideo boolVideo
|
||||||
|
wantIsWebCompatible boolWebCompatible
|
||||||
|
wantIsSupport boolSupport
|
||||||
|
}{
|
||||||
|
// Unknown and unsupported types
|
||||||
|
{TypeUnknown, !isImage, !isVideo, !isWebCompatible, !isSupport},
|
||||||
|
{mediaType("application/pdf"), !isImage, !isVideo, !isWebCompatible, !isSupport},
|
||||||
|
|
||||||
|
// Raw media types
|
||||||
|
{mediaType("image/some-raw-type"), isImage, !isVideo, !isWebCompatible, isSupport},
|
||||||
|
{mediaType("video/some-video-type"), !isImage, isVideo, !isWebCompatible, isSupport},
|
||||||
|
|
||||||
|
// Generic types
|
||||||
|
{TypeImage, isImage, !isVideo, !isWebCompatible, isSupport},
|
||||||
|
{TypeVideo, !isImage, isVideo, !isWebCompatible, isSupport},
|
||||||
|
|
||||||
|
// Web-compatible image types
|
||||||
|
{TypeJPEG, isImage, !isVideo, isWebCompatible, isSupport},
|
||||||
|
{TypePNG, isImage, !isVideo, isWebCompatible, isSupport},
|
||||||
|
{TypeWebP, isImage, !isVideo, isWebCompatible, isSupport},
|
||||||
|
{TypeBMP, isImage, !isVideo, isWebCompatible, isSupport},
|
||||||
|
{TypeGIF, isImage, !isVideo, isWebCompatible, isSupport},
|
||||||
|
|
||||||
|
// Web-compatible video types
|
||||||
|
{TypeMP4, !isImage, isVideo, isWebCompatible, isSupport},
|
||||||
|
{TypeMPEG, !isImage, isVideo, isWebCompatible, isSupport},
|
||||||
|
{TypeOGG, !isImage, isVideo, isWebCompatible, isSupport},
|
||||||
|
{TypeWEBM, !isImage, isVideo, isWebCompatible, isSupport},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
gotImage := tc.mtype.IsImage()
|
||||||
|
if got, want := gotImage, bool(tc.wantIsImage); got != want {
|
||||||
|
t.Errorf("MediaType(%q).IsImage() = %v, want: %v", tc.mtype, got, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
gotVideo := tc.mtype.IsVideo()
|
||||||
|
if got, want := gotVideo, bool(tc.wantIsVideo); got != want {
|
||||||
|
t.Errorf("MediaType(%q).IsVideo() = %v, want: %v", tc.mtype, got, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
gotWebCompatible := tc.mtype.IsWebCompatible()
|
||||||
|
if got, want := gotWebCompatible, bool(tc.wantIsWebCompatible); got != want {
|
||||||
|
t.Errorf("MediaType(%q).IsWebCompatible() = %v, want: %v", tc.mtype, got, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
gotSupport := tc.mtype.IsSupported()
|
||||||
|
if got, want := gotSupport, bool(tc.wantIsSupport); got != want {
|
||||||
|
t.Errorf("MediaType(%q).IsSupported() = %v, want: %v", tc.mtype, got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMediaTypeIsFunctions(t *testing.T) {
|
func TestMediaTypeUnknown(t *testing.T) {
|
||||||
rawType := media_type.TypeCR2
|
var got MediaType
|
||||||
pngType := media_type.TypePng
|
if want := TypeUnknown; got != want {
|
||||||
mp4Type := media_type.TypeMP4
|
t.Errorf("MediaType zero value should be TypeUnknown, which is not")
|
||||||
|
}
|
||||||
assert.True(t, rawType.IsRaw())
|
|
||||||
assert.False(t, pngType.IsRaw())
|
|
||||||
|
|
||||||
assert.True(t, pngType.IsWebCompatible())
|
|
||||||
assert.False(t, rawType.IsWebCompatible())
|
|
||||||
|
|
||||||
assert.True(t, mp4Type.IsVideo())
|
|
||||||
assert.False(t, pngType.IsVideo())
|
|
||||||
|
|
||||||
assert.True(t, pngType.IsBasicTypeSupported())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMediaTypeFromExtension(t *testing.T) {
|
|
||||||
pngType, found := media_type.GetExtensionMediaType(".PNG")
|
|
||||||
|
|
||||||
assert.True(t, found)
|
|
||||||
assert.Equal(t, media_type.TypePng, pngType)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMediaTypeGetExtensions(t *testing.T) {
|
|
||||||
assert.ElementsMatch(t, []string{".jpg", ".JPG", ".jpeg", ".JPEG"}, media_type.TypeJpeg.FileExtensions())
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package scanner_cache
|
package scanner_cache
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
@@ -8,7 +9,6 @@ import (
|
|||||||
|
|
||||||
"github.com/photoview/photoview/api/scanner/media_type"
|
"github.com/photoview/photoview/api/scanner/media_type"
|
||||||
"github.com/photoview/photoview/api/scanner/scanner_utils"
|
"github.com/photoview/photoview/api/scanner/scanner_utils"
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type AlbumScannerCache struct {
|
type AlbumScannerCache struct {
|
||||||
@@ -69,24 +69,21 @@ func (c *AlbumScannerCache) AlbumContainsPhotos(path string) *bool {
|
|||||||
// (c.photo_types)[path] = content_type
|
// (c.photo_types)[path] = content_type
|
||||||
// }
|
// }
|
||||||
|
|
||||||
func (c *AlbumScannerCache) GetMediaType(path string) (*media_type.MediaType, error) {
|
func (c *AlbumScannerCache) GetMediaType(path string) (media_type.MediaType, error) {
|
||||||
c.mutex.Lock()
|
c.mutex.Lock()
|
||||||
defer c.mutex.Unlock()
|
defer c.mutex.Unlock()
|
||||||
|
|
||||||
result, found := c.photo_types[path]
|
result, found := c.photo_types[path]
|
||||||
if found {
|
if found {
|
||||||
// log.Printf("Image cache hit: %s\n", path)
|
return result, nil
|
||||||
return &result, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mediaType, err := media_type.GetMediaType(path)
|
mediaType := media_type.GetMediaType(path)
|
||||||
if err != nil {
|
if mediaType == media_type.TypeUnknown {
|
||||||
return nil, errors.Wrapf(err, "get media type (%s)", path)
|
return mediaType, fmt.Errorf("unknown media type (%s)", path)
|
||||||
}
|
}
|
||||||
|
|
||||||
if mediaType != nil {
|
c.photo_types[path] = mediaType
|
||||||
(c.photo_types)[path] = *mediaType
|
|
||||||
}
|
|
||||||
|
|
||||||
return mediaType, nil
|
return mediaType, nil
|
||||||
}
|
}
|
||||||
@@ -122,7 +119,11 @@ func (c *AlbumScannerCache) IsPathMedia(mediaPath string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if mediaType != nil {
|
if !mediaType.IsSupported() {
|
||||||
|
log.Printf("Unsupported media type %q for file: %s\n", mediaType, mediaPath)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// Make sure file isn't empty
|
// Make sure file isn't empty
|
||||||
fileStats, err := os.Stat(mediaPath)
|
fileStats, err := os.Stat(mediaPath)
|
||||||
if err != nil || fileStats.Size() == 0 {
|
if err != nil || fileStats.Size() == 0 {
|
||||||
@@ -130,8 +131,4 @@ func (c *AlbumScannerCache) IsPathMedia(mediaPath string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf("File is not a supported media %s\n", mediaPath)
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,13 @@
|
|||||||
package processing_tasks
|
package processing_tasks
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"path"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/photoview/photoview/api/scanner/media_encoding"
|
"github.com/photoview/photoview/api/scanner/media_encoding"
|
||||||
"github.com/photoview/photoview/api/scanner/media_type"
|
"github.com/photoview/photoview/api/scanner/media_type"
|
||||||
"github.com/photoview/photoview/api/scanner/scanner_task"
|
"github.com/photoview/photoview/api/scanner/scanner_task"
|
||||||
"github.com/photoview/photoview/api/scanner/scanner_utils"
|
|
||||||
"github.com/photoview/photoview/api/utils"
|
"github.com/photoview/photoview/api/utils"
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type CounterpartFilesTask struct {
|
type CounterpartFilesTask struct {
|
||||||
@@ -19,14 +15,14 @@ type CounterpartFilesTask struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t CounterpartFilesTask) MediaFound(ctx scanner_task.TaskContext, fileInfo fs.FileInfo, mediaPath string) (skip bool, err error) {
|
func (t CounterpartFilesTask) MediaFound(ctx scanner_task.TaskContext, fileInfo fs.FileInfo, mediaPath string) (skip bool, err error) {
|
||||||
ext := filepath.Ext(mediaPath)
|
fileType := media_type.GetMediaType(mediaPath)
|
||||||
fileExtType, found := media_type.GetExtensionMediaType(ext)
|
|
||||||
if !found {
|
if !fileType.IsSupported() {
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if utils.EnvDisableRawProcessing.GetBool() {
|
if utils.EnvDisableRawProcessing.GetBool() {
|
||||||
if fileExtType.IsRaw() {
|
if !fileType.IsWebCompatible() {
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,14 +30,12 @@ func (t CounterpartFilesTask) MediaFound(ctx scanner_task.TaskContext, fileInfo
|
|||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if !fileExtType.IsBasicTypeSupported() {
|
if fileType.IsWebCompatible() {
|
||||||
return false, nil
|
_, existed := media_type.FindRawCounterpart(mediaPath)
|
||||||
}
|
if existed {
|
||||||
|
|
||||||
rawPath := media_type.RawCounterpart(mediaPath)
|
|
||||||
if rawPath != nil {
|
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
@@ -50,38 +44,19 @@ func (t CounterpartFilesTask) BeforeProcessMedia(ctx scanner_task.TaskContext, m
|
|||||||
|
|
||||||
mediaType, err := ctx.GetCache().GetMediaType(mediaData.Media.Path)
|
mediaType, err := ctx.GetCache().GetMediaType(mediaData.Media.Path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ctx, errors.Wrap(err, "scan for counterpart file")
|
return ctx, fmt.Errorf("scan for counterpart file error: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !mediaType.IsRaw() {
|
if mediaType.IsWebCompatible() {
|
||||||
return ctx, nil
|
return ctx, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
counterpartFile := scanForCompressedCounterpartFile(mediaData.Media.Path)
|
counterpartFile, ok := media_type.FindWebCounterpart(mediaData.Media.Path)
|
||||||
if counterpartFile != nil {
|
if !ok {
|
||||||
mediaData.CounterpartPath = counterpartFile
|
return ctx, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mediaData.CounterpartPath = &counterpartFile
|
||||||
|
|
||||||
return ctx, nil
|
return ctx, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func scanForCompressedCounterpartFile(imagePath string) *string {
|
|
||||||
ext := filepath.Ext(imagePath)
|
|
||||||
fileExtType, found := media_type.GetExtensionMediaType(ext)
|
|
||||||
|
|
||||||
if found {
|
|
||||||
if fileExtType.IsBasicTypeSupported() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pathWithoutExt := strings.TrimSuffix(imagePath, path.Ext(imagePath))
|
|
||||||
for _, ext := range media_type.TypeJpeg.FileExtensions() {
|
|
||||||
testPath := pathWithoutExt + ext
|
|
||||||
if scanner_utils.FileExists(testPath) {
|
|
||||||
return &testPath
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestCounterpartFilesTaskMediaFound(t *testing.T) {
|
func TestCounterpartFilesTaskMediaFound(t *testing.T) {
|
||||||
|
mediaPath := test_utils.PathFromAPIRoot("scanner/test_media/real_media")
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
file string
|
file string
|
||||||
@@ -21,56 +23,54 @@ func TestCounterpartFilesTaskMediaFound(t *testing.T) {
|
|||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "StandaloneProcessRaw",
|
name: "StandaloneProcessRaw",
|
||||||
file: "standalone.jpg",
|
file: "standalone_jpg.jpg",
|
||||||
disableRawProcessing: false,
|
disableRawProcessing: false,
|
||||||
wantSkip: false,
|
wantSkip: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "StandaloneNotProcessRaw",
|
name: "StandaloneNotProcessRaw",
|
||||||
file: "standalone.jpg",
|
file: "standalone_jpg.jpg",
|
||||||
disableRawProcessing: true,
|
disableRawProcessing: true,
|
||||||
wantSkip: false,
|
wantSkip: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "RawJpegProcessRaw",
|
name: "RawJpegProcessRaw",
|
||||||
file: "fujifilm_raw.jpg",
|
file: "raw_with_jpg.jpg",
|
||||||
disableRawProcessing: false,
|
disableRawProcessing: false,
|
||||||
wantSkip: true,
|
wantSkip: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "RawJpegNotProcessRaw",
|
name: "RawJpegNotProcessRaw",
|
||||||
file: "fujifilm_raw.jpg",
|
file: "raw_with_jpg.jpg",
|
||||||
disableRawProcessing: true,
|
disableRawProcessing: true,
|
||||||
wantSkip: false,
|
wantSkip: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "RawProcessRaw",
|
name: "RawProcessRaw",
|
||||||
file: "fujifilm_raw.raf",
|
file: "raw_with_jpg.tiff",
|
||||||
disableRawProcessing: false,
|
disableRawProcessing: false,
|
||||||
wantSkip: false,
|
wantSkip: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "RawNotProcessRaw",
|
name: "RawNotProcessRaw",
|
||||||
file: "fujifilm_raw.raf",
|
file: "raw_with_jpg.tiff",
|
||||||
disableRawProcessing: true,
|
disableRawProcessing: true,
|
||||||
wantSkip: true,
|
wantSkip: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "UnknownProcessRaw",
|
name: "UnknownProcessRaw",
|
||||||
file: "file.unknown",
|
file: "file.pdf",
|
||||||
disableRawProcessing: false,
|
disableRawProcessing: false,
|
||||||
wantSkip: true,
|
wantSkip: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "UnknownNotProcessRaw",
|
name: "UnknownNotProcessRaw",
|
||||||
file: "file.unknown",
|
file: "file.pdf",
|
||||||
disableRawProcessing: true,
|
disableRawProcessing: true,
|
||||||
wantSkip: true,
|
wantSkip: true,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
mediaPath := test_utils.PathFromAPIRoot("scanner/test_media/fake_media")
|
|
||||||
|
|
||||||
for _, tc := range tests {
|
for _, tc := range tests {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
done := test_utils.SetEnv(string(utils.EnvDisableRawProcessing), fmt.Sprintf("%v", tc.disableRawProcessing))
|
done := test_utils.SetEnv(string(utils.EnvDisableRawProcessing), fmt.Sprintf("%v", tc.disableRawProcessing))
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ func (t ProcessVideoTask) ProcessMedia(ctx scanner_task.TaskContext, mediaData *
|
|||||||
|
|
||||||
videoType, err := mediaData.ContentType()
|
videoType, err := mediaData.ContentType()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return []*models.MediaURL{}, errors.Wrap(err, "error getting video content type")
|
return []*models.MediaURL{}, fmt.Errorf("getting video content type error: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if videoOriginalURL == nil && videoType.IsWebCompatible() {
|
if videoOriginalURL == nil && videoType.IsWebCompatible() {
|
||||||
@@ -75,7 +75,7 @@ func (t ProcessVideoTask) ProcessMedia(ctx scanner_task.TaskContext, mediaData *
|
|||||||
Width: webMetadata.Width,
|
Width: webMetadata.Width,
|
||||||
Height: webMetadata.Height,
|
Height: webMetadata.Height,
|
||||||
Purpose: models.MediaOriginal,
|
Purpose: models.MediaOriginal,
|
||||||
ContentType: string(*videoType),
|
ContentType: videoType.String(),
|
||||||
FileSize: fileStats.Size(),
|
FileSize: fileStats.Size(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ func saveOriginalPhotoToDB(tx *gorm.DB, photo *models.Media, imageData *media_en
|
|||||||
Width: photoDimensions.Width,
|
Width: photoDimensions.Width,
|
||||||
Height: photoDimensions.Height,
|
Height: photoDimensions.Height,
|
||||||
Purpose: models.MediaOriginal,
|
Purpose: models.MediaOriginal,
|
||||||
ContentType: string(*contentType),
|
ContentType: contentType.String(),
|
||||||
FileSize: fileStats.Size(),
|
FileSize: fileStats.Size(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ func (t SidecarTask) AfterMediaFound(ctx scanner_task.TaskContext, media *models
|
|||||||
return errors.Wrap(err, "scan for sidecar file")
|
return errors.Wrap(err, "scan for sidecar file")
|
||||||
}
|
}
|
||||||
|
|
||||||
if !mediaType.IsRaw() {
|
if mediaType.IsWebCompatible() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,7 +58,7 @@ func (t SidecarTask) ProcessMedia(ctx scanner_task.TaskContext, mediaData *media
|
|||||||
return []*models.MediaURL{}, errors.Wrap(err, "sidecar task, process media")
|
return []*models.MediaURL{}, errors.Wrap(err, "sidecar task, process media")
|
||||||
}
|
}
|
||||||
|
|
||||||
if !mediaType.IsRaw() {
|
if mediaType.IsWebCompatible() {
|
||||||
return []*models.MediaURL{}, nil
|
return []*models.MediaURL{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
5
scripts/benchmark_api.sh
Executable file
5
scripts/benchmark_api.sh
Executable file
@@ -0,0 +1,5 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -euxo pipefail
|
||||||
|
|
||||||
|
cd "$(dirname $0)/../api"
|
||||||
|
go test ./... -bench . -benchmem
|
||||||
@@ -11,5 +11,8 @@ apt-get install -y libdav1d-dev:${DEB_HOST_ARCH} libde265-dev:${DEB_HOST_ARCH} l
|
|||||||
apt-get install -y \
|
apt-get install -y \
|
||||||
libdlib-dev:${DEB_HOST_ARCH} libblas-dev:${DEB_HOST_ARCH} libatlas-base-dev:${DEB_HOST_ARCH} liblapack-dev:${DEB_HOST_ARCH} libjpeg62-turbo-dev:${DEB_HOST_ARCH}
|
libdlib-dev:${DEB_HOST_ARCH} libblas-dev:${DEB_HOST_ARCH} libatlas-base-dev:${DEB_HOST_ARCH} liblapack-dev:${DEB_HOST_ARCH} libjpeg62-turbo-dev:${DEB_HOST_ARCH}
|
||||||
|
|
||||||
|
# Install gomagic dependencies
|
||||||
|
apt-get install -y libmagic-dev:${DEB_HOST_ARCH}
|
||||||
|
|
||||||
# Install tools for development
|
# Install tools for development
|
||||||
apt-get install -y reflex sqlite3
|
apt-get install -y reflex sqlite3
|
||||||
|
|||||||
@@ -12,3 +12,5 @@ apt-get install -y libjpeg62-turbo liblcms2-2 zlib1g libgomp1
|
|||||||
apt-get install -y libjxl0.7 liblcms2-2 liblqr-1-0 libdjvulibre21 libjpeg62-turbo libopenjp2-7 libopenexr-3-1-30 libpng16-16 libtiff6 libwebpmux3 libwebpdemux2 libwebp7 libxml2 zlib1g liblzma5 libbz2-1.0 libgomp1
|
apt-get install -y libjxl0.7 liblcms2-2 liblqr-1-0 libdjvulibre21 libjpeg62-turbo libopenjp2-7 libopenexr-3-1-30 libpng16-16 libtiff6 libwebpmux3 libwebpdemux2 libwebp7 libxml2 zlib1g liblzma5 libbz2-1.0 libgomp1
|
||||||
# go-face dependencies
|
# go-face dependencies
|
||||||
apt-get install -y libdlib19.1 libblas3 liblapack3 libjpeg62-turbo
|
apt-get install -y libdlib19.1 libblas3 liblapack3 libjpeg62-turbo
|
||||||
|
# libmagic dependencies
|
||||||
|
apt-get install -y libmagic1
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -euxo pipefail
|
set -euxo pipefail
|
||||||
|
|
||||||
cd $(dirname $0)/../api
|
cd "$(dirname $0)/../api"
|
||||||
go test ./... -v -database -filesystem -p 1 -coverprofile=coverage.txt -covermode=atomic 2>&1 | tee >(go-junit-report >test-api-coverage-report.xml)
|
go test ./... -v -database -filesystem -p 1 -coverprofile=coverage.txt -covermode=atomic 2>&1 | tee >(go-junit-report >test-api-coverage-report.xml)
|
||||||
|
|||||||
Reference in New Issue
Block a user