Skip to content

Commit 845cd35

Browse files
author
Mr. Outis
committed
s3: add support for top level empty directories
1 parent 6ad278f commit 845cd35

File tree

2 files changed

+23
-1
lines changed

2 files changed

+23
-1
lines changed

dvc/remote/s3.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,15 @@ def exists(self, path_info):
212212
fname = next(self._list_paths(path_info, max_items=1), "")
213213
return path_info.path == fname or fname.startswith(dir_path.path)
214214

215+
def makedirs(self, path_info):
216+
# We need to support creating empty directories, on S3, that means
217+
# creating an object with an empty body and a trailing slash `/`.
218+
#
219+
# We are not creating directory objects for every parent prefix,
220+
# it doesn't make sense.
221+
dir_path = path_info / ""
222+
self.s3.put_object(Bucket=path_info.bucket, Key=dir_path.path, Body="")
223+
215224
def isdir(self, path_info):
216225
# S3 doesn't have a concept for directories.
217226
#
@@ -271,4 +280,7 @@ def _generate_download_url(self, path_info, expires=3600):
271280

272281
def walk_files(self, path_info, max_items=None):
273282
for fname in self._list_paths(path_info, max_items):
283+
if fname.endswith("/"):
284+
continue
285+
274286
yield path_info.replace(path=fname)

tests/unit/remote/test_s3.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,9 +79,11 @@ def test_walk_files(remote):
7979
remote.path_info / "data/subdir/1",
8080
remote.path_info / "data/subdir/2",
8181
remote.path_info / "data/subdir/3",
82+
remote.path_info / "empty_file",
83+
remote.path_info / "foo",
8284
]
8385

84-
assert list(remote.walk_files(remote.path_info / "data")) == files
86+
assert list(remote.walk_files(remote.path_info)) == files
8587

8688

8789
def test_copy_preserve_etag_across_buckets(remote):
@@ -99,3 +101,11 @@ def test_copy_preserve_etag_across_buckets(remote):
99101
to_etag = RemoteS3.get_etag(s3, "another", "foo")
100102

101103
assert from_etag == to_etag
104+
105+
106+
def makedirs(remote):
107+
empty_dir = remote.path_info / "empty_dir"
108+
remote.remove(empty_dir)
109+
assert not remote.exists(empty_dir)
110+
remote.makedirs(empty_dir)
111+
assert remote.exists(empty_dir)

0 commit comments

Comments
 (0)