Mercurial > hg > orthanc
changeset 5901:cc5a6f3b9bbe default
support HTTP "Range" request header on attachments
author | Sebastien Jodogne <s.jodogne@gmail.com> |
---|---|
date | Wed, 04 Dec 2024 18:16:44 +0100 |
parents | 1e51e6299f7a |
children | 3d13bd97b281 |
files | NEWS OrthancFramework/Sources/FileStorage/StorageAccessor.cpp OrthancFramework/Sources/FileStorage/StorageAccessor.h OrthancFramework/Sources/HttpServer/HttpOutput.cpp OrthancFramework/Sources/HttpServer/HttpOutput.h OrthancFramework/UnitTestsSources/FileStorageTests.cpp OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp OrthancServer/Sources/ServerContext.cpp OrthancServer/Sources/ServerContext.h |
diffstat | 9 files changed, 558 insertions(+), 107 deletions(-) [+] |
line wrap: on
line diff
--- a/NEWS Mon Nov 25 17:03:47 2024 +0100 +++ b/NEWS Wed Dec 04 18:16:44 2024 +0100 @@ -5,6 +5,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
--- a/OrthancFramework/Sources/FileStorage/StorageAccessor.cpp Mon Nov 25 17:03:47 2024 +0100 +++ b/OrthancFramework/Sources/FileStorage/StorageAccessor.cpp Wed Dec 04 18:16:44 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 Nov 25 17:03:47 2024 +0100 +++ b/OrthancFramework/Sources/FileStorage/StorageAccessor.h Wed Dec 04 18:16:44 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 Nov 25 17:03:47 2024 +0100 +++ b/OrthancFramework/Sources/HttpServer/HttpOutput.cpp Wed Dec 04 18:16:44 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 Nov 25 17:03:47 2024 +0100 +++ b/OrthancFramework/Sources/HttpServer/HttpOutput.h Wed Dec 04 18:16:44 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/UnitTestsSources/FileStorageTests.cpp Mon Nov 25 17:03:47 2024 +0100 +++ b/OrthancFramework/UnitTestsSources/FileStorageTests.cpp Wed Dec 04 18:16:44 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/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp Mon Nov 25 17:03:47 2024 +0100 +++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp Wed Dec 04 18:16:44 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 Nov 25 17:03:47 2024 +0100 +++ b/OrthancServer/Sources/ServerContext.cpp Wed Dec 04 18:16:44 2024 +0100 @@ -958,20 +958,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())); } @@ -1212,8 +1202,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 */); } @@ -1288,47 +1290,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 Nov 25 17:03:47 2024 +0100 +++ b/OrthancServer/Sources/ServerContext.h Wed Dec 04 18:16:44 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; @@ -364,8 +366,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, @@ -393,13 +394,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