2632
|
1 /**
|
|
2 * Orthanc - A Lightweight, RESTful DICOM Store
|
|
3 * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
|
|
4 * Department, University Hospital of Liege, Belgium
|
|
5 * Copyright (C) 2017-2018 Osimis S.A., Belgium
|
|
6 *
|
|
7 * This program is free software: you can redistribute it and/or
|
|
8 * modify it under the terms of the GNU General Public License as
|
|
9 * published by the Free Software Foundation, either version 3 of the
|
|
10 * License, or (at your option) any later version.
|
|
11 *
|
|
12 * In addition, as a special exception, the copyright holders of this
|
|
13 * program give permission to link the code of its release with the
|
|
14 * OpenSSL project's "OpenSSL" library (or with modified versions of it
|
|
15 * that use the same license as the "OpenSSL" library), and distribute
|
|
16 * the linked executables. You must obey the GNU General Public License
|
|
17 * in all respects for all of the code used other than "OpenSSL". If you
|
|
18 * modify file(s) with this exception, you may extend this exception to
|
|
19 * your version of the file(s), but you are not obligated to do so. If
|
|
20 * you do not wish to do so, delete this exception statement from your
|
|
21 * version. If you delete this exception statement from all source files
|
|
22 * in the program, then also delete it here.
|
|
23 *
|
|
24 * This program is distributed in the hope that it will be useful, but
|
|
25 * WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
26 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
27 * General Public License for more details.
|
|
28 *
|
|
29 * You should have received a copy of the GNU General Public License
|
|
30 * along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
31 **/
|
|
32
|
|
33
|
|
34 #include "../PrecompiledHeadersServer.h"
|
|
35 #include "ArchiveJob.h"
|
|
36
|
|
37 #include "../../Core/Compression/HierarchicalZipWriter.h"
|
|
38 #include "../../Core/DicomParsing/DicomDirWriter.h"
|
|
39 #include "../../Core/Logging.h"
|
|
40 #include "../../Core/OrthancException.h"
|
|
41
|
|
42 #include <stdio.h>
|
|
43
|
|
44 #if defined(_MSC_VER)
|
|
45 #define snprintf _snprintf
|
|
46 #endif
|
|
47
|
|
48 static const uint64_t MEGA_BYTES = 1024 * 1024;
|
|
49 static const uint64_t GIGA_BYTES = 1024 * 1024 * 1024;
|
|
50 static const char* MEDIA_IMAGES_FOLDER = "IMAGES";
|
|
51
|
|
52 namespace Orthanc
|
|
53 {
|
|
54 static bool IsZip64Required(uint64_t uncompressedSize,
|
|
55 unsigned int countInstances)
|
|
56 {
|
|
57 static const uint64_t SAFETY_MARGIN = 64 * MEGA_BYTES; // Should be large enough to hold DICOMDIR
|
|
58 static const unsigned int FILES_MARGIN = 10;
|
|
59
|
|
60 /**
|
|
61 * Determine whether ZIP64 is required. Original ZIP format can
|
|
62 * store up to 2GB of data (some implementation supporting up to
|
|
63 * 4GB of data), and up to 65535 files.
|
|
64 * https://en.wikipedia.org/wiki/Zip_(file_format)#ZIP64
|
|
65 **/
|
|
66
|
|
67 const bool isZip64 = (uncompressedSize >= 2 * GIGA_BYTES - SAFETY_MARGIN ||
|
|
68 countInstances >= 65535 - FILES_MARGIN);
|
|
69
|
|
70 LOG(INFO) << "Creating a ZIP file with " << countInstances << " files of size "
|
|
71 << (uncompressedSize / MEGA_BYTES) << "MB using the "
|
|
72 << (isZip64 ? "ZIP64" : "ZIP32") << " file format";
|
|
73
|
|
74 return isZip64;
|
|
75 }
|
|
76
|
|
77
|
|
78 class ArchiveJob::ResourceIdentifiers : public boost::noncopyable
|
|
79 {
|
|
80 private:
|
|
81 ResourceType level_;
|
|
82 std::string patient_;
|
|
83 std::string study_;
|
|
84 std::string series_;
|
|
85 std::string instance_;
|
|
86
|
|
87 static void GoToParent(ServerIndex& index,
|
|
88 std::string& current)
|
|
89 {
|
|
90 std::string tmp;
|
|
91
|
|
92 if (index.LookupParent(tmp, current))
|
|
93 {
|
|
94 current = tmp;
|
|
95 }
|
|
96 else
|
|
97 {
|
|
98 throw OrthancException(ErrorCode_UnknownResource);
|
|
99 }
|
|
100 }
|
|
101
|
|
102
|
|
103 public:
|
|
104 ResourceIdentifiers(ServerIndex& index,
|
|
105 const std::string& publicId)
|
|
106 {
|
|
107 if (!index.LookupResourceType(level_, publicId))
|
|
108 {
|
|
109 throw OrthancException(ErrorCode_UnknownResource);
|
|
110 }
|
|
111
|
|
112 std::string current = publicId;;
|
|
113 switch (level_) // Do not add "break" below!
|
|
114 {
|
|
115 case ResourceType_Instance:
|
|
116 instance_ = current;
|
|
117 GoToParent(index, current);
|
|
118
|
|
119 case ResourceType_Series:
|
|
120 series_ = current;
|
|
121 GoToParent(index, current);
|
|
122
|
|
123 case ResourceType_Study:
|
|
124 study_ = current;
|
|
125 GoToParent(index, current);
|
|
126
|
|
127 case ResourceType_Patient:
|
|
128 patient_ = current;
|
|
129 break;
|
|
130
|
|
131 default:
|
|
132 throw OrthancException(ErrorCode_InternalError);
|
|
133 }
|
|
134 }
|
|
135
|
|
136 ResourceType GetLevel() const
|
|
137 {
|
|
138 return level_;
|
|
139 }
|
|
140
|
|
141 const std::string& GetIdentifier(ResourceType level) const
|
|
142 {
|
|
143 // Some sanity check to ensure enumerations are not altered
|
|
144 assert(ResourceType_Patient < ResourceType_Study);
|
|
145 assert(ResourceType_Study < ResourceType_Series);
|
|
146 assert(ResourceType_Series < ResourceType_Instance);
|
|
147
|
|
148 if (level > level_)
|
|
149 {
|
|
150 throw OrthancException(ErrorCode_InternalError);
|
|
151 }
|
|
152
|
|
153 switch (level)
|
|
154 {
|
|
155 case ResourceType_Patient:
|
|
156 return patient_;
|
|
157
|
|
158 case ResourceType_Study:
|
|
159 return study_;
|
|
160
|
|
161 case ResourceType_Series:
|
|
162 return series_;
|
|
163
|
|
164 case ResourceType_Instance:
|
|
165 return instance_;
|
|
166
|
|
167 default:
|
|
168 throw OrthancException(ErrorCode_InternalError);
|
|
169 }
|
|
170 }
|
|
171 };
|
|
172
|
|
173
|
|
174 class ArchiveJob::IArchiveVisitor : public boost::noncopyable
|
|
175 {
|
|
176 public:
|
|
177 virtual ~IArchiveVisitor()
|
|
178 {
|
|
179 }
|
|
180
|
|
181 virtual void Open(ResourceType level,
|
|
182 const std::string& publicId) = 0;
|
|
183
|
|
184 virtual void Close() = 0;
|
|
185
|
|
186 virtual void AddInstance(const std::string& instanceId,
|
|
187 const FileInfo& dicom) = 0;
|
|
188 };
|
|
189
|
|
190
|
|
191 class ArchiveJob::ArchiveIndex : public boost::noncopyable
|
|
192 {
|
|
193 private:
|
|
194 struct Instance
|
|
195 {
|
|
196 std::string id_;
|
|
197 FileInfo dicom_;
|
|
198
|
|
199 Instance(const std::string& id,
|
|
200 const FileInfo& dicom) :
|
|
201 id_(id), dicom_(dicom)
|
|
202 {
|
|
203 }
|
|
204 };
|
|
205
|
|
206 // A "NULL" value for ArchiveIndex indicates a non-expanded node
|
|
207 typedef std::map<std::string, ArchiveIndex*> Resources;
|
|
208
|
|
209 ResourceType level_;
|
|
210 Resources resources_; // Only at patient/study/series level
|
|
211 std::list<Instance> instances_; // Only at instance level
|
|
212
|
|
213
|
|
214 void AddResourceToExpand(ServerIndex& index,
|
|
215 const std::string& id)
|
|
216 {
|
|
217 if (level_ == ResourceType_Instance)
|
|
218 {
|
|
219 FileInfo tmp;
|
|
220 if (index.LookupAttachment(tmp, id, FileContentType_Dicom))
|
|
221 {
|
|
222 instances_.push_back(Instance(id, tmp));
|
|
223 }
|
|
224 }
|
|
225 else
|
|
226 {
|
|
227 resources_[id] = NULL;
|
|
228 }
|
|
229 }
|
|
230
|
|
231
|
|
232 public:
|
|
233 ArchiveIndex(ResourceType level) :
|
|
234 level_(level)
|
|
235 {
|
|
236 }
|
|
237
|
|
238 ~ArchiveIndex()
|
|
239 {
|
|
240 for (Resources::iterator it = resources_.begin();
|
|
241 it != resources_.end(); ++it)
|
|
242 {
|
|
243 delete it->second;
|
|
244 }
|
|
245 }
|
|
246
|
|
247
|
|
248 void Add(ServerIndex& index,
|
|
249 const ResourceIdentifiers& resource)
|
|
250 {
|
|
251 const std::string& id = resource.GetIdentifier(level_);
|
|
252 Resources::iterator previous = resources_.find(id);
|
|
253
|
|
254 if (level_ == ResourceType_Instance)
|
|
255 {
|
|
256 AddResourceToExpand(index, id);
|
|
257 }
|
|
258 else if (resource.GetLevel() == level_)
|
|
259 {
|
|
260 // Mark this resource for further expansion
|
|
261 if (previous != resources_.end())
|
|
262 {
|
|
263 delete previous->second;
|
|
264 }
|
|
265
|
|
266 resources_[id] = NULL;
|
|
267 }
|
|
268 else if (previous == resources_.end())
|
|
269 {
|
|
270 // This is the first time we meet this resource
|
|
271 std::auto_ptr<ArchiveIndex> child(new ArchiveIndex(GetChildResourceType(level_)));
|
|
272 child->Add(index, resource);
|
|
273 resources_[id] = child.release();
|
|
274 }
|
|
275 else if (previous->second != NULL)
|
|
276 {
|
|
277 previous->second->Add(index, resource);
|
|
278 }
|
|
279 else
|
|
280 {
|
|
281 // Nothing to do: This item is marked for further expansion
|
|
282 }
|
|
283 }
|
|
284
|
|
285
|
|
286 void Expand(ServerIndex& index)
|
|
287 {
|
|
288 if (level_ == ResourceType_Instance)
|
|
289 {
|
|
290 // Expanding an instance node makes no sense
|
|
291 return;
|
|
292 }
|
|
293
|
|
294 for (Resources::iterator it = resources_.begin();
|
|
295 it != resources_.end(); ++it)
|
|
296 {
|
|
297 if (it->second == NULL)
|
|
298 {
|
|
299 // This is resource is marked for expansion
|
|
300 std::list<std::string> children;
|
|
301 index.GetChildren(children, it->first);
|
|
302
|
|
303 std::auto_ptr<ArchiveIndex> child(new ArchiveIndex(GetChildResourceType(level_)));
|
|
304
|
|
305 for (std::list<std::string>::const_iterator
|
|
306 it2 = children.begin(); it2 != children.end(); ++it2)
|
|
307 {
|
|
308 child->AddResourceToExpand(index, *it2);
|
|
309 }
|
|
310
|
|
311 it->second = child.release();
|
|
312 }
|
|
313
|
|
314 assert(it->second != NULL);
|
|
315 it->second->Expand(index);
|
|
316 }
|
|
317 }
|
|
318
|
|
319
|
|
320 void Apply(IArchiveVisitor& visitor) const
|
|
321 {
|
|
322 if (level_ == ResourceType_Instance)
|
|
323 {
|
|
324 for (std::list<Instance>::const_iterator
|
|
325 it = instances_.begin(); it != instances_.end(); ++it)
|
|
326 {
|
|
327 visitor.AddInstance(it->id_, it->dicom_);
|
|
328 }
|
|
329 }
|
|
330 else
|
|
331 {
|
|
332 for (Resources::const_iterator it = resources_.begin();
|
|
333 it != resources_.end(); ++it)
|
|
334 {
|
|
335 assert(it->second != NULL); // There must have been a call to "Expand()"
|
|
336 visitor.Open(level_, it->first);
|
|
337 it->second->Apply(visitor);
|
|
338 visitor.Close();
|
|
339 }
|
|
340 }
|
|
341 }
|
|
342 };
|
|
343
|
|
344
|
|
345
|
|
346 class ArchiveJob::ZipCommands : public boost::noncopyable
|
|
347 {
|
|
348 private:
|
|
349 enum Type
|
|
350 {
|
|
351 Type_OpenDirectory,
|
|
352 Type_CloseDirectory,
|
|
353 Type_WriteInstance
|
|
354 };
|
|
355
|
|
356 class Command : public boost::noncopyable
|
|
357 {
|
|
358 private:
|
|
359 Type type_;
|
|
360 std::string filename_;
|
|
361 std::string instanceId_;
|
|
362 FileInfo info_;
|
|
363
|
|
364 public:
|
|
365 explicit Command(Type type) :
|
|
366 type_(type)
|
|
367 {
|
|
368 assert(type_ == Type_CloseDirectory);
|
|
369 }
|
|
370
|
|
371 Command(Type type,
|
|
372 const std::string& filename) :
|
|
373 type_(type),
|
|
374 filename_(filename)
|
|
375 {
|
|
376 assert(type_ == Type_OpenDirectory);
|
|
377 }
|
|
378
|
|
379 Command(Type type,
|
|
380 const std::string& filename,
|
|
381 const std::string& instanceId,
|
|
382 const FileInfo& info) :
|
|
383 type_(type),
|
|
384 filename_(filename),
|
|
385 instanceId_(instanceId),
|
|
386 info_(info)
|
|
387 {
|
|
388 assert(type_ == Type_WriteInstance);
|
|
389 }
|
|
390
|
|
391 void Apply(HierarchicalZipWriter& writer,
|
|
392 ServerContext& context,
|
|
393 DicomDirWriter* dicomDir,
|
|
394 const std::string& dicomDirFolder) const
|
|
395 {
|
|
396 switch (type_)
|
|
397 {
|
|
398 case Type_OpenDirectory:
|
|
399 writer.OpenDirectory(filename_.c_str());
|
|
400 break;
|
|
401
|
|
402 case Type_CloseDirectory:
|
|
403 writer.CloseDirectory();
|
|
404 break;
|
|
405
|
|
406 case Type_WriteInstance:
|
|
407 {
|
|
408 std::string content;
|
|
409
|
|
410 try
|
|
411 {
|
|
412 context.ReadAttachment(content, info_);
|
|
413 }
|
|
414 catch (OrthancException& e)
|
|
415 {
|
|
416 LOG(WARNING) << "An instance was removed after the job was issued: " << instanceId_;
|
|
417 return;
|
|
418 }
|
|
419
|
|
420 writer.OpenFile(filename_.c_str());
|
|
421 writer.Write(content);
|
|
422
|
|
423 if (dicomDir != NULL)
|
|
424 {
|
|
425 ParsedDicomFile parsed(content);
|
|
426 dicomDir->Add(dicomDirFolder, filename_, parsed);
|
|
427 }
|
|
428
|
|
429 break;
|
|
430 }
|
|
431
|
|
432 default:
|
|
433 throw OrthancException(ErrorCode_InternalError);
|
|
434 }
|
|
435 }
|
|
436 };
|
|
437
|
|
438 std::deque<Command*> commands_;
|
|
439 uint64_t uncompressedSize_;
|
|
440 unsigned int instancesCount_;
|
|
441
|
|
442
|
|
443 void ApplyInternal(HierarchicalZipWriter& writer,
|
|
444 ServerContext& context,
|
|
445 size_t index,
|
|
446 DicomDirWriter* dicomDir,
|
|
447 const std::string& dicomDirFolder) const
|
|
448 {
|
|
449 if (index >= commands_.size())
|
|
450 {
|
|
451 throw OrthancException(ErrorCode_ParameterOutOfRange);
|
|
452 }
|
|
453
|
|
454 commands_[index]->Apply(writer, context, dicomDir, dicomDirFolder);
|
|
455 }
|
|
456
|
|
457 public:
|
|
458 ZipCommands() :
|
|
459 uncompressedSize_(0),
|
|
460 instancesCount_(0)
|
|
461 {
|
|
462 }
|
|
463
|
|
464 ~ZipCommands()
|
|
465 {
|
|
466 for (std::deque<Command*>::iterator it = commands_.begin();
|
|
467 it != commands_.end(); ++it)
|
|
468 {
|
|
469 assert(*it != NULL);
|
|
470 delete *it;
|
|
471 }
|
|
472 }
|
|
473
|
|
474 size_t GetSize() const
|
|
475 {
|
|
476 return commands_.size();
|
|
477 }
|
|
478
|
|
479 unsigned int GetInstancesCount() const
|
|
480 {
|
|
481 return instancesCount_;
|
|
482 }
|
|
483
|
|
484 uint64_t GetUncompressedSize() const
|
|
485 {
|
|
486 return uncompressedSize_;
|
|
487 }
|
|
488
|
|
489 void Apply(HierarchicalZipWriter& writer,
|
|
490 ServerContext& context,
|
|
491 size_t index,
|
|
492 DicomDirWriter& dicomDir,
|
|
493 const std::string& dicomDirFolder) const
|
|
494 {
|
|
495 ApplyInternal(writer, context, index, &dicomDir, dicomDirFolder);
|
|
496 }
|
|
497
|
|
498 void Apply(HierarchicalZipWriter& writer,
|
|
499 ServerContext& context,
|
|
500 size_t index) const
|
|
501 {
|
|
502 ApplyInternal(writer, context, index, NULL, "");
|
|
503 }
|
|
504
|
|
505 void AddOpenDirectory(const std::string& filename)
|
|
506 {
|
|
507 commands_.push_back(new Command(Type_OpenDirectory, filename));
|
|
508 }
|
|
509
|
|
510 void AddCloseDirectory()
|
|
511 {
|
|
512 commands_.push_back(new Command(Type_CloseDirectory));
|
|
513 }
|
|
514
|
|
515 void AddWriteInstance(const std::string& filename,
|
|
516 const std::string& instanceId,
|
|
517 const FileInfo& info)
|
|
518 {
|
|
519 commands_.push_back(new Command(Type_WriteInstance, filename, instanceId, info));
|
|
520 instancesCount_ ++;
|
|
521 uncompressedSize_ += info.GetUncompressedSize();
|
|
522 }
|
|
523
|
|
524 bool IsZip64() const
|
|
525 {
|
|
526 return IsZip64Required(GetUncompressedSize(), GetInstancesCount());
|
|
527 }
|
|
528 };
|
|
529
|
|
530
|
|
531
|
|
532 class ArchiveJob::ArchiveIndexVisitor : public IArchiveVisitor
|
|
533 {
|
|
534 private:
|
|
535 ZipCommands& commands_;
|
|
536 ServerContext& context_;
|
|
537 char instanceFormat_[24];
|
|
538 unsigned int counter_;
|
|
539
|
|
540 static std::string GetTag(const DicomMap& tags,
|
|
541 const DicomTag& tag)
|
|
542 {
|
|
543 const DicomValue* v = tags.TestAndGetValue(tag);
|
|
544 if (v != NULL &&
|
|
545 !v->IsBinary() &&
|
|
546 !v->IsNull())
|
|
547 {
|
|
548 return v->GetContent();
|
|
549 }
|
|
550 else
|
|
551 {
|
|
552 return "";
|
|
553 }
|
|
554 }
|
|
555
|
|
556 public:
|
|
557 ArchiveIndexVisitor(ZipCommands& commands,
|
|
558 ServerContext& context) :
|
|
559 commands_(commands),
|
|
560 context_(context),
|
|
561 counter_(0)
|
|
562 {
|
|
563 if (commands.GetSize() != 0)
|
|
564 {
|
|
565 throw OrthancException(ErrorCode_BadSequenceOfCalls);
|
|
566 }
|
|
567
|
|
568 snprintf(instanceFormat_, sizeof(instanceFormat_) - 1, "%%08d.dcm");
|
|
569 }
|
|
570
|
|
571 virtual void Open(ResourceType level,
|
|
572 const std::string& publicId)
|
|
573 {
|
|
574 std::string path;
|
|
575
|
|
576 DicomMap tags;
|
|
577 if (context_.GetIndex().GetMainDicomTags(tags, publicId, level, level))
|
|
578 {
|
|
579 switch (level)
|
|
580 {
|
|
581 case ResourceType_Patient:
|
|
582 path = GetTag(tags, DICOM_TAG_PATIENT_ID) + " " + GetTag(tags, DICOM_TAG_PATIENT_NAME);
|
|
583 break;
|
|
584
|
|
585 case ResourceType_Study:
|
|
586 path = GetTag(tags, DICOM_TAG_ACCESSION_NUMBER) + " " + GetTag(tags, DICOM_TAG_STUDY_DESCRIPTION);
|
|
587 break;
|
|
588
|
|
589 case ResourceType_Series:
|
|
590 {
|
|
591 std::string modality = GetTag(tags, DICOM_TAG_MODALITY);
|
|
592 path = modality + " " + GetTag(tags, DICOM_TAG_SERIES_DESCRIPTION);
|
|
593
|
|
594 if (modality.size() == 0)
|
|
595 {
|
|
596 snprintf(instanceFormat_, sizeof(instanceFormat_) - 1, "%%08d.dcm");
|
|
597 }
|
|
598 else if (modality.size() == 1)
|
|
599 {
|
|
600 snprintf(instanceFormat_, sizeof(instanceFormat_) - 1, "%c%%07d.dcm",
|
|
601 toupper(modality[0]));
|
|
602 }
|
|
603 else if (modality.size() >= 2)
|
|
604 {
|
|
605 snprintf(instanceFormat_, sizeof(instanceFormat_) - 1, "%c%c%%06d.dcm",
|
|
606 toupper(modality[0]), toupper(modality[1]));
|
|
607 }
|
|
608
|
|
609 counter_ = 0;
|
|
610
|
|
611 break;
|
|
612 }
|
|
613
|
|
614 default:
|
|
615 throw OrthancException(ErrorCode_InternalError);
|
|
616 }
|
|
617 }
|
|
618
|
|
619 path = Toolbox::StripSpaces(Toolbox::ConvertToAscii(path));
|
|
620
|
|
621 if (path.empty())
|
|
622 {
|
|
623 path = std::string("Unknown ") + EnumerationToString(level);
|
|
624 }
|
|
625
|
|
626 commands_.AddOpenDirectory(path.c_str());
|
|
627 }
|
|
628
|
|
629 virtual void Close()
|
|
630 {
|
|
631 commands_.AddCloseDirectory();
|
|
632 }
|
|
633
|
|
634 virtual void AddInstance(const std::string& instanceId,
|
|
635 const FileInfo& dicom)
|
|
636 {
|
|
637 char filename[24];
|
|
638 snprintf(filename, sizeof(filename) - 1, instanceFormat_, counter_);
|
|
639 counter_ ++;
|
|
640
|
|
641 commands_.AddWriteInstance(filename, instanceId, dicom);
|
|
642 }
|
|
643 };
|
|
644
|
|
645
|
|
646 class ArchiveJob::MediaIndexVisitor : public IArchiveVisitor
|
|
647 {
|
|
648 private:
|
|
649 ZipCommands& commands_;
|
|
650 ServerContext& context_;
|
|
651 unsigned int counter_;
|
|
652
|
|
653 public:
|
|
654 MediaIndexVisitor(ZipCommands& commands,
|
|
655 ServerContext& context) :
|
|
656 commands_(commands),
|
|
657 context_(context),
|
|
658 counter_(0)
|
|
659 {
|
|
660 }
|
|
661
|
|
662 virtual void Open(ResourceType level,
|
|
663 const std::string& publicId)
|
|
664 {
|
|
665 }
|
|
666
|
|
667 virtual void Close()
|
|
668 {
|
|
669 }
|
|
670
|
|
671 virtual void AddInstance(const std::string& instanceId,
|
|
672 const FileInfo& dicom)
|
|
673 {
|
|
674 // "DICOM restricts the filenames on DICOM media to 8
|
|
675 // characters (some systems wrongly use 8.3, but this does not
|
|
676 // conform to the standard)."
|
|
677 std::string filename = "IM" + boost::lexical_cast<std::string>(counter_);
|
|
678 commands_.AddWriteInstance(filename, instanceId, dicom);
|
|
679
|
|
680 counter_ ++;
|
|
681 }
|
|
682 };
|
|
683
|
|
684
|
|
685 class ArchiveJob::ZipWriterIterator : public boost::noncopyable
|
|
686 {
|
|
687 private:
|
|
688 TemporaryFile& target_;
|
|
689 ServerContext& context_;
|
|
690 ZipCommands commands_;
|
|
691 std::auto_ptr<HierarchicalZipWriter> zip_;
|
|
692 std::auto_ptr<DicomDirWriter> dicomDir_;
|
|
693 bool isMedia_;
|
|
694
|
|
695 public:
|
|
696 ZipWriterIterator(TemporaryFile& target,
|
|
697 ServerContext& context,
|
|
698 ArchiveIndex& archive,
|
|
699 bool isMedia,
|
|
700 bool enableExtendedSopClass) :
|
|
701 target_(target),
|
|
702 context_(context),
|
|
703 isMedia_(isMedia)
|
|
704 {
|
|
705 if (isMedia)
|
|
706 {
|
|
707 MediaIndexVisitor visitor(commands_, context);
|
|
708 archive.Expand(context.GetIndex());
|
|
709
|
|
710 commands_.AddOpenDirectory(MEDIA_IMAGES_FOLDER);
|
|
711 archive.Apply(visitor);
|
|
712 commands_.AddCloseDirectory();
|
|
713
|
|
714 dicomDir_.reset(new DicomDirWriter);
|
|
715 dicomDir_->EnableExtendedSopClass(enableExtendedSopClass);
|
|
716 }
|
|
717 else
|
|
718 {
|
|
719 ArchiveIndexVisitor visitor(commands_, context);
|
|
720 archive.Expand(context.GetIndex());
|
|
721 archive.Apply(visitor);
|
|
722 }
|
|
723
|
|
724 zip_.reset(new HierarchicalZipWriter(target.GetPath().c_str()));
|
|
725 zip_->SetZip64(commands_.IsZip64());
|
|
726 }
|
|
727
|
|
728 size_t GetStepsCount() const
|
|
729 {
|
|
730 return commands_.GetSize() + 1;
|
|
731 }
|
|
732
|
|
733 void RunStep(size_t index)
|
|
734 {
|
|
735 if (index > commands_.GetSize())
|
|
736 {
|
|
737 throw OrthancException(ErrorCode_ParameterOutOfRange);
|
|
738 }
|
|
739 else if (index == commands_.GetSize())
|
|
740 {
|
|
741 // Last step: Add the DICOMDIR
|
|
742 if (isMedia_)
|
|
743 {
|
|
744 assert(dicomDir_.get() != NULL);
|
|
745 std::string s;
|
|
746 dicomDir_->Encode(s);
|
|
747
|
|
748 zip_->OpenFile("DICOMDIR");
|
|
749 zip_->Write(s);
|
|
750 }
|
|
751 }
|
|
752 else
|
|
753 {
|
|
754 if (isMedia_)
|
|
755 {
|
|
756 assert(dicomDir_.get() != NULL);
|
|
757 commands_.Apply(*zip_, context_, index, *dicomDir_, MEDIA_IMAGES_FOLDER);
|
|
758 }
|
|
759 else
|
|
760 {
|
|
761 assert(dicomDir_.get() == NULL);
|
|
762 commands_.Apply(*zip_, context_, index);
|
|
763 }
|
|
764 }
|
|
765 }
|
|
766
|
|
767 unsigned int GetInstancesCount() const
|
|
768 {
|
|
769 return commands_.GetInstancesCount();
|
|
770 }
|
|
771
|
|
772 uint64_t GetUncompressedSize() const
|
|
773 {
|
|
774 return commands_.GetUncompressedSize();
|
|
775 }
|
|
776 };
|
|
777
|
|
778
|
|
779 ArchiveJob::ArchiveJob(boost::shared_ptr<TemporaryFile>& target,
|
|
780 ServerContext& context,
|
|
781 bool isMedia,
|
|
782 bool enableExtendedSopClass) :
|
|
783 target_(target),
|
|
784 context_(context),
|
|
785 archive_(new ArchiveIndex(ResourceType_Patient)), // root
|
|
786 isMedia_(isMedia),
|
|
787 enableExtendedSopClass_(enableExtendedSopClass),
|
|
788 currentStep_(0),
|
|
789 instancesCount_(0),
|
|
790 uncompressedSize_(0)
|
|
791 {
|
|
792 if (target.get() == NULL)
|
|
793 {
|
|
794 throw OrthancException(ErrorCode_NullPointer);
|
|
795 }
|
|
796 }
|
|
797
|
|
798
|
|
799 void ArchiveJob::AddResource(const std::string& publicId)
|
|
800 {
|
|
801 if (writer_.get() != NULL) // Already started
|
|
802 {
|
|
803 throw OrthancException(ErrorCode_BadSequenceOfCalls);
|
|
804 }
|
|
805
|
|
806 ResourceIdentifiers resource(context_.GetIndex(), publicId);
|
|
807 archive_->Add(context_.GetIndex(), resource);
|
|
808 }
|
|
809
|
|
810
|
|
811 void ArchiveJob::SignalResubmit()
|
|
812 {
|
|
813 LOG(ERROR) << "Cannot resubmit the creation of an archive";
|
|
814 throw OrthancException(ErrorCode_BadSequenceOfCalls);
|
|
815 }
|
|
816
|
|
817
|
|
818 void ArchiveJob::Start()
|
|
819 {
|
|
820 if (writer_.get() != NULL)
|
|
821 {
|
|
822 throw OrthancException(ErrorCode_BadSequenceOfCalls);
|
|
823 }
|
|
824
|
|
825 writer_.reset(new ZipWriterIterator(*target_, context_, *archive_,
|
|
826 isMedia_, enableExtendedSopClass_));
|
|
827
|
|
828 instancesCount_ = writer_->GetInstancesCount();
|
|
829 uncompressedSize_ = writer_->GetUncompressedSize();
|
|
830 }
|
|
831
|
|
832
|
|
833 JobStepResult ArchiveJob::ExecuteStep()
|
|
834 {
|
|
835 assert(writer_.get() != NULL);
|
|
836
|
|
837 if (target_.unique())
|
|
838 {
|
|
839 LOG(WARNING) << "A client has disconnected while creating an archive";
|
|
840 return JobStepResult::Failure(ErrorCode_NetworkProtocol);
|
|
841 }
|
|
842
|
|
843 if (writer_->GetStepsCount() == 0)
|
|
844 {
|
|
845 writer_.reset(NULL); // Flush all the results
|
|
846 return JobStepResult::Success();
|
|
847 }
|
|
848 else
|
|
849 {
|
|
850 writer_->RunStep(currentStep_);
|
|
851
|
|
852 currentStep_ ++;
|
|
853
|
|
854 if (currentStep_ == writer_->GetStepsCount())
|
|
855 {
|
|
856 writer_.reset(NULL); // Flush all the results
|
|
857 return JobStepResult::Success();
|
|
858 }
|
|
859 else
|
|
860 {
|
|
861 return JobStepResult::Continue();
|
|
862 }
|
|
863 }
|
|
864 }
|
|
865
|
|
866
|
|
867 float ArchiveJob::GetProgress()
|
|
868 {
|
|
869 if (writer_.get() == NULL ||
|
|
870 writer_->GetStepsCount() == 0)
|
|
871 {
|
|
872 return 1;
|
|
873 }
|
|
874 else
|
|
875 {
|
|
876 return (static_cast<float>(currentStep_) /
|
|
877 static_cast<float>(writer_->GetStepsCount() - 1));
|
|
878 }
|
|
879 }
|
|
880
|
|
881
|
|
882 void ArchiveJob::GetJobType(std::string& target)
|
|
883 {
|
|
884 if (isMedia_)
|
|
885 {
|
|
886 target = "Media";
|
|
887 }
|
|
888 else
|
|
889 {
|
|
890 target = "Archive";
|
|
891 }
|
|
892 }
|
|
893
|
|
894
|
|
895 void ArchiveJob::GetPublicContent(Json::Value& value)
|
|
896 {
|
|
897 value["Description"] = description_;
|
|
898 value["InstancesCount"] = instancesCount_;
|
|
899 value["UncompressedSizeMB"] =
|
|
900 static_cast<unsigned int>(uncompressedSize_ / (1024llu * 1024llu));
|
|
901 }
|
|
902
|
|
903
|
|
904 void ArchiveJob::GetInternalContent(Json::Value& value)
|
|
905 {
|
|
906 // TODO
|
|
907 }
|
|
908 }
|