changeset 4232:688435755466

added DELETE in WebDAV, first working virtual filesystem
author Sebastien Jodogne <s.jodogne@gmail.com>
date Wed, 07 Oct 2020 13:00:57 +0200
parents 290ffcb0a147
children ca2a55a62c81
files OrthancFramework/Sources/HttpServer/HttpServer.cpp OrthancFramework/Sources/HttpServer/IWebDavBucket.cpp OrthancFramework/Sources/HttpServer/IWebDavBucket.h OrthancFramework/Sources/HttpServer/WebDavStorage.cpp OrthancFramework/Sources/HttpServer/WebDavStorage.h OrthancServer/Sources/main.cpp
diffstat 6 files changed, 295 insertions(+), 73 deletions(-) [+]
line wrap: on
line diff
--- a/OrthancFramework/Sources/HttpServer/HttpServer.cpp	Tue Oct 06 20:55:16 2020 +0200
+++ b/OrthancFramework/Sources/HttpServer/HttpServer.cpp	Wed Oct 07 13:00:57 2020 +0200
@@ -755,9 +755,9 @@
         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");
+        output.AddHeader("Allow", "GET, PUT, DELETE, OPTIONS, PROPFIND, HEAD, LOCK, UNLOCK, PROPPATCH, MKCOL");
 #else
-        output.AddHeader("Allow", "GET, PUT, OPTIONS, PROPFIND, HEAD");
+        output.AddHeader("Allow", "GET, PUT, DELETE, OPTIONS, PROPFIND, HEAD");
 #endif
 
         output.SendStatus(HttpStatus_200_Ok);
@@ -768,6 +768,7 @@
              method == "PROPFIND" ||
              method == "PROPPATCH" ||
              method == "PUT" ||
+             method == "DELETE" ||
              method == "HEAD" ||
              method == "LOCK" ||
              method == "UNLOCK" ||
@@ -817,20 +818,41 @@
       
             std::string answer;
           
-            if (depth == 0)
+            MimeType mime;
+            std::string content;
+            boost::posix_time::ptime modificationTime = boost::posix_time::second_clock::universal_time();
+            
+            if (bucket->second->IsExistingFolder(path))
             {
-              MimeType mime;
-              std::string content;
-              boost::posix_time::ptime modificationTime;
-            
-              if (bucket->second->IsExistingFolder(path))
+              if (depth == 0)
               {
                 IWebDavBucket::Collection c;
                 c.AddResource(new IWebDavBucket::Folder(""));
-                c.Format(answer, uri);
+                c.Format(answer, uri, true /* include display name */);
               }
-              else if (!path.empty() &&
-                       bucket->second->GetFileContent(mime, content, modificationTime, path))
+              else if (depth == 1)
+              {
+                IWebDavBucket::Collection c;
+                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, true /* include display name */);
+              }
+              else
+              {
+                throw OrthancException(ErrorCode_InternalError);
+              }
+            }
+            else if (!path.empty() &&
+                     bucket->second->GetFileContent(mime, content, modificationTime, path))
+            {
+              if (depth == 0 ||
+                  depth == 1)
               {
                 std::unique_ptr<IWebDavBucket::File> f(new IWebDavBucket::File(path.back()));
                 f->SetContentLength(content.size());
@@ -846,32 +868,23 @@
                 {
                   throw OrthancException(ErrorCode_InternalError);
                 }
+
+                // Nautilus doesn't work on DELETE, if "D:displayname"
+                // is included in the multi-status answer of PROPFIND at depth 1
+                const bool includeDisplayName = (depth == 0);
                 
                 p.resize(p.size() - 1);
-                c.Format(answer, Toolbox::FlattenUri(p));
+                c.Format(answer, Toolbox::FlattenUri(p), includeDisplayName);
               }
               else
               {
-                output.SendStatus(HttpStatus_404_NotFound);
-                return true;
+                throw OrthancException(ErrorCode_InternalError);
               }
             }
-            else if (depth == 1)
-            {
-              IWebDavBucket::Collection c;
-              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.SendStatus(HttpStatus_404_NotFound);
+              return true;
             }
 
             output.AddHeader("Content-Type", "application/xml; charset=UTF-8");
@@ -951,6 +964,24 @@
           
 
           /**
+           * WebDAV - DELETE
+           **/
+          
+          else if (method == "DELETE")
+          {
+            if (bucket->second->DeleteItem(path))
+            {
+              output.SendStatus(HttpStatus_204_NoContent);
+            }
+            else
+            {
+              output.SendStatus(HttpStatus_403_Forbidden);
+            }
+            return true;
+          }
+          
+
+          /**
            * WebDAV - MKCOL
            **/
           
