PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
server_app.cpp
Go to the documentation of this file.
1
6#include "server_app.h"
7
10
11#include <chrono>
12#include <filesystem>
13#include <iostream>
14
15namespace kcenon::pacs::example {
16
17namespace {
18
20storage::naming_scheme parse_naming_scheme(const std::string& scheme) {
21 if (scheme == "flat") {
23 }
24 if (scheme == "date_hierarchical") {
26 }
28}
29
31storage::duplicate_policy parse_duplicate_policy(const std::string& policy) {
32 if (policy == "replace") {
34 }
35 if (policy == "ignore") {
37 }
39}
40
42std::string log_prefix() {
43 auto now = std::chrono::system_clock::now();
44 auto time_t = std::chrono::system_clock::to_time_t(now);
45 char buf[32];
46 std::strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", std::localtime(&time_t));
47 return std::string("[") + buf + "] ";
48}
49
50} // namespace
51
52// =============================================================================
53// Construction / Destruction
54// =============================================================================
55
57 : config_(config) {}
58
62
63// =============================================================================
64// Lifecycle Management
65// =============================================================================
66
68 std::cout << log_prefix() << "Initializing PACS Server...\n";
69
70 if (!setup_storage()) {
71 return false;
72 }
73
74 if (!setup_database()) {
75 return false;
76 }
77
78 if (!setup_services()) {
79 return false;
80 }
81
82 if (!setup_server()) {
83 return false;
84 }
85
86 initialized_ = true;
87 std::cout << log_prefix() << "PACS Server initialized successfully\n";
88 return true;
89}
90
92 if (!initialized_) {
93 std::cerr << log_prefix() << "Error: Server not initialized\n";
94 return false;
95 }
96
97 std::cout << log_prefix() << "Starting DICOM server...\n";
98 std::cout << log_prefix() << " AE Title: " << config_.server.ae_title << "\n";
99 std::cout << log_prefix() << " Port: " << config_.server.port << "\n";
100 std::cout << log_prefix() << " Max Associations: " << config_.server.max_associations << "\n";
101
102 auto result = server_->start();
103 if (result.is_err()) {
104 std::cerr << log_prefix() << "Error: Failed to start server\n";
105 return false;
106 }
107
108 std::cout << log_prefix() << "PACS Server started successfully\n";
109 std::cout << log_prefix() << "Listening on port " << config_.server.port << "...\n";
110 std::cout << log_prefix() << "Press Ctrl+C to stop\n";
111
112 return true;
113}
114
116 if (server_ && server_->is_running()) {
117 std::cout << log_prefix() << "Stopping DICOM server...\n";
118 server_->stop();
119 std::cout << log_prefix() << "DICOM server stopped\n";
120 }
121}
122
124 if (server_) {
125 server_->wait_for_shutdown();
126 }
127}
128
133
134bool pacs_server_app::is_running() const noexcept {
135 return server_ && server_->is_running();
136}
137
139 if (!server_) {
140 return;
141 }
142
143 auto stats = server_->get_statistics();
144 auto uptime = stats.uptime();
145
146 std::cout << "\n";
147 std::cout << "=== PACS Server Statistics ===\n";
148 std::cout << "Uptime: " << uptime.count() << " seconds\n";
149 std::cout << "Total Associations: " << stats.total_associations << "\n";
150 std::cout << "Active Associations: " << stats.active_associations << "\n";
151 std::cout << "Rejected Associations: " << stats.rejected_associations << "\n";
152 std::cout << "Messages Processed: " << stats.messages_processed << "\n";
153 std::cout << "Bytes Received: " << stats.bytes_received << "\n";
154 std::cout << "Bytes Sent: " << stats.bytes_sent << "\n";
155 std::cout << "==============================\n";
156 std::cout << "\n";
157}
158
159// =============================================================================
160// Private Setup Methods
161// =============================================================================
162
164 std::cout << log_prefix() << "Setting up file storage...\n";
165 std::cout << log_prefix() << " Directory: " << config_.storage.directory << "\n";
166
167 // Create storage directory if it doesn't exist
168 std::error_code ec;
169 std::filesystem::create_directories(config_.storage.directory, ec);
170 if (ec) {
171 std::cerr << log_prefix() << "Error: Failed to create storage directory: "
172 << ec.message() << "\n";
173 return false;
174 }
175
178 storage_config.naming = parse_naming_scheme(config_.storage.naming);
179 storage_config.duplicate = parse_duplicate_policy(config_.storage.duplicate_policy);
180 storage_config.create_directories = true;
181
182 try {
183 file_storage_ = std::make_unique<storage::file_storage>(storage_config);
184 } catch (const std::exception& e) {
185 std::cerr << log_prefix() << "Error: Failed to create file storage: "
186 << e.what() << "\n";
187 return false;
188 }
189
190 std::cout << log_prefix() << "File storage ready\n";
191 return true;
192}
193
195 std::cout << log_prefix() << "Setting up database...\n";
196 std::cout << log_prefix() << " Path: " << config_.database.path << "\n";
197
198 // Create database directory if needed
199 auto db_dir = config_.database.path.parent_path();
200 if (!db_dir.empty()) {
201 std::error_code ec;
202 std::filesystem::create_directories(db_dir, ec);
203 if (ec) {
204 std::cerr << log_prefix() << "Error: Failed to create database directory: "
205 << ec.message() << "\n";
206 return false;
207 }
208 }
209
210 storage::index_config db_config;
211 db_config.wal_mode = config_.database.wal_mode;
212
213 auto result = storage::index_database::open(
214 config_.database.path.string(), db_config);
215
216 if (result.is_err()) {
217 std::cerr << log_prefix() << "Error: Failed to open database\n";
218 return false;
219 }
220
221 database_ = std::move(result.value());
222 std::cout << log_prefix() << "Database ready\n";
223 return true;
224}
225
227 std::cout << log_prefix() << "Setting up DICOM services...\n";
228 std::cout << log_prefix() << " - Verification SCP (C-ECHO)\n";
229 std::cout << log_prefix() << " - Storage SCP (C-STORE)\n";
230 std::cout << log_prefix() << " - Query SCP (C-FIND)\n";
231 std::cout << log_prefix() << " - Retrieve SCP (C-MOVE/C-GET)\n";
232 std::cout << log_prefix() << " - Worklist SCP (MWL)\n";
233 std::cout << log_prefix() << " - MPPS SCP (N-CREATE/N-SET)\n";
234 std::cout << log_prefix() << "All DICOM services configured\n";
235 return true;
236}
237
239 std::cout << log_prefix() << "Setting up DICOM server...\n";
240
241 network::server_config server_config;
242 server_config.ae_title = config_.server.ae_title;
243 server_config.port = config_.server.port;
244 server_config.max_associations = config_.server.max_associations;
245 server_config.idle_timeout = config_.server.idle_timeout;
246
247 // Set up AE whitelist if configured
249 server_config.ae_whitelist = config_.access_control.allowed_ae_titles;
250 server_config.accept_unknown_calling_ae = false;
251 }
252
253 server_ = std::make_unique<network::dicom_server>(server_config);
254
255 // Register Verification SCP
256 server_->register_service(std::make_shared<services::verification_scp>());
257
258 // Register Storage SCP
259 auto storage_scp = std::make_shared<services::storage_scp>();
260 storage_scp->set_handler(
261 [this](const auto& ds, const auto& ae, const auto& sop_class, const auto& sop_uid) {
262 return handle_store(ds, ae, sop_class, sop_uid);
263 });
264 server_->register_service(storage_scp);
265
266 // Register Query SCP
267 auto query_scp = std::make_shared<services::query_scp>();
268 query_scp->set_handler(
269 [this](auto level, const auto& keys, const auto& ae) {
270 return handle_query(level, keys, ae);
271 });
272 server_->register_service(query_scp);
273
274 // Register Retrieve SCP
275 auto retrieve_scp = std::make_shared<services::retrieve_scp>();
276 retrieve_scp->set_retrieve_handler(
277 [this](const auto& keys) {
278 return handle_retrieve(keys);
279 });
280 server_->register_service(retrieve_scp);
281
282 // Register Worklist SCP
283 auto worklist_scp = std::make_shared<services::worklist_scp>();
284 worklist_scp->set_handler(
285 [this](const auto& keys, const auto& ae) {
286 return handle_worklist_query(keys, ae);
287 });
288 server_->register_service(worklist_scp);
289
290 // Register MPPS SCP
291 auto mpps_scp = std::make_shared<services::mpps_scp>();
292 mpps_scp->set_create_handler(
293 [this](const auto& instance) {
294 return handle_mpps_create(instance);
295 });
296 mpps_scp->set_set_handler(
297 [this](const auto& uid, const auto& mods, auto status) {
298 return handle_mpps_set(uid, mods, status);
299 });
300 server_->register_service(mpps_scp);
301
302 // Set up callbacks
303 server_->on_association_established([](const network::association& assoc) {
304 std::cout << log_prefix() << "Association established: "
305 << assoc.calling_ae() << " -> " << assoc.called_ae() << "\n";
306 });
307
308 server_->on_association_released([](const network::association& assoc) {
309 std::cout << log_prefix() << "Association released: "
310 << assoc.calling_ae() << "\n";
311 });
312
313 server_->on_error([](const std::string& error) {
314 std::cerr << log_prefix() << "Server error: " << error << "\n";
315 });
316
317 std::cout << log_prefix() << "DICOM server configured\n";
318 return true;
319}
320
321// =============================================================================
322// Service Handlers
323// =============================================================================
324
326 const core::dicom_dataset& dataset,
327 const std::string& calling_ae,
328 const std::string& sop_class_uid,
329 const std::string& sop_instance_uid) {
330
331 std::cout << log_prefix() << "C-STORE from " << calling_ae
332 << ": " << sop_instance_uid << "\n";
333
334 // Store to filesystem
335 auto store_result = file_storage_->store(dataset);
336 if (store_result.is_err()) {
337 std::cerr << log_prefix() << "Storage error\n";
339 }
340
341 // Index in database
342 auto patient_id = dataset.get_string(core::tags::patient_id);
343 auto patient_name = dataset.get_string(core::tags::patient_name);
344 auto study_uid = dataset.get_string(core::tags::study_instance_uid);
345 auto series_uid = dataset.get_string(core::tags::series_instance_uid);
346
347 if (patient_id.empty()) {
348 std::cerr << log_prefix() << "Warning: Missing PatientID\n";
350 }
351
352 // Upsert patient and get pk
353 auto birth_date = dataset.get_string(core::tags::patient_birth_date);
354 auto sex = dataset.get_string(core::tags::patient_sex);
355 auto patient_result = database_->upsert_patient(
356 patient_id,
357 patient_name,
358 birth_date,
359 sex
360 );
361 if (patient_result.is_err()) {
362 std::cerr << log_prefix() << "Database error (patient)\n";
364 }
365 int64_t patient_pk = patient_result.value();
366
367 // Upsert study if we have study_uid
368 int64_t study_pk = 0;
369 if (!study_uid.empty()) {
370 auto study_date = dataset.get_string(core::tags::study_date);
371 auto study_time = dataset.get_string(core::tags::study_time);
372 auto accession = dataset.get_string(core::tags::accession_number);
373 auto study_desc = dataset.get_string(core::tags::study_description);
374 auto referring = dataset.get_string(core::tags::referring_physician_name);
375 auto study_id = dataset.get_string(core::tags::study_id);
376
377 auto study_result = database_->upsert_study(
378 patient_pk,
379 study_uid,
380 study_id,
381 study_date,
382 study_time,
383 accession,
384 referring,
385 study_desc
386 );
387 if (study_result.is_err()) {
388 std::cerr << log_prefix() << "Database error (study)\n";
390 }
391 study_pk = study_result.value();
392 }
393
394 // Upsert series if we have series_uid and study_pk
395 int64_t series_pk = 0;
396 if (!series_uid.empty() && study_pk > 0) {
397 auto modality = dataset.get_string(core::tags::modality);
398 auto series_number_str = dataset.get_string(core::tags::series_number);
399 auto series_desc = dataset.get_string(core::tags::series_description);
400
401 std::optional<int> series_number;
402 if (!series_number_str.empty()) {
403 try {
404 series_number = std::stoi(series_number_str);
405 } catch (...) {}
406 }
407
408 auto series_result = database_->upsert_series(
409 study_pk,
410 series_uid,
411 modality,
412 series_number,
413 series_desc,
414 "", // body_part_examined
415 "" // station_name
416 );
417 if (series_result.is_err()) {
418 std::cerr << log_prefix() << "Database error (series)\n";
420 }
421 series_pk = series_result.value();
422 }
423
424 // Upsert instance if we have series_pk
425 if (series_pk > 0) {
426 auto instance_number_str = dataset.get_string(core::tags::instance_number);
427 auto file_path = file_storage_->get_file_path(sop_instance_uid);
428
429 int64_t file_size = 0;
430 if (std::filesystem::exists(file_path)) {
431 file_size = static_cast<int64_t>(std::filesystem::file_size(file_path));
432 }
433
434 std::optional<int> instance_number;
435 if (!instance_number_str.empty()) {
436 try {
437 instance_number = std::stoi(instance_number_str);
438 } catch (...) {}
439 }
440
441 auto instance_result = database_->upsert_instance(
442 series_pk,
443 sop_instance_uid,
444 sop_class_uid,
445 file_path.string(),
446 file_size,
447 "", // transfer_syntax
448 instance_number
449 );
450 if (instance_result.is_err()) {
451 std::cerr << log_prefix() << "Database error (instance)\n";
452 }
453 }
454
456}
457
458std::vector<core::dicom_dataset> pacs_server_app::handle_query(
460 const core::dicom_dataset& query_keys,
461 const std::string& calling_ae) {
462
463 std::cout << log_prefix() << "C-FIND from " << calling_ae
464 << " at level " << services::to_string(level) << "\n";
465
466 std::vector<core::dicom_dataset> results;
467
468 switch (level) {
471 auto id = query_keys.get_string(core::tags::patient_id);
472 if (!id.empty()) {
473 query.patient_id = id;
474 }
475 auto name = query_keys.get_string(core::tags::patient_name);
476 if (!name.empty()) {
477 query.patient_name = name;
478 }
479
480 auto patients_result = database_->search_patients(query);
481 if (patients_result.is_ok()) {
482 for (const auto& patient : patients_result.value()) {
488 results.push_back(std::move(ds));
489 }
490 }
491 break;
492 }
493
496 auto id = query_keys.get_string(core::tags::patient_id);
497 if (!id.empty()) {
498 query.patient_id = id;
499 }
501 if (!uid.empty()) {
502 query.study_uid = uid;
503 }
504 auto date = query_keys.get_string(core::tags::study_date);
505 if (!date.empty()) {
506 query.study_date = date;
507 }
508
509 auto studies_result = database_->search_studies(query);
510 if (studies_result.is_ok()) {
511 for (const auto& study : studies_result.value()) {
518 results.push_back(std::move(ds));
519 }
520 }
521 break;
522 }
523
527 if (!uid.empty()) {
528 query.study_uid = uid;
529 }
530 auto mod = query_keys.get_string(core::tags::modality);
531 if (!mod.empty()) {
532 query.modality = mod;
533 }
534
535 auto series_result = database_->search_series(query);
536 if (series_result.is_ok()) {
537 for (const auto& series : series_result.value()) {
541 if (series.series_number.has_value()) {
543 std::to_string(series.series_number.value()));
544 }
546 results.push_back(std::move(ds));
547 }
548 }
549 break;
550 }
551
555 if (!uid.empty()) {
556 query.series_uid = uid;
557 }
558
559 auto instances_result = database_->search_instances(query);
560 if (instances_result.is_ok()) {
561 for (const auto& instance : instances_result.value()) {
565 if (instance.instance_number.has_value()) {
567 std::to_string(instance.instance_number.value()));
568 }
569 results.push_back(std::move(ds));
570 }
571 }
572 break;
573 }
574 }
575
576 std::cout << log_prefix() << " Found " << results.size() << " matches\n";
577 return results;
578}
579
580std::vector<core::dicom_file> pacs_server_app::handle_retrieve(
581 const core::dicom_dataset& query_keys) {
582
583 std::cout << log_prefix() << "C-MOVE/C-GET retrieve request\n";
584
585 std::vector<core::dicom_file> files;
586 std::vector<std::string> file_paths;
587
588 // Determine query level and get file paths
589 auto sop_uid = query_keys.get_string(core::tags::sop_instance_uid);
590 if (!sop_uid.empty()) {
591 // Instance level
592 auto path_result = database_->get_file_path(sop_uid);
593 if (path_result.is_ok() && path_result.value().has_value()) {
594 file_paths.push_back(path_result.value().value());
595 }
596 } else {
597 auto series_uid = query_keys.get_string(core::tags::series_instance_uid);
598 if (!series_uid.empty()) {
599 // Series level
600 auto series_files_result = database_->get_series_files(series_uid);
601 if (series_files_result.is_ok()) {
602 file_paths = std::move(series_files_result.value());
603 }
604 } else {
605 auto study_uid = query_keys.get_string(core::tags::study_instance_uid);
606 if (!study_uid.empty()) {
607 // Study level
608 auto study_files_result = database_->get_study_files(study_uid);
609 if (study_files_result.is_ok()) {
610 file_paths = std::move(study_files_result.value());
611 }
612 }
613 }
614 }
615
616 // Load files
617 for (const auto& path : file_paths) {
618 auto file_result = core::dicom_file::open(path);
619 if (file_result.is_ok()) {
620 files.push_back(std::move(file_result.value()));
621 }
622 }
623
624 std::cout << log_prefix() << " Found " << files.size() << " files to transfer\n";
625 return files;
626}
627
628std::vector<core::dicom_dataset> pacs_server_app::handle_worklist_query(
629 const core::dicom_dataset& query_keys,
630 const std::string& calling_ae) {
631
632 std::cout << log_prefix() << "MWL query from " << calling_ae << "\n";
633
634 std::vector<core::dicom_dataset> results;
635
637 auto id = query_keys.get_string(core::tags::patient_id);
638 if (!id.empty()) {
639 query.patient_id = id;
640 }
641
642 auto items_result = database_->query_worklist(query);
643
644 if (items_result.is_ok()) {
645 for (const auto& item : items_result.value()) {
651 results.push_back(std::move(ds));
652 }
653 }
654
655 std::cout << log_prefix() << " Found " << results.size() << " worklist items\n";
656 return results;
657}
658
660 const services::mpps_instance& instance) {
661
662 std::cout << log_prefix() << "MPPS N-CREATE: " << instance.sop_instance_uid << "\n";
663
664 auto result = database_->create_mpps(
665 instance.sop_instance_uid,
666 instance.station_ae
667 );
668
669 if (result.is_err()) {
670 return kcenon::common::make_error<std::monostate>(1, "MPPS creation failed");
671 }
672
673 return network::Result<std::monostate>(std::monostate{});
674}
675
677 const std::string& sop_instance_uid,
678 [[maybe_unused]] const core::dicom_dataset& modifications,
679 services::mpps_status new_status) {
680
681 std::cout << log_prefix() << "MPPS N-SET: " << sop_instance_uid
682 << " -> " << services::to_string(new_status) << "\n";
683
684 auto result = database_->update_mpps(
685 sop_instance_uid,
686 std::string(services::to_string(new_status))
687 );
688
689 if (result.is_err()) {
690 return kcenon::common::make_error<std::monostate>(1, "MPPS update failed");
691 }
692
693 return network::Result<std::monostate>(std::monostate{});
694}
695
696} // namespace kcenon::pacs::example
void set_string(dicom_tag tag, encoding::vr_type vr, std::string_view value)
Set a string value for the given tag.
auto get_string(dicom_tag tag, std::string_view default_value="") const -> std::string
Get the string value of an element.
static auto open(const std::filesystem::path &path) -> kcenon::pacs::Result< dicom_file >
Open and read a DICOM file from disk.
std::unique_ptr< network::dicom_server > server_
DICOM server.
Definition server_app.h:236
std::vector< core::dicom_file > handle_retrieve(const core::dicom_dataset &query_keys)
Handle C-MOVE/C-GET retrieve.
bool setup_server()
Set up DICOM server.
network::Result< std::monostate > handle_mpps_create(const services::mpps_instance &instance)
Handle MPPS N-CREATE.
bool setup_storage()
Set up file storage.
pacs_server_app(const pacs_server_config &config)
Construct server application with configuration.
bool start()
Start the DICOM server.
bool initialized_
Initialization flag.
Definition server_app.h:248
std::unique_ptr< storage::index_database > database_
Index database.
Definition server_app.h:242
bool setup_services()
Set up DICOM services.
~pacs_server_app()
Destructor - stops server if running.
std::vector< core::dicom_dataset > handle_worklist_query(const core::dicom_dataset &query_keys, const std::string &calling_ae)
Handle worklist query.
services::storage_status handle_store(const core::dicom_dataset &dataset, const std::string &calling_ae, const std::string &sop_class_uid, const std::string &sop_instance_uid)
Handle incoming C-STORE request.
bool is_running() const noexcept
Check if server is running.
std::atomic< bool > shutdown_requested_
Shutdown flag.
Definition server_app.h:245
void print_statistics() const
Get current server statistics.
void stop()
Stop the server gracefully.
network::Result< std::monostate > handle_mpps_set(const std::string &sop_instance_uid, const core::dicom_dataset &modifications, services::mpps_status new_status)
Handle MPPS N-SET.
std::vector< core::dicom_dataset > handle_query(services::query_level level, const core::dicom_dataset &query_keys, const std::string &calling_ae)
Handle C-FIND query.
bool initialize()
Initialize all components.
pacs_server_config config_
Server configuration.
Definition server_app.h:233
std::unique_ptr< storage::file_storage > file_storage_
File storage.
Definition server_app.h:239
void wait_for_shutdown()
Wait for server shutdown.
void request_shutdown()
Request shutdown.
bool setup_database()
Set up database.
std::string_view calling_ae() const noexcept
Get calling AE title.
std::string_view called_ae() const noexcept
Get called AE title.
static auto open(std::string_view db_path) -> Result< std::unique_ptr< index_database > >
Open or create a database with default configuration.
Compile-time constants for commonly used DICOM tags.
constexpr dicom_tag referring_physician_name
Referring Physician's Name.
constexpr dicom_tag study_description
Study Description.
constexpr dicom_tag patient_id
Patient ID.
constexpr dicom_tag sop_instance_uid
SOP Instance UID.
constexpr dicom_tag patient_birth_date
Patient's Birth Date.
constexpr dicom_tag accession_number
Accession Number.
constexpr dicom_tag modality
Modality.
constexpr dicom_tag study_time
Study Time.
constexpr dicom_tag patient_sex
Patient's Sex.
constexpr dicom_tag study_instance_uid
Study Instance UID.
constexpr dicom_tag series_number
Series Number.
constexpr dicom_tag series_description
Series Description.
constexpr dicom_tag sop_class_uid
SOP Class UID.
constexpr dicom_tag study_id
Study ID.
constexpr dicom_tag patient_name
Patient's Name.
constexpr dicom_tag study_date
Study Date.
constexpr dicom_tag series_instance_uid
Series Instance UID.
constexpr dicom_tag instance_number
Instance Number.
@ DA
Date (8 chars, format: YYYYMMDD)
@ IS
Integer String (12 chars max)
@ LO
Long String (64 chars max)
@ UI
Unique Identifier (64 chars max)
@ PN
Person Name (64 chars max per component group)
@ CS
Code String (16 chars max, uppercase + digits + space + underscore)
@ TM
Time (14 chars max, format: HHMMSS.FFFFFF)
@ SH
Short String (16 chars max)
kcenon::pacs::Result< T > Result
Result type alias using standardized kcenon::pacs::Result<T>
Definition association.h:56
storage_status
Storage operation status codes.
@ storage_error
Failure: Unable to process - storage error (0xC001)
@ success
Success - image stored successfully (0x0000)
mpps_status
MPPS status enumeration.
Definition mpps_scp.h:48
auto to_string(mpps_status status) -> std::string_view
Convert mpps_status to DICOM string representation.
Definition mpps_scp.h:60
query_level
DICOM Query/Retrieve level enumeration.
Definition query_scp.h:63
@ study
Study level - query study information.
@ image
Image (Instance) level - query instance information.
@ patient
Patient level - query patient demographics.
@ series
Series level - query series information.
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.
PACS Server application class.
Storage SCP status codes for C-STORE operations.
std::vector< std::string > allowed_ae_titles
Allowed AE titles (empty = accept all)
Definition config.h:84
bool wal_mode
Enable WAL (Write-Ahead Logging) mode for better concurrency.
Definition config.h:62
std::filesystem::path path
Path to SQLite database file.
Definition config.h:59
Complete PACS server configuration.
Definition config.h:90
database_config database
Database settings.
Definition config.h:98
access_control_config access_control
Access control settings.
Definition config.h:104
server_network_config server
Server network settings.
Definition config.h:92
storage_config storage
Storage settings.
Definition config.h:95
uint16_t port
Port to listen on.
Definition config.h:31
std::chrono::seconds idle_timeout
Idle timeout for associations in seconds (0 = no timeout)
Definition config.h:37
std::string ae_title
Application Entity Title for this server (max 16 chars)
Definition config.h:28
size_t max_associations
Maximum concurrent associations (0 = unlimited)
Definition config.h:34
Storage configuration.
Definition config.h:43
std::filesystem::path directory
Root directory for DICOM file storage.
Definition config.h:45
std::string duplicate_policy
Duplicate handling policy: "reject", "replace", "ignore".
Definition config.h:51
std::string naming
File naming scheme: "hierarchical" or "flat".
Definition config.h:48
std::string ae_title
Application Entity Title for this server (16 chars max)
MPPS instance data structure.
Definition mpps_scp.h:98
std::string sop_instance_uid
SOP Instance UID - unique identifier for this MPPS.
Definition mpps_scp.h:100
std::string station_ae
Performing station AE Title.
Definition mpps_scp.h:106
Configuration for file_storage.
Configuration for index database.
bool wal_mode
Enable WAL (Write-Ahead Logging) mode for better concurrency.
std::string_view uid
std::string_view name