Browse Source

Packages can now be imported over HTTP

default_compile_flags
vector-of-bool 4 years ago
parent
commit
36d10d787e
12 changed files with 300 additions and 17 deletions
  1. BIN
      data/http-test-1/neo-buffer-0.4.2.tar.gz
  2. +9
    -4
      src/dds.main.cpp
  3. +167
    -0
      src/dds/catalog/remote/http.cpp
  4. +23
    -0
      src/dds/catalog/remote/http.hpp
  5. +12
    -0
      src/dds/catalog/remote/http.test.cpp
  6. +9
    -0
      src/dds/http/session.cpp
  7. +9
    -0
      src/dds/http/session.hpp
  8. +1
    -1
      src/dds/solve/solve.cpp
  9. +21
    -11
      src/dds/source/dist.cpp
  10. +1
    -0
      src/dds/source/dist.hpp
  11. +47
    -0
      tests/catalog/import_test.py
  12. +1
    -1
      tests/dds.py

BIN
data/http-test-1/neo-buffer-0.4.2.tar.gz View File


+ 9
- 4
src/dds.main.cpp View File

@@ -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) {

+ 167
- 0
src/dds/catalog/remote/http.cpp View File

@@ -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,
};
}

+ 23
- 0
src/dds/catalog/remote/http.hpp View File

@@ -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

+ 12
- 0
src/dds/catalog/remote/http.test.cpp View File

@@ -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");
}

+ 9
- 0
src/dds/http/session.cpp View File

@@ -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;
}

+ 9
- 0
src/dds/http/session.hpp View File

@@ -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;

+ 1
- 1
src/dds/solve/solve.cpp View File

@@ -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()}}};
}


+ 21
- 11
src/dds/source/dist.cpp View File

@@ -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())};
}

+ 1
- 0
src/dds/source/dist.hpp View File

@@ -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

+ 47
- 0
tests/catalog/import_test.py View File

@@ -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()

+ 1
- 1
tests/dds.py View File

@@ -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,

Loading…
Cancel
Save