ソースを参照

Packages can now be imported over HTTP

default_compile_flags
vector-of-bool 4年前
コミット
36d10d787e
12個のファイルの変更300行の追加17行の削除
  1. バイナリ
      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

バイナリ
data/http-test-1/neo-buffer-0.4.2.tar.gz ファイルの表示


+ 9
- 4
src/dds.main.cpp ファイルの表示

@@ -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 ファイルの表示

@@ -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 ファイルの表示

@@ -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 ファイルの表示

@@ -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 ファイルの表示

@@ -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 ファイルの表示

@@ -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 ファイルの表示

@@ -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 ファイルの表示

@@ -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 ファイルの表示

@@ -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 ファイルの表示

@@ -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 ファイルの表示

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

読み込み中…
キャンセル
保存