Mercurial > hg > orthanc
changeset 5917:fa8c10f10312 get-scu
merged default -> get-scu
author | Alain Mazy <am@orthanc.team> |
---|---|
date | Mon, 09 Dec 2024 19:41:24 +0100 |
parents | 305d318f488d (current diff) cc5a6f3b9bbe (diff) |
children | |
files | NEWS OrthancServer/Sources/ServerContext.cpp OrthancServer/Sources/ServerContext.h TODO |
diffstat | 17 files changed, 690 insertions(+), 161 deletions(-) [+] |
line wrap: on
line diff
--- a/NEWS Mon Dec 09 19:41:02 2024 +0100 +++ b/NEWS Mon Dec 09 19:41:24 2024 +0100 @@ -17,6 +17,8 @@ -------- * API version upgraded to 26 +* Support HTTP "Range" request header on "{...}/attachments/{...}/data" and + "{...}/attachments/{...}/compressed-data" * Improved parsing of multiple numerical values in DICOM tags. https://discourse.orthanc-server.org/t/qido-includefield-with-sequences/4746/6 * In DICOMWeb json, the "DS - Decimal String" values were represented by float numbers @@ -49,7 +51,10 @@ * Fix C-Find queries not returning private tags in the modality worklist plugin. * Fix extremely rare error when 2 threads are trying to create the same folder in the File Storage at the same time. -* Fix crashes if handling very large images +* Fix crashes if handling very large images. +* Fix deadlock when parsing specific invalid DICOM files. +* Loading plugins: + - Orthanc will now fail to start when provided with a plugin path that can not be found. * Metrics: - fix a few metrics that were not published - added 2 metrics: orthanc_storage_cache_miss_count & orthanc_storage_cache_hit_count @@ -85,7 +90,7 @@ * Housekeeper plugin: - Added an option "LimitMainDicomTagsReconstructLevel" (allowed values: "Patient", "Study", "Series", "Instance"). This can greatly speed - up the housekeeper process, e.g. if you have only update the Study level ExtraMainDicomTags. + up the housekeeper process, e.g. if you have only updated the Study level ExtraMainDicomTags. - Fixed broken /instances/../tags route after running the Housekeeper after having changed the "IngestTranscoding". * SDK: added OrthancPluginLogMessage() as a new primitive for plugins
--- a/OrthancFramework/Resources/Patches/dcmtk-3.6.8.patch Mon Dec 09 19:41:02 2024 +0100 +++ b/OrthancFramework/Resources/Patches/dcmtk-3.6.8.patch Mon Dec 09 19:41:24 2024 +0100 @@ -1,6 +1,6 @@ diff -urEb dcmtk-DCMTK-3.6.8.orig/CMake/GenerateDCMTKConfigure.cmake dcmtk-DCMTK-3.6.8/CMake/GenerateDCMTKConfigure.cmake ---- dcmtk-DCMTK-3.6.8.orig/CMake/GenerateDCMTKConfigure.cmake 2024-01-09 17:13:10.329673608 +0100 -+++ dcmtk-DCMTK-3.6.8/CMake/GenerateDCMTKConfigure.cmake 2024-01-09 18:21:52.568142681 +0100 +--- dcmtk-DCMTK-3.6.8.orig/CMake/GenerateDCMTKConfigure.cmake 2023-12-19 11:12:57.000000000 +0100 ++++ dcmtk-DCMTK-3.6.8/CMake/GenerateDCMTKConfigure.cmake 2024-11-25 16:54:59.036009112 +0100 @@ -224,6 +224,8 @@ # Check the sizes of various types @@ -18,9 +18,11 @@ # Check for include files, libraries, and functions include("${DCMTK_CMAKE_INCLUDE}CMake/dcmtkTryCompile.cmake") +Only in dcmtk-DCMTK-3.6.8/config/include/dcmtk/config: arith.h +Only in dcmtk-DCMTK-3.6.8/config/include/dcmtk/config: osconfig.h diff -urEb dcmtk-DCMTK-3.6.8.orig/dcmdata/include/dcmtk/dcmdata/dcdict.h dcmtk-DCMTK-3.6.8/dcmdata/include/dcmtk/dcmdata/dcdict.h ---- dcmtk-DCMTK-3.6.8.orig/dcmdata/include/dcmtk/dcmdata/dcdict.h 2024-01-09 17:13:10.337673529 +0100 -+++ dcmtk-DCMTK-3.6.8/dcmdata/include/dcmtk/dcmdata/dcdict.h 2024-01-09 18:21:52.568142681 +0100 +--- dcmtk-DCMTK-3.6.8.orig/dcmdata/include/dcmtk/dcmdata/dcdict.h 2023-12-19 11:12:57.000000000 +0100 ++++ dcmtk-DCMTK-3.6.8/dcmdata/include/dcmtk/dcmdata/dcdict.h 2024-11-25 16:54:59.036009112 +0100 @@ -162,6 +162,12 @@ /// returns an iterator to the end of the repeating tag dictionary DcmDictEntryListIterator repeatingEnd() { return repDict.end(); } @@ -35,17 +37,18 @@ /** private undefined assignment operator diff -urEb dcmtk-DCMTK-3.6.8.orig/dcmdata/libsrc/dcdict.cc dcmtk-DCMTK-3.6.8/dcmdata/libsrc/dcdict.cc ---- dcmtk-DCMTK-3.6.8.orig/dcmdata/libsrc/dcdict.cc 2024-01-09 17:13:10.337673529 +0100 -+++ dcmtk-DCMTK-3.6.8/dcmdata/libsrc/dcdict.cc 2024-01-09 18:21:52.568142681 +0100 +--- dcmtk-DCMTK-3.6.8.orig/dcmdata/libsrc/dcdict.cc 2023-12-19 11:12:57.000000000 +0100 ++++ dcmtk-DCMTK-3.6.8/dcmdata/libsrc/dcdict.cc 2024-11-25 16:54:59.036009112 +0100 @@ -914,3 +914,5 @@ wrlock().clear(); wrunlock(); } + +#include "dcdict_orthanc.cc" +Only in dcmtk-DCMTK-3.6.8/dcmdata/libsrc: dcdict_orthanc.cc diff -urEb dcmtk-DCMTK-3.6.8.orig/dcmdata/libsrc/dcpxitem.cc dcmtk-DCMTK-3.6.8/dcmdata/libsrc/dcpxitem.cc ---- dcmtk-DCMTK-3.6.8.orig/dcmdata/libsrc/dcpxitem.cc 2024-01-09 17:13:10.337673529 +0100 -+++ dcmtk-DCMTK-3.6.8/dcmdata/libsrc/dcpxitem.cc 2024-01-09 18:21:52.568142681 +0100 +--- dcmtk-DCMTK-3.6.8.orig/dcmdata/libsrc/dcpxitem.cc 2023-12-19 11:12:57.000000000 +0100 ++++ dcmtk-DCMTK-3.6.8/dcmdata/libsrc/dcpxitem.cc 2024-11-25 16:54:59.036009112 +0100 @@ -31,6 +31,9 @@ #include "dcmtk/dcmdata/dcostrma.h" /* for class DcmOutputStream */ #include "dcmtk/dcmdata/dcwcache.h" /* for class DcmWriteCache */ @@ -57,8 +60,8 @@ // ******************************** diff -urEb dcmtk-DCMTK-3.6.8.orig/dcmnet/libsrc/scu.cc dcmtk-DCMTK-3.6.8/dcmnet/libsrc/scu.cc ---- dcmtk-DCMTK-3.6.8.orig/dcmnet/libsrc/scu.cc 2024-01-09 17:13:10.349673411 +0100 -+++ dcmtk-DCMTK-3.6.8/dcmnet/libsrc/scu.cc 2024-01-09 18:23:08.723435667 +0100 +--- dcmtk-DCMTK-3.6.8.orig/dcmnet/libsrc/scu.cc 2023-12-19 11:12:57.000000000 +0100 ++++ dcmtk-DCMTK-3.6.8/dcmnet/libsrc/scu.cc 2024-11-25 16:54:59.036009112 +0100 @@ -19,6 +19,11 @@ * */ @@ -72,8 +75,8 @@ #include "dcmtk/dcmdata/dcostrmf.h" /* for class DcmOutputFileStream */ diff -urEb dcmtk-DCMTK-3.6.8.orig/oflog/include/dcmtk/oflog/thread/syncpub.h dcmtk-DCMTK-3.6.8/oflog/include/dcmtk/oflog/thread/syncpub.h ---- dcmtk-DCMTK-3.6.8.orig/oflog/include/dcmtk/oflog/thread/syncpub.h 2024-01-09 17:13:10.389673016 +0100 -+++ dcmtk-DCMTK-3.6.8/oflog/include/dcmtk/oflog/thread/syncpub.h 2024-01-09 18:21:52.568142681 +0100 +--- dcmtk-DCMTK-3.6.8.orig/oflog/include/dcmtk/oflog/thread/syncpub.h 2023-12-19 11:12:57.000000000 +0100 ++++ dcmtk-DCMTK-3.6.8/oflog/include/dcmtk/oflog/thread/syncpub.h 2024-11-25 16:54:59.037009100 +0100 @@ -63,7 +63,7 @@ DCMTK_LOG4CPLUS_INLINE_EXPORT @@ -111,8 +114,8 @@ diff -urEb dcmtk-DCMTK-3.6.8.orig/oflog/libsrc/oflog.cc dcmtk-DCMTK-3.6.8/oflog/libsrc/oflog.cc ---- dcmtk-DCMTK-3.6.8.orig/oflog/libsrc/oflog.cc 2024-01-09 17:13:10.389673016 +0100 -+++ dcmtk-DCMTK-3.6.8/oflog/libsrc/oflog.cc 2024-01-09 18:21:52.568142681 +0100 +--- dcmtk-DCMTK-3.6.8.orig/oflog/libsrc/oflog.cc 2023-12-19 11:12:57.000000000 +0100 ++++ dcmtk-DCMTK-3.6.8/oflog/libsrc/oflog.cc 2024-11-25 16:54:59.037009100 +0100 @@ -19,6 +19,11 @@ * */ @@ -126,8 +129,8 @@ #include "dcmtk/oflog/oflog.h" diff -urEb dcmtk-DCMTK-3.6.8.orig/ofstd/include/dcmtk/ofstd/offile.h dcmtk-DCMTK-3.6.8/ofstd/include/dcmtk/ofstd/offile.h ---- dcmtk-DCMTK-3.6.8.orig/ofstd/include/dcmtk/ofstd/offile.h 2024-01-09 17:13:10.389673016 +0100 -+++ dcmtk-DCMTK-3.6.8/ofstd/include/dcmtk/ofstd/offile.h 2024-01-09 18:21:52.568142681 +0100 +--- dcmtk-DCMTK-3.6.8.orig/ofstd/include/dcmtk/ofstd/offile.h 2023-12-19 11:12:57.000000000 +0100 ++++ dcmtk-DCMTK-3.6.8/ofstd/include/dcmtk/ofstd/offile.h 2024-11-25 16:54:59.037009100 +0100 @@ -570,7 +570,7 @@ */ void setlinebuf() @@ -137,3 +140,18 @@ this->setvbuf(NULL, _IOLBF, 0); #else :: setlinebuf(file_); +diff -urEb dcmtk-DCMTK-3.6.8.orig/ofstd/include/dcmtk/ofstd/ofutil.h dcmtk-DCMTK-3.6.8/ofstd/include/dcmtk/ofstd/ofutil.h +--- dcmtk-DCMTK-3.6.8.orig/ofstd/include/dcmtk/ofstd/ofutil.h 2023-12-19 11:12:57.000000000 +0100 ++++ dcmtk-DCMTK-3.6.8/ofstd/include/dcmtk/ofstd/ofutil.h 2024-11-25 17:00:27.525244000 +0100 +@@ -75,8 +75,8 @@ + // copy constructor should be fine for primitive types. + inline type(const T& pt) + : t( pt ) {} +- inline type(const OFrvalue_storage& rhs) +- : t( rhs.pt ) {} ++ inline type(const type& rhs) ++ : t( rhs.t ) {} + + // automatic conversion to the underlying type + inline operator T&() const { return OFconst_cast( T&, t ); } +Only in dcmtk-DCMTK-3.6.8/ofstd/include/dcmtk/ofstd: ofutil.h~
--- a/OrthancFramework/Sources/DicomFormat/DicomStreamReader.cpp Mon Dec 09 19:41:02 2024 +0100 +++ b/OrthancFramework/Sources/DicomFormat/DicomStreamReader.cpp Mon Dec 09 19:41:24 2024 +0100 @@ -247,6 +247,10 @@ pos += length + 12; } + else + { + throw OrthancException(ErrorCode_BadFileFormat, "Invalid DICOM File: Unable to parse Meta Header"); + } } if (pos != block.size())
--- a/OrthancFramework/Sources/FileStorage/StorageAccessor.cpp Mon Dec 09 19:41:02 2024 +0100 +++ b/OrthancFramework/Sources/FileStorage/StorageAccessor.cpp Mon Dec 09 19:41:24 2024 +0100 @@ -28,16 +28,18 @@ #include "../Logging.h" #include "../StringMemoryBuffer.h" -#include "../Compatibility.h" #include "../Compression/ZlibCompressor.h" #include "../MetricsRegistry.h" #include "../OrthancException.h" +#include "../SerializationToolbox.h" #include "../Toolbox.h" #if ORTHANC_ENABLE_CIVETWEB == 1 || ORTHANC_ENABLE_MONGOOSE == 1 # include "../HttpServer/HttpStreamTranscoder.h" #endif +#include <boost/algorithm/string.hpp> + static const std::string METRICS_CREATE_DURATION = "orthanc_storage_create_duration_ms"; static const std::string METRICS_READ_DURATION = "orthanc_storage_read_duration_ms"; @@ -50,6 +52,212 @@ namespace Orthanc { + void StorageAccessor::Range::SanityCheck() const + { + if (hasStart_ && hasEnd_ && start_ > end_) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + + StorageAccessor::Range::Range(): + hasStart_(false), + start_(0), + hasEnd_(false), + end_(0) + { + } + + void StorageAccessor::Range::SetStartInclusive(uint64_t start) + { + hasStart_ = true; + start_ = start; + } + + void StorageAccessor::Range::SetEndInclusive(uint64_t end) + { + hasEnd_ = true; + end_ = end; + } + + uint64_t StorageAccessor::Range::GetStartInclusive() const + { + if (!hasStart_) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else if (hasEnd_ && start_ > end_) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + return start_; + } + } + + uint64_t StorageAccessor::Range::GetEndInclusive() const + { + if (!hasEnd_) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else if (hasStart_ && start_ > end_) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + return end_; + } + } + + std::string StorageAccessor::Range::FormatHttpContentRange(uint64_t fullSize) const + { + SanityCheck(); + + if (fullSize == 0 || + (hasStart_ && start_ >= fullSize) || + (hasEnd_ && end_ >= fullSize)) + { + throw OrthancException(ErrorCode_BadRange); + } + + std::string s = "bytes "; + + if (hasStart_) + { + s += boost::lexical_cast<std::string>(start_); + } + else + { + s += "0"; + } + + s += "-"; + + if (hasEnd_) + { + s += boost::lexical_cast<std::string>(end_); + } + else + { + s += boost::lexical_cast<std::string>(fullSize - 1); + } + + return s + "/" + boost::lexical_cast<std::string>(fullSize); + } + + void StorageAccessor::Range::Extract(std::string &target, + const std::string &source) const + { + SanityCheck(); + + if (hasStart_ && start_ >= source.size()) + { + throw OrthancException(ErrorCode_BadRange); + } + + if (hasEnd_ && end_ >= source.size()) + { + throw OrthancException(ErrorCode_BadRange); + } + + if (hasStart_ && hasEnd_) + { + target = source.substr(start_, end_ - start_ + 1); + } + else if (hasStart_) + { + target = source.substr(start_, source.size() - start_); + } + else if (hasEnd_) + { + target = source.substr(0, end_ + 1); + } + else + { + target = source; + } + } + + uint64_t StorageAccessor::Range::GetContentLength(uint64_t fullSize) const + { + SanityCheck(); + + if (fullSize == 0) + { + throw OrthancException(ErrorCode_BadRange); + } + + if (hasStart_ && start_ >= fullSize) + { + throw OrthancException(ErrorCode_BadRange); + } + + if (hasEnd_ && end_ >= fullSize) + { + throw OrthancException(ErrorCode_BadRange); + } + + if (hasStart_ && hasEnd_) + { + return end_ - start_ + 1; + } + else if (hasStart_) + { + return fullSize - start_; + } + else if (hasEnd_) + { + return end_ + 1; + } + else + { + return fullSize; + } + } + + StorageAccessor::Range StorageAccessor::Range::ParseHttpRange(const std::string& s) + { + static const std::string BYTES = "bytes="; + + if (!boost::starts_with(s, BYTES)) + { + throw OrthancException(ErrorCode_BadRange); // Range not satisfiable + } + + std::vector<std::string> tokens; + Orthanc::Toolbox::TokenizeString(tokens, s.substr(BYTES.length()), '-'); + + if (tokens.size() != 2) + { + throw OrthancException(ErrorCode_BadRange); + } + + Range range; + + uint64_t tmp; + if (!tokens[0].empty()) + { + if (SerializationToolbox::ParseUnsignedInteger64(tmp, tokens[0])) + { + range.SetStartInclusive(tmp); + } + } + + if (!tokens[1].empty()) + { + if (SerializationToolbox::ParseUnsignedInteger64(tmp, tokens[1])) + { + range.SetEndInclusive(tmp); + } + } + + range.SanityCheck(); + return range; + } + class StorageAccessor::MetricsTimer : public boost::noncopyable { private: @@ -351,15 +559,6 @@ } - void ReadStartRangeFromAreaInternal(std::string& target, - IStorageArea& area, - const std::string& fileUuid, - FileContentType contentType, - uint64_t end /* exclusive */) - { - - } - void StorageAccessor::ReadStartRange(std::string& target, const FileInfo& info, uint64_t end /* exclusive */) @@ -430,6 +629,79 @@ } + void StorageAccessor::ReadRange(std::string &target, + const FileInfo &info, + const Range &range, + bool uncompressIfNeeded) + { + if (uncompressIfNeeded && + info.GetCompressionType() != CompressionType_None) + { + // An uncompression is needed in this case + if (cache_ != NULL) + { + StorageCache::Accessor cacheAccessor(*cache_); + + std::string content; + if (cacheAccessor.Fetch(content, info.GetUuid(), info.GetContentType())) + { + range.Extract(target, content); + return; + } + } + + std::string content; + Read(content, info); + range.Extract(target, content); + } + else + { + // Access to the raw attachment is sufficient in this case + if (info.GetCompressionType() == CompressionType_None && + cache_ != NULL) + { + // Check out whether the raw attachment is already present in the cache, by chance + StorageCache::Accessor cacheAccessor(*cache_); + + std::string content; + if (cacheAccessor.Fetch(content, info.GetUuid(), info.GetContentType())) + { + range.Extract(target, content); + return; + } + } + + if (range.HasEnd() && + range.GetEndInclusive() >= info.GetCompressedSize()) + { + throw OrthancException(ErrorCode_BadRange); + } + + std::unique_ptr<IMemoryBuffer> buffer; + + if (range.HasStart() && + range.HasEnd()) + { + buffer.reset(area_.ReadRange(info.GetUuid(), info.GetContentType(), range.GetStartInclusive(), range.GetEndInclusive() + 1)); + } + else if (range.HasStart()) + { + buffer.reset(area_.ReadRange(info.GetUuid(), info.GetContentType(), range.GetStartInclusive(), info.GetCompressedSize())); + } + else if (range.HasEnd()) + { + buffer.reset(area_.ReadRange(info.GetUuid(), info.GetContentType(), 0, range.GetEndInclusive() + 1)); + } + else + { + buffer.reset(area_.Read(info.GetUuid(), info.GetContentType())); + } + + buffer->MoveToString(target); + } + } + + #if ORTHANC_ENABLE_CIVETWEB == 1 || ORTHANC_ENABLE_MONGOOSE == 1 void StorageAccessor::SetupSender(BufferHttpSender& sender, const FileInfo& info,
--- a/OrthancFramework/Sources/FileStorage/StorageAccessor.h Mon Dec 09 19:41:02 2024 +0100 +++ b/OrthancFramework/Sources/FileStorage/StorageAccessor.h Mon Dec 09 19:41:24 2024 +0100 @@ -65,6 +65,48 @@ **/ class ORTHANC_PUBLIC StorageAccessor : boost::noncopyable { + public: + class ORTHANC_PUBLIC Range + { + private: + bool hasStart_; + uint64_t start_; + bool hasEnd_; + uint64_t end_; + + void SanityCheck() const; + + public: + Range(); + + void SetStartInclusive(uint64_t start); + + void SetEndInclusive(uint64_t end); + + bool HasStart() const + { + return hasStart_; + } + + bool HasEnd() const + { + return hasEnd_; + } + + uint64_t GetStartInclusive() const; + + uint64_t GetEndInclusive() const; + + std::string FormatHttpContentRange(uint64_t fullSize) const; + + void Extract(std::string& target, + const std::string& source) const; + + uint64_t GetContentLength(uint64_t fullSize) const; + + static Range ParseHttpRange(const std::string& s); + }; + private: class MetricsTimer; @@ -117,6 +159,11 @@ void Remove(const FileInfo& info); + void ReadRange(std::string& target, + const FileInfo& info, + const Range& range, + bool uncompressIfNeeded); + #if ORTHANC_ENABLE_CIVETWEB == 1 || ORTHANC_ENABLE_MONGOOSE == 1 void AnswerFile(HttpOutput& output, const FileInfo& info,
--- a/OrthancFramework/Sources/HttpServer/HttpOutput.cpp Mon Dec 09 19:41:02 2024 +0100 +++ b/OrthancFramework/Sources/HttpServer/HttpOutput.cpp Mon Dec 09 19:41:24 2024 +0100 @@ -62,7 +62,8 @@ contentPosition_(0), keepAlive_(isKeepAlive), keepAliveTimeout_(keepAliveTimeout), - hasXContentTypeOptions_(false) + hasXContentTypeOptions_(false), + hasContentType_(false) { } @@ -105,6 +106,7 @@ void HttpOutput::StateMachine::SetContentType(const char* contentType) { + hasContentType_ = true; AddHeader("Content-Type", contentType); } @@ -380,7 +382,8 @@ stateMachine_.SetHttpStatus(status); - if (messageSize > 0) + if (messageSize > 0 && + !stateMachine_.HasContentType()) { // Assume that the body always contains a textual description of the error stateMachine_.SetContentType("text/plain");
--- a/OrthancFramework/Sources/HttpServer/HttpOutput.h Mon Dec 09 19:41:02 2024 +0100 +++ b/OrthancFramework/Sources/HttpServer/HttpOutput.h Mon Dec 09 19:41:24 2024 +0100 @@ -66,6 +66,7 @@ unsigned int keepAliveTimeout_; std::list<std::string> headers_; bool hasXContentTypeOptions_; + bool hasContentType_; std::string multipartBoundary_; std::string multipartContentType_; @@ -125,6 +126,11 @@ size_t size); void CloseStream(); + + bool HasContentType() const + { + return hasContentType_; + } }; StateMachine stateMachine_;
--- a/OrthancFramework/Sources/Lua/LuaContext.h Mon Dec 09 19:41:02 2024 +0100 +++ b/OrthancFramework/Sources/Lua/LuaContext.h Mon Dec 09 19:41:24 2024 +0100 @@ -124,5 +124,17 @@ lua_State* state, int top, bool keyToLowerCase); + +#if ORTHANC_ENABLE_CURL == 1 + void SetHttpsVerifyPeers(bool verify) + { + httpClient_.SetHttpsVerifyPeers(verify); + } + + bool IsHttpsVerifyPeers() const + { + return httpClient_.IsHttpsVerifyPeers(); + } +#endif }; }
--- a/OrthancFramework/UnitTestsSources/FileStorageTests.cpp Mon Dec 09 19:41:02 2024 +0100 +++ b/OrthancFramework/UnitTestsSources/FileStorageTests.cpp Mon Dec 09 19:41:24 2024 +0100 @@ -236,3 +236,92 @@ ASSERT_THROW(accessor.Read(r, uncompressedInfo.GetUuid(), FileContentType_Unknown), OrthancException); */ } + + +TEST(StorageAccessor, Range) +{ + { + StorageAccessor::Range range; + ASSERT_FALSE(range.HasStart()); + ASSERT_FALSE(range.HasEnd()); + ASSERT_THROW(range.GetStartInclusive(), OrthancException); + ASSERT_THROW(range.GetEndInclusive(), OrthancException); + ASSERT_EQ("bytes 0-99/100", range.FormatHttpContentRange(100)); + ASSERT_EQ("bytes 0-0/1", range.FormatHttpContentRange(1)); + ASSERT_THROW(range.FormatHttpContentRange(0), OrthancException); + ASSERT_EQ(100u, range.GetContentLength(100)); + ASSERT_EQ(1u, range.GetContentLength(1)); + ASSERT_THROW(range.GetContentLength(0), OrthancException); + + range.SetStartInclusive(10); + ASSERT_TRUE(range.HasStart()); + ASSERT_FALSE(range.HasEnd()); + ASSERT_EQ(10u, range.GetStartInclusive()); + ASSERT_THROW(range.GetEndInclusive(), OrthancException); + ASSERT_EQ("bytes 10-99/100", range.FormatHttpContentRange(100)); + ASSERT_EQ("bytes 10-10/11", range.FormatHttpContentRange(11)); + ASSERT_THROW(range.FormatHttpContentRange(10), OrthancException); + ASSERT_EQ(90u, range.GetContentLength(100)); + ASSERT_EQ(1u, range.GetContentLength(11)); + ASSERT_THROW(range.GetContentLength(10), OrthancException); + + range.SetEndInclusive(30); + ASSERT_TRUE(range.HasStart()); + ASSERT_TRUE(range.HasEnd()); + ASSERT_EQ(10u, range.GetStartInclusive()); + ASSERT_EQ(30u, range.GetEndInclusive()); + ASSERT_EQ("bytes 10-30/100", range.FormatHttpContentRange(100)); + ASSERT_EQ("bytes 10-30/31", range.FormatHttpContentRange(31)); + ASSERT_THROW(range.FormatHttpContentRange(30), OrthancException); + ASSERT_EQ(21u, range.GetContentLength(100)); + ASSERT_EQ(21u, range.GetContentLength(31)); + ASSERT_THROW(range.GetContentLength(30), OrthancException); + } + + { + StorageAccessor::Range range; + range.SetEndInclusive(20); + ASSERT_FALSE(range.HasStart()); + ASSERT_TRUE(range.HasEnd()); + ASSERT_THROW(range.GetStartInclusive(), OrthancException); + ASSERT_EQ(20u, range.GetEndInclusive()); + ASSERT_EQ("bytes 0-20/100", range.FormatHttpContentRange(100)); + ASSERT_EQ("bytes 0-20/21", range.FormatHttpContentRange(21)); + ASSERT_THROW(range.FormatHttpContentRange(20), OrthancException); + ASSERT_EQ(21u, range.GetContentLength(100)); + ASSERT_EQ(21u, range.GetContentLength(21)); + ASSERT_THROW(range.GetContentLength(20), OrthancException); + } + + { + StorageAccessor::Range range = StorageAccessor::Range::ParseHttpRange("bytes=1-30"); + ASSERT_TRUE(range.HasStart()); + ASSERT_TRUE(range.HasEnd()); + ASSERT_EQ(1u, range.GetStartInclusive()); + ASSERT_EQ(30u, range.GetEndInclusive()); + ASSERT_EQ("bytes 1-30/100", range.FormatHttpContentRange(100)); + } + + ASSERT_THROW(StorageAccessor::Range::ParseHttpRange("bytes="), OrthancException); + ASSERT_THROW(StorageAccessor::Range::ParseHttpRange("bytes=-1-30"), OrthancException); + ASSERT_THROW(StorageAccessor::Range::ParseHttpRange("bytes=100-30"), OrthancException); + + ASSERT_EQ("bytes 0-99/100", StorageAccessor::Range::ParseHttpRange("bytes=-").FormatHttpContentRange(100)); + ASSERT_EQ("bytes 0-10/100", StorageAccessor::Range::ParseHttpRange("bytes=-10").FormatHttpContentRange(100)); + ASSERT_EQ("bytes 10-99/100", StorageAccessor::Range::ParseHttpRange("bytes=10-").FormatHttpContentRange(100)); + + { + std::string s; + StorageAccessor::Range::ParseHttpRange("bytes=1-2").Extract(s, "Hello"); + ASSERT_EQ("el", s); + StorageAccessor::Range::ParseHttpRange("bytes=-2").Extract(s, "Hello"); + ASSERT_EQ("Hel", s); + StorageAccessor::Range::ParseHttpRange("bytes=3-").Extract(s, "Hello"); + ASSERT_EQ("lo", s); + StorageAccessor::Range::ParseHttpRange("bytes=-").Extract(s, "Hello"); + ASSERT_EQ("Hello", s); + StorageAccessor::Range::ParseHttpRange("bytes=4-").Extract(s, "Hello"); + ASSERT_EQ("o", s); + ASSERT_THROW(StorageAccessor::Range::ParseHttpRange("bytes=5-").Extract(s, "Hello"), OrthancException); + } +} \ No newline at end of file
--- a/OrthancFramework/UnitTestsSources/RestApiTests.cpp Mon Dec 09 19:41:02 2024 +0100 +++ b/OrthancFramework/UnitTestsSources/RestApiTests.cpp Mon Dec 09 19:41:24 2024 +0100 @@ -73,19 +73,24 @@ ASSERT_TRUE(c.IsVerbose()); c.SetVerbose(false); ASSERT_FALSE(c.IsVerbose()); + ASSERT_TRUE(c.IsRedirectionFollowed()); + c.SetRedirectionFollowed(false); + ASSERT_FALSE(c.IsRedirectionFollowed()); #if UNIT_TESTS_WITH_HTTP_CONNEXIONS == 1 - // The "http://www.orthanc-server.com/downloads/third-party/" does - // not automatically redirect to HTTPS, so we cas use it even if the - // OpenSSL/HTTPS support is disabled in curl - const std::string BASE = "http://www.orthanc-server.com/downloads/third-party/"; + // The "http://httpbin.org/get" URL does not automatically redirect + // to HTTPS, so we can use it even if the OpenSSL/HTTPS support is + // disabled in curl + const std::string URL = "http://httpbin.org/get"; + Json::Value v; - c.SetUrl(BASE + "Product.json"); + c.SetUrl(URL); c.Apply(v); ASSERT_TRUE(v.type() == Json::objectValue); - ASSERT_TRUE(v.isMember("Description")); + ASSERT_TRUE(v.isMember("url")); + ASSERT_EQ(URL, v["url"].asString()); #endif } #endif
--- a/OrthancServer/Plugins/Engine/PluginsManager.cpp Mon Dec 09 19:41:02 2024 +0100 +++ b/OrthancServer/Plugins/Engine/PluginsManager.cpp Mon Dec 09 19:41:24 2024 +0100 @@ -244,8 +244,21 @@ { if (!boost::filesystem::exists(path)) { - LOG(ERROR) << "Inexistent path to plugins: " << path; - return; + boost::filesystem::path p(path); + std::string extension = p.extension().string(); + Toolbox::ToLowerCase(extension); + + if (extension == PLUGIN_EXTENSION) + { + // if this is a plugin path, fail to start + throw OrthancException(ErrorCode_SharedLibrary, "Inexistent path to plugin: " + path); + } + else + { + // it might be a directory -> just log a warning + LOG(WARNING) << "Inexistent path to plugins: " << path; + return; + } } if (boost::filesystem::is_directory(path))
--- a/OrthancServer/Plugins/Samples/Housekeeper/Plugin.cpp Mon Dec 09 19:41:02 2024 +0100 +++ b/OrthancServer/Plugins/Samples/Housekeeper/Plugin.cpp Mon Dec 09 19:41:24 2024 +0100 @@ -859,12 +859,15 @@ "StorageCompressionChange": true, "MainDicomTagsChange": true, "UnnecessaryDicomAsJsonFiles": true, + "IngestTranscodingChange": true, "DicomWebCacheChange": true // new in 1.12.2 }, - // When rebuilding MainDicomTags, limit to a single level of resource. - // Allowed values: "Patient", "Study", "Series", "Instance" - "LimitMainDicomTagsReconstructLevel": "Study" + // When rebuilding MainDicomTags, limit to a single level of resource + // which can greatly improve performances e.g. if you have only updated + // the Study level ExtraMainDicomTags. + // Allowed values: "Patient", "Study", "Series", "Instance", "All" + "LimitMainDicomTagsReconstructLevel": "All" } } @@ -887,11 +890,12 @@ triggerOnDicomWebCacheChange_ = triggers.GetBooleanValue("DicomWebCacheChange", true); } - limitMainDicomTagsReconstructLevel_ = housekeeper.GetStringValue("LimitMainDicomTagsReconstructLevel", ""); + limitMainDicomTagsReconstructLevel_ = housekeeper.GetStringValue("LimitMainDicomTagsReconstructLevel", "All"); if (limitMainDicomTagsReconstructLevel_ != "Patient" && limitMainDicomTagsReconstructLevel_ != "Study" - && limitMainDicomTagsReconstructLevel_ != "Series" && limitMainDicomTagsReconstructLevel_ != "Instance") + && limitMainDicomTagsReconstructLevel_ != "Series" && limitMainDicomTagsReconstructLevel_ != "Instance" && limitMainDicomTagsReconstructLevel_ != "All") { ORTHANC_PLUGINS_LOG_ERROR("Housekeeper invalid value for 'LimitMainDicomTagsReconstructLevel': '" + limitMainDicomTagsReconstructLevel_ + "'"); + return -1; } else if (limitMainDicomTagsReconstructLevel_ == "Patient") {
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp Mon Dec 09 19:41:02 2024 +0100 +++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp Mon Dec 09 19:41:24 2024 +0100 @@ -47,6 +47,8 @@ #include <boost/math/special_functions/round.hpp> #include <boost/shared_ptr.hpp> +#include "../../../OrthancFramework/Sources/FileStorage/StorageAccessor.h" + /** * This semaphore is used to limit the number of concurrent HTTP * requests on CPU-intensive routes of the REST API, in order to @@ -442,7 +444,16 @@ else { // return the attachment without any transcoding - context.AnswerAttachment(call.GetOutput(), publicId, FileContentType_Dicom); + FileInfo info; + int64_t revision; + if (!context.GetIndex().LookupAttachment(info, revision, publicId, FileContentType_Dicom)) + { + throw OrthancException(ErrorCode_UnknownResource); + } + else + { + context.AnswerAttachment(call.GetOutput(), info); + } } } @@ -2239,16 +2250,15 @@ } - static bool GetAttachmentInfo(FileInfo& info, + static bool GetAttachmentInfo(FileInfo& info /* out */, + int64_t& revision /* out */, RestApiGetCall& call) { CheckValidResourceType(call); const std::string publicId = call.GetUriComponent("id", ""); - const std::string name = call.GetUriComponent("name", ""); - FileContentType contentType = StringToContentType(name); - - int64_t revision; + FileContentType contentType = StringToContentType(call.GetUriComponent("name", "")); + if (OrthancRestApi::GetIndex(call).LookupAttachment(info, revision, publicId, contentType)) { SetAttachmentETag(call.GetOutput(), revision, info); // New in Orthanc 1.9.2 @@ -2291,7 +2301,8 @@ } FileInfo info; - if (GetAttachmentInfo(info, call)) + int64_t revision; + if (GetAttachmentInfo(info, revision, call)) { Json::Value operations = Json::arrayValue; @@ -2343,7 +2354,8 @@ .SetUriArgument("name", "The name of the attachment, or its index (cf. `UserContentType` configuration option)") .AddAnswerType(MimeType_Binary, "The attachment") .SetAnswerHeader("ETag", "Revision of the attachment, to be used in further `PUT` or `DELETE` operations") - .SetHttpHeader("If-None-Match", "Optional revision of the metadata, to check if its content has changed"); + .SetHttpHeader("If-None-Match", "Optional revision of the attachment, to check if its content has changed") + .SetHttpHeader("Content-Range", "Optional content range to access part of the attachment (new in Orthanc 1.12.5)"); return; } @@ -2352,37 +2364,54 @@ CheckValidResourceType(call); std::string publicId = call.GetUriComponent("id", ""); - FileContentType type = StringToContentType(call.GetUriComponent("name", "")); + + bool hasRangeHeader = false; + StorageAccessor::Range range; + + HttpToolbox::Arguments::const_iterator rangeHeader = call.GetHttpHeaders().find("range"); + if (rangeHeader != call.GetHttpHeaders().end()) + { + hasRangeHeader = true; + range = StorageAccessor::Range::ParseHttpRange(rangeHeader->second); + } FileInfo info; - if (GetAttachmentInfo(info, call)) + int64_t revision; + if (GetAttachmentInfo(info, revision, call)) { // NB: "SetAttachmentETag()" is already invoked by "GetAttachmentInfo()" - if (uncompress) + int64_t userRevision; + std::string userMD5; + if (GetRevisionHeader(userRevision, userMD5, call, "If-None-Match") && + revision == userRevision && + info.GetUncompressedMD5() == userMD5) + { + call.GetOutput().GetLowLevelOutput().SendStatus(HttpStatus_304_NotModified); + return; + } + + if (hasRangeHeader) { - context.AnswerAttachment(call.GetOutput(), publicId, type); + std::string fragment; + context.ReadAttachmentRange(fragment, info, range, uncompress); + + uint64_t fullSize = (uncompress ? info.GetUncompressedSize() : info.GetCompressedSize()); + call.GetOutput().GetLowLevelOutput().SetContentType(MimeType_Binary); + call.GetOutput().GetLowLevelOutput().AddHeader("Content-Range", range.FormatHttpContentRange(fullSize)); + call.GetOutput().GetLowLevelOutput().SendStatus(HttpStatus_206_PartialContent, fragment); + } + else if (uncompress || + info.GetCompressionType() == CompressionType_None) + { + context.AnswerAttachment(call.GetOutput(), info); } else { - // Return the raw data (possibly compressed), as stored on the filesystem + // Access to the raw attachment (which is compressed) std::string content; - std::string attachmentId; - int64_t revision; - context.ReadAttachment(content, revision, attachmentId, publicId, type, false, true /* skipCache when you absolutely need the compressed data */); - - int64_t userRevision; - std::string userMD5; - if (GetRevisionHeader(userRevision, userMD5, call, "If-None-Match") && - revision == userRevision && - info.GetUncompressedMD5() == userMD5) - { - call.GetOutput().GetLowLevelOutput().SendStatus(HttpStatus_304_NotModified); - } - else - { - call.GetOutput().AnswerBuffer(content, MimeType_Binary); - } + context.ReadAttachment(content, info, false /* don't uncompress */, true /* skip cache */); + call.GetOutput().AnswerBuffer(content, MimeType_Binary); } } } @@ -2404,7 +2433,8 @@ } FileInfo info; - if (GetAttachmentInfo(info, call)) + int64_t revision; + if (GetAttachmentInfo(info, revision, call)) { call.GetOutput().AnswerBuffer(boost::lexical_cast<std::string>(info.GetUncompressedSize()), MimeType_PlainText); } @@ -2427,7 +2457,8 @@ } FileInfo info; - if (GetAttachmentInfo(info, call)) + int64_t revision; + if (GetAttachmentInfo(info, revision, call)) { Json::Value result = Json::objectValue; result["Uuid"] = info.GetUuid(); @@ -2458,7 +2489,8 @@ } FileInfo info; - if (GetAttachmentInfo(info, call)) + int64_t revision; + if (GetAttachmentInfo(info, revision, call)) { call.GetOutput().AnswerBuffer(boost::lexical_cast<std::string>(info.GetCompressedSize()), MimeType_PlainText); } @@ -2481,7 +2513,8 @@ } FileInfo info; - if (GetAttachmentInfo(info, call) && + int64_t revision; + if (GetAttachmentInfo(info, revision, call) && info.GetUncompressedMD5() != "") { call.GetOutput().AnswerBuffer(boost::lexical_cast<std::string>(info.GetUncompressedMD5()), MimeType_PlainText); @@ -2506,7 +2539,8 @@ } FileInfo info; - if (GetAttachmentInfo(info, call) && + int64_t revision; + if (GetAttachmentInfo(info, revision, call) && info.GetCompressedMD5() != "") { call.GetOutput().AnswerBuffer(boost::lexical_cast<std::string>(info.GetCompressedMD5()), MimeType_PlainText); @@ -2551,9 +2585,8 @@ // First check whether the compressed data is correctly stored in the disk std::string data; - std::string attachmentId; - - context.ReadAttachment(data, revision, attachmentId, publicId, StringToContentType(name), false, true /* skipCache when you absolutely need the compressed data */); + + context.ReadAttachment(data, info, false, true /* skipCache when you absolutely need the compressed data */); std::string actualMD5; Toolbox::ComputeMD5(actualMD5, data); @@ -2568,7 +2601,7 @@ } else { - context.ReadAttachment(data, revision, attachmentId, publicId, StringToContentType(name), true, true /* skipCache when you absolutely need the compressed data */); + context.ReadAttachment(data, info, true, true /* skipCache when you absolutely need the compressed data */); Toolbox::ComputeMD5(actualMD5, data); ok = (actualMD5 == info.GetUncompressedMD5()); } @@ -2778,7 +2811,8 @@ } FileInfo info; - if (GetAttachmentInfo(info, call)) + int64_t revision; + if (GetAttachmentInfo(info, revision, call)) { std::string answer = (info.GetCompressionType() == CompressionType_None) ? "0" : "1"; call.GetOutput().AnswerBuffer(answer, MimeType_PlainText);
--- a/OrthancServer/Sources/ServerContext.cpp Mon Dec 09 19:41:02 2024 +0100 +++ b/OrthancServer/Sources/ServerContext.cpp Mon Dec 09 19:41:24 2024 +0100 @@ -970,20 +970,10 @@ void ServerContext::AnswerAttachment(RestApiOutput& output, - const std::string& resourceId, - FileContentType content) + const FileInfo& attachment) { - FileInfo attachment; - int64_t revision; - if (!index_.LookupAttachment(attachment, revision, resourceId, content)) - { - throw OrthancException(ErrorCode_UnknownResource); - } - else - { - StorageAccessor accessor(area_, storageCache_, GetMetricsRegistry()); - accessor.AnswerFile(output, attachment, GetFileContentMime(content)); - } + StorageAccessor accessor(area_, storageCache_, GetMetricsRegistry()); + accessor.AnswerFile(output, attachment, GetFileContentMime(attachment.GetContentType())); } @@ -1224,8 +1214,20 @@ std::string& attachmentId, const std::string& instancePublicId) { + FileInfo attachment; int64_t revision; - ReadAttachment(dicom, revision, attachmentId, instancePublicId, FileContentType_Dicom, true /* uncompress */); + + if (!index_.LookupAttachment(attachment, revision, instancePublicId, FileContentType_Dicom)) + { + throw OrthancException(ErrorCode_InternalError, + "Unable to read attachment " + EnumerationToString(FileContentType_Dicom) + + " of instance " + instancePublicId); + } + + assert(attachment.GetContentType() == FileContentType_Dicom); + attachmentId = attachment.GetUuid(); + + ReadAttachment(dicom, attachment, true /* uncompress */); } @@ -1300,47 +1302,40 @@ void ServerContext::ReadAttachment(std::string& result, - int64_t& revision, - std::string& attachmentId, - const std::string& instancePublicId, - FileContentType content, + const FileInfo& attachment, bool uncompressIfNeeded, bool skipCache) { - FileInfo attachment; - if (!index_.LookupAttachment(attachment, revision, instancePublicId, content)) - { - throw OrthancException(ErrorCode_InternalError, - "Unable to read attachment " + EnumerationToString(content) + - " of instance " + instancePublicId); - } - - assert(attachment.GetContentType() == content); - attachmentId = attachment.GetUuid(); - - { - std::unique_ptr<StorageAccessor> accessor; + std::unique_ptr<StorageAccessor> accessor; - if (skipCache) - { - accessor.reset(new StorageAccessor(area_, GetMetricsRegistry())); - } - else - { - accessor.reset(new StorageAccessor(area_, storageCache_, GetMetricsRegistry())); - } - - if (uncompressIfNeeded) - { - accessor->Read(result, attachment); - } - else - { - // Do not uncompress the content of the storage area, return the - // raw data - accessor->ReadRaw(result, attachment); - } + if (skipCache) + { + accessor.reset(new StorageAccessor(area_, GetMetricsRegistry())); + } + else + { + accessor.reset(new StorageAccessor(area_, storageCache_, GetMetricsRegistry())); + } + + if (uncompressIfNeeded) + { + accessor->Read(result, attachment); } + else + { + // Do not uncompress the content of the storage area, return the + // raw data + accessor->ReadRaw(result, attachment); + } + } + + void ServerContext::ReadAttachmentRange(std::string &result, + const FileInfo &attachment, + const StorageAccessor::Range &range, + bool uncompressIfNeeded) + { + StorageAccessor accessor(area_, storageCache_, GetMetricsRegistry()); + accessor.ReadRange(result, attachment, range, uncompressIfNeeded); }
--- a/OrthancServer/Sources/ServerContext.h Mon Dec 09 19:41:02 2024 +0100 +++ b/OrthancServer/Sources/ServerContext.h Mon Dec 09 19:41:24 2024 +0100 @@ -38,6 +38,8 @@ #include <boost/date_time/posix_time/posix_time.hpp> +#include "../../OrthancFramework/Sources/FileStorage/StorageAccessor.h" + namespace Orthanc { class DicomInstanceToStore; @@ -366,8 +368,7 @@ bool isReconstruct = false); void AnswerAttachment(RestApiOutput& output, - const std::string& resourceId, - FileContentType content); + const FileInfo& fileInfo); void ChangeAttachmentCompression(const std::string& resourceId, FileContentType attachmentType, @@ -395,13 +396,15 @@ // This method is for low-level operations on "/instances/.../attachments/..." void ReadAttachment(std::string& result, - int64_t& revision, - std::string& attachmentId, - const std::string& instancePublicId, - FileContentType content, + const FileInfo& attachment, bool uncompressIfNeeded, bool skipCache = false); + void ReadAttachmentRange(std::string& result, + const FileInfo& attachment, + const StorageAccessor::Range& range, + bool uncompressIfNeeded); + void SetStoreMD5ForAttachments(bool storeMD5); bool IsStoreMD5ForAttachments() const
--- a/OrthancServer/UnitTestsSources/LuaServerTests.cpp Mon Dec 09 19:41:02 2024 +0100 +++ b/OrthancServer/UnitTestsSources/LuaServerTests.cpp Mon Dec 09 19:41:24 2024 +0100 @@ -138,38 +138,43 @@ TEST(Lua, Http) { Orthanc::LuaContext lua; - -#if UNIT_TESTS_WITH_HTTP_CONNEXIONS == 1 - // The "http://www.orthanc-server.com/downloads/third-party/" does - // not automatically redirect to HTTPS, so we use it even if the - // OpenSSL/HTTPS support is disabled in curl - const std::string BASE = "http://www.orthanc-server.com/downloads/third-party/"; - -#if LUA_VERSION_NUM >= 502 - // Since Lua >= 5.2.0, the function "loadstring" has been replaced by "load" - lua.Execute("JSON = load(HttpGet('" + BASE + "JSON.lua')) ()"); -#else - lua.Execute("JSON = loadstring(HttpGet('" + BASE + "JSON.lua')) ()"); -#endif - - const std::string url(BASE + "Product.json"); -#endif + ASSERT_TRUE(lua.IsHttpsVerifyPeers()); + lua.SetHttpsVerifyPeers(false); + ASSERT_FALSE(lua.IsHttpsVerifyPeers()); std::string s; lua.Execute(s, "print(HttpGet({}))"); ASSERT_EQ("nil", Orthanc::Toolbox::StripSpaces(s)); -#if UNIT_TESTS_WITH_HTTP_CONNEXIONS == 1 - lua.Execute(s, "print(string.len(HttpGet(\"" + url + "\")))"); - ASSERT_LE(100, boost::lexical_cast<int>(Orthanc::Toolbox::StripSpaces(s))); +#if UNIT_TESTS_WITH_HTTP_CONNEXIONS == 1 + // The "http://httpbin.org/get" URL does not automatically redirect + // to HTTPS, so we can use it even if the OpenSSL/HTTPS support is + // disabled in curl - // Parse a JSON file - lua.Execute(s, "print(JSON:decode(HttpGet(\"" + url + "\")) ['Product'])"); - ASSERT_EQ("OrthancClient", Orthanc::Toolbox::StripSpaces(s)); + const std::string URL = "http://httpbin.org/get"; + lua.Execute(s, "print(HttpGet(\"" + URL + "\"))"); + + Json::Value json; + Orthanc::Toolbox::ReadJson(json, s); + + ASSERT_TRUE(json.type() == Json::objectValue); + ASSERT_TRUE(json.isMember("url")); + ASSERT_EQ(URL, json["url"].asString()); #if 0 // This part of the test can only be executed if one instance of - // Orthanc is running on the localhost + // Orthanc is running on the localhost, with configuration option + // "ExecuteLuaEnabled" equals to "true" + + const std::string JSON_LUA_TOOLBOX = "https://regex.info/code/JSON.lua"; + lua.Execute("HttpGet('" + JSON_LUA_TOOLBOX + "')"); + +#if LUA_VERSION_NUM >= 502 + // Since Lua >= 5.2.0, the function "loadstring" has been replaced by "load" + lua.Execute("JSON = load(HttpGet('" + JSON_LUA_TOOLBOX + "')) ()"); +#else + lua.Execute("JSON = loadstring(HttpGet('" + JSON_LUA_TOOLBOX + "')) ()"); +#endif lua.Execute("modality = {}"); lua.Execute("table.insert(modality, 'ORTHANC')");
--- a/TODO Mon Dec 09 19:41:02 2024 +0100 +++ b/TODO Mon Dec 09 19:41:24 2024 +0100 @@ -297,8 +297,22 @@ https://groups.google.com/g/orthanc-users/c/ymtaAmgSs6Q/m/PqVBactQAQAJ * Add an index on the UUID column in the DelayedDeletion plugin: https://discourse.orthanc-server.org/t/delayeddeletion-improvement-unique-index-on-pending-uuid-column/4032 +* Orthanc shall refuse to start if one registers 2 storage plugins. + Right now, this is not possible because OrthancPluginRegisterStorageArea2 does not return any value + and it can not throw an Exception because that's a core function called from a plugin -> the Exception + can not cross the C/C++ frontier safely -> we need a OrthancPluginRegisterStorageArea3 with a return value. + Ex: install DelayedDeletion + S3 storage. Right now, the second plugin to load is just ignored with an error + message in the logs. +----------- +Housekeeper +----------- + +* The Housekeeper should just refuse to start if you are using a lossy transfer syntax because that would generate + new orthanc ids as well and there is a risk of messing up things. tools/reconstruct shall return a 400 as well. + https://discourse.orthanc-server.org/t/crashes-on-housekeeper-transcode-storing-in-s3/5330/2 + ---------------- Ideas of plugins ----------------