PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
main.cpp File Reference

Level 3 Sample: Storage Server - DICOM image reception and archiving. More...

#include "console_utils.h"
#include "signal_handler.h"
#include <kcenon/pacs/core/dicom_dataset.h>
#include <kcenon/pacs/core/dicom_tag_constants.h>
#include <kcenon/pacs/network/dicom_server.h>
#include <kcenon/pacs/network/server_config.h>
#include <kcenon/pacs/services/storage_scp.h>
#include <kcenon/pacs/services/verification_scp.h>
#include <kcenon/pacs/storage/file_storage.h>
#include <kcenon/pacs/storage/index_database.h>
#include <atomic>
#include <chrono>
#include <cstdlib>
#include <filesystem>
#include <iomanip>
#include <iostream>
#include <memory>
#include <string>
Include dependency graph for main.cpp:

Go to the source code of this file.

Functions

int main (int argc, char *argv[])
 

Detailed Description

Level 3 Sample: Storage Server - DICOM image reception and archiving.

This sample demonstrates DICOM storage concepts:

  • Storage SCP configuration (accepting images from modalities)
  • File storage with hierarchical organization
  • Index database for fast metadata queries
  • Pre/post storage hooks for validation and workflow

After completing this sample, you will understand how to:

  1. Configure a DICOM storage server to receive images
  2. Organize DICOM files in a hierarchical directory structure
  3. Index metadata in SQLite for efficient queries
  4. Implement validation and post-processing hooks
See also
DICOM PS3.4 Section B - Storage Service Class
DICOM PS3.7 Section 9.1.1 - C-STORE Service

Definition in file main.cpp.

Function Documentation

◆ main()

int main ( int argc,
char * argv[] )

Definition at line 163 of file main.cpp.

