29constexpr core::dicom_tag tag_sop_class_uid{0x0008, 0x0016};
32constexpr core::dicom_tag tag_sop_instance_uid{0x0008, 0x0018};
35constexpr std::array<std::string_view, 4> dicom_extensions = {
36 ".dcm",
".DCM",
".dicom",
".DICOM"
40[[nodiscard]]
bool is_dicom_file(
const std::filesystem::path& path) {
41 const auto ext = path.extension().string();
46 return std::find(dicom_extensions.begin(), dicom_extensions.end(), ext)
47 != dicom_extensions.end();
57 : logger_(logger ? std::move(logger) : di::null_logger()) {}
60 std::shared_ptr<di::ILogger> logger)
61 : logger_(logger ? std::move(logger) : di::null_logger()), config_(config) {}
77 uint16_t message_id) {
79 using namespace network::dimse;
82 const auto sop_class_uid = dataset.
get_string(tag_sop_class_uid);
83 if (sop_class_uid.empty()) {
86 "Missing SOP Class UID in dataset");
90 const auto sop_instance_uid = dataset.
get_string(tag_sop_instance_uid);
91 if (sop_instance_uid.empty()) {
94 "Missing SOP Instance UID in dataset");
101 "Association not established");
109 "No accepted presentation context for SOP Class: " + sop_class_uid);
113 auto request = make_c_store_rq(
121 request.set_dataset(dataset);
124 auto send_result = assoc.
send_dimse(*context_id, request);
125 if (send_result.is_err()) {
126 failures_.fetch_add(1, std::memory_order_relaxed);
127 return send_result.error();
132 if (recv_result.is_err()) {
133 failures_.fetch_add(1, std::memory_order_relaxed);
134 return recv_result.error();
137 const auto& [recv_context_id, response] = recv_result.value();
140 if (response.command() != command_field::c_store_rsp) {
141 failures_.fetch_add(1, std::memory_order_relaxed);
144 "Expected C-STORE-RSP but received " +
145 std::string(
to_string(response.command())));
151 result.
status =
static_cast<uint16_t
>(response.status());
154 if (response.command_set().contains(tag_error_comment)) {
155 result.
error_comment = response.command_set().get_string(tag_error_comment);
163 dataset.
size() *
sizeof(uint32_t),
164 std::memory_order_relaxed
167 failures_.fetch_add(1, std::memory_order_relaxed);
179 const std::vector<core::dicom_dataset>& datasets,
182 std::vector<store_result> results;
183 results.reserve(datasets.size());
185 const size_t total = datasets.size();
188 for (
const auto& dataset : datasets) {
189 auto result =
store(assoc, dataset);
191 if (result.is_ok()) {
192 results.push_back(std::move(result.value()));
199 results.push_back(std::move(failure_result));
210 if (progress_callback) {
224 const std::filesystem::path& file_path) {
227 if (!std::filesystem::exists(file_path)) {
230 "File not found: " + file_path.string());
234 if (!std::filesystem::is_regular_file(file_path)) {
237 "Not a regular file: " + file_path.string());
242 if (file_result.is_err()) {
243 const auto error_msg =
"Failed to parse DICOM file: " +
244 file_path.string() +
": " +
245 file_result.error().message;
252 const auto& dataset = file_result.value().dataset();
255 return store(assoc, dataset);
260 const std::vector<std::filesystem::path>& file_paths,
263 std::vector<store_result> results;
264 results.reserve(file_paths.size());
266 const size_t total = file_paths.size();
269 for (
const auto& file_path : file_paths) {
272 if (result.is_ok()) {
273 results.push_back(std::move(result.value()));
280 results.push_back(std::move(failure_result));
291 if (progress_callback) {
301 const std::filesystem::path& directory,
309 return store_files(assoc, files, progress_callback);
313 const std::filesystem::path& directory,
314 bool recursive)
const {
316 std::vector<std::filesystem::path> files;
318 if (!std::filesystem::exists(directory) ||
319 !std::filesystem::is_directory(directory)) {
324 for (
const auto& entry :
325 std::filesystem::recursive_directory_iterator(directory)) {
326 if (entry.is_regular_file() && is_dicom_file(entry.path())) {
327 files.push_back(entry.path());
331 for (
const auto& entry :
332 std::filesystem::directory_iterator(directory)) {
333 if (entry.is_regular_file() && is_dicom_file(entry.path())) {
334 files.push_back(entry.path());
340 std::sort(files.begin(), files.end());
354 return failures_.load(std::memory_order_relaxed);
358 return bytes_sent_.load(std::memory_order_relaxed);
363 failures_.store(0, std::memory_order_relaxed);
auto size() const noexcept -> size_t
Get the number of elements in the dataset.
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.
bool is_established() const noexcept
Check if association is established and ready for DIMSE.
Result< std::monostate > send_dimse(uint8_t context_id, const dimse::dimse_message &msg)
Send a DIMSE message.
Result< std::pair< uint8_t, dimse::dimse_message > > receive_dimse(duration timeout=default_timeout)
Receive a DIMSE message.
std::optional< uint8_t > accepted_context_id(std::string_view abstract_syntax) const
Get the presentation context ID for an abstract syntax.
std::atomic< size_t > failures_
Statistics: number of failed operations.
size_t bytes_sent() const noexcept
Get the total bytes sent since construction.
std::atomic< uint16_t > message_id_counter_
Message ID counter.
network::Result< store_result > store_file(network::association &assoc, const std::filesystem::path &file_path)
Store a DICOM file.
std::atomic< size_t > images_sent_
Statistics: number of images sent successfully.
storage_scu(std::shared_ptr< di::ILogger > logger=nullptr)
Construct a Storage SCU with default configuration.
std::vector< store_result > store_batch(network::association &assoc, const std::vector< core::dicom_dataset > &datasets, store_progress_callback progress_callback=nullptr)
Store multiple DICOM datasets.
size_t images_sent() const noexcept
Get the number of images sent since construction.
network::Result< store_result > store(network::association &assoc, const core::dicom_dataset &dataset)
Store a single DICOM dataset.
size_t failures() const noexcept
Get the number of failed store operations since construction.
storage_scu_config config_
Configuration.
void reset_statistics() noexcept
Reset statistics counters to zero.
std::atomic< size_t > bytes_sent_
Statistics: total bytes sent.
std::vector< std::filesystem::path > collect_dicom_files(const std::filesystem::path &directory, bool recursive) const
Collect DICOM files from a directory.
std::vector< store_result > store_directory(network::association &assoc, const std::filesystem::path &directory, bool recursive=true, store_progress_callback progress_callback=nullptr)
Store all DICOM files in a directory.
uint16_t next_message_id() noexcept
Get the next message ID for DIMSE operations.
std::shared_ptr< di::ILogger > logger_
Logger instance for service logging.
network::Result< store_result > store_impl(network::association &assoc, const core::dicom_dataset &dataset, uint16_t message_id)
Internal implementation of single store operation.
std::vector< store_result > store_files(network::association &assoc, const std::vector< std::filesystem::path > &file_paths, store_progress_callback progress_callback=nullptr)
Store multiple DICOM files.
DIMSE command field enumeration.
DICOM Part 10 file handling for reading/writing DICOM files.
constexpr int file_parse_failed
constexpr int not_a_regular_file
constexpr int store_unexpected_command
constexpr int store_missing_sop_instance_uid
constexpr int file_not_found_service
constexpr int association_not_established
constexpr int store_no_accepted_context
constexpr int store_missing_sop_class_uid
@ cannot_understand
Failure: Cannot understand - processing failure (0xC000)
@ completed
Procedure completed successfully.
auto to_string(mpps_status status) -> std::string_view
Convert mpps_status to DICOM string representation.
std::function< void(size_t completed, size_t total)> store_progress_callback
Progress callback type for batch operations.
Result< T > pacs_error(int code, const std::string &message, const std::string &details="")
Create a PACS error result with module context.
Result<T> type aliases and helpers for PACS system.
DICOM Storage SCP service (C-STORE handler)
DICOM Storage SCU service (C-STORE sender)
Configuration for Storage SCU service.
std::chrono::milliseconds response_timeout
Timeout for receiving C-STORE response (milliseconds)
bool continue_on_error
Continue batch operation on error (true) or stop on first error (false)
uint16_t default_priority
Default priority for C-STORE requests (0=medium, 1=high, 2=low)
Result of a C-STORE operation.
uint16_t status
DIMSE status code (0x0000 = success)
bool is_warning() const noexcept
Check if this was a warning status.
std::string error_comment
Error comment from the SCP (if any)
std::string sop_instance_uid
SOP Instance UID of the stored instance.
bool is_success() const noexcept
Check if the store operation was successful.