diff --git a/internal/app/datastore.go b/internal/app/datastore.go index 45eaff6..bfc09b5 100644 --- a/internal/app/datastore.go +++ b/internal/app/datastore.go @@ -3,11 +3,14 @@ package app import ( "database/sql" "errors" + "fmt" _ "github.com/mattn/go-sqlite3" - "log" + "github.com/sirupsen/logrus" "time" ) +var ErrorRecordNotFound = errors.New("Record not found") + type ImageMetaData struct { ImageId int PiwigoId int @@ -19,11 +22,12 @@ type ImageMetaData struct { CategoryId int } -type ImageMetadataLoader interface { - GetImageMetadata(relativePath string) (ImageMetaData, error) +func (img *ImageMetaData) String() string { + return fmt.Sprintf("ImageMetaData{ImageId:%d, PiwigoId:%d, CategoryId:%d, RelPath:%s, File:%s, Md5:%s, Change:%sS, catpath:%s}", img.ImageId, img.PiwigoId, img.CategoryId, img.RelativeImagePath, img.Filename, img.Md5Sum, img.LastChange.String(), img.CategoryPath) } -type ImageMetadataSaver interface { +type ImageMetadataProvider interface { + GetImageMetadata(relativePath string) (ImageMetaData, error) SaveImageMetadata(m ImageMetaData) error } @@ -50,68 +54,84 @@ func (d *localDataStore) Initialize(connectionString string) error { } func (d *localDataStore) GetImageMetadata(relativePath string) (ImageMetaData, error) { + logrus.Debugf("Query image metadata for file %s", relativePath) + img := ImageMetaData{} + db, err := d.openDatabase() if err != nil { - return ImageMetaData{}, err + return img, err } defer db.Close() - tx, err := db.Begin() + stmt, err := db.Prepare("SELECT imageId, piwigoId, relativePath, fileName, md5sum, lastChanged, categoryPath, categoryId FROM image WHERE relativePath = ?") if err != nil { - return ImageMetaData{}, err + return img, err } - //TODO: select entry by path - //stmt, err := tx.Prepare("select * from image WHERE relativePath = '?'") - //if err != nil { - // log.Fatal(err) - //} - - err = tx.Commit() + rows, err := stmt.Query(relativePath) if err != nil { - log.Fatal(err) + return img, err } + defer rows.Close() - return ImageMetaData{}, nil -} - -func (d *localDataStore) SaveImageMetadata(m ImageMetaData) error { - db, err := d.openDatabase() - if err != nil { - return err - } - defer db.Close() - - tx, err := db.Begin() - if err != nil { - return err - } - - if m.ImageId <= 0 { - err = d.insertImageMetaData(tx, m) + if rows.Next() { + err = rows.Scan(&img.ImageId, &img.PiwigoId, &img.RelativeImagePath, &img.Filename, &img.Md5Sum, &img.LastChange, &img.CategoryPath, &img.CategoryId) if err != nil { - return err + return img, err } } else { - // TODO: update existing entry + return img, ErrorRecordNotFound } + err = rows.Err() - err = tx.Commit() - return err + return img, err } -func (d *localDataStore) insertImageMetaData(tx *sql.Tx, m ImageMetaData) error { +func (d *localDataStore) SaveImageMetadata(img ImageMetaData) error { + logrus.Debugf("Saving imagemetadata: %s", img.String()) + db, err := d.openDatabase() + if err != nil { + return err + } + defer db.Close() + + tx, err := db.Begin() + if err != nil { + return err + } + + if img.ImageId <= 0 { + err = d.insertImageMetaData(tx, img) + } else { + err = d.updateImageMetaData(tx, img) + } + + if err != nil { + logrus.Errorf("Rolling back transaction for metadata of %s", img.RelativeImagePath) + errTx := tx.Rollback() + if errTx != nil { + logrus.Errorf("Rollback of transaction for metadata of %s failed!", img.RelativeImagePath) + } + return err + } + + logrus.Debugf("Commiting metadata for image %s", img.String()) + return tx.Commit() +} + +func (d *localDataStore) insertImageMetaData(tx *sql.Tx, data ImageMetaData) error { stmt, err := tx.Prepare("INSERT INTO image (piwigoId, relativePath, fileName, md5sum, lastChanged, categoryPath, categoryId) VALUES (?,?,?,?,?,?,?)") if err != nil { return err } - _, err = stmt.Exec(m.PiwigoId, m.RelativeImagePath, m.Filename, m.Md5Sum, m.LastChange, m.CategoryPath, m.CategoryId) + _, err = stmt.Exec(data.PiwigoId, data.RelativeImagePath, data.Filename, data.Md5Sum, data.LastChange, data.CategoryPath, data.CategoryId) return err } func (d *localDataStore) openDatabase() (*sql.DB, error) { db, err := sql.Open("sqlite3", d.connectionString) if err != nil { + logrus.Warnf("Could not open database %s", d.connectionString) return nil, err } db.SetMaxOpenConns(1) @@ -121,7 +141,7 @@ func (d *localDataStore) openDatabase() (*sql.DB, error) { func (d *localDataStore) createTablesIfNeeded(db *sql.DB) error { _, err := db.Exec("CREATE TABLE IF NOT EXISTS image (" + - "imageId INTEGER PRIMARY KEY AUTOINCREMENT," + + "imageId INTEGER PRIMARY KEY," + "piwigoId INTEGER NULL," + "relativePath NVARCHAR(1000) NOT NULL," + "fileName NVARCHAR(255) NOT NULL," + @@ -130,5 +150,19 @@ func (d *localDataStore) createTablesIfNeeded(db *sql.DB) error { "categoryPath NVARCHAR(1000) NOT NULL," + "categoryId INTEGER NULL" + ");") + if err != nil { + return err + } + + _, err = db.Exec("CREATE UNIQUE INDEX IF NOT EXISTS UX_ImageRelativePath ON image (relativePath);") + return err +} + +func (d *localDataStore) updateImageMetaData(tx *sql.Tx, data ImageMetaData) error { + stmt, err := tx.Prepare("UPDATE image SET piwigoId = ?, relativePath = ?, fileName = ?, md5sum = ?, lastChanged = ?, categoryPath = ?, categoryId = ? WHERE imageId = ?") + if err != nil { + return err + } + _, err = stmt.Exec(data.PiwigoId, data.RelativeImagePath, data.Filename, data.Md5Sum, data.LastChange, data.CategoryPath, data.CategoryId, data.ImageId) return err } diff --git a/internal/app/datastore_test.go b/internal/app/datastore_test.go new file mode 100644 index 0000000..74785bb --- /dev/null +++ b/internal/app/datastore_test.go @@ -0,0 +1,136 @@ +package app + +import ( + "os" + "strings" + "testing" + "time" +) + +var databaseFile = "./metadatatest.db" +var dbinitOk bool + +func TestDataStoreInitialize(t *testing.T) { + _ = setupDatabase(t) + cleanupDatabase(t) +} + +func TestSaveAndLoadMetadata(t *testing.T) { + if !dbinitOk { + t.Skip("Skipping test as TestDataStoreInitialize failed!") + } + dataStore := setupDatabase(t) + + filePath := "blah/foo/bar.jpg" + img := getExampleImageMetadata(filePath) + + saveImageShouldNotFail("insert", dataStore, img, t) + img.ImageId = 1 + + imgLoad := loadMetadataShouldNotFail("insert", dataStore, filePath, t) + EnsureMetadataAreEqual("insert", img, imgLoad, t) + + // updated the image again + img.Md5Sum = "123456" + saveImageShouldNotFail("update", dataStore, img, t) + + imgLoad = loadMetadataShouldNotFail("update", dataStore, filePath, t) + EnsureMetadataAreEqual("update", img, imgLoad, t) + + cleanupDatabase(t) +} + +func TestLoadMetadataNotFound(t *testing.T) { + if !dbinitOk { + t.Skip("Skipping test as TestDataStoreInitialize failed!") + } + dataStore := setupDatabase(t) + + filePath := "blah/foo/bar.jpg" + imgLoad, err := dataStore.GetImageMetadata(filePath) + if err != ErrorRecordNotFound { + t.Errorf("Unexpected error on loading non existing file %s: %s", filePath, err) + } + if imgLoad.ImageId > 0 { + t.Error("Found an image metadata that should not exist on an emtpy database.") + } + + cleanupDatabase(t) +} + +func TestUniqueIndexOnRelativeFilePath(t *testing.T) { + if !dbinitOk { + t.Skip("Skipping test as TestDataStoreInitialize failed!") + } + dataStore := setupDatabase(t) + + filePath := "blah/foo/bar.jpg" + img := getExampleImageMetadata(filePath) + + saveImageShouldNotFail("insert", dataStore, img, t) + + err := dataStore.SaveImageMetadata(img) + if err == nil { + t.Errorf("Could save duplicated image metadata. Expected error but got none!") + } + + // check if the error contains the expected column as name. If not, this indicates another problem than + // the expected duplicated insert error. + if !strings.Contains(err.Error(), "relativePath") { + t.Errorf("Got a unexpected error on saving duplicate records: %s", err) + } + + cleanupDatabase(t) +} + +func saveImageShouldNotFail(action string, dataStore *localDataStore, img ImageMetaData, t *testing.T) { + err := dataStore.SaveImageMetadata(img) + if err != nil { + t.Errorf("%s: Could not save Metadata: %s", action, err) + } +} + +func loadMetadataShouldNotFail(action string, dataStore *localDataStore, filePath string, t *testing.T) ImageMetaData { + imgLoad, err := dataStore.GetImageMetadata(filePath) + if err != nil { + t.Errorf("%s: Could not load saved Metadata: %s - %s", action, filePath, err) + } + return imgLoad +} + +func EnsureMetadataAreEqual(action string, img ImageMetaData, imgLoad ImageMetaData, t *testing.T) { + // check if both instances serialize to the same string representation + if img.String() != imgLoad.String() { + t.Errorf("%s: Invalid image loaded! expected (ignore ImageId) %s but got %s", action, img.String(), imgLoad.String()) + } +} + +func getExampleImageMetadata(filePath string) ImageMetaData { + return ImageMetaData{ + RelativeImagePath: filePath, + PiwigoId: 1, + Md5Sum: "aabbccddeeff", + LastChange: time.Now().UTC(), + Filename: "bar.jpg", + CategoryPath: "blah/foo", + CategoryId: 100, + } +} + +func cleanupDatabase(t *testing.T) { + err := os.Remove(databaseFile) + if err != nil { + t.Errorf("Failed remove test database %s: %s", databaseFile, err) + } +} + +func setupDatabase(t *testing.T) *localDataStore { + dataStore := &localDataStore{} + err := dataStore.Initialize(databaseFile) + if err != nil { + t.Errorf("Failed to init datastore: %s", err) + return nil + } + dbinitOk = true + return dataStore +}