changeset 4954:e1495a34cd39 more-tags

integration mainline->more-tags
author Sebastien Jodogne <s.jodogne@gmail.com>
date Tue, 22 Mar 2022 19:11:56 +0100
parents 3778a0433dd3 (current diff) 60cb4b379485 (diff)
children 94edc2c89768
files NEWS OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.cpp OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h OrthancFramework/UnitTestsSources/FromDcmtkTests.cpp OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h OrthancServer/Sources/main.cpp TODO
diffstat 21 files changed, 1815 insertions(+), 50 deletions(-) [+]
line wrap: on
line diff
--- a/NEWS	Mon Mar 21 15:19:42 2022 +0100
+++ b/NEWS	Tue Mar 22 19:11:56 2022 +0100
@@ -1,10 +1,11 @@
 Pending changes in the mainline
 ===============================
 
-
 General
 -------
 
+* Improved DICOM authorization checks when multiple modalities are
+  declared with the same AET.
 * New configuration "ExtraMainDicomTags" to store more tags in the Index DB
   to speed up, e.g, building C-Find, dicom-web or tools/find answers
 * New configuration "Warnings" to enable/disable individual warnings that can
@@ -34,13 +35,18 @@
   - /instances
 * new field "MainDicomTags" in the /system route response
 
+Plugins
+-------
+
+* New function in the SDK: "OrthancPluginRegisterWebDavCollection()"
+  to map a WebDAV virtual filesystem into the REST API of Orthanc.
 
 Documentation
 -------------
 
-* Removed the "LimitJobs" configuration that is not used anymore since the new
-  JobEngine has been introduced (in  Orthanc 1.4.0).  The pending list of jobs
-  is unlimited.
+* Removed the "LimitJobs" configuration that is not used anymore since
+  the new JobEngine has been introduced (in Orthanc 1.4.0). The
+  pending list of jobs is unlimited.
 
 
 Version 1.10.0 (2022-02-23)
--- a/OrthancFramework/Resources/CMake/EmscriptenParameters.cmake	Mon Mar 21 15:19:42 2022 +0100
+++ b/OrthancFramework/Resources/CMake/EmscriptenParameters.cmake	Tue Mar 22 19:11:56 2022 +0100
@@ -25,10 +25,23 @@
   set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s BINARYEN_TRAP_MODE='\"${EMSCRIPTEN_TRAP_MODE}\"'")
 endif()
 
+# If "-O3" is used (the default in "Release" mode), this results in a
+# too large memory consumption in "wasm-opt", at least in Emscripten
+# 3.1.7, which ultimately crashes the compiler. So we force "-O2"
+# (this also has the advantage of speeding up the build):
+set(CMAKE_CXX_FLAGS_RELEASE "-O2 -DNDEBUG")
+
 # "DISABLE_EXCEPTION_CATCHING" is a "compile+link" option. HOWEVER,
 # setting it inside "WASM_FLAGS" creates link errors, at least with
 # side modules. TODO: Understand why
 set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s DISABLE_EXCEPTION_CATCHING=0")
+
+# "-Wno-unused-command-line-argument" is used to avoid annoying
+# warnings about setting WASM, FETCH and ASSERTIONS, which was
+# required for earlier versions of emsdk:
+# https://groups.google.com/g/emscripten-discuss/c/VX4enWfadUE
+set(WASM_FLAGS "${WASM_FLAGS} -Wno-unused-command-line-argument")
+
 #set(WASM_FLAGS "${WASM_FLAGS} -s DISABLE_EXCEPTION_CATCHING=0")
 
 if (EMSCRIPTEN_TARGET_MODE STREQUAL "wasm")
--- a/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.cpp	Mon Mar 21 15:19:42 2022 +0100
+++ b/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.cpp	Tue Mar 22 19:11:56 2022 +0100
@@ -2660,18 +2660,10 @@
               l++;
             }
 
-            if (l == length)
-            {
-              // Not a null-terminated plain string
-              action = visitor.VisitNotSupported(parentTags, parentIndexes, tag, vr);
-            }
-            else
-            {
-              std::string ignored;
-              std::string s(reinterpret_cast<const char*>(data), l);
-              action = visitor.VisitString(ignored, parentTags, parentIndexes, tag, vr,
-                                           Toolbox::ConvertToUtf8(s, encoding, hasCodeExtensions));
-            }
+            std::string ignored;
+            std::string s(reinterpret_cast<const char*>(data), l);
+            action = visitor.VisitString(ignored, parentTags, parentIndexes, tag, vr,
+                                         Toolbox::ConvertToUtf8(s, encoding, hasCodeExtensions));
           }
           else
           {
@@ -3293,6 +3285,49 @@
     IDicomPathVisitor::Apply(visitor, dataset, path);
     return visitor.HasFound();
   }
+
+
+  bool FromDcmtkBridge::LookupStringValue(std::string& target,
+                                          DcmDataset& dataset,
+                                          const DicomTag& key)
+  {
+    DcmTagKey dcmkey(key.GetGroup(), key.GetElement());
+    
+    const char* str = NULL;
+    const Uint8* data = NULL;
+    unsigned long size = 0;
+
+    if (dataset.findAndGetString(dcmkey, str).good() &&
+        str != NULL)
+    {
+      target.assign(str);
+      return true;
+    }
+    else if (dataset.findAndGetUint8Array(dcmkey, data, &size).good() &&
+             data != NULL &&
+             size > 0)
+    {
+      /**
+       * This special case is necessary for borderline DICOM files
+       * that have DICOM tags have the "UN" value representation. New
+       * in Orthanc 1.10.1.
+       * https://groups.google.com/g/orthanc-users/c/86fobx3ZyoM/m/KBG17un6AQAJ
+       **/
+      unsigned long l = 0;
+      while (l < size &&
+             data[l] != 0)
+      {
+        l++;
+      }
+
+      target.assign(reinterpret_cast<const char*>(data), l);
+      return true;
+    }
+    else
+    {
+      return false;
+    }
+  }
 }
 
 
--- a/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h	Mon Mar 21 15:19:42 2022 +0100
+++ b/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h	Tue Mar 22 19:11:56 2022 +0100
@@ -292,5 +292,9 @@
                                    DcmDataset& dataset,
                                    const DicomPath& path,
                                    size_t sequenceIndex);
+
+    static bool LookupStringValue(std::string& target,
+                                  DcmDataset& dataset,
+                                  const DicomTag& key);
   };
 }
--- a/OrthancFramework/Sources/DicomParsing/IDicomTranscoder.cpp	Mon Mar 21 15:19:42 2022 +0100
+++ b/OrthancFramework/Sources/DicomParsing/IDicomTranscoder.cpp	Tue Mar 22 19:11:56 2022 +0100
@@ -75,13 +75,11 @@
     }
     
     DcmDataset& dataset = *dicom.getDataset();
-    
-    const char* v = NULL;
 
-    if (dataset.findAndGetString(DCM_SOPInstanceUID, v).good() &&
-        v != NULL)
+    std::string s;
+    if (FromDcmtkBridge::LookupStringValue(s, dataset, DICOM_TAG_SOP_INSTANCE_UID))
     {
-      return std::string(v);
+      return s;
     }
     else
     {
--- a/OrthancFramework/Sources/DicomParsing/ParsedDicomFile.cpp	Mon Mar 21 15:19:42 2022 +0100
+++ b/OrthancFramework/Sources/DicomParsing/ParsedDicomFile.cpp	Tue Mar 22 19:11:56 2022 +0100
@@ -76,6 +76,8 @@
 #include "Internals/DicomImageDecoder.h"
 #include "ToDcmtkBridge.h"
 
+#include "../Images/Image.h"
+#include "../Images/ImageProcessing.h"
 #include "../Images/PamReader.h"
 #include "../Logging.h"
 #include "../OrthancException.h"
@@ -1918,6 +1920,161 @@
   }
 
 
