diff OrthancServer/Plugins/Samples/AdvancedStorage/Plugin.cpp @ 5080:d7274e43ea7c attach-custom-data

allow plugins to store a customData in the Attachments table to e.g. store custom paths without requiring an external DB
author Alain Mazy <am@osimis.io>
date Thu, 08 Sep 2022 17:42:08 +0200
parents
children c673997507ea
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Samples/AdvancedStorage/Plugin.cpp	Thu Sep 08 17:42:08 2022 +0200
@@ -0,0 +1,504 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2022 Osimis S.A., Belgium
+ * Copyright (C) 2021-2022 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 "../../../../OrthancFramework/Sources/Compatibility.h"
+#include "../../../../OrthancFramework/Sources/OrthancException.h"
+#include "../../../../OrthancFramework/Sources/SystemToolbox.h"
+#include "../../../../OrthancFramework/Sources/Toolbox.h"
+#include "../../../../OrthancFramework/Sources/Logging.h"
+#include "../Common/OrthancPluginCppWrapper.h"
+
+#include <boost/filesystem.hpp>
+#include <boost/filesystem/fstream.hpp>
+#include <boost/iostreams/device/file_descriptor.hpp>
+#include <boost/iostreams/stream.hpp>
+
+
+#include <json/value.h>
+#include <json/writer.h>
+#include <string.h>
+#include <iostream>
+#include <algorithm>
+#include <map>
+#include <list>
+#include <time.h>
+
+namespace fs = boost::filesystem;
+
+fs::path absoluteRootPath_;
+bool fsyncOnWrite_ = true;
+
+
+fs::path GetLegacyRelativePath(const std::string& uuid)
+{
+
+  if (!Orthanc::Toolbox::IsUuid(uuid))
+  {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+  }
+
+  fs::path path = absoluteRootPath_;
+
+  path /= std::string(&uuid[0], &uuid[2]);
+  path /= std::string(&uuid[2], &uuid[4]);
+  path /= uuid;
+
+#if BOOST_HAS_FILESYSTEM_V3 == 1
+  path.make_preferred();
+#endif
+
+  return path;
+}
+
+fs::path GetAbsolutePath(const std::string& uuid, const std::string& customData)
+{
+  fs::path path = absoluteRootPath_;
+
+  if (!customData.empty())
+  {
+    if (customData.substr(0, 2) == "1.") // version 1
+    {
+      path /= customData.substr(2);
+    }
+    else
+    {
+      throw "TODO: unknown version";
+    }
+
+  }
+  else
+  {
+    path /= GetLegacyRelativePath(uuid);
+  }
+
+  path.make_preferred();
+  return path;
+}
+
+std::string GetCustomData(const fs::path& path)
+{
+  return std::string("1.") + path.string(); // prefix the relative path with a version
+}
+
+void AddDateDicomTagToPath(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 AddSringDicomTagToPath(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;
+  }
+}
+
+void AddIntDicomTagToPath(fs::path& path, const Json::Value& tags, const char* tagName, size_t zeroPaddingWidth = 0, const char* defaultValue = NULL)
+{
+  if (tags.isMember(tagName) && tags[tagName].isString() && tags[tagName].asString().size() > 0)
+  {
+    std::string tagValue = tags[tagName].asString();
+    if (zeroPaddingWidth > 0 && tagValue.size() < zeroPaddingWidth)
+    {
+      std::string padding(zeroPaddingWidth - tagValue.size(), '0');
+      path /= padding + tagValue; 
+    }
+    else
+    {
+      path /= tagValue;
+    }
+  }
+  else if (defaultValue != NULL)
+  {
+    path /= defaultValue;
+  }
+}
+
+fs::path GetRelativePathFromTags(const Json::Value& tags, const char* uuid, OrthancPluginContentType type, bool isCompressed)
+{
+  fs::path path;
+  
+  if (type == OrthancPluginContentType_Dicom || type == OrthancPluginContentType_DicomUntilPixelData)
+  {
+    // TODO: allow customization ... note: right now, we always need the uuid in the path !!
+
+    AddDateDicomTagToPath(path, tags, "StudyDate", "NO_STUDY_DATE");
+    AddSringDicomTagToPath(path, tags, "PatientID");  // no default value, tag is always present if the instance is accepted by Orthanc  
+    AddSringDicomTagToPath(path, tags, "StudyInstanceUID");
+    AddSringDicomTagToPath(path, tags, "SeriesInstanceUID");
+    //AddIntDicomTagToPath(path, tags, "InstanceNumber", 8, uuid);
+    path /= uuid;
+  }
+  else
+  {
+    path = GetLegacyRelativePath(uuid);
+  }
+
+  std::string extension;
+
+  switch (type)
+  {
+    case OrthancPluginContentType_Dicom:
+      extension = ".dcm";
+      break;
+    case OrthancPluginContentType_DicomUntilPixelData:
+      extension = ".dcm.head";
+      break;
+    default:
+      extension = ".unk";
+  }
+  if (isCompressed)
+  {
+    extension = extension + ".cmp"; // compression is zlib + size -> we can not use the .zip extension
+  }
+  
+  path += extension;
+
+  return path;
+}
+
+
+OrthancPluginErrorCode StorageCreate(OrthancPluginMemoryBuffer* customData,
+                                             const char* uuid,
+                                             const Json::Value& tags,
+                                             const void* content,
+                                             int64_t size,
+                                             OrthancPluginContentType type,
+                                             bool isCompressed)
+{
+  fs::path relativePath = GetRelativePathFromTags(tags, uuid, type, isCompressed);
+  std::string customDataString = GetCustomData(relativePath);
+
+  fs::path absolutePath = absoluteRootPath_ / relativePath;
+
+  if (fs::exists(absolutePath))
+  {
+    // Extremely unlikely case if uuid is included in the path: This Uuid has already been created
+    // in the past.
+    throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+
+    // TODO for the future: handle duplicates path (e.g: there's no uuid in the path and we are uploading the same file again)
+    // OrthancPlugins::LogWarning(std::string("Overwriting file \"") + path.string() + "\" (" + uuid + ")");
+  }
+
+  if (fs::exists(absolutePath.parent_path()))
+  {
+    if (!fs::is_directory(absolutePath.parent_path()))
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_DirectoryOverFile);
+    }
+  }
+  else
+  {
+    if (!fs::create_directories(absolutePath.parent_path()))
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_FileStorageCannotWrite);
+    }
+  }
+
+  Orthanc::SystemToolbox::WriteFile(content, size, absolutePath.string(), fsyncOnWrite_);
+
+  OrthancPluginCreateMemoryBuffer(OrthancPlugins::GetGlobalContext(), customData, customDataString.size());
+  memcpy(customData->data, customDataString.data(), customDataString.size());
+
+  return OrthancPluginErrorCode_Success;
+
+}
+
+OrthancPluginErrorCode StorageCreateInstance(OrthancPluginMemoryBuffer* customData,
+                                             const char* uuid,
+                                             const OrthancPluginDicomInstance*  instance,
+                                             const void* content,
+                                             int64_t size,
+                                             OrthancPluginContentType type,
+                                             bool isCompressed)
+{
+  try
+  {
+    OrthancPlugins::LogInfo(std::string("Creating instance attachment \"") + uuid + "\"");
+
+    OrthancPlugins::DicomInstance dicomInstance(instance);
+    Json::Value tags;
+    dicomInstance.GetSimplifiedJson(tags);
+
+    return StorageCreate(customData, uuid, tags, content, size, type, isCompressed);
+  }
+  catch (Orthanc::OrthancException& e)
+  {
+    return static_cast<OrthancPluginErrorCode>(e.GetErrorCode());
+  }
+  catch (...)
+  {
+    return OrthancPluginErrorCode_StorageAreaPlugin;
+  }
+
+  return OrthancPluginErrorCode_Success;
+}
+
+
+OrthancPluginErrorCode StorageCreateAttachment(OrthancPluginMemoryBuffer* customData,
+                                               const char* uuid,
+                                               const char* resourceId,
+                                               OrthancPluginResourceType resourceType,
+                                               const void* content,
+                                               int64_t size,
+                                               OrthancPluginContentType type,
+                                               bool isCompressed)
+{
+  try
+  {
+    OrthancPlugins::LogInfo(std::string("Creating attachment \"") + uuid + "\"");
+
+    //TODO_CUSTOM_DATA: get tags from the Rest API...
+    Json::Value tags;
+
+    return StorageCreate(customData, uuid, tags, content, size, type, isCompressed);
+  }
+  catch (Orthanc::OrthancException& e)
+  {
+    return static_cast<OrthancPluginErrorCode>(e.GetErrorCode());
+  }
+  catch (...)
+  {
+    return OrthancPluginErrorCode_StorageAreaPlugin;
+  }
+
+  return OrthancPluginErrorCode_Success;
+}
+
+OrthancPluginErrorCode StorageReadWhole(OrthancPluginMemoryBuffer64* target,
+                                        const char* uuid,
+                                        const char* customData,
+                                        OrthancPluginContentType type)
+{
+  OrthancPlugins::LogInfo(std::string("Reading attachment \"") + uuid + "\"");
+
+  std::string path = GetAbsolutePath(uuid, customData).string();
+
+  if (!Orthanc::SystemToolbox::IsRegularFile(path))
+  {
+    OrthancPlugins::LogError(std::string("The path does not point to a regular file: ") + path);
+    return OrthancPluginErrorCode_InexistentFile;
+  }
+
+  try
+  {
+    fs::ifstream f;
+    f.open(path, std::ifstream::in | std::ifstream::binary);
+    if (!f.good())
+    {
+      OrthancPlugins::LogError(std::string("The path does not point to a regular file: ") + path);
+      return OrthancPluginErrorCode_InexistentFile;
+    }
+
+    // get file size
+    f.seekg(0, std::ios::end);
+    std::streamsize fileSize = f.tellg();
+    f.seekg(0, std::ios::beg);
+
+    // The ReadWhole must allocate the buffer itself
+    if (OrthancPluginCreateMemoryBuffer64(OrthancPlugins::GetGlobalContext(), target, fileSize) != OrthancPluginErrorCode_Success)
+    {
+      OrthancPlugins::LogError(std::string("Unable to allocate memory to read file: ") + path);
+      return OrthancPluginErrorCode_NotEnoughMemory;
+    }
+
+    if (fileSize != 0)
+    {
+      f.read(reinterpret_cast<char*>(target->data), fileSize);
+    }
+
+    f.close();
+  }
+  catch (...)
+  {
+    OrthancPlugins::LogError(std::string("Unexpected error while reading: ") + path);
+    return OrthancPluginErrorCode_StorageAreaPlugin;
+  }
+
+  return OrthancPluginErrorCode_Success;
+}
+
+
+OrthancPluginErrorCode StorageReadRange (OrthancPluginMemoryBuffer64* target,
+                                         const char* uuid,
+                                         const char* customData,
+                                         OrthancPluginContentType type,
+                                         uint64_t rangeStart)
+{
+  OrthancPlugins::LogInfo(std::string("Reading attachment \"") + uuid + "\"");
+
+  std::string path = GetAbsolutePath(uuid, customData).string();
+
+  if (!Orthanc::SystemToolbox::IsRegularFile(path))
+  {
+    OrthancPlugins::LogError(std::string("The path does not point to a regular file: ") + path);
+    return OrthancPluginErrorCode_InexistentFile;
+  }
+
+  try
+  {
+    fs::ifstream f;
+    f.open(path, std::ifstream::in | std::ifstream::binary);
+    if (!f.good())
+    {
+      OrthancPlugins::LogError(std::string("The path does not point to a regular file: ") + path);
+      return OrthancPluginErrorCode_InexistentFile;
+    }
+
+    f.seekg(rangeStart, std::ios::beg);
+
+    // The ReadRange uses a target that has already been allocated by orthanc
+    f.read(reinterpret_cast<char*>(target->data), target->size);
+
+    f.close();
+  }
+  catch (...)
+  {
+    OrthancPlugins::LogError(std::string("Unexpected error while reading: ") + path);
+    return OrthancPluginErrorCode_StorageAreaPlugin;
+  }
+
+  return OrthancPluginErrorCode_Success;
+}
+
+
+OrthancPluginErrorCode StorageRemove (const char* uuid,
+                                      const char* customData,
+                                      OrthancPluginContentType type)
+{
+  // LOG(INFO) << "Deleting attachment \"" << uuid << "\" of type " << static_cast<int>(type);
+
+  fs::path p = GetAbsolutePath(uuid, customData);
+
+  try
+  {
+    fs::remove(p);
+  }
+  catch (...)
+  {
+    // Ignore the error
+  }
+
+  // Remove the empty parent directories, (ignoring the error code if these directories are not empty)
+
+  try
+  {
+    fs::path parent = p.parent_path();
+
+    while (parent != absoluteRootPath_)
+    {
+      fs::remove(parent);
+      parent = parent.parent_path();
+    }
+  }
+  catch (...)
+  {
+    // Ignore the error
+  }
+
+  return OrthancPluginErrorCode_Success;
+}
+
+extern "C"
+{
+
+  ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* context)
+  {
+    OrthancPlugins::SetGlobalContext(context);
+    Orthanc::Logging::InitializePluginContext(context);
+
+    /* Check the version of the Orthanc core */
+    if (OrthancPluginCheckVersion(context) == 0)
+    {
+      OrthancPlugins::ReportMinimalOrthancVersion(ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER,
+                                                  ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER,
+                                                  ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER);
+      return -1;
+    }
+
+    OrthancPlugins::LogWarning("AdvancedStorage plugin is initializing");
+    OrthancPluginSetDescription(context, "Provides alternative layout for your storage.");
+
+    OrthancPlugins::OrthancConfiguration orthancConfiguration;
+
+    OrthancPlugins::OrthancConfiguration advancedStorage;
+    orthancConfiguration.GetSection(advancedStorage, "AdvancedStorage");
+
+    bool enabled = advancedStorage.GetBooleanValue("Enable", false);
+    if (enabled)
+    {
+      /*
+        {
+          "AdvancedStorage": {
+            
+            // Enables/disables the plugin
+            "Enable": false,
+          }
+        }
+      */
+
+      absoluteRootPath_ = fs::absolute(fs::path(orthancConfiguration.GetStringValue("StorageDirectory", "OrthancStorage")));
+      LOG(WARNING) << "AdvancedStorage - Path to the storage area: " << absoluteRootPath_.string();
+
+      OrthancPluginRegisterStorageArea3(context, StorageCreateInstance, StorageCreateAttachment, StorageReadWhole, StorageReadRange, StorageRemove);
+    }
+    else
+    {
+      OrthancPlugins::LogWarning("AdvancedStorage plugin is disabled by the configuration file");
+    }
+
+    return 0;
+  }
+
+
+  ORTHANC_PLUGINS_API void OrthancPluginFinalize()
+  {
+    OrthancPlugins::LogWarning("AdvancedStorage plugin is finalizing");
+  }
+
+
+  ORTHANC_PLUGINS_API const char* OrthancPluginGetName()
+  {
+    return "advanced-storage";
+  }
+
+
+  ORTHANC_PLUGINS_API const char* OrthancPluginGetVersion()
+  {
+    return ORTHANC_PLUGIN_VERSION;
+  }
+}