comparison OrthancServer/Plugins/Samples/AdvancedStorage/Plugin.cpp @ 5081:c673997507ea attach-custom-data

advanced storage cont
author Alain Mazy <am@osimis.io>
date Tue, 13 Sep 2022 11:02:43 +0200
parents d7274e43ea7c
children 4af5f496a0dd
comparison
equal deleted inserted replaced
5080:d7274e43ea7c 5081:c673997507ea
42 #include <list> 42 #include <list>
43 #include <time.h> 43 #include <time.h>
44 44
45 namespace fs = boost::filesystem; 45 namespace fs = boost::filesystem;
46 46
47 fs::path absoluteRootPath_; 47 fs::path rootPath_;
48 bool multipleStoragesEnabled_ = false;
49 std::map<std::string, fs::path> rootPaths_;
50 std::string currentStorageId_;
51 std::string namingScheme_;
48 bool fsyncOnWrite_ = true; 52 bool fsyncOnWrite_ = true;
53 size_t maxPathLength_ = 256;
54 size_t legacyPathLength = 39; // ex "/00/f7/00f7fd8b-47bd8c3a-ff917804-d180cdbc-40cf9527"
55
56 fs::path GetRootPath()
57 {
58 if (multipleStoragesEnabled_)
59 {
60 return rootPaths_[currentStorageId_];
61 }
62
63 return rootPath_;
64 }
65
66 fs::path GetRootPath(const std::string& storageId)
67 {
68 if (multipleStoragesEnabled_)
69 {
70 if (rootPaths_.find(storageId) == rootPaths_.end())
71 {
72 throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange, std::string("Advanced Storage - storage '" + storageId + "' is not defined in configuration"));
73 }
74 return rootPaths_[storageId];
75 }
76
77 return rootPath_;
78 }
49 79
50 80
51 fs::path GetLegacyRelativePath(const std::string& uuid) 81 fs::path GetLegacyRelativePath(const std::string& uuid)
52 { 82 {
53
54 if (!Orthanc::Toolbox::IsUuid(uuid)) 83 if (!Orthanc::Toolbox::IsUuid(uuid))
55 { 84 {
56 throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); 85 throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
57 } 86 }
58 87
59 fs::path path = absoluteRootPath_; 88 fs::path path;
60 89
61 path /= std::string(&uuid[0], &uuid[2]); 90 path /= std::string(&uuid[0], &uuid[2]);
62 path /= std::string(&uuid[2], &uuid[4]); 91 path /= std::string(&uuid[2], &uuid[4]);
63 path /= uuid; 92 path /= uuid;
64 93
67 #endif 96 #endif
68 97
69 return path; 98 return path;
70 } 99 }
71 100
72 fs::path GetAbsolutePath(const std::string& uuid, const std::string& customData) 101 fs::path GetPath(const std::string& uuid, const std::string& customDataString)
73 { 102 {
74 fs::path path = absoluteRootPath_; 103 fs::path path;
75 104
76 if (!customData.empty()) 105 if (!customDataString.empty())
77 { 106 {
78 if (customData.substr(0, 2) == "1.") // version 1 107 Json::Value customData;
79 { 108 Orthanc::Toolbox::ReadJson(customData, customDataString);
80 path /= customData.substr(2); 109
110 if (customData["Version"].asInt() == 1)
111 {
112 if (customData.isMember("StorageId"))
113 {
114 path = GetRootPath(customData["StorageId"].asString());
115 }
116 else
117 {
118 path = GetRootPath();
119 }
120
121 if (customData.isMember("Path"))
122 {
123 path /= customData["Path"].asString();
124 }
125 else
126 { // we are in "legacy mode" for the path part
127 path /= GetLegacyRelativePath(uuid);
128 }
81 } 129 }
82 else 130 else
83 { 131 {
84 throw "TODO: unknown version"; 132 throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange, std::string("Advanced Storage - unknown version for custom data '" + boost::lexical_cast<std::string>(customData["Version"].asInt()) + "'"));
85 } 133 }
86 134 }
87 } 135 else // we are in "legacy mode"
88 else 136 {
89 { 137 path = GetRootPath();
90 path /= GetLegacyRelativePath(uuid); 138 path /= GetLegacyRelativePath(uuid);
91 } 139 }
92 140
93 path.make_preferred(); 141 path.make_preferred();
94 return path; 142 return path;
95 } 143 }
96 144
97 std::string GetCustomData(const fs::path& path) 145 void GetCustomData(std::string& output, const fs::path& path)
98 { 146 {
99 return std::string("1.") + path.string(); // prefix the relative path with a version 147 // if we use defaults, non need to store anything in the metadata, the plugin has the same behavior as the core of Orthanc
100 } 148 if (namingScheme_ == "OrthancDefault" && !multipleStoragesEnabled_)
101 149 {
102 void AddDateDicomTagToPath(fs::path& path, const Json::Value& tags, const char* tagName, const char* defaultValue = NULL) 150 return;
151 }
152
153 Json::Value customDataJson;
154 customDataJson["Version"] = 1;
155
156 if (namingScheme_ != "OrthancDefault")
157 { // no need to store the pathc since we are in the default mode
158 customDataJson["Path"] = path.string();
159 }
160
161 if (multipleStoragesEnabled_)
162 {
163 customDataJson["StorageId"] = currentStorageId_;
164 }
165
166 return Orthanc::Toolbox::WriteFastJson(output, customDataJson);
167 }
168
169 void AddSplitDateDicomTagToPath(fs::path& path, const Json::Value& tags, const char* tagName, const char* defaultValue = NULL)
103 { 170 {
104 if (tags.isMember(tagName) && tags[tagName].asString().size() == 8) 171 if (tags.isMember(tagName) && tags[tagName].asString().size() == 8)
105 { 172 {
106 std::string date = tags[tagName].asString(); 173 std::string date = tags[tagName].asString();
107 path /= date.substr(0, 4); 174 path /= date.substr(0, 4);
112 { 179 {
113 path /= defaultValue; 180 path /= defaultValue;
114 } 181 }
115 } 182 }
116 183
117 void AddSringDicomTagToPath(fs::path& path, const Json::Value& tags, const char* tagName, const char* defaultValue = NULL) 184 void AddStringDicomTagToPath(fs::path& path, const Json::Value& tags, const char* tagName, const char* defaultValue = NULL)
118 { 185 {
119 if (tags.isMember(tagName) && tags[tagName].isString() && tags[tagName].asString().size() > 0) 186 if (tags.isMember(tagName) && tags[tagName].isString() && tags[tagName].asString().size() > 0)
120 { 187 {
121 path /= tags[tagName].asString(); 188 path /= tags[tagName].asString();
122 } 189 }
145 { 212 {
146 path /= defaultValue; 213 path /= defaultValue;
147 } 214 }
148 } 215 }
149 216
150 fs::path GetRelativePathFromTags(const Json::Value& tags, const char* uuid, OrthancPluginContentType type, bool isCompressed) 217 std::string GetExtension(OrthancPluginContentType type, bool isCompressed)
151 { 218 {
152 fs::path path;
153
154 if (type == OrthancPluginContentType_Dicom || type == OrthancPluginContentType_DicomUntilPixelData)
155 {
156 // TODO: allow customization ... note: right now, we always need the uuid in the path !!
157
158 AddDateDicomTagToPath(path, tags, "StudyDate", "NO_STUDY_DATE");
159 AddSringDicomTagToPath(path, tags, "PatientID"); // no default value, tag is always present if the instance is accepted by Orthanc
160 AddSringDicomTagToPath(path, tags, "StudyInstanceUID");
161 AddSringDicomTagToPath(path, tags, "SeriesInstanceUID");
162 //AddIntDicomTagToPath(path, tags, "InstanceNumber", 8, uuid);
163 path /= uuid;
164 }
165 else
166 {
167 path = GetLegacyRelativePath(uuid);
168 }
169
170 std::string extension; 219 std::string extension;
171 220
172 switch (type) 221 switch (type)
173 { 222 {
174 case OrthancPluginContentType_Dicom: 223 case OrthancPluginContentType_Dicom:
183 if (isCompressed) 232 if (isCompressed)
184 { 233 {
185 extension = extension + ".cmp"; // compression is zlib + size -> we can not use the .zip extension 234 extension = extension + ".cmp"; // compression is zlib + size -> we can not use the .zip extension
186 } 235 }
187 236
188 path += extension; 237 return extension;
189 238 }
190 return path; 239
240 fs::path GetRelativePathFromTags(const Json::Value& tags, const char* uuid, OrthancPluginContentType type, bool isCompressed)
241 {
242 fs::path path;
243
244 if (!tags.isNull())
245 {
246 if (namingScheme_ == "Preset1-StudyDatePatientID")
247 {
248 if (!tags.isMember("StudyDate"))
249 {
250 LOG(WARNING) << "AdvancedStorage - No 'StudyDate' in attachment " << uuid << ". Attachment will be stored in NO_STUDY_DATE folder";
251 }
252
253 AddSplitDateDicomTagToPath(path, tags, "StudyDate", "NO_STUDY_DATE");
254 AddStringDicomTagToPath(path, tags, "PatientID"); // no default value, tag is always present if the instance is accepted by Orthanc
255
256 if (tags.isMember("PatientName") && tags["PatientName"].isString() && !tags["PatientName"].asString().empty())
257 {
258 path += std::string(" - ") + tags["PatientName"].asString();
259 }
260
261 AddStringDicomTagToPath(path, tags, "StudyDescription");
262 AddStringDicomTagToPath(path, tags, "SeriesInstanceUID");
263
264 path /= uuid;
265 path += GetExtension(type, isCompressed);
266 return path;
267 }
268 }
269
270 return GetLegacyRelativePath(uuid);
191 } 271 }
192 272
193 273
194 OrthancPluginErrorCode StorageCreate(OrthancPluginMemoryBuffer* customData, 274 OrthancPluginErrorCode StorageCreate(OrthancPluginMemoryBuffer* customData,
195 const char* uuid, 275 const char* uuid,
198 int64_t size, 278 int64_t size,
199 OrthancPluginContentType type, 279 OrthancPluginContentType type,
200 bool isCompressed) 280 bool isCompressed)
201 { 281 {
202 fs::path relativePath = GetRelativePathFromTags(tags, uuid, type, isCompressed); 282 fs::path relativePath = GetRelativePathFromTags(tags, uuid, type, isCompressed);
203 std::string customDataString = GetCustomData(relativePath); 283 std::string customDataString;
204 284 GetCustomData(customDataString, relativePath);
205 fs::path absolutePath = absoluteRootPath_ / relativePath; 285
206 286 fs::path rootPath = GetRootPath();
207 if (fs::exists(absolutePath)) 287 fs::path path = rootPath / relativePath;
288
289 LOG(INFO) << "Advanced Storage - creating attachment \"" << uuid << "\" of type " << static_cast<int>(type) << " (path = " + path.string() + ")";
290
291 // check that the final path is not 'above' the root path (this could happen if e.g., a PatientName is ../../../../toto)
292 std::string canonicalPath = fs::canonical(path).string();
293 if (!Orthanc::Toolbox::StartsWith(canonicalPath, rootPath.string()))
294 {
295 throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError, std::string("Advanced Storage - final path is above root: '") + canonicalPath + "' - '" + rootPath.string() + "'") ;
296 }
297
298 // check path length !!!!!, if too long, go back to legacy path and issue a warning
299 if (path.string().size() > maxPathLength_)
300 {
301 fs::path legacyPath = rootPath / GetLegacyRelativePath(uuid);
302 LOG(WARNING) << "Advanced Storage - WAS01 - Path is too long: '" << path.string() << "' will be stored in '" << legacyPath << "'";
303 path = legacyPath;
304 }
305
306 if (fs::exists(path))
208 { 307 {
209 // Extremely unlikely case if uuid is included in the path: This Uuid has already been created 308 // Extremely unlikely case if uuid is included in the path: This Uuid has already been created
210 // in the past. 309 // in the past.
211 throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); 310 throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError, "Advanced Storage - path already exists");
212 311
213 // TODO for the future: handle duplicates path (e.g: there's no uuid in the path and we are uploading the same file again) 312 // TODO for the future: handle duplicates path (e.g: there's no uuid in the path and we are uploading the same file again)
214 // OrthancPlugins::LogWarning(std::string("Overwriting file \"") + path.string() + "\" (" + uuid + ")"); 313 }
215 } 314
216 315 if (fs::exists(path.parent_path()))
217 if (fs::exists(absolutePath.parent_path())) 316 {
218 { 317 if (!fs::is_directory(path.parent_path()))
219 if (!fs::is_directory(absolutePath.parent_path()))
220 { 318 {
221 throw Orthanc::OrthancException(Orthanc::ErrorCode_DirectoryOverFile); 319 throw Orthanc::OrthancException(Orthanc::ErrorCode_DirectoryOverFile);
222 } 320 }
223 } 321 }
224 else 322 else
225 { 323 {
226 if (!fs::create_directories(absolutePath.parent_path())) 324 if (!fs::create_directories(path.parent_path()))
227 { 325 {
228 throw Orthanc::OrthancException(Orthanc::ErrorCode_FileStorageCannotWrite); 326 throw Orthanc::OrthancException(Orthanc::ErrorCode_FileStorageCannotWrite);
229 } 327 }
230 } 328 }
231 329
232 Orthanc::SystemToolbox::WriteFile(content, size, absolutePath.string(), fsyncOnWrite_); 330 Orthanc::SystemToolbox::WriteFile(content, size, path.string(), fsyncOnWrite_);
233 331
234 OrthancPluginCreateMemoryBuffer(OrthancPlugins::GetGlobalContext(), customData, customDataString.size()); 332 OrthancPluginCreateMemoryBuffer(OrthancPlugins::GetGlobalContext(), customData, customDataString.size());
235 memcpy(customData->data, customDataString.data(), customDataString.size()); 333 memcpy(customData->data, customDataString.data(), customDataString.size());
236 334
237 return OrthancPluginErrorCode_Success; 335 return OrthancPluginErrorCode_Success;
246 OrthancPluginContentType type, 344 OrthancPluginContentType type,
247 bool isCompressed) 345 bool isCompressed)
248 { 346 {
249 try 347 try
250 { 348 {
251 OrthancPlugins::LogInfo(std::string("Creating instance attachment \"") + uuid + "\"");
252
253 OrthancPlugins::DicomInstance dicomInstance(instance); 349 OrthancPlugins::DicomInstance dicomInstance(instance);
254 Json::Value tags; 350 Json::Value tags;
255 dicomInstance.GetSimplifiedJson(tags); 351 dicomInstance.GetSimplifiedJson(tags);
256 352
257 return StorageCreate(customData, uuid, tags, content, size, type, isCompressed); 353 return StorageCreate(customData, uuid, tags, content, size, type, isCompressed);
302 OrthancPluginErrorCode StorageReadWhole(OrthancPluginMemoryBuffer64* target, 398 OrthancPluginErrorCode StorageReadWhole(OrthancPluginMemoryBuffer64* target,
303 const char* uuid, 399 const char* uuid,
304 const char* customData, 400 const char* customData,
305 OrthancPluginContentType type) 401 OrthancPluginContentType type)
306 { 402 {
307 OrthancPlugins::LogInfo(std::string("Reading attachment \"") + uuid + "\""); 403 std::string path = GetPath(uuid, customData).string();
308 404
309 std::string path = GetAbsolutePath(uuid, customData).string(); 405 LOG(INFO) << "Advanced Storage - Reading whole attachment \"" << uuid << "\" of type " << static_cast<int>(type) << " (path = " + path + ")";
310 406
311 if (!Orthanc::SystemToolbox::IsRegularFile(path)) 407 if (!Orthanc::SystemToolbox::IsRegularFile(path))
312 { 408 {
313 OrthancPlugins::LogError(std::string("The path does not point to a regular file: ") + path); 409 OrthancPlugins::LogError(std::string("The path does not point to a regular file: ") + path);
314 return OrthancPluginErrorCode_InexistentFile; 410 return OrthancPluginErrorCode_InexistentFile;
357 const char* uuid, 453 const char* uuid,
358 const char* customData, 454 const char* customData,
359 OrthancPluginContentType type, 455 OrthancPluginContentType type,
360 uint64_t rangeStart) 456 uint64_t rangeStart)
361 { 457 {
362 OrthancPlugins::LogInfo(std::string("Reading attachment \"") + uuid + "\""); 458 std::string path = GetPath(uuid, customData).string();
363 459
364 std::string path = GetAbsolutePath(uuid, customData).string(); 460 LOG(INFO) << "Advanced Storage - Reading range of attachment \"" << uuid << "\" of type " << static_cast<int>(type) << " (path = " + path + ")";
365 461
366 if (!Orthanc::SystemToolbox::IsRegularFile(path)) 462 if (!Orthanc::SystemToolbox::IsRegularFile(path))
367 { 463 {
368 OrthancPlugins::LogError(std::string("The path does not point to a regular file: ") + path); 464 OrthancPlugins::LogError(std::string("The path does not point to a regular file: ") + path);
369 return OrthancPluginErrorCode_InexistentFile; 465 return OrthancPluginErrorCode_InexistentFile;
398 494
399 OrthancPluginErrorCode StorageRemove (const char* uuid, 495 OrthancPluginErrorCode StorageRemove (const char* uuid,
400 const char* customData, 496 const char* customData,
401 OrthancPluginContentType type) 497 OrthancPluginContentType type)
402 { 498 {
403 // LOG(INFO) << "Deleting attachment \"" << uuid << "\" of type " << static_cast<int>(type); 499 fs::path path = GetPath(uuid, customData);
404 500
405 fs::path p = GetAbsolutePath(uuid, customData); 501 LOG(INFO) << "Advanced Storage - Deleting attachment \"" << uuid << "\" of type " << static_cast<int>(type) << " (path = " + path.string() + ")";
406 502
407 try 503 try
408 { 504 {
409 fs::remove(p); 505 fs::remove(path);
410 } 506 }
411 catch (...) 507 catch (...)
412 { 508 {
413 // Ignore the error 509 // Ignore the error
414 } 510 }
415 511
416 // Remove the empty parent directories, (ignoring the error code if these directories are not empty) 512 // Remove the empty parent directories, (ignoring the error code if these directories are not empty)
417 513
418 try 514 try
419 { 515 {
420 fs::path parent = p.parent_path(); 516 fs::path parent = path.parent_path();
421 517
422 while (parent != absoluteRootPath_) 518 while (parent != GetRootPath())
423 { 519 {
424 fs::remove(parent); 520 fs::remove(parent);
425 parent = parent.parent_path(); 521 parent = parent.parent_path();
426 } 522 }
427 } 523 }
465 { 561 {
466 "AdvancedStorage": { 562 "AdvancedStorage": {
467 563
468 // Enables/disables the plugin 564 // Enables/disables the plugin
469 "Enable": false, 565 "Enable": false,
566
567 // Enables/disables support for multiple StorageDirectories
568 "MultipleStorages" : {
569 "Storages" : {
570 // The storgae ids below may never change since they are stored in DB
571 // The storage path may change in case you move your data from one place to the other
572 "1" : "/var/lib/orthanc/db",
573 "2" : "/mnt/disk2/orthanc"
574 },
575
576 // the storage on which new data is stored.
577 // There's currently no automatic changes of disks
578 "CurrentStorage" : "2",
579 },
580
581 // Defines the storage structure and file namings. Right now,
582 // only the "OrthancDefault" value shall be used in a production environment.
583 // All other values are currently experimental
584 // "OrthancDefault" = same structure and file naming as default orthanc,
585 // "Preset1-StudyDatePatientID" = split(StudyDate)/PatientID - PatientName/StudyDescription/SeriesInstanceUID/uuid.ext
586 "NamingScheme" : "OrthancDefault",
587
588 // Defines the maximum length for path used in the storage. If a file is longer
589 // than this limit, it is stored with the default orthanc naming scheme
590 // (and a warning is issued).
591 // Note, on Windows, the maximum path length is 260 bytes by default but can be increased
592 // through a configuration.
593 "MaxPathLength" : 256
470 } 594 }
471 } 595 }
472 */ 596 */
473 597
474 absoluteRootPath_ = fs::absolute(fs::path(orthancConfiguration.GetStringValue("StorageDirectory", "OrthancStorage"))); 598 fsyncOnWrite_ = orthancConfiguration.GetBooleanValue("SyncStorageArea", true);
475 LOG(WARNING) << "AdvancedStorage - Path to the storage area: " << absoluteRootPath_.string(); 599
600 const Json::Value& pluginJson = advancedStorage.GetJson();
601
602 namingScheme_ = advancedStorage.GetStringValue("NamingScheme", "OrthancDefault");
603
604 // if we have enabled multiple storage after files have been saved without this plugin, we still need the default StorageDirectory
605 rootPath_ = fs::path(orthancConfiguration.GetStringValue("StorageDirectory", "OrthancStorage"));
606 LOG(WARNING) << "AdvancedStorage - Path to the default storage area: " << rootPath_.string();
607
608 maxPathLength_ = orthancConfiguration.GetIntegerValue("MaxPathLength", 256);
609 LOG(WARNING) << "AdvancedStorage - Maximum path length: " << maxPathLength_;
610
611 if (!rootPath_.is_absolute())
612 {
613 LOG(ERROR) << "AdvancedStorage - Path to the default storage area should be an absolute path";
614 return -1;
615 }
616
617 if (rootPath_.size() > (maxPathLength_ - legacyPathLength))
618 {
619 LOG(ERROR) << "AdvancedStorage - Path to the default storage is too long";
620 return -1;
621 }
622
623 if (pluginJson.isMember("MultipleStorages"))
624 {
625 multipleStoragesEnabled_ = true;
626 const Json::Value& multipleStoragesJson = pluginJson["MultipleStorages"];
627
628 if (multipleStoragesJson.isMember("Storages") && multipleStoragesJson.isObject() && multipleStoragesJson.isMember("CurrentStorage") && multipleStoragesJson["CurrentStorage"].isString())
629 {
630 const Json::Value& storagesJson = multipleStoragesJson["Storages"];
631 Json::Value::Members storageIds = storagesJson.getMemberNames();
632
633 for (Json::Value::Members::const_iterator it = storageIds.begin(); it != storageIds.end(); ++it)
634 {
635 const Json::Value& storagePath = storagesJson[*it];
636 if (!storagePath.isString())
637 {
638 LOG(ERROR) << "AdvancedStorage - Storage path is not a string " << *it;
639 return -1;
640 }
641
642 rootPaths_[*it] = storagePath.asString();
643
644 if (!rootPaths_[*it].is_absolute())
645 {
646 LOG(ERROR) << "AdvancedStorage - Storage path shall be absolute path '" << storagePath.asString() << "'";
647 return -1;
648 }
649
650 if (storagePath.asString().size() > (maxPathLength_ - legacyPathLength))
651 {
652 LOG(ERROR) << "AdvancedStorage - Storage path is too long '" << storagePath.asString() << "'";
653 return -1;
654 }
655 }
656
657 currentStorageId_ = multipleStoragesJson["CurrentStorage"].asString();
658
659 if (rootPaths_.find(currentStorageId_) == rootPaths_.end())
660 {
661 LOG(ERROR) << "AdvancedStorage - CurrentStorage is not defined in Storages list: " << currentStorageId_;
662 return -1;
663 }
664
665 LOG(WARNING) << "AdvancedStorage - multiple storages enabled. Current storage : " << rootPaths_[currentStorageId_].string();
666 }
667 }
476 668
477 OrthancPluginRegisterStorageArea3(context, StorageCreateInstance, StorageCreateAttachment, StorageReadWhole, StorageReadRange, StorageRemove); 669 OrthancPluginRegisterStorageArea3(context, StorageCreateInstance, StorageCreateAttachment, StorageReadWhole, StorageReadRange, StorageRemove);
478 } 670 }
479 else 671 else
480 { 672 {