Orthanc/OrthancServer/Sources/OrthancWebDav.cpp
2025-06-23 19:07:37 +05:30

1711 lines
47 KiB
C++

/**
* Orthanc - A Lightweight, RESTful DICOM Store
* Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
* Department, University Hospital of Liege, Belgium
* Copyright (C) 2017-2023 Osimis S.A., Belgium
* Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
* Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, 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.
*
* 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/Compression/ZipReader.h"
#include "../../OrthancFramework/Sources/DicomFormat/DicomArray.h"
#include "../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h"
#include "../../OrthancFramework/Sources/HttpServer/WebDavStorage.h"
#include "../../OrthancFramework/Sources/Logging.h"
#include "ResourceFinder.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_DATES = "by-dates";
static const char* const BY_UIDS = "by-uids";
static const char* const UPLOADS = "uploads";
static const char* const STUDY_INFO = "study.json";
static const char* const SERIES_INFO = "series.json";
namespace Orthanc
{
static boost::posix_time::ptime GetNow()
{
return boost::posix_time::second_clock::universal_time();
}
static void ParseTime(boost::posix_time::ptime& target,
const std::string& value)
{
try
{
target = boost::posix_time::from_iso_string(value);
}
catch (std::exception& e)
{
target = GetNow();
}
}
static void LookupTime(boost::posix_time::ptime& target,
ServerContext& context,
const std::string& publicId,
ResourceType level,
MetadataType metadata)
{
std::string value;
if (context.GetIndex().LookupMetadata(value, publicId, level, metadata))
{
ParseTime(target, value);
}
else
{
target = GetNow();
}
}
class OrthancWebDav::DicomIdentifiersVisitorV2 : public ResourceFinder::IVisitor
{
private:
bool isComplete_;
Collection& target_;
public:
explicit DicomIdentifiersVisitorV2(Collection& target) :
isComplete_(false),
target_(target)
{
}
virtual void MarkAsComplete() ORTHANC_OVERRIDE
{
isComplete_ = true; // TODO
}
virtual void Apply(const FindResponse::Resource& resource,
const DicomMap& requestedTags) ORTHANC_OVERRIDE
{
DicomMap resourceTags;
resource.GetMainDicomTags(resourceTags, resource.GetLevel());
std::string uid;
bool hasUid;
std::string time;
bool hasTime;
switch (resource.GetLevel())
{
case ResourceType_Study:
hasUid = resourceTags.LookupStringValue(uid, DICOM_TAG_STUDY_INSTANCE_UID, false);
hasTime = resource.LookupMetadata(time, resource.GetLevel(), MetadataType_LastUpdate);
break;
case ResourceType_Series:
hasUid = resourceTags.LookupStringValue(uid, DICOM_TAG_SERIES_INSTANCE_UID, false);
hasTime = resource.LookupMetadata(time, resource.GetLevel(), MetadataType_LastUpdate);
break;
case ResourceType_Instance:
hasUid = resourceTags.LookupStringValue(uid, DICOM_TAG_SOP_INSTANCE_UID, false);
hasTime = resource.LookupMetadata(time, resource.GetLevel(), MetadataType_Instance_ReceptionDate);
break;
default:
throw OrthancException(ErrorCode_InternalError);
}
if (hasUid &&
!uid.empty())
{
std::unique_ptr<Resource> item;
if (resource.GetLevel() == ResourceType_Instance)
{
FileInfo info;
int64_t revision;
if (resource.LookupAttachment(info, revision, FileContentType_Dicom))
{
std::unique_ptr<File> f(new File(uid + ".dcm"));
f->SetMimeType(MimeType_Dicom);
f->SetContentLength(info.GetUncompressedSize());
item.reset(f.release());
}
}
else
{
item.reset(new Folder(uid));
}
if (item.get() != NULL)
{
if (hasTime)
{
boost::posix_time::ptime t;
ParseTime(t, time);
item->SetCreationTime(t);
}
else
{
item->SetCreationTime(GetNow());
}
target_.AddResource(item.release());
}
}
}
};
class OrthancWebDav::DicomFileVisitorV2 : public ResourceFinder::IVisitor
{
private:
ServerContext& context_;
bool success_;
std::string& target_;
boost::posix_time::ptime& time_;
public:
DicomFileVisitorV2(ServerContext& context,
std::string& target,
boost::posix_time::ptime& time) :
context_(context),
success_(false),
target_(target),
time_(time)
{
}
bool IsSuccess() const
{
return success_;
}
virtual void MarkAsComplete() ORTHANC_OVERRIDE
{
}
virtual void Apply(const FindResponse::Resource& resource,
const DicomMap& requestedTags) ORTHANC_OVERRIDE
{
if (success_)
{
success_ = false; // Two matches => Error
}
else
{
std::string s;
if (resource.LookupMetadata(s, ResourceType_Instance, MetadataType_Instance_ReceptionDate))
{
ParseTime(time_, s);
}
else
{
time_ = GetNow();
}
context_.ReadDicom(target_, resource.GetIdentifier());
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_;
static bool LookupInstanceId(std::string& instanceId,
const UriComponents& path)
{
if (path.size() == 1 &&
boost::ends_with(path[0], ".dcm"))
{
instanceId = path[0].substr(0, path[0].size() - 4);
return true;
}
else
{
return false;
}
}
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, ResourceType_Series, 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, ResourceType_Instance, MetadataType_Instance_ReceptionDate);
FileInfo info;
int64_t revision; // Ignored
if (context_.GetIndex().LookupAttachment(info, revision, ResourceType_Instance, *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
{
std::string instanceId;
if (LookupInstanceId(instanceId, path))
{
try
{
mime = MimeType_Dicom;
context_.ReadDicom(content, instanceId);
LookupTime(time, context_, instanceId, ResourceType_Instance, MetadataType_Instance_ReceptionDate);
return true;
}
catch (OrthancException&)
{
// File was removed
return false;
}
}
else
{
return false;
}
}
virtual bool DeleteItem(const UriComponents& path) ORTHANC_OVERRIDE
{
if (path.empty())
{
// Delete all
std::list<std::string> resources;
try
{
context_.GetIndex().GetChildren(resources, ResourceType_Series, parentSeries_);
}
catch (OrthancException&)
{
// Unknown (or deleted) parent series
return true;
}
for (std::list<std::string>::const_iterator it = resources.begin();
it != resources.end(); ++it)
{
Json::Value info;
context_.DeleteResource(info, *it, ResourceType_Instance);
}
return true;
}
else
{
std::string instanceId;
if (LookupInstanceId(instanceId, path))
{
Json::Value info;
return context_.DeleteResource(info, instanceId, ResourceType_Instance);
}
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* node = CreateSubfolder(path);
if (node == NULL)
{
return NULL;
}
else
{
children_[path] = node;
return node;
}
}
else
{
assert(child->second != NULL);
return child->second;
}
}
protected:
void InvalidateSubfolder(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* CreateSubfolder(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)
{
// Must be "true" to allow DELETE on folders that are
// automatically removed through recursive deletion
return true;
}
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);
}
}
}
virtual bool DeleteItem(const UriComponents& path) ORTHANC_OVERRIDE ORTHANC_FINAL
{
Refresh();
if (path.empty())
{
IWebDavBucket::Collection collection;
if (ListSubfolders(collection))
{
std::set<std::string> content;
collection.ListDisplayNames(content);
for (std::set<std::string>::const_iterator
it = content.begin(); it != content.end(); ++it)
{
INode* node = GetChild(*it);
if (node)
{
node->DeleteItem(path);
}
}
return true;
}
else
{
return false;
}
}
else
{
INode* child = GetChild(path[0]);
if (child == NULL)
{
return true;
}
else
{
// Recursivity
UriComponents subpath(path.begin() + 1, path.end());
return child->DeleteItem(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)
{
InvalidateSubfolder(*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, index_->GetLevel(), timeMetadata_);
std::unique_ptr<IWebDavBucket::Resource> resource(new IWebDavBucket::Folder(it->first));
resource->SetCreationTime(time);
target.AddResource(resource.release());
}
return true;
}
}
virtual INode* CreateSubfolder(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:
ResourceType parentLevel_;
std::string parentId_;
protected:
virtual void GetCurrentResources(std::list<std::string>& resources) ORTHANC_OVERRIDE
{
try
{
GetContext().GetIndex().GetChildren(resources, parentLevel_, 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),
parentLevel_(GetParentResourceType(level)),
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 ResourceFinder::IVisitor
{
private:
std::list<std::string>& resources_;
public:
explicit Visitor(std::list<std::string>& resources) :
resources_(resources)
{
}
virtual void MarkAsComplete() ORTHANC_OVERRIDE
{
}
virtual void Apply(const FindResponse::Resource& resource,
const DicomMap& requestedTags) ORTHANC_OVERRIDE
{
resources_.push_back(resource.GetIdentifier());
}
};
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);
ResourceFinder finder(ResourceType_Study, ResponseContentFlags_ID, GetContext().GetFindStorageAccessMode(), GetContext().GetIndex().HasFindSupport());
finder.SetDatabaseLookup(query);
finder.Execute(visitor, GetContext());
}
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 ResourceFinder::IVisitor
{
private:
std::set<std::string> months_;
public:
const std::set<std::string>& GetMonths() const
{
return months_;
}
virtual void MarkAsComplete() ORTHANC_OVERRIDE
{
}
virtual void Apply(const FindResponse::Resource& resource,
const DicomMap& requestedTags) ORTHANC_OVERRIDE
{
DicomMap mainDicomTags;
resource.GetMainDicomTags(mainDicomTags, ResourceType_Study);
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;
ResourceFinder finder(ResourceType_Study, ResponseContentFlags_ID, context_.GetFindStorageAccessMode(), context_.GetIndex().HasFindSupport());
finder.SetDatabaseLookup(query);
finder.Execute(visitor, context_);
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* CreateSubfolder(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* CreateSubfolder(const std::string& path) ORTHANC_OVERRIDE
{
return new ListOfStudiesByMonth(context_, path, templates_);
}
public:
ListOfStudiesByYear(ServerContext& context,
const Templates& templates) :
context_(context),
templates_(templates)
{
}
};
class OrthancWebDav::DicomDeleteVisitor : public ResourceFinder::IVisitor
{
private:
ServerContext& context_;
ResourceType level_;
public:
DicomDeleteVisitor(ServerContext& context,
ResourceType level) :
context_(context),
level_(level)
{
}
virtual void MarkAsComplete() ORTHANC_OVERRIDE
{
}
virtual void Apply(const FindResponse::Resource& resource,
const DicomMap& requestedTags) ORTHANC_OVERRIDE
{
Json::Value info;
context_.DeleteResource(info, resource.GetIdentifier(), level_);
}
};
void OrthancWebDav::AddVirtualFile(Collection& collection,
const UriComponents& path,
const std::string& filename)
{
MimeType mime;
std::string content;
boost::posix_time::ptime modification; // Unused, let the date be set to "GetNow()"
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());
collection.AddResource(f.release());
}
}
void OrthancWebDav::UploadWorker(OrthancWebDav* that)
{
Logging::SetCurrentThreadName("WEBDAV-UPLOAD");
assert(that != NULL);
boost::posix_time::ptime lastModification = GetNow();
while (that->uploadRunning_)
{
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(30))
{
/**
* After every 30 seconds of inactivity, remove the empty
* folders. This delay is needed to avoid removing
* just-created folders before the remote WebDAV has time to
* write files into it.
**/
LOG(TRACE) << "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))
{
bool success = false;
if (ZipReader::IsZipMemoryBuffer(content))
{
// New in Orthanc 1.8.2
std::unique_ptr<ZipReader> reader(ZipReader::CreateFromMemory(content));
std::string filename, uncompressedFile;
while (reader->ReadNextFile(filename, uncompressedFile))
{
if (!uncompressedFile.empty())
{
LOG(INFO) << "Uploading DICOM file extracted from a ZIP archive in WebDAV: " << filename;
std::unique_ptr<DicomInstanceToStore> instance(DicomInstanceToStore::CreateFromBuffer(uncompressedFile));
instance->SetOrigin(DicomInstanceOrigin::FromWebDav());
std::string publicId;
try
{
context_.Store(publicId, *instance, StoreInstanceMode_Default);
}
catch (OrthancException& e)
{
if (e.GetErrorCode() == ErrorCode_BadFileFormat)
{
LOG(ERROR) << "Cannot import non-DICOM file from ZIP archive: " << filename;
}
}
}
}
success = true;
}
else
{
std::unique_ptr<DicomInstanceToStore> instance(DicomInstanceToStore::CreateFromBuffer(content));
instance->SetOrigin(DicomInstanceOrigin::FromWebDav());
try
{
std::string publicId;
ServerContext::StoreResult result = context_.Store(publicId, *instance, StoreInstanceMode_Default);
if (result.GetStatus() == StoreStatus_Success ||
result.GetStatus() == StoreStatus_AlreadyStored)
{
LOG(INFO) << "Successfully imported DICOM instance from WebDAV: "
<< path << " (Orthanc ID: " << publicId << ")";
success = true;
}
}
catch (OrthancException& e)
{
}
}
uploads_.DeleteItem(uri);
if (!success)
{
LOG(WARNING) << "Cannot import DICOM instance from WebWAV (maybe not a DICOM file): " << path;
}
}
}
OrthancWebDav::INode& OrthancWebDav::GetRootNode(const std::string& rootPath)
{
if (rootPath == BY_PATIENTS)
{
return *patients_;
}
else if (rootPath == BY_STUDIES)
{
return *studies_;
}
else if (rootPath == BY_DATES)
{
return *dates_;
}
else
{
throw OrthancException(ErrorCode_InternalError);
}
}
OrthancWebDav::OrthancWebDav(ServerContext& context,
bool allowDicomDelete,
bool allowUpload) :
context_(context),
allowDicomDelete_(allowDicomDelete),
allowUpload_(allowUpload),
uploads_(false /* store uploads as temporary files */),
uploadRunning_(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_INFO));
}
else if (path[0] == BY_PATIENTS ||
path[0] == BY_STUDIES ||
path[0] == BY_DATES)
{
IWebDavBucket::Collection collection;
return GetRootNode(path[0]).ListCollection(collection, UriComponents(path.begin() + 1, path.end()));
}
else if (allowUpload_ &&
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_DATES));
collection.AddResource(new Folder(BY_PATIENTS));
collection.AddResource(new Folder(BY_STUDIES));
collection.AddResource(new Folder(BY_UIDS));
if (allowUpload_)
{
collection.AddResource(new Folder(UPLOADS));
}
return true;
}
else if (path[0] == BY_UIDS)
{
DatabaseLookup query;
ResourceType level;
if (path.size() == 1)
{
level = ResourceType_Study;
// TODO - Should we limit here?
}
else if (path.size() == 2)
{
AddVirtualFile(collection, path, STUDY_INFO);
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_INFO);
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;
}
ResourceFinder finder(level, ResponseContentFlags_ID, context_.GetFindStorageAccessMode(), context_.GetIndex().HasFindSupport());
finder.SetDatabaseLookup(query);
finder.SetRetrieveMetadata(true);
switch (level)
{
case ResourceType_Study:
finder.AddRequestedTag(DICOM_TAG_STUDY_INSTANCE_UID);
break;
case ResourceType_Series:
finder.AddRequestedTag(DICOM_TAG_SERIES_INSTANCE_UID);
break;
case ResourceType_Instance:
finder.AddRequestedTag(DICOM_TAG_SOP_INSTANCE_UID);
finder.SetRetrieveAttachments(true);
break;
default:
throw OrthancException(ErrorCode_InternalError);
}
DicomIdentifiersVisitorV2 visitor(collection);
finder.Execute(visitor, context_);
return true;
}
else if (path[0] == BY_PATIENTS ||
path[0] == BY_STUDIES ||
path[0] == BY_DATES)
{
return GetRootNode(path[0]).ListCollection(collection, UriComponents(path.begin() + 1, path.end()));
}
else if (allowUpload_ &&
path[0] == UPLOADS)
{
return uploads_.ListCollection(collection, UriComponents(path.begin() + 1, path.end()));
}
else
{
return false;
}
}
static bool GetOrthancJson(std::string& target,
ServerContext& context,
ResourceType level,
const DatabaseLookup& query)
{
ResourceFinder finder(level, ResponseContentFlags_ExpandTrue, context.GetFindStorageAccessMode(), context.GetIndex().HasFindSupport());
finder.SetDatabaseLookup(query);
Json::Value expanded;
finder.Execute(expanded, context, DicomToJsonFormat_Human, false /* don't add "Metadata" */);
if (expanded.size() != 1)
{
return false;
}
else
{
target = expanded[0].toStyledString();
// Replace UNIX newlines with DOS newlines
boost::replace_all(target, "\n", "\r\n");
return true;
}
}
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_INFO)
{
DatabaseLookup query;
query.AddRestConstraint(DICOM_TAG_STUDY_INSTANCE_UID, path[1],
true /* case sensitive */, true /* mandatory tag */);
mime = MimeType_Json;
return GetOrthancJson(content, context_, ResourceType_Study, query);
}
else if (path.size() == 4 &&
path[3] == SERIES_INFO)
{
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 */);
mime = MimeType_Json;
return GetOrthancJson(content, context_, ResourceType_Series, query);
}
else if (path.size() == 4 &&
boost::ends_with(path[3], ".dcm"))
{
const std::string sopInstanceUid = path[3].substr(0, path[3].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 */);
mime = MimeType_Dicom;
ResourceFinder finder(ResourceType_Instance, ResponseContentFlags_ID, context_.GetFindStorageAccessMode(), context_.GetIndex().HasFindSupport());
finder.SetDatabaseLookup(query);
finder.SetRetrieveMetadata(true);
finder.SetRetrieveAttachments(true);
DicomFileVisitorV2 visitor(context_, content, modificationTime);
finder.Execute(visitor, context_);
return visitor.IsSuccess();
}
else
{
return false;
}
}
else if (path[0] == BY_PATIENTS ||
path[0] == BY_STUDIES ||
path[0] == BY_DATES)
{
return GetRootNode(path[0]).GetFileContent(mime, content, modificationTime, UriComponents(path.begin() + 1, path.end()));
}
else if (allowUpload_ &&
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 (allowUpload_ &&
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 (allowUpload_ &&
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.empty())
{
return false;
}
else if (path[0] == BY_UIDS &&
path.size() >= 2 &&
path.size() <= 4)
{
if (allowDicomDelete_)
{
ResourceType level;
DatabaseLookup query;
query.AddRestConstraint(DICOM_TAG_STUDY_INSTANCE_UID, path[1],
true /* case sensitive */, true /* mandatory tag */);
level = ResourceType_Study;
if (path.size() >= 3)
{
if (path[2] == STUDY_INFO)
{
return true; // Allow deletion of virtual files (to avoid blocking recursive DELETE)
}
query.AddRestConstraint(DICOM_TAG_SERIES_INSTANCE_UID, path[2],
true /* case sensitive */, true /* mandatory tag */);
level = ResourceType_Series;
}
if (path.size() == 4)
{
if (path[3] == SERIES_INFO)
{
return true; // Allow deletion of virtual files (to avoid blocking recursive DELETE)
}
else if (boost::ends_with(path[3], ".dcm"))
{
const std::string sopInstanceUid = path[3].substr(0, path[3].size() - 4);
query.AddRestConstraint(DICOM_TAG_SOP_INSTANCE_UID, sopInstanceUid,
true /* case sensitive */, true /* mandatory tag */);
level = ResourceType_Instance;
}
else
{
return false;
}
}
DicomDeleteVisitor visitor(context_, level);
ResourceFinder finder(level, ResponseContentFlags_ID, context_.GetFindStorageAccessMode(), context_.GetIndex().HasFindSupport());
finder.SetDatabaseLookup(query);
finder.Execute(visitor, context_);
return true;
}
else
{
return false; // read-only
}
}
else if (path[0] == BY_PATIENTS ||
path[0] == BY_STUDIES ||
path[0] == BY_DATES)
{
if (allowDicomDelete_)
{
return GetRootNode(path[0]).DeleteItem(UriComponents(path.begin() + 1, path.end()));
}
else
{
return false; // read-only
}
}
else if (allowUpload_ &&
path[0] == UPLOADS)
{
return uploads_.DeleteItem(UriComponents(path.begin() + 1, path.end()));
}
else
{
return false;
}
}
void OrthancWebDav::Start()
{
if (uploadRunning_)
{
throw OrthancException(ErrorCode_BadSequenceOfCalls);
}
else if (allowUpload_)
{
LOG(INFO) << "Starting the WebDAV upload thread";
uploadRunning_ = true;
uploadThread_ = boost::thread(UploadWorker, this);
}
}
void OrthancWebDav::Stop()
{
if (uploadRunning_)
{
LOG(INFO) << "Stopping the WebDAV upload thread";
uploadRunning_ = false;
if (uploadThread_.joinable())
{
uploadThread_.join();
}
}
}
}