This commit is contained in:
Your Name
2025-04-30 21:30:55 +12:00
parent 69ecc77d51
commit fe9c940a22
14 changed files with 26367 additions and 2 deletions

62
.gitignore vendored Normal file
View File

@@ -0,0 +1,62 @@
# Build directories
build/
cmake-build-*/
# CMake generated files
CMakeCache.txt
CMakeFiles/
cmake_install.cmake
Makefile
*.cmake
# Compiled Object files
*.slo
*.lo
*.o
*.obj
# Precompiled Headers
*.gch
*.pch
# Compiled Dynamic libraries
*.so
*.dylib
*.dll
# Compiled Static libraries
*.lai
*.la
*.a
*.lib
# Executables
dropshell_template_registry
# IDE specific files
.vscode/
.idea/
*.swp
*.swo
*~
# OS generated files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# Temporary files
*.tmp
*.temp
*.bak
*.backup
# Log files
*.log
# Local configuration
config.json

40
CMakeLists.txt Normal file
View File

@@ -0,0 +1,40 @@
cmake_minimum_required(VERSION 3.10)
project(dropshell_template_registry)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# Find all source files in src directory
file(GLOB_RECURSE SOURCES
"src/*.cpp"
)
# Find all header files in src directory
file(GLOB_RECURSE HEADERS
"src/*.hpp"
)
# Add include directories
include_directories(
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_SOURCE_DIR}/src
)
# Create executable
add_executable(dropshell_template_registry ${SOURCES})
# Link libraries
target_link_libraries(dropshell_template_registry
pthread
)
# Set compile options for static linking
set_target_properties(dropshell_template_registry PROPERTIES
LINK_SEARCH_START_STATIC ON
LINK_SEARCH_END_STATIC ON
)
# Install target
install(TARGETS dropshell_template_registry
RUNTIME DESTINATION bin
)

41
Dockerfile Normal file
View File

@@ -0,0 +1,41 @@
FROM alpine:latest AS builder
# Install build dependencies
RUN apk add --no-cache \
build-base \
cmake \
git \
musl-dev \
nlohmann-json-dev
# Copy source code
COPY . /src
WORKDIR /src
# Build
RUN mkdir build && cd build && \
cmake .. && \
make -j$(nproc)
# Create final image
FROM alpine:latest
# Install runtime dependencies
RUN apk add --no-cache \
libstdc++
# Copy binary from builder
COPY --from=builder /src/build/dropshell_template_registry /usr/local/bin/
# Create data directory
RUN mkdir -p /data
VOLUME /data
# Set working directory
WORKDIR /data
# Expose port
EXPOSE 80
# Run server
CMD ["dropshell_template_registry", "/data/config.json"]

19
build.sh Executable file
View File

@@ -0,0 +1,19 @@
#!/bin/bash
# Exit on error
set -e
# Create build directory if it doesn't exist
mkdir -p build
# Enter build directory
cd build
# Run CMake
cmake ..
# Build the project
make -j$(nproc)
echo "Build completed successfully!"
echo "The executable is located at: $(pwd)/dropshell_template_registry"

8
config.json.example Normal file
View File

@@ -0,0 +1,8 @@
{
"write_tokens": [
"your_write_token_here"
],
"object_store_path": "/data/objects",
"host": "0.0.0.0",
"port": 80
}

47
src/config.cpp Normal file
View File

@@ -0,0 +1,47 @@
#include "config.hpp"
#include <fstream>
#include <sstream>
#include <iostream>
#include <json.hpp>
namespace dropshell {
bool load_config(const std::string& config_path, ServerConfig& config) {
try {
std::ifstream file(config_path);
if (!file.is_open()) {
std::cerr << "Failed to open config file: " << config_path << std::endl;
return false;
}
nlohmann::json j;
file >> j;
// Parse write tokens
if (j.contains("write_tokens")) {
config.write_tokens = j["write_tokens"].get<std::vector<std::string>>();
}
// Parse object store path
if (j.contains("object_store_path")) {
config.object_store_path = j["object_store_path"].get<std::string>();
}
// Parse host (optional)
if (j.contains("host")) {
config.host = j["host"].get<std::string>();
}
// Parse port (optional)
if (j.contains("port")) {
config.port = j["port"].get<uint16_t>();
}
return true;
} catch (const std::exception& e) {
std::cerr << "Error parsing config file: " << e.what() << std::endl;
return false;
}
}
} // namespace dropshell

21
src/config.hpp Normal file
View File

