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 "nlohmann/json_fwd.hpp"
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <httplib.h>
|
||||
#include <string>
|
||||
#include <nlohmann/json.hpp>
|
||||
@ -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<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("-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
|
||||
|
@ -63,13 +63,34 @@ namespace Command
|
||||
|
||||
/// vector to hold requested attribute 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
|
||||
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
|
||||
|
@ -20,6 +20,8 @@
|
||||
#include <Redmine/API.hpp>
|
||||
#include <Redmine/Exception/Api.hpp>
|
||||
#include <exception>
|
||||
#include <filesystem>
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
#include <vector>
|
||||
#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<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
|
||||
{
|
||||
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);
|
||||
|
||||
if (res == nullptr)
|
||||
{
|
||||
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");
|
||||
}
|
||||
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 " <<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");
|
||||
|
||||
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::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");
|
||||
}
|
||||
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 " <<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
|
||||
{
|
||||
@ -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);
|
||||
}
|
Loading…
Reference in New Issue
Block a user