--- a/OrthancFramework/Sources/HttpServer/IWebDavBucket.cpp	Tue Oct 06 20:55:16 2020 +0200
+++ b/OrthancFramework/Sources/HttpServer/IWebDavBucket.cpp	Wed Oct 07 13:00:57 2020 +0200
@@ -91,7 +91,8 @@
 
 
   void IWebDavBucket::Resource::Format(pugi::xml_node& node,
-                                       const std::string& parentPath) const
+                                       const std::string& parentPath,
+                                       bool includeDisplayName) const
   {
     node.set_name("D:response");
 
@@ -148,11 +149,12 @@
     SetNameInternal(name);
   }
 
-
+  
   void IWebDavBucket::File::Format(pugi::xml_node& node,
-                                   const std::string& parentPath) const
+                                   const std::string& parentPath,
+                                   bool includeDisplayName) const
   {
-    Resource::Format(node, parentPath);
+    Resource::Format(node, parentPath, includeDisplayName);
 
     pugi::xml_node prop = node.first_element_by_path("D:propstat/D:prop");
     prop.append_child("D:resourcetype");
@@ -168,9 +170,10 @@
 
 
   void IWebDavBucket::Folder::Format(pugi::xml_node& node,
-                                     const std::string& parentPath) const
+                                     const std::string& parentPath,
+                                     bool includeDisplayName) const
   {
-    Resource::Format(node, parentPath);
+    Resource::Format(node, parentPath, includeDisplayName);
         
     pugi::xml_node prop = node.first_element_by_path("D:propstat/D:prop");
     prop.append_child("D:resourcetype").append_child("D:collection");
@@ -206,7 +209,8 @@
 
 
   void IWebDavBucket::Collection::Format(std::string& target,
-                                         const std::string& parentPath) const
+                                         const std::string& parentPath,
+                                         bool includeDisplayName) const
   {
     pugi::xml_document doc;
 
@@ -218,7 +222,7 @@
     {
       assert(*it != NULL);
       pugi::xml_node n = root.append_child();
-      (*it)->Format(n, parentPath);
+      (*it)->Format(n, parentPath, includeDisplayName);
     }
 
     pugi::xml_node decl = doc.prepend_child(pugi::node_declaration);
--- a/OrthancFramework/Sources/HttpServer/IWebDavBucket.h	Tue Oct 06 20:55:16 2020 +0200
+++ b/OrthancFramework/Sources/HttpServer/IWebDavBucket.h	Wed Oct 07 13:00:57 2020 +0200
@@ -84,7 +84,8 @@
       }
 
       virtual void Format(pugi::xml_node& node,
-                          const std::string& parentPath) const;
+                          const std::string& parentPath,
+                          bool includeDisplayName) const;
     };
 
 
@@ -120,7 +121,8 @@
       void SetCreated(bool created);
 
       virtual void Format(pugi::xml_node& node,
-                          const std::string& parentPath) const ORTHANC_OVERRIDE;
+                          const std::string& parentPath,
+                          bool includeDisplayName) const ORTHANC_OVERRIDE;
     };
 
 
@@ -133,7 +135,8 @@
       }
       
       virtual void Format(pugi::xml_node& node,
-                          const std::string& parentPath) const ORTHANC_OVERRIDE;
+                          const std::string& parentPath,
+                          bool includeDisplayName) const ORTHANC_OVERRIDE;
     };
 
 
@@ -148,7 +151,8 @@
       void AddResource(Resource* resource);  // Takes ownership
 
       void Format(std::string& target,
-                  const std::string& parentPath) const;
+                  const std::string& parentPath,
+                  bool includeDisplayName) const;
     };
 
 
@@ -172,6 +176,8 @@
 
     virtual bool CreateFolder(const std::vector<std::string>& path) = 0;
 
+    virtual bool DeleteItem(const std::vector<std::string>& path) = 0;
+
     virtual void Start() = 0;
 
     // During the shutdown of the Web server, give a chance to the
--- a/OrthancFramework/Sources/HttpServer/WebDavStorage.cpp	Tue Oct 06 20:55:16 2020 +0200
+++ b/OrthancFramework/Sources/HttpServer/WebDavStorage.cpp	Wed Oct 07 13:00:57 2020 +0200
@@ -364,4 +364,11 @@
       return folder->CreateSubfolder(path.back());
     }
   }
