#include #include #include #include #include // For random number generation #include // For seeding random number generator #include // For getAllKeys #include // For litecask values #include // Include SQLite #include // For std::runtime_error #include "server.hpp" #include "hash.hpp" #include "compress.hpp" #include "string_utils.hpp" // Include the new utility header namespace simple_object_storage { // Simple RAII helper for file deletion class ScopeFileDeleter { public: ScopeFileDeleter(const std::filesystem::path& path) : path_(path), released_(false) {} ~ScopeFileDeleter() { if (!released_) { try { if (std::filesystem::exists(path_)) { std::filesystem::remove(path_); } } catch (const std::filesystem::filesystem_error& e) { std::cerr << "Error deleting temp file: " << path_ << " - " << e.what() << std::endl; } } } void release() { released_ = true; } private: std::filesystem::path path_; bool released_; }; // Helper to execute SQL and check for errors void execute_sql(sqlite3* db, const char* sql, const std::string& error_msg_prefix) { char* err_msg = nullptr; int rc = sqlite3_exec(db, sql, 0, 0, &err_msg); if (rc != SQLITE_OK) { std::string error_details = error_msg_prefix + ": " + (err_msg ? err_msg : "Unknown error"); sqlite3_free(err_msg); throw std::runtime_error(error_details); } } bool Server::init_db() { try { std::filesystem::path db_path = config_.object_store_path / "index.db"; db_ = std::make_unique(db_path); return true; } catch (const std::runtime_error& e) { std::cerr << "Database initialization error: " << e.what() << std::endl; return false; } } Server::Server(const ServerConfig& config) : config_(config), running_(false) { // Ensure object store directory exists try { std::filesystem::create_directories(config_.object_store_path); } catch (const std::filesystem::filesystem_error& e) { std::cerr << "Failed to create object store directory: " << config_.object_store_path << " - " << e.what() << std::endl; return; } // Initialize the database if (!init_db()) { // Error already printed in init_db // Consider throwing or setting an error state } } Server::~Server() { stop(); } bool Server::start() { if (!db_) { // Check if DB initialization failed std::cerr << "Database is not initialized. Cannot start server." << std::endl; return false; } setup_routes(); running_ = true; if (!server_.listen(config_.host.c_str(), config_.port)) { running_ = false; std::cerr << "Failed to listen on " << config_.host << ":" << config_.port << std::endl; return false; } return true; // Should not be reached if listen is blocking and successful } void Server::stop() { if (running_) { server_.stop(); running_ = false; std::cout << "Server stopped." << std::endl; } } void Server::setup_routes() { const std::string welcome_page = "

simple_object_storage Template Registry

