Backups: Rename album backups to exports and improve command help #1887

This commit is contained in:
Michael Mayer 2022-01-05 11:40:44 +01:00
parent 1a4158c7bc
commit 58a5f94069
40 changed files with 90 additions and 89 deletions

View File

@ -48,7 +48,7 @@ func DeleteFile(router *gin.RouterGroup) {
}
if file.FilePrimary {
log.Errorf("photo: can't delete primary file")
log.Errorf("photo: cannot delete primary file")
AbortDeleteFailed(c)
return
}

View File

@ -42,11 +42,11 @@ func PhotoUnstack(router *gin.RouterGroup) {
}
if file.FilePrimary {
log.Errorf("photo: can't unstack primary file")
log.Errorf("photo: cannot unstack primary file")
AbortBadRequest(c)
return
} else if file.FileSidecar {
log.Errorf("photo: can't unstack sidecar files")
log.Errorf("photo: cannot unstack sidecar files")
AbortBadRequest(c)
return
} else if file.FileRoot != entity.RootOriginals {
@ -70,7 +70,7 @@ func PhotoUnstack(router *gin.RouterGroup) {
stackPrimary, err := stackPhoto.PrimaryFile()
if err != nil {
log.Errorf("photo: can't find primary file for %s (unstack)", sanitize.Log(baseName))
log.Errorf("photo: cannot find primary file for %s (unstack)", sanitize.Log(baseName))
AbortUnexpected(c)
return
}
@ -96,7 +96,7 @@ func PhotoUnstack(router *gin.RouterGroup) {
if unstackFile.BasePrefix(false) == stackPhoto.PhotoName {
if conf.ReadOnly() {
log.Errorf("photo: can't rename files in read only mode (unstack %s)", sanitize.Log(baseName))
log.Errorf("photo: cannot rename files in read only mode (unstack %s)", sanitize.Log(baseName))
AbortFeatureDisabled(c)
return
}
@ -104,7 +104,7 @@ func PhotoUnstack(router *gin.RouterGroup) {
destName := fmt.Sprintf("%s.%s%s", unstackFile.AbsPrefix(false), unstackFile.Checksum(), unstackFile.Extension())
if err := unstackFile.Move(destName); err != nil {
log.Errorf("photo: can't rename %s to %s (unstack)", sanitize.Log(unstackFile.BaseName()), sanitize.Log(filepath.Base(destName)))
log.Errorf("photo: cannot rename %s to %s (unstack)", sanitize.Log(unstackFile.BaseName()), sanitize.Log(filepath.Base(destName)))
AbortUnexpected(c)
return
}

View File

@ -21,7 +21,7 @@ func TestPhotoUnstack(t *testing.T) {
app, router, _ := NewApiTest()
PhotoUnstack(router)
r := PerformRequest(app, "POST", "/api/v1/photos/pt9jtdre2lvl0yh7/files/ft2es49whhbnlqdn/unstack")
// TODO: Have a real file in place for testing the success case. This file does not exist, so it can't be unstacked.
// TODO: Have a real file in place for testing the success case. This file does not exist, so it cannot be unstacked.
assert.Equal(t, http.StatusNotFound, r.Code)
// t.Logf("RESP: %s", r.Body.String())
})

View File

@ -20,35 +20,41 @@ import (
"github.com/photoprism/photoprism/pkg/sanitize"
)
const backupDescription = "A user-defined SQL dump FILENAME or - for stdout can be passed as the first argument. " +
"The -i parameter can be omitted in this case.\n" +
" Make sure to run the command with exec -T when using Docker to prevent log messages from being sent to stdout.\n" +
" The index backup and album exports paths are automatically detected if not specified explicitly."
// BackupCommand configures the backup cli command.
var BackupCommand = cli.Command{
Name: "backup",
Usage: "Creates index database dumps and optional YAML album backups",
UsageText: `A custom database SQL dump FILENAME may be passed as first argument. Use - for stdout. The backup paths will be detected automatically if not provided.`,
Flags: backupFlags,
Action: backupAction,
Name: "backup",
Description: backupDescription,
Usage: "Creates an index SQL dump and optionally album YAML exports organized by type",
ArgsUsage: "[FILENAME | -]",
Flags: backupFlags,
Action: backupAction,
}
var backupFlags = []cli.Flag{
cli.BoolFlag{
Name: "force, f",
Usage: "replace existing backup files",
Usage: "replace existing files",
},
cli.BoolFlag{
Name: "albums, a",
Usage: "create YAML album backups",
Usage: "create album YAML exports organized by type",
},
cli.StringFlag{
Name: "albums-path",
Usage: "custom albums backup `PATH`",
Usage: "custom album exports `PATH`",
},
cli.BoolFlag{
Name: "index, i",
Usage: "create index database SQL dump",
Usage: "create index SQL dump",
},
cli.StringFlag{
Name: "index-path",
Usage: "custom database backup `PATH`",
Usage: "custom index backup `PATH`",
},
}
@ -64,13 +70,7 @@ func backupAction(ctx *cli.Context) error {
backupAlbums := ctx.Bool("albums") || albumsPath != ""
if !backupIndex && !backupAlbums {
fmt.Printf("OPTIONS:\n")
for _, flag := range backupFlags {
fmt.Printf(" %s\n", flag.String())
}
return nil
return cli.ShowSubcommandHelp(ctx)
}
start := time.Now()
@ -101,9 +101,9 @@ func backupAction(ctx *cli.Context) error {
if indexFileName != "-" {
if _, err := os.Stat(indexFileName); err == nil && !ctx.Bool("force") {
return fmt.Errorf("backup file already exists: %s", indexFileName)
return fmt.Errorf("SQL dump already exists: %s", indexFileName)
} else if err == nil {
log.Warnf("replacing existing backup file")
log.Warnf("replacing existing SQL dump")
}
// Create backup directory if not exists.
@ -113,7 +113,7 @@ func backupAction(ctx *cli.Context) error {
}
}
log.Infof("backing up database to %s", sanitize.Log(indexFileName))
log.Infof("writing SQL dump to %s", sanitize.Log(indexFileName))
}
var cmd *exec.Cmd
@ -169,18 +169,18 @@ func backupAction(ctx *cli.Context) error {
if !fs.PathWritable(albumsPath) {
if albumsPath != "" {
log.Warnf("albums backup path not writable, using default")
log.Warnf("album exports path not writable, using default")
}
albumsPath = conf.AlbumsPath()
}
log.Infof("backing up albums to %s", sanitize.Log(albumsPath))
log.Infof("exporting albums to %s", sanitize.Log(albumsPath))
if count, err := photoprism.BackupAlbums(albumsPath, true); err != nil {
return err
} else {
log.Infof("created %s", english.Plural(count, "YAML album backup", "YAML album backups"))
log.Infof("created %s", english.Plural(count, "YAML album export", "YAML album exports"))
}
}

View File

@ -17,7 +17,7 @@ import (
var ConvertCommand = cli.Command{
Name: "convert",
Usage: "Converts files in other formats to JPEG and AVC",
ArgsUsage: "[originals subfolder]",
ArgsUsage: "[ORIGINALS SUB-FOLDER]",
Action: convertAction,
}

View File

@ -19,7 +19,7 @@ var CopyCommand = cli.Command{
Name: "cp",
Aliases: []string{"copy"},
Usage: "Copies media files to originals",
ArgsUsage: "[path]",
ArgsUsage: "[PATH]",
Action: copyAction,
}

View File

@ -53,7 +53,7 @@ var FacesCommand = cli.Command{
{
Name: "index",
Usage: "Searches originals for faces",
ArgsUsage: "[originals subfolder]",
ArgsUsage: "[ORIGINALS SUB-FOLDER]",
Action: facesIndexAction,
},
{

View File

@ -19,7 +19,7 @@ var ImportCommand = cli.Command{
Name: "mv",
Aliases: []string{"import"},
Usage: "Moves media files to originals",
ArgsUsage: "[path]",
ArgsUsage: "[PATH]",
Action: importAction,
}

View File

@ -21,7 +21,7 @@ import (
var IndexCommand = cli.Command{
Name: "index",
Usage: "Indexes original media files",
ArgsUsage: "[originals subfolder]",
ArgsUsage: "[ORIGINALS SUB-FOLDER]",
Flags: indexFlags,
Action: indexAction,
}

View File

@ -22,13 +22,18 @@ import (
"github.com/photoprism/photoprism/pkg/sanitize"
)
const restoreDescription = "A user-defined SQL dump FILENAME can be passed as the first argument. " +
"The -i parameter can be omitted in this case.\n" +
" The index backup and album exports paths are automatically detected if not specified explicitly."
// RestoreCommand configures the backup cli command.
var RestoreCommand = cli.Command{
Name: "restore",
Usage: "Restores the index from database dumps and YAML album backups",
UsageText: `A custom database SQL dump FILENAME may be passed as first argument. The backup paths will be detected automatically if not provided.`,
Flags: restoreFlags,
Action: restoreAction,
Name: "restore",
Description: restoreDescription,
Usage: "Restores the index from an SQL dump and optionally albums from YAML exports",
ArgsUsage: "[FILENAME]",
Flags: restoreFlags,
Action: restoreAction,
}
var restoreFlags = []cli.Flag{
@ -38,19 +43,19 @@ var restoreFlags = []cli.Flag{
},
cli.BoolFlag{
Name: "albums, a",
Usage: "restore albums from YAML backups",
Usage: "restore albums from YAML exports",
},
cli.StringFlag{
Name: "albums-path",
Usage: "custom albums backup `PATH`",
Usage: "custom album exports `PATH`",
},
cli.BoolFlag{
Name: "index, i",
Usage: "restore index from database SQL dump",
Usage: "restore index from SQL dump",
},
cli.StringFlag{
Name: "index-path",
Usage: "custom database backup `PATH`",
Usage: "custom index backup `PATH`",
},
}
@ -65,11 +70,7 @@ func restoreAction(ctx *cli.Context) error {
restoreAlbums := ctx.Bool("albums") || albumsPath != ""
if !restoreIndex && !restoreAlbums {
for _, flag := range restoreFlags {
fmt.Println(flag.String())
}
return nil
return cli.ShowSubcommandHelp(ctx)
}
start := time.Now()
@ -97,7 +98,7 @@ func restoreAction(ctx *cli.Context) error {
}
if len(matches) == 0 {
log.Errorf("no backup files found in %s", indexPath)
log.Errorf("no SQL dumps found in %s", indexPath)
return nil
}
@ -105,7 +106,7 @@ func restoreAction(ctx *cli.Context) error {
}
if !fs.FileExists(indexFileName) {
log.Errorf("backup file not found: %s", indexFileName)
log.Errorf("SQL dump not found: %s", indexFileName)
return nil
}
@ -199,21 +200,21 @@ func restoreAction(ctx *cli.Context) error {
}
if !fs.PathExists(albumsPath) {
log.Warnf("albums backup path %s not found", sanitize.Log(albumsPath))
log.Warnf("album exports path %s not found", sanitize.Log(albumsPath))
} else {
log.Infof("restoring albums from %s", sanitize.Log(albumsPath))
if count, err := photoprism.RestoreAlbums(albumsPath, true); err != nil {
return err
} else {
log.Infof("restored %s from YAML backups", english.Plural(count, "album", "albums"))
log.Infof("restored %s from YAML exports", english.Plural(count, "album", "albums"))
}
}
}
elapsed := time.Since(start)
log.Infof("backup restored in %s", elapsed)
log.Infof("restored in %s", elapsed)
conf.Shutdown()

View File

@ -35,7 +35,7 @@ func statusAction(ctx *cli.Context) error {
var status string
if resp, err := client.Do(req); err != nil {
return fmt.Errorf("can't connect to %s:%d", conf.HttpHost(), conf.HttpPort())
return fmt.Errorf("cannot connect to %s:%d", conf.HttpHost(), conf.HttpPort())
} else if resp.StatusCode != 200 {
return fmt.Errorf("server running at %s:%d, bad status %d\n", conf.HttpHost(), conf.HttpPort(), resp.StatusCode)
} else if body, err := io.ReadAll(resp.Body); err != nil {

View File

@ -73,7 +73,7 @@ var UsersCommand = cli.Command{
Name: "delete",
Usage: "Removes an existing user",
Action: usersDeleteAction,
ArgsUsage: "[username]",
ArgsUsage: "[USERNAME]",
},
},
}

View File

@ -69,7 +69,7 @@ type ClientConfig struct {
// Years represents a list of years.
type Years []int
// ClientDisable represents disabled client features a user can't turn back on.
// ClientDisable represents disabled client features a user cannot turn back on.
type ClientDisable struct {
Backups bool `json:"backups"`
WebDAV bool `json:"webdav"`

View File

@ -35,7 +35,7 @@ func (c *Config) CreateDirectories() error {
if fs.FileExists(path) {
result = fmt.Errorf("%s is a file, not a folder: please check your configuration", sanitize.Log(path))
} else {
result = fmt.Errorf("can't create %s: please check configuration and permissions", sanitize.Log(path))
result = fmt.Errorf("cannot create %s: please check configuration and permissions", sanitize.Log(path))
}
log.Debug(err)

View File

@ -253,7 +253,7 @@ func (c *Options) SetContext(ctx *cli.Context) error {
fieldValue.SetBool(f)
}
default:
log.Warnf("can't assign value of type %s from cli flag %s", t, tagValue)
log.Warnf("cannot assign value of type %s from cli flag %s", t, tagValue)
}
}
}

View File

@ -317,7 +317,7 @@ func (m *File) AllFilesMissing() bool {
// Create inserts a new row to the database.
func (m *File) Create() error {
if m.PhotoID == 0 {
return fmt.Errorf("file: can't create file with empty photo id")
return fmt.Errorf("file: cannot create file with empty photo id")
}
if err := UnscopedDb().Create(m).Error; err != nil {
@ -345,7 +345,7 @@ func (m *File) ResolvePrimary() error {
// Save stores the file in the database.
func (m *File) Save() error {
if m.PhotoID == 0 {
return fmt.Errorf("file %s: can't save file with empty photo id", m.FileUID)
return fmt.Errorf("file %s: cannot save file with empty photo id", m.FileUID)
}
if err := UnscopedDb().Save(m).Error; err != nil {

View File

@ -189,7 +189,7 @@ func TestFile_Save(t *testing.T) {
t.Fatalf("file id should be 0: %d", file.ID)
}
assert.Equal(t, "file 123: can't save file with empty photo id", err.Error())
assert.Equal(t, "file 123: cannot save file with empty photo id", err.Error())
})
t.Run("success", func(t *testing.T) {
photo := &Photo{TakenAtLocal: time.Date(2019, 01, 15, 0, 0, 0, 0, time.UTC), PhotoTitle: "Berlin / Morning Mood"}

View File

@ -486,7 +486,7 @@ func (m *Marker) ClearSubject(src string) error {
// Face returns a matching face entity if possible.
func (m *Marker) Face() (f *Face) {
if m.MarkerUID == "" {
log.Debugf("markers: can't find face when uid is empty")
log.Debugf("markers: cannot find face when uid is empty")
return nil
}

View File

@ -17,7 +17,7 @@ type Password struct {
// NewPassword creates a new password instance.
func NewPassword(uid, password string) Password {
if uid == "" {
panic("auth: can't set password without uid")
panic("auth: cannot set password without uid")
}
m := Password{UID: uid}

View File

@ -146,7 +146,7 @@ func SavePhotoForm(model Photo, form form.Photo) error {
}
if !model.HasID() {
return errors.New("can't save form when photo id is missing")
return errors.New("cannot save form when photo id is missing")
}
// Update time fields.
@ -300,7 +300,7 @@ func (m *Photo) Find() error {
// SaveLabels updates the photo after labels have changed.
func (m *Photo) SaveLabels() error {
if !m.HasID() {
return errors.New("photo: can't save to database, id is empty")
return errors.New("photo: cannot save to database, id is empty")
}
labels := m.ClassifyLabels()

View File

@ -11,7 +11,7 @@ import (
// Optimize photo data, improve if possible.
func (m *Photo) Optimize(mergeMeta, mergeUuid, estimatePlace, force bool) (updated bool, merged Photos, err error) {
if !m.HasID() {
return false, merged, errors.New("photo: can't maintain, id is empty")
return false, merged, errors.New("photo: cannot maintain, id is empty")
}
current := *m

View File

@ -109,7 +109,7 @@ func TestPhoto_SaveLabels(t *testing.T) {
err := photo.SaveLabels()
assert.EqualError(t, err, "photo: can't save to database, id is empty")
assert.EqualError(t, err, "photo: cannot save to database, id is empty")
})
t.Run("existing photo", func(t *testing.T) {

View File

@ -171,7 +171,7 @@ func (m *Photo) UpdateTitle(labels classify.Labels) error {
// UpdateAndSaveTitle updates the photo title and saves it.
func (m *Photo) UpdateAndSaveTitle() error {
if !m.HasID() {
return fmt.Errorf("can't save photo whithout id")
return fmt.Errorf("cannot save photo whithout id")
}
m.PhotoFaces = m.FaceCount()

View File

@ -201,7 +201,7 @@ func FindUserByUID(uid string) *User {
// Delete marks the entity as deleted.
func (m *User) Delete() error {
if m.ID <= 1 {
return fmt.Errorf("can't delete system user")
return fmt.Errorf("cannot delete system user")
}
return Db().Delete(m).Error

View File

@ -335,7 +335,7 @@ func TestUser_InitPassword(t *testing.T) {
p := User{UserUID: "u000000000000010", UserName: "Hans", FullName: ""}
if err := p.Save(); err != nil {
t.Logf("can't user %s: ", err)
t.Logf("cannot user %s: ", err)
}
if err := p.SetPassword("hutfdt"); err != nil {

View File

@ -75,7 +75,7 @@ func (data Data) AspectRatio() float32 {
return aspectRatio
}
// Portrait returns true if it's a portrait picture or video based on width and height.
// Portrait returns true if it is a portrait picture or video based on width and height.
func (data Data) Portrait() bool {
return data.ActualWidth() < data.ActualHeight()
}

View File

@ -39,7 +39,7 @@ func (data *Data) JSON(jsonName, originalName string) (err error) {
jsonData, err := os.ReadFile(jsonName)
if err != nil {
return fmt.Errorf("can't read json file %s", quotedName)
return fmt.Errorf("cannot read json file %s", quotedName)
}
if bytes.Contains(jsonData, []byte("ExifToolVersion")) {

View File

@ -130,7 +130,7 @@ func (data *Data) Exiftool(jsonData []byte, originalName string) (err error) {
fieldValue.SetBool(jsonValue.Bool())
default:
log.Warnf("metadata: can't assign value of type %s to %s (exiftool)", t, tagValue)
log.Warnf("metadata: cannot assign value of type %s to %s (exiftool)", t, tagValue)
}
}
}

View File

@ -26,7 +26,7 @@ func (data *Data) XMP(fileName string) (err error) {
doc := XmpDocument{}
if err := doc.Load(fileName); err != nil {
return fmt.Errorf("metadata: can't read %s (xmp)", sanitize.Log(filepath.Base(fileName)))
return fmt.Errorf("metadata: cannot read %s (xmp)", sanitize.Log(filepath.Base(fileName)))
}
if doc.Title() != "" {

View File

@ -203,7 +203,7 @@ func TestThumb_Create(t *testing.T) {
img, err := imaging.Open(conf.ExamplesPath()+"/elephants.jpg", imaging.AutoOrientation(true))
if err != nil {
t.Errorf("can't open original: %s", err)
t.Errorf("cannot open original: %s", err)
}
res, err := thumb.Create(img, expectedFilename, 150, 150, thumb.ResampleFit, thumb.ResampleNearestNeighbor)
@ -230,7 +230,7 @@ func TestThumb_Create(t *testing.T) {
img, err := imaging.Open(conf.ExamplesPath()+"/elephants.jpg", imaging.AutoOrientation(true))
if err != nil {
t.Errorf("can't open original: %s", err)
t.Errorf("cannot open original: %s", err)
}
res, err := thumb.Create(img, expectedFilename, -1, 150, thumb.ResampleFit, thumb.ResampleNearestNeighbor)
@ -256,7 +256,7 @@ func TestThumb_Create(t *testing.T) {
img, err := imaging.Open(conf.ExamplesPath()+"/elephants.jpg", imaging.AutoOrientation(true))
if err != nil {
t.Errorf("can't open original: %s", err)
t.Errorf("cannot open original: %s", err)
}
res, err := thumb.Create(img, expectedFilename, 150, -1, thumb.ResampleFit, thumb.ResampleNearestNeighbor)

View File

@ -137,7 +137,7 @@ func MergeFaces(merge entity.Faces) (merged *entity.Face, err error) {
for i := 1; i < len(merge); i++ {
if merge[i].SubjUID != subjUID {
return merged, fmt.Errorf("faces: can't merge clusters with conflicting subjects %s <> %s",
return merged, fmt.Errorf("faces: cannot merge clusters with conflicting subjects %s <> %s",
sanitize.Log(subjUID), sanitize.Log(merge[i].SubjUID))
}
}

View File

@ -202,7 +202,7 @@ func TestMergeFaces(t *testing.T) {
result, err := MergeFaces(faces)
assert.EqualError(t, err, "faces: can't merge clusters with conflicting subjects jqynvsf28rhn6b0c <> jqynvt925h8c1asv")
assert.EqualError(t, err, "faces: cannot merge clusters with conflicting subjects jqynvsf28rhn6b0c <> jqynvt925h8c1asv")
assert.Nil(t, result)
})
t.Run("OneSubject", func(t *testing.T) {

View File

@ -10,7 +10,7 @@ import (
// SetDownloadFileID updates the local file id for remote downloads.
func SetDownloadFileID(filename string, fileId uint) error {
if len(filename) == 0 {
return errors.New("sync: can't update, filename empty")
return errors.New("sync: cannot update, filename empty")
}
// TODO: Might break on Windows

View File

@ -18,6 +18,6 @@ func TestSetDownloadFileID(t *testing.T) {
if err == nil {
t.Fatal()
}
assert.Equal(t, "sync: can't update, filename empty", err.Error())
assert.Equal(t, "sync: cannot update, filename empty", err.Error())
})
}

View File

@ -95,7 +95,7 @@ func FileByHash(fileHash string) (file entity.File, err error) {
// RenameFile renames an indexed file.
func RenameFile(srcRoot, srcName, destRoot, destName string) error {
if srcRoot == "" || srcName == "" || destRoot == "" || destName == "" {
return fmt.Errorf("can't rename %s/%s to %s/%s", srcRoot, srcName, destRoot, destName)
return fmt.Errorf("cannot rename %s/%s to %s/%s", srcRoot, srcName, destRoot, destName)
}
return Db().Exec("UPDATE files SET file_root = ?, file_name = ?, file_missing = 0, deleted_at = NULL WHERE file_root = ? AND file_name = ?", destRoot, destName, srcRoot, srcName).Error
@ -114,7 +114,7 @@ func SetPhotoPrimary(photoUID, fileUID string) error {
} else if err := Db().Model(entity.File{}).Where("photo_uid = ? AND file_missing = 0 AND file_type = 'jpg'", photoUID).Order("file_width DESC").Limit(1).Pluck("file_uid", &files).Error; err != nil {
return err
} else if len(files) == 0 {
return fmt.Errorf("can't find primary file for %s", photoUID)
return fmt.Errorf("cannot find primary file for %s", photoUID)
} else {
fileUID = files[0]
}

View File

@ -200,7 +200,7 @@ func TestSetPhotoPrimary(t *testing.T) {
if err == nil {
t.Fatal("error expected")
}
assert.Contains(t, err.Error(), "can't find primary file")
assert.Contains(t, err.Error(), "cannot find primary file")
})
}

View File

@ -166,7 +166,7 @@ func (c Client) Download(from, to string, force bool) (err error) {
if err != nil {
// Create directory
if err := os.MkdirAll(dir, os.ModePerm); err != nil {
return fmt.Errorf("webdav: can't create %s (%s)", dir, err)
return fmt.Errorf("webdav: cannot create %s (%s)", dir, err)
}
} else if !dirInfo.IsDir() {
return fmt.Errorf("webdav: %s is not a folder", dir)

View File

@ -45,7 +45,7 @@ func stack(skip int) []byte {
if !ok {
break
}
// Print this much at least. If we can't find the source, it won't show.
// Print this much at least. If we cannot find the source, it won't show.
fmt.Fprintf(buf, "%s:%d (0x%x)\n", file, line, pc)
if file != lastFile {
data, err := os.ReadFile(file)

View File

@ -10,7 +10,7 @@ import (
)
func registerRoutes(router *gin.Engine, conf *config.Config) {
// Enables automatic redirection if the current route can't be matched but a
// Enables automatic redirection if the current route cannot be matched but a
// handler for the path with (without) the trailing slash exists.
router.RedirectTrailingSlash = true

View File

@ -12,7 +12,7 @@ func Jpeg(srcFilename, jpgFilename string, orientation int) (img image.Image, er
img, err = imaging.Open(srcFilename)
if err != nil {
log.Errorf("resample: can't open %s", sanitize.Log(filepath.Base(srcFilename)))
log.Errorf("resample: cannot open %s", sanitize.Log(filepath.Base(srcFilename)))
return img, err
}