diff --git a/doc/dependency_decisions.yml b/doc/dependency_decisions.yml index 1fe363ead..accaf6c52 100644 --- a/doc/dependency_decisions.yml +++ b/doc/dependency_decisions.yml @@ -18,7 +18,7 @@ - :who: mocsharp :why: Apache-2.0 (https://github.com/aws/aws-sdk-net/raw/master/License.txt) :versions: - - 3.7.12.26 + - 3.7.13.8 :when: 2022-09-01 23:05:22.203089302 Z - - :approve - AWSSDK.S3 @@ -32,7 +32,7 @@ - :who: mocsharp :why: Apache-2.0 (https://github.com/aws/aws-sdk-net/raw/master/License.txt) :versions: - - 3.7.1.190 + - 3.7.1.203 :when: 2022-09-01 23:05:28.920368903 Z - - :approve - BoDi @@ -851,35 +851,35 @@ - :who: mocsharp :why: Apache-2.0 (https://github.com/Project-MONAI/monai-deploy-messaging/raw/main/LICENSE) :versions: - - 0.1.4 + - 0.1.5 :when: 2022-08-16 23:06:21.051573547 Z - - :approve - Monai.Deploy.Messaging.RabbitMQ - :who: mocsharp :why: Apache-2.0 (https://github.com/Project-MONAI/monai-deploy-messaging/raw/main/LICENSE) :versions: - - 0.1.4 + - 0.1.5 :when: 2022-08-16 23:06:21.511789690 Z - - :approve - Monai.Deploy.Storage - :who: mocsharp :why: Apache-2.0 (https://github.com/Project-MONAI/monai-deploy-storage/raw/main/LICENSE) :versions: - - 0.2.5 + - 0.2.7 :when: 2022-08-16 23:06:21.988183476 Z - - :approve - Monai.Deploy.Storage.MinIO - :who: mocsharp :why: Apache-2.0 (https://github.com/Project-MONAI/monai-deploy-storage/raw/main/LICENSE) :versions: - - 0.2.5 + - 0.2.7 :when: 2022-08-16 23:06:22.426838304 Z - - :approve - Monai.Deploy.Storage.S3Policy - :who: mocsharp :why: Apache-2.0 (https://github.com/Project-MONAI/monai-deploy-storage/raw/main/LICENSE) :versions: - - 0.2.5 + - 0.2.7 :when: 2022-08-16 23:06:22.881956546 Z - - :approve - Moq @@ -1878,6 +1878,7 @@ - :who: mocsharp :why: Microsoft Public License (https://github.com/fo-dicom/fo-dicom/raw/development/License.txt) :versions: + - 5.0.2 - 5.0.3 :when: 2022-08-16 23:07:29.574869349 Z - - :approve diff --git a/docs/changelog.md b/docs/changelog.md index bd3dde2a1..86b9fdcd4 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -16,6 +16,15 @@ # Changelog + +## 0.3.1 + +[GitHub Milestone 0.3.0](https://github.com/Project-MONAI/monai-deploy-informatics-gateway/milestone/7) + +- The SCU AE Title is now uppercase MONAISCU. +- Update fo-dicom to 5.0.3 +- Defaults temporary storage to use disk with ability to switch to memory. + ## 0.3.0 [GitHub Milestone 0.3.0](https://github.com/Project-MONAI/monai-deploy-informatics-gateway/milestone/3) @@ -36,7 +45,7 @@ any pending files. Files that were successfully uploaded to the temporary location (`storage>temporary`) in the bucket (`storage>temporaryBucketName`) are then moved to the payload bucket (`storage>bucketName`) before sending a workflow request. - Breaking changes in the storage configuration to allow dynamic key-value pairs. -- Breaking changes to enable dynamic loadig of the storage & the messaging libraries. +- Breaking changes to enable dynamic loading of the storage & the messaging libraries. ## 0.1.1 diff --git a/docs/compliance/third-party-licenses.md b/docs/compliance/third-party-licenses.md index ab8744fbc..f390f291f 100644 --- a/docs/compliance/third-party-licenses.md +++ b/docs/compliance/third-party-licenses.md @@ -60,15 +60,15 @@ SOFTWARE.
-AWSSDK.Core 3.7.12.26 +AWSSDK.Core 3.7.13.8 ## AWSSDK.Core -- Version: 3.7.12.26 +- Version: 3.7.13.8 - Authors: Amazon Web Services - Owners: Amazon Web Services - Project URL: https://github.com/aws/aws-sdk-net/ -- Source: [NuGet](https://www.nuget.org/packages/AWSSDK.Core/3.7.12.26) +- Source: [NuGet](https://www.nuget.org/packages/AWSSDK.Core/3.7.13.8) - License: [Apache-2.0](https://github.com/aws/aws-sdk-net/raw/master/License.txt) @@ -206,15 +206,15 @@ END OF TERMS AND CONDITIONS
-AWSSDK.SecurityToken 3.7.1.190 +AWSSDK.SecurityToken 3.7.1.203 ## AWSSDK.SecurityToken -- Version: 3.7.1.190 +- Version: 3.7.1.203 - Authors: Amazon Web Services - Owners: Amazon Web Services - Project URL: https://github.com/aws/aws-sdk-net/ -- Source: [NuGet](https://www.nuget.org/packages/AWSSDK.SecurityToken/3.7.1.190) +- Source: [NuGet](https://www.nuget.org/packages/AWSSDK.SecurityToken/3.7.1.203) - License: [Apache-2.0](https://github.com/aws/aws-sdk-net/raw/master/License.txt) @@ -6580,11 +6580,10 @@ SOFTWARE. ## Microsoft.Toolkit.HighPerformance - Version: 7.1.2 -- Authors: Microsoft -- Owners: Microsoft.Toolkit,dotnetfoundation -- Project URL: https://dot.net/ +- Authors: Microsoft.Toolkit +- Project URL: https://github.com/CommunityToolkit/WindowsCommunityToolkit - Source: [NuGet](https://www.nuget.org/packages/Microsoft.Toolkit.HighPerformance/7.1.2) -- License: [MIT]( https://github.com/CommunityToolkit/WindowsCommunityToolkit/raw/main/License.md) +- License: [MIT](https://github.com/CommunityToolkit/WindowsCommunityToolkit/raw/main/License.md) ``` @@ -6596,19 +6595,16 @@ All rights reserved. ## MIT License (MIT) -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), -to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, -and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS -IN THE SOFTWARE. +THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ```
+ +
Microsoft.Win32.Primitives 4.3.0 @@ -7290,14 +7286,14 @@ Apache License
-Monai.Deploy.Messaging 0.1.4 +Monai.Deploy.Messaging 0.1.5 ## Monai.Deploy.Messaging -- Version: 0.1.4 +- Version: 0.1.5 - Authors: MONAI Consortium - Project URL: https://github.com/Project-MONAI/monai-deploy-messaging -- Source: [NuGet](https://www.nuget.org/packages/Monai.Deploy.Messaging/0.1.4) +- Source: [NuGet](https://www.nuget.org/packages/Monai.Deploy.Messaging/0.1.5) - License: [Apache-2.0](https://github.com/Project-MONAI/monai-deploy-messaging/raw/main/LICENSE) @@ -7518,14 +7514,14 @@ By downloading this software, you agree to the license terms & all licenses list
-Monai.Deploy.Messaging.RabbitMQ 0.1.4 +Monai.Deploy.Messaging.RabbitMQ 0.1.5 ## Monai.Deploy.Messaging.RabbitMQ -- Version: 0.1.4 +- Version: 0.1.5 - Authors: MONAI Consortium - Project URL: https://github.com/Project-MONAI/monai-deploy-messaging -- Source: [NuGet](https://www.nuget.org/packages/Monai.Deploy.Messaging.RabbitMQ/0.1.4) +- Source: [NuGet](https://www.nuget.org/packages/Monai.Deploy.Messaging.RabbitMQ/0.1.5) - License: [Apache-2.0](https://github.com/Project-MONAI/monai-deploy-messaging/raw/main/LICENSE) @@ -7746,14 +7742,14 @@ By downloading this software, you agree to the license terms & all licenses list
-Monai.Deploy.Storage 0.2.5 +Monai.Deploy.Storage 0.2.7 ## Monai.Deploy.Storage -- Version: 0.2.5 +- Version: 0.2.7 - Authors: MONAI Consortium - Project URL: https://github.com/Project-MONAI/monai-deploy-storage -- Source: [NuGet](https://www.nuget.org/packages/Monai.Deploy.Storage/0.2.5) +- Source: [NuGet](https://www.nuget.org/packages/Monai.Deploy.Storage/0.2.7) - License: [Apache-2.0](https://github.com/Project-MONAI/monai-deploy-storage/raw/main/LICENSE) @@ -7974,14 +7970,14 @@ By downloading this software, you agree to the license terms & all licenses list
-Monai.Deploy.Storage.MinIO 0.2.5 +Monai.Deploy.Storage.MinIO 0.2.7 ## Monai.Deploy.Storage.MinIO -- Version: 0.2.5 +- Version: 0.2.7 - Authors: MONAI Consortium - Project URL: https://github.com/Project-MONAI/monai-deploy-storage -- Source: [NuGet](https://www.nuget.org/packages/Monai.Deploy.Storage.MinIO/0.2.5) +- Source: [NuGet](https://www.nuget.org/packages/Monai.Deploy.Storage.MinIO/0.2.7) - License: [Apache-2.0](https://github.com/Project-MONAI/monai-deploy-storage/raw/main/LICENSE) @@ -8202,14 +8198,14 @@ By downloading this software, you agree to the license terms & all licenses list
-Monai.Deploy.Storage.S3Policy 0.2.5 +Monai.Deploy.Storage.S3Policy 0.2.7 ## Monai.Deploy.Storage.S3Policy -- Version: 0.2.5 +- Version: 0.2.7 - Authors: MONAI Consortium - Project URL: https://github.com/Project-MONAI/monai-deploy-storage -- Source: [NuGet](https://www.nuget.org/packages/Monai.Deploy.Storage.S3Policy/0.2.5) +- Source: [NuGet](https://www.nuget.org/packages/Monai.Deploy.Storage.S3Policy/0.2.7) - License: [Apache-2.0](https://github.com/Project-MONAI/monai-deploy-storage/raw/main/LICENSE) @@ -9230,22 +9226,22 @@ resources legal and licencing contributors - +  Please note: our license is an adaptation of the MIT X11 License and should be read as such. Please also note: this licensing model is made possible through funding from donations and the sale of support contracts. License Copyright (c) 2000 - 2017 The Legion of the Bouncy Castle Inc. (http://www.bouncycastle.org) - Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - + +  - +  Site hosted by Tau Ceti Co-operative Ltd. @@ -27550,6 +27546,311 @@ SOFTWARE.
+
+fo-dicom 5.0.2 + +## fo-dicom + +- Version: 5.0.2 +- Authors: fo-dicom contributors +- Project URL: https://github.com/fo-dicom/fo-dicom +- Source: [NuGet](https://www.nuget.org/packages/fo-dicom/5.0.2) +- License: [Microsoft Public License](https://github.com/fo-dicom/fo-dicom/raw/development/License.txt) + + +``` +Fellow Oak DICOM + +Copyright (c) 2012-2021 fo-dicom contributors + +This software is licensed under the Microsoft Public License (MS-PL) + +Microsoft Public License (MS-PL) + +This license governs use of the accompanying software. If you use the software, you +accept this license. If you do not accept the license, do not use the software. + +1. Definitions +The terms "reproduce," "reproduction," "derivative works," and "distribution" have the +same meaning here as under U.S. copyright law. +A "contribution" is the original software, or any additions or changes to the software. +A "contributor" is any person that distributes its contribution under this license. +"Licensed patents" are a contributor's patent claims that read directly on its contribution. + +2. Grant of Rights +(A) Copyright Grant- Subject to the terms of this license, including the license conditions + and limitations in section 3, each contributor grants you a non-exclusive, worldwide, + royalty-free copyright license to reproduce its contribution, prepare derivative works + of its contribution, and distribute its contribution or any derivative works that you create. +(B) Patent Grant- Subject to the terms of this license, including the license conditions and + limitations in section 3, each contributor grants you a non-exclusive, worldwide, royalty-free + license under its licensed patents to make, have made, use, sell, offer for sale, import, + and/or otherwise dispose of its contribution in the software or derivative works of the + contribution in the software. + +3. Conditions and Limitations +(A) No Trademark License- This license does not grant you rights to use any contributors' name, + logo, or trademarks. +(B) If you bring a patent claim against any contributor over patents that you claim are infringed + by the software, your patent license from such contributor to the software ends automatically. +(C) If you distribute any portion of the software, you must retain all copyright, patent, trademark, + and attribution notices that are present in the software. +(D) If you distribute any portion of the software in source code form, you may do so only under this + license by including a complete copy of this license with your distribution. If you distribute + any portion of the software in compiled or object code form, you may only do so under a license + that complies with this license. +(E) The software is licensed "as-is." You bear the risk of using it. The contributors give no express + warranties, guarantees or conditions. You may have additional consumer rights under your local + laws which this license cannot change. To the extent permitted under your local laws, the + contributors exclude the implied warranties of merchantability, fitness for a particular purpose + and non-infringement. + + + +---- libijg (from DCMTK 3.5.4 COPYRIGHT) ---- + +Unless otherwise specified, the DCMTK software package has the +following copyright: + +/* + * Copyright (C) 1994-2004, OFFIS + * + * This software and supporting documentation were developed by + * + * Kuratorium OFFIS e.V. + * Healthcare Information and Communication Systems + * Escherweg 2 + * D-26121 Oldenburg, Germany + * + * THIS SOFTWARE IS MADE AVAILABLE, AS IS, AND OFFIS MAKES NO WARRANTY + * REGARDING THE SOFTWARE, ITS PERFORMANCE, ITS MERCHANTABILITY OR + * FITNESS FOR ANY PARTICULAR USE, FREEDOM FROM ANY COMPUTER DISEASES OR + * ITS CONFORMITY TO ANY SPECIFICATION. THE ENTIRE RISK AS TO QUALITY AND + * PERFORMANCE OF THE SOFTWARE IS WITH THE USER. + * + * Copyright of the software and supporting documentation is, unless + * otherwise stated, owned by OFFIS, and free access is hereby granted as + * a license to use this software, copy this software and prepare + * derivative works based upon this software. However, any distribution + * of this software source code or supporting documentation or derivative + * works (source code and supporting documentation) must include the + * three paragraphs of this copyright notice. + * + */ + +The dcmjpeg sub-package includes an adapted version of the Independent JPEG +Group Toolkit Version 6b, which is contained in dcmjpeg/libijg8, +dcmjpeg/libijg12 and dcmjpeg/libijg16. This toolkit is covered by the +following copyright. The original README file for the Independent JPEG +Group Toolkit is located in dcmjpeg/docs/ijg_readme.txt. + +/* + * The authors make NO WARRANTY or representation, either express or implied, + * with respect to this software, its quality, accuracy, merchantability, or + * fitness for a particular purpose. This software is provided "AS IS", and you, + * its user, assume the entire risk as to its quality and accuracy. + * + * This software is copyright (C) 1991-1998, Thomas G. Lane. + * All Rights Reserved except as specified below. + * + * Permission is hereby granted to use, copy, modify, and distribute this + * software (or portions thereof) for any purpose, without fee, subject to these + * conditions: + * (1) If any part of the source code for this software is distributed, then this + * README file must be included, with this copyright and no-warranty notice + * unaltered; and any additions, deletions, or changes to the original files + * must be clearly indicated in accompanying documentation. + * (2) If only executable code is distributed, then the accompanying + * documentation must state that "this software is based in part on the work of + * the Independent JPEG Group". + * (3) Permission for use of this software is granted only if the user accepts + * full responsibility for any undesirable consequences; the authors accept + * NO LIABILITY for damages of any kind. + * + * These conditions apply to any software derived from or based on the IJG code, + * not just to the unmodified library. If you use our work, you ought to + * acknowledge us. + * + * Permission is NOT granted for the use of any IJG author's name or company name + * in advertising or publicity relating to this software or products derived from + * it. This software may be referred to only as "the Independent JPEG Group's + * software". + * + * We specifically permit and encourage the use of this software as the basis of + * commercial products, provided that all warranty or liability claims are + * assumed by the product vendor. + */ + + + +---- OpenJPEG JPEG 2000 codec (from license.txt) ---- + +/* + * Copyright (c) 2002-2007, Communications and Remote Sensing Laboratory, Universite catholique de Louvain (UCL), Belgium + * Copyright (c) 2002-2007, Professor Benoit Macq + * Copyright (c) 2001-2003, David Janssens + * Copyright (c) 2002-2003, Yannick Verschueren + * Copyright (c) 2003-2007, Francois-Olivier Devaux and Antonin Descampe + * Copyright (c) 2005, Herve Drolon, FreeImage Team + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS `AS IS' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + + + +---- CharLS JPEG-LS codec (from License.txt) ---- + +Copyright (c) 2007-2009, Jan de Vaan +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of my employer, nor the names of its contributors may be + used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + + +---- Unity.IO.Compression (from LICENSE.TXT and PATENTS.TXT) ---- + +The MIT License (MIT) + +Copyright (c) Microsoft Corporation + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +Microsoft Patent Promise for .NET Libraries and Runtime Components + +Microsoft Corporation and its affiliates ("Microsoft") promise not to assert +any .NET Patents against you for making, using, selling, offering for sale, +importing, or distributing Covered Code, as part of either a .NET Runtime or +as part of any application designed to run on a .NET Runtime. + +If you file, maintain, or voluntarily participate in any claim in a lawsuit +alleging direct or contributory patent infringement by any Covered Code, or +inducement of patent infringement by any Covered Code, then your rights under +this promise will automatically terminate. + +This promise is not an assurance that (i) any .NET Patents are valid or +enforceable, or (ii) Covered Code does not infringe patents or other +intellectual property rights of any third party. No rights except those +expressly stated in this promise are granted, waived, or received by +Microsoft, whether by implication, exhaustion, estoppel, or otherwise. +This is a personal promise directly from Microsoft to you, and you agree as a +condition of benefiting from it that no Microsoft rights are received from +suppliers, distributors, or otherwise from any other person in connection with +this promise. + +Definitions: + +"Covered Code" means those Microsoft .NET libraries and runtime components as +made available by Microsoft at https://github.com/Microsoft/referencesource. + +".NET Patents" are those patent claims, both currently owned by Microsoft and +acquired in the future, that are necessarily infringed by Covered Code. .NET +Patents do not include any patent claims that are infringed by any Enabling +Technology, that are infringed only as a consequence of modification of +Covered Code, or that are infringed only by the combination of Covered Code +with third party code. + +".NET Runtime" means any compliant implementation in software of (a) all of +the required parts of the mandatory provisions of Standard ECMA-335 – Common +Language Infrastructure (CLI); and (b) if implemented, any additional +functionality in Microsoft's .NET Framework, as described in Microsoft's API +documentation on its MSDN website. For example, .NET Runtimes include +Microsoft's .NET Framework and those portions of the Mono Project compliant +with (a) and (b). + +"Enabling Technology" means underlying or enabling technology that may be +used, combined, or distributed in connection with Microsoft's .NET Framework +or other .NET Runtimes, such as hardware, operating systems, and applications +that run on .NET Framework or other .NET Runtimes. + + + +---- Nito.AsyncEx (from LICENSE.TXT) ---- + +The MIT License (MIT) + +Copyright (c) 2014 StephenCleary + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +``` + +
+ +
fo-dicom 5.0.3 @@ -27584,13 +27885,13 @@ A "contributor" is any person that distributes its contribution under this licen 2. Grant of Rights (A) Copyright Grant- Subject to the terms of this license, including the license conditions and limitations in section 3, each contributor grants you a non-exclusive, worldwide, - royalty-free copyright license to reproduce its contribution, prepare derivative works - of its contribution, and distribute its contribution or any derivative works that you create. + royalty-free copyright license to reproduce its contribution, prepare derivative works + of its contribution, and distribute its contribution or any derivative works that you create. (B) Patent Grant- Subject to the terms of this license, including the license conditions and limitations in section 3, each contributor grants you a non-exclusive, worldwide, royalty-free - license under its licensed patents to make, have made, use, sell, offer for sale, import, - and/or otherwise dispose of its contribution in the software or derivative works of the - contribution in the software. + license under its licensed patents to make, have made, use, sell, offer for sale, import, + and/or otherwise dispose of its contribution in the software or derivative works of the + contribution in the software. 3. Conditions and Limitations (A) No Trademark License- This license does not grant you rights to use any contributors' name, @@ -27601,13 +27902,13 @@ A "contributor" is any person that distributes its contribution under this licen and attribution notices that are present in the software. (D) If you distribute any portion of the software in source code form, you may do so only under this license by including a complete copy of this license with your distribution. If you distribute - any portion of the software in compiled or object code form, you may only do so under a license - that complies with this license. + any portion of the software in compiled or object code form, you may only do so under a license + that complies with this license. (E) The software is licensed "as-is." You bear the risk of using it. The contributors give no express warranties, guarantees or conditions. You may have additional consumer rights under your local - laws which this license cannot change. To the extent permitted under your local laws, the - contributors exclude the implied warranties of merchantability, fitness for a particular purpose - and non-infringement. + laws which this license cannot change. To the extent permitted under your local laws, the + contributors exclude the implied warranties of merchantability, fitness for a particular purpose + and non-infringement. diff --git a/src/Api/Monai.Deploy.InformaticsGateway.Api.csproj b/src/Api/Monai.Deploy.InformaticsGateway.Api.csproj index 332e40ae6..9de708ae6 100644 --- a/src/Api/Monai.Deploy.InformaticsGateway.Api.csproj +++ b/src/Api/Monai.Deploy.InformaticsGateway.Api.csproj @@ -30,8 +30,8 @@ - - + + diff --git a/src/Api/Storage/Payload.cs b/src/Api/Storage/Payload.cs index d0fcb4e3e..af584bb20 100644 --- a/src/Api/Storage/Payload.cs +++ b/src/Api/Storage/Payload.cs @@ -66,6 +66,8 @@ public enum PayloadState public bool HasTimedOut { get => ElapsedTime().TotalSeconds >= Timeout; } + public TimeSpan Elapsed { get { return DateTime.UtcNow.Subtract(DateTimeCreated); } } + public string CallingAeTitle { get => Files.OfType().Select(p => p.CallingAeTitle).FirstOrDefault(); } public string CalledAeTitle { get => Files.OfType().Select(p => p.CalledAeTitle).FirstOrDefault(); } diff --git a/src/Api/Storage/StorageObjectMetadata.cs b/src/Api/Storage/StorageObjectMetadata.cs index 7e17289da..fae88b73a 100644 --- a/src/Api/Storage/StorageObjectMetadata.cs +++ b/src/Api/Storage/StorageObjectMetadata.cs @@ -16,6 +16,7 @@ using System; using System.IO; +using System.Runtime; using System.Text.Json.Serialization; using Ardalis.GuardClauses; @@ -123,9 +124,27 @@ public void SetUploaded(string bucketName) if (Data is not null && Data.CanSeek) { - Data.Close(); - Data.Dispose(); - Data = null; + if (Data is FileStream fileStream) + { + var filename = fileStream.Name; + Data.Close(); + Data.Dispose(); + Data = null; + System.IO.File.Delete(filename); + } + else // MemoryStream + { + Data.Close(); + Data.Dispose(); + Data = null; + + // When IG stores all received/downloaded data in-memory using MemoryStream, LOH grows tremendously and thus impacts the performance and + // memory usage. The following makes sure LOH is compacted after the data is uploaded. + GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce; +#pragma warning disable S1215 // "GC.Collect" should not be called + GC.Collect(); +#pragma warning restore S1215 // "GC.Collect" should not be called + } } } diff --git a/src/Configuration/ConfigurationValidator.cs b/src/Configuration/ConfigurationValidator.cs index bd8c18db2..86d7cb51d 100644 --- a/src/Configuration/ConfigurationValidator.cs +++ b/src/Configuration/ConfigurationValidator.cs @@ -17,6 +17,8 @@ using System; using System.Collections.Generic; +using System.IO; +using System.IO.Abstractions; using System.Text.RegularExpressions; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -29,6 +31,7 @@ namespace Monai.Deploy.InformaticsGateway.Configuration public class ConfigurationValidator : IValidateOptions { private readonly ILogger _logger; + private readonly IFileSystem _fileSystem; private readonly List _validationErrors; /// @@ -36,9 +39,10 @@ public class ConfigurationValidator : IValidateOptions /// InformaticsGatewayConfiguration to be validated /// Logger to be used by ConfigurationValidator - public ConfigurationValidator(ILogger logger) + public ConfigurationValidator(ILogger logger, IFileSystem fileSystem) { _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _fileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem)); _validationErrors = new List(); } @@ -82,6 +86,38 @@ private bool IsStorageValid(StorageConfiguration storage) valid &= IsValueInRange("InformaticsGateway>storage>reserveSpaceGB", 1, 999, storage.ReserveSpaceGB); valid &= IsValueInRange("InformaticsGateway>storage>payloadProcessThreads", 1, 128, storage.PayloadProcessThreads); valid &= IsValueInRange("InformaticsGateway>storage>concurrentUploads", 1, 128, storage.ConcurrentUploads); + valid &= IsValueInRange("InformaticsGateway>storage>memoryThreshold", 1, int.MaxValue, storage.MemoryThreshold); + + if (storage.TemporaryDataStorage == TemporaryDataStorageLocation.Disk) + { + valid &= IsNotNullOrWhiteSpace("InformaticsGateway>storage>bufferRootPath", storage.BufferStorageRootPath); + valid &= IsValidDirectory("InformaticsGateway>storage>bufferRootPath", storage.BufferStorageRootPath); + valid &= IsValueInRange("InformaticsGateway>storage>bufferSize", 1, int.MaxValue, storage.BufferSize); + } + + return valid; + } + + private bool IsValidDirectory(string source, string directory) + { + var valid = true; + try + { + if (!_fileSystem.Directory.Exists(directory)) + { + valid = false; + _validationErrors.Add($"Directory `{directory}` specified in `{source}` does not exist."); + } + else + { + using var _ = _fileSystem.File.Create(Path.Combine(directory, Path.GetRandomFileName()), 1, FileOptions.DeleteOnClose); + } + } + catch (Exception ex) + { + valid = false; + _validationErrors.Add($"Directory `{directory}` specified in `{source}` is not accessible: {ex.Message}."); + } return valid; } diff --git a/src/Configuration/Monai.Deploy.InformaticsGateway.Configuration.csproj b/src/Configuration/Monai.Deploy.InformaticsGateway.Configuration.csproj index 27d6b14ba..47241673e 100644 --- a/src/Configuration/Monai.Deploy.InformaticsGateway.Configuration.csproj +++ b/src/Configuration/Monai.Deploy.InformaticsGateway.Configuration.csproj @@ -30,8 +30,8 @@ - - + + diff --git a/src/Configuration/StorageConfiguration.cs b/src/Configuration/StorageConfiguration.cs index 8995e52c8..bb6f01aaf 100644 --- a/src/Configuration/StorageConfiguration.cs +++ b/src/Configuration/StorageConfiguration.cs @@ -22,6 +22,13 @@ namespace Monai.Deploy.InformaticsGateway.Configuration { public class StorageConfiguration : StorageServiceConfiguration { + /// + /// Gets or sets whether to store temporary data in Memory or on Disk. + /// Defaults to Memory. + /// + [ConfigurationKeyName("tempStorageLocation")] + public TemporaryDataStorageLocation TemporaryDataStorage { get; set; } = TemporaryDataStorageLocation.Disk; + /// /// Gets or sets the path used for buffering incoming data. /// Defaults to ./temp. @@ -29,6 +36,14 @@ public class StorageConfiguration : StorageServiceConfiguration [ConfigurationKeyName("bufferRootPath")] public string BufferStorageRootPath { get; set; } = "./temp"; + /// + /// Gets or sets the number of bytes buffered for reads and writes to the temporary file. + /// Defaults to 128000. + /// + [ConfigurationKeyName("bufferSize")] + public int BufferSize { get; set; } = 128000; + + /// /// Gets or set the maximum memory buffer size in bytes with default to 30MiB. /// diff --git a/src/Configuration/TemporaryDataStorageLocation.cs b/src/Configuration/TemporaryDataStorageLocation.cs new file mode 100644 index 000000000..01fe8305d --- /dev/null +++ b/src/Configuration/TemporaryDataStorageLocation.cs @@ -0,0 +1,24 @@ +/* + * Copyright 2022 MONAI Consortium + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Monai.Deploy.InformaticsGateway.Configuration +{ + public enum TemporaryDataStorageLocation + { + Memory, + Disk + } +} diff --git a/src/Configuration/Test/ConfigurationValidatorTest.cs b/src/Configuration/Test/ConfigurationValidatorTest.cs index 9c60a5c4b..d1fb6375d 100644 --- a/src/Configuration/Test/ConfigurationValidatorTest.cs +++ b/src/Configuration/Test/ConfigurationValidatorTest.cs @@ -15,6 +15,8 @@ */ using System; +using System.IO; +using System.IO.Abstractions; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Monai.Deploy.InformaticsGateway.SharedTest; @@ -26,17 +28,21 @@ namespace Monai.Deploy.InformaticsGateway.Configuration.Test public class ConfigurationValidatorTest { private readonly Mock> _logger; + private readonly Mock _fileSystem; public ConfigurationValidatorTest() { _logger = new Mock>(); + _fileSystem = new Mock(); + _fileSystem.Setup(p => p.Directory.Exists(It.IsAny())).Returns(true); + _fileSystem.Setup(p => p.File.Create(It.IsAny(), It.IsAny(), It.IsAny())).Returns(FileStream.Null); } [Fact(DisplayName = "ConfigurationValidator test with all valid settings")] public void AllValid() { var config = MockValidConfiguration(); - var valid = new ConfigurationValidator(_logger.Object).Validate("", config); + var valid = new ConfigurationValidator(_logger.Object, _fileSystem.Object).Validate("", config); Assert.True(valid == ValidateOptionsResult.Success); } @@ -46,7 +52,7 @@ public void InvalidScpPort() var config = MockValidConfiguration(); config.Dicom.Scp.Port = Int32.MaxValue; - var valid = new ConfigurationValidator(_logger.Object).Validate("", config); + var valid = new ConfigurationValidator(_logger.Object, _fileSystem.Object).Validate("", config); var validationMessage = $"Invalid port number '{Int32.MaxValue}' specified for InformaticsGateway>dicom>scp>port."; Assert.Equal(validationMessage, valid.FailureMessage); @@ -59,7 +65,7 @@ public void InvalidScpMaxAssociations() var config = MockValidConfiguration(); config.Dicom.Scp.MaximumNumberOfAssociations = 0; - var valid = new ConfigurationValidator(_logger.Object).Validate("", config); + var valid = new ConfigurationValidator(_logger.Object, _fileSystem.Object).Validate("", config); var validationMessage = $"Value of InformaticsGateway>dicom>scp>max-associations must be between {1} and {1000}."; Assert.Equal(validationMessage, valid.FailureMessage); @@ -72,7 +78,7 @@ public void StorageWithInvalidWatermark() var config = MockValidConfiguration(); config.Storage.Watermark = 1000; - var valid = new ConfigurationValidator(_logger.Object).Validate("", config); + var valid = new ConfigurationValidator(_logger.Object, _fileSystem.Object).Validate("", config); var validationMessage = "Value of InformaticsGateway>storage>watermark must be between 1 and 100."; Assert.Equal(validationMessage, valid.FailureMessage); @@ -85,7 +91,7 @@ public void StorageWithInvalidReservedSpace() var config = MockValidConfiguration(); config.Storage.ReserveSpaceGB = 9999; - var valid = new ConfigurationValidator(_logger.Object).Validate("", config); + var valid = new ConfigurationValidator(_logger.Object, _fileSystem.Object).Validate("", config); var validationMessage = "Value of InformaticsGateway>storage>reserveSpaceGB must be between 1 and 999."; Assert.Equal(validationMessage, valid.FailureMessage); @@ -98,7 +104,7 @@ public void StorageWithInvalidTemporaryBucketName() var config = MockValidConfiguration(); config.Storage.TemporaryStorageBucket = " "; - var valid = new ConfigurationValidator(_logger.Object).Validate("", config); + var valid = new ConfigurationValidator(_logger.Object, _fileSystem.Object).Validate("", config); var validationMessages = new[] { "Value for InformaticsGateway>storage>temporaryBucketName is required.", "Value for InformaticsGateway>storage>temporaryBucketName does not conform to Amazon S3 bucket naming requirements." }; Assert.Equal(string.Join(Environment.NewLine, validationMessages), valid.FailureMessage); @@ -114,7 +120,7 @@ public void StorageWithInvalidBucketName() var config = MockValidConfiguration(); config.Storage.StorageServiceBucketName = ""; - var valid = new ConfigurationValidator(_logger.Object).Validate("", config); + var valid = new ConfigurationValidator(_logger.Object, _fileSystem.Object).Validate("", config); var validationMessages = new[] { "Value for InformaticsGateway>storage>bucketName is required.", "Value for InformaticsGateway>storage>bucketName does not conform to Amazon S3 bucket naming requirements." }; Assert.Equal(string.Join(Environment.NewLine, validationMessages), valid.FailureMessage); @@ -124,6 +130,25 @@ public void StorageWithInvalidBucketName() } } + [Fact(DisplayName = "ConfigurationValidator test with inaccessible directory")] + public void StorageWithInaccessbleDirectory() + { + _fileSystem.Setup(p => p.File.Create(It.IsAny(), It.IsAny(), It.IsAny())).Throws(new UnauthorizedAccessException("error")); + + var config = MockValidConfiguration(); + config.Storage.TemporaryDataStorage = TemporaryDataStorageLocation.Disk; + config.Storage.BufferStorageRootPath = "/blabla"; + + var valid = new ConfigurationValidator(_logger.Object, _fileSystem.Object).Validate("", config); + + var validationMessages = new[] { $"Directory `/blabla` specified in `InformaticsGateway>storage>bufferRootPath` is not accessible: error." }; + Assert.Equal(string.Join(Environment.NewLine, validationMessages), valid.FailureMessage); + foreach (var message in validationMessages) + { + _logger.VerifyLogging(message, LogLevel.Error, Times.Once()); + } + } + private static InformaticsGatewayConfiguration MockValidConfiguration() { var config = new InformaticsGatewayConfiguration(); diff --git a/src/Database/PayloadConfiguration.cs b/src/Database/PayloadConfiguration.cs index 171b10ac1..b40c9e7c1 100644 --- a/src/Database/PayloadConfiguration.cs +++ b/src/Database/PayloadConfiguration.cs @@ -57,6 +57,7 @@ public void Configure(EntityTypeBuilder builder) builder.Ignore(j => j.CalledAeTitle); builder.Ignore(j => j.CallingAeTitle); builder.Ignore(j => j.HasTimedOut); + builder.Ignore(j => j.Elapsed); builder.Ignore(j => j.Count); } } diff --git a/src/InformaticsGateway/Common/FileStorageMetadataExtensions.cs b/src/InformaticsGateway/Common/FileStorageMetadataExtensions.cs index dabb80437..a45fe10d3 100644 --- a/src/InformaticsGateway/Common/FileStorageMetadataExtensions.cs +++ b/src/InformaticsGateway/Common/FileStorageMetadataExtensions.cs @@ -14,41 +14,92 @@ * limitations under the License. */ -using System.IO; +using System.IO.Abstractions; using System.Text; using System.Threading.Tasks; using Ardalis.GuardClauses; using FellowOakDicom; using Monai.Deploy.InformaticsGateway.Api.Storage; +using Monai.Deploy.InformaticsGateway.Configuration; namespace Monai.Deploy.InformaticsGateway.Common { internal static class FileStorageMetadataExtensions { - public static async Task SetDataStreams(this DicomFileStorageMetadata dicomFileStorageMetadata, DicomFile dicomFile, string dicomJson) + public static async Task SetDataStreams( + this DicomFileStorageMetadata dicomFileStorageMetadata, + DicomFile dicomFile, + string dicomJson, + TemporaryDataStorageLocation storageLocation, + IFileSystem fileSystem = null, + string temporaryStoragePath = "") { Guard.Against.Null(dicomFile, nameof(dicomFile)); Guard.Against.Null(dicomJson, nameof(dicomJson)); // allow empty here - dicomFileStorageMetadata.File.Data = new MemoryStream(); + switch (storageLocation) + { + case TemporaryDataStorageLocation.Disk: + Guard.Against.Null(fileSystem, nameof(fileSystem)); + Guard.Against.NullOrWhiteSpace(temporaryStoragePath, nameof(temporaryStoragePath)); + + var tempFile = fileSystem.Path.Combine(temporaryStoragePath, $@"{fileSystem.Path.GetRandomFileName()}"); + dicomFileStorageMetadata.File.Data = fileSystem.File.Create(tempFile); + break; + default: + dicomFileStorageMetadata.File.Data = new System.IO.MemoryStream(); + break; + } + await dicomFile.SaveAsync(dicomFileStorageMetadata.File.Data).ConfigureAwait(false); - dicomFileStorageMetadata.File.Data.Seek(0, SeekOrigin.Begin); + dicomFileStorageMetadata.File.Data.Seek(0, System.IO.SeekOrigin.Begin); - SetTextStream(dicomFileStorageMetadata.JsonFile, dicomJson); + await SetTextStream(dicomFileStorageMetadata.JsonFile, dicomJson, storageLocation, fileSystem, temporaryStoragePath); } - public static void SetDataStream(this FhirFileStorageMetadata fhirFileStorageMetadata, string json) - => SetTextStream(fhirFileStorageMetadata.File, json); + public static async Task SetDataStream( + this FhirFileStorageMetadata fhirFileStorageMetadata, + string json, + TemporaryDataStorageLocation storageLocation, + IFileSystem fileSystem = null, + string temporaryStoragePath = "") + => await SetTextStream(fhirFileStorageMetadata.File, json, storageLocation, fileSystem, temporaryStoragePath); - public static void SetDataStream(this Hl7FileStorageMetadata hl7FileStorageMetadata, string message) - => SetTextStream(hl7FileStorageMetadata.File, message); + public static async Task SetDataStream( + this Hl7FileStorageMetadata hl7FileStorageMetadata, + string message, + TemporaryDataStorageLocation storageLocation, + IFileSystem fileSystem = null, + string temporaryStoragePath = "") + => await SetTextStream(hl7FileStorageMetadata.File, message, storageLocation, fileSystem, temporaryStoragePath); - private static void SetTextStream(StorageObjectMetadata storageObjectMetadata, string message) + private static async Task SetTextStream( + StorageObjectMetadata storageObjectMetadata, + string message, + TemporaryDataStorageLocation storageLocation, + IFileSystem fileSystem = null, + string temporaryStoragePath = "") { Guard.Against.Null(message, nameof(message)); // allow empty here - storageObjectMetadata.Data = new MemoryStream(Encoding.UTF8.GetBytes(message)); - storageObjectMetadata.Data.Seek(0, SeekOrigin.Begin); + switch (storageLocation) + { + case TemporaryDataStorageLocation.Disk: + Guard.Against.Null(fileSystem, nameof(fileSystem)); + Guard.Against.NullOrWhiteSpace(temporaryStoragePath, nameof(temporaryStoragePath)); + + var tempFile = fileSystem.Path.Combine(temporaryStoragePath, $@"{fileSystem.Path.GetRandomFileName()}"); + var stream = fileSystem.File.Create(tempFile); + var data = Encoding.UTF8.GetBytes(message); + await stream.WriteAsync(data, 0, data.Length); + storageObjectMetadata.Data = stream; + break; + default: + storageObjectMetadata.Data = new System.IO.MemoryStream(Encoding.UTF8.GetBytes(message)); + break; + } + + storageObjectMetadata.Data.Seek(0, System.IO.SeekOrigin.Begin); } } } diff --git a/src/InformaticsGateway/Logging/Log.700.PayloadService.cs b/src/InformaticsGateway/Logging/Log.700.PayloadService.cs index 588669efe..5cb9aaa3f 100644 --- a/src/InformaticsGateway/Logging/Log.700.PayloadService.cs +++ b/src/InformaticsGateway/Logging/Log.700.PayloadService.cs @@ -53,8 +53,8 @@ public static partial class Log [LoggerMessage(EventId = 711, Level = LogLevel.Information, Message = "Publishing workflow request message ID={messageId}...")] public static partial void PublishingWorkflowRequest(this ILogger logger, string messageId); - [LoggerMessage(EventId = 712, Level = LogLevel.Information, Message = "Workflow request published to {queue}, message ID={messageId}.")] - public static partial void WorkflowRequestPublished(this ILogger logger, string queue, string messageId); + [LoggerMessage(EventId = 712, Level = LogLevel.Information, Message = "Workflow request published to {queue}, message ID={messageId}. Payload took {payloadElapsedTime} to complete.")] + public static partial void WorkflowRequestPublished(this ILogger logger, string queue, string messageId, TimeSpan payloadElapsedTime); [LoggerMessage(EventId = 713, Level = LogLevel.Information, Message = "Restoring payloads from database.")] public static partial void StartupRestoreFromDatabase(this ILogger logger); diff --git a/src/InformaticsGateway/Monai.Deploy.InformaticsGateway.csproj b/src/InformaticsGateway/Monai.Deploy.InformaticsGateway.csproj index 5097878b7..7a31a6b38 100644 --- a/src/InformaticsGateway/Monai.Deploy.InformaticsGateway.csproj +++ b/src/InformaticsGateway/Monai.Deploy.InformaticsGateway.csproj @@ -47,9 +47,9 @@ - - - + + + diff --git a/src/InformaticsGateway/Services/Connectors/DataRetrievalService.cs b/src/InformaticsGateway/Services/Connectors/DataRetrievalService.cs index ea39f5d0b..ada4e956f 100644 --- a/src/InformaticsGateway/Services/Connectors/DataRetrievalService.cs +++ b/src/InformaticsGateway/Services/Connectors/DataRetrievalService.cs @@ -17,6 +17,7 @@ using System; using System.Collections.Generic; +using System.IO.Abstractions; using System.Linq; using System.Net.Http; using System.Net.Http.Headers; @@ -55,6 +56,7 @@ internal class DataRetrievalService : IHostedService, IMonaiService, IDisposable private readonly IObjectUploadQueue _uploadQueue; private readonly IPayloadAssembler _payloadAssembler; private readonly IDicomToolkit _dicomToolkit; + private readonly IFileSystem _fileSystem; private bool _disposedValue; public ServiceStatus Status { get; set; } @@ -77,6 +79,7 @@ public DataRetrievalService( _uploadQueue = _rootScope.ServiceProvider.GetService() ?? throw new ServiceNotFoundException(nameof(IObjectUploadQueue)); _payloadAssembler = _rootScope.ServiceProvider.GetService() ?? throw new ServiceNotFoundException(nameof(IPayloadAssembler)); _dicomToolkit = _rootScope.ServiceProvider.GetService() ?? throw new ServiceNotFoundException(nameof(IDicomToolkit)); + _fileSystem = _rootScope.ServiceProvider.GetService() ?? throw new ServiceNotFoundException(nameof(IFileSystem)); } public Task StartAsync(CancellationToken cancellationToken) @@ -330,7 +333,7 @@ private async Task RetrieveFhirResource(string transactionId, HttpClient h } var fhirFile = new FhirFileStorageMetadata(transactionId, resource.Type, resource.Id, fhirFormat); - fhirFile.SetDataStream(json); + await fhirFile.SetDataStream(json, _options.Value.Storage.TemporaryDataStorage, _fileSystem, _options.Value.Storage.BufferStorageRootPath); retrievedResources.Add(fhirFile.Id, fhirFile); return true; } @@ -509,7 +512,7 @@ private async Task RetrieveInstances(string transactionId, IDicomWebClient dicom } var dicomFileStorageMetadata = SaveFile(transactionId, file, uids); - await dicomFileStorageMetadata.SetDataStreams(file, file.ToJson(_options.Value.Dicom.WriteDicomJson, _options.Value.Dicom.ValidateDicomOnSerialization)).ConfigureAwait(false); + await dicomFileStorageMetadata.SetDataStreams(file, file.ToJson(_options.Value.Dicom.WriteDicomJson, _options.Value.Dicom.ValidateDicomOnSerialization), _options.Value.Storage.TemporaryDataStorage, _fileSystem, _options.Value.Storage.BufferStorageRootPath).ConfigureAwait(false); retrievedInstance.Add(uids.Identifier, dicomFileStorageMetadata); count++; } @@ -539,7 +542,7 @@ private async Task SaveFiles(string transactionId, IAsyncEnumerable f } var dicomFileStorageMetadata = SaveFile(transactionId, file, uids); - await dicomFileStorageMetadata.SetDataStreams(file, file.ToJson(_options.Value.Dicom.WriteDicomJson, _options.Value.Dicom.ValidateDicomOnSerialization)).ConfigureAwait(false); + await dicomFileStorageMetadata.SetDataStreams(file, file.ToJson(_options.Value.Dicom.WriteDicomJson, _options.Value.Dicom.ValidateDicomOnSerialization), _options.Value.Storage.TemporaryDataStorage, _fileSystem, _options.Value.Storage.BufferStorageRootPath).ConfigureAwait(false); retrievedInstance.Add(uids.Identifier, dicomFileStorageMetadata); } } diff --git a/src/InformaticsGateway/Services/Connectors/PayloadNotificationActionHandler.cs b/src/InformaticsGateway/Services/Connectors/PayloadNotificationActionHandler.cs index c45c1cbc4..4a50c3fd7 100644 --- a/src/InformaticsGateway/Services/Connectors/PayloadNotificationActionHandler.cs +++ b/src/InformaticsGateway/Services/Connectors/PayloadNotificationActionHandler.cs @@ -138,7 +138,7 @@ await _messageBrokerPublisherService.Publish( _options.Value.Messaging.Topics.WorkflowRequest, message.ToMessage()).ConfigureAwait(false); - _logger.WorkflowRequestPublished(_options.Value.Messaging.Topics.WorkflowRequest, message.MessageId); + _logger.WorkflowRequestPublished(_options.Value.Messaging.Topics.WorkflowRequest, message.MessageId, payload.Elapsed); } private async Task UpdatePayloadState(Payload payload) diff --git a/src/InformaticsGateway/Services/DicomWeb/IStreamsWriter.cs b/src/InformaticsGateway/Services/DicomWeb/IStreamsWriter.cs index 35cf29a15..c09943d4c 100644 --- a/src/InformaticsGateway/Services/DicomWeb/IStreamsWriter.cs +++ b/src/InformaticsGateway/Services/DicomWeb/IStreamsWriter.cs @@ -17,6 +17,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.IO.Abstractions; using System.Threading; using System.Threading.Tasks; using Ardalis.GuardClauses; @@ -43,6 +44,7 @@ internal interface IStreamsWriter internal class StreamsWriter : IStreamsWriter { private readonly ILogger _logger; + private readonly IFileSystem _fileSystem; private readonly IObjectUploadQueue _uploadQueue; private readonly IDicomToolkit _dicomToolkit; private readonly IPayloadAssembler _payloadAssembler; @@ -56,13 +58,15 @@ public StreamsWriter( IDicomToolkit dicomToolkit, IPayloadAssembler payloadAssembler, IOptions configuration, - ILogger logger) + ILogger logger, + IFileSystem fileSystem) { _uploadQueue = fileStore ?? throw new ArgumentNullException(nameof(fileStore)); _dicomToolkit = dicomToolkit ?? throw new ArgumentNullException(nameof(dicomToolkit)); _payloadAssembler = payloadAssembler ?? throw new ArgumentNullException(nameof(payloadAssembler)); _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _fileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem)); _resultDicomDataset = new DicomDataset(); _failureCount = 0; _storedCount = 0; @@ -165,7 +169,7 @@ private async Task SaveInstance(Stream stream, string studyInstanceUid, string w dicomInfo.SetWorkflows(workflowName); } - await dicomInfo.SetDataStreams(dicomFile, dicomFile.ToJson(_configuration.Value.Dicom.WriteDicomJson, _configuration.Value.Dicom.ValidateDicomOnSerialization)).ConfigureAwait(false); + await dicomInfo.SetDataStreams(dicomFile, dicomFile.ToJson(_configuration.Value.Dicom.WriteDicomJson, _configuration.Value.Dicom.ValidateDicomOnSerialization), _configuration.Value.Storage.TemporaryDataStorage, _fileSystem, _configuration.Value.Storage.BufferStorageRootPath).ConfigureAwait(false); _uploadQueue.Queue(dicomInfo); // for DICOMweb, use correlation ID as the grouping key diff --git a/src/InformaticsGateway/Services/Fhir/FhirJsonReader.cs b/src/InformaticsGateway/Services/Fhir/FhirJsonReader.cs index 9b8ab8b99..2f56dffb5 100644 --- a/src/InformaticsGateway/Services/Fhir/FhirJsonReader.cs +++ b/src/InformaticsGateway/Services/Fhir/FhirJsonReader.cs @@ -16,6 +16,7 @@ using System; using System.IO; +using System.IO.Abstractions; using System.Text.Json; using System.Text.Json.Nodes; using System.Threading; @@ -23,9 +24,11 @@ using Ardalis.GuardClauses; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Microsoft.Net.Http.Headers; using Monai.Deploy.InformaticsGateway.Api.Storage; using Monai.Deploy.InformaticsGateway.Common; +using Monai.Deploy.InformaticsGateway.Configuration; using Monai.Deploy.InformaticsGateway.Logging; namespace Monai.Deploy.InformaticsGateway.Services.Fhir @@ -33,10 +36,14 @@ namespace Monai.Deploy.InformaticsGateway.Services.Fhir internal class FhirJsonReader : IFHirRequestReader { private readonly ILogger _logger; + private readonly IOptions _options; + private readonly IFileSystem _fileSystem; - public FhirJsonReader(ILogger logger) + public FhirJsonReader(ILogger logger, IOptions options, IFileSystem fileSystem) { _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _options = options ?? throw new ArgumentNullException(nameof(options)); + _fileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem)); } public async Task GetContentAsync(HttpRequest request, string correlationId, string resourceType, MediaTypeHeaderValue mediaTypeHeaderValue, CancellationToken cancellationToken) @@ -68,7 +75,7 @@ public async Task GetContentAsync(HttpRequest request, string c result.RawData = jsonDoc.ToJsonString(new JsonSerializerOptions { WriteIndented = true }); var fileMetadata = new FhirFileStorageMetadata(correlationId, result.InternalResourceType, resourceId, Api.Rest.FhirStorageFormat.Json); - fileMetadata.SetDataStream(result.RawData); + await fileMetadata.SetDataStream(result.RawData, _options.Value.Storage.TemporaryDataStorage, _fileSystem, _options.Value.Storage.BufferStorageRootPath); result.Metadata = fileMetadata; return result; diff --git a/src/InformaticsGateway/Services/Fhir/FhirService.cs b/src/InformaticsGateway/Services/Fhir/FhirService.cs index 59c3b8cf0..f66dbc830 100644 --- a/src/InformaticsGateway/Services/Fhir/FhirService.cs +++ b/src/InformaticsGateway/Services/Fhir/FhirService.cs @@ -15,6 +15,7 @@ */ using System; +using System.IO.Abstractions; using System.Text.Json; using System.Threading; using System.Threading.Tasks; @@ -41,6 +42,7 @@ internal class FhirService : IFhirService private readonly ILogger _logger; private readonly IPayloadAssembler _payloadAssembler; private readonly IObjectUploadQueue _uploadQueue; + private readonly IFileSystem _fileSystem; public FhirService(IServiceScopeFactory serviceScopeFactory, IOptions configuration) { @@ -51,6 +53,7 @@ public FhirService(IServiceScopeFactory serviceScopeFactory, IOptions>() ?? throw new ServiceNotFoundException(nameof(ILogger)); _payloadAssembler = scope.ServiceProvider.GetService() ?? throw new ServiceNotFoundException(nameof(IPayloadAssembler)); _uploadQueue = scope.ServiceProvider.GetService() ?? throw new ServiceNotFoundException(nameof(IObjectUploadQueue)); + _fileSystem = scope.ServiceProvider.GetService() ?? throw new ServiceNotFoundException(nameof(IFileSystem)); } public async Task StoreAsync(HttpRequest request, string correlationId, string resourceType, CancellationToken cancellationToken) @@ -100,13 +103,13 @@ private IFHirRequestReader GetRequestReader(MediaTypeHeaderValue mediaTypeHeader if (mediaTypeHeaderValue.MediaType.Equals(ContentTypes.ApplicationFhirJson, StringComparison.OrdinalIgnoreCase)) { var logger = scope.ServiceProvider.GetService>() ?? throw new ServiceNotFoundException(nameof(ILogger)); - return new FhirJsonReader(logger); + return new FhirJsonReader(logger, _configuration, _fileSystem); } if (mediaTypeHeaderValue.MediaType.Equals(ContentTypes.ApplicationFhirXml, StringComparison.OrdinalIgnoreCase)) { var logger = scope.ServiceProvider.GetService>() ?? throw new ServiceNotFoundException(nameof(ILogger)); - return new FhirXmlReader(logger); + return new FhirXmlReader(logger, _configuration, _fileSystem); } throw new UnsupportedContentTypeException($"Media type of '{mediaTypeHeaderValue.MediaType}' is not supported."); diff --git a/src/InformaticsGateway/Services/Fhir/FhirXmlReader.cs b/src/InformaticsGateway/Services/Fhir/FhirXmlReader.cs index 07610c6b2..2d975db8a 100644 --- a/src/InformaticsGateway/Services/Fhir/FhirXmlReader.cs +++ b/src/InformaticsGateway/Services/Fhir/FhirXmlReader.cs @@ -16,6 +16,7 @@ using System; using System.IO; +using System.IO.Abstractions; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -23,9 +24,11 @@ using Ardalis.GuardClauses; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Microsoft.Net.Http.Headers; using Monai.Deploy.InformaticsGateway.Api.Storage; using Monai.Deploy.InformaticsGateway.Common; +using Monai.Deploy.InformaticsGateway.Configuration; using Monai.Deploy.InformaticsGateway.Logging; namespace Monai.Deploy.InformaticsGateway.Services.Fhir @@ -33,10 +36,14 @@ namespace Monai.Deploy.InformaticsGateway.Services.Fhir internal class FhirXmlReader : IFHirRequestReader { private readonly ILogger _logger; + private readonly IOptions _options; + private readonly IFileSystem _fileSystem; - public FhirXmlReader(ILogger logger) + public FhirXmlReader(ILogger logger, IOptions options, IFileSystem fileSystem) { _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _options = options ?? throw new ArgumentNullException(nameof(options)); + _fileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem)); } public async Task GetContentAsync(HttpRequest request, string correlationId, string resourceType, MediaTypeHeaderValue mediaTypeHeaderValue, CancellationToken cancellationToken) @@ -80,7 +87,7 @@ public async Task GetContentAsync(HttpRequest request, string c result.InternalResourceType = rootNode.Name; var fileMetadata = new FhirFileStorageMetadata(correlationId, result.InternalResourceType, resourceId, Api.Rest.FhirStorageFormat.Xml); - fileMetadata.SetDataStream(result.RawData); + await fileMetadata.SetDataStream(result.RawData, _options.Value.Storage.TemporaryDataStorage, _fileSystem, _options.Value.Storage.BufferStorageRootPath); result.Metadata = fileMetadata; return result; diff --git a/src/InformaticsGateway/Services/HealthLevel7/MllpService.cs b/src/InformaticsGateway/Services/HealthLevel7/MllpService.cs index 3ba770ffd..ba657f8b2 100644 --- a/src/InformaticsGateway/Services/HealthLevel7/MllpService.cs +++ b/src/InformaticsGateway/Services/HealthLevel7/MllpService.cs @@ -17,6 +17,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.IO.Abstractions; using System.Threading; using System.Threading.Tasks; using Ardalis.GuardClauses; @@ -43,6 +44,7 @@ internal sealed class MllpService : IHostedService, IDisposable, IMonaiService private readonly IMllpClientFactory _mllpClientFactory; private readonly IObjectUploadQueue _uploadQueue; private readonly IPayloadAssembler _payloadAssembler; + private readonly IFileSystem _fileSystem; private readonly IServiceScope _serviceScope; private readonly ILoggerFactory _logginFactory; private readonly ILogger _logger; @@ -79,6 +81,7 @@ public MllpService(IServiceScopeFactory serviceScopeFactory, _mllpClientFactory = _serviceScope.ServiceProvider.GetService() ?? throw new ServiceNotFoundException(nameof(IMllpClientFactory)); _uploadQueue = _serviceScope.ServiceProvider.GetService() ?? throw new ServiceNotFoundException(nameof(IObjectUploadQueue)); _payloadAssembler = _serviceScope.ServiceProvider.GetService() ?? throw new ServiceNotFoundException(nameof(IPayloadAssembler)); + _fileSystem = _serviceScope.ServiceProvider.GetService() ?? throw new ServiceNotFoundException(nameof(IFileSystem)); _activeTasks = new ConcurrentDictionary(); } @@ -158,7 +161,7 @@ private async Task OnDisconnect(IMllpClient client, MllpClientResult result) foreach (var message in result.Messages) { var hl7Fileetadata = new Hl7FileStorageMetadata(client.ClientId.ToString()); - hl7Fileetadata.SetDataStream(message.HL7Message); + await hl7Fileetadata.SetDataStream(message.HL7Message, _configuration.Value.Storage.TemporaryDataStorage, _fileSystem, _configuration.Value.Storage.BufferStorageRootPath); _uploadQueue.Queue(hl7Fileetadata); await _payloadAssembler.Queue(client.ClientId.ToString(), hl7Fileetadata).ConfigureAwait(false); } diff --git a/src/InformaticsGateway/Services/Http/MonaiHealthCheck.cs b/src/InformaticsGateway/Services/Http/MonaiHealthCheck.cs index 5f13aab52..007995a2f 100644 --- a/src/InformaticsGateway/Services/Http/MonaiHealthCheck.cs +++ b/src/InformaticsGateway/Services/Http/MonaiHealthCheck.cs @@ -16,7 +16,6 @@ using System; using System.Linq; -using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Diagnostics.HealthChecks; @@ -41,23 +40,14 @@ public Task CheckHealthAsync(HealthCheckContext context, Canc { return Task.FromResult(HealthCheckResult.Healthy()); } + var unhealthyServices = services.Where(item => item.Value != Api.Rest.ServiceStatus.Running).ToDictionary(k => k.Key, v => (object)v.Value); - if (services.Values.All(p => p == Api.Rest.ServiceStatus.Stopped || - p == Api.Rest.ServiceStatus.Cancelled || - p == Api.Rest.ServiceStatus.Unknown)) + if (unhealthyServices.Count == services.Count) { - return Task.FromResult(HealthCheckResult.Unhealthy()); + return Task.FromResult(HealthCheckResult.Unhealthy(data: unhealthyServices)); } - var sb = new StringBuilder(); - foreach (var service in services.Keys) - { - if (services[service] != Api.Rest.ServiceStatus.Running) - { - sb.AppendLine($"{service}: {services[service]}"); - } - } - return Task.FromResult(HealthCheckResult.Degraded(sb.ToString())); + return Task.FromResult(HealthCheckResult.Degraded(data: unhealthyServices)); } } } diff --git a/src/InformaticsGateway/Services/Scp/ApplicationEntityHandler.cs b/src/InformaticsGateway/Services/Scp/ApplicationEntityHandler.cs index 46bec278e..a856c09fd 100644 --- a/src/InformaticsGateway/Services/Scp/ApplicationEntityHandler.cs +++ b/src/InformaticsGateway/Services/Scp/ApplicationEntityHandler.cs @@ -15,12 +15,14 @@ */ using System; +using System.IO.Abstractions; using System.Linq; using System.Threading.Tasks; using Ardalis.GuardClauses; using FellowOakDicom.Network; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Monai.Deploy.InformaticsGateway.Api; using Monai.Deploy.InformaticsGateway.Api.Storage; using Monai.Deploy.InformaticsGateway.Common; @@ -34,11 +36,12 @@ namespace Monai.Deploy.InformaticsGateway.Services.Scp internal class ApplicationEntityHandler : IDisposable, IApplicationEntityHandler { private readonly ILogger _logger; + private readonly IOptions _options; private readonly IServiceScope _serviceScope; private readonly IPayloadAssembler _payloadAssembler; private readonly IObjectUploadQueue _uploadQueue; - + private readonly IFileSystem _fileSystem; private MonaiApplicationEntity _configuration; private DicomJsonOptions _dicomJsonOptions; private bool _validateDicomValueOnJsonSerialization; @@ -46,14 +49,17 @@ internal class ApplicationEntityHandler : IDisposable, IApplicationEntityHandler public ApplicationEntityHandler( IServiceScopeFactory serviceScopeFactory, - ILogger logger) + ILogger logger, + IOptions options) { Guard.Against.Null(serviceScopeFactory, nameof(serviceScopeFactory)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _options = options ?? throw new ArgumentNullException(nameof(options)); _serviceScope = serviceScopeFactory.CreateScope(); _payloadAssembler = _serviceScope.ServiceProvider.GetService() ?? throw new ServiceNotFoundException(nameof(IPayloadAssembler)); _uploadQueue = _serviceScope.ServiceProvider.GetService() ?? throw new ServiceNotFoundException(nameof(IObjectUploadQueue)); + _fileSystem = _serviceScope.ServiceProvider.GetService() ?? throw new ServiceNotFoundException(nameof(IFileSystem)); } public void Configure(MonaiApplicationEntity monaiApplicationEntity, DicomJsonOptions dicomJsonOptions, bool validateDicomValuesOnJsonSerialization) @@ -95,7 +101,7 @@ public async Task HandleInstanceAsync(DicomCStoreRequest request, string calledA dicomInfo.SetWorkflows(_configuration.Workflows.ToArray()); } - await dicomInfo.SetDataStreams(request.File, request.File.ToJson(_dicomJsonOptions, _validateDicomValueOnJsonSerialization)).ConfigureAwait(false); + await dicomInfo.SetDataStreams(request.File, request.File.ToJson(_dicomJsonOptions, _validateDicomValueOnJsonSerialization), _options.Value.Storage.TemporaryDataStorage, _fileSystem, _options.Value.Storage.BufferStorageRootPath).ConfigureAwait(false); _uploadQueue.Queue(dicomInfo); var dicomTag = FellowOakDicom.DicomTag.Parse(_configuration.Grouping); diff --git a/src/InformaticsGateway/Test/Common/DicomFileStorageMetadataExtensionsTest.cs b/src/InformaticsGateway/Test/Common/DicomFileStorageMetadataExtensionsTest.cs index 3f389a152..a572279f4 100644 --- a/src/InformaticsGateway/Test/Common/DicomFileStorageMetadataExtensionsTest.cs +++ b/src/InformaticsGateway/Test/Common/DicomFileStorageMetadataExtensionsTest.cs @@ -16,6 +16,7 @@ using System; using System.IO; +using System.IO.Abstractions.TestingHelpers; using System.Text; using System.Threading.Tasks; using FellowOakDicom; @@ -31,7 +32,7 @@ namespace Monai.Deploy.InformaticsGateway.Test.Common public class DicomFileStorageMetadataExtensionsTest { [Fact] - public async Task GivenADicomFileStorageMetadata_WhenSetDataStreamsIsCalled_ExpectDataStreamsAreSet() + public async Task GivenADicomFileStorageMetadata_WhenSetDataStreamsIsCalledWithInMemoryStore_ExpectDataStreamsAreSet() { var metadata = new DicomFileStorageMetadata( Guid.NewGuid().ToString(), @@ -42,7 +43,7 @@ public async Task GivenADicomFileStorageMetadata_WhenSetDataStreamsIsCalled_Expe var dicom = InstanceGenerator.GenerateDicomFile(); var json = dicom.ToJson(DicomJsonOptions.Complete, false); - await metadata.SetDataStreams(dicom, json).ConfigureAwait(false); + await metadata.SetDataStreams(dicom, json, TemporaryDataStorageLocation.Memory).ConfigureAwait(false); Assert.NotNull(metadata.File.Data); Assert.NotNull(metadata.JsonFile.Data); @@ -59,7 +60,38 @@ public async Task GivenADicomFileStorageMetadata_WhenSetDataStreamsIsCalled_Expe } [Fact] - public async Task GivenADicomFileStorageMetadataWithInvalidDSValue_WhenSetDataStreamsIsCalledWithValidation_ThrowsFormatException() + public async Task GivenADicomFileStorageMetadata_WhenSetDataStreamsIsCalledWithDiskStore_ExpectDataStreamsAreSet() + { + var fileSystem = new MockFileSystem(); + fileSystem.AddDirectory("/temp"); + + var metadata = new DicomFileStorageMetadata( + Guid.NewGuid().ToString(), + Guid.NewGuid().ToString(), + Guid.NewGuid().ToString(), + Guid.NewGuid().ToString(), + Guid.NewGuid().ToString()); + + var dicom = InstanceGenerator.GenerateDicomFile(); + var json = dicom.ToJson(DicomJsonOptions.Complete, false); + await metadata.SetDataStreams(dicom, json, TemporaryDataStorageLocation.Disk, fileSystem, "/temp").ConfigureAwait(false); + + Assert.NotNull(metadata.File.Data); + Assert.NotNull(metadata.JsonFile.Data); + + var ms = new MemoryStream(); + await dicom.SaveAsync(ms).ConfigureAwait(false); + Assert.Equal(ms.ToArray(), (metadata.File.Data as MemoryStream).ToArray()); + + var jsonFromStream = Encoding.UTF8.GetString((metadata.JsonFile.Data as MemoryStream).ToArray()); + Assert.Equal(json.Trim(), jsonFromStream.Trim()); + + var dicomFileFromJson = DicomJson.ConvertJsonToDicom(json); + Assert.Equal(dicom.Dataset, dicomFileFromJson); + } + + [Fact] + public void GivenADicomFileStorageMetadataWithInvalidDSValue_WhenSetDataStreamsIsCalledWithValidation_ThrowsFormatException() { var metadata = new DicomFileStorageMetadata( Guid.NewGuid().ToString(), @@ -94,7 +126,7 @@ public async Task GivenADicomFileStorageMetadataWithInvalidDSValue_WhenSetDataSt dicom.Dataset.Add(DicomTag.PixelSpacing, "0.68300002813334234392234", "0.2354257587243524352345"); var json = dicom.ToJson(DicomJsonOptions.Complete, false); - await metadata.SetDataStreams(dicom, json).ConfigureAwait(false); + await metadata.SetDataStreams(dicom, json, TemporaryDataStorageLocation.Memory).ConfigureAwait(false); Assert.NotNull(metadata.File.Data); Assert.NotNull(metadata.JsonFile.Data); diff --git a/src/InformaticsGateway/Test/Services/Connectors/DataRetrievalServiceTest.cs b/src/InformaticsGateway/Test/Services/Connectors/DataRetrievalServiceTest.cs index bb2f0fc69..39ab94464 100644 --- a/src/InformaticsGateway/Test/Services/Connectors/DataRetrievalServiceTest.cs +++ b/src/InformaticsGateway/Test/Services/Connectors/DataRetrievalServiceTest.cs @@ -17,6 +17,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.IO.Abstractions; using System.Net.Http; using System.Net.Http.Headers; using System.Text.Json; @@ -54,6 +55,7 @@ public class DataRetrievalServiceTest private readonly Mock _uploadQueue; private readonly Mock _payloadAssembler; private readonly Mock _dicomToolkit; + private readonly Mock _fileSystem; private readonly Mock _inferenceRequestStore; private readonly Mock> _loggerDicomWebClient; @@ -74,6 +76,7 @@ public DataRetrievalServiceTest() _serviceScopeFactory = new Mock(); _uploadQueue = new Mock(); _dicomToolkit = new Mock(); + _fileSystem = new Mock(); _options = Options.Create(new InformaticsGatewayConfiguration()); _serviceScope = new Mock(); @@ -90,12 +93,14 @@ public DataRetrievalServiceTest() services.AddScoped(p => _payloadAssembler.Object); services.AddScoped(p => _dicomToolkit.Object); services.AddScoped(p => _inferenceRequestStore.Object); + services.AddScoped(p => _fileSystem.Object); _serviceProvider = services.BuildServiceProvider(); _serviceScopeFactory.Setup(p => p.CreateScope()).Returns(_serviceScope.Object); _serviceScope.Setup(p => p.ServiceProvider).Returns(_serviceProvider); _logger.Setup(p => p.IsEnabled(It.IsAny())).Returns(true); + _options.Value.Storage.TemporaryDataStorage = TemporaryDataStorageLocation.Memory; } [RetryFact(5, 250)] diff --git a/src/InformaticsGateway/Test/Services/DicomWeb/StreamsWriterTest.cs b/src/InformaticsGateway/Test/Services/DicomWeb/StreamsWriterTest.cs index c6295fc2f..451d0633d 100644 --- a/src/InformaticsGateway/Test/Services/DicomWeb/StreamsWriterTest.cs +++ b/src/InformaticsGateway/Test/Services/DicomWeb/StreamsWriterTest.cs @@ -17,6 +17,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.IO.Abstractions.TestingHelpers; using System.Threading.Tasks; using FellowOakDicom; using FellowOakDicom.Network; @@ -38,6 +39,7 @@ namespace Monai.Deploy.InformaticsGateway.Test.Services.DicomWeb public class StreamsWriterTest { private readonly Mock> _logger; + private readonly MockFileSystem _fileSystem; private readonly Mock _uploadQueue; private readonly Mock _dicomToolkit; private readonly Mock _payloadAssembler; @@ -50,17 +52,19 @@ public StreamsWriterTest() _payloadAssembler = new Mock(); _configuration = Options.Create(new InformaticsGatewayConfiguration()); _logger = new Mock>(); + _fileSystem = new MockFileSystem(); } [Fact] public void GivenAStreamsWriter_WhenInitialized_ExpectParametersToBeValidated() { - Assert.Throws(() => new StreamsWriter(null, null, null, null, null)); - Assert.Throws(() => new StreamsWriter(_uploadQueue.Object, null, null, null, null)); - Assert.Throws(() => new StreamsWriter(_uploadQueue.Object, _dicomToolkit.Object, null, null, null)); - Assert.Throws(() => new StreamsWriter(_uploadQueue.Object, _dicomToolkit.Object, _payloadAssembler.Object, null, null)); - Assert.Throws(() => new StreamsWriter(_uploadQueue.Object, _dicomToolkit.Object, _payloadAssembler.Object, _configuration, null)); - var exception = Record.Exception(() => new StreamsWriter(_uploadQueue.Object, _dicomToolkit.Object, _payloadAssembler.Object, _configuration, _logger.Object)); + Assert.Throws(() => new StreamsWriter(null, null, null, null, null, null)); + Assert.Throws(() => new StreamsWriter(_uploadQueue.Object, null, null, null, null, null)); + Assert.Throws(() => new StreamsWriter(_uploadQueue.Object, _dicomToolkit.Object, null, null, null, null)); + Assert.Throws(() => new StreamsWriter(_uploadQueue.Object, _dicomToolkit.Object, _payloadAssembler.Object, null, null, null)); + Assert.Throws(() => new StreamsWriter(_uploadQueue.Object, _dicomToolkit.Object, _payloadAssembler.Object, _configuration, null, null)); + Assert.Throws(() => new StreamsWriter(_uploadQueue.Object, _dicomToolkit.Object, _payloadAssembler.Object, _configuration, _logger.Object, null)); + var exception = Record.Exception(() => new StreamsWriter(_uploadQueue.Object, _dicomToolkit.Object, _payloadAssembler.Object, _configuration, _logger.Object, _fileSystem)); Assert.Null(exception); } @@ -72,7 +76,7 @@ public async Task GivenAHttpStream_WhenFailedToOpenAsDicomInstance_ExpectStatus4 .Throws(new Exception("error")); var studyInstanceUid = DicomUIDGenerator.GenerateDerivedFromUUID().UID; - var writer = new StreamsWriter(_uploadQueue.Object, _dicomToolkit.Object, _payloadAssembler.Object, _configuration, _logger.Object); + var writer = new StreamsWriter(_uploadQueue.Object, _dicomToolkit.Object, _payloadAssembler.Object, _configuration, _logger.Object, _fileSystem); var streams = GenerateDicomStreams(studyInstanceUid); var result = await writer.Save( @@ -94,7 +98,7 @@ public async Task GivingADicomInstanceWithoutAMatchingStudyInstanceUid_WhenSavei _payloadAssembler.Setup(p => p.Queue(It.IsAny(), It.IsAny(), It.IsAny())); var studyInstanceUid = DicomUIDGenerator.GenerateDerivedFromUUID().UID; - var writer = new StreamsWriter(_uploadQueue.Object, _dicomToolkit.Object, _payloadAssembler.Object, _configuration, _logger.Object); + var writer = new StreamsWriter(_uploadQueue.Object, _dicomToolkit.Object, _payloadAssembler.Object, _configuration, _logger.Object, _fileSystem); var correlationId = Guid.NewGuid().ToString(); var streams = GenerateDicomStreams(studyInstanceUid); @@ -132,7 +136,7 @@ public async Task GivingAValidDicomInstance_WhenSaveingInstance_ExpectInstanceTo _payloadAssembler.Setup(p => p.Queue(It.IsAny(), It.IsAny(), It.IsAny())); var studyInstanceUid = DicomUIDGenerator.GenerateDerivedFromUUID().UID; - var writer = new StreamsWriter(_uploadQueue.Object, _dicomToolkit.Object, _payloadAssembler.Object, _configuration, _logger.Object); + var writer = new StreamsWriter(_uploadQueue.Object, _dicomToolkit.Object, _payloadAssembler.Object, _configuration, _logger.Object, _fileSystem); var correlationId = Guid.NewGuid().ToString(); var streams = GenerateDicomStreams(studyInstanceUid); diff --git a/src/InformaticsGateway/Test/Services/Fhir/FhirJsonReaderTest.cs b/src/InformaticsGateway/Test/Services/Fhir/FhirJsonReaderTest.cs index d224f7a05..c2449c179 100644 --- a/src/InformaticsGateway/Test/Services/Fhir/FhirJsonReaderTest.cs +++ b/src/InformaticsGateway/Test/Services/Fhir/FhirJsonReaderTest.cs @@ -16,11 +16,14 @@ using System; using System.IO; +using System.IO.Abstractions; +using System.IO.Abstractions.TestingHelpers; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Microsoft.Net.Http.Headers; using Monai.Deploy.InformaticsGateway.Configuration; using Monai.Deploy.InformaticsGateway.Services.Fhir; @@ -31,13 +34,17 @@ namespace Monai.Deploy.InformaticsGateway.Test.Services.Fhir { public class FhirJsonReaderTest { - private readonly InformaticsGatewayConfiguration _config; private readonly Mock> _logger; + private readonly IOptions _options; + private readonly IFileSystem _fileSystem; public FhirJsonReaderTest() { - _config = new InformaticsGatewayConfiguration(); _logger = new Mock>(); + _options = Options.Create(new InformaticsGatewayConfiguration()); + _fileSystem = new MockFileSystem(); + + _options.Value.Storage.TemporaryDataStorage = TemporaryDataStorageLocation.Memory; } [Fact] @@ -46,7 +53,7 @@ public async Task GetContentAsync_WhenCalled_EnsuresArgumentsAreValid() var request = new Mock(); var correlationId = Guid.NewGuid().ToString(); var resourceType = "Patient"; - var reader = new FhirJsonReader(_logger.Object); + var reader = new FhirJsonReader(_logger.Object, _options, _fileSystem); await Assert.ThrowsAsync(async () => await reader.GetContentAsync(null, null, null, null, CancellationToken.None)); await Assert.ThrowsAsync(async () => await reader.GetContentAsync(request.Object, null, null, null, CancellationToken.None)); @@ -62,7 +69,7 @@ public async Task GetContentAsync_WhenCalledWithEmptyContent_ThrowsException() var correlationId = Guid.NewGuid().ToString(); var resourceType = "Patient"; var contentType = new MediaTypeHeaderValue(ContentTypes.ApplicationFhirJson); - var reader = new FhirJsonReader(_logger.Object); + var reader = new FhirJsonReader(_logger.Object, _options, _fileSystem); await Assert.ThrowsAsync(async () => { @@ -83,7 +90,7 @@ public async Task GetContentAsync_WhenCalledWithNonXmlContent_ThrowsException() var correlationId = Guid.NewGuid().ToString(); var resourceType = "Patient"; var contentType = new MediaTypeHeaderValue(ContentTypes.ApplicationFhirJson); - var reader = new FhirJsonReader(_logger.Object); + var reader = new FhirJsonReader(_logger.Object, _options, _fileSystem); await Assert.ThrowsAnyAsync(async () => { @@ -106,7 +113,7 @@ public async Task GetContentAsync_WhenCalledWithNoId_ReturnsOriginalWithId(strin var correlationId = Guid.NewGuid().ToString(); var resourceType = "Patient"; var contentType = new MediaTypeHeaderValue(ContentTypes.ApplicationFhirJson); - var reader = new FhirJsonReader(_logger.Object); + var reader = new FhirJsonReader(_logger.Object, _options, _fileSystem); var data = System.Text.Encoding.UTF8.GetBytes(xml); using var stream = new System.IO.MemoryStream(); diff --git a/src/InformaticsGateway/Test/Services/Fhir/FhirServiceTest.cs b/src/InformaticsGateway/Test/Services/Fhir/FhirServiceTest.cs index ccb22751e..95d61d121 100644 --- a/src/InformaticsGateway/Test/Services/Fhir/FhirServiceTest.cs +++ b/src/InformaticsGateway/Test/Services/Fhir/FhirServiceTest.cs @@ -16,6 +16,7 @@ using System; using System.IO; +using System.IO.Abstractions; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -43,6 +44,7 @@ public class FhirServiceTest private readonly Mock> _logger; private readonly Mock> _loggerJson; private readonly Mock> _loggerXml; + private readonly Mock _fileSystem; private readonly Mock _payloadAssembler; private readonly Mock _uploadQueue; @@ -62,6 +64,7 @@ public FhirServiceTest() _logger = new Mock>(); _loggerJson = new Mock>(); _loggerXml = new Mock>(); + _fileSystem = new Mock(); _httpRequest = new Mock(); @@ -73,11 +76,13 @@ public FhirServiceTest() services.AddScoped(p => _loggerXml.Object); services.AddScoped(p => _uploadQueue.Object); services.AddScoped(p => _payloadAssembler.Object); + services.AddScoped(p => _fileSystem.Object); _serviceProvider = services.BuildServiceProvider(); _serviceScopeFactory.Setup(p => p.CreateScope()).Returns(_serviceScope.Object); _serviceScope.Setup(p => p.ServiceProvider).Returns(_serviceProvider); _logger.Setup(p => p.IsEnabled(It.IsAny())).Returns(true); + _options.Value.Storage.TemporaryDataStorage = TemporaryDataStorageLocation.Memory; } [RetryFact] diff --git a/src/InformaticsGateway/Test/Services/Fhir/FhirXmlReaderTest.cs b/src/InformaticsGateway/Test/Services/Fhir/FhirXmlReaderTest.cs index 1475e954a..c01dd00cc 100644 --- a/src/InformaticsGateway/Test/Services/Fhir/FhirXmlReaderTest.cs +++ b/src/InformaticsGateway/Test/Services/Fhir/FhirXmlReaderTest.cs @@ -16,12 +16,16 @@ using System; using System.IO; +using System.IO.Abstractions; +using System.IO.Abstractions.TestingHelpers; using System.Threading; using System.Threading.Tasks; using System.Xml; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Microsoft.Net.Http.Headers; +using Monai.Deploy.InformaticsGateway.Configuration; using Monai.Deploy.InformaticsGateway.Services.Fhir; using Moq; using Xunit; @@ -31,10 +35,15 @@ namespace Monai.Deploy.InformaticsGateway.Test.Services.Fhir public class FhirXmlReaderTest { private readonly Mock> _logger; + private readonly IOptions _options; + private readonly IFileSystem _fileSystem; public FhirXmlReaderTest() { _logger = new Mock>(); + _options = Options.Create(new InformaticsGatewayConfiguration()); + _fileSystem = new MockFileSystem(); + _options.Value.Storage.TemporaryDataStorage = TemporaryDataStorageLocation.Memory; } [Fact] @@ -43,7 +52,7 @@ public async Task GetContentAsync_WhenCalled_EnsuresArgumentsAreValid() var request = new Mock(); var correlationId = Guid.NewGuid().ToString(); var resourceType = "Patient"; - var reader = new FhirXmlReader(_logger.Object); + var reader = new FhirXmlReader(_logger.Object, _options, _fileSystem); await Assert.ThrowsAsync(async () => await reader.GetContentAsync(null, null, null, null, CancellationToken.None)); await Assert.ThrowsAsync(async () => await reader.GetContentAsync(request.Object, null, null, null, CancellationToken.None)); @@ -59,7 +68,7 @@ public async Task GetContentAsync_WhenCalledWithEmptyContent_ThrowsException() var correlationId = Guid.NewGuid().ToString(); var resourceType = "Patient"; var contentType = new MediaTypeHeaderValue(ContentTypes.ApplicationFhirXml); - var reader = new FhirXmlReader(_logger.Object); + var reader = new FhirXmlReader(_logger.Object, _options, _fileSystem); await Assert.ThrowsAsync(async () => { @@ -80,7 +89,7 @@ public async Task GetContentAsync_WhenCalledWithNonXmlContent_ThrowsException() var correlationId = Guid.NewGuid().ToString(); var resourceType = "Patient"; var contentType = new MediaTypeHeaderValue(ContentTypes.ApplicationFhirXml); - var reader = new FhirXmlReader(_logger.Object); + var reader = new FhirXmlReader(_logger.Object, _options, _fileSystem); await Assert.ThrowsAsync(async () => { @@ -100,7 +109,7 @@ public async Task GetContentAsync_WhenCalledWithNonFhirContent_ThrowsException() var correlationId = Guid.NewGuid().ToString(); var resourceType = "Patient"; var contentType = new MediaTypeHeaderValue(ContentTypes.ApplicationFhirXml); - var reader = new FhirXmlReader(_logger.Object); + var reader = new FhirXmlReader(_logger.Object, _options, _fileSystem); await Assert.ThrowsAsync(async () => { @@ -123,7 +132,7 @@ public async Task GetContentAsync_WhenCalledWithNoId_ReturnsOriginalWithId(strin var correlationId = Guid.NewGuid().ToString(); var resourceType = "Patient"; var contentType = new MediaTypeHeaderValue(ContentTypes.ApplicationFhirXml); - var reader = new FhirXmlReader(_logger.Object); + var reader = new FhirXmlReader(_logger.Object, _options, _fileSystem); var data = System.Text.Encoding.UTF8.GetBytes(xml); using var stream = new System.IO.MemoryStream(); diff --git a/src/InformaticsGateway/Test/Services/HealthLevel7/MllpServiceTest.cs b/src/InformaticsGateway/Test/Services/HealthLevel7/MllpServiceTest.cs index d4f374425..5cfcecca8 100644 --- a/src/InformaticsGateway/Test/Services/HealthLevel7/MllpServiceTest.cs +++ b/src/InformaticsGateway/Test/Services/HealthLevel7/MllpServiceTest.cs @@ -16,6 +16,8 @@ using System; using System.Collections.Generic; +using System.IO; +using System.IO.Abstractions; using System.Net; using System.Threading; using System.Threading.Tasks; @@ -47,7 +49,7 @@ public class MllpServiceTest private readonly Mock _uploadQueue; private readonly Mock _payloadAssembler; private readonly Mock _tcpListener; - + private readonly Mock _fileSystem; private readonly CancellationTokenSource _cancellationTokenSource; private readonly Mock _serviceScope; private readonly Mock> _logger; @@ -64,6 +66,7 @@ public MllpServiceTest() _uploadQueue = new Mock(); _payloadAssembler = new Mock(); _tcpListener = new Mock(); + _fileSystem = new Mock(); _cancellationTokenSource = new CancellationTokenSource(); _serviceScope = new Mock(); @@ -77,10 +80,14 @@ public MllpServiceTest() services.AddScoped(p => _mllpClientFactory.Object); services.AddScoped(p => _uploadQueue.Object); services.AddScoped(p => _payloadAssembler.Object); + services.AddScoped(p => _fileSystem.Object); _serviceProvider = services.BuildServiceProvider(); _serviceScopeFactory.Setup(p => p.CreateScope()).Returns(_serviceScope.Object); _serviceScope.Setup(p => p.ServiceProvider).Returns(_serviceProvider); + _fileSystem.Setup(p => p.Path.Combine(It.IsAny(), It.IsAny())).Returns((string path1, string path2) => System.IO.Path.Combine(path1, path2)); + _fileSystem.Setup(p => p.File.Create(It.IsAny())).Returns(FileStream.Null); + _loggerFactory.Setup(p => p.CreateLogger(It.IsAny())).Returns(_logger.Object); _tcpListenerFactory.Setup(p => p.CreateTcpListener(It.IsAny(), It.IsAny())).Returns(_tcpListener.Object); _logger.Setup(p => p.IsEnabled(It.IsAny())).Returns(true); diff --git a/src/InformaticsGateway/Test/Services/Http/MonaiHealthCheckTest.cs b/src/InformaticsGateway/Test/Services/Http/MonaiHealthCheckTest.cs new file mode 100644 index 000000000..ebf559a3f --- /dev/null +++ b/src/InformaticsGateway/Test/Services/Http/MonaiHealthCheckTest.cs @@ -0,0 +1,90 @@ +/* + * Copyright 2022 MONAI Consortium + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Monai.Deploy.InformaticsGateway.Repositories; +using Monai.Deploy.InformaticsGateway.Services.Http; +using Moq; +using Xunit; + +namespace Monai.Deploy.InformaticsGateway.Test.Services.Http +{ + public class MonaiHealthCheckTest + { + private readonly Mock _monaiServiceLocator; + + public MonaiHealthCheckTest() + { + _monaiServiceLocator = new Mock(); + } + + [Fact] + public async Task GivenAllServicesRunning_WhenCheckHealthAsyncIsCalled_ReturnsHealthy() + { + _monaiServiceLocator.Setup(p => p.GetServiceStatus()).Returns(new Dictionary() + { + { "A", Api.Rest.ServiceStatus.Running }, + { "B", Api.Rest.ServiceStatus.Running }, + { "C", Api.Rest.ServiceStatus.Running }, + }); + + var svc = new MonaiHealthCheck(_monaiServiceLocator.Object); + var result = await svc.CheckHealthAsync(null); + Assert.Equal(HealthStatus.Healthy, result.Status); + } + + [Fact] + public async Task GivenSomeServicesNotRunning_WhenCheckHealthAsyncIsCalled_ReturnsDegraded() + { + _monaiServiceLocator.Setup(p => p.GetServiceStatus()).Returns(new Dictionary() + { + { "A", Api.Rest.ServiceStatus.Running }, + { "B", Api.Rest.ServiceStatus.Cancelled }, + { "C", Api.Rest.ServiceStatus.Stopped }, + }); + + var svc = new MonaiHealthCheck(_monaiServiceLocator.Object); + var result = await svc.CheckHealthAsync(null); + Assert.Equal(HealthStatus.Degraded, result.Status); + Assert.Equal(Api.Rest.ServiceStatus.Cancelled, result.Data["B"]); + Assert.Equal(Api.Rest.ServiceStatus.Stopped, result.Data["C"]); + } + + [Fact] + public async Task GivenAllServicesNotRunning_WhenCheckHealthAsyncIsCalled_ReturnsUnhealthy() + { + _monaiServiceLocator.Setup(p => p.GetServiceStatus()).Returns(new Dictionary() + { + { "A", Api.Rest.ServiceStatus.Stopped }, + { "B", Api.Rest.ServiceStatus.Cancelled }, + { "C", Api.Rest.ServiceStatus.Stopped }, + }); + + var svc = new MonaiHealthCheck(_monaiServiceLocator.Object); + var result = await svc.CheckHealthAsync(null); + + Assert.Equal(HealthStatus.Unhealthy, result.Status); + Assert.Equal(Api.Rest.ServiceStatus.Stopped, result.Data["A"]); + Assert.Equal(Api.Rest.ServiceStatus.Cancelled, result.Data["B"]); + Assert.Equal(Api.Rest.ServiceStatus.Stopped, result.Data["C"]); + } + } +} diff --git a/src/InformaticsGateway/Test/Services/Scp/ApplicationEntityHandlerTest.cs b/src/InformaticsGateway/Test/Services/Scp/ApplicationEntityHandlerTest.cs index 0d7cad791..6edd2d50f 100644 --- a/src/InformaticsGateway/Test/Services/Scp/ApplicationEntityHandlerTest.cs +++ b/src/InformaticsGateway/Test/Services/Scp/ApplicationEntityHandlerTest.cs @@ -16,14 +16,18 @@ using System; using System.Collections.Generic; +using System.IO; +using System.IO.Abstractions; using System.Threading.Tasks; using FellowOakDicom; using FellowOakDicom.Network; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Monai.Deploy.InformaticsGateway.Api; using Monai.Deploy.InformaticsGateway.Api.Storage; using Monai.Deploy.InformaticsGateway.Common; +using Monai.Deploy.InformaticsGateway.Configuration; using Monai.Deploy.InformaticsGateway.Services.Connectors; using Monai.Deploy.InformaticsGateway.Services.Scp; using Monai.Deploy.InformaticsGateway.Services.Storage; @@ -41,7 +45,8 @@ public class ApplicationEntityHandlerTest private readonly Mock _serviceScope; private readonly Mock _payloadAssembler; private readonly Mock _uploadQueue; - + private readonly IOptions _options; + private readonly Mock _fileSystem; private readonly IServiceProvider _serviceProvider; public ApplicationEntityHandlerTest() @@ -52,24 +57,30 @@ public ApplicationEntityHandlerTest() _payloadAssembler = new Mock(); _uploadQueue = new Mock(); + _options = Options.Create(new InformaticsGatewayConfiguration()); + _fileSystem = new Mock(); var services = new ServiceCollection(); services.AddScoped(p => _payloadAssembler.Object); services.AddScoped(p => _uploadQueue.Object); + services.AddScoped(p => _fileSystem.Object); _serviceProvider = services.BuildServiceProvider(); _serviceScopeFactory.Setup(p => p.CreateScope()).Returns(_serviceScope.Object); _serviceScope.Setup(p => p.ServiceProvider).Returns(_serviceProvider); + _fileSystem.Setup(p => p.Path.Combine(It.IsAny(), It.IsAny())).Returns((string path1, string path2) => System.IO.Path.Combine(path1, path2)); + _fileSystem.Setup(p => p.File.Create(It.IsAny())).Returns(FileStream.Null); _logger.Setup(p => p.IsEnabled(It.IsAny())).Returns(true); } [RetryFact(5, 250)] public void GivenAApplicationEntityHandler_WhenInitialized_ExpectParametersToBeValidated() { - Assert.Throws(() => new ApplicationEntityHandler(null, null)); - Assert.Throws(() => new ApplicationEntityHandler(_serviceScopeFactory.Object, null)); + Assert.Throws(() => new ApplicationEntityHandler(null, null, null)); + Assert.Throws(() => new ApplicationEntityHandler(_serviceScopeFactory.Object, null, null)); + Assert.Throws(() => new ApplicationEntityHandler(_serviceScopeFactory.Object, _logger.Object, null)); - _ = new ApplicationEntityHandler(_serviceScopeFactory.Object, _logger.Object); + _ = new ApplicationEntityHandler(_serviceScopeFactory.Object, _logger.Object, _options); } [RetryFact(5, 250)] @@ -83,7 +94,7 @@ public async Task GivenAApplicationEntityHandler_WhenHandleInstanceAsyncIsCalled IgnoredSopClasses = new List { DicomUID.SecondaryCaptureImageStorage.UID } }; - var handler = new ApplicationEntityHandler(_serviceScopeFactory.Object, _logger.Object); + var handler = new ApplicationEntityHandler(_serviceScopeFactory.Object, _logger.Object, _options); var request = GenerateRequest(); var dicomToolkit = new DicomToolkit(); @@ -103,7 +114,7 @@ public async Task GivenACStoreRequest_WhenTheSopClassIsInTheIgnoreList_ExpectIns IgnoredSopClasses = new List { DicomUID.SecondaryCaptureImageStorage.UID } }; - var handler = new ApplicationEntityHandler(_serviceScopeFactory.Object, _logger.Object); + var handler = new ApplicationEntityHandler(_serviceScopeFactory.Object, _logger.Object, _options); handler.Configure(aet, Configuration.DicomJsonOptions.Complete, true); var request = GenerateRequest(); @@ -127,7 +138,7 @@ public async Task GivenACStoreRequest_WhenTheSopClassIsNotInTheAllowedList_Expec AllowedSopClasses = new List { DicomUID.UltrasoundImageStorage.UID } }; - var handler = new ApplicationEntityHandler(_serviceScopeFactory.Object, _logger.Object); + var handler = new ApplicationEntityHandler(_serviceScopeFactory.Object, _logger.Object, _options); handler.Configure(aet, Configuration.DicomJsonOptions.Complete, true); var request = GenerateRequest(); @@ -150,7 +161,7 @@ public async Task GivenACStoreRequest_WhenHandleInstanceAsyncIsCalled_ExpectADic Workflows = new List() { "AppA", "AppB", Guid.NewGuid().ToString() } }; - var handler = new ApplicationEntityHandler(_serviceScopeFactory.Object, _logger.Object); + var handler = new ApplicationEntityHandler(_serviceScopeFactory.Object, _logger.Object, _options); handler.Configure(aet, Configuration.DicomJsonOptions.Complete, true); var request = GenerateRequest(); diff --git a/src/InformaticsGateway/Test/Services/Storage/ObjectUploadServiceTest.cs b/src/InformaticsGateway/Test/Services/Storage/ObjectUploadServiceTest.cs index 5a7e445c3..d844eaec8 100644 --- a/src/InformaticsGateway/Test/Services/Storage/ObjectUploadServiceTest.cs +++ b/src/InformaticsGateway/Test/Services/Storage/ObjectUploadServiceTest.cs @@ -45,7 +45,6 @@ public class ObjectUploadServiceTest private readonly IObjectUploadQueue _uploadQueue; private readonly Mock _storageService; private readonly Mock _storageMetadataWrapperRepository; - private readonly CancellationTokenSource _cancellationTokenSource; private readonly ServiceProvider _serviceProvider; private readonly Mock _serviceScope; @@ -124,7 +123,7 @@ public async Task GivenADicomFileStorageMetadata_WhenQueuedForUpload_ExpectTwoFi } [Fact] - public void GivenAFhirFileStorageMetadata_WhenQueuedForUpload_ExpectSingleFileToBeUploaded() + public async Task GivenAFhirFileStorageMetadata_WhenQueuedForUpload_ExpectSingleFileToBeUploaded() { var countdownEvent = new CountdownEvent(1); _storageService.Setup(p => p.PutObjectAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny())) @@ -137,7 +136,7 @@ public void GivenAFhirFileStorageMetadata_WhenQueuedForUpload_ExpectSingleFileTo Assert.Equal(ServiceStatus.Running, svc.Status); - var file = GenerateFhirFileStorageMetadata(); + var file = await GenerateFhirFileStorageMetadata(); _uploadQueue.Queue(file); Assert.True(countdownEvent.Wait(TimeSpan.FromSeconds(3))); @@ -145,11 +144,11 @@ public void GivenAFhirFileStorageMetadata_WhenQueuedForUpload_ExpectSingleFileTo _storageService.Verify(p => p.PutObjectAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny()), Times.Once()); } - private FhirFileStorageMetadata GenerateFhirFileStorageMetadata() + private async Task GenerateFhirFileStorageMetadata() { var file = new FhirFileStorageMetadata(Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), FhirStorageFormat.Json); - file.SetDataStream("[]"); + await file.SetDataStream("[]", TemporaryDataStorageLocation.Memory); return file; } @@ -169,7 +168,7 @@ private async Task GenerateDicomFileStorageMetadata() { DicomTag.SOPClassUID, DicomUID.SecondaryCaptureImageStorage.UID } }; var dicomFile = new DicomFile(dataset); - await file.SetDataStreams(dicomFile, "[]"); + await file.SetDataStreams(dicomFile, "[]", TemporaryDataStorageLocation.Memory); return file; } } diff --git a/tests/Integration.Test/Monai.Deploy.InformaticsGateway.Integration.Test.csproj b/tests/Integration.Test/Monai.Deploy.InformaticsGateway.Integration.Test.csproj index f0966a510..48c0ec7eb 100644 --- a/tests/Integration.Test/Monai.Deploy.InformaticsGateway.Integration.Test.csproj +++ b/tests/Integration.Test/Monai.Deploy.InformaticsGateway.Integration.Test.csproj @@ -33,8 +33,8 @@ - - + + diff --git a/tests/Integration.Test/configs/informatics-gateway.json b/tests/Integration.Test/configs/informatics-gateway.json index 8686009f8..c6306be8f 100644 --- a/tests/Integration.Test/configs/informatics-gateway.json +++ b/tests/Integration.Test/configs/informatics-gateway.json @@ -38,7 +38,7 @@ } }, "storage": { - "bufferRootPath": "./temp", + "bufferRootPath": "/payloads", "tempStorageRootPath": "/incoming", "bucketName": "monaideploy", "storageRootPath": "/payloads", diff --git a/tests/Integration.Test/debug.sh b/tests/Integration.Test/debug.sh index c0fe2d81c..e43986ea6 100755 --- a/tests/Integration.Test/debug.sh +++ b/tests/Integration.Test/debug.sh @@ -55,9 +55,9 @@ function env_setup() { [ -d $BIN_DIR ] && info "Removing $BIN_DIR..." && sudo rm -r $BIN_DIR - if [[ $(docker-compose ps -q | wc -l) -ne 0 ]]; then + if [[ $(docker compose ps -q | wc -l) -ne 0 ]]; then info "Stopping existing services..." - docker-compose $LOADDEV down + docker compose $LOADDEV down fi if (dotnet tool list --global | grep livingdoc &>/dev/null); then @@ -78,8 +78,8 @@ function build() { } function start_services() { - info "Starting dependencies docker-compose $LOADDEV up -d --force-recreate..." - docker-compose $LOADDEV up -d --force-recreate + info "Starting dependencies docker compose $LOADDEV up -d --force-recreate..." + docker compose $LOADDEV up -d --force-recreate HOST_IP=$(docker network inspect testrunner | jq -r .[0].IPAM.Config[0].Gateway) info "Host IP = $HOST_IP" @@ -107,14 +107,14 @@ function tear_down() { set -e info "Stopping services..." - docker-compose $LOADDEV down --remove-orphans + docker compose $LOADDEV down --remove-orphans } function main() { env_setup "$@" build start_services - docker-compose logs -f + docker compose logs -f } main "$@" diff --git a/tests/Integration.Test/run.sh b/tests/Integration.Test/run.sh index b6c129b43..fd4074d39 100755 --- a/tests/Integration.Test/run.sh +++ b/tests/Integration.Test/run.sh @@ -87,9 +87,9 @@ function env_setup() { esac done - if [[ $(docker-compose ps -q | wc -l) -ne 0 ]]; then + if [[ $(docker compose ps -q | wc -l) -ne 0 ]]; then info "Stopping existing services..." - docker-compose $LOADDEV down + docker compose $LOADDEV down fi if (dotnet tool list --global | grep livingdoc &>/dev/null); then @@ -110,8 +110,8 @@ function build() { } function start_services() { - info "Starting dependencies docker-compose $LOADDEV up -d --force-recreate..." - docker-compose $LOADDEV up -d --force-recreate + info "Starting dependencies docker compose $LOADDEV up -d --force-recreate..." + docker compose $LOADDEV up -d --force-recreate HOST_IP=$(docker network inspect testrunner | jq -r .[0].IPAM.Config[0].Gateway) info "Host IP = $HOST_IP" @@ -190,7 +190,7 @@ function generate_reports() { function save_logs() { [ -d $RUN_DIR ] && info "Clearning $RUN_DIR..." && sudo rm -r $RUN_DIR info "Saving service log..." - docker-compose $LOADDEV logs --no-color -t > "$LOG_DIR/services.log" + docker compose $LOADDEV logs --no-color -t > "$LOG_DIR/services.log" } function tear_down() { @@ -200,7 +200,7 @@ function tear_down() { set -e info "Stopping services..." - docker-compose $LOADDEV down --remove-orphans + docker compose $LOADDEV down --remove-orphans } function main() {