+  void ParsedDicomFile::ListOverlays(std::set<uint16_t>& groups) const
+  {
+    DcmDataset& dataset = *const_cast<ParsedDicomFile&>(*this).GetDcmtkObject().getDataset();
+
+    // "Repeating Groups shall only be allowed in the even Groups (6000-601E,eeee)"
+    // https://dicom.nema.org/medical/dicom/2021e/output/chtml/part05/sect_7.6.html
+
+    for (uint16_t group = 0x6000; group <= 0x601e; group += 2)
+    {
+      if (dataset.tagExists(DcmTagKey(group, 0x0010)))
+      {
+        groups.insert(group);
+      }
+    }
+  }
+
+
+  static unsigned int Ceiling(unsigned int a,
+                              unsigned int b)
+  {
+    if (a % b == 0)
+    {
+      return a / b;
+    }
+    else
+    {
+      return a / b + 1;
+    }
+  }
+  
+
+  ImageAccessor* ParsedDicomFile::DecodeOverlay(int& originX,
+                                                int& originY,
+                                                uint16_t group) const
+  {
+    // https://dicom.nema.org/medical/dicom/current/output/chtml/part03/sect_C.9.2.html
+
+    DcmDataset& dataset = *const_cast<ParsedDicomFile&>(*this).GetDcmtkObject().getDataset();
+
+    Uint16 rows, columns, bitsAllocated, bitPosition;
+    const Sint16* origin = NULL;
+    unsigned long originSize = 0;
+    const Uint8* overlayData = NULL;
+    unsigned long overlaySize = 0;
+    
+    if (dataset.findAndGetUint16(DcmTagKey(group, 0x0010), rows).good() &&
+        dataset.findAndGetUint16(DcmTagKey(group, 0x0011), columns).good() &&
+        dataset.findAndGetSint16Array(DcmTagKey(group, 0x0050), origin, &originSize).good() &&
+        origin != NULL &&
+        originSize == 2 &&
+        dataset.findAndGetUint16(DcmTagKey(group, 0x0100), bitsAllocated).good() &&
+        bitsAllocated == 1 &&
+        dataset.findAndGetUint16(DcmTagKey(group, 0x0102), bitPosition).good() &&
+        bitPosition == 0 &&
+        dataset.findAndGetUint8Array(DcmTagKey(group, 0x3000), overlayData, &overlaySize).good() &&
+        overlayData != NULL)
+    {
+      unsigned int expectedSize = Ceiling(rows * columns, 8);
+      if (overlaySize < expectedSize)
+      {
+        throw OrthancException(ErrorCode_CorruptedFile, "Overlay doesn't have a valid number of bits");
+      }
+      
+      originX = origin[1];
+      originY = origin[0];
+
+      std::unique_ptr<ImageAccessor> overlay(new Image(Orthanc::PixelFormat_Grayscale8, columns, rows, false));
+
+      unsigned int posBit = 0;
+      for (int y = 0; y < rows; y++)
+      {
+        uint8_t* target = reinterpret_cast<uint8_t*>(overlay->GetRow(y));
+        
+        for (int x = 0; x < columns; x++)
+        {
+          uint8_t source = overlayData[posBit / 8];
+          uint8_t mask = 1 << (posBit % 8);
+
+          *target = ((source & mask) ? 255 : 0);
+
+          target++;
+          posBit++;
+        }
+      }
+      
+      return overlay.release();
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_CorruptedFile, "Invalid overlay");
+    }
+  }
+
+  
+  ImageAccessor* ParsedDicomFile::DecodeAllOverlays(int& originX,
+                                                    int& originY) const
+  {
+    std::set<uint16_t> groups;
+    ListOverlays(groups);
+
+    if (groups.empty())
+    {
+      originX = 0;
+      originY = 0;
+      return new Image(PixelFormat_Grayscale8, 0, 0, false);
+    }
+    else
+    {
+      std::set<uint16_t>::const_iterator it = groups.begin();
+      assert(it != groups.end());
+      
+      std::unique_ptr<ImageAccessor> result(DecodeOverlay(originX, originY, *it));
+      assert(result.get() != NULL);
+      ++it;
+
+      int right = originX + static_cast<int>(result->GetWidth());
+      int bottom = originY + static_cast<int>(result->GetHeight());
+
+      while (it != groups.end())
+      {
+        int ox, oy;
+        std::unique_ptr<ImageAccessor> overlay(DecodeOverlay(ox, oy, *it));
+        assert(overlay.get() != NULL);
+
+        int mergedX = std::min(originX, ox);
+        int mergedY = std::min(originY, oy);
+        right = std::max(right, ox + static_cast<int>(overlay->GetWidth()));
+        bottom = std::max(bottom, oy + static_cast<int>(overlay->GetHeight()));
+
+        assert(right >= mergedX && bottom >= mergedY);
+        unsigned int width = static_cast<unsigned int>(right - mergedX);
+        unsigned int height = static_cast<unsigned int>(bottom - mergedY);
+        
+        std::unique_ptr<ImageAccessor> merged(new Image(PixelFormat_Grayscale8, width, height, false));
+        ImageProcessing::Set(*merged, 0);
+
+        ImageAccessor a;
+        merged->GetRegion(a, originX - mergedX, originY - mergedY, result->GetWidth(), result->GetHeight());
+        ImageProcessing::Maximum(a, *result);
+
+        merged->GetRegion(a, ox - mergedX, oy - mergedY, overlay->GetWidth(), overlay->GetHeight());
+        ImageProcessing::Maximum(a, *overlay);
+
+        originX = mergedX;
+        originY = mergedY;
+        result.reset(merged.release());
+        
+        ++it;
+      }
+
+      return result.release();
+    }
+  }
+
+
 #if ORTHANC_BUILDING_FRAMEWORK_LIBRARY == 1
   // Alias for binary compatibility with Orthanc Framework 1.7.2 => don't use it anymore
   void ParsedDicomFile::DatasetToJson(Json::Value& target,
--- a/OrthancFramework/Sources/DicomParsing/ParsedDicomFile.h	Mon Mar 21 15:19:42 2022 +0100
+++ b/OrthancFramework/Sources/DicomParsing/ParsedDicomFile.h	Tue Mar 22 19:11:56 2022 +0100
@@ -300,5 +300,14 @@
     void GetRescale(double& rescaleIntercept,
                     double& rescaleSlope,
                     unsigned int frame) const;
+
+    void ListOverlays(std::set<uint16_t>& groups) const;
+
+    ImageAccessor* DecodeOverlay(int& originX,
+                                 int& originY,
+                                 uint16_t group) const;
+
+    ImageAccessor* DecodeAllOverlays(int& originX,
+                                     int& originY) const;
   };
 }
--- a/OrthancFramework/Sources/HttpServer/IWebDavBucket.h	Mon Mar 21 15:19:42 2022 +0100
+++ b/OrthancFramework/Sources/HttpServer/IWebDavBucket.h	Tue Mar 22 19:11:56 2022 +0100
@@ -117,8 +117,6 @@
         return mime_;
       }
 
-      void SetCreated(bool created);
-
       virtual void Format(pugi::xml_node& node,
                           const std::string& parentPath) const ORTHANC_OVERRIDE;
     };
--- a/OrthancFramework/Sources/Images/ImageProcessing.cpp	Mon Mar 21 15:19:42 2022 +0100
+++ b/OrthancFramework/Sources/Images/ImageProcessing.cpp	Tue Mar 22 19:11:56 2022 +0100
@@ -2964,4 +2964,78 @@
         throw OrthancException(ErrorCode_NotImplemented);
     }          
   }
+
+
+  template <typename PixelType,
+            typename Functor>
+  static void ApplyImageOntoImage(Functor f,
+                                  ImageAccessor& image /* inout */,
+                                  const ImageAccessor& other)
+  {
+    const unsigned int width = image.GetWidth();
+    const unsigned int height = image.GetHeight();
+    
+    if (width != other.GetWidth() ||
+        height != other.GetHeight())
+    {
+      throw OrthancException(ErrorCode_IncompatibleImageSize);
+    }
+    else if (image.GetFormat() != other.GetFormat() ||
+             GetBytesPerPixel(image.GetFormat()) != sizeof(PixelType))
+    {
+      throw OrthancException(ErrorCode_IncompatibleImageFormat);
+    }
+    else
+    {
+      for (unsigned int y = 0; y < height; y++)
+      {
+        PixelType* p = reinterpret_cast<PixelType*>(image.GetRow(y));
+        const PixelType* q = reinterpret_cast<const PixelType*>(other.GetConstRow(y));
+        
+        for (unsigned int x = 0; x < width; x++, p++, q++)
+        {
+          f(*p, *q);
+        }
+      }
+    }
+  }
+
+
+  namespace
+  {
+    // For older version of gcc, templated functors cannot be defined
+    // as types internal to functions, hence the anonymous namespace
+    
+    struct MaximumFunctor
+    {
+      void operator() (uint8_t& a, const uint8_t& b)
+      {
+        a = std::max(a, b);
+      }
+
+      void operator() (uint16_t& a, const uint16_t& b)
+      {
+        a = std::max(a, b);
+      }
+    };
+  }
+  
+
+  void ImageProcessing::Maximum(ImageAccessor& image,
+                                const ImageAccessor& other)
+  {
+    switch (image.GetFormat())
+    {
+      case PixelFormat_Grayscale8:
+        ApplyImageOntoImage<uint8_t, MaximumFunctor>(MaximumFunctor(), image, other);
+        return;
+
+      case PixelFormat_Grayscale16:
+        ApplyImageOntoImage<uint16_t, MaximumFunctor>(MaximumFunctor(), image, other);
+        return;
+
+      default:
+        throw OrthancException(ErrorCode_NotImplemented);
+    }
+  }
 }
--- a/OrthancFramework/Sources/Images/ImageProcessing.h	Mon Mar 21 15:19:42 2022 +0100
+++ b/OrthancFramework/Sources/Images/ImageProcessing.h	Tue Mar 22 19:11:56 2022 +0100
@@ -218,5 +218,8 @@
     static void ConvertJpegYCbCrToRgb(ImageAccessor& image /* inplace */);
 
     static void SwapEndianness(ImageAccessor& image /* inplace */);
+
+    static void Maximum(ImageAccessor& image /* inout */,
+                        const ImageAccessor& other);
   };
 }
--- a/OrthancFramework/Sources/SystemToolbox.cpp	Mon Mar 21 15:19:42 2022 +0100
+++ b/OrthancFramework/Sources/SystemToolbox.cpp	Tue Mar 22 19:11:56 2022 +0100
@@ -817,6 +817,14 @@
     {
       return MimeType_Ico;
     }
+    else if (extension == ".gz")
+    {
+      return MimeType_Gzip;
+    }
+    else if (extension == ".zip")
+    {
+      return MimeType_Zip;
+    }
 
     // Default type
     else
--- a/OrthancFramework/UnitTestsSources/FromDcmtkTests.cpp	Mon Mar 21 15:19:42 2022 +0100
+++ b/OrthancFramework/UnitTestsSources/FromDcmtkTests.cpp	Tue Mar 22 19:11:56 2022 +0100
@@ -49,7 +49,6 @@
 #include "../Sources/Images/ImageBuffer.h"
 #include "../Sources/Images/ImageProcessing.h"
 #include "../Sources/Images/PngReader.h"