+
+
+  bool WebDavStorage::DeleteItem(const std::vector<std::string>& path)
+  {
+    // TODO
+    return false;
+  }
 }
--- a/OrthancFramework/Sources/HttpServer/WebDavStorage.h	Tue Oct 06 20:55:16 2020 +0200
+++ b/OrthancFramework/Sources/HttpServer/WebDavStorage.h	Wed Oct 07 13:00:57 2020 +0200
@@ -59,6 +59,8 @@
 
     virtual bool CreateFolder(const std::vector<std::string>& path) ORTHANC_OVERRIDE;
 
+    virtual bool DeleteItem(const std::vector<std::string>& path) ORTHANC_OVERRIDE;
+
     virtual void Start() ORTHANC_OVERRIDE
     {
     }
--- a/OrthancServer/Sources/main.cpp	Tue Oct 06 20:55:16 2020 +0200
+++ b/OrthancServer/Sources/main.cpp	Wed Oct 07 13:00:57 2020 +0200
@@ -743,7 +743,7 @@
   }
 
 
-  virtual bool CreateFolder(const UriComponents& path)
+  virtual bool CreateFolder(const UriComponents& path) ORTHANC_OVERRIDE
   {
     if (IsUploadedFolder(path))
     {
@@ -756,6 +756,11 @@
     }
   }
 
+  virtual bool DeleteItem(const std::vector<std::string>& path) ORTHANC_OVERRIDE
+  {
+    return false;  // read-only
+  }
+
   virtual void Start() ORTHANC_OVERRIDE
   {
     LOG(WARNING) << "Starting WebDAV";
@@ -771,13 +776,36 @@
 
 
 
-static const char* const DICOM_IDENTIFIERS = "DicomIdentifiers";
+static const char* const BY_UIDS = "by-uids";
 
 class DummyBucket2 : public IWebDavBucket  // TODO
 {
 private:
   ServerContext&  context_;
 
+
+  static void LookupTime(boost::posix_time::ptime& target,
+                         ServerContext& context,
+                         const std::string& publicId,
+                         MetadataType metadata)
+  {
+    std::string value;
+    if (context.GetIndex().LookupMetadata(value, publicId, metadata))
+    {
+      try
+      {
+        target = boost::posix_time::from_iso_string(value);
+        return;
+      }
+      catch (std::exception& e)
+      {
+      }
+    }
+
+    target = boost::posix_time::second_clock::universal_time();  // Now
+  }
+
+  
   class DicomIdentifiersVisitor : public ServerContext::ILookupVisitor
   {
   private:
@@ -813,18 +841,23 @@
                        const Json::Value* dicomAsJson  /* unused (*) */)  ORTHANC_OVERRIDE
     {
       DicomTag tag(0, 0);
+      MetadataType dateMetadata;
+
       switch (level_)
       {
         case ResourceType_Study:
           tag = DICOM_TAG_STUDY_INSTANCE_UID;
+          dateMetadata = MetadataType_LastUpdate;
           break;
 
         case ResourceType_Series:
           tag = DICOM_TAG_SERIES_INSTANCE_UID;
+          dateMetadata = MetadataType_LastUpdate;
           break;
         
         case ResourceType_Instance:
           tag = DICOM_TAG_SOP_INSTANCE_UID;
+          dateMetadata = MetadataType_Instance_ReceptionDate;
           break;
 
         default:
@@ -835,6 +868,8 @@
       if (mainDicomTags.LookupStringValue(s, tag, false) &&
           !s.empty())
       {
+        std::unique_ptr<Resource> resource;
+
         if (level_ == ResourceType_Instance)
         {
           FileInfo info;
@@ -843,13 +878,19 @@
             std::unique_ptr<File> f(new File(s + ".dcm"));
             f->SetMimeType(MimeType_Dicom);
             f->SetContentLength(info.GetUncompressedSize());
-            target_.AddResource(f.release());
+            resource.reset(f.release());
           }
         }
         else
         {
-          target_.AddResource(new Folder(s));
+          resource.reset(new Folder(s));
         }
+
+        boost::posix_time::ptime t;
+        LookupTime(t, context_, publicId, dateMetadata);
+        resource->SetCreationTime(t);
+        
+        target_.AddResource(resource.release());
       }
     }
   };
@@ -860,13 +901,16 @@
     ServerContext&  context_;
     bool            success_;
     std::string&    target_;
+    boost::posix_time::ptime&  modificationTime_;
 
   public:
     DicomFileVisitor(ServerContext& context,
-                     std::string& target) :
+                     std::string& target,
+                     boost::posix_time::ptime& modificationTime) :
       context_(context),
       success_(false),
