changeset 4228:c8c0bbaaace3

write access to webdav
author Sebastien Jodogne <s.jodogne@gmail.com>
date Tue, 06 Oct 2020 12:45:11 +0200
parents 7fff7e683d65
children 013d6c6b2387
files OrthancFramework/Resources/CMake/CivetwebConfiguration.cmake OrthancFramework/Resources/Patches/civetweb-1.12.patch OrthancFramework/Sources/HttpServer/HttpServer.cpp OrthancFramework/Sources/HttpServer/HttpServer.h OrthancFramework/Sources/HttpServer/IWebDavBucket.cpp OrthancFramework/Sources/HttpServer/IWebDavBucket.h OrthancServer/Sources/main.cpp
diffstat 7 files changed, 888 insertions(+), 113 deletions(-) [+]
line wrap: on
line diff
--- a/OrthancFramework/Resources/CMake/CivetwebConfiguration.cmake	Mon Oct 05 10:55:24 2020 +0200
+++ b/OrthancFramework/Resources/CMake/CivetwebConfiguration.cmake	Tue Oct 06 12:45:11 2020 +0200
@@ -88,6 +88,7 @@
 
   add_definitions(
     -DCIVETWEB_HAS_DISABLE_KEEP_ALIVE=1
+    -DCIVETWEB_HAS_WEBDAV_WRITING=1
     )
 
 else()
@@ -112,10 +113,16 @@
   # up multipart transfers, as encountered in DICOMweb.
   CHECK_LIBRARY_EXISTS(civetweb mg_disable_keep_alive "" CIVETWEB_HAS_DISABLE_KEEP_ALIVE)
   if (CIVETWEB_HAS_DISABLE_KEEP_ALIVE)
-    add_definitions(-DCIVETWEB_HAS_DISABLE_KEEP_ALIVE=1)
+    add_definitions(
+      -DCIVETWEB_HAS_DISABLE_KEEP_ALIVE=1
+      -DCIVETWEB_HAS_WEBDAV_WRITING=1
+      )
     message("Performance: Your system-wide distribution of civetweb is configured for best performance")
   else()
-    message(WARNING "Performance: Your system-wide distribution of civetweb does not feature the mg_disable_keep_alive() function")
-    add_definitions(-DCIVETWEB_HAS_DISABLE_KEEP_ALIVE=0)
+    message(WARNING "Performance: Your system-wide distribution of civetweb does not feature the mg_disable_keep_alive() function, and WebDAV will only be available for read-only access")
+    add_definitions(
+      -DCIVETWEB_HAS_DISABLE_KEEP_ALIVE=0
+      -DCIVETWEB_HAS_WEBDAV_WRITING=0
+      )
   endif()
 endif()
--- a/OrthancFramework/Resources/Patches/civetweb-1.12.patch	Mon Oct 05 10:55:24 2020 +0200
+++ b/OrthancFramework/Resources/Patches/civetweb-1.12.patch	Tue Oct 06 12:45:11 2020 +0200
@@ -1,6 +1,6 @@
 diff -urEb civetweb-1.12.orig/include/civetweb.h civetweb-1.12/include/civetweb.h
---- civetweb-1.12.orig/include/civetweb.h	2020-04-02 12:07:20.727054140 +0200
-+++ civetweb-1.12/include/civetweb.h	2020-04-02 12:07:42.734996559 +0200
+--- civetweb-1.12.orig/include/civetweb.h	2020-10-06 12:39:10.634902843 +0200
++++ civetweb-1.12/include/civetweb.h	2020-10-06 12:39:30.630872089 +0200
 @@ -1614,6 +1614,9 @@
                                    struct mg_error_data *error);
  #endif
@@ -12,9 +12,21 @@
  }
  #endif /* __cplusplus */
 diff -urEb civetweb-1.12.orig/src/civetweb.c civetweb-1.12/src/civetweb.c
---- civetweb-1.12.orig/src/civetweb.c	2020-04-02 12:07:20.731054129 +0200
-+++ civetweb-1.12/src/civetweb.c	2020-04-02 12:07:52.250971600 +0200
-@@ -20704,5 +20704,12 @@
+--- civetweb-1.12.orig/src/civetweb.c	2020-10-06 12:39:10.638902837 +0200
++++ civetweb-1.12/src/civetweb.c	2020-10-06 12:41:40.110671929 +0200
+@@ -10525,6 +10525,11 @@
+     /* + MicroSoft extensions
+      * https://msdn.microsoft.com/en-us/library/aa142917.aspx */
+ 
++    /* Added by SJ, for write access to WebDAV on Windows >= 7 */
++    {"LOCK", 1, 1, 0, 0, 0},
++    {"UNLOCK", 1, 0, 0, 0, 0},
++    {"PROPPATCH", 1, 1, 0, 0, 0},
++    
+     /* REPORT method (RFC 3253) */
+     {"REPORT", 1, 1, 1, 1, 1},
+     /* REPORT method only allowed for CGI/Lua/LSP and callbacks. */
+@@ -20704,5 +20709,12 @@
  	return 1;
  }
  
--- a/OrthancFramework/Sources/HttpServer/HttpServer.cpp	Mon Oct 05 10:55:24 2020 +0200
+++ b/OrthancFramework/Sources/HttpServer/HttpServer.cpp	Tue Oct 06 12:45:11 2020 +0200
@@ -45,7 +45,9 @@
 #  if !defined(CIVETWEB_HAS_DISABLE_KEEP_ALIVE)
 #    error Macro CIVETWEB_HAS_DISABLE_KEEP_ALIVE must be defined
 #  endif
