From 39a1f8c7e1776cfabbb85d1b8b47dd4fb5d2e743 Mon Sep 17 00:00:00 2001 From: Dominik Meyer Date: Tue, 6 Aug 2024 23:28:16 +0200 Subject: [PATCH] feat: support uploading files to projects --- include/Redmine/API.hpp | 14 +++ src/Redmine-CLI/Command/Project.cpp | 17 ++++ src/Redmine-CLI/Command/Project.hpp | 23 ++++- src/Redmine/API.cpp | 143 +++++++++++++++++++--------- 4 files changed, 153 insertions(+), 44 deletions(-) diff --git a/include/Redmine/API.hpp b/include/Redmine/API.hpp index 1b99d88..e614371 100644 --- a/include/Redmine/API.hpp +++ b/include/Redmine/API.hpp @@ -18,6 +18,8 @@ #include "Redmine/IssueStatus.hpp" #include "nlohmann/json_fwd.hpp" +#include +#include #include #include #include @@ -53,6 +55,17 @@ namespace Redmine nlohmann::json get(const std::string &path) const; void put(const std::string &path, const nlohmann::json &data) const; + void post(const std::string &path, const nlohmann::json &data) const; + void processGenericErrors_(httplib::Result &res) const; + + /** + * @brief upload a file to the redmine server + * + * @param file - the file to upload + * @param filename - the filename used inside redmine + * @return std::string - the redmine internal token (reference) for the uploaded file + */ + std::string upload_(const std::filesystem::path &file, const std::string &filename) const; public: @@ -116,6 +129,7 @@ namespace Redmine */ std::vector getProjects() const; + void uploadFileToProject(const std::uint64_t projectId, const std::string &filePath, const std::string &fileName, const std::string &description, const std::uint32_t version) const; /*@}*/ }; diff --git a/src/Redmine-CLI/Command/Project.cpp b/src/Redmine-CLI/Command/Project.cpp index 1cc6405..3c41298 100644 --- a/src/Redmine-CLI/Command/Project.cpp +++ b/src/Redmine-CLI/Command/Project.cpp @@ -37,6 +37,23 @@ output_(TEXT) list_->add_option("-f,--fields", fields_, "The fields of the project object to print. Supported: name, id"); list_->add_option("-o,--output", output_, "The Output Format to use. Supported: TEXT, YAML")->transform(CLI::CheckedTransformer(outputMap, CLI::ignore_case)); + + upload_ = project->add_subcommand("upload", "Upload a File to the Project"); + upload_->needs(redmine_->getTokenOption())->needs(redmine_->getUrlOption()); + upload_->callback([&](){uploadFile_();}); + upload_->add_option("-p,--project", projectId_, "The ID of the Project to upload the file to")->required()->check(CLI::NonNegativeNumber); + upload_->add_option("-f,--filename", fileName_, "The name of the file to upload")->required(); + upload_->add_option("-d,--description", fileDescription_, "The Description of the file")->required(); + upload_->add_option("-v,--version", fileVersion_,"The ID of the Version the file is related to. Use non existing version id for no version.")->required()->check(CLI::NonNegativeNumber); + upload_->add_option("file", filePath_, "The path to the file to upload")->required()->check(CLI::ExistingFile); + +} + +void Command::Project::uploadFile_() const +{ + Redmine::API client{redmine_->getUrl(), redmine_->getToken()}; + + client.uploadFileToProject(projectId_, filePath_, fileName_, fileDescription_,fileVersion_); } void Command::Project::getList_() const diff --git a/src/Redmine-CLI/Command/Project.hpp b/src/Redmine-CLI/Command/Project.hpp index d5bb85a..4bca1cc 100644 --- a/src/Redmine-CLI/Command/Project.hpp +++ b/src/Redmine-CLI/Command/Project.hpp @@ -63,13 +63,34 @@ namespace Command /// vector to hold requested attribute fields std::vector fields_; - + + /// the project id to work on + std::uint64_t projectId_; + + /// the file path for uploading files + std::string filePath_; + + /// the filename for uploading files + std::string fileName_; + + /// the description for uploading files + std::string fileDescription_; + + /// the version of the uploading files + std::uint32_t fileVersion_; + /// pointer to the list subcommand CLI::App* list_; + /// pointer to the upload subcommand + CLI::App* upload_; + + + /// the actual implementation of the list command void getList_() const; + void uploadFile_() const; public: /** * @brief Construct a new CLIProcessor object diff --git a/src/Redmine/API.cpp b/src/Redmine/API.cpp index f2006cb..25afb3a 100644 --- a/src/Redmine/API.cpp +++ b/src/Redmine/API.cpp @@ -20,6 +20,8 @@ #include #include #include +#include +#include #include #include #define LOGURU_WITH_STREAMS 1 @@ -56,6 +58,44 @@ Redmine::API::API(const std::string &serverURL, const std::string &authToken) authToken_=authToken; } +void Redmine::API::processGenericErrors_(httplib::Result &res) const +{ + if (res == nullptr) + { + LOG_S(FATAL) << "failed to get data from API endpoint"; + throw std::runtime_error("API request failed"); + } + + if (res->status == httplib::StatusCode::Unauthorized_401) + { + LOG_S(ERROR) << "authentication with API server failed (401)"; + throw std::runtime_error("authentication failed"); + } + else if (res->status == httplib::StatusCode::NotFound_404) + { + LOG_S(ERROR) << "unknown API endpoint (404)"; + throw std::runtime_error("unknown API endpoint"); + } + else if (res->status == httplib::StatusCode::UnprocessableContent_422) + { + nlohmann::json error=nlohmann::json::parse(res->body); + std::vector errors=error["errors"].get>(); + std::stringstream strStream; + for (auto e : errors) + { + strStream << e << ","; + } + LOG_S(ERROR) << "Redmine API error unprocessible Content: " << strStream.str(); + throw Redmine::Exception::Api("Unprocessible Content"); + } + else if (res->status != httplib::StatusCode::OK_200 && res->status != httplib::StatusCode::NoContent_204 && res->status != httplib::StatusCode::Created_201) + { + LOG_S(ERROR) << "unsupported error " << httplib::status_message(res->status); + throw std::runtime_error("unsupported error"); + } + +} + nlohmann::json Redmine::API::get(const std::string &path) const { DLOG_S(INFO) << "getting API endpoint " << path+".json" << " from " <status == httplib::StatusCode::Unauthorized_401) - { - DLOG_S(ERROR) << "authentication with API server failed (401)"; - throw std::runtime_error("authentication failed"); - } - else if (res->status == httplib::StatusCode::NotFound_404) - { - DLOG_S(ERROR) << "unknown API endpoint (404)"; - throw std::runtime_error("unknown API endpoint"); - } - else if (res->status != httplib::StatusCode::OK_200) - { - LOG_S(ERROR) << "unsupported error " << httplib::status_message(res->status); - throw std::runtime_error("unsupported error"); - } + processGenericErrors_(res); DLOG_S(INFO) << "get successful"; return nlohmann::json::parse(res->body); } + void Redmine::API::put(const std::string &path, const nlohmann::json &data) const { DLOG_S(INFO) << "putting API endpoint " << path+".json" << " from " <status == httplib::StatusCode::Unauthorized_401) - { - DLOG_S(ERROR) << "authentication with API server failed (401)"; - throw std::runtime_error("authentication failed"); - } - else if (res->status == httplib::StatusCode::NotFound_404) - { - DLOG_S(ERROR) << "unknown API endpoint (404)"; - throw std::runtime_error("unknown API endpoint"); - } - else if (res->status == httplib::StatusCode::UnprocessableContent_422) - { - nlohmann::json error=nlohmann::json::parse(res->body); - std::vector errors=error["errors"].get>(); - throw Redmine::Exception::Api("Unprocessible Content",errors); - } - else if (res->status != httplib::StatusCode::OK_200 && res->status != httplib::StatusCode::NoContent_204) - { - LOG_S(ERROR) << "unsupported error " << httplib::status_message(res->status); - LOG_S(ERROR) << res->body; - throw std::runtime_error("unsupported error"); - } + processGenericErrors_(res); DLOG_S(INFO) << "put successful"; } + void Redmine::API::post(const std::string &path, const nlohmann::json &data) const + { + DLOG_S(INFO) << "posting to API endpoint " << path+".json" << " at " <body); + + DLOG_S(INFO) << "upload successful"; + return response["upload"]["token"]; +} + Redmine::User Redmine::API::getMyAccount() const { @@ -185,4 +226,20 @@ bool Redmine::API::ready() const return false; } return true; +} + +void Redmine::API::uploadFileToProject(const std::uint64_t projectId, const std::string &filePath, const std::string &fileName, const std::string &description, const std::uint32_t version) const +{ + + std::string token = upload_(filePath, fileName); + + nlohmann::json file; + file["token"] = token; + file["filename"] = fileName; + file["description"] = description; + file["version_id"]= version; + nlohmann::json upload; + upload["file"] = file; + + post("/projects/"+std::to_string(projectId)+"/files", upload); } \ No newline at end of file