@@ -459,8 +459,10 @@ struct cli_repo { | |||
"Import a source distribution archive file into the repository"}; | |||
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, | |||
"replace-if-exists", | |||
@@ -476,8 +478,11 @@ struct cli_repo { | |||
auto import_sdists = [&](dds::repository repo) { | |||
auto if_exists_action | |||
= 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); | |||
} | |||
if (import_stdin) { |
@@ -0,0 +1,167 @@ | |||
#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, | |||
}; | |||
} |
@@ -0,0 +1,23 @@ | |||
#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 |
@@ -0,0 +1,12 @@ | |||
#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"); | |||
} |
@@ -2,6 +2,8 @@ | |||
#include <dds/error/errors.hpp> | |||
#include <dds/util/fs.hpp> | |||
#include <dds/util/log.hpp> | |||
#include <dds/util/result.hpp> | |||
#include <fmt/format.h> | |||
#include <fmt/ostream.h> | |||
@@ -45,6 +47,8 @@ void download_into(Out&& out, In&& in, http_response_info resp) { | |||
} // namespace | |||
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 sock = neo::socket::open_connected(addr, neo::socket::type::stream); | |||
@@ -52,6 +56,8 @@ http_session http_session::connect(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 sock = neo::socket::open_connected(addr, neo::socket::type::stream); | |||
@@ -85,6 +91,8 @@ void http_session::send_head(http_request_params params) { | |||
.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); | |||
std::pair<std::string_view, std::string_view> headers[] = { | |||
@@ -105,6 +113,7 @@ http_response_info http_session::recv_head() { | |||
_state); | |||
auto r | |||
= _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; | |||
return r; | |||
} |
@@ -12,6 +12,15 @@ | |||
namespace dds { | |||
struct e_http_url { | |||
std::string value; | |||
}; | |||
struct e_http_connect { | |||
std::string host; | |||
int port; | |||
}; | |||
struct http_request_params { | |||
std::string_view method; | |||
std::string_view path; |
@@ -92,7 +92,7 @@ struct solver_provider { | |||
dds_log(debug, "No candidate for requirement {}", req.dep.to_string()); | |||
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()}}}; | |||
} | |||
@@ -1,5 +1,6 @@ | |||
#include "./dist.hpp" | |||
#include <dds/catalog/remote/http.hpp> | |||
#include <dds/error/errors.hpp> | |||
#include <dds/library/root.hpp> | |||
#include <dds/temp.hpp> | |||
@@ -122,26 +123,35 @@ sdist dds::create_sdist_in_dir(path_ref out, const sdist_params& params) { | |||
sdist sdist::from_directory(path_ref where) { | |||
auto pkg_man = package_manifest::load_from_directory(where); | |||
// 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}; | |||
} | |||
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(); | |||
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()); | |||
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())}; | |||
} | |||
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(); | |||
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())}; | |||
} |
@@ -51,5 +51,6 @@ void create_sdist_targz(path_ref, const sdist_params&); | |||
temporary_sdist expand_sdist_targz(path_ref targz); | |||
temporary_sdist expand_sdist_from_istream(std::istream&, std::string_view input_name); | |||
temporary_sdist download_expand_sdist_targz(std::string_view); | |||
} // namespace dds |
@@ -1,9 +1,27 @@ | |||
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.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): | |||
dds.scope.enter_context(ensure_dir(dds.build_dir)) | |||
dds.catalog_create() | |||
@@ -27,3 +45,32 @@ def test_import_json(dds: DDS): | |||
dds.set_contents(json_fpath, | |||
json.dumps(import_data).encode())) | |||
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() |
@@ -49,7 +49,7 @@ class DDS: | |||
def run_unchecked(self, cmd: proc.CommandLine, *, | |||
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) | |||
def run(self, cmd: proc.CommandLine, *, cwd: Path = None, |