163 {
165
166
167
168
169
170
171
172
173
174
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
183 const std::filesystem::path storage_root = "./dicom_archive";
184 std::filesystem::create_directories(storage_root);
185
186
189 fs_config.
naming = stg::naming_scheme::uid_hierarchical;
190 fs_config.
duplicate = stg::duplicate_policy::replace;
193
195 {"Root Path", storage_root.string()},
196 {"Naming Scheme", "uid_hierarchical"},
197 {"Duplicate Policy", "replace"},
198 {"File Extension", ".dcm"}
199 });
200
201
202 auto file_storage = std::make_shared<stg::file_storage>(fs_config);
203
205
206
207
208
209
210
211
212
213
214
215
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()) {
228 db_result.error().message);
229 return 1;
230 }
231 auto index_db = std::move(db_result.value());
232
234 {"Database Path", db_path.string()},
235 {"Storage Engine", "SQLite"},
236 {"Mode", "WAL (Write-Ahead Logging)"}
237 });
238
240
241
242
243
244
245
246
247
248
249
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
259
260
261 auto storage_scp = std::make_shared<svc::storage_scp>(scp_config);
262
263
264 std::atomic<int> store_count{0};
265 std::atomic<int> reject_count{0};
266
267
268 storage_scp->set_handler(
270 const std::string& calling_ae,
271 const std::string& sop_class_uid,
273
274 std::cout << "\n[" << current_timestamp() << "] "
277
278
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";
283
284
285 auto store_result = file_storage->store(dataset);
286 if (store_result.is_err()) {
289 << store_result.error().message << "\n";
290 reject_count++;
291 return svc::storage_status::storage_error;
292 }
293
294
295 auto file_path = file_storage->get_file_path(sop_instance_uid);
296
297
298 if (!update_index(*index_db, dataset, file_path)) {
299 reject_count++;
300 return svc::storage_status::storage_error;
301 }
302
303 store_count++;
306 << ") -> " << file_path.filename().string() << "\n";
307
308 return svc::storage_status::success;
309 }
310 );
311
312
313
314 storage_scp->set_pre_store_handler(
316
317 if (dataset.
get_string(tags::patient_id).empty()) {
320 << " Missing Patient ID\n";
321 reject_count++;
322 return false;
323 }
324
325
326 if (dataset.
get_string(tags::study_instance_uid).empty()) {
329 << " Missing Study Instance UID\n";
330 reject_count++;
331 return false;
332 }
333
334 return true;
335 }
336 );
337
338
339
340 storage_scp->set_post_store_handler(
342 const std::string& ,
343 const std::string& ,
344 const std::string& ,
345 const std::string& ) {
346
347
348
349
350
351 }
352 );
353
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
362
363
364
365
366
367
368
369
371
372
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
382 }
383 }
384
385
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";
394
395
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
401 std::atomic<int> active_connections{0};
402
403 server->on_association_established(
405 active_connections++;
406 std::cout << "[" << current_timestamp() << "] "
410 << " (active: " << active_connections << ")\n";
411 }
412 );
413
414 server->on_association_released(
416 active_connections--;
417 std::cout << "[" << current_timestamp() << "] "
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
437 << "Shutdown signal received..."
439 server->stop();
440 });
441
442
443 auto start_result = server->start();
444 if (start_result.is_err()) {
446 start_result.error().message);
447 return 1;
448 }
449
450
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
470
471
472
473
474
475
477
478
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"}
485 });
486
487
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
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"}
504 });
505 }
506
507
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
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 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.
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.