comparison OrthancServer/ServerJobs/ArchiveJob.cpp @ 2632:2406ae891747 jobs

ArchiveJob
author Sebastien Jodogne <s.jodogne@gmail.com>
date Fri, 25 May 2018 18:09:27 +0200
parents
children c72eb844758c
comparison
equal deleted inserted replaced
2631:a963b5e2d963 2632:2406ae891747
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 }