cli: Ajoute des messages d'erreur plus explicites lorsqu'une requête échoue
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@@ -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";
|
||||
} else {
|
||||
Package p = repository.getPackageInfo(packageName);
|
||||
if (color) {
|
||||
std::cout << "\x1B[32m" << p->getShortName() << "\x1B[0m";
|
||||
std::cout << "\x1B[32m" << p.getShortName() << "\x1B[0m";
|
||||
} else {
|
||||
std::cout << p->getShortName();
|
||||
std::cout << p.getShortName();
|
||||
}
|
||||
if (!p->getVersions().empty()) {
|
||||
const Version &latest = p->getVersions().front();
|
||||
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 << "\n\t" << p.getDescription() << "\n\n";
|
||||
for (const Version &version : p.getVersions()) {
|
||||
std::cout << "\t" << version.getIdentifier() << " (" << version.getPublishedDate() << ")\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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 {
|
||||
|
@@ -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.
|
||||
*/
|
||||
|
@@ -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"};
|
||||
try {
|
||||
action->run(repository);
|
||||
} catch (const oki::APIException &e) {
|
||||
std::cerr << e.what() << "\n";
|
||||
}
|
||||
delete action;
|
||||
return 0;
|
||||
}
|
||||
|
@@ -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"};
|
||||
}
|
||||
}
|
||||
|
@@ -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;
|
||||
};
|
||||
|
@@ -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,34 +41,29 @@ 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
|
||||
HttpRequest request = createRequest(apiUrl + "/api/version" + std::string{packageName} + "?version=" + packageVersion);
|
||||
json data = tryReadRequest(request);
|
||||
return data.get<std::string>();
|
||||
}
|
||||
|
||||
|
@@ -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;
|
||||
};
|
||||
|
@@ -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;
|
||||
|
Reference in New Issue
Block a user