PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
remote_nodes_endpoints.cpp
Go to the documentation of this file.
1// BSD 3-Clause License
2// Copyright (c) 2021-2025, 🍀☀🌕🌥 🌊
3// See the LICENSE file in the project root for full license information.
4
14// IMPORTANT: Include Crow FIRST before any PACS headers to avoid forward
15// declaration conflicts
16#include "crow.h"
17
18// Workaround for Windows: DELETE is defined as a macro in <winnt.h>
19// which conflicts with crow::HTTPMethod::DELETE
20#ifdef DELETE
21#undef DELETE
22#endif
23
35
36#include <chrono>
37#include <sstream>
38
40
41namespace {
42
46void add_cors_headers(crow::response& res, const rest_server_context& ctx) {
47 if (ctx.config && !ctx.config->cors_allowed_origins.empty()) {
48 res.add_header("Access-Control-Allow-Origin",
49 ctx.config->cors_allowed_origins);
50 }
51}
52
56std::string format_timestamp(std::chrono::system_clock::time_point tp) {
57 if (tp == std::chrono::system_clock::time_point{}) {
58 return "";
59 }
60 auto time_t_val = std::chrono::system_clock::to_time_t(tp);
61 std::tm tm_val{};
62#ifdef _WIN32
63 gmtime_s(&tm_val, &time_t_val);
64#else
65 gmtime_r(&time_t_val, &tm_val);
66#endif
67 char buf[32];
68 std::strftime(buf, sizeof(buf), "%Y-%m-%dT%H:%M:%SZ", &tm_val);
69 return buf;
70}
71
75std::string node_to_json(const client::remote_node& node) {
76 std::ostringstream oss;
77 oss << R"({"node_id":")" << json_escape(node.node_id)
78 << R"(","name":")" << json_escape(node.name)
79 << R"(","ae_title":")" << json_escape(node.ae_title)
80 << R"(","host":")" << json_escape(node.host)
81 << R"(","port":)" << node.port
82 << R"(,"supports_find":)" << (node.supports_find ? "true" : "false")
83 << R"(,"supports_move":)" << (node.supports_move ? "true" : "false")
84 << R"(,"supports_get":)" << (node.supports_get ? "true" : "false")
85 << R"(,"supports_store":)" << (node.supports_store ? "true" : "false")
86 << R"(,"supports_worklist":)" << (node.supports_worklist ? "true" : "false")
87 << R"(,"connection_timeout_sec":)" << node.connection_timeout.count()
88 << R"(,"dimse_timeout_sec":)" << node.dimse_timeout.count()
89 << R"(,"max_associations":)" << node.max_associations
90 << R"(,"status":")" << client::to_string(node.status) << '"';
91
92 auto last_verified = format_timestamp(node.last_verified);
93 if (!last_verified.empty()) {
94 oss << R"(,"last_verified":")" << last_verified << '"';
95 }
96
97 auto last_error_time = format_timestamp(node.last_error);
98 if (!last_error_time.empty()) {
99 oss << R"(,"last_error":")" << last_error_time << '"';
100 }
101
102 if (!node.last_error_message.empty()) {
103 oss << R"(,"last_error_message":")" << json_escape(node.last_error_message) << '"';
104 }
105
106 auto created_at = format_timestamp(node.created_at);
107 if (!created_at.empty()) {
108 oss << R"(,"created_at":")" << created_at << '"';
109 }
110
111 auto updated_at = format_timestamp(node.updated_at);
112 if (!updated_at.empty()) {
113 oss << R"(,"updated_at":")" << updated_at << '"';
114 }
115
116 oss << '}';
117 return oss.str();
118}
119
123std::string nodes_to_json(const std::vector<client::remote_node>& nodes,
124 size_t total_count) {
125 std::ostringstream oss;
126 oss << R"({"nodes":[)";
127 for (size_t i = 0; i < nodes.size(); ++i) {
128 if (i > 0) {
129 oss << ',';
130 }
131 oss << node_to_json(nodes[i]);
132 }
133 oss << R"(],"total":)" << total_count << '}';
134 return oss.str();
135}
136
140std::optional<client::remote_node> parse_node_from_json(const std::string& body,
141 std::string& error_message) {
142 client::remote_node node;
143
144 // Simple JSON parsing (in production, use a proper JSON library)
145 auto get_string_value = [&body](const std::string& key) -> std::string {
146 std::string search = "\"" + key + "\":\"";
147 auto pos = body.find(search);
148 if (pos == std::string::npos) {
149 return "";
150 }
151 pos += search.length();
152 auto end_pos = body.find('"', pos);
153 if (end_pos == std::string::npos) {
154 return "";
155 }
156 return body.substr(pos, end_pos - pos);
157 };
158
159 auto get_int_value = [&body](const std::string& key) -> std::optional<int64_t> {
160 std::string search = "\"" + key + "\":";
161 auto pos = body.find(search);
162 if (pos == std::string::npos) {
163 return std::nullopt;
164 }
165 pos += search.length();
166 // Skip whitespace
167 while (pos < body.length() && (body[pos] == ' ' || body[pos] == '\t')) {
168 ++pos;
169 }
170 if (pos >= body.length()) {
171 return std::nullopt;
172 }
173 try {
174 size_t idx = 0;
175 auto val = std::stoll(body.substr(pos), &idx);
176 return val;
177 } catch (...) {
178 return std::nullopt;
179 }
180 };
181
182 auto get_bool_value = [&body](const std::string& key,
183 bool default_val) -> bool {
184 std::string search = "\"" + key + "\":";
185 auto pos = body.find(search);
186 if (pos == std::string::npos) {
187 return default_val;
188 }
189 pos += search.length();
190 // Skip whitespace
191 while (pos < body.length() && (body[pos] == ' ' || body[pos] == '\t')) {
192 ++pos;
193 }
194 if (pos >= body.length()) {
195 return default_val;
196 }
197 if (body.substr(pos, 4) == "true") {
198 return true;
199 }
200 if (body.substr(pos, 5) == "false") {
201 return false;
202 }
203 return default_val;
204 };
205
206 // Required fields
207 node.name = get_string_value("name");
208 node.ae_title = get_string_value("ae_title");
209 node.host = get_string_value("host");
210
211 if (node.ae_title.empty()) {
212 error_message = "ae_title is required";
213 return std::nullopt;
214 }
215
216 if (node.host.empty()) {
217 error_message = "host is required";
218 return std::nullopt;
219 }
220
221 // Optional fields with defaults
222 auto port_val = get_int_value("port");
223 node.port = port_val.value_or(104);
224
225 node.supports_find = get_bool_value("supports_find", true);
226 node.supports_move = get_bool_value("supports_move", true);
227 node.supports_get = get_bool_value("supports_get", false);
228 node.supports_store = get_bool_value("supports_store", true);
229 node.supports_worklist = get_bool_value("supports_worklist", false);
230
231 auto connection_timeout = get_int_value("connection_timeout_sec");
232 if (connection_timeout) {
233 node.connection_timeout = std::chrono::seconds(*connection_timeout);
234 }
235
236 auto dimse_timeout = get_int_value("dimse_timeout_sec");
237 if (dimse_timeout) {
238 node.dimse_timeout = std::chrono::seconds(*dimse_timeout);
239 }
240
241 auto max_assoc = get_int_value("max_associations");
242 if (max_assoc) {
243 node.max_associations = static_cast<size_t>(*max_assoc);
244 }
245
246 return node;
247}
248
252std::pair<size_t, size_t> parse_pagination(const crow::request& req) {
253 size_t limit = 20;
254 size_t offset = 0;
255
256 auto limit_param = req.url_params.get("limit");
257 if (limit_param) {
258 try {
259 limit = std::stoul(limit_param);
260 if (limit > 100) {
261 limit = 100;
262 }
263 } catch (...) {
264 // Use default
265 }
266 }
267
268 auto offset_param = req.url_params.get("offset");
269 if (offset_param) {
270 try {
271 offset = std::stoul(offset_param);
272 } catch (...) {
273 // Use default
274 }
275 }
276
277 return {limit, offset};
278}
279
280} // namespace
281
282// Internal implementation function called from rest_server.cpp
283void register_remote_nodes_endpoints_impl(crow::SimpleApp& app,
284 std::shared_ptr<rest_server_context> ctx) {
285 // GET /api/v1/remote-nodes - List remote nodes (paginated)
286 CROW_ROUTE(app, "/api/v1/remote-nodes")
287 .methods(crow::HTTPMethod::GET)([ctx](const crow::request& req) {
288 crow::response res;
289 res.add_header("Content-Type", "application/json");
290 add_cors_headers(res, *ctx);
291
292 if (!ctx->node_manager) {
293 res.code = 503;
294 res.body = make_error_json("SERVICE_UNAVAILABLE",
295 "Remote node manager not configured");
296 return res;
297 }
298
299 // Parse pagination
300 auto [limit, offset] = parse_pagination(req);
301
302 // Filter by status if provided
303 auto status_param = req.url_params.get("status");
304 std::vector<client::remote_node> nodes;
305
306 if (status_param) {
307 auto status = client::node_status_from_string(status_param);
308 nodes = ctx->node_manager->list_nodes_by_status(status);
309 } else {
310 nodes = ctx->node_manager->list_nodes();
311 }
312
313 size_t total_count = nodes.size();
314
315 // Apply pagination
316 std::vector<client::remote_node> paginated;
317 for (size_t i = offset; i < nodes.size() && paginated.size() < limit; ++i) {
318 paginated.push_back(nodes[i]);
319 }
320
321 res.code = 200;
322 res.body = nodes_to_json(paginated, total_count);
323 return res;
324 });
325
326 // POST /api/v1/remote-nodes - Create a new remote node
327 CROW_ROUTE(app, "/api/v1/remote-nodes")
328 .methods(crow::HTTPMethod::POST)([ctx](const crow::request& req) {
329 crow::response res;
330 res.add_header("Content-Type", "application/json");
331 add_cors_headers(res, *ctx);
332
333 if (!ctx->node_manager) {
334 res.code = 503;
335 res.body = make_error_json("SERVICE_UNAVAILABLE",
336 "Remote node manager not configured");
337 return res;
338 }
339
340 std::string error_message;
341 auto node_opt = parse_node_from_json(req.body, error_message);
342 if (!node_opt) {
343 res.code = 400;
344 res.body = make_error_json("INVALID_REQUEST", error_message);
345 return res;
346 }
347
348 auto& node = *node_opt;
349
350 // Generate node_id if not provided
351 if (node.node_id.empty()) {
352 // Use ae_title + timestamp as default node_id
353 auto now = std::chrono::system_clock::now();
354 auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
355 now.time_since_epoch()).count();
356 node.node_id = node.ae_title + "_" + std::to_string(ms);
357 }
358
359 auto result = ctx->node_manager->add_node(node);
360 if (!result.is_ok()) {
361 res.code = 409;
362 res.body = make_error_json("CONFLICT", result.error().message);
363 return res;
364 }
365
366 // Retrieve the created node to get full details
367 auto created_node = ctx->node_manager->get_node(node.node_id);
368 if (!created_node) {
369 res.code = 201;
370 res.body = node_to_json(node);
371 return res;
372 }
373
374 res.code = 201;
375 res.body = node_to_json(*created_node);
376 return res;
377 });
378
379 // GET /api/v1/remote-nodes/<nodeId> - Get a specific remote node
380 CROW_ROUTE(app, "/api/v1/remote-nodes/<string>")
381 .methods(crow::HTTPMethod::GET)(
382 [ctx](const crow::request& /*req*/, const std::string& node_id) {
383 crow::response res;
384 res.add_header("Content-Type", "application/json");
385 add_cors_headers(res, *ctx);
386
387 if (!ctx->node_manager) {
388 res.code = 503;
389 res.body = make_error_json("SERVICE_UNAVAILABLE",
390 "Remote node manager not configured");
391 return res;
392 }
393
394 auto node = ctx->node_manager->get_node(node_id);
395 if (!node) {
396 res.code = 404;
397 res.body = make_error_json("NOT_FOUND", "Remote node not found");
398 return res;
399 }
400
401 res.code = 200;
402 res.body = node_to_json(*node);
403 return res;
404 });
405
406 // PUT /api/v1/remote-nodes/<nodeId> - Update a remote node
407 CROW_ROUTE(app, "/api/v1/remote-nodes/<string>")
408 .methods(crow::HTTPMethod::PUT)(
409 [ctx](const crow::request& req, const std::string& node_id) {
410 crow::response res;
411 res.add_header("Content-Type", "application/json");
412 add_cors_headers(res, *ctx);
413
414 if (!ctx->node_manager) {
415 res.code = 503;
416 res.body = make_error_json("SERVICE_UNAVAILABLE",
417 "Remote node manager not configured");
418 return res;
419 }
420
421 // Check if node exists
422 auto existing = ctx->node_manager->get_node(node_id);
423 if (!existing) {
424 res.code = 404;
425 res.body = make_error_json("NOT_FOUND", "Remote node not found");
426 return res;
427 }
428
429 std::string error_message;
430 auto node_opt = parse_node_from_json(req.body, error_message);
431 if (!node_opt) {
432 res.code = 400;
433 res.body = make_error_json("INVALID_REQUEST", error_message);
434 return res;
435 }
436
437 auto& node = *node_opt;
438 node.node_id = node_id; // Preserve the node_id from URL
439
440 auto result = ctx->node_manager->update_node(node);
441 if (!result.is_ok()) {
442 res.code = 500;
443 res.body = make_error_json("UPDATE_FAILED", result.error().message);
444 return res;
445 }
446
447 // Retrieve the updated node
448 auto updated_node = ctx->node_manager->get_node(node_id);
449 if (!updated_node) {
450 res.code = 200;
451 res.body = node_to_json(node);
452 return res;
453 }
454
455 res.code = 200;
456 res.body = node_to_json(*updated_node);
457 return res;
458 });
459
460 // DELETE /api/v1/remote-nodes/<nodeId> - Delete a remote node
461 CROW_ROUTE(app, "/api/v1/remote-nodes/<string>")
462 .methods(crow::HTTPMethod::DELETE)(
463 [ctx](const crow::request& /*req*/, const std::string& node_id) {
464 crow::response res;
465 add_cors_headers(res, *ctx);
466
467 if (!ctx->node_manager) {
468 res.add_header("Content-Type", "application/json");
469 res.code = 503;
470 res.body = make_error_json("SERVICE_UNAVAILABLE",
471 "Remote node manager not configured");
472 return res;
473 }
474
475 // Check if node exists
476 auto existing = ctx->node_manager->get_node(node_id);
477 if (!existing) {
478 res.add_header("Content-Type", "application/json");
479 res.code = 404;
480 res.body = make_error_json("NOT_FOUND", "Remote node not found");
481 return res;
482 }
483
484 auto result = ctx->node_manager->remove_node(node_id);
485 if (!result.is_ok()) {
486 res.add_header("Content-Type", "application/json");
487 res.code = 500;
488 res.body = make_error_json("DELETE_FAILED", result.error().message);
489 return res;
490 }
491
492 res.code = 204;
493 return res;
494 });
495
496 // POST /api/v1/remote-nodes/<nodeId>/verify - Verify node connectivity
497 CROW_ROUTE(app, "/api/v1/remote-nodes/<string>/verify")
498 .methods(crow::HTTPMethod::POST)(
499 [ctx](const crow::request& /*req*/, const std::string& node_id) {
500 crow::response res;
501 res.add_header("Content-Type", "application/json");
502 add_cors_headers(res, *ctx);
503
504 if (!ctx->node_manager) {
505 res.code = 503;
506 res.body = make_error_json("SERVICE_UNAVAILABLE",
507 "Remote node manager not configured");
508 return res;
509 }
510
511 // Check if node exists
512 auto existing = ctx->node_manager->get_node(node_id);
513 if (!existing) {
514 res.code = 404;
515 res.body = make_error_json("NOT_FOUND", "Remote node not found");
516 return res;
517 }
518
519 auto start = std::chrono::steady_clock::now();
520 auto result = ctx->node_manager->verify_node(node_id);
521 auto end = std::chrono::steady_clock::now();
522 auto elapsed_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
523 end - start).count();
524
525 if (result.is_ok()) {
526 std::ostringstream oss;
527 oss << R"({"success":true,"response_time_ms":)" << elapsed_ms << '}';
528 res.code = 200;
529 res.body = oss.str();
530 } else {
531 std::ostringstream oss;
532 oss << R"({"success":false,"error":")"
533 << json_escape(result.error().message)
534 << R"(","response_time_ms":)" << elapsed_ms << '}';
535 res.code = 200;
536 res.body = oss.str();
537 }
538 return res;
539 });
540
541 // GET /api/v1/remote-nodes/<nodeId>/status - Get node status
542 CROW_ROUTE(app, "/api/v1/remote-nodes/<string>/status")
543 .methods(crow::HTTPMethod::GET)(
544 [ctx](const crow::request& /*req*/, const std::string& node_id) {
545 crow::response res;
546 res.add_header("Content-Type", "application/json");
547 add_cors_headers(res, *ctx);
548
549 if (!ctx->node_manager) {
550 res.code = 503;
551 res.body = make_error_json("SERVICE_UNAVAILABLE",
552 "Remote node manager not configured");
553 return res;
554 }
555
556 auto node = ctx->node_manager->get_node(node_id);
557 if (!node) {
558 res.code = 404;
559 res.body = make_error_json("NOT_FOUND", "Remote node not found");
560 return res;
561 }
562
563 auto stats = ctx->node_manager->get_statistics(node_id);
564
565 std::ostringstream oss;
566 oss << R"({"status":")" << client::to_string(node->status) << '"';
567
568 auto last_verified = format_timestamp(node->last_verified);
569 if (!last_verified.empty()) {
570 oss << R"(,"last_verified":")" << last_verified << '"';
571 }
572
573 if (!node->last_error_message.empty()) {
574 oss << R"(,"last_error_message":")"
575 << json_escape(node->last_error_message) << '"';
576 }
577
578 oss << R"(,"total_connections":)" << stats.total_connections
579 << R"(,"active_connections":)" << stats.active_connections
580 << R"(,"successful_operations":)" << stats.successful_operations
581 << R"(,"failed_operations":)" << stats.failed_operations;
582
583 if (stats.avg_response_time.count() > 0) {
584 oss << R"(,"avg_response_time_ms":)" << stats.avg_response_time.count();
585 }
586
587 oss << '}';
588
589 res.code = 200;
590 res.body = oss.str();
591 return res;
592 });
593
594 // POST /api/v1/remote-nodes/<nodeId>/query - Query remote PACS
595 CROW_ROUTE(app, "/api/v1/remote-nodes/<string>/query")
596 .methods(crow::HTTPMethod::POST)(
597 [ctx](const crow::request& req, const std::string& node_id) {
598 crow::response res;
599 res.add_header("Content-Type", "application/json");
600 add_cors_headers(res, *ctx);
601
602 if (!ctx->node_manager) {
603 res.code = 503;
604 res.body = make_error_json("SERVICE_UNAVAILABLE",
605 "Remote node manager not configured");
606 return res;
607 }
608
609 auto node = ctx->node_manager->get_node(node_id);
610 if (!node) {
611 res.code = 404;
612 res.body = make_error_json("NOT_FOUND", "Remote node not found");
613 return res;
614 }
615
616 if (!node->supports_find) {
617 res.code = 400;
618 res.body = make_error_json("UNSUPPORTED",
619 "Remote node does not support C-FIND queries");
620 return res;
621 }
622
623 // Parse query parameters from request body
624 auto get_str = [&req](const std::string& key) -> std::string {
625 std::string search = "\"" + key + "\":\"";
626 auto pos = req.body.find(search);
627 if (pos == std::string::npos) return "";
628 pos += search.length();
629 auto end_pos = req.body.find('"', pos);
630 if (end_pos == std::string::npos) return "";
631 return req.body.substr(pos, end_pos - pos);
632 };
633
634 auto level = get_str("level");
635 if (level.empty()) level = "STUDY";
636
637 // Determine calling AE title from dicom_server config
638 std::string calling_ae = "PACS_SCU";
639 if (ctx->dicom_server) {
640 calling_ae = ctx->dicom_server->config().ae_title;
641 }
642
643 // Build association config for C-FIND
644 network::association_config assoc_config;
645 assoc_config.calling_ae_title = calling_ae;
646 assoc_config.called_ae_title = node->ae_title;
647 assoc_config.proposed_contexts.push_back({
648 1,
650 {"1.2.840.10008.1.2.1"} // Explicit VR Little Endian
651 });
652
653 auto timeout = std::chrono::milliseconds(
654 node->connection_timeout.count() * 1000);
655 auto assoc_result = network::association::connect(
656 node->host,
657 static_cast<uint16_t>(node->port),
658 assoc_config,
659 timeout);
660
661 if (assoc_result.is_err()) {
662 res.code = 502;
663 res.body = make_error_json("CONNECTION_FAILED",
664 "Failed to connect to remote PACS: " +
665 assoc_result.error().message);
666 return res;
667 }
668
669 auto& assoc = assoc_result.value();
671
672 // Helper to format query results as JSON
673 auto format_query_response = [](
674 const services::query_result& qr) -> std::string {
675 std::ostringstream oss;
676 oss << R"({"matches":[)";
677 for (size_t i = 0; i < qr.matches.size(); ++i) {
678 if (i > 0) oss << ',';
679 oss << '{';
680 bool first_field = true;
681 for (const auto& [tag, element] : qr.matches[i]) {
682 auto val = element.as_string();
683 if (val.is_err()) continue;
684 if (!first_field) oss << ',';
685 first_field = false;
686 oss << '"' << json_escape(tag.to_string())
687 << R"(":")" << json_escape(val.value()) << '"';
688 }
689 oss << '}';
690 }
691 oss << R"(],"total_matches":)" << qr.matches.size()
692 << R"(,"elapsed_ms":)" << qr.elapsed.count()
693 << R"(,"status":")"
694 << (qr.is_success() ? "success" : "error")
695 << R"("})";
696 return oss.str();
697 };
698
699 // Helper to handle query result and build response
700 auto handle_result = [&](
702 (void)assoc.release();
703 if (result.is_err()) {
704 res.code = 502;
705 res.body = make_error_json("QUERY_FAILED",
706 "C-FIND query failed: " + result.error().message);
707 } else {
708 res.code = 200;
709 res.body = format_query_response(result.value());
710 }
711 };
712
713 // Execute query based on level
714 if (level == "PATIENT") {
716 keys.patient_name = get_str("patient_name");
717 keys.patient_id = get_str("patient_id");
718 keys.birth_date = get_str("birth_date");
719 keys.sex = get_str("sex");
720 auto result = scu.find_patients(assoc, keys);
721 handle_result(result);
722 } else if (level == "STUDY") {
724 keys.patient_id = get_str("patient_id");
725 keys.study_uid = get_str("study_uid");
726 keys.study_date = get_str("study_date");
727 keys.accession_number = get_str("accession_number");
728 keys.modality = get_str("modality");
729 keys.study_description = get_str("study_description");
730 auto result = scu.find_studies(assoc, keys);
731 handle_result(result);
732 } else if (level == "SERIES") {
734 keys.study_uid = get_str("study_uid");
735 keys.series_uid = get_str("series_uid");
736 keys.modality = get_str("modality");
737 keys.series_number = get_str("series_number");
738 if (keys.study_uid.empty()) {
739 (void)assoc.release();
740 res.code = 400;
741 res.body = make_error_json("INVALID_REQUEST",
742 "study_uid is required for SERIES level queries");
743 return res;
744 }
745 auto result = scu.find_series(assoc, keys);
746 handle_result(result);
747 } else if (level == "IMAGE") {
749 keys.series_uid = get_str("series_uid");
750 keys.sop_instance_uid = get_str("sop_instance_uid");
751 keys.instance_number = get_str("instance_number");
752 if (keys.series_uid.empty()) {
753 (void)assoc.release();
754 res.code = 400;
755 res.body = make_error_json("INVALID_REQUEST",
756 "series_uid is required for IMAGE level queries");
757 return res;
758 }
759 auto result = scu.find_instances(assoc, keys);
760 handle_result(result);
761 } else {
762 (void)assoc.release();
763 res.code = 400;
764 res.body = make_error_json("INVALID_REQUEST",
765 "Invalid query level. Use PATIENT, STUDY, SERIES, "
766 "or IMAGE.");
767 return res;
768 }
769
770 return res;
771 });
772
773 // POST /api/v1/remote-nodes/<nodeId>/retrieve - Retrieve from remote PACS
774 CROW_ROUTE(app, "/api/v1/remote-nodes/<string>/retrieve")
775 .methods(crow::HTTPMethod::POST)(
776 [ctx](const crow::request& req, const std::string& node_id) {
777 crow::response res;
778 res.add_header("Content-Type", "application/json");
779 add_cors_headers(res, *ctx);
780
781 if (!ctx->node_manager) {
782 res.code = 503;
783 res.body = make_error_json("SERVICE_UNAVAILABLE",
784 "Remote node manager not configured");
785 return res;
786 }
787
788 if (!ctx->job_manager) {
789 res.code = 503;
790 res.body = make_error_json("SERVICE_UNAVAILABLE",
791 "Job manager not configured");
792 return res;
793 }
794
795 auto node = ctx->node_manager->get_node(node_id);
796 if (!node) {
797 res.code = 404;
798 res.body = make_error_json("NOT_FOUND", "Remote node not found");
799 return res;
800 }
801
802 if (!node->supports_move && !node->supports_get) {
803 res.code = 400;
804 res.body = make_error_json("UNSUPPORTED",
805 "Remote node does not support C-MOVE or C-GET");
806 return res;
807 }
808
809 // Parse retrieve parameters from request body
810 auto get_str = [&req](const std::string& key) -> std::string {
811 std::string search = "\"" + key + "\":\"";
812 auto pos = req.body.find(search);
813 if (pos == std::string::npos) return "";
814 pos += search.length();
815 auto end_pos = req.body.find('"', pos);
816 if (end_pos == std::string::npos) return "";
817 return req.body.substr(pos, end_pos - pos);
818 };
819
820 auto study_uid = get_str("study_uid");
821 if (study_uid.empty()) {
822 res.code = 400;
823 res.body = make_error_json("INVALID_REQUEST",
824 "study_uid is required");
825 return res;
826 }
827
828 auto series_uid = get_str("series_uid");
829 auto priority_str = get_str("priority");
830
831 // Map priority string to enum
832 auto priority = client::job_priority::normal;
833 if (priority_str == "low") {
834 priority = client::job_priority::low;
835 } else if (priority_str == "high") {
837 } else if (priority_str == "urgent") {
839 }
840
841 // Create retrieve job
842 std::optional<std::string_view> series_opt;
843 if (!series_uid.empty()) {
844 series_opt = series_uid;
845 }
846
847 auto job_id = ctx->job_manager->create_retrieve_job(
848 node_id, study_uid, series_opt, priority);
849
850 // Start the job
851 auto start_result = ctx->job_manager->start_job(job_id);
852 if (start_result.is_err()) {
853 res.code = 500;
854 res.body = make_error_json("JOB_START_FAILED",
855 "Failed to start retrieve job: " +
856 start_result.error().message);
857 return res;
858 }
859
860 std::ostringstream oss;
861 oss << R"({"job_id":")" << json_escape(job_id)
862 << R"(","status":"pending"})";
863
864 res.code = 202;
865 res.body = oss.str();
866 return res;
867 });
868}
869
870} // namespace kcenon::pacs::web::endpoints
DICOM Association management per PS3.8.
static Result< association > connect(const std::string &host, uint16_t port, const association_config &config, duration timeout=default_timeout)
Initiate an SCU association to a remote SCP.
network::Result< query_result > find_patients(network::association &assoc, const patient_query_keys &keys)
Query for patients.
network::Result< query_result > find_instances(network::association &assoc, const instance_query_keys &keys)
Query for instances within a series.
network::Result< query_result > find_studies(network::association &assoc, const study_query_keys &keys)
Query for studies.
network::Result< query_result > find_series(network::association &assoc, const series_query_keys &keys)
Query for series within a study.
Multi-threaded DICOM server for handling multiple associations.
Job manager for asynchronous DICOM operations.
node_status node_status_from_string(std::string_view str) noexcept
Parse node_status from string.
Definition remote_node.h:80
@ low
Background operations.
@ high
User-requested operations.
@ urgent
Critical operations.
constexpr const char * to_string(job_type type) noexcept
Convert job_type to string representation.
Definition job_types.h:54
constexpr int connection_timeout
Definition result.h:95
constexpr std::string_view study_root_find_sop_class_uid
Study Root Query/Retrieve Information Model - FIND.
Definition query_scp.h:42
void register_remote_nodes_endpoints_impl(crow::SimpleApp &app, std::shared_ptr< rest_server_context > ctx)
std::string make_error_json(std::string_view code, std::string_view message)
Create JSON error response body with details.
Definition rest_types.h:79
std::string json_escape(std::string_view s)
Escape a string for JSON.
Definition rest_types.h:101
DICOM Query SCU service (C-FIND sender)
Remote PACS node data structures for client operations.
Remote PACS node manager for client operations.
Remote PACS node management REST API endpoints.
Configuration for REST API server.
Common types and utilities for REST API.
DICOM Server configuration structures.
Configuration for SCU association request.
std::string called_ae_title
Remote AE Title (16 chars max)
std::string calling_ae_title
Our AE Title (16 chars max)
std::vector< proposed_presentation_context > proposed_contexts
Query keys for IMAGE (Instance) level queries.
Definition query_scu.h:155
std::string sop_instance_uid
SOP Instance UID (0008,0018)
Definition query_scu.h:157
std::string series_uid
Series Instance UID (0020,000E) - Required.
Definition query_scu.h:156
std::string instance_number
Instance Number (0020,0013)
Definition query_scu.h:158
Query keys for PATIENT level queries.
Definition query_scu.h:123
std::string birth_date
Patient's Birth Date (0010,0030)
Definition query_scu.h:126
std::string sex
Patient's Sex (0010,0040)
Definition query_scu.h:127
std::string patient_id
Patient ID (0010,0020)
Definition query_scu.h:125
std::string patient_name
Patient's Name (0010,0010)
Definition query_scu.h:124
Result of a C-FIND query operation.
Definition query_scu.h:91
Query keys for SERIES level queries.
Definition query_scu.h:145
std::string series_number
Series Number (0020,0011)
Definition query_scu.h:149
std::string series_uid
Series Instance UID (0020,000E)
Definition query_scu.h:147
std::string study_uid
Study Instance UID (0020,000D) - Required.
Definition query_scu.h:146
std::string modality
Modality (0008,0060)
Definition query_scu.h:148
Query keys for STUDY level queries.
Definition query_scu.h:133
std::string accession_number
Accession Number (0008,0050)
Definition query_scu.h:137
std::string study_uid
Study Instance UID (0020,000D)
Definition query_scu.h:135
std::string study_description
Study Description (0008,1030)
Definition query_scu.h:139
std::string modality
Modalities in Study (0008,0061)
Definition query_scu.h:138
std::string patient_id
Patient ID (0010,0020) - for filtering.
Definition query_scu.h:134
std::string study_date
Study Date (0008,0020) - YYYYMMDD or range.
Definition query_scu.h:136
System API endpoints for REST server.