Skip to content

Commit 3ca4fd1

Browse files
committed
rf: readonly fixes from #105
1 parent fa20b85 commit 3ca4fd1

File tree

3 files changed

+54
-15
lines changed

3 files changed

+54
-15
lines changed

heudiconv/bids.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
from .parser import find_files
1717
from .utils import (load_json, save_json, create_file_if_missing,
18-
json_dumps_pretty, set_readonly)
18+
json_dumps_pretty, set_readonly, is_readonly)
1919

2020
lgr = logging.getLogger(__name__)
2121

@@ -142,11 +142,14 @@ def tuneup_bids_json_files(json_files):
142142
'_magnitude%d.json' % i)['EchoTime'])
143143
except IOError as exc:
144144
lgr.error("Failed to open magnitude file: %s", exc)
145-
# might have been made R/O already
146-
set_readonly(json_phasediffname, False)
147-
#json.dump(json_, open(json_phasediffname, 'w'), indent=2)
145+
# might have been made R/O already, but if not -- it will be set
146+
# only later in the pipeline, so we must not make it read-only yet
147+
was_readonly = is_readonly(json_phasediffname)
148+
if was_readonly:
149+
set_readonly(json_phasediffname, False)
148150
save_json(json_phasediffname, json_, indent=2)
149-
set_readonly(json_phasediffname)
151+
if was_readonly:
152+
set_readonly(json_phasediffname)
150153

151154

152155
def add_participant_record(studydir, subject, age, sex):

heudiconv/utils.py

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,11 @@ def safe_copyfile(src, dest):
276276
shutil.copyfile(src, dest)
277277

278278

279+
# Globals to check filewriting permissions
280+
ALL_CAN_WRITE = (stat.S_IWUSR | stat.S_IWGRP | stat.S_IWOTH)
281+
ALL_CAN_READ = (stat.S_IRUSR | stat.S_IRGRP | stat.S_IROTH)
282+
assert ALL_CAN_READ >> 1 == ALL_CAN_WRITE # Assumption in the code
283+
279284
def set_readonly(path, read_only=True):
280285
"""Make file read only or writeable while preserving "access levels"
281286
@@ -290,9 +295,6 @@ def set_readonly(path, read_only=True):
290295
writeable for levels where it is readable
291296
292297
"""
293-
ALL_CAN_WRITE = (stat.S_IWUSR | stat.S_IWGRP | stat.S_IWOTH)
294-
ALL_CAN_READ = (stat.S_IRUSR | stat.S_IRGRP | stat.S_IROTH)
295-
assert ALL_CAN_READ >> 1 == ALL_CAN_WRITE # Assumption in the code
296298

297299
# get current permissions
298300
perms = stat.S_IMODE(os.lstat(path).st_mode)
@@ -305,6 +307,15 @@ def set_readonly(path, read_only=True):
305307
whocanread = perms & ALL_CAN_READ
306308
thosecanwrite = whocanread >> 1
307309
new_perms = perms | thosecanwrite
308-
# apply and return those target permissions
309-
os.chmod(path, new_perms)
310-
return new_perms
310+
# apply and return those target permissions
311+
os.chmod(path, new_perms)
312+
return new_perms
313+
314+
315+
def is_readonly(path):
316+
"""Return True if it is a fully read-only file (dereferences the symlink)
317+
"""
318+
# get current permissions
319+
perms = stat.S_IMODE(os.lstat(os.path.realpath(path)).st_mode)
320+
# should be true if anyone is allowed to write
321+
return not bool(perms & ALL_CAN_WRITE)

tests/test_main.py

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22

33
from heudiconv.cli.run import main as runner
44
from heudiconv import __version__
5-
from heudiconv.utils import create_file_if_missing, json_dumps_pretty
5+
from heudiconv.utils import (create_file_if_missing,
6+
json_dumps_pretty,
7+
set_readonly,
8+
is_readonly)
69
from heudiconv.bids import (populate_bids_templates,
710
add_participant_record,
811
get_formatted_scans_key_row,
@@ -18,9 +21,7 @@
1821
from mock import patch
1922
from os.path import join as opj
2023
from six.moves import StringIO
21-
22-
23-
import heudiconv
24+
import stat
2425

2526

2627
@patch('sys.stdout', new_callable=StringIO)
@@ -215,3 +216,27 @@ def test__find_subj_ses():
215216
's1')
216217
assert find_subj_ses(
217218
'fmap/sub-01-fmap_acq-3mm_acq-3mm_phasediff.nii.gz') == ('01', None)
219+
220+
221+
def test_make_readonly(tmpdir):
222+
# we could test it all without torturing a poor file, but for going all
223+
# the way, let's do it on a file
224+
path = tmpdir.join('f')
225+
pathname = str(path)
226+
with open(pathname, 'w'):
227+
pass
228+
symname = pathname + 'link'
229+
os.symlink(pathname, symname)
230+
for orig, ro, rw in [
231+
(0o600, 0o400, 0o600), # fully returned
232+
(0o624, 0o404, 0o606), # it will not get write bit where it is not readable
233+
(0o1777, 0o1555, 0o1777), # and other bits should be preserved
234+
]:
235+
os.chmod(pathname, orig)
236+
assert not is_readonly(pathname)
237+
assert set_readonly(pathname) == ro
238+
assert is_readonly(pathname)
239+
assert stat.S_IMODE(os.lstat(pathname).st_mode) == ro
240+
# and it should go back if we set it back to non-read_only
241+
assert set_readonly(pathname, read_only=False) == rw
242+
assert not is_readonly(pathname)

0 commit comments

Comments
 (0)