PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
main.cpp
Go to the documentation of this file.
1
22
23#include <algorithm>
24#include <filesystem>
25#include <iomanip>
26#include <iostream>
27#include <sstream>
28#include <string>
29#include <vector>
30
31namespace fs = std::filesystem;
32using namespace kcenon::pacs::storage;
33
34namespace {
35
39enum class command_type {
40 patients,
41 studies,
42 series,
43 instances,
44 stats,
45 vacuum,
46 verify,
47 help
48};
49
53struct options {
54 std::string db_path;
55 command_type command{command_type::help};
56
57 // Filter options
58 std::string patient_id;
59 std::string patient_name;
60 std::string study_uid;
61 std::string series_uid;
62 std::string modality;
63 std::string date_from;
64 std::string date_to;
65
66 // Pagination
67 size_t limit{50};
68 size_t offset{0};
69
70 // Output options
71 bool verbose{false};
72};
73
78void print_usage(const char* program_name) {
79 std::cout << R"(
80Database Browser - PACS Index Viewer
81
82Usage: )" << program_name
83 << R"( <database> <command> [options]
84
85Commands:
86 patients List all patients
87 studies List studies (optionally filtered by patient)
88 series List series (optionally filtered by study)
89 instances List instances (optionally filtered by series)
90 stats Show database statistics
91 vacuum Reclaim unused space in the database
92 verify Verify file existence for all instances
93
94Filter Options:
95 --patient-id <id> Filter by patient ID
96 --patient-name <name> Filter by patient name (supports * wildcard)
97 --study-uid <uid> Filter by Study Instance UID
98 --series-uid <uid> Filter by Series Instance UID
99 --modality <mod> Filter by modality (e.g., CT, MR, XR)
100 --from <YYYYMMDD> Filter by date range start
101 --to <YYYYMMDD> Filter by date range end
102
103Pagination Options:
104 --limit <n> Maximum results to show (default: 50)
105 --offset <n> Skip first n results (default: 0)
106
107General Options:
108 --verbose, -v Show additional details
109 --help, -h Show this help message
110
111Examples:
112 )" << program_name
113 << R"( pacs.db patients
114 )" << program_name
115 << R"( pacs.db studies --patient-id "12345"
116 )" << program_name
117 << R"( pacs.db studies --from 20240101 --to 20241231
118 )" << program_name
119 << R"( pacs.db series --study-uid "1.2.3.4.5"
120 )" << program_name
121 << R"( pacs.db instances --series-uid "1.2.3.4.5.6"
122 )" << program_name
123 << R"( pacs.db stats
124 )" << program_name
125 << R"( pacs.db vacuum
126 )" << program_name
127 << R"( pacs.db verify
128
129Exit Codes:
130 0 Success
131 1 Invalid arguments or command
132 2 Database error
133)";
134}
135
141command_type parse_command(const std::string& cmd) {
142 if (cmd == "patients") {
143 return command_type::patients;
144 }
145 if (cmd == "studies") {
146 return command_type::studies;
147 }
148 if (cmd == "series") {
149 return command_type::series;
150 }
151 if (cmd == "instances") {
152 return command_type::instances;
153 }
154 if (cmd == "stats") {
155 return command_type::stats;
156 }
157 if (cmd == "vacuum") {
158 return command_type::vacuum;
159 }
160 if (cmd == "verify") {
161 return command_type::verify;
162 }
163 return command_type::help;
164}
165
173bool parse_arguments(int argc, char* argv[], options& opts) {
174 if (argc < 3) {
175 return false;
176 }
177
178 opts.db_path = argv[1];
179 opts.command = parse_command(argv[2]);
180
181 if (opts.command == command_type::help) {
182 // Check if it's actually --help flag
183 std::string arg1 = argv[1];
184 if (arg1 == "--help" || arg1 == "-h") {
185 return false;
186 }
187 std::cerr << "Error: Unknown command '" << argv[2] << "'\n";
188 return false;
189 }
190
191 for (int i = 3; i < argc; ++i) {
192 std::string arg = argv[i];
193
194 if (arg == "--help" || arg == "-h") {
195 return false;
196 }
197 if (arg == "--verbose" || arg == "-v") {
198 opts.verbose = true;
199 } else if (arg == "--patient-id" && i + 1 < argc) {
200 opts.patient_id = argv[++i];
201 } else if (arg == "--patient-name" && i + 1 < argc) {
202 opts.patient_name = argv[++i];
203 } else if (arg == "--study-uid" && i + 1 < argc) {
204 opts.study_uid = argv[++i];
205 } else if (arg == "--series-uid" && i + 1 < argc) {
206 opts.series_uid = argv[++i];
207 } else if (arg == "--modality" && i + 1 < argc) {
208 opts.modality = argv[++i];
209 } else if (arg == "--from" && i + 1 < argc) {
210 opts.date_from = argv[++i];
211 } else if (arg == "--to" && i + 1 < argc) {
212 opts.date_to = argv[++i];
213 } else if (arg == "--limit" && i + 1 < argc) {
214 opts.limit = std::stoull(argv[++i]);
215 } else if (arg == "--offset" && i + 1 < argc) {
216 opts.offset = std::stoull(argv[++i]);
217 } else if (arg[0] == '-') {
218 std::cerr << "Error: Unknown option '" << arg << "'\n";
219 return false;
220 }
221 }
222
223 return true;
224}
225
231std::string format_date(const std::string& date) {
232 if (date.length() == 8) {
233 return date.substr(0, 4) + "-" + date.substr(4, 2) + "-" +
234 date.substr(6, 2);
235 }
236 return date.empty() ? "-" : date;
237}
238
244std::string format_size(int64_t bytes) {
245 constexpr int64_t KB = 1024;
246 constexpr int64_t MB = KB * 1024;
247 constexpr int64_t GB = MB * 1024;
248
249 std::ostringstream oss;
250 oss << std::fixed << std::setprecision(1);
251
252 if (bytes >= GB) {
253 oss << static_cast<double>(bytes) / GB << " GB";
254 } else if (bytes >= MB) {
255 oss << static_cast<double>(bytes) / MB << " MB";
256 } else if (bytes >= KB) {
257 oss << static_cast<double>(bytes) / KB << " KB";
258 } else {
259 oss << bytes << " B";
260 }
261
262 return oss.str();
263}
264
271std::string truncate(const std::string& str, size_t max_len) {
272 if (str.length() <= max_len) {
273 return str;
274 }
275 if (max_len <= 3) {
276 return str.substr(0, max_len);
277 }
278 return str.substr(0, max_len - 3) + "...";
279}
280
285void print_separator(const std::vector<size_t>& widths) {
286 for (size_t i = 0; i < widths.size(); ++i) {
287 if (i > 0) {
288 std::cout << "+";
289 }
290 std::cout << std::string(widths[i] + 2, '-');
291 }
292 std::cout << "\n";
293}
294
300void print_row(const std::vector<std::string>& values,
301 const std::vector<size_t>& widths) {
302 for (size_t i = 0; i < values.size(); ++i) {
303 if (i > 0) {
304 std::cout << "|";
305 }
306 std::cout << " " << std::left << std::setw(static_cast<int>(widths[i]))
307 << truncate(values[i], widths[i]) << " ";
308 }
309 std::cout << "\n";
310}
311
318int list_patients(index_database& db, const options& opts) {
320 if (!opts.patient_id.empty()) {
321 query.patient_id = opts.patient_id;
322 }
323 if (!opts.patient_name.empty()) {
324 query.patient_name = opts.patient_name;
325 }
326 query.limit = opts.limit;
327 query.offset = opts.offset;
328
329 auto patients_result = db.search_patients(query);
330 if (patients_result.is_err()) {
331 std::cerr << "Error: " << patients_result.error().message << "\n";
332 return 2;
333 }
334 const auto& patients = patients_result.value();
335
336 auto total_result = db.patient_count();
337 size_t total = total_result.is_ok() ? total_result.value() : 0;
338
339 std::cout << "\n=== Patients (" << patients.size();
340 if (opts.limit > 0 && patients.size() == opts.limit) {
341 std::cout << " of " << total;
342 }
343 std::cout << " total) ===\n\n";
344
345 if (patients.empty()) {
346 std::cout << "No patients found.\n";
347 return 0;
348 }
349
350 // Table layout
351 std::vector<std::string> headers = {"ID", "Name", "Birth Date", "Sex",
352 "Studies"};
353 std::vector<size_t> widths = {12, 24, 12, 4, 8};
354
355 print_row(headers, widths);
356 print_separator(widths);
357
358 for (const auto& patient : patients) {
359 auto study_count_result = db.study_count(patient.patient_id);
360 size_t study_count = study_count_result.is_ok() ? study_count_result.value() : 0;
361 std::vector<std::string> row = {patient.patient_id, patient.patient_name,
362 format_date(patient.birth_date),
363 patient.sex.empty() ? "-" : patient.sex,
364 std::to_string(study_count)};
365 print_row(row, widths);
366 }
367
368 if (opts.verbose && !patients.empty()) {
369 std::cout << "\nShowing " << patients.size() << " of " << total
370 << " patients";
371 if (opts.offset > 0) {
372 std::cout << " (offset: " << opts.offset << ")";
373 }
374 std::cout << "\n";
375 }
376
377 return 0;
378}
379
386int list_studies(index_database& db, const options& opts) {
388 if (!opts.patient_id.empty()) {
389 query.patient_id = opts.patient_id;
390 }
391 if (!opts.patient_name.empty()) {
392 query.patient_name = opts.patient_name;
393 }
394 if (!opts.study_uid.empty()) {
395 query.study_uid = opts.study_uid;
396 }
397 if (!opts.modality.empty()) {
398 query.modality = opts.modality;
399 }
400 if (!opts.date_from.empty()) {
401 query.study_date_from = opts.date_from;
402 }
403 if (!opts.date_to.empty()) {
404 query.study_date_to = opts.date_to;
405 }
406 query.limit = opts.limit;
407 query.offset = opts.offset;
408
409 auto studies_result = db.search_studies(query);
410 if (studies_result.is_err()) {
411 std::cerr << "Error: " << studies_result.error().message << "\n";
412 return 2;
413 }
414 const auto& studies = studies_result.value();
415
416 auto total_result = db.study_count();
417 size_t total = total_result.is_ok() ? total_result.value() : 0;
418
419 std::cout << "\n=== Studies (" << studies.size();
420 if (opts.limit > 0 && studies.size() == opts.limit) {
421 std::cout << " of " << total;
422 }
423 std::cout << " total) ===\n\n";
424
425 if (studies.empty()) {
426 std::cout << "No studies found.\n";
427 return 0;
428 }
429
430 // Table layout
431 std::vector<std::string> headers = {"Study UID", "Date", "Description",
432 "Modalities", "Series"};
433 std::vector<size_t> widths = {28, 12, 24, 12, 7};
434
435 print_row(headers, widths);
436 print_separator(widths);
437
438 for (const auto& study : studies) {
439 std::vector<std::string> row = {
440 study.study_uid, format_date(study.study_date),
441 study.study_description.empty() ? "-" : study.study_description,
442 study.modalities_in_study.empty() ? "-" : study.modalities_in_study,
443 std::to_string(study.num_series)};
444 print_row(row, widths);
445 }
446
447 if (opts.verbose) {
448 std::cout << "\nShowing " << studies.size() << " of " << total
449 << " studies\n";
450 }
451
452 return 0;
453}
454
461int list_series(index_database& db, const options& opts) {
463 if (!opts.study_uid.empty()) {
464 query.study_uid = opts.study_uid;
465 }
466 if (!opts.series_uid.empty()) {
467 query.series_uid = opts.series_uid;
468 }
469 if (!opts.modality.empty()) {
470 query.modality = opts.modality;
471 }
472 query.limit = opts.limit;
473 query.offset = opts.offset;
474
475 auto series_result = db.search_series(query);
476 if (series_result.is_err()) {
477 std::cerr << "Error: " << series_result.error().message << "\n";
478 return 2;
479 }
480 const auto& series_list = series_result.value();
481
482 auto total_result = db.series_count();
483 size_t total = total_result.is_ok() ? total_result.value() : 0;
484
485 std::cout << "\n=== Series (" << series_list.size();
486 if (opts.limit > 0 && series_list.size() == opts.limit) {
487 std::cout << " of " << total;
488 }
489 std::cout << " total) ===\n\n";
490
491 if (series_list.empty()) {
492 std::cout << "No series found.\n";
493 return 0;
494 }
495
496 // Table layout
497 std::vector<std::string> headers = {"Series UID", "Modality", "Number",
498 "Description", "Instances"};
499 std::vector<size_t> widths = {28, 10, 7, 24, 10};
500
501 print_row(headers, widths);
502 print_separator(widths);
503
504 for (const auto& s : series_list) {
505 std::string series_num =
506 s.series_number.has_value() ? std::to_string(*s.series_number) : "-";
507 std::vector<std::string> row = {
508 s.series_uid, s.modality.empty() ? "-" : s.modality, series_num,
509 s.series_description.empty() ? "-" : s.series_description,
510 std::to_string(s.num_instances)};
511 print_row(row, widths);
512 }
513
514 if (opts.verbose) {
515 std::cout << "\nShowing " << series_list.size() << " of " << total
516 << " series\n";
517 }
518
519 return 0;
520}
521
528int list_instances(index_database& db, const options& opts) {
530 if (!opts.series_uid.empty()) {
531 query.series_uid = opts.series_uid;
532 }
533 query.limit = opts.limit;
534 query.offset = opts.offset;
535
536 auto instances_result = db.search_instances(query);
537 if (instances_result.is_err()) {
538 std::cerr << "Error: " << instances_result.error().message << "\n";
539 return 2;
540 }
541 const auto& instances = instances_result.value();
542
543 auto total_result = db.instance_count();
544 size_t total = total_result.is_ok() ? total_result.value() : 0;
545
546 std::cout << "\n=== Instances (" << instances.size();
547 if (opts.limit > 0 && instances.size() == opts.limit) {
548 std::cout << " of " << total;
549 }
550 std::cout << " total) ===\n\n";
551
552 if (instances.empty()) {
553 std::cout << "No instances found.\n";
554 return 0;
555 }
556
557 // Table layout
558 std::vector<std::string> headers = {"SOP Instance UID", "Number", "Size",
559 "File Path"};
560 std::vector<size_t> widths = {32, 7, 10, 40};
561
562 print_row(headers, widths);
563 print_separator(widths);
564
565 for (const auto& inst : instances) {
566 std::string inst_num =
567 inst.instance_number.has_value()
568 ? std::to_string(*inst.instance_number)
569 : "-";
570 std::vector<std::string> row = {inst.sop_uid, inst_num,
571 format_size(inst.file_size),
572 inst.file_path};
573 print_row(row, widths);
574 }
575
576 if (opts.verbose) {
577 std::cout << "\nShowing " << instances.size() << " of " << total
578 << " instances\n";
579 }
580
581 return 0;
582}
583
590int show_stats(index_database& db, [[maybe_unused]] const options& opts) {
591 auto stats_result = db.get_storage_stats();
592 if (stats_result.is_err()) {
593 std::cerr << "Error: " << stats_result.error().message << "\n";
594 return 2;
595 }
596 const auto& stats = stats_result.value();
597
598 std::cout << "\n";
599 std::cout << "========================================\n";
600 std::cout << " Database Statistics\n";
601 std::cout << "========================================\n";
602 std::cout << "\n";
603 std::cout << " Database Path: " << db.path() << "\n";
604 std::cout << " Schema Version: " << db.schema_version() << "\n";
605 std::cout << " Database Size: " << format_size(stats.database_size)
606 << "\n";
607 std::cout << "\n";
608 std::cout << " --- Record Counts ---\n";
609 std::cout << " Patients: " << stats.total_patients << "\n";
610 std::cout << " Studies: " << stats.total_studies << "\n";
611 std::cout << " Series: " << stats.total_series << "\n";
612 std::cout << " Instances: " << stats.total_instances << "\n";
613 std::cout << "\n";
614 std::cout << " --- Storage Usage ---\n";
615 std::cout << " Total File Size: " << format_size(stats.total_file_size)
616 << "\n";
617
618 if (stats.total_instances > 0) {
619 auto avg_size = stats.total_file_size / stats.total_instances;
620 std::cout << " Average File Size: " << format_size(avg_size) << "\n";
621 }
622
623 std::cout << "========================================\n";
624
625 return 0;
626}
627
634int do_vacuum(index_database& db, [[maybe_unused]] const options& opts) {
635 std::cout << "Performing VACUUM operation...\n";
636
637 auto stats_before_result = db.get_storage_stats();
638 if (stats_before_result.is_err()) {
639 std::cerr << "Error: " << stats_before_result.error().message << "\n";
640 return 2;
641 }
642 const auto& stats_before = stats_before_result.value();
643
644 auto result = db.vacuum();
645 if (result.is_err()) {
646 std::cerr << "Error: VACUUM failed: " << result.error().message
647 << "\n";
648 return 2;
649 }
650
651 auto stats_after_result = db.get_storage_stats();
652 if (stats_after_result.is_err()) {
653 std::cerr << "Error: " << stats_after_result.error().message << "\n";
654 return 2;
655 }
656 const auto& stats_after = stats_after_result.value();
657
658 std::cout << "VACUUM completed successfully.\n";
659 std::cout << " Before: " << format_size(stats_before.database_size) << "\n";
660 std::cout << " After: " << format_size(stats_after.database_size) << "\n";
661
662 auto saved = stats_before.database_size - stats_after.database_size;
663 if (saved > 0) {
664 std::cout << " Saved: " << format_size(saved) << "\n";
665 }
666
667 return 0;
668}
669
676int do_verify(index_database& db, const options& opts) {
677 std::cout << "Verifying file existence...\n\n";
678
680 query.limit = 0; // No limit for verification
681 auto instances_result = db.search_instances(query);
682 if (instances_result.is_err()) {
683 std::cerr << "Error: " << instances_result.error().message << "\n";
684 return 2;
685 }
686 const auto& instances = instances_result.value();
687
688 size_t total = instances.size();
689 size_t existing = 0;
690 size_t missing = 0;
691 std::vector<std::string> missing_files;
692
693 for (const auto& inst : instances) {
694 if (fs::exists(inst.file_path)) {
695 ++existing;
696 } else {
697 ++missing;
698 if (opts.verbose || missing_files.size() < 10) {
699 missing_files.push_back(inst.file_path);
700 }
701 }
702 }
703
704 std::cout << "========================================\n";
705 std::cout << " File Verification Results\n";
706 std::cout << "========================================\n";
707 std::cout << " Total Instances: " << total << "\n";
708 std::cout << " Files Found: " << existing << "\n";
709 std::cout << " Files Missing: " << missing << "\n";
710 std::cout << "========================================\n";
711
712 if (missing > 0) {
713 std::cout << "\nMissing Files";
714 if (!opts.verbose && missing > 10) {
715 std::cout << " (showing first 10)";
716 }
717 std::cout << ":\n";
718 for (const auto& path : missing_files) {
719 std::cout << " - " << path << "\n";
720 }
721 if (!opts.verbose && missing > missing_files.size()) {
722 std::cout << " ... and " << (missing - missing_files.size())
723 << " more\n";
724 }
725 std::cout << "\nUse --verbose to see all missing files.\n";
726 return 1;
727 }
728
729 std::cout << "\nAll files verified successfully.\n";
730 return 0;
731}
732
733} // namespace
734
735int main(int argc, char* argv[]) {
736 std::cout << R"(
737 ____ ____ ____
738 | _ \| __ ) | __ ) _ __ _____ _____ ___ _ __
739 | | | | _ \ | _ \| '__/ _ \ \ /\ / / __|/ _ \ '__|
740 | |_| | |_) | | |_) | | | (_) \ V V /\__ \ __/ |
741 |____/|____/ |____/|_| \___/ \_/\_/ |___/\___|_|
742
743 PACS Index Database Browser
744)" << "\n";
745
746 options opts;
747
748 if (!parse_arguments(argc, argv, opts)) {
749 print_usage(argv[0]);
750 return 1;
751 }
752
753 // Check database file exists
754 if (!fs::exists(opts.db_path)) {
755 std::cerr << "Error: Database file not found: " << opts.db_path << "\n";
756 return 2;
757 }
758
759 // Open database
760 auto db_result = index_database::open(opts.db_path);
761 if (db_result.is_err()) {
762 std::cerr << "Error: Failed to open database: "
763 << db_result.error().message << "\n";
764 return 2;
765 }
766
767 auto& db = *db_result.value();
768
769 // Execute command
770 switch (opts.command) {
771 case command_type::patients:
772 return list_patients(db, opts);
773 case command_type::studies:
774 return list_studies(db, opts);
775 case command_type::series:
776 return list_series(db, opts);
777 case command_type::instances:
778 return list_instances(db, opts);
779 case command_type::stats:
780 return show_stats(db, opts);
781 case command_type::vacuum:
782 return do_vacuum(db, opts);
783 case command_type::verify:
784 return do_verify(db, opts);
785 case command_type::help:
786 print_usage(argv[0]);
787 return 0;
788 }
789
790 return 0;
791}
auto patient_count() const -> Result< size_t >
Get total patient count.
auto vacuum() -> VoidResult
Reclaim unused space in the database.
auto study_count() const -> Result< size_t >
Get total study count.
auto search_studies(const study_query &query) const -> Result< std::vector< study_record > >
Search studies with query criteria.
auto series_count() const -> Result< size_t >
Get total series count.
auto path() const -> std::string_view
Get the database file path.
auto search_instances(const instance_query &query) const -> Result< std::vector< instance_record > >
Search instances with query criteria.
auto instance_count() const -> Result< size_t >
Get total instance count.
auto search_series(const series_query &query) const -> Result< std::vector< series_record > >
Search series with query criteria.
auto schema_version() const -> int
Get the current schema version.
auto search_patients(const patient_query &query) const -> Result< std::vector< patient_record > >
Search patients with query criteria.
auto get_storage_stats() const -> Result< storage_stats >
Get storage statistics.
PACS index database for metadata storage and retrieval.
int main()
Definition main.cpp:84
constexpr dicom_tag patient_id
Patient ID.
constexpr dicom_tag modality
Modality.
constexpr dicom_tag patient_name
Patient's Name.
const atna_coded_value query
Query (110112)