changeset 5811:a451777236fb attach-custom-data

advanced storage: fix + customizable path
author Alain Mazy <am@orthanc.team>
date Tue, 24 Sep 2024 17:52:45 +0200
parents 1f6642c04541
children 3f7985dc2df4
files OrthancServer/CMakeLists.txt OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.h OrthancServer/Plugins/Samples/AdvancedStorage/Plugin.cpp
diffstat 3 files changed, 224 insertions(+), 62 deletions(-) [+]
line wrap: on
line diff
--- a/OrthancServer/CMakeLists.txt	Tue Sep 24 15:15:51 2024 +0200
+++ b/OrthancServer/CMakeLists.txt	Tue Sep 24 17:52:45 2024 +0200
@@ -825,6 +825,14 @@
     list(APPEND ADVANCED_STORAGE_RESOURCES ${AUTOGENERATED_DIR}/AdvancedStorage.rc)
   endif()
 
+  EmbedResources(
+    --target=AdvancedStorageDicomResources
+    --namespace=Orthanc.FrameworkResources
+    --framework-path=${CMAKE_SOURCE_DIR}/../OrthancFramework/Sources
+    ${LIBICU_RESOURCES}
+    ${DCMTK_DICTIONARIES}
+    )
+
   set_source_files_properties(
     ${CMAKE_SOURCE_DIR}/Plugins/Samples/AdvancedStorage/Plugin.cpp
     PROPERTIES COMPILE_DEFINITIONS "ADVANCED_STORAGE_VERSION=\"${ORTHANC_VERSION}\""
@@ -832,13 +840,9 @@
 
   add_library(AdvancedStorage SHARED 
     ${CMAKE_SOURCE_DIR}/Plugins/Samples/AdvancedStorage/Plugin.cpp
-    ${ORTHANC}
-    ${CMAKE_SOURCE_DIR}/../OrthancFramework/Sources/OrthancException.cpp
-    ${CMAKE_SOURCE_DIR}/../OrthancFramework/Sources/SystemToolbox.cpp
-    ${CMAKE_SOURCE_DIR}/../OrthancFramework/Sources/Toolbox.cpp
-    ${CMAKE_SOURCE_DIR}/../OrthancFramework/Sources/Logging.cpp
-    ${CMAKE_SOURCE_DIR}/../OrthancFramework/Sources/ChunkedBuffer.cpp
-    ${CMAKE_SOURCE_DIR}/../OrthancFramework/Sources/Enumerations.cpp
+    ${CMAKE_SOURCE_DIR}/Plugins/Samples/AdvancedStorage/OrthancFrameworkDependencies.cpp
+    ${AUTOGENERATED_DIR}/AdvancedStorageDicomResources.cpp
+    ${ADVANCED_STORAGE_RESOURCES}
     )
 
   DefineSourceBasenameForTarget(AdvancedStorage)
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.h	Tue Sep 24 15:15:51 2024 +0200
+++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.h	Tue Sep 24 17:52:45 2024 +0200
@@ -7,54 +7,95 @@
  * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
  *
  * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU Affero General Public License
- * as published by the Free Software Foundation, either version 3 of
- * the License, or (at your option) any later version.
- *
+ * 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
- * Affero General Public License for more details.
- * 
- * You should have received a copy of the GNU Affero General Public License
+ * 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/>.
  **/
 
 
-
 #pragma once
 
-#include "IndexBackend.h"
-
-
-#if defined(ORTHANC_PLUGINS_VERSION_IS_ABOVE)         // Macro introduced in Orthanc 1.3.1
-#  if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 12, 0)
+#if ORTHANC_ENABLE_PLUGINS == 1
 
-namespace OrthancDatabases
-{  
-  /**
-   * @brief Bridge between C and C++ database engines.
-   * 
-   * Class creating the bridge between the C low-level primitives for
-   * custom database engines, and the high-level IDatabaseBackend C++
-   * interface, through ProtocolBuffers for Orthanc >= 1.12.0.
-   **/
-  class DatabaseBackendAdapterV4
+#include "../../../OrthancFramework/Sources/SharedLibrary.h"
+#include "../../Sources/Database/IDatabaseWrapper.h"
+#include "../Include/orthanc/OrthancCPlugin.h"
+#include "PluginsErrorDictionary.h"
+
+namespace Orthanc
+{
+  class OrthancPluginDatabaseV4 : public IDatabaseWrapper
   {
   private:
-    // This class cannot be instantiated
-    DatabaseBackendAdapterV4()
-    {
-    }
+    class Transaction;
+
+    SharedLibrary&                          library_;
+    PluginsErrorDictionary&                 errorDictionary_;
+    _OrthancPluginRegisterDatabaseBackendV4 definition_;
+    std::string                             serverIdentifier_;
+    bool                                    open_;
+    unsigned int                            databaseVersion_;
+    IDatabaseWrapper::Capabilities          dbCapabilities_;
+
+    void CheckSuccess(OrthancPluginErrorCode code) const;
 
   public:
-    static void Register(IndexBackend* backend,
-                         size_t countConnections,
-                         unsigned int maxDatabaseRetries);
+    OrthancPluginDatabaseV4(SharedLibrary& library,
+                            PluginsErrorDictionary& errorDictionary,
+                            const _OrthancPluginRegisterDatabaseBackendV4& database,
+                            const std::string& serverIdentifier);
+
+    virtual ~OrthancPluginDatabaseV4();
+
+    const _OrthancPluginRegisterDatabaseBackendV4& GetDefinition() const
+    {
+      return definition_;
+    }
+
+    PluginsErrorDictionary& GetErrorDictionary() const
+    {
+      return errorDictionary_;
+    }
+
+    const std::string& GetServerIdentifier() const
+    {
+      return serverIdentifier_;
+    }
+    
+    virtual void Open() ORTHANC_OVERRIDE;
 
-    static void Finalize();
+    virtual void Close() ORTHANC_OVERRIDE;
+
+    const SharedLibrary& GetSharedLibrary() const
+    {
+      return library_;
+    }
+
+    virtual void FlushToDisk() ORTHANC_OVERRIDE;
+
+    virtual IDatabaseWrapper::ITransaction* StartTransaction(TransactionType type,
+                                                             IDatabaseListener& listener)
+      ORTHANC_OVERRIDE;
+
+    virtual unsigned int GetDatabaseVersion() ORTHANC_OVERRIDE;
+
+    virtual void Upgrade(unsigned int targetVersion,
+                         IStorageArea& storageArea) ORTHANC_OVERRIDE;    
+
+    virtual uint64_t MeasureLatency() ORTHANC_OVERRIDE;
+
+    virtual const Capabilities GetDatabaseCapabilities() const ORTHANC_OVERRIDE;
+
+    virtual bool HasIntegratedFind() const ORTHANC_OVERRIDE;
   };
 }
 
-#  endif
 #endif
--- a/OrthancServer/Plugins/Samples/AdvancedStorage/Plugin.cpp	Tue Sep 24 15:15:51 2024 +0200
+++ b/OrthancServer/Plugins/Samples/AdvancedStorage/Plugin.cpp	Tue Sep 24 17:52:45 2024 +0200
@@ -26,6 +26,7 @@
 #include "../../../../OrthancFramework/Sources/SystemToolbox.h"
 #include "../../../../OrthancFramework/Sources/Toolbox.h"
 #include "../../../../OrthancFramework/Sources/Logging.h"
+#include "../../../../OrthancFramework/Sources/DicomFormat/DicomInstanceHasher.h"
 #include "../Common/OrthancPluginCppWrapper.h"
 
 #include <boost/filesystem.hpp>
@@ -168,31 +169,60 @@
   return Orthanc::Toolbox::WriteFastJson(output, customDataJson);
 }
 
-void AddSplitDateDicomTagToPath(fs::path& path, const Json::Value& tags, const char* tagName, const char* defaultValue = NULL)
+// void AddSplitDateDicomTagToPath(fs::path& path, const Json::Value& tags, const char* tagName, const char* defaultValue = NULL)
+// {
+//   if (tags.isMember(tagName) && tags[tagName].asString().size() == 8)
+//   {
+//     std::string date = tags[tagName].asString();
+//     path /= date.substr(0, 4);
+//     path /= date.substr(4, 2);
+//     path /= date.substr(6, 2);
+//   }
+//   else if (defaultValue != NULL)
+//   {
+//     path /= defaultValue;
+//   }
+// }
+
+// void AddStringDicomTagToPath(fs::path& path, const Json::Value& tags, const char* tagName, const char* defaultValue = NULL)
+// {
+//   if (tags.isMember(tagName) && tags[tagName].isString() && tags[tagName].asString().size() > 0)
+//   {
+//     path /= tags[tagName].asString();
+//   }
+//   else if (defaultValue != NULL)
+//   {
+//     path /= defaultValue;
+//   }
+// }
+
+std::string GetSplitDateDicomTagToPath(const Json::Value& tags, const char* tagName, const char* defaultValue = NULL)
 {
   if (tags.isMember(tagName) && tags[tagName].asString().size() == 8)
   {
     std::string date = tags[tagName].asString();
-    path /= date.substr(0, 4);
-    path /= date.substr(4, 2);
-    path /= date.substr(6, 2);
+    return date.substr(0, 4) + "/" + date.substr(4, 2) + "/" + date.substr(6, 2);
   }
   else if (defaultValue != NULL)
   {
-    path /= defaultValue;
+    return defaultValue;
   }
+
+  return "";
 }
 
-void AddStringDicomTagToPath(fs::path& path, const Json::Value& tags, const char* tagName, const char* defaultValue = NULL)
+std::string GetStringDicomTagForPath(const Json::Value& tags, const char* tagName, const char* defaultValue = NULL)
 {
   if (tags.isMember(tagName) && tags[tagName].isString() && tags[tagName].asString().size() > 0)
   {
-    path /= tags[tagName].asString();
+    return tags[tagName].asString();
   }
   else if (defaultValue != NULL)
   {
-    path /= defaultValue;
+    return defaultValue;
   }
+  
+  return "";
 }
 
 void AddIntDicomTagToPath(fs::path& path, const Json::Value& tags, const char* tagName, size_t zeroPaddingWidth = 0, const char* defaultValue = NULL)
@@ -242,31 +272,112 @@
 fs::path GetRelativePathFromTags(const Json::Value& tags, const char* uuid, OrthancPluginContentType type, bool isCompressed)
 {
   fs::path path;
+  bool foundUuid = false;
 
   if (!tags.isNull())
   { 
-    if (namingScheme_ == "Preset1-StudyDatePatientID")
+    std::vector<std::string> folderNames;
+    Orthanc::Toolbox::SplitString(folderNames, namingScheme_, '/');
+
+    for (std::vector<std::string>::const_iterator it = folderNames.begin(); it != folderNames.end(); ++it)
     {
-      if (!tags.isMember("StudyDate"))
+      std::string folderName = *it;
+
+      if (folderName.find("$StudyDate$") != std::string::npos)
+      {
+        boost::replace_all(folderName, "$StudyDate$", GetStringDicomTagForPath(tags, "StudyDate", "NO_STUDY_DATE"));
+      }
+      
+      if (folderName.find("$split(StudyDate)$"))
+      {
+        boost::replace_all(folderName, "$split(StudyDate)$", GetSplitDateDicomTagToPath(tags, "StudyDate", "NO_STUDY_DATE"));
+      }
+
+      if (folderName.find("$PatientBirthDate$") != std::string::npos)
+      {
+        boost::replace_all(folderName, "$PatientBirthDate$", GetStringDicomTagForPath(tags, "PatientBirthDate", "NO_PATIENT_BIRTH_DATE"));
+      }
+      
+      if (folderName.find("$split(PatientBirthDate)$"))
       {
-        LOG(WARNING) << "AdvancedStorage - No 'StudyDate' in attachment " << uuid << ".  Attachment will be stored in NO_STUDY_DATE folder";
+        boost::replace_all(folderName, "$split(PatientBirthDate)$", GetSplitDateDicomTagToPath(tags, "PatientBirthDate", "NO_PATIENT_BIRTH_DATE"));
+      }
+
+      if (folderName.find("$PatientID$") != std::string::npos)
+      {
+        boost::replace_all(folderName, "$PatientID$", GetStringDicomTagForPath(tags, "PatientID", "EMPTY_PATIENT_ID"));
+      }
+
+      if (folderName.find("$StudyDescription$") != std::string::npos)
+      {
+        boost::replace_all(folderName, "$StudyDescription$", GetStringDicomTagForPath(tags, "StudyDescription", "NO_STUDY_DESCRIPTION"));
+      }
+
+      if (folderName.find("$SeriesDescription$") != std::string::npos)
+      {
+        boost::replace_all(folderName, "$SeriesDescription$", GetStringDicomTagForPath(tags, "SeriesDescription", "NO_SERIES_DESCRIPTION"));
+      }
+
+      if (folderName.find("$StudyInstanceUID$") != std::string::npos)
+      {
+        boost::replace_all(folderName, "$StudyInstanceUID$", GetStringDicomTagForPath(tags, "StudyDescription", "NO_STUDY_INSTANCE_UID"));
       }
 
-      AddSplitDateDicomTagToPath(path, tags, "StudyDate", "NO_STUDY_DATE");
-      AddStringDicomTagToPath(path, tags, "PatientID");  // no default value, tag is always present if the instance is accepted by Orthanc  
-      
-      if (tags.isMember("PatientName") && tags["PatientName"].isString() && !tags["PatientName"].asString().empty())
+      if (folderName.find("$SeriesInstanceUID$") != std::string::npos)
+      {
+        boost::replace_all(folderName, "$SeriesInstanceUID$", GetStringDicomTagForPath(tags, "SeriesInstanceUID", "NO_SERIES_INSTANCE_UID"));
+      }
+
+      if (folderName.find("$SOPInstanceUID$") != std::string::npos)
+      {
+        boost::replace_all(folderName, "$SOPInstanceUID$", GetStringDicomTagForPath(tags, "SOPInstanceUID", "NO_SOP_INSTANCE_UID"));
+      }
+
+      if (folderName.find("$OrthancPatientID$") != std::string::npos)
       {
-        path += std::string(" - ") + tags["PatientName"].asString();
+        Orthanc::DicomInstanceHasher hasher(tags["PatientID"].asString(), tags["StudyInstanceUID"].asString(), tags["SeriesInstanceUID"].asString(), tags["SOPInstanceUID"].asString());
+        boost::replace_all(folderName, "$OrthancPatientID$", hasher.HashPatient());
+      }
+
+      if (folderName.find("$OrthancStudyID$") != std::string::npos)
+      {
+        Orthanc::DicomInstanceHasher hasher(tags["PatientID"].asString(), tags["StudyInstanceUID"].asString(), tags["SeriesInstanceUID"].asString(), tags["SOPInstanceUID"].asString());
+        boost::replace_all(folderName, "$OrthancStudyID$", hasher.HashPatient());
+      }
+
+      if (folderName.find("$OrthancSeriesID$") != std::string::npos)
+      {
+        Orthanc::DicomInstanceHasher hasher(tags["PatientID"].asString(), tags["StudyInstanceUID"].asString(), tags["SeriesInstanceUID"].asString(), tags["SOPInstanceUID"].asString());
+        boost::replace_all(folderName, "$OrthancSeriesID$", hasher.HashPatient());
       }
 
-      AddStringDicomTagToPath(path, tags, "StudyDescription");
-      AddStringDicomTagToPath(path, tags, "SeriesInstanceUID");
+      if (folderName.find("$OrthancInstanceID$") != std::string::npos)
+      {
+        Orthanc::DicomInstanceHasher hasher(tags["PatientID"].asString(), tags["StudyInstanceUID"].asString(), tags["SeriesInstanceUID"].asString(), tags["SOPInstanceUID"].asString());
+        boost::replace_all(folderName, "$OrthancInstanceID$", hasher.HashPatient());
+      }
+
+      if (folderName.find("$UUID$") != std::string::npos)
+      {
+        boost::replace_all(folderName, "$UUID$", uuid);
+        foundUuid = true;
+      }
 
-      path /= uuid;
-      path += GetExtension(type, isCompressed);
-      return path;
+      if (folderName.find("$.ext$") != std::string::npos)
+      {
+        boost::replace_all(folderName, "$.ext$", GetExtension(type, isCompressed));
+      }
+
+      path /= folderName;
     }
+
+    if (!foundUuid)
+    {
+      LOG(WARNING) << "Your naming scheme does not contain $UUID$.  There is a high risk of files being overwritten.  Using the default naming scheme.";
+      return GetLegacyRelativePath(uuid);
+    }
+
+    return path;
   }
 
   return GetLegacyRelativePath(uuid);
@@ -588,6 +699,7 @@
             // All other values are currently experimental
             // "OrthancDefault" = same structure and file naming as default orthanc, 
             // "Preset1-StudyDatePatientID" = split(StudyDate)/PatientID - PatientName/StudyDescription/SeriesInstanceUID/uuid.ext
+            // "Preset2-OrthancStudyID-OrthancSeriesID-UUID" = OrthancStudyID/OrthancSeriesID/uuid.ext
             "NamingScheme" : "OrthancDefault",
 
             // Defines the maximum length for path used in the storage.  If a file is longer
@@ -605,6 +717,11 @@
       const Json::Value& pluginJson = advancedStorage.GetJson();
 
       namingScheme_ = advancedStorage.GetStringValue("NamingScheme", "OrthancDefault");
+      if (namingScheme_ != "OrthancDefault" && namingScheme_.find("$UUID") == std::string::npos)
+      {
+        LOG(ERROR) << "AdvancedStorage - Your naming scheme does not contain $UUID$.  The risk of files being overwritten is to high.";
+        return -1;
+      }
 
       // if we have enabled multiple storage after files have been saved without this plugin, we still need the default StorageDirectory
       rootPath_ = fs::path(orthancConfiguration.GetStringValue("StorageDirectory", "OrthancStorage"));
@@ -615,7 +732,7 @@
 
       if (!rootPath_.is_absolute())
       {
-        LOG(ERROR) << "AdvancedStorage - Path to the default storage area should be an absolute path '" << rootPath_ << "'";
+        LOG(ERROR) << "AdvancedStorage - Path to the default storage area should be an absolute path " << rootPath_ << " (\"StorageDirectory\" in main Orthanc configuration)";
         return -1;
       }