PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
main.cpp
Go to the documentation of this file.
1
29
30#include <algorithm>
31#include <chrono>
32#include <cstdlib>
33#include <filesystem>
34#include <fstream>
35#include <iomanip>
36#include <iostream>
37#include <map>
38#include <numeric>
39#include <sstream>
40#include <string>
41#include <vector>
42
43namespace {
44
45// =============================================================================
46// Constants
47// =============================================================================
48
50constexpr const char* version_string = "1.0.0";
51
53constexpr const char* default_calling_ae = "STORESCU";
54
56constexpr const char* default_called_ae = "ANY-SCP";
57
59constexpr auto default_connection_timeout = std::chrono::seconds{30};
60
62constexpr auto default_acse_timeout = std::chrono::seconds{30};
63
65constexpr auto default_dimse_timeout = std::chrono::seconds{0};
66
68constexpr size_t max_ae_title_length = 16;
69
71constexpr size_t default_max_pdu_size = 16384;
72
73// =============================================================================
74// Transfer Syntax Constants
75// =============================================================================
76
78namespace ts {
79constexpr const char* implicit_vr_le = "1.2.840.10008.1.2";
80constexpr const char* explicit_vr_le = "1.2.840.10008.1.2.1";
81constexpr const char* explicit_vr_be = "1.2.840.10008.1.2.2";
82constexpr const char* jpeg_baseline = "1.2.840.10008.1.2.4.50";
83constexpr const char* jpeg_extended = "1.2.840.10008.1.2.4.51";
84constexpr const char* jpeg_lossless = "1.2.840.10008.1.2.4.70";
85constexpr const char* jpeg2000_lossless = "1.2.840.10008.1.2.4.90";
86constexpr const char* jpeg2000_lossy = "1.2.840.10008.1.2.4.91";
87constexpr const char* rle = "1.2.840.10008.1.2.5";
88} // namespace ts
89
90// =============================================================================
91// Output Modes
92// =============================================================================
93
97enum class verbosity_level {
98 quiet,
99 normal,
100 verbose,
101 debug
102};
103
107enum class transfer_syntax_mode {
108 prefer_lossless,
109 propose_implicit,
110 propose_explicit,
111 propose_all
112};
113
114// =============================================================================
115// Command Line Options
116// =============================================================================
117
121struct options {
122 // Network options
123 std::string peer_host;
124 uint16_t peer_port{0};
125 std::string calling_ae_title{default_calling_ae};
126 std::string called_ae_title{default_called_ae};
127
128 // Timeout options
129 std::chrono::seconds connection_timeout{default_connection_timeout};
130 std::chrono::seconds acse_timeout{default_acse_timeout};
131 std::chrono::seconds dimse_timeout{default_dimse_timeout};
132
133 // Input files/directories
134 std::vector<std::filesystem::path> input_paths;
135 bool recursive{false};
136 std::string scan_pattern{"*.dcm"};
137
138 // Transfer syntax options
139 transfer_syntax_mode ts_mode{transfer_syntax_mode::propose_all};
140
141 // Batch options
142 bool continue_on_error{true};
143 size_t max_pdu_size{default_max_pdu_size};
144
145 // Progress options
146 bool show_progress{false};
147 std::string report_file;
148
149 // Output options
150 verbosity_level verbosity{verbosity_level::normal};
151
152 // TLS options (for future extension)
153 bool use_tls{false};
154 std::string tls_cert_file;
155 std::string tls_key_file;
156 std::string tls_ca_file;
157
158 // Help/version flags
159 bool show_help{false};
160 bool show_version{false};
161};
162
166struct file_store_result {
167 std::filesystem::path file_path;
168 std::string sop_class_uid;
169 std::string sop_instance_uid;
170 bool success{false};
171 uint16_t status_code{0};
172 std::string error_message;
173 size_t file_size{0};
174 std::chrono::milliseconds transfer_time{0};
175};
176
180struct store_statistics {
181 size_t total_files{0};
182 size_t successful{0};
183 size_t warnings{0};
184 size_t failed{0};
185 size_t total_bytes{0};
186 std::chrono::milliseconds total_time{0};
187 std::chrono::milliseconds association_time{0};
188
189 [[nodiscard]] double success_rate() const {
190 return total_files > 0
191 ? (static_cast<double>(successful) / total_files) * 100.0
192 : 0.0;
193 }
194
195 [[nodiscard]] double throughput_mbps() const {
196 if (total_time.count() == 0) return 0.0;
197 double bytes_per_sec =
198 static_cast<double>(total_bytes) / (total_time.count() / 1000.0);
199 return bytes_per_sec / (1024.0 * 1024.0);
200 }
201};
202
203// =============================================================================
204// Output Functions
205// =============================================================================
206
210void print_banner() {
211 std::cout << R"(
212 ____ _____ ___ ____ _____ ____ ____ _ _
213 / ___|_ _/ _ \| _ \| ____| / ___| / ___| | | |
214 \___ \ | || | | | |_) | _| \___ \| | | | | |
215 ___) || || |_| | _ <| |___ ___) | |___| |_| |
216 |____/ |_| \___/|_| \_\_____| |____/ \____|\___/
217
218 DICOM Image Sender v)" << version_string
219 << R"(
220)" << "\n";
221}
222
227void print_usage(const char* program_name) {
228 std::cout << "Usage: " << program_name
229 << R"( [options] <peer> <port> <dcmfile-in> [dcmfile-in...]
230
231Arguments:
232 peer Remote host address (IP or hostname)
233 port Remote port number (typically 104 or 11112)
234 dcmfile-in DICOM file(s) or directory to send
235
236Options:
237 -h, --help Show this help message and exit
238 -v, --verbose Verbose output mode
239 -d, --debug Debug output mode (more details than verbose)
240 -q, --quiet Quiet mode (minimal output)
241 --version Show version information
242
243Network Options:
244 -aet, --aetitle <aetitle> Calling AE Title (default: STORESCU)
245 -aec, --call <aetitle> Called AE Title (default: ANY-SCP)
246 -to, --timeout <seconds> Connection timeout (default: 30)
247 -ta, --acse-timeout <seconds> ACSE timeout (default: 30)
248 -td, --dimse-timeout <seconds> DIMSE timeout (default: 0=infinite)
249
250Transfer Options:
251 -r, --recursive Recursively process directories
252 -xs, --prefer-lossless Prefer lossless transfer syntaxes
253 -xv, --propose-implicit Propose only Implicit VR Little Endian
254 -xe, --propose-explicit Propose only Explicit VR Little Endian
255 +xa, --propose-all Propose all transfer syntaxes (default)
256
257Batch Options:
258 --scan-pattern <pattern> File pattern for directory scan (default: *.dcm)
259 --continue-on-error Continue after failures (default)
260 --stop-on-error Stop on first error
261 --max-pdu <size> Maximum PDU size (default: 16384)
262
263Progress Options:
264 -p, --progress Show progress bar
265 --report-file <file> Write transfer report to file
266
267TLS Options (not yet implemented):
268 --tls Enable TLS connection
269 --tls-cert <file> TLS certificate file
270 --tls-key <file> TLS private key file
271 --tls-ca <file> TLS CA certificate file
272
273Examples:
274 # Send single file
275 )" << program_name << R"( localhost 11112 image.dcm
276
277 # Send with custom AE Titles
278 )" << program_name << R"( -aet MYSCU -aec PACS localhost 11112 image.dcm
279
280 # Send directory recursively with progress
281 )" << program_name << R"( -r --progress localhost 11112 ./patient_data/
282
283 # Send with report file
284 )" << program_name << R"( --report-file transfer.log localhost 11112 *.dcm
285
286 # Prefer lossless transfer syntax
287 )" << program_name << R"( --prefer-lossless localhost 11112 *.dcm
288
289Exit Codes:
290 0 Success - All files sent successfully
291 1 Error - One or more files failed to send
292 2 Error - Invalid arguments or connection failure
293)";
294}
295
299void print_version() {
300 std::cout << "store_scu version " << version_string << "\n";
301 std::cout << "PACS System DICOM Utilities\n";
302 std::cout << "Copyright (c) 2024\n";
303}
304
305// =============================================================================
306// Argument Parsing Helpers
307// =============================================================================
308
312bool parse_timeout(const std::string& value, std::chrono::seconds& result,
313 const std::string& option_name) {
314 try {
315 int seconds = std::stoi(value);
316 if (seconds < 0) {
317 std::cerr << "Error: " << option_name << " must be non-negative\n";
318 return false;
319 }
320 result = std::chrono::seconds{seconds};
321 return true;
322 } catch (const std::exception&) {
323 std::cerr << "Error: Invalid value for " << option_name << ": '"
324 << value << "'\n";
325 return false;
326 }
327}
328
332bool parse_size(const std::string& value, size_t& result,
333 const std::string& option_name, size_t min_value = 0) {
334 try {
335 long long val = std::stoll(value);
336 if (val < 0 || static_cast<size_t>(val) < min_value) {
337 std::cerr << "Error: " << option_name << " must be at least "
338 << min_value << "\n";
339 return false;
340 }
341 result = static_cast<size_t>(val);
342 return true;
343 } catch (const std::exception&) {
344 std::cerr << "Error: Invalid value for " << option_name << ": '"
345 << value << "'\n";
346 return false;
347 }
348}
349
353bool validate_ae_title(const std::string& ae_title,
354 const std::string& option_name) {
355 if (ae_title.empty()) {
356 std::cerr << "Error: " << option_name << " cannot be empty\n";
357 return false;
358 }
359 if (ae_title.length() > max_ae_title_length) {
360 std::cerr << "Error: " << option_name << " exceeds "
361 << max_ae_title_length << " characters\n";
362 return false;
363 }
364 return true;
365}
366
370bool parse_arguments(int argc, char* argv[], options& opts) {
371 std::vector<std::string> positional_args;
372
373 for (int i = 1; i < argc; ++i) {
374 std::string arg = argv[i];
375
376 // Help options
377 if (arg == "-h" || arg == "--help") {
378 opts.show_help = true;
379 return true;
380 }
381 if (arg == "--version") {
382 opts.show_version = true;
383 return true;
384 }
385
386 // Verbosity options
387 if (arg == "-v" || arg == "--verbose") {
388 opts.verbosity = verbosity_level::verbose;
389 continue;
390 }
391 if (arg == "-d" || arg == "--debug") {
392 opts.verbosity = verbosity_level::debug;
393 continue;
394 }
395 if (arg == "-q" || arg == "--quiet") {
396 opts.verbosity = verbosity_level::quiet;
397 continue;
398 }
399
400 // Network options with values
401 if ((arg == "-aet" || arg == "--aetitle") && i + 1 < argc) {
402 opts.calling_ae_title = argv[++i];
403 if (!validate_ae_title(opts.calling_ae_title, "Calling AE Title")) {
404 return false;
405 }
406 continue;
407 }
408 if ((arg == "-aec" || arg == "--call") && i + 1 < argc) {
409 opts.called_ae_title = argv[++i];
410 if (!validate_ae_title(opts.called_ae_title, "Called AE Title")) {
411 return false;
412 }
413 continue;
414 }
415
416 // Timeout options
417 if ((arg == "-to" || arg == "--timeout") && i + 1 < argc) {
418 if (!parse_timeout(argv[++i], opts.connection_timeout,
419 "Connection timeout")) {
420 return false;
421 }
422 continue;
423 }
424 if ((arg == "-ta" || arg == "--acse-timeout") && i + 1 < argc) {
425 if (!parse_timeout(argv[++i], opts.acse_timeout, "ACSE timeout")) {
426 return false;
427 }
428 continue;
429 }
430 if ((arg == "-td" || arg == "--dimse-timeout") && i + 1 < argc) {
431 if (!parse_timeout(argv[++i], opts.dimse_timeout, "DIMSE timeout")) {
432 return false;
433 }
434 continue;
435 }
436
437 // Transfer options
438 if (arg == "-r" || arg == "--recursive") {
439 opts.recursive = true;
440 continue;
441 }
442 if (arg == "-xs" || arg == "--prefer-lossless") {
443 opts.ts_mode = transfer_syntax_mode::prefer_lossless;
444 continue;
445 }
446 if (arg == "-xv" || arg == "--propose-implicit") {
447 opts.ts_mode = transfer_syntax_mode::propose_implicit;
448 continue;
449 }
450 if (arg == "-xe" || arg == "--propose-explicit") {
451 opts.ts_mode = transfer_syntax_mode::propose_explicit;
452 continue;
453 }
454 if (arg == "+xa" || arg == "--propose-all") {
455 opts.ts_mode = transfer_syntax_mode::propose_all;
456 continue;
457 }
458
459 // Batch options
460 if (arg == "--scan-pattern" && i + 1 < argc) {
461 opts.scan_pattern = argv[++i];
462 continue;
463 }
464 if (arg == "--continue-on-error") {
465 opts.continue_on_error = true;
466 continue;
467 }
468 if (arg == "--stop-on-error") {
469 opts.continue_on_error = false;
470 continue;
471 }
472 if (arg == "--max-pdu" && i + 1 < argc) {
473 if (!parse_size(argv[++i], opts.max_pdu_size, "Max PDU size", 4096)) {
474 return false;
475 }
476 continue;
477 }
478
479 // Progress options
480 if (arg == "-p" || arg == "--progress") {
481 opts.show_progress = true;
482 continue;
483 }
484 if (arg == "--report-file" && i + 1 < argc) {
485 opts.report_file = argv[++i];
486 continue;
487 }
488
489 // TLS options
490 if (arg == "--tls") {
491 opts.use_tls = true;
492 continue;
493 }
494 if (arg == "--tls-cert" && i + 1 < argc) {
495 opts.tls_cert_file = argv[++i];
496 continue;
497 }
498 if (arg == "--tls-key" && i + 1 < argc) {
499 opts.tls_key_file = argv[++i];
500 continue;
501 }
502 if (arg == "--tls-ca" && i + 1 < argc) {
503 opts.tls_ca_file = argv[++i];
504 continue;
505 }
506
507 // Check for unknown options
508 if (arg.starts_with("-") && arg != "-") {
509 std::cerr << "Error: Unknown option '" << arg << "'\n";
510 return false;
511 }
512
513 // Positional arguments
514 positional_args.push_back(arg);
515 }
516
517 // Validate positional arguments (need at least peer, port, and one file)
518 if (positional_args.size() < 3) {
519 std::cerr
520 << "Error: Expected <peer> <port> <dcmfile-in> [dcmfile-in...]\n";
521 return false;
522 }
523
524 opts.peer_host = positional_args[0];
525
526 // Parse port
527 try {
528 int port_int = std::stoi(positional_args[1]);
529 if (port_int < 1 || port_int > 65535) {
530 std::cerr << "Error: Port must be between 1 and 65535\n";
531 return false;
532 }
533 opts.peer_port = static_cast<uint16_t>(port_int);
534 } catch (const std::exception&) {
535 std::cerr << "Error: Invalid port number '" << positional_args[1]
536 << "'\n";
537 return false;
538 }
539
540 // Collect input paths
541 for (size_t i = 2; i < positional_args.size(); ++i) {
542 opts.input_paths.emplace_back(positional_args[i]);
543 }
544
545 // TLS validation
546 if (opts.use_tls) {
547 std::cerr << "Warning: TLS support is not yet implemented\n";
548 }
549
550 return true;
551}
552
553// =============================================================================
554// File Collection Helpers
555// =============================================================================
556
560bool is_dicom_file_candidate(const std::filesystem::path& path) {
561 auto ext = path.extension().string();
562 std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower);
563 return ext == ".dcm" || ext == ".dicom" || ext.empty();
564}
565
569std::vector<std::filesystem::path> collect_files(
570 const std::vector<std::filesystem::path>& input_paths, bool recursive) {
571 std::vector<std::filesystem::path> files;
572
573 for (const auto& path : input_paths) {
574 if (!std::filesystem::exists(path)) {
575 std::cerr << "Warning: Path does not exist: " << path.string()
576 << "\n";
577 continue;
578 }
579
580 if (std::filesystem::is_regular_file(path)) {
581 files.push_back(path);
582 } else if (std::filesystem::is_directory(path)) {
583 if (recursive) {
584 for (const auto& entry :
585 std::filesystem::recursive_directory_iterator(path)) {
586 if (entry.is_regular_file() &&
587 is_dicom_file_candidate(entry.path())) {
588 files.push_back(entry.path());
589 }
590 }
591 } else {
592 for (const auto& entry :
593 std::filesystem::directory_iterator(path)) {
594 if (entry.is_regular_file() &&
595 is_dicom_file_candidate(entry.path())) {
596 files.push_back(entry.path());
597 }
598 }
599 }
600 }
601 }
602
603 return files;
604}
605
606// =============================================================================
607// Progress Display
608// =============================================================================
609
613void show_progress_bar(size_t current, size_t total, int width = 40) {
614 if (total == 0) return;
615
616 float progress = static_cast<float>(current) / static_cast<float>(total);
617 int filled = static_cast<int>(progress * width);
618
619 std::cout << "\r[";
620 for (int i = 0; i < width; ++i) {
621 if (i < filled) {
622 std::cout << "=";
623 } else if (i == filled) {
624 std::cout << ">";
625 } else {
626 std::cout << " ";
627 }
628 }
629 std::cout << "] " << std::setw(3) << static_cast<int>(progress * 100)
630 << "% "
631 << "(" << current << "/" << total << ")" << std::flush;
632}
633
637std::string format_size(size_t bytes) {
638 constexpr size_t KB = 1024;
639 constexpr size_t MB = KB * 1024;
640 constexpr size_t GB = MB * 1024;
641
642 std::ostringstream oss;
643 oss << std::fixed << std::setprecision(2);
644
645 if (bytes >= GB) {
646 oss << static_cast<double>(bytes) / GB << " GB";
647 } else if (bytes >= MB) {
648 oss << static_cast<double>(bytes) / MB << " MB";
649 } else if (bytes >= KB) {
650 oss << static_cast<double>(bytes) / KB << " KB";
651 } else {
652 oss << bytes << " B";
653 }
654
655 return oss.str();
656}
657
661std::string format_duration(std::chrono::milliseconds duration) {
662 auto ms = duration.count();
663
664 if (ms < 1000) {
665 return std::to_string(ms) + " ms";
666 }
667
668 auto seconds = ms / 1000;
669 auto minutes = seconds / 60;
670 seconds %= 60;
671
672 std::ostringstream oss;
673 if (minutes > 0) {
674 oss << minutes << "m ";
675 }
676 oss << seconds << "s";
677 return oss.str();
678}
679
680// =============================================================================
681// Transfer Syntax Helpers
682// =============================================================================
683
687std::vector<std::string> get_transfer_syntaxes(transfer_syntax_mode mode) {
688 switch (mode) {
689 case transfer_syntax_mode::propose_implicit:
690 return {ts::implicit_vr_le};
691 case transfer_syntax_mode::propose_explicit:
692 return {ts::explicit_vr_le};
693 case transfer_syntax_mode::prefer_lossless:
694 return {ts::jpeg_lossless, ts::jpeg2000_lossless, ts::rle,
695 ts::explicit_vr_le, ts::implicit_vr_le};
696 case transfer_syntax_mode::propose_all:
697 default:
698 return {ts::explicit_vr_le, ts::implicit_vr_le,
699 ts::explicit_vr_be, ts::jpeg_baseline,
700 ts::jpeg_extended, ts::jpeg_lossless,
701 ts::jpeg2000_lossless, ts::jpeg2000_lossy,
702 ts::rle};
703 }
704}
705
706// =============================================================================
707// Report Generation
708// =============================================================================
709
713void generate_report(const std::string& report_file,
714 const std::vector<file_store_result>& results,
715 const store_statistics& stats, const options& opts) {
716 std::ofstream ofs(report_file);
717 if (!ofs.is_open()) {
718 std::cerr << "Warning: Could not open report file: " << report_file
719 << "\n";
720 return;
721 }
722
723 auto now = std::chrono::system_clock::now();
724 auto time = std::chrono::system_clock::to_time_t(now);
725
726 ofs << "========================================\n";
727 ofs << " DICOM Store SCU Transfer Report\n";
728 ofs << "========================================\n";
729 ofs << "Generated: " << std::ctime(&time);
730 ofs << "\n";
731
732 ofs << "Connection Info:\n";
733 ofs << " Peer: " << opts.peer_host << ":" << opts.peer_port
734 << "\n";
735 ofs << " Calling AE: " << opts.calling_ae_title << "\n";
736 ofs << " Called AE: " << opts.called_ae_title << "\n";
737 ofs << "\n";
738
739 ofs << "Summary:\n";
740 ofs << " Total Files: " << stats.total_files << "\n";
741 ofs << " Successful: " << stats.successful << "\n";
742 ofs << " Warnings: " << stats.warnings << "\n";
743 ofs << " Failed: " << stats.failed << "\n";
744 ofs << " Data Sent: " << format_size(stats.total_bytes) << "\n";
745 ofs << " Duration: " << format_duration(stats.total_time) << "\n";
746 ofs << std::fixed << std::setprecision(2);
747 ofs << " Throughput: " << stats.throughput_mbps() << " MB/s\n";
748 ofs << " Success Rate: " << stats.success_rate() << "%\n";
749 ofs << "\n";
750
751 if (stats.failed > 0) {
752 ofs << "Failed Transfers:\n";
753 ofs << "----------------------------------------\n";
754 for (const auto& result : results) {
755 if (!result.success) {
756 ofs << " File: " << result.file_path.filename().string()
757 << "\n";
758 ofs << " Error: " << result.error_message << "\n";
759 ofs << " Status: 0x" << std::hex << result.status_code
760 << std::dec << "\n";
761 ofs << "\n";
762 }
763 }
764 }
765
766 ofs << "All Transfers:\n";
767 ofs << "----------------------------------------\n";
768 for (const auto& result : results) {
769 ofs << (result.success ? "[OK] " : "[FAIL]") << " ";
770 ofs << result.file_path.filename().string();
771 if (result.success) {
772 ofs << " (" << format_size(result.file_size) << ", "
773 << result.transfer_time.count() << "ms)";
774 } else {
775 ofs << " - " << result.error_message;
776 }
777 ofs << "\n";
778 }
779
780 ofs.close();
781}
782
783// =============================================================================
784// Main Store Implementation
785// =============================================================================
786
790std::vector<std::pair<std::filesystem::path, std::string>> analyze_files(
791 const std::vector<std::filesystem::path>& files, bool verbose) {
792 using namespace kcenon::pacs::core;
793
794 std::vector<std::pair<std::filesystem::path, std::string>> valid_files;
795
796 for (const auto& file_path : files) {
797 auto result = dicom_file::open(file_path);
798 if (result.is_ok()) {
799 auto sop_class = result.value().sop_class_uid();
800 if (!sop_class.empty()) {
801 valid_files.emplace_back(file_path, sop_class);
802 } else if (verbose) {
803 std::cerr << "Warning: No SOP Class UID in file: "
804 << file_path.string() << "\n";
805 }
806 } else if (verbose) {
807 std::cerr << "Warning: Skipping invalid file: "
808 << file_path.string() << "\n";
809 }
810 }
811
812 return valid_files;
813}
814
818int perform_store(const options& opts) {
819 using namespace kcenon::pacs::network;
820 using namespace kcenon::pacs::services;
821 using namespace kcenon::pacs::core;
822
823 bool is_quiet = opts.verbosity == verbosity_level::quiet;
824 bool is_verbose = opts.verbosity == verbosity_level::verbose ||
825 opts.verbosity == verbosity_level::debug;
826
827 store_statistics stats;
828 std::vector<file_store_result> results;
829 auto start_time = std::chrono::steady_clock::now();
830
831 // Collect files
832 if (!is_quiet) {
833 std::cout << "Scanning for DICOM files...\n";
834 }
835 auto files = collect_files(opts.input_paths, opts.recursive);
836
837 if (files.empty()) {
838 std::cerr << "Error: No DICOM files found\n";
839 return 2;
840 }
841
842 if (!is_quiet) {
843 std::cout << "Found " << files.size() << " file(s) to analyze\n";
844 }
845
846 // Analyze files
847 if (!is_quiet) {
848 std::cout << "Analyzing files...\n";
849 }
850 auto valid_files = analyze_files(files, is_verbose);
851
852 if (valid_files.empty()) {
853 std::cerr << "Error: No valid DICOM files found\n";
854 return 2;
855 }
856
857 // Collect unique SOP classes
858 std::vector<std::string> sop_classes;
859 for (const auto& [path, sop_class] : valid_files) {
860 if (std::find(sop_classes.begin(), sop_classes.end(), sop_class) ==
861 sop_classes.end()) {
862 sop_classes.push_back(sop_class);
863 }
864 }
865
866 if (!is_quiet) {
867 std::cout << "Valid DICOM files: " << valid_files.size() << "\n";
868 std::cout << "SOP Classes found: " << sop_classes.size() << "\n\n";
869 }
870
871 stats.total_files = valid_files.size();
872
873 // Print connection info
874 if (!is_quiet) {
875 std::cout << "Connecting to " << opts.peer_host << ":"
876 << opts.peer_port << "\n";
877 std::cout << " Calling AE Title: " << opts.calling_ae_title << "\n";
878 std::cout << " Called AE Title: " << opts.called_ae_title << "\n";
879
880 if (is_verbose) {
881 std::cout << " Connection Timeout: "
882 << opts.connection_timeout.count() << "s\n";
883 std::cout << " Max PDU Size: " << opts.max_pdu_size << "\n";
884 }
885 std::cout << "\n";
886 }
887
888 // Configure association
889 association_config config;
890 config.calling_ae_title = opts.calling_ae_title;
891 config.called_ae_title = opts.called_ae_title;
892 config.implementation_class_uid = "1.2.826.0.1.3680043.2.1545.1";
893 config.implementation_version_name = "STORE_SCU_100";
894
895 // Build presentation contexts
896 auto transfer_syntaxes = get_transfer_syntaxes(opts.ts_mode);
897 uint8_t context_id = 1;
898
899 for (const auto& sop_class : sop_classes) {
900 config.proposed_contexts.push_back(
901 {context_id, sop_class, transfer_syntaxes});
902 context_id += 2; // Context IDs must be odd
903 }
904
905 // Establish association
906 auto timeout = std::chrono::duration_cast<std::chrono::milliseconds>(
907 opts.connection_timeout);
908 auto connect_result =
909 association::connect(opts.peer_host, opts.peer_port, config, timeout);
910
911 if (connect_result.is_err()) {
912 std::cerr << "Error: Failed to establish association: "
913 << connect_result.error().message << "\n";
914 return 2;
915 }
916
917 auto& assoc = connect_result.value();
918 auto connect_time = std::chrono::steady_clock::now();
919 stats.association_time =
920 std::chrono::duration_cast<std::chrono::milliseconds>(connect_time -
921 start_time);
922
923 if (!is_quiet) {
924 std::cout << "Association established in "
925 << stats.association_time.count() << " ms\n\n";
926 }
927
928 // Create storage SCU
929 storage_scu_config scu_config;
930 scu_config.continue_on_error = opts.continue_on_error;
931 scu_config.response_timeout =
932 std::chrono::duration_cast<std::chrono::milliseconds>(
933 opts.dimse_timeout.count() > 0 ? opts.dimse_timeout
934 : std::chrono::seconds{30});
935
936 storage_scu scu{scu_config};
937
938 // Send files
939 if (!is_quiet) {
940 std::cout << "Sending files...\n";
941 }
942
943 for (size_t i = 0; i < valid_files.size(); ++i) {
944 const auto& [file_path, sop_class] = valid_files[i];
945
946 file_store_result file_result;
947 file_result.file_path = file_path;
948 file_result.sop_class_uid = sop_class;
949
950 // Get file size
951 try {
952 file_result.file_size = std::filesystem::file_size(file_path);
953 } catch (...) {
954 file_result.file_size = 0;
955 }
956
957 if (opts.show_progress && !is_quiet) {
958 show_progress_bar(i + 1, valid_files.size());
959 }
960
961 auto file_start = std::chrono::steady_clock::now();
962 auto result = scu.store_file(assoc, file_path);
963 auto file_end = std::chrono::steady_clock::now();
964
965 file_result.transfer_time =
966 std::chrono::duration_cast<std::chrono::milliseconds>(file_end -
967 file_start);
968
969 if (result.is_ok()) {
970 const auto& store_result = result.value();
971 file_result.sop_instance_uid = store_result.sop_instance_uid;
972 file_result.status_code = store_result.status;
973
974 if (store_result.is_success()) {
975 file_result.success = true;
976 stats.successful++;
977 stats.total_bytes += file_result.file_size;
978
979 if (is_verbose && !opts.show_progress) {
980 std::cout << " [OK] " << file_path.filename().string()
981 << " (" << format_size(file_result.file_size)
982 << ")\n";
983 }
984 } else if (store_result.is_warning()) {
985 file_result.success = true;
986 stats.warnings++;
987 stats.successful++;
988 stats.total_bytes += file_result.file_size;
989
990 if (is_verbose && !opts.show_progress) {
991 std::cout << " [WARN] " << file_path.filename().string()
992 << " (Status: 0x" << std::hex
993 << store_result.status << std::dec << ")\n";
994 }
995 } else {
996 file_result.success = false;
997 file_result.error_message = store_result.error_comment;
998 stats.failed++;
999
1000 if (is_verbose && !opts.show_progress) {
1001 std::cout << " [FAIL] " << file_path.filename().string()
1002 << " - " << store_result.error_comment << "\n";
1003 }
1004
1005 if (!opts.continue_on_error) {
1006 results.push_back(file_result);
1007 break;
1008 }
1009 }
1010 } else {
1011 file_result.success = false;
1012 file_result.error_message = result.error().message;
1013 stats.failed++;
1014
1015 if (is_verbose && !opts.show_progress) {
1016 std::cout << " [FAIL] " << file_path.filename().string()
1017 << " - " << result.error().message << "\n";
1018 }
1019
1020 if (!opts.continue_on_error) {
1021 results.push_back(file_result);
1022 break;
1023 }
1024 }
1025
1026 results.push_back(file_result);
1027 }
1028
1029 if (opts.show_progress && !is_quiet) {
1030 std::cout << "\n";
1031 }
1032
1033 // Release association
1034 if (!is_quiet) {
1035 std::cout << "\nReleasing association...\n";
1036 }
1037 auto release_result = assoc.release(timeout);
1038 if (release_result.is_err() && is_verbose) {
1039 std::cerr << "Warning: Release failed: " << release_result.error().message
1040 << "\n";
1041 }
1042
1043 auto end_time = std::chrono::steady_clock::now();
1044 stats.total_time = std::chrono::duration_cast<std::chrono::milliseconds>(
1045 end_time - start_time);
1046
1047 // Print summary
1048 if (!is_quiet) {
1049 std::cout << "\n";
1050 std::cout << "========================================\n";
1051 std::cout << " Summary\n";
1052 std::cout << "========================================\n";
1053 std::cout << " Files processed: " << stats.total_files << "\n";
1054 std::cout << " Successful: " << stats.successful << "\n";
1055 if (stats.warnings > 0) {
1056 std::cout << " Warnings: " << stats.warnings << "\n";
1057 }
1058 std::cout << " Failed: " << stats.failed << "\n";
1059 std::cout << " Data sent: " << format_size(stats.total_bytes)
1060 << "\n";
1061 std::cout << " Total time: " << format_duration(stats.total_time)
1062 << "\n";
1063 std::cout << std::fixed << std::setprecision(2);
1064 std::cout << " Throughput: " << stats.throughput_mbps()
1065 << " MB/s\n";
1066
1067 if (stats.total_files > 0) {
1068 auto avg_time = stats.total_time.count() / stats.total_files;
1069 std::cout << " Avg time/file: " << avg_time << " ms\n";
1070 }
1071
1072 std::cout << "========================================\n";
1073 }
1074
1075 // Generate report file if requested
1076 if (!opts.report_file.empty()) {
1077 generate_report(opts.report_file, results, stats, opts);
1078 if (!is_quiet) {
1079 std::cout << "Report written to: " << opts.report_file << "\n";
1080 }
1081 }
1082
1083 // Return appropriate exit code
1084 if (stats.failed == 0) {
1085 if (!is_quiet) {
1086 std::cout << "Status: SUCCESS\n";
1087 }
1088 return 0;
1089 } else if (stats.successful > 0) {
1090 if (!is_quiet) {
1091 std::cout << "Status: PARTIAL FAILURE\n";
1092 }
1093 return 1;
1094 } else {
1095 if (!is_quiet) {
1096 std::cout << "Status: FAILURE\n";
1097 }
1098 return 1;
1099 }
1100}
1101
1102} // namespace
1103
1104// =============================================================================
1105// Main Entry Point
1106// =============================================================================
1107
1108int main(int argc, char* argv[]) {
1109 options opts;
1110
1111 if (!parse_arguments(argc, argv, opts)) {
1112 if (!opts.show_help && !opts.show_version) {
1113 std::cerr << "\nUse --help for usage information.\n";
1114 return 2;
1115 }
1116 }
1117
1118 if (opts.show_version) {
1119 print_version();
1120 return 0;
1121 }
1122
1123 if (opts.show_help) {
1124 print_banner();
1125 print_usage(argv[0]);
1126 return 0;
1127 }
1128
1129 // Print banner unless quiet mode
1130 if (opts.verbosity != verbosity_level::quiet) {
1131 print_banner();
1132 }
1133
1134 return perform_store(opts);
1135}
DICOM Association management per PS3.8.
DICOM Part 10 file handling for reading/writing DICOM files.
Compile-time constants for commonly used DICOM tags.
int main()
Definition main.cpp:84
constexpr dicom_tag sop_instance_uid
SOP Instance UID.
constexpr dicom_tag sop_class_uid
SOP Class UID.
constexpr int connection_timeout
Definition result.h:95
constexpr int timeout
Lock timeout exceeded.
Transfer Syntax UIDs.
Definition main.cpp:78
DICOM Storage SCU service (C-STORE sender)
Configuration for SCU association request.
std::string called_ae_title
Remote AE Title (16 chars max)
std::string calling_ae_title
Our AE Title (16 chars max)
std::vector< proposed_presentation_context > proposed_contexts
Configuration for Storage SCU service.
Definition storage_scu.h:80
std::chrono::milliseconds response_timeout
Timeout for receiving C-STORE response (milliseconds)
Definition storage_scu.h:85
bool continue_on_error
Continue batch operation on error (true) or stop on first error (false)
Definition storage_scu.h:88
Result of a C-STORE operation.
Definition storage_scu.h:43
uint16_t status
DIMSE status code (0x0000 = success)
Definition storage_scu.h:48
bool is_warning() const noexcept
Check if this was a warning status.
Definition storage_scu.h:59
std::string error_comment
Error comment from the SCP (if any)
Definition storage_scu.h:51
std::string sop_instance_uid
SOP Instance UID of the stored instance.
Definition storage_scu.h:45
bool is_success() const noexcept
Check if the store operation was successful.
Definition storage_scu.h:54