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);