53std::optional<transaction_state>
parse_state(std::string_view str)
noexcept {
66 std::lock_guard lock(
mutex_);
72 std::string_view transaction_uid)
const {
73 std::lock_guard lock(
mutex_);
82 std::lock_guard lock(
mutex_);
92 std::lock_guard lock(
mutex_);
93 std::vector<commitment_transaction> result;
98 if (limit > 0 && result.size() >= limit)
break;
99 result.push_back(it->second);
105 std::lock_guard lock(
mutex_);
115std::string format_timestamp(std::chrono::system_clock::time_point tp) {
116 auto time_t = std::chrono::system_clock::to_time_t(tp);
119 gmtime_s(&tm, &time_t);
121 gmtime_r(&time_t, &tm);
124 std::strftime(buf,
sizeof(buf),
"%Y-%m-%dT%H:%M:%SZ", &tm);
133std::string failed_reference_to_json(
134 const std::pair<services::sop_reference,
136 return R
"({"sopClassUID":")" + json_escape(entry.first.sop_class_uid) +
137 R"(","sopInstanceUID":")" + json_escape(entry.first.sop_instance_uid) +
138 R"(","failureReason":")" +
145 std::ostringstream ss;
153 ss << R"("createdAt":")" << format_timestamp(txn.created_at) << R"(",)";
156 ss << R
"("completedAt":")" << format_timestamp(*txn.completed_at) << R"(",)";
160 ss << R
"("requestedReferences":[)";
162 if (i > 0) ss <<
",";
168 ss << R
"("successReferences":[)";
170 if (i > 0) ss <<
",";
176 ss << R
"("failedReferences":[)";
178 if (i > 0) ss <<
",";
187 const std::vector<commitment_transaction>& transactions) {
188 std::ostringstream ss;
190 for (
size_t i = 0; i < transactions.size(); ++i) {
191 if (i > 0) ss <<
",";
205std::string extract_json_string(std::string_view json, std::string_view key) {
206 auto key_pattern = std::string(
"\"") + std::string(key) +
"\"";
207 auto pos = json.find(key_pattern);
208 if (pos == std::string_view::npos)
return {};
210 pos = json.find(
'"', pos + key_pattern.size());
211 if (pos == std::string_view::npos)
return {};
214 auto end = json.find(
'"', pos);
215 if (end == std::string_view::npos)
return {};
217 return std::string(json.substr(pos, end - pos));
223 std::string_view json_body,
224 std::string_view study_uid) {
228 if (json_body.empty()) {
234 auto seq_pos = json_body.find(
"\"referencedSOPSequence\"");
235 if (seq_pos == std::string_view::npos) {
236 result.
error_message =
"Missing referencedSOPSequence in request body";
241 auto arr_start = json_body.find(
'[', seq_pos);
242 if (arr_start == std::string_view::npos) {
243 result.
error_message =
"Invalid referencedSOPSequence format";
249 size_t arr_end = std::string_view::npos;
250 for (
size_t i = arr_start; i < json_body.size(); ++i) {
251 if (json_body[i] ==
'[') ++depth;
252 else if (json_body[i] ==
']') {
261 if (arr_end == std::string_view::npos) {
262 result.
error_message =
"Unterminated referencedSOPSequence array";
267 auto array_content = json_body.substr(arr_start + 1, arr_end - arr_start - 1);
271 while (pos < array_content.size()) {
272 auto obj_start = array_content.find(
'{', pos);
273 if (obj_start == std::string_view::npos)
break;
275 size_t obj_depth = 0;
276 size_t obj_end = std::string_view::npos;
277 for (
size_t i = obj_start; i < array_content.size(); ++i) {
278 if (array_content[i] ==
'{') ++obj_depth;
279 else if (array_content[i] ==
'}') {
281 if (obj_depth == 0) {
288 if (obj_end == std::string_view::npos)
break;
290 auto obj = array_content.substr(obj_start, obj_end - obj_start + 1);
291 auto sop_class = extract_json_string(obj,
"sopClassUID");
292 auto sop_instance = extract_json_string(obj,
"sopInstanceUID");
294 if (sop_instance.empty()) {
295 result.
error_message =
"Missing sopInstanceUID in reference";
299 result.
references.push_back({std::move(sop_class), std::move(sop_instance)});
304 result.
error_message =
"No valid SOP references found in request";
325void add_cors_headers(crow::response& res,
const rest_server_context& ctx) {
326 if (ctx.config !=
nullptr && !ctx.config->cors_allowed_origins.empty()) {
327 res.add_header(
"Access-Control-Allow-Origin",
328 ctx.config->cors_allowed_origins);
332bool check_storage_commitment_auth(
333 const std::shared_ptr<rest_server_context>& ctx,
334 const crow::request& req,
336 const std::vector<std::string>& required_scopes) {
337 if (ctx->oauth2 && ctx->oauth2->enabled()) {
338 auto auth = ctx->oauth2->authenticate(req, res);
339 if (!auth)
return false;
341 if (!required_scopes.empty()) {
342 return ctx->oauth2->require_any_scope(
343 auth->claims, res, required_scopes);
350std::string generate_transaction_uid() {
352 auto now = std::chrono::system_clock::now();
353 auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
354 now.time_since_epoch())
357 static thread_local std::mt19937 gen(std::random_device{}());
358 std::uniform_int_distribution<uint32_t> dist(1, 999999);
360 std::ostringstream ss;
361 ss <<
"2.25." << ms <<
"." << dist(gen);
365void verify_instances_async(
366 std::shared_ptr<rest_server_context> ctx,
367 std::shared_ptr<storage_commitment::transaction_store> store,
368 std::string transaction_uid) {
370 auto txn_opt =
store->find(transaction_uid);
371 if (!txn_opt)
return;
375 for (
const auto& ref : txn.requested_references) {
376 if (!ctx->database) {
377 txn.failed_references.emplace_back(
382 auto instance = ctx->database->find_instance(ref.sop_instance_uid);
383 if (instance.has_value()) {
384 txn.success_references.push_back(ref);
386 txn.failed_references.emplace_back(
391 txn.completed_at = std::chrono::system_clock::now();
393 if (txn.failed_references.empty()) {
395 }
else if (txn.success_references.empty()) {
407 crow::SimpleApp& app,
408 std::shared_ptr<rest_server_context> ctx) {
411 auto store = std::make_shared<storage_commitment::transaction_store>();
415 CROW_ROUTE(app,
"/dicomweb/storage-commitments")
416 .methods(crow::HTTPMethod::POST)(
417 [ctx, store](
const crow::request& req) {
419 res.add_header(
"Content-Type",
"application/dicom+json");
420 add_cors_headers(res, *ctx);
422 if (!check_storage_commitment_auth(
423 ctx, req, res, {
"dicomweb.write"})) {
427 if (!ctx->database) {
430 "DATABASE_UNAVAILABLE",
431 "Storage service is not available");
441 "INVALID_REQUEST", parsed.error_message);
450 txn.
created_at = std::chrono::system_clock::now();
460 res.add_header(
"Content-Location",
461 "/dicomweb/storage-commitments/" +
469 CROW_ROUTE(app,
"/dicomweb/studies/<string>/commitment")
470 .methods(crow::HTTPMethod::POST)(
471 [ctx, store](
const crow::request& req,
472 const std::string& study_uid) {
474 res.add_header(
"Content-Type",
"application/dicom+json");
475 add_cors_headers(res, *ctx);
477 if (!check_storage_commitment_auth(
478 ctx, req, res, {
"dicomweb.write"})) {
482 if (!ctx->database) {
485 "DATABASE_UNAVAILABLE",
486 "Storage service is not available");
491 req.body, study_uid);
496 "INVALID_REQUEST", parsed.error_message);
505 txn.
created_at = std::chrono::system_clock::now();
512 res.add_header(
"Content-Location",
513 "/dicomweb/storage-commitments/" +
521 CROW_ROUTE(app,
"/dicomweb/storage-commitments/<string>")
522 .methods(crow::HTTPMethod::GET)(
523 [ctx, store](
const crow::request& req,
524 const std::string& transaction_uid) {
526 res.add_header(
"Content-Type",
"application/dicom+json");
527 add_cors_headers(res, *ctx);
529 if (!check_storage_commitment_auth(
530 ctx, req, res, {
"dicomweb.read"})) {
534 auto txn = store->find(transaction_uid);
539 "Transaction not found: " + transaction_uid);
550 CROW_ROUTE(app,
"/dicomweb/storage-commitments")
551 .methods(crow::HTTPMethod::GET)(
552 [ctx, store](
const crow::request& req) {
554 res.add_header(
"Content-Type",
"application/dicom+json");
555 add_cors_headers(res, *ctx);
557 if (!check_storage_commitment_auth(
558 ctx, req, res, {
"dicomweb.read"})) {
563 auto limit_param = req.url_params.get(
"limit");
564 if (limit_param !=
nullptr) {
566 int val = std::stoi(limit_param);
568 limit =
static_cast<size_t>(val);
575 auto transactions = store->list(limit);
std::map< std::string, commitment_transaction, std::less<> > transactions_
std::optional< commitment_transaction > find(std::string_view transaction_uid) const
Find a transaction by its UID.
void add(commitment_transaction txn)
Add a new transaction.
bool update(const commitment_transaction &txn)
Update an existing transaction.
size_t size() const
Get the number of stored transactions.
std::vector< commitment_transaction > list(size_t limit=0) const
List all transactions (newest first)
PACS index database for metadata storage and retrieval.
@ store
C-STORE operation.
commitment_failure_reason
Failure reason codes for Storage Commitment.
@ no_such_object_instance
Referenced SOP Instance not found in storage.
@ processing_failure
General processing failure.
auto to_string(mpps_status status) -> std::string_view
Convert mpps_status to DICOM string representation.
@ storage_commitment
Storage Commitment Push Model Service Class.
void register_storage_commitment_endpoints_impl(crow::SimpleApp &app, std::shared_ptr< rest_server_context > ctx)
transaction_state
Transaction state for DICOMweb Storage Commitment.
@ partial
Some instances verified, others failed.
@ success
All referenced instances verified present and intact.
@ failure
Commitment verification failed.
@ pending
Commitment request received, verification in progress.
parse_result parse_commitment_request(std::string_view json_body, std::string_view study_uid="")
std::string transactions_to_json(const std::vector< commitment_transaction > &transactions)
Serialize a list of transactions to DICOM JSON array.
std::optional< transaction_state > parse_state(std::string_view str) noexcept
Parse transaction state from string.
std::string_view to_string(transaction_state state) noexcept
Convert transaction state to string.
std::string transaction_to_json(const commitment_transaction &txn)
Serialize a commitment transaction to DICOM JSON.
std::string make_error_json(std::string_view code, std::string_view message)
Create JSON error response body with details.
std::string json_escape(std::string_view s)
Escape a string for JSON.
OAuth 2.0 middleware for DICOMweb endpoint authorization.
Configuration for REST API server.
Common types and utilities for REST API.
DICOMweb Storage Commitment REST endpoints (Supplement 234)
Reference to a SOP Instance in a commitment request.
std::string sop_class_uid
Referenced SOP Class UID (0008,1150)
std::string sop_instance_uid
Referenced SOP Instance UID (0008,1155)
A storage commitment transaction record.
transaction_state state
Current state of the transaction.
std::vector< std::pair< services::sop_reference, services::commitment_failure_reason > > failed_references
Failed references with reasons (populated when verified)
std::string transaction_uid
Unique transaction identifier (DICOM UID format)
std::vector< services::sop_reference > success_references
Successfully committed references (populated when verified)
std::optional< std::chrono::system_clock::time_point > completed_at
Timestamp when verification completed.
std::string study_instance_uid
Study Instance UID (optional, for study-level commitment)
std::vector< services::sop_reference > requested_references
SOP Instance references requested for commitment.
std::chrono::system_clock::time_point created_at
Timestamp when the commitment was requested.
Parse a commitment request from JSON body.
std::string error_message
std::vector< services::sop_reference > references
System API endpoints for REST server.