44std::atomic<kcenon::pacs::network::dicom_server*> g_server{
nullptr};
47std::atomic<bool> g_running{
true};
50std::atomic<kcenon::pacs::storage::file_storage*> g_file_storage{
nullptr};
56void signal_handler(
int signal) {
57 std::cout <<
"\nReceived signal " << signal <<
", shutting down...\n";
60 auto* server = g_server.load();
69void install_signal_handlers() {
70 std::signal(SIGINT, signal_handler);
71 std::signal(SIGTERM, signal_handler);
73 std::signal(SIGHUP, signal_handler);
81void print_usage(
const char* program_name) {
83Storage SCP - DICOM Image Receiver
85Usage: )" << program_name << R"( <port> <ae_title> [options]
88 port Port number to listen on (typically 104 or 11112)
89 ae_title Application Entity Title for this server (max 16 chars)
92 --storage-dir <path> Directory to store received DICOM files
95 --index-db <path> SQLite database for indexing (optional)
96 --accept <modalities> Comma-separated list of accepted modalities
97 (CT,MR,US,XR,CR,DX,NM,PT,SC,SR)
98 --naming <scheme> File naming scheme: hierarchical (default),
100 --duplicate <policy> Duplicate handling: reject (default), replace, ignore
101 --max-assoc <n> Maximum concurrent associations (default: 10)
102 --timeout <sec> Idle timeout in seconds (default: 300)
103 --help Show this help message
106 )" << program_name << R"( 11112 MY_PACS --storage-dir ./received
107 )" << program_name << R"( 11112 MY_PACS --storage-dir ./received --index-db ./pacs.db
108 )" << program_name << R"( 11112 MY_PACS --storage-dir ./archive --accept "CT,MR"
111 - Press Ctrl+C to stop the server gracefully
112 - Files are stored in hierarchical structure: StudyUID/SeriesUID/SOPUID.dcm
113 - Without --accept, all standard storage SOP classes are accepted
117 1 Error - Failed to start server or invalid arguments
124struct store_scp_args {
126 std::string ae_title;
127 std::filesystem::path storage_dir;
128 std::filesystem::path index_db;
129 std::vector<std::string> accepted_modalities;
132 size_t max_associations = 10;
133 uint32_t idle_timeout = 300;
139std::vector<std::string> modality_to_sop_classes(
const std::string& modality) {
140 static const std::map<std::string, std::vector<std::string>> modality_map = {
141 {
"CT", {
"1.2.840.10008.5.1.4.1.1.2",
"1.2.840.10008.5.1.4.1.1.2.1"}},
142 {
"MR", {
"1.2.840.10008.5.1.4.1.1.4",
"1.2.840.10008.5.1.4.1.1.4.1"}},
143 {
"US", {
"1.2.840.10008.5.1.4.1.1.6.1"}},
144 {
"CR", {
"1.2.840.10008.5.1.4.1.1.1"}},
145 {
"DX", {
"1.2.840.10008.5.1.4.1.1.1.1",
"1.2.840.10008.5.1.4.1.1.1.1.1"}},
146 {
"XR", {
"1.2.840.10008.5.1.4.1.1.12.1",
"1.2.840.10008.5.1.4.1.1.12.2"}},
147 {
"NM", {
"1.2.840.10008.5.1.4.1.1.20"}},
148 {
"PT", {
"1.2.840.10008.5.1.4.1.1.128",
"1.2.840.10008.5.1.4.1.1.130"}},
149 {
"SC", {
"1.2.840.10008.5.1.4.1.1.7"}},
150 {
"SR", {
"1.2.840.10008.5.1.4.1.1.88.11",
"1.2.840.10008.5.1.4.1.1.88.22",
151 "1.2.840.10008.5.1.4.1.1.88.33"}}
154 auto it = modality_map.find(modality);
155 if (it != modality_map.end()) {
164std::vector<std::string> parse_modalities(
const std::string& input) {
165 std::vector<std::string> sop_classes;
166 std::istringstream ss(input);
169 while (std::getline(ss, modality,
',')) {
175 for (
auto& c : modality) {
176 c =
static_cast<char>(std::toupper(
static_cast<unsigned char>(c)));
179 auto classes = modality_to_sop_classes(modality);
180 sop_classes.insert(sop_classes.end(), classes.begin(), classes.end());
193bool parse_arguments(
int argc,
char* argv[], store_scp_args& args) {
199 for (
int i = 1; i < argc; ++i) {
200 if (std::string(argv[i]) ==
"--help" || std::string(argv[i]) ==
"-h") {
207 int port_int = std::stoi(argv[1]);
208 if (port_int < 1 || port_int > 65535) {
209 std::cerr <<
"Error: Port must be between 1 and 65535\n";
212 args.port =
static_cast<uint16_t
>(port_int);
213 }
catch (
const std::exception&) {
214 std::cerr <<
"Error: Invalid port number '" << argv[1] <<
"'\n";
219 args.ae_title = argv[2];
220 if (args.ae_title.length() > 16) {
221 std::cerr <<
"Error: AE title exceeds 16 characters\n";
226 for (
int i = 3; i < argc; ++i) {
227 std::string arg = argv[i];
229 if (arg ==
"--storage-dir" && i + 1 < argc) {
230 args.storage_dir = argv[++i];
231 }
else if (arg ==
"--index-db" && i + 1 < argc) {
232 args.index_db = argv[++i];
233 }
else if (arg ==
"--accept" && i + 1 < argc) {
234 args.accepted_modalities = parse_modalities(argv[++i]);
235 }
else if (arg ==
"--naming" && i + 1 < argc) {
236 std::string scheme = argv[++i];
237 if (scheme ==
"hierarchical") {
239 }
else if (scheme ==
"date") {
241 }
else if (scheme ==
"flat") {
244 std::cerr <<
"Error: Unknown naming scheme '" << scheme <<
"'\n";
247 }
else if (arg ==
"--duplicate" && i + 1 < argc) {
248 std::string policy = argv[++i];
249 if (policy ==
"reject") {
251 }
else if (policy ==
"replace") {
253 }
else if (policy ==
"ignore") {
256 std::cerr <<
"Error: Unknown duplicate policy '" << policy <<
"'\n";
259 }
else if (arg ==
"--max-assoc" && i + 1 < argc) {
261 int val = std::stoi(argv[++i]);
263 std::cerr <<
"Error: max-assoc must be positive\n";
266 args.max_associations =
static_cast<size_t>(val);
267 }
catch (
const std::exception&) {
268 std::cerr <<
"Error: Invalid max-assoc value\n";
271 }
else if (arg ==
"--timeout" && i + 1 < argc) {
273 int val = std::stoi(argv[++i]);
275 std::cerr <<
"Error: timeout cannot be negative\n";
278 args.idle_timeout =
static_cast<uint32_t
>(val);
279 }
catch (
const std::exception&) {
280 std::cerr <<
"Error: Invalid timeout value\n";
284 std::cerr <<
"Error: Unknown option '" << arg <<
"'\n";
290 if (args.storage_dir.empty()) {
291 std::cerr <<
"Error: --storage-dir is required\n";
302std::string current_timestamp() {
303 auto now = std::chrono::system_clock::now();
304 auto time_t_now = std::chrono::system_clock::to_time_t(now);
305 auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
306 now.time_since_epoch()) % 1000;
308 std::ostringstream oss;
309 oss << std::put_time(std::localtime(&time_t_now),
"%Y-%m-%d %H:%M:%S");
310 oss <<
'.' << std::setfill(
'0') << std::setw(3) << ms.count();
317std::string format_bytes(
size_t bytes) {
318 const char*
units[] = {
"B",
"KB",
"MB",
"GB",
"TB"};
320 double size =
static_cast<double>(bytes);
322 while (size >= 1024.0 && unit_index < 4) {
327 std::ostringstream oss;
328 if (unit_index == 0) {
329 oss << bytes <<
" " <<
units[unit_index];
331 oss << std::fixed << std::setprecision(2) << size <<
" " <<
units[unit_index];
341bool run_server(
const store_scp_args& args) {
347 std::cout <<
"\nStarting Storage SCP...\n";
348 std::cout <<
" AE Title: " << args.ae_title <<
"\n";
349 std::cout <<
" Port: " << args.port <<
"\n";
350 std::cout <<
" Storage Directory: " << args.storage_dir <<
"\n";
351 if (!args.index_db.empty()) {
352 std::cout <<
" Index Database: " << args.index_db <<
"\n";
354 std::cout <<
" Max Associations: " << args.max_associations <<
"\n";
355 std::cout <<
" Idle Timeout: " << args.idle_timeout <<
" seconds\n";
356 if (!args.accepted_modalities.empty()) {
357 std::cout <<
" Accepted Classes: " << args.accepted_modalities.size()
358 <<
" SOP class(es)\n";
360 std::cout <<
" Accepted Classes: All standard storage classes\n";
366 if (!std::filesystem::exists(args.storage_dir)) {
367 if (!std::filesystem::create_directories(args.storage_dir, ec)) {
368 std::cerr <<
"Failed to create storage directory: " << ec.message() <<
"\n";
371 std::cout <<
"Created storage directory: " << args.storage_dir <<
"\n";
376 storage_config.
root_path = args.storage_dir;
377 storage_config.
naming = args.naming;
378 storage_config.
duplicate = args.duplicate;
382 file_storage
storage{storage_config};
386 server_config config;
387 config.ae_title = args.ae_title;
388 config.port = args.port;
389 config.max_associations = args.max_associations;
390 config.idle_timeout = std::chrono::seconds{args.idle_timeout};
391 config.implementation_class_uid =
"1.2.826.0.1.3680043.2.1545.1";
392 config.implementation_version_name =
"STORE_SCP_001";
395 dicom_server server{config};
400 if (!args.accepted_modalities.empty()) {
401 scp_config.accepted_sop_classes = args.accepted_modalities;
408 auto storage_service = std::make_shared<storage_scp>(
scp_config);
411 storage_service->set_handler(
412 [&storage](
const dicom_dataset& dataset,
413 const std::string& calling_ae,
414 [[maybe_unused]]
const std::string& sop_class_uid,
415 [[maybe_unused]]
const std::string& sop_instance_uid) -> storage_status {
418 auto patient_name = dataset.get_string(tags::patient_name,
"Unknown");
419 auto study_desc = dataset.get_string(tags::study_description,
"");
420 auto modality = dataset.get_string(tags::modality,
"??");
422 std::cout <<
"[" << current_timestamp() <<
"] "
423 <<
"C-STORE from " << calling_ae <<
": "
425 if (!study_desc.empty()) {
426 std::cout <<
" (" << study_desc <<
")";
431 auto result =
storage.store(dataset);
432 if (result.is_err()) {
433 std::cerr <<
"[" << current_timestamp() <<
"] "
434 <<
"Storage failed: " << result.error().message <<
"\n";
435 return storage_status::out_of_resources;
438 return storage_status::success;
442 storage_service->set_pre_store_handler([](
const dicom_dataset& dataset) {
444 if (!dataset.contains(tags::study_instance_uid) ||
445 !dataset.contains(tags::series_instance_uid) ||
446 !dataset.contains(tags::sop_instance_uid)) {
447 std::cerr <<
"[" << current_timestamp() <<
"] "
448 <<
"Rejected: Missing required UID attributes\n";
455 server.register_service(storage_service);
458 server.on_association_established([](
const association& assoc) {
459 std::cout <<
"[" << current_timestamp() <<
"] "
460 <<
"Association established from: " << assoc.calling_ae()
461 <<
" -> " << assoc.called_ae() <<
"\n";
464 server.on_association_released([](
const association& assoc) {
465 std::cout <<
"[" << current_timestamp() <<
"] "
466 <<
"Association released: " << assoc.calling_ae() <<
"\n";
469 server.on_error([](
const std::string& error) {
470 std::cerr <<
"[" << current_timestamp() <<
"] "
471 <<
"Error: " <<
error <<
"\n";
475 auto result = server.start();
476 if (result.is_err()) {
477 std::cerr <<
"Failed to start server: " << result.error().message <<
"\n";
479 g_file_storage =
nullptr;
483 std::cout <<
"=================================================\n";
484 std::cout <<
" Storage SCP is running on port " << args.port <<
"\n";
485 std::cout <<
" Storage: " << args.storage_dir <<
"\n";
486 std::cout <<
" Press Ctrl+C to stop\n";
487 std::cout <<
"=================================================\n\n";
490 server.wait_for_shutdown();
493 auto server_stats = server.get_statistics();
494 auto storage_stats =
storage.get_statistics();
497 std::cout <<
"=================================================\n";
498 std::cout <<
" Server Statistics\n";
499 std::cout <<
"=================================================\n";
500 std::cout <<
" Total Associations: " << server_stats.total_associations <<
"\n";
501 std::cout <<
" Rejected Associations: " << server_stats.rejected_associations <<
"\n";
502 std::cout <<
" Messages Processed: " << server_stats.messages_processed <<
"\n";
503 std::cout <<
" Images Received: " << storage_service->images_received() <<
"\n";
504 std::cout <<
" Bytes Received: " << format_bytes(storage_service->bytes_received()) <<
"\n";
505 std::cout <<
" Uptime: " << server_stats.uptime().count() <<
" seconds\n";
506 std::cout <<
"=================================================\n";
507 std::cout <<
" Storage Statistics\n";
508 std::cout <<
"=================================================\n";
509 std::cout <<
" Total Instances: " << storage_stats.total_instances <<
"\n";
510 std::cout <<
" Total Size: " << format_bytes(storage_stats.total_bytes) <<
"\n";
511 std::cout <<
"=================================================\n";
514 g_file_storage =
nullptr;
520int main(
int argc,
char* argv[]) {
522 ____ _____ ___ ____ _____ ____ ____ ____
523 / ___|_ _/ _ \| _ \| ____| / ___| / ___| _ \
524 \___ \ | || | | | |_) | _| \___ \| | | |_) |
525 ___) || || |_| | _ <| |___ ___) | |___| __/
526 |____/ |_| \___/|_| \_\_____| |____/ \____|_|
528 DICOM Image Receiver Server
533 if (!parse_arguments(argc, argv, args)) {
534 print_usage(argv[0]);
539 install_signal_handlers();
541 bool success = run_server(args);
543 std::cout <<
"\nStorage SCP terminated\n";
544 return success ? 0 : 1;
DICOM Dataset - ordered collection of Data Elements.
DICOM Part 10 file handling for reading/writing DICOM files.
Multi-threaded DICOM server for handling multiple associations.
Compile-time constants for commonly used DICOM tags.
Filesystem-based DICOM storage with hierarchical organization.
@ error
Node returned an error.
@ ignore
Silently accept duplicate (return success)
@ reject
Reject duplicates with error status.
@ replace
Replace existing instance with new one.
@ storage
Storage Service Class.
naming_scheme
Naming scheme for DICOM file organization.
@ flat
{SOPUID}.dcm (flat structure)
@ date_hierarchical
YYYY/MM/DD/{StudyUID}/{SOPUID}.dcm.
@ uid_hierarchical
{StudyUID}/{SeriesUID}/{SOPUID}.dcm
duplicate_policy
Policy for handling duplicate SOP Instance UIDs.
@ ignore
Skip silently if instance exists.
@ reject
Return error if instance already exists.
@ replace
Overwrite existing instance.
DICOM Server configuration structures.
DICOM Storage SCP service (C-STORE handler)
Configuration for SCP to accept associations.
Configuration for file_storage.
bool create_directories
Create directories automatically if they don't exist.
duplicate_policy duplicate
How to handle duplicate instances.
std::filesystem::path root_path
Root directory for storage.
naming_scheme naming
File organization scheme.