-      target_(target)
+      target_(target),
+      modificationTime_(modificationTime)
     {
     }
 
@@ -874,7 +918,58 @@
     {
       return success_;
     }
+
+    virtual bool IsDicomAsJsonNeeded() const ORTHANC_OVERRIDE
+    {
+      return false;   // (*)
+    }
       
+    virtual void MarkAsComplete() ORTHANC_OVERRIDE
+    {
+    }
+
+    virtual void Visit(const std::string& publicId,
+                       const std::string& instanceId   /* unused     */,
+                       const DicomMap& mainDicomTags,
+                       const Json::Value* dicomAsJson  /* unused (*) */)  ORTHANC_OVERRIDE
+    {
+      if (success_)
+      {
+        success_ = false;  // Two matches => Error
+      }
+      else
+      {
+        LookupTime(modificationTime_, context_, publicId, MetadataType_Instance_ReceptionDate);
+        context_.ReadDicom(target_, publicId);
+        success_ = true;
+      }
+    }
+  };
+  
+  class OrthancJsonVisitor : public ServerContext::ILookupVisitor
+  {
+  private:
+    ServerContext&  context_;
+    bool            success_;
+    std::string&    target_;
+    ResourceType    level_;
+
+  public:
+    OrthancJsonVisitor(ServerContext& context,
+                       std::string& target,
+                       ResourceType level) :
+      context_(context),
+      success_(false),
+      target_(target),
+      level_(level)
+    {
+    }
+
+    bool IsSuccess() const
+    {
+      return success_;
+    }
+
     virtual bool IsDicomAsJsonNeeded() const ORTHANC_OVERRIDE
     {
       return false;   // (*)
@@ -889,10 +984,47 @@
                        const DicomMap& mainDicomTags,
                        const Json::Value* dicomAsJson  /* unused (*) */)  ORTHANC_OVERRIDE
     {
-      context_.ReadDicom(target_, publicId);
-      success_ = true;
+      Json::Value info;
+      if (context_.GetIndex().LookupResource(info, publicId, level_))
+      {
+        if (success_)
+        {
+          success_ = false;  // Two matches => Error
+        }
+        else
+        {
+          target_ = info.toStyledString();
+
+          // Replace UNIX newlines with DOS newlines 
+          boost::replace_all(target_, "\n", "\r\n");
+
+          success_ = true;
+        }
+      }
     }
   };
+
+
+  void AddVirtualFile(Collection& collection,
+                      const UriComponents& path,
+                      const std::string& filename)
+  {
+    MimeType mime;
+    std::string content;
+    boost::posix_time::ptime modification;
+
+    UriComponents p = path;
+    p.push_back(filename);
+
+    if (GetFileContent(mime, content, modification, p))
+    {
+      std::unique_ptr<File> f(new File(filename));
+      f->SetMimeType(mime);
+      f->SetContentLength(content.size());
+      f->SetCreationTime(modification);
+      collection.AddResource(f.release());
+    }
+  }    
   
 public:
   DummyBucket2(ServerContext& context) :
@@ -906,10 +1038,10 @@
     {
       return true;
     }
