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/event"
|
||||
"github.com/photoprism/photoprism/internal/file"
|
||||
"github.com/photoprism/photoprism/internal/form"
|
||||
"github.com/photoprism/photoprism/internal/photoprism"
|
||||
"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/binding"
|
||||
|
@ -339,7 +341,7 @@ func DownloadAlbum(router *gin.RouterGroup, conf *config.Config) {
|
|||
}
|
||||
|
||||
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)
|
||||
zipFileName := path.Join(zipPath, zipBaseName)
|
||||
|
||||
|
@ -362,21 +364,21 @@ func DownloadAlbum(router *gin.RouterGroup, conf *config.Config) {
|
|||
zipWriter := zip.NewWriter(newZipFile)
|
||||
defer zipWriter.Close()
|
||||
|
||||
for _, file := range p {
|
||||
fileName := path.Join(conf.OriginalsPath(), file.FileName)
|
||||
fileAlias := file.DownloadFileName()
|
||||
for _, f := range p {
|
||||
fileName := path.Join(conf.OriginalsPath(), f.FileName)
|
||||
fileAlias := f.DownloadFileName()
|
||||
|
||||
if util.Exists(fileName) {
|
||||
if file.Exists(fileName) {
|
||||
if err := addFileToZip(zipWriter, fileName, fileAlias); err != nil {
|
||||
log.Error(err)
|
||||
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": util.UcFirst("failed to create zip file")})
|
||||
return
|
||||
}
|
||||
log.Infof("album: added \"%s\" as \"%s\"", file.FileName, fileAlias)
|
||||
log.Infof("album: added \"%s\" as \"%s\"", f.FileName, fileAlias)
|
||||
} else {
|
||||
log.Warnf("album: \"%s\" is missing", file.FileName)
|
||||
file.FileMissing = true
|
||||
conf.Db().Save(&file)
|
||||
log.Warnf("album: \"%s\" is missing", f.FileName)
|
||||
f.FileMissing = true
|
||||
conf.Db().Save(&f)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -385,7 +387,7 @@ func DownloadAlbum(router *gin.RouterGroup, conf *config.Config) {
|
|||
zipWriter.Close()
|
||||
newZipFile.Close()
|
||||
|
||||
if !util.Exists(zipFileName) {
|
||||
if !file.Exists(zipFileName) {
|
||||
log.Errorf("could not find zip file: %s", zipFileName)
|
||||
c.Data(404, "image/svg+xml", photoIconSvg)
|
||||
return
|
||||
|
@ -411,7 +413,7 @@ func AlbumThumbnail(router *gin.RouterGroup, conf *config.Config) {
|
|||
typeName := c.Param("type")
|
||||
uuid := c.Param("uuid")
|
||||
|
||||
thumbType, ok := photoprism.ThumbnailTypes[typeName]
|
||||
thumbType, ok := thumb.Types[typeName]
|
||||
|
||||
if !ok {
|
||||
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())
|
||||
|
||||
file, err := q.FindAlbumThumbByUUID(uuid)
|
||||
f, err := q.FindAlbumThumbByUUID(uuid)
|
||||
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
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)
|
||||
c.Data(http.StatusNotFound, "image/svg+xml", photoIconSvg)
|
||||
|
||||
// Set missing flag so that the file doesn't show up in search results anymore
|
||||
file.FileMissing = true
|
||||
conf.Db().Save(&file)
|
||||
f.FileMissing = true
|
||||
conf.Db().Save(&f)
|
||||
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") != "" {
|
||||
downloadFileName := file.DownloadFileName()
|
||||
downloadFileName := f.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
|
||||
func BatchAlbumsDelete(router *gin.RouterGroup, conf *config.Config) {
|
||||
router.POST("/batch/albums/delete", func(c *gin.Context) {
|
||||
|
|
|
@ -5,8 +5,8 @@ import (
|
|||
"path"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/config"
|
||||
"github.com/photoprism/photoprism/internal/file"
|
||||
"github.com/photoprism/photoprism/internal/query"
|
||||
"github.com/photoprism/photoprism/internal/util"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
@ -24,26 +24,26 @@ func GetDownload(router *gin.RouterGroup, conf *config.Config) {
|
|||
fileHash := c.Param("hash")
|
||||
|
||||
q := query.New(conf.OriginalsPath(), conf.Db())
|
||||
file, err := q.FindFileByHash(fileHash)
|
||||
f, err := q.FindFileByHash(fileHash)
|
||||
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(404, gin.H{"error": err.Error()})
|
||||
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)
|
||||
c.Data(404, "image/svg+xml", photoIconSvg)
|
||||
|
||||
// Set missing flag so that the file doesn't show up in search results anymore
|
||||
file.FileMissing = true
|
||||
conf.Db().Save(&file)
|
||||
f.FileMissing = true
|
||||
conf.Db().Save(&f)
|
||||
return
|
||||
}
|
||||
|
||||
downloadFileName := file.DownloadFileName()
|
||||
downloadFileName := f.DownloadFileName()
|
||||
|
||||
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", downloadFileName))
|
||||
|
||||
|
|
|
@ -8,11 +8,10 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/photoprism/photoprism/internal/config"
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
"github.com/photoprism/photoprism/internal/util"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/photoprism/photoprism/internal/file"
|
||||
"github.com/photoprism/photoprism/internal/photoprism"
|
||||
)
|
||||
|
||||
|
@ -61,7 +60,7 @@ func StartImport(router *gin.RouterGroup, conf *config.Config) {
|
|||
|
||||
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 {
|
||||
log.Errorf("import: could not deleted empty directory \"%s\": %s", path, err)
|
||||
} else {
|
||||
|
|
|
@ -55,11 +55,6 @@ func StartIndexing(router *gin.RouterGroup, conf *config.Config) {
|
|||
return
|
||||
}
|
||||
|
||||
// Set thumbnails JPEG quality and size
|
||||
photoprism.JpegQuality = conf.ThumbQuality()
|
||||
photoprism.MaxThumbWidth = conf.ThumbSize()
|
||||
photoprism.MaxThumbHeight = conf.ThumbSize()
|
||||
|
||||
path := conf.OriginalsPath()
|
||||
|
||||
event.Info(fmt.Sprintf("indexing photos in \"%s\"", filepath.Base(path)))
|
||||
|
|
|
@ -12,9 +12,10 @@ import (
|
|||
"github.com/gin-gonic/gin/binding"
|
||||
"github.com/photoprism/photoprism/internal/config"
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
"github.com/photoprism/photoprism/internal/file"
|
||||
"github.com/photoprism/photoprism/internal/form"
|
||||
"github.com/photoprism/photoprism/internal/photoprism"
|
||||
"github.com/photoprism/photoprism/internal/query"
|
||||
"github.com/photoprism/photoprism/internal/thumb"
|
||||
"github.com/photoprism/photoprism/internal/util"
|
||||
)
|
||||
|
||||
|
@ -123,7 +124,7 @@ func LabelThumbnail(router *gin.RouterGroup, conf *config.Config) {
|
|||
labelUUID := c.Param("uuid")
|
||||
start := time.Now()
|
||||
|
||||
thumbType, ok := photoprism.ThumbnailTypes[typeName]
|
||||
thumbType, ok := thumb.Types[typeName]
|
||||
|
||||
if !ok {
|
||||
log.Errorf("invalid type: %s", typeName)
|
||||
|
@ -142,7 +143,7 @@ func LabelThumbnail(router *gin.RouterGroup, conf *config.Config) {
|
|||
return
|
||||
}
|
||||
|
||||
file, err := q.FindLabelThumbByUUID(labelUUID)
|
||||
f, err := q.FindLabelThumbByUUID(labelUUID)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(err.Error())
|
||||
|
@ -150,19 +151,19 @@ func LabelThumbnail(router *gin.RouterGroup, conf *config.Config) {
|
|||
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)
|
||||
c.Data(http.StatusOK, "image/svg+xml", labelIconSvg)
|
||||
|
||||
// Set missing flag so that the file doesn't show up in search results anymore
|
||||
file.FileMissing = true
|
||||
conf.Db().Save(&file)
|
||||
f.FileMissing = true
|
||||
conf.Db().Save(&f)
|
||||
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)
|
||||
|
||||
if err != nil {
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
|
||||
"github.com/photoprism/photoprism/internal/config"
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
"github.com/photoprism/photoprism/internal/file"
|
||||
"github.com/photoprism/photoprism/internal/query"
|
||||
"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) {
|
||||
router.GET("/photos/:uuid/download", func(c *gin.Context) {
|
||||
q := query.New(conf.OriginalsPath(), conf.Db())
|
||||
file, err := q.FindFileByPhotoUUID(c.Param("uuid"))
|
||||
f, err := q.FindFileByPhotoUUID(c.Param("uuid"))
|
||||
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(404, gin.H{"error": err.Error()})
|
||||
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"))
|
||||
c.Data(404, "image/svg+xml", photoIconSvg)
|
||||
|
||||
// Set missing flag so that the file doesn't show up in search results anymore
|
||||
file.FileMissing = true
|
||||
conf.Db().Save(&file)
|
||||
f.FileMissing = true
|
||||
conf.Db().Save(&f)
|
||||
return
|
||||
}
|
||||
|
||||
downloadFileName := file.DownloadFileName()
|
||||
downloadFileName := f.DownloadFileName()
|
||||
|
||||
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", downloadFileName))
|
||||
|
||||
|
|
|
@ -5,12 +5,11 @@ import (
|
|||
"net/http"
|
||||
"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/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
|
||||
|
@ -23,7 +22,7 @@ func GetThumbnail(router *gin.RouterGroup, conf *config.Config) {
|
|||
fileHash := c.Param("hash")
|
||||
typeName := c.Param("type")
|
||||
|
||||
thumbType, ok := photoprism.ThumbnailTypes[typeName]
|
||||
thumbType, ok := thumb.Types[typeName]
|
||||
|
||||
if !ok {
|
||||
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())
|
||||
file, err := q.FindFileByHash(fileHash)
|
||||
f, err := q.FindFileByHash(fileHash)
|
||||
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
||||
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)
|
||||
c.Data(http.StatusNotFound, "image/svg+xml", photoIconSvg)
|
||||
|
||||
// Set missing flag so that the file doesn't show up in search results anymore
|
||||
file.FileMissing = true
|
||||
conf.Db().Save(&file)
|
||||
f.FileMissing = true
|
||||
conf.Db().Save(&f)
|
||||
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") != "" {
|
||||
downloadFileName := file.DownloadFileName()
|
||||
downloadFileName := f.DownloadFileName()
|
||||
|
||||
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", downloadFileName))
|
||||
}
|
||||
|
|
|
@ -12,10 +12,10 @@ import (
|
|||
"github.com/disintegration/imaging"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/photoprism/photoprism/internal/config"
|
||||
"github.com/photoprism/photoprism/internal/file"
|
||||
"github.com/photoprism/photoprism/internal/form"
|
||||
"github.com/photoprism/photoprism/internal/photoprism"
|
||||
"github.com/photoprism/photoprism/internal/query"
|
||||
"github.com/photoprism/photoprism/internal/util"
|
||||
"github.com/photoprism/photoprism/internal/thumb"
|
||||
)
|
||||
|
||||
// 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])
|
||||
|
||||
if util.Exists(previewFilename) {
|
||||
if file.Exists(previewFilename) {
|
||||
c.File(previewFilename)
|
||||
return
|
||||
}
|
||||
|
@ -60,22 +60,22 @@ func GetPreview(router *gin.RouterGroup, conf *config.Config) {
|
|||
y := 0
|
||||
|
||||
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 {
|
||||
fileName := path.Join(conf.OriginalsPath(), file.FileName)
|
||||
for _, f := range p {
|
||||
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)
|
||||
c.Data(http.StatusNotFound, "image/svg+xml", photoIconSvg)
|
||||
|
||||
// Set missing flag so that the file doesn't show up in search results anymore
|
||||
file.FileMissing = true
|
||||
conf.Db().Save(&file)
|
||||
f.FileMissing = true
|
||||
conf.Db().Save(&f)
|
||||
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 {
|
||||
log.Error(err)
|
||||
|
|
|
@ -11,8 +11,10 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/config"
|
||||
"github.com/photoprism/photoprism/internal/file"
|
||||
"github.com/photoprism/photoprism/internal/form"
|
||||
"github.com/photoprism/photoprism/internal/query"
|
||||
"github.com/photoprism/photoprism/internal/rnd"
|
||||
"github.com/photoprism/photoprism/internal/util"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
@ -44,7 +46,7 @@ func CreateZip(router *gin.RouterGroup, conf *config.Config) {
|
|||
}
|
||||
|
||||
zipPath := path.Join(conf.ExportPath(), "zip")
|
||||
zipToken := util.RandomToken(3)
|
||||
zipToken := rnd.Token(3)
|
||||
zipYear := time.Now().Format("January-2006")
|
||||
zipBaseName := fmt.Sprintf("Photos-%s-%s.zip", zipYear, zipToken)
|
||||
zipFileName := path.Join(zipPath, zipBaseName)
|
||||
|
@ -68,21 +70,21 @@ func CreateZip(router *gin.RouterGroup, conf *config.Config) {
|
|||
zipWriter := zip.NewWriter(newZipFile)
|
||||
defer zipWriter.Close()
|
||||
|
||||
for _, file := range files {
|
||||
fileName := path.Join(conf.OriginalsPath(), file.FileName)
|
||||
fileAlias := file.DownloadFileName()
|
||||
for _, f := range files {
|
||||
fileName := path.Join(conf.OriginalsPath(), f.FileName)
|
||||
fileAlias := f.DownloadFileName()
|
||||
|
||||
if util.Exists(fileName) {
|
||||
if file.Exists(fileName) {
|
||||
if err := addFileToZip(zipWriter, fileName, fileAlias); err != nil {
|
||||
log.Error(err)
|
||||
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": util.UcFirst("failed to create zip file")})
|
||||
return
|
||||
}
|
||||
log.Infof("zip: added \"%s\" as \"%s\"", file.FileName, fileAlias)
|
||||
log.Infof("zip: added \"%s\" as \"%s\"", f.FileName, fileAlias)
|
||||
} else {
|
||||
log.Warnf("zip: \"%s\" is missing", file.FileName)
|
||||
file.FileMissing = true
|
||||
conf.Db().Save(&file)
|
||||
log.Warnf("zip: \"%s\" is missing", f.FileName)
|
||||
f.FileMissing = true
|
||||
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))
|
||||
|
||||
if !util.Exists(zipFileName) {
|
||||
if !file.Exists(zipFileName) {
|
||||
log.Errorf("could not find zip file: %s", zipFileName)
|
||||
c.Data(404, "image/svg+xml", photoIconSvg)
|
||||
return
|
||||
|
|
|
@ -12,14 +12,14 @@ import (
|
|||
"syscall"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
"github.com/photoprism/photoprism/internal/util"
|
||||
"github.com/photoprism/photoprism/internal/file"
|
||||
"github.com/sevlyar/go-daemon"
|
||||
)
|
||||
|
||||
var log = event.Log
|
||||
|
||||
func childAlreadyRunning(filePath string) (pid int, running bool) {
|
||||
if !util.Exists(filePath) {
|
||||
if !file.Exists(filePath) {
|
||||
return pid, false
|
||||
}
|
||||
|
||||
|
|
|
@ -10,8 +10,8 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/config"
|
||||
"github.com/photoprism/photoprism/internal/file"
|
||||
"github.com/photoprism/photoprism/internal/server"
|
||||
"github.com/photoprism/photoprism/internal/util"
|
||||
"github.com/sevlyar/go-daemon"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
@ -96,7 +96,7 @@ func startAction(ctx *cli.Context) error {
|
|||
}
|
||||
|
||||
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())
|
||||
}
|
||||
|
||||
|
|
|
@ -32,10 +32,6 @@ func thumbnailsAction(ctx *cli.Context) error {
|
|||
|
||||
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 {
|
||||
log.Error(err)
|
||||
return err
|
||||
|
|
|
@ -6,7 +6,7 @@ import (
|
|||
|
||||
"github.com/photoprism/photoprism/internal/colors"
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
"github.com/photoprism/photoprism/internal/util"
|
||||
"github.com/photoprism/photoprism/internal/file"
|
||||
)
|
||||
|
||||
// HTTP client / Web UI config values
|
||||
|
@ -118,8 +118,8 @@ func (c *Config) ClientConfig() ClientConfig {
|
|||
categories[i].Title = strings.Title(l.LabelName)
|
||||
}
|
||||
|
||||
jsHash := util.Hash(c.HttpStaticBuildPath() + "/app.js")
|
||||
cssHash := util.Hash(c.HttpStaticBuildPath() + "/app.css")
|
||||
jsHash := file.Hash(c.HttpStaticBuildPath() + "/app.js")
|
||||
cssHash := file.Hash(c.HttpStaticBuildPath() + "/app.css")
|
||||
|
||||
// Feature Flags
|
||||
var flags []string
|
||||
|
|
|
@ -10,6 +10,7 @@ import (
|
|||
_ "github.com/jinzhu/gorm/dialects/sqlite"
|
||||
gc "github.com/patrickmn/go-cache"
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
"github.com/photoprism/photoprism/internal/thumb"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
@ -22,6 +23,15 @@ type Config struct {
|
|||
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) {
|
||||
log.SetFormatter(&logrus.TextFormatter{
|
||||
DisableColors: false,
|
||||
|
@ -44,6 +54,10 @@ func NewConfig(ctx *cli.Context) *Config {
|
|||
|
||||
log.SetLevel(c.LogLevel())
|
||||
|
||||
thumb.JpegQuality = c.ThumbQuality()
|
||||
thumb.MaxWidth = c.ThumbSize()
|
||||
thumb.MaxHeight = c.ThumbSize()
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
|
@ -180,17 +194,32 @@ func (c *Config) Workers() int {
|
|||
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 {
|
||||
if c.config.ThumbQuality > 100 {
|
||||
return 100
|
||||
}
|
||||
|
||||
if c.config.ThumbQuality < 25 {
|
||||
return 25
|
||||
}
|
||||
|
||||
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 {
|
||||
if c.config.ThumbSize > 16384 {
|
||||
return 16384
|
||||
}
|
||||
|
||||
if c.config.ThumbSize < 720 {
|
||||
return 720
|
||||
}
|
||||
|
||||
return c.config.ThumbSize
|
||||
}
|
||||
|
||||
|
||||
// GeoCodingApi returns the preferred geo coding api (none, osm or places).
|
||||
func (c *Config) GeoCodingApi() string {
|
||||
switch c.config.GeoCodingApi {
|
||||
|
|
|
@ -3,7 +3,7 @@ package config
|
|||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/util"
|
||||
"github.com/photoprism/photoprism/internal/file"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
|
@ -17,7 +17,7 @@ func TestNewConfig(t *testing.T) {
|
|||
|
||||
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.ReadOnly())
|
||||
}
|
||||
|
|
|
@ -138,6 +138,9 @@ func (c *Config) connectToDatabase(ctx context.Context) error {
|
|||
}
|
||||
}
|
||||
|
||||
db.LogMode(false)
|
||||
db.SetLogger(log)
|
||||
|
||||
c.db = db
|
||||
return err
|
||||
}
|
||||
|
@ -185,7 +188,7 @@ func (c *Config) ImportSQL(filename string) {
|
|||
continue
|
||||
}
|
||||
|
||||
var result struct {}
|
||||
var result struct{}
|
||||
|
||||
err := q.Raw(stmt).Scan(&result).Error
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ import (
|
|||
"os/exec"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/util"
|
||||
"github.com/photoprism/photoprism/internal/file"
|
||||
)
|
||||
|
||||
func findExecutable(configBin, defaultBin string) (result string) {
|
||||
|
@ -19,7 +19,7 @@ func findExecutable(configBin, defaultBin string) (result string) {
|
|||
result = path
|
||||
}
|
||||
|
||||
if !util.Exists(result) {
|
||||
if !file.Exists(result) {
|
||||
result = ""
|
||||
}
|
||||
|
||||
|
|
|
@ -227,13 +227,13 @@ var GlobalFlags = []cli.Flag{
|
|||
},
|
||||
cli.IntFlag{
|
||||
Name: "thumb-quality, q",
|
||||
Usage: "jpeg quality of thumbnails (0-100)",
|
||||
Usage: "jpeg quality of thumbnails (25-100)",
|
||||
Value: 95,
|
||||
EnvVar: "PHOTOPRISM_THUMB_QUALITY",
|
||||
},
|
||||
cli.IntFlag{
|
||||
Name: "thumb-size",
|
||||
Usage: "max thumbnail size in pixels",
|
||||
Usage: "max thumbnail size in pixels (720-16384)",
|
||||
Value: 8192,
|
||||
EnvVar: "PHOTOPRISM_THUMB_SIZE",
|
||||
},
|
||||
|
|
|
@ -8,7 +8,7 @@ import (
|
|||
|
||||
_ "github.com/jinzhu/gorm/dialects/mysql"
|
||||
_ "github.com/jinzhu/gorm/dialects/sqlite"
|
||||
"github.com/photoprism/photoprism/internal/util"
|
||||
"github.com/photoprism/photoprism/internal/file"
|
||||
"github.com/urfave/cli"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
@ -87,7 +87,7 @@ func NewParams(ctx *cli.Context) *Params {
|
|||
c.Name = ctx.App.Name
|
||||
c.Copyright = ctx.App.Copyright
|
||||
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 {
|
||||
log.Debug(err)
|
||||
|
@ -103,21 +103,21 @@ func NewParams(ctx *cli.Context) *Params {
|
|||
}
|
||||
|
||||
func (c *Params) expandFilenames() {
|
||||
c.ConfigPath = util.ExpandedFilename(c.ConfigPath)
|
||||
c.ResourcesPath = util.ExpandedFilename(c.ResourcesPath)
|
||||
c.AssetsPath = util.ExpandedFilename(c.AssetsPath)
|
||||
c.CachePath = util.ExpandedFilename(c.CachePath)
|
||||
c.OriginalsPath = util.ExpandedFilename(c.OriginalsPath)
|
||||
c.ImportPath = util.ExpandedFilename(c.ImportPath)
|
||||
c.ExportPath = util.ExpandedFilename(c.ExportPath)
|
||||
c.SqlServerPath = util.ExpandedFilename(c.SqlServerPath)
|
||||
c.PIDFilename = util.ExpandedFilename(c.PIDFilename)
|
||||
c.LogFilename = util.ExpandedFilename(c.LogFilename)
|
||||
c.ConfigPath = file.ExpandFilename(c.ConfigPath)
|
||||
c.ResourcesPath = file.ExpandFilename(c.ResourcesPath)
|
||||
c.AssetsPath = file.ExpandFilename(c.AssetsPath)
|
||||
c.CachePath = file.ExpandFilename(c.CachePath)
|
||||
c.OriginalsPath = file.ExpandFilename(c.OriginalsPath)
|
||||
c.ImportPath = file.ExpandFilename(c.ImportPath)
|
||||
c.ExportPath = file.ExpandFilename(c.ExportPath)
|
||||
c.SqlServerPath = file.ExpandFilename(c.SqlServerPath)
|
||||
c.PIDFilename = file.ExpandFilename(c.PIDFilename)
|
||||
c.LogFilename = file.ExpandFilename(c.LogFilename)
|
||||
}
|
||||
|
||||
// SetValuesFromFile uses a yaml config file to initiate the configuration entity.
|
||||
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))
|
||||
}
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ package config
|
|||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/util"
|
||||
"github.com/photoprism/photoprism/internal/file"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
|
@ -17,7 +17,7 @@ func TestNewParams(t *testing.T) {
|
|||
|
||||
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.ReadOnly)
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ import (
|
|||
"io/ioutil"
|
||||
"os"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/util"
|
||||
"github.com/photoprism/photoprism/internal/file"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
|
@ -20,7 +20,7 @@ func NewSettings() *Settings {
|
|||
|
||||
// SetValuesFromFile uses a yaml config file to initiate the configuration entity.
|
||||
func (s *Settings) SetValuesFromFile(fileName string) error {
|
||||
if !util.Exists(fileName) {
|
||||
if !file.Exists(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.
|
||||
func (s *Settings) WriteValuesToFile(fileName string) error {
|
||||
if !util.Exists(fileName) {
|
||||
if !file.Exists(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/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/urfave/cli"
|
||||
)
|
||||
|
@ -29,7 +30,7 @@ func testDataPath(assetsPath string) string {
|
|||
}
|
||||
|
||||
func NewTestParams() *Params {
|
||||
assetsPath := util.ExpandedFilename("../../assets")
|
||||
assetsPath := file.ExpandFilename("../../assets")
|
||||
|
||||
testDataPath := testDataPath(assetsPath)
|
||||
|
||||
|
@ -52,7 +53,7 @@ func NewTestParams() *Params {
|
|||
}
|
||||
|
||||
func NewTestParamsError() *Params {
|
||||
assetsPath := util.ExpandedFilename("../..")
|
||||
assetsPath := file.ExpandFilename("../..")
|
||||
|
||||
testDataPath := testDataPath("../../assets")
|
||||
|
||||
|
@ -93,6 +94,10 @@ func NewTestConfig() *Config {
|
|||
|
||||
c.ImportSQL(c.ExamplesPath() + "/fixtures.sql")
|
||||
|
||||
thumb.JpegQuality = c.ThumbQuality()
|
||||
thumb.MaxWidth = c.ThumbSize()
|
||||
thumb.MaxHeight = c.ThumbSize()
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
|
@ -140,8 +145,8 @@ func (c *Config) RemoveTestData(t *testing.T) {
|
|||
}
|
||||
|
||||
func (c *Config) DownloadTestData(t *testing.T) {
|
||||
if util.Exists(TestDataZip) {
|
||||
hash := util.Hash(TestDataZip)
|
||||
if file.Exists(TestDataZip) {
|
||||
hash := file.Hash(TestDataZip)
|
||||
|
||||
if hash != TestDataHash {
|
||||
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)
|
||||
|
||||
if err := util.Download(TestDataZip, TestDataURL); err != nil {
|
||||
if err := file.Download(TestDataZip, TestDataURL); err != nil {
|
||||
fmt.Printf("Download failed: %s\n", err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
"github.com/photoprism/photoprism/internal/util"
|
||||
"github.com/photoprism/photoprism/internal/file"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
@ -26,7 +26,7 @@ func TestNewTestParams(t *testing.T) {
|
|||
|
||||
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)
|
||||
}
|
||||
|
||||
|
@ -43,7 +43,7 @@ func TestNewTestParamsError(t *testing.T) {
|
|||
|
||||
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.False(t, c.Debug)
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ import (
|
|||
|
||||
"github.com/jinzhu/gorm"
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
"github.com/photoprism/photoprism/internal/util"
|
||||
"github.com/photoprism/photoprism/internal/rnd"
|
||||
)
|
||||
|
||||
var log = event.Log
|
||||
|
@ -32,7 +32,7 @@ func ID(prefix rune) string {
|
|||
result := make([]byte, 0, 17)
|
||||
result = append(result, byte(prefix))
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -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 (
|
||||
"archive/zip"
|
||||
|
@ -9,8 +16,12 @@ import (
|
|||
"os/user"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
)
|
||||
|
||||
var log = event.Log
|
||||
|
||||
// Returns true if file exists
|
||||
func Exists(filename string) bool {
|
||||
info, err := os.Stat(filename)
|
||||
|
@ -30,7 +41,7 @@ func Overwrite(fileName string, data []byte) bool {
|
|||
}
|
||||
|
||||
// Returns full path; ~ replaced with actual home directory
|
||||
func ExpandedFilename(filename string) string {
|
||||
func ExpandFilename(filename string) string {
|
||||
if filename == "" {
|
||||
return ""
|
||||
}
|
||||
|
@ -50,32 +61,6 @@ func ExpandedFilename(filename string) string {
|
|||
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
|
||||
// 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) {
|
||||
|
@ -151,7 +136,7 @@ func Download(filepath string, url string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func DirectoryIsEmpty(path string) bool {
|
||||
func IsEmpty(path string) bool {
|
||||
f, err := os.Open(path)
|
||||
|
||||
if err != nil {
|
|
@ -1,4 +1,4 @@
|
|||
package util
|
||||
package file
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
@ -29,19 +29,19 @@ func TestOverwrite(t *testing.T) {
|
|||
|
||||
func TestExpandedFilename(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.IsType(t, "", filename)
|
||||
})
|
||||
t.Run("empty filename", func(t *testing.T) {
|
||||
filename := ExpandedFilename("")
|
||||
filename := ExpandFilename("")
|
||||
assert.Equal(t, "", filename)
|
||||
assert.IsType(t, "", filename)
|
||||
})
|
||||
t.Run("~ in filename", func(t *testing.T) {
|
||||
usr, _ := user.Current()
|
||||
expected := usr.HomeDir + "/test.jpg"
|
||||
filename := ExpandedFilename("~/test.jpg")
|
||||
filename := ExpandFilename("~/test.jpg")
|
||||
assert.Equal(t, expected, filename)
|
||||
assert.IsType(t, "", filename)
|
||||
})
|
||||
|
@ -49,14 +49,14 @@ func TestExpandedFilename(t *testing.T) {
|
|||
|
||||
func TestDirectoryIsEmpty(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) {
|
||||
assert.Equal(t, false, DirectoryIsEmpty("./xxx"))
|
||||
assert.Equal(t, false, IsEmpty("./xxx"))
|
||||
})
|
||||
t.Run("empty path", func(t *testing.T) {
|
||||
os.Mkdir("./testdata/emptyDir", 0777)
|
||||
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 (
|
||||
"crypto/sha1"
|
|
@ -1,4 +1,4 @@
|
|||
package util
|
||||
package file
|
||||
|
||||
import (
|
||||
"testing"
|
|
@ -1,4 +1,4 @@
|
|||
package util
|
||||
package file
|
||||
|
||||
import (
|
||||
"net/http"
|
|
@ -1,4 +1,4 @@
|
|||
package util
|
||||
package file
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
@ -8,12 +8,12 @@ import (
|
|||
|
||||
func TestMimeType(t *testing.T) {
|
||||
t.Run("jpg", func(t *testing.T) {
|
||||
filename := ExpandedFilename("./testdata/test.jpg")
|
||||
filename := ExpandFilename("./testdata/test.jpg")
|
||||
mimeType := MimeType(filename)
|
||||
assert.Equal(t, "image/jpeg", mimeType)
|
||||
})
|
||||
t.Run("not existing filename", func(t *testing.T) {
|
||||
filename := ExpandedFilename("./testdata/xxx.jpg")
|
||||
filename := ExpandFilename("./testdata/xxx.jpg")
|
||||
mimeType := MimeType(filename)
|
||||
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 (
|
||||
"archive/zip"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ZipFiles compresses one or many files into a single zip archive file.
|
||||
// Param 1: filename is the output zip file's name.
|
||||
// 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)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -21,7 +22,7 @@ func ZipFiles(filename string, files []string) error {
|
|||
|
||||
// Add files to zip
|
||||
for _, file := range files {
|
||||
if err = AddFileToZip(zipWriter, file); err != nil {
|
||||
if err = AddToZip(zipWriter, file); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
@ -29,7 +30,7 @@ func ZipFiles(filename string, files []string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func AddFileToZip(zipWriter *zip.Writer, filename string) error {
|
||||
func AddToZip(zipWriter *zip.Writer, filename string) error {
|
||||
|
||||
fileToZip, err := os.Open(filename)
|
||||
if err != nil {
|
||||
|
@ -59,3 +60,29 @@ func AddFileToZip(zipWriter *zip.Writer, filename string) error {
|
|||
_, err = io.Copy(writer, fileToZip)
|
||||
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"
|
||||
"sync"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/util"
|
||||
"github.com/photoprism/photoprism/internal/file"
|
||||
tf "github.com/tensorflow/tensorflow/tensorflow/go"
|
||||
"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.
|
||||
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)
|
||||
}
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/config"
|
||||
"github.com/photoprism/photoprism/internal/util"
|
||||
"github.com/photoprism/photoprism/internal/file"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
|
@ -30,7 +30,7 @@ func TestConvert_ToJpeg(t *testing.T) {
|
|||
|
||||
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)
|
||||
|
||||
|
@ -66,7 +66,7 @@ func TestConvert_ToJpeg(t *testing.T) {
|
|||
|
||||
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)
|
||||
|
||||
|
@ -92,7 +92,7 @@ func TestConvert_Path(t *testing.T) {
|
|||
|
||||
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)
|
||||
|
||||
|
@ -108,15 +108,15 @@ func TestConvert_Path(t *testing.T) {
|
|||
|
||||
existingJpegFilename := conf.ImportPath() + "/raw/IMG_2567.jpg"
|
||||
|
||||
oldHash := util.Hash(existingJpegFilename)
|
||||
oldHash := file.Hash(existingJpegFilename)
|
||||
|
||||
os.Remove(existingJpegFilename)
|
||||
|
||||
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")
|
||||
}
|
||||
|
|
|
@ -12,8 +12,7 @@ import (
|
|||
"github.com/photoprism/photoprism/internal/config"
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/util"
|
||||
"github.com/photoprism/photoprism/internal/file"
|
||||
)
|
||||
|
||||
// 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 {
|
||||
// Remove empty directories from import path
|
||||
for _, directory := range directories {
|
||||
if util.DirectoryIsEmpty(directory) {
|
||||
if file.IsEmpty(directory) {
|
||||
if err := os.Remove(directory); err != nil {
|
||||
log.Errorf("import: could not deleted empty directory \"%s\" (%s)", directory, err)
|
||||
} else {
|
||||
|
@ -202,8 +201,8 @@ func (imp *Import) DestinationFilename(mainFile *MediaFile, mediaFile *MediaFile
|
|||
|
||||
result := pathName + string(os.PathSeparator) + fileName + fileExtension
|
||||
|
||||
for util.Exists(result) {
|
||||
if mediaFile.Hash() == util.Hash(result) {
|
||||
for file.Exists(result) {
|
||||
if mediaFile.Hash() == file.Hash(result) {
|
||||
return result, fmt.Errorf("file already exists: %s", result)
|
||||
}
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ import (
|
|||
|
||||
"github.com/djherbis/times"
|
||||
"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.
|
||||
|
@ -22,7 +22,7 @@ type MediaFile struct {
|
|||
dateCreated time.Time
|
||||
timeZone string
|
||||
hash string
|
||||
fileType FileType
|
||||
fileType file.Type
|
||||
mimeType string
|
||||
perceptualHash string
|
||||
width int
|
||||
|
@ -33,13 +33,13 @@ type MediaFile struct {
|
|||
|
||||
// NewMediaFile returns a new MediaFile.
|
||||
func NewMediaFile(filename string) (*MediaFile, error) {
|
||||
if !util.Exists(filename) {
|
||||
if !file.Exists(filename) {
|
||||
return nil, fmt.Errorf("file does not exist: %s", filename)
|
||||
}
|
||||
|
||||
instance := &MediaFile{
|
||||
filename: filename,
|
||||
fileType: FileTypeOther,
|
||||
fileType: file.TypeOther,
|
||||
}
|
||||
|
||||
return instance, nil
|
||||
|
@ -231,7 +231,7 @@ func (m *MediaFile) CanonicalNameFromFileWithDirectory() string {
|
|||
// Hash return a sha1 hash of a MediaFile based on the filename.
|
||||
func (m *MediaFile) Hash() string {
|
||||
if len(m.hash) == 0 {
|
||||
m.hash = util.Hash(m.Filename())
|
||||
m.hash = file.Hash(m.Filename())
|
||||
}
|
||||
|
||||
return m.hash
|
||||
|
@ -242,7 +242,7 @@ func (m *MediaFile) EditedFilename() string {
|
|||
basename := filepath.Base(m.filename)
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -381,7 +381,7 @@ func (m *MediaFile) MimeType() string {
|
|||
return m.mimeType
|
||||
}
|
||||
|
||||
m.mimeType = util.MimeType(m.Filename())
|
||||
m.mimeType = file.MimeType(m.Filename())
|
||||
|
||||
return m.mimeType
|
||||
}
|
||||
|
@ -397,7 +397,7 @@ func (m *MediaFile) openFile() (*os.File, error) {
|
|||
|
||||
// Exists checks if a media file exists by filename.
|
||||
func (m *MediaFile) Exists() bool {
|
||||
return util.Exists(m.Filename())
|
||||
return file.Exists(m.Filename())
|
||||
}
|
||||
|
||||
// Remove a media file.
|
||||
|
@ -480,13 +480,13 @@ func (m *MediaFile) IsJpeg() bool {
|
|||
}
|
||||
|
||||
// Type returns the type of the media file.
|
||||
func (m *MediaFile) Type() FileType {
|
||||
return FileExtensions[m.Extension()]
|
||||
func (m *MediaFile) Type() file.Type {
|
||||
return file.Ext[m.Extension()]
|
||||
}
|
||||
|
||||
// HasType returns true if this media file is of a given type.
|
||||
func (m *MediaFile) HasType(t FileType) bool {
|
||||
if t == FileTypeJpeg {
|
||||
func (m *MediaFile) HasType(t file.Type) bool {
|
||||
if t == file.TypeJpeg {
|
||||
return m.IsJpeg()
|
||||
}
|
||||
|
||||
|
@ -495,28 +495,28 @@ func (m *MediaFile) HasType(t FileType) bool {
|
|||
|
||||
// IsRaw returns true if this media file a RAW file.
|
||||
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.
|
||||
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).
|
||||
func (m *MediaFile) IsSidecar() bool {
|
||||
switch m.Type() {
|
||||
case FileTypeXMP:
|
||||
case file.TypeXMP:
|
||||
return true
|
||||
case FileTypeAAE:
|
||||
case file.TypeAAE:
|
||||
return true
|
||||
case FileTypeXML:
|
||||
case file.TypeXML:
|
||||
return true
|
||||
case FileTypeYaml:
|
||||
case file.TypeYaml:
|
||||
return true
|
||||
case FileTypeText:
|
||||
case file.TypeText:
|
||||
return true
|
||||
case FileTypeMarkdown:
|
||||
case file.TypeMarkdown:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
|
@ -526,7 +526,7 @@ func (m *MediaFile) IsSidecar() bool {
|
|||
// IsVideo returns true if this media file is a video file.
|
||||
func (m *MediaFile) IsVideo() bool {
|
||||
switch m.Type() {
|
||||
case FileTypeMovie:
|
||||
case file.TypeMovie:
|
||||
return true
|
||||
}
|
||||
|
||||
|
@ -544,9 +544,9 @@ func (m *MediaFile) Jpeg() (*MediaFile, error) {
|
|||
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)
|
||||
}
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ import (
|
|||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/util"
|
||||
"github.com/photoprism/photoprism/internal/file"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/config"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
@ -514,7 +514,7 @@ func TestMediaFile_Move(t *testing.T) {
|
|||
f, err := NewMediaFile(conf.ExamplesPath() + "/table_white.jpg")
|
||||
assert.Nil(t, err)
|
||||
f.Copy(origName)
|
||||
assert.True(t, util.Exists(origName))
|
||||
assert.True(t, file.Exists(origName))
|
||||
|
||||
m, err := NewMediaFile(origName)
|
||||
assert.Nil(t, err)
|
||||
|
@ -523,7 +523,7 @@ func TestMediaFile_Move(t *testing.T) {
|
|||
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())
|
||||
}
|
||||
|
||||
|
@ -539,7 +539,7 @@ func TestMediaFile_Copy(t *testing.T) {
|
|||
mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/table_white.jpg")
|
||||
assert.Nil(t, err)
|
||||
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) {
|
||||
|
|
|
@ -16,7 +16,7 @@ import (
|
|||
|
||||
"github.com/disintegration/imaging"
|
||||
"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"
|
||||
"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))
|
||||
|
||||
if !util.Exists(fileName) {
|
||||
if !file.Exists(fileName) {
|
||||
e := fmt.Errorf("tensorflow: label rules file not found in \"%s\"", filepath.Base(fileName))
|
||||
log.Error(e.Error())
|
||||
return e
|
||||
|
|
|
@ -3,85 +3,19 @@ package photoprism
|
|||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"image/png"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/config"
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
"github.com/photoprism/photoprism/internal/file"
|
||||
"github.com/photoprism/photoprism/internal/thumb"
|
||||
|
||||
"github.com/disintegration/imaging"
|
||||
"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.
|
||||
func CreateThumbnailsFromOriginals(originalsPath string, thumbnailsPath string, force bool) 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.
|
||||
func (m *MediaFile) Thumbnail(path string, typeName string) (filename string, err error) {
|
||||
thumbType, ok := ThumbnailTypes[typeName]
|
||||
thumbType, ok := thumb.Types[typeName]
|
||||
|
||||
if !ok {
|
||||
log.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 {
|
||||
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))
|
||||
}
|
||||
|
||||
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) {
|
||||
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 sourceImgType string
|
||||
|
||||
for _, name := range DefaultThumbnails {
|
||||
thumbType := ThumbnailTypes[name]
|
||||
for _, name := range thumb.DefaultTypes {
|
||||
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)
|
||||
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)
|
||||
|
||||
return err
|
||||
} else {
|
||||
if !force && util.Exists(fileName) {
|
||||
if !force && file.Exists(fileName) {
|
||||
continue
|
||||
}
|
||||
|
||||
if thumbType.Source != "" {
|
||||
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 {
|
||||
_, err = CreateThumbnail(img, fileName, thumbType.Width, thumbType.Height, thumbType.Options...)
|
||||
_, err = thumb.Create(img, fileName, thumbType.Width, thumbType.Height, thumbType.Options...)
|
||||
}
|
||||
} 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
|
||||
}
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
"github.com/disintegration/imaging"
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
"github.com/photoprism/photoprism/internal/nsfw"
|
||||
"github.com/photoprism/photoprism/internal/thumb"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/config"
|
||||
"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()
|
||||
|
||||
thumbsPath := conf.CachePath() + "/_tmp"
|
||||
|
@ -129,25 +130,25 @@ func TestThumbnails_ThumbnailFilename(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.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) {
|
||||
_, 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())
|
||||
})
|
||||
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())
|
||||
})
|
||||
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())
|
||||
})
|
||||
t.Run("empty thumbpath", func(t *testing.T) {
|
||||
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())
|
||||
})
|
||||
}
|
||||
|
@ -169,9 +170,9 @@ func TestThumbnails_ThumbnailFromFile(t *testing.T) {
|
|||
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.FileExists(t, thumb)
|
||||
assert.FileExists(t, thumbnail)
|
||||
})
|
||||
|
||||
t.Run("hash too short", func(t *testing.T) {
|
||||
|
@ -180,7 +181,7 @@ func TestThumbnails_ThumbnailFromFile(t *testing.T) {
|
|||
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())
|
||||
})
|
||||
t.Run("filename too short", func(t *testing.T) {
|
||||
|
@ -189,7 +190,7 @@ func TestThumbnails_ThumbnailFromFile(t *testing.T) {
|
|||
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())
|
||||
})
|
||||
}
|
||||
|
@ -210,7 +211,7 @@ func TestThumbnails_CreateThumbnail(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 {
|
||||
t.Error(err)
|
||||
|
@ -222,13 +223,13 @@ func TestThumbnails_CreateThumbnail(t *testing.T) {
|
|||
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.NotNil(t, thumb)
|
||||
assert.NotNil(t, thumbnail)
|
||||
|
||||
bounds := thumb.Bounds()
|
||||
bounds := thumbnail.Bounds()
|
||||
|
||||
assert.Equal(t, 150, bounds.Dx())
|
||||
assert.Equal(t, 99, bounds.Dy())
|
||||
|
@ -236,7 +237,7 @@ func TestThumbnails_CreateThumbnail(t *testing.T) {
|
|||
assert.FileExists(t, expectedFilename)
|
||||
})
|
||||
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 {
|
||||
t.Error(err)
|
||||
|
@ -248,7 +249,7 @@ func TestThumbnails_CreateThumbnail(t *testing.T) {
|
|||
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())
|
||||
bounds := thumbnail.Bounds()
|
||||
|
@ -256,7 +257,7 @@ func TestThumbnails_CreateThumbnail(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 {
|
||||
t.Error(err)
|
||||
|
@ -268,7 +269,7 @@ func TestThumbnails_CreateThumbnail(t *testing.T) {
|
|||
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())
|
||||
bounds := thumbnail.Bounds()
|
||||
|
@ -294,7 +295,7 @@ func TestThumbnails_CreateDefaultThumbnails(t *testing.T) {
|
|||
|
||||
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)
|
||||
|
||||
|
|
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 (
|
||||
"crypto/rand"
|
||||
|
@ -9,7 +9,7 @@ import (
|
|||
uuid "github.com/satori/go.uuid"
|
||||
)
|
||||
|
||||
func RandomToken(size uint) string {
|
||||
func Token(size uint) string {
|
||||
if size > 10 || size < 1 {
|
||||
log.Fatalf("size out of range: %d", size)
|
||||
}
|
||||
|
@ -32,14 +32,14 @@ func RandomToken(size uint) string {
|
|||
return string(result[:size])
|
||||
}
|
||||
|
||||
func RandomPassword() string {
|
||||
return RandomToken(8)
|
||||
func Password() string {
|
||||
return Token(8)
|
||||
}
|
||||
|
||||
func ID() string {
|
||||
result := make([]byte, 0, 16)
|
||||
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)
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package util
|
||||
package rnd
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
@ -8,24 +8,24 @@ import (
|
|||
|
||||
func TestRandomToken(t *testing.T) {
|
||||
t.Run("size 4", func(t *testing.T) {
|
||||
token := RandomToken(4)
|
||||
token := Token(4)
|
||||
assert.NotEmpty(t, token)
|
||||
})
|
||||
t.Run("size 8", func(t *testing.T) {
|
||||
token := RandomToken(9)
|
||||
token := Token(9)
|
||||
assert.NotEmpty(t, token)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRandomPassword(t *testing.T) {
|
||||
pw := RandomPassword()
|
||||
pw := Password()
|
||||
t.Logf("password: %s", pw)
|
||||
assert.Equal(t, 8, len(pw))
|
||||
}
|
||||
|
||||
func BenchmarkRandomPassword(b *testing.B) {
|
||||
for n := 0; n < b.N; n++ {
|
||||
RandomPassword()
|
||||
Password()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -45,12 +45,12 @@ func BenchmarkUUID(b *testing.B) {
|
|||
|
||||
func BenchmarkRandomToken4(b *testing.B) {
|
||||
for n := 0; n < b.N; n++ {
|
||||
RandomToken(4)
|
||||
Token(4)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkRandomToken3(b *testing.B) {
|
||||
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