Backend: Refactor thumbnail package #157
Signed-off-by: Michael Mayer <michael@liquidbytes.net>
This commit is contained in:
parent
17f6cd9593
commit
e43983d579
|
@ -12,9 +12,11 @@ import (
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/entity"
|
"github.com/photoprism/photoprism/internal/entity"
|
||||||
"github.com/photoprism/photoprism/internal/event"
|
"github.com/photoprism/photoprism/internal/event"
|
||||||
|
"github.com/photoprism/photoprism/internal/file"
|
||||||
"github.com/photoprism/photoprism/internal/form"
|
"github.com/photoprism/photoprism/internal/form"
|
||||||
"github.com/photoprism/photoprism/internal/photoprism"
|
|
||||||
"github.com/photoprism/photoprism/internal/query"
|
"github.com/photoprism/photoprism/internal/query"
|
||||||
|
"github.com/photoprism/photoprism/internal/rnd"
|
||||||
|
"github.com/photoprism/photoprism/internal/thumb"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/gin-gonic/gin/binding"
|
"github.com/gin-gonic/gin/binding"
|
||||||
|
@ -339,7 +341,7 @@ func DownloadAlbum(router *gin.RouterGroup, conf *config.Config) {
|
||||||
}
|
}
|
||||||
|
|
||||||
zipPath := path.Join(conf.ExportPath(), "album")
|
zipPath := path.Join(conf.ExportPath(), "album")
|
||||||
zipToken := util.RandomToken(3)
|
zipToken := rnd.Token(3)
|
||||||
zipBaseName := fmt.Sprintf("%s-%s.zip", strings.Title(a.AlbumSlug), zipToken)
|
zipBaseName := fmt.Sprintf("%s-%s.zip", strings.Title(a.AlbumSlug), zipToken)
|
||||||
zipFileName := path.Join(zipPath, zipBaseName)
|
zipFileName := path.Join(zipPath, zipBaseName)
|
||||||
|
|
||||||
|
@ -362,21 +364,21 @@ func DownloadAlbum(router *gin.RouterGroup, conf *config.Config) {
|
||||||
zipWriter := zip.NewWriter(newZipFile)
|
zipWriter := zip.NewWriter(newZipFile)
|
||||||
defer zipWriter.Close()
|
defer zipWriter.Close()
|
||||||
|
|
||||||
for _, file := range p {
|
for _, f := range p {
|
||||||
fileName := path.Join(conf.OriginalsPath(), file.FileName)
|
fileName := path.Join(conf.OriginalsPath(), f.FileName)
|
||||||
fileAlias := file.DownloadFileName()
|
fileAlias := f.DownloadFileName()
|
||||||
|
|
||||||
if util.Exists(fileName) {
|
if file.Exists(fileName) {
|
||||||
if err := addFileToZip(zipWriter, fileName, fileAlias); err != nil {
|
if err := addFileToZip(zipWriter, fileName, fileAlias); err != nil {
|
||||||
log.Error(err)
|
log.Error(err)
|
||||||
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": util.UcFirst("failed to create zip file")})
|
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": util.UcFirst("failed to create zip file")})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Infof("album: added \"%s\" as \"%s\"", file.FileName, fileAlias)
|
log.Infof("album: added \"%s\" as \"%s\"", f.FileName, fileAlias)
|
||||||
} else {
|
} else {
|
||||||
log.Warnf("album: \"%s\" is missing", file.FileName)
|
log.Warnf("album: \"%s\" is missing", f.FileName)
|
||||||
file.FileMissing = true
|
f.FileMissing = true
|
||||||
conf.Db().Save(&file)
|
conf.Db().Save(&f)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -385,7 +387,7 @@ func DownloadAlbum(router *gin.RouterGroup, conf *config.Config) {
|
||||||
zipWriter.Close()
|
zipWriter.Close()
|
||||||
newZipFile.Close()
|
newZipFile.Close()
|
||||||
|
|
||||||
if !util.Exists(zipFileName) {
|
if !file.Exists(zipFileName) {
|
||||||
log.Errorf("could not find zip file: %s", zipFileName)
|
log.Errorf("could not find zip file: %s", zipFileName)
|
||||||
c.Data(404, "image/svg+xml", photoIconSvg)
|
c.Data(404, "image/svg+xml", photoIconSvg)
|
||||||
return
|
return
|
||||||
|
@ -411,7 +413,7 @@ func AlbumThumbnail(router *gin.RouterGroup, conf *config.Config) {
|
||||||
typeName := c.Param("type")
|
typeName := c.Param("type")
|
||||||
uuid := c.Param("uuid")
|
uuid := c.Param("uuid")
|
||||||
|
|
||||||
thumbType, ok := photoprism.ThumbnailTypes[typeName]
|
thumbType, ok := thumb.Types[typeName]
|
||||||
|
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Errorf("invalid type: %s", typeName)
|
log.Errorf("invalid type: %s", typeName)
|
||||||
|
@ -421,7 +423,7 @@ func AlbumThumbnail(router *gin.RouterGroup, conf *config.Config) {
|
||||||
|
|
||||||
q := query.New(conf.OriginalsPath(), conf.Db())
|
q := query.New(conf.OriginalsPath(), conf.Db())
|
||||||
|
|
||||||
file, err := q.FindAlbumThumbByUUID(uuid)
|
f, err := q.FindAlbumThumbByUUID(uuid)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Debugf("album has no photos yet, using generic thumb image: %s", uuid)
|
log.Debugf("album has no photos yet, using generic thumb image: %s", uuid)
|
||||||
|
@ -429,21 +431,21 @@ func AlbumThumbnail(router *gin.RouterGroup, conf *config.Config) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
fileName := path.Join(conf.OriginalsPath(), file.FileName)
|
fileName := path.Join(conf.OriginalsPath(), f.FileName)
|
||||||
|
|
||||||
if !util.Exists(fileName) {
|
if !file.Exists(fileName) {
|
||||||
log.Errorf("could not find original for thumbnail: %s", fileName)
|
log.Errorf("could not find original for thumbnail: %s", fileName)
|
||||||
c.Data(http.StatusNotFound, "image/svg+xml", photoIconSvg)
|
c.Data(http.StatusNotFound, "image/svg+xml", photoIconSvg)
|
||||||
|
|
||||||
// Set missing flag so that the file doesn't show up in search results anymore
|
// Set missing flag so that the file doesn't show up in search results anymore
|
||||||
file.FileMissing = true
|
f.FileMissing = true
|
||||||
conf.Db().Save(&file)
|
conf.Db().Save(&f)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if thumbnail, err := photoprism.ThumbnailFromFile(fileName, file.FileHash, conf.ThumbnailsPath(), thumbType.Width, thumbType.Height, thumbType.Options...); err == nil {
|
if thumbnail, err := thumb.FromFile(fileName, f.FileHash, conf.ThumbnailsPath(), thumbType.Width, thumbType.Height, thumbType.Options...); err == nil {
|
||||||
if c.Query("download") != "" {
|
if c.Query("download") != "" {
|
||||||
downloadFileName := file.DownloadFileName()
|
downloadFileName := f.DownloadFileName()
|
||||||
|
|
||||||
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", downloadFileName))
|
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", downloadFileName))
|
||||||
}
|
}
|
||||||
|
|
|
@ -90,7 +90,6 @@ func BatchPhotosRestore(router *gin.RouterGroup, conf *config.Config) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// POST /api/v1/batch/albums/delete
|
// POST /api/v1/batch/albums/delete
|
||||||
func BatchAlbumsDelete(router *gin.RouterGroup, conf *config.Config) {
|
func BatchAlbumsDelete(router *gin.RouterGroup, conf *config.Config) {
|
||||||
router.POST("/batch/albums/delete", func(c *gin.Context) {
|
router.POST("/batch/albums/delete", func(c *gin.Context) {
|
||||||
|
|
|
@ -5,8 +5,8 @@ import (
|
||||||
"path"
|
"path"
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/config"
|
"github.com/photoprism/photoprism/internal/config"
|
||||||
|
"github.com/photoprism/photoprism/internal/file"
|
||||||
"github.com/photoprism/photoprism/internal/query"
|
"github.com/photoprism/photoprism/internal/query"
|
||||||
"github.com/photoprism/photoprism/internal/util"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
@ -24,26 +24,26 @@ func GetDownload(router *gin.RouterGroup, conf *config.Config) {
|
||||||
fileHash := c.Param("hash")
|
fileHash := c.Param("hash")
|
||||||
|
|
||||||
q := query.New(conf.OriginalsPath(), conf.Db())
|
q := query.New(conf.OriginalsPath(), conf.Db())
|
||||||
file, err := q.FindFileByHash(fileHash)
|
f, err := q.FindFileByHash(fileHash)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.AbortWithStatusJSON(404, gin.H{"error": err.Error()})
|
c.AbortWithStatusJSON(404, gin.H{"error": err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
fileName := path.Join(conf.OriginalsPath(), file.FileName)
|
fileName := path.Join(conf.OriginalsPath(), f.FileName)
|
||||||
|
|
||||||
if !util.Exists(fileName) {
|
if !file.Exists(fileName) {
|
||||||
log.Errorf("could not find original: %s", fileHash)
|
log.Errorf("could not find original: %s", fileHash)
|
||||||
c.Data(404, "image/svg+xml", photoIconSvg)
|
c.Data(404, "image/svg+xml", photoIconSvg)
|
||||||
|
|
||||||
// Set missing flag so that the file doesn't show up in search results anymore
|
// Set missing flag so that the file doesn't show up in search results anymore
|
||||||
file.FileMissing = true
|
f.FileMissing = true
|
||||||
conf.Db().Save(&file)
|
conf.Db().Save(&f)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
downloadFileName := file.DownloadFileName()
|
downloadFileName := f.DownloadFileName()
|
||||||
|
|
||||||
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", downloadFileName))
|
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", downloadFileName))
|
||||||
|
|
||||||
|
|
|
@ -8,11 +8,10 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/photoprism/photoprism/internal/config"
|
"github.com/photoprism/photoprism/internal/config"
|
||||||
"github.com/photoprism/photoprism/internal/event"
|
"github.com/photoprism/photoprism/internal/event"
|
||||||
"github.com/photoprism/photoprism/internal/util"
|
"github.com/photoprism/photoprism/internal/file"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/photoprism/photoprism/internal/photoprism"
|
"github.com/photoprism/photoprism/internal/photoprism"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -61,7 +60,7 @@ func StartImport(router *gin.RouterGroup, conf *config.Config) {
|
||||||
|
|
||||||
imp.Start(path)
|
imp.Start(path)
|
||||||
|
|
||||||
if subPath != "" && path != conf.ImportPath() && util.DirectoryIsEmpty(path) {
|
if subPath != "" && path != conf.ImportPath() && file.IsEmpty(path) {
|
||||||
if err := os.Remove(path); err != nil {
|
if err := os.Remove(path); err != nil {
|
||||||
log.Errorf("import: could not deleted empty directory \"%s\": %s", path, err)
|
log.Errorf("import: could not deleted empty directory \"%s\": %s", path, err)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -55,11 +55,6 @@ func StartIndexing(router *gin.RouterGroup, conf *config.Config) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set thumbnails JPEG quality and size
|
|
||||||
photoprism.JpegQuality = conf.ThumbQuality()
|
|
||||||
photoprism.MaxThumbWidth = conf.ThumbSize()
|
|
||||||
photoprism.MaxThumbHeight = conf.ThumbSize()
|
|
||||||
|
|
||||||
path := conf.OriginalsPath()
|
path := conf.OriginalsPath()
|
||||||
|
|
||||||
event.Info(fmt.Sprintf("indexing photos in \"%s\"", filepath.Base(path)))
|
event.Info(fmt.Sprintf("indexing photos in \"%s\"", filepath.Base(path)))
|
||||||
|
|
|
@ -12,9 +12,10 @@ import (
|
||||||
"github.com/gin-gonic/gin/binding"
|
"github.com/gin-gonic/gin/binding"
|
||||||
"github.com/photoprism/photoprism/internal/config"
|
"github.com/photoprism/photoprism/internal/config"
|
||||||
"github.com/photoprism/photoprism/internal/event"
|
"github.com/photoprism/photoprism/internal/event"
|
||||||
|
"github.com/photoprism/photoprism/internal/file"
|
||||||
"github.com/photoprism/photoprism/internal/form"
|
"github.com/photoprism/photoprism/internal/form"
|
||||||
"github.com/photoprism/photoprism/internal/photoprism"
|
|
||||||
"github.com/photoprism/photoprism/internal/query"
|
"github.com/photoprism/photoprism/internal/query"
|
||||||
|
"github.com/photoprism/photoprism/internal/thumb"
|
||||||
"github.com/photoprism/photoprism/internal/util"
|
"github.com/photoprism/photoprism/internal/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -123,7 +124,7 @@ func LabelThumbnail(router *gin.RouterGroup, conf *config.Config) {
|
||||||
labelUUID := c.Param("uuid")
|
labelUUID := c.Param("uuid")
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
|
|
||||||
thumbType, ok := photoprism.ThumbnailTypes[typeName]
|
thumbType, ok := thumb.Types[typeName]
|
||||||
|
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Errorf("invalid type: %s", typeName)
|
log.Errorf("invalid type: %s", typeName)
|
||||||
|
@ -142,7 +143,7 @@ func LabelThumbnail(router *gin.RouterGroup, conf *config.Config) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
file, err := q.FindLabelThumbByUUID(labelUUID)
|
f, err := q.FindLabelThumbByUUID(labelUUID)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf(err.Error())
|
log.Errorf(err.Error())
|
||||||
|
@ -150,19 +151,19 @@ func LabelThumbnail(router *gin.RouterGroup, conf *config.Config) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
fileName := path.Join(conf.OriginalsPath(), file.FileName)
|
fileName := path.Join(conf.OriginalsPath(), f.FileName)
|
||||||
|
|
||||||
if !util.Exists(fileName) {
|
if !file.Exists(fileName) {
|
||||||
log.Errorf("could not find original for thumbnail: %s", fileName)
|
log.Errorf("could not find original for thumbnail: %s", fileName)
|
||||||
c.Data(http.StatusOK, "image/svg+xml", labelIconSvg)
|
c.Data(http.StatusOK, "image/svg+xml", labelIconSvg)
|
||||||
|
|
||||||
// Set missing flag so that the file doesn't show up in search results anymore
|
// Set missing flag so that the file doesn't show up in search results anymore
|
||||||
file.FileMissing = true
|
f.FileMissing = true
|
||||||
conf.Db().Save(&file)
|
conf.Db().Save(&f)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if thumbnail, err := photoprism.ThumbnailFromFile(fileName, file.FileHash, conf.ThumbnailsPath(), thumbType.Width, thumbType.Height, thumbType.Options...); err == nil {
|
if thumbnail, err := thumb.FromFile(fileName, f.FileHash, conf.ThumbnailsPath(), thumbType.Width, thumbType.Height, thumbType.Options...); err == nil {
|
||||||
thumbData, err := ioutil.ReadFile(thumbnail)
|
thumbData, err := ioutil.ReadFile(thumbnail)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -7,6 +7,7 @@ import (
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/config"
|
"github.com/photoprism/photoprism/internal/config"
|
||||||
"github.com/photoprism/photoprism/internal/event"
|
"github.com/photoprism/photoprism/internal/event"
|
||||||
|
"github.com/photoprism/photoprism/internal/file"
|
||||||
"github.com/photoprism/photoprism/internal/query"
|
"github.com/photoprism/photoprism/internal/query"
|
||||||
"github.com/photoprism/photoprism/internal/util"
|
"github.com/photoprism/photoprism/internal/util"
|
||||||
|
|
||||||
|
@ -73,26 +74,26 @@ func UpdatePhoto(router *gin.RouterGroup, conf *config.Config) {
|
||||||
func GetPhotoDownload(router *gin.RouterGroup, conf *config.Config) {
|
func GetPhotoDownload(router *gin.RouterGroup, conf *config.Config) {
|
||||||
router.GET("/photos/:uuid/download", func(c *gin.Context) {
|
router.GET("/photos/:uuid/download", func(c *gin.Context) {
|
||||||
q := query.New(conf.OriginalsPath(), conf.Db())
|
q := query.New(conf.OriginalsPath(), conf.Db())
|
||||||
file, err := q.FindFileByPhotoUUID(c.Param("uuid"))
|
f, err := q.FindFileByPhotoUUID(c.Param("uuid"))
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.AbortWithStatusJSON(404, gin.H{"error": err.Error()})
|
c.AbortWithStatusJSON(404, gin.H{"error": err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
fileName := path.Join(conf.OriginalsPath(), file.FileName)
|
fileName := path.Join(conf.OriginalsPath(), f.FileName)
|
||||||
|
|
||||||
if !util.Exists(fileName) {
|
if !file.Exists(fileName) {
|
||||||
log.Errorf("could not find original: %s", c.Param("uuid"))
|
log.Errorf("could not find original: %s", c.Param("uuid"))
|
||||||
c.Data(404, "image/svg+xml", photoIconSvg)
|
c.Data(404, "image/svg+xml", photoIconSvg)
|
||||||
|
|
||||||
// Set missing flag so that the file doesn't show up in search results anymore
|
// Set missing flag so that the file doesn't show up in search results anymore
|
||||||
file.FileMissing = true
|
f.FileMissing = true
|
||||||
conf.Db().Save(&file)
|
conf.Db().Save(&f)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
downloadFileName := file.DownloadFileName()
|
downloadFileName := f.DownloadFileName()
|
||||||
|
|
||||||
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", downloadFileName))
|
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", downloadFileName))
|
||||||
|
|
||||||
|
|
|
@ -5,12 +5,11 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"path"
|
"path"
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/config"
|
|
||||||
"github.com/photoprism/photoprism/internal/query"
|
|
||||||
"github.com/photoprism/photoprism/internal/util"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/photoprism/photoprism/internal/photoprism"
|
"github.com/photoprism/photoprism/internal/config"
|
||||||
|
"github.com/photoprism/photoprism/internal/file"
|
||||||
|
"github.com/photoprism/photoprism/internal/query"
|
||||||
|
"github.com/photoprism/photoprism/internal/thumb"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GET /api/v1/thumbnails/:hash/:type
|
// GET /api/v1/thumbnails/:hash/:type
|
||||||
|
@ -23,7 +22,7 @@ func GetThumbnail(router *gin.RouterGroup, conf *config.Config) {
|
||||||
fileHash := c.Param("hash")
|
fileHash := c.Param("hash")
|
||||||
typeName := c.Param("type")
|
typeName := c.Param("type")
|
||||||
|
|
||||||
thumbType, ok := photoprism.ThumbnailTypes[typeName]
|
thumbType, ok := thumb.Types[typeName]
|
||||||
|
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Errorf("invalid type: %s", typeName)
|
log.Errorf("invalid type: %s", typeName)
|
||||||
|
@ -32,28 +31,28 @@ func GetThumbnail(router *gin.RouterGroup, conf *config.Config) {
|
||||||
}
|
}
|
||||||
|
|
||||||
q := query.New(conf.OriginalsPath(), conf.Db())
|
q := query.New(conf.OriginalsPath(), conf.Db())
|
||||||
file, err := q.FindFileByHash(fileHash)
|
f, err := q.FindFileByHash(fileHash)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
fileName := path.Join(conf.OriginalsPath(), file.FileName)
|
fileName := path.Join(conf.OriginalsPath(), f.FileName)
|
||||||
|
|
||||||
if !util.Exists(fileName) {
|
if !file.Exists(fileName) {
|
||||||
log.Errorf("could not find original for thumbnail: %s", fileName)
|
log.Errorf("could not find original for thumbnail: %s", fileName)
|
||||||
c.Data(http.StatusNotFound, "image/svg+xml", photoIconSvg)
|
c.Data(http.StatusNotFound, "image/svg+xml", photoIconSvg)
|
||||||
|
|
||||||
// Set missing flag so that the file doesn't show up in search results anymore
|
// Set missing flag so that the file doesn't show up in search results anymore
|
||||||
file.FileMissing = true
|
f.FileMissing = true
|
||||||
conf.Db().Save(&file)
|
conf.Db().Save(&f)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if thumbnail, err := photoprism.ThumbnailFromFile(fileName, file.FileHash, conf.ThumbnailsPath(), thumbType.Width, thumbType.Height, thumbType.Options...); err == nil {
|
if thumbnail, err := thumb.FromFile(fileName, f.FileHash, conf.ThumbnailsPath(), thumbType.Width, thumbType.Height, thumbType.Options...); err == nil {
|
||||||
if c.Query("download") != "" {
|
if c.Query("download") != "" {
|
||||||
downloadFileName := file.DownloadFileName()
|
downloadFileName := f.DownloadFileName()
|
||||||
|
|
||||||
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", downloadFileName))
|
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", downloadFileName))
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,10 +12,10 @@ import (
|
||||||
"github.com/disintegration/imaging"
|
"github.com/disintegration/imaging"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/photoprism/photoprism/internal/config"
|
"github.com/photoprism/photoprism/internal/config"
|
||||||
|
"github.com/photoprism/photoprism/internal/file"
|
||||||
"github.com/photoprism/photoprism/internal/form"
|
"github.com/photoprism/photoprism/internal/form"
|
||||||
"github.com/photoprism/photoprism/internal/photoprism"
|
|
||||||
"github.com/photoprism/photoprism/internal/query"
|
"github.com/photoprism/photoprism/internal/query"
|
||||||
"github.com/photoprism/photoprism/internal/util"
|
"github.com/photoprism/photoprism/internal/thumb"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GET /api/v1/preview
|
// GET /api/v1/preview
|
||||||
|
@ -33,7 +33,7 @@ func GetPreview(router *gin.RouterGroup, conf *config.Config) {
|
||||||
|
|
||||||
previewFilename := fmt.Sprintf("%s/%s.jpg", thumbPath, t[6:8])
|
previewFilename := fmt.Sprintf("%s/%s.jpg", thumbPath, t[6:8])
|
||||||
|
|
||||||
if util.Exists(previewFilename) {
|
if file.Exists(previewFilename) {
|
||||||
c.File(previewFilename)
|
c.File(previewFilename)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -60,22 +60,22 @@ func GetPreview(router *gin.RouterGroup, conf *config.Config) {
|
||||||
y := 0
|
y := 0
|
||||||
|
|
||||||
preview := imaging.New(width, height, color.NRGBA{255, 255, 255, 255})
|
preview := imaging.New(width, height, color.NRGBA{255, 255, 255, 255})
|
||||||
thumbType, _ := photoprism.ThumbnailTypes["tile_224"]
|
thumbType, _ := thumb.Types["tile_224"]
|
||||||
|
|
||||||
for _, file := range p {
|
for _, f := range p {
|
||||||
fileName := path.Join(conf.OriginalsPath(), file.FileName)
|
fileName := path.Join(conf.OriginalsPath(), f.FileName)
|
||||||
|
|
||||||
if !util.Exists(fileName) {
|
if !file.Exists(fileName) {
|
||||||
log.Errorf("could not find original for thumbnail: %s", fileName)
|
log.Errorf("could not find original for thumbnail: %s", fileName)
|
||||||
c.Data(http.StatusNotFound, "image/svg+xml", photoIconSvg)
|
c.Data(http.StatusNotFound, "image/svg+xml", photoIconSvg)
|
||||||
|
|
||||||
// Set missing flag so that the file doesn't show up in search results anymore
|
// Set missing flag so that the file doesn't show up in search results anymore
|
||||||
file.FileMissing = true
|
f.FileMissing = true
|
||||||
conf.Db().Save(&file)
|
conf.Db().Save(&f)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
thumbnail, err := photoprism.ThumbnailFromFile(fileName, file.FileHash, conf.ThumbnailsPath(), thumbType.Width, thumbType.Height, thumbType.Options...)
|
thumbnail, err := thumb.FromFile(fileName, f.FileHash, conf.ThumbnailsPath(), thumbType.Width, thumbType.Height, thumbType.Options...)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(err)
|
log.Error(err)
|
||||||
|
|
|
@ -11,8 +11,10 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/config"
|
"github.com/photoprism/photoprism/internal/config"
|
||||||
|
"github.com/photoprism/photoprism/internal/file"
|
||||||
"github.com/photoprism/photoprism/internal/form"
|
"github.com/photoprism/photoprism/internal/form"
|
||||||
"github.com/photoprism/photoprism/internal/query"
|
"github.com/photoprism/photoprism/internal/query"
|
||||||
|
"github.com/photoprism/photoprism/internal/rnd"
|
||||||
"github.com/photoprism/photoprism/internal/util"
|
"github.com/photoprism/photoprism/internal/util"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
@ -44,7 +46,7 @@ func CreateZip(router *gin.RouterGroup, conf *config.Config) {
|
||||||
}
|
}
|
||||||
|
|
||||||
zipPath := path.Join(conf.ExportPath(), "zip")
|
zipPath := path.Join(conf.ExportPath(), "zip")
|
||||||
zipToken := util.RandomToken(3)
|
zipToken := rnd.Token(3)
|
||||||
zipYear := time.Now().Format("January-2006")
|
zipYear := time.Now().Format("January-2006")
|
||||||
zipBaseName := fmt.Sprintf("Photos-%s-%s.zip", zipYear, zipToken)
|
zipBaseName := fmt.Sprintf("Photos-%s-%s.zip", zipYear, zipToken)
|
||||||
zipFileName := path.Join(zipPath, zipBaseName)
|
zipFileName := path.Join(zipPath, zipBaseName)
|
||||||
|
@ -68,21 +70,21 @@ func CreateZip(router *gin.RouterGroup, conf *config.Config) {
|
||||||
zipWriter := zip.NewWriter(newZipFile)
|
zipWriter := zip.NewWriter(newZipFile)
|
||||||
defer zipWriter.Close()
|
defer zipWriter.Close()
|
||||||
|
|
||||||
for _, file := range files {
|
for _, f := range files {
|
||||||
fileName := path.Join(conf.OriginalsPath(), file.FileName)
|
fileName := path.Join(conf.OriginalsPath(), f.FileName)
|
||||||
fileAlias := file.DownloadFileName()
|
fileAlias := f.DownloadFileName()
|
||||||
|
|
||||||
if util.Exists(fileName) {
|
if file.Exists(fileName) {
|
||||||
if err := addFileToZip(zipWriter, fileName, fileAlias); err != nil {
|
if err := addFileToZip(zipWriter, fileName, fileAlias); err != nil {
|
||||||
log.Error(err)
|
log.Error(err)
|
||||||
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": util.UcFirst("failed to create zip file")})
|
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": util.UcFirst("failed to create zip file")})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Infof("zip: added \"%s\" as \"%s\"", file.FileName, fileAlias)
|
log.Infof("zip: added \"%s\" as \"%s\"", f.FileName, fileAlias)
|
||||||
} else {
|
} else {
|
||||||
log.Warnf("zip: \"%s\" is missing", file.FileName)
|
log.Warnf("zip: \"%s\" is missing", f.FileName)
|
||||||
file.FileMissing = true
|
f.FileMissing = true
|
||||||
conf.Db().Save(&file)
|
conf.Db().Save(&f)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -103,7 +105,7 @@ func DownloadZip(router *gin.RouterGroup, conf *config.Config) {
|
||||||
|
|
||||||
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", zipBaseName))
|
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", zipBaseName))
|
||||||
|
|
||||||
if !util.Exists(zipFileName) {
|
if !file.Exists(zipFileName) {
|
||||||
log.Errorf("could not find zip file: %s", zipFileName)
|
log.Errorf("could not find zip file: %s", zipFileName)
|
||||||
c.Data(404, "image/svg+xml", photoIconSvg)
|
c.Data(404, "image/svg+xml", photoIconSvg)
|
||||||
return
|
return
|
||||||
|
|
|
@ -12,14 +12,14 @@ import (
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/event"
|
"github.com/photoprism/photoprism/internal/event"
|
||||||
"github.com/photoprism/photoprism/internal/util"
|
"github.com/photoprism/photoprism/internal/file"
|
||||||
"github.com/sevlyar/go-daemon"
|
"github.com/sevlyar/go-daemon"
|
||||||
)
|
)
|
||||||
|
|
||||||
var log = event.Log
|
var log = event.Log
|
||||||
|
|
||||||
func childAlreadyRunning(filePath string) (pid int, running bool) {
|
func childAlreadyRunning(filePath string) (pid int, running bool) {
|
||||||
if !util.Exists(filePath) {
|
if !file.Exists(filePath) {
|
||||||
return pid, false
|
return pid, false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,8 +10,8 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/config"
|
"github.com/photoprism/photoprism/internal/config"
|
||||||
|
"github.com/photoprism/photoprism/internal/file"
|
||||||
"github.com/photoprism/photoprism/internal/server"
|
"github.com/photoprism/photoprism/internal/server"
|
||||||
"github.com/photoprism/photoprism/internal/util"
|
|
||||||
"github.com/sevlyar/go-daemon"
|
"github.com/sevlyar/go-daemon"
|
||||||
"github.com/urfave/cli"
|
"github.com/urfave/cli"
|
||||||
)
|
)
|
||||||
|
@ -96,7 +96,7 @@ func startAction(ctx *cli.Context) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
if child != nil {
|
if child != nil {
|
||||||
if !util.Overwrite(conf.PIDFilename(), []byte(strconv.Itoa(child.Pid))) {
|
if !file.Overwrite(conf.PIDFilename(), []byte(strconv.Itoa(child.Pid))) {
|
||||||
log.Fatalf("failed writing process id to \"%s\"", conf.PIDFilename())
|
log.Fatalf("failed writing process id to \"%s\"", conf.PIDFilename())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -32,10 +32,6 @@ func thumbnailsAction(ctx *cli.Context) error {
|
||||||
|
|
||||||
log.Infof("creating thumbnails in \"%s\"", conf.ThumbnailsPath())
|
log.Infof("creating thumbnails in \"%s\"", conf.ThumbnailsPath())
|
||||||
|
|
||||||
photoprism.JpegQuality = conf.ThumbQuality()
|
|
||||||
photoprism.MaxThumbWidth = conf.ThumbSize()
|
|
||||||
photoprism.MaxThumbHeight = conf.ThumbSize()
|
|
||||||
|
|
||||||
if err := photoprism.CreateThumbnailsFromOriginals(conf.OriginalsPath(), conf.ThumbnailsPath(), ctx.Bool("force")); err != nil {
|
if err := photoprism.CreateThumbnailsFromOriginals(conf.OriginalsPath(), conf.ThumbnailsPath(), ctx.Bool("force")); err != nil {
|
||||||
log.Error(err)
|
log.Error(err)
|
||||||
return err
|
return err
|
||||||
|
|
|
@ -6,7 +6,7 @@ import (
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/colors"
|
"github.com/photoprism/photoprism/internal/colors"
|
||||||
"github.com/photoprism/photoprism/internal/entity"
|
"github.com/photoprism/photoprism/internal/entity"
|
||||||
"github.com/photoprism/photoprism/internal/util"
|
"github.com/photoprism/photoprism/internal/file"
|
||||||
)
|
)
|
||||||
|
|
||||||
// HTTP client / Web UI config values
|
// HTTP client / Web UI config values
|
||||||
|
@ -118,8 +118,8 @@ func (c *Config) ClientConfig() ClientConfig {
|
||||||
categories[i].Title = strings.Title(l.LabelName)
|
categories[i].Title = strings.Title(l.LabelName)
|
||||||
}
|
}
|
||||||
|
|
||||||
jsHash := util.Hash(c.HttpStaticBuildPath() + "/app.js")
|
jsHash := file.Hash(c.HttpStaticBuildPath() + "/app.js")
|
||||||
cssHash := util.Hash(c.HttpStaticBuildPath() + "/app.css")
|
cssHash := file.Hash(c.HttpStaticBuildPath() + "/app.css")
|
||||||
|
|
||||||
// Feature Flags
|
// Feature Flags
|
||||||
var flags []string
|
var flags []string
|
||||||
|
|
|
@ -10,6 +10,7 @@ import (
|
||||||
_ "github.com/jinzhu/gorm/dialects/sqlite"
|
_ "github.com/jinzhu/gorm/dialects/sqlite"
|
||||||
gc "github.com/patrickmn/go-cache"
|
gc "github.com/patrickmn/go-cache"
|
||||||
"github.com/photoprism/photoprism/internal/event"
|
"github.com/photoprism/photoprism/internal/event"
|
||||||
|
"github.com/photoprism/photoprism/internal/thumb"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"github.com/urfave/cli"
|
"github.com/urfave/cli"
|
||||||
)
|
)
|
||||||
|
@ -22,6 +23,15 @@ type Config struct {
|
||||||
config *Params
|
config *Params
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
for name, t := range thumb.Types {
|
||||||
|
if t.Public {
|
||||||
|
thumbnail := Thumbnail{Name: name, Width: t.Width, Height: t.Height}
|
||||||
|
Thumbnails = append(Thumbnails, thumbnail)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func initLogger(debug bool) {
|
func initLogger(debug bool) {
|
||||||
log.SetFormatter(&logrus.TextFormatter{
|
log.SetFormatter(&logrus.TextFormatter{
|
||||||
DisableColors: false,
|
DisableColors: false,
|
||||||
|
@ -44,6 +54,10 @@ func NewConfig(ctx *cli.Context) *Config {
|
||||||
|
|
||||||
log.SetLevel(c.LogLevel())
|
log.SetLevel(c.LogLevel())
|
||||||
|
|
||||||
|
thumb.JpegQuality = c.ThumbQuality()
|
||||||
|
thumb.MaxWidth = c.ThumbSize()
|
||||||
|
thumb.MaxHeight = c.ThumbSize()
|
||||||
|
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -180,17 +194,32 @@ func (c *Config) Workers() int {
|
||||||
return runtime.NumCPU()
|
return runtime.NumCPU()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ThumbQuality returns the thumbnail jpeg quality setting (0-100).
|
// ThumbQuality returns the thumbnail jpeg quality setting (25-100).
|
||||||
func (c *Config) ThumbQuality() int {
|
func (c *Config) ThumbQuality() int {
|
||||||
|
if c.config.ThumbQuality > 100 {
|
||||||
|
return 100
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.config.ThumbQuality < 25 {
|
||||||
|
return 25
|
||||||
|
}
|
||||||
|
|
||||||
return c.config.ThumbQuality
|
return c.config.ThumbQuality
|
||||||
}
|
}
|
||||||
|
|
||||||
// ThumbSize returns the thumbnail size limit in pixels.
|
// ThumbSize returns the thumbnail size limit in pixels (720-16384).
|
||||||
func (c *Config) ThumbSize() int {
|
func (c *Config) ThumbSize() int {
|
||||||
|
if c.config.ThumbSize > 16384 {
|
||||||
|
return 16384
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.config.ThumbSize < 720 {
|
||||||
|
return 720
|
||||||
|
}
|
||||||
|
|
||||||
return c.config.ThumbSize
|
return c.config.ThumbSize
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// GeoCodingApi returns the preferred geo coding api (none, osm or places).
|
// GeoCodingApi returns the preferred geo coding api (none, osm or places).
|
||||||
func (c *Config) GeoCodingApi() string {
|
func (c *Config) GeoCodingApi() string {
|
||||||
switch c.config.GeoCodingApi {
|
switch c.config.GeoCodingApi {
|
||||||
|
|
|
@ -3,7 +3,7 @@ package config
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/util"
|
"github.com/photoprism/photoprism/internal/file"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -17,7 +17,7 @@ func TestNewConfig(t *testing.T) {
|
||||||
|
|
||||||
assert.IsType(t, new(Config), c)
|
assert.IsType(t, new(Config), c)
|
||||||
|
|
||||||
assert.Equal(t, util.ExpandedFilename("../../assets"), c.AssetsPath())
|
assert.Equal(t, file.ExpandFilename("../../assets"), c.AssetsPath())
|
||||||
assert.False(t, c.Debug())
|
assert.False(t, c.Debug())
|
||||||
assert.False(t, c.ReadOnly())
|
assert.False(t, c.ReadOnly())
|
||||||
}
|
}
|
||||||
|
|
|
@ -138,6 +138,9 @@ func (c *Config) connectToDatabase(ctx context.Context) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
db.LogMode(false)
|
||||||
|
db.SetLogger(log)
|
||||||
|
|
||||||
c.db = db
|
c.db = db
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -185,7 +188,7 @@ func (c *Config) ImportSQL(filename string) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
var result struct {}
|
var result struct{}
|
||||||
|
|
||||||
err := q.Raw(stmt).Scan(&result).Error
|
err := q.Raw(stmt).Scan(&result).Error
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ import (
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/util"
|
"github.com/photoprism/photoprism/internal/file"
|
||||||
)
|
)
|
||||||
|
|
||||||
func findExecutable(configBin, defaultBin string) (result string) {
|
func findExecutable(configBin, defaultBin string) (result string) {
|
||||||
|
@ -19,7 +19,7 @@ func findExecutable(configBin, defaultBin string) (result string) {
|
||||||
result = path
|
result = path
|
||||||
}
|
}
|
||||||
|
|
||||||
if !util.Exists(result) {
|
if !file.Exists(result) {
|
||||||
result = ""
|
result = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -227,13 +227,13 @@ var GlobalFlags = []cli.Flag{
|
||||||
},
|
},
|
||||||
cli.IntFlag{
|
cli.IntFlag{
|
||||||
Name: "thumb-quality, q",
|
Name: "thumb-quality, q",
|
||||||
Usage: "jpeg quality of thumbnails (0-100)",
|
Usage: "jpeg quality of thumbnails (25-100)",
|
||||||
Value: 95,
|
Value: 95,
|
||||||
EnvVar: "PHOTOPRISM_THUMB_QUALITY",
|
EnvVar: "PHOTOPRISM_THUMB_QUALITY",
|
||||||
},
|
},
|
||||||
cli.IntFlag{
|
cli.IntFlag{
|
||||||
Name: "thumb-size",
|
Name: "thumb-size",
|
||||||
Usage: "max thumbnail size in pixels",
|
Usage: "max thumbnail size in pixels (720-16384)",
|
||||||
Value: 8192,
|
Value: 8192,
|
||||||
EnvVar: "PHOTOPRISM_THUMB_SIZE",
|
EnvVar: "PHOTOPRISM_THUMB_SIZE",
|
||||||
},
|
},
|
||||||
|
|
|
@ -8,7 +8,7 @@ import (
|
||||||
|
|
||||||
_ "github.com/jinzhu/gorm/dialects/mysql"
|
_ "github.com/jinzhu/gorm/dialects/mysql"
|
||||||
_ "github.com/jinzhu/gorm/dialects/sqlite"
|
_ "github.com/jinzhu/gorm/dialects/sqlite"
|
||||||
"github.com/photoprism/photoprism/internal/util"
|
"github.com/photoprism/photoprism/internal/file"
|
||||||
"github.com/urfave/cli"
|
"github.com/urfave/cli"
|
||||||
"gopkg.in/yaml.v2"
|
"gopkg.in/yaml.v2"
|
||||||
)
|
)
|
||||||
|
@ -87,7 +87,7 @@ func NewParams(ctx *cli.Context) *Params {
|
||||||
c.Name = ctx.App.Name
|
c.Name = ctx.App.Name
|
||||||
c.Copyright = ctx.App.Copyright
|
c.Copyright = ctx.App.Copyright
|
||||||
c.Version = ctx.App.Version
|
c.Version = ctx.App.Version
|
||||||
c.ConfigFile = util.ExpandedFilename(ctx.GlobalString("config-file"))
|
c.ConfigFile = file.ExpandFilename(ctx.GlobalString("config-file"))
|
||||||
|
|
||||||
if err := c.SetValuesFromFile(c.ConfigFile); err != nil {
|
if err := c.SetValuesFromFile(c.ConfigFile); err != nil {
|
||||||
log.Debug(err)
|
log.Debug(err)
|
||||||
|
@ -103,21 +103,21 @@ func NewParams(ctx *cli.Context) *Params {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Params) expandFilenames() {
|
func (c *Params) expandFilenames() {
|
||||||
c.ConfigPath = util.ExpandedFilename(c.ConfigPath)
|
c.ConfigPath = file.ExpandFilename(c.ConfigPath)
|
||||||
c.ResourcesPath = util.ExpandedFilename(c.ResourcesPath)
|
c.ResourcesPath = file.ExpandFilename(c.ResourcesPath)
|
||||||
c.AssetsPath = util.ExpandedFilename(c.AssetsPath)
|
c.AssetsPath = file.ExpandFilename(c.AssetsPath)
|
||||||
c.CachePath = util.ExpandedFilename(c.CachePath)
|
c.CachePath = file.ExpandFilename(c.CachePath)
|
||||||
c.OriginalsPath = util.ExpandedFilename(c.OriginalsPath)
|
c.OriginalsPath = file.ExpandFilename(c.OriginalsPath)
|
||||||
c.ImportPath = util.ExpandedFilename(c.ImportPath)
|
c.ImportPath = file.ExpandFilename(c.ImportPath)
|
||||||
c.ExportPath = util.ExpandedFilename(c.ExportPath)
|
c.ExportPath = file.ExpandFilename(c.ExportPath)
|
||||||
c.SqlServerPath = util.ExpandedFilename(c.SqlServerPath)
|
c.SqlServerPath = file.ExpandFilename(c.SqlServerPath)
|
||||||
c.PIDFilename = util.ExpandedFilename(c.PIDFilename)
|
c.PIDFilename = file.ExpandFilename(c.PIDFilename)
|
||||||
c.LogFilename = util.ExpandedFilename(c.LogFilename)
|
c.LogFilename = file.ExpandFilename(c.LogFilename)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetValuesFromFile uses a yaml config file to initiate the configuration entity.
|
// SetValuesFromFile uses a yaml config file to initiate the configuration entity.
|
||||||
func (c *Params) SetValuesFromFile(fileName string) error {
|
func (c *Params) SetValuesFromFile(fileName string) error {
|
||||||
if !util.Exists(fileName) {
|
if !file.Exists(fileName) {
|
||||||
return errors.New(fmt.Sprintf("config file not found: \"%s\"", fileName))
|
return errors.New(fmt.Sprintf("config file not found: \"%s\"", fileName))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ package config
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/util"
|
"github.com/photoprism/photoprism/internal/file"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -17,7 +17,7 @@ func TestNewParams(t *testing.T) {
|
||||||
|
|
||||||
assert.IsType(t, new(Params), c)
|
assert.IsType(t, new(Params), c)
|
||||||
|
|
||||||
assert.Equal(t, util.ExpandedFilename("../../assets"), c.AssetsPath)
|
assert.Equal(t, file.ExpandFilename("../../assets"), c.AssetsPath)
|
||||||
assert.False(t, c.Debug)
|
assert.False(t, c.Debug)
|
||||||
assert.False(t, c.ReadOnly)
|
assert.False(t, c.ReadOnly)
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@ import (
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/util"
|
"github.com/photoprism/photoprism/internal/file"
|
||||||
"gopkg.in/yaml.v2"
|
"gopkg.in/yaml.v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -20,7 +20,7 @@ func NewSettings() *Settings {
|
||||||
|
|
||||||
// SetValuesFromFile uses a yaml config file to initiate the configuration entity.
|
// SetValuesFromFile uses a yaml config file to initiate the configuration entity.
|
||||||
func (s *Settings) SetValuesFromFile(fileName string) error {
|
func (s *Settings) SetValuesFromFile(fileName string) error {
|
||||||
if !util.Exists(fileName) {
|
if !file.Exists(fileName) {
|
||||||
return fmt.Errorf("settings file not found: \"%s\"", fileName)
|
return fmt.Errorf("settings file not found: \"%s\"", fileName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,7 +35,7 @@ func (s *Settings) SetValuesFromFile(fileName string) error {
|
||||||
|
|
||||||
// WriteValuesToFile uses a yaml config file to initiate the configuration entity.
|
// WriteValuesToFile uses a yaml config file to initiate the configuration entity.
|
||||||
func (s *Settings) WriteValuesToFile(fileName string) error {
|
func (s *Settings) WriteValuesToFile(fileName string) error {
|
||||||
if !util.Exists(fileName) {
|
if !file.Exists(fileName) {
|
||||||
return fmt.Errorf("settings file not found: \"%s\"", fileName)
|
return fmt.Errorf("settings file not found: \"%s\"", fileName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,8 @@ import (
|
||||||
|
|
||||||
_ "github.com/jinzhu/gorm/dialects/mysql"
|
_ "github.com/jinzhu/gorm/dialects/mysql"
|
||||||
_ "github.com/jinzhu/gorm/dialects/sqlite"
|
_ "github.com/jinzhu/gorm/dialects/sqlite"
|
||||||
"github.com/photoprism/photoprism/internal/util"
|
"github.com/photoprism/photoprism/internal/file"
|
||||||
|
"github.com/photoprism/photoprism/internal/thumb"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"github.com/urfave/cli"
|
"github.com/urfave/cli"
|
||||||
)
|
)
|
||||||
|
@ -29,7 +30,7 @@ func testDataPath(assetsPath string) string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTestParams() *Params {
|
func NewTestParams() *Params {
|
||||||
assetsPath := util.ExpandedFilename("../../assets")
|
assetsPath := file.ExpandFilename("../../assets")
|
||||||
|
|
||||||
testDataPath := testDataPath(assetsPath)
|
testDataPath := testDataPath(assetsPath)
|
||||||
|
|
||||||
|
@ -52,7 +53,7 @@ func NewTestParams() *Params {
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTestParamsError() *Params {
|
func NewTestParamsError() *Params {
|
||||||
assetsPath := util.ExpandedFilename("../..")
|
assetsPath := file.ExpandFilename("../..")
|
||||||
|
|
||||||
testDataPath := testDataPath("../../assets")
|
testDataPath := testDataPath("../../assets")
|
||||||
|
|
||||||
|
@ -93,6 +94,10 @@ func NewTestConfig() *Config {
|
||||||
|
|
||||||
c.ImportSQL(c.ExamplesPath() + "/fixtures.sql")
|
c.ImportSQL(c.ExamplesPath() + "/fixtures.sql")
|
||||||
|
|
||||||
|
thumb.JpegQuality = c.ThumbQuality()
|
||||||
|
thumb.MaxWidth = c.ThumbSize()
|
||||||
|
thumb.MaxHeight = c.ThumbSize()
|
||||||
|
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -140,8 +145,8 @@ func (c *Config) RemoveTestData(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Config) DownloadTestData(t *testing.T) {
|
func (c *Config) DownloadTestData(t *testing.T) {
|
||||||
if util.Exists(TestDataZip) {
|
if file.Exists(TestDataZip) {
|
||||||
hash := util.Hash(TestDataZip)
|
hash := file.Hash(TestDataZip)
|
||||||
|
|
||||||
if hash != TestDataHash {
|
if hash != TestDataHash {
|
||||||
os.Remove(TestDataZip)
|
os.Remove(TestDataZip)
|
||||||
|
@ -149,17 +154,17 @@ func (c *Config) DownloadTestData(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !util.Exists(TestDataZip) {
|
if !file.Exists(TestDataZip) {
|
||||||
fmt.Printf("downloading latest test data zip file from %s\n", TestDataURL)
|
fmt.Printf("downloading latest test data zip file from %s\n", TestDataURL)
|
||||||
|
|
||||||
if err := util.Download(TestDataZip, TestDataURL); err != nil {
|
if err := file.Download(TestDataZip, TestDataURL); err != nil {
|
||||||
fmt.Printf("Download failed: %s\n", err.Error())
|
fmt.Printf("Download failed: %s\n", err.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Config) UnzipTestData(t *testing.T) {
|
func (c *Config) UnzipTestData(t *testing.T) {
|
||||||
if _, err := util.Unzip(TestDataZip, testDataPath(c.AssetsPath())); err != nil {
|
if _, err := file.Unzip(TestDataZip, testDataPath(c.AssetsPath())); err != nil {
|
||||||
t.Logf("could not unzip test data: %s\n", err.Error())
|
t.Logf("could not unzip test data: %s\n", err.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/jinzhu/gorm"
|
"github.com/jinzhu/gorm"
|
||||||
"github.com/photoprism/photoprism/internal/util"
|
"github.com/photoprism/photoprism/internal/file"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/urfave/cli"
|
"github.com/urfave/cli"
|
||||||
)
|
)
|
||||||
|
@ -26,7 +26,7 @@ func TestNewTestParams(t *testing.T) {
|
||||||
|
|
||||||
assert.IsType(t, new(Params), c)
|
assert.IsType(t, new(Params), c)
|
||||||
|
|
||||||
assert.Equal(t, util.ExpandedFilename("../../assets"), c.AssetsPath)
|
assert.Equal(t, file.ExpandFilename("../../assets"), c.AssetsPath)
|
||||||
assert.False(t, c.Debug)
|
assert.False(t, c.Debug)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -43,7 +43,7 @@ func TestNewTestParamsError(t *testing.T) {
|
||||||
|
|
||||||
assert.IsType(t, new(Params), c)
|
assert.IsType(t, new(Params), c)
|
||||||
|
|
||||||
assert.Equal(t, util.ExpandedFilename("../.."), c.AssetsPath)
|
assert.Equal(t, file.ExpandFilename("../.."), c.AssetsPath)
|
||||||
assert.Equal(t, "../../assets/testdata/cache", c.CachePath)
|
assert.Equal(t, "../../assets/testdata/cache", c.CachePath)
|
||||||
assert.False(t, c.Debug)
|
assert.False(t, c.Debug)
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,7 @@ import (
|
||||||
|
|
||||||
"github.com/jinzhu/gorm"
|
"github.com/jinzhu/gorm"
|
||||||
"github.com/photoprism/photoprism/internal/event"
|
"github.com/photoprism/photoprism/internal/event"
|
||||||
"github.com/photoprism/photoprism/internal/util"
|
"github.com/photoprism/photoprism/internal/rnd"
|
||||||
)
|
)
|
||||||
|
|
||||||
var log = event.Log
|
var log = event.Log
|
||||||
|
@ -32,7 +32,7 @@ func ID(prefix rune) string {
|
||||||
result := make([]byte, 0, 17)
|
result := make([]byte, 0, 17)
|
||||||
result = append(result, byte(prefix))
|
result = append(result, byte(prefix))
|
||||||
result = append(result, strconv.FormatInt(time.Now().UTC().Unix(), 36)[0:6]...)
|
result = append(result, strconv.FormatInt(time.Now().UTC().Unix(), 36)[0:6]...)
|
||||||
result = append(result, util.RandomToken(10)...)
|
result = append(result, rnd.Token(10)...)
|
||||||
|
|
||||||
return string(result)
|
return string(result)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,11 @@
|
||||||
package util
|
/*
|
||||||
|
This package encapsulates file related constants and functions.
|
||||||
|
|
||||||
|
Additional information can be found in our Developer Guide:
|
||||||
|
|
||||||
|
https://github.com/photoprism/photoprism/wiki
|
||||||
|
*/
|
||||||
|
package file
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"archive/zip"
|
"archive/zip"
|
||||||
|
@ -9,8 +16,12 @@ import (
|
||||||
"os/user"
|
"os/user"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/photoprism/photoprism/internal/event"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var log = event.Log
|
||||||
|
|
||||||
// Returns true if file exists
|
// Returns true if file exists
|
||||||
func Exists(filename string) bool {
|
func Exists(filename string) bool {
|
||||||
info, err := os.Stat(filename)
|
info, err := os.Stat(filename)
|
||||||
|
@ -30,7 +41,7 @@ func Overwrite(fileName string, data []byte) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns full path; ~ replaced with actual home directory
|
// Returns full path; ~ replaced with actual home directory
|
||||||
func ExpandedFilename(filename string) string {
|
func ExpandFilename(filename string) string {
|
||||||
if filename == "" {
|
if filename == "" {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
@ -50,32 +61,6 @@ func ExpandedFilename(filename string) string {
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract Zip file in destination directory
|
|
||||||
func Unzip(src, dest string) (fileNames []string, err error) {
|
|
||||||
r, err := zip.OpenReader(src)
|
|
||||||
if err != nil {
|
|
||||||
return fileNames, err
|
|
||||||
}
|
|
||||||
|
|
||||||
defer r.Close()
|
|
||||||
|
|
||||||
for _, f := range r.File {
|
|
||||||
// Skip directories like __OSX
|
|
||||||
if strings.HasPrefix(f.Name, "__") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
fn, err := copyToFile(f, dest)
|
|
||||||
if err != nil {
|
|
||||||
return fileNames, err
|
|
||||||
}
|
|
||||||
|
|
||||||
fileNames = append(fileNames, fn)
|
|
||||||
}
|
|
||||||
|
|
||||||
return fileNames, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// copyToFile copies the zip file to destination
|
// copyToFile copies the zip file to destination
|
||||||
// if the zip file is a directory, a directory is created at the destination.
|
// if the zip file is a directory, a directory is created at the destination.
|
||||||
func copyToFile(f *zip.File, dest string) (fileName string, err error) {
|
func copyToFile(f *zip.File, dest string) (fileName string, err error) {
|
||||||
|
@ -151,7 +136,7 @@ func Download(filepath string, url string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func DirectoryIsEmpty(path string) bool {
|
func IsEmpty(path string) bool {
|
||||||
f, err := os.Open(path)
|
f, err := os.Open(path)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
|
@ -1,4 +1,4 @@
|
||||||
package util
|
package file
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
|
@ -29,19 +29,19 @@ func TestOverwrite(t *testing.T) {
|
||||||
|
|
||||||
func TestExpandedFilename(t *testing.T) {
|
func TestExpandedFilename(t *testing.T) {
|
||||||
t.Run("test.jpg", func(t *testing.T) {
|
t.Run("test.jpg", func(t *testing.T) {
|
||||||
filename := ExpandedFilename("./testdata/test.jpg")
|
filename := ExpandFilename("./testdata/test.jpg")
|
||||||
assert.Contains(t, filename, "/testdata/test.jpg")
|
assert.Contains(t, filename, "/testdata/test.jpg")
|
||||||
assert.IsType(t, "", filename)
|
assert.IsType(t, "", filename)
|
||||||
})
|
})
|
||||||
t.Run("empty filename", func(t *testing.T) {
|
t.Run("empty filename", func(t *testing.T) {
|
||||||
filename := ExpandedFilename("")
|
filename := ExpandFilename("")
|
||||||
assert.Equal(t, "", filename)
|
assert.Equal(t, "", filename)
|
||||||
assert.IsType(t, "", filename)
|
assert.IsType(t, "", filename)
|
||||||
})
|
})
|
||||||
t.Run("~ in filename", func(t *testing.T) {
|
t.Run("~ in filename", func(t *testing.T) {
|
||||||
usr, _ := user.Current()
|
usr, _ := user.Current()
|
||||||
expected := usr.HomeDir + "/test.jpg"
|
expected := usr.HomeDir + "/test.jpg"
|
||||||
filename := ExpandedFilename("~/test.jpg")
|
filename := ExpandFilename("~/test.jpg")
|
||||||
assert.Equal(t, expected, filename)
|
assert.Equal(t, expected, filename)
|
||||||
assert.IsType(t, "", filename)
|
assert.IsType(t, "", filename)
|
||||||
})
|
})
|
||||||
|
@ -49,14 +49,14 @@ func TestExpandedFilename(t *testing.T) {
|
||||||
|
|
||||||
func TestDirectoryIsEmpty(t *testing.T) {
|
func TestDirectoryIsEmpty(t *testing.T) {
|
||||||
t.Run("not empty path", func(t *testing.T) {
|
t.Run("not empty path", func(t *testing.T) {
|
||||||
assert.Equal(t, false, DirectoryIsEmpty("./testdata"))
|
assert.Equal(t, false, IsEmpty("./testdata"))
|
||||||
})
|
})
|
||||||
t.Run("not existing path", func(t *testing.T) {
|
t.Run("not existing path", func(t *testing.T) {
|
||||||
assert.Equal(t, false, DirectoryIsEmpty("./xxx"))
|
assert.Equal(t, false, IsEmpty("./xxx"))
|
||||||
})
|
})
|
||||||
t.Run("empty path", func(t *testing.T) {
|
t.Run("empty path", func(t *testing.T) {
|
||||||
os.Mkdir("./testdata/emptyDir", 0777)
|
os.Mkdir("./testdata/emptyDir", 0777)
|
||||||
defer os.RemoveAll("./testdata/emptyDir")
|
defer os.RemoveAll("./testdata/emptyDir")
|
||||||
assert.Equal(t, true, DirectoryIsEmpty("./testdata/emptyDir"))
|
assert.Equal(t, true, IsEmpty("./testdata/emptyDir"))
|
||||||
})
|
})
|
||||||
}
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package util
|
package file
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/sha1"
|
"crypto/sha1"
|
|
@ -1,4 +1,4 @@
|
||||||
package util
|
package file
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
|
@ -1,4 +1,4 @@
|
||||||
package util
|
package file
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
|
@ -1,4 +1,4 @@
|
||||||
package util
|
package file
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
@ -8,12 +8,12 @@ import (
|
||||||
|
|
||||||
func TestMimeType(t *testing.T) {
|
func TestMimeType(t *testing.T) {
|
||||||
t.Run("jpg", func(t *testing.T) {
|
t.Run("jpg", func(t *testing.T) {
|
||||||
filename := ExpandedFilename("./testdata/test.jpg")
|
filename := ExpandFilename("./testdata/test.jpg")
|
||||||
mimeType := MimeType(filename)
|
mimeType := MimeType(filename)
|
||||||
assert.Equal(t, "image/jpeg", mimeType)
|
assert.Equal(t, "image/jpeg", mimeType)
|
||||||
})
|
})
|
||||||
t.Run("not existing filename", func(t *testing.T) {
|
t.Run("not existing filename", func(t *testing.T) {
|
||||||
filename := ExpandedFilename("./testdata/xxx.jpg")
|
filename := ExpandFilename("./testdata/xxx.jpg")
|
||||||
mimeType := MimeType(filename)
|
mimeType := MimeType(filename)
|
||||||
assert.Equal(t, "", mimeType)
|
assert.Equal(t, "", mimeType)
|
||||||
})
|
})
|
BIN
internal/file/testdata/test.jpg
vendored
Normal file
BIN
internal/file/testdata/test.jpg
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 11 KiB |
97
internal/file/types.go
Normal file
97
internal/file/types.go
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
package file
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "image/gif" // Import for image.
|
||||||
|
_ "image/jpeg"
|
||||||
|
_ "image/png"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Type string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// JPEG image file.
|
||||||
|
TypeJpeg Type = "jpg"
|
||||||
|
// PNG image file.
|
||||||
|
TypePng Type = "png"
|
||||||
|
// RAW image file.
|
||||||
|
TypeRaw Type = "raw"
|
||||||
|
// High Efficiency Image File Format.
|
||||||
|
TypeHEIF Type = "heif" // High Efficiency Image File Format
|
||||||
|
// Movie file.
|
||||||
|
TypeMovie Type = "mov"
|
||||||
|
// Adobe XMP sidecar file (XML).
|
||||||
|
TypeXMP Type = "xmp"
|
||||||
|
// Apple sidecar file (XML).
|
||||||
|
TypeAAE Type = "aae"
|
||||||
|
// XML metadata / config / sidecar file.
|
||||||
|
TypeXML Type = "xml"
|
||||||
|
// YAML metadata / config / sidecar file.
|
||||||
|
TypeYaml Type = "yml"
|
||||||
|
// Text config / sidecar file.
|
||||||
|
TypeText Type = "txt"
|
||||||
|
// Markdown text sidecar file.
|
||||||
|
TypeMarkdown Type = "md"
|
||||||
|
// Unknown file format.
|
||||||
|
TypeOther Type = "unknown"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// MimeTypeJpeg is jpeg image type
|
||||||
|
MimeTypeJpeg = "image/jpeg"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Ext lists all the available and supported image file formats.
|
||||||
|
var Ext = map[string]Type{
|
||||||
|
".crw": TypeRaw,
|
||||||
|
".cr2": TypeRaw,
|
||||||
|
".nef": TypeRaw,
|
||||||
|
".arw": TypeRaw,
|
||||||
|
".dng": TypeRaw,
|
||||||
|
".mov": TypeMovie,
|
||||||
|
".avi": TypeMovie,
|
||||||
|
".yml": TypeYaml,
|
||||||
|
".jpg": TypeJpeg,
|
||||||
|
".thm": TypeJpeg,
|
||||||
|
".jpeg": TypeJpeg,
|
||||||
|
".xmp": TypeXMP,
|
||||||
|
".aae": TypeAAE,
|
||||||
|
".heif": TypeHEIF,
|
||||||
|
".heic": TypeHEIF,
|
||||||
|
".3fr": TypeRaw,
|
||||||
|
".ari": TypeRaw,
|
||||||
|
".bay": TypeRaw,
|
||||||
|
".cr3": TypeRaw,
|
||||||
|
".cap": TypeRaw,
|
||||||
|
".data": TypeRaw,
|
||||||
|
".dcs": TypeRaw,
|
||||||
|
".dcr": TypeRaw,
|
||||||
|
".drf": TypeRaw,
|
||||||
|
".eip": TypeRaw,
|
||||||
|
".erf": TypeRaw,
|
||||||
|
".fff": TypeRaw,
|
||||||
|
".gpr": TypeRaw,
|
||||||
|
".iiq": TypeRaw,
|
||||||
|
".k25": TypeRaw,
|
||||||
|
".kdc": TypeRaw,
|
||||||
|
".mdc": TypeRaw,
|
||||||
|
".mef": TypeRaw,
|
||||||
|
".mos": TypeRaw,
|
||||||
|
".mrw": TypeRaw,
|
||||||
|
".nrw": TypeRaw,
|
||||||
|
".obm": TypeRaw,
|
||||||
|
".orf": TypeRaw,
|
||||||
|
".pef": TypeRaw,
|
||||||
|
".ptx": TypeRaw,
|
||||||
|
".pxn": TypeRaw,
|
||||||
|
".r3d": TypeRaw,
|
||||||
|
".raf": TypeRaw,
|
||||||
|
".raw": TypeRaw,
|
||||||
|
".rwl": TypeRaw,
|
||||||
|
".rw2": TypeRaw,
|
||||||
|
".rwz": TypeRaw,
|
||||||
|
".sr2": TypeRaw,
|
||||||
|
".srf": TypeRaw,
|
||||||
|
".srw": TypeRaw,
|
||||||
|
".tif": TypeRaw,
|
||||||
|
".x3f": TypeRaw,
|
||||||
|
}
|
|
@ -1,15 +1,16 @@
|
||||||
package util
|
package file
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"archive/zip"
|
"archive/zip"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ZipFiles compresses one or many files into a single zip archive file.
|
// ZipFiles compresses one or many files into a single zip archive file.
|
||||||
// Param 1: filename is the output zip file's name.
|
// Param 1: filename is the output zip file's name.
|
||||||
// Param 2: files is a list of files to add to the zip.
|
// Param 2: files is a list of files to add to the zip.
|
||||||
func ZipFiles(filename string, files []string) error {
|
func Zip(filename string, files []string) error {
|
||||||
newZipFile, err := os.Create(filename)
|
newZipFile, err := os.Create(filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -21,7 +22,7 @@ func ZipFiles(filename string, files []string) error {
|
||||||
|
|
||||||
// Add files to zip
|
// Add files to zip
|
||||||
for _, file := range files {
|
for _, file := range files {
|
||||||
if err = AddFileToZip(zipWriter, file); err != nil {
|
if err = AddToZip(zipWriter, file); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -29,7 +30,7 @@ func ZipFiles(filename string, files []string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func AddFileToZip(zipWriter *zip.Writer, filename string) error {
|
func AddToZip(zipWriter *zip.Writer, filename string) error {
|
||||||
|
|
||||||
fileToZip, err := os.Open(filename)
|
fileToZip, err := os.Open(filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -59,3 +60,29 @@ func AddFileToZip(zipWriter *zip.Writer, filename string) error {
|
||||||
_, err = io.Copy(writer, fileToZip)
|
_, err = io.Copy(writer, fileToZip)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Extract Zip file in destination directory
|
||||||
|
func Unzip(src, dest string) (fileNames []string, err error) {
|
||||||
|
r, err := zip.OpenReader(src)
|
||||||
|
if err != nil {
|
||||||
|
return fileNames, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer r.Close()
|
||||||
|
|
||||||
|
for _, f := range r.File {
|
||||||
|
// Skip directories like __OSX
|
||||||
|
if strings.HasPrefix(f.Name, "__") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
fn, err := copyToFile(f, dest)
|
||||||
|
if err != nil {
|
||||||
|
return fileNames, err
|
||||||
|
}
|
||||||
|
|
||||||
|
fileNames = append(fileNames, fn)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fileNames, nil
|
||||||
|
}
|
|
@ -9,7 +9,7 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/util"
|
"github.com/photoprism/photoprism/internal/file"
|
||||||
tf "github.com/tensorflow/tensorflow/tensorflow/go"
|
tf "github.com/tensorflow/tensorflow/tensorflow/go"
|
||||||
"github.com/tensorflow/tensorflow/tensorflow/go/op"
|
"github.com/tensorflow/tensorflow/tensorflow/go/op"
|
||||||
)
|
)
|
||||||
|
@ -30,7 +30,7 @@ func NewDetector(modelPath string) *Detector {
|
||||||
|
|
||||||
// LabelsFromFile returns matching labels for a jpeg media file.
|
// LabelsFromFile returns matching labels for a jpeg media file.
|
||||||
func (t *Detector) LabelsFromFile(filename string) (result Labels, err error) {
|
func (t *Detector) LabelsFromFile(filename string) (result Labels, err error) {
|
||||||
if util.MimeType(filename) != "image/jpeg" {
|
if file.MimeType(filename) != "image/jpeg" {
|
||||||
return result, fmt.Errorf("nsfw: \"%s\" is not a jpeg file", filename)
|
return result, fmt.Errorf("nsfw: \"%s\" is not a jpeg file", filename)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/config"
|
"github.com/photoprism/photoprism/internal/config"
|
||||||
"github.com/photoprism/photoprism/internal/util"
|
"github.com/photoprism/photoprism/internal/file"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -30,7 +30,7 @@ func TestConvert_ToJpeg(t *testing.T) {
|
||||||
|
|
||||||
jpegFilename := conf.ImportPath() + "/fern_green.jpg"
|
jpegFilename := conf.ImportPath() + "/fern_green.jpg"
|
||||||
|
|
||||||
assert.Truef(t, util.Exists(jpegFilename), "file does not exist: %s", jpegFilename)
|
assert.Truef(t, file.Exists(jpegFilename), "file does not exist: %s", jpegFilename)
|
||||||
|
|
||||||
t.Logf("Testing RAW to JPEG convert with %s", jpegFilename)
|
t.Logf("Testing RAW to JPEG convert with %s", jpegFilename)
|
||||||
|
|
||||||
|
@ -66,7 +66,7 @@ func TestConvert_ToJpeg(t *testing.T) {
|
||||||
|
|
||||||
imageRaw, _ := convert.ToJpeg(rawMediaFile)
|
imageRaw, _ := convert.ToJpeg(rawMediaFile)
|
||||||
|
|
||||||
assert.True(t, util.Exists(conf.ImportPath()+"/raw/IMG_2567.jpg"), "Jpeg file was not found - is Darktable installed?")
|
assert.True(t, file.Exists(conf.ImportPath()+"/raw/IMG_2567.jpg"), "Jpeg file was not found - is Darktable installed?")
|
||||||
|
|
||||||
assert.NotEqual(t, rawFilename, imageRaw.filename)
|
assert.NotEqual(t, rawFilename, imageRaw.filename)
|
||||||
|
|
||||||
|
@ -92,7 +92,7 @@ func TestConvert_Path(t *testing.T) {
|
||||||
|
|
||||||
jpegFilename := conf.ImportPath() + "/raw/canon_eos_6d.jpg"
|
jpegFilename := conf.ImportPath() + "/raw/canon_eos_6d.jpg"
|
||||||
|
|
||||||
assert.True(t, util.Exists(jpegFilename), "Jpeg file was not found - is Darktable installed?")
|
assert.True(t, file.Exists(jpegFilename), "Jpeg file was not found - is Darktable installed?")
|
||||||
|
|
||||||
image, err := NewMediaFile(jpegFilename)
|
image, err := NewMediaFile(jpegFilename)
|
||||||
|
|
||||||
|
@ -108,15 +108,15 @@ func TestConvert_Path(t *testing.T) {
|
||||||
|
|
||||||
existingJpegFilename := conf.ImportPath() + "/raw/IMG_2567.jpg"
|
existingJpegFilename := conf.ImportPath() + "/raw/IMG_2567.jpg"
|
||||||
|
|
||||||
oldHash := util.Hash(existingJpegFilename)
|
oldHash := file.Hash(existingJpegFilename)
|
||||||
|
|
||||||
os.Remove(existingJpegFilename)
|
os.Remove(existingJpegFilename)
|
||||||
|
|
||||||
convert.Path(conf.ImportPath())
|
convert.Path(conf.ImportPath())
|
||||||
|
|
||||||
newHash := util.Hash(existingJpegFilename)
|
newHash := file.Hash(existingJpegFilename)
|
||||||
|
|
||||||
assert.True(t, util.Exists(existingJpegFilename), "Jpeg file was not found - is Darktable installed?")
|
assert.True(t, file.Exists(existingJpegFilename), "Jpeg file was not found - is Darktable installed?")
|
||||||
|
|
||||||
assert.NotEqual(t, oldHash, newHash, "Fingerprint of old and new JPEG file must not be the same")
|
assert.NotEqual(t, oldHash, newHash, "Fingerprint of old and new JPEG file must not be the same")
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,8 +12,7 @@ import (
|
||||||
"github.com/photoprism/photoprism/internal/config"
|
"github.com/photoprism/photoprism/internal/config"
|
||||||
"github.com/photoprism/photoprism/internal/entity"
|
"github.com/photoprism/photoprism/internal/entity"
|
||||||
"github.com/photoprism/photoprism/internal/event"
|
"github.com/photoprism/photoprism/internal/event"
|
||||||
|
"github.com/photoprism/photoprism/internal/file"
|
||||||
"github.com/photoprism/photoprism/internal/util"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Import represents an importer that can copy/move MediaFiles to the originals directory.
|
// Import represents an importer that can copy/move MediaFiles to the originals directory.
|
||||||
|
@ -164,7 +163,7 @@ func (imp *Import) Start(importPath string) {
|
||||||
if imp.removeEmptyDirectories {
|
if imp.removeEmptyDirectories {
|
||||||
// Remove empty directories from import path
|
// Remove empty directories from import path
|
||||||
for _, directory := range directories {
|
for _, directory := range directories {
|
||||||
if util.DirectoryIsEmpty(directory) {
|
if file.IsEmpty(directory) {
|
||||||
if err := os.Remove(directory); err != nil {
|
if err := os.Remove(directory); err != nil {
|
||||||
log.Errorf("import: could not deleted empty directory \"%s\" (%s)", directory, err)
|
log.Errorf("import: could not deleted empty directory \"%s\" (%s)", directory, err)
|
||||||
} else {
|
} else {
|
||||||
|
@ -202,8 +201,8 @@ func (imp *Import) DestinationFilename(mainFile *MediaFile, mediaFile *MediaFile
|
||||||
|
|
||||||
result := pathName + string(os.PathSeparator) + fileName + fileExtension
|
result := pathName + string(os.PathSeparator) + fileName + fileExtension
|
||||||
|
|
||||||
for util.Exists(result) {
|
for file.Exists(result) {
|
||||||
if mediaFile.Hash() == util.Hash(result) {
|
if mediaFile.Hash() == file.Hash(result) {
|
||||||
return result, fmt.Errorf("file already exists: %s", result)
|
return result, fmt.Errorf("file already exists: %s", result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,7 @@ import (
|
||||||
|
|
||||||
"github.com/djherbis/times"
|
"github.com/djherbis/times"
|
||||||
"github.com/photoprism/photoprism/internal/entity"
|
"github.com/photoprism/photoprism/internal/entity"
|
||||||
"github.com/photoprism/photoprism/internal/util"
|
"github.com/photoprism/photoprism/internal/file"
|
||||||
)
|
)
|
||||||
|
|
||||||
// MediaFile represents a single photo, video or sidecar file.
|
// MediaFile represents a single photo, video or sidecar file.
|
||||||
|
@ -22,7 +22,7 @@ type MediaFile struct {
|
||||||
dateCreated time.Time
|
dateCreated time.Time
|
||||||
timeZone string
|
timeZone string
|
||||||
hash string
|
hash string
|
||||||
fileType FileType
|
fileType file.Type
|
||||||
mimeType string
|
mimeType string
|
||||||
perceptualHash string
|
perceptualHash string
|
||||||
width int
|
width int
|
||||||
|
@ -33,13 +33,13 @@ type MediaFile struct {
|
||||||
|
|
||||||
// NewMediaFile returns a new MediaFile.
|
// NewMediaFile returns a new MediaFile.
|
||||||
func NewMediaFile(filename string) (*MediaFile, error) {
|
func NewMediaFile(filename string) (*MediaFile, error) {
|
||||||
if !util.Exists(filename) {
|
if !file.Exists(filename) {
|
||||||
return nil, fmt.Errorf("file does not exist: %s", filename)
|
return nil, fmt.Errorf("file does not exist: %s", filename)
|
||||||
}
|
}
|
||||||
|
|
||||||
instance := &MediaFile{
|
instance := &MediaFile{
|
||||||
filename: filename,
|
filename: filename,
|
||||||
fileType: FileTypeOther,
|
fileType: file.TypeOther,
|
||||||
}
|
}
|
||||||
|
|
||||||
return instance, nil
|
return instance, nil
|
||||||
|
@ -231,7 +231,7 @@ func (m *MediaFile) CanonicalNameFromFileWithDirectory() string {
|
||||||
// Hash return a sha1 hash of a MediaFile based on the filename.
|
// Hash return a sha1 hash of a MediaFile based on the filename.
|
||||||
func (m *MediaFile) Hash() string {
|
func (m *MediaFile) Hash() string {
|
||||||
if len(m.hash) == 0 {
|
if len(m.hash) == 0 {
|
||||||
m.hash = util.Hash(m.Filename())
|
m.hash = file.Hash(m.Filename())
|
||||||
}
|
}
|
||||||
|
|
||||||
return m.hash
|
return m.hash
|
||||||
|
@ -242,7 +242,7 @@ func (m *MediaFile) EditedFilename() string {
|
||||||
basename := filepath.Base(m.filename)
|
basename := filepath.Base(m.filename)
|
||||||
|
|
||||||
if strings.ToUpper(basename[:4]) == "IMG_" && strings.ToUpper(basename[:5]) != "IMG_E" {
|
if strings.ToUpper(basename[:4]) == "IMG_" && strings.ToUpper(basename[:5]) != "IMG_E" {
|
||||||
if filename := filepath.Dir(m.filename) + string(os.PathSeparator) + basename[:4] + "E" + basename[4:]; util.Exists(filename) {
|
if filename := filepath.Dir(m.filename) + string(os.PathSeparator) + basename[:4] + "E" + basename[4:]; file.Exists(filename) {
|
||||||
return filename
|
return filename
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -381,7 +381,7 @@ func (m *MediaFile) MimeType() string {
|
||||||
return m.mimeType
|
return m.mimeType
|
||||||
}
|
}
|
||||||
|
|
||||||
m.mimeType = util.MimeType(m.Filename())
|
m.mimeType = file.MimeType(m.Filename())
|
||||||
|
|
||||||
return m.mimeType
|
return m.mimeType
|
||||||
}
|
}
|
||||||
|
@ -397,7 +397,7 @@ func (m *MediaFile) openFile() (*os.File, error) {
|
||||||
|
|
||||||
// Exists checks if a media file exists by filename.
|
// Exists checks if a media file exists by filename.
|
||||||
func (m *MediaFile) Exists() bool {
|
func (m *MediaFile) Exists() bool {
|
||||||
return util.Exists(m.Filename())
|
return file.Exists(m.Filename())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove a media file.
|
// Remove a media file.
|
||||||
|
@ -480,13 +480,13 @@ func (m *MediaFile) IsJpeg() bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Type returns the type of the media file.
|
// Type returns the type of the media file.
|
||||||
func (m *MediaFile) Type() FileType {
|
func (m *MediaFile) Type() file.Type {
|
||||||
return FileExtensions[m.Extension()]
|
return file.Ext[m.Extension()]
|
||||||
}
|
}
|
||||||
|
|
||||||
// HasType returns true if this media file is of a given type.
|
// HasType returns true if this media file is of a given type.
|
||||||
func (m *MediaFile) HasType(t FileType) bool {
|
func (m *MediaFile) HasType(t file.Type) bool {
|
||||||
if t == FileTypeJpeg {
|
if t == file.TypeJpeg {
|
||||||
return m.IsJpeg()
|
return m.IsJpeg()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -495,28 +495,28 @@ func (m *MediaFile) HasType(t FileType) bool {
|
||||||
|
|
||||||
// IsRaw returns true if this media file a RAW file.
|
// IsRaw returns true if this media file a RAW file.
|
||||||
func (m *MediaFile) IsRaw() bool {
|
func (m *MediaFile) IsRaw() bool {
|
||||||
return m.HasType(FileTypeRaw)
|
return m.HasType(file.TypeRaw)
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsHEIF returns true if this media file is a High Efficiency Image File Format file.
|
// IsHEIF returns true if this media file is a High Efficiency Image File Format file.
|
||||||
func (m *MediaFile) IsHEIF() bool {
|
func (m *MediaFile) IsHEIF() bool {
|
||||||
return m.HasType(FileTypeHEIF)
|
return m.HasType(file.TypeHEIF)
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsSidecar returns true if this media file is a sidecar file (containing metadata).
|
// IsSidecar returns true if this media file is a sidecar file (containing metadata).
|
||||||
func (m *MediaFile) IsSidecar() bool {
|
func (m *MediaFile) IsSidecar() bool {
|
||||||
switch m.Type() {
|
switch m.Type() {
|
||||||
case FileTypeXMP:
|
case file.TypeXMP:
|
||||||
return true
|
return true
|
||||||
case FileTypeAAE:
|
case file.TypeAAE:
|
||||||
return true
|
return true
|
||||||
case FileTypeXML:
|
case file.TypeXML:
|
||||||
return true
|
return true
|
||||||
case FileTypeYaml:
|
case file.TypeYaml:
|
||||||
return true
|
return true
|
||||||
case FileTypeText:
|
case file.TypeText:
|
||||||
return true
|
return true
|
||||||
case FileTypeMarkdown:
|
case file.TypeMarkdown:
|
||||||
return true
|
return true
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
|
@ -526,7 +526,7 @@ func (m *MediaFile) IsSidecar() bool {
|
||||||
// IsVideo returns true if this media file is a video file.
|
// IsVideo returns true if this media file is a video file.
|
||||||
func (m *MediaFile) IsVideo() bool {
|
func (m *MediaFile) IsVideo() bool {
|
||||||
switch m.Type() {
|
switch m.Type() {
|
||||||
case FileTypeMovie:
|
case file.TypeMovie:
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -544,9 +544,9 @@ func (m *MediaFile) Jpeg() (*MediaFile, error) {
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
jpegFilename := fmt.Sprintf("%s.%s", m.DirectoryBasename(), FileTypeJpeg)
|
jpegFilename := fmt.Sprintf("%s.%s", m.DirectoryBasename(), file.TypeJpeg)
|
||||||
|
|
||||||
if !util.Exists(jpegFilename) {
|
if !file.Exists(jpegFilename) {
|
||||||
return nil, fmt.Errorf("jpeg file does not exist: %s", jpegFilename)
|
return nil, fmt.Errorf("jpeg file does not exist: %s", jpegFilename)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/util"
|
"github.com/photoprism/photoprism/internal/file"
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/config"
|
"github.com/photoprism/photoprism/internal/config"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
@ -514,7 +514,7 @@ func TestMediaFile_Move(t *testing.T) {
|
||||||
f, err := NewMediaFile(conf.ExamplesPath() + "/table_white.jpg")
|
f, err := NewMediaFile(conf.ExamplesPath() + "/table_white.jpg")
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
f.Copy(origName)
|
f.Copy(origName)
|
||||||
assert.True(t, util.Exists(origName))
|
assert.True(t, file.Exists(origName))
|
||||||
|
|
||||||
m, err := NewMediaFile(origName)
|
m, err := NewMediaFile(origName)
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
|
@ -523,7 +523,7 @@ func TestMediaFile_Move(t *testing.T) {
|
||||||
t.Errorf("failed to move: %s", err)
|
t.Errorf("failed to move: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
assert.True(t, util.Exists(destName))
|
assert.True(t, file.Exists(destName))
|
||||||
assert.Equal(t, destName, m.Filename())
|
assert.Equal(t, destName, m.Filename())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -539,7 +539,7 @@ func TestMediaFile_Copy(t *testing.T) {
|
||||||
mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/table_white.jpg")
|
mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/table_white.jpg")
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
mediaFile.Copy(tmpPath + "table_whitecopy.jpg")
|
mediaFile.Copy(tmpPath + "table_whitecopy.jpg")
|
||||||
assert.True(t, util.Exists(tmpPath+"table_whitecopy.jpg"))
|
assert.True(t, file.Exists(tmpPath+"table_whitecopy.jpg"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMediaFile_Extension(t *testing.T) {
|
func TestMediaFile_Extension(t *testing.T) {
|
||||||
|
|
|
@ -16,7 +16,7 @@ import (
|
||||||
|
|
||||||
"github.com/disintegration/imaging"
|
"github.com/disintegration/imaging"
|
||||||
"github.com/photoprism/photoprism/internal/config"
|
"github.com/photoprism/photoprism/internal/config"
|
||||||
"github.com/photoprism/photoprism/internal/util"
|
"github.com/photoprism/photoprism/internal/file"
|
||||||
tf "github.com/tensorflow/tensorflow/tensorflow/go"
|
tf "github.com/tensorflow/tensorflow/tensorflow/go"
|
||||||
"gopkg.in/yaml.v2"
|
"gopkg.in/yaml.v2"
|
||||||
)
|
)
|
||||||
|
@ -69,7 +69,7 @@ func (t *TensorFlow) loadLabelRules() (err error) {
|
||||||
|
|
||||||
log.Debugf("tensorflow: loading label rules from \"%s\"", filepath.Base(fileName))
|
log.Debugf("tensorflow: loading label rules from \"%s\"", filepath.Base(fileName))
|
||||||
|
|
||||||
if !util.Exists(fileName) {
|
if !file.Exists(fileName) {
|
||||||
e := fmt.Errorf("tensorflow: label rules file not found in \"%s\"", filepath.Base(fileName))
|
e := fmt.Errorf("tensorflow: label rules file not found in \"%s\"", filepath.Base(fileName))
|
||||||
log.Error(e.Error())
|
log.Error(e.Error())
|
||||||
return e
|
return e
|
||||||
|
|
|
@ -3,85 +3,19 @@ package photoprism
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"image"
|
"image"
|
||||||
"image/png"
|
|
||||||
"os"
|
"os"
|
||||||
"path"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/config"
|
|
||||||
"github.com/photoprism/photoprism/internal/event"
|
"github.com/photoprism/photoprism/internal/event"
|
||||||
|
"github.com/photoprism/photoprism/internal/file"
|
||||||
|
"github.com/photoprism/photoprism/internal/thumb"
|
||||||
|
|
||||||
"github.com/disintegration/imaging"
|
"github.com/disintegration/imaging"
|
||||||
"github.com/photoprism/photoprism/internal/util"
|
"github.com/photoprism/photoprism/internal/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
|
||||||
MaxThumbWidth = 8192
|
|
||||||
MaxThumbHeight = 8192
|
|
||||||
JpegQuality = 95
|
|
||||||
JpegQualitySmall = 80
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
ResampleFillCenter ResampleOption = iota
|
|
||||||
ResampleFillTopLeft
|
|
||||||
ResampleFillBottomRight
|
|
||||||
ResampleFit
|
|
||||||
ResampleResize
|
|
||||||
ResampleNearestNeighbor
|
|
||||||
ResampleLanczos
|
|
||||||
ResamplePng
|
|
||||||
)
|
|
||||||
|
|
||||||
type ResampleOption int
|
|
||||||
|
|
||||||
var ResampleMethods = map[ResampleOption]string{
|
|
||||||
ResampleFillCenter: "center",
|
|
||||||
ResampleFillTopLeft: "left",
|
|
||||||
ResampleFillBottomRight: "right",
|
|
||||||
ResampleFit: "fit",
|
|
||||||
ResampleResize: "resize",
|
|
||||||
}
|
|
||||||
|
|
||||||
type ThumbnailType struct {
|
|
||||||
Source string
|
|
||||||
Width int
|
|
||||||
Height int
|
|
||||||
Public bool
|
|
||||||
Options []ResampleOption
|
|
||||||
}
|
|
||||||
|
|
||||||
var ThumbnailTypes = map[string]ThumbnailType{
|
|
||||||
"tile_50": {"tile_500", 50, 50, false, []ResampleOption{ResampleFillCenter, ResampleLanczos}},
|
|
||||||
"tile_100": {"tile_500", 100, 100, false, []ResampleOption{ResampleFillCenter, ResampleLanczos}},
|
|
||||||
"tile_224": {"tile_500", 224, 224, false, []ResampleOption{ResampleFillCenter, ResampleLanczos}},
|
|
||||||
"tile_500": {"", 500, 500, false, []ResampleOption{ResampleFillCenter, ResampleLanczos}},
|
|
||||||
"colors": {"fit_720", 3, 3, false, []ResampleOption{ResampleResize, ResampleNearestNeighbor, ResamplePng}},
|
|
||||||
"left_224": {"fit_720", 224, 224, false, []ResampleOption{ResampleFillTopLeft, ResampleLanczos}},
|
|
||||||
"right_224": {"fit_720", 224, 224, false, []ResampleOption{ResampleFillBottomRight, ResampleLanczos}},
|
|
||||||
"fit_720": {"", 720, 720, true, []ResampleOption{ResampleFit, ResampleLanczos}},
|
|
||||||
"fit_1280": {"fit_2048", 1280, 1024, true, []ResampleOption{ResampleFit, ResampleLanczos}},
|
|
||||||
"fit_1920": {"fit_2048", 1920, 1200, true, []ResampleOption{ResampleFit, ResampleLanczos}},
|
|
||||||
"fit_2048": {"", 2048, 2048, true, []ResampleOption{ResampleFit, ResampleLanczos}},
|
|
||||||
"fit_2560": {"", 2560, 1600, true, []ResampleOption{ResampleFit, ResampleLanczos}},
|
|
||||||
"fit_3840": {"", 3840, 2400, true, []ResampleOption{ResampleFit, ResampleLanczos}},
|
|
||||||
}
|
|
||||||
|
|
||||||
var DefaultThumbnails = []string{
|
|
||||||
"fit_3840", "fit_2560", "fit_2048", "fit_1920", "fit_1280", "fit_720", "right_224", "left_224", "colors", "tile_500", "tile_224", "tile_100", "tile_50",
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
for name, t := range ThumbnailTypes {
|
|
||||||
if t.Public {
|
|
||||||
thumb := config.Thumbnail{Name: name, Width: t.Width, Height: t.Height}
|
|
||||||
config.Thumbnails = append(config.Thumbnails, thumb)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateThumbnailsFromOriginals creates default thumbnails for all originals.
|
// CreateThumbnailsFromOriginals creates default thumbnails for all originals.
|
||||||
func CreateThumbnailsFromOriginals(originalsPath string, thumbnailsPath string, force bool) error {
|
func CreateThumbnailsFromOriginals(originalsPath string, thumbnailsPath string, force bool) error {
|
||||||
err := filepath.Walk(originalsPath, func(filename string, fileInfo os.FileInfo, err error) error {
|
err := filepath.Walk(originalsPath, func(filename string, fileInfo os.FileInfo, err error) error {
|
||||||
|
@ -120,14 +54,14 @@ func CreateThumbnailsFromOriginals(originalsPath string, thumbnailsPath string,
|
||||||
|
|
||||||
// Thumbnail returns a thumbnail filename.
|
// Thumbnail returns a thumbnail filename.
|
||||||
func (m *MediaFile) Thumbnail(path string, typeName string) (filename string, err error) {
|
func (m *MediaFile) Thumbnail(path string, typeName string) (filename string, err error) {
|
||||||
thumbType, ok := ThumbnailTypes[typeName]
|
thumbType, ok := thumb.Types[typeName]
|
||||||
|
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Errorf("invalid type: %s", typeName)
|
log.Errorf("invalid type: %s", typeName)
|
||||||
return "", fmt.Errorf("invalid type: %s", typeName)
|
return "", fmt.Errorf("invalid type: %s", typeName)
|
||||||
}
|
}
|
||||||
|
|
||||||
thumbnail, err := ThumbnailFromFile(m.Filename(), m.Hash(), path, thumbType.Width, thumbType.Height, thumbType.Options...)
|
thumbnail, err := thumb.FromFile(m.Filename(), m.Hash(), path, thumbType.Width, thumbType.Height, thumbType.Options...)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("could not create thumbnail: %s", err)
|
log.Errorf("could not create thumbnail: %s", err)
|
||||||
|
@ -148,157 +82,6 @@ func (m *MediaFile) Resample(path string, typeName string) (img image.Image, err
|
||||||
return imaging.Open(filename, imaging.AutoOrientation(true))
|
return imaging.Open(filename, imaging.AutoOrientation(true))
|
||||||
}
|
}
|
||||||
|
|
||||||
func ResampleOptions(opts ...ResampleOption) (method ResampleOption, filter imaging.ResampleFilter, format FileType) {
|
|
||||||
method = ResampleFit
|
|
||||||
filter = imaging.Lanczos
|
|
||||||
format = FileTypeJpeg
|
|
||||||
|
|
||||||
for _, option := range opts {
|
|
||||||
switch option {
|
|
||||||
case ResamplePng:
|
|
||||||
format = FileTypePng
|
|
||||||
case ResampleNearestNeighbor:
|
|
||||||
filter = imaging.NearestNeighbor
|
|
||||||
case ResampleLanczos:
|
|
||||||
filter = imaging.Lanczos
|
|
||||||
case ResampleFillTopLeft:
|
|
||||||
method = ResampleFillTopLeft
|
|
||||||
case ResampleFillCenter:
|
|
||||||
method = ResampleFillCenter
|
|
||||||
case ResampleFillBottomRight:
|
|
||||||
method = ResampleFillBottomRight
|
|
||||||
case ResampleFit:
|
|
||||||
method = ResampleFit
|
|
||||||
case ResampleResize:
|
|
||||||
method = ResampleResize
|
|
||||||
default:
|
|
||||||
panic(fmt.Errorf("not a valid resample option: %d", option))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return method, filter, format
|
|
||||||
}
|
|
||||||
|
|
||||||
func Resample(img image.Image, width, height int, opts ...ResampleOption) (result image.Image) {
|
|
||||||
method, filter, _ := ResampleOptions(opts...)
|
|
||||||
|
|
||||||
if method == ResampleFit {
|
|
||||||
result = imaging.Fit(img, width, height, filter)
|
|
||||||
} else if method == ResampleFillCenter {
|
|
||||||
result = imaging.Fill(img, width, height, imaging.Center, filter)
|
|
||||||
} else if method == ResampleFillTopLeft {
|
|
||||||
result = imaging.Fill(img, width, height, imaging.TopLeft, filter)
|
|
||||||
} else if method == ResampleFillBottomRight {
|
|
||||||
result = imaging.Fill(img, width, height, imaging.BottomRight, filter)
|
|
||||||
} else if method == ResampleResize {
|
|
||||||
result = imaging.Resize(img, width, height, filter)
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func ThumbnailPostfix(width, height int, opts ...ResampleOption) (result string) {
|
|
||||||
method, _, format := ResampleOptions(opts...)
|
|
||||||
|
|
||||||
result = fmt.Sprintf("%dx%d_%s.%s", width, height, ResampleMethods[method], format)
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func ThumbnailFilename(hash string, thumbPath string, width, height int, opts ...ResampleOption) (filename string, err error) {
|
|
||||||
if width < 0 || width > MaxThumbWidth {
|
|
||||||
return "", fmt.Errorf("width has an invalid value: %d", width)
|
|
||||||
}
|
|
||||||
|
|
||||||
if height < 0 || height > MaxThumbHeight {
|
|
||||||
return "", fmt.Errorf("height has an invalid value: %d", height)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(hash) < 4 {
|
|
||||||
return "", fmt.Errorf("file hash is empty or too short: %s", hash)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(thumbPath) == 0 {
|
|
||||||
return "", fmt.Errorf("thumbnail path is empty: %s", thumbPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
postfix := ThumbnailPostfix(width, height, opts...)
|
|
||||||
p := path.Join(thumbPath, hash[0:1], hash[1:2], hash[2:3])
|
|
||||||
|
|
||||||
if err := os.MkdirAll(p, os.ModePerm); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
filename = fmt.Sprintf("%s/%s_%s", p, hash, postfix)
|
|
||||||
|
|
||||||
return filename, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func ThumbnailFromFile(imageFilename string, hash string, thumbPath string, width, height int, opts ...ResampleOption) (fileName string, err error) {
|
|
||||||
if len(hash) < 4 {
|
|
||||||
return "", fmt.Errorf("file hash is empty or too short: %s", hash)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(imageFilename) < 4 {
|
|
||||||
return "", fmt.Errorf("image filename is empty or too short: %s", imageFilename)
|
|
||||||
}
|
|
||||||
|
|
||||||
fileName, err = ThumbnailFilename(hash, thumbPath, width, height, opts...)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("can't determine thumb filename: %s", err)
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
if util.Exists(fileName) {
|
|
||||||
return fileName, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
img, err := imaging.Open(imageFilename, imaging.AutoOrientation(true))
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("can't open original: %s", err)
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := CreateThumbnail(img, fileName, width, height, opts...); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return fileName, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func CreateThumbnail(img image.Image, fileName string, width, height int, opts ...ResampleOption) (result image.Image, err error) {
|
|
||||||
if width < 0 || width > MaxThumbWidth {
|
|
||||||
return img, fmt.Errorf("width has an invalid value: %d", width)
|
|
||||||
}
|
|
||||||
|
|
||||||
if height < 0 || height > MaxThumbHeight {
|
|
||||||
return img, fmt.Errorf("height has an invalid value: %d", height)
|
|
||||||
}
|
|
||||||
|
|
||||||
result = Resample(img, width, height, opts...)
|
|
||||||
|
|
||||||
var saveOption imaging.EncodeOption
|
|
||||||
|
|
||||||
if filepath.Ext(fileName) == "."+string(FileTypePng) {
|
|
||||||
saveOption = imaging.PNGCompressionLevel(png.DefaultCompression)
|
|
||||||
} else if width <= 150 && height <= 150 {
|
|
||||||
saveOption = imaging.JPEGQuality(JpegQualitySmall)
|
|
||||||
} else {
|
|
||||||
saveOption = imaging.JPEGQuality(JpegQuality)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = imaging.Save(result, fileName, saveOption)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("failed to save thumbnail: %v", err)
|
|
||||||
return result, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MediaFile) CreateDefaultThumbnails(thumbPath string, force bool) (err error) {
|
func (m *MediaFile) CreateDefaultThumbnails(thumbPath string, force bool) (err error) {
|
||||||
defer util.ProfileTime(time.Now(), fmt.Sprintf("creating thumbnails for \"%s\"", m.Filename()))
|
defer util.ProfileTime(time.Now(), fmt.Sprintf("creating thumbnails for \"%s\"", m.Filename()))
|
||||||
|
|
||||||
|
@ -314,31 +97,31 @@ func (m *MediaFile) CreateDefaultThumbnails(thumbPath string, force bool) (err e
|
||||||
var sourceImg image.Image
|
var sourceImg image.Image
|
||||||
var sourceImgType string
|
var sourceImgType string
|
||||||
|
|
||||||
for _, name := range DefaultThumbnails {
|
for _, name := range thumb.DefaultTypes {
|
||||||
thumbType := ThumbnailTypes[name]
|
thumbType := thumb.Types[name]
|
||||||
|
|
||||||
if thumbType.Height > MaxThumbHeight || thumbType.Width > MaxThumbWidth {
|
if thumbType.Height > thumb.MaxHeight || thumbType.Width > thumb.MaxWidth {
|
||||||
log.Debugf("thumbs: size exceeds limit (width %d, height %d)", thumbType.Width, thumbType.Height)
|
log.Debugf("thumbs: size exceeds limit (width %d, height %d)", thumbType.Width, thumbType.Height)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if fileName, err := ThumbnailFilename(hash, thumbPath, thumbType.Width, thumbType.Height, thumbType.Options...); err != nil {
|
if fileName, err := thumb.Filename(hash, thumbPath, thumbType.Width, thumbType.Height, thumbType.Options...); err != nil {
|
||||||
log.Errorf("could not create %s thumbnail: \"%s\"", name, err)
|
log.Errorf("could not create %s thumbnail: \"%s\"", name, err)
|
||||||
|
|
||||||
return err
|
return err
|
||||||
} else {
|
} else {
|
||||||
if !force && util.Exists(fileName) {
|
if !force && file.Exists(fileName) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if thumbType.Source != "" {
|
if thumbType.Source != "" {
|
||||||
if thumbType.Source == sourceImgType && sourceImg != nil {
|
if thumbType.Source == sourceImgType && sourceImg != nil {
|
||||||
_, err = CreateThumbnail(sourceImg, fileName, thumbType.Width, thumbType.Height, thumbType.Options...)
|
_, err = thumb.Create(sourceImg, fileName, thumbType.Width, thumbType.Height, thumbType.Options...)
|
||||||
} else {
|
} else {
|
||||||
_, err = CreateThumbnail(img, fileName, thumbType.Width, thumbType.Height, thumbType.Options...)
|
_, err = thumb.Create(img, fileName, thumbType.Width, thumbType.Height, thumbType.Options...)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
sourceImg, err = CreateThumbnail(img, fileName, thumbType.Width, thumbType.Height, thumbType.Options...)
|
sourceImg, err = thumb.Create(img, fileName, thumbType.Width, thumbType.Height, thumbType.Options...)
|
||||||
sourceImgType = name
|
sourceImgType = name
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"github.com/disintegration/imaging"
|
"github.com/disintegration/imaging"
|
||||||
"github.com/photoprism/photoprism/internal/entity"
|
"github.com/photoprism/photoprism/internal/entity"
|
||||||
"github.com/photoprism/photoprism/internal/nsfw"
|
"github.com/photoprism/photoprism/internal/nsfw"
|
||||||
|
"github.com/photoprism/photoprism/internal/thumb"
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/config"
|
"github.com/photoprism/photoprism/internal/config"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
@ -117,7 +118,7 @@ func TestThumbnails_Resample(t *testing.T) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestThumbnails_ThumbnailFilename(t *testing.T) {
|
func TestThumbnails_Filename(t *testing.T) {
|
||||||
conf := config.TestConfig()
|
conf := config.TestConfig()
|
||||||
|
|
||||||
thumbsPath := conf.CachePath() + "/_tmp"
|
thumbsPath := conf.CachePath() + "/_tmp"
|
||||||
|
@ -129,25 +130,25 @@ func TestThumbnails_ThumbnailFilename(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Run("", func(t *testing.T) {
|
t.Run("", func(t *testing.T) {
|
||||||
filename, err := ThumbnailFilename("99988", thumbsPath, 150, 150, ResampleFit, ResampleNearestNeighbor)
|
filename, err := thumb.Filename("99988", thumbsPath, 150, 150, thumb.ResampleFit, thumb.ResampleNearestNeighbor)
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
assert.Equal(t, "/go/src/github.com/photoprism/photoprism/assets/testdata/cache/_tmp/9/9/9/99988_150x150_fit.jpg", filename)
|
assert.Equal(t, "/go/src/github.com/photoprism/photoprism/assets/testdata/cache/_tmp/9/9/9/99988_150x150_fit.jpg", filename)
|
||||||
})
|
})
|
||||||
t.Run("hash too short", func(t *testing.T) {
|
t.Run("hash too short", func(t *testing.T) {
|
||||||
_, err := ThumbnailFilename("999", thumbsPath, 150, 150, ResampleFit, ResampleNearestNeighbor)
|
_, err := thumb.Filename("999", thumbsPath, 150, 150, thumb.ResampleFit, thumb.ResampleNearestNeighbor)
|
||||||
assert.Equal(t, "file hash is empty or too short: 999", err.Error())
|
assert.Equal(t, "file hash is empty or too short: 999", err.Error())
|
||||||
})
|
})
|
||||||
t.Run("invalid width", func(t *testing.T) {
|
t.Run("invalid width", func(t *testing.T) {
|
||||||
_, err := ThumbnailFilename("99988", thumbsPath, -4, 150, ResampleFit, ResampleNearestNeighbor)
|
_, err := thumb.Filename("99988", thumbsPath, -4, 150, thumb.ResampleFit, thumb.ResampleNearestNeighbor)
|
||||||
assert.Equal(t, "width has an invalid value: -4", err.Error())
|
assert.Equal(t, "width has an invalid value: -4", err.Error())
|
||||||
})
|
})
|
||||||
t.Run("invalid height", func(t *testing.T) {
|
t.Run("invalid height", func(t *testing.T) {
|
||||||
_, err := ThumbnailFilename("99988", thumbsPath, 200, -1, ResampleFit, ResampleNearestNeighbor)
|
_, err := thumb.Filename("99988", thumbsPath, 200, -1, thumb.ResampleFit, thumb.ResampleNearestNeighbor)
|
||||||
assert.Equal(t, "height has an invalid value: -1", err.Error())
|
assert.Equal(t, "height has an invalid value: -1", err.Error())
|
||||||
})
|
})
|
||||||
t.Run("empty thumbpath", func(t *testing.T) {
|
t.Run("empty thumbpath", func(t *testing.T) {
|
||||||
path := ""
|
path := ""
|
||||||
_, err := ThumbnailFilename("99988", path, 200, 150, ResampleFit, ResampleNearestNeighbor)
|
_, err := thumb.Filename("99988", path, 200, 150, thumb.ResampleFit, thumb.ResampleNearestNeighbor)
|
||||||
assert.Equal(t, "thumbnail path is empty: ", err.Error())
|
assert.Equal(t, "thumbnail path is empty: ", err.Error())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -169,9 +170,9 @@ func TestThumbnails_ThumbnailFromFile(t *testing.T) {
|
||||||
FileHash: "1234568889",
|
FileHash: "1234568889",
|
||||||
}
|
}
|
||||||
|
|
||||||
thumb, err := ThumbnailFromFile(fileModel.FileName, fileModel.FileHash, thumbsPath, 224, 224)
|
thumbnail, err := thumb.FromFile(fileModel.FileName, fileModel.FileHash, thumbsPath, 224, 224)
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
assert.FileExists(t, thumb)
|
assert.FileExists(t, thumbnail)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("hash too short", func(t *testing.T) {
|
t.Run("hash too short", func(t *testing.T) {
|
||||||
|
@ -180,7 +181,7 @@ func TestThumbnails_ThumbnailFromFile(t *testing.T) {
|
||||||
FileHash: "123",
|
FileHash: "123",
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := ThumbnailFromFile(fileModel.FileName, fileModel.FileHash, thumbsPath, 224, 224)
|
_, err := thumb.FromFile(fileModel.FileName, fileModel.FileHash, thumbsPath, 224, 224)
|
||||||
assert.Equal(t, "file hash is empty or too short: 123", err.Error())
|
assert.Equal(t, "file hash is empty or too short: 123", err.Error())
|
||||||
})
|
})
|
||||||
t.Run("filename too short", func(t *testing.T) {
|
t.Run("filename too short", func(t *testing.T) {
|
||||||
|
@ -189,7 +190,7 @@ func TestThumbnails_ThumbnailFromFile(t *testing.T) {
|
||||||
FileHash: "12367890",
|
FileHash: "12367890",
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := ThumbnailFromFile(fileModel.FileName, fileModel.FileHash, thumbsPath, 224, 224)
|
_, err := thumb.FromFile(fileModel.FileName, fileModel.FileHash, thumbsPath, 224, 224)
|
||||||
assert.Equal(t, "image filename is empty or too short: xxx", err.Error())
|
assert.Equal(t, "image filename is empty or too short: xxx", err.Error())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -210,7 +211,7 @@ func TestThumbnails_CreateThumbnail(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Run("valid parameter", func(t *testing.T) {
|
t.Run("valid parameter", func(t *testing.T) {
|
||||||
expectedFilename, err := ThumbnailFilename("12345", thumbsPath, 150, 150, ResampleFit, ResampleNearestNeighbor)
|
expectedFilename, err := thumb.Filename("12345", thumbsPath, 150, 150, thumb.ResampleFit, thumb.ResampleNearestNeighbor)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Error(err)
|
t.Error(err)
|
||||||
|
@ -222,13 +223,13 @@ func TestThumbnails_CreateThumbnail(t *testing.T) {
|
||||||
t.Errorf("can't open original: %s", err)
|
t.Errorf("can't open original: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
thumb, err := CreateThumbnail(img, expectedFilename, 150, 150, ResampleFit, ResampleNearestNeighbor)
|
thumbnail, err := thumb.Create(img, expectedFilename, 150, 150, thumb.ResampleFit, thumb.ResampleNearestNeighbor)
|
||||||
|
|
||||||
assert.Empty(t, err)
|
assert.Empty(t, err)
|
||||||
|
|
||||||
assert.NotNil(t, thumb)
|
assert.NotNil(t, thumbnail)
|
||||||
|
|
||||||
bounds := thumb.Bounds()
|
bounds := thumbnail.Bounds()
|
||||||
|
|
||||||
assert.Equal(t, 150, bounds.Dx())
|
assert.Equal(t, 150, bounds.Dx())
|
||||||
assert.Equal(t, 99, bounds.Dy())
|
assert.Equal(t, 99, bounds.Dy())
|
||||||
|
@ -236,7 +237,7 @@ func TestThumbnails_CreateThumbnail(t *testing.T) {
|
||||||
assert.FileExists(t, expectedFilename)
|
assert.FileExists(t, expectedFilename)
|
||||||
})
|
})
|
||||||
t.Run("invalid width", func(t *testing.T) {
|
t.Run("invalid width", func(t *testing.T) {
|
||||||
expectedFilename, err := ThumbnailFilename("12345", thumbsPath, 150, 150, ResampleFit, ResampleNearestNeighbor)
|
expectedFilename, err := thumb.Filename("12345", thumbsPath, 150, 150, thumb.ResampleFit, thumb.ResampleNearestNeighbor)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Error(err)
|
t.Error(err)
|
||||||
|
@ -248,7 +249,7 @@ func TestThumbnails_CreateThumbnail(t *testing.T) {
|
||||||
t.Errorf("can't open original: %s", err)
|
t.Errorf("can't open original: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
thumbnail, err := CreateThumbnail(img, expectedFilename, -1, 150, ResampleFit, ResampleNearestNeighbor)
|
thumbnail, err := thumb.Create(img, expectedFilename, -1, 150, thumb.ResampleFit, thumb.ResampleNearestNeighbor)
|
||||||
|
|
||||||
assert.Equal(t, "width has an invalid value: -1", err.Error())
|
assert.Equal(t, "width has an invalid value: -1", err.Error())
|
||||||
bounds := thumbnail.Bounds()
|
bounds := thumbnail.Bounds()
|
||||||
|
@ -256,7 +257,7 @@ func TestThumbnails_CreateThumbnail(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("invalid height", func(t *testing.T) {
|
t.Run("invalid height", func(t *testing.T) {
|
||||||
expectedFilename, err := ThumbnailFilename("12345", thumbsPath, 150, 150, ResampleFit, ResampleNearestNeighbor)
|
expectedFilename, err := thumb.Filename("12345", thumbsPath, 150, 150, thumb.ResampleFit, thumb.ResampleNearestNeighbor)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Error(err)
|
t.Error(err)
|
||||||
|
@ -268,7 +269,7 @@ func TestThumbnails_CreateThumbnail(t *testing.T) {
|
||||||
t.Errorf("can't open original: %s", err)
|
t.Errorf("can't open original: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
thumbnail, err := CreateThumbnail(img, expectedFilename, 150, -1, ResampleFit, ResampleNearestNeighbor)
|
thumbnail, err := thumb.Create(img, expectedFilename, 150, -1, thumb.ResampleFit, thumb.ResampleNearestNeighbor)
|
||||||
|
|
||||||
assert.Equal(t, "height has an invalid value: -1", err.Error())
|
assert.Equal(t, "height has an invalid value: -1", err.Error())
|
||||||
bounds := thumbnail.Bounds()
|
bounds := thumbnail.Bounds()
|
||||||
|
@ -294,7 +295,7 @@ func TestThumbnails_CreateDefaultThumbnails(t *testing.T) {
|
||||||
|
|
||||||
assert.Empty(t, err)
|
assert.Empty(t, err)
|
||||||
|
|
||||||
thumbFilename, err := ThumbnailFilename(m.Hash(), thumbsPath, ThumbnailTypes["tile_50"].Width, ThumbnailTypes["tile_50"].Height, ThumbnailTypes["tile_50"].Options...)
|
thumbFilename, err := thumb.Filename(m.Hash(), thumbsPath, thumb.Types["tile_50"].Width, thumb.Types["tile_50"].Height, thumb.Types["tile_50"].Options...)
|
||||||
|
|
||||||
assert.Empty(t, err)
|
assert.Empty(t, err)
|
||||||
|
|
||||||
|
|
14
internal/rnd/rnd.go
Normal file
14
internal/rnd/rnd.go
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
/*
|
||||||
|
Package config contains random token functions.
|
||||||
|
|
||||||
|
Additional information can be found in our Developer Guide:
|
||||||
|
|
||||||
|
https://github.com/photoprism/photoprism/wiki
|
||||||
|
*/
|
||||||
|
package rnd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/photoprism/photoprism/internal/event"
|
||||||
|
)
|
||||||
|
|
||||||
|
var log = event.Log
|
|
@ -1,4 +1,4 @@
|
||||||
package util
|
package rnd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
|
@ -9,7 +9,7 @@ import (
|
||||||
uuid "github.com/satori/go.uuid"
|
uuid "github.com/satori/go.uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
func RandomToken(size uint) string {
|
func Token(size uint) string {
|
||||||
if size > 10 || size < 1 {
|
if size > 10 || size < 1 {
|
||||||
log.Fatalf("size out of range: %d", size)
|
log.Fatalf("size out of range: %d", size)
|
||||||
}
|
}
|
||||||
|
@ -32,14 +32,14 @@ func RandomToken(size uint) string {
|
||||||
return string(result[:size])
|
return string(result[:size])
|
||||||
}
|
}
|
||||||
|
|
||||||
func RandomPassword() string {
|
func Password() string {
|
||||||
return RandomToken(8)
|
return Token(8)
|
||||||
}
|
}
|
||||||
|
|
||||||
func ID() string {
|
func ID() string {
|
||||||
result := make([]byte, 0, 16)
|
result := make([]byte, 0, 16)
|
||||||
result = append(result, strconv.FormatInt(time.Now().UTC().Unix(), 36)[0:6]...)
|
result = append(result, strconv.FormatInt(time.Now().UTC().Unix(), 36)[0:6]...)
|
||||||
result = append(result, RandomToken(10)...)
|
result = append(result, Token(10)...)
|
||||||
|
|
||||||
return string(result)
|
return string(result)
|
||||||
}
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package util
|
package rnd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
@ -8,24 +8,24 @@ import (
|
||||||
|
|
||||||
func TestRandomToken(t *testing.T) {
|
func TestRandomToken(t *testing.T) {
|
||||||
t.Run("size 4", func(t *testing.T) {
|
t.Run("size 4", func(t *testing.T) {
|
||||||
token := RandomToken(4)
|
token := Token(4)
|
||||||
assert.NotEmpty(t, token)
|
assert.NotEmpty(t, token)
|
||||||
})
|
})
|
||||||
t.Run("size 8", func(t *testing.T) {
|
t.Run("size 8", func(t *testing.T) {
|
||||||
token := RandomToken(9)
|
token := Token(9)
|
||||||
assert.NotEmpty(t, token)
|
assert.NotEmpty(t, token)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRandomPassword(t *testing.T) {
|
func TestRandomPassword(t *testing.T) {
|
||||||
pw := RandomPassword()
|
pw := Password()
|
||||||
t.Logf("password: %s", pw)
|
t.Logf("password: %s", pw)
|
||||||
assert.Equal(t, 8, len(pw))
|
assert.Equal(t, 8, len(pw))
|
||||||
}
|
}
|
||||||
|
|
||||||
func BenchmarkRandomPassword(b *testing.B) {
|
func BenchmarkRandomPassword(b *testing.B) {
|
||||||
for n := 0; n < b.N; n++ {
|
for n := 0; n < b.N; n++ {
|
||||||
RandomPassword()
|
Password()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -45,12 +45,12 @@ func BenchmarkUUID(b *testing.B) {
|
||||||
|
|
||||||
func BenchmarkRandomToken4(b *testing.B) {
|
func BenchmarkRandomToken4(b *testing.B) {
|
||||||
for n := 0; n < b.N; n++ {
|
for n := 0; n < b.N; n++ {
|
||||||
RandomToken(4)
|
Token(4)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func BenchmarkRandomToken3(b *testing.B) {
|
func BenchmarkRandomToken3(b *testing.B) {
|
||||||
for n := 0; n < b.N; n++ {
|
for n := 0; n < b.N; n++ {
|
||||||
RandomToken(3)
|
Token(3)
|
||||||
}
|
}
|
||||||
}
|
}
|
165
internal/thumb/create.go
Normal file
165
internal/thumb/create.go
Normal file
|
@ -0,0 +1,165 @@
|
||||||
|
package thumb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
"image/png"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/photoprism/photoprism/internal/file"
|
||||||
|
|
||||||
|
"github.com/disintegration/imaging"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ResampleOptions(opts ...ResampleOption) (method ResampleOption, filter imaging.ResampleFilter, format file.Type) {
|
||||||
|
method = ResampleFit
|
||||||
|
filter = imaging.Lanczos
|
||||||
|
format = file.TypeJpeg
|
||||||
|
|
||||||
|
for _, option := range opts {
|
||||||
|
switch option {
|
||||||
|
case ResamplePng:
|
||||||
|
format = file.TypePng
|
||||||
|
case ResampleNearestNeighbor:
|
||||||
|
filter = imaging.NearestNeighbor
|
||||||
|
case ResampleLanczos:
|
||||||
|
filter = imaging.Lanczos
|
||||||
|
case ResampleFillTopLeft:
|
||||||
|
method = ResampleFillTopLeft
|
||||||
|
case ResampleFillCenter:
|
||||||
|
method = ResampleFillCenter
|
||||||
|
case ResampleFillBottomRight:
|
||||||
|
method = ResampleFillBottomRight
|
||||||
|
case ResampleFit:
|
||||||
|
method = ResampleFit
|
||||||
|
case ResampleResize:
|
||||||
|
method = ResampleResize
|
||||||
|
default:
|
||||||
|
panic(fmt.Errorf("not a valid resample option: %d", option))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return method, filter, format
|
||||||
|
}
|
||||||
|
|
||||||
|
func Resample(img image.Image, width, height int, opts ...ResampleOption) (result image.Image) {
|
||||||
|
method, filter, _ := ResampleOptions(opts...)
|
||||||
|
|
||||||
|
if method == ResampleFit {
|
||||||
|
result = imaging.Fit(img, width, height, filter)
|
||||||
|
} else if method == ResampleFillCenter {
|
||||||
|
result = imaging.Fill(img, width, height, imaging.Center, filter)
|
||||||
|
} else if method == ResampleFillTopLeft {
|
||||||
|
result = imaging.Fill(img, width, height, imaging.TopLeft, filter)
|
||||||
|
} else if method == ResampleFillBottomRight {
|
||||||
|
result = imaging.Fill(img, width, height, imaging.BottomRight, filter)
|
||||||
|
} else if method == ResampleResize {
|
||||||
|
result = imaging.Resize(img, width, height, filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func Postfix(width, height int, opts ...ResampleOption) (result string) {
|
||||||
|
method, _, format := ResampleOptions(opts...)
|
||||||
|
|
||||||
|
result = fmt.Sprintf("%dx%d_%s.%s", width, height, ResampleMethods[method], format)
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func Filename(hash string, thumbPath string, width, height int, opts ...ResampleOption) (filename string, err error) {
|
||||||
|
if width < 0 || width > MaxWidth {
|
||||||
|
return "", fmt.Errorf("width has an invalid value: %d", width)
|
||||||
|
}
|
||||||
|
|
||||||
|
if height < 0 || height > MaxHeight {
|
||||||
|
return "", fmt.Errorf("height has an invalid value: %d", height)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(hash) < 4 {
|
||||||
|
return "", fmt.Errorf("file hash is empty or too short: %s", hash)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(thumbPath) == 0 {
|
||||||
|
return "", fmt.Errorf("thumbnail path is empty: %s", thumbPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
postfix := Postfix(width, height, opts...)
|
||||||
|
p := path.Join(thumbPath, hash[0:1], hash[1:2], hash[2:3])
|
||||||
|
|
||||||
|
if err := os.MkdirAll(p, os.ModePerm); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
filename = fmt.Sprintf("%s/%s_%s", p, hash, postfix)
|
||||||
|
|
||||||
|
return filename, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func FromFile(imageFilename string, hash string, thumbPath string, width, height int, opts ...ResampleOption) (fileName string, err error) {
|
||||||
|
if len(hash) < 4 {
|
||||||
|
return "", fmt.Errorf("file hash is empty or too short: %s", hash)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(imageFilename) < 4 {
|
||||||
|
return "", fmt.Errorf("image filename is empty or too short: %s", imageFilename)
|
||||||
|
}
|
||||||
|
|
||||||
|
fileName, err = Filename(hash, thumbPath, width, height, opts...)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("can't determine thumb filename: %s", err)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if file.Exists(fileName) {
|
||||||
|
return fileName, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
img, err := imaging.Open(imageFilename, imaging.AutoOrientation(true))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("can't open original: %s", err)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := Create(img, fileName, width, height, opts...); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return fileName, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Create(img image.Image, fileName string, width, height int, opts ...ResampleOption) (result image.Image, err error) {
|
||||||
|
if width < 0 || width > MaxWidth {
|
||||||
|
return img, fmt.Errorf("width has an invalid value: %d", width)
|
||||||
|
}
|
||||||
|
|
||||||
|
if height < 0 || height > MaxHeight {
|
||||||
|
return img, fmt.Errorf("height has an invalid value: %d", height)
|
||||||
|
}
|
||||||
|
|
||||||
|
result = Resample(img, width, height, opts...)
|
||||||
|
|
||||||
|
var saveOption imaging.EncodeOption
|
||||||
|
|
||||||
|
if filepath.Ext(fileName) == "."+string(file.TypePng) {
|
||||||
|
saveOption = imaging.PNGCompressionLevel(png.DefaultCompression)
|
||||||
|
} else if width <= 150 && height <= 150 {
|
||||||
|
saveOption = imaging.JPEGQuality(JpegQualitySmall)
|
||||||
|
} else {
|
||||||
|
saveOption = imaging.JPEGQuality(JpegQuality)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = imaging.Save(result, fileName, saveOption)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed to save thumbnail: %v", err)
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
14
internal/thumb/thumb.go
Normal file
14
internal/thumb/thumb.go
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
/*
|
||||||
|
This package encapsulates JPEG thumbnail generation.
|
||||||
|
|
||||||
|
Additional information can be found in our Developer Guide:
|
||||||
|
|
||||||
|
https://github.com/photoprism/photoprism/wiki
|
||||||
|
*/
|
||||||
|
package thumb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/photoprism/photoprism/internal/event"
|
||||||
|
)
|
||||||
|
|
||||||
|
var log = event.Log
|
57
internal/thumb/types.go
Normal file
57
internal/thumb/types.go
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
package thumb
|
||||||
|
|
||||||
|
var (
|
||||||
|
MaxWidth = 8192
|
||||||
|
MaxHeight = 8192
|
||||||
|
JpegQuality = 95
|
||||||
|
JpegQualitySmall = 80
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ResampleFillCenter ResampleOption = iota
|
||||||
|
ResampleFillTopLeft
|
||||||
|
ResampleFillBottomRight
|
||||||
|
ResampleFit
|
||||||
|
ResampleResize
|
||||||
|
ResampleNearestNeighbor
|
||||||
|
ResampleLanczos
|
||||||
|
ResamplePng
|
||||||
|
)
|
||||||
|
|
||||||
|
type ResampleOption int
|
||||||
|
|
||||||
|
var ResampleMethods = map[ResampleOption]string{
|
||||||
|
ResampleFillCenter: "center",
|
||||||
|
ResampleFillTopLeft: "left",
|
||||||
|
ResampleFillBottomRight: "right",
|
||||||
|
ResampleFit: "fit",
|
||||||
|
ResampleResize: "resize",
|
||||||
|
}
|
||||||
|
|
||||||
|
type Type struct {
|
||||||
|
Source string
|
||||||
|
Width int
|
||||||
|
Height int
|
||||||
|
Public bool
|
||||||
|
Options []ResampleOption
|
||||||
|
}
|
||||||
|
|
||||||
|
var Types = map[string]Type{
|
||||||
|
"tile_50": {"tile_500", 50, 50, false, []ResampleOption{ResampleFillCenter, ResampleLanczos}},
|
||||||
|
"tile_100": {"tile_500", 100, 100, false, []ResampleOption{ResampleFillCenter, ResampleLanczos}},
|
||||||
|
"tile_224": {"tile_500", 224, 224, false, []ResampleOption{ResampleFillCenter, ResampleLanczos}},
|
||||||
|
"tile_500": {"", 500, 500, false, []ResampleOption{ResampleFillCenter, ResampleLanczos}},
|
||||||
|
"colors": {"fit_720", 3, 3, false, []ResampleOption{ResampleResize, ResampleNearestNeighbor, ResamplePng}},
|
||||||
|
"left_224": {"fit_720", 224, 224, false, []ResampleOption{ResampleFillTopLeft, ResampleLanczos}},
|
||||||
|
"right_224": {"fit_720", 224, 224, false, []ResampleOption{ResampleFillBottomRight, ResampleLanczos}},
|
||||||
|
"fit_720": {"", 720, 720, true, []ResampleOption{ResampleFit, ResampleLanczos}},
|
||||||
|
"fit_1280": {"fit_2048", 1280, 1024, true, []ResampleOption{ResampleFit, ResampleLanczos}},
|
||||||
|
"fit_1920": {"fit_2048", 1920, 1200, true, []ResampleOption{ResampleFit, ResampleLanczos}},
|
||||||
|
"fit_2048": {"", 2048, 2048, true, []ResampleOption{ResampleFit, ResampleLanczos}},
|
||||||
|
"fit_2560": {"", 2560, 1600, true, []ResampleOption{ResampleFit, ResampleLanczos}},
|
||||||
|
"fit_3840": {"", 3840, 2400, true, []ResampleOption{ResampleFit, ResampleLanczos}},
|
||||||
|
}
|
||||||
|
|
||||||
|
var DefaultTypes = []string{
|
||||||
|
"fit_3840", "fit_2560", "fit_2048", "fit_1920", "fit_1280", "fit_720", "right_224", "left_224", "colors", "tile_500", "tile_224", "tile_100", "tile_50",
|
||||||
|
}
|
Loading…
Reference in a new issue