-#include "../Sources/Images/PngWriter.h"
 #include "../Sources/Logging.h"
 #include "../Sources/OrthancException.h"
 
--- a/OrthancServer/Plugins/Engine/OrthancPlugins.cpp	Mon Mar 21 15:19:42 2022 +0100
+++ b/OrthancServer/Plugins/Engine/OrthancPlugins.cpp	Tue Mar 22 19:11:56 2022 +0100
@@ -39,6 +39,7 @@
 #include "../../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h"
 #include "../../../OrthancFramework/Sources/DicomParsing/Internals/DicomImageDecoder.h"
 #include "../../../OrthancFramework/Sources/DicomParsing/ToDcmtkBridge.h"
+#include "../../../OrthancFramework/Sources/HttpServer/HttpServer.h"
 #include "../../../OrthancFramework/Sources/HttpServer/HttpToolbox.h"
 #include "../../../OrthancFramework/Sources/Images/Image.h"
 #include "../../../OrthancFramework/Sources/Images/ImageProcessing.h"
@@ -75,6 +76,344 @@
 
 namespace Orthanc
 {
+  class OrthancPlugins::WebDavCollection : public IWebDavBucket
+  {
+  private:
+    PluginsErrorDictionary&                      errorDictionary_;
+    std::string                                  uri_;
+    OrthancPluginWebDavIsExistingFolderCallback  isExistingFolder_;
+    OrthancPluginWebDavListFolderCallback        listFolder_;
+    OrthancPluginWebDavRetrieveFileCallback      retrieveFile_;
+    OrthancPluginWebDavStoreFileCallback         storeFile_;
+    OrthancPluginWebDavCreateFolderCallback      createFolder_;
+    OrthancPluginWebDavDeleteItemCallback        deleteItem_;
+    void*                                        payload_;
+
+    class PathHelper : public boost::noncopyable
+    {
+    private:
+      std::vector<const char*>  items_;
+
+    public:
+      PathHelper(const std::vector<std::string>& path)
+      {
+        items_.resize(path.size());
+        for (size_t i = 0; i < path.size(); i++)
+        {
+          items_[i] = path[i].c_str();
+        }
+      }
+      
+      uint32_t GetSize() const
+      {
+        return static_cast<uint32_t>(items_.size());
+      }
+
+      const char* const* GetItems() const
+      {
+        return (items_.empty() ? NULL : &items_[0]);
+      }
+    };
+
+
+    static MimeType ParseMimeType(const char* mimeType)
+    {
+      MimeType mime;
+      if (LookupMimeType(mime, mimeType))
+      {
+        return mime;
+      }
+      else
+      {
+        LOG(WARNING) << "Unknown MIME type in plugin: " << mimeType;
+        return MimeType_Binary;
+      }
+    }
+    
+    static OrthancPluginErrorCode AddFile(
+      OrthancPluginWebDavCollection*  collection,
+      const char*                     displayName,
+      uint64_t                        contentSize,
+      const char*                     mimeType,
+      const char*                     creationTime)
+    {
+      try
+      {
+        std::unique_ptr<File> f(new File(displayName));
+        f->SetCreationTime(boost::posix_time::from_iso_string(creationTime));
+        f->SetContentLength(contentSize);
+
+        if (mimeType == NULL ||
+            std::string(mimeType).empty())
+        {
+          f->SetMimeType(SystemToolbox::AutodetectMimeType(displayName));
+        }
+        else
+        {
+          f->SetMimeType(ParseMimeType(mimeType));
+        }
+        
+        reinterpret_cast<Collection*>(collection)->AddResource(f.release());
+        return OrthancPluginErrorCode_Success;
+      }
+      catch (OrthancException& e)
+      {
+        return static_cast<OrthancPluginErrorCode>(e.GetErrorCode());
+      }
+      catch (...)
+      {
+        return OrthancPluginErrorCode_InternalError;
+      }
+    }
+    
+    static OrthancPluginErrorCode AddFolder(
+      OrthancPluginWebDavCollection*  collection,
+      const char*                     displayName,
+      const char*                     creationTime)
+    {
+      try
+      {
+        std::unique_ptr<Folder> f(new Folder(displayName));
+        f->SetCreationTime(boost::posix_time::from_iso_string(creationTime));
+        reinterpret_cast<Collection*>(collection)->AddResource(f.release());
+        return OrthancPluginErrorCode_Success;
+      }
+      catch (OrthancException& e)
+      {
+        return static_cast<OrthancPluginErrorCode>(e.GetErrorCode());
+      }
+      catch (boost::bad_lexical_cast&)
+      {
+        LOG(ERROR) << "Presumably ill-formed date in the plugin";
+        return OrthancPluginErrorCode_ParameterOutOfRange;
+      }
+      catch (...)
+      {
+        return OrthancPluginErrorCode_InternalError;
+      }
+    }
+
+
+    class ContentTarget : public boost::noncopyable
+    {
+    private:
+      bool                       isSent_;
+      MimeType&                  mime_;
+      std::string&               content_;
+      boost::posix_time::ptime&  modificationTime_;
+
+    public:
+      ContentTarget(const std::string& displayName,
+                    MimeType& mime,
+                    std::string& content,
+                    boost::posix_time::ptime& modificationTime) :
+        isSent_(false),
+        mime_(mime),
+        content_(content),
+        modificationTime_(modificationTime)
+      {
+        mime = SystemToolbox::AutodetectMimeType(displayName);
+      }
+
+      bool IsSent() const
+      {
+        return isSent_;
+      }
+      
+      static OrthancPluginErrorCode RetrieveFile(
+        OrthancPluginWebDavCollection*  collection,
+        const void*                     data,
+        uint64_t                        size,
+        const char*                     mimeType,
+        const char*                     creationTime)
+      {
+        ContentTarget& target = *reinterpret_cast<ContentTarget*>(collection);
+        
+        if (target.isSent_)
+        {
+          return OrthancPluginErrorCode_BadSequenceOfCalls;
+        }
+        else
+        {
+          try
+          {
+            target.isSent_ = true;
+
+            if (mimeType != NULL &&
+                !std::string(mimeType).empty())
+            {
+              target.mime_ = ParseMimeType(mimeType);
+            }
+            
+            target.content_.assign(reinterpret_cast<const char*>(data), size);
+            target.modificationTime_ = boost::posix_time::from_iso_string(creationTime);
+            return OrthancPluginErrorCode_Success;
+          }
+          catch (Orthanc::OrthancException& e)
+          {
+            return static_cast<OrthancPluginErrorCode>(e.GetErrorCode());
+          }
+          catch (boost::bad_lexical_cast&)
+          {
+            LOG(ERROR) << "Presumably ill-formed date in the plugin";
+            return OrthancPluginErrorCode_ParameterOutOfRange;
+          }
+          catch (...)
+          {
+            return OrthancPluginErrorCode_InternalError;
+          }
+        }
+      }
+    };
+
+
+  public:
+    WebDavCollection(PluginsErrorDictionary& errorDictionary,
+                     const _OrthancPluginRegisterWebDavCollection& p) :
+      errorDictionary_(errorDictionary),
+      uri_(p.uri),
+      isExistingFolder_(p.isExistingFolder),
+      listFolder_(p.listFolder),
+      retrieveFile_(p.retrieveFile),
+      storeFile_(p.storeFile),
+      createFolder_(p.createFolder),
+      deleteItem_(p.deleteItem),
+      payload_(p.payload)
+    {
+    }
+
+    const std::string& GetUri() const
+    {
+      return uri_;
+    }
+
+    virtual bool IsExistingFolder(const std::vector<std::string>& path)
+    {
+      PathHelper helper(path);
+
+      uint8_t isExisting;
+      OrthancPluginErrorCode code = isExistingFolder_(&isExisting, helper.GetSize(), helper.GetItems(), payload_);
+
+      if (code == OrthancPluginErrorCode_Success)
+      {
+        return (isExisting != 0);
+      }
+      else
+      {
+        errorDictionary_.LogError(code, true);
+        throw OrthancException(static_cast<ErrorCode>(code));
+      }
+    }
+
+    virtual bool ListCollection(Collection& collection,
+                                const std::vector<std::string>& path)
+    {
+      PathHelper helper(path);
+
+      uint8_t isExisting;
+      OrthancPluginErrorCode code = listFolder_(&isExisting, reinterpret_cast<OrthancPluginWebDavCollection*>(&collection), 
+                                                AddFile, AddFolder, helper.GetSize(), helper.GetItems(), payload_);
+
+      if (code == OrthancPluginErrorCode_Success)
+      {
+        return (isExisting != 0);
+      }
+      else
+      {
+        errorDictionary_.LogError(code, true);
+        throw OrthancException(static_cast<ErrorCode>(code));
+      }
+    }
+
+    virtual bool GetFileContent(MimeType& mime,
+                                std::string& content,
+                                boost::posix_time::ptime& modificationTime, 
+                                const std::vector<std::string>& path)
+    {
+      PathHelper helper(path);
+      
+      ContentTarget target(path.back(), mime, content, modificationTime);
+      OrthancPluginErrorCode code = retrieveFile_(
+        reinterpret_cast<OrthancPluginWebDavCollection*>(&target),
+        ContentTarget::RetrieveFile, helper.GetSize(), helper.GetItems(), payload_);
+      
+      if (code == OrthancPluginErrorCode_Success)
+      {
+        return target.IsSent();
+      }
+      else
+      {
+        errorDictionary_.LogError(code, true);
+        throw OrthancException(static_cast<ErrorCode>(code));
+      }
+    }
+
+    virtual bool StoreFile(const std::string& content,
+                           const std::vector<std::string>& path)
+    {
+      PathHelper helper(path);
+
+      uint8_t isReadOnly;
+      OrthancPluginErrorCode code = storeFile_(&isReadOnly, helper.GetSize(), helper.GetItems(),
+                                                content.empty() ? NULL : content.c_str(), content.size(), payload_);
+
+      if (code == OrthancPluginErrorCode_Success)
+      {
+        return (isReadOnly != 0);
+      }
+      else
+      {
+        errorDictionary_.LogError(code, true);
+        throw OrthancException(static_cast<ErrorCode>(code));
+      }
+    }
+
+    virtual bool CreateFolder(const std::vector<std::string>& path)
+    {
+      PathHelper helper(path);
+
+      uint8_t isReadOnly;
+      OrthancPluginErrorCode code = createFolder_(&isReadOnly, helper.GetSize(), helper.GetItems(), payload_);
+
+      if (code == OrthancPluginErrorCode_Success)
+      {
+        return (isReadOnly != 0);
+      }
+      else
+      {
+        errorDictionary_.LogError(code, true);
+        throw OrthancException(static_cast<ErrorCode>(code));
+      }
+    }      
+
+    virtual bool DeleteItem(const std::vector<std::string>& path)
+    {
+      PathHelper helper(path);
+
+      uint8_t isReadOnly;
+      OrthancPluginErrorCode code = deleteItem_(&isReadOnly, helper.GetSize(), helper.GetItems(), payload_);
+
+      if (code == OrthancPluginErrorCode_Success)
+      {
+        return (isReadOnly != 0);
+      }
+      else
+      {
+        errorDictionary_.LogError(code, true);
+        throw OrthancException(static_cast<ErrorCode>(code));
+      }
+    }
+
+    virtual void Start()
+    {
+    }
+
+    virtual void Stop()
+    {
+    }
+  };
+  
+
   static void CopyToMemoryBuffer(OrthancPluginMemoryBuffer& target,
                                  const void* data,
                                  size_t size)
@@ -1164,6 +1503,7 @@
     typedef std::list<OrthancPluginRefreshMetricsCallback>  RefreshMetricsCallbacks;
     typedef std::list<StorageCommitmentScp*>  StorageCommitmentScpCallbacks;
     typedef std::map<Property, std::string>  Properties;
+    typedef std::list<WebDavCollection*>  WebDavCollections;
 
     PluginsManager manager_;
 
@@ -1184,6 +1524,7 @@
     OrthancPluginReceivedInstanceCallback  receivedInstanceCallback_;  // New in Orthanc 1.10.0
     RefreshMetricsCallbacks refreshMetricsCallbacks_;
     StorageCommitmentScpCallbacks storageCommitmentScpCallbacks_;
+    WebDavCollections webDavCollections_;  // New in Orthanc 1.10.1
     std::unique_ptr<StorageAreaFactory>  storageArea_;
     std::set<std::string> authorizationTokens_;
 
@@ -1768,7 +2109,13 @@
          it != pimpl_->storageCommitmentScpCallbacks_.end(); ++it)
     {
       delete *it;
-    } 
+    }
+
+    for (PImpl::WebDavCollections::iterator it = pimpl_->webDavCollections_.begin();
+         it != pimpl_->webDavCollections_.end(); ++it)
+    {
+      delete *it;
+    }
   }
 
 