-
+#  if !defined(CIVETWEB_HAS_WEBDAV_WRITING)
+#    error Macro CIVETWEB_HAS_WEBDAV_WRITING must be defined
+#  endif
 #else
 #  error "Either Mongoose or Civetweb must be enabled to compile this file"
 #endif
@@ -343,6 +345,38 @@
   }
                                                   
 
+  static PostDataStatus ReadBodyWithoutContentLength(std::string& body,
+                                                     struct mg_connection *connection)
+  {
+    // Store the individual chunks in a temporary file, then read it
+    // back into the memory buffer "body"
+    FileBuffer buffer;
+
+    std::string tmp(1024 * 1024, 0);
+      
+    for (;;)
+    {
+      int r = mg_read(connection, &tmp[0], tmp.size());
+      if (r < 0)
+      {
+        return PostDataStatus_Failure;
+      }
+      else if (r == 0)
+      {
+        break;
+      }
+      else
+      {
+        buffer.Append(tmp.c_str(), r);
+      }
+    }
+
+    buffer.Read(body);
+
+    return PostDataStatus_Success;
+  }
+                                                  
+
   static PostDataStatus ReadBodyToString(std::string& body,
                                          struct mg_connection *connection,
                                          const IHttpHandler::Arguments& headers)
@@ -356,32 +390,8 @@
     }
     else
     {
-      // No Content-Length. Store the individual chunks in a temporary
-      // file, then read it back into the memory buffer "body"
-      FileBuffer buffer;
-
-      std::string tmp(1024 * 1024, 0);
-      
-      for (;;)
-      {
-        int r = mg_read(connection, &tmp[0], tmp.size());
-        if (r < 0)
-        {
-          return PostDataStatus_Failure;
-        }
-        else if (r == 0)
-        {
-          break;
-        }
-        else
-        {
-          buffer.Append(tmp.c_str(), r);
-        }
-      }
-
-      buffer.Read(body);
-
-      return PostDataStatus_Success;
+      // No Content-Length
+      return ReadBodyWithoutContentLength(body, connection);
     }
   }
 
@@ -696,11 +706,35 @@
   }
 
 
-  bool HttpServer::HandleWebDav(HttpOutput& output,
-                                const std::string& method,
-                                const IHttpHandler::Arguments& headers,
-                                const std::string& uri)
+#if ORTHANC_ENABLE_PUGIXML == 1
+
+#  if CIVETWEB_HAS_WEBDAV_WRITING == 0
+  static void AnswerWebDavReadOnly(HttpOutput& output,
+                                   const std::string& uri)
   {
+    LOG(ERROR) << "Orthanc was compiled without support for read-write access to WebDAV: " << uri;
+    output.SendStatus(HttpStatus_403_Forbidden);
+  }    
+#  endif
+  
+  static bool HandleWebDav(HttpOutput& output,
+                           const HttpServer::WebDavBuckets& buckets,
+                           const std::string& method,
+                           const IHttpHandler::Arguments& headers,
+                           const std::string& uri,
+                           struct mg_connection *connection /* to read the PUT body if need be */)
+  {
+    if (buckets.empty())
+    {
+      return false;  // Speed up things if WebDAV is not used
+    }
+    
+    /**
+     * The "buckets" maps an URI relative to the root of the
+     * bucket, to the content of the bucket. The root URI does *not*
+     * contain a trailing slash.
+     **/
+    
     if (method == "OPTIONS")
     {
       // Remove the trailing slash, if any (necessary for davfs2)
@@ -711,38 +745,37 @@
         s.resize(s.size() - 1);
       }
       
-      WebDavBuckets::const_iterator bucket = webDavBuckets_.find(s);
-      if (bucket == webDavBuckets_.end())
+      HttpServer::WebDavBuckets::const_iterator bucket = buckets.find(s);
+      if (bucket == buckets.end())
       {
         return false;
       }
       else
       {
-        output.AddHeader("DAV", "1");
-        output.AddHeader("Allow", "GET, POST, PUT, DELETE, PROPFIND");
+        output.AddHeader("DAV", "1,2");  // Necessary for Windows XP
+
+#if CIVETWEB_HAS_WEBDAV_WRITING == 1
+        output.AddHeader("Allow", "GET, PUT, OPTIONS, PROPFIND, HEAD, LOCK, UNLOCK, PROPPATCH, MKCOL");
+#else
+        output.AddHeader("Allow", "GET, PUT, OPTIONS, PROPFIND, HEAD");
+#endif
+
         output.SendStatus(HttpStatus_200_Ok);
         return true;
       }
     }
