"Import a source distribution archive file into the repository"}; | "Import a source distribution archive file into the repository"}; | ||||
common_flags _common{cmd}; | common_flags _common{cmd}; | ||||
args::PositionalList<dds::fs::path> | |||||
sdist_paths{cmd, "sdist-path", "Path to one or more source distribution archive"}; | |||||
args::PositionalList<std::string> | |||||
sdist_paths{cmd, | |||||
"sdist-path-or-url", | |||||
"Path/URL to one or more source distribution archives"}; | |||||
args::Flag force{cmd, | args::Flag force{cmd, | ||||
"replace-if-exists", | "replace-if-exists", | ||||
auto import_sdists = [&](dds::repository repo) { | auto import_sdists = [&](dds::repository repo) { | ||||
auto if_exists_action | auto if_exists_action | ||||
= force.Get() ? dds::if_exists::replace : dds::if_exists::throw_exc; | = force.Get() ? dds::if_exists::replace : dds::if_exists::throw_exc; | ||||
for (auto& tgz_path : sdist_paths.Get()) { | |||||
auto tmp_sd = dds::expand_sdist_targz(tgz_path); | |||||
for (std::string_view tgz_where : sdist_paths.Get()) { | |||||
auto tmp_sd | |||||
= (tgz_where.starts_with("http://") || tgz_where.starts_with("https://")) | |||||
? dds::download_expand_sdist_targz(tgz_where) | |||||
: dds::expand_sdist_targz(tgz_where); | |||||
repo.add_sdist(tmp_sd.sdist, if_exists_action); | repo.add_sdist(tmp_sd.sdist, if_exists_action); | ||||
} | } | ||||
if (import_stdin) { | if (import_stdin) { |
#include "./http.hpp" | |||||
#include <dds/error/errors.hpp> | |||||
#include <dds/http/session.hpp> | |||||
#include <dds/temp.hpp> | |||||
#include <dds/util/log.hpp> | |||||
#include <neo/tar/util.hpp> | |||||
#include <neo/url.hpp> | |||||
#include <neo/url/query.hpp> | |||||
using namespace dds; | |||||
namespace { | |||||
void http_download_with_redir(neo::url url, path_ref dest) { | |||||
for (auto redir_count = 0;; ++redir_count) { | |||||
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)); | |||||
sess.send_head({.method = "GET", .path = url.path_string()}); | |||||
auto res_head = sess.recv_head(); | |||||
if (res_head.is_error()) { | |||||
dds_log(error, | |||||
"Received an HTTP {} {} for [{}]", | |||||
res_head.status, | |||||
res_head.status_message, | |||||
url.to_string()); | |||||
throw_external_error<errc::http_download_failure>( | |||||
"HTTP error while downloading resource [{}]. Got: HTTP {} '{}'", | |||||
url.to_string(), | |||||
res_head.status, | |||||
res_head.status_message); | |||||
} | |||||
if (res_head.is_redirect()) { | |||||
dds_log(trace, | |||||
"Received HTTP redirect for [{}]: {} {}", | |||||
url.to_string(), | |||||
res_head.status, | |||||
res_head.status_message); | |||||
if (redir_count == 100) { | |||||
throw_external_error<errc::http_download_failure>("Too many redirects on URL"); | |||||
} | |||||
auto loc = res_head.headers.find("Location"); | |||||
if (!loc) { | |||||
throw_external_error<errc::http_download_failure>( | |||||
"HTTP endpoint told us to redirect without sending a 'Location' header " | |||||
"(Received " | |||||
"HTTP {} '{}')", | |||||
res_head.status, | |||||
res_head.status_message); | |||||
} | |||||
dds_log(debug, | |||||
"Redirect [{}]: {} {} to [{}]", | |||||
url.to_string(), | |||||
res_head.status, | |||||
res_head.status_message, | |||||
loc->value); | |||||
auto new_url = neo::url::try_parse(loc->value); | |||||
auto err = std::get_if<neo::url_validation_error>(&new_url); | |||||
if (err) { | |||||
throw_external_error<errc::http_download_failure>( | |||||
"Server returned an invalid URL for HTTP redirection [{}]", loc->value); | |||||
} | |||||
url = std::move(std::get<neo::url>(new_url)); | |||||
continue; | |||||
} | |||||
// Not a redirect nor an error: Download the body | |||||
dds_log(trace, | |||||
"HTTP {} {} [{}]: Saving to [{}]", | |||||
res_head.status, | |||||
res_head.status_message, | |||||
url.to_string(), | |||||
dest.string()); | |||||
sess.recv_body_to_file(res_head, dest); | |||||
break; | |||||
} | |||||
} | |||||
} // namespace | |||||
void http_remote_listing::pull_to(path_ref dest) const { | |||||
neo::url url; | |||||
try { | |||||
url = neo::url::parse(this->url); | |||||
} catch (const neo::url_validation_error& e) { | |||||
throw_user_error<errc::invalid_remote_url>("Failed to parse the string '{}' as a URL: {}", | |||||
this->url, | |||||
e.what()); | |||||
} | |||||
dds_log(trace, "Downloading HTTP remote from [{}]", url.to_string()); | |||||
if (url.scheme != "http" && url.scheme != "https") { | |||||
dds_log(error, "Unsupported URL scheme '{}' (in [{}])", url.scheme, url.to_string()); | |||||
throw_user_error<errc::invalid_remote_url>( | |||||
"The given URL download is not supported. (Only 'http' URLs are supported, " | |||||
"got '{}')", | |||||
this->url); | |||||
} | |||||
neo_assert(invariant, | |||||
!!url.host, | |||||
"The given URL did not have a host part. This shouldn't be possible... Please file " | |||||
"a bug report.", | |||||
this->url); | |||||
auto tdir = dds::temporary_dir::create(); | |||||
auto url_path = fs::path(url.path_string()); | |||||
auto fname = url_path.filename(); | |||||
if (fname.empty()) { | |||||
fname = "dds-download.tmp"; | |||||
} | |||||
auto dl_path = tdir.path() / fname; | |||||
fs::create_directory(dl_path.parent_path()); | |||||
http_download_with_redir(url, dl_path); | |||||
neo_assert(invariant, | |||||
fs::is_regular_file(dl_path), | |||||
"HTTP client did not properly download the file??", | |||||
this->url, | |||||
dl_path); | |||||
fs::create_directories(dest); | |||||
dds_log(debug, "Expanding downloaded source distribution into {}", dest.string()); | |||||
std::ifstream infile{dl_path, std::ios::binary}; | |||||
neo::expand_directory_targz( | |||||
neo::expand_options{ | |||||
.destination_directory = dest, | |||||
.input_name = dl_path.string(), | |||||
.strip_components = this->strip_components, | |||||
}, | |||||
infile); | |||||
} | |||||
http_remote_listing http_remote_listing::from_url(std::string_view sv) { | |||||
auto url = neo::url::parse(sv); | |||||
dds_log(trace, "Create HTTP remote listing from URL [{}]", sv); | |||||
auto q = url.query; | |||||
unsigned strip_components = 0; | |||||
std::optional<lm::usage> auto_lib; | |||||
if (q) { | |||||
neo::basic_query_string_view qsv{*q}; | |||||
for (auto qstr : qsv) { | |||||
if (qstr.key_raw() == "dds_lm") { | |||||
auto_lib = lm::split_usage_string(qstr.value_decoded()); | |||||
} else if (qstr.key_raw() == "dds_strpcmp") { | |||||
strip_components = static_cast<unsigned>(std::stoul(qstr.value_decoded())); | |||||
} else { | |||||
dds_log(warn, "Unknown query string parameter in package url: '{}'", qstr.string()); | |||||
} | |||||
} | |||||
} | |||||
return http_remote_listing{ | |||||
.url = url.to_string(), | |||||
.strip_components = strip_components, | |||||
.auto_lib = auto_lib, | |||||
}; | |||||
} |
#pragma once | |||||
#include <dds/package/id.hpp> | |||||
#include <dds/util/fs.hpp> | |||||
#include <libman/package.hpp> | |||||
#include <string> | |||||
#include <string_view> | |||||
namespace dds { | |||||
struct http_remote_listing { | |||||
std::string url; | |||||
unsigned strip_components = 0; | |||||
std::optional<lm::usage> auto_lib{}; | |||||
void pull_to(path_ref path) const; | |||||
static http_remote_listing from_url(std::string_view sv); | |||||
}; | |||||
} // namespace dds |
#include <dds/catalog/remote/http.hpp> | |||||
#include <dds/error/errors.hpp> | |||||
#include <dds/source/dist.hpp> | |||||
#include <dds/util/log.hpp> | |||||
#include <catch2/catch.hpp> | |||||
TEST_CASE("Convert URL to an HTTP remote listing") { | |||||
auto remote = dds::http_remote_listing::from_url( | |||||
"http://localhost:8000/neo-buffer-0.4.2.tar.gz?dds_strpcmp=1"); | |||||
} |
#include <dds/error/errors.hpp> | #include <dds/error/errors.hpp> | ||||
#include <dds/util/fs.hpp> | #include <dds/util/fs.hpp> | ||||
#include <dds/util/log.hpp> | |||||
#include <dds/util/result.hpp> | |||||
#include <fmt/format.h> | #include <fmt/format.h> | ||||
#include <fmt/ostream.h> | #include <fmt/ostream.h> | ||||
} // namespace | } // namespace | ||||
http_session http_session::connect(const std::string& host, int port) { | http_session http_session::connect(const std::string& host, int port) { | ||||
DDS_E_SCOPE(e_http_connect{host, port}); | |||||
auto addr = neo::address::resolve(host, std::to_string(port)); | auto addr = neo::address::resolve(host, std::to_string(port)); | ||||
auto sock = neo::socket::open_connected(addr, neo::socket::type::stream); | auto sock = neo::socket::open_connected(addr, neo::socket::type::stream); | ||||
} | } | ||||
http_session http_session::connect_ssl(const std::string& host, int port) { | http_session http_session::connect_ssl(const std::string& host, int port) { | ||||
DDS_E_SCOPE(e_http_connect{host, port}); | |||||
auto addr = neo::address::resolve(host, std::to_string(port)); | auto addr = neo::address::resolve(host, std::to_string(port)); | ||||
auto sock = neo::socket::open_connected(addr, neo::socket::type::stream); | auto sock = neo::socket::open_connected(addr, neo::socket::type::stream); | ||||
.parse_tail = neo::const_buffer(), | .parse_tail = neo::const_buffer(), | ||||
}; | }; | ||||
dds_log(trace, "Send: HTTP {} to {}{}", params.method, host_string(), params.path); | |||||
auto cl_str = std::to_string(params.content_length); | auto cl_str = std::to_string(params.content_length); | ||||
std::pair<std::string_view, std::string_view> headers[] = { | std::pair<std::string_view, std::string_view> headers[] = { | ||||
_state); | _state); | ||||
auto r | auto r | ||||
= _do_io([&](auto&& io) { return neo::http::read_response_head<http_response_info>(io); }); | = _do_io([&](auto&& io) { return neo::http::read_response_head<http_response_info>(io); }); | ||||
dds_log(trace, "Recv: HTTP {} {}", r.status, r.status_message); | |||||
_state = _state_t::recvd_head; | _state = _state_t::recvd_head; | ||||
return r; | return r; | ||||
} | } |
namespace dds { | namespace dds { | ||||
struct e_http_url { | |||||
std::string value; | |||||
}; | |||||
struct e_http_connect { | |||||
std::string host; | |||||
int port; | |||||
}; | |||||
struct http_request_params { | struct http_request_params { | ||||
std::string_view method; | std::string_view method; | ||||
std::string_view path; | std::string_view path; |
dds_log(debug, "No candidate for requirement {}", req.dep.to_string()); | dds_log(debug, "No candidate for requirement {}", req.dep.to_string()); | ||||
return std::nullopt; | return std::nullopt; | ||||
} | } | ||||
dds_log(debug, "Select candidate {}@{}", cand->to_string()); | |||||
dds_log(debug, "Select candidate {}", cand->to_string()); | |||||
return req_type{dependency{cand->name, {cand->version, cand->version.next_after()}}}; | return req_type{dependency{cand->name, {cand->version, cand->version.next_after()}}}; | ||||
} | } | ||||
#include "./dist.hpp" | #include "./dist.hpp" | ||||
#include <dds/catalog/remote/http.hpp> | |||||
#include <dds/error/errors.hpp> | #include <dds/error/errors.hpp> | ||||
#include <dds/library/root.hpp> | #include <dds/library/root.hpp> | ||||
#include <dds/temp.hpp> | #include <dds/temp.hpp> | ||||
sdist sdist::from_directory(path_ref where) { | sdist sdist::from_directory(path_ref where) { | ||||
auto pkg_man = package_manifest::load_from_directory(where); | auto pkg_man = package_manifest::load_from_directory(where); | ||||
// Code paths should only call here if they *know* that the sdist is valid | // Code paths should only call here if they *know* that the sdist is valid | ||||
neo_assert(invariant, | |||||
pkg_man.has_value(), | |||||
"All dirs in the repo should be proper source distributions. If you see this, it " | |||||
"means one of the directories in the repository is not a valid sdist.", | |||||
where.string()); | |||||
if (!pkg_man.has_value()) { | |||||
throw_user_error<errc::invalid_pkg_filesystem>( | |||||
"The given directory [{}] does not contain a package manifest file. All source " | |||||
"distribution directories are required to contain a package manifest.", | |||||
where.string()); | |||||
} | |||||
return sdist{pkg_man.value(), where}; | return sdist{pkg_man.value(), where}; | ||||
} | } | ||||
temporary_sdist dds::expand_sdist_targz(path_ref targz_path) { | temporary_sdist dds::expand_sdist_targz(path_ref targz_path) { | ||||
auto infile = open(targz_path, std::ios::binary | std::ios::in); | |||||
return expand_sdist_from_istream(infile, targz_path.string()); | |||||
} | |||||
temporary_sdist dds::expand_sdist_from_istream(std::istream& is, std::string_view input_name) { | |||||
auto tempdir = temporary_dir::create(); | auto tempdir = temporary_dir::create(); | ||||
dds_log(debug, "Expanding source ditsribution content into {}", tempdir.path().string()); | |||||
dds_log(debug, | |||||
"Expanding source distribution content from [{}] into [{}]", | |||||
input_name, | |||||
tempdir.path().string()); | |||||
fs::create_directories(tempdir.path()); | fs::create_directories(tempdir.path()); | ||||
neo::expand_directory_targz(tempdir.path(), targz_path); | |||||
neo::expand_directory_targz({.destination_directory = tempdir.path(), .input_name = input_name}, | |||||
is); | |||||
return {tempdir, sdist::from_directory(tempdir.path())}; | return {tempdir, sdist::from_directory(tempdir.path())}; | ||||
} | } | ||||
temporary_sdist dds::expand_sdist_from_istream(std::istream& is, std::string_view input_name) { | |||||
temporary_sdist dds::download_expand_sdist_targz(std::string_view url_str) { | |||||
auto remote = http_remote_listing::from_url(url_str); | |||||
auto tempdir = temporary_dir::create(); | auto tempdir = temporary_dir::create(); | ||||
dds_log(debug, "Expanding source ditsribution content into {}", tempdir.path().string()); | |||||
fs::create_directories(tempdir.path()); | |||||
neo::expand_directory_targz(tempdir.path(), is, input_name); | |||||
remote.pull_to(tempdir.path()); | |||||
return {tempdir, sdist::from_directory(tempdir.path())}; | return {tempdir, sdist::from_directory(tempdir.path())}; | ||||
} | } |
temporary_sdist expand_sdist_targz(path_ref targz); | temporary_sdist expand_sdist_targz(path_ref targz); | ||||
temporary_sdist expand_sdist_from_istream(std::istream&, std::string_view input_name); | temporary_sdist expand_sdist_from_istream(std::istream&, std::string_view input_name); | ||||
temporary_sdist download_expand_sdist_targz(std::string_view); | |||||
} // namespace dds | } // namespace dds |
import json | import json | ||||
from pathlib import Path | |||||
from functools import partial | |||||
from concurrent.futures import ThreadPoolExecutor | |||||
from http.server import SimpleHTTPRequestHandler, HTTPServer | |||||
import time | |||||
import pytest | |||||
from tests import dds, DDS | from tests import dds, DDS | ||||
from tests.fileutil import ensure_dir | from tests.fileutil import ensure_dir | ||||
class DirectoryServingHTTPRequestHandler(SimpleHTTPRequestHandler): | |||||
def __init__(self, *args, **kwargs) -> None: | |||||
self.dir = kwargs.pop('dir') | |||||
super().__init__(*args, **kwargs) | |||||
def translate_path(self, path) -> str: | |||||
abspath = Path(super().translate_path(path)) | |||||
relpath = abspath.relative_to(Path.cwd()) | |||||
return self.dir / relpath | |||||
def test_import_json(dds: DDS): | def test_import_json(dds: DDS): | ||||
dds.scope.enter_context(ensure_dir(dds.build_dir)) | dds.scope.enter_context(ensure_dir(dds.build_dir)) | ||||
dds.catalog_create() | dds.catalog_create() | ||||
dds.set_contents(json_fpath, | dds.set_contents(json_fpath, | ||||
json.dumps(import_data).encode())) | json.dumps(import_data).encode())) | ||||
dds.catalog_import(json_fpath) | dds.catalog_import(json_fpath) | ||||
@pytest.yield_fixture | |||||
def http_import_server(): | |||||
handler = partial( | |||||
DirectoryServingHTTPRequestHandler, | |||||
dir=Path.cwd() / 'data/http-test-1') | |||||
addr = ('0.0.0.0', 8000) | |||||
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( | |||||
[ | |||||
'repo', | |||||
dds.repo_dir_arg, | |||||
'import', | |||||
'https://github.com/vector-of-bool/neo-buffer/archive/0.4.2.tar.gz?dds_strpcmp=1', | |||||
], | |||||
cwd=dds.repo_dir, | |||||
) | |||||
assert dds.repo_dir.joinpath('neo-buffer@0.4.2').is_dir() |
def run_unchecked(self, cmd: proc.CommandLine, *, | def run_unchecked(self, cmd: proc.CommandLine, *, | ||||
cwd: Path = None) -> subprocess.CompletedProcess: | cwd: Path = None) -> subprocess.CompletedProcess: | ||||
full_cmd = itertools.chain([self.dds_exe], cmd) | |||||
full_cmd = itertools.chain([self.dds_exe, '-ltrace'], cmd) | |||||
return proc.run(full_cmd, cwd=cwd or self.source_root) | return proc.run(full_cmd, cwd=cwd or self.source_root) | ||||
def run(self, cmd: proc.CommandLine, *, cwd: Path = None, | def run(self, cmd: proc.CommandLine, *, cwd: Path = None, |