# HG changeset patch # User Sebastien Jodogne # Date 1439291716 -7200 # Node ID 8bd0d897763fead8f2230d65e8ed44825e07b8f4 # Parent eb46cc06389a1c2d01ee0c5f898b015735df7f13 refactoring: IHttpStreamAnswer diff -r eb46cc06389a -r 8bd0d897763f CMakeLists.txt --- a/CMakeLists.txt Tue Aug 11 10:36:05 2015 +0200 +++ b/CMakeLists.txt Tue Aug 11 13:15:16 2015 +0200 @@ -100,6 +100,7 @@ Core/FileStorage/CompressedFileStorageAccessor.cpp Core/FileStorage/FileStorageAccessor.cpp Core/HttpClient.cpp + Core/HttpServer/BufferHttpSender.cpp Core/HttpServer/EmbeddedResourceHttpHandler.cpp Core/HttpServer/FilesystemHttpHandler.cpp Core/HttpServer/HttpToolbox.cpp diff -r eb46cc06389a -r 8bd0d897763f Core/HttpServer/BufferHttpSender.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/HttpServer/BufferHttpSender.cpp Tue Aug 11 13:15:16 2015 +0200 @@ -0,0 +1,75 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + +#include "../PrecompiledHeaders.h" +#include "BufferHttpSender.h" + +#include "../OrthancException.h" + +namespace Orthanc +{ + bool BufferHttpSender::ReadNextChunk() + { + if (done_) + { + return false; + } + else + { + done_ = false; + return true; + } + } + + const char* BufferHttpSender::GetChunkContent() + { + if (done_) + { + throw OrthancException(ErrorCode_InternalError); + } + else + { + return buffer_.c_str(); + } + } + + size_t BufferHttpSender::GetChunkSize() + { + if (done_) + { + throw OrthancException(ErrorCode_InternalError); + } + else + { + return buffer_.size(); + } + } +} diff -r eb46cc06389a -r 8bd0d897763f Core/HttpServer/BufferHttpSender.h --- a/Core/HttpServer/BufferHttpSender.h Tue Aug 11 10:36:05 2015 +0200 +++ b/Core/HttpServer/BufferHttpSender.h Tue Aug 11 13:15:16 2015 +0200 @@ -39,24 +39,13 @@ { private: std::string buffer_; + bool done_; - protected: - virtual uint64_t GetFileSize() + public: + BufferHttpSender() : done_(false) { - return buffer_.size(); } - virtual bool SendData(HttpOutput& output) - { - if (buffer_.size()) - { - output.SendBody(&buffer_[0], buffer_.size()); - } - - return true; - } - - public: std::string& GetBuffer() { return buffer_; @@ -66,5 +55,21 @@ { return buffer_; } + + + /** + * Implementation of the IHttpStreamAnswer interface. + **/ + + virtual uint64_t GetContentLength() + { + return buffer_.size(); + } + + virtual bool ReadNextChunk(); + + virtual const char* GetChunkContent(); + + virtual size_t GetChunkSize(); }; } diff -r eb46cc06389a -r 8bd0d897763f Core/HttpServer/FilesystemHttpHandler.cpp --- a/Core/HttpServer/FilesystemHttpHandler.cpp Tue Aug 11 10:36:05 2015 +0200 +++ b/Core/HttpServer/FilesystemHttpHandler.cpp Tue Aug 11 13:15:16 2015 +0200 @@ -155,9 +155,8 @@ if (fs::exists(p) && fs::is_regular_file(p)) { - FilesystemHttpSender(p).Send(output); // TODO COMPRESSION - - //output.AnswerFileAutodetectContentType(p.string()); + FilesystemHttpSender sender(p); + output.Answer(sender); // TODO COMPRESSION } else if (listDirectoryContent_ && fs::exists(p) && diff -r eb46cc06389a -r 8bd0d897763f Core/HttpServer/FilesystemHttpSender.cpp --- a/Core/HttpServer/FilesystemHttpSender.cpp Tue Aug 11 10:36:05 2015 +0200 +++ b/Core/HttpServer/FilesystemHttpSender.cpp Tue Aug 11 13:15:16 2015 +0200 @@ -36,68 +36,50 @@ #include + +static const size_t CHUNK_SIZE = 64 * 1024; // Use 64KB chunks + namespace Orthanc { - void FilesystemHttpSender::Setup() - { - //SetDownloadFilename(path_.filename().string()); - -#if BOOST_HAS_FILESYSTEM_V3 == 1 - SetContentType(Toolbox::AutodetectMimeType(path_.filename().string())); -#else - SetContentType(Toolbox::AutodetectMimeType(path_.filename())); -#endif - } - - uint64_t FilesystemHttpSender::GetFileSize() - { - return Toolbox::GetFileSize(path_.string()); - } - - bool FilesystemHttpSender::SendData(HttpOutput& output) + void FilesystemHttpSender::Open() { - FILE* fp = fopen(path_.string().c_str(), "rb"); - if (!fp) - { - return false; - } - - std::vector buffer(1024 * 1024); // Chunks of 1MB + SetFilename(path_.filename().string()); + file_.open(path_.string().c_str(), std::ifstream::binary); - for (;;) - { - size_t nbytes = fread(&buffer[0], 1, buffer.size(), fp); - if (nbytes == 0) - { - break; - } - else - { - output.SendBody(&buffer[0], nbytes); - } - } + file_.seekg(0, file_.end); + size_ = file_.tellg(); + file_.seekg(0, file_.beg); - fclose(fp); - - return true; + chunk_.resize(CHUNK_SIZE); + chunkSize_ = 0; } FilesystemHttpSender::FilesystemHttpSender(const char* path) { path_ = std::string(path); - Setup(); + Open(); } FilesystemHttpSender::FilesystemHttpSender(const boost::filesystem::path& path) { path_ = path; - Setup(); + Open(); } FilesystemHttpSender::FilesystemHttpSender(const FilesystemStorage& storage, const std::string& uuid) { path_ = storage.GetPath(uuid).string(); - Setup(); + Open(); + } + + + bool FilesystemHttpSender::ReadNextChunk() + { + file_.read(&chunk_[0], chunk_.size()); + + chunkSize_ = file_.gcount(); + + return chunkSize_ > 0; } } diff -r eb46cc06389a -r 8bd0d897763f Core/HttpServer/FilesystemHttpSender.h --- a/Core/HttpServer/FilesystemHttpSender.h Tue Aug 11 10:36:05 2015 +0200 +++ b/Core/HttpServer/FilesystemHttpSender.h Tue Aug 11 13:15:16 2015 +0200 @@ -34,19 +34,20 @@ #include "HttpFileSender.h" #include "../FileStorage/FilesystemStorage.h" +#include + namespace Orthanc { class FilesystemHttpSender : public HttpFileSender { private: boost::filesystem::path path_; - - void Setup(); + std::ifstream file_; + uint64_t size_; + std::string chunk_; + size_t chunkSize_; - protected: - virtual uint64_t GetFileSize(); - - virtual bool SendData(HttpOutput& output); + void Open(); public: FilesystemHttpSender(const char* path); @@ -55,5 +56,27 @@ FilesystemHttpSender(const FilesystemStorage& storage, const std::string& uuid); + + + /** + * Implementation of the IHttpStreamAnswer interface. + **/ + + virtual uint64_t GetContentLength() + { + return size_; + } + + virtual bool ReadNextChunk(); + + virtual const char* GetChunkContent() + { + return chunk_.c_str(); + } + + virtual size_t GetChunkSize() + { + return chunkSize_; + } }; } diff -r eb46cc06389a -r 8bd0d897763f Core/HttpServer/HttpFileSender.cpp --- a/Core/HttpServer/HttpFileSender.cpp Tue Aug 11 10:36:05 2015 +0200 +++ b/Core/HttpServer/HttpFileSender.cpp Tue Aug 11 13:15:16 2015 +0200 @@ -34,34 +34,40 @@ #include "HttpFileSender.h" #include "../OrthancException.h" +#include "../Toolbox.h" #include namespace Orthanc { - void HttpFileSender::SendHeader(HttpOutput& output) + void HttpFileSender::SetFilename(const std::string& filename) { - if (contentType_.size() > 0) - { - output.SetContentType(contentType_.c_str()); - } + filename_ = filename; - if (downloadFilename_.size() > 0) + if (contentType_.empty()) { - output.SetContentFilename(downloadFilename_.c_str()); + contentType_ = Toolbox::AutodetectMimeType(filename); } - - output.SetContentLength(GetFileSize()); } - void HttpFileSender::Send(HttpOutput& output) + + bool HttpFileSender::HasContentFilename(std::string& filename) { - SendHeader(output); - - if (!SendData(output)) + if (!filename_.empty()) { - throw OrthancException(ErrorCode_InternalError); - //output.SendHeader(HttpStatus_500_InternalServerError); + filename = filename_; + } + } + + std::string HttpFileSender::GetContentType() + { + if (contentType_.empty()) + { + return "application/octet-stream"; + } + else + { + return contentType_; } } } diff -r eb46cc06389a -r 8bd0d897763f Core/HttpServer/HttpFileSender.h --- a/Core/HttpServer/HttpFileSender.h Tue Aug 11 10:36:05 2015 +0200 +++ b/Core/HttpServer/HttpFileSender.h Tue Aug 11 13:15:16 2015 +0200 @@ -36,29 +36,13 @@ namespace Orthanc { - class HttpFileSender + class HttpFileSender : public IHttpStreamAnswer { private: std::string contentType_; - std::string downloadFilename_; - - void SendHeader(HttpOutput& output); - - protected: - virtual uint64_t GetFileSize() = 0; - - virtual bool SendData(HttpOutput& output) = 0; + std::string filename_; public: - virtual ~HttpFileSender() - { - } - - void ResetContentType() - { - contentType_.clear(); - } - void SetContentType(const std::string& contentType) { contentType_ = contentType; @@ -69,21 +53,27 @@ return contentType_; } - void ResetDownloadFilename() + void SetFilename(const std::string& filename); + + const std::string& GetFilename() const { - downloadFilename_.clear(); + return filename_; } - void SetDownloadFilename(const std::string& filename) + + /** + * Implementation of the IHttpStreamAnswer interface. No + * compression is supported. + **/ + + virtual HttpCompression GetHttpCompression(bool /*gzipAllowed*/, + bool /*deflateAllowed*/) { - downloadFilename_ = filename; + return HttpCompression_None; } - const std::string& GetDownloadFilename() const - { - return downloadFilename_; - } - - void Send(HttpOutput& output); + virtual bool HasContentFilename(std::string& filename); + + virtual std::string GetContentType(); }; } diff -r eb46cc06389a -r 8bd0d897763f Core/HttpServer/HttpOutput.cpp --- a/Core/HttpServer/HttpOutput.cpp Tue Aug 11 10:36:05 2015 +0200 +++ b/Core/HttpServer/HttpOutput.cpp Tue Aug 11 13:15:16 2015 +0200 @@ -215,6 +215,41 @@ } + void HttpOutput::StateMachine::CloseBody() + { + switch (state_) + { + case State_WritingHeader: + LOG(ERROR) << "Closing the HTTP body, but the header has not been sent yet"; + throw OrthancException(ErrorCode_BadSequenceOfCalls); + + case State_WritingBody: + if (!hasContentLength_ || + contentPosition_ == contentLength_) + { + state_ = State_Done; + } + else + { + LOG(ERROR) << "The body size has not reached what was declared with SetContentSize()"; + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + break; + + case State_WritingMultipart: + LOG(ERROR) << "Cannot invoke CloseBody() with multipart outputs"; + throw OrthancException(ErrorCode_BadSequenceOfCalls); + + case State_Done: + return; // Ignore + + default: + throw OrthancException(ErrorCode_InternalError); + } + } + + HttpCompression HttpOutput::GetPreferredCompression(size_t bodySize) const { #if 0 @@ -351,9 +386,11 @@ SendBody(str.size() == 0 ? NULL : str.c_str(), str.size()); } - void HttpOutput::SendBody() + void HttpOutput::SendEmptyBody() { + stateMachine_.SetContentLength(0); stateMachine_.SendBody(NULL, 0); + stateMachine_.CloseBody(); } @@ -460,4 +497,52 @@ stateMachine_.SendMultipartItem(NULL, 0); } } + + + void HttpOutput::Answer(IHttpStreamAnswer& stream) + { + stateMachine_.SetContentLength(stream.GetContentLength()); + + std::string contentType = stream.GetContentType(); + if (contentType.empty()) + { + contentType = "application/octet-stream"; + } + + stateMachine_.SetContentType(contentType.c_str()); + + std::string filename; + if (stream.HasContentFilename(filename)) + { + SetContentFilename(filename.c_str()); + } + + HttpCompression compression = stream.GetHttpCompression(isGzipAllowed_, isDeflateAllowed_); + + switch (compression) + { + case HttpCompression_None: + break; + + case HttpCompression_Gzip: + stateMachine_.AddHeader("Content-Encoding", "gzip"); + break; + + case HttpCompression_Deflate: + stateMachine_.AddHeader("Content-Encoding", "deflate"); + break; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + while (stream.ReadNextChunk()) + { + stateMachine_.SendBody(stream.GetChunkContent(), + stream.GetChunkSize()); + } + + stateMachine_.CloseBody(); + } + } diff -r eb46cc06389a -r 8bd0d897763f Core/HttpServer/HttpOutput.h --- a/Core/HttpServer/HttpOutput.h Tue Aug 11 10:36:05 2015 +0200 +++ b/Core/HttpServer/HttpOutput.h Tue Aug 11 13:15:16 2015 +0200 @@ -37,6 +37,7 @@ #include #include "../Enumerations.h" #include "IHttpOutputStream.h" +#include "IHttpStreamAnswer.h" #include "../Uuid.h" namespace Orthanc @@ -102,6 +103,8 @@ void CloseMultipart(); + void CloseBody(); + State GetState() const { return state_; @@ -155,11 +158,6 @@ stateMachine_.SetContentFilename(filename); } - void SetContentLength(uint64_t length) - { - stateMachine_.SetContentLength(length); - } - void SetCookie(const std::string& cookie, const std::string& value) { @@ -177,7 +175,7 @@ void SendBody(const std::string& str); - void SendBody(); + void SendEmptyBody(); void SendMethodNotAllowed(const std::string& allowed); @@ -207,5 +205,7 @@ { return stateMachine_.GetState() == StateMachine::State_WritingMultipart; } + + void Answer(IHttpStreamAnswer& stream); }; } diff -r eb46cc06389a -r 8bd0d897763f Core/HttpServer/IHttpStreamAnswer.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/HttpServer/IHttpStreamAnswer.h Tue Aug 11 13:15:16 2015 +0200 @@ -0,0 +1,62 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include +#include + +namespace Orthanc +{ + class IHttpStreamAnswer : public boost::noncopyable + { + public: + virtual ~IHttpStreamAnswer() + { + } + + virtual HttpCompression GetHttpCompression(bool gzipAllowed, + bool deflateAllowed) = 0; + + virtual bool HasContentFilename(std::string& filename) = 0; + + virtual std::string GetContentType() = 0; + + virtual uint64_t GetContentLength() = 0; + + virtual bool ReadNextChunk() = 0; + + virtual const char* GetChunkContent() = 0; + + virtual size_t GetChunkSize() = 0; + }; +} diff -r eb46cc06389a -r 8bd0d897763f Core/HttpServer/MongooseServer.cpp --- a/Core/HttpServer/MongooseServer.cpp Tue Aug 11 10:36:05 2015 +0200 +++ b/Core/HttpServer/MongooseServer.cpp Tue Aug 11 13:15:16 2015 +0200 @@ -695,7 +695,7 @@ return; case PostDataStatus_Pending: - output.SendBody(); + output.SendEmptyBody(); return; default: diff -r eb46cc06389a -r 8bd0d897763f Core/RestApi/RestApiOutput.cpp --- a/Core/RestApi/RestApiOutput.cpp Tue Aug 11 10:36:05 2015 +0200 +++ b/Core/RestApi/RestApiOutput.cpp Tue Aug 11 13:15:16 2015 +0200 @@ -78,10 +78,10 @@ } - void RestApiOutput::AnswerFile(HttpFileSender& sender) + void RestApiOutput::AnswerStream(IHttpStreamAnswer& stream) { CheckStatus(); - sender.Send(output_); + output_.Answer(stream); alreadySent_ = true; } diff -r eb46cc06389a -r 8bd0d897763f Core/RestApi/RestApiOutput.h --- a/Core/RestApi/RestApiOutput.h Tue Aug 11 10:36:05 2015 +0200 +++ b/Core/RestApi/RestApiOutput.h Tue Aug 11 13:15:16 2015 +0200 @@ -55,16 +55,6 @@ ~RestApiOutput(); - HttpOutput& GetLowLevelOutput() - { - return output_; - } - - void MarkLowLevelOutputDone() - { - alreadySent_ = true; - } - void SetConvertJsonToXml(bool convert) { convertJsonToXml_ = convert; @@ -75,7 +65,7 @@ return convertJsonToXml_; } - void AnswerFile(HttpFileSender& sender); + void AnswerStream(IHttpStreamAnswer& stream); void AnswerJson(const Json::Value& value); diff -r eb46cc06389a -r 8bd0d897763f OrthancServer/OrthancRestApi/OrthancRestArchive.cpp --- a/OrthancServer/OrthancRestApi/OrthancRestArchive.cpp Tue Aug 11 10:36:05 2015 +0200 +++ b/OrthancServer/OrthancRestApi/OrthancRestArchive.cpp Tue Aug 11 13:15:16 2015 +0200 @@ -298,10 +298,10 @@ // Prepare the sending of the ZIP file FilesystemHttpSender sender(tmp.GetPath().c_str()); sender.SetContentType("application/zip"); - sender.SetDownloadFilename(id + ".zip"); + sender.SetFilename(id + ".zip"); // Send the ZIP - call.GetOutput().AnswerFile(sender); + call.GetOutput().AnswerStream(sender); // The temporary file is automatically removed thanks to the RAII } @@ -359,10 +359,10 @@ // Prepare the sending of the ZIP file FilesystemHttpSender sender(tmp.GetPath().c_str()); sender.SetContentType("application/zip"); - sender.SetDownloadFilename(id + ".zip"); + sender.SetFilename(id + ".zip"); // Send the ZIP - call.GetOutput().AnswerFile(sender); + call.GetOutput().AnswerStream(sender); // The temporary file is automatically removed thanks to the RAII } diff -r eb46cc06389a -r 8bd0d897763f OrthancServer/ParsedDicomFile.cpp --- a/OrthancServer/ParsedDicomFile.cpp Tue Aug 11 10:36:05 2015 +0200 +++ b/OrthancServer/ParsedDicomFile.cpp Tue Aug 11 13:15:16 2015 +0200 @@ -255,46 +255,93 @@ } - static void AnswerDicomField(RestApiOutput& output, - DcmElement& element, - E_TransferSyntax transferSyntax) + namespace { - // This element is nor a sequence, neither a pixel-data - std::string buffer; - buffer.resize(65536); - Uint32 length = element.getLength(transferSyntax); - Uint32 offset = 0; + class DicomFieldStream : public IHttpStreamAnswer + { + private: + DcmElement& element_; + uint32_t length_; + uint32_t offset_; + std::string chunk_; + size_t chunkSize_; + + public: + DicomFieldStream(DcmElement& element, + E_TransferSyntax transferSyntax) : + element_(element), + length_(element.getLength(transferSyntax)), + offset_(0) + { + static const size_t CHUNK_SIZE = 64 * 1024; // Use a 64KB chunk + chunk_.resize(CHUNK_SIZE); + } - output.GetLowLevelOutput().SetContentType(CONTENT_TYPE_OCTET_STREAM); - output.GetLowLevelOutput().SetContentLength(length); + virtual HttpCompression GetHttpCompression(bool /*gzipAllowed*/, + bool /*deflateAllowed*/) + { + // No support for compression + return HttpCompression_None; + } - while (offset < length) - { - Uint32 nbytes; - if (length - offset < buffer.size()) + virtual bool HasContentFilename(std::string& filename) { - nbytes = length - offset; + return false; } - else + + virtual std::string GetContentType() { - nbytes = buffer.size(); + return ""; } - OFCondition cond = element.getPartialValue(&buffer[0], offset, nbytes); - - if (cond.good()) + virtual uint64_t GetContentLength() { - output.GetLowLevelOutput().SendBody(&buffer[0], nbytes); - offset += nbytes; + return length_; } - else + + virtual bool ReadNextChunk() { - LOG(ERROR) << "Error while sending a DICOM field: " << cond.text(); - return; + assert(offset_ < length_); + + if (offset_ == length_) + { + return false; + } + else + { + if (length_ - offset_ < chunk_.size()) + { + chunkSize_ = length_ - offset_; + } + else + { + chunkSize_ = chunk_.size(); + } + + OFCondition cond = element_.getPartialValue(&chunk_[0], offset_, chunkSize_); + + offset_ += chunkSize_; + + if (!cond.good()) + { + LOG(ERROR) << "Error while sending a DICOM field: " << cond.text(); + throw OrthancException(ErrorCode_InternalError); + } + + return true; + } } - } - - output.MarkLowLevelOutputDone(); + + virtual const char *GetChunkContent() + { + return chunk_.c_str(); + } + + virtual size_t GetChunkSize() + { + return chunkSize_; + } + }; } @@ -365,7 +412,8 @@ { // This is the case for raw, uncompressed image buffers assert(*blockUri == "0"); - AnswerDicomField(output, *element, transferSyntax); + DicomFieldStream stream(*element, transferSyntax); + output.AnswerStream(stream); } } } @@ -406,7 +454,8 @@ //element->getVR() != EVR_UNKNOWN && // This would forbid private tags element->getVR() != EVR_SQ) { - AnswerDicomField(output, *element, transferSyntax); + DicomFieldStream stream(*element, transferSyntax); + output.AnswerStream(stream); } } diff -r eb46cc06389a -r 8bd0d897763f OrthancServer/ServerContext.cpp --- a/OrthancServer/ServerContext.cpp Tue Aug 11 10:36:05 2015 +0200 +++ b/OrthancServer/ServerContext.cpp Tue Aug 11 13:15:16 2015 +0200 @@ -317,8 +317,8 @@ std::auto_ptr sender(accessor_.ConstructHttpFileSender(attachment.GetUuid(), attachment.GetContentType())); sender->SetContentType(GetMimeType(content)); - sender->SetDownloadFilename(instancePublicId + ".dcm"); - output.AnswerFile(*sender); + sender->SetFilename(instancePublicId + ".dcm"); + output.AnswerStream(*sender); }