/
/
/
1#include "bag_recorder_backend/crow_server.hpp"
2
3#include <chrono>
4#include <cstdlib>
5#include <filesystem>
6#include <fstream>
7#include <nlohmann/json.hpp>
8#include <sstream>
9
10namespace bag_recorder_backend {
11
12CrowServer::CrowServer(std::shared_ptr<BagRecorderNode> recorder_node, int port)
13 : recorder_node_(recorder_node), server_running_(false), should_stop_(false), port_(port), cors_enabled_(true) {
14 setup_routes();
15}
16
17CrowServer::~CrowServer() { stop(); }
18
19void CrowServer::start() {
20 if (server_running_) {
21 return;
22 }
23
24 server_running_ = true;
25 should_stop_ = false;
26
27 // Configure and start server
28 app_.port(port_);
29 app_.multithreaded();
30 app_.run();
31}
32
33void CrowServer::stop() {
34 if (!server_running_) {
35 return;
36 }
37
38 should_stop_ = true;
39 server_running_ = false;
40 app_.stop();
41}
42
43bool CrowServer::is_running() const { return server_running_; }
44
45void CrowServer::set_port(int port) { port_ = port; }
46
47void CrowServer::enable_cors(bool enable) { cors_enabled_ = enable; }
48
49void CrowServer::setup_routes() {
50 // API Routes
51 CROW_ROUTE(app_, "/api/status").methods("GET"_method)([this]() { return handle_get_status(); });
52
53 CROW_ROUTE(app_, "/api/topics").methods("GET"_method)([this]() { return handle_get_topics(); });
54
55 CROW_ROUTE(app_, "/api/recording/start").methods("POST"_method)([this](const crow::request &req) { return handle_start_recording(req); });
56
57 CROW_ROUTE(app_, "/api/recording/stop").methods("POST"_method)([this]() { return handle_stop_recording(); });
58
59 CROW_ROUTE(app_, "/api/config").methods("GET"_method)([this]() { return handle_get_config(); });
60
61 CROW_ROUTE(app_, "/api/config").methods("POST"_method)([this](const crow::request &req) { return handle_set_config(req); });
62
63 CROW_ROUTE(app_, "/api/recordings").methods("GET"_method)([this]() { return handle_get_recordings(); });
64
65 CROW_ROUTE(app_, "/api/recordings/<string>").methods("DELETE"_method)([this](const std::string &filename) { return handle_delete_recording(filename); });
66
67 CROW_ROUTE(app_, "/api/recordings/<string>/download").methods("GET"_method)([this](const std::string &filename) { return handle_download_recording(filename); });
68
69 CROW_ROUTE(app_, "/api/recordings/download").methods("POST"_method)([this](const crow::request &req) { return handle_download_multiple_recordings(req); });
70
71 CROW_ROUTE(app_, "/api/recordings/delete").methods("POST"_method)([this](const crow::request &req) { return handle_delete_multiple_recordings(req); });
72}
73
74crow::response CrowServer::handle_get_status() {
75 auto status = recorder_node_->get_status();
76 auto json_response = recording_status_to_json(status);
77
78 crow::response res(200, json_response.dump());
79 res.set_header("Content-Type", "application/json");
80 return add_cors_headers(std::move(res));
81}
82
83crow::response CrowServer::handle_get_topics() {
84 auto topics = recorder_node_->get_available_topics();
85 nlohmann::json json_response;
86 json_response["topics"] = topics;
87
88 crow::response res(200, json_response.dump());
89 res.set_header("Content-Type", "application/json");
90 return add_cors_headers(std::move(res));
91}
92
93crow::response CrowServer::handle_start_recording(const crow::request &req) {
94 try {
95 auto json_data = nlohmann::json::parse(req.body);
96 auto config = json_to_recording_config(json_data);
97
98 bool success = recorder_node_->start_recording(config);
99
100 nlohmann::json response;
101 response["success"] = success;
102 response["message"] = success ? "Recording started" : "Failed to start recording";
103
104 crow::response res(success ? 200 : 400, response.dump());
105 res.set_header("Content-Type", "application/json");
106 return add_cors_headers(std::move(res));
107
108 } catch (const std::exception &e) {
109 nlohmann::json response;
110 response["success"] = false;
111 response["message"] = std::string("Error: ") + e.what();
112
113 crow::response res(400, response.dump());
114 res.set_header("Content-Type", "application/json");
115 return add_cors_headers(std::move(res));
116 }
117}
118
119crow::response CrowServer::handle_stop_recording() {
120 bool success = recorder_node_->stop_recording();
121
122 nlohmann::json response;
123 response["success"] = success;
124 response["message"] = success ? "Recording stopped" : "Failed to stop recording";
125
126 crow::response res(success ? 200 : 400, response.dump());
127 res.set_header("Content-Type", "application/json");
128 return add_cors_headers(std::move(res));
129}
130
131crow::response CrowServer::handle_get_config() {
132 nlohmann::json config;
133 config["output_directory"] = recorder_node_->get_output_directory();
134 config["storage_id"] = "sqlite3";
135 config["serialization_format"] = "cdr";
136
137 crow::response res(200, config.dump());
138 res.set_header("Content-Type", "application/json");
139 return add_cors_headers(std::move(res));
140}
141
142crow::response CrowServer::handle_set_config(const crow::request &req) {
143 try {
144 auto json_data = nlohmann::json::parse(req.body);
145
146 if (json_data.contains("output_directory")) {
147 recorder_node_->set_output_directory(json_data["output_directory"]);
148 }
149
150 nlohmann::json response;
151 response["success"] = true;
152 response["message"] = "Configuration updated";
153
154 crow::response res(200, response.dump());
155 res.set_header("Content-Type", "application/json");
156 return add_cors_headers(std::move(res));
157
158 } catch (const std::exception &e) {
159 nlohmann::json response;
160 response["success"] = false;
161 response["message"] = std::string("Error: ") + e.what();
162
163 crow::response res(400, response.dump());
164 res.set_header("Content-Type", "application/json");
165 return add_cors_headers(std::move(res));
166 }
167}
168
169crow::response CrowServer::handle_get_recordings() {
170 nlohmann::json recordings = nlohmann::json::array();
171
172 try {
173 std::string output_dir = recorder_node_->get_output_directory();
174
175 if (std::filesystem::exists(output_dir)) {
176 for (const auto &entry : std::filesystem::directory_iterator(output_dir)) {
177 if (entry.is_directory()) {
178 nlohmann::json recording;
179 recording["name"] = entry.path().filename().string();
180 recording["path"] = entry.path().string();
181
182 // Calculate total size of all files in the bag directory
183 uintmax_t total_size = 0;
184 if (std::filesystem::exists(entry) && std::filesystem::is_directory(entry)) {
185 try {
186 for (const auto &file_entry : std::filesystem::recursive_directory_iterator(entry)) {
187 if (file_entry.is_regular_file()) {
188 total_size += std::filesystem::file_size(file_entry);
189 }
190 }
191 } catch (const std::exception &) {
192 total_size = 0;
193 }
194 }
195
196 recording["size"] = total_size;
197 recordings.push_back(recording);
198 }
199 }
200 }
201 } catch (const std::exception &e) {
202 nlohmann::json error_recording;
203 error_recording["name"] = "ERROR";
204 error_recording["path"] = e.what();
205 error_recording["size"] = 0;
206 recordings.push_back(error_recording);
207 }
208
209 nlohmann::json response;
210 response["recordings"] = recordings;
211
212 crow::response res(200, response.dump());
213 res.set_header("Content-Type", "application/json");
214 return add_cors_headers(std::move(res));
215}
216
217crow::response CrowServer::handle_delete_recording(const std::string &filename) {
218 nlohmann::json response;
219
220 try {
221 std::string output_dir = recorder_node_->get_output_directory();
222 std::filesystem::path recording_path = std::filesystem::path(output_dir) / filename;
223
224 // Basic security check - filename should not contain path traversal
225 if (filename.find("..") != std::string::npos || filename.find("/") != std::string::npos) {
226 response["success"] = false;
227 response["message"] = "Invalid recording name";
228 crow::response res(400, response.dump());
229 res.set_header("Content-Type", "application/json");
230 return add_cors_headers(std::move(res));
231 }
232
233 // Check if the recording directory exists
234 if (!std::filesystem::exists(recording_path) || !std::filesystem::is_directory(recording_path)) {
235 response["success"] = false;
236 response["message"] = "Recording not found";
237 crow::response res(404, response.dump());
238 res.set_header("Content-Type", "application/json");
239 return add_cors_headers(std::move(res));
240 }
241
242 // Don't delete if currently recording
243 if (recorder_node_->get_status().is_recording) {
244 auto current_bag_path = recorder_node_->get_status().current_bag_path;
245 if (!current_bag_path.empty() && current_bag_path.find(filename) != std::string::npos) {
246 response["success"] = false;
247 response["message"] = "Cannot delete recording that is currently in progress";
248 crow::response res(409, response.dump());
249 res.set_header("Content-Type", "application/json");
250 return add_cors_headers(std::move(res));
251 }
252 }
253
254 // Delete the directory and all its contents
255 std::uintmax_t deleted_files = std::filesystem::remove_all(recording_path);
256
257 response["success"] = true;
258 response["message"] = "Recording deleted successfully";
259 response["deleted_files"] = deleted_files;
260
261 crow::response res(200, response.dump());
262 res.set_header("Content-Type", "application/json");
263 return add_cors_headers(std::move(res));
264
265 } catch (const std::exception &e) {
266 response["success"] = false;
267 response["message"] = std::string("Failed to delete recording: ") + e.what();
268
269 crow::response res(500, response.dump());
270 res.set_header("Content-Type", "application/json");
271 return add_cors_headers(std::move(res));
272 }
273}
274
275crow::response CrowServer::add_cors_headers(crow::response &&res) {
276 if (cors_enabled_) {
277 res.set_header("Access-Control-Allow-Origin", "*");
278 res.set_header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
279 res.set_header("Access-Control-Allow-Headers", "Content-Type, Authorization");
280 }
281 return std::move(res);
282}
283
284std::string CrowServer::create_compressed_archive(const std::vector<std::string> &bag_paths, const std::string &archive_name) {
285 // Generate unique temporary file path
286 auto now = std::chrono::high_resolution_clock::now();
287 auto timestamp = std::chrono::duration_cast<std::chrono::microseconds>(now.time_since_epoch()).count();
288 std::string temp_archive_path = "/tmp/" + archive_name + "_" + std::to_string(timestamp) + ".tar.gz";
289
290 // Build tar command
291 std::stringstream tar_cmd;
292 tar_cmd << "tar -czf \"" << temp_archive_path << "\"";
293
294 // Add each bag directory to the archive
295 for (const auto &bag_path : bag_paths) {
296 tar_cmd << " -C \"" << std::filesystem::path(bag_path).parent_path().string() << "\" \"" << std::filesystem::path(bag_path).filename().string() << "\"";
297 }
298
299 // Execute tar command
300 int result = std::system(tar_cmd.str().c_str());
301 if (result != 0) {
302 std::filesystem::remove(temp_archive_path);
303 throw std::runtime_error("Failed to create compressed archive");
304 }
305
306 return temp_archive_path;
307}
308
309crow::response CrowServer::handle_download_recording(const std::string &filename) {
310 nlohmann::json response;
311
312 try {
313 std::string output_dir = recorder_node_->get_output_directory();
314 std::filesystem::path recording_path = std::filesystem::path(output_dir) / filename;
315
316 // Security check - filename should not contain path traversal
317 if (filename.find("..") != std::string::npos || filename.find("/") != std::string::npos) {
318 response["success"] = false;
319 response["message"] = "Invalid recording name";
320 crow::response res(400, response.dump());
321 res.set_header("Content-Type", "application/json");
322 return add_cors_headers(std::move(res));
323 }
324
325 // Check if the recording directory exists
326 if (!std::filesystem::exists(recording_path) || !std::filesystem::is_directory(recording_path)) {
327 response["success"] = false;
328 response["message"] = "Recording not found";
329 crow::response res(404, response.dump());
330 res.set_header("Content-Type", "application/json");
331 return add_cors_headers(std::move(res));
332 }
333
334 // Don't allow download if currently recording
335 if (recorder_node_->get_status().is_recording) {
336 auto current_bag_path = recorder_node_->get_status().current_bag_path;
337 if (!current_bag_path.empty() && current_bag_path.find(filename) != std::string::npos) {
338 response["success"] = false;
339 response["message"] = "Cannot download recording that is currently in progress";
340 crow::response res(409, response.dump());
341 res.set_header("Content-Type", "application/json");
342 return add_cors_headers(std::move(res));
343 }
344 }
345
346 // Create compressed archive
347 std::vector<std::string> bag_paths = {recording_path.string()};
348 std::string temp_archive_path = create_compressed_archive(bag_paths, filename);
349
350 // Read compressed file
351 std::ifstream file(temp_archive_path, std::ios::binary);
352 if (!file) {
353 std::filesystem::remove(temp_archive_path);
354 response["success"] = false;
355 response["message"] = "Failed to read compressed file";
356 crow::response res(500, response.dump());
357 res.set_header("Content-Type", "application/json");
358 return add_cors_headers(std::move(res));
359 }
360
361 // Read file content
362 std::string content((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
363 file.close();
364
365 // Clean up temporary file
366 std::filesystem::remove(temp_archive_path);
367
368 // Create response with appropriate headers
369 crow::response res(200, content);
370 res.set_header("Content-Type", "application/x-tar");
371 res.set_header("Content-Disposition", "attachment; filename=\"" + filename + ".tar.gz\"");
372 res.set_header("Content-Length", std::to_string(content.length()));
373 return add_cors_headers(std::move(res));
374
375 } catch (const std::exception &e) {
376 response["success"] = false;
377 response["message"] = std::string("Failed to download recording: ") + e.what();
378
379 crow::response res(500, response.dump());
380 res.set_header("Content-Type", "application/json");
381 return add_cors_headers(std::move(res));
382 }
383}
384
385crow::response CrowServer::handle_download_multiple_recordings(const crow::request &req) {
386 nlohmann::json response;
387
388 try {
389 auto json_data = nlohmann::json::parse(req.body);
390
391 if (!json_data.contains("filenames") || !json_data["filenames"].is_array()) {
392 response["success"] = false;
393 response["message"] = "Request must contain 'filenames' array";
394 crow::response res(400, response.dump());
395 res.set_header("Content-Type", "application/json");
396 return add_cors_headers(std::move(res));
397 }
398
399 auto filenames = json_data["filenames"].get<std::vector<std::string>>();
400
401 if (filenames.empty()) {
402 response["success"] = false;
403 response["message"] = "No filenames provided";
404 crow::response res(400, response.dump());
405 res.set_header("Content-Type", "application/json");
406 return add_cors_headers(std::move(res));
407 }
408
409 std::string output_dir = recorder_node_->get_output_directory();
410 std::vector<std::string> valid_bag_paths;
411 std::vector<std::string> invalid_files;
412
413 // Validate all filenames and collect paths
414 for (const auto &filename : filenames) {
415 // Security check
416 if (filename.find("..") != std::string::npos || filename.find("/") != std::string::npos) {
417 invalid_files.push_back(filename + " (invalid characters)");
418 continue;
419 }
420
421 std::filesystem::path recording_path = std::filesystem::path(output_dir) / filename;
422
423 // Check if recording exists
424 if (!std::filesystem::exists(recording_path) || !std::filesystem::is_directory(recording_path)) {
425 invalid_files.push_back(filename + " (not found)");
426 continue;
427 }
428
429 // Don't allow download if currently recording
430 if (recorder_node_->get_status().is_recording) {
431 auto current_bag_path = recorder_node_->get_status().current_bag_path;
432 if (!current_bag_path.empty() && current_bag_path.find(filename) != std::string::npos) {
433 invalid_files.push_back(filename + " (currently recording)");
434 continue;
435 }
436 }
437
438 valid_bag_paths.push_back(recording_path.string());
439 }
440
441 // If no valid files, return error
442 if (valid_bag_paths.empty()) {
443 response["success"] = false;
444 response["message"] = "No valid recordings found";
445 response["invalid_files"] = invalid_files;
446 crow::response res(400, response.dump());
447 res.set_header("Content-Type", "application/json");
448 return add_cors_headers(std::move(res));
449 }
450
451 // Create archive name
452 std::string archive_name = "bag_recordings";
453 if (valid_bag_paths.size() == 1) {
454 archive_name = std::filesystem::path(valid_bag_paths[0]).filename().string();
455 }
456
457 // Create compressed archive
458 std::string temp_archive_path = create_compressed_archive(valid_bag_paths, archive_name);
459
460 // Read compressed file
461 std::ifstream file(temp_archive_path, std::ios::binary);
462 if (!file) {
463 std::filesystem::remove(temp_archive_path);
464 response["success"] = false;
465 response["message"] = "Failed to read compressed file";
466 crow::response res(500, response.dump());
467 res.set_header("Content-Type", "application/json");
468 return add_cors_headers(std::move(res));
469 }
470
471 // Read file content
472 std::string content((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
473 file.close();
474
475 // Clean up temporary file
476 std::filesystem::remove(temp_archive_path);
477
478 // Create response with appropriate headers
479 crow::response res(200, content);
480 res.set_header("Content-Type", "application/x-tar");
481 res.set_header("Content-Disposition", "attachment; filename=\"" + archive_name + ".tar.gz\"");
482 res.set_header("Content-Length", std::to_string(content.length()));
483 res.set_header("X-Processed-Files", std::to_string(valid_bag_paths.size()));
484
485 return add_cors_headers(std::move(res));
486
487 } catch (const std::exception &e) {
488 response["success"] = false;
489 response["message"] = std::string("Failed to download recordings: ") + e.what();
490
491 crow::response res(500, response.dump());
492 res.set_header("Content-Type", "application/json");
493 return add_cors_headers(std::move(res));
494 }
495}
496
497crow::response CrowServer::handle_delete_multiple_recordings(const crow::request &req) {
498 nlohmann::json response;
499
500 try {
501 auto json_data = nlohmann::json::parse(req.body);
502
503 if (!json_data.contains("filenames") || !json_data["filenames"].is_array()) {
504 response["success"] = false;
505 response["message"] = "Invalid request: 'filenames' array required";
506 crow::response res(400, response.dump());
507 res.set_header("Content-Type", "application/json");
508 return add_cors_headers(std::move(res));
509 }
510
511 std::vector<std::string> filenames = json_data["filenames"].get<std::vector<std::string>>();
512 std::string output_dir = recorder_node_->get_output_directory();
513
514 std::vector<std::string> deleted_files;
515 std::vector<std::string> failed_files;
516 int total_files_deleted = 0;
517
518 for (const auto &filename : filenames) {
519 try {
520 // Basic security check - filename should not contain path traversal
521 if (filename.find("..") != std::string::npos || filename.find("/") != std::string::npos) {
522 failed_files.push_back(filename + " (invalid filename)");
523 continue;
524 }
525
526 std::filesystem::path recording_path = std::filesystem::path(output_dir) / filename;
527
528 // Check if the recording directory exists
529 if (!std::filesystem::exists(recording_path) || !std::filesystem::is_directory(recording_path)) {
530 failed_files.push_back(filename + " (not found)");
531 continue;
532 }
533
534 // Don't delete if currently recording
535 if (recorder_node_->get_status().is_recording) {
536 auto current_bag_path = recorder_node_->get_status().current_bag_path;
537 if (!current_bag_path.empty() && current_bag_path.find(filename) != std::string::npos) {
538 failed_files.push_back(filename + " (currently recording)");
539 continue;
540 }
541 }
542
543 // Delete the directory and all its contents
544 std::uintmax_t deleted_count = std::filesystem::remove_all(recording_path);
545 total_files_deleted += deleted_count;
546 deleted_files.push_back(filename);
547
548 } catch (const std::exception &e) {
549 failed_files.push_back(filename + " (" + std::string(e.what()) + ")");
550 }
551 }
552
553 response["success"] = true;
554 response["message"] = "Batch delete completed";
555 response["deleted_recordings"] = deleted_files;
556 response["failed_recordings"] = failed_files;
557 response["total_files_deleted"] = total_files_deleted;
558
559 int status_code = failed_files.empty() ? 200 : 207; // 207 Multi-Status if some failed
560 crow::response res(status_code, response.dump());
561 res.set_header("Content-Type", "application/json");
562 return add_cors_headers(std::move(res));
563
564 } catch (const std::exception &e) {
565 response["success"] = false;
566 response["message"] = std::string("Error processing batch delete: ") + e.what();
567
568 crow::response res(400, response.dump());
569 res.set_header("Content-Type", "application/json");
570 return add_cors_headers(std::move(res));
571 }
572}
573
574// Helper functions
575nlohmann::json recording_status_to_json(const RecordingStatus &status) {
576 nlohmann::json json;
577 json["is_recording"] = status.is_recording;
578 json["current_bag_path"] = status.current_bag_path;
579 json["recorded_messages"] = status.recorded_messages;
580 json["recording_duration"] = status.recording_duration;
581 json["active_topics"] = status.active_topics;
582 return json;
583}
584
585nlohmann::json recording_config_to_json(const RecordingConfig &config) {
586 nlohmann::json json;
587 json["output_path"] = config.output_path;
588 json["topics"] = config.topics;
589 json["storage_id"] = config.storage_id;
590 json["serialization_format"] = config.serialization_format;
591 return json;
592}
593
594RecordingConfig json_to_recording_config(const nlohmann::json &json) {
595 RecordingConfig config;
596
597 if (json.contains("output_path")) {
598 config.output_path = json["output_path"];
599 }
600
601 if (json.contains("topics")) {
602 config.topics = json["topics"].get<std::vector<std::string>>();
603 }
604
605 if (json.contains("storage_id")) {
606 config.storage_id = json["storage_id"];
607 }
608
609 if (json.contains("serialization_format")) {
610 config.serialization_format = json["serialization_format"];
611 }
612
613 return config;
614}
615
616} // namespace bag_recorder_backend
617