163 {
164 kcenon::pacs::samples::print_header("Storage Server - Level 3 Sample");
165
166 // ===========================================================================
167 // Part 1: Storage Configuration
168 // ===========================================================================
169 // DICOM storage systems need several components:
170 // - Root directory for storing DICOM files
171 // - Naming scheme for file organization (UID-based hierarchy is common)
172 // - Duplicate handling policy (reject, replace, or ignore)
173 // - File extension for saved files
174
175 kcenon::pacs::samples::print_section("Part 1: Storage Configuration");
176
177 std::cout << "DICOM storage systems organize files hierarchically:\n";
178 std::cout << " - UID-based: {root}/{StudyUID}/{SeriesUID}/{SOPUID}.dcm\n";
179 std::cout << " - Date-based: {root}/YYYY/MM/DD/{StudyUID}/{SOPUID}.dcm\n";
180 std::cout << " - Flat: {root}/{SOPUID}.dcm\n\n";
181
182 // Create storage root directory
183 const std::filesystem::path storage_root = "./dicom_archive";
184 std::filesystem::create_directories(storage_root);
185
186 // Configure file storage
187 stg::file_storage_config fs_config;
188 fs_config.root_path = storage_root;
189 fs_config.naming = stg::naming_scheme::uid_hierarchical;
190 fs_config.duplicate = stg::duplicate_policy::replace;
191 fs_config.create_directories = true;
192 fs_config.file_extension = ".dcm";
193
194 kcenon::pacs::samples::print_table("File Storage Configuration", {
195 {"Root Path", storage_root.string()},
196 {"Naming Scheme", "uid_hierarchical"},
197 {"Duplicate Policy", "replace"},
198 {"File Extension", ".dcm"}
199 });
200
201 // Create file storage backend
202 auto file_storage = std::make_shared<stg::file_storage>(fs_config);
203
204 kcenon::pacs::samples::print_success("Part 1 complete - File storage configured!");
205
206 // ===========================================================================
207 // Part 2: Index Database
208 // ===========================================================================
209 // The index database enables fast queries without reading every DICOM file.
210 // It stores:
211 // - Patient demographics (name, ID, birth date, sex)
212 // - Study metadata (date, description, accession number)
213 // - Series metadata (modality, series number, description)
214 // - Instance references (SOP UID, file path, file size)
215
216 kcenon::pacs::samples::print_section("Part 2: Index Database");
217
218 std::cout << "Index database stores metadata for fast queries:\n";
219 std::cout << " - Patient demographics\n";
220 std::cout << " - Study/Series/Instance metadata\n";
221 std::cout << " - File paths for retrieval\n\n";
222
223 const auto db_path = storage_root / "index.db";
224
225 auto db_result = stg::index_database::open(db_path.string());
226 if (db_result.is_err()) {
227 kcenon::pacs::samples::print_error("Failed to open database: " +
228 db_result.error().message);
229 return 1;
230 }
231 auto index_db = std::move(db_result.value());
232
233 kcenon::pacs::samples::print_table("Database Configuration", {
234 {"Database Path", db_path.string()},
235 {"Storage Engine", "SQLite"},
236 {"Mode", "WAL (Write-Ahead Logging)"}
237 });
238
239 kcenon::pacs::samples::print_success("Part 2 complete - Database initialized!");
240
241 // ===========================================================================
242 // Part 3: Storage SCP Setup
243 // ===========================================================================
244 // Storage SCP (Service Class Provider) handles incoming C-STORE requests.
245 // Key configuration options:
246 // - Accepted SOP Classes: Which image types to accept (CT, MR, US, etc.)
247 // - Duplicate Policy: How to handle images that already exist
248 // - Handler callbacks: Custom logic for storage, validation, post-processing
249
250 kcenon::pacs::samples::print_section("Part 3: Storage SCP Setup");
251
252 std::cout << "Storage SCP handles C-STORE requests from modalities:\n";
253 std::cout << " 1. Pre-store handler: Validate incoming data\n";
254 std::cout << " 2. Storage handler: Store to filesystem + index\n";
255 std::cout << " 3. Post-store handler: Trigger workflows\n\n";
256
257 // Configure Storage SCP (empty = accept all standard storage SOP classes)
258 svc::storage_scp_config scp_config;
259 // scp_config.accepted_sop_classes = { ... }; // Leave empty to accept all
260
261 auto storage_scp = std::make_shared<svc::storage_scp>(scp_config);
262
263 // Statistics tracking
264 std::atomic<int> store_count{0};
265 std::atomic<int> reject_count{0};
266
267 // Main storage handler - called for each received image
268 storage_scp->set_handler(
269 [&](const core::dicom_dataset& dataset,
270 const std::string& calling_ae,
271 const std::string& sop_class_uid,
272 const std::string& sop_instance_uid) -> svc::storage_status {
273
274 std::cout << "\n[" << current_timestamp() << "] "
276 << kcenon::pacs::samples::colors::reset << " From: " << calling_ae << "\n";
277
278 // Extract key metadata for logging
279 std::cout << " Patient: " << dataset.get_string(tags::patient_name) << "\n";
280 std::cout << " Study: " << dataset.get_string(tags::study_description) << "\n";
281 std::cout << " Modality: " << dataset.get_string(tags::modality) << "\n";
282 std::cout << " SOP Class: " << sop_class_uid << "\n";
283
284 // Store to filesystem
285 auto store_result = file_storage->store(dataset);
286 if (store_result.is_err()) {
287 std::cout << " " << kcenon::pacs::samples::colors::red << "Storage failed: "
289 << store_result.error().message << "\n";
290 reject_count++;
291 return svc::storage_status::storage_error;
292 }
293
294 // Get the stored file path for database indexing
295 auto file_path = file_storage->get_file_path(sop_instance_uid);
296
297 // Update index database
298 if (!update_index(*index_db, dataset, file_path)) {
299 reject_count++;
300 return svc::storage_status::storage_error;
301 }
302
303 store_count++;
304 std::cout << " " << kcenon::pacs::samples::colors::green << "Stored"
305 << kcenon::pacs::samples::colors::reset << " (#" << store_count
306 << ") -> " << file_path.filename().string() << "\n";
307
308 return svc::storage_status::success;
309 }
310 );
311
312 // Pre-store validation handler (optional)
313 // Return false to reject the storage request
314 storage_scp->set_pre_store_handler(
315 [&reject_count](const core::dicom_dataset& dataset) -> bool {
316 // Example validation: Require Patient ID
317 if (dataset.get_string(tags::patient_id).empty()) {
318 std::cout << " " << kcenon::pacs::samples::colors::yellow << "[REJECTED]"
320 << " Missing Patient ID\n";
321 reject_count++;
322 return false;
323 }
324
325 // Example validation: Require Study Instance UID
326 if (dataset.get_string(tags::study_instance_uid).empty()) {
327 std::cout << " " << kcenon::pacs::samples::colors::yellow << "[REJECTED]"
329 << " Missing Study Instance UID\n";
330 reject_count++;
331 return false;
332 }
333
334 return true; // Accept the image
335 }
336 );
337
338 // Post-store notification handler (optional)
339 // Called after successful storage for workflow integration
340 storage_scp->set_post_store_handler(
341 [](const core::dicom_dataset& /*dataset*/,
342 const std::string& /*patient_id*/,
343 const std::string& /*study_uid*/,
344 const std::string& /*series_uid*/,
345 const std::string& /*sop_instance_uid*/) {
346 // Example: Trigger downstream processing
347 // - Send to AI analysis service
348 // - Notify worklist system
349 // - Queue for auto-routing
350 // For this sample, we just acknowledge the callback
351 }
352 );
353
354 kcenon::pacs::samples::print_table("Storage SCP Configuration", {
355 {"SOP Classes", "All standard storage classes"},
356 {"Duplicate Policy", "Replace existing"},
357 {"Pre-store Handler", "Validate Patient ID and Study UID"},
358 {"Post-store Handler", "Workflow notification"}
359 });
360
361 kcenon::pacs::samples::print_success("Part 3 complete - Storage SCP configured!");
362
363 // ===========================================================================
364 // Part 4: Server Startup
365 // ===========================================================================
366 // Create the DICOM server and register services:
367 // - Verification SCP (C-ECHO) for connectivity testing
368 // - Storage SCP (C-STORE) for receiving images
369
370 kcenon::pacs::samples::print_section("Part 4: Running Server");
371
372 // Parse optional port argument
373 uint16_t port = 11112;
374 if (argc > 1) {
375 try {
376 int port_arg = std::stoi(argv[1]);
377 if (port_arg > 0 && port_arg < 65536) {
378 port = static_cast<uint16_t>(port_arg);
379 }
380 } catch (const std::exception&) {
381 // Keep default port
382 }
383 }
384
385 // Configure server
386 net::server_config server_config;
387 server_config.ae_title = "STORE_SCP";
388 server_config.port = port;
389 server_config.max_associations = 20;
390 server_config.idle_timeout = std::chrono::seconds(60);
391 server_config.max_pdu_size = 65536; // 64KB for efficient image transfer
392 server_config.implementation_class_uid = "1.2.410.200001.1.1";
393 server_config.implementation_version_name = "PACS_SAMPLE_3.0";
394
395 // Create server and register services
396 auto server = std::make_unique<net::dicom_server>(server_config);
397 server->register_service(std::make_shared<svc::verification_scp>());
398 server->register_service(storage_scp);
399
400 // Connection tracking
401 std::atomic<int> active_connections{0};
402
403 server->on_association_established(
404 [&active_connections](const net::association& assoc) {
405 active_connections++;
406 std::cout << "[" << current_timestamp() << "] "
409 << assoc.calling_ae() << " -> " << assoc.called_ae()
410 << " (active: " << active_connections << ")\n";
411 }
412 );
413
414 server->on_association_released(
415 [&active_connections](const net::association& assoc) {
416 active_connections--;
417 std::cout << "[" << current_timestamp() << "] "
420 << assoc.calling_ae() << " disconnected"
421 << " (active: " << active_connections << ")\n";
422 }
423 );
424
425 server->on_error(
426 [](const std::string& error_msg) {
427 std::cerr << "[" << current_timestamp() << "] "
430 << error_msg << "\n";
431 }
432 );
433
434 // Install signal handler for graceful shutdown
435 kcenon::pacs::samples::scoped_signal_handler sig_handler([&server]() {
436 std::cout << "\n" << kcenon::pacs::samples::colors::yellow
437 << "Shutdown signal received..."
439 server->stop();
440 });
441
442 // Start the server
443 auto start_result = server->start();
444 if (start_result.is_err()) {
445 kcenon::pacs::samples::print_error("Failed to start server: " +
446 start_result.error().message);
447 return 1;
448 }
449
450 // Print server running banner
452 "Storage Server Running",
453 "",
454 "Test with DCMTK:",
455 " storescu -aec STORE_SCP localhost " + std::to_string(port) + " image.dcm",
456 "",
457 "Generate test data (from Level 1):",
458 " ./hello_dicom # Creates hello_dicom_output.dcm",
459 " storescu -aec STORE_SCP localhost " + std::to_string(port) +
460 " hello_dicom_output.dcm",
461 "",
462 "Verify connectivity:",
463 " echoscu -aec STORE_SCP localhost " + std::to_string(port),
464 "",
465 "Press Ctrl+C to stop"
466 });
467
468 // Wait for shutdown signal
469 sig_handler.wait();
470
471 // ===========================================================================
472 // Part 5: Statistics and Cleanup
473 // ===========================================================================
474 // Display final statistics and verify database contents.
475
476 kcenon::pacs::samples::print_section("Final Statistics");
477
478 // Server statistics
479 auto stats = server->get_statistics();
480 kcenon::pacs::samples::print_table("Server Statistics", {
481 {"Total Associations", std::to_string(stats.total_associations)},
482 {"Messages Processed", std::to_string(stats.messages_processed)},
483 {"Bytes Received", std::to_string(stats.bytes_received)},
484 {"Uptime", std::to_string(stats.uptime().count()) + " seconds"}
485 });
486
487 // Storage statistics
488 kcenon::pacs::samples::print_table("Storage Statistics", {
489 {"Images Stored", std::to_string(store_count.load())},
490 {"Images Rejected", std::to_string(reject_count.load())},
491 {"Storage Path", storage_root.string()}
492 });
493
494 // Database statistics
495 auto db_stats_result = index_db->get_storage_stats();
496 if (db_stats_result.is_ok()) {
497 auto db_stats = db_stats_result.value();
498 kcenon::pacs::samples::print_table("Database Statistics", {
499 {"Patients", std::to_string(db_stats.total_patients)},
500 {"Studies", std::to_string(db_stats.total_studies)},
501 {"Series", std::to_string(db_stats.total_series)},
502 {"Instances", std::to_string(db_stats.total_instances)},
503 {"Total File Size", std::to_string(db_stats.total_file_size) + " bytes"}
504 });
505 }
506
507 // Summary
509 "Congratulations! You have learned:",
510 "",
511 "1. File Storage - Hierarchical DICOM file organization",
512 "2. Index Database - SQLite-based metadata indexing",
513 "3. Storage SCP - Receiving images via C-STORE",
514 "4. Validation - Pre-store data validation hooks",
515 "5. Workflow - Post-store notification handlers",
516 "",
517 "Query the database:",
518 " sqlite3 " + db_path.string() + " \"SELECT * FROM patients;\"",
519 "",
520 "Next step: Level 4 - Mini PACS (Query/Retrieve)"
521 });
522
523 kcenon::pacs::samples::print_success("Storage Server terminated successfully.");
524
525 return 0;
526}
Represents a DICOM Dataset (ordered collection of Data Elements)
auto get_string(dicom_tag tag, std::string_view default_value="") const -> std::string
Get the string value of an element.
DICOM Association management class.
std::string_view calling_ae() const noexcept
Get calling AE title.
std::string_view called_ae() const noexcept
Get called AE title.
void wait()
Block until shutdown signal is received.
constexpr dicom_tag sop_class_uid
SOP Class UID.
constexpr const char * reset
Reset all formatting.
constexpr const char * red
constexpr const char * yellow
constexpr const char * green
constexpr const char * cyan
void print_error(std::string_view message)
Print an error message.
void print_table(std::string_view title, const std::vector< std::pair< std::string, std::string > > &rows)
Print a key-value table.
void print_header(std::string_view title)
Print a header/title with decorative border.
void print_success(std::string_view message)
Print a success message.
void print_section(std::string_view title)
Print a section header.
void print_box(const std::vector< std::string > &lines)
Print text in a decorative box.
storage_status
Storage operation status codes.
Configuration for DICOM server.
std::string ae_title
Application Entity Title for this server (16 chars max)
Configuration for Storage SCP service.
Definition storage_scp.h:49
Configuration for file_storage.
std::string file_extension
File extension for DICOM files.
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.

References kcenon::pacs::network::server_config::ae_title, kcenon::pacs::network::association::called_ae(), kcenon::pacs::network::association::calling_ae(), kcenon::pacs::storage::file_storage_config::create_directories, kcenon::pacs::samples::colors::cyan, kcenon::pacs::storage::file_storage_config::duplicate, kcenon::pacs::storage::file_storage_config::file_extension, kcenon::pacs::core::dicom_dataset::get_string(), kcenon::pacs::samples::colors::green, kcenon::pacs::storage::file_storage_config::naming, kcenon::pacs::samples::print_box(), kcenon::pacs::samples::print_error(), kcenon::pacs::samples::print_header(), kcenon::pacs::samples::print_section(), kcenon::pacs::samples::print_success(), kcenon::pacs::samples::print_table(), kcenon::pacs::samples::colors::red, kcenon::pacs::samples::colors::reset, kcenon::pacs::storage::file_storage_config::root_path, kcenon::pacs::samples::scoped_signal_handler::wait(), and kcenon::pacs::samples::colors::yellow.

Here is the call graph for this function: