[med-svn] [Git][med-team/orthanc][upstream] New upstream version 1.10.1+dfsg

Sebastien Jodogne (@jodogne-guest) gitlab at salsa.debian.org
Wed Mar 23 21:14:41 GMT 2022



Sebastien Jodogne pushed to branch upstream at Debian Med / orthanc


Commits:
c9bda5f9 by jodogne-guest at 2022-03-23T21:05:49+01:00
New upstream version 1.10.1+dfsg
- - - - -


30 changed files:

- .hg_archival.txt
- CITATION.cff
- LinuxCompilation.txt
- NEWS
- OrthancFramework/Resources/CMake/DownloadOrthancFramework.cmake
- OrthancFramework/Resources/CMake/EmscriptenParameters.cmake
- OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake
- OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.cpp
- OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h
- OrthancFramework/Sources/DicomParsing/IDicomTranscoder.cpp
- OrthancFramework/Sources/DicomParsing/ParsedDicomFile.cpp
- OrthancFramework/Sources/DicomParsing/ParsedDicomFile.h
- OrthancFramework/Sources/HttpServer/IWebDavBucket.h
- OrthancFramework/Sources/Images/ImageProcessing.cpp
- OrthancFramework/Sources/Images/ImageProcessing.h
- OrthancFramework/Sources/SystemToolbox.cpp
- OrthancFramework/UnitTestsSources/FromDcmtkTests.cpp
- OrthancFramework/UnitTestsSources/ImageProcessingTests.cpp
- OrthancServer/OrthancExplorer/libs/jquery-file-upload/js/jquery.fileupload.js
- OrthancServer/OrthancExplorer/libs/jquery.blockui.js
- OrthancServer/Plugins/Engine/OrthancPlugins.cpp
- OrthancServer/Plugins/Engine/OrthancPlugins.h
- OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h
- OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp
- OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.h
- + OrthancServer/Plugins/Samples/WebDavFilesystem/CMakeLists.txt
- + OrthancServer/Plugins/Samples/WebDavFilesystem/Plugin.cpp
- OrthancServer/Resources/Configuration.json
- OrthancServer/Sources/main.cpp
- TODO


Changes:

=====================================
.hg_archival.txt
=====================================
@@ -1,6 +1,6 @@
 repo: 3959d33612ccaadc0d4d707227fbed09ac35e5fe
-node: 4dae452af79fb90a76638006116203a92743c56a
-branch: Orthanc-1.10.0
+node: 7ae27bc1f4bb6ec01cb99e11c778a8ae786f8c5c
+branch: Orthanc-1.10.1
 latesttag: toa2020012703
-latesttagdistance: 1048
-changessincelatesttag: 1262
+latesttagdistance: 1072
+changessincelatesttag: 1286


=====================================
CITATION.cff
=====================================
@@ -10,5 +10,5 @@ authors:
 doi: "10.1007/s10278-018-0082-y"
 license: "GPL-3.0-or-later"
 repository-code: "https://hg.orthanc-server.com/orthanc/"
-version: 1.10.0
-date-released: 2022-02-23
+version: 1.10.1
+date-released: 2022-03-23


=====================================
LinuxCompilation.txt
=====================================
@@ -247,9 +247,15 @@ Additional information
 
 * It has been reported that distributions coming with Boost >= 1.70.0
   might need the option "-DBoost_NO_BOOST_CMAKE=ON" to be added to the
-  "cmake" command-line.
+  "cmake" command line.
   https://groups.google.com/d/msg/orthanc-users/nXq2qOndw9c/0PGnaOqiAgAJ
-  
+
+* Starting with Orthanc 1.10.0, if you use a distribution with an old
+  version of gcc (typically gcc 4.8 on CentOS), you might have to add
+  the option "-DCMAKE_CXX_FLAGS=-std=c++11" when invoking the "cmake"
+  command line. This flag was previously automatically added, but this
+  feature was removed according to the following discussion:
+  https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1000222
 
 
 