"; // Welcome page server_.Get("/", [welcome_page](const httplib::Request&, httplib::Response& res) { res.set_content(welcome_page, "text/html"); }); server_.Get("/index.html", [welcome_page](const httplib::Request&, httplib::Response& res) { res.set_content(welcome_page, "text/html"); }); // Get object by hash or label:tag server_.Get("/object/(.*)", [this](const httplib::Request& req, httplib::Response& res) { handle_get_object(req, res); }); // Get hash for label:tag server_.Get("/hash/(.*)", [this](const httplib::Request& req, httplib::Response& res) { handle_get_hash(req, res); }); // Get directory listing server_.Get("/dir", [this](const httplib::Request& req, httplib::Response& res) { handle_get_directory(req, res); }); // Upload object server_.Put("/([^/]+)/(.*)", [this](const httplib::Request& req, httplib::Response& res) { handle_put_object(req, res); }); // Get metadata for label:tag server_.Get("/meta/(.*)", [this](const httplib::Request& req, httplib::Response& res) { handle_get_metadata(req, res); }); } void Server::handle_get_object(const httplib::Request& req, httplib::Response& res) { const auto& key = req.matches[1].str(); std::string hash_str; // Check if the key looks like a hash (numeric) bool is_hash_lookup = true; for (char c : key) { if (!std::isdigit(c)) { is_hash_lookup = false; break; } } if (!is_hash_lookup) { // Lookup by label:tag in the database dbEntry entry; if (!db_->get(key, entry)) { res.status = 404; res.set_content("Object not found (label:tag)", "text/plain"); return; } hash_str = entry.hash; } else { // Lookup directly by hash hash_str = key; } if (hash_str.empty()) { res.status = 404; res.set_content("Object hash could not be determined", "text/plain"); return; } // Construct the file path using the hash string std::filesystem::path file_path = config_.object_store_path / hash_str; if (!std::filesystem::exists(file_path) || !std::filesystem::is_regular_file(file_path)) { res.status = 404; res.set_content("Object file not found for hash: " + hash_str, "text/plain"); return; } // Send file using Response::set_file_content std::string content_type = "application/octet-stream"; // Basic default res.set_file_content(file_path.string(), content_type); } void Server::handle_get_hash(const httplib::Request& req, httplib::Response& res) { const auto& label_tag = req.matches[1].str(); dbEntry entry; if (!db_->get(label_tag, entry)) { res.status = 404; res.set_content("Label:tag not found", "text/plain"); return; } res.set_content(entry.hash, "text/plain"); } void Server::handle_get_directory(const httplib::Request& /*req*/, httplib::Response& res) { std::stringstream ss; std::vector entries; if (!db_->list(entries)) { res.status = 500; res.set_content("Database error retrieving directory", "text/plain"); return; } for (const auto& entry : entries) { ss << entry.label_tag << "," << entry.hash << "\n"; } res.set_content(ss.str(), "text/plain"); } void Server::handle_put_object(const httplib::Request& req, httplib::Response& res) { const auto& token = req.matches[1].str(); const auto& label_tag = req.matches[2].str(); if (!validate_write_token(token)) { res.status = 403; res.set_content("Invalid write token", "text/plain"); return; } auto [label, tag] = parse_label_tag(label_tag); if (label.empty() || tag.empty()) { res.status = 400; res.set_content("Invalid label:tag format", "text/plain"); return; } // Generate a random number for the temporary filename std::mt19937_64 rng(std::chrono::high_resolution_clock::now().time_since_epoch().count()); std::uniform_int_distribution dist; uint64_t random_num = dist(rng); std::string temp_filename = "temp_" + std::to_string(random_num); // Create temporary file std::filesystem::path temp_path = config_.object_store_path / temp_filename; std::ofstream temp_file(temp_path, std::ios::binary); if (!temp_file.is_open()) { res.status = 500; res.set_content("Failed to create temporary file", "text/plain"); return; } // Write request body to temporary file temp_file.write(req.body.data(), req.body.size()); temp_file.close(); // Ensure the temporary file is removed even if errors occur ScopeFileDeleter temp_file_deleter(temp_path); // Calculate hash uint64_t hash = hash_file(temp_path.string()); if (hash == 0) { res.status = 500; res.set_content("Failed to calculate hash", "text/plain"); return; } nlohmann::json metadata; // Check for filename query parameter std::string filename = ""; if (req.has_param("filename")) { filename = req.get_param_value("filename"); metadata["original_filename"] = filename; } // Check if filename ends with ".tgz" using the utility function if (utils::ends_with(filename, ".tgz")) { metadata["tgz_content_hash"] = get_hash_from_tgz(temp_path.string()); } add_file_metadata(temp_path.string(), metadata); // Move file to final location std::string hash_str = std::to_string(hash); std::filesystem::path final_path = config_.object_store_path / hash_str; if (!std::filesystem::exists(final_path)) { try { std::filesystem::rename(temp_path, final_path); temp_file_deleter.release(); } catch (const std::filesystem::filesystem_error& e) { std::cerr << "Error renaming temp file: " << e.what() << std::endl; res.status = 500; res.set_content("Failed to store object file", "text/plain"); return; } } // Update database index dbEntry entry; entry.label_tag = label_tag; entry.hash = hash_str; entry.metadata = metadata; // Store the potentially updated metadata if (!db_->update_or_insert(entry)) { res.status = 500; res.set_content("Failed to update database index", "text/plain"); // Attempt to clean up the moved file if index fails try { if (std::filesystem::exists(final_path)) std::filesystem::remove(final_path); } catch(...) {}; return; } res.set_content(hash_str, "text/plain"); } void Server::handle_get_metadata(const httplib::Request& req, httplib::Response& res) { const auto& label_tag = req.matches[1].str(); dbEntry entry; if (!db_->get(label_tag, entry)) { res.status = 404; res.set_content("Metadata not found for label:tag: " + label_tag, "text/plain"); return; } try { res.set_content(entry.metadata.dump(), "application/json"); } catch (const nlohmann::json::exception& e) { std::cerr << "Error serializing metadata for " << label_tag << ": " << e.what() << std::endl; res.status = 500; res.set_content("Internal server error: Failed to serialize metadata", "text/plain"); } } 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 Server::parse_label_tag(const std::string& label_tag) const { size_t colon_pos = label_tag.find(':'); if (colon_pos == std::string::npos || colon_pos == 0 || colon_pos == label_tag.length() - 1) { return {"", ""}; } return {label_tag.substr(0, colon_pos), label_tag.substr(colon_pos + 1)}; } void Server::add_file_metadata(const std::string &file_path, nlohmann::json &metadata) const { // get the file size metadata["file_size"] = std::filesystem::file_size(file_path); // get the file modification time auto ftime = std::filesystem::last_write_time(file_path); auto sctp = std::chrono::time_point_cast( ftime - std::filesystem::file_time_type::clock::now() + std::chrono::system_clock::now() ); metadata["file_modification_time"] = std::chrono::system_clock::to_time_t(sctp); } } // namespace simple_object_storage