-    else if (method == "PROPFIND")
+    else if (method == "GET" ||
+             method == "PROPFIND" ||
+             method == "PROPPATCH" ||
+             method == "PUT" ||
+             method == "HEAD" ||
+             method == "LOCK" ||
+             method == "UNLOCK" ||
+             method == "MKCOL")
     {
-      IHttpHandler::Arguments::const_iterator i = headers.find("depth");
-      if (i == headers.end())
-      {
-        throw OrthancException(ErrorCode_NetworkProtocol, "WebDAV PROPFIND without depth");
-      }
-
-      int depth = boost::lexical_cast<int>(i->second);
-      if (depth != 0 &&
-          depth != 1)
-      {
-        throw OrthancException(
-          ErrorCode_NetworkProtocol,
-          "WebDAV PROPFIND at unsupported depth (can only be 0 or 1): " + i->second);
-      }
-      
-      for (WebDavBuckets::const_iterator bucket = webDavBuckets_.begin();
-           bucket != webDavBuckets_.end(); ++bucket)
+      // Locate the WebDAV bucket of interest, if any
+      for (HttpServer::WebDavBuckets::const_iterator bucket = buckets.begin();
+           bucket != buckets.end(); ++bucket)
       {
         assert(!bucket->first.empty() &&
                bucket->first[bucket->first.size() - 1] != '/' &&
@@ -760,42 +793,222 @@
           std::vector<std::string> path;
           Toolbox::SplitUriComponents(path, s);
 
-          std::string answer;
+
+          /**
+           * WebDAV - PROPFIND
+           **/
+          
+          if (method == "PROPFIND")
+          {
+            IHttpHandler::Arguments::const_iterator i = headers.find("depth");
+            if (i == headers.end())
+            {
+              throw OrthancException(ErrorCode_NetworkProtocol, "WebDAV PROPFIND without depth");
+            }
+
+            int depth = boost::lexical_cast<int>(i->second);
+            if (depth != 0 &&
+                depth != 1)
+            {
+              throw OrthancException(
+                ErrorCode_NetworkProtocol,
+                "WebDAV PROPFIND at unsupported depth (can only be 0 or 1): " + i->second);
+            }
+      
+            std::string answer;
           
-          if (depth == 0)
-          {
-            if (bucket->second->IsExistingFolder(path))
+            if (depth == 0)
+            {
+              MimeType mime;
+              std::string content;
+              boost::posix_time::ptime modificationTime;
+            
+              if (bucket->second->IsExistingFolder(path))
+              {
+                IWebDavBucket::Collection c;
+                c.AddResource(new IWebDavBucket::Folder(""));
+                c.Format(answer, uri);
+              }
+              else if (!path.empty() &&
+                       bucket->second->GetFileContent(mime, content, modificationTime, path))
+              {
+                std::unique_ptr<IWebDavBucket::File> f(new IWebDavBucket::File(path.back()));
+                f->SetContentLength(content.size());
+                f->SetModificationTime(modificationTime);
+                f->SetMimeType(mime);
+
+                IWebDavBucket::Collection c;
+                c.AddResource(f.release());
+
+                std::vector<std::string> p;
+                Toolbox::SplitUriComponents(p, uri);
+                if (p.empty())
+                {
+                  throw OrthancException(ErrorCode_InternalError);
+                }
+                
+                p.resize(p.size() - 1);
+                c.Format(answer, Toolbox::FlattenUri(p));
+              }
+              else
+              {
+                output.SendStatus(HttpStatus_404_NotFound);
+                return true;
+              }
+            }
+            else if (depth == 1)
             {
               IWebDavBucket::Collection c;
-              c.AddResource(new IWebDavBucket::Folder(""));
+              c.AddResource(new IWebDavBucket::Folder(""));  // Necessary for empty folders
+              
+              if (!bucket->second->ListCollection(c, path))
+              {
+                output.SendStatus(HttpStatus_404_NotFound);
+                return true;
+              }
+
               c.Format(answer, uri);
             }
             else
             {
+              throw OrthancException(ErrorCode_InternalError);
+            }
+
+            output.AddHeader("Content-Type", "application/xml; charset=UTF-8");
+            output.SendStatus(HttpStatus_207_MultiStatus, answer);
+            return true;
+          }
+          
+
+          /**
+           * WebDAV - GET and HEAD
+           **/
+          
+          else if (method == "GET" ||
+                   method == "HEAD")
+          {
+            MimeType mime;
+            std::string content;
+            boost::posix_time::ptime modificationTime;
+            
+            if (bucket->second->GetFileContent(mime, content, modificationTime, path))
+            {
+              output.AddHeader("Content-Type", EnumerationToString(mime));
+
+              // "Last-Modified" is necessary on Windows XP. The "Z"
+              // suffix is mandatory on Windows >= 7.
+              output.AddHeader("Last-Modified", boost::posix_time::to_iso_extended_string(modificationTime) + "Z");
+
+              if (method == "GET")
+              {
+                output.Answer(content);
+              }
+              else
+              {
+                output.SendStatus(HttpStatus_200_Ok);
+              }
+            }
+            else
+            {
               output.SendStatus(HttpStatus_404_NotFound);
-              return true;
-            }
-          }
-          else if (depth == 1)
-          {
-            IWebDavBucket::Collection c;
-            if (!bucket->second->ListCollection(c, path))
-            {
-              output.SendStatus(HttpStatus_404_NotFound);
-              return true;
             }
 
-            c.Format(answer, uri);
+            return true;
+          }
+
+          
+          /**
+           * WebDAV - PUT
+           **/
+          
+          else if (method == "PUT")
+          {
+#if CIVETWEB_HAS_WEBDAV_WRITING == 1           
+            std::string body;
+            if (ReadBodyToString(body, connection, headers) == PostDataStatus_Success)
+            {
+              if (bucket->second->StoreFile(body, path))
+              {
+                //output.SendStatus(HttpStatus_200_Ok);
+                output.SendStatus(HttpStatus_201_Created);
+              }
+              else
+              {
+                output.SendStatus(HttpStatus_403_Forbidden);
+              }
+            }
+            else
+            {
+              LOG(ERROR) << "Cannot read the content of a file to be stored in WebDAV";
+              output.SendStatus(HttpStatus_400_BadRequest);
+            }
+#else
+            AnswerWebDavReadOnly(output, uri);
+#endif
+
+            return true;
+          }
+          
+
+          /**
+           * WebDAV - MKCOL
+           **/
+          
+          else if (method == "MKCOL")
+          {
+#if CIVETWEB_HAS_WEBDAV_WRITING == 1           
+            if (bucket->second->CreateFolder(path))
+            {
+              //output.SendStatus(HttpStatus_200_Ok);
+              output.SendStatus(HttpStatus_201_Created);
+            }
+            else
+            {
+              output.SendStatus(HttpStatus_403_Forbidden);
+            }
+#else
+            AnswerWebDavReadOnly(output, uri);
+#endif
+
+            return true;
+          }
+          
+
+          /**
+           * WebDAV - Faking PROPPATCH, LOCK and UNLOCK
+           **/
+          
+          else if (method == "PROPPATCH")
+          {
+#if CIVETWEB_HAS_WEBDAV_WRITING == 1           
+            IWebDavBucket::AnswerFakedProppatch(output, uri);
+#else
+            AnswerWebDavReadOnly(output, uri);
+#endif
+            return true;
+          }
+          else if (method == "LOCK")
+          {
+#if CIVETWEB_HAS_WEBDAV_WRITING == 1           
+            IWebDavBucket::AnswerFakedLock(output, uri);
+#else
+            AnswerWebDavReadOnly(output, uri);
+#endif
+            return true;
+          }
+          else if (method == "UNLOCK")
+          {
+#if CIVETWEB_HAS_WEBDAV_WRITING == 1           
+            IWebDavBucket::AnswerFakedUnlock(output);
+#else
+            AnswerWebDavReadOnly(output, uri);
+#endif
+            return true;
           }
           else
           {
             throw OrthancException(ErrorCode_InternalError);
           }
-          
-          output.AddHeader("DAV", "1");
-          output.AddHeader("Content-Type", "application/xml");
-          output.SendStatus(HttpStatus_207_MultiStatus, answer);
-          return true;
         }
       }
       
@@ -803,11 +1016,16 @@
     }
     else
     {
+      /**
+       * WebDAV - Unapplicable method (such as POST and DELETE)
+       **/
+          
       return false;
     }
-  }
+  } 
+#endif /* ORTHANC_ENABLE_PUGIXML == 1 */
 
