From 25e8a85a3ee7e82303b06ceb910c676b99e0a8ba Mon Sep 17 00:00:00 2001 From: MaximeBICMTL Date: Fri, 1 May 2026 15:36:17 +0000 Subject: [PATCH 1/2] on demand tar file download --- .../electrophysiology/recording.class.inc | 41 ++++++++++++++++--- .../jsx/components/DownloadPanel.js | 37 +++++++++++++---- .../jsx/electrophysiologySessionView.js | 3 ++ src/Http/FileStream.php | 34 +++++++++++++++ 4 files changed, 101 insertions(+), 14 deletions(-) diff --git a/modules/api/php/endpoints/candidate/visit/electrophysiology/recording.class.inc b/modules/api/php/endpoints/candidate/visit/electrophysiology/recording.class.inc index 8a157798ed..8eb91fd945 100644 --- a/modules/api/php/endpoints/candidate/visit/electrophysiology/recording.class.inc +++ b/modules/api/php/endpoints/candidate/visit/electrophysiology/recording.class.inc @@ -170,12 +170,9 @@ class Recording extends Endpoint implements \LORIS\Middleware\ETagCalculator return new \LORIS\Http\Response\JSON\NotFound($e->getMessage()); } - $mimetype = substr($recording->getMetadata('header'), 0, 4) === 'hdf5' ? - 'application/x.minc2' : 'application/octet-stream'; - $info = $recording->getFileInfo(); - if (!$info->isFile()) { + if (!$info->isFile() && !$info->isDir()) { error_log('file in database but not in file system'); return new \LORIS\Http\Response\JSON\NotFound(); } @@ -185,13 +182,45 @@ class Recording extends Endpoint implements \LORIS\Middleware\ETagCalculator return new \LORIS\Http\Response\JSON\NotFound(); } - $body = new \LORIS\Http\FileStream($info->getRealPath(), 'r'); + // Tar the acquisition file if it is actually a directory (such as for MEG + // CTF acquisitions). + if ($info->isDir()) { + $filename = $this->_filename . '.tar'; + $mimetype = 'application/x-tar'; + + $tarfile = sys_get_temp_dir() . '/recording_' . uniqid() . '.tar'; + + try { + $phar = new \PharData($tarfile); + $phar->buildFromDirectory($info->getRealPath()); + } catch (\Exception $e) { + $this->logger->error( + 'Failed to create tar archive: ' . $e->getMessage() + ); + + if (file_exists($tarfile)) { + unlink($tarfile); + } + + return new \LORIS\Http\Response\JSON\InternalServerError(); + } + + $filepath = $tarfile; + } else { + $filename = $this->_filename; + $filepath = $info->getRealPath(); + $mimetype = substr($recording->getMetadata('header'), 0, 4) === 'hdf5' + ? 'application/x.minc2' + : 'application/octet-stream'; + } + + $body = new \LORIS\Http\FileStream($filepath, 'r', true); return (new \LORIS\Http\Response()) ->withHeader('Content-Type', $mimetype) ->withHeader( 'Content-Disposition', - 'attachment; filename=' . $this->_filename + 'attachment; filename=' . $filename ) ->withBody($body); } diff --git a/modules/electrophysiology_browser/jsx/components/DownloadPanel.js b/modules/electrophysiology_browser/jsx/components/DownloadPanel.js index a2d60828d9..4a76a8373e 100644 --- a/modules/electrophysiology_browser/jsx/components/DownloadPanel.js +++ b/modules/electrophysiology_browser/jsx/components/DownloadPanel.js @@ -33,7 +33,7 @@ class DownloadPanel extends Component { * @return {JSX} - React markup for the component */ render() { - const {t} = this.props; + const {t, dccid, visit, physioFileName} = this.props; return ( { const disabled = (download.file === ''); + let recordingFileUrl = `/api/v0.0.3/candidates/${dccid}/` + + `${visit}/recordings/${physioFileName}`; + + switch (type) { + case 'physiological_event_files': + recordingFileUrl += '/bidsfiles/events'; + break; + case 'all_files': + recordingFileUrl += '/bidsfiles/archive'; + break; + case 'physiological_channel_file': + recordingFileUrl += '/bidsfiles/channels'; + break; + case 'physiological_electrode_file': + recordingFileUrl += '/bidsfiles/electrodes'; + break; + } + // Ignore physiological_coord_system_file return type !== 'physiological_coord_system_file' ? ( @@ -93,13 +111,13 @@ class DownloadPanel extends Component { : diff --git a/src/Http/FileStream.php b/src/Http/FileStream.php index df5657d1dc..fe8795e691 100644 --- a/src/Http/FileStream.php +++ b/src/Http/FileStream.php @@ -27,4 +27,38 @@ */ class FileStream extends \Laminas\Diactoros\Stream implements \Psr\Http\Message\StreamInterface { + /** + * @var bool Whether the file be deleted when the stream is closed, used for + * temporary files. + */ + private bool $deleteOnClose; + + /** + * Constructor + * + * @param string $stream The path to the file or a stream resource + * @param string $mode The mode to open the stream with + * @param bool $deleteOnClose If true, delete the file when the stream is closed + */ + public function __construct( + string $stream, + string $mode = 'r', + bool $deleteOnClose = false, + ) { + parent::__construct($stream, $mode); + $this->deleteOnClose = $deleteOnClose; + } + + /** + * {@inheritdoc} + */ + public function close(): void + { + if ($this->deleteOnClose + && is_string($this->resource) + && file_exists($this->resource) + ) { + unlink($this->resource); + } + } } From 933c15cc06408893a4e30e0e969cc9d672f0a19f Mon Sep 17 00:00:00 2001 From: MaximeBICMTL Date: Mon, 11 May 2026 03:02:39 +0000 Subject: [PATCH 2/2] use new class instead of existing one --- .../electrophysiology/recording.class.inc | 27 ++------ src/Http/ArchiveStream.php | 66 +++++++++++++++++++ src/Http/FileStream.php | 34 ---------- 3 files changed, 70 insertions(+), 57 deletions(-) create mode 100644 src/Http/ArchiveStream.php diff --git a/modules/api/php/endpoints/candidate/visit/electrophysiology/recording.class.inc b/modules/api/php/endpoints/candidate/visit/electrophysiology/recording.class.inc index 8eb91fd945..540c5164b5 100644 --- a/modules/api/php/endpoints/candidate/visit/electrophysiology/recording.class.inc +++ b/modules/api/php/endpoints/candidate/visit/electrophysiology/recording.class.inc @@ -170,7 +170,8 @@ class Recording extends Endpoint implements \LORIS\Middleware\ETagCalculator return new \LORIS\Http\Response\JSON\NotFound($e->getMessage()); } - $info = $recording->getFileInfo(); + $info = $recording->getFileInfo(); + $filepath = $info->getRealPath(); if (!$info->isFile() && !$info->isDir()) { error_log('file in database but not in file system'); @@ -187,35 +188,15 @@ class Recording extends Endpoint implements \LORIS\Middleware\ETagCalculator if ($info->isDir()) { $filename = $this->_filename . '.tar'; $mimetype = 'application/x-tar'; - - $tarfile = sys_get_temp_dir() . '/recording_' . uniqid() . '.tar'; - - try { - $phar = new \PharData($tarfile); - $phar->buildFromDirectory($info->getRealPath()); - } catch (\Exception $e) { - $this->logger->error( - 'Failed to create tar archive: ' . $e->getMessage() - ); - - if (file_exists($tarfile)) { - unlink($tarfile); - } - - return new \LORIS\Http\Response\JSON\InternalServerError(); - } - - $filepath = $tarfile; + $body = new \LORIS\Http\ArchiveStream($filepath); } else { $filename = $this->_filename; - $filepath = $info->getRealPath(); $mimetype = substr($recording->getMetadata('header'), 0, 4) === 'hdf5' ? 'application/x.minc2' : 'application/octet-stream'; + $body = new \LORIS\Http\FileStream($filepath, 'r'); } - $body = new \LORIS\Http\FileStream($filepath, 'r', true); - return (new \LORIS\Http\Response()) ->withHeader('Content-Type', $mimetype) ->withHeader( diff --git a/src/Http/ArchiveStream.php b/src/Http/ArchiveStream.php new file mode 100644 index 0000000000..975e659f7d --- /dev/null +++ b/src/Http/ArchiveStream.php @@ -0,0 +1,66 @@ +archivePath = sys_get_temp_dir() . '/' . uniqid() . '.tar'; + + $phar = new \PharData($this->archivePath); + $phar->buildFromDirectory($directoryPath); + + parent::__construct($this->archivePath, 'r'); + } + + /** + * {@inheritdoc} + */ + public function close(): void + { + parent::close(); + + if (file_exists($this->archivePath)) { + unlink($this->archivePath); + } + } +} diff --git a/src/Http/FileStream.php b/src/Http/FileStream.php index fe8795e691..df5657d1dc 100644 --- a/src/Http/FileStream.php +++ b/src/Http/FileStream.php @@ -27,38 +27,4 @@ */ class FileStream extends \Laminas\Diactoros\Stream implements \Psr\Http\Message\StreamInterface { - /** - * @var bool Whether the file be deleted when the stream is closed, used for - * temporary files. - */ - private bool $deleteOnClose; - - /** - * Constructor - * - * @param string $stream The path to the file or a stream resource - * @param string $mode The mode to open the stream with - * @param bool $deleteOnClose If true, delete the file when the stream is closed - */ - public function __construct( - string $stream, - string $mode = 'r', - bool $deleteOnClose = false, - ) { - parent::__construct($stream, $mode); - $this->deleteOnClose = $deleteOnClose; - } - - /** - * {@inheritdoc} - */ - public function close(): void - { - if ($this->deleteOnClose - && is_string($this->resource) - && file_exists($this->resource) - ) { - unlink($this->resource); - } - } }