@@ -3,6 +3,8 @@ | |||
#include <dds/catalog/get.hpp> | |||
#include <dds/dym.hpp> | |||
#include <dds/error/errors.hpp> | |||
#include <dds/http/session.hpp> | |||
#include <dds/remote/remote.hpp> | |||
#include <dds/repo/repo.hpp> | |||
#include <dds/repoman/repoman.hpp> | |||
#include <dds/source/dist.hpp> | |||
@@ -697,6 +699,80 @@ struct cli_repo { | |||
} | |||
} import_{*this}; | |||
struct { | |||
cli_repo& parent; | |||
args::Command cmd{parent.repo_group, "add", "Add a remote repository"}; | |||
common_flags _flags{cmd}; | |||
catalog_path_flag cat_path{cmd}; | |||
args::Positional<std::string> url{cmd, | |||
"<url>", | |||
"URL of a repository to add", | |||
args::Options::Required}; | |||
args::Flag update{cmd, "update", "Update catalog contents immediately", {"update", 'U'}}; | |||
int run() { | |||
return boost::leaf::try_handle_all( // | |||
[&]() -> dds::result<int> { | |||
try { | |||
auto cat = cat_path.open(); | |||
auto repo = dds::remote_repository::connect(url.Get()); | |||
repo.store(cat.database()); | |||
if (update) { | |||
repo.update_catalog(cat.database()); | |||
} | |||
} catch (...) { | |||
return dds::capture_exception(); | |||
} | |||
return 0; | |||
}, | |||
[&](neo::url_validation_error url_err, dds::e_url_string bad_url) { | |||
dds_log(error, "Invalid URL [{}]: {}", bad_url.value, url_err.what()); | |||
return 1; | |||
}, | |||
[&](const json5::parse_error& e, dds::e_http_url bad_url) { | |||
dds_log(error, | |||
"Error parsing JSON downloaded from URL [{}]: {}", | |||
bad_url.value, | |||
e.what()); | |||
return 1; | |||
}, | |||
[](dds::e_sqlite3_error_exc e, dds::e_url_string url) { | |||
dds_log(error, | |||
"Error accessing remote database (From {}): {}", | |||
url.value, | |||
e.message); | |||
return 1; | |||
}, | |||
[](dds::e_sqlite3_error_exc e) { | |||
dds_log(error, "Unexpected database error: {}", e.message); | |||
return 1; | |||
}, | |||
[&](dds::e_system_error_exc e, dds::e_http_connect conn) { | |||
dds_log(error, | |||
"Error opening connection to [{}:{}]: {}", | |||
conn.host, | |||
conn.port, | |||
e.message); | |||
return 1; | |||
}, | |||
[](const std::exception& e) { | |||
dds_log(error, "An unknown unhandled exception occurred: {}", e.what()); | |||
return 1; | |||
}, | |||
[](dds::e_system_error_exc e) { | |||
dds_log(error, "An unknown system_error occurred: {}", e.message); | |||
return 42; | |||
}, | |||
[](boost::leaf::diagnostic_info const& info) { | |||
dds_log(error, "An unnknown error occurred? {}", info); | |||
return 42; | |||
}); | |||
} | |||
} add{*this}; | |||
struct { | |||
cli_repo& parent; | |||
args::Command cmd{parent.repo_group, "init", "Initialize a directory as a repository"}; | |||
@@ -720,6 +796,8 @@ struct cli_repo { | |||
return init.run(); | |||
} else if (import_.cmd) { | |||
return import_.run(); | |||
} else if (add.cmd) { | |||
return add.run(); | |||
} else { | |||
assert(false); | |||
std::terminate(); |
@@ -81,7 +81,7 @@ void migrate_repodb_3(nsql::database& db) { | |||
db.exec(R"( | |||
CREATE TABLE dds_cat_remotes ( | |||
remote_id INTEGER PRIMARY KEY AUTOINCREMENT, | |||
ident TEXT NOT NULL UNIQUE, | |||
name TEXT NOT NULL UNIQUE, | |||
gen_ident TEXT NOT NULL, | |||
remote_url TEXT NOT NULL | |||
); |
@@ -0,0 +1,111 @@ | |||
#include "./remote.hpp" | |||
#include <dds/error/errors.hpp> | |||
#include <dds/http/session.hpp> | |||
#include <dds/temp.hpp> | |||
#include <dds/util/result.hpp> | |||
#include <neo/sqlite3/exec.hpp> | |||
#include <neo/sqlite3/single.hpp> | |||
#include <neo/sqlite3/transaction.hpp> | |||
#include <neo/url.hpp> | |||
#include <neo/utility.hpp> | |||
using namespace dds; | |||
namespace nsql = neo::sqlite3; | |||
namespace { | |||
struct remote_db { | |||
temporary_dir _tempdir; | |||
nsql::database db; | |||
static remote_db download_and_open(neo::url const& url) { | |||
neo_assert(expects, | |||
url.host.has_value(), | |||
"URL does not have a hostname??", | |||
url.to_string()); | |||
auto sess = url.scheme == "https" | |||
? http_session::connect_ssl(*url.host, url.port_or_default_port_or(443)) | |||
: http_session::connect(*url.host, url.port_or_default_port_or(80)); | |||
auto tempdir = temporary_dir::create(); | |||
auto repo_db_dl = tempdir.path() / "repo.db"; | |||
fs::create_directories(tempdir.path()); | |||
sess.download_file( | |||
{ | |||
.method = "GET", | |||
.path = url.path, | |||
}, | |||
repo_db_dl); | |||
auto db = nsql::open(repo_db_dl.string()); | |||
return {tempdir, std::move(db)}; | |||
} | |||
static remote_db download_and_open_for_base(neo::url url) { | |||
auto repo_url = url; | |||
repo_url.path = fs::path(url.path).append("repo.db").string(); | |||
return download_and_open(repo_url); | |||
} | |||
static remote_db download_and_open_for_base(std::string_view url_str) { | |||
return download_and_open_for_base(neo::url::parse(url_str)); | |||
} | |||
}; | |||
} // namespace | |||
remote_repository remote_repository::connect(std::string_view url_str) { | |||
DDS_E_SCOPE(e_url_string{std::string(url_str)}); | |||
const auto url = neo::url::parse(url_str); | |||
auto db = remote_db::download_and_open_for_base(url); | |||
auto name_st = db.db.prepare("SELECT name FROM dds_repo_meta"); | |||
auto [name] = nsql::unpack_single<std::string>(name_st); | |||
remote_repository ret; | |||
ret._base_url = url; | |||
ret._name = name; | |||
return ret; | |||
} | |||
void remote_repository::store(nsql::database_ref db) { | |||
auto st = db.prepare(R"( | |||
INSERT INTO dds_cat_remotes (name, gen_ident, remote_url) | |||
VALUES (?, ?, ?) | |||
)"); | |||
nsql::exec(st, _name, "[placeholder]", _base_url.to_string()); | |||
} | |||
void remote_repository::update_catalog(nsql::database_ref db) { | |||
auto rdb = remote_db::download_and_open_for_base(_base_url); | |||
auto db_path = rdb._tempdir.path() / "repo.db"; | |||
auto rid_st = db.prepare("SELECT remote_id FROM dds_cat_remotes WHERE name = ?"); | |||
rid_st.bindings()[1] = _name; | |||
auto [remote_id] = nsql::unpack_single<std::int64_t>(rid_st); | |||
nsql::transaction_guard tr{db}; | |||
nsql::exec(db.prepare("ATTACH DATABASE ? AS remote"), db_path.string()); | |||
nsql::exec( // | |||
db.prepare(R"( | |||
DELETE FROM dds_cat_pkgs | |||
WHERE remote_id = ? | |||
)"), | |||
remote_id); | |||
nsql::exec( // | |||
db.prepare(R"( | |||
INSERT INTO dds_cat_pkgs | |||
(name, version, description, remote_url, remote_id) | |||
SELECT | |||
name, | |||
version, | |||
description, | |||
printf('dds:%s/%s', name, version), | |||
?1 | |||
FROM remote.dds_repo_packages | |||
)"), | |||
remote_id); | |||
} |
@@ -0,0 +1,30 @@ | |||
#pragma once | |||
#include <dds/util/fs.hpp> | |||
#include <dds/util/result.hpp> | |||
#include <neo/concepts.hpp> | |||
#include <neo/sqlite3/database.hpp> | |||
#include <neo/url.hpp> | |||
#include <string_view> | |||
#include <variant> | |||
namespace dds { | |||
class remote_repository { | |||
std::string _name; | |||
neo::url _base_url; | |||
remote_repository() = default; | |||
public: | |||
static remote_repository connect(std::string_view url); | |||
// const repository_manifest& manifest() const noexcept; | |||
void store(neo::sqlite3::database_ref); | |||
void update_catalog(neo::sqlite3::database_ref); | |||
}; | |||
} // namespace dds |
@@ -30,6 +30,10 @@ struct e_sqlite3_error_exc { | |||
std::error_code code; | |||
}; | |||
struct e_url_string { | |||
std::string value; | |||
}; | |||
/** | |||
* @brief Capture currently in-flight special exceptions as new error object. Works around a bug in | |||
* Boost.LEAF when catching std::system error. |
@@ -67,6 +67,21 @@ def http_import_server(): | |||
httpd.shutdown() | |||
@pytest.yield_fixture | |||
def http_repo_server(): | |||
handler = partial( | |||
DirectoryServingHTTPRequestHandler, | |||
dir=Path.cwd() / 'data/test-repo-1') | |||
addr = ('0.0.0.0', 4646) | |||
pool = ThreadPoolExecutor() | |||
with HTTPServer(addr, handler) as httpd: | |||
pool.submit(lambda: httpd.serve_forever(poll_interval=0.1)) | |||
try: | |||
yield | |||
finally: | |||
httpd.shutdown() | |||
def test_import_http(dds: DDS, http_import_server): | |||
dds.repo_dir.mkdir(parents=True, exist_ok=True) | |||
dds.run( | |||
@@ -74,8 +89,21 @@ def test_import_http(dds: DDS, http_import_server): | |||
'repo', | |||
dds.repo_dir_arg, | |||
'import', | |||
'https://github.com/vector-of-bool/neo-buffer/archive/0.4.2.tar.gz?dds_strpcmp=1', | |||
'http://localhost:8000/neo-buffer-0.4.2.tar.gz', | |||
], | |||
cwd=dds.repo_dir, | |||
) | |||
assert dds.repo_dir.joinpath('neo-buffer@0.4.2').is_dir() | |||
def test_repo_add(dds: DDS, http_repo_server): | |||
dds.repo_dir.mkdir(parents=True, exist_ok=True) | |||
dds.run([ | |||
'repo', | |||
dds.repo_dir_arg, | |||
'add', | |||
dds.catalog_path_arg, | |||
'http://localhost:4646', | |||
'--update', | |||
]) | |||
# dds.build_deps(['neo-url@0.2.1']) |
@@ -69,13 +69,17 @@ class DDS: | |||
def project_dir_arg(self) -> str: | |||
return f'--project-dir={self.source_root}' | |||
@property | |||
def catalog_path_arg(self) -> str: | |||
return f'--catalog={self.catalog_path}' | |||
def build_deps(self, args: proc.CommandLine, *, | |||
toolchain: str = None) -> subprocess.CompletedProcess: | |||
return self.run([ | |||
'build-deps', | |||
f'--toolchain={toolchain or self.default_builtin_toolchain}', | |||
f'--catalog={self.catalog_path}', | |||
f'--repo-dir={self.repo_dir}', | |||
self.catalog_path_arg, | |||
self.repo_dir_arg, | |||
f'--out={self.deps_build_dir}', | |||
f'--lmi-path={self.lmi_path}', | |||
args, |