cli: Ajoute des messages d'erreur plus explicites lorsqu'une requête échoue

This commit is contained in:
2022-11-19 12:35:06 +01:00
parent aecb0403de
commit 648dc3dae3
10 changed files with 210 additions and 56 deletions

View File

@@ -9,18 +9,16 @@ namespace oki {
InstallAction::InstallAction(const char *packageName) : packageName{packageName} {}
void InstallAction::run(Repository &repository) {
std::optional<Package> p = repository.showPackage(packageName);
if (p->getVersions().empty()) {
Package p = repository.getPackageInfo(packageName);
if (p.getVersions().empty()) {
throw APIException{"The packet doesn't have any version"};
} else if (p == std::nullopt) {
throw APIException("This packet doesn't exist");
} else {
Manifest manifest = Manifest::fromFile(OKI_MANIFEST_FILE);
manifest.addDeclaredPackage(packageName, p->getVersions().front().getIdentifier());
manifest.addDeclaredPackage(packageName, p.getVersions().front().getIdentifier());
manifest.saveFile(OKI_MANIFEST_FILE);
fs::create_directories(OKI_PACKAGES_DIRECTORY);
repository.download(p->getVersions().front(), OKI_PACKAGES_DIRECTORY);
repository.download(p.getVersions().front(), OKI_PACKAGES_DIRECTORY);
}
}
}
}

View File

@@ -10,23 +10,19 @@ namespace oki {
void ShowAction::run(Repository &repository) {
bool color = acceptColor();
std::optional<Package> p = repository.showPackage(packageName);
if (p == std::nullopt) {
std::cerr << "This packet doesn't exist\n";
Package p = repository.getPackageInfo(packageName);
if (color) {
std::cout << "\x1B[32m" << p.getShortName() << "\x1B[0m";
} else {
if (color) {
std::cout << "\x1B[32m" << p->getShortName() << "\x1B[0m";
} else {
std::cout << p->getShortName();
}
if (!p->getVersions().empty()) {
const Version &latest = p->getVersions().front();
std::cout << "/" << latest.getIdentifier() << " (" << latest.getPublishedDate() << ")";
}
std::cout << "\n\t" << p->getDescription() << "\n\n";
for (const Version &version : p->getVersions()) {
std::cout << "\t" << version.getIdentifier() << " (" << version.getPublishedDate() << ")\n";
}
std::cout << p.getShortName();
}
if (!p.getVersions().empty()) {
const Version &latest = p.getVersions().front();
std::cout << "/" << latest.getIdentifier() << " (" << latest.getPublishedDate() << ")";
}
std::cout << "\n\t" << p.getDescription() << "\n\n";
for (const Version &version : p.getVersions()) {
std::cout << "\t" << version.getIdentifier() << " (" << version.getPublishedDate() << ")\n";
}
}
}

View File

@@ -1,5 +1,6 @@
#include "HttpRequest.h"
#include <cstring>
#include <curl/curl.h>
namespace oki {
@@ -16,19 +17,66 @@ namespace oki {
return fwrite(in, size, nmemb, out);
}
HttpRequest::HttpRequest(std::string_view url) : curl{curl_easy_init()}, url{url} {
static std::size_t writeContentTypeHeader(char *in, std::size_t size, std::size_t nmemb, std::string *out) {
std::size_t totalSize = size * nmemb;
std::size_t contentTypeLength = sizeof("Content-Type") - 1; // Équivalent à strlen mais à la compilation
if (contentTypeLength >= (totalSize - 4)) {
return totalSize; // Trop court
}
if (strncasecmp(in, "Content-Type", contentTypeLength) == 0) { // Cherche si le début de l'en-tête est 'Content-Type'
const char *value = in + contentTypeLength + 2; // Se positionne au début de la valeur, après ':'
std::size_t length = totalSize - contentTypeLength - 4; // Il reste à la lire la valeur, sans ': ' au début, ni \r et \n à la fin
out->append(value, length);
}
return totalSize;
}
HttpRequest::HttpRequest(std::string_view url) : curl{curl_easy_init()}, headers{nullptr}, url{url} {
curl_easy_setopt(curl, CURLOPT_URL, this->url.c_str());
}
std::string HttpRequest::get() {
HttpRequest::HttpRequest(const HttpRequest &request) : curl{curl_easy_init()}, headers{nullptr}, url{request.url} {
auto *currentHeader = static_cast<struct curl_slist *>(request.headers);
while (currentHeader) {
addHeader(currentHeader->data);
currentHeader = currentHeader->next;
}
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
}
HttpRequest &HttpRequest::operator=(HttpRequest other) {
std::swap(curl, other.curl);
std::swap(headers, other.headers);
std::swap(url, other.url);
return *this;
}
void HttpRequest::addHeader(const std::string &header) {
addHeader(header.c_str());
}
void HttpRequest::addHeader(const char *header) {
// Ajoute un nouveau maillon à la liste chaînée, la chaîne de caractères 'headers' étant dupliquée.
headers = curl_slist_append(static_cast<struct curl_slist *>(headers), header);
}
HttpResponse HttpRequest::get() {
std::string buffer;
std::string contentType;
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeCallback);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &buffer);
curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, writeContentTypeHeader);
curl_easy_setopt(curl, CURLOPT_HEADERDATA, &contentType);
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
CURLcode res = curl_easy_perform(curl);
if (res != CURLE_OK) {
throw RequestException{static_cast<int>(res)};
}
return buffer;
int httpStatus = 0;
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &httpStatus);
return HttpResponse{httpStatus, std::move(contentType), std::move(buffer)};
}
void HttpRequest::download(const std::filesystem::path &path) {
@@ -46,10 +94,30 @@ namespace oki {
}
}
const std::string &HttpRequest::getUrl() const {
return url;
}
HttpRequest::~HttpRequest() {
curl_slist_free_all(static_cast<struct curl_slist *>(headers));
curl_easy_cleanup(curl);
}
HttpResponse::HttpResponse(int statusCode, std::string contentType, std::string content)
: statusCode{statusCode}, contentType{std::move(contentType)}, content{std::move(content)} {}
int HttpResponse::getStatusCode() const {
return statusCode;
}
const std::string &HttpResponse::getContentType() const {
return contentType;
}
const std::string &HttpResponse::getContent() const {
return content;
}
RequestException::RequestException(int code) : code{code} {}
const char *RequestException::what() const noexcept {
@@ -61,4 +129,4 @@ namespace oki {
const char *APIException::what() const noexcept {
return this->msg.c_str();
}
}
}

