Mercurial > hg > orthanc
changeset 6000:a791ba035e39 default tip
new filename args in some API route
author | Alain Mazy <am@orthanc.team> |
---|---|
date | Wed, 12 Feb 2025 14:34:05 +0100 (10 hours ago) |
parents | c2fd0249996b |
children | |
files | NEWS OrthancFramework/Sources/FileStorage/StorageAccessor.cpp OrthancFramework/Sources/FileStorage/StorageAccessor.h OrthancServer/Sources/OrthancRestApi/OrthancRestArchive.cpp OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp OrthancServer/Sources/ServerContext.cpp OrthancServer/Sources/ServerContext.h OrthancServer/Sources/ServerJobs/ArchiveJob.cpp OrthancServer/Sources/ServerJobs/ArchiveJob.h |
diffstat | 9 files changed, 132 insertions(+), 48 deletions(-) [+] |
line wrap: on
line diff
--- a/NEWS Mon Feb 10 12:27:17 2025 +0100 +++ b/NEWS Wed Feb 12 14:34:05 2025 +0100 @@ -1,6 +1,17 @@ Pending changes in the mainline =============================== +REST API +-------- + +* API version upgraded to 28 +* GET /studies/../archive and sibbling routes now all accept a 'filename' GET argument. +* POST /studies/../archive and sibbling routes now all accept a 'Filename' query argument. +* GET /instances/../file and sibbling ../attachments/../data routes now all accept a 'filename' GET argument. + + + + Maintenance -----------
--- a/OrthancFramework/Sources/FileStorage/StorageAccessor.cpp Mon Feb 10 12:27:17 2025 +0100 +++ b/OrthancFramework/Sources/FileStorage/StorageAccessor.cpp Wed Feb 12 14:34:05 2025 +0100 @@ -736,9 +736,10 @@ #if ORTHANC_ENABLE_CIVETWEB == 1 || ORTHANC_ENABLE_MONGOOSE == 1 void StorageAccessor::AnswerFile(HttpOutput& output, const FileInfo& info, - MimeType mime) + MimeType mime, + const std::string& contentFilename) { - AnswerFile(output, info, EnumerationToString(mime)); + AnswerFile(output, info, EnumerationToString(mime), contentFilename); } #endif @@ -746,10 +747,12 @@ #if ORTHANC_ENABLE_CIVETWEB == 1 || ORTHANC_ENABLE_MONGOOSE == 1 void StorageAccessor::AnswerFile(HttpOutput& output, const FileInfo& info, - const std::string& mime) + const std::string& mime, + const std::string& contentFilename) { BufferHttpSender sender; SetupSender(sender, info, mime); + sender.SetContentFilename(contentFilename); HttpStreamTranscoder transcoder(sender, CompressionType_None); // since 1.11.2, the storage accessor only returns uncompressed buffers output.Answer(transcoder); @@ -760,9 +763,10 @@ #if ORTHANC_ENABLE_CIVETWEB == 1 || ORTHANC_ENABLE_MONGOOSE == 1 void StorageAccessor::AnswerFile(RestApiOutput& output, const FileInfo& info, - MimeType mime) + MimeType mime, + const std::string& contentFilename) { - AnswerFile(output, info, EnumerationToString(mime)); + AnswerFile(output, info, EnumerationToString(mime), contentFilename); } #endif @@ -770,11 +774,13 @@ #if ORTHANC_ENABLE_CIVETWEB == 1 || ORTHANC_ENABLE_MONGOOSE == 1 void StorageAccessor::AnswerFile(RestApiOutput& output, const FileInfo& info, - const std::string& mime) + const std::string& mime, + const std::string& contentFilename) { BufferHttpSender sender; SetupSender(sender, info, mime); - + sender.SetContentFilename(contentFilename); + HttpStreamTranscoder transcoder(sender, CompressionType_None); // since 1.11.2, the storage accessor only returns uncompressed buffers output.AnswerStream(transcoder); }
--- a/OrthancFramework/Sources/FileStorage/StorageAccessor.h Mon Feb 10 12:27:17 2025 +0100 +++ b/OrthancFramework/Sources/FileStorage/StorageAccessor.h Wed Feb 12 14:34:05 2025 +0100 @@ -167,19 +167,23 @@ #if ORTHANC_ENABLE_CIVETWEB == 1 || ORTHANC_ENABLE_MONGOOSE == 1 void AnswerFile(HttpOutput& output, const FileInfo& info, - MimeType mime); + MimeType mime, + const std::string& contentFilename); void AnswerFile(HttpOutput& output, const FileInfo& info, - const std::string& mime); + const std::string& mime, + const std::string& contentFilename); void AnswerFile(RestApiOutput& output, const FileInfo& info, - MimeType mime); + MimeType mime, + const std::string& contentFilename); void AnswerFile(RestApiOutput& output, const FileInfo& info, - const std::string& mime); + const std::string& mime, + const std::string& contentFilename); #endif private: void ReadStartRangeInternal(std::string& target,
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestArchive.cpp Mon Feb 10 12:27:17 2025 +0100 +++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestArchive.cpp Wed Feb 12 14:34:05 2025 +0100 @@ -42,6 +42,11 @@ 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_FILENAME = "Filename"; + + static const char* const GET_TRANSCODE = "transcode"; + static const char* const GET_FILENAME = "filename"; + static const char* const GET_RESOURCES = "resources"; static const char* const CONFIG_LOADER_THREADS = "ZipLoaderThreads"; @@ -115,8 +120,10 @@ DicomTransferSyntax& syntax, /* out */ int& priority, /* out */ unsigned int& loaderThreads, /* out */ + std::string& filename, /* out */ const Json::Value& body, /* in */ - const bool defaultExtended /* in */) + const bool defaultExtended /* in */, + const std::string& defaultFilename /* in */) { synchronous = OrthancRestApi::IsSynchronousJobRequest (true /* synchronous by default */, body); @@ -144,6 +151,16 @@ 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 @@ -487,6 +504,7 @@ } else { + job->SetFilename(filename); OrthancRestApi::SubmitGenericJob(output, context, job.release(), false, priority); } } @@ -508,6 +526,9 @@ .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_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") @@ -539,8 +560,11 @@ .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("Resources", RestApiCallDocumentation::Type_JsonListOfStrings, - "The list of Orthanc identifiers of interest.", false); + .SetRequestField(KEY_RESOURCES, RestApiCallDocumentation::Type_JsonListOfStrings, + "The list of Orthanc identifiers of interest.", false) + .SetRequestField(KEY_FILENAME, RestApiCallDocumentation::Type_String, + "Filename to set in the \"Content-Disposition\" HTTP header " + "(including file extension)", false); return; } @@ -553,8 +577,9 @@ DicomTransferSyntax transferSyntax; int priority; unsigned int loaderThreads; + std::string filename; GetJobParameters(synchronous, extended, transcode, transferSyntax, - priority, loaderThreads, body, DEFAULT_IS_EXTENDED); + 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); @@ -566,7 +591,7 @@ job->SetLoaderThreads(loaderThreads); - SubmitJob(call.GetOutput(), context, job, priority, synchronous, "Archive.zip"); + SubmitJob(call.GetOutput(), context, job, priority, synchronous, filename); } else { @@ -580,9 +605,6 @@ bool DEFAULT_IS_EXTENDED /* only makes sense for media (i.e. not ZIP archives) */ > static void CreateBatchGet(RestApiGetCall& call) { - static const char* const TRANSCODE = "transcode"; - static const char* const RESOURCES = "resources"; - if (call.IsDocumentation()) { std::string m = (IS_MEDIA ? "DICOMDIR media" : "ZIP archive"); @@ -591,10 +613,13 @@ .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(TRANSCODE, RestApiCallDocumentation::Type_String, + .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(RESOURCES, RestApiCallDocumentation::Type_String, + .SetHttpGetArgument(GET_RESOURCES, RestApiCallDocumentation::Type_String, "A comma separated list of Orthanc resource identifiers to include in the " + m + ".", true); return; } @@ -603,26 +628,28 @@ bool transcode = false; DicomTransferSyntax transferSyntax = DicomTransferSyntax_LittleEndianImplicit; // Initialize variable to avoid warnings - if (call.HasArgument(TRANSCODE)) + if (call.HasArgument(GET_TRANSCODE)) { transcode = true; - transferSyntax = GetTransferSyntax(call.GetArgument(TRANSCODE, "")); + transferSyntax = GetTransferSyntax(call.GetArgument(GET_TRANSCODE, "")); } - if (!call.HasArgument(RESOURCES)) + if (!call.HasArgument(GET_RESOURCES)) { - throw OrthancException(Orthanc::ErrorCode_BadRequest, std::string("Missing ") + RESOURCES + " argument"); + 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(RESOURCES, "")); + AddResourcesOfInterestFromString(*job, call.GetArgument(GET_RESOURCES, "")); if (transcode) { job->SetTranscode(transferSyntax); } - SubmitJob(call.GetOutput(), context, job, 0, true, "Archive.zip"); + const std::string filename = call.GetArgument(GET_FILENAME, "Archive.zip"); // New in Orthanc 1.12.7 + + SubmitJob(call.GetOutput(), context, job, 0, true, filename); } @@ -630,8 +657,6 @@ bool IS_MEDIA> static void CreateSingleGet(RestApiGetCall& call) { - static const char* const TRANSCODE = "transcode"; - static const char* const FILENAME = "filename"; if (call.IsDocumentation()) { @@ -646,10 +671,10 @@ "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(FILENAME, RestApiCallDocumentation::Type_String, + .SetHttpGetArgument(GET_FILENAME, RestApiCallDocumentation::Type_String, "Filename to set in the \"Content-Disposition\" HTTP header " "(including file extension)", false) - .SetHttpGetArgument(TRANSCODE, RestApiCallDocumentation::Type_String, + .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) .AddAnswerType(MimeType_Zip, "ZIP file containing the archive"); @@ -665,7 +690,7 @@ ServerContext& context = OrthancRestApi::GetContext(call); const std::string id = call.GetUriComponent("id", ""); - const std::string filename = call.GetArgument(FILENAME, id + ".zip"); // New in Orthanc 1.11.0 + const std::string filename = call.GetArgument(GET_FILENAME, id + ".zip"); // New in Orthanc 1.11.0 bool extended; if (IS_MEDIA) @@ -680,9 +705,9 @@ 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(TRANSCODE)) + if (call.HasArgument(GET_TRANSCODE)) { - job->SetTranscode(GetTransferSyntax(call.GetArgument(TRANSCODE, ""))); + job->SetTranscode(GetTransferSyntax(call.GetArgument(GET_TRANSCODE, ""))); } { @@ -726,8 +751,9 @@ DicomTransferSyntax transferSyntax; int priority; unsigned int loaderThreads; + std::string filename; GetJobParameters(synchronous, extended, transcode, transferSyntax, - priority, loaderThreads, body, false /* by default, not extented */); + 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); @@ -739,7 +765,7 @@ job->SetLoaderThreads(loaderThreads); - SubmitJob(call.GetOutput(), context, job, priority, synchronous, id + ".zip"); + SubmitJob(call.GetOutput(), context, job, priority, synchronous, filename); } else {
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp Mon Feb 10 12:27:17 2025 +0100 +++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp Wed Feb 12 14:34:05 2025 +0100 @@ -338,7 +338,8 @@ static void GetInstanceFile(RestApiGetCall& call) { - static const char* const TRANSCODE = "transcode"; + static const char* const GET_TRANSCODE = "transcode"; + static const char* const GET_FILENAME = "filename"; if (call.IsDocumentation()) { @@ -348,9 +349,12 @@ .SetDescription("Download one DICOM instance") .SetUriArgument("id", "Orthanc identifier of the DICOM instance of interest") .SetHttpHeader("Accept", "This HTTP header can be set to retrieve the DICOM instance in DICOMweb format") - .SetHttpGetArgument(TRANSCODE, RestApiCallDocumentation::Type_String, + .SetHttpGetArgument(GET_TRANSCODE, RestApiCallDocumentation::Type_String, "If present, the DICOM file will be transcoded to the provided " "transfer syntax: https://orthanc.uclouvain.be/book/faq/transcoding.html", false) + .SetHttpGetArgument(GET_FILENAME, RestApiCallDocumentation::Type_String, + "Filename to set in the \"Content-Disposition\" HTTP header " + "(including file extension)", false) .AddAnswerType(MimeType_Dicom, "The DICOM instance") .AddAnswerType(MimeType_DicomWebJson, "The DICOM instance, in DICOMweb JSON format") .AddAnswerType(MimeType_DicomWebXml, "The DICOM instance, in DICOMweb XML format"); @@ -399,15 +403,18 @@ } } - if (call.HasArgument(TRANSCODE)) + const std::string filename = call.GetArgument(GET_FILENAME, publicId + ".dcm"); // New in Orthanc 1.12.7 + + if (call.HasArgument(GET_TRANSCODE)) { std::string source; std::string attachmentId; std::string transcoded; context.ReadDicom(source, attachmentId, publicId); - if (context.TranscodeWithCache(transcoded, source, publicId, attachmentId, GetTransferSyntax(call.GetArgument(TRANSCODE, "")))) + if (context.TranscodeWithCache(transcoded, source, publicId, attachmentId, GetTransferSyntax(call.GetArgument(GET_TRANSCODE, "")))) { + call.GetOutput().SetContentFilename(filename.c_str()); call.GetOutput().AnswerBuffer(transcoded, MimeType_Dicom); } } @@ -422,7 +429,7 @@ } else { - context.AnswerAttachment(call.GetOutput(), info); + context.AnswerAttachment(call.GetOutput(), info, filename); } } } @@ -2298,6 +2305,8 @@ { const ResourceType level = GetResourceTypeFromUri(call); + static const char* const GET_FILENAME = "filename"; + if (call.IsDocumentation()) { std::string r = GetResourceTypeText(level, false /* plural */, false /* upper case */); @@ -2308,11 +2317,15 @@ std::string(uncompress ? "" : ". The attachment will not be decompressed if `StorageCompression` is `true`.")) .SetUriArgument("id", "Orthanc identifier of the " + r + " of interest") .SetUriArgument("name", "The name of the attachment, or its index (cf. `UserContentType` configuration option)") + .SetHttpGetArgument(GET_FILENAME, RestApiCallDocumentation::Type_String, + "Filename to set in the \"Content-Disposition\" HTTP header " + "(including file extension)", false) .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 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; + + return; } ServerContext& context = OrthancRestApi::GetContext(call); @@ -2343,6 +2356,8 @@ return; } + const std::string filename = call.GetArgument(GET_FILENAME, info.GetUuid()); // New in Orthanc 1.12.7 + if (hasRangeHeader) { std::string fragment; @@ -2356,7 +2371,7 @@ else if (uncompress || info.GetCompressionType() == CompressionType_None) { - context.AnswerAttachment(call.GetOutput(), info); + context.AnswerAttachment(call.GetOutput(), info, filename); } else {
--- a/OrthancServer/Sources/ServerContext.cpp Mon Feb 10 12:27:17 2025 +0100 +++ b/OrthancServer/Sources/ServerContext.cpp Wed Feb 12 14:34:05 2025 +0100 @@ -985,10 +985,11 @@ void ServerContext::AnswerAttachment(RestApiOutput& output, - const FileInfo& attachment) + const FileInfo& attachment, + const std::string& filename) { StorageAccessor accessor(area_, storageCache_, GetMetricsRegistry()); - accessor.AnswerFile(output, attachment, GetFileContentMime(attachment.GetContentType())); + accessor.AnswerFile(output, attachment, GetFileContentMime(attachment.GetContentType()), filename); }
--- a/OrthancServer/Sources/ServerContext.h Mon Feb 10 12:27:17 2025 +0100 +++ b/OrthancServer/Sources/ServerContext.h Wed Feb 12 14:34:05 2025 +0100 @@ -361,7 +361,8 @@ bool isReconstruct = false); void AnswerAttachment(RestApiOutput& output, - const FileInfo& fileInfo); + const FileInfo& fileInfo, + const std::string& filename); void ChangeAttachmentCompression(ResourceType level, const std::string& resourceId,
--- a/OrthancServer/Sources/ServerJobs/ArchiveJob.cpp Mon Feb 10 12:27:17 2025 +0100 +++ b/OrthancServer/Sources/ServerJobs/ArchiveJob.cpp Wed Feb 12 14:34:05 2025 +0100 @@ -1243,6 +1243,7 @@ archive_(new ArchiveIndex(GetArchiveResourceType(jobLevel))), // get patient Info from this level isMedia_(isMedia), enableExtendedSopClass_(enableExtendedSopClass), + filename_("archive.zip"), currentStep_(0), instancesCount_(0), uncompressedSize_(0), @@ -1296,7 +1297,18 @@ } } - + void ArchiveJob::SetFilename(const std::string& filename) + { + if (writer_.get() != NULL) // Already started + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + filename_ = filename; + } + } + void ArchiveJob::AddResource(const std::string& publicId, bool mustExist, ResourceType expectedType) @@ -1582,7 +1594,7 @@ const DynamicTemporaryFile& f = dynamic_cast<DynamicTemporaryFile&>(accessor.GetItem()); f.GetFile().Read(output); mime = MimeType_Zip; - filename = "archive.zip"; + filename = filename_; return true; } else
--- a/OrthancServer/Sources/ServerJobs/ArchiveJob.h Mon Feb 10 12:27:17 2025 +0100 +++ b/OrthancServer/Sources/ServerJobs/ArchiveJob.h Wed Feb 12 14:34:05 2025 +0100 @@ -57,6 +57,7 @@ bool isMedia_; bool enableExtendedSopClass_; std::string description_; + std::string filename_; boost::shared_ptr<ZipWriterIterator> writer_; size_t currentStep_; @@ -91,6 +92,13 @@ return description_; } + void SetFilename(const std::string& filename); + + const std::string& GetFilename() const + { + return filename_; + } + void AddResource(const std::string& publicId, bool mustExist, ResourceType expectedType);