@@ -5265,6 +5612,15 @@
         return true;
       }
 
+      case _OrthancPluginService_RegisterWebDavCollection:
+      {
+        CLOG(INFO, PLUGINS) << "Plugin has registered a WebDAV collection";
+        const _OrthancPluginRegisterWebDavCollection& p =
+          *reinterpret_cast<const _OrthancPluginRegisterWebDavCollection*>(parameters);
+        pimpl_->webDavCollections_.push_back(new WebDavCollection(GetErrorDictionary(), p));
+        return true;
+      }
+
       default:
       {
         // This service is unknown to the Orthanc plugin engine
@@ -5862,4 +6218,22 @@
     boost::recursive_mutex::scoped_lock lock(pimpl_->invokeServiceMutex_);
     return pimpl_->maxDatabaseRetries_;
   }
+
+
+  void OrthancPlugins::RegisterWebDavCollections(HttpServer& target)
+  {
+    boost::recursive_mutex::scoped_lock lock(pimpl_->invokeServiceMutex_);
+
+    while (!pimpl_->webDavCollections_.empty())
+    {
+      WebDavCollection* collection = pimpl_->webDavCollections_.front();
+      assert(collection != NULL);
+
+      UriComponents components;
+      Toolbox::SplitUriComponents(components, collection->GetUri());
+      target.Register(components, collection);
+      
+      pimpl_->webDavCollections_.pop_front();
+    }
+  }
 }
--- a/OrthancServer/Plugins/Engine/OrthancPlugins.h	Mon Mar 21 15:19:42 2022 +0100
+++ b/OrthancServer/Plugins/Engine/OrthancPlugins.h	Tue Mar 22 19:11:56 2022 +0100
@@ -62,6 +62,7 @@
 
 namespace Orthanc
 {
+  class HttpServer;
   class ServerContext;
 
   class OrthancPlugins : 
@@ -89,6 +90,7 @@
     class DicomInstanceFromCallback;
     class DicomInstanceFromBuffer;
     class DicomInstanceFromTranscoded;
+    class WebDavCollection;
     
     void RegisterRestCallback(const void* parameters,
                               bool lock);
@@ -394,6 +396,8 @@
     bool IsValidAuthorizationToken(const std::string& token) const;
 
     unsigned int GetMaxDatabaseRetries() const;
+
+    void RegisterWebDavCollections(HttpServer& target);
   };
 }
 
--- a/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h	Mon Mar 21 15:19:42 2022 +0100
+++ b/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h	Tue Mar 22 19:11:56 2022 +0100
@@ -30,6 +30,7 @@
  *    - Possibly register a callback to keep/discard/modify incoming DICOM instances using OrthancPluginRegisterReceivedInstanceCallback().
  *    - Possibly register a custom transcoder for DICOM images using OrthancPluginRegisterTranscoderCallback().
  *    - Possibly register a callback to discard instances received through DICOM C-STORE using OrthancPluginRegisterIncomingCStoreInstanceFilter().
+ *    - Possibly register a callback to branch a WebDAV virtual filesystem using OrthancPluginRegisterWebDavCollection().
  * -# <tt>void OrthancPluginFinalize()</tt>:
  *    This function is invoked by Orthanc during its shutdown. The plugin
  *    must free all its memory.
@@ -119,7 +120,7 @@
 
 #define ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER     1
 #define ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER     10
-#define ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER  0
+#define ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER  1
 
 
 #if !defined(ORTHANC_PLUGINS_VERSION_IS_ABOVE)
@@ -466,6 +467,7 @@
     _OrthancPluginService_RegisterStorageArea2 = 1016,         /* New in Orthanc 1.9.0 */
     _OrthancPluginService_RegisterIncomingCStoreInstanceFilter = 1017,  /* New in Orthanc 1.10.0 */
     _OrthancPluginService_RegisterReceivedInstanceCallback = 1018,  /* New in Orthanc 1.10.0 */
+    _OrthancPluginService_RegisterWebDavCollection = 1019,     /* New in Orthanc 1.10.1 */
 
     /* Sending answers to REST calls */
     _OrthancPluginService_AnswerBuffer = 2000,
@@ -1065,7 +1067,7 @@
 
   /**
    * @brief Opaque structure that represents the HTTP connection to the client application.
-   * @ingroup Callback
+   * @ingroup Callbacks
    **/
   typedef struct _OrthancPluginRestOutput_t OrthancPluginRestOutput;
 
@@ -1372,7 +1374,7 @@
    * @param headersKeys The keys of the HTTP headers (always converted to low-case).
    * @param headersValues The values of the HTTP headers.
    * @return 0 if forbidden access, 1 if allowed access, -1 if error.