View File

@@ -4,12 +4,49 @@
#include <string>
namespace oki {
/**
* Décrit la réponse du serveur après une requête HTTP.
*/
class HttpResponse {
private:
int statusCode;
std::string contentType;
std::string content;
public:
HttpResponse(int statusCode, std::string contentType, std::string content);
/**
* Récupère le code de statut HTTP.
*
* @return Le statut de la réponse.
*/
int getStatusCode() const;
/**
* Récupère le type MIME retourné par le serveur.
*
* Si aucune en-tête n'a été trouvée, alors le retour est une chaîne de caractère vide.
*
* @return Le type MIME.
*/
const std::string &getContentType() const;
/**
* Retourne le contenu de la réponse.
*
* @return Le contenu.
*/
const std::string &getContent() const;
};
/**
* Une requête HTTP, préparée par CURL.
*/
class HttpRequest {
private:
void *curl;
void *headers;
std::string url;
public:
@@ -20,12 +57,40 @@ namespace oki {
*/
explicit HttpRequest(std::string_view url);
/**
* Copie les paramètres d'une requête HTTP.
*
* @param request La requête à copier.
*/
HttpRequest(const HttpRequest &request);
/**
* Copie et assigne les paramètres d'une requête HTTP.
*
* @param request La requête à copier.
*/
HttpRequest &operator=(HttpRequest other);
/**
* Ajoute une nouvelle en-tête HTTP, sans vérifier les doublons.
*
* @param header Le contenu de l'en-tête.
*/
void addHeader(const std::string &header);
/**
* Ajoute une nouvelle en-tête HTTP, sans vérifier les doublons.
*
* @param header Le contenu de l'en-tête (terminé par le caractère null '\0').
*/
void addHeader(const char *header);
/**
* Exécute la requête avec une méthode GET et capture le résultat dans une chaîne de caractères.
*
* @return Le contenu de la réponse du serveur.
*/
std::string get();
HttpResponse get();
/**
* Exécute la requête avec une méthode GET et télécharge la réponse dans un fichier.
@@ -34,6 +99,13 @@ namespace oki {
*/
void download(const std::filesystem::path &path);
/**
* Récupère l'url de la requête.
*
* @return L'url complète.
*/
const std::string &getUrl() const;
/**
* Vide la requête.
*/

View File

@@ -1,6 +1,8 @@
#include <iostream>
#include <string>
#include "cli/options.h"
#include "io/HttpRequest.h"
#include "repository/RemoteRepository.h"
namespace fs = std::filesystem;
@@ -10,7 +12,11 @@ using namespace oki;
int main(int argc, char *argv[]) {
CliAction *action = parseArguments(argc, argv);
RemoteRepository repository{"http://localhost:8000"};
action->run(repository);
try {
action->run(repository);
} catch (const oki::APIException &e) {
std::cerr << e.what() << "\n";
}
delete action;
return 0;
}

View File

@@ -1,6 +1,5 @@
#include "LocalRepository.h"
#include "../io/HttpRequest.h"
#include <iostream>
namespace fs = std::filesystem;
@@ -21,16 +20,16 @@ namespace oki {
}
void LocalRepository::download(const Version &packageVersion, const fs::path &destination) {
std::cerr << "TODO : downloading " << packageVersion.getIdentifier() << "\n";
std::cerr << "TODO : downloading " << packageVersion.getIdentifier() << " at " << destination << "\n";
}
std::optional<Package> LocalRepository::showPackage(std::string_view packageName) {
Package LocalRepository::getPackageInfo(std::string_view packageName) {
std::cerr << "TODO : show " << packageName << "\n";
throw APIException{"Not implemented"};
throw std::logic_error{"Not implemented"};
}
std::string LocalRepository::getPackageURL(std::string_view packageName, std::string packageVersion) {
std::cerr << "TODO : " << packageName << "\n";
return "";
std::cerr << "TODO : " << packageName << "/" << packageVersion << "\n";
throw std::logic_error{"Not implemented"};
}
}

View File

@@ -11,7 +11,7 @@ namespace oki {
explicit LocalRepository(std::filesystem::path root);
void createIfNotExists();
std::vector<Package> listPackages() override;
std::optional<Package> showPackage(std::string_view packageName) override;
Package getPackageInfo(std::string_view packageName) override;
std::string getPackageURL(std::string_view packageName, std::string packageVersion) override;
void download(const Version &packageVersion, const std::filesystem::path &destination) override;
};

View File

@@ -8,11 +8,31 @@
using json = nlohmann::json;
namespace oki {
static HttpRequest createRequest(std::string_view url) {
HttpRequest request{url};
request.addHeader("Accept: application/json");
request.addHeader("User-Agent: oki/0.1");
return request;
}
static json tryReadRequest(HttpRequest &request) {
HttpResponse response = request.get();
if (!response.getContentType().starts_with("application/json")) {
throw APIException{"Invalid content type received (" + response.getContentType() + ") from " + request.getUrl()};
}
json data = json::parse(response.getContent());
auto it = data.find("error");
if (response.getStatusCode() >= 400 || it != data.end()) {
throw APIException{(it == data.end() ? "Invalid request" : it->get<std::string>()) + ", tried " + request.getUrl()};
}
return data;
}
RemoteRepository::RemoteRepository(std::string_view apiUrl) : apiUrl{apiUrl} {}
std::vector<Package> RemoteRepository::listPackages() {
HttpRequest request{apiUrl + "/api/list"};
json data = json::parse(request.get());
HttpRequest request = createRequest(apiUrl + "/api/list");
json data = tryReadRequest(request);
std::vector<Package> packages;
for (const auto &item : data.at("packages")) {
packages.emplace_back(item.at("short_name").get<std::string>(), item.at("description").get<std::string>());
@@ -21,35 +41,30 @@ namespace oki {
}
void RemoteRepository::download(const Version &packageVersion, const std::filesystem::path &destination) {
HttpRequest request{apiUrl + packageVersion.getDownloadUrl()};
HttpRequest request = createRequest(apiUrl + packageVersion.getDownloadUrl());
TmpFile tmp;
request.download(tmp.getFilename());
Extractor extractor{destination};
extractor.extract(tmp.getFilename());
}
std::optional<Package> RemoteRepository::showPackage(std::string_view packageName) {
HttpRequest request{apiUrl + "/api/info/" + std::string{packageName}};
json data = json::parse(request.get());
Package RemoteRepository::getPackageInfo(std::string_view packageName) {
HttpRequest request = createRequest(apiUrl + "/api/info/" + std::string{packageName});
json data = tryReadRequest(request);
std::vector<Version> versions;
if (data.contains("error")) {
throw APIException(data.at("error").get<std::string>());
}
if (data.contains("versions")) {
for (const auto &item : data.at("versions")) {
auto it = data.find("versions");
if (it != data.end()) {
for (const auto &item : *it) {
versions.emplace_back(item.at("identifier").get<std::string>(), item.at("published_date").get<std::string>(), item.at("download_url").get<std::string>());
}
}
return Package{data.at("short_name").get<std::string>(), data.at("description").get<std::string>(), versions};
return {data.at("short_name").get<std::string>(), data.at("description").get<std::string>(), versions};
}
std::string RemoteRepository::getPackageURL(std::string_view packageName, std::string packageVersion) {
HttpRequest request{apiUrl + "/api/version" + std::string{packageName} + "?version=" + packageVersion};
json data = json::parse(request.get());
if (data.contains("error")) {
throw APIException(data.at("error").get<std::string>());
} else
return data.get<std::string>();
HttpRequest request = createRequest(apiUrl + "/api/version" + std::string{packageName} + "?version=" + packageVersion);
json data = tryReadRequest(request);
return data.get<std::string>();
}
}

View File

@@ -10,7 +10,7 @@ namespace oki {
public:
explicit RemoteRepository(std::string_view apiUrl);
std::vector<Package> listPackages() override;
std::optional<Package> showPackage(std::string_view packageName) override;
Package getPackageInfo(std::string_view packageName) override;
std::string getPackageURL(std::string_view packageName, std::string packageVersion) override;
void download(const Version &packageVersion, const std::filesystem::path &destination) override;
};

View File

@@ -26,7 +26,7 @@ namespace oki {
* @param packageName Le nom du paquet à utiliser.
* @return Les informations de ce paquet.
*/
virtual std::optional<Package> showPackage(std::string_view packageName) = 0;
virtual Package getPackageInfo(std::string_view packageName) = 0;
virtual std::string getPackageURL(std::string_view packageName, std::string packageVersion) = 0;
virtual void download(const Version &packageVersion, const std::filesystem::path &destination) = 0;
virtual ~Repository() = default;