@@ -0,0 +1,21 @@
#ifndef CONFIG_HPP
#define CONFIG_HPP
#include <string>
#include <vector>
#include <filesystem>
namespace dropshell {
struct ServerConfig {
std::vector<std::string> write_tokens;
std::filesystem::path object_store_path;
std::string host = "0.0.0.0";
uint16_t port = 80;
};
bool load_config(const std::string& config_path, ServerConfig& config);
} // namespace dropshell
#endif

View File

@@ -1,7 +1,7 @@
#include "utils/hash.hpp"
#include "hash.hpp"
#define XXH_INLINE_ALL
#include "contrib/xxhash.hpp"
#include "xxhash.hpp"
#include <fstream>
#include <filesystem>

25578
src/json.hpp Normal file

File diff suppressed because it is too large Load Diff

187
src/json_fwd.hpp Normal file
View File

@@ -0,0 +1,187 @@
// __ _____ _____ _____
// __| | __| | | | JSON for Modern C++
// | | |__ | | | | | | version 3.12.0
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
//
// SPDX-FileCopyrightText: 2013 - 2025 Niels Lohmann <https://nlohmann.me>
// SPDX-License-Identifier: MIT
#ifndef INCLUDE_NLOHMANN_JSON_FWD_HPP_
#define INCLUDE_NLOHMANN_JSON_FWD_HPP_
#include <cstdint> // int64_t, uint64_t
#include <map> // map
#include <memory> // allocator
#include <string> // string
#include <vector> // vector
// #include <nlohmann/detail/abi_macros.hpp>
// __ _____ _____ _____
// __| | __| | | | JSON for Modern C++
// | | |__ | | | | | | version 3.12.0
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
//
// SPDX-FileCopyrightText: 2013 - 2025 Niels Lohmann <https://nlohmann.me>
// SPDX-License-Identifier: MIT
// This file contains all macro definitions affecting or depending on the ABI
#ifndef JSON_SKIP_LIBRARY_VERSION_CHECK
#if defined(NLOHMANN_JSON_VERSION_MAJOR) && defined(NLOHMANN_JSON_VERSION_MINOR) && defined(NLOHMANN_JSON_VERSION_PATCH)
#if NLOHMANN_JSON_VERSION_MAJOR != 3 || NLOHMANN_JSON_VERSION_MINOR != 12 || NLOHMANN_JSON_VERSION_PATCH != 0
#warning "Already included a different version of the library!"
#endif
#endif
#endif
#define NLOHMANN_JSON_VERSION_MAJOR 3 // NOLINT(modernize-macro-to-enum)
#define NLOHMANN_JSON_VERSION_MINOR 12 // NOLINT(modernize-macro-to-enum)
#define NLOHMANN_JSON_VERSION_PATCH 0 // NOLINT(modernize-macro-to-enum)
#ifndef JSON_DIAGNOSTICS
#define JSON_DIAGNOSTICS 0
#endif
#ifndef JSON_DIAGNOSTIC_POSITIONS
#define JSON_DIAGNOSTIC_POSITIONS 0
#endif
#ifndef JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON
#define JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON 0
#endif
#if JSON_DIAGNOSTICS
#define NLOHMANN_JSON_ABI_TAG_DIAGNOSTICS _diag
#else
#define NLOHMANN_JSON_ABI_TAG_DIAGNOSTICS
#endif
#if JSON_DIAGNOSTIC_POSITIONS
#define NLOHMANN_JSON_ABI_TAG_DIAGNOSTIC_POSITIONS _dp
#else
#define NLOHMANN_JSON_ABI_TAG_DIAGNOSTIC_POSITIONS
#endif
#if JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON
#define NLOHMANN_JSON_ABI_TAG_LEGACY_DISCARDED_VALUE_COMPARISON _ldvcmp
#else
#define NLOHMANN_JSON_ABI_TAG_LEGACY_DISCARDED_VALUE_COMPARISON
#endif
#ifndef NLOHMANN_JSON_NAMESPACE_NO_VERSION
#define NLOHMANN_JSON_NAMESPACE_NO_VERSION 0
#endif
// Construct the namespace ABI tags component
#define NLOHMANN_JSON_ABI_TAGS_CONCAT_EX(a, b, c) json_abi ## a ## b ## c
#define NLOHMANN_JSON_ABI_TAGS_CONCAT(a, b, c) \
NLOHMANN_JSON_ABI_TAGS_CONCAT_EX(a, b, c)
#define NLOHMANN_JSON_ABI_TAGS \
NLOHMANN_JSON_ABI_TAGS_CONCAT( \
NLOHMANN_JSON_ABI_TAG_DIAGNOSTICS, \
NLOHMANN_JSON_ABI_TAG_LEGACY_DISCARDED_VALUE_COMPARISON, \
NLOHMANN_JSON_ABI_TAG_DIAGNOSTIC_POSITIONS)
// Construct the namespace version component
#define NLOHMANN_JSON_NAMESPACE_VERSION_CONCAT_EX(major, minor, patch) \
_v ## major ## _ ## minor ## _ ## patch
#define NLOHMANN_JSON_NAMESPACE_VERSION_CONCAT(major, minor, patch) \
NLOHMANN_JSON_NAMESPACE_VERSION_CONCAT_EX(major, minor, patch)
#if NLOHMANN_JSON_NAMESPACE_NO_VERSION
#define NLOHMANN_JSON_NAMESPACE_VERSION
#else
#define NLOHMANN_JSON_NAMESPACE_VERSION \
NLOHMANN_JSON_NAMESPACE_VERSION_CONCAT(NLOHMANN_JSON_VERSION_MAJOR, \
NLOHMANN_JSON_VERSION_MINOR, \
NLOHMANN_JSON_VERSION_PATCH)
#endif
// Combine namespace components
#define NLOHMANN_JSON_NAMESPACE_CONCAT_EX(a, b) a ## b
#define NLOHMANN_JSON_NAMESPACE_CONCAT(a, b) \
NLOHMANN_JSON_NAMESPACE_CONCAT_EX(a, b)
#ifndef NLOHMANN_JSON_NAMESPACE
#define NLOHMANN_JSON_NAMESPACE \
nlohmann::NLOHMANN_JSON_NAMESPACE_CONCAT( \
NLOHMANN_JSON_ABI_TAGS, \
NLOHMANN_JSON_NAMESPACE_VERSION)
#endif
#ifndef NLOHMANN_JSON_NAMESPACE_BEGIN
#define NLOHMANN_JSON_NAMESPACE_BEGIN \
namespace nlohmann \
{ \
inline namespace NLOHMANN_JSON_NAMESPACE_CONCAT( \
NLOHMANN_JSON_ABI_TAGS, \
NLOHMANN_JSON_NAMESPACE_VERSION) \
{
#endif
#ifndef NLOHMANN_JSON_NAMESPACE_END
#define NLOHMANN_JSON_NAMESPACE_END \
} /* namespace (inline namespace) NOLINT(readability/namespace) */ \
} // namespace nlohmann
#endif
/*!
@brief namespace for Niels Lohmann
@see https://github.com/nlohmann
@since version 1.0.0
*/
NLOHMANN_JSON_NAMESPACE_BEGIN
/*!
@brief default JSONSerializer template argument
This serializer ignores the template arguments and uses ADL
([argument-dependent lookup](https://en.cppreference.com/w/cpp/language/adl))
for serialization.
*/
template<typename T = void, typename SFINAE = void>
struct adl_serializer;
/// a class to store JSON values
/// @sa https://json.nlohmann.me/api/basic_json/
template<template<typename U, typename V, typename... Args> class ObjectType =
std::map,
template<typename U, typename... Args> class ArrayType = std::vector,
class StringType = std::string, class BooleanType = bool,
class NumberIntegerType = std::int64_t,
class NumberUnsignedType = std::uint64_t,
class NumberFloatType = double,
template<typename U> class AllocatorType = std::allocator,
template<typename T, typename SFINAE = void> class JSONSerializer =
adl_serializer,
class BinaryType = std::vector<std::uint8_t>, // cppcheck-suppress syntaxError
class CustomBaseClass = void>
class basic_json;
/// @brief JSON Pointer defines a string syntax for identifying a specific value within a JSON document
/// @sa https://json.nlohmann.me/api/json_pointer/
template<typename RefStringType>
class json_pointer;
/*!
@brief default specialization
@sa https://json.nlohmann.me/api/json/
*/
using json = basic_json<>;
/// @brief a minimal map-like container that preserves insertion order
/// @sa https://json.nlohmann.me/api/ordered_map/
template<class Key, class T, class IgnoredLess, class Allocator>
struct ordered_map;
/// @brief specialization that maintains the insertion order of object keys
/// @sa https://json.nlohmann.me/api/ordered_json/
using ordered_json = basic_json<nlohmann::ordered_map>;
NLOHMANN_JSON_NAMESPACE_END
#endif // INCLUDE_NLOHMANN_JSON_FWD_HPP_