-
+  
   static void InternalCallback(HttpOutput& output /* out */,
                                HttpMethod& method /* out */,
                                HttpServer& server,
@@ -910,21 +1128,39 @@
     // Compute the HTTP method, taking method faking into consideration
     method = HttpMethod_Get;
 
+#if ORTHANC_ENABLE_PUGIXML == 1
     bool isWebDav = false;
+#endif
+    
     HttpMethod filterMethod;
+
     
     if (ExtractMethod(method, request, headers, argumentsGET))
     {
       LOG(INFO) << EnumerationToString(method) << " " << Toolbox::FlattenUri(uri);
       filterMethod = method;
     }
+#if ORTHANC_ENABLE_PUGIXML == 1
     else if (!strcmp(request->request_method, "OPTIONS") ||
-             !strcmp(request->request_method, "PROPFIND"))
+             !strcmp(request->request_method, "PROPFIND") ||
+             !strcmp(request->request_method, "HEAD"))
     {
-      LOG(INFO) << "Incoming WebDAV request: " << request->request_method << " " << requestUri;
+      LOG(INFO) << "Incoming read-only WebDAV request: "
+                << request->request_method << " " << requestUri;
       filterMethod = HttpMethod_Get;
       isWebDav = true;
     }
+    else if (!strcmp(request->request_method, "PROPPATCH") ||
+             !strcmp(request->request_method, "LOCK") ||
+             !strcmp(request->request_method, "UNLOCK") ||
+             !strcmp(request->request_method, "MKCOL"))
+    {
+      LOG(INFO) << "Incoming read-write WebDAV request: "
+                << request->request_method << " " << requestUri;
+      filterMethod = HttpMethod_Put;
+      isWebDav = true;
+    }
+#endif /* ORTHANC_ENABLE_PUGIXML == 1 */
     else
     {
       LOG(INFO) << "Unknown HTTP method: " << request->request_method;
@@ -949,7 +1185,9 @@
     }
 
 
-    if (server.HandleWebDav(output, request->request_method, headers, requestUri))
+#if ORTHANC_ENABLE_PUGIXML == 1
+    if (HandleWebDav(output, server.GetWebDavBuckets(), request->request_method,
+                     headers, requestUri, connection))
     {
       return;
     }
@@ -960,6 +1198,7 @@
       output.SendStatus(HttpStatus_404_NotFound);
       return;
     }
+#endif
 
 
     bool found = false;
@@ -1381,6 +1620,14 @@
         }
       }
 
+#if ORTHANC_ENABLE_PUGIXML == 1    
+      for (WebDavBuckets::iterator it = webDavBuckets_.begin(); it != webDavBuckets_.end(); ++it)
+      {
+        assert(it->second != NULL);
+        it->second->Start();
+      }
+#endif
+
       LOG(WARNING) << "HTTP server listening on port: " << GetPortNumber()
                    << " (HTTPS encryption is "
                    << (IsSslEnabled() ? "enabled" : "disabled")
