feat: support uploading files to projects
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Dominik Meyer 2024-08-06 23:28:16 +02:00
parent 18742e09f6
commit 39a1f8c7e1
Signed by: byterazor
GPG Key ID: EABDA0FD5981BC97
4 changed files with 153 additions and 44 deletions

View File

@ -18,6 +18,8 @@
#include "Redmine/IssueStatus.hpp" #include "Redmine/IssueStatus.hpp"
#include "nlohmann/json_fwd.hpp" #include "nlohmann/json_fwd.hpp"
#include <cstdint>
#include <filesystem>
#include <httplib.h> #include <httplib.h>
#include <string> #include <string>
#include <nlohmann/json.hpp> #include <nlohmann/json.hpp>
@ -53,6 +55,17 @@ namespace Redmine
nlohmann::json get(const std::string &path) const; nlohmann::json get(const std::string &path) const;
void put(const std::string &path, const nlohmann::json &data) 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: public:
@ -116,6 +129,7 @@ namespace Redmine
*/ */
std::vector<Redmine::Project> getProjects() const; std::vector<Redmine::Project> 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;
/*@}*/ /*@}*/
}; };

View File

@ -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("-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)); 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 void Command::Project::getList_() const

View File

@ -64,12 +64,33 @@ namespace Command
/// vector to hold requested attribute fields /// vector to hold requested attribute fields
std::vector<std::string> fields_; std::vector<std::string> 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 /// pointer to the list subcommand
CLI::App* list_; CLI::App* list_;
/// pointer to the upload subcommand
CLI::App* upload_;
/// the actual implementation of the list command /// the actual implementation of the list command
void getList_() const; void getList_() const;
void uploadFile_() const;
public: public:
/** /**
* @brief Construct a new CLIProcessor object * @brief Construct a new CLIProcessor object

View File

@ -20,6 +20,8 @@
#include <Redmine/API.hpp> #include <Redmine/API.hpp>
#include <Redmine/Exception/Api.hpp> #include <Redmine/Exception/Api.hpp>
#include <exception> #include <exception>
#include <filesystem>
#include <sstream>
#include <stdexcept> #include <stdexcept>
#include <vector> #include <vector>
#define LOGURU_WITH_STREAMS 1 #define LOGURU_WITH_STREAMS 1
@ -56,6 +58,44 @@ Redmine::API::API(const std::string &serverURL, const std::string &authToken)
authToken_=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<std::string> errors=error["errors"].get<std::vector<std::string>>();
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 nlohmann::json Redmine::API::get(const std::string &path) const
{ {
DLOG_S(INFO) << "getting API endpoint " << path+".json" << " from " <<redmineApiURL_; DLOG_S(INFO) << "getting API endpoint " << path+".json" << " from " <<redmineApiURL_;
@ -69,32 +109,13 @@ Redmine::API::API(const std::string &serverURL, const std::string &authToken)
auto res = client.Get(path + ".json" , headers); auto res = client.Get(path + ".json" , headers);
if (res == nullptr) processGenericErrors_(res);
{
DLOG_S(ERROR) << "failed to get data from API endpoint";
throw std::runtime_error("API request failed");
}
if (res->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");
}
DLOG_S(INFO) << "get successful"; DLOG_S(INFO) << "get successful";
return nlohmann::json::parse(res->body); return nlohmann::json::parse(res->body);
} }
void Redmine::API::put(const std::string &path, const nlohmann::json &data) const void Redmine::API::put(const std::string &path, const nlohmann::json &data) const
{ {
DLOG_S(INFO) << "putting API endpoint " << path+".json" << " from " <<redmineApiURL_; DLOG_S(INFO) << "putting API endpoint " << path+".json" << " from " <<redmineApiURL_;
@ -103,32 +124,52 @@ void Redmine::API::put(const std::string &path, const nlohmann::json &data) con
auto res = client.Put(path + ".json", data.dump(),"application/json"); auto res = client.Put(path + ".json", data.dump(),"application/json");
if (res->status == httplib::StatusCode::Unauthorized_401) processGenericErrors_(res);
{
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<std::string> errors=error["errors"].get<std::vector<std::string>>();
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");
}
DLOG_S(INFO) << "put successful"; 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 " <<redmineApiURL_;
httplib::Client client{redmineApiURL_};
client.set_basic_auth(authToken_, "");
auto res = client.Post(path + ".json", data.dump(),"application/json");
processGenericErrors_(res);
DLOG_S(INFO) << "post successful";
}
std::string Redmine::API::upload_(const std::filesystem::path &file, const std::string &filename) const
{
httplib::Client client{redmineApiURL_};
client.set_basic_auth(authToken_, "");
if (!std::filesystem::exists(file))
{
LOG_S(ERROR) << "file " << file << " does not exist";
throw std::runtime_error("file does not exist");
}
std::ifstream fileStream{file};
std::stringstream fileBuffer;
fileBuffer << fileStream.rdbuf();
std::string path = "/uploads.json?filename=" + filename;
DLOG_S(INFO) << "uploading file " << file.filename() << " to API endpoint " << path << " at " <<redmineApiURL_;
auto res = client.Post(path, fileBuffer.str(),"application/octet-stream");
processGenericErrors_(res);
nlohmann::json response=nlohmann::json::parse(res->body);
DLOG_S(INFO) << "upload successful";
return response["upload"]["token"];
}
Redmine::User Redmine::API::getMyAccount() const Redmine::User Redmine::API::getMyAccount() const
{ {
@ -186,3 +227,19 @@ bool Redmine::API::ready() const
} }
return true; 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);
}