Skip to content

Commit 26debfb

Browse files
authored
Merge pull request #62 from ArcanaFramework/full-mounts
Full mount list for copy mode requirements
2 parents 98c89e9 + b24b8a1 commit 26debfb

File tree

3 files changed

+193
-48
lines changed

3 files changed

+193
-48
lines changed

fileformats/core/fileset.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1305,7 +1305,10 @@ def copy(
13051305
# Rule out any copy modes that are not supported given the collation mode
13061306
# and file-system mounts the paths and destination directory reside on
13071307
constraints = []
1308-
if FsMountIdentifier.on_cifs(dest_dir) and mode & self.CopyMode.symlink:
1308+
if (
1309+
not FsMountIdentifier.symlinks_supported(dest_dir)
1310+
and mode & self.CopyMode.symlink
1311+
):
13091312
supported_modes -= self.CopyMode.symlink
13101313
constraint = (
13111314
f"Destination directory is on CIFS mount ({dest_dir}) "

fileformats/core/fs_mount_identifier.py

Lines changed: 38 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import os
22
import typing as ty
3+
import string
34
from pathlib import Path
5+
import platform
46
import re
57
from contextlib import contextmanager
68
import subprocess as sp
@@ -12,7 +14,7 @@ class FsMountIdentifier:
1214
features that can be used (e.g. symlinks)"""
1315

1416
@classmethod
15-
def on_cifs(cls, path: os.PathLike) -> bool:
17+
def symlinks_supported(cls, path: os.PathLike) -> bool:
1618
"""
1719
Check whether a file path is on a CIFS filesystem mounted in a POSIX host.
1820
@@ -27,12 +29,12 @@ def on_cifs(cls, path: os.PathLike) -> bool:
2729
2830
This check is written to support disabling symlinks on CIFS shares.
2931
30-
NB: This function and sub-functions are copied from the nipype.utils.filemanip module
32+
NB: This function and sub-functions are modified from the nipype.utils.filemanip module
3133
3234
3335
NB: Adapted from https://github.com/nipy/nipype
3436
"""
35-
return cls.get_mount(path)[1] == "cifs"
37+
return cls.get_mount(path)[1] != "cifs"
3638

3739
@classmethod
3840
def on_same_mount(cls, path1: os.PathLike, path2: os.PathLike) -> bool:
@@ -54,19 +56,21 @@ def get_mount(cls, path: os.PathLike) -> ty.Tuple[Path, str]:
5456
the root of the mount the path sits on
5557
fstype : str
5658
the type of the file-system (e.g. ext4 or cifs)"""
57-
try:
58-
# Only the first match (most recent parent) counts, mount table sorted longest
59-
# to shortest
60-
return next(
61-
(Path(p), t)
62-
for p, t in cls.get_mount_table()
63-
if str(path).startswith(p)
59+
strpath = str(Path(path).absolute())
60+
mount_table = cls.get_mount_table()
61+
matches = sorted(
62+
((Path(p), t) for p, t in mount_table if strpath.startswith(p)),
63+
key=lambda m: len(str(m[0])),
64+
)
65+
if not matches:
66+
raise ValueError(
67+
f"Path {strpath} is not on a known mount point:\n{mount_table}"
6468
)
65-
except StopIteration:
66-
return (Path("/"), "ext4")
69+
# return mount point with longest matching prefix
70+
return matches[-1]
6771

6872
@classmethod
69-
def generate_cifs_table(cls) -> ty.List[ty.Tuple[str, str]]:
73+
def generate_mount_table(cls) -> ty.List[ty.Tuple[str, str]]:
7074
"""
7175
Construct a reverse-length-ordered list of mount points that fall under a CIFS mount.
7276
@@ -76,7 +80,25 @@ def generate_cifs_table(cls) -> ty.List[ty.Tuple[str, str]]:
7680
empty list.
7781
7882
"""
83+
if platform.system() == "Windows":
84+
drive_names = [
85+
c + ":" for c in string.ascii_uppercase if os.path.exists(c + ":")
86+
]
87+
drives = []
88+
for drive_name in drive_names:
89+
result = sp.run(
90+
["fsutil", "fsinfo", "fstype", drive_name],
91+
capture_output=True,
92+
text=True,
93+
)
94+
fstype = result.stdout.strip().split(" ")[-1].lower()
95+
drives.append((drive_name, fstype))
96+
return drives
7997
exit_code, output = sp.getstatusoutput("mount")
98+
if exit_code != 0:
99+
raise RuntimeError(
100+
"Failed to get mount table (exit code {}): {}".format(exit_code, output)
101+
)
80102
return cls.parse_mount_table(exit_code, output)
81103

82104
@classmethod
@@ -90,10 +112,6 @@ def parse_mount_table(
90112
outputs
91113
92114
"""
93-
# Not POSIX
94-
if exit_code != 0:
95-
return []
96-
97115
# Linux mount example: sysfs on /sys type sysfs (rw,nosuid,nodev,noexec)
98116
# <PATH>^^^^ ^^^^^<FSTYPE>
99117
# OSX mount example: /dev/disk2 on / (hfs, local, journaled)
@@ -105,28 +123,23 @@ def parse_mount_table(
105123
matches = [(ll, pattern.match(ll)) for ll in output.strip().splitlines() if ll]
106124

107125
# (path, fstype) tuples, sorted by path length (longest first)
108-
mount_info = sorted(
126+
mounts = sorted(
109127
(match.groups() for _, match in matches if match is not None),
110128
key=lambda x: len(x[0]),
111129
reverse=True,
112130
)
113-
cifs_paths = [path for path, fstype in mount_info if fstype.lower() == "cifs"]
114131

115132
# Report failures as warnings
116133
for line, match in matches:
117134
if match is None:
118135
logger.debug("Cannot parse mount line: '%s'", line)
119136

120-
return [
121-
mount
122-
for mount in mount_info
123-
if any(mount[0].startswith(path) for path in cifs_paths)
124-
]
137+
return mounts
125138

126139
@classmethod
127140
def get_mount_table(cls) -> ty.List[ty.Tuple[str, str]]:
128141
if cls._mount_table is None:
129-
cls._mount_table = cls.generate_cifs_table()
142+
cls._mount_table = cls.generate_mount_table()
130143
return cls._mount_table
131144

132145
@classmethod

fileformats/core/tests/test_fs_mount_identifier.py

Lines changed: 151 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import os.path
2+
import platform
23
import pytest
34
from fileformats.core.fs_mount_identifier import FsMountIdentifier
45
from fileformats.generic import File
@@ -33,7 +34,32 @@
3334
gvfsd-fuse on /run/user/1002/gvfs type fuse.gvfsd-fuse (rw,nosuid,nodev,relatime,user_id=1002,group_id=1002)
3435
""",
3536
0,
36-
[],
37+
[
38+
("/sys/fs/cgroup/cpu,cpuacct", "cgroup"),
39+
("/sys/firmware/efi/efivars", "efivarfs"),
40+
("/proc/sys/fs/binfmt_misc", "autofs"),
41+
("/sys/fs/fuse/connections", "fusectl"),
42+
("/sys/fs/cgroup/systemd", "cgroup"),
43+
("/sys/fs/cgroup/freezer", "cgroup"),
44+
("/sys/fs/cgroup/cpuset", "cgroup"),
45+
("/sys/kernel/security", "securityfs"),
46+
("/var/lib/docker/aufs", "ext4"),
47+
("/sys/fs/cgroup/pids", "cgroup"),
48+
("/run/user/1002/gvfs", "fuse.gvfsd-fuse"),
49+
("/sys/kernel/debug", "debugfs"),
50+
("/sys/fs/cgroup", "tmpfs"),
51+
("/sys/fs/pstore", "pstore"),
52+
("/dev/hugepages", "hugetlbfs"),
53+
("/dev/mqueue", "mqueue"),
54+
("/boot/efi", "vfat"),
55+
("/dev/pts", "devpts"),
56+
("/dev/shm", "tmpfs"),
57+
("/proc", "proc"),
58+
("/sys", "sysfs"),
59+
("/dev", "devtmpfs"),
60+
("/run", "tmpfs"),
61+
("/", "ext4"),
62+
],
3763
),
3864
# OS X, no CIFS
3965
(
@@ -53,7 +79,22 @@
5379
/dev/disk1s3 on /Volumes/Boot OS X (hfs, local, journaled, nobrowse)
5480
""",
5581
0,
56-
[],
82+
[
83+
("/Volumes/MyBookData", "hfs"),
84+
("/Volumes/AFNI_SHARE", "nfs"),
85+
("/Volumes/Boot OS X", "hfs"),
86+
("/Volumes/INCOMING", "nfs"),
87+
("/Volumes/raid.bot", "nfs"),
88+
("/Volumes/raid.top", "autofs"),
89+
("/Network/Servers", "autofs"),
90+
("/Volumes/safni", "autofs"),
91+
("/Volumes/afni", "nfs"),
92+
("/Volumes/afni", "nfs"),
93+
("/home", "autofs"),
94+
("/dev", "devfs"),
95+
("/net", "autofs"),
96+
("/", "hfs"),
97+
],
5798
),
5899
# Non-zero exit code
59100
("", 1, []),
@@ -85,7 +126,32 @@
85126
gvfsd-fuse on /run/user/1002/gvfs type fuse.gvfsd-fuse (rw,nosuid,nodev,relatime,user_id=1002,group_id=1002)
86127
""",
87128
0,
88-
[],
129+
[
130+
("/sys/fs/cgroup/cpu,cpuacct", "cgroup"),
131+
("/sys/firmware/efi/efivars", "efivarfs"),
132+
("/proc/sys/fs/binfmt_misc", "autofs"),
133+
("/sys/fs/fuse/connections", "fusectl"),
134+
("/sys/fs/cgroup/systemd", "cgroup"),
135+
("/sys/fs/cgroup/freezer", "cgroup"),
136+
("/sys/fs/cgroup/cpuset", "cgroup"),
137+
("/sys/kernel/security", "securityfs"),
138+
("/var/lib/docker/aufs", "ext4"),
139+
("/sys/fs/cgroup/pids", "cgroup"),
140+
("/run/user/1002/gvfs", "fuse.gvfsd-fuse"),
141+
("/sys/kernel/debug", "debugfs"),
142+
("/sys/fs/cgroup", "tmpfs"),
143+
("/sys/fs/pstore", "pstore"),
144+
("/dev/hugepages", "hugetlbfs"),
145+
("/dev/mqueue", "mqueue"),
146+
("/boot/efi", "vfat"),
147+
("/dev/pts", "devpts"),
148+
("/dev/shm", "tmpfs"),
149+
("/proc", "proc"),
150+
("/sys", "sysfs"),
151+
("/dev", "devtmpfs"),
152+
("/run", "tmpfs"),
153+
("/", "ext4"),
154+
],
89155
),
90156
# Variant of OS X example with CIFS added manually
91157
(
@@ -98,7 +164,15 @@
98164
elros:/volume2/AFNI_SHARE on /Volumes/AFNI_SHARE (nfs)
99165
""",
100166
0,
101-
[("/Volumes/afni/fraid", "nfs"), ("/Volumes/afni", "cifs")],
167+
[
168+
("/Volumes/afni/fraid", "nfs"),
169+
("/Volumes/AFNI_SHARE", "nfs"),
170+
("/Volumes/INCOMING", "nfs"),
171+
("/Volumes/raid.bot", "nfs"),
172+
("/Volumes/afni", "cifs"),
173+
("/dev", "devfs"),
174+
("/", "hfs"),
175+
],
102176
),
103177
# From Windows: docker run --rm -it -v C:\:/data busybox mount
104178
(
@@ -140,7 +214,44 @@
140214
tmpfs on /sys/firmware type tmpfs (ro,relatime)
141215
""",
142216
0,
143-
[("/data", "cifs")],
217+
[
218+
("/sys/fs/cgroup/perf_event", "cgroup"),
219+
("/sys/fs/cgroup/net_prio", "cgroup"),
220+
("/sys/fs/cgroup/cpuacct", "cgroup"),
221+
("/sys/fs/cgroup/devices", "cgroup"),
222+
("/sys/fs/cgroup/freezer", "cgroup"),
223+
("/sys/fs/cgroup/net_cls", "cgroup"),
224+
("/sys/fs/cgroup/hugetlb", "cgroup"),
225+
("/sys/fs/cgroup/systemd", "cgroup"),
226+
("/sys/fs/cgroup/cpuset", "cgroup"),
227+
("/sys/fs/cgroup/memory", "cgroup"),
228+
("/sys/fs/cgroup/blkio", "cgroup"),
229+
("/sys/fs/cgroup/pids", "cgroup"),
230+
("/proc/sysrq-trigger", "proc"),
231+
("/sys/fs/cgroup/cpu", "cgroup"),
232+
("/proc/sched_debug", "tmpfs"),
233+
("/etc/resolv.conf", "ext4"),
234+
("/proc/timer_list", "tmpfs"),
235+
("/sys/fs/cgroup", "tmpfs"),
236+
("/etc/hostname", "ext4"),
237+
("/sys/firmware", "tmpfs"),
238+
("/dev/console", "devpts"),
239+
("/dev/mqueue", "mqueue"),
240+
("/proc/kcore", "tmpfs"),
241+
("/etc/hosts", "ext4"),
242+
("/proc/scsi", "tmpfs"),
243+
("/proc/bus", "proc"),
244+
("/proc/irq", "proc"),
245+
("/proc/sys", "proc"),
246+
("/dev/pts", "devpts"),
247+
("/dev/shm", "tmpfs"),
248+
("/proc/fs", "proc"),
249+
("/proc", "proc"),
250+
("/data", "cifs"),
251+
("/dev", "tmpfs"),
252+
("/sys", "sysfs"),
253+
("/", "overlay"),
254+
],
144255
),
145256
# From @yarikoptic - added blank lines to test for resilience
146257
(
@@ -153,7 +264,13 @@
153264
154265
""",
155266
0,
156-
[],
267+
[
268+
("/dev/ptmx", "devpts"),
269+
("/dev/shm", "tmpfs"),
270+
("/dev/pts", "devpts"),
271+
("/proc", "proc"),
272+
("/sys", "sysfs"),
273+
],
157274
),
158275
)
159276

@@ -163,27 +280,24 @@ def test_parse_mount_table(output, exit_code, expected):
163280
assert FsMountIdentifier.parse_mount_table(exit_code, output) == expected
164281

165282

166-
def test_cifs_check():
167-
assert isinstance(FsMountIdentifier.get_mount_table(), list)
168-
assert isinstance(FsMountIdentifier.on_cifs("/"), bool)
169-
fake_table = [("/scratch/tmp", "ext4"), ("/scratch", "cifs")]
283+
@pytest.mark.skipif(
284+
platform.system() == "Windows", reason="Windows does not have mount table"
285+
)
286+
def test_mount_check():
287+
fake_table = [("/", "ext4"), ("/scratch/tmp", "ext4"), ("/scratch", "cifs")]
170288
cifs_targets = [
171-
("/scratch/tmp/x/y", False),
172-
("/scratch/tmp/x", False),
173-
("/scratch/x/y", True),
174-
("/scratch/x", True),
175-
("/x/y", False),
176-
("/x", False),
177-
("/", False),
289+
("/scratch/tmp/x/y", True),
290+
("/scratch/tmp/x", True),
291+
("/scratch/x/y", False),
292+
("/scratch/x", False),
293+
("/x/y", True),
294+
("/x", True),
295+
("/", True),
178296
]
179297

180-
with FsMountIdentifier.patch_table([]):
181-
for target, _ in cifs_targets:
182-
assert FsMountIdentifier.on_cifs(target) is False
183-
184298
with FsMountIdentifier.patch_table(fake_table):
185299
for target, expected in cifs_targets:
186-
assert FsMountIdentifier.on_cifs(target) is expected
300+
assert FsMountIdentifier.symlinks_supported(target) is expected
187301

188302

189303
def test_copy_constraints(tmp_path):
@@ -242,3 +356,18 @@ def test_copy_constraints(tmp_path):
242356
assert (
243357
os.stat(ext4_file).st_ino != os.stat(ext4_file_on_cifs).st_ino
244358
) # Not hardlink
359+
360+
361+
def test_generate_mount_table():
362+
mount_table = FsMountIdentifier.get_mount_table()
363+
assert isinstance(mount_table, list)
364+
# We can't test the actual mount table, but we can test that the function actually
365+
# runs and returns at least one mount/drive
366+
assert mount_table
367+
368+
369+
@pytest.mark.skipif(
370+
platform.system() == "Windows", reason="Windows does not have mount table"
371+
)
372+
def test_symlink_supported():
373+
assert isinstance(FsMountIdentifier.symlinks_supported("/"), bool)

0 commit comments

Comments
 (0)