@@ -1395,6 +1642,15 @@
     if (IsRunning())
     {
       mg_stop(pimpl_->context_);
+      
+#if ORTHANC_ENABLE_PUGIXML == 1    
+      for (WebDavBuckets::iterator it = webDavBuckets_.begin(); it != webDavBuckets_.end(); ++it)
+      {
+        assert(it->second != NULL);
+        it->second->Stop();
+      }
+#endif
+
       pimpl_->context_ = NULL;
     }
   }
--- a/OrthancFramework/Sources/HttpServer/HttpServer.h	Mon Oct 05 10:55:24 2020 +0200
+++ b/OrthancFramework/Sources/HttpServer/HttpServer.h	Tue Oct 06 12:45:11 2020 +0200
@@ -76,6 +76,11 @@
 
   class ORTHANC_PUBLIC HttpServer : public boost::noncopyable
   {
+  public:
+#if ORTHANC_ENABLE_PUGIXML == 1
+    typedef std::map<std::string, IWebDavBucket*>  WebDavBuckets;
+#endif
+    
   private:
     // http://stackoverflow.com/questions/311166/stdauto-ptr-or-boostshared-ptr-for-pimpl-idiom
     struct PImpl;
@@ -103,7 +108,6 @@
     unsigned int requestTimeout_;  // In seconds
 
 #if ORTHANC_ENABLE_PUGIXML == 1
-    typedef std::map<std::string, IWebDavBucket*>  WebDavBuckets;
     WebDavBuckets webDavBuckets_;
 #endif
     
@@ -236,13 +240,15 @@
     }
 
 #if ORTHANC_ENABLE_PUGIXML == 1
+    WebDavBuckets& GetWebDavBuckets()
+    {
+      return webDavBuckets_;
+    }      
+#endif
+
+#if ORTHANC_ENABLE_PUGIXML == 1
     void Register(const std::vector<std::string>& root,
                   IWebDavBucket* bucket); // Takes ownership
 #endif
-
-    bool HandleWebDav(HttpOutput& output,
-                      const std::string& method,
-                      const IHttpHandler::Arguments& headers,
-                      const std::string& uri);
   };
 }
--- a/OrthancFramework/Sources/HttpServer/IWebDavBucket.cpp	Mon Oct 05 10:55:24 2020 +0200
+++ b/OrthancFramework/Sources/HttpServer/IWebDavBucket.cpp	Tue Oct 06 12:45:11 2020 +0200
@@ -23,6 +23,7 @@
 #include "../PrecompiledHeaders.h"
 #include "IWebDavBucket.h"
 
+#include "HttpOutput.h"
 #include "../OrthancException.h"
 #include "../Toolbox.h"
 
@@ -90,7 +91,7 @@
 
 
   void IWebDavBucket::Resource::Format(pugi::xml_node& node,
-                                        const std::string& parentPath) const
+                                       const std::string& parentPath) const
   {
     node.set_name("D:response");
 
@@ -98,8 +99,11 @@
     node.append_child("D:href").append_child(pugi::node_pcdata).set_value(s.c_str());
 
     pugi::xml_node propstat = node.append_child("D:propstat");
-    propstat.append_child("D:status").append_child(pugi::node_pcdata).
-      set_value("HTTP/1.1 200 OK");
+
+    static const HttpStatus status = HttpStatus_200_Ok;
+    s = ("HTTP/1.1 " + boost::lexical_cast<std::string>(status) + " " +
+         std::string(EnumerationToString(status)));
+    propstat.append_child("D:status").append_child(pugi::node_pcdata).set_value(s.c_str());
 
     pugi::xml_node prop = propstat.append_child("D:prop");
 
@@ -111,6 +115,12 @@
     prop.append_child("D:getlastmodified").append_child(pugi::node_pcdata).set_value(s.c_str());
 
 #if 0
+    // Maybe used by davfs2
+    prop.append_child("D:quota-available-bytes");
+    prop.append_child("D:quota-used-bytes");
+#endif
+    
+#if 0
     prop.append_child("D:lockdiscovery");
     pugi::xml_node lock = prop.append_child("D:supportedlock");
 
@@ -217,4 +227,84 @@
 
     Toolbox::XmlToString(target, doc);
   }
