Universal sidecar configuration, indexing with multiple roots #268 #348

Slowly getting to the point where only very few people are able to maintain this codebase :)

Signed-off-by: Michael Mayer <michael@liquidbytes.net>
This commit is contained in:
Michael Mayer 2020-06-07 10:09:35 +02:00
parent 9bd2a867be
commit a91206a509
60 changed files with 468 additions and 319 deletions

View file

@ -39,10 +39,8 @@ services:
PHOTOPRISM_THUMB_SIZE: 2048 # Default thumbnail size limit (default 2048, min 720, max 3840)
PHOTOPRISM_THUMB_LIMIT: 3840 # On-demand thumbnail size limit (default 2048, min 720, max 3840)
PHOTOPRISM_JPEG_QUALITY: 90 # Use 95 for high-quality thumbnails (requires more storage)
PHOTOPRISM_JPEG_HIDDEN: "true" # Create JPEG files in .photoprism (when converting other file types)
PHOTOPRISM_SIDECAR_JSON: "true" # Read metadata from JSON sidecar files created by exiftool
PHOTOPRISM_SIDECAR_YAML: "true" # Backup photo metadata to YAML sidecar files
PHOTOPRISM_SIDECAR_HIDDEN: "true" # Create JSON and YAML sidecar files in .photoprism (if enabled)
CODECOV_TOKEN:
CODECOV_ENV:
CODECOV_URL:

View file

@ -44,10 +44,8 @@ services:
PHOTOPRISM_THUMB_SIZE: 2048 # Default thumbnail size limit (default 2048, min 720, max 3840)
PHOTOPRISM_THUMB_LIMIT: 3840 # On-demand thumbnail size limit (default 2048, min 720, max 3840)
PHOTOPRISM_JPEG_QUALITY: 90 # Use 95 for high-quality thumbnails (requires more storage)
PHOTOPRISM_JPEG_HIDDEN: "true" # Create JPEG files in .photoprism (when converting other file types)
PHOTOPRISM_SIDECAR_JSON: "true" # Read metadata from JSON sidecar files created by exiftool
PHOTOPRISM_SIDECAR_YAML: "true" # Backup photo metadata to YAML sidecar files
PHOTOPRISM_SIDECAR_HIDDEN: "true" # Create JSON and YAML sidecar files in .photoprism (if enabled)
photoprism-db:
image: mariadb:10.5

View file

@ -19,14 +19,12 @@ ENV PHOTOPRISM_UPLOAD_NSFW false
ENV PHOTOPRISM_DETECT_NSFW false
ENV PHOTOPRISM_SIDECAR_JSON true
ENV PHOTOPRISM_SIDECAR_YAML false
ENV PHOTOPRISM_SIDECAR_HIDDEN true
ENV PHOTOPRISM_GEOCODING_API places
ENV PHOTOPRISM_THUMB_FILTER lanczos
ENV PHOTOPRISM_THUMB_UNCACHED true
ENV PHOTOPRISM_THUMB_SIZE 3840
ENV PHOTOPRISM_THUMB_LIMIT 3840
ENV PHOTOPRISM_JPEG_QUALITY 95
ENV PHOTOPRISM_JPEG_HIDDEN false
ENV PHOTOPRISM_SITE_CAPTION "Try our demo"
# Import example photos

View file

@ -40,14 +40,12 @@ services:
# PHOTOPRISM_DATABASE_DSN: "photoprism:photoprism@tcp(photoprism-db:3306)/photoprism?charset=utf8mb4,utf8&parseTime=true"
# PHOTOPRISM_SIDECAR_JSON: "true" # Read metadata from JSON sidecar files created by exiftool
# PHOTOPRISM_SIDECAR_YAML: "true" # Backup photo metadata to YAML sidecar files
PHOTOPRISM_SIDECAR_HIDDEN: "true" # Create JSON and YAML sidecar files in .photoprism (if enabled)
PHOTOPRISM_THUMB_FILTER: "lanczos" # Resample filter, best to worst: blackman, lanczos, cubic, linear
PHOTOPRISM_THUMB_UNCACHED: "false" # On-demand rendering of default thumbnails (high memory and cpu usage)
PHOTOPRISM_THUMB_SIZE: 2048 # Default thumbnail size limit (default 2048, min 720, max 3840)
# PHOTOPRISM_THUMB_SIZE: 3840 # For retina screens (requires more storage)
PHOTOPRISM_THUMB_LIMIT: 3840 # On-demand thumbnail size limit (default 2048, min 720, max 3840)
PHOTOPRISM_JPEG_QUALITY: 90 # Use 95 for high-quality thumbnails (requires more storage)
PHOTOPRISM_JPEG_HIDDEN: "true" # Create JPEG files in .photoprism (when converting other file types)
PHOTOPRISM_STORAGE_PATH: "/photoprism/storage" # Storage PATH for generated files like cache and index
volumes:
- "~/Pictures/Originals:/photoprism/originals" # [local path]:[container path]

View file

@ -39,14 +39,12 @@ services:
# PHOTOPRISM_DATABASE_DSN: "photoprism:photoprism@tcp(photoprism-db:3306)/photoprism?charset=utf8mb4,utf8&parseTime=true"
# PHOTOPRISM_SIDECAR_JSON: "true" # Read metadata from JSON sidecar files created by exiftool
# PHOTOPRISM_SIDECAR_YAML: "true" # Backup photo metadata to YAML sidecar files
PHOTOPRISM_SIDECAR_HIDDEN: "true" # Create JSON and YAML sidecar files in .photoprism (if enabled)
PHOTOPRISM_THUMB_FILTER: "lanczos" # Resample filter, best to worst: blackman, lanczos, cubic, linear
PHOTOPRISM_THUMB_UNCACHED: "false" # On-demand rendering of default thumbnails (high memory and cpu usage)
PHOTOPRISM_THUMB_SIZE: 2048 # Default thumbnail size limit (default 2048, min 720, max 3840)
# PHOTOPRISM_THUMB_SIZE: 3840 # For retina screens (requires more storage)
PHOTOPRISM_THUMB_LIMIT: 3840 # On-demand thumbnail size limit (default 2048, min 720, max 3840)
PHOTOPRISM_JPEG_QUALITY: 90 # Use 95 for high-quality thumbnails (requires more storage)
PHOTOPRISM_JPEG_HIDDEN: "true" # Create JPEG files in .photoprism (when converting other file types)
PHOTOPRISM_STORAGE_PATH: "/photoprism/storage" # Storage PATH for generated files like cache and index
volumes:
- "~/Pictures/Originals:/photoprism/originals" # [local path]:[container path]

View file

