Added first datastore implementation with tests
This commit is contained in:
parent
9ca31bcb2b
commit
b917511e95
@ -3,11 +3,14 @@ package app
|
|||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
_ "github.com/mattn/go-sqlite3"
|
_ "github.com/mattn/go-sqlite3"
|
||||||
"log"
|
"github.com/sirupsen/logrus"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var ErrorRecordNotFound = errors.New("Record not found")
|
||||||
|
|
||||||
type ImageMetaData struct {
|
type ImageMetaData struct {
|
||||||
ImageId int
|
ImageId int
|
||||||
PiwigoId int
|
PiwigoId int
|
||||||
@ -19,11 +22,12 @@ type ImageMetaData struct {
|
|||||||
CategoryId int
|
CategoryId int
|
||||||
}
|
}
|
||||||
|
|
||||||
type ImageMetadataLoader interface {
|
func (img *ImageMetaData) String() string {
|
||||||
GetImageMetadata(relativePath string) (ImageMetaData, error)
|
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
|
SaveImageMetadata(m ImageMetaData) error
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -50,68 +54,84 @@ func (d *localDataStore) Initialize(connectionString string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *localDataStore) GetImageMetadata(relativePath string) (ImageMetaData, error) {
|
func (d *localDataStore) GetImageMetadata(relativePath string) (ImageMetaData, error) {
|
||||||
|
logrus.Debugf("Query image metadata for file %s", relativePath)
|
||||||
|
img := ImageMetaData{}
|
||||||
|
|
||||||
db, err := d.openDatabase()
|
db, err := d.openDatabase()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ImageMetaData{}, err
|
return img, err
|
||||||
}
|
}
|
||||||
defer db.Close()
|
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 {
|
if err != nil {
|
||||||
return ImageMetaData{}, err
|
return img, err
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO: select entry by path
|
rows, err := stmt.Query(relativePath)
|
||||||
//stmt, err := tx.Prepare("select * from image WHERE relativePath = '?'")
|
|
||||||
//if err != nil {
|
|
||||||
// log.Fatal(err)
|
|
||||||
//}
|
|
||||||
|
|
||||||
err = tx.Commit()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
return img, err
|
||||||
}
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
return ImageMetaData{}, nil
|
if rows.Next() {
|
||||||
}
|
err = rows.Scan(&img.ImageId, &img.PiwigoId, &img.RelativeImagePath, &img.Filename, &img.Md5Sum, &img.LastChange, &img.CategoryPath, &img.CategoryId)
|
||||||
|
|
||||||
func (d *localDataStore) SaveImageMetadata(m ImageMetaData) error {
|
|
||||||
db, err := d.openDatabase()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return img, 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
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// TODO: update existing entry
|
return img, ErrorRecordNotFound
|
||||||
|
}
|
||||||
|
err = rows.Err()
|
||||||
|
|
||||||
|
return img, err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = tx.Commit()
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *localDataStore) insertImageMetaData(tx *sql.Tx, m ImageMetaData) error {
|
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 (?,?,?,?,?,?,?)")
|
stmt, err := tx.Prepare("INSERT INTO image (piwigoId, relativePath, fileName, md5sum, lastChanged, categoryPath, categoryId) VALUES (?,?,?,?,?,?,?)")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *localDataStore) openDatabase() (*sql.DB, error) {
|
func (d *localDataStore) openDatabase() (*sql.DB, error) {
|
||||||
db, err := sql.Open("sqlite3", d.connectionString)
|
db, err := sql.Open("sqlite3", d.connectionString)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
logrus.Warnf("Could not open database %s", d.connectionString)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
db.SetMaxOpenConns(1)
|
db.SetMaxOpenConns(1)
|
||||||
@ -121,7 +141,7 @@ func (d *localDataStore) openDatabase() (*sql.DB, error) {
|
|||||||
|
|
||||||
func (d *localDataStore) createTablesIfNeeded(db *sql.DB) error {
|
func (d *localDataStore) createTablesIfNeeded(db *sql.DB) error {
|
||||||
_, err := db.Exec("CREATE TABLE IF NOT EXISTS image (" +
|
_, err := db.Exec("CREATE TABLE IF NOT EXISTS image (" +
|
||||||
"imageId INTEGER PRIMARY KEY AUTOINCREMENT," +
|
"imageId INTEGER PRIMARY KEY," +
|
||||||
"piwigoId INTEGER NULL," +
|
"piwigoId INTEGER NULL," +
|
||||||
"relativePath NVARCHAR(1000) NOT NULL," +
|
"relativePath NVARCHAR(1000) NOT NULL," +
|
||||||
"fileName NVARCHAR(255) NOT NULL," +
|
"fileName NVARCHAR(255) NOT NULL," +
|
||||||
@ -130,5 +150,19 @@ func (d *localDataStore) createTablesIfNeeded(db *sql.DB) error {
|
|||||||
"categoryPath NVARCHAR(1000) NOT NULL," +
|
"categoryPath NVARCHAR(1000) NOT NULL," +
|
||||||
"categoryId INTEGER 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
|
return err
|
||||||
}
|
}
|
||||||
|
136
internal/app/datastore_test.go
Normal file
136
internal/app/datastore_test.go
Normal 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
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user