@@ -125,6 +125,15 @@ class ReadableBinaryFile(Protocol):
125
125
def read (self , size : int = ...) -> bytes : ...
126
126
127
127
128
+ class NonCloseableBufferedReader (io .BufferedReader ):
129
+ def __init__ (self , raw : ReadableBinaryFile , * args : Any , ** kwargs : Any ):
130
+ super ().__init__ (cast (io .RawIOBase , raw ), * args , ** kwargs )
131
+
132
+ def close (self ) -> None :
133
+ # Don't close the underlying file object, just flush the buffer.
134
+ self .flush ()
135
+
136
+
128
137
class StreamingBodyIOAdapter (io .RawIOBase ):
129
138
"""
130
139
Wrapper to adapt a boto3 S3 object to a standard Python "file-like" object
@@ -467,6 +476,16 @@ def store_file(
467
476
468
477
readable = _to_stream (contents = contents )
469
478
479
+ # If `readable` is an actual file-like object(like files from `open()` or Django’s
480
+ # `UploadedFile`) wrap it with `NonCloseableBufferedReader` to prevent closing
481
+ # by boto3 after upload. No need to do this for BytesIO/StringIO because they are
482
+ # in-memory buffers thus do not necessarily need protection from `close()`. Also,
483
+ # `BufferedReader` expects a raw binary stream, not a BytesIO or StringIO.
484
+ if hasattr (readable , "read" ) and not isinstance (
485
+ readable , (io .BytesIO , io .StringIO )
486
+ ):
487
+ readable = NonCloseableBufferedReader (readable )
488
+
470
489
# `boto_client.upload_fileobj` is type annotated with `Fileobj: BinaryIO`. However, in
471
490
# practice the only file-like method it needs is `read(size=...)`. This cast allows us to
472
491
# use `upload_fileobj` with any Fileobj that implements the `ReadableBinaryFile` protocol
@@ -489,6 +508,9 @@ def store_file(
489
508
ExtraArgs = extra_args ,
490
509
)
491
510
511
+ if isinstance (file_obj , NonCloseableBufferedReader ):
512
+ file_obj .detach ()
513
+
492
514
return self .bucket_name , key_path
493
515
494
516
def store_versioned_file (
0 commit comments