Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion rsconnect/bundle.py
Original file line number Diff line number Diff line change
Expand Up @@ -512,7 +512,15 @@ def make_hasher():

def file_checksum(path: str | Path) -> str:
"""Calculate the md5 hex digest of the specified file"""
with open(path, "rb") as f:
try:
file = open(path, "rb")
except FileNotFoundError as exc:
if os.path.islink(path):
raise RSConnectException(
f"Cannot bundle dangling symlink '{path}'. Fix or exclude it and try again."
) from exc
raise
with file as f:
m = make_hasher()
chunk_size = 64 * 1024

Expand Down
36 changes: 36 additions & 0 deletions tests/test_bundle.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,42 @@ def create_fake_quarto_rendered_output(target_dir, name):
fp.write("// fake resource.js\n")


@pytest.mark.parametrize("link_name", ["data.csv", ".venv/bin/python"])
def test_api_bundle_reports_dangling_symlink(tmp_path, link_name):
(tmp_path / "app.py").write_text("app = object()\n")
(tmp_path / "requirements.txt").write_text("flask\n")
link = tmp_path / link_name
link.parent.mkdir(parents=True, exist_ok=True)
try:
link.symlink_to("missing-target")
except OSError:
pytest.skip("symlinks are unavailable")
environment = Environment.create_python_environment(str(tmp_path))

with pytest.raises(RSConnectException) as error:
make_api_bundle(str(tmp_path), "app:app", AppModes.PYTHON_API, environment, [], [])

assert str(link) in str(error.value)
assert "dangling symlink" in str(error.value)


def test_api_bundle_keeps_valid_symlink(tmp_path):
(tmp_path / "app.py").write_text("app = object()\n")
(tmp_path / "requirements.txt").write_text("flask\n")
(tmp_path / "data.csv").write_text("data\n")
try:
(tmp_path / "data-link.csv").symlink_to("data.csv")
except OSError:
pytest.skip("symlinks are unavailable")
environment = Environment.create_python_environment(str(tmp_path))

with make_api_bundle(str(tmp_path), "app:app", AppModes.PYTHON_API, environment, [], []) as bundle:
with tarfile.open(mode="r:gz", fileobj=bundle) as tar:
manifest = json.loads(tar.extractfile("manifest.json").read())
assert manifest["files"]["data-link.csv"]["checksum"] == file_checksum(tmp_path / "data.csv")
assert tar.getmember("data-link.csv").issym()


class TestBundle(TestCase):
@staticmethod
def python_version():
Expand Down
17 changes: 17 additions & 0 deletions tests/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2154,6 +2154,23 @@ def test_no_package_json(self, tmp_path):


class TestWriteManifestFramework:
def test_dangling_symlink_reports_clear_error(self, tmp_path):
(tmp_path / "app.py").write_text("app = object()\n")
(tmp_path / "requirements.txt").write_text("flask\n")
link = tmp_path / "data.csv"
try:
link.symlink_to("missing-target")
except OSError:
pytest.skip("symlinks are unavailable")

result = CliRunner().invoke(cli, ["write-manifest", "api", str(tmp_path)])

assert result.exit_code == 1
assert str(link) in result.output
assert "dangling symlink" in result.output
assert "Traceback" not in result.output
assert "Internal error" not in result.output

def test_malformed_pyproject_with_requirements_file(self, tmp_path):
(tmp_path / "app.py").write_text("app = object()\n")
(tmp_path / "requirements.txt").write_text("flask\n")
Expand Down
Loading