#include #include #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) { qDebug() << "open"; { 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()) { 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.exec()) return setDatabaseError(q); if (!q.next()) return setDatabaseError(q); m_memory = false; m_dirty = false; m_open = true; m_image = q.value(0).toByteArray(); } fileReload(); /* Call after unlocking mutex to allow accesses by receivers */ return true; } bool SQLiteSaveFile::clearNew() { qDebug() << "clearNew"; { QMutexLocker l(&m_dbMut); QSqlDatabase new_db(QSqlDatabase::addDatabase("QSQLITE", QUuid::createUuid().toString())); new_db.setDatabaseName(":memory:"); if (!new_db.open()) { setDatabaseError(new_db); new_db.close(); return false; } QSqlDatabase old_db = m_db; m_db = new_db; if (!initDb(true)) { 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) { for (auto const &q: { "CREATE TABLE IF NOT EXISTS metadata (key TEXT, value TEXT)", "CREATE TABLE IF NOT EXISTS tags (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, anchor_x REAL, anchor_y REAL, meta TEXT)", "CREATE TABLE IF NOT EXISTS blobs (name TEXT, data BLOB)"}) { if (!runSql(q)) return false; } if (setCreationDate) { if (!setMetaLocked("creationTime", QDateTime::currentDateTimeUtc().toMSecsSinceEpoch())) return false; } return true; } static sqlite3 *getSqliteHandle(QSqlDatabase &db) { QVariant v = db.driver()->handle(); assert (v.isValid()); assert (!qstrcmp(v.typeName(), "sqlite3*")); return *static_cast(v.data()); } bool SQLiteSaveFile::saveAs(const QString &filename) { qDebug() << "saveAs" << filename; QMutexLocker l(&m_dbMut); QFile f(filename); QSqlDatabase new_db(QSqlDatabase::addDatabase("QSQLITE", QUuid::createUuid().toString())); { new_db.setDatabaseName(f.fileName()); if (!new_db.open()) goto err_cleanup; sqlite3 *old_handle = getSqliteHandle(m_db); sqlite3 *new_handle = getSqliteHandle(new_db); sqlite3_backup *bck = sqlite3_backup_init(new_handle, "main", old_handle, "main"); if (!bck) goto err_cleanup; if (sqlite3_backup_step(bck, -1) != SQLITE_DONE) goto err_cleanup; if (sqlite3_backup_finish(bck) != SQLITE_DONE) goto err_cleanup; m_db.close(); m_db = new_db; m_memory = false; return true; } err_cleanup: setDatabaseError(new_db); new_db.close(); return false; } QList SQLiteSaveFile::getAllTags() { qDebug() << "getAllTags"; QMutexLocker l(&m_dbMut); resetError(); QList rv; QSqlQuery q("SELECT id, name, anchor_x, anchor_y, meta FROM tags", m_db); if (!q.exec()) { setDatabaseError(q); return QList(); } 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(); return rv; } bool SQLiteSaveFile::updateTag(Tag tag) { QMutexLocker l(&m_dbMut); 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; 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) { qDebug() << "createTag"; QMutexLocker l(&m_dbMut); resetError(); QSqlQuery q("INSERT INTO tags(name, anchor_x, anchor_y, meta) VALUES (?, ?, ?, ?)", m_db); q.addBindValue(tag.name); q.addBindValue(tag.anchor.x()); q.addBindValue(tag.anchor.y()); q.addBindValue(QJsonDocument::fromVariant(tag.metadata).toJson()); if (!q.exec()) return setDatabaseError(q); Tag created_tag(q.lastInsertId().toLongLong(), tag); m_dirty = true; tagChange(TagChange::CREATED, created_tag); return true; } bool SQLiteSaveFile::setMetaLocked(const QString &key, const QVariant &value) { return runSql("INSERT OR REPLACE INTO metadata(key, value) VALUES (?, ?)", {key, value}); } bool SQLiteSaveFile::setMetaLocked(std::initializer_list> 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) { QMutexLocker l(&m_dbMut); m_dirty = true; return setMetaLocked(key, value); } bool SQLiteSaveFile::setMeta(std::initializer_list> metas) { QMutexLocker l(&m_dbMut); m_dirty = true; 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 { qDebug() << "getMeta " << key; resetError(); QSqlQuery q("SELECT value FROM metadata WHERE key=?", m_db); q.addBindValue(key); if (!q.exec()) { setDatabaseError(q); return QVariant(); } if (!q.next()) { setDatabaseError(q); return QVariant(); } return q.value(0); } bool SQLiteSaveFile::runSql(QString query, std::initializer_list bindings) { qDebug() << "runSql " << query; resetError(); QSqlQuery q(query, m_db); 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; return runSql("INSERT OR REPLACE INTO blobs(name, data) VALUES ('image', ?)", {m_image}); } 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) { } bool SQLiteSaveFile::setDatabaseError(const QSqlQuery &q) const { if (!q.lastError().isValid()) return true; qDebug() << "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; qDebug() << "Database error: " << db.lastError().text(); setError(SQLiteError, QString("Project file database error: %1").arg(db.lastError().text())); return false; }