/* * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ module; #include "config.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include export module katja.job; import katja.utils; import katja.pkgtools; import katja.slackpkg; namespace katja { static std::forward_list> repos; void pk_backend_initialize() { int ret; std::uint8_t i; sqlite3 *db; sqlite3_stmt *stmt; curl_global_init(CURL_GLOBAL_DEFAULT); /* Open the database. We will need it to save the time the configuration file was last modified. */ auto path = std::filesystem::path(LOCALSTATEDIR) / "cache" / "katja" / "metadata.db"; if (sqlite3_open(path.c_str(), &db) != SQLITE_OK) { std::cerr << path.native() << ": " << sqlite3_errmsg(db) << std::endl; } std::chrono::time_point file_info = std::filesystem::last_write_time(path); auto microseconds = std::chrono::duration_cast(file_info.time_since_epoch()); if ((ret = sqlite3_prepare_v2(db, "UPDATE cache_info SET value = ? WHERE key LIKE 'last_modification'", -1, &stmt, nullptr)) == SQLITE_OK) { ret = sqlite3_bind_int(stmt, 1, microseconds.count()); if (ret == SQLITE_OK) { ret = sqlite3_step(stmt); } sqlite3_finalize(stmt); } if ((ret != SQLITE_OK) && (ret != SQLITE_DONE)) { std::cerr << path.native() << ": " << sqlite3_errstr(ret) << std::endl; } else if (!sqlite3_changes(db)) { std::cerr << "Failed to update database: " << path.native() << std::endl; } sqlite3_close_v2(db); } void pk_backend_destroy() { repos.clear(); curl_global_cleanup(); } JobData *pk_backend_start_job() { JobData *job_data = new DummyJobData(); auto db_filename = std::filesystem::path(LOCALSTATEDIR) / "cache" / "katja" / "metadata.db"; if (sqlite3_open(db_filename.native().c_str(), &job_data->db) == SQLITE_OK) { /* Some SQLite settings */ sqlite3_exec(job_data->db, "PRAGMA foreign_keys = ON", nullptr, nullptr, nullptr); } else { std::cerr << db_filename << ": " << sqlite3_errmsg(job_data->db) << std::endl; } return job_data; } void pk_backend_stop_job(JobData *job_data) { if (job_data->curl) { curl_easy_cleanup(job_data->curl); } sqlite3_close(job_data->db); delete job_data; } // for filters -1 means requesting not installed, 1 - installed, 0 - no filters. void pk_backend_search_thread(JobData *job_data, std::uint64_t filters, const std::vector& vals, const char *user_data) { std::string search = boost::algorithm::join(vals, "%"); const char *generate_query = "SELECT (p1.name || ';' || p1.ver || ';' || p1.arch || ';' || r.repo), p1.summary, " "p1.full_name FROM pkglist AS p1 NATURAL JOIN repos AS r " "WHERE p1.%s LIKE '%%%q%%' AND p1.ext NOT LIKE 'obsolete' AND p1.repo_order = " "(SELECT MIN(p2.repo_order) FROM pkglist AS p2 WHERE p2.name = p1.name GROUP BY p2.name)"; char *query = sqlite3_mprintf(generate_query, user_data, search.c_str()); sqlite3_stmt *stmt; if ((sqlite3_prepare_v2(job_data->db, query, -1, &stmt, nullptr) == SQLITE_OK)) { /* Now we're ready to output all packages */ while (sqlite3_step(stmt) == SQLITE_ROW) { katja::Info info = katja::is_installed( reinterpret_cast(sqlite3_column_text(stmt, 2))); if ((info == katja::Info::installed || info == katja::Info::updating) && filters != -1) { job_data->package(katja::Info::installed, reinterpret_cast(sqlite3_column_text(stmt, 0)), reinterpret_cast(sqlite3_column_text(stmt, 1))); } else if (info == katja::Info::installing && filters != 1) { job_data->package(katja::Info::available, reinterpret_cast(sqlite3_column_text(stmt, 0)), reinterpret_cast(sqlite3_column_text(stmt, 1))); } } sqlite3_finalize(stmt); } else { std::cerr << sqlite3_errmsg(job_data->db) << std::endl; } sqlite3_free(query); } void pk_backend_search_files(JobData *job_data, const std::vector& values) { char *query; sqlite3_stmt *stmt; Info ret; std::string search = boost::algorithm::join(values, "%"); query = sqlite3_mprintf("SELECT (p.name || ';' || p.ver || ';' || p.arch || ';' || r.repo), p.summary, " "p.full_name FROM filelist AS f NATURAL JOIN pkglist AS p NATURAL JOIN repos AS r " "WHERE f.filename LIKE '%%%q%%' GROUP BY f.full_name", search.c_str()); if ((sqlite3_prepare_v2(job_data->db, query, -1, &stmt, nullptr) == SQLITE_OK)) { /* Now we're ready to output all packages */ while (sqlite3_step(stmt) == SQLITE_ROW) { ret = is_installed((char*) sqlite3_column_text(stmt, 2)); if ((ret == Info::installed) || (ret == Info::updating)) { job_data->package(katja::Info::installed, (char*) sqlite3_column_text(stmt, 0), (char*) sqlite3_column_text(stmt, 1)); } else if (ret == katja::Info::installing) { job_data->package(katja::Info::available, (char*) sqlite3_column_text(stmt, 0), (char*) sqlite3_column_text(stmt, 1)); } } sqlite3_finalize(stmt); } else { std::cerr << sqlite3_errmsg(job_data->db) << std::endl; } sqlite3_free(query); } void pk_backend_get_details(JobData *job_data, char **package_ids) { std::optional homepage; std::size_t i; std::regex expr; std::smatch match_info; sqlite3_stmt *stmt; std::string desc; std::vector tokens; boost::split(tokens, package_ids[i], boost::is_any_of(";")); if ((sqlite3_prepare_v2(job_data->db, "SELECT p.desc, p.cat, p.uncompressed FROM pkglist AS p NATURAL JOIN repos AS r " "WHERE name LIKE @name AND r.repo LIKE @repo", -1, &stmt, nullptr) != SQLITE_OK)) { std::cerr << sqlite3_errmsg(job_data->db) << std::endl; goto out; } sqlite3_bind_text(stmt, 1, tokens[0].c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_text(stmt, 2, tokens[3].c_str(), -1, SQLITE_TRANSIENT); if (sqlite3_step(stmt) != SQLITE_ROW) { goto out; } desc = reinterpret_cast(sqlite3_column_text(stmt, 0)); /* Regular expression for searching a homepage */ expr = std::regex("(?:http|ftp):\\/\\/[[:word:]\\/\\-\\.]+[[:word:]\\/](?=\\.?$)", std::regex::optimize); if (std::regex_match(std::cbegin(desc), std::cend(desc), match_info, expr)) { homepage = match_info[0]; /* URL */ /* Remove the last sentence with the copied URL */ for (i = desc.size() - 1; i > 0; i--) { if ((desc[i - 1] == '.') && (desc[i] == ' ')) { desc.erase(std::cbegin(desc) + i); break; } } } /* Ready */ job_data->details(package_ids[0], (char *) sqlite3_column_text(stmt, 1), desc.c_str(), homepage.has_value() ? homepage.value().c_str() : nullptr, sqlite3_column_int(stmt, 2)); out: sqlite3_finalize(stmt); } void pk_backend_resolve(JobData *job_data, char **packages) { char **val; sqlite3_stmt *stmt; if ((sqlite3_prepare_v2(job_data->db, "SELECT (p1.name || ';' || p1.ver || ';' || p1.arch || ';' || r.repo), p1.summary, " "p1.full_name FROM pkglist AS p1 NATURAL JOIN repos AS r " "WHERE p1.name LIKE @search AND p1.repo_order = " "(SELECT MIN(p2.repo_order) FROM pkglist AS p2 WHERE p2.name = p1.name GROUP BY p2.name)", -1, &stmt, nullptr) == SQLITE_OK)) { /* Output packages matching each pattern */ for (val = packages; *val; val++) { sqlite3_bind_text(stmt, 1, *val, -1, SQLITE_TRANSIENT); while (sqlite3_step(stmt) == SQLITE_ROW) { job_data->package(katja::Info::available, (char *) sqlite3_column_text(stmt, 0), (char *) sqlite3_column_text(stmt, 1)); } sqlite3_clear_bindings(stmt); sqlite3_reset(stmt); } sqlite3_finalize(stmt); } else { std::cerr << sqlite3_errmsg(job_data->db) << std::endl; } } void pk_backend_download_packages(JobData *job_data, char **package_ids, const char *directory) { unsigned i; sqlite3_stmt *stmt; if ((sqlite3_prepare_v2(job_data->db, "SELECT summary, (full_name || '.' || ext) FROM pkglist NATURAL JOIN repos " "WHERE name LIKE @name AND ver LIKE @ver AND arch LIKE @arch AND repo LIKE @repo", -1, &stmt, nullptr) != SQLITE_OK)) { std::cerr << sqlite3_errmsg(job_data->db) << std::endl; goto out; } for (i = 0; package_ids[i]; ++i) { std::vector tokens; boost::split(tokens, package_ids[i], boost::is_any_of(";")); sqlite3_bind_text(stmt, 1, tokens[0].c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_text(stmt, 2, tokens[1].c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_text(stmt, 3, tokens[2].c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_text(stmt, 4, tokens[3].c_str(), -1, SQLITE_TRANSIENT); if (sqlite3_step(stmt) == SQLITE_ROW) { auto repo = cmp_repo(std::begin(repos), std::end(repos), tokens[3].c_str()); if (repo != std::end(repos)) { (*repo)->download(job_data, directory, tokens[0].c_str()); auto path = std::filesystem::path(directory) / reinterpret_cast(sqlite3_column_text(stmt, 1)); std::vector to_strv{ path }; job_data->files(to_strv); } } sqlite3_clear_bindings(stmt); sqlite3_reset(stmt); } out: sqlite3_finalize(stmt); } void pk_backend_install_packages(JobData *job_data, const std::vector package_ids) { unsigned i; double percent_step; std::list install_list; sqlite3_stmt *pkglist_stmt = nullptr, *collection_stmt = nullptr; if ((sqlite3_prepare_v2(job_data->db, "SELECT summary, cat FROM pkglist NATURAL JOIN repos " "WHERE name LIKE @name AND ver LIKE @ver AND arch LIKE @arch AND repo LIKE @repo", -1, &pkglist_stmt, nullptr) != SQLITE_OK) || (sqlite3_prepare_v2(job_data->db, "SELECT (c.collection_pkg || ';' || p.ver || ';' || p.arch || ';' || r.repo), p.summary, " "p.full_name, p.ext FROM collections AS c " "JOIN pkglist AS p ON c.collection_pkg = p.name " "JOIN repos AS r ON p.repo_order = r.repo_order " "WHERE c.name LIKE @name AND r.repo LIKE @repo", -1, &collection_stmt, nullptr) != SQLITE_OK)) { std::cerr << sqlite3_errmsg(job_data->db) << std::endl; goto out; } for (const std::string& package_id : package_ids) { std::vector tokens; boost::split(tokens, package_id, boost::is_any_of(";")); sqlite3_bind_text(pkglist_stmt, 1, tokens[0].c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_text(pkglist_stmt, 2, tokens[1].c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_text(pkglist_stmt, 3, tokens[2].c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_text(pkglist_stmt, 4, tokens[3].c_str(), -1, SQLITE_TRANSIENT); if (sqlite3_step(pkglist_stmt) == SQLITE_ROW) { /* If it isn't a collection */ if (strcmp(reinterpret_cast(sqlite3_column_text(pkglist_stmt, 1)), "collections")) { install_list.push_back(package_id); } else { sqlite3_bind_text(collection_stmt, 1, tokens[0].c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_text(collection_stmt, 2, tokens[3].c_str(), -1, SQLITE_TRANSIENT); while (sqlite3_step(collection_stmt) == SQLITE_ROW) { katja::Info ret = is_installed((char*) sqlite3_column_text(collection_stmt, 2)); if ((ret == Info::installing) || (ret == Info::updating)) { install_list.push_back( reinterpret_cast(sqlite3_column_text(collection_stmt, 0))); } } sqlite3_clear_bindings(collection_stmt); sqlite3_reset(collection_stmt); } } sqlite3_clear_bindings(pkglist_stmt); sqlite3_reset(pkglist_stmt); } if (!install_list.empty()) { /* / 2 means total percentage for installing and for downloading */ percent_step = 100.0 / install_list.size() / 2; /* Download the packages */ auto dest_dir_name = std::filesystem::path(LOCALSTATEDIR) / "cache" / "katja" / "downloads"; for (const auto& l : install_list) { job_data->set_percentage(percent_step * i); std::vector tokens; boost::split(tokens, l, boost::is_any_of(";")); auto repo = cmp_repo(std::begin(repos), std::end(repos), tokens[3].c_str()); if (repo != std::end(repos)) { (*repo)->download(job_data, dest_dir_name.native().c_str(), tokens[0].c_str()); } } /* Install the packages */ for (const auto& l : install_list) { job_data->set_percentage(percent_step * i); std::vector tokens; boost::split(tokens, l, boost::is_any_of(";")); auto repo = cmp_repo(std::begin(repos), std::end(repos), tokens[3].c_str()); if (repo != std::end(repos)) { (*repo)->install(job_data, tokens[0].c_str()); } } } out: sqlite3_finalize(pkglist_stmt); sqlite3_finalize(collection_stmt); } void pk_backend_remove_packages(JobData* job_data, const std::vector& package_ids) { unsigned i; double percent_step; /* Add percent_step percents per removed package */ percent_step = 100.0 / package_ids.size(); for (i = 0; i < package_ids[i].size(); i++) { job_data->set_percentage(percent_step * i); std::vector tokens; boost::split(tokens, package_ids[i], boost::is_any_of(";")); std::string cmd_line = "/sbin/removepkg " + tokens[0]; /* Pkgtools return always 0 */ system(cmd_line.c_str()); job_data->set_percentage(100); } } void pk_backend_get_updates(JobData *job_data) { std::filesystem::path pkg_metadata_filename; std::filesystem::path pkg_metadata_dir; sqlite3_stmt *stmt; if ((sqlite3_prepare_v2(job_data->db, "SELECT p1.full_name, p1.name, p1.ver, p1.arch, r.repo, p1.summary, p1.ext " "FROM pkglist AS p1 NATURAL JOIN repos AS r " "WHERE p1.name LIKE @name AND p1.repo_order = " "(SELECT MIN(p2.repo_order) FROM pkglist AS p2 WHERE p2.name = p1.name GROUP BY p2.name)", -1, &stmt, nullptr) != SQLITE_OK)) { std::cerr << sqlite3_errmsg(job_data->db) << std::endl; goto out; } /* Read the package metadata directory and comprare all installed packages with ones in the cache */ pkg_metadata_dir = std::filesystem::path("/var/log/packages"); for (const auto& pkg_metadata_file_info : std::filesystem::directory_iterator{ pkg_metadata_dir }) { pkg_metadata_filename = pkg_metadata_file_info.path(); std::vector tokens = split_package_name(pkg_metadata_filename).value(); /* Select the package from the database */ sqlite3_bind_text(stmt, 1, tokens[0].c_str(), -1, SQLITE_TRANSIENT); /* If there are more packages with the same name, remember the one from the * repository with the lowest order. */ if ((sqlite3_step(stmt) == SQLITE_ROW) || cmp_repo(std::begin(repos), std::end(repos), (const char *) sqlite3_column_text(stmt, 4)) != std::end(repos)) { std::string full_name{ reinterpret_cast(sqlite3_column_text(stmt, 0)) }; if (full_name != pkg_metadata_filename) { /* Update available */ std::array id_parts{ 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)) }; std::string pkg_id = boost::algorithm::join(id_parts, ";"); std::string desc{ reinterpret_cast(sqlite3_column_text(stmt, 5)) }; job_data->package(katja::Info::updating, pkg_id.c_str(), desc.c_str()); } } sqlite3_clear_bindings(stmt); sqlite3_reset(stmt); } out: sqlite3_finalize(stmt); } void pk_backend_update_packages(JobData *job_data, char **package_ids) { char *cmd_line; unsigned i; /* Download the packages */ auto dest_dir_name = std::filesystem::path(LOCALSTATEDIR) / "cache" / "katja" / "downloads"; for (i = 0; package_ids[i]; i++) { std::vector tokens; boost::split(tokens, package_ids[i], boost::is_any_of(";")); auto repo = cmp_repo(std::begin(repos), std::end(repos), tokens[3].c_str()); if (repo != std::end(repos)) { (*repo)->download(job_data, dest_dir_name.c_str(), tokens[0].c_str()); } } /* Install the packages */ for (i = 0; package_ids[i]; i++) { std::vector tokens; boost::split(tokens, package_ids[i], boost::is_any_of(";")); auto repo = cmp_repo(std::begin(repos), std::end(repos), tokens[3].c_str()); if (repo != std::end(repos)) { (*repo)->install(job_data, tokens[0].c_str()); } } } void pk_backend_refresh_cache(JobData *job_data, bool force) { char *db_err; int ret; std::forward_list file_list; sqlite3_stmt *stmt = nullptr; /* Create temporary directory */ std::filesystem::path tmp_dir_name = std::filesystem::temp_directory_path() / boost::filesystem::unique_path("katja.%%%%%%").native(); std::filesystem::create_directories(tmp_dir_name); /* Force the complete cache refresh if the read configuration file is newer than the metadata cache */ if (!force) { auto path = std::filesystem::path(LOCALSTATEDIR) / "cache" / "katja" / "metadata.db"; std::chrono::time_point file_info = std::filesystem::last_write_time(path); auto microseconds = std::chrono::duration_cast(file_info.time_since_epoch()); ret = sqlite3_prepare_v2(job_data->db, "SELECT value FROM cache_info WHERE key LIKE 'last_modification'", -1, &stmt, nullptr); if ((ret != SQLITE_OK) || ((ret = sqlite3_step(stmt)) != SQLITE_ROW)) { std::cerr << path << ": " << sqlite3_errstr(ret) << std::endl; goto out; } if ((std::uint32_t) sqlite3_column_int(stmt, 0) > microseconds.count()) { force = true; } } if (force) /* It should empty all tables */ { if (sqlite3_exec(job_data->db, "DELETE FROM repos", nullptr, 0, &db_err) != SQLITE_OK) { std::cerr << db_err << std::endl; sqlite3_free(db_err); goto out; } } // Get list of files that should be downloaded. for (auto& l : repos) { file_list.prepend_range(l->collect_cache_info(tmp_dir_name)); } /* Download repository */ for (auto& l : file_list) { get_file(&job_data->curl, l.first.c_str(), l.second); } /* Refresh cache */ for (auto& l : repos) { l->generate_cache(job_data, tmp_dir_name); } out: sqlite3_finalize(stmt); std::filesystem::remove_all(tmp_dir_name); } }