+
+
+  void IWebDavBucket::AnswerFakedProppatch(HttpOutput& output,
+                                           const std::string& uri)
+  {
+    /**
+     * This is a fake implementation. The goal is to make happy the
+     * WebDAV clients that set properties (such as Windows >= 7).
+     **/
+            
+    pugi::xml_document doc;
+
+    pugi::xml_node root = doc.append_child("D:multistatus");
+    root.append_attribute("xmlns:D").set_value("DAV:");
+
+    pugi::xml_node response = root.append_child("D:response");
+    response.append_child("D:href").append_child(pugi::node_pcdata).set_value(uri.c_str());
+
+    response.append_child("D:propstat");
+
+    pugi::xml_node decl = doc.prepend_child(pugi::node_declaration);
+    decl.append_attribute("version").set_value("1.0");
+    decl.append_attribute("encoding").set_value("UTF-8");
+
+    std::string s;
+    Toolbox::XmlToString(s, doc);
+
+    output.AddHeader("Content-Type", "application/xml");
+    output.SendStatus(HttpStatus_207_MultiStatus, s);    
+  }
+
+
+  void IWebDavBucket::AnswerFakedLock(HttpOutput& output,
+                                      const std::string& uri)
+  {
+    /**
+     * This is a fake implementation. No lock is actually
+     * created. The goal is to make happy the WebDAV clients
+     * that use locking (such as Windows >= 7).
+     **/
+            
+    pugi::xml_document doc;
+
+    pugi::xml_node root = doc.append_child("D:prop");
+    root.append_attribute("xmlns:D").set_value("DAV:");
+
+    pugi::xml_node activelock = root.append_child("D:lockdiscovery").append_child("D:activelock");
+    activelock.append_child("D:locktype").append_child("D:write");
+    activelock.append_child("D:lockscope").append_child("D:exclusive");
+    activelock.append_child("D:depth").append_child(pugi::node_pcdata).set_value("0");
+    activelock.append_child("D:timeout").append_child(pugi::node_pcdata).set_value("Second-3599");
+
+    activelock.append_child("D:lockroot").append_child("D:href")
+      .append_child(pugi::node_pcdata).set_value(uri.c_str());
+    activelock.append_child("D:owner");
+
+    std::string token = Toolbox::GenerateUuid();
+    boost::erase_all(token, "-");
+    token = "opaquelocktoken:0x" + token;
+            
+    activelock.append_child("D:locktoken").append_child("D:href").
+      append_child(pugi::node_pcdata).set_value(token.c_str());
+            
+    pugi::xml_node decl = doc.prepend_child(pugi::node_declaration);
+    decl.append_attribute("version").set_value("1.0");
+    decl.append_attribute("encoding").set_value("UTF-8");
+
+    std::string s;
+    Toolbox::XmlToString(s, doc);
+
+    output.AddHeader("Lock-Token", token);  // Necessary for davfs2
+    output.AddHeader("Content-Type", "application/xml");
+    output.SendStatus(HttpStatus_201_Created, s);
+  }
+
+
+  void IWebDavBucket::AnswerFakedUnlock(HttpOutput& output)
+  {
+    output.SendStatus(HttpStatus_204_NoContent);
+  }
 }
--- a/OrthancFramework/Sources/HttpServer/IWebDavBucket.h	Mon Oct 05 10:55:24 2020 +0200
+++ b/OrthancFramework/Sources/HttpServer/IWebDavBucket.h	Tue Oct 06 12:45:11 2020 +0200
@@ -41,6 +41,8 @@
 
 namespace Orthanc
 {
+  class HttpOutput;
+  
   class IWebDavBucket : public boost::noncopyable
   {
   public:
@@ -115,6 +117,8 @@
         return mime_;
       }
 
+      void SetCreated(bool created);
+
       virtual void Format(pugi::xml_node& node,
                           const std::string& parentPath) const ORTHANC_OVERRIDE;
     };
@@ -146,7 +150,7 @@
       void Format(std::string& target,
                   const std::string& parentPath) const;
     };
-    
+
 
     virtual ~IWebDavBucket()
     {
@@ -157,7 +161,30 @@
     virtual bool ListCollection(Collection& collection,
                                 const std::vector<std::string>& path) = 0;
 
-    virtual bool GetFileContent(std::string& content,
+    virtual bool GetFileContent(MimeType& mime,
+                                std::string& content,
+                                boost::posix_time::ptime& modificationTime, 
                                 const std::vector<std::string>& path) = 0;
+
+    // "false" returns indicate a read-only target
+    virtual bool StoreFile(const std::string& content,
+                           const std::vector<std::string>& path) = 0;
+
+    virtual bool CreateFolder(const std::vector<std::string>& path) = 0;
+
+    virtual void Start() = 0;
+
+    // During the shutdown of the Web server, give a chance to the
+    // bucket to end its pending operations
+    virtual void Stop() = 0;
+
+
+    static void AnswerFakedProppatch(HttpOutput& output,
+                                     const std::string& uri);
+
+    static void AnswerFakedLock(HttpOutput& output,
+                                const std::string& uri);
+
+    static void AnswerFakedUnlock(HttpOutput& output);
   };
 }
--- a/OrthancServer/Sources/main.cpp	Mon Oct 05 10:55:24 2020 +0200
+++ b/OrthancServer/Sources/main.cpp	Tue Oct 06 12:45:11 2020 +0200
@@ -613,29 +613,290 @@
 
 
 