=====================================
NEWS
=====================================
@@ -2,6 +2,29 @@ Pending changes in the mainline
 ===============================
 
 
+Version 1.10.1 (2022-03-23)
+===========================
+
+General
+-------
+
+* Improved DICOM authorization checks when multiple modalities are
+  declared with the same AET.
+
+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.
+
+
 Version 1.10.0 (2022-02-23)
 ===========================
 
@@ -58,7 +81,9 @@ Lua
 Plugins
 -------
 
-* New function in the SDK: OrthancPluginRegisterIncomingCStoreInstanceFilter()
+* New functions in the SDK:
+  - OrthancPluginRegisterIncomingCStoreInstanceFilter()
+  - OrthancPluginRegisterReceivedInstanceCallback()
 
 Maintenance
 -----------


=====================================
OrthancFramework/Resources/CMake/DownloadOrthancFramework.cmake
=====================================
@@ -142,6 +142,8 @@ if (ORTHANC_FRAMEWORK_SOURCE STREQUAL "hg" OR
         set(ORTHANC_FRAMEWORK_MD5 "4b5d05683d747c29b2860ad79d11e62e")
       elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.9.7")
         set(ORTHANC_FRAMEWORK_MD5 "c912bbb860d640d3ae3003b5c9698205")
+      elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.10.0")
+        set(ORTHANC_FRAMEWORK_MD5 "8610c82d9153f22e929f2110f8f60279")
 
       # Below this point are development snapshots that were used to
       # release some plugin, before an official release of the Orthanc


=====================================
OrthancFramework/Resources/CMake/EmscriptenParameters.cmake
=====================================
@@ -25,10 +25,23 @@ if (NOT "${EMSCRIPTEN_TRAP_MODE}" STREQUAL "")
   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")


=====================================
OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake
=====================================
@@ -24,7 +24,7 @@
 #####################################################################
 
 # Version of the build, should always be "mainline" except in release branches
-set(ORTHANC_VERSION "1.10.0")
+set(ORTHANC_VERSION "1.10.1")
 
 # Version of the database schema. History:
 #   * Orthanc 0.1.0 -> Orthanc 0.3.0 = no versioning


=====================================
OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.cpp
=====================================
@@ -2601,18 +2601,10 @@ namespace Orthanc
               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
           {
@@ -3234,6 +3226,49 @@ namespace Orthanc
     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;
+    }
+  }
 }
 
 


=====================================
OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h
=====================================
@@ -283,5 +283,9 @@ namespace Orthanc
                                    DcmDataset& dataset,
                                    const DicomPath& path,
                                    size_t sequenceIndex);
+
+    static bool LookupStringValue(std::string& target,
+                                  DcmDataset& dataset,
+                                  const DicomTag& key);
   };
 }


=====================================
OrthancFramework/Sources/DicomParsing/IDicomTranscoder.cpp
=====================================
@@ -75,13 +75,11 @@ namespace Orthanc
     }
     
     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
     {


=====================================
OrthancFramework/Sources/DicomParsing/ParsedDicomFile.cpp
=====================================
@@ -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,175 @@ namespace Orthanc
   }
 
 
+  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;
+    DcmElement* overlayElement = NULL;
+    Uint8* overlayData = NULL;
+    
+    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.findAndGetElement(DcmTagKey(group, 0x3000), overlayElement).good() &&
+        overlayElement != NULL &&
+        overlayElement->getUint8Array(overlayData).good() &&
+        overlayData != NULL)
+    {
+      /**
+       * WARNING - It might seem easier to use
+       * "dataset.findAndGetUint8Array()" that directly gives the size
+       * of the overlay data (using the "count" parameter), instead of
+       * "dataset.findAndGetElement()". Unfortunately, this does *not*
+       * work with Emscripten/WebAssembly, that reports a "count" that
+       * is half the number of bytes, presumably because of
+       * discrepancies in the way sizeof are computed inside DCMTK.
+       * The method "getLengthField()" reports the correct number of
+       * bytes, even if targeting WebAssembly.
+       **/
+
+      unsigned int expectedSize = Ceiling(rows * columns, 8);
+      if (overlayElement->getLengthField() < 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,


=====================================
OrthancFramework/Sources/DicomParsing/ParsedDicomFile.h
=====================================
@@ -300,5 +300,14 @@ namespace Orthanc
     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;
   };
 }


