460 lines
14 KiB
C++
460 lines
14 KiB
C++
#include <QMessageBox>
|
|
|
|
#include "sqlitebackend.h"
|
|
|
|
SQLiteSaveFile::SQLiteSaveFile(QObject *parent) :
|
|
QObject(parent)
|
|
, m_lastError(NoError)
|
|
, m_lastErrorString(QString())
|
|
, m_open(false)
|
|
, m_dirty(false)
|
|
{
|
|
clearNew();
|
|
}
|
|
|
|
bool SQLiteSaveFile::open(const QString &filename)
|
|
{
|
|
auto dbg = qDebug() << "open";
|
|
{ /* Emit signals only after unlocking mutex to allow accesses by receivers */
|
|
QMutexLocker l(&m_dbMut);
|
|
QFile f(filename);
|
|
if (!f.exists()) {
|
|
setError(FileNotFoundError, QString("File \"%1\"does not exist.").arg(filename));
|
|
return false;
|
|
}
|
|
|
|
QSqlDatabase new_db(QSqlDatabase::addDatabase("QSQLITE", QUuid::createUuid().toString()));
|
|
new_db.setDatabaseName(f.fileName());
|
|
if (!new_db.open()) {
|
|
qWarning() << "Cannot open db, closing" << filename;
|
|
setDatabaseError(new_db);
|
|
new_db.close();
|
|
return false;
|
|
}
|
|
|
|
m_db.close();
|
|
m_db = new_db;
|
|
|
|
/* Try to load image, ignore if image is unset */
|
|
QSqlQuery q("SELECT data FROM blobs WHERE name = 'image'", m_db);
|
|
if (!q.next())
|
|
return setDatabaseError(q);
|
|
|
|
m_memory = false;
|
|
m_dirty = false;
|
|
m_open = true;
|
|
m_image = q.value(0).toByteArray();
|
|
dbg << QString("Loaded %1 byte image").arg(m_image.size());
|
|
}
|
|
fileReload();
|
|
return true;
|
|
}
|
|
|
|
bool SQLiteSaveFile::clearNew()
|
|
{
|
|
qDebug() << "clearNew";
|
|
{ /* Emit signals only after unlocking mutex to allow accesses by receivers */
|
|
QMutexLocker l(&m_dbMut);
|
|
|
|
QSqlDatabase new_db(QSqlDatabase::addDatabase("QSQLITE", QUuid::createUuid().toString()));
|
|
new_db.setDatabaseName(":memory:");
|
|
if (!new_db.open()) {
|
|
setDatabaseError(new_db);
|
|
qWarning() << "Cannot open new mem db, closing";
|
|
new_db.close();
|
|
return false;
|
|
}
|
|
|
|
QSqlDatabase old_db = m_db;
|
|
m_db = new_db;
|
|
|
|
if (!initDb(true)) {
|
|
qWarning() << "Cannot init new mem db, closing";
|
|
m_db.close();
|
|
m_db = old_db;
|
|
return false;
|
|
}
|
|
|
|
old_db.close();
|
|
m_memory = true;
|
|
m_dirty = false;
|
|
m_open = true;
|
|
m_image = QByteArray();
|
|
}
|
|
fileReload(); /* Call after unlocking mutex to allow accesses by receivers */
|
|
return true;
|
|
}
|
|
|
|
bool SQLiteSaveFile::initDb(bool setCreationDate, const QString &schema_name)
|
|
{
|
|
for (auto const &q: {
|
|
"CREATE TABLE IF NOT EXISTS %1.metadata (key TEXT PRIMARY KEY, value TEXT)",
|
|
"CREATE TABLE IF NOT EXISTS %1.tags (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, anchor_x REAL, anchor_y REAL, meta TEXT)",
|
|
"CREATE TABLE IF NOT EXISTS %1.blobs (name TEXT, data BLOB)"}) {
|
|
/* We cannot use sqlite's value binding for the table/schema name. This is an sqlite API restriction.
|
|
* The schema_name parameter here is not user-supplied, so this is safe. */
|
|
if (!runSql(QString(q).arg(schema_name)))
|
|
return false;
|
|
}
|
|
|
|
if (setCreationDate) {
|
|
if (!setMetaLocked("creationTime", QDateTime::currentDateTimeUtc().toMSecsSinceEpoch()))
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool SQLiteSaveFile::saveAs(const QString &filename)
|
|
{
|
|
/* Using the SQLite backup API is a royal PITA on windows due to Qt not including an actual sqlite3.dll there, so we just emulate its functionality. */
|
|
qDebug() << "saveAs" << filename;
|
|
QMutexLocker l(&m_dbMut);
|
|
QFile::remove(filename);
|
|
qInfo() << "removed existing file" << filename;
|
|
|
|
if (!runSql("ATTACH DATABASE ? AS target", {filename}))
|
|
return false;
|
|
|
|
if (!initDb(false, "target")) {
|
|
/* Fire and forget */
|
|
QSqlQuery q("DETACH DATABASE target");
|
|
return false;
|
|
}
|
|
|
|
for (const auto &table : {"metadata", "tags", "blobs"}) {
|
|
/* We cannot use sqlite's value binding for the table/schema name. This is an sqlite API restriction.
|
|
* The schema_name parameter here is not user-supplied, so this is safe. */
|
|
if (!runSql(QString("INSERT INTO target.%1 SELECT * FROM main.%1").arg(table))) {
|
|
/* Fire and forget */
|
|
QSqlQuery q("DETACH DATABASE target");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
m_db.close();
|
|
|
|
/* Re-create DB. We do not want to make any assumptions about the insides of Qt's driver.
|
|
* It might cache things internally and we might throw it off-balance if we swap out the DB attachment underneath. */
|
|
QSqlDatabase new_db(QSqlDatabase::addDatabase("QSQLITE", QUuid::createUuid().toString()));
|
|
new_db.setDatabaseName(filename);
|
|
if (!new_db.open())
|
|
return setDatabaseError(new_db);
|
|
|
|
m_db = new_db;
|
|
m_memory = false;
|
|
m_dirty = false;
|
|
return true;
|
|
}
|
|
|
|
QList<Tag> SQLiteSaveFile::getAllTags()
|
|
{
|
|
auto dbg = qDebug() << "getAllTags()";
|
|
QMutexLocker l(&m_dbMut);
|
|
resetError();
|
|
QList<Tag> rv;
|
|
|
|
QSqlQuery q("SELECT id, name, anchor_x, anchor_y, meta FROM tags", m_db);
|
|
if (!setDatabaseError(q))
|
|
return QList<Tag>();
|
|
|
|
while (q.next()) {
|
|
rv << Tag {
|
|
q.value(0).toLongLong(),
|
|
q.value(1).toString(),
|
|
q.value(2).toFloat(),
|
|
q.value(3).toFloat(),
|
|
q.value(4).toByteArray()
|
|
};
|
|
}
|
|
|
|
if (!setDatabaseError(q))
|
|
return QList<Tag>();
|
|
|
|
dbg << QString("%1 tags").arg(rv.size());
|
|
return rv;
|
|
}
|
|
|
|
bool SQLiteSaveFile::updateTag(Tag tag)
|
|
{
|
|
auto dbg = qDebug() << "updating tag" << tag.id << tag.name;
|
|
{
|
|
QMutexLocker l(&m_dbMut);
|
|
QSqlQuery q(m_db);
|
|
q.prepare("SELECT 1 FROM tags WHERE id=? AND name=?");
|
|
q.addBindValue(tag.id);
|
|
q.addBindValue(tag.name);
|
|
if (!q.exec()) {
|
|
setDatabaseError(q);
|
|
return false;
|
|
}
|
|
bool nameChanged = !q.next();
|
|
if (!setDatabaseError(q))
|
|
return false;
|
|
|
|
if (nameChanged)
|
|
setMetaLocked("lastTagName", tag.name);
|
|
|
|
if (!runSql("UPDATE tags SET name=?, anchor_x=?, anchor_y=?, meta=? WHERE id=?", {
|
|
tag.name, tag.anchor.x(), tag.anchor.y(), QJsonDocument::fromVariant(tag.metadata).toJson(), tag.id
|
|
})) {
|
|
return false;
|
|
}
|
|
|
|
m_dirty = true;
|
|
}
|
|
dbg << "calling handlers";
|
|
tagChange(TagChange::CHANGED, tag);
|
|
return true;
|
|
}
|
|
|
|
bool SQLiteSaveFile::deleteTag(Tag tag)
|
|
{
|
|
{
|
|
QMutexLocker l(&m_dbMut);
|
|
if (!runSql("DELETE FROM tags WHERE id=?", {tag.id}))
|
|
return false;
|
|
|
|
m_dirty = true;
|
|
}
|
|
tagChange(TagChange::DELETED, tag);
|
|
return true;
|
|
}
|
|
|
|
bool SQLiteSaveFile::createTag(Tag tag)
|
|
{
|
|
QSqlQuery q(m_db);
|
|
auto dbg = qDebug() << "createTag";
|
|
{
|
|
QMutexLocker l(&m_dbMut);
|
|
resetError();
|
|
if (!runSql("INSERT INTO tags(name, anchor_x, anchor_y, meta) VALUES (?, ?, ?, ?)", {
|
|
tag.name,
|
|
tag.anchor.x(),
|
|
tag.anchor.y(),
|
|
QJsonDocument::fromVariant(tag.metadata).toJson()
|
|
})) {
|
|
return false;
|
|
}
|
|
m_dirty = true;
|
|
}
|
|
auto id = q.lastInsertId().toLongLong();
|
|
dbg << "id" << id;
|
|
tagChange(TagChange::CREATED, Tag(id, tag));
|
|
return true;
|
|
}
|
|
|
|
QString SQLiteSaveFile::getNextAutoTagName()
|
|
{
|
|
QVariant lookupResult = getMeta("lastTagName");
|
|
QString lastTagName = "U0";
|
|
if (lookupResult.isValid() && !lookupResult.toString().isNull())
|
|
lastTagName = lookupResult.toString();
|
|
QString newName = "U1";
|
|
|
|
QRegularExpression name_re("^(.*?)(\\d+)$");
|
|
auto res = name_re.match(lastTagName);
|
|
if (res.hasMatch()) {
|
|
bool ok = false;
|
|
int numericSuffix = res.captured(2).toInt(&ok);
|
|
QString stringPrefix = res.captured(1);
|
|
qDebug() << "Name has match" << stringPrefix << numericSuffix;
|
|
|
|
if (ok) {
|
|
do {
|
|
numericSuffix ++;
|
|
newName = QString("%1%2").arg(stringPrefix).arg(numericSuffix);
|
|
} while (numericSuffix<10000 && !tagNameIsFree(newName));
|
|
}
|
|
}
|
|
return newName;
|
|
}
|
|
|
|
bool SQLiteSaveFile::createTagAt(const QPointF &anchor)
|
|
{
|
|
QString newName = getNextAutoTagName();
|
|
if (!setMeta("lastTagName", newName))
|
|
return false;
|
|
return createTag(Tag(newName, anchor));
|
|
}
|
|
|
|
bool SQLiteSaveFile::tagNameIsFree(const QString &name)
|
|
{
|
|
QMutexLocker l(&m_dbMut);
|
|
QSqlQuery q(m_db);
|
|
q.prepare("SELECT name FROM tags WHERE name=?");
|
|
q.addBindValue(name);
|
|
if (!q.exec()) {
|
|
setDatabaseError(q);
|
|
return true;
|
|
}
|
|
|
|
if (q.next())
|
|
return false;
|
|
|
|
setDatabaseError(q);
|
|
return true;
|
|
}
|
|
|
|
bool SQLiteSaveFile::setMetaLocked(const QString &key, const QVariant &value)
|
|
{
|
|
qDebug() << QString("setMeta: %1=%2").arg(key).arg(value.toString());
|
|
return runSql("INSERT OR REPLACE INTO metadata(key, value) VALUES (?, ?)", {key, value});
|
|
}
|
|
|
|
bool SQLiteSaveFile::setMetaLocked(std::initializer_list<QPair<QString, QVariant>> metas)
|
|
{
|
|
for (const auto &meta : metas) {
|
|
if (!setMetaLocked(meta.first, meta.second))
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool SQLiteSaveFile::setMeta(const QString &key, const QVariant &value, bool setDirty) {
|
|
QMutexLocker l(&m_dbMut);
|
|
m_dirty = m_dirty || setDirty;
|
|
return setMetaLocked(key, value);
|
|
}
|
|
|
|
bool SQLiteSaveFile::setMeta(std::initializer_list<QPair<QString, QVariant>> metas, bool setDirty) {
|
|
QMutexLocker l(&m_dbMut);
|
|
m_dirty = m_dirty || setDirty;
|
|
return setMetaLocked(metas);
|
|
}
|
|
|
|
const QVariant SQLiteSaveFile::getMeta(const QString &key) const {
|
|
QMutexLocker l(&m_dbMut);
|
|
return getMetaLocked(key);
|
|
}
|
|
|
|
const QVariant SQLiteSaveFile::getMetaLocked(const QString &key) const
|
|
{
|
|
resetError();
|
|
QSqlQuery q(m_db);
|
|
q.prepare("SELECT value FROM metadata WHERE key=?");
|
|
q.addBindValue(key);
|
|
|
|
if (!q.exec()) {
|
|
setDatabaseError(q);
|
|
return QVariant();
|
|
}
|
|
|
|
if (!q.next()) {
|
|
setDatabaseError(q);
|
|
return QVariant();
|
|
}
|
|
|
|
qDebug() << QString("getMeta: %1=%2").arg(key).arg(q.value(0).toString());
|
|
return q.value(0);
|
|
}
|
|
|
|
bool SQLiteSaveFile::runSql(QString query, std::initializer_list<QVariant> bindings)
|
|
{
|
|
qDebug() << "runSql:" << query << "db: open=" << m_db.isOpen() << "valid=" << m_db.isValid() << "error state" << m_db.lastError().text();
|
|
resetError();
|
|
QSqlQuery q(m_db);
|
|
q.prepare(query);
|
|
for (const QVariant &v : bindings) {
|
|
q.addBindValue(v);
|
|
}
|
|
|
|
if (!q.exec()) {
|
|
return setDatabaseError(q);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool SQLiteSaveFile::loadImageFromDisk(const QString &filename)
|
|
{
|
|
{
|
|
QMutexLocker l(&m_dbMut);
|
|
QFile f(filename);
|
|
resetError();
|
|
|
|
if (!f.open(QIODevice::ReadOnly)) {
|
|
setError(ImageOpenError, QString("Failed to open image: %1").arg(f.errorString()));
|
|
return false;
|
|
}
|
|
|
|
m_image = f.readAll();
|
|
if (f.error() != QFileDevice::NoError) {
|
|
setError(ImageReadError, QString("Failed to read image: %1").arg(f.errorString()));
|
|
return false;
|
|
}
|
|
|
|
if (!setMetaLocked({
|
|
{"imagePathOriginal", f.fileName()},
|
|
{"imagePathAbsolute", QFileInfo(f).absoluteFilePath()},
|
|
{"imageLoadedTime", QDateTime::currentDateTimeUtc().toMSecsSinceEpoch()}}))
|
|
return false;
|
|
|
|
m_dirty = true;
|
|
if (!runSql("INSERT OR REPLACE INTO blobs(name, data) VALUES ('image', ?)", {m_image}))
|
|
return false;
|
|
}
|
|
/* Emit signal with mutex unlocked */
|
|
imageLoaded(m_image);
|
|
return true;
|
|
}
|
|
|
|
bool SQLiteSaveFile::reloadImageFromDisk()
|
|
{
|
|
const QString &p = getMeta("imagePathOriginal").toString();
|
|
if (QFile(p).exists())
|
|
return loadImageFromDisk(p);
|
|
|
|
const QString &q = getMeta("imagePathAbsolute").toString();
|
|
if (QFile(q).exists())
|
|
return loadImageFromDisk(q);
|
|
|
|
return false;
|
|
}
|
|
|
|
Tag::Tag(long long id, QString name, qreal anchor_x, qreal anchor_y, QByteArray metadata)
|
|
: id(id)
|
|
, name(name)
|
|
, anchor(QPointF(anchor_x, anchor_y))
|
|
, metadata(QJsonDocument::fromJson(metadata).object().toVariantMap())
|
|
, valid(true)
|
|
{
|
|
}
|
|
|
|
Tag::Tag(long long id, const Tag &other)
|
|
: id(id)
|
|
, name(other.name)
|
|
, anchor(other.anchor)
|
|
, metadata(other.metadata)
|
|
, valid(true)
|
|
{
|
|
}
|
|
|
|
Tag::Tag(QString name, const QPointF &anchor, const QVariantMap metadata)
|
|
: id(-1)
|
|
, name(name)
|
|
, anchor(anchor)
|
|
, metadata(metadata)
|
|
, valid(false)
|
|
{
|
|
}
|
|
|
|
bool SQLiteSaveFile::setDatabaseError(const QSqlQuery &q) const
|
|
{
|
|
if (!q.lastError().isValid())
|
|
return true;
|
|
|
|
qWarning() << "Query error: " << q.lastError().text();
|
|
setError(SQLiteError, QString("Project file database error executing %1: %2").arg(q.executedQuery()).arg(q.lastError().text()));
|
|
|
|
return false;
|
|
}
|
|
|
|
bool SQLiteSaveFile::setDatabaseError(const QSqlDatabase &db) const
|
|
{
|
|
if (!db.lastError().isValid())
|
|
return true;
|
|
|
|
qWarning() << "Database error: " << db.lastError().text();
|
|
setError(SQLiteError, QString("Project file database error: %1").arg(db.lastError().text()));
|
|
return false;
|
|
}
|