See More

#include #include #include #include #include #include #include #include #include "FileDialog.h" #include "RemoteDatabase.h" #include "Settings.h" #include "sqlite.h" #include "version.h" RemoteDatabase::RemoteDatabase() : m_dbLocal(nullptr) { } RemoteDatabase::~RemoteDatabase() { // Close local storage db - but only if it was created/opened in the meantime if(m_dbLocal) sqlite3_close(m_dbLocal); } void RemoteDatabase::localAssureOpened() { // This function should be called first in each RemoteDatabase::local* function. It assures the database for storing // the local database information is opened and ready. If the database file doesn't exist yet it is created by this // function. If the database file is already created and opened this function does nothing. The reason to open the // database on first use instead of doing that in the constructor of this class is that this way no database file is // going to be created and no database handle is held when it's not actually needed. For people not interested in // the dbhub.io functionality this means no unnecessary files being created. // Check if database is already opened and return if it is if(m_dbLocal) return; // Make sure the directory exists QString database_directory = QStandardPaths::writableLocation( #if QT_VERSION >= QT_VERSION_CHECK(5, 4, 0) QStandardPaths::AppDataLocation #else QStandardPaths::GenericDataLocation #endif ); QDir().mkpath(database_directory); // Open file QString database_file = database_directory + "/remotedbs.db"; if(sqlite3_open_v2(database_file.toUtf8(), &m_dbLocal, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, nullptr) != SQLITE_OK) { QMessageBox::warning(nullptr, qApp->applicationName(), tr("Error opening local databases list.\n%1").arg(QString::fromUtf8(sqlite3_errmsg(m_dbLocal)))); return; } // Create local local table if it doesn't exists yet char* errmsg; QString statement = QString("CREATE TABLE IF NOT EXISTS \"local\"(" "\"id\" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT," "\"identity\" TEXT NOT NULL," "\"name\" TEXT NOT NULL," "\"url\" TEXT NOT NULL," "\"commit_id\" TEXT NOT NULL," "\"file\" TEXT NOT NULL UNIQUE," "\"modified\" INTEGER DEFAULT 0," "\"branch\" TEXT NOT NULL DEFAULT \"main\"" ")"); if(sqlite3_exec(m_dbLocal, statement.toUtf8(), nullptr, nullptr, &errmsg) != SQLITE_OK) { QMessageBox::warning(nullptr, qApp->applicationName(), tr("Error creating local databases list.\n%1").arg(QString::fromUtf8(errmsg))); sqlite3_free(errmsg); sqlite3_close(m_dbLocal); m_dbLocal = nullptr; return; } } QString RemoteDatabase::localAdd(QString filename, QString identity, const QUrl& url, const std::string& new_commit_id, const std::string& branch) { localAssureOpened(); // Remove the path QFileInfo f(identity); identity = f.fileName(); // Check if this file has already been checked in std::string last_commit_id = localLastCommitId(identity, url.toString(), branch); if(last_commit_id.empty()) { // The file hasn't been checked in yet. So add a new record for it. // Generate a new file name to save the file under filename = QString("%2_%1.remotedb").arg(QDateTime::currentMSecsSinceEpoch()).arg(filename); // Insert database into local database list QString sql = QString("INSERT INTO local(identity, name, url, commit_id, file, branch) VALUES(?, ?, ?, ?, ?, ?)"); sqlite3_stmt* stmt; if(sqlite3_prepare_v2(m_dbLocal, sql.toUtf8(), -1, &stmt, nullptr) != SQLITE_OK) return QString(); if(sqlite3_bind_text(stmt, 1, identity.toUtf8(), identity.toUtf8().length(), SQLITE_TRANSIENT)) { sqlite3_finalize(stmt); return QString(); } if(sqlite3_bind_text(stmt, 2, url.fileName().toUtf8(), url.fileName().toUtf8().length(), SQLITE_TRANSIENT)) { sqlite3_finalize(stmt); return QString(); } if(sqlite3_bind_text(stmt, 3, url.toString(QUrl::PrettyDecoded | QUrl::RemoveQuery).toUtf8(), url.toString(QUrl::PrettyDecoded | QUrl::RemoveQuery).toUtf8().length(), SQLITE_TRANSIENT)) { sqlite3_finalize(stmt); return QString(); } if(sqlite3_bind_text(stmt, 4, new_commit_id.c_str(), static_cast(new_commit_id.size()), SQLITE_TRANSIENT)) { sqlite3_finalize(stmt); return QString(); } if(sqlite3_bind_text(stmt, 5, filename.toUtf8(), filename.size(), SQLITE_TRANSIENT)) { sqlite3_finalize(stmt); return QString(); } if(sqlite3_bind_text(stmt, 6, branch.c_str(), static_cast(branch.size()), SQLITE_TRANSIENT)) { sqlite3_finalize(stmt); return QString(); } if(sqlite3_step(stmt) != SQLITE_DONE) { sqlite3_finalize(stmt); return QString(); } sqlite3_finalize(stmt); // Return full path to the new file return Settings::getValue("remote", "clonedirectory").toString() + "/" + filename; } // If we get here, the file has been checked in before. Check next if it has been updated in the meantime. if(last_commit_id != new_commit_id) { // The file has already been checked in and the commit ids are different. If they weren't we wouldn't need to update anything QString sql = QString("UPDATE local SET commit_id=? WHERE identity=? AND url=? AND branch=?"); sqlite3_stmt* stmt; if(sqlite3_prepare_v2(m_dbLocal, sql.toUtf8(), -1, &stmt, nullptr) != SQLITE_OK) return QString(); if(sqlite3_bind_text(stmt, 1, new_commit_id.c_str(), static_cast(new_commit_id.size()), SQLITE_TRANSIENT)) { sqlite3_finalize(stmt); return QString(); } if(sqlite3_bind_text(stmt, 2, identity.toUtf8(), identity.toUtf8().length(), SQLITE_TRANSIENT)) { sqlite3_finalize(stmt); return QString(); } if(sqlite3_bind_text(stmt, 3, url.toString(QUrl::PrettyDecoded | QUrl::RemoveQuery).toUtf8(), url.toString(QUrl::PrettyDecoded | QUrl::RemoveQuery).toUtf8().length(), SQLITE_TRANSIENT)) { sqlite3_finalize(stmt); return QString(); } if(sqlite3_bind_text(stmt, 4, branch.c_str(), static_cast(branch.size()), SQLITE_TRANSIENT)) { sqlite3_finalize(stmt); return QString(); } if(sqlite3_step(stmt) != SQLITE_DONE) { sqlite3_finalize(stmt); return QString(); } sqlite3_finalize(stmt); } // If we got here, the file was already checked in (and was either updated or not (obviously)). This mean we can just return the file name as // we know it. return localExists(url, identity, branch); } QString RemoteDatabase::localExists(const QUrl& url, QString identity, const std::string& branch) { localAssureOpened(); // Extract commit id from url and remove query part afterwards QString url_commit_id = QUrlQuery(url).queryItemValue("commit"); // Query commit id and filename for the given combination of url and identity QString sql = QString("SELECT id, commit_id, file FROM local WHERE url=? AND identity=? AND branch=?"); sqlite3_stmt* stmt; if(sqlite3_prepare_v2(m_dbLocal, sql.toUtf8(), -1, &stmt, nullptr) != SQLITE_OK) return QString(); if(sqlite3_bind_text(stmt, 1, url.toString(QUrl::PrettyDecoded | QUrl::RemoveQuery).toUtf8(), url.toString(QUrl::PrettyDecoded | QUrl::RemoveQuery).toUtf8().length(), SQLITE_TRANSIENT)) { sqlite3_finalize(stmt); return QString(); } QFileInfo f(identity); // Remove the path identity = f.fileName(); if(sqlite3_bind_text(stmt, 2, identity.toUtf8(), identity.toUtf8().length(), SQLITE_TRANSIENT)) { sqlite3_finalize(stmt); return QString(); } if(sqlite3_bind_text(stmt, 3, branch.c_str(), static_cast(branch.size()), SQLITE_TRANSIENT)) { sqlite3_finalize(stmt); return QString(); } if(sqlite3_step(stmt) != SQLITE_ROW) { // If there was either an error or no record was found for this combination of url and // identity, stop here. sqlite3_finalize(stmt); return QString(); } // Having come here we can assume that at least some local clone for the given combination of // url, identity and branch exists. So extract all the information we have on it. QString local_commit_id = QString::fromUtf8(reinterpret_cast(sqlite3_column_text(stmt, 1))); QString local_file = QString::fromUtf8(reinterpret_cast(sqlite3_column_text(stmt, 2))); sqlite3_finalize(stmt); // There are three possibilities now: either we didn't get any commit id in the URL in which case we just return the file we got, no matter what. // Or the requested commit id is the same as the local commit id in which case we return the file we got as well. // Or the requested commit id differ in which case we return no match. if(url_commit_id.isNull() || local_commit_id == url_commit_id) { // Both commit ids are the same. That's the perfect match, so we can open the local file if it still exists return localCheckFile(local_file); } else { // The commit ids differ. This means we have no match return QString(); } } QString RemoteDatabase::localCheckFile(const QString& local_file) { // This function takes the file name of a locally cloned database and checks if this file still exists. If it has been deleted in the meantime it returns // an empty string and deletes the file from the clone database. If the file still exists, it returns the full path to the file. localAssureOpened(); // Build the full path to where the file should be QString full_path = Settings::getValue("remote", "clonedirectory").toString() + "/" + local_file; // Check if the database still exists. If so return its path, if not return an empty string to redownload it if(QFile::exists(full_path)) { return full_path; } else { // Remove the apparently invalid entry from the local clones database to avoid future lookups and confusions. The file column should // be unique for the entire table because the files are all in the same directory and their names need to be unique because of this. localDeleteFile(local_file); // Return empty string to indicate a redownload request return QString(); } } std::string RemoteDatabase::localLastCommitId(QString identity, const QUrl& url, const std::string& branch) { localAssureOpened(); // Query commit id for that file name QString sql = QString("SELECT commit_id FROM local WHERE identity=? AND url=? AND branch=?"); sqlite3_stmt* stmt; if(sqlite3_prepare_v2(m_dbLocal, sql.toUtf8(), -1, &stmt, nullptr) != SQLITE_OK) return std::string(); QFileInfo f(identity); // Remove the path identity = f.fileName(); if(sqlite3_bind_text(stmt, 1, identity.toUtf8(), identity.toUtf8().length(), SQLITE_TRANSIENT)) { sqlite3_finalize(stmt); return std::string(); } if(sqlite3_bind_text(stmt, 2, url.toString(QUrl::PrettyDecoded | QUrl::RemoveQuery).toUtf8(), url.toString(QUrl::PrettyDecoded | QUrl::RemoveQuery).toUtf8().size(), SQLITE_TRANSIENT)) { sqlite3_finalize(stmt); return std::string(); } if(sqlite3_bind_text(stmt, 3, branch.c_str(), static_cast(branch.size()), SQLITE_TRANSIENT)) { sqlite3_finalize(stmt); return std::string(); } if(sqlite3_step(stmt) != SQLITE_ROW) { // If there was either an error or no record was found for this file name, stop here. sqlite3_finalize(stmt); return std::string(); } // Having come here we can assume that at least some local clone with the given file name std::string local_commit_id = reinterpret_cast(sqlite3_column_text(stmt, 0)); sqlite3_finalize(stmt); return local_commit_id; } std::vector<:localfileinfo> RemoteDatabase::localGetLocalFiles(QString identity) { localAssureOpened(); // Get all rows for this identity QString sql = QString("SELECT name, url, commit_id, file, branch FROM local WHERE identity=? ORDER BY url"); sqlite3_stmt* stmt; if(sqlite3_prepare_v2(m_dbLocal, sql.toUtf8(), -1, &stmt, nullptr) != SQLITE_OK) return {}; QFileInfo f(identity); identity = f.fileName(); if(sqlite3_bind_text(stmt, 1, identity.toUtf8(), identity.toUtf8().length(), SQLITE_TRANSIENT)) { sqlite3_finalize(stmt); return {}; } std::vector<:localfileinfo> result; while(sqlite3_step(stmt) == SQLITE_ROW) { result.emplace_back(reinterpret_cast(sqlite3_column_text(stmt, 0)), reinterpret_cast(sqlite3_column_text(stmt, 1)), reinterpret_cast(sqlite3_column_text(stmt, 2)), reinterpret_cast(sqlite3_column_text(stmt, 3)), reinterpret_cast(sqlite3_column_text(stmt, 4)), identity.toStdString()); } sqlite3_finalize(stmt); return result; } RemoteDatabase::LocalFileInfo RemoteDatabase::localGetLocalFileInfo(QString filename) { localAssureOpened(); // Find this file in our database QString sql = QString("SELECT name, url, commit_id, branch, identity FROM local WHERE file=?"); sqlite3_stmt* stmt; if(sqlite3_prepare_v2(m_dbLocal, sql.toUtf8(), -1, &stmt, nullptr) != SQLITE_OK) return {}; // Remove the path for querying the file name filename = QFileInfo(filename).fileName(); if(sqlite3_bind_text(stmt, 1, filename.toUtf8(), filename.toUtf8().length(), SQLITE_TRANSIENT)) { sqlite3_finalize(stmt); return {}; } if(sqlite3_step(stmt) != SQLITE_ROW) { // If there was either an error or no record was found for this file name, stop here. sqlite3_finalize(stmt); return {}; } // Retrieve and return all the information we have RemoteDatabase::LocalFileInfo result(reinterpret_cast(sqlite3_column_text(stmt, 0)), reinterpret_cast(sqlite3_column_text(stmt, 1)), reinterpret_cast(sqlite3_column_text(stmt, 2)), filename.toStdString(), reinterpret_cast(sqlite3_column_text(stmt, 3)), reinterpret_cast(sqlite3_column_text(stmt, 4))); sqlite3_finalize(stmt); return result; } void RemoteDatabase::localDeleteFile(QString filename) { localAssureOpened(); // Remove the file's entry in our database QString sql = QString("DELETE FROM local WHERE file=?"); sqlite3_stmt* stmt; if(sqlite3_prepare_v2(m_dbLocal, sql.toUtf8(), -1, &stmt, nullptr) != SQLITE_OK) return; if(sqlite3_bind_text(stmt, 1, filename.toUtf8(), filename.toUtf8().length(), SQLITE_TRANSIENT)) { sqlite3_finalize(stmt); return; } if(sqlite3_step(stmt) != SQLITE_DONE) { sqlite3_finalize(stmt); return; } sqlite3_finalize(stmt); // Delete the actual file on disk QFile::remove(Settings::getValue("remote", "clonedirectory").toString() + "/" + filename); } QString RemoteDatabase::LocalFileInfo::user_name() const { // Figure out the user name from the URL QString path = QUrl(QString::fromStdString(url)).path(); if(path.count('/') < 2 || !path.startsWith('/')) return QString(); else return path.mid(1, path.indexOf('/', 1) - 1); }