-   * @ingroup Callback
+   * @ingroup Callbacks
    * @deprecated Please instead use OrthancPluginIncomingHttpRequestFilter2()
    **/
   typedef int32_t (*OrthancPluginIncomingHttpRequestFilter) (
@@ -1408,7 +1410,7 @@
    * @param getArgumentsKeys The keys of the GET arguments (only for the GET HTTP method).
    * @param getArgumentsValues The values of the GET arguments (only for the GET HTTP method).
    * @return 0 if forbidden access, 1 if allowed access, -1 if error.
-   * @ingroup Callback
+   * @ingroup Callbacks
    **/
   typedef int32_t (*OrthancPluginIncomingHttpRequestFilter2) (
     OrthancPluginHttpMethod  method,
@@ -7410,7 +7412,7 @@
 
   /**
    * @brief Opaque structure that reads the content of a HTTP request body during a chunked HTTP transfer.
-   * @ingroup Callback
+   * @ingroup Callbacks
    **/
   typedef struct _OrthancPluginServerChunkedRequestReader_t OrthancPluginServerChunkedRequestReader;
 
@@ -7705,7 +7707,7 @@
    * @param factory Factory function that creates the handler object
    * for incoming storage commitment requests.
    * @param destructor Destructor function to destroy the handler object.
-   * @param lookup Callback method to get the status of one DICOM instance.
+   * @param lookup Callback function to get the status of one DICOM instance.
    * @return 0 if success, other value if error.
    * @ingroup DicomCallbacks
    **/
@@ -7747,7 +7749,7 @@
    * 
    * @param instance The received DICOM instance.
    * @return 0 to discard the instance, 1 to store the instance, -1 if error.
-   * @ingroup Callback
+   * @ingroup Callbacks
    **/
   typedef int32_t (*OrthancPluginIncomingDicomInstanceFilter) (
     const OrthancPluginDicomInstance* instance);
@@ -7809,7 +7811,7 @@
    * DIMSE status to be sent by the C-STORE SCP of Orthanc
    * @param instance The received DICOM instance.
    * @return 0 to discard the instance, 1 to store the instance, -1 if error.
-   * @ingroup Callback
+   * @ingroup Callbacks
    **/
   typedef int32_t (*OrthancPluginIncomingCStoreInstanceFilter) (
     uint16_t* dimseStatus /* out */,
@@ -7876,7 +7878,7 @@
    * @return `OrthancPluginReceivedInstanceAction_KeepAsIs` to accept the instance as is,<br/>
    *         `OrthancPluginReceivedInstanceAction_Modify` to store the modified DICOM contained in `modifiedDicomBuffer`,<br/>
    *         `OrthancPluginReceivedInstanceAction_Discard` to tell Orthanc to discard the instance.
-   * @ingroup Callback
+   * @ingroup Callbacks
    **/
   typedef OrthancPluginReceivedInstanceAction (*OrthancPluginReceivedInstanceCallback) (
     OrthancPluginMemoryBuffer64* modifiedDicomBuffer,
@@ -8474,7 +8476,7 @@
   
 
   /**
-   * @brief Generate a token to grant full access to the REST API of Orthanc
+   * @brief Generate a token to grant full access to the REST API of Orthanc.
    *
    * This function generates a token that can be set in the HTTP
    * header "Authorization" so as to grant full access to the REST API
@@ -8729,6 +8731,279 @@
   }
 
 
+
+  /**
+   * @brief Opaque structure that represents a WebDAV collection.
+   * @ingroup Callbacks
+   **/
+  typedef struct _OrthancPluginWebDavCollection_t OrthancPluginWebDavCollection;
+
+
+  /**
+   * @brief Declare a file while returning the content of a folder.
+   *
+   * This function declares a file while returning the content of a
+   * WebDAV folder.
+   *
+   * @param collection Context of the collection.
+   * @param name Base name of the file.
+   * @param dateTime The date and time of creation of the file.
+   * Check out the documentation of OrthancPluginWebDavRetrieveFile() for more information.
+   * @param size Size of the file.
+   * @param mimeType The MIME type of the file. If empty or set to `NULL`,
+   * Orthanc will do a best guess depending on the file extension.
+   * @return 0 if success, other value if error.
+   * @ingroup Callbacks
+   **/
+  typedef OrthancPluginErrorCode (*OrthancPluginWebDavAddFile) (
+    OrthancPluginWebDavCollection*  collection,
+    const char*                     name,
+    uint64_t                        size,
+    const char*                     mimeType,
+    const char*                     dateTime);
+
+  
+  /**
+   * @brief Declare a subfolder while returning the content of a folder.
+   *
+   * This function declares a subfolder while returning the content of a
+   * WebDAV folder.
+   *
+   * @param collection Context of the collection.
+   * @param name Base name of the subfolder.
+   * @param dateTime The date and time of creation of the subfolder.
+   * Check out the documentation of OrthancPluginWebDavRetrieveFile() for more information.
+   * @return 0 if success, other value if error.
+   * @ingroup Callbacks
+   **/
+  typedef OrthancPluginErrorCode (*OrthancPluginWebDavAddFolder) (
+    OrthancPluginWebDavCollection*  collection,
+    const char*                     name,
+    const char*                     dateTime);
+
+
+  /**
+   * @brief Retrieve the content of a file.
+   *
+   * This function is used to forward the content of a file from a
+   * WebDAV collection, to the core of Orthanc.
+   *
+   * @param collection Context of the collection.
+   * @param data Content of the file.
+   * @param size Size of the file.
+   * @param mimeType The MIME type of the file. If empty or set to `NULL`,
+   * Orthanc will do a best guess depending on the file extension.
+   * @param dateTime The date and time of creation of the file.
+   * It must be formatted as an ISO string of form
+   * `YYYYMMDDTHHMMSS,fffffffff` where T is the date-time
+   * separator. It must be expressed in UTC (it is the responsibility
+   * of the plugin to do the possible timezone
+   * conversions). Internally, this string will be parsed using
+   * `boost::posix_time::from_iso_string()`.
+   * @return 0 if success, other value if error.
+   * @ingroup Callbacks
+   **/
+  typedef OrthancPluginErrorCode (*OrthancPluginWebDavRetrieveFile) (
+    OrthancPluginWebDavCollection*  collection,
+    const void*                     data,
+    uint64_t                        size,
+    const char*                     mimeType,
+    const char*                     dateTime);
+
+  
+  /**
+   * @brief Callback for testing the existence of a folder.
+   *
+   * Signature of a callback function that tests whether the given
+   * path in the WebDAV collection exists and corresponds to a folder.
+   *
+   * @param isExisting Pointer to a Boolean that must be set to `1` if the folder exists, or `0` otherwise.
+   * @param pathSize Number of levels in the path.
+   * @param pathItems Items making the path.
+   * @param payload The user payload.
+   * @return 0 if success, other value if error.
+   * @ingroup Callbacks
+   **/
+  typedef OrthancPluginErrorCode (*OrthancPluginWebDavIsExistingFolderCallback) (
+    uint8_t*                        isExisting, /* out */
+    uint32_t                        pathSize,
+    const char* const*              pathItems,
+    void*                           payload);
+
+  
+  /**
+   * @brief Callback for listing the content of a folder.
+   *
+   * Signature of a callback function that lists the content of a
+   * folder in the WebDAV collection. The callback must call the
+   * provided `addFile()` and `addFolder()` functions to emit the
+   * content of the folder.
+   *
+   * @param isExisting Pointer to a Boolean that must be set to `1` if the folder exists, or `0` otherwise.
+   * @param collection Context to be provided to `addFile()` and `addFolder()` functions.
+   * @param addFile Function to add a file to the list.
+   * @param addFolder Function to add a folder to the list.
+   * @param pathSize Number of levels in the path.
+   * @param pathItems Items making the path.
+   * @param payload The user payload.
+   * @return 0 if success, other value if error.
+   * @ingroup Callbacks
+   **/
+  typedef OrthancPluginErrorCode (*OrthancPluginWebDavListFolderCallback) (
+    uint8_t*                        isExisting, /* out */
+    OrthancPluginWebDavCollection*  collection,
+    OrthancPluginWebDavAddFile      addFile,
+    OrthancPluginWebDavAddFolder    addFolder,
+    uint32_t                        pathSize,
+    const char* const*              pathItems,
+    void*                           payload);
+
+  
+  /**
+   * @brief Callback for retrieving the content of a file.
+   *
+   * Signature of a callback function that retrieves the content of a
+   * file in the WebDAV collection. The callback must call the
+   * provided `retrieveFile()` function to emit the actual content of
+   * the file.
+   *
+   * @param collection Context to be provided to `retrieveFile()` function.
+   * @param retrieveFile Function to return the content of the file.
+   * @param pathSize Number of levels in the path.
+   * @param pathItems Items making the path.
+   * @param payload The user payload.
+   * @return 0 if success, other value if error.
+   * @ingroup Callbacks
+   **/
+  typedef OrthancPluginErrorCode (*OrthancPluginWebDavRetrieveFileCallback) (
+    OrthancPluginWebDavCollection*  collection,
+    OrthancPluginWebDavRetrieveFile retrieveFile,
+    uint32_t                        pathSize,
+    const char* const*              pathItems,
+    void*                           payload);
+
+  
+  /**
+   * @brief Callback to store a file.
+   *
+   * Signature of a callback function that stores a file into the
+   * WebDAV collection.
+   *
+   * @param isReadOnly Pointer to a Boolean that must be set to `1` if the collection is read-only, or `0` otherwise.
+   * @param pathSize Number of levels in the path.
+   * @param pathItems Items making the path.
+   * @param data Content of the file to be stored.
+   * @param size Size of the file to be stored.
+   * @param payload The user payload.
+   * @return 0 if success, other value if error.
+   * @ingroup Callbacks
+   **/
+  typedef OrthancPluginErrorCode (*OrthancPluginWebDavStoreFileCallback) (
+    uint8_t*                        isReadOnly, /* out */
+    uint32_t                        pathSize,
+    const char* const*              pathItems,
+    const void*                     data,
+    uint64_t                        size,
+    void*                           payload);
+
+  
+  /**
+   * @brief Callback to create a folder.
+   *
+   * Signature of a callback function that creates a folder in the
+   * WebDAV collection.
+   *
+   * @param isReadOnly Pointer to a Boolean that must be set to `1` if the collection is read-only, or `0` otherwise.
+   * @param pathSize Number of levels in the path.
+   * @param pathItems Items making the path.
+   * @param payload The user payload.
+   * @return 0 if success, other value if error.
+   * @ingroup Callbacks
+   **/
+  typedef OrthancPluginErrorCode (*OrthancPluginWebDavCreateFolderCallback) (
+    uint8_t*                        isReadOnly, /* out */
+    uint32_t                        pathSize,
+    const char* const*              pathItems,
+    void*                           payload);
+
+  
+  /**
+   * @brief Callback to remove a file or a folder.
+   *
+   * Signature of a callback function that removes a file or a folder
+   * from the WebDAV collection.
+   *
+   * @param isReadOnly Pointer to a Boolean that must be set to `1` if the collection is read-only, or `0` otherwise.
+   * @param pathSize Number of levels in the path.
+   * @param pathItems Items making the path.
+   * @param payload The user payload.
+   * @return 0 if success, other value if error.
+   * @ingroup Callbacks
+   **/
+  typedef OrthancPluginErrorCode (*OrthancPluginWebDavDeleteItemCallback) (
+    uint8_t*                        isReadOnly, /* out */
+    uint32_t                        pathSize,
+    const char* const*              pathItems,
+    void*                           payload);
+
+  
+  typedef struct
+  {
+    const char*                                  uri;
+    OrthancPluginWebDavIsExistingFolderCallback  isExistingFolder;
+    OrthancPluginWebDavListFolderCallback        listFolder;
+    OrthancPluginWebDavRetrieveFileCallback      retrieveFile;
+    OrthancPluginWebDavStoreFileCallback         storeFile;
+    OrthancPluginWebDavCreateFolderCallback      createFolder;
+    OrthancPluginWebDavDeleteItemCallback        deleteItem;
+    void*                                        payload;
+  } _OrthancPluginRegisterWebDavCollection;
+
+  /**
+   * @brief Register a WebDAV virtual filesystem.
+   *
+   * This function maps a WebDAV collection onto the given URI in the
+   * REST API of Orthanc. This function must be called during the
+   * initialization of the plugin, i.e. inside the
+   * OrthancPluginInitialize() public function.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param uri URI where to map the WebDAV collection (must start with a `/` character).
+   * @param isExistingFolder Callback method to test for the existence of a folder.
+   * @param listFolder Callback method to list the content of a folder.
+   * @param retrieveFile Callback method to retrieve the content of a file.
+   * @param storeFile Callback method to store a file into the WebDAV collection.
+   * @param createFolder Callback method to create a folder.
+   * @param deleteItem Callback method to delete a file or a folder.
+   * @param payload The user payload.
+   * @return 0 if success, other value if error.
+   * @ingroup Callbacks
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginRegisterWebDavCollection(
+    OrthancPluginContext*                        context,
+    const char*                                  uri,
+    OrthancPluginWebDavIsExistingFolderCallback  isExistingFolder,
+    OrthancPluginWebDavListFolderCallback        listFolder,
+    OrthancPluginWebDavRetrieveFileCallback      retrieveFile,
+    OrthancPluginWebDavStoreFileCallback         storeFile,
+    OrthancPluginWebDavCreateFolderCallback      createFolder,
+    OrthancPluginWebDavDeleteItemCallback        deleteItem,
+    void*                                        payload)
+  {
+    _OrthancPluginRegisterWebDavCollection params;
+    params.uri = uri;
+    params.isExistingFolder = isExistingFolder;
+    params.listFolder = listFolder;
+    params.retrieveFile = retrieveFile;
+    params.storeFile = storeFile;
+    params.createFolder = createFolder;
+    params.deleteItem = deleteItem;
+    params.payload = payload;
+
+    return context->InvokeService(context, _OrthancPluginService_RegisterWebDavCollection, &params);
+  }
+  
+
 #ifdef  __cplusplus
 }
 #endif
--- a/OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp	Mon Mar 21 15:19:42 2022 +0100
+++ b/OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp	Tue Mar 22 19:11:56 2022 +0100
@@ -2526,7 +2526,7 @@
         }
         catch (...)
         {
-          return OrthancPluginErrorCode_InternalError;
+          return OrthancPluginErrorCode_Plugin;
         }
       }
     }    
@@ -3548,4 +3548,238 @@
     }
   }
 #endif
+
+
+#if HAS_ORTHANC_PLUGIN_WEBDAV == 1
+  static std::vector<std::string> WebDavConvertPath(uint32_t pathSize,
+                                                    const char* const*  pathItems)
+  {
+    std::vector<std::string> result(pathSize);
+
+    for (uint32_t i = 0; i < pathSize; i++)
+    {
+      result[i] = pathItems[i];
+    }
+
+    return result;
+  }
+#endif
+  
+    
+#if HAS_ORTHANC_PLUGIN_WEBDAV == 1
+  static OrthancPluginErrorCode WebDavIsExistingFolder(uint8_t*            isExisting,
+                                                       uint32_t            pathSize,
+                                                       const char* const*  pathItems,
+                                                       void*               payload)
+  {
+    IWebDavCollection& that = *reinterpret_cast<IWebDavCollection*>(payload);
+
+    try
+    {
+      *isExisting = (that.IsExistingFolder(WebDavConvertPath(pathSize, pathItems)) ? 1 : 0);
+      return OrthancPluginErrorCode_Success;
+    }
+    catch (ORTHANC_PLUGINS_EXCEPTION_CLASS& e)
+    {
+      return static_cast<OrthancPluginErrorCode>(e.GetErrorCode());
+    }
+    catch (...)
+    {
+      return OrthancPluginErrorCode_Plugin;
+    }
+  }
+#endif
+
+  
+#if HAS_ORTHANC_PLUGIN_WEBDAV == 1
+  static OrthancPluginErrorCode WebDavListFolder(uint8_t*                        isExisting,
+                                                 OrthancPluginWebDavCollection*  collection,
+                                                 OrthancPluginWebDavAddFile      addFile,
+                                                 OrthancPluginWebDavAddFolder    addFolder,
+                                                 uint32_t                        pathSize,
+                                                 const char* const*              pathItems,
+                                                 void*                           payload)
+  {
+    IWebDavCollection& that = *reinterpret_cast<IWebDavCollection*>(payload);
+      
+    try
+    {
+      std::list<IWebDavCollection::FileInfo> files;
+      std::list<IWebDavCollection::FolderInfo> subfolders;
+      
+      if (!that.ListFolder(files, subfolders, WebDavConvertPath(pathSize, pathItems)))
+      {
+        *isExisting = 0;
+      }
+      else
+      {
+        *isExisting = 1;
+      
+        for (std::list<IWebDavCollection::FileInfo>::const_iterator
+               it = files.begin(); it != files.end(); ++it)
+        {
+          OrthancPluginErrorCode code = addFile(
+            collection, it->GetName().c_str(), it->GetContentSize(),
+            it->GetMimeType().c_str(), it->GetDateTime().c_str());
+        
+          if (code != OrthancPluginErrorCode_Success)
+          {
+            return code;
+          }
+        }
+      
+        for (std::list<IWebDavCollection::FolderInfo>::const_iterator it =
+               subfolders.begin(); it != subfolders.end(); ++it)
+        {
+          OrthancPluginErrorCode code = addFolder(
+            collection, it->GetName().c_str(), it->GetDateTime().c_str());
+        
+          if (code != OrthancPluginErrorCode_Success)
+          {
+            return code;
+          }
+        }
+      }
+      
+      return OrthancPluginErrorCode_Success;
+    }
+    catch (ORTHANC_PLUGINS_EXCEPTION_CLASS& e)
+    {
+      return static_cast<OrthancPluginErrorCode>(e.GetErrorCode());
+    }
+    catch (...)
+    {
+      return OrthancPluginErrorCode_Plugin;
+    }
+  }
+#endif    
+
+
+#if HAS_ORTHANC_PLUGIN_WEBDAV == 1
+  static OrthancPluginErrorCode WebDavRetrieveFile(OrthancPluginWebDavCollection*   collection,
+                                                   OrthancPluginWebDavRetrieveFile  retrieveFile,
+                                                   uint32_t                         pathSize,
+                                                   const char* const*               pathItems,
+                                                   void*                            payload)
+  {
+    IWebDavCollection& that = *reinterpret_cast<IWebDavCollection*>(payload);
+
+    try
+    {
+      std::string content, mime, dateTime;
+        
+      if (that.GetFile(content, mime, dateTime, WebDavConvertPath(pathSize, pathItems)))
+      {
+        return retrieveFile(collection, content.empty() ? NULL : content.c_str(),
+                            content.size(), mime.c_str(), dateTime.c_str());
+      }
+      else
+      {
+        // Inexisting file
+        return OrthancPluginErrorCode_Success;
+      }
+    }
+    catch (ORTHANC_PLUGINS_EXCEPTION_CLASS& e)
+    {
+      return static_cast<OrthancPluginErrorCode>(e.GetErrorCode());
+    }
+    catch (...)
+    {
+      return OrthancPluginErrorCode_InternalError;
+    }
+  }  
+#endif
+
+
+#if HAS_ORTHANC_PLUGIN_WEBDAV == 1
+  static OrthancPluginErrorCode WebDavStoreFileCallback(uint8_t*            isReadOnly, /* out */
+                                                        uint32_t            pathSize,
+                                                        const char* const*  pathItems,
+                                                        const void*         data,
+                                                        uint64_t            size,
+                                                        void*               payload)
+  {
+    IWebDavCollection& that = *reinterpret_cast<IWebDavCollection*>(payload);
+
+    try
+    {
+      *isReadOnly = (that.StoreFile(WebDavConvertPath(pathSize, pathItems), data, size) ? 1 : 0);
+      return OrthancPluginErrorCode_Success;
+    }
+    catch (ORTHANC_PLUGINS_EXCEPTION_CLASS& e)
+    {
+      return static_cast<OrthancPluginErrorCode>(e.GetErrorCode());
+    }
+    catch (...)
+    {
+      return OrthancPluginErrorCode_InternalError;
+    }
+  }
+#endif
+
+  
+#if HAS_ORTHANC_PLUGIN_WEBDAV == 1
+  static OrthancPluginErrorCode WebDavCreateFolderCallback(uint8_t*            isReadOnly, /* out */
+                                                           uint32_t            pathSize,
+                                                           const char* const*  pathItems,
+                                                           void*               payload)
+  {
+    IWebDavCollection& that = *reinterpret_cast<IWebDavCollection*>(payload);
+
+    try
+    {
+      *isReadOnly = (that.CreateFolder(WebDavConvertPath(pathSize, pathItems)) ? 1 : 0);
+      return OrthancPluginErrorCode_Success;
+    }
+    catch (ORTHANC_PLUGINS_EXCEPTION_CLASS& e)
+    {
+      return static_cast<OrthancPluginErrorCode>(e.GetErrorCode());
+    }
+    catch (...)
+    {
+      return OrthancPluginErrorCode_InternalError;
+    }
+  }
+#endif
+  
+  
+#if HAS_ORTHANC_PLUGIN_WEBDAV == 1
+  static OrthancPluginErrorCode WebDavDeleteItemCallback(uint8_t*            isReadOnly, /* out */
+                                                         uint32_t            pathSize,
+                                                         const char* const*  pathItems,
+                                                         void*               payload)
+  {
+    IWebDavCollection& that = *reinterpret_cast<IWebDavCollection*>(payload);
+
+    try
+    {
+      *isReadOnly = (that.DeleteItem(WebDavConvertPath(pathSize, pathItems)) ? 1 : 0);
+      return OrthancPluginErrorCode_Success;
+    }
+    catch (ORTHANC_PLUGINS_EXCEPTION_CLASS& e)
+    {
+      return static_cast<OrthancPluginErrorCode>(e.GetErrorCode());
+    }
+    catch (...)
+    {
+      return OrthancPluginErrorCode_InternalError;
+    }
+  }
+#endif
+
+  
+#if HAS_ORTHANC_PLUGIN_WEBDAV == 1
+  void IWebDavCollection::Register(const std::string& uri,
+                                   IWebDavCollection& collection)
+  {
+    OrthancPluginErrorCode code = OrthancPluginRegisterWebDavCollection(
+      GetGlobalContext(), uri.c_str(), WebDavIsExistingFolder, WebDavListFolder, WebDavRetrieveFile,
+      WebDavStoreFileCallback, WebDavCreateFolderCallback, WebDavDeleteItemCallback, &collection);
+
+    if (code != OrthancPluginErrorCode_Success)
+    {
+      ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code);
+    }
+  }
+#endif
 }
--- a/OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.h	Mon Mar 21 15:19:42 2022 +0100
+++ b/OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.h	Tue Mar 22 19:11:56 2022 +0100
@@ -115,6 +115,12 @@
 #  define HAS_ORTHANC_PLUGIN_STORAGE_COMMITMENT_SCP  0
 #endif
 
+#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 10, 1)
+#  define HAS_ORTHANC_PLUGIN_WEBDAV  1
+#else
+#  define HAS_ORTHANC_PLUGIN_WEBDAV  0
+#endif
+
 
 
 namespace OrthancPlugins
@@ -1248,4 +1254,107 @@
                                     const std::string& transferSyntax);
 #endif
   };
