Skip to content

Commit 31b7fef

Browse files
Merge pull request #2979 from wxtim/fix.install_broken_symlinks_in_directories
allow installation of broken symlinks within folders
2 parents 6a2d311 + 629349e commit 31b7fef

File tree

5 files changed

+161
-3
lines changed

5 files changed

+161
-3
lines changed

metomi/rose/checksum.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,16 @@ def get_checksum(name, checksum_func=None):
7070
for filename in filenames:
7171
filepath = os.path.join(path, filename)
7272
source = os.path.join(name, filepath)
73-
checksum = checksum_func(source, name)
74-
mode = os.stat(os.path.realpath(source)).st_mode
73+
74+
if os.path.islink(source) and not os.path.exists(source):
75+
# If the source is a broken link within a directory
76+
# install without failing (unlike a single file)
77+
checksum = os.path.realpath(source)
78+
mode = os.lstat(source).st_mode
79+
else:
80+
checksum = checksum_func(source, name)
81+
mode = os.stat(os.path.realpath(source)).st_mode
82+
7583
path_and_checksum_list.append((filepath, checksum, mode))
7684
return path_and_checksum_list
7785

metomi/rose/loc_handlers/rsync_remote_check.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,10 @@ def main(path, str_blob, str_tree):
6262
if filename.startswith("."):
6363
continue
6464
name = os.path.join(dirpath, filename)
65-
stat = os.stat(name)
65+
if os.path.islink(name) and not os.path.exists(name):
66+
stat = os.lstat(name)
67+
else:
68+
stat = os.stat(name)
6669
print(stat.st_mode, stat.st_mtime, stat.st_size, name)
6770
elif os.path.isfile(path):
6871
print(str_blob)

metomi/rose/tests/test_checksum.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
# Copyright (C) British Crown (Met Office) & Contributors.
2+
# This file is part of Rose, a framework for meteorological suites.
3+
#
4+
# Rose is free software: you can redistribute it and/or modify
5+
# it under the terms of the GNU General Public License as published by
6+
# the Free Software Foundation, either version 3 of the License, or
7+
# (at your option) any later version.
8+
#
9+
# Rose is distributed in the hope that it will be useful,
10+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
# GNU General Public License for more details.
13+
#
14+
# You should have received a copy of the GNU General Public License
15+
# along with Rose. If not, see <http://www.gnu.org/licenses/>.
16+
# -----------------------------------------------------------------------------
17+
18+
import hashlib
19+
from pathlib import Path
20+
import pytest
21+
22+
from metomi.rose.checksum import get_checksum
23+
24+
HELLO_WORLD = hashlib.md5(b'Hello World').hexdigest()
25+
HELLO_JUPITER = hashlib.md5(b'Hello Jupiter').hexdigest()
26+
27+
28+
@pytest.fixture(scope='module')
29+
def checksums_setup(tmp_path_factory):
30+
"""provide some exemplars for checksum to work on."""
31+
tmp_path = tmp_path_factory.getbasetemp()
32+
(tmp_path / 'foo').write_text('Hello World')
33+
(tmp_path / 'bar').mkdir()
34+
(tmp_path / 'bar/baz').write_text('Hello Jupiter')
35+
(tmp_path / 'goodlink').symlink_to('foo')
36+
(tmp_path / 'badlink').symlink_to('bad')
37+
yield tmp_path
38+
39+
40+
@pytest.fixture(scope='module')
41+
def checksums_dir(checksums_setup):
42+
yield (
43+
{a: (b, c) for a, b, c in get_checksum(str(checksums_setup))},
44+
checksums_setup
45+
)
46+
47+
48+
def test_checksum_single_file(checksums_setup):
49+
res = get_checksum(str(checksums_setup / 'foo'))[0][1]
50+
assert res == HELLO_WORLD
51+
52+
53+
def test_checksum_custom_checksum_function(checksums_setup):
54+
res = get_checksum(
55+
str(checksums_setup / 'foo'),
56+
# Last 3 letters of the reversed filename:
57+
checksum_func=lambda x, _: x[-1:-4:-1]
58+
)[0][1]
59+
assert res == 'oof'
60+
61+
62+
def test_get_checksum_for_all_files(checksums_dir):
63+
assert len(checksums_dir[0]) == 7
64+
65+
66+
def test_get_checksum_for_goodlink(checksums_dir):
67+
assert checksums_dir[0]['goodlink'][0] == HELLO_WORLD
68+
69+
70+
def test_get_checksum_for_badlink(checksums_dir):
71+
assert checksums_dir[0]['badlink'][0] == str(checksums_dir[1] / 'bad')
72+
73+
74+
def test_get_checksum_for_subdir_file(checksums_dir):
75+
assert checksums_dir[0]['bar/baz'][0] == HELLO_JUPITER
76+
77+
78+
def test_get_checksum_for_non_path():
79+
unlikely = '/var/tmp/ariuaibvnoijunhoiujoiuj'
80+
assert not Path(unlikely).exists()
81+
with pytest.raises(FileNotFoundError, match=unlikely):
82+
get_checksum(unlikely)

