feat: support uploading files to projects
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
parent
18742e09f6
commit
39a1f8c7e1
@ -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;
|
||||||
/*@}*/
|
/*@}*/
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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);
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user