=====================================
OrthancFramework/Sources/HttpServer/IWebDavBucket.h
=====================================
@@ -117,8 +117,6 @@ namespace Orthanc
         return mime_;
       }
 
-      void SetCreated(bool created);
-
       virtual void Format(pugi::xml_node& node,
                           const std::string& parentPath) const ORTHANC_OVERRIDE;
     };


=====================================
OrthancFramework/Sources/Images/ImageProcessing.cpp
=====================================
@@ -2964,4 +2964,78 @@ namespace Orthanc
         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);
+    }
+  }
 }


=====================================
OrthancFramework/Sources/Images/ImageProcessing.h
=====================================
@@ -218,5 +218,8 @@ namespace Orthanc
     static void ConvertJpegYCbCrToRgb(ImageAccessor& image /* inplace */);
 
     static void SwapEndianness(ImageAccessor& image /* inplace */);
+
+    static void Maximum(ImageAccessor& image /* inout */,
+                        const ImageAccessor& other);
   };
 }


=====================================
OrthancFramework/Sources/SystemToolbox.cpp
=====================================
@@ -817,6 +817,14 @@ namespace Orthanc
     {
       return MimeType_Ico;
     }
+    else if (extension == ".gz")
+    {
+      return MimeType_Gzip;
+    }
+    else if (extension == ".zip")
+    {
+      return MimeType_Zip;
+    }
 
     // Default type
     else


=====================================
OrthancFramework/UnitTestsSources/FromDcmtkTests.cpp
=====================================
@@ -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"
 


=====================================
OrthancFramework/UnitTestsSources/ImageProcessingTests.cpp
=====================================
@@ -1247,7 +1247,7 @@ TEST(ImageProcessing, FillPolygon)
     ASSERT_EQ(6u, segments.GetSize());
     for (size_t i = 0; i < segments.GetSize(); i++)
     {
-      ASSERT_EQ(100 + i, segments.GetY(i));
+      ASSERT_EQ(100 + static_cast<int>(i), segments.GetY(i));
       ASSERT_EQ(10, segments.GetX1(i));
       ASSERT_EQ(10, segments.GetX2(i));
     }
@@ -1311,7 +1311,7 @@ TEST(ImageProcessing, FillPolygon)
 
     for (size_t i = 0; i < segments.GetSize(); i++)
     {
-      ASSERT_EQ(i + 50, segments.GetY(i));
+      ASSERT_EQ(50 + static_cast<int>(i), segments.GetY(i));
       ASSERT_EQ(10, segments.GetX1(i));
       ASSERT_EQ(200, segments.GetX2(i));
     }


