Mercurial > hg > orthanc
changeset 4240:799c0c527ced
reorganization
author | Sebastien Jodogne <s.jodogne@gmail.com> |
---|---|
date | Fri, 09 Oct 2020 12:02:40 +0200 |
parents | c8754c4c1862 |
children | 3510da0e260c |
files | OrthancServer/CMakeLists.txt OrthancServer/Sources/OrthancWebDav.cpp OrthancServer/Sources/OrthancWebDav.h OrthancServer/Sources/main.cpp |
diffstat | 4 files changed, 1578 insertions(+), 1605 deletions(-) [+] |
line wrap: on
line diff
--- a/OrthancServer/CMakeLists.txt Fri Oct 09 11:38:03 2020 +0200 +++ b/OrthancServer/CMakeLists.txt Fri Oct 09 12:02:40 2020 +0200 @@ -116,6 +116,7 @@ ${CMAKE_SOURCE_DIR}/Sources/OrthancRestApi/OrthancRestModalities.cpp ${CMAKE_SOURCE_DIR}/Sources/OrthancRestApi/OrthancRestResources.cpp ${CMAKE_SOURCE_DIR}/Sources/OrthancRestApi/OrthancRestSystem.cpp + ${CMAKE_SOURCE_DIR}/Sources/OrthancWebDav.cpp ${CMAKE_SOURCE_DIR}/Sources/QueryRetrieveHandler.cpp ${CMAKE_SOURCE_DIR}/Sources/Search/DatabaseConstraint.cpp ${CMAKE_SOURCE_DIR}/Sources/Search/DatabaseLookup.cpp
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/OrthancWebDav.cpp Fri Oct 09 12:02:40 2020 +0200 @@ -0,0 +1,1446 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 Osimis S.A., 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. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * 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 "OrthancWebDav.h" + +#include "../../OrthancFramework/Sources/DicomFormat/DicomArray.h" +#include "../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h" +#include "../../OrthancFramework/Sources/HttpServer/WebDavStorage.h" +#include "Search/DatabaseLookup.h" +#include "ServerContext.h" + +#include <boost/regex.hpp> +#include <boost/algorithm/string/predicate.hpp> + + +static const char* const BY_PATIENTS = "by-patients"; +static const char* const BY_STUDIES = "by-studies"; +static const char* const BY_DATE = "by-dates"; +static const char* const BY_UIDS = "by-uids"; +static const char* const UPLOADS = "uploads"; +static const char* const MAIN_DICOM_TAGS = "MainDicomTags"; + + +namespace Orthanc +{ + static boost::posix_time::ptime GetNow() + { + return boost::posix_time::second_clock::universal_time(); + } + + + 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 = GetNow(); + } + + + class OrthancWebDav::DicomIdentifiersVisitor : public ServerContext::ILookupVisitor + { + private: + ServerContext& context_; + bool isComplete_; + Collection& target_; + ResourceType level_; + + public: + DicomIdentifiersVisitor(ServerContext& context, + Collection& target, + ResourceType level) : + context_(context), + isComplete_(false), + target_(target), + level_(level) + { + } + + virtual bool IsDicomAsJsonNeeded() const ORTHANC_OVERRIDE + { + return false; // (*) + } + + virtual void MarkAsComplete() ORTHANC_OVERRIDE + { + isComplete_ = true; // TODO + } + + virtual void Visit(const std::string& publicId, + const std::string& instanceId /* unused */, + const DicomMap& mainDicomTags, + const Json::Value* dicomAsJson /* unused (*) */) ORTHANC_OVERRIDE + { + DicomTag tag(0, 0); + MetadataType timeMetadata; + + switch (level_) + { + case ResourceType_Study: + tag = DICOM_TAG_STUDY_INSTANCE_UID; + timeMetadata = MetadataType_LastUpdate; + break; + + case ResourceType_Series: + tag = DICOM_TAG_SERIES_INSTANCE_UID; + timeMetadata = MetadataType_LastUpdate; + break; + + case ResourceType_Instance: + tag = DICOM_TAG_SOP_INSTANCE_UID; + timeMetadata = MetadataType_Instance_ReceptionDate; + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + + std::string s; + if (mainDicomTags.LookupStringValue(s, tag, false) && + !s.empty()) + { + std::unique_ptr<Resource> resource; + + if (level_ == ResourceType_Instance) + { + FileInfo info; + if (context_.GetIndex().LookupAttachment(info, publicId, FileContentType_Dicom)) + { + std::unique_ptr<File> f(new File(s + ".dcm")); + f->SetMimeType(MimeType_Dicom); + f->SetContentLength(info.GetUncompressedSize()); + resource.reset(f.release()); + } + } + else + { + resource.reset(new Folder(s)); + } + + if (resource.get() != NULL) + { + boost::posix_time::ptime t; + LookupTime(t, context_, publicId, timeMetadata); + resource->SetCreationTime(t); + target_.AddResource(resource.release()); + } + } + } + }; + + + class OrthancWebDav::DicomFileVisitor : public ServerContext::ILookupVisitor + { + private: + ServerContext& context_; + bool success_; + std::string& target_; + boost::posix_time::ptime& time_; + + public: + DicomFileVisitor(ServerContext& context, + std::string& target, + boost::posix_time::ptime& time) : + context_(context), + success_(false), + target_(target), + time_(time) + { + } + + bool IsSuccess() const + { + 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(time_, context_, publicId, MetadataType_Instance_ReceptionDate); + context_.ReadDicom(target_, publicId); + success_ = true; + } + } + }; + + + class OrthancWebDav::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; // (*) + } + + 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 + { + 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; + } + } + } + }; + + + class OrthancWebDav::ResourcesIndex : public boost::noncopyable + { + public: + typedef std::map<std::string, std::string> Map; + + private: + ServerContext& context_; + ResourceType level_; + std::string template_; + Map pathToResource_; + Map resourceToPath_; + + void CheckInvariants() + { +#ifndef NDEBUG + assert(pathToResource_.size() == resourceToPath_.size()); + + for (Map::const_iterator it = pathToResource_.begin(); it != pathToResource_.end(); ++it) + { + assert(resourceToPath_[it->second] == it->first); + } + + for (Map::const_iterator it = resourceToPath_.begin(); it != resourceToPath_.end(); ++it) + { + assert(pathToResource_[it->second] == it->first); + } +#endif + } + + void AddTags(DicomMap& target, + const std::string& resourceId, + ResourceType tagsFromLevel) + { + DicomMap tags; + if (context_.GetIndex().GetMainDicomTags(tags, resourceId, level_, tagsFromLevel)) + { + target.Merge(tags); + } + } + + void Register(const std::string& resourceId) + { + // Don't register twice the same resource + if (resourceToPath_.find(resourceId) == resourceToPath_.end()) + { + std::string name = template_; + + DicomMap tags; + + AddTags(tags, resourceId, level_); + + if (level_ == ResourceType_Study) + { + AddTags(tags, resourceId, ResourceType_Patient); + } + + DicomArray arr(tags); + for (size_t i = 0; i < arr.GetSize(); i++) + { + const DicomElement& element = arr.GetElement(i); + if (!element.GetValue().IsNull() && + !element.GetValue().IsBinary()) + { + const std::string tag = FromDcmtkBridge::GetTagName(element.GetTag(), ""); + boost::replace_all(name, "{{" + tag + "}}", element.GetValue().GetContent()); + } + } + + // Blank the tags that were not matched + static const boost::regex REGEX_BLANK_TAGS("{{.*?}}"); // non-greedy match + name = boost::regex_replace(name, REGEX_BLANK_TAGS, ""); + + // UTF-8 characters cannot be used on Windows XP + name = Toolbox::ConvertToAscii(name); + boost::replace_all(name, "/", ""); + boost::replace_all(name, "\\", ""); + + // Trim sequences of spaces as one single space + static const boost::regex REGEX_TRIM_SPACES("{{.*?}}"); + name = boost::regex_replace(name, REGEX_TRIM_SPACES, " "); + name = Toolbox::StripSpaces(name); + + size_t count = 0; + for (;;) + { + std::string path = name; + if (count > 0) + { + path += " (" + boost::lexical_cast<std::string>(count) + ")"; + } + + if (pathToResource_.find(path) == pathToResource_.end()) + { + pathToResource_[path] = resourceId; + resourceToPath_[resourceId] = path; + return; + } + + count++; + } + + throw OrthancException(ErrorCode_InternalError); + } + } + + public: + ResourcesIndex(ServerContext& context, + ResourceType level, + const std::string& templateString) : + context_(context), + level_(level), + template_(templateString) + { + } + + ResourceType GetLevel() const + { + return level_; + } + + void Refresh(std::set<std::string>& removedPaths /* out */, + const std::set<std::string>& resources) + { + CheckInvariants(); + + // Detect the resources that have been removed since last refresh + removedPaths.clear(); + std::set<std::string> removedResources; + + for (Map::iterator it = resourceToPath_.begin(); it != resourceToPath_.end(); ++it) + { + if (resources.find(it->first) == resources.end()) + { + const std::string& path = it->second; + + assert(pathToResource_.find(path) != pathToResource_.end()); + pathToResource_.erase(path); + removedPaths.insert(path); + + removedResources.insert(it->first); // Delay the removal to avoid disturbing the iterator + } + } + + // Remove the missing resources + for (std::set<std::string>::const_iterator it = removedResources.begin(); it != removedResources.end(); ++it) + { + assert(resourceToPath_.find(*it) != resourceToPath_.end()); + resourceToPath_.erase(*it); + } + + CheckInvariants(); + + for (std::set<std::string>::const_iterator it = resources.begin(); it != resources.end(); ++it) + { + Register(*it); + } + + CheckInvariants(); + } + + const Map& GetPathToResource() const + { + return pathToResource_; + } + }; + + + class OrthancWebDav::InstancesOfSeries : public INode + { + private: + ServerContext& context_; + std::string parentSeries_; + + public: + InstancesOfSeries(ServerContext& context, + const std::string& parentSeries) : + context_(context), + parentSeries_(parentSeries) + { + } + + virtual bool ListCollection(IWebDavBucket::Collection& target, + const UriComponents& path) ORTHANC_OVERRIDE + { + if (path.empty()) + { + std::list<std::string> resources; + try + { + context_.GetIndex().GetChildren(resources, parentSeries_); + } + catch (OrthancException&) + { + // Unknown (or deleted) parent series + return false; + } + + for (std::list<std::string>::const_iterator + it = resources.begin(); it != resources.end(); ++it) + { + boost::posix_time::ptime time; + LookupTime(time, context_, *it, MetadataType_Instance_ReceptionDate); + + FileInfo info; + if (context_.GetIndex().LookupAttachment(info, *it, FileContentType_Dicom)) + { + std::unique_ptr<File> resource(new File(*it + ".dcm")); + resource->SetMimeType(MimeType_Dicom); + resource->SetContentLength(info.GetUncompressedSize()); + resource->SetCreationTime(time); + target.AddResource(resource.release()); + } + } + + return true; + } + else + { + return false; + } + } + + virtual bool GetFileContent(MimeType& mime, + std::string& content, + boost::posix_time::ptime& time, + const UriComponents& path) ORTHANC_OVERRIDE + { + if (path.size() == 1 && + boost::ends_with(path[0], ".dcm")) + { + std::string instanceId = path[0].substr(0, path[0].size() - 4); + + try + { + mime = MimeType_Dicom; + context_.ReadDicom(content, instanceId); + LookupTime(time, context_, instanceId, MetadataType_Instance_ReceptionDate); + return true; + } + catch (OrthancException&) + { + // File was removed + return false; + } + } + else + { + return false; + } + } + }; + + + + /** + * The "InternalNode" class corresponds to a non-leaf node in the + * WebDAV tree, that only contains subfolders (no file). + * + * TODO: Implement a LRU index to dynamically remove the oldest + * children on high RAM usage. + **/ + class OrthancWebDav::InternalNode : public INode + { + private: + typedef std::map<std::string, INode*> Children; + + Children children_; + + INode* GetChild(const std::string& path) // Don't delete the result pointer! + { + Children::const_iterator child = children_.find(path); + if (child == children_.end()) + { + INode* child = CreateChild(path); + + if (child == NULL) + { + return NULL; + } + else + { + children_[path] = child; + return child; + } + } + else + { + assert(child->second != NULL); + return child->second; + } + } + + protected: + void RemoveSubfolder(const std::string& path) + { + Children::iterator child = children_.find(path); + if (child != children_.end()) + { + assert(child->second != NULL); + delete child->second; + children_.erase(child); + } + } + + virtual void Refresh() = 0; + + virtual bool ListSubfolders(IWebDavBucket::Collection& target) = 0; + + virtual INode* CreateChild(const std::string& path) = 0; + + public: + virtual ~InternalNode() + { + for (Children::iterator it = children_.begin(); it != children_.end(); ++it) + { + assert(it->second != NULL); + delete it->second; + } + } + + virtual bool ListCollection(IWebDavBucket::Collection& target, + const UriComponents& path) + ORTHANC_OVERRIDE ORTHANC_FINAL + { + Refresh(); + + if (path.empty()) + { + return ListSubfolders(target); + } + else + { + // Recursivity + INode* child = GetChild(path[0]); + if (child == NULL) + { + return false; + } + else + { + UriComponents subpath(path.begin() + 1, path.end()); + return child->ListCollection(target, subpath); + } + } + } + + virtual bool GetFileContent(MimeType& mime, + std::string& content, + boost::posix_time::ptime& time, + const UriComponents& path) + ORTHANC_OVERRIDE ORTHANC_FINAL + { + if (path.empty()) + { + return false; // An internal node doesn't correspond to a file + } + else + { + // Recursivity + Refresh(); + + INode* child = GetChild(path[0]); + if (child == NULL) + { + return false; + } + else + { + UriComponents subpath(path.begin() + 1, path.end()); + return child->GetFileContent(mime, content, time, subpath); + } + } + } + }; + + + class OrthancWebDav::ListOfResources : public InternalNode + { + private: + ServerContext& context_; + const Templates& templates_; + std::unique_ptr<ResourcesIndex> index_; + MetadataType timeMetadata_; + + protected: + virtual void Refresh() ORTHANC_OVERRIDE ORTHANC_FINAL + { + std::list<std::string> resources; + GetCurrentResources(resources); + + std::set<std::string> removedPaths; + index_->Refresh(removedPaths, std::set<std::string>(resources.begin(), resources.end())); + + // Remove the children whose associated resource doesn't exist anymore + for (std::set<std::string>::const_iterator + it = removedPaths.begin(); it != removedPaths.end(); ++it) + { + RemoveSubfolder(*it); + } + } + + virtual bool ListSubfolders(IWebDavBucket::Collection& target) ORTHANC_OVERRIDE ORTHANC_FINAL + { + if (index_->GetLevel() == ResourceType_Instance) + { + // Not a collection, no subfolders + return false; + } + else + { + const ResourcesIndex::Map& paths = index_->GetPathToResource(); + + for (ResourcesIndex::Map::const_iterator it = paths.begin(); it != paths.end(); ++it) + { + boost::posix_time::ptime time; + LookupTime(time, context_, it->second, timeMetadata_); + + std::unique_ptr<IWebDavBucket::Resource> resource(new IWebDavBucket::Folder(it->first)); + resource->SetCreationTime(time); + target.AddResource(resource.release()); + } + + return true; + } + } + + virtual INode* CreateChild(const std::string& path) ORTHANC_OVERRIDE ORTHANC_FINAL + { + ResourcesIndex::Map::const_iterator resource = index_->GetPathToResource().find(path); + if (resource == index_->GetPathToResource().end()) + { + return NULL; + } + else + { + return CreateResourceNode(resource->second); + } + } + + ServerContext& GetContext() const + { + return context_; + } + + virtual void GetCurrentResources(std::list<std::string>& resources) = 0; + + virtual INode* CreateResourceNode(const std::string& resource) = 0; + + public: + ListOfResources(ServerContext& context, + ResourceType level, + const Templates& templates) : + context_(context), + templates_(templates) + { + Templates::const_iterator t = templates.find(level); + if (t == templates.end()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + index_.reset(new ResourcesIndex(context, level, t->second)); + + if (level == ResourceType_Instance) + { + timeMetadata_ = MetadataType_Instance_ReceptionDate; + } + else + { + timeMetadata_ = MetadataType_LastUpdate; + } + } + + ResourceType GetLevel() const + { + return index_->GetLevel(); + } + + const Templates& GetTemplates() const + { + return templates_; + } + }; + + + + class OrthancWebDav::SingleDicomResource : public ListOfResources + { + private: + std::string parentId_; + + protected: + virtual void GetCurrentResources(std::list<std::string>& resources) ORTHANC_OVERRIDE + { + try + { + GetContext().GetIndex().GetChildren(resources, parentId_); + } + catch (OrthancException&) + { + // Unknown parent resource + resources.clear(); + } + } + + virtual INode* CreateResourceNode(const std::string& resource) ORTHANC_OVERRIDE + { + if (GetLevel() == ResourceType_Instance) + { + return NULL; + } + else if (GetLevel() == ResourceType_Series) + { + return new InstancesOfSeries(GetContext(), resource); + } + else + { + ResourceType l = GetChildResourceType(GetLevel()); + return new SingleDicomResource(GetContext(), l, resource, GetTemplates()); + } + } + + public: + SingleDicomResource(ServerContext& context, + ResourceType level, + const std::string& parentId, + const Templates& templates) : + ListOfResources(context, level, templates), + parentId_(parentId) + { + } + }; + + + class OrthancWebDav::RootNode : public ListOfResources + { + protected: + virtual void GetCurrentResources(std::list<std::string>& resources) ORTHANC_OVERRIDE + { + GetContext().GetIndex().GetAllUuids(resources, GetLevel()); + } + + virtual INode* CreateResourceNode(const std::string& resource) ORTHANC_OVERRIDE + { + if (GetLevel() == ResourceType_Series) + { + return new InstancesOfSeries(GetContext(), resource); + } + else + { + ResourceType l = GetChildResourceType(GetLevel()); + return new SingleDicomResource(GetContext(), l, resource, GetTemplates()); + } + } + + public: + RootNode(ServerContext& context, + ResourceType level, + const Templates& templates) : + ListOfResources(context, level, templates) + { + } + }; + + + class OrthancWebDav::ListOfStudiesByDate : public ListOfResources + { + private: + std::string year_; + std::string month_; + + class Visitor : public ServerContext::ILookupVisitor + { + private: + std::list<std::string>& resources_; + + public: + Visitor(std::list<std::string>& resources) : + resources_(resources) + { + } + + 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 + { + resources_.push_back(publicId); + } + }; + + protected: + virtual void GetCurrentResources(std::list<std::string>& resources) ORTHANC_OVERRIDE + { + DatabaseLookup query; + query.AddRestConstraint(DICOM_TAG_STUDY_DATE, year_ + month_ + "01-" + year_ + month_ + "31", + true /* case sensitive */, true /* mandatory tag */); + + Visitor visitor(resources); + GetContext().Apply(visitor, query, ResourceType_Study, 0 /* since */, 0 /* no limit */); + } + + virtual INode* CreateResourceNode(const std::string& resource) ORTHANC_OVERRIDE + { + return new SingleDicomResource(GetContext(), ResourceType_Series, resource, GetTemplates()); + } + + public: + ListOfStudiesByDate(ServerContext& context, + const std::string& year, + const std::string& month, + const Templates& templates) : + ListOfResources(context, ResourceType_Study, templates), + year_(year), + month_(month) + { + if (year.size() != 4 || + month.size() != 2) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + }; + + + class OrthancWebDav::ListOfStudiesByMonth : public InternalNode + { + private: + ServerContext& context_; + std::string year_; + const Templates& templates_; + + class Visitor : public ServerContext::ILookupVisitor + { + private: + std::set<std::string> months_; + + public: + Visitor() + { + } + + const std::set<std::string>& GetMonths() const + { + return months_; + } + + 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 + { + std::string s; + if (mainDicomTags.LookupStringValue(s, DICOM_TAG_STUDY_DATE, false) && + s.size() == 8) + { + months_.insert(s.substr(4, 2)); // Get the month from "YYYYMMDD" + } + } + }; + + protected: + virtual void Refresh() ORTHANC_OVERRIDE + { + } + + virtual bool ListSubfolders(IWebDavBucket::Collection& target) ORTHANC_OVERRIDE + { + DatabaseLookup query; + query.AddRestConstraint(DICOM_TAG_STUDY_DATE, year_ + "0101-" + year_ + "1231", + true /* case sensitive */, true /* mandatory tag */); + + Visitor visitor; + context_.Apply(visitor, query, ResourceType_Study, 0 /* since */, 0 /* no limit */); + + for (std::set<std::string>::const_iterator it = visitor.GetMonths().begin(); + it != visitor.GetMonths().end(); ++it) + { + target.AddResource(new IWebDavBucket::Folder(year_ + "-" + *it)); + } + + return true; + } + + virtual INode* CreateChild(const std::string& path) ORTHANC_OVERRIDE + { + if (path.size() != 7) // Format: "YYYY-MM" + { + throw OrthancException(ErrorCode_InternalError); + } + else + { + const std::string year = path.substr(0, 4); + const std::string month = path.substr(5, 2); + return new ListOfStudiesByDate(context_, year, month, templates_); + } + } + + public: + ListOfStudiesByMonth(ServerContext& context, + const std::string& year, + const Templates& templates) : + context_(context), + year_(year), + templates_(templates) + { + if (year_.size() != 4) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + }; + + + class OrthancWebDav::ListOfStudiesByYear : public InternalNode + { + private: + ServerContext& context_; + const Templates& templates_; + + protected: + virtual void Refresh() ORTHANC_OVERRIDE + { + } + + virtual bool ListSubfolders(IWebDavBucket::Collection& target) ORTHANC_OVERRIDE + { + std::list<std::string> resources; + context_.GetIndex().GetAllUuids(resources, ResourceType_Study); + + std::set<std::string> years; + + for (std::list<std::string>::const_iterator it = resources.begin(); it != resources.end(); ++it) + { + DicomMap tags; + std::string studyDate; + if (context_.GetIndex().GetMainDicomTags(tags, *it, ResourceType_Study, ResourceType_Study) && + tags.LookupStringValue(studyDate, DICOM_TAG_STUDY_DATE, false) && + studyDate.size() == 8) + { + years.insert(studyDate.substr(0, 4)); // Get the year from "YYYYMMDD" + } + } + + for (std::set<std::string>::const_iterator it = years.begin(); it != years.end(); ++it) + { + target.AddResource(new IWebDavBucket::Folder(*it)); + } + + return true; + } + + virtual INode* CreateChild(const std::string& path) ORTHANC_OVERRIDE + { + return new ListOfStudiesByMonth(context_, path, templates_); + } + + public: + ListOfStudiesByYear(ServerContext& context, + const Templates& templates) : + context_(context), + templates_(templates) + { + } + }; + + + void OrthancWebDav::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()); + } + } + + + void OrthancWebDav::UploadWorker(OrthancWebDav* that) + { + assert(that != NULL); + + boost::posix_time::ptime lastModification = GetNow(); + + while (that->running_) + { + std::unique_ptr<IDynamicObject> obj(that->uploadQueue_.Dequeue(100)); + if (obj.get() != NULL) + { + that->Upload(reinterpret_cast<const SingleValueObject<std::string>&>(*obj).GetValue()); + lastModification = GetNow(); + } + else if (GetNow() - lastModification > boost::posix_time::seconds(10)) + { + // After every 10 seconds of inactivity, remove the empty folders + LOG(INFO) << "Cleaning up the empty WebDAV upload folders"; + that->uploads_.RemoveEmptyFolders(); + lastModification = GetNow(); + } + } + } + + + void OrthancWebDav::Upload(const std::string& path) + { + UriComponents uri; + Toolbox::SplitUriComponents(uri, path); + + LOG(INFO) << "Upload from WebDAV: " << path; + + MimeType mime; + std::string content; + boost::posix_time::ptime time; + if (uploads_.GetFileContent(mime, content, time, uri)) + { + DicomInstanceToStore instance; + // instance.SetOrigin(DicomInstanceOrigin_WebDav); + instance.SetBuffer(content.c_str(), content.size()); + + std::string publicId; + StoreStatus status = context_.Store(publicId, instance, StoreInstanceMode_Default); + if (status == StoreStatus_Success || + status == StoreStatus_AlreadyStored) + { + LOG(INFO) << "Successfully imported DICOM instance from WebDAV: " << path << " (Orthanc ID: " << publicId << ")"; + uploads_.DeleteItem(uri); + } + else + { + LOG(WARNING) << "Cannot import DICOM instance from WebWAV: " << path; + } + } + } + + + OrthancWebDav::OrthancWebDav(ServerContext& context) : + context_(context), + uploads_(false /* store uploads as temporary files */), + running_(false) + { + patientsTemplates_[ResourceType_Patient] = "{{PatientID}} - {{PatientName}}"; + patientsTemplates_[ResourceType_Study] = "{{StudyDate}} - {{StudyDescription}}"; + patientsTemplates_[ResourceType_Series] = "{{Modality}} - {{SeriesDescription}}"; + + studiesTemplates_[ResourceType_Study] = "{{PatientID}} - {{PatientName}} - {{StudyDescription}}"; + studiesTemplates_[ResourceType_Series] = patientsTemplates_[ResourceType_Series]; + + patients_.reset(new RootNode(context, ResourceType_Patient, patientsTemplates_)); + studies_.reset(new RootNode(context, ResourceType_Study, studiesTemplates_)); + dates_.reset(new ListOfStudiesByYear(context, studiesTemplates_)); + } + + + bool OrthancWebDav::IsExistingFolder(const UriComponents& path) + { + if (path.empty()) + { + return true; + } + else if (path[0] == BY_UIDS) + { + return (path.size() <= 3 && + (path.size() != 3 || path[2] != "study.json")); + } + else if (path[0] == BY_PATIENTS) + { + IWebDavBucket::Collection tmp; + return patients_->ListCollection(tmp, UriComponents(path.begin() + 1, path.end())); + } + else if (path[0] == BY_STUDIES) + { + IWebDavBucket::Collection tmp; + return studies_->ListCollection(tmp, UriComponents(path.begin() + 1, path.end())); + } + else if (path[0] == BY_DATE) + { + IWebDavBucket::Collection tmp; + return dates_->ListCollection(tmp, UriComponents(path.begin() + 1, path.end())); + } + else if (path[0] == UPLOADS) + { + return uploads_.IsExistingFolder(UriComponents(path.begin() + 1, path.end())); + } + else + { + return false; + } + } + + + bool OrthancWebDav::ListCollection(Collection& collection, + const UriComponents& path) + { + if (path.empty()) + { + collection.AddResource(new Folder(BY_DATE)); + collection.AddResource(new Folder(BY_PATIENTS)); + collection.AddResource(new Folder(BY_STUDIES)); + collection.AddResource(new Folder(BY_UIDS)); + collection.AddResource(new Folder(UPLOADS)); + return true; + } + else if (path[0] == 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 */); + query.AddRestConstraint(DICOM_TAG_SERIES_INSTANCE_UID, path[2], + true /* case sensitive */, true /* mandatory tag */); + } + else + { + return false; + } + + DicomIdentifiersVisitor visitor(context_, collection, level); + context_.Apply(visitor, query, level, 0 /* since */, limit); + + return true; + } + else if (path[0] == BY_PATIENTS) + { + return patients_->ListCollection(collection, UriComponents(path.begin() + 1, path.end())); + } + else if (path[0] == BY_STUDIES) + { + return studies_->ListCollection(collection, UriComponents(path.begin() + 1, path.end())); + } + else if (path[0] == BY_DATE) + { + return dates_->ListCollection(collection, UriComponents(path.begin() + 1, path.end())); + } + else if (path[0] == UPLOADS) + { + return uploads_.ListCollection(collection, UriComponents(path.begin() + 1, path.end())); + } + else + { + return false; + } + } + + + bool OrthancWebDav::GetFileContent(MimeType& mime, + std::string& content, + boost::posix_time::ptime& modificationTime, + const UriComponents& path) + { + if (path.empty()) + { + return false; + } + else if (path[0] == BY_UIDS) + { + 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 */); + + OrthancJsonVisitor visitor(context_, content, ResourceType_Study); + context_.Apply(visitor, query, ResourceType_Study, 0 /* since */, 0 /* no limit */); + + 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 */); + + OrthancJsonVisitor visitor(context_, content, ResourceType_Series); + context_.Apply(visitor, query, ResourceType_Series, 0 /* since */, 0 /* no limit */); + + 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; + } + } + else if (path[0] == BY_PATIENTS) + { + return patients_->GetFileContent(mime, content, modificationTime, UriComponents(path.begin() + 1, path.end())); + } + else if (path[0] == BY_STUDIES) + { + return studies_->GetFileContent(mime, content, modificationTime, UriComponents(path.begin() + 1, path.end())); + } + else if (path[0] == UPLOADS) + { + return uploads_.GetFileContent(mime, content, modificationTime, UriComponents(path.begin() + 1, path.end())); + } + else + { + return false; + } + } + + + bool OrthancWebDav::StoreFile(const std::string& content, + const UriComponents& path) + { + if (path.size() >= 1 && + path[0] == UPLOADS) + { + UriComponents subpath(UriComponents(path.begin() + 1, path.end())); + + if (uploads_.StoreFile(content, subpath)) + { + if (!content.empty()) + { + uploadQueue_.Enqueue(new SingleValueObject<std::string>(Toolbox::FlattenUri(subpath))); + } + return true; + } + else + { + return false; + } + } + else + { + return false; + } + } + + + bool OrthancWebDav::CreateFolder(const UriComponents& path) + { + if (path.size() >= 1 && + path[0] == UPLOADS) + { + return uploads_.CreateFolder(UriComponents(path.begin() + 1, path.end())); + } + else + { + return false; + } + } + + + bool OrthancWebDav::DeleteItem(const std::vector<std::string>& path) + { + if (path.size() >= 1 && + path[0] == UPLOADS) + { + return uploads_.DeleteItem(UriComponents(path.begin() + 1, path.end())); + } + else + { + return false; // read-only + } + } + + + void OrthancWebDav::Start() + { + if (running_) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + LOG(INFO) << "Starting the WebDAV upload thread"; + running_ = true; + uploadThread_ = boost::thread(UploadWorker, this); + } + } + + + void OrthancWebDav::Stop() + { + if (running_) + { + LOG(INFO) << "Stopping the WebDAV upload thread"; + running_ = false; + if (uploadThread_.joinable()) + { + uploadThread_.join(); + } + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/OrthancWebDav.h Fri Oct 09 12:02:40 2020 +0200 @@ -0,0 +1,128 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 Osimis S.A., 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. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * 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/>. + **/ + + +#pragma once + +#include "../../OrthancFramework/Sources/HttpServer/WebDavStorage.h" +#include "../../OrthancFramework/Sources/MultiThreading/SharedMessageQueue.h" +#include "../../OrthancFramework/Sources/Toolbox.h" + + +namespace Orthanc +{ + class ServerContext; + + class OrthancWebDav : public IWebDavBucket + { + private: + typedef std::map<ResourceType, std::string> Templates; + + class DicomIdentifiersVisitor; + class DicomFileVisitor; + class OrthancJsonVisitor; + class InstancesOfSeries; + class InternalNode; + class ListOfResources; + class ListOfStudiesByDate; + class ListOfStudiesByMonth; + class ListOfStudiesByYear; + class ResourcesIndex; + class RootNode; + class SingleDicomResource; + + class INode : public boost::noncopyable + { + public: + virtual ~INode() + { + } + + virtual bool ListCollection(IWebDavBucket::Collection& target, + const UriComponents& path) = 0; + + virtual bool GetFileContent(MimeType& mime, + std::string& content, + boost::posix_time::ptime& time, + const UriComponents& path) = 0; + }; + + + void AddVirtualFile(Collection& collection, + const UriComponents& path, + const std::string& filename); + + static void UploadWorker(OrthancWebDav* that); + + void Upload(const std::string& path); + + ServerContext& context_; + std::unique_ptr<INode> patients_; + std::unique_ptr<INode> studies_; + std::unique_ptr<INode> dates_; + Templates patientsTemplates_; + Templates studiesTemplates_; + WebDavStorage uploads_; + SharedMessageQueue uploadQueue_; + boost::thread uploadThread_; + bool running_; + + public: + OrthancWebDav(ServerContext& context); + + virtual ~OrthancWebDav() + { + Stop(); + } + + virtual bool IsExistingFolder(const UriComponents& path) ORTHANC_OVERRIDE; + + virtual bool ListCollection(Collection& collection, + const UriComponents& path) ORTHANC_OVERRIDE; + + virtual bool GetFileContent(MimeType& mime, + std::string& content, + boost::posix_time::ptime& modificationTime, + const UriComponents& path) ORTHANC_OVERRIDE; + + virtual bool StoreFile(const std::string& content, + const UriComponents& path) ORTHANC_OVERRIDE; + + virtual bool CreateFolder(const UriComponents& path) ORTHANC_OVERRIDE; + + virtual bool DeleteItem(const std::vector<std::string>& path) ORTHANC_OVERRIDE; + + virtual void Start() ORTHANC_OVERRIDE; + + virtual void Stop() ORTHANC_OVERRIDE; + }; +}
--- a/OrthancServer/Sources/main.cpp Fri Oct 09 11:38:03 2020 +0200 +++ b/OrthancServer/Sources/main.cpp Fri Oct 09 12:02:40 2020 +0200 @@ -34,8 +34,6 @@ #include "PrecompiledHeadersServer.h" #include "OrthancRestApi/OrthancRestApi.h" -#include <boost/algorithm/string/predicate.hpp> - #include "../../OrthancFramework/Sources/Compatibility.h" #include "../../OrthancFramework/Sources/DicomFormat/DicomArray.h" #include "../../OrthancFramework/Sources/DicomNetworking/DicomAssociationParameters.h" @@ -52,14 +50,13 @@ #include "OrthancGetRequestHandler.h" #include "OrthancInitialization.h" #include "OrthancMoveRequestHandler.h" +#include "OrthancWebDav.h" #include "ServerContext.h" #include "ServerJobs/StorageCommitmentScpJob.h" #include "ServerToolbox.h" #include "StorageCommitmentReports.h" -#include "../../OrthancFramework/Sources/HttpServer/WebDavStorage.h" // TODO -#include "Search/DatabaseLookup.h" // TODO -#include <boost/regex.hpp> // TODO +#include <boost/algorithm/string/predicate.hpp> using namespace Orthanc; @@ -615,1603 +612,6 @@ }; - - - - - -static const char* const UPLOAD_FOLDER = "upload"; - -class DummyBucket : public IWebDavBucket // TODO -{ -private: - ServerContext& context_; - WebDavStorage storage_; - - bool IsUploadedFolder(const UriComponents& path) const - { - return (path.size() >= 1 && path[0] == UPLOAD_FOLDER); - } - -public: - DummyBucket(ServerContext& context, - bool isMemory) : - context_(context), - storage_(isMemory) - { - } - - virtual bool IsExistingFolder(const UriComponents& path) ORTHANC_OVERRIDE - { - if (IsUploadedFolder(path)) - { - return storage_.IsExistingFolder(UriComponents(path.begin() + 1, path.end())); - } - 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 (IsUploadedFolder(path)) - { - return storage_.ListCollection(collection, UriComponents(path.begin() + 1, path.end())); - } - 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")); - f->SetContentLength(1024 * i); - f->SetMimeType(MimeType_PlainText); - collection.AddResource(f.release()); - } - - for (unsigned int i = 0; i < 5; i++) - { - 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& time, - const UriComponents& path) ORTHANC_OVERRIDE - { - if (path.empty()) - { - return false; - } - else if (IsUploadedFolder(path)) - { - return storage_.GetFileContent(mime, content, time, - UriComponents(path.begin() + 1, path.end())); - } - else if (path.back() == "IM0.dcm" || - path.back() == "IM1.dcm" || - path.back() == "IM2.dcm" || - path.back() == "IM3.dcm" || - path.back() == "IM4.dcm") - { - time = 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 - { - return false; - } - } - - - virtual bool StoreFile(const std::string& content, - const UriComponents& path) ORTHANC_OVERRIDE - { - if (IsUploadedFolder(path)) - { - return storage_.StoreFile(content, UriComponents(path.begin() + 1, path.end())); - } - else - { - LOG(WARNING) << "Writing to a read-only location in WebDAV: " << Toolbox::FlattenUri(path); - return false; - } - } - - - virtual bool CreateFolder(const UriComponents& path) ORTHANC_OVERRIDE - { - if (IsUploadedFolder(path)) - { - return storage_.CreateFolder(UriComponents(path.begin() + 1, path.end())); - } - else - { - LOG(WARNING) << "Writing to a read-only location in WebDAV: " << Toolbox::FlattenUri(path); - return false; - } - } - - virtual bool DeleteItem(const std::vector<std::string>& path) ORTHANC_OVERRIDE - { - return false; // read-only - } - - virtual void Start() ORTHANC_OVERRIDE - { - LOG(WARNING) << "Starting WebDAV"; - } - - virtual void Stop() ORTHANC_OVERRIDE - { - LOG(WARNING) << "Stopping WebDAV"; - } -}; - - - - - -static const char* const BY_PATIENTS = "by-patients"; -static const char* const BY_STUDIES = "by-studies"; -static const char* const BY_DATE = "by-dates"; -static const char* const BY_UIDS = "by-uids"; -static const char* const UPLOADS = "uploads"; -static const char* const MAIN_DICOM_TAGS = "MainDicomTags"; - -class DummyBucket2 : public IWebDavBucket // TODO -{ -private: - typedef std::map<ResourceType, std::string> Templates; - - - static boost::posix_time::ptime GetNow() - { - return boost::posix_time::second_clock::universal_time(); - } - - - 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 = GetNow(); - } - - - class DicomIdentifiersVisitor : public ServerContext::ILookupVisitor - { - private: - ServerContext& context_; - bool isComplete_; - Collection& target_; - ResourceType level_; - - public: - DicomIdentifiersVisitor(ServerContext& context, - Collection& target, - ResourceType level) : - context_(context), - isComplete_(false), - target_(target), - level_(level) - { - } - - virtual bool IsDicomAsJsonNeeded() const ORTHANC_OVERRIDE - { - return false; // (*) - } - - virtual void MarkAsComplete() ORTHANC_OVERRIDE - { - isComplete_ = true; // TODO - } - - virtual void Visit(const std::string& publicId, - const std::string& instanceId /* unused */, - const DicomMap& mainDicomTags, - const Json::Value* dicomAsJson /* unused (*) */) ORTHANC_OVERRIDE - { - DicomTag tag(0, 0); - MetadataType timeMetadata; - - switch (level_) - { - case ResourceType_Study: - tag = DICOM_TAG_STUDY_INSTANCE_UID; - timeMetadata = MetadataType_LastUpdate; - break; - - case ResourceType_Series: - tag = DICOM_TAG_SERIES_INSTANCE_UID; - timeMetadata = MetadataType_LastUpdate; - break; - - case ResourceType_Instance: - tag = DICOM_TAG_SOP_INSTANCE_UID; - timeMetadata = MetadataType_Instance_ReceptionDate; - break; - - default: - throw OrthancException(ErrorCode_InternalError); - } - - std::string s; - if (mainDicomTags.LookupStringValue(s, tag, false) && - !s.empty()) - { - std::unique_ptr<Resource> resource; - - if (level_ == ResourceType_Instance) - { - FileInfo info; - if (context_.GetIndex().LookupAttachment(info, publicId, FileContentType_Dicom)) - { - std::unique_ptr<File> f(new File(s + ".dcm")); - f->SetMimeType(MimeType_Dicom); - f->SetContentLength(info.GetUncompressedSize()); - resource.reset(f.release()); - } - } - else - { - resource.reset(new Folder(s)); - } - - if (resource.get() != NULL) - { - boost::posix_time::ptime t; - LookupTime(t, context_, publicId, timeMetadata); - resource->SetCreationTime(t); - target_.AddResource(resource.release()); - } - } - } - }; - - class DicomFileVisitor : public ServerContext::ILookupVisitor - { - private: - ServerContext& context_; - bool success_; - std::string& target_; - boost::posix_time::ptime& time_; - - public: - DicomFileVisitor(ServerContext& context, - std::string& target, - boost::posix_time::ptime& time) : - context_(context), - success_(false), - target_(target), - time_(time) - { - } - - bool IsSuccess() const - { - 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(time_, 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; // (*) - } - - 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 - { - 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()); - } - } - - - - - class ResourcesIndex : public boost::noncopyable - { - public: - typedef std::map<std::string, std::string> Map; - - private: - ServerContext& context_; - ResourceType level_; - std::string template_; - Map pathToResource_; - Map resourceToPath_; - - void CheckInvariants() - { -#ifndef NDEBUG - assert(pathToResource_.size() == resourceToPath_.size()); - - for (Map::const_iterator it = pathToResource_.begin(); it != pathToResource_.end(); ++it) - { - assert(resourceToPath_[it->second] == it->first); - } - - for (Map::const_iterator it = resourceToPath_.begin(); it != resourceToPath_.end(); ++it) - { - assert(pathToResource_[it->second] == it->first); - } -#endif - } - - void AddTags(DicomMap& target, - const std::string& resourceId, - ResourceType tagsFromLevel) - { - DicomMap tags; - if (context_.GetIndex().GetMainDicomTags(tags, resourceId, level_, tagsFromLevel)) - { - target.Merge(tags); - } - } - - void Register(const std::string& resourceId) - { - // Don't register twice the same resource - if (resourceToPath_.find(resourceId) == resourceToPath_.end()) - { - std::string name = template_; - - DicomMap tags; - - AddTags(tags, resourceId, level_); - - if (level_ == ResourceType_Study) - { - AddTags(tags, resourceId, ResourceType_Patient); - } - - DicomArray arr(tags); - for (size_t i = 0; i < arr.GetSize(); i++) - { - const DicomElement& element = arr.GetElement(i); - if (!element.GetValue().IsNull() && - !element.GetValue().IsBinary()) - { - const std::string tag = FromDcmtkBridge::GetTagName(element.GetTag(), ""); - boost::replace_all(name, "{{" + tag + "}}", element.GetValue().GetContent()); - } - } - - // Blank the tags that were not matched - static const boost::regex REGEX_BLANK_TAGS("{{.*?}}"); // non-greedy match - name = boost::regex_replace(name, REGEX_BLANK_TAGS, ""); - - // UTF-8 characters cannot be used on Windows XP - name = Toolbox::ConvertToAscii(name); - boost::replace_all(name, "/", ""); - boost::replace_all(name, "\\", ""); - - // Trim sequences of spaces as one single space - static const boost::regex REGEX_TRIM_SPACES("{{.*?}}"); - name = boost::regex_replace(name, REGEX_TRIM_SPACES, " "); - name = Toolbox::StripSpaces(name); - - size_t count = 0; - for (;;) - { - std::string path = name; - if (count > 0) - { - path += " (" + boost::lexical_cast<std::string>(count) + ")"; - } - - if (pathToResource_.find(path) == pathToResource_.end()) - { - pathToResource_[path] = resourceId; - resourceToPath_[resourceId] = path; - return; - } - - count++; - } - - throw OrthancException(ErrorCode_InternalError); - } - } - - public: - ResourcesIndex(ServerContext& context, - ResourceType level, - const std::string& templateString) : - context_(context), - level_(level), - template_(templateString) - { - } - - ResourceType GetLevel() const - { - return level_; - } - - void Refresh(std::set<std::string>& removedPaths /* out */, - const std::set<std::string>& resources) - { - CheckInvariants(); - - // Detect the resources that have been removed since last refresh - removedPaths.clear(); - std::set<std::string> removedResources; - - for (Map::iterator it = resourceToPath_.begin(); it != resourceToPath_.end(); ++it) - { - if (resources.find(it->first) == resources.end()) - { - const std::string& path = it->second; - - assert(pathToResource_.find(path) != pathToResource_.end()); - pathToResource_.erase(path); - removedPaths.insert(path); - - removedResources.insert(it->first); // Delay the removal to avoid disturbing the iterator - } - } - - // Remove the missing resources - for (std::set<std::string>::const_iterator it = removedResources.begin(); it != removedResources.end(); ++it) - { - assert(resourceToPath_.find(*it) != resourceToPath_.end()); - resourceToPath_.erase(*it); - } - - CheckInvariants(); - - for (std::set<std::string>::const_iterator it = resources.begin(); it != resources.end(); ++it) - { - Register(*it); - } - - CheckInvariants(); - } - - const Map& GetPathToResource() const - { - return pathToResource_; - } - }; - - - class INode : public boost::noncopyable - { - public: - virtual ~INode() - { - } - - virtual bool ListCollection(IWebDavBucket::Collection& target, - const UriComponents& path) = 0; - - virtual bool GetFileContent(MimeType& mime, - std::string& content, - boost::posix_time::ptime& time, - const UriComponents& path) = 0; - }; - - - - class InstancesOfSeries : public INode - { - private: - ServerContext& context_; - std::string parentSeries_; - - public: - InstancesOfSeries(ServerContext& context, - const std::string& parentSeries) : - context_(context), - parentSeries_(parentSeries) - { - } - - virtual bool ListCollection(IWebDavBucket::Collection& target, - const UriComponents& path) ORTHANC_OVERRIDE - { - if (path.empty()) - { - std::list<std::string> resources; - try - { - context_.GetIndex().GetChildren(resources, parentSeries_); - } - catch (OrthancException&) - { - // Unknown (or deleted) parent series - return false; - } - - for (std::list<std::string>::const_iterator - it = resources.begin(); it != resources.end(); ++it) - { - boost::posix_time::ptime time; - LookupTime(time, context_, *it, MetadataType_Instance_ReceptionDate); - - FileInfo info; - if (context_.GetIndex().LookupAttachment(info, *it, FileContentType_Dicom)) - { - std::unique_ptr<File> resource(new File(*it + ".dcm")); - resource->SetMimeType(MimeType_Dicom); - resource->SetContentLength(info.GetUncompressedSize()); - resource->SetCreationTime(time); - target.AddResource(resource.release()); - } - } - - return true; - } - else - { - return false; - } - } - - virtual bool GetFileContent(MimeType& mime, - std::string& content, - boost::posix_time::ptime& time, - const UriComponents& path) ORTHANC_OVERRIDE - { - if (path.size() == 1 && - boost::ends_with(path[0], ".dcm")) - { - std::string instanceId = path[0].substr(0, path[0].size() - 4); - - try - { - mime = MimeType_Dicom; - context_.ReadDicom(content, instanceId); - LookupTime(time, context_, instanceId, MetadataType_Instance_ReceptionDate); - return true; - } - catch (OrthancException&) - { - // File was removed - return false; - } - } - else - { - return false; - } - } - }; - - - - /** - * The "InternalNode" class corresponds to a non-leaf node in the - * WebDAV tree, that only contains subfolders (no file). - * - * TODO: Implement a LRU index to dynamically remove the oldest - * children on high RAM usage. - **/ - class InternalNode : public INode - { - private: - typedef std::map<std::string, INode*> Children; - - Children children_; - - INode* GetChild(const std::string& path) // Don't delete the result pointer! - { - Children::const_iterator child = children_.find(path); - if (child == children_.end()) - { - INode* child = CreateChild(path); - - if (child == NULL) - { - return NULL; - } - else - { - children_[path] = child; - return child; - } - } - else - { - assert(child->second != NULL); - return child->second; - } - } - - protected: - void RemoveSubfolder(const std::string& path) - { - Children::iterator child = children_.find(path); - if (child != children_.end()) - { - assert(child->second != NULL); - delete child->second; - children_.erase(child); - } - } - - virtual void Refresh() = 0; - - virtual bool ListSubfolders(IWebDavBucket::Collection& target) = 0; - - virtual INode* CreateChild(const std::string& path) = 0; - - public: - virtual ~InternalNode() - { - for (Children::iterator it = children_.begin(); it != children_.end(); ++it) - { - assert(it->second != NULL); - delete it->second; - } - } - - virtual bool ListCollection(IWebDavBucket::Collection& target, - const UriComponents& path) - ORTHANC_OVERRIDE ORTHANC_FINAL - { - Refresh(); - - if (path.empty()) - { - return ListSubfolders(target); - } - else - { - // Recursivity - INode* child = GetChild(path[0]); - if (child == NULL) - { - return false; - } - else - { - UriComponents subpath(path.begin() + 1, path.end()); - return child->ListCollection(target, subpath); - } - } - } - - virtual bool GetFileContent(MimeType& mime, - std::string& content, - boost::posix_time::ptime& time, - const UriComponents& path) - ORTHANC_OVERRIDE ORTHANC_FINAL - { - if (path.empty()) - { - return false; // An internal node doesn't correspond to a file - } - else - { - // Recursivity - Refresh(); - - INode* child = GetChild(path[0]); - if (child == NULL) - { - return false; - } - else - { - UriComponents subpath(path.begin() + 1, path.end()); - return child->GetFileContent(mime, content, time, subpath); - } - } - } - }; - - - class ListOfResources : public InternalNode - { - private: - ServerContext& context_; - const Templates& templates_; - std::unique_ptr<ResourcesIndex> index_; - MetadataType timeMetadata_; - - protected: - virtual void Refresh() ORTHANC_OVERRIDE ORTHANC_FINAL - { - std::list<std::string> resources; - GetCurrentResources(resources); - - std::set<std::string> removedPaths; - index_->Refresh(removedPaths, std::set<std::string>(resources.begin(), resources.end())); - - // Remove the children whose associated resource doesn't exist anymore - for (std::set<std::string>::const_iterator - it = removedPaths.begin(); it != removedPaths.end(); ++it) - { - RemoveSubfolder(*it); - } - } - - virtual bool ListSubfolders(IWebDavBucket::Collection& target) ORTHANC_OVERRIDE ORTHANC_FINAL - { - if (index_->GetLevel() == ResourceType_Instance) - { - // Not a collection, no subfolders - return false; - } - else - { - const ResourcesIndex::Map& paths = index_->GetPathToResource(); - - for (ResourcesIndex::Map::const_iterator it = paths.begin(); it != paths.end(); ++it) - { - boost::posix_time::ptime time; - LookupTime(time, context_, it->second, timeMetadata_); - - std::unique_ptr<IWebDavBucket::Resource> resource(new IWebDavBucket::Folder(it->first)); - resource->SetCreationTime(time); - target.AddResource(resource.release()); - } - - return true; - } - } - - virtual INode* CreateChild(const std::string& path) ORTHANC_OVERRIDE ORTHANC_FINAL - { - ResourcesIndex::Map::const_iterator resource = index_->GetPathToResource().find(path); - if (resource == index_->GetPathToResource().end()) - { - return NULL; - } - else - { - return CreateResourceNode(resource->second); - } - } - - ServerContext& GetContext() const - { - return context_; - } - - virtual void GetCurrentResources(std::list<std::string>& resources) = 0; - - virtual INode* CreateResourceNode(const std::string& resource) = 0; - - public: - ListOfResources(ServerContext& context, - ResourceType level, - const Templates& templates) : - context_(context), - templates_(templates) - { - Templates::const_iterator t = templates.find(level); - if (t == templates.end()) - { - throw OrthancException(ErrorCode_ParameterOutOfRange); - } - - index_.reset(new ResourcesIndex(context, level, t->second)); - - if (level == ResourceType_Instance) - { - timeMetadata_ = MetadataType_Instance_ReceptionDate; - } - else - { - timeMetadata_ = MetadataType_LastUpdate; - } - } - - ResourceType GetLevel() const - { - return index_->GetLevel(); - } - - const Templates& GetTemplates() const - { - return templates_; - } - }; - - - - class SingleDicomResource : public ListOfResources - { - private: - std::string parentId_; - - protected: - virtual void GetCurrentResources(std::list<std::string>& resources) ORTHANC_OVERRIDE - { - try - { - GetContext().GetIndex().GetChildren(resources, parentId_); - } - catch (OrthancException&) - { - // Unknown parent resource - resources.clear(); - } - } - - virtual INode* CreateResourceNode(const std::string& resource) ORTHANC_OVERRIDE - { - if (GetLevel() == ResourceType_Instance) - { - return NULL; - } - else if (GetLevel() == ResourceType_Series) - { - return new InstancesOfSeries(GetContext(), resource); - } - else - { - ResourceType l = GetChildResourceType(GetLevel()); - return new SingleDicomResource(GetContext(), l, resource, GetTemplates()); - } - } - - public: - SingleDicomResource(ServerContext& context, - ResourceType level, - const std::string& parentId, - const Templates& templates) : - ListOfResources(context, level, templates), - parentId_(parentId) - { - } - }; - - - class RootNode : public ListOfResources - { - protected: - virtual void GetCurrentResources(std::list<std::string>& resources) ORTHANC_OVERRIDE - { - GetContext().GetIndex().GetAllUuids(resources, GetLevel()); - } - - virtual INode* CreateResourceNode(const std::string& resource) ORTHANC_OVERRIDE - { - if (GetLevel() == ResourceType_Series) - { - return new InstancesOfSeries(GetContext(), resource); - } - else - { - ResourceType l = GetChildResourceType(GetLevel()); - return new SingleDicomResource(GetContext(), l, resource, GetTemplates()); - } - } - - public: - RootNode(ServerContext& context, - ResourceType level, - const Templates& templates) : - ListOfResources(context, level, templates) - { - } - }; - - - class ListOfStudiesByDate : public ListOfResources - { - private: - std::string year_; - std::string month_; - - class Visitor : public ServerContext::ILookupVisitor - { - private: - std::list<std::string>& resources_; - - public: - Visitor(std::list<std::string>& resources) : - resources_(resources) - { - } - - 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 - { - resources_.push_back(publicId); - } - }; - - protected: - virtual void GetCurrentResources(std::list<std::string>& resources) ORTHANC_OVERRIDE - { - DatabaseLookup query; - query.AddRestConstraint(DICOM_TAG_STUDY_DATE, year_ + month_ + "01-" + year_ + month_ + "31", - true /* case sensitive */, true /* mandatory tag */); - - Visitor visitor(resources); - GetContext().Apply(visitor, query, ResourceType_Study, 0 /* since */, 0 /* no limit */); - } - - virtual INode* CreateResourceNode(const std::string& resource) ORTHANC_OVERRIDE - { - return new SingleDicomResource(GetContext(), ResourceType_Series, resource, GetTemplates()); - } - - public: - ListOfStudiesByDate(ServerContext& context, - const std::string& year, - const std::string& month, - const Templates& templates) : - ListOfResources(context, ResourceType_Study, templates), - year_(year), - month_(month) - { - if (year.size() != 4 || - month.size() != 2) - { - throw OrthancException(ErrorCode_ParameterOutOfRange); - } - } - }; - - - class ListOfStudiesByMonth : public InternalNode - { - private: - ServerContext& context_; - std::string year_; - const Templates& templates_; - - class Visitor : public ServerContext::ILookupVisitor - { - private: - std::set<std::string> months_; - - public: - Visitor() - { - } - - const std::set<std::string>& GetMonths() const - { - return months_; - } - - 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 - { - std::string s; - if (mainDicomTags.LookupStringValue(s, DICOM_TAG_STUDY_DATE, false) && - s.size() == 8) - { - months_.insert(s.substr(4, 2)); // Get the month from "YYYYMMDD" - } - } - }; - - protected: - virtual void Refresh() ORTHANC_OVERRIDE - { - } - - virtual bool ListSubfolders(IWebDavBucket::Collection& target) ORTHANC_OVERRIDE - { - DatabaseLookup query; - query.AddRestConstraint(DICOM_TAG_STUDY_DATE, year_ + "0101-" + year_ + "1231", - true /* case sensitive */, true /* mandatory tag */); - - Visitor visitor; - context_.Apply(visitor, query, ResourceType_Study, 0 /* since */, 0 /* no limit */); - - for (std::set<std::string>::const_iterator it = visitor.GetMonths().begin(); - it != visitor.GetMonths().end(); ++it) - { - target.AddResource(new IWebDavBucket::Folder(year_ + "-" + *it)); - } - - return true; - } - - virtual INode* CreateChild(const std::string& path) ORTHANC_OVERRIDE - { - if (path.size() != 7) // Format: "YYYY-MM" - { - throw OrthancException(ErrorCode_InternalError); - } - else - { - const std::string year = path.substr(0, 4); - const std::string month = path.substr(5, 2); - return new ListOfStudiesByDate(context_, year, month, templates_); - } - } - - public: - ListOfStudiesByMonth(ServerContext& context, - const std::string& year, - const Templates& templates) : - context_(context), - year_(year), - templates_(templates) - { - if (year_.size() != 4) - { - throw OrthancException(ErrorCode_ParameterOutOfRange); - } - } - }; - - - class ListOfStudiesByYear : public InternalNode - { - private: - ServerContext& context_; - const Templates& templates_; - - protected: - virtual void Refresh() ORTHANC_OVERRIDE - { - } - - virtual bool ListSubfolders(IWebDavBucket::Collection& target) ORTHANC_OVERRIDE - { - std::list<std::string> resources; - context_.GetIndex().GetAllUuids(resources, ResourceType_Study); - - std::set<std::string> years; - - for (std::list<std::string>::const_iterator it = resources.begin(); it != resources.end(); ++it) - { - DicomMap tags; - std::string studyDate; - if (context_.GetIndex().GetMainDicomTags(tags, *it, ResourceType_Study, ResourceType_Study) && - tags.LookupStringValue(studyDate, DICOM_TAG_STUDY_DATE, false) && - studyDate.size() == 8) - { - years.insert(studyDate.substr(0, 4)); // Get the year from "YYYYMMDD" - } - } - - for (std::set<std::string>::const_iterator it = years.begin(); it != years.end(); ++it) - { - target.AddResource(new IWebDavBucket::Folder(*it)); - } - - return true; - } - - virtual INode* CreateChild(const std::string& path) ORTHANC_OVERRIDE - { - return new ListOfStudiesByMonth(context_, path, templates_); - } - - public: - ListOfStudiesByYear(ServerContext& context, - const Templates& templates) : - context_(context), - templates_(templates) - { - } - }; - - - static void UploadWorker(DummyBucket2* that) - { - assert(that != NULL); - - boost::posix_time::ptime lastModification = GetNow(); - - while (that->running_) - { - std::unique_ptr<IDynamicObject> obj(that->uploadQueue_.Dequeue(100)); - if (obj.get() != NULL) - { - that->Upload(reinterpret_cast<const SingleValueObject<std::string>&>(*obj).GetValue()); - lastModification = GetNow(); - } - else if (GetNow() - lastModification > boost::posix_time::seconds(10)) - { - // After every 10 seconds of inactivity, remove the empty folders - LOG(INFO) << "Cleaning up the empty WebDAV upload folders"; - that->uploads_.RemoveEmptyFolders(); - lastModification = GetNow(); - } - } - } - - void Upload(const std::string& path) - { - UriComponents uri; - Toolbox::SplitUriComponents(uri, path); - - LOG(INFO) << "Upload from WebDAV: " << path; - - MimeType mime; - std::string content; - boost::posix_time::ptime time; - if (uploads_.GetFileContent(mime, content, time, uri)) - { - DicomInstanceToStore instance; - // instance.SetOrigin(DicomInstanceOrigin_WebDav); - instance.SetBuffer(content.c_str(), content.size()); - - std::string publicId; - StoreStatus status = context_.Store(publicId, instance, StoreInstanceMode_Default); - if (status == StoreStatus_Success || - status == StoreStatus_AlreadyStored) - { - LOG(INFO) << "Successfully imported DICOM instance from WebDAV: " << path << " (Orthanc ID: " << publicId << ")"; - uploads_.DeleteItem(uri); - } - else - { - LOG(WARNING) << "Cannot import DICOM instance from WebWAV: " << path; - } - } - } - - - ServerContext& context_; - std::unique_ptr<INode> patients_; - std::unique_ptr<INode> studies_; - std::unique_ptr<INode> dates_; - Templates patientsTemplates_; - Templates studiesTemplates_; - WebDavStorage uploads_; - SharedMessageQueue uploadQueue_; - boost::thread uploadThread_; - bool running_; - -public: - DummyBucket2(ServerContext& context) : - context_(context), - uploads_(false /* store uploads as temporary files */), - running_(false) - { - patientsTemplates_[ResourceType_Patient] = "{{PatientID}} - {{PatientName}}"; - patientsTemplates_[ResourceType_Study] = "{{StudyDate}} - {{StudyDescription}}"; - patientsTemplates_[ResourceType_Series] = "{{Modality}} - {{SeriesDescription}}"; - - studiesTemplates_[ResourceType_Study] = "{{PatientID}} - {{PatientName}} - {{StudyDescription}}"; - studiesTemplates_[ResourceType_Series] = patientsTemplates_[ResourceType_Series]; - - patients_.reset(new RootNode(context, ResourceType_Patient, patientsTemplates_)); - studies_.reset(new RootNode(context, ResourceType_Study, studiesTemplates_)); - dates_.reset(new ListOfStudiesByYear(context, studiesTemplates_)); - } - - virtual ~DummyBucket2() - { - Stop(); - } - - virtual bool IsExistingFolder(const UriComponents& path) ORTHANC_OVERRIDE - { - if (path.empty()) - { - return true; - } - else if (path[0] == BY_UIDS) - { - return (path.size() <= 3 && - (path.size() != 3 || path[2] != "study.json")); - } - else if (path[0] == BY_PATIENTS) - { - IWebDavBucket::Collection tmp; - return patients_->ListCollection(tmp, UriComponents(path.begin() + 1, path.end())); - } - else if (path[0] == BY_STUDIES) - { - IWebDavBucket::Collection tmp; - return studies_->ListCollection(tmp, UriComponents(path.begin() + 1, path.end())); - } - else if (path[0] == BY_DATE) - { - IWebDavBucket::Collection tmp; - return dates_->ListCollection(tmp, UriComponents(path.begin() + 1, path.end())); - } - else if (path[0] == UPLOADS) - { - return uploads_.IsExistingFolder(UriComponents(path.begin() + 1, path.end())); - } - else - { - return false; - } - } - - virtual bool ListCollection(Collection& collection, - const UriComponents& path) ORTHANC_OVERRIDE - { - if (path.empty()) - { - collection.AddResource(new Folder(BY_DATE)); - collection.AddResource(new Folder(BY_PATIENTS)); - collection.AddResource(new Folder(BY_STUDIES)); - collection.AddResource(new Folder(BY_UIDS)); - collection.AddResource(new Folder(UPLOADS)); - return true; - } - else if (path[0] == 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 */); - query.AddRestConstraint(DICOM_TAG_SERIES_INSTANCE_UID, path[2], - true /* case sensitive */, true /* mandatory tag */); - } - else - { - return false; - } - - DicomIdentifiersVisitor visitor(context_, collection, level); - context_.Apply(visitor, query, level, 0 /* since */, limit); - - return true; - } - else if (path[0] == BY_PATIENTS) - { - return patients_->ListCollection(collection, UriComponents(path.begin() + 1, path.end())); - } - else if (path[0] == BY_STUDIES) - { - return studies_->ListCollection(collection, UriComponents(path.begin() + 1, path.end())); - } - else if (path[0] == BY_DATE) - { - return dates_->ListCollection(collection, UriComponents(path.begin() + 1, path.end())); - } - else if (path[0] == UPLOADS) - { - return uploads_.ListCollection(collection, UriComponents(path.begin() + 1, path.end())); - } - else - { - return false; - } - } - - virtual bool GetFileContent(MimeType& mime, - std::string& content, - boost::posix_time::ptime& modificationTime, - const UriComponents& path) ORTHANC_OVERRIDE - { - if (path.empty()) - { - return false; - } - else if (path[0] == BY_UIDS) - { - 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 */); - - OrthancJsonVisitor visitor(context_, content, ResourceType_Study); - context_.Apply(visitor, query, ResourceType_Study, 0 /* since */, 0 /* no limit */); - - 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 */); - - OrthancJsonVisitor visitor(context_, content, ResourceType_Series); - context_.Apply(visitor, query, ResourceType_Series, 0 /* since */, 0 /* no limit */); - - 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; - } - } - else if (path[0] == BY_PATIENTS) - { - return patients_->GetFileContent(mime, content, modificationTime, UriComponents(path.begin() + 1, path.end())); - } - else if (path[0] == BY_STUDIES) - { - return studies_->GetFileContent(mime, content, modificationTime, UriComponents(path.begin() + 1, path.end())); - } - else if (path[0] == UPLOADS) - { - return uploads_.GetFileContent(mime, content, modificationTime, UriComponents(path.begin() + 1, path.end())); - } - else - { - return false; - } - } - - - virtual bool StoreFile(const std::string& content, - const UriComponents& path) ORTHANC_OVERRIDE - { - if (path.size() >= 1 && - path[0] == UPLOADS) - { - UriComponents subpath(UriComponents(path.begin() + 1, path.end())); - - if (uploads_.StoreFile(content, subpath)) - { - if (!content.empty()) - { - uploadQueue_.Enqueue(new SingleValueObject<std::string>(Toolbox::FlattenUri(subpath))); - } - return true; - } - else - { - return false; - } - } - else - { - return false; - } - } - - - virtual bool CreateFolder(const UriComponents& path) - { - if (path.size() >= 1 && - path[0] == UPLOADS) - { - return uploads_.CreateFolder(UriComponents(path.begin() + 1, path.end())); - } - else - { - return false; - } - } - - virtual bool DeleteItem(const std::vector<std::string>& path) ORTHANC_OVERRIDE - { - if (path.size() >= 1 && - path[0] == UPLOADS) - { - return uploads_.DeleteItem(UriComponents(path.begin() + 1, path.end())); - } - else - { - return false; // read-only - } - } - - virtual void Start() ORTHANC_OVERRIDE - { - if (running_) - { - throw OrthancException(ErrorCode_BadSequenceOfCalls); - } - else - { - LOG(INFO) << "Starting the WebDAV upload thread"; - running_ = true; - uploadThread_ = boost::thread(UploadWorker, this); - } - } - - virtual void Stop() ORTHANC_OVERRIDE - { - if (running_) - { - LOG(INFO) << "Stopping the WebDAV upload thread"; - running_ = false; - if (uploadThread_.joinable()) - { - uploadThread_.join(); - } - } - } -}; - - - static void PrintHelp(const char* path) { std::cout @@ -2653,9 +1053,7 @@ UriComponents root; // TODO root.push_back("a"); root.push_back("b"); - //httpServer.Register(root, new WebDavStorage(true)); - //httpServer.Register(root, new DummyBucket(context, true)); - httpServer.Register(root, new DummyBucket2(context)); + httpServer.Register(root, new OrthancWebDav(context)); } if (httpServer.GetPortNumber() < 1024)