Mercurial > hg > orthanc
diff OrthancServer/Sources/OrthancWebDav.cpp @ 4240:799c0c527ced
reorganization
author | Sebastien Jodogne <s.jodogne@gmail.com> |
---|---|
date | Fri, 09 Oct 2020 12:02:40 +0200 |
parents | |
children | 3510da0e260c |
line wrap: on
line diff
--- /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(); + } + } + } +}