Skip to content

Commit e943663

Browse files
committed
Preserve relative links within the upload directory.
This matches a corresponding update to the gobbler backend and allows users to explicitly deduplicate their uploads when they need to store the same file multiple times, e.g., for delayed arrays with shared seeds.
1 parent ea6ff72 commit e943663

File tree

2 files changed

+71
-7
lines changed

2 files changed

+71
-7
lines changed

src/pygobbler/upload_directory.py

+36-7
Original file line numberDiff line numberDiff line change
@@ -48,14 +48,13 @@ def upload_directory(project: str, asset: str, version: str, directory: str, sta
4848
if os.path.islink(src):
4949
slink = os.readlink(src)
5050

51-
if slink == "" or not os.path.isabs(slink):
52-
try:
53-
os.link(src, dest)
54-
except:
55-
import shutil
56-
shutil.copy(src, dest)
57-
else:
51+
if slink == "":
52+
_link_or_copy(src, dest)
53+
elif _is_absolute_or_local_link(slink, f):
5854
os.symlink(slink, dest)
55+
else:
56+
full_src = os.path.normpath(os.path.join(os.path.dirname(src), slink))
57+
_link_or_copy(full_src, dest)
5958

6059
directory = newdir
6160

@@ -68,3 +67,33 @@ def upload_directory(project: str, asset: str, version: str, directory: str, sta
6867
}
6968
ut.dump_request(staging, url, "upload", req)
7069
return
70+
71+
72+
def _is_absolute_or_local_link(target: str, link_path: str) -> bool:
73+
if os.path.isabs(target):
74+
return True
75+
76+
pre_length = len(link_path.split("/"))
77+
post_fragments = target.split("/")[:-1]
78+
79+
# link_path should be relative, so the idea is to check whether the
80+
# 'target' file is still a child of 'dirname(link_path)'.
81+
for x in post_fragments:
82+
if x == ".":
83+
continue
84+
elif x == "..":
85+
pre_length -= 1
86+
if pre_length < 0:
87+
return False
88+
else:
89+
pre_length += 1
90+
91+
return True
92+
93+
94+
def _link_or_copy(src: str, dest: str):
95+
try:
96+
os.link(src, dest)
97+
except:
98+
import shutil
99+
shutil.copy(src, dest)

tests/test_upload_directory.py

+35
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,41 @@ def test_upload_directory_links(setup):
8383
assert "link" not in man["whee"]
8484

8585

86+
def test_upload_directory_relative_links(setup):
87+
_, staging, registry, url = pyg.start_gobbler()
88+
89+
dest = tempfile.mkdtemp()
90+
with open(os.path.join(dest, "blah.txt"), "w") as handle:
91+
handle.write("motto motto")
92+
os.symlink("blah.txt", os.path.join(dest, "whee.txt")) # relative links inside the directory are retained.
93+
os.mkdir(os.path.join(dest, "foo"))
94+
os.symlink("../whee.txt", os.path.join(dest, "foo/bar.txt"))
95+
96+
fid, outside = tempfile.mkstemp(dir=os.path.dirname(dest))
97+
with open(outside, "w") as handle:
98+
handle.write("toki wa kizuna uta")
99+
os.symlink(os.path.join("../../", os.path.basename(outside)), os.path.join(dest, "foo/outer.txt")) # relative links outside the directory are lost.
100+
101+
pyg.upload_directory(
102+
project="test-more-upload",
103+
asset="nicole",
104+
version="1",
105+
directory=dest,
106+
staging=staging,
107+
url=url
108+
)
109+
110+
man = pyg.fetch_manifest("test-more-upload", "nicole", "1", registry=registry, url=url)
111+
assert sorted(man.keys()) == [ "blah.txt", "foo/bar.txt", "foo/outer.txt", "whee.txt" ]
112+
assert "link" in man["whee.txt"]
113+
assert "link" not in man["foo/outer.txt"]
114+
assert "link" in man["foo/bar.txt"]
115+
assert "link" not in man["blah.txt"]
116+
assert man["foo/outer.txt"]["size"] == 18
117+
assert man["whee.txt"]["size"] == man["foo/bar.txt"]["size"]
118+
assert man["whee.txt"]["size"] == man["blah.txt"]["size"]
119+
120+
86121
def test_upload_directory_staging(setup):
87122
_, staging, registry, url = pyg.start_gobbler()
88123

0 commit comments

Comments
 (0)