Mercurial > hg > orthanc
diff OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp @ 5807:8279eaab0d1d attach-custom-data
merged default -> attach-custom-data
author | Alain Mazy <am@orthanc.team> |
---|---|
date | Tue, 24 Sep 2024 11:39:52 +0200 |
parents | d7274e43ea7c 0c218d90096e |
children | 023a99146dd0 |
line wrap: on
line diff
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp Thu Sep 15 18:13:17 2022 +0200 +++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp Tue Sep 24 11:39:52 2024 +0200 @@ -2,8 +2,9 @@ * Orthanc - A Lightweight, RESTful DICOM Store * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2022 Osimis S.A., Belgium - * Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -58,6 +59,8 @@ static const char* const IGNORE_LENGTH = "ignore-length"; static const char* const RECONSTRUCT_FILES = "ReconstructFiles"; +static const char* const LIMIT_TO_THIS_LEVEL_MAIN_DICOM_TAGS = "LimitToThisLevelMainDicomTags"; +static const char* const ARG_WHOLE = "whole"; namespace Orthanc @@ -67,19 +70,19 @@ switch (type) { case Orthanc::ResourceType_Instance: - return "https://demo.orthanc-server.com/instances/d94d9a03-3003b047-a4affc69-322313b2-680530a2"; + return "https://orthanc.uclouvain.be/demo/instances/6582b1c0-292ad5ab-ba0f088f-f7a1766f-9a29a54f"; break; case Orthanc::ResourceType_Series: - return "https://demo.orthanc-server.com/series/37836232-d13a2350-fa1dedc5-962b31aa-010f8e52"; + return "https://orthanc.uclouvain.be/demo/series/37836232-d13a2350-fa1dedc5-962b31aa-010f8e52"; break; case Orthanc::ResourceType_Study: - return "https://demo.orthanc-server.com/studies/27f7126f-4f66fb14-03f4081b-f9341db2-53925988"; + return "https://orthanc.uclouvain.be/demo/studies/27f7126f-4f66fb14-03f4081b-f9341db2-53925988"; break; case Orthanc::ResourceType_Patient: - return "https://demo.orthanc-server.com/patients/46e6332c-677825b6-202fcf7c-f787bc5f-7b07c382"; + return "https://orthanc.uclouvain.be/demo/patients/46e6332c-677825b6-202fcf7c-f787bc5f-7b07c382"; break; default: @@ -216,7 +219,7 @@ "If present, retrieve detailed information about the individual " + resources, false) .AddAnswerType(MimeType_Json, "JSON array containing either the Orthanc identifiers, or detailed information " "about the reported " + resources + " (if `expand` argument is provided)") - .SetHttpGetSample("https://demo.orthanc-server.com/" + resources + "?since=0&limit=2", true); + .SetHttpGetSample("https://orthanc.uclouvain.be/demo/" + resources + "?since=0&limit=2", true); return; } @@ -254,7 +257,7 @@ index.GetAllUuids(result, resourceType); } - AnswerListOfResources(call.GetOutput(), context, result, resourceType, call.HasArgument("expand"), + AnswerListOfResources(call.GetOutput(), context, result, resourceType, call.HasArgument("expand") && call.GetBooleanArgument("expand", true), OrthancRestApi::GetDicomFormat(call, DicomToJsonFormat_Human), requestedTags, true /* allowStorageAccess */); @@ -342,8 +345,10 @@ { call.GetDocumentation() .SetTag("Patients") - .SetSummary("Protect one patient against recycling") - .SetDescription("Check out configuration options `MaximumStorageSize` and `MaximumPatientCount`") + .SetSummary("Protect/Unprotect a patient against recycling") + .SetDescription("Protects a patient by sending `1` or `true` in the payload request. " + "Unprotects a patient by sending `0` or `false` in the payload requests. " + "More info: https://orthanc.uclouvain.be/book/faq/features.html#recycling-protection") .SetUriArgument("id", "Orthanc identifier of the patient of interest"); return; } @@ -361,6 +366,8 @@ static void GetInstanceFile(RestApiGetCall& call) { + static const char* const TRANSCODE = "transcode"; + if (call.IsDocumentation()) { call.GetDocumentation() @@ -369,6 +376,9 @@ .SetDescription("Download one DICOM instance") .SetUriArgument("id", "Orthanc identifier of the DICOM instance of interest") .SetHttpHeader("Accept", "This HTTP header can be set to retrieve the DICOM instance in DICOMweb format") + .SetHttpGetArgument(TRANSCODE, RestApiCallDocumentation::Type_String, + "If present, the DICOM file will be transcoded to the provided " + "transfer syntax: https://orthanc.uclouvain.be/book/faq/transcoding.html", false) .AddAnswerType(MimeType_Dicom, "The DICOM instance") .AddAnswerType(MimeType_DicomWebJson, "The DICOM instance, in DICOMweb JSON format") .AddAnswerType(MimeType_DicomWebXml, "The DICOM instance, in DICOMweb XML format"); @@ -417,7 +427,23 @@ } } - context.AnswerAttachment(call.GetOutput(), publicId, FileContentType_Dicom); + if (call.HasArgument(TRANSCODE)) + { + std::string source; + std::string attachmentId; + std::string transcoded; + context.ReadDicom(source, attachmentId, publicId); + + if (context.TranscodeWithCache(transcoded, source, publicId, attachmentId, GetTransferSyntax(call.GetArgument(TRANSCODE, "")))) + { + call.GetOutput().AnswerBuffer(transcoded, MimeType_Dicom); + } + } + else + { + // return the attachment without any transcoding + context.AnswerAttachment(call.GetOutput(), publicId, FileContentType_Dicom); + } } @@ -428,7 +454,10 @@ call.GetDocumentation() .SetTag("Instances") .SetSummary("Write DICOM onto filesystem") - .SetDescription("Write the DICOM file onto the filesystem where Orthanc is running") + .SetDescription("Write the DICOM file onto the filesystem where Orthanc is running. This is insecure for " + "Orthanc servers that are remotely accessible since one could overwrite any system file. " + "Since Orthanc 1.12.0, this route is disabled by default, but can be enabled using " + "the `RestApiWriteToFileSystemEnabled` configuration option.") .SetUriArgument("id", "Orthanc identifier of the DICOM instance of interest") .AddRequestType(MimeType_PlainText, "Target path on the filesystem"); return; @@ -436,6 +465,14 @@ ServerContext& context = OrthancRestApi::GetContext(call); + if (!context.IsRestApiWriteToFileSystemEnabled()) + { + LOG(ERROR) << "The URI /instances/../export is disallowed for security, " + << "check your configuration option `RestApiWriteToFileSystemEnabled`"; + call.GetOutput().SignalError(HttpStatus_403_Forbidden); + return; + } + std::string publicId = call.GetUriComponent("id", ""); std::string dicom; @@ -450,7 +487,8 @@ template <DicomToJsonFormat format> - static void GetInstanceTagsInternal(RestApiGetCall& call) + static void GetInstanceTagsInternal(RestApiGetCall& call, + bool whole) { ServerContext& context = OrthancRestApi::GetContext(call); @@ -458,56 +496,90 @@ std::set<DicomTag> ignoreTagLength; ParseSetOfTags(ignoreTagLength, call, IGNORE_LENGTH); - - if (format != DicomToJsonFormat_Full || - !ignoreTagLength.empty()) + + if (whole) { - Json::Value full; - context.ReadDicomAsJson(full, publicId, ignoreTagLength); - AnswerDicomAsJson(call, full, format); + // This is new in Orthanc 1.12.4. Reference: + // https://discourse.orthanc-server.org/t/private-tags-with-group-7fe0-are-not-provided-via-rest-api/4744 + const DicomToJsonFlags flags = static_cast<DicomToJsonFlags>(DicomToJsonFlags_Default & ~DicomToJsonFlags_StopAfterPixelData); + + Json::Value answer; + + { + ServerContext::DicomCacheLocker locker(OrthancRestApi::GetContext(call), publicId); + locker.GetDicom().DatasetToJson(answer, format, flags, + ORTHANC_MAXIMUM_TAG_LENGTH, ignoreTagLength); + } + + call.GetOutput().AnswerJson(answer); } else { - // This path allows one to avoid the JSON decoding if no - // simplification is asked, and if no "ignore-length" argument - // is present - Json::Value full; - context.ReadDicomAsJson(full, publicId); - call.GetOutput().AnswerJson(full); + if (format != DicomToJsonFormat_Full || + !ignoreTagLength.empty()) + { + Json::Value full; + context.ReadDicomAsJson(full, publicId, ignoreTagLength); + AnswerDicomAsJson(call, full, format); + } + else + { + // This path allows one to avoid the JSON decoding if no + // simplification is asked, and if no "ignore-length" argument + // is present + Json::Value full; + context.ReadDicomAsJson(full, publicId); + call.GetOutput().AnswerJson(full); + } } } + static void DocumentGetInstanceTags(RestApiGetCall& call) + { + call.GetDocumentation() + .SetTag("Instances") + .SetUriArgument("id", "Orthanc identifier of the DICOM instance of interest") + .SetHttpGetArgument( + IGNORE_LENGTH, RestApiCallDocumentation::Type_JsonListOfStrings, + "Also include the DICOM tags that are provided in this list, even if their associated value is long", false) + .SetHttpGetArgument( + ARG_WHOLE, RestApiCallDocumentation::Type_Boolean, "Whether to read the whole DICOM file from the " + "storage area (new in Orthanc 1.12.4). If set to \"false\" (default value), the DICOM file is read " + "until the pixel data tag (7fe0,0010) to optimize access to storage. Setting the option " + "to \"true\" provides access to the DICOM tags stored after the pixel data tag.", false) + .AddAnswerType(MimeType_Json, "JSON object containing the DICOM tags and their associated value"); + } + + static void GetInstanceTags(RestApiGetCall& call) { if (call.IsDocumentation()) { OrthancRestApi::DocumentDicomFormat(call, DicomToJsonFormat_Full); + DocumentGetInstanceTags(call); call.GetDocumentation() - .SetTag("Instances") .SetSummary("Get DICOM tags") .SetDescription("Get the DICOM tags in the specified format. By default, the `full` format is used, which " "combines hexadecimal tags with human-readable description.") - .SetUriArgument("id", "Orthanc identifier of the DICOM instance of interest") - .SetHttpGetArgument(IGNORE_LENGTH, RestApiCallDocumentation::Type_JsonListOfStrings, - "Also include the DICOM tags that are provided in this list, even if their associated value is long", false) - .AddAnswerType(MimeType_Json, "JSON object containing the DICOM tags and their associated value") - .SetTruncatedJsonHttpGetSample("https://demo.orthanc-server.com/instances/7c92ce8e-bbf67ed2-ffa3b8c1-a3b35d94-7ff3ae26/tags", 10); + .SetTruncatedJsonHttpGetSample("https://orthanc.uclouvain.be/demo/instances/7c92ce8e-bbf67ed2-ffa3b8c1-a3b35d94-7ff3ae26/tags", 10); return; } + const bool whole = call.GetBooleanArgument(ARG_WHOLE, false); + switch (OrthancRestApi::GetDicomFormat(call, DicomToJsonFormat_Full)) { case DicomToJsonFormat_Human: - GetInstanceTagsInternal<DicomToJsonFormat_Human>(call); + GetInstanceTagsInternal<DicomToJsonFormat_Human>(call, whole); break; case DicomToJsonFormat_Short: - GetInstanceTagsInternal<DicomToJsonFormat_Short>(call); + GetInstanceTagsInternal<DicomToJsonFormat_Short>(call, whole); break; case DicomToJsonFormat_Full: - GetInstanceTagsInternal<DicomToJsonFormat_Full>(call); + GetInstanceTagsInternal<DicomToJsonFormat_Full>(call, whole); break; default: @@ -520,20 +592,16 @@ { if (call.IsDocumentation()) { + DocumentGetInstanceTags(call); call.GetDocumentation() - .SetTag("Instances") .SetSummary("Get human-readable tags") .SetDescription("Get the DICOM tags in human-readable format (same as the `/instances/{id}/tags?simplify` route)") - .SetUriArgument("id", "Orthanc identifier of the DICOM instance of interest") - .SetHttpGetArgument(IGNORE_LENGTH, RestApiCallDocumentation::Type_JsonListOfStrings, - "Also include the DICOM tags that are provided in this list, even if their associated value is long", false) - .AddAnswerType(MimeType_Json, "JSON object containing the DICOM tags and their associated value") - .SetTruncatedJsonHttpGetSample("https://demo.orthanc-server.com/instances/7c92ce8e-bbf67ed2-ffa3b8c1-a3b35d94-7ff3ae26/simplified-tags", 10); + .SetTruncatedJsonHttpGetSample("https://orthanc.uclouvain.be/demo/instances/7c92ce8e-bbf67ed2-ffa3b8c1-a3b35d94-7ff3ae26/simplified-tags", 10); return; } else { - GetInstanceTagsInternal<DicomToJsonFormat_Human>(call); + GetInstanceTagsInternal<DicomToJsonFormat_Human>(call, call.GetBooleanArgument(ARG_WHOLE, false)); } } @@ -548,7 +616,7 @@ .SetDescription("List the frames that are available in the DICOM instance of interest") .SetUriArgument("id", "Orthanc identifier of the DICOM instance of interest") .AddAnswerType(MimeType_Json, "The list of the indices of the available frames") - .SetHttpGetSample("https://demo.orthanc-server.com/instances/7c92ce8e-bbf67ed2-ffa3b8c1-a3b35d94-7ff3ae26/frames", true); + .SetHttpGetSample("https://orthanc.uclouvain.be/demo/instances/7c92ce8e-bbf67ed2-ffa3b8c1-a3b35d94-7ff3ae26/frames", true); return; } @@ -628,7 +696,8 @@ } virtual void Handle(const std::string& type, - const std::string& subtype) ORTHANC_OVERRIDE + const std::string& subtype, + const HttpContentNegociation::Dictionary& parameters) ORTHANC_OVERRIDE { assert(type == "image"); assert(subtype == "png"); @@ -647,7 +716,8 @@ } virtual void Handle(const std::string& type, - const std::string& subtype) ORTHANC_OVERRIDE + const std::string& subtype, + const HttpContentNegociation::Dictionary& parameters) ORTHANC_OVERRIDE { assert(type == "image"); assert(subtype == "x-portable-arbitrarymap"); @@ -687,7 +757,8 @@ } virtual void Handle(const std::string& type, - const std::string& subtype) ORTHANC_OVERRIDE + const std::string& subtype, + const HttpContentNegociation::Dictionary& parameters) ORTHANC_OVERRIDE { assert(type == "image"); assert(subtype == "jpeg"); @@ -1446,7 +1517,7 @@ .SetTag("Instances") .SetSummary("Decode frame for Matlab") .SetDescription(description + ", and export this frame as a Octave/Matlab matrix to be imported with `eval()`: " - "https://book.orthanc-server.com/faq/matlab.html") + "https://orthanc.uclouvain.be/book/faq/matlab.html") .SetUriArgument("id", "Orthanc identifier of the DICOM instance of interest") .AddAnswerType(MimeType_PlainText, "Octave/Matlab matrix"); return; @@ -1666,6 +1737,8 @@ .SetUriArgument("id", "Orthanc identifier of the " + r + " of interest") .SetHttpGetArgument("expand", RestApiCallDocumentation::Type_String, "If present, also retrieve the value of the individual metadata", false) + .SetHttpGetArgument("numeric", RestApiCallDocumentation::Type_String, + "If present, use the numeric identifier of the metadata instead of its symbolic name", false) .AddAnswerType(MimeType_Json, "JSON array containing the names of the available metadata, " "or JSON associative array mapping metadata to their values (if `expand` argument is provided)") .SetHttpGetSample(GetDocumentationSampleResource(t) + "/metadata", true); @@ -1683,15 +1756,26 @@ Json::Value result; - if (call.HasArgument("expand")) + bool isNumeric = call.HasArgument("numeric"); + + if (call.HasArgument("expand") && call.GetBooleanArgument("expand", true)) { result = Json::objectValue; for (Metadata::const_iterator it = metadata.begin(); it != metadata.end(); ++it) { - std::string key = EnumerationToString(it->first); + std::string key; + if (isNumeric) + { + key = boost::lexical_cast<std::string>(it->first); + } + else + { + key = EnumerationToString(it->first); + } + result[key] = it->second; - } + } } else { @@ -1699,7 +1783,14 @@ for (Metadata::const_iterator it = metadata.begin(); it != metadata.end(); ++it) { - result.append(EnumerationToString(it->first)); + if (isNumeric) + { + result.append(it->first); + } + else + { + result.append(EnumerationToString(it->first)); + } } } @@ -1857,7 +1948,8 @@ std::string name = call.GetUriComponent("name", ""); MetadataType metadata = StringToMetadata(name); - if (IsUserMetadata(metadata)) // It is forbidden to modify internal metadata + if (IsUserMetadata(metadata) || // It is forbidden to delete internal metadata... + call.GetRequestOrigin() == RequestOrigin_Plugins) // ...except for plugins { bool found; int64_t revision; @@ -1923,7 +2015,8 @@ std::string value; call.BodyToString(value); - if (IsUserMetadata(metadata)) // It is forbidden to modify internal metadata + if (IsUserMetadata(metadata) || // It is forbidden to modify internal metadata... + call.GetRequestOrigin() == RequestOrigin_Plugins) // ...except for plugins { int64_t oldRevision; std::string oldMD5; @@ -1958,6 +2051,129 @@ + // Handling of labels ------------------------------------------------------- + + static void ListLabels(RestApiGetCall& call) + { + if (call.IsDocumentation()) + { + ResourceType t = StringToResourceType(call.GetFullUri()[0].c_str()); + std::string r = GetResourceTypeText(t, false /* plural */, false /* upper case */); + call.GetDocumentation() + .SetTag(GetResourceTypeText(t, true /* plural */, true /* upper case */)) + .SetSummary("List labels") + .SetDescription("Get the labels that are associated with the given " + r + " (new in Orthanc 1.12.0)") + .SetUriArgument("id", "Orthanc identifier of the " + r + " of interest") + .AddAnswerType(MimeType_Json, "JSON array containing the names of the labels") + .SetHttpGetSample(GetDocumentationSampleResource(t) + "/labels", true); + return; + } + + assert(!call.GetFullUri().empty()); + const std::string publicId = call.GetUriComponent("id", ""); + ResourceType level = StringToResourceType(call.GetFullUri() [0].c_str()); + + std::set<std::string> labels; + OrthancRestApi::GetIndex(call).ListLabels(labels, publicId, level); + + Json::Value result = Json::arrayValue; + + for (std::set<std::string>::const_iterator it = labels.begin(); it != labels.end(); ++it) + { + result.append(*it); + } + + call.GetOutput().AnswerJson(result); + } + + + static void GetLabel(RestApiGetCall& call) + { + if (call.IsDocumentation()) + { + ResourceType t = StringToResourceType(call.GetFullUri()[0].c_str()); + std::string r = GetResourceTypeText(t, false /* plural */, false /* upper case */); + call.GetDocumentation() + .SetTag(GetResourceTypeText(t, true /* plural */, true /* upper case */)) + .SetSummary("Test label") + .SetDescription("Test whether the " + r + " is associated with the given label") + .SetUriArgument("id", "Orthanc identifier of the " + r + " of interest") + .SetUriArgument("label", "The label of interest") + .AddAnswerType(MimeType_PlainText, "Empty string is returned in the case of presence, error 404 in the case of absence"); + return; + } + + CheckValidResourceType(call); + + assert(!call.GetFullUri().empty()); + const std::string publicId = call.GetUriComponent("id", ""); + const ResourceType level = StringToResourceType(call.GetFullUri() [0].c_str()); + + std::string label = call.GetUriComponent("label", ""); + + std::set<std::string> labels; + OrthancRestApi::GetIndex(call).ListLabels(labels, publicId, level); + + if (labels.find(label) != labels.end()) + { + call.GetOutput().AnswerBuffer("", MimeType_PlainText); + } + } + + + static void AddLabel(RestApiPutCall& call) + { + if (call.IsDocumentation()) + { + ResourceType t = StringToResourceType(call.GetFullUri()[0].c_str()); + std::string r = GetResourceTypeText(t, false /* plural */, false /* upper case */); + call.GetDocumentation() + .SetTag(GetResourceTypeText(t, true /* plural */, true /* upper case */)) + .SetSummary("Add label") + .SetDescription("Associate a label with a " + r) + .SetUriArgument("id", "Orthanc identifier of the " + r + " of interest") + .SetUriArgument("label", "The label to be added"); + return; + } + + CheckValidResourceType(call); + + std::string publicId = call.GetUriComponent("id", ""); + const ResourceType level = StringToResourceType(call.GetFullUri() [0].c_str()); + + std::string label = call.GetUriComponent("label", ""); + OrthancRestApi::GetIndex(call).ModifyLabel(publicId, level, label, StatelessDatabaseOperations::LabelOperation_Add); + + call.GetOutput().AnswerBuffer("", MimeType_PlainText); + } + + + static void RemoveLabel(RestApiDeleteCall& call) + { + if (call.IsDocumentation()) + { + ResourceType t = StringToResourceType(call.GetFullUri()[0].c_str()); + std::string r = GetResourceTypeText(t, false /* plural */, false /* upper case */); + call.GetDocumentation() + .SetTag(GetResourceTypeText(t, true /* plural */, true /* upper case */)) + .SetSummary("Remove label") + .SetDescription("Remove a label associated with a " + r) + .SetUriArgument("id", "Orthanc identifier of the " + r + " of interest") + .SetUriArgument("label", "The label to be removed"); + return; + } + + CheckValidResourceType(call); + + std::string publicId = call.GetUriComponent("id", ""); + const ResourceType level = StringToResourceType(call.GetFullUri() [0].c_str()); + + std::string label = call.GetUriComponent("label", ""); + OrthancRestApi::GetIndex(call).ModifyLabel(publicId, level, label, StatelessDatabaseOperations::LabelOperation_Remove); + + call.GetOutput().AnswerBuffer("", MimeType_PlainText); + } + // Handling of attached files ----------------------------------------------- @@ -2070,7 +2286,7 @@ .SetSummary("List operations on attachments") .SetDescription("Get the list of the operations that are available for attachments associated with the given " + r) .AddAnswerType(MimeType_Json, "List of the available operations") - .SetHttpGetSample("https://demo.orthanc-server.com/instances/d94d9a03-3003b047-a4affc69-322313b2-680530a2/attachments/dicom", true); + .SetHttpGetSample("https://orthanc.uclouvain.be/demo/instances/6582b1c0-292ad5ab-ba0f088f-f7a1766f-9a29a54f/attachments/dicom", true); return; } @@ -2106,8 +2322,6 @@ operations.append("verify-md5"); } - operations.append("uuid"); - call.GetOutput().AnswerJson(operations); } } @@ -2153,8 +2367,9 @@ { // Return the raw data (possibly compressed), as stored on the filesystem std::string content; + std::string attachmentId; int64_t revision; - context.ReadAttachment(content, revision, publicId, type, false, true /* skipCache when you absolutely need the compressed data */); + context.ReadAttachment(content, revision, attachmentId, publicId, type, false, true /* skipCache when you absolutely need the compressed data */); int64_t userRevision; std::string userMD5; @@ -2207,7 +2422,7 @@ .SetSummary("Get info about the attachment") .SetDescription("Get all the information about the attachment associated with the given " + r) .AddAnswerType(MimeType_Json, "JSON object containing the information about the attachment") - .SetHttpGetSample("https://demo.orthanc-server.com/instances/7c92ce8e-bbf67ed2-ffa3b8c1-a3b35d94-7ff3ae26/attachments/dicom/info", true); + .SetHttpGetSample("https://orthanc.uclouvain.be/demo/instances/7c92ce8e-bbf67ed2-ffa3b8c1-a3b35d94-7ff3ae26/attachments/dicom/info", true); return; } @@ -2336,7 +2551,9 @@ // First check whether the compressed data is correctly stored in the disk std::string data; - context.ReadAttachment(data, revision, publicId, StringToContentType(name), false, true /* skipCache when you absolutely need the compressed data */); + std::string attachmentId; + + context.ReadAttachment(data, revision, attachmentId, publicId, StringToContentType(name), false, true /* skipCache when you absolutely need the compressed data */); std::string actualMD5; Toolbox::ComputeMD5(actualMD5, data); @@ -2351,7 +2568,7 @@ } else { - context.ReadAttachment(data, revision, publicId, StringToContentType(name), true, true /* skipCache when you absolutely need the compressed data */); + context.ReadAttachment(data, revision, attachmentId, publicId, StringToContentType(name), true, true /* skipCache when you absolutely need the compressed data */); Toolbox::ComputeMD5(actualMD5, data); ok = (actualMD5 == info.GetUncompressedMD5()); } @@ -2396,7 +2613,8 @@ ResourceType resourceType = StringToResourceType(call.GetFullUri()[0].c_str()); FileContentType contentType = StringToContentType(name); - if (IsUserContentType(contentType)) // It is forbidden to modify internal attachments + if (IsUserContentType(contentType) || // It is forbidden to modify internal attachments... + call.GetRequestOrigin() == RequestOrigin_Plugins) // ...except for plugins { int64_t oldRevision; std::string oldMD5; @@ -2455,7 +2673,8 @@ FileContentType contentType = StringToContentType(name); bool allowed; - if (IsUserContentType(contentType)) + if (IsUserContentType(contentType) || // It is forbidden to delete internal attachments... + call.GetRequestOrigin() == RequestOrigin_Plugins) // ...except for plugins { allowed = true; } @@ -2580,7 +2799,7 @@ .SetSummary("Get raw tag") .SetDescription("Get the raw content of one DICOM tag in the hierarchy of DICOM dataset") .SetUriArgument("id", "Orthanc identifier of the DICOM instance of interest") - .SetUriArgument("...", "Path to the DICOM tag. This is the interleaving of one DICOM tag, possibly followed " + .SetUriArgument("path", "Path to the DICOM tag. This is the interleaving of one DICOM tag, possibly followed " "by an index for sequences. Sequences are accessible as, for instance, `/0008-1140/1/0008-1150`") .AddAnswerType(MimeType_Binary, "The raw value of the tag of intereset " "(binary data, whose memory layout depends on the underlying transfer syntax), " @@ -2938,6 +3157,8 @@ static const char* const KEY_QUERY = "Query"; static const char* const KEY_REQUESTED_TAGS = "RequestedTags"; static const char* const KEY_SINCE = "Since"; + static const char* const KEY_LABELS = "Labels"; // New in Orthanc 1.12.0 + static const char* const KEY_LABELS_CONSTRAINT = "LabelsConstraint"; // New in Orthanc 1.12.0 if (call.IsDocumentation()) { @@ -2948,7 +3169,7 @@ .SetSummary("Look for local resources") .SetDescription("This URI can be used to perform a search on the content of the local Orthanc server, " "in a way that is similar to querying remote DICOM modalities using C-FIND SCU: " - "https://book.orthanc-server.com/users/rest.html#performing-finds-within-orthanc") + "https://orthanc.uclouvain.be/book/users/rest.html#performing-finds-within-orthanc") .SetRequestField(KEY_CASE_SENSITIVE, RestApiCallDocumentation::Type_Boolean, "Enable case-sensitive search for PN value representations (defaults to configuration option `CaseSensitivePN`)", false) .SetRequestField(KEY_EXPAND, RestApiCallDocumentation::Type_Boolean, @@ -2967,6 +3188,10 @@ "all Main Dicom Tags to keep backward compatibility with Orthanc prior to 1.11.0.", false) .SetRequestField(KEY_QUERY, RestApiCallDocumentation::Type_JsonObject, "Associative array containing the filter on the values of the DICOM tags", true) + .SetRequestField(KEY_LABELS, RestApiCallDocumentation::Type_JsonListOfStrings, + "List of strings specifying which labels to look for in the resources (new in Orthanc 1.12.0)", true) + .SetRequestField(KEY_LABELS_CONSTRAINT, RestApiCallDocumentation::Type_String, + "Constraint on the labels, can be `All`, `Any`, or `None` (defaults to `All`, new in Orthanc 1.12.0)", true) .AddAnswerType(MimeType_Json, "JSON array containing either the Orthanc identifiers, or detailed information " "about the reported resources (if `Expand` argument is `true`)"); return; @@ -2997,25 +3222,37 @@ request[KEY_CASE_SENSITIVE].type() != Json::booleanValue) { throw OrthancException(ErrorCode_BadRequest, - "Field \"" + std::string(KEY_CASE_SENSITIVE) + "\" should be a Boolean"); + "Field \"" + std::string(KEY_CASE_SENSITIVE) + "\" must be a Boolean"); } else if (request.isMember(KEY_LIMIT) && request[KEY_LIMIT].type() != Json::intValue) { throw OrthancException(ErrorCode_BadRequest, - "Field \"" + std::string(KEY_LIMIT) + "\" should be an integer"); + "Field \"" + std::string(KEY_LIMIT) + "\" must be an integer"); } else if (request.isMember(KEY_SINCE) && request[KEY_SINCE].type() != Json::intValue) { throw OrthancException(ErrorCode_BadRequest, - "Field \"" + std::string(KEY_SINCE) + "\" should be an integer"); + "Field \"" + std::string(KEY_SINCE) + "\" must be an integer"); } else if (request.isMember(KEY_REQUESTED_TAGS) && request[KEY_REQUESTED_TAGS].type() != Json::arrayValue) { throw OrthancException(ErrorCode_BadRequest, - "Field \"" + std::string(KEY_REQUESTED_TAGS) + "\" should be an array"); + "Field \"" + std::string(KEY_REQUESTED_TAGS) + "\" must be an array"); + } + else if (request.isMember(KEY_LABELS) && + request[KEY_LABELS].type() != Json::arrayValue) + { + throw OrthancException(ErrorCode_BadRequest, + "Field \"" + std::string(KEY_LABELS) + "\" must be an array of strings"); + } + else if (request.isMember(KEY_LABELS_CONSTRAINT) && + request[KEY_LABELS_CONSTRAINT].type() != Json::stringValue) + { + throw OrthancException(ErrorCode_BadRequest, + "Field \"" + std::string(KEY_LABELS_CONSTRAINT) + "\" must be an array of strings"); } else { @@ -3038,7 +3275,7 @@ if (tmp < 0) { throw OrthancException(ErrorCode_ParameterOutOfRange, - "Field \"" + std::string(KEY_LIMIT) + "\" should be a positive integer"); + "Field \"" + std::string(KEY_LIMIT) + "\" must be a positive integer"); } limit = static_cast<size_t>(tmp); @@ -3051,7 +3288,7 @@ if (tmp < 0) { throw OrthancException(ErrorCode_ParameterOutOfRange, - "Field \"" + std::string(KEY_SINCE) + "\" should be a positive integer"); + "Field \"" + std::string(KEY_SINCE) + "\" must be a positive integer"); } since = static_cast<size_t>(tmp); @@ -3074,7 +3311,7 @@ if (request[KEY_QUERY][members[i]].type() != Json::stringValue) { throw OrthancException(ErrorCode_BadRequest, - "Tag \"" + members[i] + "\" should be associated with a string"); + "Tag \"" + members[i] + "\" must be associated with a string"); } const std::string value = request[KEY_QUERY][members[i]].asString(); @@ -3089,8 +3326,48 @@ } } + std::set<std::string> labels; + + if (request.isMember(KEY_LABELS)) // New in Orthanc 1.12.0 + { + for (Json::Value::ArrayIndex i = 0; i < request[KEY_LABELS].size(); i++) + { + if (request[KEY_LABELS][i].type() != Json::stringValue) + { + throw OrthancException(ErrorCode_BadRequest, "Field \"" + std::string(KEY_LABELS) + "\" must contain strings"); + } + else + { + labels.insert(request[KEY_LABELS][i].asString()); + } + } + } + + LabelsConstraint labelsConstraint = LabelsConstraint_All; + + if (request.isMember(KEY_LABELS_CONSTRAINT)) + { + const std::string& s = request[KEY_LABELS_CONSTRAINT].asString(); + if (s == "All") + { + labelsConstraint = LabelsConstraint_All; + } + else if (s == "Any") + { + labelsConstraint = LabelsConstraint_Any; + } + else if (s == "None") + { + labelsConstraint = LabelsConstraint_None; + } + else + { + throw OrthancException(ErrorCode_BadRequest, "Field \"" + std::string(KEY_LABELS_CONSTRAINT) + "\" must be \"All\", \"Any\", or \"None\""); + } + } + FindVisitor visitor(OrthancRestApi::GetDicomFormat(request, DicomToJsonFormat_Human), context.GetFindStorageAccessMode()); - context.Apply(visitor, query, level, since, limit); + context.Apply(visitor, query, level, labels, labelsConstraint, since, limit); visitor.Answer(call.GetOutput(), context, level, expand, requestedTags); } } @@ -3113,12 +3390,15 @@ .SetDescription("Get detailed information about the child " + children + " of the DICOM " + resource + " whose Orthanc identifier is provided in the URL") .SetUriArgument("id", "Orthanc identifier of the " + resource + " of interest") + .SetHttpGetArgument("expand", RestApiCallDocumentation::Type_String, + "If false or missing, only retrieve the list of child " + children, false) .AddAnswerType(MimeType_Json, "JSON array containing information about the child DICOM " + children) .SetTruncatedJsonHttpGetSample(GetDocumentationSampleResource(start) + "/" + children, 5); return; } ServerIndex& index = OrthancRestApi::GetIndex(call); + ServerContext& context = OrthancRestApi::GetContext(call); std::set<DicomTag> requestedTags; OrthancRestApi::GetRequestedTags(requestedTags, call); @@ -3144,21 +3424,10 @@ a.splice(a.begin(), b); } - Json::Value result = Json::arrayValue; - - const DicomToJsonFormat format = OrthancRestApi::GetDicomFormat(call, DicomToJsonFormat_Human); - - for (std::list<std::string>::const_iterator - it = a.begin(); it != a.end(); ++it) - { - Json::Value resource; - if (OrthancRestApi::GetContext(call).ExpandResource(resource, *it, end, format, requestedTags, true /* allowStorageAccess */)) - { - result.append(resource); - } - } - - call.GetOutput().AnswerJson(result); + AnswerListOfResources(call.GetOutput(), context, a, type, !call.HasArgument("expand") || call.GetBooleanArgument("expand", false), // this "expand" is the only one to have a false default value to keep backward compatibility + OrthancRestApi::GetDicomFormat(call, DicomToJsonFormat_Human), + requestedTags, + true /* allowStorageAccess */); } @@ -3325,7 +3594,7 @@ "Same information as the `Slices` field, but in a compact form") .SetAnswerField("Type", RestApiCallDocumentation::Type_String, "Can be `Volume` (for 3D volumes) or `Sequence` (notably for cine images)") - .SetTruncatedJsonHttpGetSample("https://demo.orthanc-server.com/series/1e2c125c-411b8e86-3f4fe68e-a7584dd3-c6da78f0/ordered-slices", 10); + .SetTruncatedJsonHttpGetSample("https://orthanc.uclouvain.be/demo/series/1e2c125c-411b8e86-3f4fe68e-a7584dd3-c6da78f0/ordered-slices", 10); return; } @@ -3352,7 +3621,7 @@ "combines hexadecimal tags with human-readable description.") .SetUriArgument("id", "Orthanc identifier of the DICOM instance of interest") .AddAnswerType(MimeType_Json, "JSON object containing the DICOM tags and their associated value") - .SetHttpGetSample("https://demo.orthanc-server.com/instances/7c92ce8e-bbf67ed2-ffa3b8c1-a3b35d94-7ff3ae26/header", true); + .SetHttpGetSample("https://orthanc.uclouvain.be/demo/instances/7c92ce8e-bbf67ed2-ffa3b8c1-a3b35d94-7ff3ae26/header", true); return; } @@ -3385,7 +3654,7 @@ .SetDescription("Remove all the attachments of the type \"DICOM-as-JSON\" that are associated will all the " "DICOM instances stored in Orthanc. These summaries will be automatically re-created on the next access. " "This is notably useful after changes to the `Dictionary` configuration option. " - "https://book.orthanc-server.com/faq/orthanc-storage.html#storage-area"); + "https://orthanc.uclouvain.be/book/faq/orthanc-storage.html#storage-area"); return; } @@ -3413,12 +3682,21 @@ call.GetOutput().AnswerBuffer("", MimeType_PlainText); } - void DocumentReconstructFilesField(RestApiPostCall& call) + void DocumentReconstructFilesField(RestApiPostCall& call, bool documentLimitField) { call.GetDocumentation() .SetRequestField(RECONSTRUCT_FILES, RestApiCallDocumentation::Type_Boolean, "Also reconstruct the files of the resources (e.g: apply IngestTranscoding, StorageCompression). " "'false' by default. (New in Orthanc 1.11.0)", false); + if (documentLimitField) + { + call.GetDocumentation() + .SetRequestField(LIMIT_TO_THIS_LEVEL_MAIN_DICOM_TAGS, RestApiCallDocumentation::Type_Boolean, + "Only reconstruct this level MainDicomTags by re-reading them from a random child instance of the resource. " + "This option is much faster than a full reconstruct and is useful e.g. if you have modified the " + "'ExtraMainDicomTags' at the Study level to optimize the speed of some C-Find. " + "'false' by default. (New in Orthanc 1.12.4)", false); + } } bool GetReconstructFilesField(const RestApiPostCall& call) @@ -3440,6 +3718,26 @@ return reconstructFiles; } + bool GetLimitToThisLevelMainDicomTags(const RestApiPostCall& call) + { + bool limitToThisLevel = false; + Json::Value request; + + if (call.GetBodySize() > 0 && call.ParseJsonRequest(request) && request.isMember(LIMIT_TO_THIS_LEVEL_MAIN_DICOM_TAGS)) + { + if (!request[LIMIT_TO_THIS_LEVEL_MAIN_DICOM_TAGS].isBool()) + { + throw OrthancException(ErrorCode_BadFileFormat, + "The field " + std::string(LIMIT_TO_THIS_LEVEL_MAIN_DICOM_TAGS) + " must contain a Boolean"); + } + + limitToThisLevel = request[LIMIT_TO_THIS_LEVEL_MAIN_DICOM_TAGS].asBool(); + } + + return limitToThisLevel; + } + + template <enum ResourceType type> static void ReconstructResource(RestApiPostCall& call) { @@ -3455,13 +3753,13 @@ "Beware that this is a time-consuming operation, as all the children DICOM instances will be " "parsed again, and the Orthanc index will be updated accordingly.") .SetUriArgument("id", "Orthanc identifier of the " + resource + " of interest"); - DocumentReconstructFilesField(call); + DocumentReconstructFilesField(call, true); return; } ServerContext& context = OrthancRestApi::GetContext(call); - ServerToolbox::ReconstructResource(context, call.GetUriComponent("id", ""), GetReconstructFilesField(call)); + ServerToolbox::ReconstructResource(context, call.GetUriComponent("id", ""), GetReconstructFilesField(call), GetLimitToThisLevelMainDicomTags(call), type); call.GetOutput().AnswerBuffer("", MimeType_PlainText); } @@ -3479,7 +3777,7 @@ "as all the DICOM instances will be parsed again, and as all the Orthanc index will be regenerated. " "If you have a large database to process, it is advised to use the Housekeeper plugin to perform " "this action resource by resource"); - DocumentReconstructFilesField(call); + DocumentReconstructFilesField(call, false); return; } @@ -3493,7 +3791,7 @@ for (std::list<std::string>::const_iterator study = studies.begin(); study != studies.end(); ++study) { - ServerToolbox::ReconstructResource(context, *study, reconstructFiles); + ServerToolbox::ReconstructResource(context, *study, reconstructFiles, false, ResourceType_Study /* dummy */); } call.GetOutput().AnswerBuffer("", MimeType_PlainText); @@ -3843,6 +4141,12 @@ Register("/" + resourceTypes[i] + "/{id}/metadata/{name}", GetMetadata); Register("/" + resourceTypes[i] + "/{id}/metadata/{name}", SetMetadata); + // New in Orthanc 1.12.0 + Register("/" + resourceTypes[i] + "/{id}/labels", ListLabels); + Register("/" + resourceTypes[i] + "/{id}/labels/{label}", GetLabel); + Register("/" + resourceTypes[i] + "/{id}/labels/{label}", RemoveLabel); + Register("/" + resourceTypes[i] + "/{id}/labels/{label}", AddLabel); + Register("/" + resourceTypes[i] + "/{id}/attachments", ListAttachments); Register("/" + resourceTypes[i] + "/{id}/attachments/{name}", DeleteAttachment); Register("/" + resourceTypes[i] + "/{id}/attachments/{name}", GetAttachmentOperations);