Working
This commit is contained in:
10
README.md
10
README.md
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## Introduction
|
## Introduction
|
||||||
|
|
||||||
The simple_object_storage template registry is a very simple C++ webserver
|
Simple Object Storage is a very simple C++ webserver
|
||||||
which provide a store of binary objects (the objects can be large),
|
which provide a store of binary objects (the objects can be large),
|
||||||
which are available over http.
|
which are available over http.
|
||||||
|
|
||||||
@@ -21,8 +21,14 @@ Write access is controlled by tokens.
|
|||||||
- `curl http://localhost:8123/meta/squashkiwi:latest`
|
- `curl http://localhost:8123/meta/squashkiwi:latest`
|
||||||
- a simple welcome page is served at `/index.html` for those browsing to the site.
|
- a simple welcome page is served at `/index.html` for those browsing to the site.
|
||||||
- to upload a file (via http put)
|
- to upload a file (via http put)
|
||||||
- `curl -T object_file http://localhost:8123/WRITE_TOKEN/LABEL:TAG?filename="FILENAME"`
|
- `curl -T object_file http://localhost:8123/upload?token="WRITE_TOKEN"\&labeltag="LABEL:TAG"\&filename="FILENAME"`
|
||||||
- the object_file is uploaded, hashed, added to the registry (if that hash doesn't already exist), and {label:tag,hash} is added to the directory index.
|
- the object_file is uploaded, hashed, added to the registry (if that hash doesn't already exist), and {label:tag,hash} is added to the directory index.
|
||||||
|
- to delete a label/tag (object remains):
|
||||||
|
- `curl http://localhost:8123/deletetag?token="WRITE_TOKEN"\&labeltag="LABEL:TAG"`
|
||||||
|
- to delete an object (and all tags on that object):
|
||||||
|
- `curl http://localhost:8123/deleteobject?token="WRITE_TOKEN"\&hash="HASH"`
|
||||||
|
- add a tag to an existing object:
|
||||||
|
- `curl http://localhost:8123/appendtag?token="WRITE_TOKEN"\&hash="HASH"\&labeltag="LABEL:TAG"`
|
||||||
- the server is configured via a configuration file which allows setting:
|
- the server is configured via a configuration file which allows setting:
|
||||||
- the list of write access tokens
|
- the list of write access tokens
|
||||||
- the location for the object store (path on disk)
|
- the location for the object store (path on disk)
|
||||||
|
@@ -166,6 +166,20 @@ bool Database::remove(const std::string& label_tag) {
|
|||||||
return success;
|
return success;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool Database::remove_by_hash(const std::string& hash) {
|
||||||
|
std::string sql = "DELETE FROM objects WHERE hash = ?;";
|
||||||
|
sqlite3_stmt* stmt;
|
||||||
|
|
||||||
|
if (sqlite3_prepare_v2(db_, sql.c_str(), -1, &stmt, nullptr) != SQLITE_OK) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlite3_bind_text(stmt, 1, hash.c_str(), -1, SQLITE_STATIC);
|
||||||
|
bool success = sqlite3_step(stmt) == SQLITE_DONE;
|
||||||
|
sqlite3_finalize(stmt);
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
bool Database::get(const std::string& label_tag, dbEntry& entry) {
|
bool Database::get(const std::string& label_tag, dbEntry& entry) {
|
||||||
std::string sql = "SELECT hash, metadata FROM objects WHERE label_tag = ?;";
|
std::string sql = "SELECT hash, metadata FROM objects WHERE label_tag = ?;";
|
||||||
sqlite3_stmt* stmt;
|
sqlite3_stmt* stmt;
|
||||||
|
@@ -24,6 +24,7 @@ class Database {
|
|||||||
~Database();
|
~Database();
|
||||||
bool insert(const dbEntry& entry);
|
bool insert(const dbEntry& entry);
|
||||||
bool remove(const std::string& label_tag);
|
bool remove(const std::string& label_tag);
|
||||||
|
bool remove_by_hash(const std::string& hash);
|
||||||
bool get(const std::string& label_tag, dbEntry& entry);
|
bool get(const std::string& label_tag, dbEntry& entry);
|
||||||
bool update(const std::string& label_tag, const dbEntry& entry);
|
bool update(const std::string& label_tag, const dbEntry& entry);
|
||||||
bool list(std::vector<dbEntry>& entries);
|
bool list(std::vector<dbEntry>& entries);
|
||||||
|
190
src/server.cpp
190
src/server.cpp
@@ -49,6 +49,32 @@ bool Server::init_db() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool Server::validate_write_request(const httplib::Request &req, httplib::Response &res, const std::vector<std::string> &required_params, std::map<std::string, std::string> ¶ms)
|
||||||
|
{
|
||||||
|
for (const auto& param : req.params) {
|
||||||
|
params[param.first] = param.second;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for required parameters
|
||||||
|
for (const auto& param : required_params) {
|
||||||
|
if (!req.has_param(param)) {
|
||||||
|
res.status = 400;
|
||||||
|
res.set_content("Missing required query parameter: " + param, "text/plain");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// check token is valid
|
||||||
|
bool write_token_valid = std::find(config_.write_tokens.begin(), config_.write_tokens.end(), params["token"]) != config_.write_tokens.end();
|
||||||
|
if (!write_token_valid) {
|
||||||
|
res.status = 403;
|
||||||
|
res.set_content("Invalid write token", "text/plain");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
Server::Server(const ServerConfig& config)
|
Server::Server(const ServerConfig& config)
|
||||||
: config_(config), running_(false) {
|
: config_(config), running_(false) {
|
||||||
// Ensure object store directory exists
|
// Ensure object store directory exists
|
||||||
@@ -121,7 +147,7 @@ void Server::setup_routes() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Upload object
|
// Upload object
|
||||||
server_.Put("/([^/]+)/(.*)", [this](const httplib::Request& req, httplib::Response& res) {
|
server_.Put("/(.*)", [this](const httplib::Request& req, httplib::Response& res) {
|
||||||
handle_put_object(req, res);
|
handle_put_object(req, res);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -129,6 +155,21 @@ void Server::setup_routes() {
|
|||||||
server_.Get("/meta/(.*)", [this](const httplib::Request& req, httplib::Response& res) {
|
server_.Get("/meta/(.*)", [this](const httplib::Request& req, httplib::Response& res) {
|
||||||
handle_get_metadata(req, res);
|
handle_get_metadata(req, res);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Delete a label/tag (object remains)
|
||||||
|
server_.Get("/deletetag", [this](const httplib::Request& req, httplib::Response& res) {
|
||||||
|
handle_delete_tag(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Delete an object (and all tags on that object)
|
||||||
|
server_.Get("/deleteobject", [this](const httplib::Request& req, httplib::Response& res) {
|
||||||
|
handle_delete_object(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add a tag to an existing object
|
||||||
|
server_.Get("/appendtag", [this](const httplib::Request& req, httplib::Response& res) {
|
||||||
|
handle_append_tag(req, res);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void Server::handle_get_object(const httplib::Request& req, httplib::Response& res) {
|
void Server::handle_get_object(const httplib::Request& req, httplib::Response& res) {
|
||||||
@@ -208,22 +249,29 @@ void Server::handle_get_directory(const httplib::Request& /*req*/, httplib::Resp
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Server::handle_put_object(const httplib::Request& req, httplib::Response& res) {
|
void Server::handle_put_object(const httplib::Request& req, httplib::Response& res) {
|
||||||
const auto& token = req.matches[1].str();
|
// Check all request parameters first before processing any data
|
||||||
const auto& label_tag = req.matches[2].str();
|
|
||||||
|
std::map<std::string, std::string> params;
|
||||||
if (!validate_write_token(token)) {
|
if (!validate_write_request(req, res, {"token", "labeltag", "filename"}, params)) {
|
||||||
res.status = 403;
|
|
||||||
res.set_content("Invalid write token", "text/plain");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto [label, tag] = parse_label_tag(label_tag);
|
// 1. Check we're in the /upload path
|
||||||
|
if (req.path != "/upload") {
|
||||||
|
res.status = 404;
|
||||||
|
res.set_content("Not found - put requests must be to /upload", "text/plain");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto [label, tag] = parse_label_tag(params["labeltag"]);
|
||||||
if (label.empty() || tag.empty()) {
|
if (label.empty() || tag.empty()) {
|
||||||
res.status = 400;
|
res.status = 400;
|
||||||
res.set_content("Invalid label:tag format", "text/plain");
|
res.set_content("Invalid label:tag format", "text/plain");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Now that all parameters are validated, process the upload
|
||||||
|
|
||||||
// Generate a random number for the temporary filename
|
// Generate a random number for the temporary filename
|
||||||
std::mt19937_64 rng(std::chrono::high_resolution_clock::now().time_since_epoch().count());
|
std::mt19937_64 rng(std::chrono::high_resolution_clock::now().time_since_epoch().count());
|
||||||
std::uniform_int_distribution<uint64_t> dist;
|
std::uniform_int_distribution<uint64_t> dist;
|
||||||
@@ -258,21 +306,18 @@ void Server::handle_put_object(const httplib::Request& req, httplib::Response& r
|
|||||||
|
|
||||||
// Check for filename query parameter
|
// Check for filename query parameter
|
||||||
std::string filename = "";
|
std::string filename = "";
|
||||||
if (req.has_param("filename")) {
|
if (req.has_param("filename"))
|
||||||
filename = req.get_param_value("filename");
|
metadata["original_filename"] = params["filename"];
|
||||||
metadata["original_filename"] = filename;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if filename ends with ".tgz" using the utility function
|
// Check if filename ends with ".tgz" using the utility function
|
||||||
if (utils::ends_with(filename, ".tgz")) {
|
if (utils::ends_with(params["filename"], ".tgz")) {
|
||||||
metadata["tgz_content_hash"] = get_hash_from_tgz(temp_path.string());
|
metadata["tgz_content_hash"] = get_hash_from_tgz(temp_path.string());
|
||||||
}
|
}
|
||||||
|
|
||||||
add_file_metadata(temp_path.string(), metadata);
|
add_file_metadata(temp_path.string(), metadata);
|
||||||
|
|
||||||
// Move file to final location
|
// Move file to final location
|
||||||
std::string hash_str = std::to_string(hash);
|
std::filesystem::path final_path = config_.object_store_path / std::to_string(hash);
|
||||||
std::filesystem::path final_path = config_.object_store_path / hash_str;
|
|
||||||
if (!std::filesystem::exists(final_path)) {
|
if (!std::filesystem::exists(final_path)) {
|
||||||
try {
|
try {
|
||||||
std::filesystem::rename(temp_path, final_path);
|
std::filesystem::rename(temp_path, final_path);
|
||||||
@@ -287,8 +332,8 @@ void Server::handle_put_object(const httplib::Request& req, httplib::Response& r
|
|||||||
|
|
||||||
// Update database index
|
// Update database index
|
||||||
dbEntry entry;
|
dbEntry entry;
|
||||||
entry.label_tag = label_tag;
|
entry.label_tag = params["labeltag"];
|
||||||
entry.hash = hash_str;
|
entry.hash = std::to_string(hash);
|
||||||
entry.metadata = metadata; // Store the potentially updated metadata
|
entry.metadata = metadata; // Store the potentially updated metadata
|
||||||
|
|
||||||
if (!db_->update_or_insert(entry)) {
|
if (!db_->update_or_insert(entry)) {
|
||||||
@@ -299,7 +344,7 @@ void Server::handle_put_object(const httplib::Request& req, httplib::Response& r
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
res.set_content(hash_str, "text/plain");
|
res.set_content(std::to_string(hash), "text/plain");
|
||||||
}
|
}
|
||||||
|
|
||||||
void Server::handle_get_metadata(const httplib::Request& req, httplib::Response& res) {
|
void Server::handle_get_metadata(const httplib::Request& req, httplib::Response& res) {
|
||||||
@@ -321,10 +366,6 @@ void Server::handle_get_metadata(const httplib::Request& req, httplib::Response&
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Server::validate_write_token(const std::string& token) const {
|
|
||||||
return std::find(config_.write_tokens.begin(), config_.write_tokens.end(), token) != config_.write_tokens.end();
|
|
||||||
}
|
|
||||||
|
|
||||||
std::pair<std::string, std::string> Server::parse_label_tag(const std::string& label_tag) const {
|
std::pair<std::string, std::string> Server::parse_label_tag(const std::string& label_tag) const {
|
||||||
size_t colon_pos = label_tag.find(':');
|
size_t colon_pos = label_tag.find(':');
|
||||||
if (colon_pos == std::string::npos || colon_pos == 0 || colon_pos == label_tag.length() - 1) {
|
if (colon_pos == std::string::npos || colon_pos == 0 || colon_pos == label_tag.length() - 1) {
|
||||||
@@ -347,4 +388,109 @@ void Server::add_file_metadata(const std::string &file_path, nlohmann::json &met
|
|||||||
metadata["file_modification_time"] = std::chrono::system_clock::to_time_t(sctp);
|
metadata["file_modification_time"] = std::chrono::system_clock::to_time_t(sctp);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Server::handle_delete_tag(const httplib::Request& req, httplib::Response& res) {
|
||||||
|
std::map<std::string, std::string> params;
|
||||||
|
if (!validate_write_request(req, res, {"token", "labeltag"}, params)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate label:tag format
|
||||||
|
auto [label, tag] = parse_label_tag(params["labeltag"]);
|
||||||
|
if (label.empty() || tag.empty()) {
|
||||||
|
res.status = 400;
|
||||||
|
res.set_content("Invalid label:tag format", "text/plain");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the label:tag from the database
|
||||||
|
if (!db_->remove(params["labeltag"])) {
|
||||||
|
res.status = 404;
|
||||||
|
res.set_content("Label:tag not found or deletion failed", "text/plain");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.set_content("Label:tag deleted successfully", "text/plain");
|
||||||
|
}
|
||||||
|
|
||||||
|
void Server::handle_delete_object(const httplib::Request& req, httplib::Response& res) {
|
||||||
|
std::map<std::string, std::string> params;
|
||||||
|
if (!validate_write_request(req, res, {"token", "hash"}, params)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate that the hash exists as a file
|
||||||
|
std::filesystem::path file_path = config_.object_store_path / params["hash"];
|
||||||
|
if (!std::filesystem::exists(file_path) || !std::filesystem::is_regular_file(file_path)) {
|
||||||
|
res.status = 404;
|
||||||
|
res.set_content("Object not found for hash: " + params["hash"], "text/plain");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove all tags that reference this hash
|
||||||
|
if (!db_->remove_by_hash(params["hash"])) {
|
||||||
|
res.status = 500;
|
||||||
|
res.set_content("Failed to remove some or all associated tags", "text/plain");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the file
|
||||||
|
try {
|
||||||
|
std::filesystem::remove(file_path);
|
||||||
|
} catch (const std::filesystem::filesystem_error& e) {
|
||||||
|
std::cerr << "Error deleting object file: " << e.what() << std::endl;
|
||||||
|
res.status = 500;
|
||||||
|
res.set_content("Failed to delete object file: " + std::string(e.what()), "text/plain");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.set_content("Object and all associated tags deleted successfully", "text/plain");
|
||||||
|
}
|
||||||
|
|
||||||
|
void Server::handle_append_tag(const httplib::Request& req, httplib::Response& res) {
|
||||||
|
std::map<std::string, std::string> params;
|
||||||
|
if (!validate_write_request(req, res, {"token", "labeltag", "hash"}, params)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate label:tag format
|
||||||
|
auto [label, tag] = parse_label_tag(params["labeltag"]);
|
||||||
|
if (label.empty() || tag.empty()) {
|
||||||
|
res.status = 400;
|
||||||
|
res.set_content("Invalid label:tag format", "text/plain");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate that the hash exists as a file
|
||||||
|
std::filesystem::path file_path = config_.object_store_path / params["hash"];
|
||||||
|
if (!std::filesystem::exists(file_path) || !std::filesystem::is_regular_file(file_path)) {
|
||||||
|
res.status = 404;
|
||||||
|
res.set_content("Object not found for hash: " + params["hash"], "text/plain");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the label:tag already exists
|
||||||
|
dbEntry existing_entry;
|
||||||
|
if (db_->get(params["labeltag"], existing_entry)) {
|
||||||
|
if (existing_entry.hash == params["hash"]) {
|
||||||
|
// Label:tag already points to this hash, nothing to do
|
||||||
|
res.set_content("Label:tag already points to this hash", "text/plain");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new entry with the label:tag pointing to the hash
|
||||||
|
dbEntry entry;
|
||||||
|
entry.label_tag = params["labeltag"];
|
||||||
|
entry.hash = params["hash"];
|
||||||
|
entry.metadata = nlohmann::json({}); // Empty metadata for appended tags
|
||||||
|
|
||||||
|
if (!db_->update_or_insert(entry)) {
|
||||||
|
res.status = 500;
|
||||||
|
res.set_content("Failed to append tag", "text/plain");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.set_content("Tag appended successfully", "text/plain");
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace simple_object_storage
|
} // namespace simple_object_storage
|
@@ -28,12 +28,18 @@ private:
|
|||||||
void handle_get_directory(const httplib::Request& req, httplib::Response& res);
|
void handle_get_directory(const httplib::Request& req, httplib::Response& res);
|
||||||
void handle_put_object(const httplib::Request& req, httplib::Response& res);
|
void handle_put_object(const httplib::Request& req, httplib::Response& res);
|
||||||
void handle_get_metadata(const httplib::Request& req, httplib::Response& res);
|
void handle_get_metadata(const httplib::Request& req, httplib::Response& res);
|
||||||
bool validate_write_token(const std::string& token) const;
|
void handle_delete_tag(const httplib::Request& req, httplib::Response& res);
|
||||||
|
void handle_delete_object(const httplib::Request& req, httplib::Response& res);
|
||||||
|
void handle_append_tag(const httplib::Request& req, httplib::Response& res);
|
||||||
std::pair<std::string, std::string> parse_label_tag(const std::string& label_tag) const;
|
std::pair<std::string, std::string> parse_label_tag(const std::string& label_tag) const;
|
||||||
void add_file_metadata(const std::string &file_path, nlohmann::json &metadata) const;
|
void add_file_metadata(const std::string &file_path, nlohmann::json &metadata) const;
|
||||||
|
|
||||||
bool init_db();
|
bool init_db();
|
||||||
|
|
||||||
|
bool validate_write_request(const httplib::Request& req, httplib::Response& res, const std::vector<std::string>& required_params,
|
||||||
|
std::map<std::string, std::string>& params);
|
||||||
|
|
||||||
|
private:
|
||||||
const ServerConfig& config_;
|
const ServerConfig& config_;
|
||||||
httplib::Server server_;
|
httplib::Server server_;
|
||||||
std::unique_ptr<Database> db_;
|
std::unique_ptr<Database> db_;
|
||||||
|
39
test.sh
Executable file
39
test.sh
Executable file
@@ -0,0 +1,39 @@
|
|||||||
|
#! /bin/bash
|
||||||
|
|
||||||
|
SCRIPT_DIR=$(dirname $0)
|
||||||
|
SCRIPT_NAME=$(basename $0)
|
||||||
|
|
||||||
|
# test jq is installed
|
||||||
|
if ! command -v jq &> /dev/null; then
|
||||||
|
echo "jq could not be found"
|
||||||
|
echo "sudo apt-get install jq"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# read ~/.config/simple_object_storage/config.json
|
||||||
|
CONFIG_PATH="${HOME}/.config/simple_object_storage/config.json"
|
||||||
|
if [ ! -f "${CONFIG_PATH}" ]; then
|
||||||
|
echo "config file not found at ${CONFIG_PATH}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
CONFIG=$(cat "${CONFIG_PATH}")
|
||||||
|
|
||||||
|
# get the host and port from the config
|
||||||
|
HOST=$(echo $CONFIG | jq -r '.host')
|
||||||
|
PORT=$(echo $CONFIG | jq -r '.port')
|
||||||
|
|
||||||
|
# extract the first write token from the config
|
||||||
|
WRITE_TOKEN=$(echo $CONFIG | jq -r '.write_tokens[0]')
|
||||||
|
|
||||||
|
BASE_URL="http://${HOST}:${PORT}"
|
||||||
|
|
||||||
|
BASE_TAG="autotest"
|
||||||
|
|
||||||
|
# test every action in the README.md file, leaving the system in the same state it was found
|
||||||
|
# and print the output of each action
|
||||||
|
|
||||||
|
# upload this script as an object
|
||||||
|
echo "uploading ${SCRIPT_DIR}/${SCRIPT_NAME} to ${BASE_TAG}:test1"
|
||||||
|
OBJECT_HASH=$(curl "${BASE_URL}/upload?token=${WRITE_TOKEN}&labeltag=${BASE_TAG}:test1&filename=${SCRIPT_NAME}" -T ${SCRIPT_DIR}/${SCRIPT_NAME} | jq -r '.hash')
|
||||||
|
echo "received hash ${OBJECT_HASH}"
|
||||||
|
|
Reference in New Issue
Block a user