/* ReaPack: Package manager for REAPER * Copyright (C) 2015-2025 Christian Fillion * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ #include "registry.hpp" #include "errors.hpp" #include "index.hpp" #include "package.hpp" #include "path.hpp" #include "remote.hpp" #include #include Registry::Registry(const Path &path) : m_db(path.join()) { migrate(); // entry queries m_insertEntry = m_db.prepare( "INSERT INTO entries(remote, category, package, desc, type, version, author, flags)" "VALUES(?, ?, ?, ?, ?, ?, ?, ?);" ); m_updateEntry = m_db.prepare( "UPDATE entries " "SET desc = ?, type = ?, version = ?, author = ?, flags = ? WHERE id = ?" ); m_setFlags = m_db.prepare("UPDATE entries SET flags = ? WHERE id = ?"); m_findEntry = m_db.prepare( "SELECT id, remote, category, package, desc, type, version, author, flags " "FROM entries WHERE remote = ? AND category = ? AND package = ? LIMIT 1" ); m_allEntries = m_db.prepare( "SELECT id, remote, category, package, desc, type, version, author, flags " "FROM entries WHERE remote = ?" ); m_forgetEntry = m_db.prepare("DELETE FROM entries WHERE id = ?"); // file queries m_getOwner = m_db.prepare( "SELECT e.id, remote, category, package, desc, e.type, version, author, flags " "FROM entries e JOIN files f ON f.entry = e.id WHERE f.path = ? LIMIT 1" ); m_getFiles = m_db.prepare( "SELECT path, main, type FROM files WHERE entry = ? ORDER BY path" ); m_insertFile = m_db.prepare("INSERT INTO files VALUES(NULL, ?, ?, ?, ?)"); m_clearFiles = m_db.prepare( "DELETE FROM files WHERE entry = (" " SELECT id FROM entries WHERE remote = ? AND category = ? AND package = ?" ")" ); m_forgetFiles = m_db.prepare("DELETE FROM files WHERE entry = ?"); // lock the database m_db.begin(); } void Registry::migrate() { const Database::Version version{0, 6}; const Database::Version ¤t = m_db.version(); if(!current) { // new database! m_db.exec( "CREATE TABLE entries (" " id INTEGER PRIMARY KEY," " remote TEXT NOT NULL," " category TEXT NOT NULL," " package TEXT NOT NULL," " desc TEXT NOT NULL," " type INTEGER NOT NULL," " version TEXT NOT NULL," " author TEXT NOT NULL," " flags INTEGER DEFAULT 0," " UNIQUE(remote, category, package)" ");" "CREATE TABLE files (" " id INTEGER PRIMARY KEY," " entry INTEGER NOT NULL," " path TEXT UNIQUE NOT NULL," " main INTEGER NOT NULL," " type INTEGER NOT NULL," " FOREIGN KEY(entry) REFERENCES entries(id)" ");" ); m_db.setVersion(version); return; } else if(current < version) { m_db.begin(); switch(current.major) { case 0: // current major schema version break; default: throw reapack_error( "The package registry was created by a newer version of ReaPack"); } switch(current.minor) { case 1: m_db.exec("ALTER TABLE entries ADD COLUMN pinned INTEGER NOT NULL DEFAULT 0;"); [[fallthrough]]; case 2: m_db.exec("ALTER TABLE files ADD COLUMN type INTEGER NOT NULL DEFAULT 0;"); [[fallthrough]]; case 3: m_db.exec("ALTER TABLE entries ADD COLUMN desc TEXT NOT NULL DEFAULT '';"); [[fallthrough]]; case 4: convertImplicitSections(); [[fallthrough]]; case 5: m_db.exec("ALTER TABLE entries RENAME COLUMN pinned TO flags;"); break; } m_db.setVersion(version); m_db.commit(); } } auto Registry::push(const Version *ver, const int flags, std::vector *conflicts) -> Entry { m_db.savepoint(); bool hasConflicts = false; const Package *pkg = ver->package(); const Category *cat = pkg->category(); const Index *ri = cat->index(); m_clearFiles->bind(1, ri->name()); m_clearFiles->bind(2, cat->name()); m_clearFiles->bind(3, pkg->name()); m_clearFiles->exec(); auto entryId = getEntry(ver->package()).id; // register or update package and version if(entryId) { int col = 1; m_updateEntry->bind(col++, pkg->description()); m_updateEntry->bind(col++, pkg->type()); m_updateEntry->bind(col++, ver->name().toString()); m_updateEntry->bind(col++, ver->author()); m_updateEntry->bind(col++, flags); m_updateEntry->bind(col++, entryId); m_updateEntry->exec(); } else { int col = 1; m_insertEntry->bind(col++, ri->name()); m_insertEntry->bind(col++, cat->name()); m_insertEntry->bind(col++, pkg->name()); m_insertEntry->bind(col++, pkg->description()); m_insertEntry->bind(col++, pkg->type()); m_insertEntry->bind(col++, ver->name().toString()); m_insertEntry->bind(col++, ver->author()); m_insertEntry->bind(col++, flags); m_insertEntry->exec(); entryId = m_db.lastInsertId(); } // register files for(const Source *src : ver->sources()) { const Path &path = src->targetPath(); int col = 1; m_insertFile->bind(col++, entryId); m_insertFile->bind(col++, path.join(false)); m_insertFile->bind(col++, src->sections()); m_insertFile->bind(col++, src->typeOverride()); try { m_insertFile->exec(); } catch(const reapack_error &) { if(conflicts && m_db.errorCode() == SQLITE_CONSTRAINT) { hasConflicts = true; conflicts->push_back(path); } else { m_db.restore(); throw; } } } if(hasConflicts) { m_db.restore(); return {}; } else { m_db.release(); return { entryId, ri->name(), cat->name(), pkg->name(), pkg->description(), pkg->type(), ver->name(), ver->author(), flags }; } } void Registry::setFlags(const Entry &entry, const int flags) { m_setFlags->bind(1, flags); m_setFlags->bind(2, entry.id); m_setFlags->exec(); } auto Registry::getEntry(const Package *pkg) const -> Entry { Entry entry{}; const Category *cat = pkg->category(); const Index *ri = cat->index(); m_findEntry->bind(1, ri->name()); m_findEntry->bind(2, cat->name()); m_findEntry->bind(3, pkg->name()); m_findEntry->exec([&] { fillEntry(m_findEntry, &entry); return false; }); return entry; } auto Registry::getEntries(const std::string &remoteName) const -> std::vector { std::vector list; m_allEntries->bind(1, remoteName); m_allEntries->exec([&] { Entry entry{}; fillEntry(m_allEntries, &entry); list.push_back(entry); return true; }); return list; } auto Registry::getFiles(const Entry &entry) const -> std::vector { if(!entry) // skip processing for new packages return {}; std::vector files; m_getFiles->bind(1, entry.id); m_getFiles->exec([&] { int col = 0; File file{ m_getFiles->stringColumn(col++), static_cast(m_getFiles->intColumn(col++)), static_cast(m_getFiles->intColumn(col++)), }; if(!file.type) // < v1.0rc2 file.type = entry.type; files.push_back(file); return true; }); return files; } auto Registry::getMainFiles(const Entry &entry) const -> std::vector { if(!entry) return {}; const std::vector &allFiles = getFiles(entry); std::vector mainFiles; std::copy_if(allFiles.begin(), allFiles.end(), std::back_inserter(mainFiles), [&](const File &f) { return f.sections; }); return mainFiles; } auto Registry::getOwner(const Path &path) const -> Entry { Entry entry{}; m_getOwner->bind(1, path.join(false)); m_getOwner->exec([&] { fillEntry(m_getOwner, &entry); return false; }); return entry; } void Registry::forget(const Entry &entry) { m_forgetFiles->bind(1, entry.id); m_forgetFiles->exec(); m_forgetEntry->bind(1, entry.id); m_forgetEntry->exec(); } void Registry::convertImplicitSections() { // convert from v1.0 main=true format to v1.1 flag format Statement entries("SELECT id, category FROM entries", &m_db); entries.exec([&] { const auto id = entries.intColumn(0); const std::string &category = entries.stringColumn(1); const auto section = Source::detectSection(category); Statement update("UPDATE files SET main = ? WHERE entry = ? AND main != 0", &m_db); update.bind(1, section); update.bind(2, id); update.exec(); return true; }); } void Registry::fillEntry(const Statement *stmt, Entry *entry) const { int col = 0; entry->id = stmt->intColumn(col++); entry->remote = stmt->stringColumn(col++); entry->category = stmt->stringColumn(col++); entry->package = stmt->stringColumn(col++); entry->description = stmt->stringColumn(col++); entry->type = static_cast(stmt->intColumn(col++)); entry->version.tryParse(stmt->stringColumn(col++)); entry->author = stmt->stringColumn(col++); entry->flags = static_cast(stmt->intColumn(col++)); }