Orthanc/OrthancServer/Sources/OrthancRestApi/OrthancRestArchive.cpp
2025-06-23 19:07:37 +05:30

840 lines
30 KiB
C++

/**
* Orthanc - A Lightweight, RESTful DICOM Store
* Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
* Department, University Hospital of Liege, Belgium
* Copyright (C) 2017-2023 Osimis S.A., Belgium
* Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
* Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
*
* This program is free software: you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* 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 <http://www.gnu.org/licenses/>.
**/
#include "../PrecompiledHeadersServer.h"
#include "OrthancRestApi.h"
#include "../../../OrthancFramework/Sources/Compression/ZipWriter.h"
#include "../../../OrthancFramework/Sources/HttpServer/FilesystemHttpSender.h"
#include "../../../OrthancFramework/Sources/Logging.h"
#include "../../../OrthancFramework/Sources/OrthancException.h"
#include "../../../OrthancFramework/Sources/SerializationToolbox.h"
#include "../../../OrthancFramework/Sources/Toolbox.h"
#include "../OrthancConfiguration.h"
#include "../ServerContext.h"
#include "../ServerJobs/ArchiveJob.h"
#include <boost/filesystem/fstream.hpp>
namespace Orthanc
{
static const char* const KEY_RESOURCES = "Resources";
static const char* const KEY_EXTENDED = "Extended";
static const char* const KEY_TRANSCODE = "Transcode";
static const char* const KEY_LOSSY_QUALITY = "LossyQuality";
static const char* const KEY_FILENAME = "Filename";
static const char* const GET_TRANSCODE = "transcode";
static const char* const GET_LOSSY_QUALITY = "lossy-quality";
static const char* const GET_FILENAME = "filename";
static const char* const GET_RESOURCES = "resources";
static const char* const CONFIG_LOADER_THREADS = "ZipLoaderThreads";
static void AddResourcesOfInterestFromString(ArchiveJob& job,
const std::string& resourcesList)
{
std::set<std::string> resources;
Toolbox::SplitString(resources, resourcesList, ',');
for (std::set<std::string>::const_iterator it = resources.begin(); it != resources.end(); ++it)
{
job.AddResource(*it, false, ResourceType_Patient /* dummy value */);
}
}
static void AddResourcesOfInterestFromArray(ArchiveJob& job,
const Json::Value& resources)
{
if (resources.type() != Json::arrayValue)
{
throw OrthancException(ErrorCode_BadFileFormat,
"Expected a list of strings (Orthanc identifiers)");
}
for (Json::Value::ArrayIndex i = 0; i < resources.size(); i++)
{
if (resources[i].type() != Json::stringValue)
{
throw OrthancException(ErrorCode_BadFileFormat,
"Expected a list of strings (Orthanc identifiers)");
}
else
{
job.AddResource(resources[i].asString(), false, ResourceType_Patient /* dummy value */);
}
}
}
static void AddResourcesOfInterest(ArchiveJob& job /* inout */,
const Json::Value& body /* in */)
{
if (body.type() == Json::arrayValue)
{
AddResourcesOfInterestFromArray(job, body);
}
else if (body.type() == Json::objectValue)
{
if (body.isMember(KEY_RESOURCES))
{
AddResourcesOfInterestFromArray(job, body[KEY_RESOURCES]);
}
else
{
throw OrthancException(ErrorCode_BadFileFormat,
"Missing field " + std::string(KEY_RESOURCES) +
" in the JSON body");
}
}
else
{
throw OrthancException(ErrorCode_BadFileFormat);
}
}
static void GetJobParameters(bool& synchronous, /* out */
bool& extended, /* out */
bool& transcode, /* out */
DicomTransferSyntax& syntax, /* out */
unsigned int& lossyQuality, /* out */
int& priority, /* out */
unsigned int& loaderThreads, /* out */
std::string& filename, /* out */
const Json::Value& body, /* in */
const bool defaultExtended /* in */,
const std::string& defaultFilename /* in */)
{
synchronous = OrthancRestApi::IsSynchronousJobRequest
(true /* synchronous by default */, body);
priority = OrthancRestApi::GetJobRequestPriority(body);
if (body.type() == Json::objectValue &&
body.isMember(KEY_EXTENDED))
{
extended = SerializationToolbox::ReadBoolean(body, KEY_EXTENDED);
}
else
{
extended = defaultExtended;
}
if (body.type() == Json::objectValue &&
body.isMember(KEY_TRANSCODE))
{
transcode = true;
syntax = Orthanc::GetTransferSyntax(SerializationToolbox::ReadString(body, KEY_TRANSCODE));
{
OrthancConfiguration::ReaderLock lock;
lossyQuality = lock.GetConfiguration().GetDicomLossyTranscodingQuality();
}
if (body.isMember(KEY_LOSSY_QUALITY))
{
lossyQuality = SerializationToolbox::ReadUnsignedInteger(body, KEY_LOSSY_QUALITY);
}
}
else
{
transcode = false;
}
if (body.type() == Json::objectValue &&
body.isMember(KEY_FILENAME) && body[KEY_FILENAME].isString())
{
filename = body[KEY_FILENAME].asString();
}
else
{
filename = defaultFilename;
}
{
OrthancConfiguration::ReaderLock lock;
loaderThreads = lock.GetConfiguration().GetUnsignedIntegerParameter(CONFIG_LOADER_THREADS, 0); // New in Orthanc 1.10.0
}
}
namespace
{
class SynchronousZipChunk : public IDynamicObject
{
private:
std::string chunk_;
bool done_;
explicit SynchronousZipChunk(bool done) :
done_(done)
{
}
public:
static SynchronousZipChunk* CreateDone()
{
return new SynchronousZipChunk(true);
}
static SynchronousZipChunk* CreateChunk(const std::string& chunk)
{
std::unique_ptr<SynchronousZipChunk> item(new SynchronousZipChunk(false));
item->chunk_ = chunk;
return item.release();
}
bool IsDone() const
{
return done_;
}
void SwapString(std::string& target)
{
if (done_)
{
throw OrthancException(ErrorCode_BadSequenceOfCalls);
}
else
{
target.swap(chunk_);
}
}
};
class SynchronousZipStream : public ZipWriter::IOutputStream
{
private:
boost::shared_ptr<SharedMessageQueue> queue_;
uint64_t archiveSize_;
public:
explicit SynchronousZipStream(const boost::shared_ptr<SharedMessageQueue>& queue) :
queue_(queue),
archiveSize_(0)
{
}
virtual uint64_t GetArchiveSize() const ORTHANC_OVERRIDE
{
return archiveSize_;
}
virtual void Write(const std::string& chunk) ORTHANC_OVERRIDE
{
if (queue_.unique())
{
throw OrthancException(ErrorCode_NetworkProtocol,
"HTTP client has disconnected while creating an archive in synchronous mode");
}
else
{
queue_->Enqueue(SynchronousZipChunk::CreateChunk(chunk));
archiveSize_ += chunk.size();
}
}
virtual void Close() ORTHANC_OVERRIDE
{
queue_->Enqueue(SynchronousZipChunk::CreateDone());
}
};
class SynchronousZipSender : public IHttpStreamAnswer
{
private:
ServerContext& context_;
std::string jobId_;
boost::shared_ptr<SharedMessageQueue> queue_;
std::string filename_;
bool done_;
std::string chunk_;
public:
SynchronousZipSender(ServerContext& context,
const std::string& jobId,
const boost::shared_ptr<SharedMessageQueue>& queue,
const std::string& filename) :
context_(context),
jobId_(jobId),
queue_(queue),
filename_(filename),
done_(false)
{
}
virtual HttpCompression SetupHttpCompression(bool gzipAllowed,
bool deflateAllowed) ORTHANC_OVERRIDE
{
// This function is not called by HttpOutput::AnswerWithoutBuffering()
throw OrthancException(ErrorCode_InternalError);
}
virtual bool HasContentFilename(std::string& filename) ORTHANC_OVERRIDE
{
filename = filename_;
return true;
}
virtual std::string GetContentType() ORTHANC_OVERRIDE
{
return EnumerationToString(MimeType_Zip);
}
virtual uint64_t GetContentLength() ORTHANC_OVERRIDE
{
throw OrthancException(ErrorCode_InternalError);
}
virtual bool ReadNextChunk() ORTHANC_OVERRIDE
{
for (;;)
{
std::unique_ptr<IDynamicObject> obj(queue_->Dequeue(100));
if (obj.get() == NULL)
{
// Check that the job is still active, which indicates
// that more data might still be returned
JobState state;
if (context_.GetJobsEngine().GetRegistry().GetState(state, jobId_) &&
(state == JobState_Pending ||
state == JobState_Running ||
state == JobState_Success))
{
continue;
}
else
{
return false;
}
}
else
{
SynchronousZipChunk& item = dynamic_cast<SynchronousZipChunk&>(*obj);
if (item.IsDone())
{
done_ = true;
}
else
{
item.SwapString(chunk_);
done_ = false;
}
return !done_;
}
}
}
virtual const char* GetChunkContent() ORTHANC_OVERRIDE
{
if (done_)
{
throw OrthancException(ErrorCode_InternalError);
}
else
{
return (chunk_.empty() ? NULL : chunk_.c_str());
}
}
virtual size_t GetChunkSize() ORTHANC_OVERRIDE
{
if (done_)
{
throw OrthancException(ErrorCode_InternalError);
}
else
{
return chunk_.size();
}
}
};
class SynchronousTemporaryStream : public ZipWriter::IOutputStream
{
private:
boost::shared_ptr<TemporaryFile> temp_;
boost::filesystem::ofstream file_;
uint64_t archiveSize_;
public:
explicit SynchronousTemporaryStream(const boost::shared_ptr<TemporaryFile>& temp) :
temp_(temp),
archiveSize_(0)
{
file_.open(temp_->GetPath(), std::ofstream::out | std::ofstream::binary);
if (!file_.good())
{
throw OrthancException(ErrorCode_CannotWriteFile);
}
}
virtual uint64_t GetArchiveSize() const ORTHANC_OVERRIDE
{
return archiveSize_;
}
virtual void Write(const std::string& chunk) ORTHANC_OVERRIDE
{
if (!chunk.empty())
{
try
{
file_.write(chunk.c_str(), chunk.size());
if (!file_.good())
{
file_.close();
throw OrthancException(ErrorCode_CannotWriteFile);
}
}
catch (boost::filesystem::filesystem_error&)
{
throw OrthancException(ErrorCode_CannotWriteFile);
}
catch (...) // To catch "std::system_error&" in C++11
{
throw OrthancException(ErrorCode_CannotWriteFile);
}
}
archiveSize_ += chunk.size();
}
virtual void Close() ORTHANC_OVERRIDE
{
try
{
file_.close();
}
catch (boost::filesystem::filesystem_error&)
{
throw OrthancException(ErrorCode_CannotWriteFile);
}
catch (...) // To catch "std::system_error&" in C++11
{
throw OrthancException(ErrorCode_CannotWriteFile);
}
}
};
}
static void SubmitJob(RestApiOutput& output,
ServerContext& context,
std::unique_ptr<ArchiveJob>& job,
int priority,
bool synchronous,
const std::string& filename)
{
if (job.get() == NULL)
{
throw OrthancException(ErrorCode_NullPointer);
}
job->SetDescription("REST API");
if (synchronous)
{
bool streaming;
{
OrthancConfiguration::ReaderLock lock;
streaming = lock.GetConfiguration().GetBooleanParameter("SynchronousZipStream", true); // New in Orthanc 1.9.4
}
if (streaming)
{
LOG(INFO) << "Streaming a ZIP archive";
boost::shared_ptr<SharedMessageQueue> queue(new SharedMessageQueue);
job->AcquireSynchronousTarget(new SynchronousZipStream(queue));
std::string jobId;
context.GetJobsEngine().GetRegistry().Submit(jobId, job.release(), priority);
SynchronousZipSender sender(context, jobId, queue, filename);
output.AnswerWithoutBuffering(sender);
// If we reach this line, this means that
// "SynchronousZipSender::ReadNextChunk()" has returned "false"
}
else
{
// This was the only behavior in Orthanc <= 1.9.3
LOG(INFO) << "Not streaming a ZIP archive (use of a temporary file)";
boost::shared_ptr<TemporaryFile> tmp;
{
OrthancConfiguration::ReaderLock lock;
tmp.reset(lock.GetConfiguration().CreateTemporaryFile());
}
job->AcquireSynchronousTarget(new SynchronousTemporaryStream(tmp));
Json::Value publicContent;
context.GetJobsEngine().GetRegistry().SubmitAndWait
(publicContent, job.release(), priority);
{
// The archive is now created: Prepare the sending of the ZIP file
FilesystemHttpSender sender(tmp->GetPath(), MimeType_Zip);
sender.SetContentFilename(filename);
// Send the ZIP
output.AnswerStream(sender);
}
}
}
else
{
job->SetFilename(filename);
OrthancRestApi::SubmitGenericJob(output, context, job.release(), false, priority);
}
}
static void DocumentPostArguments(RestApiPostCall& call,
bool isMedia,
bool defaultExtended)
{
call.GetDocumentation()
.SetRequestField("Synchronous", RestApiCallDocumentation::Type_Boolean,
"If `true`, create the archive in synchronous mode, which means that the HTTP answer will directly "
"contain the ZIP file. This is the default, easy behavior. However, if global configuration option "
"\"SynchronousZipStream\" is set to \"false\", asynchronous transfers should be preferred for "
"large amount of data, as the creation of the temporary file might lead to network timeouts.", false)
.SetRequestField("Asynchronous", RestApiCallDocumentation::Type_Boolean,
"If `true`, create the archive in asynchronous mode, which means that a job is submitted to create "
"the archive in background.", false)
.SetRequestField(KEY_TRANSCODE, RestApiCallDocumentation::Type_String,
"If present, the DICOM files in the archive will be transcoded to the provided "
"transfer syntax: https://orthanc.uclouvain.be/book/faq/transcoding.html", false)
.SetRequestField(KEY_LOSSY_QUALITY, RestApiCallDocumentation::Type_Number,
"If transcoding to a lossy transfer syntax, this entry defines the quality "
"as an integer between 1 and 100. If not provided, the value is defined "
"by the \"DicomLossyTranscodingQuality\" configuration. (new in v1.12.7)", false)
.SetRequestField(KEY_FILENAME, RestApiCallDocumentation::Type_String,
"Filename to set in the \"Content-Disposition\" HTTP header "
"(including file extension)", false)
.SetRequestField("Priority", RestApiCallDocumentation::Type_Number,
"In asynchronous mode, the priority of the job. The higher the value, the higher the priority.", false)
.AddAnswerType(MimeType_Zip, "In synchronous mode, the ZIP file containing the archive")
.AddAnswerType(MimeType_Json, "In asynchronous mode, information about the job that has been submitted to "
"generate the archive: https://orthanc.uclouvain.be/book/users/advanced-rest.html#jobs")
.SetAnswerField("ID", RestApiCallDocumentation::Type_String, "Identifier of the job")
.SetAnswerField("Path", RestApiCallDocumentation::Type_String, "Path to access the job in the REST API");
if (isMedia)
{
call.GetDocumentation().SetRequestField(
KEY_EXTENDED, RestApiCallDocumentation::Type_Boolean, "If `true`, will include additional "
"tags such as `SeriesDescription`, leading to a so-called *extended DICOMDIR*. Default value is " +
std::string(defaultExtended ? "`true`" : "`false`") + ".", false);
}
}
template <bool IS_MEDIA,
bool DEFAULT_IS_EXTENDED /* only makes sense for media (i.e. not ZIP archives) */ >
static void CreateBatchPost(RestApiPostCall& call)
{
if (call.IsDocumentation())
{
DocumentPostArguments(call, IS_MEDIA, DEFAULT_IS_EXTENDED);
std::string m = (IS_MEDIA ? "DICOMDIR media" : "ZIP archive");
call.GetDocumentation()
.SetTag("System")
.SetSummary("Create " + m)
.SetDescription("Create a " + m + " containing the DICOM resources (patients, studies, series, or instances) "
"whose Orthanc identifiers are provided in the body")
.SetRequestField(KEY_RESOURCES, RestApiCallDocumentation::Type_JsonListOfStrings,
"The list of Orthanc identifiers of interest.", false);
return;
}
ServerContext& context = OrthancRestApi::GetContext(call);
Json::Value body;
if (call.ParseJsonRequest(body))
{
bool synchronous, extended, transcode;
DicomTransferSyntax transferSyntax;
int priority;
unsigned int loaderThreads;
std::string filename;
unsigned int lossyQuality;
GetJobParameters(synchronous, extended, transcode, transferSyntax, lossyQuality,
priority, loaderThreads, filename, body, DEFAULT_IS_EXTENDED, "Archive.zip");
std::unique_ptr<ArchiveJob> job(new ArchiveJob(context, IS_MEDIA, extended, ResourceType_Patient));
AddResourcesOfInterest(*job, body);
if (transcode)
{
job->SetTranscode(transferSyntax);
job->SetLossyQuality(lossyQuality);
}
job->SetLoaderThreads(loaderThreads);
SubmitJob(call.GetOutput(), context, job, priority, synchronous, filename);
}
else
{
throw OrthancException(ErrorCode_BadFileFormat,
"Expected a list of resources to archive in the body");
}
}
static unsigned int GetLossyQuality(const RestApiGetCall& call)
{
unsigned int lossyQuality;
OrthancConfiguration::ReaderLock lock;
lossyQuality = lock.GetConfiguration().GetDicomLossyTranscodingQuality();
lossyQuality = call.GetUnsignedInteger32Argument(GET_LOSSY_QUALITY, lossyQuality);
return lossyQuality;
}
template <bool IS_MEDIA,
bool DEFAULT_IS_EXTENDED /* only makes sense for media (i.e. not ZIP archives) */ >
static void CreateBatchGet(RestApiGetCall& call)
{
if (call.IsDocumentation())
{
std::string m = (IS_MEDIA ? "DICOMDIR media" : "ZIP archive");
call.GetDocumentation()
.SetTag("System")
.SetSummary("Create " + m)
.SetDescription("Create a " + m + " containing the DICOM resources (patients, studies, series, or instances) "
"whose Orthanc identifiers are provided in the 'resources' argument")
.SetHttpGetArgument(GET_FILENAME, RestApiCallDocumentation::Type_String,
"Filename to set in the \"Content-Disposition\" HTTP header "
"(including file extension)", false)
.SetHttpGetArgument(GET_TRANSCODE, RestApiCallDocumentation::Type_String,
"If present, the DICOM files will be transcoded to the provided "
"transfer syntax: https://orthanc.uclouvain.be/book/faq/transcoding.html", false)
.SetHttpGetArgument(GET_LOSSY_QUALITY, RestApiCallDocumentation::Type_Number,
"If transcoding to a lossy transfer syntax, this entry defines the quality "
"as an integer between 1 and 100. If not provided, the value is defined "
"by the \"DicomLossyTranscodingQuality\" configuration. (new in v1.12.7)", false)
.SetHttpGetArgument(GET_RESOURCES, RestApiCallDocumentation::Type_String,
"A comma separated list of Orthanc resource identifiers to include in the " + m + ".", true);
return;
}
ServerContext& context = OrthancRestApi::GetContext(call);
if (!call.HasArgument(GET_RESOURCES))
{
throw OrthancException(Orthanc::ErrorCode_BadRequest, std::string("Missing ") + GET_RESOURCES + " argument");
}
std::unique_ptr<ArchiveJob> job(new ArchiveJob(context, IS_MEDIA, DEFAULT_IS_EXTENDED, ResourceType_Patient));
AddResourcesOfInterestFromString(*job, call.GetArgument(GET_RESOURCES, ""));
if (call.HasArgument(GET_TRANSCODE))
{
job->SetTranscode(GetTransferSyntax(call.GetArgument(GET_TRANSCODE, "")));
job->SetLossyQuality(GetLossyQuality(call));
}
const std::string filename = call.GetArgument(GET_FILENAME, "Archive.zip"); // New in Orthanc 1.12.7
SubmitJob(call.GetOutput(), context, job, 0, true, filename);
}
template <ResourceType LEVEL,
bool IS_MEDIA>
static void CreateSingleGet(RestApiGetCall& call)
{
if (call.IsDocumentation())
{
ResourceType t = StringToResourceType(call.GetFullUri()[0].c_str());
std::string r = GetResourceTypeText(t, false /* plural */, false /* upper case */);
std::string m = (IS_MEDIA ? "DICOMDIR media" : "ZIP archive");
call.GetDocumentation()
.SetTag(GetResourceTypeText(t, true /* plural */, true /* upper case */))
.SetSummary("Create " + m)
.SetDescription("Synchronously create a " + m + " containing the DICOM " + r +
" whose Orthanc identifier is provided in the URL. This flavor is synchronous, "
"which might *not* be desirable to archive large amount of data, as it might "
"lead to network timeouts. Prefer the asynchronous version using `POST` method.")
.SetUriArgument("id", "Orthanc identifier of the " + r + " of interest")
.SetHttpGetArgument(GET_FILENAME, RestApiCallDocumentation::Type_String,
"Filename to set in the \"Content-Disposition\" HTTP header "
"(including file extension)", false)
.SetHttpGetArgument(GET_TRANSCODE, RestApiCallDocumentation::Type_String,
"If present, the DICOM files in the archive will be transcoded to the provided "
"transfer syntax: https://orthanc.uclouvain.be/book/faq/transcoding.html", false)
.SetHttpGetArgument(GET_LOSSY_QUALITY, RestApiCallDocumentation::Type_Number,
"If transcoding to a lossy transfer syntax, this entry defines the quality "
"as an integer between 1 and 100. If not provided, the value is defined "
"by the \"DicomLossyTranscodingQuality\" configuration. (new in v1.12.7)", false)
.AddAnswerType(MimeType_Zip, "ZIP file containing the archive");
if (IS_MEDIA)
{
call.GetDocumentation().SetHttpGetArgument(
"extended", RestApiCallDocumentation::Type_String,
"If present, will include additional tags such as `SeriesDescription`, leading to a so-called *extended DICOMDIR*", false);
}
return;
}
ServerContext& context = OrthancRestApi::GetContext(call);
const std::string id = call.GetUriComponent("id", "");
const std::string filename = call.GetArgument(GET_FILENAME, id + ".zip"); // New in Orthanc 1.11.0
bool extended;
if (IS_MEDIA)
{
extended = call.HasArgument("extended");
}
else
{
extended = false;
}
std::unique_ptr<ArchiveJob> job(new ArchiveJob(context, IS_MEDIA, extended, (LEVEL == ResourceType_Patient ? ResourceType_Patient : ResourceType_Study))); // use patient info from study except when exporting a patient
job->AddResource(id, true, LEVEL);
if (call.HasArgument(GET_TRANSCODE))
{
job->SetTranscode(GetTransferSyntax(call.GetArgument(GET_TRANSCODE, "")));
job->SetLossyQuality(GetLossyQuality(call));
}
{
OrthancConfiguration::ReaderLock lock;
unsigned int loaderThreads = lock.GetConfiguration().GetUnsignedIntegerParameter(CONFIG_LOADER_THREADS, 0); // New in Orthanc 1.10.0
job->SetLoaderThreads(loaderThreads);
}
SubmitJob(call.GetOutput(), context, job, 0 /* priority */,
true /* synchronous */, filename);
}
template <ResourceType LEVEL,
bool IS_MEDIA>
static void CreateSinglePost(RestApiPostCall& call)
{
if (call.IsDocumentation())
{
DocumentPostArguments(call, IS_MEDIA, false /* not extended by default */);
ResourceType t = StringToResourceType(call.GetFullUri()[0].c_str());
std::string r = GetResourceTypeText(t, false /* plural */, false /* upper case */);
std::string m = (IS_MEDIA ? "DICOMDIR media" : "ZIP archive");
call.GetDocumentation()
.SetTag(GetResourceTypeText(t, true /* plural */, true /* upper case */))
.SetSummary("Create " + m)
.SetDescription("Create a " + m + " containing the DICOM " + r +
" whose Orthanc identifier is provided in the URL")
.SetUriArgument("id", "Orthanc identifier of the " + r + " of interest");
return;
}
ServerContext& context = OrthancRestApi::GetContext(call);
std::string id = call.GetUriComponent("id", "");
Json::Value body;
if (call.ParseJsonRequest(body))
{
bool synchronous, extended, transcode;
DicomTransferSyntax transferSyntax;
int priority;
unsigned int loaderThreads;
std::string filename;
unsigned int lossyQuality;
GetJobParameters(synchronous, extended, transcode, transferSyntax, lossyQuality,
priority, loaderThreads, filename, body, false /* by default, not extented */, id + ".zip");
std::unique_ptr<ArchiveJob> job(new ArchiveJob(context, IS_MEDIA, extended, LEVEL));
job->AddResource(id, true, LEVEL);
if (transcode)
{
job->SetTranscode(transferSyntax);
job->SetLossyQuality(lossyQuality);
}
job->SetLoaderThreads(loaderThreads);
SubmitJob(call.GetOutput(), context, job, priority, synchronous, filename);
}
else
{
throw OrthancException(ErrorCode_BadFileFormat);
}
}
void OrthancRestApi::RegisterArchive()
{
Register("/patients/{id}/archive", CreateSingleGet<ResourceType_Patient, false /* ZIP */>);
Register("/patients/{id}/archive", CreateSinglePost<ResourceType_Patient, false /* ZIP */>);
Register("/patients/{id}/media", CreateSingleGet<ResourceType_Patient, true /* media */>);
Register("/patients/{id}/media", CreateSinglePost<ResourceType_Patient, true /* media */>);
Register("/series/{id}/archive", CreateSingleGet<ResourceType_Series, false /* ZIP */>);
Register("/series/{id}/archive", CreateSinglePost<ResourceType_Series, false /* ZIP */>);
Register("/series/{id}/media", CreateSingleGet<ResourceType_Series, true /* media */>);
Register("/series/{id}/media", CreateSinglePost<ResourceType_Series, true /* media */>);
Register("/studies/{id}/archive", CreateSingleGet<ResourceType_Study, false /* ZIP */>);
Register("/studies/{id}/archive", CreateSinglePost<ResourceType_Study, false /* ZIP */>);
Register("/studies/{id}/media", CreateSingleGet<ResourceType_Study, true /* media */>);
Register("/studies/{id}/media", CreateSinglePost<ResourceType_Study, true /* media */>);
Register("/tools/create-archive",
CreateBatchPost<false /* ZIP */, false /* extended makes no sense in ZIP */>);
Register("/tools/create-media",
CreateBatchPost<true /* media */, false /* not extended by default */>);
Register("/tools/create-media-extended",
CreateBatchPost<true /* media */, true /* extended by default */>);
Register("/tools/create-archive",
CreateBatchGet<false /* ZIP */, false /* extended makes no sense in ZIP */>);
Register("/tools/create-media",
CreateBatchGet<true /* media */, false /* not extended by default */>);
Register("/tools/create-media-extended",
CreateBatchGet<true /* media */, true /* extended by default */>);
}
}