-    else if (path.front() == DICOM_IDENTIFIERS &&
-             path.size() <= 3)
+    else if (path.front() == BY_UIDS)
     {
-      return true;
+      return (path.size() <= 3 &&
+              (path.size() != 3 || path[2] != "study.json"));
     }
     else
     {
@@ -922,26 +1054,32 @@
   {
     if (path.empty())
     {
-      collection.AddResource(new Folder(DICOM_IDENTIFIERS));
+      collection.AddResource(new Folder(BY_UIDS));
       return true;
     }
-    else if (path.front() == DICOM_IDENTIFIERS)
+    else if (path.front() == BY_UIDS)
     {
       DatabaseLookup query;
       ResourceType level;
+      size_t limit = 0;  // By default, no limits
 
       if (path.size() == 1)
       {
         level = ResourceType_Study;
+        limit = 100;  // TODO
       }
       else if (path.size() == 2)
       {
+        AddVirtualFile(collection, path, "study.json");
+
         level = ResourceType_Series;
         query.AddRestConstraint(DICOM_TAG_STUDY_INSTANCE_UID, path[1],
                                 true /* case sensitive */, true /* mandatory tag */);
       }      
       else if (path.size() == 3)
       {
+        AddVirtualFile(collection, path, "series.json");
+
         level = ResourceType_Instance;
         query.AddRestConstraint(DICOM_TAG_STUDY_INSTANCE_UID, path[1],
                                 true /* case sensitive */, true /* mandatory tag */);
@@ -954,7 +1092,7 @@
       }
 
       DicomIdentifiersVisitor visitor(context_, collection, level);
-      context_.Apply(visitor, query, level, 0 /* since */, 100 /* limit */);
+      context_.Apply(visitor, query, level, 0 /* since */, limit);
       
       return true;
     }
@@ -969,32 +1107,60 @@
                               boost::posix_time::ptime& modificationTime, 
                               const UriComponents& path) ORTHANC_OVERRIDE
   {
-    if (path.size() == 4 &&
-        path[0] == DICOM_IDENTIFIERS &&
-        boost::ends_with(path[3], ".dcm"))
+    if (!path.empty() &&
+        path[0] == BY_UIDS)
     {
-      std::string sopInstanceUid = path[3];
-      sopInstanceUid.resize(sopInstanceUid.size() - 4);
+      if (path.size() == 3 &&
+          path[2] == "study.json")
+      {
+        DatabaseLookup query;
+        query.AddRestConstraint(DICOM_TAG_STUDY_INSTANCE_UID, path[1],
+                                true /* case sensitive */, true /* mandatory tag */);
       
-      mime = MimeType_Dicom;
+        OrthancJsonVisitor visitor(context_, content, ResourceType_Study);
+        context_.Apply(visitor, query, ResourceType_Study, 0 /* since */, 0 /* no limit */);
 
-      DatabaseLookup query;
-      query.AddRestConstraint(DICOM_TAG_STUDY_INSTANCE_UID, path[1],
+        mime = MimeType_Json;
+        return visitor.IsSuccess();
+      }
+      else if (path.size() == 4 &&
+               path[3] == "series.json")
+      {
+        DatabaseLookup query;
+        query.AddRestConstraint(DICOM_TAG_STUDY_INSTANCE_UID, path[1],
+                                true /* case sensitive */, true /* mandatory tag */);
+        query.AddRestConstraint(DICOM_TAG_SERIES_INSTANCE_UID, path[2],
                                 true /* case sensitive */, true /* mandatory tag */);
-      query.AddRestConstraint(DICOM_TAG_SERIES_INSTANCE_UID, path[2],
-                              true /* case sensitive */, true /* mandatory tag */);
-      query.AddRestConstraint(DICOM_TAG_SOP_INSTANCE_UID, sopInstanceUid,
-                              true /* case sensitive */, true /* mandatory tag */);
       
-      DicomFileVisitor visitor(context_, content);
-      context_.Apply(visitor, query, ResourceType_Instance, 0 /* since */, 100 /* limit */);
+        OrthancJsonVisitor visitor(context_, content, ResourceType_Series);
+        context_.Apply(visitor, query, ResourceType_Series, 0 /* since */, 0 /* no limit */);
 
-      return visitor.IsSuccess();
+        mime = MimeType_Json;
+        return visitor.IsSuccess();
+      }
+      else if (path.size() == 4 &&
+               boost::ends_with(path[3], ".dcm"))
+      {
+        std::string sopInstanceUid = path[3];
+        sopInstanceUid.resize(sopInstanceUid.size() - 4);
+        
+        DatabaseLookup query;
+        query.AddRestConstraint(DICOM_TAG_STUDY_INSTANCE_UID, path[1],
+                                true /* case sensitive */, true /* mandatory tag */);
+        query.AddRestConstraint(DICOM_TAG_SERIES_INSTANCE_UID, path[2],
+                                true /* case sensitive */, true /* mandatory tag */);
+        query.AddRestConstraint(DICOM_TAG_SOP_INSTANCE_UID, sopInstanceUid,
+                                true /* case sensitive */, true /* mandatory tag */);
+      
+        DicomFileVisitor visitor(context_, content, modificationTime);
+        context_.Apply(visitor, query, ResourceType_Instance, 0 /* since */, 0 /* no limit */);
+        
+        mime = MimeType_Dicom;
+        return visitor.IsSuccess();
+      }
     }
-    else
-    {
-      return false;
-    }
+      
+    return false;
   }
 
   
@@ -1010,6 +1176,12 @@
     return false;
   }
 
+  virtual bool DeleteItem(const std::vector<std::string>& path) ORTHANC_OVERRIDE
+  {
+    LOG(WARNING) << "DELETE: " << Toolbox::FlattenUri(path);
+    return false;  // read-only
+  }
+
   virtual void Start() ORTHANC_OVERRIDE
   {
   }