+
+
+
+#if HAS_ORTHANC_PLUGIN_WEBDAV == 1
+  class IWebDavCollection : public boost::noncopyable
+  {
+  public:
+    class FileInfo
+    {
+    private:
+      std::string  name_;
+      uint64_t     contentSize_;
+      std::string  mime_;
+      std::string  dateTime_;
+
+    public:
+      FileInfo(const std::string& name,
+               uint64_t contentSize,
+               const std::string& dateTime) :
+        name_(name),
+        contentSize_(contentSize),
+        dateTime_(dateTime)
+      {
+      }
+
+      const std::string& GetName() const
+      {
+        return name_;
+      }
+
+      uint64_t GetContentSize() const
+      {
+        return contentSize_;
+      }
+
+      void SetMimeType(const std::string& mime)
+      {
+        mime_ = mime;
+      }
+
+      const std::string& GetMimeType() const
+      {
+        return mime_;
+      }
+
+      const std::string& GetDateTime() const
+      {
+        return dateTime_;
+      }
+    };
+  
+    class FolderInfo
+    {
+    private:
+      std::string  name_;
+      std::string  dateTime_;
+
+    public:
+      FolderInfo(const std::string& name,
+                 const std::string& dateTime) :
+        name_(name),
+        dateTime_(dateTime)
+      {
+      }
+
+      const std::string& GetName() const
+      {
+        return name_;
+      }
+
+      const std::string& GetDateTime() const
+      {
+        return dateTime_;
+      }
+    };
+  
+    virtual ~IWebDavCollection()
+    {
+    }
+
+    virtual bool IsExistingFolder(const std::vector<std::string>& path) = 0;
+
+    virtual bool ListFolder(std::list<FileInfo>& files,
+                            std::list<FolderInfo>& subfolders,
+                            const std::vector<std::string>& path) = 0;
+  
+    virtual bool GetFile(std::string& content /* out */,
+                         std::string& mime /* out */,
+                         std::string& dateTime /* out */,
+                         const std::vector<std::string>& path) = 0;
+
+    virtual bool StoreFile(const std::vector<std::string>& path,
+                           const void* data,
+                           size_t size) = 0;
+
+    virtual bool CreateFolder(const std::vector<std::string>& path) = 0;
+
+    virtual bool DeleteItem(const std::vector<std::string>& path) = 0;
+
+    static void Register(const std::string& uri,
+                         IWebDavCollection& collection);
+  };
+#endif
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Samples/WebDavFilesystem/CMakeLists.txt	Tue Mar 22 19:11:56 2022 +0100
@@ -0,0 +1,42 @@
+# 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/>.
+
+
+cmake_minimum_required(VERSION 2.8)
+
+project(WebDavFilesystem)
+
+SET(STATIC_BUILD OFF CACHE BOOL "Static build of the third-party libraries (necessary for Windows)")
+SET(ALLOW_DOWNLOADS OFF CACHE BOOL "Allow CMake to download packages")
+
+SET(USE_SYSTEM_JSONCPP ON CACHE BOOL "Use the system version of JsonCpp")
+SET(USE_SYSTEM_BOOST ON CACHE BOOL "Use the system version of boost")
+
+include(${CMAKE_SOURCE_DIR}/../Common/OrthancPlugins.cmake)
+include(${CMAKE_SOURCE_DIR}/../../../../OrthancFramework/Resources/CMake/JsonCppConfiguration.cmake)
+include(${CMAKE_SOURCE_DIR}/../../../../OrthancFramework/Resources/CMake/BoostConfiguration.cmake)
+
+add_definitions(-DHAS_ORTHANC_EXCEPTION=0)
+
+add_library(WebDavFilesystem SHARED
+  Plugin.cpp
+  ${CMAKE_SOURCE_DIR}/../Common/OrthancPluginCppWrapper.cpp
+  ${JSONCPP_SOURCES}
+  ${BOOST_SOURCES}
+  )
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Samples/WebDavFilesystem/Plugin.cpp	Tue Mar 22 19:11:56 2022 +0100
@@ -0,0 +1,390 @@
+/**
+ * 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 "../Common/OrthancPluginCppWrapper.h"
+
+#include <boost/thread/mutex.hpp>
+
+
+class Resource : public boost::noncopyable
+{
+private:
+  boost::posix_time::ptime  dateTime_;
+
+public:
+  Resource() :
+    dateTime_(boost::posix_time::second_clock::universal_time())      
+  {
+  }
+    
+  virtual ~Resource()
+  {
+  }
+
+  const boost::posix_time::ptime& GetDateTime() const
+  {
+    return dateTime_;
+  }
+
+  virtual bool IsFolder() const = 0;
+
+  virtual Resource* LookupPath(const std::vector<std::string>& path) = 0;
+};
+
+
+class File : public Resource
+{
+private:
+  std::string  content_;
+    
+public:
+  File(const void* data,
+       size_t size) :
+    content_(reinterpret_cast<const char*>(data), size)
+  {
+  }
+
+  const std::string& GetContent() const
+  {
+    return content_;
+  }
+
+  virtual bool IsFolder() const
+  {
+    return false;
+  }
+    
+  virtual Resource* LookupPath(const std::vector<std::string>& path)
+  {
+    if (path.empty())
+    {
+      return this;
+    }
+    else
+    {
+      return NULL;
+    }
+  }
+};
+
+
+class Folder : public Resource
+{
+private:
+  typedef std::map<std::string, Resource*>  Content;
+
+  Content content_;
+
+public:
+  virtual ~Folder()
+  {
+    for (Content::iterator it = content_.begin(); it != content_.end(); ++it)
+    {
+      assert(it->second != NULL);
+      delete it->second;
+    }
+  }
+
+  virtual bool IsFolder() const
+  {
+    return true;
+  }
+
+  virtual Resource* LookupPath(const std::vector<std::string>& path)
+  {
+    if (path.empty())
+    {
+      return this;
+    }
+    else
+    {
+      Content::const_iterator found = content_.find(path[0]);
+      if (found == content_.end())
+      {
+        return NULL;
+      }
+      else
+      {          
+        std::vector<std::string> childPath(path.size() - 1);
+          
+        for (size_t i = 0; i < childPath.size(); i++)
+        {
+          childPath[i] = path[i + 1];
+        }
+          
+        return found->second->LookupPath(childPath);
+      }
+    }
+  }
+
+  void ListContent(std::list<OrthancPlugins::IWebDavCollection::FileInfo>& files,
+                   std::list<OrthancPlugins::IWebDavCollection::FolderInfo>& subfolders) const
+  {
+    for (Content::const_iterator it = content_.begin(); it != content_.end(); ++it)
+    {
+      assert(it->second != NULL);
+
+      const std::string dateTime = boost::posix_time::to_iso_string(it->second->GetDateTime());
+        
+      if (it->second->IsFolder())
+      {
+        subfolders.push_back(OrthancPlugins::IWebDavCollection::FolderInfo(it->first, dateTime));
+      }
+      else
+      {
+        const File& f = dynamic_cast<const File&>(*it->second);
+        files.push_back(OrthancPlugins::IWebDavCollection::FileInfo(it->first, f.GetContent().size(), dateTime));
+      }
+    }
+  }
+
+  void StoreFile(const std::string& name,
+                 File* f)
+  {
+    std::unique_ptr<File> protection(f);
+
+    if (content_.find(name) != content_.end())
+    {
+      OrthancPlugins::LogError("Already existing: " + name);
+      ORTHANC_PLUGINS_THROW_EXCEPTION(BadRequest);
+    }
+    else
+    {
+      content_[name] = protection.release();
+    }
+  }
+
+  void CreateSubfolder(const std::string& name)
+  {
+    if (content_.find(name) != content_.end())
+    {
+      OrthancPlugins::LogError("Already existing: " + name);
+      ORTHANC_PLUGINS_THROW_EXCEPTION(BadRequest);
+    }
+    else
+    {
+      content_[name] = new Folder;
+    }
+  }
+
+  void DeleteItem(const std::string& name)
+  {
+    Content::iterator found = content_.find(name);
+
+    if (found == content_.end())
+    {
+      OrthancPlugins::LogError("Cannot delete inexistent path: " + name);
+      ORTHANC_PLUGINS_THROW_EXCEPTION(InexistentItem);
+    }
+    else
+    {
+      assert(found->second != NULL);
+      delete found->second;
+      content_.erase(found);
+    }
+  }
+};
+
+
+class WebDavFilesystem : public OrthancPlugins::IWebDavCollection
+{
+private:
+  boost::mutex               mutex_;
+  std::unique_ptr<Resource>  root_;
+
+  static std::vector<std::string> GetParentPath(const std::vector<std::string>& path)
+  {
+    if (path.empty())
+    {
+      OrthancPlugins::LogError("Empty path");
+      ORTHANC_PLUGINS_THROW_EXCEPTION(ParameterOutOfRange);
+    }
+    else
+    {
+      std::vector<std::string> p(path.size() - 1);
+          
+      for (size_t i = 0; i < p.size(); i++)
+      {
+        p[i] = path[i];
+      }
+
+      return p;
+    }
+  }
+
+public:
+  WebDavFilesystem() :
+    root_(new Folder)
+  {
+  }
+  
+  virtual bool IsExistingFolder(const std::vector<std::string>& path)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+    
+    Resource* resource = root_->LookupPath(path);
+    return (resource != NULL &&
+            resource->IsFolder());
+  }
+
+  virtual bool ListFolder(std::list<FileInfo>& files,
+                          std::list<FolderInfo>& subfolders,
+                          const std::vector<std::string>& path)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+    
+    Resource* resource = root_->LookupPath(path);
+    if (resource != NULL &&
+        resource->IsFolder())
+    {
+      dynamic_cast<Folder&>(*resource).ListContent(files, subfolders);
+      return true;
+    }
+    else
+    {
+      return false;
+    }
+  }
+  
+  virtual bool GetFile(std::string& content /* out */,
+                       std::string& mime /* out */,
+                       std::string& dateTime /* out */,
+                       const std::vector<std::string>& path)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+    
+    Resource* resource = root_->LookupPath(path);
+    if (resource != NULL &&
+        !resource->IsFolder())
+    {
+      const File& file = dynamic_cast<const File&>(*resource);
+      content = file.GetContent();
+      mime = "";  // Let the Orthanc core autodetect the MIME type
+      dateTime = boost::posix_time::to_iso_string(file.GetDateTime());
+      return true;
+    }
+    else
+    {
+      return false;
+    }
+  }
+
+  virtual bool StoreFile(const std::vector<std::string>& path,
+                         const void* data,
+                         size_t size)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+    
+    Resource* parent = root_->LookupPath(GetParentPath(path));
+    if (parent != NULL &&
+        parent->IsFolder())
+    {
+      dynamic_cast<Folder&>(*parent).StoreFile(path.back(), new File(data, size));
+      return true;
+    }
+    else
+    {
+      return false;
+    }
+  }
+  
+  virtual bool CreateFolder(const std::vector<std::string>& path)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+    
+    Resource* parent = root_->LookupPath(GetParentPath(path));
+    if (parent != NULL &&
+        parent->IsFolder())
+    {
+      dynamic_cast<Folder&>(*parent).CreateSubfolder(path.back());
+      return true;
+    }
+    else
+    {
+      return false;
+    }
+  }
+
+  virtual bool DeleteItem(const std::vector<std::string>& path)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+    
+    Resource* parent = root_->LookupPath(GetParentPath(path));
+    if (parent != NULL &&
+        parent->IsFolder())
+    {
+      dynamic_cast<Folder&>(*parent).DeleteItem(path.back());
+      return true;
+    }
+    else
+    {
+      return false;
+    }    
+  }
+};
+
+
+
+extern "C"
+{
+  ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* c)
+  {
+    OrthancPlugins::SetGlobalContext(c);
+    OrthancPluginLogWarning(c, "WebDAV plugin is initializing");
+
+    /* Check the version of the Orthanc core */
+    if (OrthancPluginCheckVersion(c) == 0)
+    {
+      char info[1024];
+      sprintf(info, "Your version of Orthanc (%s) must be above %d.%d.%d to run this plugin",
+              c->orthancVersion,
+              ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER,
+              ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER,
+              ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER);
+      OrthancPluginLogError(c, info);
+      return -1;
+    }
+
+    static WebDavFilesystem filesystem;
+    OrthancPlugins::IWebDavCollection::Register("/webdav-plugin", filesystem);
+
+    return 0;
+  }
+
+
+  ORTHANC_PLUGINS_API void OrthancPluginFinalize()
+  {
+    OrthancPluginLogWarning(OrthancPlugins::GetGlobalContext(), "WebDAV plugin is finalizing");
+  }
+
+
+  ORTHANC_PLUGINS_API const char* OrthancPluginGetName()
+  {
+    return "webdav-sample";
+  }
+
+
+  ORTHANC_PLUGINS_API const char* OrthancPluginGetVersion()
+  {
+    return "0.0";
+  }
+}
--- a/OrthancServer/Sources/main.cpp	Mon Mar 21 15:19:42 2022 +0100
+++ b/OrthancServer/Sources/main.cpp	Tue Mar 22 19:11:56 2022 +0100
@@ -430,29 +430,53 @@
       }
       else
       {
-        // If there are multiple modalities with the same AET, consider the one matching this IP
+        // If there are multiple modalities with the same AET, consider the one matching this IP 
+        // or check if the operation is allowed for all modalities
+        bool allowedForAllModalities = true;
+
         for (std::list<RemoteModalityParameters>::const_iterator
                it = modalities.begin(); it != modalities.end(); ++it)
         {
-          if (it->GetHost() == remoteIp)
+          if (it->IsRequestAllowed(type))
           {
-            if (it->IsRequestAllowed(type))
+            if (checkIp &&
+                it->GetHost() == remoteIp)
             {
               return true;
             }
-            else
-            {
-              ReportDisallowedCommand(remoteIp, remoteAet, type);
-              return false;
-            }
+          }
+          else
+          {
+            allowedForAllModalities = false;
           }
         }
 
-        LOG(WARNING) << "DICOM authorization rejected for AET " << remoteAet
-                     << " on IP " << remoteIp << ": " << modalities.size()
-                     << " modalites found with this AET in configuration option "
-                     << "\"DicomModalities\", but none of them matches the IP";
-        return false;
+        if (allowedForAllModalities)
+        {
+          return true;
+        }
+        else
+        {
+          ReportDisallowedCommand(remoteIp, remoteAet, type);
+
+          if (checkIp)
+          {
+            LOG(WARNING) << "DICOM authorization rejected for AET " << remoteAet
+                         << " on IP " << remoteIp << ": " << modalities.size()
+                         << " modalites found with this AET in configuration option "
+                         << "\"DicomModalities\", but the operation is allowed for none "
+                         << "of them matching the IP";
+          }
+          else
+          {
+            LOG(WARNING) << "DICOM authorization rejected for AET " << remoteAet
+                         << " on IP " << remoteIp << ": " << modalities.size()
+                         << " modalites found with this AET in configuration option "
+                         << "\"DicomModalities\", but the operation is not allowed for"
+                         << "all of them";
+          }
+          return false;
+        }
       }
     }
   }
