Skip to content

Commit 6d04cd1

Browse files
geoffjukeswillmcgugan
authored andcommitted
Enable upload/download ExtraArgs (#33)
* enable server-side bucket to bucket copies The 'copy_object' method permits setting different source and target buckets. When different, a server-side copy takes place between the buckets. Appropriate ACLs must exist in AWS, but this is a quick method for copying keys between buckets within the same account and region. Copying across regions and/or accounts is more complex, and is not enabled with this change. * Allow settable extra_args for uploads and downloads Allow setting upload and download `extra_args` values for the lifetime of the filesystem instance. Ticket #32 has details * Moved args to constructor and removed bucket copy * load unquoted json if value is a string * Expose `acl` and `cache-control` directly and via opener * Small cleanup * (bugfix) Only set if value exists * Boto3 changed from `extra_args` to `ExtraArgs` at some point This has been tested and works. I uploaded a file to a private bucket, setting the max-age and ACL.
1 parent 8ff843b commit 6d04cd1

File tree

4 files changed

+82
-9
lines changed

4 files changed

+82
-9
lines changed

README.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,36 @@ source filesystem to the S3 filesystem.
5959
See [Moving and Copying](https://docs.pyfilesystem.org/en/latest/guide.html#moving-and-copying)
6060
for more information.
6161

62+
## ExtraArgs
63+
64+
S3 objects have additional properties, beyond a traditional
65+
filesystem. These options can be set using the ``upload_args``
66+
and ``download_args`` properties. which are handed to upload
67+
and download methods, as appropriate, for the lifetime of the
68+
filesystem instance.
69+
70+
For example, to set the ``cache-control`` header of all objects
71+
uploaded to a bucket:
72+
73+
```python
74+
import fs, fs.mirror
75+
s3fs = S3FS('example', upload_args={"CacheControl": "max-age=2592000", "ACL": "public-read"})
76+
fs.mirror.mirror('/path/to/mirror', s3fs)
77+
```
78+
79+
see [the Boto3 docs](https://boto3.readthedocs.io/en/latest/reference/customizations/s3.html#boto3.s3.transfer.S3Transfer.ALLOWED_UPLOAD_ARGS)
80+
for more information.
81+
82+
`acl` and `cache_control` are exposed explicitly for convenience, and can be used in URLs.
83+
It is important to URL-Escape the `cache_control` value in a URL, as it may contain special characters.
84+
85+
```python
86+
import fs, fs.mirror
87+
with open fs.open_fs('s3://example?acl=public-read&cache_control=max-age%3D2592000%2Cpublic') as s3fs
88+
fs.mirror.mirror('/path/to/mirror', s3fs)
89+
```
90+
91+
6292
## S3 URLs
6393

6494
You can get a public URL to a file on a S3 bucket as follows:

README.rst

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,34 @@ filesystem to the S3 filesystem. See `Moving and
6464
Copying <https://docs.pyfilesystem.org/en/latest/guide.html#moving-and-copying>`__
6565
for more information.
6666

67+
ExtraArgs
68+
---------
69+
70+
S3 objects have additional properties, beyond a traditional
71+
filesystem. These options can be set using the ``upload_args``
72+
and ``download_args`` properties. which are handed to upload
73+
and download methods, as appropriate, for the lifetime of the
74+
filesystem instance.
75+
76+
For example, to set the ``cache-control`` header of all objects
77+
uploaded to a bucket:
78+
79+
.. code:: python
80+
import fs, fs.mirror
81+
s3fs = S3FS('example', upload_args={"CacheControl": "max-age=2592000", "ACL": "public-read"})
82+
fs.mirror.mirror('/path/to/mirror', s3fs)
83+
84+
see `the Boto3 docs <https://boto3.readthedocs.io/en/latest/reference/customizations/s3.html#boto3.s3.transfer.S3Transfer.ALLOWED_UPLOAD_ARGS>`__
85+
for more information.
86+
87+
``acl`` and ``cache_control`` are exposed explicitly for convenience, and can be used in URLs.
88+
It is important to URL-Escape the ``cache_control`` value in a URL, as it may contain special characters.
89+
90+
.. code:: python
91+
import fs, fs.mirror
92+
with open fs.open_fs('s3://example?acl=public-read&cache_control=max-age%3D2592000%2Cpublic') as s3fs
93+
fs.mirror.mirror('/path/to/mirror', s3fs)
94+
6795
S3 URLs
6896
-------
6997

fs_s3fs/_s3fs.py

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from ssl import SSLError
1313
import tempfile
1414
import threading
15+
import json
1516

1617
import boto3
1718
from botocore.exceptions import ClientError, EndpointConnectionError
@@ -269,7 +270,11 @@ def __init__(self,
269270
endpoint_url=None,
270271
region=None,
271272
delimiter='/',
272-
strict=True):
273+
strict=True,
274+
cache_control=None,
275+
acl=None,
276+
upload_args=None,
277+
download_args=None):
273278
_creds = (aws_access_key_id, aws_secret_access_key)
274279
if any(_creds) and not all(_creds):
275280
raise ValueError(
@@ -287,6 +292,14 @@ def __init__(self,
287292
self.delimiter = delimiter
288293
self.strict = strict
289294
self._tlocal = threading.local()
295+
if cache_control or acl:
296+
upload_args = upload_args or {}
297+
if cache_control:
298+
upload_args['CacheControl'] = cache_control
299+
if acl:
300+
upload_args['ACL'] = acl
301+
self.upload_args = upload_args
302+
self.download_args = download_args
290303
super(S3FS, self).__init__()
291304

292305
def __repr__(self):
@@ -540,7 +553,7 @@ def on_close_create(s3file):
540553
s3file.raw.seek(0)
541554
with s3errors(path):
542555
self.client.upload_fileobj(
543-
s3file.raw, self._bucket_name, _key
556+
s3file.raw, self._bucket_name, _key, ExtraArgs=self.upload_args
544557
)
545558
finally:
546559
s3file.raw.close()
@@ -568,7 +581,7 @@ def on_close_create(s3file):
568581
try:
569582
with s3errors(path):
570583
self.client.download_fileobj(
571-
self._bucket_name, _key, s3file.raw
584+
self._bucket_name, _key, s3file.raw, ExtraArgs=self.download_args
572585
)
573586
except errors.ResourceNotFound:
574587
pass
@@ -589,15 +602,15 @@ def on_close(s3file):
589602
s3file.raw.seek(0, os.SEEK_SET)
590603
with s3errors(path):
591604
self.client.upload_fileobj(
592-
s3file.raw, self._bucket_name, _key
605+
s3file.raw, self._bucket_name, _key, ExtraArgs=self.upload_args
593606
)
594607
finally:
595608
s3file.raw.close()
596609

597610
s3file = S3File.factory(path, _mode, on_close=on_close)
598611
with s3errors(path):
599612
self.client.download_fileobj(
600-
self._bucket_name, _key, s3file.raw
613+
self._bucket_name, _key, s3file.raw, ExtraArgs=self.download_args
601614
)
602615
s3file.seek(0, os.SEEK_SET)
603616
return s3file
@@ -660,7 +673,7 @@ def getbytes(self, path):
660673
bytes_file = io.BytesIO()
661674
with s3errors(path):
662675
self.client.download_fileobj(
663-
self._bucket_name, _key, bytes_file
676+
self._bucket_name, _key, bytes_file, ExtraArgs=self.download_args
664677
)
665678
return bytes_file.getvalue()
666679

@@ -674,7 +687,7 @@ def getfile(self, path, file, chunk_size=None, **options):
674687
_key = self._path_to_key(_path)
675688
with s3errors(path):
676689
self.client.download_fileobj(
677-
self._bucket_name, _key, file
690+
self._bucket_name, _key, file, ExtraArgs=self.download_args
678691
)
679692

680693
def exists(self, path):
@@ -758,7 +771,7 @@ def setbytes(self, path, contents):
758771
bytes_file = io.BytesIO(contents)
759772
with s3errors(path):
760773
self.client.upload_fileobj(
761-
bytes_file, self._bucket_name, _key
774+
bytes_file, self._bucket_name, _key, ExtraArgs=self.upload_args
762775
)
763776

764777
def setbinfile(self, path, file):
@@ -776,7 +789,7 @@ def setbinfile(self, path, file):
776789
pass
777790

778791
with s3errors(path):
779-
self.client.upload_fileobj(file, self._bucket_name, _key)
792+
self.client.upload_fileobj(file, self._bucket_name, _key, ExtraArgs=self.upload_args)
780793

781794
def copy(self, src_path, dst_path, overwrite=False):
782795
if not overwrite and self.exists(dst_path):

fs_s3fs/opener.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ def open_fs(self, fs_url, parse_result, writeable, create, cwd):
3333
aws_access_key_id=parse_result.username or None,
3434
aws_secret_access_key=parse_result.password or None,
3535
endpoint_url=parse_result.params.get('endpoint_url', None),
36+
acl=parse_result.params.get('acl', None),
37+
cache_control=parse_result.params.get('cache_control', None),
3638
strict=strict
3739
)
3840
return s3fs

0 commit comments

Comments
 (0)