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