=====================================
OrthancServer/OrthancExplorer/libs/jquery-file-upload/js/jquery.fileupload.js
=====================================
@@ -278,7 +278,7 @@
         _initProgressListener: function (options) {
             var that = this,
                 xhr = options.xhr ? options.xhr() : $.ajaxSettings.xhr();
-            // Accesss to the native XHR object is required to add event listeners
+            // Access to the native XHR object is required to add event listeners
             // for the upload progress event:
             if (xhr.upload) {
                 $(xhr.upload).bind('progress', function (e) {


=====================================
OrthancServer/OrthancExplorer/libs/jquery.blockui.js
=====================================
@@ -145,7 +145,7 @@ $.blockUI.defaults = {
 	// enable if you want key and mouse events to be disabled for content that is blocked
 	bindEvents: true,
 
-	// be default blockUI will supress tab navigation from leaving blocking content
+	// be default blockUI will suppress tab navigation from leaving blocking content
 	// (if bindEvents is true)
 	constrainTabKey: true,
 
@@ -221,7 +221,7 @@ function install(el, opts) {
 	var z = opts.baseZ;
 
 	// blockUI uses 3 layers for blocking, for simplicity they are all used on every platform;
-	// layer1 is the iframe layer which is used to supress bleed through of underlying content
+	// layer1 is the iframe layer which is used to suppress bleed through of underlying content
 	// layer2 is the overlay layer which has opacity and a wait cursor (by default)
 	// layer3 is the message content that is displayed while blocking
 


=====================================
OrthancServer/Plugins/Engine/OrthancPlugins.cpp
=====================================
@@ -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:
+      explicit 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 @@ namespace Orthanc
     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 @@ namespace Orthanc
     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 @@ namespace Orthanc
          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 @@ namespace Orthanc
         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 @@ namespace Orthanc
     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();
+    }
+  }
 }


=====================================
OrthancServer/Plugins/Engine/OrthancPlugins.h
=====================================
@@ -62,6 +62,7 @@ namespace Orthanc
 
 namespace Orthanc
 {
+  class HttpServer;
   class ServerContext;
 
   class OrthancPlugins : 
@@ -89,6 +90,7 @@ namespace Orthanc
     class DicomInstanceFromCallback;
     class DicomInstanceFromBuffer;
     class DicomInstanceFromTranscoded;
+    class WebDavCollection;
     
     void RegisterRestCallback(const void* parameters,
                               bool lock);
@@ -394,6 +396,8 @@ namespace Orthanc
     bool IsValidAuthorizationToken(const std::string& token) const;
 
     unsigned int GetMaxDatabaseRetries() const;
+
+    void RegisterWebDavCollections(HttpServer& target);
   };
 }
 


=====================================
OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h
=====================================
@@ -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)
@@ -465,6 +466,7 @@ extern "C"
     _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,
@@ -1064,7 +1066,7 @@ extern "C"
 
   /**
    * @brief Opaque structure that represents the HTTP connection to the client application.
-   * @ingroup Callback
+   * @ingroup Callbacks
    **/
   typedef struct _OrthancPluginRestOutput_t OrthancPluginRestOutput;
 
@@ -1371,7 +1373,7 @@ extern "C"
    * @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) (
@@ -1407,7 +1409,7 @@ extern "C"
    * @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,
@@ -7409,7 +7411,7 @@ extern "C"
 
   /**
    * @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;
 
@@ -7704,7 +7706,7 @@ extern "C"
    * @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
    **/
@@ -7746,7 +7748,7 @@ extern "C"
    * 
    * @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);
@@ -7808,7 +7810,7 @@ extern "C"
    * 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 */,
@@ -7875,7 +7877,7 @@ extern "C"
    * @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,
