54std::string current_timestamp() {
55 auto now = std::chrono::system_clock::now();
56 auto time_t_now = std::chrono::system_clock::to_time_t(now);
57 auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
58 now.time_since_epoch()) % 1000;
60 std::ostringstream oss;
61 oss << std::put_time(std::localtime(&time_t_now),
"%H:%M:%S");
62 oss <<
'.' << std::setfill(
'0') << std::setw(3) << ms.count();
82 const std::filesystem::path& file_path) {
91 if (patient_result.is_err()) {
92 std::cerr <<
" Database error (patient): "
93 << patient_result.error().message <<
"\n";
96 int64_t patient_pk = patient_result.value();
106 ds.
get_string(tags::referring_physician_name),
110 if (study_result.is_err()) {
111 std::cerr <<
" Database error (study): "
112 << study_result.error().message <<
"\n";
115 int64_t study_pk = study_result.value();
130 if (series_result.is_err()) {
131 std::cerr <<
" Database error (series): "
132 << series_result.error().message <<
"\n";
135 int64_t series_pk = series_result.value();
138 auto file_size = std::filesystem::exists(file_path)
139 ?
static_cast<int64_t
>(std::filesystem::file_size(file_path))
152 if (instance_result.is_err()) {
153 std::cerr <<
" Database error (instance): "
154 << instance_result.error().message <<
"\n";
163int main(
int argc,
char* argv[]) {
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";
183 const std::filesystem::path storage_root =
"./dicom_archive";
184 std::filesystem::create_directories(storage_root);
189 fs_config.
naming = stg::naming_scheme::uid_hierarchical;
190 fs_config.
duplicate = stg::duplicate_policy::replace;
195 {
"Root Path", storage_root.string()},
196 {
"Naming Scheme",
"uid_hierarchical"},
197 {
"Duplicate Policy",
"replace"},
198 {
"File Extension",
".dcm"}
202 auto file_storage = std::make_shared<stg::file_storage>(fs_config);
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";
223 const auto db_path = storage_root /
"index.db";
225 auto db_result = stg::index_database::open(db_path.string());
226 if (db_result.is_err()) {
228 db_result.error().message);
231 auto index_db = std::move(db_result.value());
234 {
"Database Path", db_path.string()},
235 {
"Storage Engine",
"SQLite"},
236 {
"Mode",
"WAL (Write-Ahead Logging)"}
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";
261 auto storage_scp = std::make_shared<svc::storage_scp>(scp_config);
264 std::atomic<int> store_count{0};
265 std::atomic<int> reject_count{0};
268 storage_scp->set_handler(
270 const std::string& calling_ae,
271 const std::string& sop_class_uid,
274 std::cout <<
"\n[" << current_timestamp() <<
"] "
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";
285 auto store_result = file_storage->store(dataset);
286 if (store_result.is_err()) {
289 << store_result.error().message <<
"\n";
291 return svc::storage_status::storage_error;
295 auto file_path = file_storage->get_file_path(sop_instance_uid);
298 if (!update_index(*index_db, dataset, file_path)) {
300 return svc::storage_status::storage_error;
306 <<
") -> " << file_path.filename().string() <<
"\n";
308 return svc::storage_status::success;
314 storage_scp->set_pre_store_handler(
317 if (dataset.
get_string(tags::patient_id).empty()) {
320 <<
" Missing Patient ID\n";
326 if (dataset.
get_string(tags::study_instance_uid).empty()) {
329 <<
" Missing Study Instance UID\n";
340 storage_scp->set_post_store_handler(
345 const std::string& ) {
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"}
373 uint16_t port = 11112;
376 int port_arg = std::stoi(argv[1]);
377 if (port_arg > 0 && port_arg < 65536) {
378 port =
static_cast<uint16_t
>(port_arg);
380 }
catch (
const std::exception&) {
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;
392 server_config.implementation_class_uid =
"1.2.410.200001.1.1";
393 server_config.implementation_version_name =
"PACS_SAMPLE_3.0";
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);
401 std::atomic<int> active_connections{0};
403 server->on_association_established(
405 active_connections++;
406 std::cout <<
"[" << current_timestamp() <<
"] "
410 <<
" (active: " << active_connections <<
")\n";
414 server->on_association_released(
416 active_connections--;
417 std::cout <<
"[" << current_timestamp() <<
"] "
421 <<
" (active: " << active_connections <<
")\n";
426 [](
const std::string& error_msg) {
427 std::cerr <<
"[" << current_timestamp() <<
"] "
430 << error_msg <<
"\n";
437 <<
"Shutdown signal received..."
443 auto start_result = server->start();
444 if (start_result.is_err()) {
446 start_result.error().message);
452 "Storage Server Running",
455 " storescu -aec STORE_SCP localhost " + std::to_string(port) +
" image.dcm",
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",
462 "Verify connectivity:",
463 " echoscu -aec STORE_SCP localhost " + std::to_string(port),
465 "Press Ctrl+C to stop"
479 auto stats = server->get_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"}
489 {
"Images Stored", std::to_string(store_count.load())},
490 {
"Images Rejected", std::to_string(reject_count.load())},
491 {
"Storage Path", storage_root.string()}
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();
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"}
509 "Congratulations! You have learned:",
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",
517 "Query the database:",
518 " sqlite3 " + db_path.string() +
" \"SELECT * FROM patients;\"",
520 "Next step: Level 4 - Mini PACS (Query/Retrieve)"
Represents a DICOM Dataset (ordered collection of Data Elements)
auto get_numeric(dicom_tag tag) const -> std::optional< T >
Get the numeric value of an element.
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.
PACS index database manager.
auto upsert_instance(int64_t series_pk, std::string_view sop_uid, std::string_view sop_class_uid, std::string_view file_path, int64_t file_size, std::string_view transfer_syntax="", std::optional< int > instance_number=std::nullopt) -> Result< int64_t >
Insert or update an instance record.
auto upsert_patient(std::string_view patient_id, std::string_view patient_name="", std::string_view birth_date="", std::string_view sex="") -> Result< int64_t >
Insert or update a patient record.
auto upsert_series(int64_t study_pk, std::string_view series_uid, std::string_view modality="", std::optional< int > series_number=std::nullopt, std::string_view series_description="", std::string_view body_part_examined="", std::string_view station_name="") -> Result< int64_t >
Insert or update a series record.
auto upsert_study(int64_t patient_pk, std::string_view study_uid, std::string_view study_id="", std::string_view study_date="", std::string_view study_time="", std::string_view accession_number="", std::string_view referring_physician="", std::string_view study_description="") -> Result< int64_t >
Insert or update a study record.
Console output utilities for developer samples.
DICOM Dataset - ordered collection of Data Elements.
Multi-threaded DICOM server for handling multiple associations.
Compile-time constants for commonly used DICOM tags.
Filesystem-based DICOM storage with hierarchical organization.
PACS index database for metadata storage and retrieval.
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.
DICOM Server configuration structures.
Cross-platform signal handling for graceful shutdown.
DICOM Storage SCP service (C-STORE handler)
Configuration for DICOM server.
std::string ae_title
Application Entity Title for this server (16 chars max)
Configuration for Storage SCP service.
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.
DICOM Verification SCP service (C-ECHO handler)