25
src/main.cpp Normal file
View File

@@ -0,0 +1,25 @@
#include "server.hpp"
#include "config.hpp"
#include <iostream>
#include <string>
int main(int argc, char* argv[]) {
if (argc != 2) {
std::cerr << "Usage: " << argv[0] << " <config_file>" << std::endl;
return 1;
}
dropshell::ServerConfig config;
if (!dropshell::load_config(argv[1], config)) {
std::cerr << "Failed to load configuration" << std::endl;
return 1;
}
dropshell::Server server(config);
if (!server.start()) {
std::cerr << "Failed to start server" << std::endl;
return 1;
}
return 0;
}

290
src/server.cpp Normal file
View File

@@ -0,0 +1,290 @@
#include "server.hpp"
#include "hash.hpp"
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <cstring>
#include <sstream>
#include <fstream>
#include <filesystem>
#include <algorithm>
#include <iostream>
namespace dropshell {
Server::Server(const ServerConfig& config)
: config_(config), server_socket_(-1), running_(false) {
// Create object store directory if it doesn't exist
std::filesystem::create_directories(config_.object_store_path);
}
Server::~Server() {
stop();
}
bool Server::start() {
// Create socket
server_socket_ = socket(AF_INET, SOCK_STREAM, 0);
if (server_socket_ < 0) {
std::cerr << "Failed to create socket" << std::endl;
return false;
}
// Set socket options
int opt = 1;
if (setsockopt(server_socket_, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) < 0) {
std::cerr << "Failed to set socket options" << std::endl;
close(server_socket_);
return false;
}
// Bind socket
struct sockaddr_in address;
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(config_.port);
if (bind(server_socket_, (struct sockaddr*)&address, sizeof(address)) < 0) {
std::cerr << "Failed to bind socket" << std::endl;
close(server_socket_);
return false;
}
// Listen for connections
if (listen(server_socket_, 10) < 0) {
std::cerr << "Failed to listen on socket" << std::endl;
close(server_socket_);
return false;
}
running_ = true;
std::cout << "Server started on port " << config_.port << std::endl;
// Accept connections
while (running_) {
int client_socket = accept(server_socket_, nullptr, nullptr);
if (client_socket < 0) {
if (running_) {
std::cerr << "Failed to accept connection" << std::endl;
}
continue;
}
// Handle connection in a new thread
std::thread([this, client_socket]() {
handle_connection(client_socket);
close(client_socket);
}).detach();
}
return true;
}
void Server::stop() {
running_ = false;
if (server_socket_ >= 0) {
close(server_socket_);
server_socket_ = -1;
}
}
void Server::handle_connection(int client_socket) {
char buffer[4096];
int bytes_read = read(client_socket, buffer, sizeof(buffer) - 1);
if (bytes_read <= 0) {
return;
}
buffer[bytes_read] = '\0';
std::string request(buffer);
std::istringstream iss(request);
std::string method, path, version;
iss >> method >> path >> version;
// Parse path
if (path.empty() || path[0] != '/') {
send_response(client_socket, 400, "text/plain", "Invalid path");
return;
}
if (path == "/index.html") {
send_response(client_socket, 200, "text/html",
"<html><body><h1>Dropshell Template Registry</h1></body></html>");
return;
}
if (path == "/dir") {
handle_get_directory(client_socket);
return;
}
if (path.find("/object/") == 0) {
handle_get_object(client_socket, path.substr(8));
return;
}
if (path.find("/hash/") == 0) {
handle_get_hash(client_socket, path.substr(6));
return;
}
// Handle PUT requests
if (method == "PUT") {
size_t token_pos = path.find('/', 1);
if (token_pos != std::string::npos) {
std::string token = path.substr(1, token_pos - 1);
std::string label_tag = path.substr(token_pos + 1);
handle_put_object(client_socket, token, label_tag);
return;
}
}
send_response(client_socket, 404, "text/plain", "Not found");
}
void Server::handle_get_object(int client_socket, const std::string& path) {
uint64_t hash = 0;
try {
hash = std::stoull(path);
} catch (...) {
// Try to find hash by label:tag
auto it = label_tag_index_.find(path);
if (it == label_tag_index_.end()) {
send_response(client_socket, 404, "text/plain", "Object not found");
return;
}
hash = it->second;
}
auto it = object_index_.find(hash);
if (it == object_index_.end()) {
send_response(client_socket, 404, "text/plain", "Object not found");
return;
}
std::filesystem::path file_path = config_.object_store_path / std::to_string(hash);
if (!std::filesystem::exists(file_path)) {
send_response(client_socket, 404, "text/plain", "Object file not found");
return;
}
send_file(client_socket, file_path);
}
void Server::handle_get_hash(int client_socket, const std::string& path) {
auto it = label_tag_index_.find(path);
if (it == label_tag_index_.end()) {
send_response(client_socket, 404, "text/plain", "Label:tag not found");
return;
}
send_response(client_socket, 200, "text/plain", std::to_string(it->second));
}
void Server::handle_get_directory(int client_socket) {
std::stringstream ss;
for (const auto& [hash, info] : object_index_) {
ss << info.label << ":" << info.tag << "," << hash << "\n";
}
send_response(client_socket, 200, "text/plain", ss.str());
}
void Server::handle_put_object(int client_socket, const std::string& token, const std::string& label_tag) {
if (!validate_write_token(token)) {
send_response(client_socket, 403, "text/plain", "Invalid write token");
return;
}
auto [label, tag] = parse_label_tag(label_tag);
if (label.empty() || tag.empty()) {
send_response(client_socket, 400, "text/plain", "Invalid label:tag format");
return;
}
// Create temporary file
std::filesystem::path temp_path = config_.object_store_path / "temp";
std::ofstream temp_file(temp_path, std::ios::binary);
if (!temp_file) {
send_response(client_socket, 500, "text/plain", "Failed to create temporary file");
return;
}
// Read request body
char buffer[4096];
int bytes_read;
while ((bytes_read = read(client_socket, buffer, sizeof(buffer))) > 0) {
temp_file.write(buffer, bytes_read);
}
temp_file.close();
// Calculate hash
uint64_t hash = hash_file(temp_path.string());
if (hash == 0) {
std::filesystem::remove(temp_path);
send_response(client_socket, 500, "text/plain", "Failed to calculate hash");
return;
}
// Move file to final location
std::filesystem::path final_path = config_.object_store_path / std::to_string(hash);
if (!std::filesystem::exists(final_path)) {
std::filesystem::rename(temp_path, final_path);
} else {
std::filesystem::remove(temp_path);
}
// Update indices
object_index_[hash] = {label, tag, hash};
label_tag_index_[label + ":" + tag] = hash;
send_response(client_socket, 200, "text/plain", std::to_string(hash));
}
void Server::send_response(int client_socket, int status_code, const std::string& content_type, const std::string& body) {
std::stringstream ss;
ss << "HTTP/1.1 " << status_code << " " << (status_code == 200 ? "OK" : "Error") << "\r\n";
ss << "Content-Type: " << content_type << "\r\n";
ss << "Content-Length: " << body.length() << "\r\n";
ss << "Connection: close\r\n\r\n";
ss << body;
std::string response = ss.str();
write(client_socket, response.c_str(), response.length());
}
void Server::send_file(int client_socket, const std::filesystem::path& file_path) {
std::ifstream file(file_path, std::ios::binary);
if (!file) {
send_response(client_socket, 500, "text/plain", "Failed to open file");
return;
}
std::stringstream ss;
ss << "HTTP/1.1 200 OK\r\n";
ss << "Content-Type: application/octet-stream\r\n";
ss << "Connection: close\r\n\r\n";
std::string header = ss.str();
write(client_socket, header.c_str(), header.length());
char buffer[4096];
while (file.read(buffer, sizeof(buffer))) {
write(client_socket, buffer, file.gcount());
}
if (file.gcount() > 0) {
write(client_socket, buffer, file.gcount());
}
}
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 {
size_t colon_pos = label_tag.find(':');
if (colon_pos == std::string::npos) {
return {"", ""};
}
return {label_tag.substr(0, colon_pos), label_tag.substr(colon_pos + 1)};
}
} // namespace dropshell

47
src/server.hpp Normal file
View File

@@ -0,0 +1,47 @@
#ifndef SERVER_HPP
#define SERVER_HPP
#include "config.hpp"
#include <string>
#include <unordered_map>
#include <memory>
#include <thread>
#include <atomic>
namespace dropshell {
class Server {
public:
Server(const ServerConfig& config);
~Server();
bool start();
void stop();
private:
struct ObjectInfo {
std::string label;
std::string tag;
uint64_t hash;
};
void handle_connection(int client_socket);
void handle_get_object(int client_socket, const std::string& path);
void handle_get_hash(int client_socket, const std::string& path);
void handle_get_directory(int client_socket);
void handle_put_object(int client_socket, const std::string& token, const std::string& label_tag);
void send_response(int client_socket, int status_code, const std::string& content_type, const std::string& body);
void send_file(int client_socket, const std::filesystem::path& file_path);
bool validate_write_token(const std::string& token) const;
std::pair<std::string, std::string> parse_label_tag(const std::string& label_tag) const;
const ServerConfig& config_;
int server_socket_;
std::atomic<bool> running_;
std::unordered_map<uint64_t, ObjectInfo> object_index_;
std::unordered_map<std::string, uint64_t> label_tag_index_;
};
} // namespace dropshell
#endif