PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
sync_config_repository.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
14
15#include <chrono>
16#include <cstdio>
17#include <sstream>
18
19#ifdef PACS_WITH_DATABASE_SYSTEM
20
21namespace kcenon::pacs::storage {
22
23namespace {
24
26[[nodiscard]] std::string to_timestamp_string(
27 std::chrono::system_clock::time_point tp) {
28 if (tp == std::chrono::system_clock::time_point{}) {
29 return "";
30 }
31 auto time = std::chrono::system_clock::to_time_t(tp);
32 std::tm tm{};
33#ifdef _WIN32
34 gmtime_s(&tm, &time);
35#else
36 gmtime_r(&time, &tm);
37#endif
38 char buf[32];
39 std::strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", &tm);
40 return buf;
41}
42
44[[nodiscard]] std::chrono::system_clock::time_point from_timestamp_string(
45 const std::string& str) {
46 if (str.empty()) {
47 return {};
48 }
49 std::tm tm{};
50 if (std::sscanf(str.c_str(),
51 "%d-%d-%d %d:%d:%d",
52 &tm.tm_year,
53 &tm.tm_mon,
54 &tm.tm_mday,
55 &tm.tm_hour,
56 &tm.tm_min,
57 &tm.tm_sec) < 6) {
58 return {};
59 }
60 tm.tm_year -= 1900;
61 tm.tm_mon -= 1;
62#ifdef _WIN32
63 auto time = _mkgmtime(&tm);
64#else
65 auto time = timegm(&tm);
66#endif
67 return std::chrono::system_clock::from_time_t(time);
68}
69
70} // namespace
71
72// =============================================================================
73// Constructor
74// =============================================================================
75
76sync_config_repository::sync_config_repository(
77 std::shared_ptr<pacs_database_adapter> db)
78 : base_repository(std::move(db), "sync_configs", "config_id") {}
79
80// =============================================================================
81// Domain-Specific Queries
82// =============================================================================
83
84auto sync_config_repository::find_by_config_id(std::string_view config_id)
85 -> result_type {
86 return find_by_id(std::string(config_id));
87}
88
89auto sync_config_repository::find_enabled() -> list_result_type {
90 return find_where("enabled", "=", static_cast<int64_t>(1));
91}
92
93auto sync_config_repository::find_by_source_node(std::string_view node_id)
94 -> list_result_type {
95 return find_where("source_node_id", "=", std::string(node_id));
96}
97
98auto sync_config_repository::update_stats(
99 std::string_view config_id,
100 bool success,
101 size_t studies_synced) -> VoidResult {
102 if (!db() || !db()->is_connected()) {
103 return VoidResult(kcenon::common::error_info{
104 -1, "Database not connected", "sync_config_repository"});
105 }
106
107 std::ostringstream sql;
108 if (success) {
109 sql << R"(
110 UPDATE sync_configs SET
111 total_syncs = total_syncs + 1,
112 studies_synced = studies_synced + )"
113 << studies_synced << R"(,
114 last_sync = datetime('now'),
115 last_successful_sync = datetime('now'),
116 updated_at = datetime('now')
117 WHERE config_id = ')"
118 << config_id << "'";
119 } else {
120 sql << R"(
121 UPDATE sync_configs SET
122 total_syncs = total_syncs + 1,
123 last_sync = datetime('now'),
124 updated_at = datetime('now')
125 WHERE config_id = ')"
126 << config_id << "'";
127 }
128
129 auto result = storage_session().update(sql.str());
130 if (result.is_err()) {
131 return VoidResult(result.error());
132 }
133
134 return kcenon::common::ok();
135}
136
137// =============================================================================
138// base_repository Overrides
139// =============================================================================
140
141auto sync_config_repository::map_row_to_entity(const database_row& row) const
142 -> client::sync_config {
143 client::sync_config config;
144
145 config.pk = std::stoll(row.at("pk"));
146 config.config_id = row.at("config_id");
147 config.source_node_id = row.at("source_node_id");
148 config.name = row.at("name");
149 config.enabled = (row.at("enabled") == "1");
150 config.lookback = std::chrono::hours(std::stoi(row.at("lookback_hours")));
151 config.modalities = deserialize_vector(row.at("modalities_json"));
152 config.patient_id_patterns =
153 deserialize_vector(row.at("patient_patterns_json"));
154 config.direction =
155 client::sync_direction_from_string(row.at("sync_direction"));
156 config.delete_missing = (row.at("delete_missing") == "1");
157 config.overwrite_existing = (row.at("overwrite_existing") == "1");
158 config.sync_metadata_only = (row.at("sync_metadata_only") == "1");
159 config.schedule_cron = row.at("schedule_cron");
160 config.last_sync = parse_timestamp(row.at("last_sync"));
161 config.last_successful_sync =
162 parse_timestamp(row.at("last_successful_sync"));
163 config.total_syncs = std::stoull(row.at("total_syncs"));
164 config.studies_synced = std::stoull(row.at("studies_synced"));
165
166 return config;
167}
168
169auto sync_config_repository::entity_to_row(const client::sync_config& entity)
170 const -> std::map<std::string, database_value> {
171 return {{"config_id", entity.config_id},
172 {"source_node_id", entity.source_node_id},
173 {"name", entity.name},
174 {"enabled", static_cast<int64_t>(entity.enabled ? 1 : 0)},
175 {"lookback_hours", static_cast<int64_t>(entity.lookback.count())},
176 {"modalities_json", serialize_vector(entity.modalities)},
177 {"patient_patterns_json",
178 serialize_vector(entity.patient_id_patterns)},
179 {"sync_direction", std::string(client::to_string(entity.direction))},
180 {"delete_missing", static_cast<int64_t>(entity.delete_missing ? 1 : 0)},
181 {"overwrite_existing",
182 static_cast<int64_t>(entity.overwrite_existing ? 1 : 0)},
183 {"sync_metadata_only",
184 static_cast<int64_t>(entity.sync_metadata_only ? 1 : 0)},
185 {"schedule_cron", entity.schedule_cron},
186 {"last_sync", format_timestamp(entity.last_sync)},
187 {"last_successful_sync",
188 format_timestamp(entity.last_successful_sync)},
189 {"total_syncs", static_cast<int64_t>(entity.total_syncs)},
190 {"studies_synced", static_cast<int64_t>(entity.studies_synced)}};
191}
192
193auto sync_config_repository::get_pk(const client::sync_config& entity) const
194 -> std::string {
195 return entity.config_id;
196}
197
198auto sync_config_repository::has_pk(const client::sync_config& entity) const
199 -> bool {
200 return !entity.config_id.empty();
201}
202
203auto sync_config_repository::select_columns() const
204 -> std::vector<std::string> {
205 return {"pk",
206 "config_id",
207 "source_node_id",
208 "name",
209 "enabled",
210 "lookback_hours",
211 "modalities_json",
212 "patient_patterns_json",
213 "sync_direction",
214 "delete_missing",
215 "overwrite_existing",
216 "sync_metadata_only",
217 "schedule_cron",
218 "last_sync",
219 "last_successful_sync",
220 "total_syncs",
221 "studies_synced"};
222}
223
224// =============================================================================
225// Private Helpers
226// =============================================================================
227
228auto sync_config_repository::parse_timestamp(const std::string& str) const
229 -> std::chrono::system_clock::time_point {
230 return from_timestamp_string(str);
231}
232
233auto sync_config_repository::format_timestamp(
234 std::chrono::system_clock::time_point tp) const -> std::string {
235 return to_timestamp_string(tp);
236}
237
238std::string sync_config_repository::serialize_vector(
239 const std::vector<std::string>& vec) {
240 if (vec.empty()) return "[]";
241
242 std::ostringstream oss;
243 oss << "[";
244 for (size_t i = 0; i < vec.size(); ++i) {
245 if (i > 0) oss << ",";
246 oss << "\"";
247 for (char c : vec[i]) {
248 if (c == '"')
249 oss << "\\\"";
250 else if (c == '\\')
251 oss << "\\\\";
252 else
253 oss << c;
254 }
255 oss << "\"";
256 }
257 oss << "]";
258 return oss.str();
259}
260
261std::vector<std::string> sync_config_repository::deserialize_vector(
262 std::string_view json) {
263 std::vector<std::string> result;
264 if (json.empty() || json == "[]") return result;
265
266 size_t pos = 0;
267 while (pos < json.size()) {
268 auto start = json.find('"', pos);
269 if (start == std::string_view::npos) break;
270
271 size_t end = start + 1;
272 while (end < json.size()) {
273 if (json[end] == '\\' && end + 1 < json.size()) {
274 end += 2;
275 } else if (json[end] == '"') {
276 break;
277 } else {
278 ++end;
279 }
280 }
281
282 if (end < json.size()) {
283 std::string value{json.substr(start + 1, end - start - 1)};
284 std::string unescaped;
285 for (size_t i = 0; i < value.size(); ++i) {
286 if (value[i] == '\\' && i + 1 < value.size()) {
287 unescaped += value[++i];
288 } else {
289 unescaped += value[i];
290 }
291 }
292 result.push_back(std::move(unescaped));
293 }
294
295 pos = end + 1;
296 }
297
298 return result;
299}
300
301} // namespace kcenon::pacs::storage
302
303#endif // PACS_WITH_DATABASE_SYSTEM
@ move
C-MOVE move request/response.
Repository for sync config records using base_repository pattern.