@ -15,6 +15,7 @@ import (
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/form"
"github.com/photoprism/photoprism/internal/photoprism"
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/internal/service"
"github.com/photoprism/photoprism/internal/thumb"
@ -381,7 +382,8 @@ func DownloadAlbum(router *gin.RouterGroup, conf *config.Config) {
defer func() { _ = zipWriter.Close() }()
for _, f := range p {
fileName := path.Join(conf.OriginalsPath(), f.FileName)
fileName := photoprism.FileName(f.FileRoot, f.FileName)
fileAlias := f.ShareFileName()
if fs.FileExists(fileName) {
@ -477,7 +479,7 @@ func AlbumThumbnail(router *gin.RouterGroup, conf *config.Config) {
return
}
fileName := path.Join(conf.OriginalsPath(), f.FileName)
fileName := photoprism.FileName(f.FileRoot, f.FileName)
if !fs.FileExists(fileName) {
log.Errorf("album-thumbnail: could not find original for %s", fileName)

View file

@ -15,12 +15,10 @@ import (
// NewApiTest returns new API test helper
func NewApiTest() (app *gin.Engine, router *gin.RouterGroup, conf *config.Config) {
conf = config.TestConfig()
service.SetConfig(conf)
gin.SetMode(gin.TestMode)
app = gin.New()
router = app.Group("/api/v1")
return app, router, conf
return app, router, service.Config()
}
// Performs API request with empty request body.
@ -46,6 +44,7 @@ func TestMain(m *testing.M) {
log.SetLevel(logrus.DebugLevel)
c := config.TestConfig()
service.SetConfig(c)
code := m.Run()

View file

@ -3,9 +3,9 @@ package api
import (
"fmt"
"net/http"
"path"
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/photoprism"
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/pkg/fs"
"github.com/photoprism/photoprism/pkg/txt"
@ -37,7 +37,7 @@ func GetDownload(router *gin.RouterGroup, conf *config.Config) {
return
}
fileName := path.Join(conf.OriginalsPath(), f.FileName)
fileName := photoprism.FileName(f.FileRoot, f.FileName)
if !fs.FileExists(fileName) {
log.Errorf("download: file %s is missing", txt.Quote(f.FileName))

View file

@ -90,7 +90,7 @@ func GetFolders(router *gin.RouterGroup, conf *config.Config, urlPath, rootName,
// GET /api/v1/folders/originals
func GetFoldersOriginals(router *gin.RouterGroup, conf *config.Config) {
GetFolders(router, conf, "originals", entity.RootDefault, conf.OriginalsPath())
GetFolders(router, conf, "originals", entity.RootOriginals, conf.OriginalsPath())
}
// GET /api/v1/folders/import

View file

@ -46,7 +46,7 @@ func TestGetFoldersOriginals(t *testing.T) {
assert.Equal(t, "", folder.FolderDescription)
assert.Equal(t, entity.TypeDefault, folder.FolderType)
assert.Equal(t, entity.SortOrderName, folder.FolderOrder)
assert.Equal(t, entity.RootDefault, folder.Root)
assert.Equal(t, entity.RootOriginals, folder.Root)
assert.IsType(t, "", folder.FolderUID)
assert.Equal(t, false, folder.FolderFavorite)
assert.Equal(t, false, folder.FolderIgnore)
@ -85,7 +85,7 @@ func TestGetFoldersOriginals(t *testing.T) {
assert.Equal(t, "", folder.FolderDescription)
assert.Equal(t, entity.TypeDefault, folder.FolderType)
assert.Equal(t, entity.SortOrderName, folder.FolderOrder)
assert.Equal(t, entity.RootDefault, folder.Root)
assert.Equal(t, entity.RootOriginals, folder.Root)
assert.IsType(t, "", folder.FolderUID)
assert.Equal(t, false, folder.FolderFavorite)
assert.Equal(t, false, folder.FolderIgnore)

View file

@ -4,7 +4,6 @@ import (
"encoding/json"
"fmt"
"net/http"
"path"
"path/filepath"
"strconv"
"time"
@ -15,6 +14,7 @@ import (
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/form"
"github.com/photoprism/photoprism/internal/photoprism"
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/internal/service"
"github.com/photoprism/photoprism/internal/thumb"
@ -221,7 +221,7 @@ func LabelThumbnail(router *gin.RouterGroup, conf *config.Config) {
return
}
fileName := path.Join(conf.OriginalsPath(), f.FileName)
fileName := photoprism.FileName(f.FileRoot, f.FileName)
if !fs.FileExists(fileName) {
log.Errorf("label-thumbnail: file %s is missing", txt.Quote(f.FileName))

View file

@ -3,13 +3,13 @@ package api
import (
"fmt"
"net/http"
"path"
"github.com/gin-gonic/gin"
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/form"
"github.com/photoprism/photoprism/internal/photoprism"
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/pkg/fs"
"github.com/photoprism/photoprism/pkg/txt"
@ -19,12 +19,12 @@ import (
func SavePhotoAsYaml(p entity.Photo, conf *config.Config) {
// Write YAML sidecar file (optional).
if conf.SidecarYaml() {
yamlFile := p.YamlFileName(conf.OriginalsPath(), conf.SidecarHidden())
yamlFile := p.YamlFileName(conf.OriginalsPath(), conf.SidecarPath())
if err := p.SaveAsYaml(yamlFile); err != nil {
log.Errorf("photo: %s (update yaml)", err)
} else {
log.Infof("photo: updated yaml file %s", txt.Quote(fs.RelativeName(yamlFile, conf.OriginalsPath())))
log.Infof("photo: updated yaml file %s", txt.Quote(fs.Rel(yamlFile, conf.OriginalsPath())))
}
}
}
@ -126,7 +126,7 @@ func GetPhotoDownload(router *gin.RouterGroup, conf *config.Config) {
return
}
fileName := path.Join(conf.OriginalsPath(), f.FileName)
fileName := photoprism.FileName(f.FileRoot, f.FileName)
if !fs.FileExists(fileName) {
log.Errorf("photo: file %s is missing", txt.Quote(f.FileName))

View file

@ -4,12 +4,12 @@ import (
"encoding/json"
"fmt"
"net/http"
"path"
"path/filepath"
"time"
"github.com/gin-gonic/gin"
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/photoprism"
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/internal/service"
"github.com/photoprism/photoprism/internal/thumb"
@ -102,7 +102,7 @@ func GetThumbnail(router *gin.RouterGroup, conf *config.Config) {
return
}
fileName := path.Join(conf.OriginalsPath(), f.FileName)
fileName := photoprism.FileName(f.FileRoot, f.FileName)
if !fs.FileExists(fileName) {
log.Errorf("thumbnail: file %s is missing", txt.Quote(f.FileName))

View file

@ -13,6 +13,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/photoprism/photoprism/internal/config"
"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/pkg/fs"
@ -63,7 +64,7 @@ func GetPreview(router *gin.RouterGroup, conf *config.Config) {
thumbType, _ := thumb.Types["tile_224"]
for _, f := range p {
fileName := path.Join(conf.OriginalsPath(), f.FileName)
fileName := photoprism.FileName(f.FileRoot, f.FileName)
if !fs.FileExists(fileName) {
log.Errorf("preview: file %s is missing", txt.Quote(f.FileName))

View file

@ -2,10 +2,10 @@ package api
import (
"net/http"
"path"
"github.com/gin-gonic/gin"
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/photoprism"
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/internal/video"
"github.com/photoprism/photoprism/pkg/fs"
@ -59,7 +59,7 @@ func GetVideo(router *gin.RouterGroup, conf *config.Config) {
return
}
fileName := path.Join(conf.OriginalsPath(), f.FileName)
fileName := photoprism.FileName(f.FileRoot, f.FileName)
if !fs.FileExists(fileName) {
log.Errorf("video: file %s is missing", txt.Quote(f.FileName))

View file

@ -12,6 +12,7 @@ import (
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/form"
"github.com/photoprism/photoprism/internal/photoprism"
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/pkg/fs"
"github.com/photoprism/photoprism/pkg/rnd"
@ -82,7 +83,7 @@ func CreateZip(router *gin.RouterGroup, conf *config.Config) {
defer zipWriter.Close()
for _, f := range files {
fileName := path.Join(conf.OriginalsPath(), f.FileName)
fileName := photoprism.FileName(f.FileRoot, f.FileName)
fileAlias := f.ShareFileName()
if fs.FileExists(fileName) {

View file

@ -92,7 +92,7 @@ func configAction(ctx *cli.Context) error {
fmt.Printf("%-25s %s\n", "exiftool-bin", conf.ExifToolBin())
fmt.Printf("%-25s %t\n", "sidecar-json", conf.SidecarJson())
fmt.Printf("%-25s %t\n", "sidecar-yaml", conf.SidecarYaml())
fmt.Printf("%-25s %t\n", "sidecar-hidden", conf.SidecarHidden())
fmt.Printf("%-25s %s\n", "sidecar-path", conf.SidecarPath())
// Places / Geocoding API configuration.
fmt.Printf("%-25s %s\n", "geocoding-api", conf.GeoCodingApi())
@ -106,7 +106,6 @@ func configAction(ctx *cli.Context) error {
fmt.Printf("%-25s %d\n", "thumb-limit", conf.ThumbLimit())
fmt.Printf("%-25s %s\n", "thumb-path", conf.ThumbPath())
fmt.Printf("%-25s %d\n", "jpeg-quality", conf.JpegQuality())
fmt.Printf("%-25s %t\n", "jpeg-hidden", conf.JpegHidden())
return nil
}

View file

@ -54,7 +54,7 @@ func purgeAction(ctx *cli.Context) error {
if subPath == "" {
log.Infof("removing missing files in %s", txt.Quote(filepath.Base(conf.OriginalsPath())))
} else {
log.Infof("removing missing files in %s", txt.Quote(fs.RelativeName(filepath.Join(conf.OriginalsPath(), subPath), filepath.Dir(conf.OriginalsPath()))))
log.Infof("removing missing files in %s", txt.Quote(fs.Rel(filepath.Join(conf.OriginalsPath(), subPath), filepath.Dir(conf.OriginalsPath()))))
}
if conf.ReadOnly() {

View file

@ -59,6 +59,12 @@ func (c *Config) CreateDirectories() error {
return createError(c.ImportPath(), err)
}
if filepath.IsAbs(c.SidecarPath()) {
if err := os.MkdirAll(c.SidecarPath(), os.ModePerm); err != nil {
return createError(c.SidecarPath(), err)
}
}
if err := os.MkdirAll(c.CachePath(), os.ModePerm); err != nil {
return createError(c.CachePath(), err)
}
@ -188,9 +194,13 @@ func (c *Config) SidecarYaml() bool {
return c.params.SidecarYaml
}
// SidecarHidden returns true if new sidecar files should be created in a .photoprism sub directory (hidden).
func (c *Config) SidecarHidden() bool {
return c.params.SidecarHidden
// SidecarPath returns the storage path for automatically created sidecar files.
func (c *Config) SidecarPath() string {
if c.params.SidecarPath == "" {
c.params.SidecarPath = filepath.Join(c.StoragePath(), "sidecar")
}
return c.params.SidecarPath
}
// HeifConvertBin returns the heif-convert executable file name.

View file

@ -239,6 +239,12 @@ var GlobalFlags = []cli.Flag{
Usage: "create JSON and YAML sidecar files in .photoprism if enabled",
EnvVar: "PHOTOPRISM_SIDECAR_HIDDEN",
},
cli.StringFlag{
Name: "sidecar-path",
Usage: "storage `PATH` for automatically created sidecar files (relative or absolute)",
Value: "",
EnvVar: "PHOTOPRISM_SIDECAR_PATH",
},
cli.BoolFlag{
Name: "detect-nsfw",
Usage: "flag photos as private that may be offensive",
@ -296,9 +302,4 @@ var GlobalFlags = []cli.Flag{
Value: 90,
EnvVar: "PHOTOPRISM_JPEG_QUALITY",
},
cli.BoolFlag{
Name: "jpeg-hidden",
Usage: "create JPEG files in .photoprism when converting other file types",
EnvVar: "PHOTOPRISM_JPEG_HIDDEN",
},
}

View file

@ -70,7 +70,7 @@ type Params struct {
ExifToolBin string `yaml:"exiftool-bin" flag:"exiftool-bin"`
SidecarJson bool `yaml:"sidecar-json" flag:"sidecar-json"`
SidecarYaml bool `yaml:"sidecar-yaml" flag:"sidecar-yaml"`
SidecarHidden bool `yaml:"sidecar-hidden" flag:"sidecar-hidden"`
SidecarPath string `yaml:"sidecar-path" flag:"sidecar-path"`
PIDFilename string `yaml:"pid-filename" flag:"pid-filename"`
LogFilename string `yaml:"log-filename" flag:"log-filename"`
DetachServer bool `yaml:"detach-server" flag:"detach-server"`
@ -83,7 +83,6 @@ type Params struct {
ThumbUncached bool `yaml:"thumb-uncached" flag:"thumb-uncached"`
ThumbSize int `yaml:"thumb-size" flag:"thumb-size"`
ThumbLimit int `yaml:"thumb-limit" flag:"thumb-limit"`
JpegHidden bool `yaml:"jpeg-hidden" flag:"jpeg-hidden"`
JpegQuality int `yaml:"jpeg-quality" flag:"jpeg-quality"`
}

View file

@ -6,11 +6,6 @@ import (
"github.com/photoprism/photoprism/internal/thumb"
)
// JpegHidden returns true if JPEG files should be created in a .photoprism sub directory (hidden).
func (c *Config) JpegHidden() bool {
return c.params.JpegHidden
}
// JpegQuality returns the jpeg quality for resampling, use 95 for high-quality thumbs (25-100).
func (c *Config) JpegQuality() int {
if c.params.JpegQuality > 100 {

View file

@ -60,7 +60,7 @@ func NewTestParams() *Params {
ReadOnly: false,
DetectNSFW: true,
UploadNSFW: false,
SidecarHidden: true,
SidecarPath: fs.HiddenPath,
DarktableBin: "/usr/bin/darktable-cli",
ExifToolBin: "/usr/bin/exiftool",
AssetsPath: assetsPath,

View file

@ -38,9 +38,11 @@ const (
TypeRaw = "raw"
TypeText = "text"
RootDefault = ""
RootImport = "import"
RootPath = "/"
RootOriginals = ""
RootExamples = "examples"
RootSidecar = "sidecar"
RootImport = "import"
RootPath = "/"
Updated = "updated"
Created = "created"

View file

@ -86,7 +86,7 @@ func (m *Folder) SetValuesFromPath() {
s = strings.TrimSpace(s)
if s == "" || s == RootPath {
if m.Root == RootDefault {
if m.Root == RootOriginals {
m.FolderTitle = "Originals"
return

View file

@ -8,8 +8,8 @@ import (
func TestNewFolder(t *testing.T) {
t.Run("2020/05", func(t *testing.T) {
folder := NewFolder(RootDefault, "2020/05", nil)
assert.Equal(t, RootDefault, folder.Root)
folder := NewFolder(RootOriginals, "2020/05", nil)
assert.Equal(t, RootOriginals, folder.Root)
assert.Equal(t, "2020/05", folder.Path)
assert.Equal(t, "May 2020", folder.FolderTitle)
assert.Equal(t, "", folder.FolderDescription)
@ -25,7 +25,7 @@ func TestNewFolder(t *testing.T) {
})
t.Run("/2020/05/01/", func(t *testing.T) {
folder := NewFolder(RootDefault, "/2020/05/01/", nil)
folder := NewFolder(RootOriginals, "/2020/05/01/", nil)
assert.Equal(t, "2020/05/01", folder.Path)
assert.Equal(t, "May 2020", folder.FolderTitle)
assert.Equal(t, 2020, folder.FolderYear)
@ -43,7 +43,7 @@ func TestNewFolder(t *testing.T) {
})
t.Run("/2020/05/23/Iceland 2020", func(t *testing.T) {
folder := NewFolder(RootDefault, "/2020/05/23/Iceland 2020", nil)
folder := NewFolder(RootOriginals, "/2020/05/23/Iceland 2020", nil)
assert.Equal(t, "2020/05/23/Iceland 2020", folder.Path)
assert.Equal(t, "Iceland 2020", folder.FolderTitle)
assert.Equal(t, 2020, folder.FolderYear)
@ -52,7 +52,7 @@ func TestNewFolder(t *testing.T) {
})
t.Run("/London/2020/05/23", func(t *testing.T) {
folder := NewFolder(RootDefault, "/London/2020/05/23", nil)
folder := NewFolder(RootOriginals, "/London/2020/05/23", nil)
assert.Equal(t, "London/2020/05/23", folder.Path)
assert.Equal(t, "May 23, 2020", folder.FolderTitle)
assert.Equal(t, 2020, folder.FolderYear)
@ -61,7 +61,7 @@ func TestNewFolder(t *testing.T) {
})
t.Run("empty", func(t *testing.T) {
folder := NewFolder(RootDefault, "", nil)
folder := NewFolder(RootOriginals, "", nil)
assert.Equal(t, "", folder.Path)
assert.Equal(t, "Originals", folder.FolderTitle)
assert.Equal(t, 0, folder.FolderYear)
@ -70,7 +70,7 @@ func TestNewFolder(t *testing.T) {
})
t.Run("root", func(t *testing.T) {
folder := NewFolder(RootDefault, RootPath, nil)
folder := NewFolder(RootOriginals, RootPath, nil)
assert.Equal(t, "", folder.Path)
assert.Equal(t, "Originals", folder.FolderTitle)
assert.Equal(t, 0, folder.FolderYear)
@ -80,7 +80,7 @@ func TestNewFolder(t *testing.T) {
}
func TestFirstOrCreateFolder(t *testing.T) {
folder := NewFolder(RootDefault, RootPath, nil)
folder := NewFolder(RootOriginals, RootPath, nil)
result := FirstOrCreateFolder(&folder)
if result == nil {
@ -95,7 +95,7 @@ func TestFirstOrCreateFolder(t *testing.T) {
t.Errorf("FolderCountry should be 'zz'")
}
found := FindFolder(RootDefault, RootPath)
found := FindFolder(RootOriginals, RootPath)
if found == nil {
t.Fatal("found should not be nil")

View file

@ -12,8 +12,8 @@ import (
// Location used to associate photos to location
type Location struct {
ID string `gorm:"type:varbinary(16);primary_key;auto_increment:false;" json:"ID" yaml:"ID"`
PlaceID string `gorm:"type:varbinary(16);" json:"-" yaml:"PlaceID"`
ID string `gorm:"type:varbinary(24);primary_key;auto_increment:false;" json:"ID" yaml:"ID"`
PlaceID string `gorm:"type:varbinary(24);" json:"-" yaml:"PlaceID"`
Place *Place `gorm:"PRELOAD:true" json:"Place" yaml:"-"`
LocName string `gorm:"type:varchar(255);" json:"Name" yaml:"Name,omitempty"`
LocCategory string `gorm:"type:varchar(64);" json:"Category" yaml:"Category,omitempty"`

View file

@ -50,8 +50,8 @@ type Photo struct {
PhotoFavorite bool `json:"Favorite" yaml:"Favorite,omitempty"`
PhotoPrivate bool `json:"Private" yaml:"Private,omitempty"`
TimeZone string `gorm:"type:varbinary(64);" json:"TimeZone" yaml:"-"`
PlaceID string `gorm:"type:varbinary(16);index;" json:"PlaceID" yaml:"-"`
LocationID string `gorm:"type:varbinary(16);index;" json:"LocationID" yaml:"-"`
PlaceID string `gorm:"type:varbinary(24);index;" json:"PlaceID" yaml:"-"`
LocationID string `gorm:"type:varbinary(24);index;" json:"LocationID" yaml:"-"`
LocSrc string `gorm:"type:varbinary(8);" json:"LocSrc" yaml:"LocSrc,omitempty"`
PhotoLat float32 `gorm:"type:FLOAT;index;" json:"Lat" yaml:"Lat,omitempty"`
PhotoLng float32 `gorm:"type:FLOAT;index;" json:"Lng" yaml:"Lng,omitempty"`

View file

@ -63,10 +63,6 @@ func (m *Photo) LoadFromYaml(fileName string) error {
}
// YamlFileName returns the YAML backup file name.
func (m *Photo) YamlFileName(originalsPath string, hidden bool) string {
if hidden {
return filepath.Join(originalsPath, m.PhotoPath, fs.HiddenPath, m.PhotoName) + fs.YamlExt
}
return filepath.Join(originalsPath, m.PhotoPath, m.PhotoName) + fs.YamlExt
func (m *Photo) YamlFileName(originalsPath, sidecarPath string) string {
return fs.FileName(filepath.Join(originalsPath, m.PhotoPath, m.PhotoName), sidecarPath, originalsPath, fs.YamlExt, false)
}

View file

@ -11,7 +11,7 @@ import (
// Place used to associate photos to places
type Place struct {
ID string `gorm:"type:varbinary(16);primary_key;auto_increment:false;" json:"PlaceID" yaml:"PlaceID"`
ID string `gorm:"type:varbinary(24);primary_key;auto_increment:false;" json:"PlaceID" yaml:"PlaceID"`
LocLabel string `gorm:"type:varbinary(768);unique_index;" json:"Label" yaml:"Label"`
LocCity string `gorm:"type:varchar(255);" json:"City" yaml:"City,omitempty"`
LocState string `gorm:"type:varchar(255);" json:"State" yaml:"State,omitempty"`

View file

@ -0,0 +1,23 @@
package photoprism
import (
"github.com/photoprism/photoprism/internal/config"
)
var conf *config.Config
func SetConfig(c *config.Config) {
if c == nil {
panic("config is nil")
}
conf = c
}
func Config() *config.Config {
if conf == nil {
panic("config is nil")
}
return conf
}

View file

@ -58,7 +58,7 @@ func (c *Convert) Start(path string) error {
}
ignore.Log = func(fileName string) {
log.Infof(`convert: ignored "%s"`, fs.RelativeName(fileName, path))
log.Infof(`convert: ignored "%s"`, fs.Rel(fileName, path))
}
err := godirwalk.Walk(path, &godirwalk.Options{
@ -134,8 +134,8 @@ func (c *Convert) ConvertCommand(mf *MediaFile, jpegName string, xmpName string)
}
// ToJson uses exiftool to export metadata to a json file.
func (c *Convert) ToJson(mf *MediaFile, hidden bool) (*MediaFile, error) {
jsonName := fs.TypeJson.FindSub(mf.FileName(), fs.HiddenPath, c.conf.Settings().Index.Group)
func (c *Convert) ToJson(mf *MediaFile) (*MediaFile, error) {
jsonName := fs.TypeJson.FindFirst(mf.FileName(), []string{c.conf.SidecarPath(), c.conf.OriginalsPath(), fs.HiddenPath}, c.conf.OriginalsPath(), c.conf.Settings().Index.Group)
result, err := NewMediaFile(jsonName)
@ -147,15 +147,11 @@ func (c *Convert) ToJson(mf *MediaFile, hidden bool) (*MediaFile, error) {
return nil, fmt.Errorf("convert: metadata export to json disabled in read only mode (%s)", mf.RelativeName(c.conf.OriginalsPath()))
}
if hidden {
jsonName = mf.HiddenName(".json", c.conf.Settings().Index.Group)
} else {
jsonName = mf.RelatedName(".json", c.conf.Settings().Index.Group)
}
jsonName = fs.FileName(mf.FileName(), c.conf.SidecarPath(), c.conf.OriginalsPath(), ".json", c.conf.Settings().Index.Group)
fileName := mf.RelativeName(c.conf.OriginalsPath())
log.Infof("convert: %s -> %s", fileName, fs.RelativeName(jsonName, c.conf.OriginalsPath()))
log.Infof("convert: %s -> %s", fileName, filepath.Base(jsonName))
cmd := exec.Command(c.conf.ExifToolBin(), "-j", mf.FileName())
@ -188,7 +184,7 @@ func (c *Convert) ToJson(mf *MediaFile, hidden bool) (*MediaFile, error) {
}
// ToJpeg converts a single image file to JPEG if possible.
func (c *Convert) ToJpeg(image *MediaFile, hidden bool) (*MediaFile, error) {
func (c *Convert) ToJpeg(image *MediaFile) (*MediaFile, error) {
if c.conf.ReadOnly() {
return nil, errors.New("convert: disabled in read-only mode")
}
@ -201,7 +197,7 @@ func (c *Convert) ToJpeg(image *MediaFile, hidden bool) (*MediaFile, error) {
return image, nil
}
jpegName := fs.TypeJpeg.FindSub(image.FileName(), fs.HiddenPath, c.conf.Settings().Index.Group)
jpegName := fs.TypeJpeg.FindFirst(image.FileName(), []string{c.conf.SidecarPath(), fs.HiddenPath}, c.conf.OriginalsPath(), c.conf.Settings().Index.Group)
mediaFile, err := NewMediaFile(jpegName)
@ -213,15 +209,10 @@ func (c *Convert) ToJpeg(image *MediaFile, hidden bool) (*MediaFile, error) {
return nil, fmt.Errorf("convert: disabled in read only mode (%s)", image.RelativeName(c.conf.OriginalsPath()))
}
if hidden {
jpegName = image.HiddenName(fs.JpegExt, c.conf.Settings().Index.Group)
} else {
jpegName = image.RelatedName(fs.JpegExt, c.conf.Settings().Index.Group)
}
jpegName = fs.FileName(image.FileName(), c.conf.SidecarPath(), c.conf.OriginalsPath(), fs.JpegExt, c.conf.Settings().Index.Group)
fileName := image.RelativeName(c.conf.OriginalsPath())
log.Infof("convert: %s -> %s", fileName, fs.RelativeName(jpegName, c.conf.OriginalsPath()))
log.Infof("convert: %s -> %s", fileName, filepath.Base(jpegName))
xmpName := fs.TypeXMP.Find(image.FileName(), c.conf.Settings().Index.Group)

View file

@ -28,7 +28,7 @@ func TestConvert_ToJpeg(t *testing.T) {
t.Run("gopher-video.mp4", func(t *testing.T) {
fileName := conf.ExamplesPath() + "/gopher-video.mp4"
outputName := conf.ExamplesPath() + "/gopher-video.jpg"
outputName := conf.ExamplesPath() + "/.photoprism/gopher-video.jpg"
_ = os.Remove(outputName)
@ -40,7 +40,7 @@ func TestConvert_ToJpeg(t *testing.T) {
t.Fatal(err)
}
jpegFile, err := convert.ToJpeg(mf, false)
jpegFile, err := convert.ToJpeg(mf)
if err != nil {
t.Fatal(err)
@ -67,7 +67,7 @@ func TestConvert_ToJpeg(t *testing.T) {
t.Fatal(err)
}
imageJpeg, err := convert.ToJpeg(mf, true)
imageJpeg, err := convert.ToJpeg(mf)
if err != nil {
t.Fatal(err)
@ -89,7 +89,7 @@ func TestConvert_ToJpeg(t *testing.T) {
t.Fatalf("%s for %s", err.Error(), rawFilename)
}
imageRaw, err := convert.ToJpeg(rawMediaFile, true)
imageRaw, err := convert.ToJpeg(rawMediaFile)
if err != nil {
t.Fatalf("%s for %s", err.Error(), rawFilename)
@ -128,7 +128,7 @@ func TestConvert_ToJson(t *testing.T) {
t.Fatal(err)
}
jsonFile, err := convert.ToJson(mf, true)
jsonFile, err := convert.ToJson(mf)
if err != nil {
t.Fatal(err)
@ -150,7 +150,7 @@ func TestConvert_ToJson(t *testing.T) {
t.Run("IMG_4120.JPG", func(t *testing.T) {
fileName := conf.ExamplesPath() + "/IMG_4120.JPG"
outputName := conf.ExamplesPath() + "/IMG_4120.json"
outputName := conf.ExamplesPath() + "/.photoprism/IMG_4120.json"
_ = os.Remove(outputName)
@ -163,7 +163,7 @@ func TestConvert_ToJson(t *testing.T) {
t.Fatal(err)
}
jsonFile, err := convert.ToJson(mf, false)
jsonFile, err := convert.ToJson(mf)
if err != nil {
t.Fatal(err)
@ -196,7 +196,7 @@ func TestConvert_ToJson(t *testing.T) {
t.Fatal(err)
}
jsonFile, err := convert.ToJson(mf, true)
jsonFile, err := convert.ToJson(mf)
if err != nil {
t.Fatal(err)
@ -222,16 +222,20 @@ func TestConvert_Start(t *testing.T) {
convert := NewConvert(conf)
convert.Start(conf.ImportPath())
err := convert.Start(conf.ImportPath())
jpegFilename := conf.ImportPath() + "/raw/canon_eos_6d.jpg"
if err != nil {
t.Fatal(err)
}
jpegFilename := conf.ImportPath() + "/raw/.photoprism/canon_eos_6d.jpg"
assert.True(t, fs.FileExists(jpegFilename), "Jpeg file was not found - is Darktable installed?")
image, err := NewMediaFile(jpegFilename)
if err != nil {
t.Fatal(err.Error())
t.Fatal(err)
}
assert.Equal(t, jpegFilename, image.fileName, "FileName must be the same")
@ -240,7 +244,7 @@ func TestConvert_Start(t *testing.T) {
assert.Equal(t, "Canon EOS 6D", infoRaw.CameraModel, "UpdateCamera model should be Canon EOS M10")
existingJpegFilename := conf.ImportPath() + "/raw/IMG_2567.jpg"
existingJpegFilename := conf.ImportPath() + "/raw/.photoprism/IMG_2567.jpg"
oldHash := fs.Hash(existingJpegFilename)

View file

@ -9,7 +9,7 @@ type ConvertJob struct {
func ConvertWorker(jobs <-chan ConvertJob) {
for job := range jobs {
if _, err := job.convert.ToJpeg(job.image, job.convert.conf.JpegHidden()); err != nil {
if _, err := job.convert.ToJpeg(job.image); err != nil {
fileName := job.image.RelativeName(job.convert.conf.OriginalsPath())
log.Errorf("convert: could not create jpeg for %s (%s)", fileName, strings.TrimSpace(err.Error()))
}

View file

@ -0,0 +1,20 @@
package photoprism
import (
"path"
"github.com/photoprism/photoprism/internal/entity"
)
func FileName(fileRoot, fileName string) string {
switch fileRoot {
case entity.RootSidecar:
return path.Join(Config().SidecarPath(), fileName)
case entity.RootImport:
return path.Join(Config().ImportPath(), fileName)
case entity.RootExamples:
return path.Join(Config().ExamplesPath(), fileName)
default:
return path.Join(Config().OriginalsPath(), fileName)
}
}

View file

@ -91,7 +91,7 @@ func (imp *Import) Start(opt ImportOptions) map[string]bool {
}
ignore.Log = func(fileName string) {
log.Infof(`import: ignored "%s"`, fs.RelativeName(fileName, importPath))
log.Infof(`import: ignored "%s"`, fs.Rel(fileName, importPath))
}
err := godirwalk.Walk(importPath, &godirwalk.Options{
@ -117,7 +117,7 @@ func (imp *Import) Start(opt ImportOptions) map[string]bool {
if skip, result := fs.SkipWalk(fileName, isDir, isSymlink, done, ignore); skip {
if isDir && result != filepath.SkipDir {
folder := entity.NewFolder(entity.RootImport, fs.RelativeName(fileName, imp.conf.ImportPath()), nil)
folder := entity.NewFolder(entity.RootImport, fs.Rel(fileName, imp.conf.ImportPath()), nil)
if err := folder.Create(); err == nil {
log.Infof("import: added folder /%s", folder.Path)
@ -182,9 +182,9 @@ func (imp *Import) Start(opt ImportOptions) map[string]bool {
for _, directory := range directories {
if fs.IsEmpty(directory) {
if err := os.Remove(directory); err != nil {
log.Errorf("import: could not delete empty folder %s (%s)", txt.Quote(fs.RelativeName(directory, importPath)), err)
log.Errorf("import: could not delete empty folder %s (%s)", txt.Quote(fs.Rel(directory, importPath)), err)
} else {
log.Infof("import: deleted empty folder %s", txt.Quote(fs.RelativeName(directory, importPath)))
log.Infof("import: deleted empty folder %s", txt.Quote(fs.Rel(directory, importPath)))
}
}
}
@ -198,7 +198,7 @@ func (imp *Import) Start(opt ImportOptions) map[string]bool {
}
if err := os.Remove(file); err != nil {
log.Errorf("import: could not remove %s (%s)", txt.Quote(fs.RelativeName(file, importPath)), err.Error())
log.Errorf("import: could not remove %s (%s)", txt.Quote(fs.Rel(file, importPath)), err.Error())
}
}
}
@ -231,7 +231,7 @@ func (imp *Import) DestinationFilename(mainFile *MediaFile, mediaFile *MediaFile
if !mediaFile.IsSidecar() {
if f, err := entity.FirstFileByHash(mediaFile.Hash()); err == nil {
existingFilename := filepath.Join(imp.conf.OriginalsPath(), f.FileName)
existingFilename := FileName(f.FileRoot, f.FileName)
if fs.FileExists(existingFilename) {
return existingFilename, fmt.Errorf("%s is identical to %s (sha1 %s)", txt.Quote(filepath.Base(mediaFile.FileName())), txt.Quote(f.FileName), mediaFile.Hash())
} else {
@ -249,7 +249,7 @@ func (imp *Import) DestinationFilename(mainFile *MediaFile, mediaFile *MediaFile
for fs.FileExists(result) {
if mediaFile.Hash() == fs.Hash(result) {
return result, fmt.Errorf("%s already exists", txt.Quote(fs.RelativeName(result, imp.originalsPath())))
return result, fmt.Errorf("%s already exists", txt.Quote(fs.Rel(result, imp.originalsPath())))
}
iteration++

View file

@ -29,7 +29,7 @@ func ImportWorker(jobs <-chan ImportJob) {
importPath := job.ImportOpt.Path
if related.Main == nil {
log.Warnf("import: no media file found for %s", txt.Quote(fs.RelativeName(job.FileName, importPath)))
log.Warnf("import: no media file found for %s", txt.Quote(fs.Rel(job.FileName, importPath)))
continue
}
@ -50,18 +50,18 @@ func ImportWorker(jobs <-chan ImportJob) {
if related.Main.HasSameName(f) {
destinationMainFilename = destinationFilename
log.Infof("import: moving main %s file %s to %s", f.FileType(), txt.Quote(relativeFilename), txt.Quote(fs.RelativeName(destinationFilename, imp.originalsPath())))
log.Infof("import: moving main %s file %s to %s", f.FileType(), txt.Quote(relativeFilename), txt.Quote(fs.Rel(destinationFilename, imp.originalsPath())))
} else {
log.Infof("import: moving related %s file %s to %s", f.FileType(), txt.Quote(relativeFilename), txt.Quote(fs.RelativeName(destinationFilename, imp.originalsPath())))
log.Infof("import: moving related %s file %s to %s", f.FileType(), txt.Quote(relativeFilename), txt.Quote(fs.Rel(destinationFilename, imp.originalsPath())))
}
if opt.Move {
if err := f.Move(destinationFilename); err != nil {
log.Errorf("import: could not move file to %s (%s)", txt.Quote(fs.RelativeName(destinationMainFilename, imp.originalsPath())), err.Error())
log.Errorf("import: could not move file to %s (%s)", txt.Quote(fs.Rel(destinationMainFilename, imp.originalsPath())), err.Error())
}
} else {
if err := f.Copy(destinationFilename); err != nil {
log.Errorf("import: could not copy file to %s (%s)", txt.Quote(fs.RelativeName(destinationMainFilename, imp.originalsPath())), err.Error())
log.Errorf("import: could not copy file to %s (%s)", txt.Quote(fs.Rel(destinationMainFilename, imp.originalsPath())), err.Error())
}
}
} else {
@ -69,7 +69,7 @@ func ImportWorker(jobs <-chan ImportJob) {
if opt.RemoveExistingFiles {
if err := f.Remove(); err != nil {
log.Errorf("import: could not delete %s (%s)", txt.Quote(fs.RelativeName(f.FileName(), importPath)), err.Error())
log.Errorf("import: could not delete %s (%s)", txt.Quote(fs.Rel(f.FileName(), importPath)), err.Error())
} else {
log.Infof("import: deleted %s (already exists)", txt.Quote(relativeFilename))
}
@ -81,16 +81,16 @@ func ImportWorker(jobs <-chan ImportJob) {
f, err := NewMediaFile(destinationMainFilename)
if err != nil {
log.Errorf("import: could not import %s (%s)", txt.Quote(fs.RelativeName(destinationMainFilename, imp.originalsPath())), err.Error())
log.Errorf("import: could not import %s (%s)", txt.Quote(fs.Rel(destinationMainFilename, imp.originalsPath())), err.Error())
continue
}
if !f.HasJpeg() {
if jpegFile, err := imp.convert.ToJpeg(f, imp.conf.JpegHidden()); err != nil {
if jpegFile, err := imp.convert.ToJpeg(f); err != nil {
log.Errorf("import: creating jpeg failed (%s)", err.Error())
continue
} else {
log.Infof("import: %s created", fs.RelativeName(jpegFile.FileName(), imp.originalsPath()))
log.Infof("import: %s created", fs.Rel(jpegFile.FileName(), imp.originalsPath()))
}
}
@ -104,17 +104,17 @@ func ImportWorker(jobs <-chan ImportJob) {
}
if imp.conf.SidecarJson() && !f.HasJson() {
if jsonFile, err := imp.convert.ToJson(f, imp.conf.SidecarHidden()); err != nil {
if jsonFile, err := imp.convert.ToJson(f); err != nil {
log.Errorf("import: creating json sidecar file failed (%s)", err.Error())
} else {
log.Infof("import: %s created", fs.RelativeName(jsonFile.FileName(), imp.originalsPath()))
log.Infof("import: %s created", fs.Rel(jsonFile.FileName(), imp.originalsPath()))
}
}
related, err := f.RelatedFiles(imp.conf.Settings().Index.Group)
if err != nil {
log.Errorf("import: could not index %s (%s)", txt.Quote(fs.RelativeName(destinationMainFilename, imp.originalsPath())), err.Error())
log.Errorf("import: could not index %s (%s)", txt.Quote(fs.Rel(destinationMainFilename, imp.originalsPath())), err.Error())
continue
}
@ -142,7 +142,7 @@ func ImportWorker(jobs <-chan ImportJob) {
continue
}
} else {
log.Warnf("import: no main file for %s (conversion to jpeg failed?)", fs.RelativeName(destinationMainFilename, imp.originalsPath()))
log.Warnf("import: no main file for %s (conversion to jpeg failed?)", fs.Rel(destinationMainFilename, imp.originalsPath()))
}
for _, f := range related.Files {

View file

@ -107,7 +107,7 @@ func (ind *Index) Start(opt IndexOptions) map[string]bool {
}
ignore.Log = func(fileName string) {
log.Infof(`index: ignored "%s"`, fs.RelativeName(fileName, originalsPath))
log.Infof(`index: ignored "%s"`, fs.Rel(fileName, originalsPath))
}
err := godirwalk.Walk(optionsPath, &godirwalk.Options{
@ -121,7 +121,7 @@ func (ind *Index) Start(opt IndexOptions) map[string]bool {
if skip, result := fs.SkipWalk(fileName, isDir, isSymlink, done, ignore); skip {
if isDir && result != filepath.SkipDir {
folder := entity.NewFolder(entity.RootDefault, fs.RelativeName(fileName, originalsPath), nil)
folder := entity.NewFolder(entity.RootOriginals, fs.Rel(fileName, originalsPath), nil)
if err := folder.Create(); err == nil {
log.Infof("index: added folder /%s", folder.Path)

View file

@ -68,13 +68,12 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
description := entity.Details{}
labels := classify.Labels{}
fileBase := m.Base(ind.conf.Settings().Index.Group)
filePath := m.RelativePath(ind.originalsPath())
fileRoot := entity.RootDefault
fileName := m.RelativeName(ind.originalsPath())
quotedName := txt.Quote(m.RelativeName(ind.originalsPath()))
fileHash := ""
fileRoot, fileBase, filePath, fileName := m.PathNameInfo()
quotedName := txt.Quote(fileName)
fileSize, fileModified := m.Stat()
fileHash := ""
fileChanged := true
fileExists := false
photoExists := false
@ -95,7 +94,7 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
fileQuery = entity.UnscopedDb().First(&file, "file_hash = ?", fileHash)
fileExists = fileQuery.Error == nil
if fileExists && fs.FileExists(filepath.Join(ind.conf.OriginalsPath(), file.FileName)) {
if fileExists && fs.FileExists(FileName(file.FileRoot, file.FileName)) {
result.Status = IndexDuplicate
return result
}
@ -139,11 +138,11 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
} else {
photo.PhotoQuality = -1
if yamlName := fs.TypeYaml.FindSub(m.FileName(), fs.HiddenPath, ind.conf.Settings().Index.Group); yamlName != "" {
if yamlName := fs.TypeYaml.FindFirst(m.FileName(), []string{Config().SidecarPath(), fs.HiddenPath}, Config().OriginalsPath(), Config().Settings().Index.Group); yamlName != "" {
if err := photo.LoadFromYaml(yamlName); err != nil {
log.Errorf("index: %s (restore from yaml) for %s", err.Error(), quotedName)
} else {
log.Infof("index: restored from %s", txt.Quote(fs.RelativeName(yamlName, ind.originalsPath())))
log.Infof("index: restored from %s", txt.Quote(fs.Rel(yamlName, Config().OriginalsPath())))
}
}
}
@ -186,7 +185,7 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
switch {
case m.IsJpeg():
// Color information
if p, err := m.Colors(ind.thumbPath()); err != nil {
if p, err := m.Colors(Config().ThumbPath()); err != nil {
log.Errorf("index: %s for %s", err.Error(), quotedName)
} else {
file.FileMainColor = p.MainColor.Name()
@ -275,11 +274,11 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
if file.FilePrimary {
primaryFile = file
if !ind.conf.TensorFlowOff() {
if !Config().TensorFlowOff() {
// Image classification via TensorFlow.
labels = ind.classifyImage(m)
if !photoExists && ind.conf.Settings().Features.Private && ind.conf.DetectNSFW() {
if !photoExists && Config().Settings().Features.Private && Config().DetectNSFW() {
photo.PhotoPrivate = ind.NSFW(m)
}
}
@ -532,13 +531,13 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
}
// Write YAML sidecar file (optional).
if file.FilePrimary && ind.conf.SidecarYaml() {
yamlFile := photo.YamlFileName(ind.originalsPath(), ind.conf.SidecarHidden())
if file.FilePrimary && Config().SidecarYaml() {
yamlFile := photo.YamlFileName(Config().OriginalsPath(), Config().SidecarPath())
if err := photo.SaveAsYaml(yamlFile); err != nil {
log.Errorf("index: %s (update yaml) for %s", err.Error(), quotedName)
} else {
log.Infof("index: updated yaml file %s", txt.Quote(fs.RelativeName(yamlFile, ind.originalsPath())))
log.Infof("index: updated yaml file %s", txt.Quote(fs.Rel(yamlFile, Config().OriginalsPath())))
}
}
@ -547,7 +546,7 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
// NSFW returns true if media file might be offensive and detection is enabled.
func (ind *Index) NSFW(jpeg *MediaFile) bool {
filename, err := jpeg.Thumbnail(ind.thumbPath(), "fit_720")
filename, err := jpeg.Thumbnail(Config().ThumbPath(), "fit_720")
if err != nil {
log.Error(err)
@ -559,7 +558,7 @@ func (ind *Index) NSFW(jpeg *MediaFile) bool {
return false
} else {
if nsfwLabels.NSFW(nsfw.ThresholdHigh) {
log.Warnf("index: %s might contain offensive content", txt.Quote(jpeg.RelativeName(ind.originalsPath())))
log.Warnf("index: %s might contain offensive content", txt.Quote(jpeg.RelativeName(Config().OriginalsPath())))
return true
}
}
@ -582,7 +581,7 @@ func (ind *Index) classifyImage(jpeg *MediaFile) (results classify.Labels) {
var labels classify.Labels
for _, thumb := range thumbs {
filename, err := jpeg.Thumbnail(ind.thumbPath(), thumb)
filename, err := jpeg.Thumbnail(Config().ThumbPath(), thumb)
if err != nil {
log.Error(err)

View file

@ -24,7 +24,7 @@ func IndexWorker(jobs <-chan IndexJob) {
// Skip sidecar files without related media file.
if related.Main == nil {
log.Warnf("index: no media file found for %s", txt.Quote(fs.RelativeName(job.FileName, ind.originalsPath())))
log.Warnf("index: no media file found for %s", txt.Quote(fs.Rel(job.FileName, ind.originalsPath())))
continue
}
@ -37,11 +37,11 @@ func IndexWorker(jobs <-chan IndexJob) {
f := related.Main
if opt.Convert && !f.HasJpeg() {
if jpegFile, err := ind.convert.ToJpeg(f, ind.conf.JpegHidden()); err != nil {
if jpegFile, err := ind.convert.ToJpeg(f); err != nil {
log.Errorf("index: creating jpeg failed (%s)", err.Error())
continue
} else {
log.Infof("index: %s created", fs.RelativeName(jpegFile.FileName(), ind.originalsPath()))
log.Infof("index: %s created", fs.Rel(jpegFile.FileName(), ind.originalsPath()))
if err := jpegFile.ResampleDefault(ind.thumbPath(), false); err != nil {
log.Errorf("index: could not create default thumbnails (%s)", err.Error())
@ -53,10 +53,10 @@ func IndexWorker(jobs <-chan IndexJob) {
}
if ind.conf.SidecarJson() && !f.HasJson() {
if jsonFile, err := ind.convert.ToJson(f, ind.conf.SidecarHidden()); err != nil {
if jsonFile, err := ind.convert.ToJson(f); err != nil {
log.Errorf("index: creating json sidecar file failed (%s)", err.Error())
} else {
log.Infof("index: %s created", fs.RelativeName(jsonFile.FileName(), ind.originalsPath()))
log.Infof("index: %s created", fs.Rel(jsonFile.FileName(), ind.originalsPath()))
}
}

View file

@ -253,10 +253,8 @@ func (m *MediaFile) EditedName() string {
// RelatedFiles returns files which are related to this file.
func (m *MediaFile) RelatedFiles(stripSequence bool) (result RelatedFiles, err error) {
baseFilename := m.AbsBase(stripSequence)
// escape any meta characters in the file name
baseFilename = regexp.QuoteMeta(baseFilename)
matches, err := filepath.Glob(baseFilename + "*")
matches, err := filepath.Glob(regexp.QuoteMeta(m.AbsBase(stripSequence)) + "*")
if err != nil {
return result, err
@ -292,7 +290,7 @@ func (m *MediaFile) RelatedFiles(stripSequence bool) (result RelatedFiles, err e
// Add hidden JPEG if exists.
if !result.ContainsJpeg() && result.Main != nil {
if jpegName := fs.TypeJpeg.FindSub(result.Main.FileName(), fs.HiddenPath, stripSequence); jpegName != "" {
if jpegName := fs.TypeJpeg.FindFirst(result.Main.FileName(), []string{Config().SidecarPath(), fs.HiddenPath}, Config().OriginalsPath(), stripSequence); jpegName != "" {
if resultFile, err := NewMediaFile(jpegName); err == nil {
result.Files = append(result.Files, resultFile)
}
@ -304,6 +302,27 @@ func (m *MediaFile) RelatedFiles(stripSequence bool) (result RelatedFiles, err e
return result, nil
}
// PathNameInfo returns file name infos for indexing.
func (m *MediaFile) PathNameInfo() (fileRoot, fileBase, relativePath, relativeName string) {
fileRoot = m.Root()
var rootPath string
switch fileRoot {
case entity.RootSidecar:
rootPath = Config().SidecarPath()
case entity.RootImport:
rootPath = Config().ImportPath()
default:
rootPath = Config().OriginalsPath()
}
fileBase = m.Base(Config().Settings().Index.Group)
relativePath = m.RelativePath(rootPath)
relativeName = m.RelativeName(rootPath)
return fileRoot, fileBase, relativePath, relativeName
}
// FileName returns the filename.
func (m *MediaFile) FileName() string {
return m.fileName
@ -319,9 +338,9 @@ func (m *MediaFile) SetFileName(fileName string) {
m.fileName = fileName
}
// RelativeName returns the relative filename.
// Rel returns the relative filename.
func (m *MediaFile) RelativeName(directory string) string {
return fs.RelativeName(m.fileName, directory)
return fs.Rel(m.fileName, directory)
}
// RelativePath returns the relative path without filename.
@ -355,7 +374,7 @@ func (m *MediaFile) RelativePath(directory string) string {
return pathname
}
// RelativeBase returns the relative filename.
// RelBase returns the relative filename.
func (m *MediaFile) RelativeBase(directory string, stripSequence bool) string {
if relativePath := m.RelativePath(directory); relativePath != "" {
return filepath.Join(relativePath, m.Base(stripSequence))
@ -364,31 +383,53 @@ func (m *MediaFile) RelativeBase(directory string, stripSequence bool) string {
return m.Base(stripSequence)
}
// Directory returns the directory
// Directory returns the file path.
func (m *MediaFile) Directory() string {
return filepath.Dir(m.fileName)
}
// SubDirectory returns a sub directory name.
func (m *MediaFile) SubDirectory(dir string) string {
return filepath.Join(filepath.Dir(m.fileName), dir)
}
// Base returns the filename base without any extensions and path.
func (m *MediaFile) Base(stripSequence bool) string {
return fs.Base(m.FileName(), stripSequence)
}
// Base returns the filename base without any extensions and path.
func (m *MediaFile) Root() string {
if strings.HasPrefix(m.FileName(), Config().OriginalsPath()) {
return entity.RootOriginals
}
importPath := Config().ImportPath()
if importPath != "" && strings.HasPrefix(m.FileName(), importPath) {
return entity.RootImport
}
sidecarPath := Config().SidecarPath()
if sidecarPath != "" && strings.HasPrefix(m.FileName(), sidecarPath) {
return entity.RootSidecar
}
examplesPath := Config().ExamplesPath()
if examplesPath != "" && strings.HasPrefix(m.FileName(), examplesPath) {
return entity.RootExamples
}
return ""
}
// AbsBase returns the directory and base filename without any extensions.
func (m *MediaFile) AbsBase(stripSequence bool) string {
return fs.AbsBase(m.FileName(), stripSequence)
}
// HiddenName returns the a filename with the same base name and a given extension in a hidden sub directory.
func (m *MediaFile) HiddenName(fileExt string, stripSequence bool) string {
return fs.SubFileName(m.FileName(), fs.HiddenPath, fileExt, stripSequence)
}
// RelatedName returns the a filename with the same base name and a given extension in the same directory.
func (m *MediaFile) RelatedName(fileExt string, stripSequence bool) string {
return m.AbsBase(stripSequence) + fileExt
}
// MimeType returns the mime type.
func (m *MediaFile) MimeType() string {
if m.mimeType != "" {
@ -597,7 +638,7 @@ func (m *MediaFile) Jpeg() (*MediaFile, error) {
return m, nil
}
jpegFilename := fs.TypeJpeg.FindSub(m.FileName(), fs.HiddenPath, false)
jpegFilename := fs.TypeJpeg.FindFirst(m.FileName(), []string{Config().SidecarPath(), fs.HiddenPath}, Config().OriginalsPath(), false)
if jpegFilename == "" {
return nil, fmt.Errorf("no jpeg found for %s", m.FileName())
@ -612,7 +653,7 @@ func (m *MediaFile) HasJpeg() bool {
return true
}
return fs.TypeJpeg.FindSub(m.FileName(), fs.HiddenPath, false) != ""
return fs.TypeJpeg.FindFirst(m.FileName(), []string{Config().SidecarPath(), fs.HiddenPath}, Config().OriginalsPath(), false) != ""
}
// HasJson returns true if this file has or is a json sidecar file.
@ -621,7 +662,7 @@ func (m *MediaFile) HasJson() bool {
return true
}
return fs.TypeJson.FindSub(m.FileName(), fs.HiddenPath, false) != ""
return fs.TypeJson.FindFirst(m.FileName(), []string{Config().SidecarPath(), fs.HiddenPath}, Config().OriginalsPath(), false) != ""
}
func (m *MediaFile) decodeDimensions() error {

View file

@ -20,7 +20,7 @@ func (m *MediaFile) MetaData() (result meta.Data) {
err = errors.New("not a photo")
}
if jsonFile := fs.TypeJson.FindSub(m.FileName(), fs.HiddenPath, false); jsonFile == "" {
if jsonFile := fs.TypeJson.FindFirst(m.FileName(), []string{Config().OriginalsPath(), Config().SidecarPath(), fs.HiddenPath}, Config().OriginalsPath(), false); jsonFile == "" {
log.Debugf("mediafile: no json sidecar file found for %s", txt.Quote(filepath.Base(m.FileName())))
} else if jsonErr := m.metaData.JSON(jsonFile, m.BaseName()); jsonErr != nil {
log.Debug(jsonErr)

View file

@ -130,17 +130,23 @@ func TestMediaFile_Exif_HEIF(t *testing.T) {
img, err := NewMediaFile(conf.ExamplesPath() + "/iphone_7.heic")
assert.Nil(t, err)
if err != nil {
t.Fatal(err)
}
info := img.MetaData()
assert.IsType(t, meta.Data{}, info)
assert.Nil(t, err)
convert := NewConvert(conf)
jpeg, err := convert.ToJpeg(img, true)
jpeg, err := convert.ToJpeg(img)
if err != nil {
t.Fatal(err)
}
t.Logf("JPEG FILENAME: %s", jpeg.FileName())
assert.Nil(t, err)

View file

@ -13,6 +13,7 @@ func TestMain(m *testing.M) {
log.SetLevel(logrus.DebugLevel)
c := config.TestConfig()
SetConfig(c)
code := m.Run()

View file

@ -3,7 +3,6 @@ package photoprism
import (
"errors"
"fmt"
"path"
"runtime"
"time"
@ -30,11 +29,6 @@ func NewPurge(conf *config.Config) *Purge {
return instance
}
// originalsPath returns the original media files path as string.
func (prg *Purge) originalsPath() string {
return prg.conf.OriginalsPath()
}
// Start removes missing files from search results.
func (prg *Purge) Start(opt PurgeOptions) (purgedFiles map[string]bool, purgedPhotos map[string]bool, err error) {
var ignore map[string]bool
@ -47,7 +41,6 @@ func (prg *Purge) Start(opt PurgeOptions) (purgedFiles map[string]bool, purgedPh
purgedFiles = make(map[string]bool)
purgedPhotos = make(map[string]bool)
originalsPath := prg.originalsPath()
if err := mutex.MainWorker.Start(); err != nil {
err = fmt.Errorf("purge: %s", err.Error())
@ -69,7 +62,7 @@ func (prg *Purge) Start(opt PurgeOptions) (purgedFiles map[string]bool, purgedPh
offset := 0
for {
files, err := query.ExistingFiles(limit, offset, opt.Path)
files, err := query.Files(limit, offset, opt.Path, true)
if err != nil {
return purgedFiles, purgedPhotos, err
@ -84,16 +77,29 @@ func (prg *Purge) Start(opt PurgeOptions) (purgedFiles map[string]bool, purgedPh
return purgedFiles, purgedPhotos, errors.New("purge canceled")
}
fileName := path.Join(prg.conf.OriginalsPath(), file.FileName)
fileName := FileName(file.FileRoot, file.FileName)
if ignore[fileName] || purgedFiles[fileName] {
continue
}
if !fs.FileExists(fileName) {
if file.FileMissing {
if fs.FileExists(fileName) {
if opt.Dry {
log.Infof("purge: found %s", txt.Quote(file.FileName))
continue
}
if err := file.Update("FileMissing", false); err != nil {
log.Errorf("purge: %s", err)
} else {
log.Infof("purge: found %s", txt.Quote(file.FileName))
}
}
} else if !fs.FileExists(fileName) {
if opt.Dry {
purgedFiles[fileName] = true
log.Infof("purge: file %s would be removed", txt.Quote(fs.RelativeName(fileName, originalsPath)))
log.Infof("purge: file %s would be removed", txt.Quote(file.FileName))
continue
}
@ -101,7 +107,7 @@ func (prg *Purge) Start(opt PurgeOptions) (purgedFiles map[string]bool, purgedPh
log.Errorf("purge: %s", err)
} else {
purgedFiles[fileName] = true
log.Infof("purge: removed file %s", txt.Quote(fs.RelativeName(fileName, originalsPath)))
log.Infof("purge: removed file %s", txt.Quote(file.FileName))
}
}
}

View file

@ -54,7 +54,7 @@ func (rs *Resample) Start(force bool) error {
}
ignore.Log = func(fileName string) {
log.Infof(`resample: ignored "%s"`, fs.RelativeName(fileName, originalsPath))
log.Infof(`resample: ignored "%s"`, fs.Rel(fileName, originalsPath))
}
err := godirwalk.Walk(originalsPath, &godirwalk.Options{

View file

@ -23,13 +23,17 @@ func FilesByPath(rootName, pathName string) (files entity.Files, err error) {
return files, err
}
// ExistingFiles returns not-missing and not-deleted file entities in the range of limit and offset sorted by id.
func ExistingFiles(limit int, offset int, pathName string) (files entity.Files, err error) {
// Files returns not-missing and not-deleted file entities in the range of limit and offset sorted by id.
func Files(limit int, offset int, pathName string, includeMissing bool) (files entity.Files, err error) {
if strings.HasPrefix(pathName, "/") {
pathName = pathName[1:]
}
stmt := Db().Unscoped().Where("file_missing = 0 AND deleted_at IS NULL")
stmt := Db()
if !includeMissing {
stmt = stmt.Where("file_missing = 0")
}
if pathName != "" {
stmt = stmt.Where("files.file_name LIKE ?", pathName+"/%")

View file

@ -9,7 +9,7 @@ import (
func TestFilesByPath(t *testing.T) {
t.Run("files found", func(t *testing.T) {
files, err := FilesByPath(entity.RootDefault, "2016/11")
files, err := FilesByPath(entity.RootOriginals, "2016/11")
t.Logf("files: %+v", files)
@ -23,7 +23,7 @@ func TestFilesByPath(t *testing.T) {
func TestExistingFiles(t *testing.T) {
t.Run("files found", func(t *testing.T) {
files, err := ExistingFiles(1000, 0, "/")
files, err := Files(1000, 0, "/", true)
t.Logf("files: %+v", files)
@ -33,7 +33,7 @@ func TestExistingFiles(t *testing.T) {
assert.LessOrEqual(t, 5, len(files))
})
t.Run("search for files path", func(t *testing.T) {
files, err := ExistingFiles(1000, 0, "Photos")
files, err := Files(1000, 0, "Photos", true)
t.Logf("files: %+v", files)

View file

@ -9,7 +9,7 @@ import (
func TestFoldersByPath(t *testing.T) {
t.Run("root", func(t *testing.T) {
folders, err := FoldersByPath(entity.RootDefault, "testdata", "", false)
folders, err := FoldersByPath(entity.RootOriginals, "testdata", "", false)
t.Logf("folders: %+v", folders)
@ -21,7 +21,7 @@ func TestFoldersByPath(t *testing.T) {
})
t.Run("subdirectory", func(t *testing.T) {
folders, err := FoldersByPath(entity.RootDefault, "testdata", "directory", false)
folders, err := FoldersByPath(entity.RootOriginals, "testdata", "directory", false)
t.Logf("folders: %+v", folders)

View file

@ -16,8 +16,8 @@ import (
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/pkg/txt"
"github.com/jinzhu/inflection"
"github.com/jinzhu/gorm"
"github.com/jinzhu/inflection"
)
var log = event.Log

View file

@ -34,6 +34,8 @@ func SetConfig(c *config.Config) {
}
conf = c
photoprism.SetConfig(c)
}
func Config() *config.Config {

View file

@ -60,7 +60,7 @@ func TestResample(t *testing.T) {
assert.Equal(t, 50, boundsNew.Max.Y)
})
t.Run("left_224 options", func(t *testing.T) {
left_224 := Types["left_224"]
left224 := Types["left_224"]
src := "testdata/example.jpg"
@ -77,7 +77,7 @@ func TestResample(t *testing.T) {
assert.Equal(t, 750, bounds.Max.X)
assert.Equal(t, 500, bounds.Max.Y)
result := Resample(img, left_224.Width, left_224.Height, left_224.Options...)
result := Resample(img, left224.Width, left224.Height, left224.Options...)
boundsNew := result.Bounds()
@ -85,7 +85,7 @@ func TestResample(t *testing.T) {
assert.Equal(t, 224, boundsNew.Max.Y)
})
t.Run("right_224 options", func(t *testing.T) {
right_224 := Types["right_224"]
right224 := Types["right_224"]
src := "testdata/example.jpg"
@ -102,7 +102,7 @@ func TestResample(t *testing.T) {
assert.Equal(t, 750, bounds.Max.X)
assert.Equal(t, 500, bounds.Max.Y)
result := Resample(img, right_224.Width, right_224.Height, right_224.Options...)
result := Resample(img, right224.Width, right224.Height, right224.Options...)
boundsNew := result.Bounds()
@ -110,7 +110,7 @@ func TestResample(t *testing.T) {
assert.Equal(t, 224, boundsNew.Max.Y)
})
t.Run("fit_1280 options", func(t *testing.T) {
fit_1280 := Types["fit_1280"]
fit1280 := Types["fit_1280"]
src := "testdata/example.jpg"
@ -127,7 +127,7 @@ func TestResample(t *testing.T) {
assert.Equal(t, 750, bounds.Max.X)
assert.Equal(t, 500, bounds.Max.Y)
result := Resample(img, fit_1280.Width, fit_1280.Height, fit_1280.Options...)
result := Resample(img, fit1280.Width, fit1280.Height, fit1280.Options...)
boundsNew := result.Bounds()

View file

@ -2,7 +2,6 @@ package workers
import (
"fmt"
"path"
"path/filepath"
"github.com/photoprism/photoprism/internal/config"
@ -10,6 +9,7 @@ import (
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/form"
"github.com/photoprism/photoprism/internal/mutex"
"github.com/photoprism/photoprism/internal/photoprism"
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/internal/remote"
"github.com/photoprism/photoprism/internal/remote/webdav"
@ -88,7 +88,7 @@ func (worker *Share) Start() (err error) {
}
}
srcFileName := path.Join(worker.conf.OriginalsPath(), file.File.FileName)
srcFileName := photoprism.FileName(file.File.FileRoot, file.File.FileName)
if a.ShareSize != "" {
thumbType, ok := thumb.Types[a.ShareSize]

View file

@ -8,6 +8,7 @@ import (
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/mutex"
"github.com/photoprism/photoprism/internal/photoprism"
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/internal/remote/webdav"
)
@ -37,7 +38,7 @@ func (worker *Sync) upload(a entity.Account) (complete bool, err error) {
return false, nil
}
fileName := path.Join(worker.conf.OriginalsPath(), file.FileName)
fileName := photoprism.FileName(file.FileRoot, file.FileName)
remoteName := path.Join(a.SyncPath, file.FileName)
remoteDir := filepath.Dir(remoteName)

View file

@ -1,8 +1,6 @@
package fs
import (
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
@ -42,9 +40,9 @@ func Base(fileName string, stripSequence bool) string {
return basename
}
// RelativeBase returns the relative filename.
func RelativeBase(fileName, dir string, stripSequence bool) string {
if name := RelativeName(fileName, dir); name != "" {
// RelBase returns the relative filename.
func RelBase(fileName, dir string, stripSequence bool) string {
if name := Rel(fileName, dir); name != "" {
return AbsBase(name, stripSequence)
}
@ -55,18 +53,3 @@ func RelativeBase(fileName, dir string, stripSequence bool) string {
func AbsBase(fileName string, stripSequence bool) string {
return filepath.Join(filepath.Dir(fileName), Base(fileName, stripSequence))
}
// SubFileName returns the a filename with the same base name and a given extension in a sub directory.
func SubFileName(fileName, subDir, fileExt string, stripSequence bool) string {
baseName := Base(fileName, stripSequence)
dirName := filepath.Join(filepath.Dir(fileName), subDir)
if err := os.MkdirAll(dirName, os.ModePerm); err != nil {
fmt.Println(err.Error())
return ""
}
result := filepath.Join(dirName, baseName) + fileExt
return result
}

View file

@ -89,30 +89,30 @@ func TestBase(t *testing.T) {
})
}
func TestRelativeBase(t *testing.T) {
func TestRelBase(t *testing.T) {
t.Run("/foo/bar.0000.ZIP", func(t *testing.T) {
regular := RelativeBase("/foo/bar.0000.ZIP", "/bar", false)
regular := RelBase("/foo/bar.0000.ZIP", "/bar", false)
assert.Equal(t, "/foo/bar.0000", regular)
stripped := RelativeBase("/foo/bar.0000.ZIP", "/bar", true)
stripped := RelBase("/foo/bar.0000.ZIP", "/bar", true)
assert.Equal(t, "/foo/bar.0000", stripped)
})
t.Run("/foo/bar.00001.ZIP", func(t *testing.T) {
regular := RelativeBase("/foo/bar.00001.ZIP", "/bar", false)
regular := RelBase("/foo/bar.00001.ZIP", "/bar", false)
assert.Equal(t, "/foo/bar.00001", regular)
stripped := RelativeBase("/foo/bar.00001.ZIP", "/bar", true)
stripped := RelBase("/foo/bar.00001.ZIP", "/bar", true)
assert.Equal(t, "/foo/bar", stripped)
})
t.Run("Test copy 3.jpg", func(t *testing.T) {
result := RelativeBase("/testdata/foo/Test copy 3.jpg", "/testdata", false)
result := RelBase("/testdata/foo/Test copy 3.jpg", "/testdata", false)
assert.Equal(t, "foo/Test copy 3", result)
})
t.Run("Test (3).jpg", func(t *testing.T) {
result := RelativeBase("/testdata/foo/Test (3).jpg", "/testdata", false)
result := RelBase("/testdata/foo/Test (3).jpg", "/testdata", false)
assert.Equal(t, "foo/Test (3)", result)
})
}
@ -129,25 +129,4 @@ func TestBaseAbs(t *testing.T) {
assert.Equal(t, "/testdata/Test (4)", result)
})
}
func TestSubFileName(t *testing.T) {
t.Run("Test copy 3.jpg", func(t *testing.T) {
result := SubFileName("/testdata/Test (4).jpg", ".photoprism", ".xmp", true)
assert.Equal(t, "/testdata/.photoprism/Test.xmp", result)
})
t.Run("Test (3).jpg", func(t *testing.T) {
result := SubFileName("/testdata/Test (4).jpg", ".photoprism", ".xmp", false)
assert.Equal(t, "/testdata/.photoprism/Test (4).xmp", result)
})
t.Run("FOO.XMP", func(t *testing.T) {
result := SubFileName("/testdata/FOO.XMP", ".photoprism", ".jpeg", true)
assert.Equal(t, "/testdata/.photoprism/FOO.jpeg", result)
})
}

View file

@ -154,48 +154,6 @@ func (t FileType) Find(fileName string, stripSequence bool) string {
return ""
}
// Find returns the first filename with the same base name and a given type (also searches a sub directory).
func (t FileType) FindSub(fileName, subDir string, stripSequence bool) string {
base := Base(fileName, stripSequence)
dir := filepath.Dir(fileName)
prefix := filepath.Join(dir, base)
prefixLower := filepath.Join(dir, strings.ToLower(base))
prefixUpper := filepath.Join(dir, strings.ToUpper(base))
prefixHidden := filepath.Join(dir, subDir, base)
prefixLowerHidden := filepath.Join(dir, subDir, strings.ToLower(base))
prefixUpperHidden := filepath.Join(dir, subDir, strings.ToUpper(base))
for _, ext := range TypeExt[t] {
if info, err := os.Stat(prefix + ext); err == nil && info.Mode().IsRegular() {
return filepath.Join(dir, info.Name())
}
if info, err := os.Stat(prefixLower + ext); err == nil && info.Mode().IsRegular() {
return filepath.Join(dir, info.Name())
}
if info, err := os.Stat(prefixUpper + ext); err == nil && info.Mode().IsRegular() {
return filepath.Join(dir, info.Name())
}
if info, err := os.Stat(prefixHidden + ext); err == nil && info.Mode().IsRegular() {
return filepath.Join(dir, subDir, info.Name())
}
if info, err := os.Stat(prefixLowerHidden + ext); err == nil && info.Mode().IsRegular() {
return filepath.Join(dir, subDir, info.Name())
}
if info, err := os.Stat(prefixUpperHidden + ext); err == nil && info.Mode().IsRegular() {
return filepath.Join(dir, subDir, info.Name())
}
}
return ""
}
// GetFileType returns the (expected) type for a given file name.
func GetFileType(fileName string) FileType {
fileExt := strings.ToLower(filepath.Ext(fileName))
@ -207,3 +165,47 @@ func GetFileType(fileName string) FileType {
return result
}
// FindFirst searches a list of directories for the first file with the same base name and a given type.
func (t FileType) FindFirst(fileName string, dirs []string, baseDir string, stripSequence bool) string {
fileBase := Base(fileName, stripSequence)
fileBaseLower := strings.ToLower(fileBase)
fileBaseUpper := strings.ToUpper(fileBase)
fileDir := filepath.Dir(fileName)
search := append([]string{fileDir}, dirs...)
for _, ext := range TypeExt[t] {
lastDir := ""
for _, dir := range search {
if dir == "" || dir == lastDir {
continue
}
lastDir = dir
if dir != fileDir {
if filepath.IsAbs(dir) {
dir = filepath.Join(dir, Rel(fileDir, baseDir))
} else {
dir = filepath.Join(fileDir, dir)
}
}
if info, err := os.Stat(filepath.Join(dir, fileBase) + ext); err == nil && info.Mode().IsRegular() {
return filepath.Join(dir, info.Name())
}
if info, err := os.Stat(filepath.Join(dir, fileBaseLower) + ext); err == nil && info.Mode().IsRegular() {
return filepath.Join(dir, info.Name())
}
if info, err := os.Stat(filepath.Join(dir, fileBaseUpper) + ext); err == nil && info.Mode().IsRegular() {
return filepath.Join(dir, info.Name())
}
}
}
return ""
}

View file

@ -43,64 +43,93 @@ func TestFileType_Find(t *testing.T) {
result := TypeJpeg.Find("testdata/test (2).xmp", true)
assert.Equal(t, "testdata/test.jpg", result)
})
t.Run("name upper", func(t *testing.T) {
result := TypeJpeg.Find("testdata/CATYELLOW.xmp", true)
assert.Equal(t, "testdata/CATYELLOW.jpg", result)
})
t.Run("name lower", func(t *testing.T) {
result := TypeJpeg.Find("testdata/chameleon_lime.xmp", true)
assert.Equal(t, "testdata/chameleon_lime.jpg", result)
})
}
func TestFileType_FindHidden(t *testing.T) {
hiddenPath := ".photoprism"
func TestFileType_FindFirst(t *testing.T) {
dirs := []string{HiddenPath}
t.Run("find xmp", func(t *testing.T) {
result := TypeXMP.FindSub("testdata/test.jpg", hiddenPath, false)
result := TypeXMP.FindFirst("testdata/test.jpg", dirs, "", false)
assert.Equal(t, "testdata/.photoprism/test.xmp", result)
})
t.Run("find xmp upper ext", func(t *testing.T) {
result := TypeXMP.FindSub("testdata/test.PNG", hiddenPath, false)
result := TypeXMP.FindFirst("testdata/test.PNG", dirs, "", false)
assert.Equal(t, "testdata/.photoprism/test.xmp", result)
})
t.Run("find xmp without sequence", func(t *testing.T) {
result := TypeXMP.FindSub("testdata/test (2).jpg", hiddenPath, false)
result := TypeXMP.FindFirst("testdata/test (2).jpg", dirs, "", false)
assert.Equal(t, "", result)
})
t.Run("find xmp with sequence", func(t *testing.T) {
result := TypeXMP.FindSub("testdata/test (2).jpg", hiddenPath, true)
result := TypeXMP.FindFirst("testdata/test (2).jpg", dirs, "", true)
assert.Equal(t, "testdata/.photoprism/test.xmp", result)
})
t.Run("find jpg", func(t *testing.T) {
result := TypeJpeg.FindSub("testdata/test.xmp", hiddenPath, false)
result := TypeJpeg.FindFirst("testdata/test.xmp", dirs, "", false)
assert.Equal(t, "testdata/test.jpg", result)
})
t.Run("find jpg abs", func(t *testing.T) {
result := TypeJpeg.FindFirst(Abs("testdata/test.xmp"), dirs, "", false)
assert.Equal(t, Abs("testdata/test.jpg"), result)
})
t.Run("upper ext", func(t *testing.T) {
result := TypeJpeg.FindSub("testdata/test.XMP", hiddenPath, false)
result := TypeJpeg.FindFirst("testdata/test.XMP", dirs, "", false)
assert.Equal(t, "testdata/test.jpg", result)
})
t.Run("with sequence", func(t *testing.T) {
result := TypeJpeg.FindSub("testdata/test (2).xmp", hiddenPath, false)
result := TypeJpeg.FindFirst("testdata/test (2).xmp", dirs, "", false)
assert.Equal(t, "", result)
})
t.Run("strip sequence", func(t *testing.T) {
result := TypeJpeg.FindSub("testdata/test (2).xmp", hiddenPath, true)
result := TypeJpeg.FindFirst("testdata/test (2).xmp", dirs, "", true)
assert.Equal(t, "testdata/test.jpg", result)
})
t.Run("name upper", func(t *testing.T) {
result := TypeJpeg.FindSub("testdata/CATYELLOW.xmp", hiddenPath, true)
result := TypeJpeg.FindFirst("testdata/CATYELLOW.xmp", dirs, "", true)
assert.Equal(t, "testdata/CATYELLOW.jpg", result)
})
t.Run("name lower", func(t *testing.T) {
result := TypeJpeg.FindSub("testdata/chameleon_lime.xmp", hiddenPath, true)
result := TypeJpeg.FindFirst("testdata/chameleon_lime.xmp", dirs, "", true)
assert.Equal(t, "testdata/chameleon_lime.jpg", result)
})
t.Run("example_bmp_notfound", func(t *testing.T) {
result := TypeBitmap.FindFirst("testdata/example.00001.jpg", dirs, "", true)
assert.Equal(t, "", result)
})
t.Run("example_bmp_found", func(t *testing.T) {
result := TypeBitmap.FindFirst("testdata/example.00001.jpg", []string{"directory"}, "", true)
assert.Equal(t, "testdata/directory/example.bmp", result)
})
t.Run("example_png_found", func(t *testing.T) {
result := TypePng.FindFirst("testdata/example.00001.jpg", []string{"directory", "directory/subdirectory"}, "", true)
assert.Equal(t, "testdata/directory/subdirectory/example.png", result)
})
t.Run("example_bmp_found", func(t *testing.T) {
result := TypeBitmap.FindFirst(Abs("testdata/example.00001.jpg"), []string{"directory"}, Abs("testdata"), true)
assert.Equal(t, Abs("testdata/directory/example.bmp"), result)
})
}

View file

@ -1,16 +1,47 @@
package fs
import (
"fmt"
"os"
"path/filepath"
"strings"
)
// RelativeName returns the file name relative to directory.
func RelativeName(fileName, dir string) string {
// FileName returns the a relative filename with the same base and a given extension in a directory.
func FileName(fileName, dirName, baseDir, fileExt string, stripSequence bool) string {
fileDir := filepath.Dir(fileName)
baseName := Base(fileName, stripSequence)
if dirName == "" || dirName == "." {
dirName = fileDir
} else if fileDir != dirName {
if filepath.IsAbs(dirName) {
dirName = filepath.Join(dirName, Rel(fileDir, baseDir))
} else {
dirName = filepath.Join(fileDir, dirName)
}
}
if err := os.MkdirAll(dirName, os.ModePerm); err != nil {
fmt.Println(err.Error())
return ""
}
result := filepath.Join(dirName, baseName) + fileExt
return result
}
// Rel returns the file name relative to a directory.
func Rel(fileName, dir string) string {
if fileName == dir {
return ""
}
if dir == "" {
return fileName
}
if index := strings.Index(fileName, dir); index == 0 {
if index := strings.LastIndex(dir, string(os.PathSeparator)); index == len(dir)-1 {
pos := len(dir)

View file

@ -1,28 +1,60 @@
package fs
import (
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
)
func TestRelativeName(t *testing.T) {
func TestRel(t *testing.T) {
t.Run("same", func(t *testing.T) {
assert.Equal(t, "", RelativeName("/some/path", "/some/path"))
assert.Equal(t, "", Rel("/some/path", "/some/path"))
})
t.Run("short", func(t *testing.T) {
assert.Equal(t, "/some/", RelativeName("/some/", "/some/path"))
assert.Equal(t, "/some/", Rel("/some/", "/some/path"))
})
t.Run("empty", func(t *testing.T) {
assert.Equal(t, "", RelativeName("", "/some/path"))
assert.Equal(t, "", Rel("", "/some/path"))
})
t.Run("/some/path", func(t *testing.T) {
assert.Equal(t, "foo/bar.baz", RelativeName("/some/path/foo/bar.baz", "/some/path"))
assert.Equal(t, "foo/bar.baz", Rel("/some/path/foo/bar.baz", "/some/path"))
})
t.Run("/some/path/", func(t *testing.T) {
assert.Equal(t, "foo/bar.baz", RelativeName("/some/path/foo/bar.baz", "/some/path/"))
assert.Equal(t, "foo/bar.baz", Rel("/some/path/foo/bar.baz", "/some/path/"))
})
t.Run("/some/path/bar", func(t *testing.T) {
assert.Equal(t, "/some/path/foo/bar.baz", RelativeName("/some/path/foo/bar.baz", "/some/path/bar"))
assert.Equal(t, "/some/path/foo/bar.baz", Rel("/some/path/foo/bar.baz", "/some/path/bar"))
})
}
func TestFileName(t *testing.T) {
t.Run("Test copy 3.jpg", func(t *testing.T) {
result := FileName("testdata/Test (4).jpg", ".photoprism", Abs("testdata"), ".xmp", true)
assert.Equal(t, "testdata/.photoprism/Test.xmp", result)
})
t.Run("Test (3).jpg", func(t *testing.T) {
result := FileName("testdata/Test (4).jpg", ".photoprism", Abs("testdata"), ".xmp", false)
assert.Equal(t, "testdata/.photoprism/Test (4).xmp", result)
})
t.Run("FOO.XMP", func(t *testing.T) {
result := FileName("testdata/FOO.XMP", ".photoprism/sub", Abs("testdata"), ".jpeg", true)
assert.Equal(t, "testdata/.photoprism/sub/FOO.jpeg", result)
})
t.Run("Test copy 3.jpg", func(t *testing.T) {
tempDir := filepath.Join(os.TempDir(), HiddenPath)
// t.Logf("TEMP DIR, ABS NAME: %s, %s", tempDir, Abs("testdata/Test (4).jpg"))
result := FileName(Abs("testdata/Test (4).jpg"), tempDir, Abs("testdata"), ".xmp", true)
assert.Equal(t, tempDir+"/Test.xmp", result)
})
}