# HG changeset patch # User Sebastien Jodogne # Date 1602068457 -7200 # Node ID 688435755466e3b95a4b2a5ded805ac12f64343f # Parent 290ffcb0a147446199a0a34c24343a601c45959e added DELETE in WebDAV, first working virtual filesystem diff -r 290ffcb0a147 -r 688435755466 OrthancFramework/Sources/HttpServer/HttpServer.cpp --- 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 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 **/ diff -r 290ffcb0a147 -r 688435755466 OrthancFramework/Sources/HttpServer/IWebDavBucket.cpp --- 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); diff -r 290ffcb0a147 -r 688435755466 OrthancFramework/Sources/HttpServer/IWebDavBucket.h --- 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& path) = 0; + virtual bool DeleteItem(const std::vector& path) = 0; + virtual void Start() = 0; // During the shutdown of the Web server, give a chance to the diff -r 290ffcb0a147 -r 688435755466 OrthancFramework/Sources/HttpServer/WebDavStorage.cpp --- 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& path) + { + // TODO + return false; + } } diff -r 290ffcb0a147 -r 688435755466 OrthancFramework/Sources/HttpServer/WebDavStorage.h --- 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& path) ORTHANC_OVERRIDE; + virtual bool DeleteItem(const std::vector& path) ORTHANC_OVERRIDE; + virtual void Start() ORTHANC_OVERRIDE { } diff -r 290ffcb0a147 -r 688435755466 OrthancServer/Sources/main.cpp --- 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& 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; + if (level_ == ResourceType_Instance) { FileInfo info; @@ -843,13 +878,19 @@ std::unique_ptr 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 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& path) ORTHANC_OVERRIDE + { + LOG(WARNING) << "DELETE: " << Toolbox::FlattenUri(path); + return false; // read-only + } + virtual void Start() ORTHANC_OVERRIDE { }