+static const char* const UPLOAD_FOLDER = "upload";
+
 class DummyBucket : public IWebDavBucket  // TODO
 {
 private:
   ServerContext& context_;
 
+  static void RemoveFirstElement(std::vector<std::string>& target,
+                                 const std::vector<std::string>& source)
+  {
+    if (source.empty())
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+    else
+    {
+      target.resize(source.size() - 1);
+      for (size_t i = 1; i < source.size(); i++)
+      {
+        target[i - 1] = source[i];
+      }
+    }
+  }
+  
+  class UploadedFile : public boost::noncopyable
+  {
+  private:
+    std::string               content_;
+    MimeType                  mime_;
+    boost::posix_time::ptime  time_;
+
+    void Touch()
+    {
+      time_ = boost::posix_time::second_clock::universal_time();
+    }
+    
+  public:
+    UploadedFile() :
+      mime_(MimeType_Binary)
+    {
+      Touch();
+    }
+    
+    void SetContent(const std::string& content,
+                    MimeType mime)
+    {
+      content_ = content;
+      mime_ = mime;
+      Touch();
+    }
+
+    MimeType GetMimeType() const
+    {
+      return mime_;
+    }
+
+    const std::string& GetContent() const
+    {
+      return content_;
+    }
+
+    const boost::posix_time::ptime& GetTime() const
+    {
+      return time_;
+    }
+  };
+
+
+  class UploadedFolder : public boost::noncopyable
+  {
+  private:
+    typedef std::map<std::string, UploadedFile*>    Files;
+    typedef std::map<std::string, UploadedFolder*>  Subfolders;
+
+    Files       files_;
+    Subfolders  subfolders_;
+
+    void CheckName(const std::string& name)
+    {
+      if (name.empty() ||
+          name.find('/') != std::string::npos ||
+          name.find('\\') != std::string::npos ||
+          name.find('\0') != std::string::npos)
+      {
+        throw OrthancException(ErrorCode_ParameterOutOfRange,
+                               "Bad resource name for WebDAV: " + name);
+      }
+    }
+
+    bool IsExisting(const std::string& name) const
+    {
+      return (files_.find(name) != files_.end() ||
+              subfolders_.find(name) != subfolders_.end());
+    }
+
+  public:
+    ~UploadedFolder()
+    {
+      for (Files::iterator it = files_.begin(); it != files_.end(); ++it)
+      {
+        assert(it->second != NULL);
+        delete it->second;
+      }        
+
+      for (Subfolders::iterator it = subfolders_.begin(); it != subfolders_.end(); ++it)
+      {
+        assert(it->second != NULL);
+        delete it->second;
+      }        
+    }
+
+    const UploadedFile* LookupFile(const std::string& name) const
+    {
+      Files::const_iterator found = files_.find(name);
+      if (found == files_.end())
+      {
+        return NULL;
+      }
+      else
+      {
+        assert(found->second != NULL);
+        return found->second;
+      }
+    }
+
+    bool CreateSubfolder(const std::string& name)
+    {
+      CheckName(name);
+
+      if (IsExisting(name))
+      {
+        LOG(ERROR) << "WebDAV folder already existing: " << name;
+        return false;
+      }
+      else
+      {
+        subfolders_[name] = new UploadedFolder;
+        return true;
+      }
+    }
+
+    bool StoreFile(const std::string& name,
+                   const std::string& content,
+                   MimeType mime)
+    {
+      CheckName(name);
+
+      if (subfolders_.find(name) != subfolders_.end())
+      {
+        LOG(ERROR) << "WebDAV folder already existing: " << name;
+        return false;
+      }
+
+      Files::iterator found = files_.find(name);
+      if (found == files_.end())
+      {
+        std::unique_ptr<UploadedFile> f(new UploadedFile);
+        f->SetContent(content, mime);
+        files_[name] = f.release();
+      }
+      else
+      {
+        assert(found->second != NULL);
+        found->second->SetContent(content, mime);
+      }
+      
+      return true;
+    }
+
+    UploadedFolder* LookupSubfolder(const UriComponents& path)
+    {
+      if (path.empty())
+      {
+        return this;
+      }
+      else
+      {
+        Subfolders::const_iterator found = subfolders_.find(path[0]);
+        if (found == subfolders_.end())
+        {
+          return NULL;
+        }
+        else
+        {
+          assert(found->second != NULL);
+
+          UriComponents p;
+          RemoveFirstElement(p, path);
+          
+          return found->second->LookupSubfolder(p);
+        }
+      }
+    }
+
+    void ListCollection(Collection& collection) const
+    {
+      for (Files::const_iterator it = files_.begin(); it != files_.end(); ++it)
+      {
+        assert(it->second != NULL);
+        
+        std::unique_ptr<File> f(new File(it->first));
+        f->SetContentLength(it->second->GetContent().size());
+        f->SetCreationTime(it->second->GetTime());
+        collection.AddResource(f.release());
+      }
+
+      for (Subfolders::const_iterator it = subfolders_.begin(); it != subfolders_.end(); ++it)
+      {
+        collection.AddResource(new Folder(it->first));
+      }
+    }
+  };
+
+  bool IsUploadedFolder(const UriComponents& path) const
+  {
+    return (path.size() >= 1 &&
+            path[0] == UPLOAD_FOLDER);
+  }
+
+  UploadedFolder  uploads_;
+  boost::recursive_mutex    mutex_;
+
+  UploadedFolder* LookupUploadedFolder(const UriComponents& path)
+  {
+    if (IsUploadedFolder(path))
+    {
+      UriComponents p;
+      RemoveFirstElement(p, path);
+      
+      return uploads_.LookupSubfolder(p);
+    }
+    else
+    {
+      return NULL;
+    }
+  }
+  
 public:
   DummyBucket(ServerContext& context) :
     context_(context)
   {
   }
   
-  virtual bool IsExistingFolder(const std::vector<std::string>& path) ORTHANC_OVERRIDE
+  virtual bool IsExistingFolder(const UriComponents& path) ORTHANC_OVERRIDE
   {
-    return (path.size() == 0 ||
-            (path.size() == 1 && path[0] == "Folder1") ||
-            (path.size() == 2 && path[0] == "Folder1" && path[1] == "Folder2"));
+    boost::recursive_mutex::scoped_lock lock(mutex_);
+    
+    if (IsUploadedFolder(path))
+    {
+      return LookupUploadedFolder(path) != NULL;
+    }
+    else
+    {
+      return (path.size() == 0 ||
+              (path.size() == 1 && path[0] == "Folder1") ||
+              (path.size() == 2 && path[0] == "Folder1" && path[1] == "Folder2"));
+    }
   }
 
   virtual bool ListCollection(Collection& collection,
                               const UriComponents& path) ORTHANC_OVERRIDE
   {
-    if (IsExistingFolder(path))
+    boost::recursive_mutex::scoped_lock lock(mutex_);
+    
+    if (IsUploadedFolder(path))
     {
+      const UploadedFolder* folder = LookupUploadedFolder(path);
+      if (folder == NULL)
+      {
+        return false;
+      }
+      else
+      {
+        folder->ListCollection(collection);
+        return true;
+      }
+    }
+    else if (IsExistingFolder(path))
+    {
+      if (path.empty())
+      {
+        collection.AddResource(new Folder(UPLOAD_FOLDER));
+      }
+      
       for (unsigned int i = 0; i < 5; i++)
       {
         std::unique_ptr<File> f(new File("IM" + boost::lexical_cast<std::string>(i) + ".dcm"));
@@ -648,7 +909,65 @@
       {
         collection.AddResource(new Folder("Folder" + boost::lexical_cast<std::string>(i)));
       }
-        
+
+      return true;
+    }
+    else
+    {
+      return false;
+    }
+  }
+
+  virtual bool GetFileContent(MimeType& mime,
+                              std::string& content,
+                              boost::posix_time::ptime& modificationTime, 
+                              const UriComponents& path) ORTHANC_OVERRIDE
+  {
+    boost::recursive_mutex::scoped_lock lock(mutex_);
+    
+    if (path.empty())
+    {
+      return false;
+    }
+    else if (IsUploadedFolder(path))
+    {
+      std::vector<std::string> p(path.begin(), path.end() - 1);
+      
+      const UploadedFolder* folder = LookupUploadedFolder(p);
+      if (folder == NULL)
+      {
+        return false;
+      }
+
+      const UploadedFile* file = folder->LookupFile(path.back());
+      if (file == NULL)
+      {
+        return false;
+      }
+      else
+      {
+        mime = file->GetMimeType();
+        content = file->GetContent();
+        modificationTime = file->GetTime();
+        return true;
+      }
+    }
+    else if (path.back() == "IM0.dcm" ||
+             path.back() == "IM1.dcm" ||
+             path.back() == "IM2.dcm" ||
+             path.back() == "IM3.dcm" ||
+             path.back() == "IM4.dcm")
+    {
+      modificationTime = boost::posix_time::second_clock::universal_time();
+
+      std::string s;
+      for (size_t i = 0; i < path.size(); i++)
+      {
+        s += "/" + path[i];
+      }
+      
+      content = "Hello world!\r\n" + s + "\r\n";
+      mime = MimeType_PlainText;
       return true;
     }
     else
@@ -657,17 +976,75 @@
     }
   }
 
-  virtual bool GetFileContent(std::string& content,
-                              const UriComponents& path) ORTHANC_OVERRIDE
+  
+  virtual bool StoreFile(const std::string& content,
+                         const UriComponents& path) ORTHANC_OVERRIDE
   {
-    std::string s = "/";
-    for (size_t i = 0; i < path.size(); i++)
+    boost::recursive_mutex::scoped_lock lock(mutex_);
+    
+    if (IsUploadedFolder(path))
+    {
+      std::vector<std::string> p(path.begin(), path.end() - 1);
+      
+      UploadedFolder* folder = LookupUploadedFolder(p);
+      if (folder == NULL)
+      {
+        return false;
+      }
+      else
+      {
+        printf("STORING %d bytes at %s\n", content.size(), path.back().c_str());
+        return folder->StoreFile(path.back(), content, SystemToolbox::AutodetectMimeType(path.back()));
+      }
+    }
+    else
+    {
+      LOG(WARNING) << "Writing to a read-only location in WebDAV: " << Toolbox::FlattenUri(path);
+      return false;
+    }
+  }
+
+
+  virtual bool CreateFolder(const UriComponents& path)
+  {
+    boost::recursive_mutex::scoped_lock lock(mutex_);
+    
+    if (IsUploadedFolder(path))
     {
-      s += path[i] + "/";
-    }
+      std::vector<std::string> p(path.begin(), path.end() - 1);
       
-    content = "Hello world!\r\n" + s + "\r\n";
-    return true;
+      UploadedFolder* folder = LookupUploadedFolder(p);
+      if (folder == NULL)
+      {
+        return false;
+      }
+      else
+      {
+        printf("CREATING FOLDER %s\n", path.back().c_str());
+        return folder->CreateSubfolder(path.back());
+      }
+    }
+    else
+    {
+      LOG(WARNING) << "Writing to a read-only location in WebDAV: " << Toolbox::FlattenUri(path);
+      return false;
+    }
+  }
+
+
+  virtual void Start() ORTHANC_OVERRIDE
+  {
+    boost::recursive_mutex::scoped_lock lock(mutex_);
+    
+    LOG(WARNING) << "Starting WebDAV";
+  }
+
+
+  virtual void Stop() ORTHANC_OVERRIDE
+  {
+    boost::recursive_mutex::scoped_lock lock(mutex_);
+    
+    LOG(WARNING) << "Stopping WebDAV";
   }
 };
 
@@ -1111,7 +1488,7 @@
     httpServer.Register(context.GetHttpHandler());
 
     {
-      std::vector<std::string> root;  // TODO
+      UriComponents root;  // TODO
       root.push_back("a");
       root.push_back("b");
       httpServer.Register(root, new DummyBucket(context));