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

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

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

#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

#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

#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



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

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



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;

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

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



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

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

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



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

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

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

+ 1
- 1
tests/dds.py View File



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,

Loading…
Cancel
Save