# HG changeset patch # User Sebastien Jodogne # Date 1733334000 -3600 # Node ID 3d13bd97b2814cbbcc0302328cf1757a4486cc28 # Parent 76f84b76bc740c556695fc10b65b343ea4d41a3d# Parent cc5a6f3b9bbe4b433d35d9c048e8146165880262 integration mainline->find-refactoring diff -r 76f84b76bc74 -r 3d13bd97b281 NEWS --- a/NEWS Tue Dec 03 14:27:31 2024 +0100 +++ b/NEWS Wed Dec 04 18:40:00 2024 +0100 @@ -23,6 +23,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 /system, added a new field "Capabilities" with new values: diff -r 76f84b76bc74 -r 3d13bd97b281 OrthancFramework/Sources/FileStorage/StorageAccessor.cpp --- a/OrthancFramework/Sources/FileStorage/StorageAccessor.cpp Tue Dec 03 14:27:31 2024 +0100 +++ b/OrthancFramework/Sources/FileStorage/StorageAccessor.cpp Wed Dec 04 18:40:00 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 + 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(start_); + } + else + { + s += "0"; + } + + s += "-"; + + if (hasEnd_) + { + s += boost::lexical_cast(end_); + } + else + { + s += boost::lexical_cast(fullSize - 1); + } + + return s + "/" + boost::lexical_cast(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 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 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, diff -r 76f84b76bc74 -r 3d13bd97b281 OrthancFramework/Sources/FileStorage/StorageAccessor.h --- a/OrthancFramework/Sources/FileStorage/StorageAccessor.h Tue Dec 03 14:27:31 2024 +0100 +++ b/OrthancFramework/Sources/FileStorage/StorageAccessor.h Wed Dec 04 18:40:00 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, diff -r 76f84b76bc74 -r 3d13bd97b281 OrthancFramework/Sources/HttpServer/HttpOutput.cpp --- a/OrthancFramework/Sources/HttpServer/HttpOutput.cpp Tue Dec 03 14:27:31 2024 +0100 +++ b/OrthancFramework/Sources/HttpServer/HttpOutput.cpp Wed Dec 04 18:40:00 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"); diff -r 76f84b76bc74 -r 3d13bd97b281 OrthancFramework/Sources/HttpServer/HttpOutput.h --- a/OrthancFramework/Sources/HttpServer/HttpOutput.h Tue Dec 03 14:27:31 2024 +0100 +++ b/OrthancFramework/Sources/HttpServer/HttpOutput.h Wed Dec 04 18:40:00 2024 +0100 @@ -66,6 +66,7 @@ unsigned int keepAliveTimeout_; std::list 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_; diff -r 76f84b76bc74 -r 3d13bd97b281 OrthancFramework/UnitTestsSources/FileStorageTests.cpp --- a/OrthancFramework/UnitTestsSources/FileStorageTests.cpp Tue Dec 03 14:27:31 2024 +0100 +++ b/OrthancFramework/UnitTestsSources/FileStorageTests.cpp Wed Dec 04 18:40:00 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 diff -r 76f84b76bc74 -r 3d13bd97b281 OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp --- a/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp Tue Dec 03 14:27:31 2024 +0100 +++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp Wed Dec 04 18:40:00 2024 +0100 @@ -50,6 +50,8 @@ #include #include +#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 @@ -406,7 +408,16 @@ else { // return the attachment without any transcoding - context.AnswerAttachment(call.GetOutput(), ResourceType_Instance, publicId, FileContentType_Dicom); + FileInfo info; + int64_t revision; + if (!context.GetIndex().LookupAttachment(info, revision, ResourceType_Instance, publicId, FileContentType_Dicom)) + { + throw OrthancException(ErrorCode_UnknownResource); + } + else + { + context.AnswerAttachment(call.GetOutput(), info); + } } } @@ -2187,15 +2198,14 @@ } - static bool GetAttachmentInfo(FileInfo& info, + static bool GetAttachmentInfo(FileInfo& info /* out */, + int64_t& revision /* out */, ResourceType level, RestApiGetCall& 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, level, publicId, contentType)) { SetAttachmentETag(call.GetOutput(), revision, info); // New in Orthanc 1.9.2 @@ -2239,7 +2249,8 @@ } FileInfo info; - if (GetAttachmentInfo(info, level, call)) + int64_t revision; + if (GetAttachmentInfo(info, revision, level, call)) { Json::Value operations = Json::arrayValue; @@ -2292,44 +2303,62 @@ .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; } ServerContext& context = OrthancRestApi::GetContext(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, level, call)) + int64_t revision; + if (GetAttachmentInfo(info, revision, level, 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(), level, 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, level, 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); } } } @@ -2352,7 +2381,8 @@ } FileInfo info; - if (GetAttachmentInfo(info, level, call)) + int64_t revision; + if (GetAttachmentInfo(info, revision, level, call)) { call.GetOutput().AnswerBuffer(boost::lexical_cast(info.GetUncompressedSize()), MimeType_PlainText); } @@ -2376,7 +2406,8 @@ } FileInfo info; - if (GetAttachmentInfo(info, level, call)) + int64_t revision; + if (GetAttachmentInfo(info, revision, level, call)) { Json::Value result = Json::objectValue; result["Uuid"] = info.GetUuid(); @@ -2408,7 +2439,8 @@ } FileInfo info; - if (GetAttachmentInfo(info, level, call)) + int64_t revision; + if (GetAttachmentInfo(info, revision, level, call)) { call.GetOutput().AnswerBuffer(boost::lexical_cast(info.GetCompressedSize()), MimeType_PlainText); } @@ -2432,7 +2464,8 @@ } FileInfo info; - if (GetAttachmentInfo(info, level, call) && + int64_t revision; + if (GetAttachmentInfo(info, revision, level, call) && info.GetUncompressedMD5() != "") { call.GetOutput().AnswerBuffer(boost::lexical_cast(info.GetUncompressedMD5()), MimeType_PlainText); @@ -2458,7 +2491,8 @@ } FileInfo info; - if (GetAttachmentInfo(info, level, call) && + int64_t revision; + if (GetAttachmentInfo(info, revision, level, call) && info.GetCompressedMD5() != "") { call.GetOutput().AnswerBuffer(boost::lexical_cast(info.GetCompressedMD5()), MimeType_PlainText); @@ -2503,9 +2537,7 @@ // First check whether the compressed data is correctly stored in the disk std::string data; - std::string attachmentId; - - context.ReadAttachment(data, revision, attachmentId, level, 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); @@ -2520,7 +2552,7 @@ } else { - context.ReadAttachment(data, revision, attachmentId, level, 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()); } @@ -2729,7 +2761,8 @@ } FileInfo info; - if (GetAttachmentInfo(info, level, call)) + int64_t revision; + if (GetAttachmentInfo(info, revision, level, call)) { std::string answer = (info.GetCompressionType() == CompressionType_None) ? "0" : "1"; call.GetOutput().AnswerBuffer(answer, MimeType_PlainText); diff -r 76f84b76bc74 -r 3d13bd97b281 OrthancServer/Sources/ServerContext.cpp --- a/OrthancServer/Sources/ServerContext.cpp Tue Dec 03 14:27:31 2024 +0100 +++ b/OrthancServer/Sources/ServerContext.cpp Wed Dec 04 18:40:00 2024 +0100 @@ -962,21 +962,10 @@ void ServerContext::AnswerAttachment(RestApiOutput& output, - ResourceType level, - const std::string& resourceId, - FileContentType content) + const FileInfo& attachment) { - FileInfo attachment; - int64_t revision; - if (!index_.LookupAttachment(attachment, revision, level, 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())); } @@ -1296,8 +1285,20 @@ std::string& attachmentId, const std::string& instancePublicId) { + FileInfo attachment; int64_t revision; - ReadAttachment(dicom, revision, attachmentId, ResourceType_Instance, instancePublicId, FileContentType_Dicom, true /* uncompress */); + + if (!index_.LookupAttachment(attachment, revision, ResourceType_Instance, 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 */); } @@ -1372,48 +1373,40 @@ void ServerContext::ReadAttachment(std::string& result, - int64_t& revision, - std::string& attachmentId, - ResourceType level, - const std::string& instancePublicId, - FileContentType content, + const FileInfo& attachment, bool uncompressIfNeeded, bool skipCache) { - FileInfo attachment; - if (!index_.LookupAttachment(attachment, revision, level, instancePublicId, content)) + std::unique_ptr accessor; + + if (skipCache) { - throw OrthancException(ErrorCode_InternalError, - "Unable to read attachment " + EnumerationToString(content) + - " of instance " + instancePublicId); + accessor.reset(new StorageAccessor(area_, GetMetricsRegistry())); + } + else + { + accessor.reset(new StorageAccessor(area_, storageCache_, GetMetricsRegistry())); } - assert(attachment.GetContentType() == content); - attachmentId = attachment.GetUuid(); - + if (uncompressIfNeeded) + { + accessor->Read(result, attachment); + } + else { - std::unique_ptr accessor; - - if (skipCache) - { - accessor.reset(new StorageAccessor(area_, GetMetricsRegistry())); - } - else - { - accessor.reset(new StorageAccessor(area_, storageCache_, GetMetricsRegistry())); - } + // Do not uncompress the content of the storage area, return the + // raw data + accessor->ReadRaw(result, attachment); + } + } - 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); } diff -r 76f84b76bc74 -r 3d13bd97b281 OrthancServer/Sources/ServerContext.h --- a/OrthancServer/Sources/ServerContext.h Tue Dec 03 14:27:31 2024 +0100 +++ b/OrthancServer/Sources/ServerContext.h Wed Dec 04 18:40:00 2024 +0100 @@ -38,6 +38,8 @@ #include +#include "../../OrthancFramework/Sources/FileStorage/StorageAccessor.h" + namespace Orthanc { class DicomInstanceToStore; @@ -356,9 +358,7 @@ bool isReconstruct = false); void AnswerAttachment(RestApiOutput& output, - ResourceType level, - const std::string& resourceId, - FileContentType content); + const FileInfo& fileInfo); void ChangeAttachmentCompression(ResourceType level, const std::string& resourceId, @@ -393,14 +393,15 @@ // This method is for low-level operations on "/instances/.../attachments/..." void ReadAttachment(std::string& result, - int64_t& revision, - std::string& attachmentId, - ResourceType level, - 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