Skip to content

Commit f34a9f8

Browse files
authored
core.py: fix exclusive write for small files (#974)
1 parent 87b45ac commit f34a9f8

File tree

3 files changed

+86
-17
lines changed

3 files changed

+86
-17
lines changed

s3fs/core.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2458,6 +2458,7 @@ def n_bytes_left() -> int:
24582458

24592459
def commit(self):
24602460
logger.debug("Commit %s" % self)
2461+
match = {"IfNoneMatch": "*"} if "x" in self.mode else {}
24612462
if self.tell() == 0:
24622463
if self.buffer is not None:
24632464
logger.debug("Empty file committed %s" % self)
@@ -2471,15 +2472,11 @@ def commit(self):
24712472
kw = dict(Key=self.key, Bucket=self.bucket, Body=data, **self.kwargs)
24722473
if self.acl:
24732474
kw["ACL"] = self.acl
2474-
write_result = self._call_s3("put_object", **kw)
2475+
write_result = self._call_s3("put_object", **kw, **match)
24752476
else:
24762477
raise RuntimeError
24772478
else:
24782479
logger.debug("Complete multi-part upload for %s " % self)
2479-
if "x" in self.mode:
2480-
match = {"IfNoneMatch": "*"}
2481-
else:
2482-
match = {}
24832480
part_info = {"Parts": self.parts}
24842481
write_result = self._call_s3(
24852482
"complete_multipart_upload",

s3fs/tests/derived/s3fs_test.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ class TestS3fsPipe(abstract.AbstractPipeTests, S3fsFixtures):
3333

3434

3535
class TestS3fsOpen(abstract.AbstractOpenTests, S3fsFixtures):
36-
3736
test_open_exclusive = pytest.mark.xfail(
3837
reason="complete_multipart_upload doesn't implement condition in moto"
3938
)(abstract.AbstractOpenTests.test_open_exclusive)

s3fs/tests/test_s3fs.py

Lines changed: 84 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import fsspec.core
1919
from dateutil.tz import tzutc
2020

21+
import botocore
2122
import s3fs.core
2223
from s3fs.core import S3FileSystem
2324
from s3fs.utils import ignoring, SSEParams
@@ -2888,7 +2889,14 @@ def test_exist_after_delete(s3):
28882889
assert not s3.exists(test_dir)
28892890

28902891

2891-
@pytest.mark.xfail(reason="moto doesn't support conditional MPU")
2892+
# condition: True if running on botocore < 1.36.0
2893+
# The below tests for exclusive writes will fail on older versions of botocore.
2894+
old_botocore = version.parse(botocore.__version__) < version.parse("1.36.0")
2895+
2896+
2897+
@pytest.mark.xfail(
2898+
reason="moto doesn't support IfNoneMatch for MPU when object created via MPU"
2899+
)
28922900
def test_pipe_exclusive_big(s3):
28932901
chunksize = 5 * 2**20 # minimum allowed
28942902
data = b"x" * chunksize * 3
@@ -2899,17 +2907,82 @@ def test_pipe_exclusive_big(s3):
28992907
assert not s3.list_multipart_uploads(test_bucket_name)
29002908

29012909

2902-
@pytest.mark.xfail(reason="moto doesn't support conditional MPU")
2903-
def test_put_exclusive_big(s3, tempdir):
2910+
@pytest.mark.xfail(
2911+
old_botocore, reason="botocore<1.33.0 lacks IfNoneMatch support", strict=True
2912+
)
2913+
def test_pipe_exclusive_big_after_small(s3):
2914+
"""Test conditional MPU after creating object via put_object
2915+
2916+
This test is required because moto's implementation of IfNoneMatch for MPU
2917+
only works when the object is initially created via put_object and not via
2918+
MPU.
2919+
"""
29042920
chunksize = 5 * 2**20 # minimum allowed
2905-
data = b"x" * chunksize * 3
2906-
fn = f"{tempdir}/afile"
2907-
with open(fn, "wb") as f:
2908-
f.write(fn)
2909-
s3.put(fn, f"{test_bucket_name}/afile", data, mode="overwrite", chunksize=chunksize)
2910-
s3.put(fn, f"{test_bucket_name}/afile", data, mode="overwrite", chunksize=chunksize)
2921+
2922+
# First, create object via put_object (small upload)
2923+
s3.pipe(f"{test_bucket_name}/afile", b"small", mode="overwrite")
2924+
2925+
# Now try multipart upload with mode="create" (should fail)
29112926
with pytest.raises(FileExistsError):
2912-
s3.put(
2913-
fn, f"{test_bucket_name}/afile", data, mode="create", chunksize=chunksize
2927+
s3.pipe(
2928+
f"{test_bucket_name}/afile",
2929+
b"c" * chunksize * 3,
2930+
mode="create",
2931+
chunksize=chunksize,
29142932
)
2933+
2934+
assert not s3.list_multipart_uploads(test_bucket_name)
2935+
2936+
2937+
@pytest.mark.xfail(
2938+
reason="moto doesn't support IfNoneMatch for MPU when object created via MPU"
2939+
)
2940+
def test_put_exclusive_big(s3, tmpdir):
2941+
chunksize = 5 * 2**20 # minimum allowed
2942+
fn = f"{tmpdir}/afile"
2943+
with open(fn, "wb") as f:
2944+
f.write(b"x" * chunksize * 3)
2945+
s3.put(fn, f"{test_bucket_name}/afile", mode="overwrite", chunksize=chunksize)
2946+
s3.put(fn, f"{test_bucket_name}/afile", mode="overwrite", chunksize=chunksize)
2947+
with pytest.raises(FileExistsError):
2948+
s3.put(fn, f"{test_bucket_name}/afile", mode="create", chunksize=chunksize)
2949+
assert not s3.list_multipart_uploads(test_bucket_name)
2950+
2951+
2952+
@pytest.mark.xfail(
2953+
old_botocore, reason="botocore<1.33.0 lacks IfNoneMatch support", strict=True
2954+
)
2955+
def test_put_exclusive_big_after_small(s3, tmpdir):
2956+
"""Test conditional MPU after creating object via put_object.
2957+
2958+
This test is required because moto's implementation of IfNoneMatch for MPU
2959+
only works when the object is initially created via put_object and not via
2960+
MPU.
2961+
"""
2962+
chunksize = 5 * 2**20 # minimum allowed
2963+
fn = str(tmpdir.join("afile"))
2964+
with open(fn, "wb") as f:
2965+
f.write(b"x" * chunksize * 3)
2966+
2967+
# First, create object via put_object (small upload)
2968+
s3.pipe(f"{test_bucket_name}/afile", b"small", mode="overwrite")
2969+
2970+
# Now try multipart upload with mode="create" (should fail)
2971+
with pytest.raises(FileExistsError):
2972+
s3.put(fn, f"{test_bucket_name}/afile", mode="create", chunksize=chunksize)
2973+
2974+
assert not s3.list_multipart_uploads(test_bucket_name)
2975+
2976+
2977+
@pytest.mark.xfail(
2978+
old_botocore, reason="botocore<1.33.0 lacks IfNoneMatch support", strict=True
2979+
)
2980+
def test_put_exclusive_small(s3, tmpdir):
2981+
fn = f"{tmpdir}/afile"
2982+
with open(fn, "wb") as f:
2983+
f.write(b"x")
2984+
s3.put(fn, f"{test_bucket_name}/afile", mode="overwrite")
2985+
s3.put(fn, f"{test_bucket_name}/afile", mode="overwrite")
2986+
with pytest.raises(FileExistsError):
2987+
s3.put(fn, f"{test_bucket_name}/afile", mode="create")
29152988
assert not s3.list_multipart_uploads(test_bucket_name)

0 commit comments

Comments
 (0)