@@ -1157,6 +1181,13 @@
       }
     }
 
+#if ORTHANC_ENABLE_PLUGINS == 1
+    if (plugins != NULL)
+    {
+      plugins->RegisterWebDavCollections(httpServer);
+    }
+#endif
+
     MyHttpExceptionFormatter exceptionFormatter(httpDescribeErrors, plugins);
         
     httpServer.SetIncomingHttpRequestFilter(httpFilter);
--- a/TODO	Mon Mar 21 15:19:42 2022 +0100
+++ b/TODO	Tue Mar 22 19:11:56 2022 +0100
@@ -28,8 +28,10 @@
 * (1) Accept extra DICOM tags dictionaries in the DCMTK format '.dic' (easier to use than declare
   them in the Orthanc configuration file).  Even the standard dictionaries could be 
   overriden by these custom dictionaries.
-* provide more flexibility wrt Dicom TLS ciphers and TLS version:
+* Provide more flexibility wrt Dicom TLS ciphers and TLS version:
   https://groups.google.com/g/orthanc-users/c/X4IhmXCSr7I/m/EirawAFcBwAJ
+* Provide a configuration option to index additional tags as "MainDicomTags":
+  https://groups.google.com/g/orthanc-users/c/04oPNMHMzfE/m/kgHCroPWAQAJ
 
 
 ============================