@@ -8473,7 +8475,7 @@ extern "C"
   
 
   /**
-   * @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
@@ -8728,6 +8730,279 @@ extern "C"
   }
 
 
+
+  /**
+   * @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


=====================================
OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp
=====================================
@@ -511,6 +511,22 @@ namespace OrthancPlugins
   }
 
 
+  void OrthancString::ToJsonWithoutComments(Json::Value& target) const
+  {
+    if (str_ == NULL)
+    {
+      LogError("Cannot convert an empty memory buffer to JSON");
+      ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError);
+    }
+
+    if (!ReadJsonWithoutComments(target, str_))
+    {
+      LogError("Cannot convert some memory buffer to JSON");
+      ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
+    }
+  }
+
+
   void MemoryBuffer::DicomToJson(Json::Value& target,
                                  OrthancPluginDicomToJsonFormat format,
                                  OrthancPluginDicomToJsonFlags flags,
@@ -645,7 +661,7 @@ namespace OrthancPlugins
       ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError);
     }
 
-    str.ToJson(configuration_);
+    str.ToJsonWithoutComments(configuration_);
 
     if (configuration_.type() != Json::objectValue)
     {
@@ -2510,7 +2526,7 @@ namespace OrthancPlugins
         }
         catch (...)
         {
-          return OrthancPluginErrorCode_InternalError;
+          return OrthancPluginErrorCode_Plugin;
         }
       }
     }    
@@ -3532,4 +3548,238 @@ namespace OrthancPlugins
     }
   }
 #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
 }


=====================================
OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.h
=====================================
@@ -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
@@ -300,7 +306,9 @@ namespace OrthancPlugins
     void ToString(std::string& target) const;
 
     void ToJson(Json::Value& target) const;
-  };
+  
+    void ToJsonWithoutComments(Json::Value& target) const;
+};
 
 
   class OrthancConfiguration : public boost::noncopyable
@@ -1246,4 +1254,107 @@ namespace OrthancPlugins
                                     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
 }


=====================================
OrthancServer/Plugins/Samples/WebDavFilesystem/CMakeLists.txt
=====================================
@@ -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}
+  )


=====================================
OrthancServer/Plugins/Samples/WebDavFilesystem/Plugin.cpp
=====================================
@@ -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";
+  }
+}


=====================================
OrthancServer/Resources/Configuration.json
=====================================
@@ -559,11 +559,6 @@
   // Instance level. Setting this option to "0" means no limit.
   "LimitFindInstances" : 0,
 
-  // The maximum number of active jobs in the Orthanc scheduler. When
-  // this limit is reached, the addition of new jobs is blocked until
-  // some job finishes.
-  "LimitJobs" : 10,
-
   // If this option is set to "true" (default behavior until Orthanc
   // 1.3.2), Orthanc will log the resources that are exported to other
   // DICOM modalities or Orthanc peers, inside the URI


=====================================
OrthancServer/Sources/main.cpp
=====================================
@@ -430,29 +430,53 @@ public:
       }
       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;
+        }
       }
     }
   }
@@ -1156,6 +1180,13 @@ static bool StartHttpServer(ServerContext& context,
       }
     }
 
+#if ORTHANC_ENABLE_PLUGINS == 1
+    if (plugins != NULL)
+    {
+      plugins->RegisterWebDavCollections(httpServer);
+    }
+#endif
+
     MyHttpExceptionFormatter exceptionFormatter(httpDescribeErrors, plugins);
         
     httpServer.SetIncomingHttpRequestFilter(httpFilter);


=====================================
TODO
=====================================
@@ -7,6 +7,9 @@ For higher-level ideas in the roadmap, please first read the
 https://book.orthanc-server.com/contributing.html
 
 
+Some features are being funded by and OpenCollective one-time donations.
+selected features are marked with priorities ((1) - higher, (2) - medium, (3) - nice to have)
+
 =======
 General
 =======
@@ -22,14 +25,32 @@ General
 * Toolbox::ComputeMD5() fails on files larger than 4GB
 * Add an option to run Orthanc in read-only mode both for DICOM and for Rest API.
 * Logging: add some information like Thread-Id to contextualize the logs
-* Accept extra DICOM tags dictionaries in the DCMTK format '.dic' (easier to use than declare
+* (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:
+  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
+
 
 ============================
 Documentation (Orthanc Book)
 ============================
 
+* Write a getting started guide (step by step) for each platform to replace
+  https://book.orthanc-server.com/users/cookbook.html :
+  - Ubuntu/Debian
+  - Windows
+  - OSX
+  - Docker on Linux
+  Each step by step guide should contain:
+  - get binaries
+  - launch
+  - open explorer
+  - edit configuration file
+  - restart and observe changes
+
 * Explain how log rotation can be achieved (no built-in support in Orthanc)
 * Explain how to interface with Mirth/NextGen Connect
   https://en.wikipedia.org/wiki/NextGen_Connect
@@ -58,25 +79,25 @@ REST API
 Mid-term
 --------
 
-* Archive jobs: Resume downloads using "range requests":
+* (1) Archive jobs: Resume downloads using "range requests":
   https://developer.mozilla.org/en-US/docs/Web/HTTP/Range_requests
-* Create DICOM from DICOMweb JSON ("application/dicom+json")
+* (3) Create DICOM from DICOMweb JSON ("application/dicom+json")
   with "/tools/create-dicom"
-* Create multi-frame images with /tools/create-dicom (by adding a
+* (2) Create multi-frame images with /tools/create-dicom (by adding a
   "MultiFrame" flag to avoid creating a series), or modify PixelData
   of a multi-frame image:
   https://groups.google.com/g/orthanc-users/c/y3-xa_GcdLM/m/m0Kr5G5UPAAJ
-* Specify the transfer syntax in /tools/create-dicom
-* In the /studies/{id}/anonymize route, add an option to remove
+* (1) Specify the transfer syntax in /tools/create-dicom
+* (1) In the /studies/{id}/anonymize route, add an option to remove
   secondary captures.  They usually contains Patient info in the
   image. The SOPClassUID might be used to identify such secondary
   captures.
 * Support "/preview" and "/matlab" for LUT color images
 * Try to transcode files if a simple decoding fails:
   https://groups.google.com/g/orthanc-users/c/b8168-NkAhA/m/Df3j-CO9CgAJ
-* Add asynchronous mode in "/modalitities/.../move" for C-MOVE SCU:
+* (1) Add asynchronous mode in "/modalitities/.../move" for C-MOVE SCU:
   https://groups.google.com/g/orthanc-users/c/G3_jBy4X4NQ/m/8BanTsdMBQAJ
-* Ranges of DICOM tags for "Keep" and "Remove" in ".../modify" and ".../anonymize": 
+* (2) Ranges of DICOM tags for "Keep" and "Remove" in ".../modify" and ".../anonymize": 
   https://groups.google.com/g/orthanc-users/c/6dETktKo9v8/m/b0LUvSfwAgAJ
 
 ---------
@@ -133,11 +154,11 @@ Internationalization
 Performance
 ===========
 
-* ServerContext::DicomCacheLocker => give access to the raw buffer,
+* (3) ServerContext::DicomCacheLocker => give access to the raw buffer,
   useful in ServerContext::DecodeDicomInstance()
-* DicomMap: create a cache to the main DICOM tags index
-* Check out rapidjson: https://github.com/miloyip/nativejson-benchmark
-* Optimize tools/find with ModalitiesInStudies: 
+* (2) DicomMap: create a cache to the main DICOM tags index
+* (3) Check out rapidjson: https://github.com/miloyip/nativejson-benchmark
+* (2) Optimize tools/find with ModalitiesInStudies: 
   https://groups.google.com/g/orthanc-users/c/aN8nqcRd3jw/m/pmc9ylVeAwAJ.
   One solution could be: Filter first without ModalitiesInStudies and then
   cycle through the responses to filter out with ModalitiesInStudies
@@ -253,7 +274,10 @@ Code quality and integration tests
 
 * Have a look at openQA (cf. GNU Health)
 * Add integration tests for LUT
-
+* Add more complex testing scenarios like data-migration, change of 
+  configuration files, multiple orthanc interacting togethers with various 
+  config.  This should probably look like the python toolbox tests ...
+  
 
 ---------------------
 External applications



View it on GitLab: https://salsa.debian.org/med-team/orthanc/-/commit/c9bda5f99923a9f229e869b773a58e95eb7a88fb

-- 
View it on GitLab: https://salsa.debian.org/med-team/orthanc/-/commit/c9bda5f99923a9f229e869b773a58e95eb7a88fb
You're receiving this email because of your account on salsa.debian.org.


-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://alioth-lists.debian.net/pipermail/debian-med-commit/attachments/20220323/306a983a/attachment-0001.htm>


More information about the debian-med-commit mailing list