PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
storage_scu.cpp
Go to the documentation of this file.
1// BSD 3-Clause License
2// Copyright (c) 2021-2025, 🍀☀🌕🌥 🌊
3// See the LICENSE file in the project root for full license information.
4
16
17#include <algorithm>
18#include <array>
19
20namespace kcenon::pacs::services {
21
22// =============================================================================
23// DICOM Tag Constants (for dataset extraction)
24// =============================================================================
25
26namespace {
27
29constexpr core::dicom_tag tag_sop_class_uid{0x0008, 0x0016};
30
32constexpr core::dicom_tag tag_sop_instance_uid{0x0008, 0x0018};
33
35constexpr std::array<std::string_view, 4> dicom_extensions = {
36 ".dcm", ".DCM", ".dicom", ".DICOM"
37};
38
40[[nodiscard]] bool is_dicom_file(const std::filesystem::path& path) {
41 const auto ext = path.extension().string();
42 if (ext.empty()) {
43 // Files without extension might still be DICOM
44 return true;
45 }
46 return std::find(dicom_extensions.begin(), dicom_extensions.end(), ext)
47 != dicom_extensions.end();
48}
49
50} // namespace
51
52// =============================================================================
53// Construction
54// =============================================================================
55
56storage_scu::storage_scu(std::shared_ptr<di::ILogger> logger)
57 : logger_(logger ? std::move(logger) : di::null_logger()) {}
58
60 std::shared_ptr<di::ILogger> logger)
61 : logger_(logger ? std::move(logger) : di::null_logger()), config_(config) {}
62
63// =============================================================================
64// Single Image Operations
65// =============================================================================
66
69 const core::dicom_dataset& dataset) {
70
71 return store_impl(assoc, dataset, next_message_id());
72}
73
76 const core::dicom_dataset& dataset,
77 uint16_t message_id) {
78
79 using namespace network::dimse;
80
81 // Extract SOP Class UID from dataset
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");
87 }
88
89 // Extract SOP Instance UID from 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");
95 }
96
97 // Verify association is established
98 if (!assoc.is_established()) {
101 "Association not established");
102 }
103
104 // Get accepted presentation context for this SOP class
105 auto context_id = assoc.accepted_context_id(sop_class_uid);
106 if (!context_id) {
109 "No accepted presentation context for SOP Class: " + sop_class_uid);
110 }
111
112 // Build C-STORE-RQ message
113 auto request = make_c_store_rq(
114 message_id,
115 sop_class_uid,
116 sop_instance_uid,
118 );
119
120 // Attach the dataset
121 request.set_dataset(dataset);
122
123 // Send the request
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();
128 }
129
130 // Receive the response
131 auto recv_result = assoc.receive_dimse(config_.response_timeout);
132 if (recv_result.is_err()) {
133 failures_.fetch_add(1, std::memory_order_relaxed);
134 return recv_result.error();
135 }
136
137 const auto& [recv_context_id, response] = recv_result.value();
138
139 // Verify it's a C-STORE response
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())));
146 }
147
148 // Build result from response
149 store_result result;
150 result.sop_instance_uid = sop_instance_uid;
151 result.status = static_cast<uint16_t>(response.status());
152
153 // Extract error comment if present
154 if (response.command_set().contains(tag_error_comment)) {
155 result.error_comment = response.command_set().get_string(tag_error_comment);
156 }
157
158 // Update statistics
159 if (result.is_success() || result.is_warning()) {
160 images_sent_.fetch_add(1, std::memory_order_relaxed);
161 // Estimate dataset size
162 bytes_sent_.fetch_add(
163 dataset.size() * sizeof(uint32_t),
164 std::memory_order_relaxed
165 );
166 } else {
167 failures_.fetch_add(1, std::memory_order_relaxed);
168 }
169
170 return result;
171}
172
173// =============================================================================
174// Batch Operations
175// =============================================================================
176
177std::vector<store_result> storage_scu::store_batch(
179 const std::vector<core::dicom_dataset>& datasets,
180 store_progress_callback progress_callback) {
181
182 std::vector<store_result> results;
183 results.reserve(datasets.size());
184
185 const size_t total = datasets.size();
186 size_t completed = 0;
187
188 for (const auto& dataset : datasets) {
189 auto result = store(assoc, dataset);
190
191 if (result.is_ok()) {
192 results.push_back(std::move(result.value()));
193 } else {
194 // Create a failure result
195 store_result failure_result;
196 failure_result.sop_instance_uid = dataset.get_string(tag_sop_instance_uid);
197 failure_result.status = static_cast<uint16_t>(storage_status::cannot_understand);
198 failure_result.error_comment = result.error().message;
199 results.push_back(std::move(failure_result));
200
201 // Stop on error if configured
203 break;
204 }
205 }
206
207 ++completed;
208
209 // Report progress
210 if (progress_callback) {
211 progress_callback(completed, total);
212 }
213 }
214
215 return results;
216}
217
218// =============================================================================
219// File-based Operations
220// =============================================================================
221
224 const std::filesystem::path& file_path) {
225
226 // Check if file exists
227 if (!std::filesystem::exists(file_path)) {
230 "File not found: " + file_path.string());
231 }
232
233 // Check if it's a regular file
234 if (!std::filesystem::is_regular_file(file_path)) {
237 "Not a regular file: " + file_path.string());
238 }
239
240 // Parse the DICOM file
241 auto file_result = core::dicom_file::open(file_path);
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;
246 logger_->error(error_msg);
249 }
250
251 // Get the dataset from the parsed file
252 const auto& dataset = file_result.value().dataset();
253
254 // Store the dataset using the existing store method
255 return store(assoc, dataset);
256}
257
258std::vector<store_result> storage_scu::store_files(
260 const std::vector<std::filesystem::path>& file_paths,
261 store_progress_callback progress_callback) {
262
263 std::vector<store_result> results;
264 results.reserve(file_paths.size());
265
266 const size_t total = file_paths.size();
267 size_t completed = 0;
268
269 for (const auto& file_path : file_paths) {
270 auto result = store_file(assoc, file_path);
271
272 if (result.is_ok()) {
273 results.push_back(std::move(result.value()));
274 } else {
275 // Create a failure result
276 store_result failure_result;
277 failure_result.sop_instance_uid = file_path.filename().string();
278 failure_result.status = static_cast<uint16_t>(storage_status::cannot_understand);
279 failure_result.error_comment = result.error().message;
280 results.push_back(std::move(failure_result));
281
282 // Stop on error if configured
284 break;
285 }
286 }
287
288 ++completed;
289
290 // Report progress
291 if (progress_callback) {
292 progress_callback(completed, total);
293 }
294 }
295
296 return results;
297}
298
299std::vector<store_result> storage_scu::store_directory(
301 const std::filesystem::path& directory,
302 bool recursive,
303 store_progress_callback progress_callback) {
304
305 // Collect all DICOM files from the directory
306 auto files = collect_dicom_files(directory, recursive);
307
308 // Delegate to store_files for the actual processing
309 return store_files(assoc, files, progress_callback);
310}
311
312std::vector<std::filesystem::path> storage_scu::collect_dicom_files(
313 const std::filesystem::path& directory,
314 bool recursive) const {
315
316 std::vector<std::filesystem::path> files;
317
318 if (!std::filesystem::exists(directory) ||
319 !std::filesystem::is_directory(directory)) {
320 return files;
321 }
322
323 if (recursive) {
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());
328 }
329 }
330 } else {
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());
335 }
336 }
337 }
338
339 // Sort files for deterministic order
340 std::sort(files.begin(), files.end());
341
342 return files;
343}
344
345// =============================================================================
346// Statistics
347// =============================================================================
348
349size_t storage_scu::images_sent() const noexcept {
350 return images_sent_.load(std::memory_order_relaxed);
351}
352
353size_t storage_scu::failures() const noexcept {
354 return failures_.load(std::memory_order_relaxed);
355}
356
357size_t storage_scu::bytes_sent() const noexcept {
358 return bytes_sent_.load(std::memory_order_relaxed);
359}
360
362 images_sent_.store(0, std::memory_order_relaxed);
363 failures_.store(0, std::memory_order_relaxed);
364 bytes_sent_.store(0, std::memory_order_relaxed);
365}
366
367// =============================================================================
368// Private Helpers
369// =============================================================================
370
371uint16_t storage_scu::next_message_id() noexcept {
372 uint16_t id = message_id_counter_.fetch_add(1, std::memory_order_relaxed);
373 // Wrap around at 0xFFFF, skip 0 (reserved)
374 if (id == 0) {
375 id = message_id_counter_.fetch_add(1, std::memory_order_relaxed);
376 }
377 return id;
378}
379
380} // namespace kcenon::pacs::services
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
Definition result.h:206
constexpr int not_a_regular_file
Definition result.h:204
constexpr int store_unexpected_command
Definition result.h:151
constexpr int store_missing_sop_instance_uid
Definition result.h:147
constexpr int file_not_found_service
Definition result.h:203
constexpr int association_not_established
Definition result.h:202
constexpr int store_no_accepted_context
Definition result.h:148
constexpr int store_missing_sop_class_uid
Definition result.h:146
@ 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.
Definition mpps_scp.h:60
std::function< void(size_t completed, size_t total)> store_progress_callback
Progress callback type for batch operations.
Definition storage_scu.h:75
Result< T > pacs_error(int code, const std::string &message, const std::string &details="")
Create a PACS error result with module context.
Definition result.h:234
Result<T> type aliases and helpers for PACS system.
DIMSE status codes.
DICOM Storage SCP service (C-STORE handler)
DICOM Storage SCU service (C-STORE sender)
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
uint16_t default_priority
Default priority for C-STORE requests (0=medium, 1=high, 2=low)
Definition storage_scu.h:82
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