286 lines
6.8 KiB
Go
286 lines
6.8 KiB
Go
package photoprism
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"runtime/debug"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/karrick/godirwalk"
|
|
"github.com/photoprism/photoprism/internal/config"
|
|
"github.com/photoprism/photoprism/internal/entity"
|
|
"github.com/photoprism/photoprism/internal/event"
|
|
"github.com/photoprism/photoprism/internal/mutex"
|
|
"github.com/photoprism/photoprism/pkg/fs"
|
|
"github.com/photoprism/photoprism/pkg/txt"
|
|
)
|
|
|
|
// Import represents an importer that can copy/move MediaFiles to the originals directory.
|
|
type Import struct {
|
|
conf *config.Config
|
|
index *Index
|
|
convert *Convert
|
|
}
|
|
|
|
// NewImport returns a new importer and expects its dependencies as arguments.
|
|
func NewImport(conf *config.Config, index *Index, convert *Convert) *Import {
|
|
instance := &Import{
|
|
conf: conf,
|
|
index: index,
|
|
convert: convert,
|
|
}
|
|
|
|
return instance
|
|
}
|
|
|
|
// originalsPath returns the original media files path as string.
|
|
func (imp *Import) originalsPath() string {
|
|
return imp.conf.OriginalsPath()
|
|
}
|
|
|
|
// thumbPath returns the thumbnails cache path as string.
|
|
func (imp *Import) thumbPath() string {
|
|
return imp.conf.ThumbPath()
|
|
}
|
|
|
|
// Start imports media files from a directory and converts/indexes them as needed.
|
|
func (imp *Import) Start(opt ImportOptions) fs.Done {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
log.Errorf("import: %s (panic)\nstack: %s", r, debug.Stack())
|
|
}
|
|
}()
|
|
|
|
var directories []string
|
|
done := make(fs.Done)
|
|
ind := imp.index
|
|
importPath := opt.Path
|
|
|
|
if !fs.PathExists(importPath) {
|
|
event.Error(fmt.Sprintf("import: %s does not exist", importPath))
|
|
return done
|
|
}
|
|
|
|
if err := mutex.MainWorker.Start(); err != nil {
|
|
event.Error(fmt.Sprintf("import: %s", err.Error()))
|
|
return done
|
|
}
|
|
|
|
defer mutex.MainWorker.Stop()
|
|
|
|
if err := ind.tensorFlow.Init(); err != nil {
|
|
log.Errorf("import: %s", err.Error())
|
|
return done
|
|
}
|
|
|
|
jobs := make(chan ImportJob)
|
|
|
|
// Start a fixed number of goroutines to import files.
|
|
var wg sync.WaitGroup
|
|
var numWorkers = ind.conf.Workers()
|
|
wg.Add(numWorkers)
|
|
for i := 0; i < numWorkers; i++ {
|
|
go func() {
|
|
ImportWorker(jobs)
|
|
wg.Done()
|
|
}()
|
|
}
|
|
|
|
filesImported := 0
|
|
indexOpt := IndexOptionsAll()
|
|
ignore := fs.NewIgnoreList(fs.IgnoreFile, true, false)
|
|
|
|
if err := ignore.Dir(importPath); err != nil {
|
|
log.Infof("import: %s", err)
|
|
}
|
|
|
|
ignore.Log = func(fileName string) {
|
|
log.Infof(`import: ignored "%s"`, fs.RelName(fileName, importPath))
|
|
}
|
|
|
|
err := godirwalk.Walk(importPath, &godirwalk.Options{
|
|
ErrorCallback: func(fileName string, err error) godirwalk.ErrorAction {
|
|
log.Errorf("import: %s", strings.Replace(err.Error(), importPath, "", 1))
|
|
return godirwalk.SkipNode
|
|
},
|
|
Callback: func(fileName string, info *godirwalk.Dirent) error {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
log.Errorf("import: %s (panic)\nstack: %s", r, debug.Stack())
|
|
}
|
|
}()
|
|
|
|
if mutex.MainWorker.Canceled() {
|
|
return errors.New("import canceled")
|
|
}
|
|
|
|
isDir := info.IsDir()
|
|
isSymlink := info.IsSymlink()
|
|
|
|
if isDir {
|
|
if fileName != importPath {
|
|
directories = append(directories, fileName)
|
|
}
|
|
}
|
|
|
|
if skip, result := fs.SkipWalk(fileName, isDir, isSymlink, done, ignore); skip {
|
|
if isDir && result != filepath.SkipDir {
|
|
folder := entity.NewFolder(entity.RootImport, fs.RelName(fileName, imp.conf.ImportPath()), fs.BirthTime(fileName))
|
|
|
|
if err := folder.Create(); err == nil {
|
|
log.Infof("import: added folder /%s", folder.Path)
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
done[fileName] = fs.Found
|
|
|
|
if !fs.IsMedia(fileName) {
|
|
return nil
|
|
}
|
|
|
|
mf, err := NewMediaFile(fileName)
|
|
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
|
|
if mf.FileSize() == 0 {
|
|
log.Infof("import: skipped empty file %s", txt.Quote(mf.BaseName()))
|
|
return nil
|
|
}
|
|
|
|
related, err := mf.RelatedFiles(imp.conf.Settings().StackSequences())
|
|
|
|
if err != nil {
|
|
event.Error(fmt.Sprintf("import: %s", err.Error()))
|
|
|
|
return nil
|
|
}
|
|
|
|
var files MediaFiles
|
|
|
|
for _, f := range related.Files {
|
|
if f.FileSize() == 0 || done[f.FileName()].Processed() {
|
|
continue
|
|
}
|
|
|
|
files = append(files, f)
|
|
filesImported++
|
|
done[f.FileName()] = fs.Processed
|
|
}
|
|
|
|
done[fileName] = fs.Processed
|
|
|
|
related.Files = files
|
|
|
|
jobs <- ImportJob{
|
|
FileName: fileName,
|
|
Related: related,
|
|
IndexOpt: indexOpt,
|
|
ImportOpt: opt,
|
|
Imp: imp,
|
|
}
|
|
|
|
return nil
|
|
},
|
|
Unsorted: false,
|
|
FollowSymbolicLinks: true,
|
|
})
|
|
|
|
close(jobs)
|
|
wg.Wait()
|
|
|
|
sort.Slice(directories, func(i, j int) bool {
|
|
return len(directories[i]) > len(directories[j])
|
|
})
|
|
|
|
if opt.RemoveEmptyDirectories {
|
|
// Remove empty directories from import path
|
|
for _, directory := range directories {
|
|
if fs.IsEmpty(directory) {
|
|
if err := os.Remove(directory); err != nil {
|
|
log.Errorf("import: failed deleting empty folder %s (%s)", txt.Quote(fs.RelName(directory, importPath)), err)
|
|
} else {
|
|
log.Infof("import: deleted empty folder %s", txt.Quote(fs.RelName(directory, importPath)))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if opt.RemoveDotFiles {
|
|
// Remove hidden .files if option is enabled
|
|
for _, file := range ignore.Hidden() {
|
|
if !fs.FileExists(file) {
|
|
continue
|
|
}
|
|
|
|
if err := os.Remove(file); err != nil {
|
|
log.Errorf("import: failed removing %s (%s)", txt.Quote(fs.RelName(file, importPath)), err.Error())
|
|
}
|
|
}
|
|
}
|
|
|
|
if err != nil {
|
|
log.Error(err.Error())
|
|
}
|
|
|
|
if filesImported > 0 {
|
|
if err := entity.UpdatePhotoCounts(); err != nil {
|
|
log.Errorf("import: %s", err)
|
|
}
|
|
}
|
|
|
|
runtime.GC()
|
|
|
|
return done
|
|
}
|
|
|
|
// Cancel stops the current import operation.
|
|
func (imp *Import) Cancel() {
|
|
mutex.MainWorker.Cancel()
|
|
}
|
|
|
|
// DestinationFilename returns the destination filename of a MediaFile to be imported.
|
|
func (imp *Import) DestinationFilename(mainFile *MediaFile, mediaFile *MediaFile) (string, error) {
|
|
fileName := mainFile.CanonicalName()
|
|
fileExtension := mediaFile.Extension()
|
|
dateCreated := mainFile.DateCreated()
|
|
|
|
if !mediaFile.IsSidecar() {
|
|
if f, err := entity.FirstFileByHash(mediaFile.Hash()); err == nil {
|
|
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 {
|
|
return existingFilename, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
// Mon Jan 2 15:04:05 -0700 MST 2006
|
|
pathName := filepath.Join(imp.originalsPath(), dateCreated.Format("2006/01"))
|
|
|
|
iteration := 0
|
|
|
|
result := filepath.Join(pathName, fileName+fileExtension)
|
|
|
|
for fs.FileExists(result) {
|
|
if mediaFile.Hash() == fs.Hash(result) {
|
|
return result, fmt.Errorf("%s already exists", txt.Quote(fs.RelName(result, imp.originalsPath())))
|
|
}
|
|
|
|
iteration++
|
|
|
|
result = filepath.Join(pathName, fileName+"."+fmt.Sprintf("%05d", iteration)+fileExtension)
|
|
}
|
|
|
|
return result, nil
|
|
}
|