1 /*
2 *
3 * Copyright (C) 2013-2017, OFFIS e.V.
4 * All rights reserved. See COPYRIGHT file for details.
5 *
6 * This software and supporting documentation were developed by
7 *
8 * OFFIS e.V.
9 * R&D Division Health
10 * Escherweg 2
11 * D-26121 Oldenburg, Germany
12 *
13 *
14 * Module: dcmnet
15 *
16 * Author: Joerg Riesmeier
17 *
18 * Purpose: DICOM Storage Service Class Provider (SCP)
19 *
20 */
21
22
23 #include "dcmtk/config/osconfig.h" /* make sure OS specific configuration is included first */
24
25 #include "dcmtk/dcmnet/dstorscp.h"
26 #include "dcmtk/dcmnet/diutil.h"
27
28
29 // constant definitions
30
31 const char *DcmStorageSCP::DEF_StandardSubdirectory = "data";
32 const char *DcmStorageSCP::DEF_UndefinedSubdirectory = "undef";
33 const char *DcmStorageSCP::DEF_FilenameExtension = "";
34
35
36 // implementation of the main interface class
37
DcmStorageSCP()38 DcmStorageSCP::DcmStorageSCP()
39 : DcmSCP(),
40 OutputDirectory(),
41 StandardSubdirectory(DEF_StandardSubdirectory),
42 UndefinedSubdirectory(DEF_UndefinedSubdirectory),
43 FilenameExtension(DEF_FilenameExtension),
44 DirectoryGeneration(DGM_Default),
45 FilenameGeneration(FGM_Default),
46 FilenameCreator(),
47 DatasetStorage(DSM_Default)
48 {
49 // make sure that the SCP at least supports C-ECHO with default transfer syntax
50 OFList<OFString> transferSyntaxes;
51 transferSyntaxes.push_back(UID_LittleEndianImplicitTransferSyntax);
52 addPresentationContext(UID_VerificationSOPClass, transferSyntaxes);
53 }
54
55
~DcmStorageSCP()56 DcmStorageSCP::~DcmStorageSCP()
57 {
58 // clear internal state
59 clear();
60 }
61
62
clear()63 void DcmStorageSCP::clear()
64 {
65 // DcmSCP::clear();
66 OutputDirectory.clear();
67 StandardSubdirectory = DEF_StandardSubdirectory;
68 UndefinedSubdirectory = DEF_UndefinedSubdirectory;
69 FilenameExtension = DEF_FilenameExtension;
70 DirectoryGeneration = DGM_Default;
71 FilenameGeneration = FGM_Default;
72 DatasetStorage = DSM_Default;
73 }
74
75
76 // get methods
77
getOutputDirectory() const78 const OFString &DcmStorageSCP::getOutputDirectory() const
79 {
80 return OutputDirectory;
81 }
82
83
getDirectoryGenerationMode() const84 DcmStorageSCP::E_DirectoryGenerationMode DcmStorageSCP::getDirectoryGenerationMode() const
85 {
86 return DirectoryGeneration;
87 }
88
89
getFilenameGenerationMode() const90 DcmStorageSCP::E_FilenameGenerationMode DcmStorageSCP::getFilenameGenerationMode() const
91 {
92 return FilenameGeneration;
93 }
94
95
getFilenameExtension() const96 const OFString &DcmStorageSCP::getFilenameExtension() const
97 {
98 return FilenameExtension;
99 }
100
101
getDatasetStorageMode() const102 DcmStorageSCP::E_DatasetStorageMode DcmStorageSCP::getDatasetStorageMode() const
103 {
104 return DatasetStorage;
105 }
106
107
108 // set methods
109
setOutputDirectory(const OFString & directory)110 OFCondition DcmStorageSCP::setOutputDirectory(const OFString &directory)
111 {
112 OFCondition status = EC_Normal;
113 if (directory.empty())
114 {
115 // empty directory refers to the current directory
116 if (OFStandard::isWriteable("."))
117 OutputDirectory.clear();
118 else
119 status = EC_DirectoryNotWritable;
120 } else {
121 // check whether given directory exists and is writable
122 if (OFStandard::dirExists(directory))
123 {
124 if (OFStandard::isWriteable(directory))
125 OFStandard::normalizeDirName(OutputDirectory, directory);
126 else
127 status = EC_DirectoryNotWritable;
128 } else
129 status = EC_DirectoryDoesNotExist;
130 }
131 return status;
132 }
133
134
setDirectoryGenerationMode(const E_DirectoryGenerationMode mode)135 void DcmStorageSCP::setDirectoryGenerationMode(const E_DirectoryGenerationMode mode)
136 {
137 DirectoryGeneration = mode;
138 }
139
140
setFilenameGenerationMode(const E_FilenameGenerationMode mode)141 void DcmStorageSCP::setFilenameGenerationMode(const E_FilenameGenerationMode mode)
142 {
143 FilenameGeneration = mode;
144 }
145
146
setFilenameExtension(const OFString & extension)147 void DcmStorageSCP::setFilenameExtension(const OFString &extension)
148 {
149 FilenameExtension = extension;
150 }
151
152
setDatasetStorageMode(const E_DatasetStorageMode mode)153 void DcmStorageSCP::setDatasetStorageMode(const E_DatasetStorageMode mode)
154 {
155 DatasetStorage = mode;
156 }
157
158
159 // further public methods
160
loadAssociationConfiguration(const OFString & filename,const OFString & profile)161 OFCondition DcmStorageSCP::loadAssociationConfiguration(const OFString &filename,
162 const OFString &profile)
163 {
164 // first, try to load the configuration file
165 OFCondition status = loadAssociationCfgFile(filename);
166 // and then, try to select the desired profile
167 if (status.good())
168 status = setAndCheckAssociationProfile(profile);
169 return status;
170 }
171
172
173 // protected methods
174
handleIncomingCommand(T_DIMSE_Message * incomingMsg,const DcmPresentationContextInfo & presInfo)175 OFCondition DcmStorageSCP::handleIncomingCommand(T_DIMSE_Message *incomingMsg,
176 const DcmPresentationContextInfo &presInfo)
177 {
178 OFCondition status = EC_IllegalParameter;
179 if (incomingMsg != NULL)
180 {
181 // check whether we've received a supported command
182 if (incomingMsg->CommandField == DIMSE_C_ECHO_RQ)
183 {
184 // handle incoming C-ECHO request
185 status = handleECHORequest(incomingMsg->msg.CEchoRQ, presInfo.presentationContextID);
186 }
187 else if (incomingMsg->CommandField == DIMSE_C_STORE_RQ)
188 {
189 // handle incoming C-STORE request
190 T_DIMSE_C_StoreRQ &storeReq = incomingMsg->msg.CStoreRQ;
191 Uint16 rspStatusCode = STATUS_STORE_Error_CannotUnderstand;
192 // special case: bit preserving mode
193 if (DatasetStorage == DGM_StoreBitPreserving)
194 {
195 OFString filename;
196 // generate filename with full path (and create subdirectories if needed)
197 status = generateSTORERequestFilename(storeReq, filename);
198 if (status.good())
199 {
200 if (OFStandard::fileExists(filename))
201 DCMNET_WARN("file already exists, overwriting: " << filename);
202 // receive dataset directly to file
203 status = receiveSTORERequest(storeReq, presInfo.presentationContextID, filename);
204 if (status.good())
205 {
206 // call the notification handler (default implementation outputs to the logger)
207 notifyInstanceStored(filename, storeReq.AffectedSOPClassUID, storeReq.AffectedSOPInstanceUID);
208 rspStatusCode = STATUS_Success;
209 }
210 }
211 } else {
212 DcmFileFormat fileformat;
213 DcmDataset *reqDataset = fileformat.getDataset();
214 // receive dataset in memory
215 status = receiveSTORERequest(storeReq, presInfo.presentationContextID, reqDataset);
216 if (status.good())
217 {
218 // do we need to store the received dataset at all?
219 if (DatasetStorage == DSM_Ignore)
220 {
221 // output debug message that dataset is not stored
222 DCMNET_DEBUG("received dataset is not stored since the storage mode is set to 'ignore'");
223 rspStatusCode = STATUS_Success;
224 } else {
225 // check and process C-STORE request
226 rspStatusCode = checkAndProcessSTORERequest(storeReq, fileformat);
227 }
228 }
229 }
230 // send C-STORE response (with DIMSE status code)
231 if (status.good())
232 status = sendSTOREResponse(presInfo.presentationContextID, storeReq, rspStatusCode);
233 else if (status == DIMSE_OUTOFRESOURCES)
234 {
235 // do not overwrite the previous error status
236 sendSTOREResponse(presInfo.presentationContextID, storeReq, STATUS_STORE_Refused_OutOfResources);
237 }
238 } else {
239 // unsupported command
240 OFString tempStr;
241 DCMNET_ERROR("cannot handle this kind of DIMSE command (0x"
242 << STD_NAMESPACE hex << STD_NAMESPACE setfill('0') << STD_NAMESPACE setw(4)
243 << OFstatic_cast(unsigned int, incomingMsg->CommandField)
244 << "), we are a Storage SCP only");
245 DCMNET_DEBUG(DIMSE_dumpMessage(tempStr, *incomingMsg, DIMSE_INCOMING));
246 // TODO: provide more information on this error?
247 status = DIMSE_BADCOMMANDTYPE;
248 }
249 }
250 return status;
251 }
252
253
checkAndProcessSTORERequest(const T_DIMSE_C_StoreRQ & reqMessage,DcmFileFormat & fileformat)254 Uint16 DcmStorageSCP::checkAndProcessSTORERequest(const T_DIMSE_C_StoreRQ &reqMessage,
255 DcmFileFormat &fileformat)
256 {
257 DCMNET_DEBUG("checking and processing C-STORE request");
258 Uint16 statusCode = STATUS_STORE_Error_CannotUnderstand;
259 DcmDataset *dataset = fileformat.getDataset();
260 // perform some basic checks on the request dataset
261 if ((dataset != NULL) && !dataset->isEmpty())
262 {
263 OFString filename;
264 OFString directoryName;
265 OFString sopClassUID = reqMessage.AffectedSOPClassUID;
266 OFString sopInstanceUID = reqMessage.AffectedSOPInstanceUID;
267 // generate filename with full path
268 OFCondition status = generateDirAndFilename(filename, directoryName, sopClassUID, sopInstanceUID, dataset);
269 if (status.good())
270 {
271 DCMNET_DEBUG("generated filename for received object: " << filename);
272 // create the output directory (if needed)
273 status = OFStandard::createDirectory(directoryName, OutputDirectory /* rootDir */);
274 if (status.good())
275 {
276 if (OFStandard::fileExists(filename))
277 DCMNET_WARN("file already exists, overwriting: " << filename);
278 // store the received dataset to file (with default settings)
279 status = fileformat.saveFile(filename);
280 if (status.good())
281 {
282 // call the notification handler (default implementation outputs to the logger)
283 notifyInstanceStored(filename, sopClassUID, sopInstanceUID, dataset);
284 statusCode = STATUS_Success;
285 } else {
286 DCMNET_ERROR("cannot store received object: " << filename << ": " << status.text());
287 statusCode = STATUS_STORE_Refused_OutOfResources;
288
289 // delete incomplete file
290 OFStandard::deleteFile(filename);
291 }
292 } else {
293 DCMNET_ERROR("cannot create directory for received object: " << directoryName << ": " << status.text());
294 statusCode = STATUS_STORE_Refused_OutOfResources;
295 }
296 } else
297 DCMNET_ERROR("cannot generate directory or file name for received object: " << status.text());
298 }
299 return statusCode;
300 }
301
302
generateSTORERequestFilename(const T_DIMSE_C_StoreRQ & reqMessage,OFString & filename)303 OFCondition DcmStorageSCP::generateSTORERequestFilename(const T_DIMSE_C_StoreRQ &reqMessage,
304 OFString &filename)
305 {
306 OFString directoryName;
307 OFString sopClassUID = reqMessage.AffectedSOPClassUID;
308 OFString sopInstanceUID = reqMessage.AffectedSOPInstanceUID;
309 // generate filename (with full path)
310 OFCondition status = generateDirAndFilename(filename, directoryName, sopClassUID, sopInstanceUID);
311 if (status.good())
312 {
313 DCMNET_DEBUG("generated filename for object to be received: " << filename);
314 // create the output directory (if needed)
315 status = OFStandard::createDirectory(directoryName, OutputDirectory /* rootDir */);
316 if (status.bad())
317 DCMNET_ERROR("cannot create directory for object to be received: " << directoryName << ": " << status.text());
318 } else
319 DCMNET_ERROR("cannot generate directory or file name for object to be received: " << status.text());
320 return status;
321 }
322
323
notifyInstanceStored(const OFString & filename,const OFString &,const OFString &,DcmDataset *) const324 void DcmStorageSCP::notifyInstanceStored(const OFString &filename,
325 const OFString & /*sopClassUID*/,
326 const OFString & /*sopInstanceUID*/,
327 DcmDataset * /*dataset*/) const
328 {
329 // by default, output some useful information
330 DCMNET_INFO("Stored received object to file: " << filename);
331 }
332
333
generateDirAndFilename(OFString & filename,OFString & directoryName,OFString & sopClassUID,OFString & sopInstanceUID,DcmDataset * dataset)334 OFCondition DcmStorageSCP::generateDirAndFilename(OFString &filename,
335 OFString &directoryName,
336 OFString &sopClassUID,
337 OFString &sopInstanceUID,
338 DcmDataset *dataset)
339 {
340 OFCondition status = EC_Normal;
341 // get SOP class and instance UID (if not yet known from the command set)
342 if (dataset != NULL)
343 {
344 if (sopClassUID.empty())
345 dataset->findAndGetOFString(DCM_SOPClassUID, sopClassUID);
346 if (sopInstanceUID.empty())
347 dataset->findAndGetOFString(DCM_SOPInstanceUID, sopInstanceUID);
348 }
349 // generate directory name
350 OFString generatedDirName;
351 switch (DirectoryGeneration)
352 {
353 case DGM_NoSubdirectory:
354 // do nothing (default)
355 break;
356 // use series date (if available) for subdirectory structure
357 case DGM_SeriesDate:
358 if (dataset != NULL)
359 {
360 OFString seriesDate;
361 DcmElement *element = NULL;
362 // try to get the series date from the dataset
363 if (dataset->findAndGetElement(DCM_SeriesDate, element).good() && (element->ident() == EVR_DA))
364 {
365 OFString dateValue;
366 DcmDate *dateElement = OFstatic_cast(DcmDate *, element);
367 // output ISO format is: YYYY-MM-DD
368 if (dateElement->getISOFormattedDate(dateValue).good() && (dateValue.length() == 10))
369 {
370 OFOStringStream stream;
371 stream << StandardSubdirectory << PATH_SEPARATOR
372 << dateValue.substr(0, 4) << PATH_SEPARATOR
373 << dateValue.substr(5 ,2) << PATH_SEPARATOR
374 << dateValue.substr(8, 2) << OFStringStream_ends;
375 OFSTRINGSTREAM_GETSTR(stream, tmpString)
376 generatedDirName = tmpString;
377 OFSTRINGSTREAM_FREESTR(tmpString);
378 }
379 }
380 // alternatively, if that fails, use the current system date
381 if (generatedDirName.empty())
382 {
383 OFString currentDate;
384 status = DcmDate::getCurrentDate(currentDate);
385 if (status.good())
386 {
387 OFOStringStream stream;
388 stream << UndefinedSubdirectory << PATH_SEPARATOR
389 << currentDate << OFStringStream_ends;
390 OFSTRINGSTREAM_GETSTR(stream, tmpString)
391 generatedDirName = tmpString;
392 OFSTRINGSTREAM_FREESTR(tmpString);
393 }
394 }
395 } else {
396 DCMNET_DEBUG("received dataset is not available in order to determine the SeriesDate "
397 << DCM_SeriesDate << ", are you using the bit preserving mode?");
398 // no DICOM dataset given, so we cannot determine the series date
399 status = EC_CouldNotGenerateDirectoryName;
400 }
401 break;
402 }
403 if (status.good())
404 {
405 // combine the generated directory name with the output directory
406 OFStandard::combineDirAndFilename(directoryName, OutputDirectory, generatedDirName);
407 // generate filename
408 OFString generatedFileName;
409 switch (FilenameGeneration)
410 {
411 // use modality prefix and SOP instance UID (default)
412 case FGM_SOPInstanceUID:
413 {
414 if (sopClassUID.empty())
415 status = NET_EC_InvalidSOPClassUID;
416 else if (sopInstanceUID.empty())
417 status = NET_EC_InvalidSOPInstanceUID;
418 else {
419 OFOStringStream stream;
420 stream << dcmSOPClassUIDToModality(sopClassUID.c_str(), "UNKNOWN")
421 << '.' << sopInstanceUID << FilenameExtension << OFStringStream_ends;
422 OFSTRINGSTREAM_GETSTR(stream, tmpString)
423 generatedFileName = tmpString;
424 OFSTRINGSTREAM_FREESTR(tmpString);
425 // combine the generated file name with the directory name
426 OFStandard::combineDirAndFilename(filename, directoryName, generatedFileName);
427 }
428 break;
429 }
430 // unique filename based on modality prefix and newly generated UID
431 case FGM_UniqueFromNewUID:
432 {
433 char uidBuffer[70];
434 dcmGenerateUniqueIdentifier(uidBuffer);
435 OFOStringStream stream;
436 stream << dcmSOPClassUIDToModality(sopClassUID.c_str(), "UNKNOWN")
437 << ".X." << uidBuffer << FilenameExtension << OFStringStream_ends;
438 OFSTRINGSTREAM_GETSTR(stream, tmpString)
439 generatedFileName = tmpString;
440 OFSTRINGSTREAM_FREESTR(tmpString);
441 // combine the generated file name with the directory name
442 OFStandard::combineDirAndFilename(filename, directoryName, generatedFileName);
443 break;
444 }
445 // unique pseudo-random filename (also checks for existing files, so we need some special handling)
446 case FGM_ShortUniquePseudoRandom:
447 {
448 OFString prefix = dcmSOPClassUIDToModality(sopClassUID.c_str(), "XX");
449 prefix += '_';
450 // TODO: we might want to use a more appropriate seed value
451 unsigned int seed = OFstatic_cast(unsigned int, time(NULL));
452 if (!FilenameCreator.makeFilename(seed, directoryName.c_str(), prefix.c_str(), FilenameExtension.c_str(), filename))
453 status = EC_CouldNotGenerateFilename;
454 break;
455 }
456 // use current system time and modality suffix for filename
457 case FGM_CurrentSystemTime:
458 {
459 OFString timeStamp;
460 // get the date/time as: YYYYMMDDHHMMSS.FFFFFF
461 if (DcmDateTime::getCurrentDateTime(timeStamp, OFTrue /* seconds */, OFTrue /* fraction */).good())
462 {
463 OFOStringStream stream;
464 stream << timeStamp << '.' << dcmSOPClassUIDToModality(sopClassUID.c_str(), "UNKNOWN")
465 << FilenameExtension << OFStringStream_ends;
466 OFSTRINGSTREAM_GETSTR(stream, tmpString)
467 generatedFileName = tmpString;
468 OFSTRINGSTREAM_FREESTR(tmpString);
469 // combine the generated file name
470 OFStandard::combineDirAndFilename(filename, directoryName, generatedFileName);
471 } else
472 status = EC_CouldNotGenerateFilename;
473 break;
474 }
475
476 }
477 }
478 return status;
479 }
480