#include #include #include "sqlitebackend.h" SQLiteSaveFile::SQLiteSaveFile(QObject *parent, QString filename) : QObject(parent) , lastError(NoError) , lastErrorString(QString()) , db(QSqlDatabase::addDatabase("QSQLITE", QUuid::createUuid().toString())) , filename(filename) , m_isOpen(false) { connect(); } 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; } bool SQLiteSaveFile::connect() { QMutexLocker l(&dbMut); resetError(); m_isOpen = false; imageData = QByteArray(); bool newlyCreated = QFile(filename).exists(); db.setDatabaseName(filename); if (!db.open()) { setDatabaseError(db); db.close(); return false; } if (!initDb(newlyCreated)) { db.close(); return false; } m_isOpen = true; /* Try to load image, ignore if image is unset */ QSqlQuery q("SELECT data FROM blobs WHERE name = 'image'", db); if (!q.next()) return setDatabaseError(q); imageData = q.value(0).toByteArray(); 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) { QMutexLocker l(&dbMut); QFile f(filename); QSqlDatabase old_db = db; 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; } sqlite3 *old_handle = getSqliteHandle(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; db = new_db; old_db.close(); return true; err_cleanup: setDatabaseError(new_db); new_db.close(); return false; } bool SQLiteSaveFile::clearNew() { QMutexLocker l(&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; } if (!initDb()) { db.close(); return false; } imageData = QByteArray(); return true; } QList SQLiteSaveFile::getAllTags() { QMutexLocker l(&dbMut); resetError(); QList rv; QSqlQuery q("SELECT (id, name, anchor_x, anchor_y, meta) FROM tags", db); 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(&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; tagChange(TagChange::CHANGED, tag); return true; } bool SQLiteSaveFile::deleteTag(Tag tag) { QMutexLocker l(&dbMut); if (!runSql("DELETE FROM tags WHERE id=?", {tag.id})) return false; tagChange(TagChange::DELETED, tag); return true; } bool SQLiteSaveFile::createTag(Tag tag) { QMutexLocker l(&dbMut); resetError(); QSqlQuery q("INSERT INTO tags(name, anchor_x, anchor_y, meta) VALUES (?, ?, ?, ?)", 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); 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(&dbMut); return setMetaLocked(key, value); } bool SQLiteSaveFile::setMeta(std::initializer_list> metas) { QMutexLocker l(&dbMut); return setMetaLocked(metas); } QVariant SQLiteSaveFile::getMeta(const QString &key) { QMutexLocker l(&dbMut); return getMetaLocked(key); } QVariant SQLiteSaveFile::getMetaLocked(const QString &key) { resetError(); QSqlQuery q("SELECT value FROM metadata WHERE key=?", db); q.addBindValue(key); if (!q.next()) { setDatabaseError(q); return QVariant(); } return q.value(0); } bool SQLiteSaveFile::runSql(QString query, std::initializer_list bindings) { resetError(); QSqlQuery q(query, db); for (const QVariant &v : bindings) q.addBindValue(v); q.exec(); return setDatabaseError(q); } bool SQLiteSaveFile::loadImageFromDisk(const QString &filename) { QMutexLocker l(&dbMut); QFile f(filename); resetError(); if (!f.open(QIODevice::ReadOnly)) { setError(ImageOpenError, QString("Failed to open image: %1").arg(f.errorString())); return false; } imageData = 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; return runSql("INSERT OR REPLACE INTO blobs(name, data) VALUES ('image', ?)", {imageData}); } 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()) { } Tag::Tag() : id(-1) { } Tag::Tag(long long id, const Tag &other) : id(id) , name(other.name) , anchor(other.anchor) , metadata(other.metadata) { } bool SQLiteSaveFile::setDatabaseError(const QSqlQuery &q) { if (!q.lastError().isValid()) return true; setError(SQLiteError, QString("Project file database error: %1").arg(q.lastError().text())); return false; } bool SQLiteSaveFile::setDatabaseError(const QSqlDatabase &db) { if (!db.lastError().isValid()) return true; setError(SQLiteError, QString("Project file database error: %1").arg(db.lastError().text())); return false; }