Skip to content

Commit 8d0d02e

Browse files
committed
on demand tar file download
1 parent b972697 commit 8d0d02e

4 files changed

Lines changed: 101 additions & 14 deletions

File tree

modules/api/php/endpoints/candidate/visit/electrophysiology/recording.class.inc

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -170,12 +170,9 @@ class Recording extends Endpoint implements \LORIS\Middleware\ETagCalculator
170170
return new \LORIS\Http\Response\JSON\NotFound($e->getMessage());
171171
}
172172

173-
$mimetype = substr($recording->getMetadata('header'), 0, 4) === 'hdf5' ?
174-
'application/x.minc2' : 'application/octet-stream';
175-
176173
$info = $recording->getFileInfo();
177174

178-
if (!$info->isFile()) {
175+
if (!$info->isFile() && !$info->isDir()) {
179176
error_log('file in database but not in file system');
180177
return new \LORIS\Http\Response\JSON\NotFound();
181178
}
@@ -185,13 +182,45 @@ class Recording extends Endpoint implements \LORIS\Middleware\ETagCalculator
185182
return new \LORIS\Http\Response\JSON\NotFound();
186183
}
187184

188-
$body = new \LORIS\Http\FileStream($info->getRealPath(), 'r');
185+
// Tar the acquisition file if it is actually a directory (such as for MEG
186+
// CTF acquisitions).
187+
if ($info->isDir()) {
188+
$filename = $this->_filename . '.tar';
189+
$mimetype = 'application/x-tar';
190+
191+
$tarfile = sys_get_temp_dir() . '/recording_' . uniqid() . '.tar';
192+
193+
try {
194+
$phar = new \PharData($tarfile);
195+
$phar->buildFromDirectory($info->getRealPath());
196+
} catch (\Exception $e) {
197+
$this->logger->error(
198+
'Failed to create tar archive: ' . $e->getMessage()
199+
);
200+
201+
if (file_exists($tarfile)) {
202+
unlink($tarfile);
203+
}
204+
205+
return new \LORIS\Http\Response\JSON\InternalServerError();
206+
}
207+
208+
$filepath = $tarfile;
209+
} else {
210+
$filename = $this->_filename;
211+
$filepath = $info->getRealPath();
212+
$mimetype = substr($recording->getMetadata('header'), 0, 4) === 'hdf5'
213+
? 'application/x.minc2'
214+
: 'application/octet-stream';
215+
}
216+
217+
$body = new \LORIS\Http\FileStream($filepath, 'r', true);
189218

190219
return (new \LORIS\Http\Response())
191220
->withHeader('Content-Type', $mimetype)
192221
->withHeader(
193222
'Content-Disposition',
194-
'attachment; filename=' . $this->_filename
223+
'attachment; filename=' . $filename
195224
)
196225
->withBody($body);
197226
}

modules/electrophysiology_browser/jsx/components/DownloadPanel.js

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ class DownloadPanel extends Component {
3333
* @return {JSX} - React markup for the component
3434
*/
3535
render() {
36-
const {t} = this.props;
36+
const {t, dccid, visit, physioFileName} = this.props;
3737
return (
3838
<Panel
3939
id={this.props.id}
@@ -59,6 +59,24 @@ class DownloadPanel extends Component {
5959
{Object.entries(panel.links).map(([type, download], j) => {
6060
const disabled = (download.file === '');
6161

62+
let recordingFileUrl = `/api/v0.0.3/candidates/${dccid}/${visit}/`
63+
+ `recordings/${physioFileName}`;
64+
65+
switch (type) {
66+
case 'physiological_event_files':
67+
recordingFileUrl += '/bidsfiles/events';
68+
break;
69+
case 'all_files':
70+
recordingFileUrl += '/bidsfiles/archive';
71+
break;
72+
case 'physiological_channel_file':
73+
recordingFileUrl += '/bidsfiles/channels';
74+
break;
75+
case 'physiological_electrode_file':
76+
recordingFileUrl += '/bidsfiles/electrodes';
77+
break;
78+
}
79+
6280
// Ignore physiological_coord_system_file
6381
return type !== 'physiological_coord_system_file'
6482
? (
@@ -93,13 +111,13 @@ class DownloadPanel extends Component {
93111
: <a
94112
className='btn btn-primary download col-xs-6'
95113
href={
96-
(type ==
97-
'physiological_event_files' ||
98-
type == 'all_files') ?
99-
this.state.annotationsAction
100-
+ '?physioFileID=' + this.state.physioFileID
101-
+ '&filePath=' + download.file
102-
: '/mri/jiv/get_file.php?file=' + download.file
114+
(type == 'physiological_event_files')
115+
? (
116+
this.state.annotationsAction
117+
+ '?physioFileID=' + this.state.physioFileID
118+
+ '&filePath=' + download.file
119+
)
120+
: recordingFileUrl
103121
}
104122
target='_blank'
105123
style={{
@@ -141,7 +159,10 @@ class DownloadPanel extends Component {
141159

142160
DownloadPanel.propTypes = {
143161
downloads: PropTypes.array,
162+
dccid: PropTypes.string,
163+
visit: PropTypes.string,
144164
physioFileID: PropTypes.number,
165+
physioFileName: PropTypes.string,
145166
outputType: PropTypes.string,
146167
id: PropTypes.string,
147168
t: PropTypes.func,

modules/electrophysiology_browser/jsx/electrophysiologySessionView.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -504,7 +504,10 @@ class ElectrophysiologySessionView extends Component {
504504
<DownloadPanel
505505
id={'file_download_' + i}
506506
downloads={this.state.database[i].file.downloads}
507+
dccid={this.state.patient.info.dccid}
508+
visit={this.state.patient.info.visit_label}
507509
physioFileID={this.state.database[i].file.id}
510+
physioFileName={this.state.database[i].file.name}
508511
outputType={this.state.database[i].file.output_type}
509512
t={t}
510513
/>

src/Http/FileStream.php

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,38 @@
2727
*/
2828
class FileStream extends \Laminas\Diactoros\Stream implements \Psr\Http\Message\StreamInterface
2929
{
30+
/**
31+
* @var bool Whether the file be deleted when the stream is closed, used for
32+
* temporary files.
33+
*/
34+
private bool $deleteOnClose;
35+
36+
/**
37+
* Constructor
38+
*
39+
* @param string $stream The path to the file or a stream resource
40+
* @param string $mode The mode to open the stream with
41+
* @param bool $deleteOnClose If true, delete the file when the stream is closed
42+
*/
43+
public function __construct(
44+
string $stream,
45+
string $mode = 'r',
46+
bool $deleteOnClose = false,
47+
) {
48+
parent::__construct($stream, $mode);
49+
$this->deleteOnClose = $deleteOnClose;
50+
}
51+
52+
/**
53+
* {@inheritdoc}
54+
*/
55+
public function close(): void
56+
{
57+
if ($this->deleteOnClose
58+
&& is_string($this->resource)
59+
&& file_exists($this->resource)
60+
) {
61+
unlink($this->resource);
62+
}
63+
}
3064
}

0 commit comments

Comments
 (0)