changeset 4943:47d734fa30f6

adding function OrthancPluginRegisterWebDavCollection() to the plugin SDK
author Sebastien Jodogne <s.jodogne@gmail.com>
date Wed, 16 Mar 2022 17:21:02 +0100
parents bd7ad1cb40b6
children 6a59dc426f93
files NEWS OrthancFramework/Sources/HttpServer/IWebDavBucket.h OrthancFramework/Sources/SystemToolbox.cpp 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/Sources/main.cpp
diffstat 11 files changed, 1490 insertions(+), 20 deletions(-) [+]
line wrap: on
line diff
--- a/NEWS	Wed Mar 16 10:55:13 2022 +0100
+++ b/NEWS	Wed Mar 16 17:21:02 2022 +0100
@@ -4,16 +4,21 @@
 General
 -------
 
-* Improved DICOM authorization checks when multiple modalities are declared with
-  the same AET.
-
+* 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.
+* 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/Sources/HttpServer/IWebDavBucket.h	Wed Mar 16 10:55:13 2022 +0100
+++ b/OrthancFramework/Sources/HttpServer/IWebDavBucket.h	Wed Mar 16 17:21:02 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/SystemToolbox.cpp	Wed Mar 16 10:55:13 2022 +0100
+++ b/OrthancFramework/Sources/SystemToolbox.cpp	Wed Mar 16 17:21:02 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/OrthancServer/Plugins/Engine/OrthancPlugins.cpp	Wed Mar 16 10:55:13 2022 +0100
+++ b/OrthancServer/Plugins/Engine/OrthancPlugins.cpp	Wed Mar 16 17:21:02 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	Wed Mar 16 10:55:13 2022 +0100
+++ b/OrthancServer/Plugins/Engine/OrthancPlugins.h	Wed Mar 16 17:21:02 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	Wed Mar 16 10:55:13 2022 +0100
+++ b/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h	Wed Mar 16 17:21:02 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)
@@ -465,6 +466,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,
@@ -1064,7 +1066,7 @@
 
   /**
    * @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 @@
    * @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 @@
    * @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 @@
 
   /**
    * @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 @@
    * @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 @@
    * 
    * @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 @@
    * 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 @@
    * @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 @@
   
 
   /**
-   * @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 @@
   }
 
 
+
+  /**
+   * @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	Wed Mar 16 10:55:13 2022 +0100
+++ b/OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp	Wed Mar 16 17:21:02 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	Wed Mar 16 10:55:13 2022 +0100
+++ b/OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.h	Wed Mar 16 17:21:02 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,131 @@
                                     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
+
+
+#if HAS_ORTHANC_PLUGIN_WEBDAV == 1
+  class ReadOnlyWebDavCollection : public IWebDavCollection
+  {
+  public:
+    virtual bool StoreFile(const std::vector<std::string>& path,
+                           const void* data,
+                           size_t size)
+    {
+      return false;
+    }
+
+    virtual bool CreateFolder(const std::vector<std::string>& path)
+    {
+      return false;
+    }
+
+    virtual bool DeleteItem(const std::vector<std::string>& path)
+    {
+      return false;
+    }
+  };  
+#endif
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Samples/WebDavFilesystem/CMakeLists.txt	Wed Mar 16 17:21:02 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	Wed Mar 16 17:21:02 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	Wed Mar 16 10:55:13 2022 +0100
+++ b/OrthancServer/Sources/main.cpp	Wed Mar 16 17:21:02 2022 +0100
@@ -1180,6 +1180,13 @@
       }
     }
 
+#if ORTHANC_ENABLE_PLUGINS == 1
+    if (plugins != NULL)
+    {
+      plugins->RegisterWebDavCollections(httpServer);
+    }
+#endif
+
     MyHttpExceptionFormatter exceptionFormatter(httpDescribeErrors, plugins);
         
     httpServer.SetIncomingHttpRequestFilter(httpFilter);