sphinx/api/configuration/file-creation.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,11 @@ root directory to install file targets with a relative path:
116116
``~/.ssh/config`` to specify the user ID for logging into ``HOST``
117117
if required.
118118

119+
.. note::
120+
121+
* When installing files, broken symbolic links are rejected.
122+
* When installing directories containing broken symlinks
123+
they will be copied.
119124

120125
.. rose:conf:: file:TARGET
121126
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
#!/usr/bin/env bash
2+
#-------------------------------------------------------------------------------
3+
# Copyright (C) British Crown (Met Office) & Contributors.
4+
#
5+
# This file is part of Rose, a framework for meteorological suites.
6+
#
7+
# Rose is free software: you can redistribute it and/or modify
8+
# it under the terms of the GNU General Public License as published by
9+
# the Free Software Foundation, either version 3 of the License, or
10+
# (at your option) any later version.
11+
#
12+
# Rose is distributed in the hope that it will be useful,
13+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
14+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15+
# GNU General Public License for more details.
16+
#
17+
# You should have received a copy of the GNU General Public License
18+
# along with Rose. If not, see <http://www.gnu.org/licenses/>.
19+
#-------------------------------------------------------------------------------
20+
# Test "rose app-run", file installation will not fail if a broken
21+
# symlink is contained in a repository or directory.
22+
# See https://github.com/metomi/rose/issues/2946
23+
24+
. "$(dirname "$0")/test_header"
25+
26+
tests 4
27+
28+
test_init <<__CONFIG__
29+
[command]
30+
default=true
31+
32+
[file:destination]
33+
source=${TEST_DIR}/source
34+
__CONFIG__
35+
36+
# Create a broken symlink dir
37+
mkdir -p ${TEST_DIR}/source/
38+
touch ${TEST_DIR}/source/missing
39+
ln -s ${TEST_DIR}/source/missing ${TEST_DIR}/source/link
40+
rm ${TEST_DIR}/source/missing
41+
42+
test_setup
43+
44+
# It doesn't fail when rsync installs broken symlink dirs:
45+
run_pass "${TEST_KEY_BASE}-rsync" rose app-run --config=../config
46+
file_grep_fail "${TEST_KEY_BASE}-rsync.err" \
47+
"No such file" \
48+
"${TEST_KEY_BASE}-rsync.err"
49+
50+
# Turn the source file into a Git repo and try to use the git file-handler:
51+
git -C ${TEST_DIR}/source init
52+
git -C ${TEST_DIR}/source add .
53+
git -C ${TEST_DIR}/source commit -a -m "my_commit"
54+
sed -i 'sXsource=.*Xsource=git:../source::./::HEADX' ../config/rose-app.conf
55+
56+
# It doesn't fail when git installs broken symlink dirs:
57+
run_pass "${TEST_KEY_BASE}-git" rose app-run --config=../config
58+
file_grep_fail "${TEST_KEY_BASE}-git.err" \
59+
"No such file" \
60+
"${TEST_KEY_BASE}-git.err"

0 commit comments

Comments
 (0)