Added first datastore implementation with tests

This commit is contained in:
Philipp Häfelfinger 2019-03-15 22:50:48 +01:00
parent 9ca31bcb2b
commit b917511e95
2 changed files with 209 additions and 39 deletions

View File

@ -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 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
}
defer db.Close()
tx, err := db.Begin()
if err != nil {
return err
}
if m.ImageId <= 0 {
err = d.insertImageMetaData(tx, m)
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
}

View File

@ -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
}