24using kcenon::common::make_error;
25using kcenon::common::ok;
30constexpr int kMissingRequiredUid = -1;
31constexpr int kDuplicateInstance = -2;
32constexpr int kFileNotFound = -3;
33constexpr int kFileWriteError = -4;
34constexpr int kFileReadError = -5;
35constexpr int kDirectoryCreateError = -6;
36constexpr int kIntegrityError = -7;
39auto generate_temp_filename(
const std::filesystem::path& base)
40 -> std::filesystem::path {
41 static std::random_device rd;
42 static std::mt19937 gen(rd());
43 static std::uniform_int_distribution<uint64_t> dist;
45 auto temp_name = base.filename().string() +
".tmp." +
46 std::to_string(dist(gen));
47 return base.parent_path() / temp_name;
61 std::filesystem::create_directories(config_.root_path, ec);
67 (void)rebuild_index();
81 if (study_uid.empty() || series_uid.empty() || sop_uid.empty()) {
82 return make_error<std::monostate>(
84 "Missing required UID (Study, Series, or SOP Instance UID)",
89 std::filesystem::path file_path;
90 switch (config_.naming) {
92 file_path = build_path(study_uid, series_uid, sop_uid);
96 if (study_date.empty()) {
98 auto now = std::chrono::system_clock::now();
99 auto time = std::chrono::system_clock::to_time_t(now);
102 localtime_s(&tm_buf, &time);
104 localtime_r(&time, &tm_buf);
107 std::strftime(date_str,
sizeof(date_str),
"%Y%m%d", &tm_buf);
108 study_date = date_str;
110 file_path = build_date_path(study_date, study_uid, sop_uid);
114 file_path = config_.root_path /
115 (sanitize_uid(sop_uid) + config_.file_extension);
121 std::shared_lock lock(mutex_);
122 if (index_.contains(sop_uid)) {
123 switch (config_.duplicate) {
125 return make_error<std::monostate>(
127 "Instance already exists: " + sop_uid,
139 if (config_.create_directories) {
141 std::filesystem::create_directories(file_path.parent_path(), ec);
143 return make_error<std::monostate>(
144 kDirectoryCreateError,
145 "Failed to create directory: " + ec.message(),
155 auto temp_path = generate_temp_filename(file_path);
156 auto save_result = dicom_file.save(temp_path);
158 if (save_result.is_err()) {
159 std::filesystem::remove(temp_path);
160 return make_error<std::monostate>(
162 "Failed to write DICOM file: " + save_result.error().message,
168 std::filesystem::rename(temp_path, file_path, ec);
170 std::filesystem::remove(temp_path);
171 return make_error<std::monostate>(
173 "Failed to rename temp file: " + ec.message(),
179 std::unique_lock lock(mutex_);
180 index_[sop_uid] = file_path;
188 std::filesystem::path file_path;
191 std::shared_lock lock(mutex_);
192 auto it = index_.find(std::string{sop_instance_uid});
193 if (it == index_.end()) {
194 return make_error<core::dicom_dataset>(
196 "Instance not found: " + std::string{sop_instance_uid},
199 file_path = it->second;
204 if (open_result.is_err()) {
205 return make_error<core::dicom_dataset>(
207 "Failed to read DICOM file: " + open_result.error().message,
211 return open_result.value().dataset();
215 std::filesystem::path file_path;
218 std::unique_lock lock(mutex_);
219 auto it = index_.find(std::string{sop_instance_uid});
220 if (it == index_.end()) {
224 file_path = it->second;
230 std::filesystem::remove(file_path, ec);
234 auto parent = file_path.parent_path();
235 while (parent != config_.root_path) {
236 if (std::filesystem::is_empty(parent)) {
237 std::filesystem::remove(parent, ec);
238 parent = parent.parent_path();
248 std::shared_lock lock(mutex_);
249 return index_.contains(std::string{sop_instance_uid});
254 std::vector<core::dicom_dataset> results;
256 std::vector<std::filesystem::path> paths_to_check;
258 std::shared_lock lock(mutex_);
259 paths_to_check.reserve(index_.size());
260 for (
const auto& [
uid, path] : index_) {
261 paths_to_check.push_back(path);
265 for (
const auto& path : paths_to_check) {
267 if (open_result.is_err()) {
271 const auto& dataset = open_result.value().dataset();
272 if (matches_query(dataset, query)) {
273 results.push_back(dataset);
283 std::set<std::string> studies;
284 std::set<std::string> series;
285 std::set<std::string> patients;
287 std::vector<std::filesystem::path> paths;
289 std::shared_lock lock(
mutex_);
290 stats.total_instances =
index_.size();
291 paths.reserve(
index_.size());
293 paths.push_back(path);
297 for (
const auto& path : paths) {
299 stats.total_bytes += std::filesystem::file_size(path, ec);
303 if (open_result.is_ok()) {
304 const auto& ds = open_result.value().dataset();
309 if (!study_uid.empty()) {
310 studies.insert(study_uid);
312 if (!series_uid.empty()) {
313 series.insert(series_uid);
315 if (!patient_id.empty()) {
316 patients.insert(patient_id);
321 stats.studies_count = studies.size();
322 stats.series_count = series.size();
323 stats.patients_count = patients.size();
329 std::vector<std::pair<std::string, std::filesystem::path>> entries;
331 std::shared_lock lock(mutex_);
332 entries.reserve(index_.size());
333 for (
const auto& [
uid, path] : index_) {
334 entries.emplace_back(
uid, path);
338 std::vector<std::string> invalid_entries;
340 for (
const auto& [
uid, path] : entries) {
341 if (!std::filesystem::exists(path)) {
342 invalid_entries.push_back(
uid +
" (file missing)");
347 if (open_result.is_err()) {
348 invalid_entries.push_back(
uid +
" (invalid DICOM)");
355 if (file_uid !=
uid) {
356 invalid_entries.push_back(
uid +
" (UID mismatch)");
360 if (!invalid_entries.empty()) {
361 std::string message =
"Integrity check failed for " +
362 std::to_string(invalid_entries.size()) +
364 return make_error<std::monostate>(kIntegrityError, message,
376 -> std::filesystem::path {
377 std::shared_lock lock(mutex_);
378 auto it = index_.find(std::string{sop_instance_uid});
379 if (it != index_.end()) {
387 if (!std::filesystem::exists(source)) {
388 return make_error<std::monostate>(
390 "Source directory does not exist: " + source.string(),
395 for (
const auto& entry :
396 std::filesystem::recursive_directory_iterator(source, ec)) {
397 if (!entry.is_regular_file()) {
403 if (open_result.is_err()) {
408 auto store_result = store(open_result.value().dataset());
409 if (store_result.is_err()) {
423 std::unique_lock lock(mutex_);
426 if (!std::filesystem::exists(config_.root_path)) {
431 for (
const auto& entry :
432 std::filesystem::recursive_directory_iterator(config_.root_path, ec)) {
433 if (!entry.is_regular_file()) {
438 if (!config_.file_extension.empty() &&
439 entry.path().extension() != config_.file_extension) {
445 if (open_result.is_err()) {
451 if (!sop_uid.empty()) {
452 index_[sop_uid] = entry.path();
464 std::string_view series_uid,
465 std::string_view sop_uid)
const
466 -> std::filesystem::path {
467 return config_.root_path / sanitize_uid(study_uid) /
468 sanitize_uid(series_uid) /
469 (sanitize_uid(sop_uid) + config_.file_extension);
473 std::string_view study_uid,
474 std::string_view sop_uid)
const
475 -> std::filesystem::path {
477 std::string year =
"unknown";
478 std::string month =
"01";
479 std::string day =
"01";
481 if (study_date.length() >= 8) {
482 year = std::string{study_date.substr(0, 4)};
483 month = std::string{study_date.substr(4, 2)};
484 day = std::string{study_date.substr(6, 2)};
487 return config_.root_path / year / month / day / sanitize_uid(study_uid) /
488 (sanitize_uid(sop_uid) + config_.file_extension);
492 const std::filesystem::path& path) {
493 std::unique_lock lock(
mutex_);
498 std::unique_lock lock(
mutex_);
510 for (
const auto& [tag, element] : query) {
511 auto query_value = element.as_string().unwrap_or(
"");
512 if (query_value.empty()) {
516 auto dataset_value = dataset.get_string(tag);
519 if (query_value.find(
'*') != std::string::npos ||
520 query_value.find(
'?') != std::string::npos) {
522 std::string pattern = query_value;
525 for (
char c : pattern) {
528 }
else if (c ==
'?') {
530 }
else if (c ==
'.' || c ==
'[' || c ==
']' || c ==
'(' ||
531 c ==
')' || c ==
'+' || c ==
'^' || c ==
'$' ||
532 c ==
'|' || c ==
'\\') {
542 if (query_value.front() ==
'*' && query_value.back() ==
'*') {
545 query_value.substr(1, query_value.length() - 2);
546 if (dataset_value.find(inner) == std::string::npos) {
549 }
else if (query_value.front() ==
'*') {
551 auto suffix = query_value.substr(1);
552 if (dataset_value.length() < suffix.length() ||
553 dataset_value.substr(dataset_value.length() -
554 suffix.length()) != suffix) {
557 }
else if (query_value.back() ==
'*') {
559 auto prefix = query_value.substr(0, query_value.length() - 1);
560 if (dataset_value.substr(0, prefix.length()) != prefix) {
566 if (dataset_value != query_value) {
577 result.reserve(
uid.length());
582 if (std::isalnum(
static_cast<unsigned char>(c)) || c ==
'.') {
if(!color.empty()) style.color
static auto open(const std::filesystem::path &path) -> kcenon::pacs::Result< dicom_file >
Open and read a DICOM file from disk.
static auto create(dicom_dataset dataset, const encoding::transfer_syntax &ts) -> dicom_file
Create a new DICOM file from a dataset.
static const transfer_syntax explicit_vr_little_endian
Explicit VR Little Endian (1.2.840.10008.1.2.1)
auto retrieve(std::string_view sop_instance_uid) -> Result< core::dicom_dataset > override
Retrieve a DICOM dataset by SOP Instance UID.
auto import_directory(const std::filesystem::path &source) -> VoidResult
Import DICOM files from a directory.
auto build_date_path(std::string_view study_date, std::string_view study_uid, std::string_view sop_uid) const -> std::filesystem::path
Build filesystem path using date-based hierarchy.
auto find(const core::dicom_dataset &query) -> Result< std::vector< core::dicom_dataset > > override
Find DICOM datasets matching query criteria.
std::unordered_map< std::string, std::filesystem::path > index_
Mapping from SOP Instance UID to file path.
void update_index(const std::string &sop_uid, const std::filesystem::path &path)
Update internal index with new mapping.
file_storage(const file_storage_config &config)
Construct file storage with configuration.
auto get_file_path(std::string_view sop_instance_uid) const -> std::filesystem::path
Get the filesystem path for a SOP Instance UID.
void remove_from_index(const std::string &sop_uid)
Remove entry from internal index.
static auto sanitize_uid(std::string_view uid) -> std::string
Sanitize UID for use in filesystem path.
auto rebuild_index() -> VoidResult
Rebuild the internal index from filesystem.
file_storage_config config_
Storage configuration.
std::shared_mutex mutex_
Mutex for thread-safe access.
auto root_path() const -> const std::filesystem::path &
Get the root storage path.
auto get_statistics() const -> storage_statistics override
Get storage statistics.
auto exists(std::string_view sop_instance_uid) const -> bool override
Check if a DICOM instance exists.
auto remove(std::string_view sop_instance_uid) -> VoidResult override
Remove a DICOM file by SOP Instance UID.
auto store(const core::dicom_dataset &dataset) -> VoidResult override
Store a DICOM dataset to filesystem.
static auto matches_query(const core::dicom_dataset &dataset, const core::dicom_dataset &query) -> bool
Check if dataset matches query criteria.
auto verify_integrity() -> VoidResult override
Verify storage integrity.
auto build_path(std::string_view study_uid, std::string_view series_uid, std::string_view sop_uid) const -> std::filesystem::path
Build filesystem path for a dataset.
Compile-time constants for commonly used DICOM tags.
Filesystem-based DICOM storage with hierarchical organization.
@ flat
{SOPUID}.dcm (flat structure)
@ date_hierarchical
YYYY/MM/DD/{StudyUID}/{SOPUID}.dcm.
@ uid_hierarchical
{StudyUID}/{SeriesUID}/{SOPUID}.dcm
@ ignore
Skip silently if instance exists.
@ reject
Return error if instance already exists.
@ replace
Overwrite existing instance.
Configuration for file_storage.
bool create_directories
Create directories automatically if they don't exist.
std::filesystem::path root_path
Root directory for storage.
Storage statistics structure.