Skip to content

Commit 321bdaa

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 321bdaa

File tree

2 files changed

+72
-7
lines changed

2 files changed

+72
-7
lines changed

src/pygobbler/upload_directory.py

+37-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,34 @@ 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+
# Both 'target' and 'link_path' should be relative at this point, so the
77+
# idea is to check whether 'os.path.join(os.path.dirname(link_path),
78+
# target)' is still a child of 'os.path.dirname(link_path)'.
79+
pre_length = len(link_path.split("/"))
80+
post_fragments = target.split("/")[:-1]
81+
82+
for x in post_fragments:
83+
if x == ".":
84+
continue
85+
elif x == "..":
86+
pre_length -= 1
87+
if pre_length < 0:
88+
return False
89+
else:
90+
pre_length += 1
91+
92+
return True
93+
94+
95+
def _link_or_copy(src: str, dest: str):
96+
try:
97+
os.link(src, dest)
98+
except:
99+
import shutil
100+
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)