PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
sync_conflict_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_conflict_repository::sync_conflict_repository(
70 std::shared_ptr<pacs_database_adapter> db)
71 : base_repository(std::move(db), "sync_conflicts", "study_uid") {}
72
73auto sync_conflict_repository::find_by_study_uid(std::string_view study_uid)
74 -> result_type {
75 return find_by_id(std::string(study_uid));
76}
77
78auto sync_conflict_repository::find_by_config(std::string_view config_id)
79 -> list_result_type {
80 return find_where("config_id", "=", std::string(config_id));
81}
82
83auto sync_conflict_repository::find_unresolved() -> list_result_type {
84 return find_where("resolved", "=", static_cast<int64_t>(0));
85}
86
87auto sync_conflict_repository::resolve(
88 std::string_view study_uid,
89 client::conflict_resolution resolution) -> VoidResult {
90 if (!db() || !db()->is_connected()) {
91 return VoidResult(kcenon::common::error_info{
92 -1, "Database not connected", "sync_conflict_repository"});
93 }
94
95 std::ostringstream sql;
96 sql << R"(
97 UPDATE sync_conflicts SET
98 resolved = 1,
99 resolution = ')"
100 << client::to_string(resolution) << R"(',
101 resolved_at = datetime('now')
102 WHERE study_uid = ')"
103 << study_uid << "'";
104
105 auto result = storage_session().update(sql.str());
106 if (result.is_err()) {
107 return VoidResult(result.error());
108 }
109
110 return kcenon::common::ok();
111}
112
113auto sync_conflict_repository::cleanup_old(std::chrono::hours max_age)
114 -> Result<size_t> {
115 if (!db() || !db()->is_connected()) {
116 return Result<size_t>(kcenon::common::error_info{
117 -1, "Database not connected", "sync_conflict_repository"});
118 }
119
120 std::ostringstream sql;
121 sql << R"(
122 DELETE FROM sync_conflicts
123 WHERE resolved = 1 AND resolved_at < datetime('now', '-)"
124 << max_age.count() << R"( hours'))";
125
126 auto result = storage_session().remove(sql.str());
127 if (result.is_err()) {
128 return Result<size_t>(result.error());
129 }
130
131 return Result<size_t>(static_cast<size_t>(result.value()));
132}
133
134auto sync_conflict_repository::map_row_to_entity(const database_row& row) const
135 -> client::sync_conflict {
136 client::sync_conflict conflict;
137
138 conflict.pk = std::stoll(row.at("pk"));
139 conflict.config_id = row.at("config_id");
140 conflict.study_uid = row.at("study_uid");
141 conflict.patient_id = row.at("patient_id");
142 conflict.conflict_type =
143 client::sync_conflict_type_from_string(row.at("conflict_type"));
144 conflict.local_modified = parse_timestamp(row.at("local_modified"));
145 conflict.remote_modified = parse_timestamp(row.at("remote_modified"));
146 conflict.local_instance_count =
147 std::stoull(row.at("local_instance_count"));
148 conflict.remote_instance_count =
149 std::stoull(row.at("remote_instance_count"));
150 conflict.resolved = (row.at("resolved") == "1");
151 conflict.resolution_used =
152 client::conflict_resolution_from_string(row.at("resolution"));
153 conflict.detected_at = parse_timestamp(row.at("detected_at"));
154
155 auto resolved_at_str = row.at("resolved_at");
156 if (!resolved_at_str.empty()) {
157 conflict.resolved_at = parse_timestamp(resolved_at_str);
158 }
159
160 return conflict;
161}
162
163auto sync_conflict_repository::entity_to_row(
164 const client::sync_conflict& entity) const
165 -> std::map<std::string, database_value> {
166 std::map<std::string, database_value> row = {
167 {"config_id", entity.config_id},
168 {"study_uid", entity.study_uid},
169 {"patient_id", entity.patient_id},
170 {"conflict_type", std::string(client::to_string(entity.conflict_type))},
171 {"local_modified", format_timestamp(entity.local_modified)},
172 {"remote_modified", format_timestamp(entity.remote_modified)},
173 {"local_instance_count",
174 static_cast<int64_t>(entity.local_instance_count)},
175 {"remote_instance_count",
176 static_cast<int64_t>(entity.remote_instance_count)},
177 {"resolved", static_cast<int64_t>(entity.resolved ? 1 : 0)},
178 {"resolution",
179 entity.resolved ? std::string(client::to_string(entity.resolution_used))
180 : std::string("")},
181 {"detected_at", format_timestamp(entity.detected_at)}};
182
183 if (entity.resolved_at.has_value()) {
184 row["resolved_at"] = format_timestamp(entity.resolved_at.value());
185 }
186
187 return row;
188}
189
190auto sync_conflict_repository::get_pk(const client::sync_conflict& entity) const
191 -> std::string {
192 return entity.study_uid;
193}
194
195auto sync_conflict_repository::has_pk(const client::sync_conflict& entity) const
196 -> bool {
197 return !entity.study_uid.empty();
198}
199
200auto sync_conflict_repository::select_columns() const
201 -> std::vector<std::string> {
202 return {"pk",
203 "config_id",
204 "study_uid",
205 "patient_id",
206 "conflict_type",
207 "local_modified",
208 "remote_modified",
209 "local_instance_count",
210 "remote_instance_count",
211 "resolved",
212 "resolution",
213 "detected_at",
214 "resolved_at"};
215}
216
217auto sync_conflict_repository::parse_timestamp(const std::string& str) const
218 -> std::chrono::system_clock::time_point {
219 return from_timestamp_string(str);
220}
221
222auto sync_conflict_repository::format_timestamp(
223 std::chrono::system_clock::time_point tp) const -> std::string {
224 return to_timestamp_string(tp);
225}
226
227} // namespace kcenon::pacs::storage
228
229#endif // PACS_WITH_DATABASE_SYSTEM
@ move
C-MOVE move request/response.
Repository for sync conflict records using base_repository pattern.