diff --git a/README.md b/README.md index 9f42d67..b10ab18 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ You can build the source code with the following steps: 2. Change into the directory you have cloned this repository -3. Run `npm run install` command to build source code +3. Run `npm run build` command to build source code 4. Enable the DICOM Viewer app in Nextcloud diff --git a/lib/Controller/DisplayController.php b/lib/Controller/DisplayController.php index e1dd072..9bcbf64 100755 --- a/lib/Controller/DisplayController.php +++ b/lib/Controller/DisplayController.php @@ -7,7 +7,9 @@ include_once realpath(dirname(__FILE__)).DIRECTORY_SEPARATOR.'..'.DIRECTORY_SEPARATOR.'Nanodicom'.DIRECTORY_SEPARATOR.'nanodicom.php'; use Nanodicom; +use OC\Files\Filesystem; use OCA\DICOMViewer\AppInfo\Application; +use OCP\App\IAppManager; use OCP\AppFramework\Controller; use OCP\AppFramework\Http\EmptyContentSecurityPolicy; use OCP\AppFramework\Http\TemplateResponse; @@ -19,12 +21,14 @@ use OCP\ILogger; use OCP\IRequest; use OCP\IURLGenerator; +use OCP\IUserSession; use OCP\Share\IManager; class DisplayController extends Controller { /** @var IURLGenerator */ private $urlGenerator; + private ?IAppManager $appManager = null; /** * @param IRequest $request @@ -36,7 +40,8 @@ public function __construct(IConfig $config, ILogger $logger, IMimeTypeDetector $mimeTypeDetector, IRootFolder $rootFolder, - IManager $shareManager) { + IManager $shareManager, + IUserSession $userSession) { parent::__construct(Application::APP_ID, $request); $this->config = $config; $this->urlGenerator = $urlGenerator; @@ -44,22 +49,31 @@ public function __construct(IConfig $config, $this->mimeTypeDetector = $mimeTypeDetector; $this->rootFolder = $rootFolder; $this->shareManager = $shareManager; + $this->userSession = $userSession; $this->publicViewerFolderPath = null; $this->publicViewerAssetsFolderPath = null; - $appsPaths = $this->config->getSystemValue('apps_paths'); - foreach($appsPaths as $appsPath) { - $viewerFolder = $appsPath['path'] . '/dicomviewer/js/public/viewer'; - if (file_exists($viewerFolder)) { - $this->publicViewerFolderPath = $viewerFolder; - $this->publicViewerAssetsFolderPath = $viewerFolder . '/assets'; - break; - } + + $app_path = $this->getAppManager()->getAppPath('dicomviewer'); + $viewerFolder = $app_path . '/js/public/viewer'; + if (file_exists($viewerFolder)) { + $this->publicViewerFolderPath = $viewerFolder; + $this->publicViewerAssetsFolderPath = $viewerFolder . '/assets'; + } else { + $this->logger->error('Unable to find dicom viewer folder: ' . $viewerFolder); } $this->dataFolder = $this->config->getSystemValue('datadirectory'); } + private function getAppManager(): IAppManager { + if ($this->appManager !== null) { + return $this->appManager; + } + $this->appManager = \OCP\Server::get(IAppManager::class); + return $this->appManager; + } + private function getNextcloudBasePath() { if ($this->config->getSystemValueBool('htaccess.IgnoreFrontController', false) || getenv('front_controller_active') === 'true') { return $this->urlGenerator->getWebroot(); @@ -107,13 +121,13 @@ private function cleanDICOMTagValue($value) { return $value; } - private function getAllDICOMFilesInFolder($parentPathToRemove, $folderNode) { + private function getAllDICOMFilesInFolder($parentPathToRemove, $folderNode, $isOpenNoExtension) { $filepaths = array(); $nodes = $folderNode->getDirectoryListing(); foreach($nodes as $node) { if ($node->getType() == 'dir') { - $filepaths = array_merge($filepaths, $this->getAllDICOMFilesInFolder($parentPathToRemove, $node)); - } else if ($node->getType() == 'file' && $node->getMimetype() == 'application/dicom') { + $filepaths = array_merge($filepaths, $this->getAllDICOMFilesInFolder($parentPathToRemove, $node, $isOpenNoExtension)); + } else if ($node->getType() == 'file' && ($isOpenNoExtension || $node->getMimetype() == 'application/dicom')) { array_push($filepaths, implode('', explode($parentPathToRemove, $node->getPath(), 2))); } } @@ -139,7 +153,7 @@ private function getContentSecurityPolicy() { return $policy; } - private function generateDICOMJson($dicomFilePaths, $selectedFileFullPath, $parentFullPath, $downloadUrlPrefix, $isPublic, $singlePublicFileDownload) { + private function generateDICOMJson($dicomFilePaths, $selectedFileFullPath, $parentFullPath, $currentUserPathToFile, $downloadUrlPrefix, $isPublic, $singlePublicFileDownload) { $dicomJson = array('studies' => array()); foreach($dicomFilePaths as $dicomFilePath) { @@ -153,6 +167,8 @@ private function generateDICOMJson($dicomFilePaths, $selectedFileFullPath, $pare $urlParamFiles = substr($dicomFilePath, strrpos($dicomFilePath, '/') + 1); $fileUrlPath = $downloadUrlPrefix.'?path='.$urlParamPath.'&files='.$urlParamFiles; } + } else if ($currentUserPathToFile != null) { + $fileUrlPath = $downloadUrlPrefix.strstr($dicomFilePath, $currentUserPathToFile); } else { $fileUrlPath = $downloadUrlPrefix.$dicomFilePath; } @@ -161,6 +177,12 @@ private function generateDICOMJson($dicomFilePaths, $selectedFileFullPath, $pare $fileFullPath = $parentFullPath.$dicomFilePath; $dicom = Nanodicom::factory($fileFullPath); + + if (!$dicom->is_dicom()) { + // Do not parse if it is not a DICOM file + continue; + } + $dicom->parse()->profiler_diff('parse'); $StudyInstanceUID = $this->cleanDICOMTagValue($dicom->value(0x0020, 0x000D)); @@ -425,23 +447,28 @@ public function getDICOMViewerAssetSub(string $assetpath): StreamResponse { public function getDICOMJson(): JSONResponse { $fileQueryParams = explode('|', $this->getQueryParam('file')); $userId = $fileQueryParams[0]; - $filepath = ltrim($fileQueryParams[1], '/'); + $fileid = $fileQueryParams[1]; + $isOpenNoExtension = count($fileQueryParams) > 2 && $fileQueryParams[2] == 1; + // Find the file path located in the filesystem $userFolder = $this->rootFolder->getUserFolder($userId); - $selectedFileFullPath = $this->dataFolder.$userFolder->get($filepath)->getPath(); - $dicomFolder = $userFolder->get($filepath)->getParent(); + $file = $userFolder->getById((int)$fileid)[0]; + $selectedFileFullPath = $file->getType() == 'dir' ? null : $this->dataFolder.$file->getPath(); - $parentPathToRemove = $dicomFolder->getParent()->getPath(); - if ($userFolder->getPath() == $dicomFolder->getPath()) { - $parentPathToRemove = '/'.$userId.'/files'; - } + // Find the file path by current user (e.g. file path in the shared folder) + $currentUser = $this->userSession->getUser(); + $currentUserId = $currentUser->getUID(); + $currentUserFolder = $this->rootFolder->getUserFolder($currentUserId); + $currentUserPathToFile = implode('', explode($currentUserFolder->getPath(), $currentUserFolder->getById((int)$fileid)[0]->getParent()->getPath(), 2)); // Get all DICOM files in the folder and sub folders - $dicomFilePaths = $this->getAllDICOMFilesInFolder($parentPathToRemove, $dicomFolder); + $parentPathToRemove = '/'.$userId.'/files'; + $dicomFolder = $file->getType() == 'dir' ? $file : $file->getParent(); + $dicomFilePaths = $this->getAllDICOMFilesInFolder($parentPathToRemove, $dicomFolder, $isOpenNoExtension); $dicomParentFullPath = $this->dataFolder.'/'.$userId.'/files'; - $downloadUrlPrefix = 'remote.php/dav/files/'.$userId; - $dicomJson = $this->generateDICOMJson($dicomFilePaths, $selectedFileFullPath, $dicomParentFullPath, $downloadUrlPrefix, false, false); + $downloadUrlPrefix = 'remote.php/dav/files/'.$currentUserId; + $dicomJson = $this->generateDICOMJson($dicomFilePaths, $selectedFileFullPath, $dicomParentFullPath, $currentUserPathToFile, $downloadUrlPrefix, false, false); $response = new JSONResponse($dicomJson); return $response; } @@ -476,7 +503,7 @@ public function getPublicDICOMJson(): JSONResponse { // Get all DICOM files in the share folder and sub folders $parentPathToRemove = $shareNode->getPath(); - $dicomFilePaths = $this->getAllDICOMFilesInFolder($parentPathToRemove, $shareNode); + $dicomFilePaths = $this->getAllDICOMFilesInFolder($parentPathToRemove, $shareNode, false); } else { $selectedFileFullPath = null; $dicomParentFullPath = $this->dataFolder; @@ -487,7 +514,7 @@ public function getPublicDICOMJson(): JSONResponse { } $downloadUrlPrefix = $this->getNextcloudBasePath().'/s/'.$shareToken.'/download'; - $dicomJson = $this->generateDICOMJson($dicomFilePaths, $selectedFileFullPath, $dicomParentFullPath, $downloadUrlPrefix, true, $singlePublicFileDownload); + $dicomJson = $this->generateDICOMJson($dicomFilePaths, $selectedFileFullPath, $dicomParentFullPath, null, $downloadUrlPrefix, true, $singlePublicFileDownload); $response = new JSONResponse($dicomJson); return $response; diff --git a/package-lock.json b/package-lock.json index 233eea8..2c2bb4c 100755 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,8 @@ "@nextcloud/auth": "^2.2.1", "@nextcloud/axios": "^2.4.0", "@nextcloud/dialogs": "^5.0.3", + "@nextcloud/files": "^3.1.1", + "@nextcloud/l10n": "^2.2.0", "@nextcloud/logger": "^2.7.0", "@nextcloud/router": "^2.2.0", "cornerstone-core": "^2.2.8", @@ -1873,9 +1875,9 @@ "peer": true }, "node_modules/@buttercup/fetch": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/@buttercup/fetch/-/fetch-0.1.2.tgz", - "integrity": "sha512-mDBtsysQ0Gnrp4FamlRJGpu7HUHwbyLC4uUav1I7QAqThFAa/4d1cdZCxrV5gKvh6zO1fu95bILNJi4Y2hALhQ==", + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@buttercup/fetch/-/fetch-0.2.1.tgz", + "integrity": "sha512-sCgECOx8wiqY8NN1xN22BqqKzXYIG2AicNLlakOAI4f0WgyLVUbAigMf8CZhBtJxdudTcB1gD5lciqi44jwJvg==", "optionalDependencies": { "node-fetch": "^3.3.0" } @@ -3338,23 +3340,35 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "node_modules/@nextcloud/files": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@nextcloud/files/-/files-3.0.0.tgz", - "integrity": "sha512-zk5oIuVDyk2gWBKCJ+0B1HE3VjhuGnz2iLNbTcbRuTjMYb6aYCAEn1LY0dXbUQG93ehndYJCOdaYri/TaGrlXw==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@nextcloud/files/-/files-3.1.1.tgz", + "integrity": "sha512-PwGxh/AcKeDehYSf/L+OpYNzZ2eK5xA1l/lVjufwa7I+u2onCo6qjYSqvc9Dh4Myzixjmt5YiA+Um/gx/Kq4NA==", "dependencies": { "@nextcloud/auth": "^2.2.1", "@nextcloud/l10n": "^2.2.0", "@nextcloud/logger": "^2.7.0", "@nextcloud/paths": "^2.1.0", - "@nextcloud/router": "^2.2.0", + "@nextcloud/router": "^3.0.0", "is-svg": "^5.0.0", - "webdav": "^5.3.0" + "webdav": "^5.4.0" }, "engines": { "node": "^20.0.0", "npm": "^9.0.0" } }, + "node_modules/@nextcloud/files/node_modules/@nextcloud/router": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@nextcloud/router/-/router-3.0.0.tgz", + "integrity": "sha512-RlPrOPw94yT9rmt3+2sUs2cmWzqhX5eFW+i/EHymJEKgURVtnqCcXjIcAiLTfgsCCdAS1hGapBL8j8rhHk1FHQ==", + "dependencies": { + "@nextcloud/typings": "^1.7.0" + }, + "engines": { + "node": "^20.0.0", + "npm": "^10.0.0" + } + }, "node_modules/@nextcloud/initial-state": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@nextcloud/initial-state/-/initial-state-2.1.0.tgz", @@ -18094,20 +18108,20 @@ } }, "node_modules/web-streams-polyfill": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz", - "integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", "optional": true, "engines": { "node": ">= 8" } }, "node_modules/webdav": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/webdav/-/webdav-5.3.1.tgz", - "integrity": "sha512-wzZdTHtMuSIXqHGBznc8FM2L94Mc/17Tbn9ppoMybRO0bjWOSIeScdVXWX5qqHsg00EjfiOcwMqGFx6ghIhccQ==", + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/webdav/-/webdav-5.5.0.tgz", + "integrity": "sha512-SHSDe6n8lBuwwyX+uePB1N1Yn35ebd3locl/LbADMWpcEoowyFdIbnH3fv17T4Jf2tOa1Vwjr/Lld3t0dOio1w==", "dependencies": { - "@buttercup/fetch": "^0.1.1", + "@buttercup/fetch": "^0.2.1", "base-64": "^1.0.0", "byte-length": "^1.0.2", "fast-xml-parser": "^4.2.4", diff --git a/package.json b/package.json index 9814baa..be5ddfe 100755 --- a/package.json +++ b/package.json @@ -19,7 +19,9 @@ "@nextcloud/auth": "^2.2.1", "@nextcloud/axios": "^2.4.0", "@nextcloud/dialogs": "^5.0.3", + "@nextcloud/files": "^3.1.1", "@nextcloud/logger": "^2.7.0", + "@nextcloud/l10n": "^2.2.0", "@nextcloud/router": "^2.2.0", "cornerstone-core": "^2.2.8", "cornerstone-math": "^0.1.7", diff --git a/src/main.js b/src/main.js index be86fea..e381957 100755 --- a/src/main.js +++ b/src/main.js @@ -1,5 +1,6 @@ import DICOMView from './views/DICOMView.vue'; import generateFullUrl from './utils/generateFullUrl'; +import registerFileActions from './utils/registerFileActions'; import './sidebar'; // Add MimeType Icon @@ -17,3 +18,5 @@ OCA.Viewer.registerHandler({ canCompare: true, }); + +registerFileActions(); diff --git a/src/utils/AppIcon.js b/src/utils/AppIcon.js new file mode 100644 index 0000000..962d850 --- /dev/null +++ b/src/utils/AppIcon.js @@ -0,0 +1,18 @@ +const AppIcon = ` + + + + + dcm + +`; + +export default AppIcon; diff --git a/src/utils/registerFileActions.js b/src/utils/registerFileActions.js new file mode 100644 index 0000000..40935dc --- /dev/null +++ b/src/utils/registerFileActions.js @@ -0,0 +1,39 @@ +import { registerFileAction, FileAction, FileType, Permission } from '@nextcloud/files'; +import { translate as t } from '@nextcloud/l10n'; +import { generateUrl } from "@nextcloud/router"; +import AppIcon from './AppIcon'; + +function openWithDICOMViewer(node) { + const dicomUrl = window.location.protocol + '//' + window.location.host + generateUrl(`/apps/dicomviewer/dicomjson?file=${node.owner}|${node.fileid}|1`); + + // Open viewer in a new tab + const tab = window.open('about:blank'); + tab.location = generateUrl(`/apps/dicomviewer/ncviewer/viewer/dicomjson?url=${dicomUrl}`); + tab.focus(); +} + +const fileAction = new FileAction({ + id: 'dicomviewer', + order: -10000, + iconSvgInline() { + return AppIcon; + }, + displayName() { + return t('dicomviewer', 'Open with DICOM Viewer'); + }, + enabled(nodes) { + return nodes.length === 1 && (nodes[0].permissions & Permission.READ) !== 0 && nodes[0].type === FileType.Folder; + }, + async execBatch(nodes, view, dir) { + openWithDICOMViewer(nodes[0]); + return Promise.all([Promise.resolve(true)]); + }, + async exec(node, view, dir) { + openWithDICOMViewer(node); + return true; + }, +}); + +export default () => { + registerFileAction(fileAction); +}; diff --git a/src/views/DICOMView.vue b/src/views/DICOMView.vue index 61202e7..a310307 100755 --- a/src/views/DICOMView.vue +++ b/src/views/DICOMView.vue @@ -20,14 +20,13 @@ export default { const shareToken = getPublicShareToken(); dicomUrl = shareToken && window.location.protocol + '//' + window.location.host + generateUrl(`/apps/dicomviewer/publicdicomjson?file=${shareToken}|${file.filename}`); } else { - dicomUrl = window.location.protocol + '//' + window.location.host + generateUrl(`/apps/dicomviewer/dicomjson?file=${file.ownerId}|${file.filename}`); + dicomUrl = window.location.protocol + '//' + window.location.host + generateUrl(`/apps/dicomviewer/dicomjson?file=${file.ownerId}|${file.fileid}`); } - if (dicomUrl) { - const tab = window.open('about:blank'); - tab.location = generateUrl(`/apps/dicomviewer/ncviewer/viewer/dicomjson?url=${dicomUrl}`); - tab.focus(); - } + // Open viewer in a new tab + const tab = window.open('about:blank'); + tab.location = generateUrl(`/apps/dicomviewer/ncviewer/viewer/dicomjson?url=${dicomUrl}`); + tab.focus(); // Close the loading modal this.$parent.close();