Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for ACL, File Content Type, Cache Max Age, and user Metadata added #171

Closed
5 changes: 4 additions & 1 deletion piccolo_api/media/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@
"-",
"_",
".",
"(",
")",
)


Expand Down Expand Up @@ -199,7 +201,8 @@ async def store_file(
"""
Stores the file in whichever storage you're using, and returns a key
which uniquely identifes the file.

:param file_name:
The file name with which the file will be stored.
:param file:
The file to store.
:param user:
Expand Down
79 changes: 79 additions & 0 deletions piccolo_api/media/content_type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
CONTENT_TYPE = {
"aac": "audio/aac",
"abw": "application/x-abiword",
"arc": "application/x-freearc",
"avif": "image/avif",
"avi": "video/x-msvideo",
"azw": "application/vnd.amazon.ebook",
"bin": "application/octet-stream",
"bmp": "image/bmp",
"bz": "application/x-bzip",
"bz2": "application/x-bzip2",
"cda": "application/x-cdf",
"csh": "application/x-csh",
"css": "text/css",
"csv": "text/csv",
"doc": "application/msword",
"docx": "application/vnd.openxmlformats-officedocument"
".wordprocessingml.document",
"eot": "application/vnd.ms-fontobject",
"epub": "application/epub+zip",
"gz": "application/gzip",
"gif": "image/gif",
"htm": "text/html",
"ico": "image/vnd.microsoft.icon",
"ics": "text/calendar",
"jar": "application/java-archive",
"jpeg": "image/jpeg",
"js": "text/javascript",
"json": "application/json",
"jsonld": "application/ld+json",
"mid": "audio/x-midi",
"mjs": "text/javascript",
"mp3": "audio/mpeg",
"mp4": "video/mp4",
"mpeg": "video/mpeg",
"mpkg": "application/vnd.apple.installer+xml",
"odp": "application/vnd.oasis.opendocument.presentation",
"ods": "application/vnd.oasis.opendocument.spreadsheet",
"odt": "application/vnd.oasis.opendocument.text",
"oga": "audio/ogg",
"ogv": "video/ogg",
"ogx": "application/ogg",
"opus": "audio/opus",
"otf": "font/otf",
"png": "image/png",
"pdf": "application/pdf",
"php": "application/x-httpd-php",
"ppt": "application/vnd.ms-powerpoint",
"pptx": "application/vnd.openxmlformats-officedocument"
".presentationml.presentation",
"rar": "application/vnd.rar",
"rtf": "application/rtf",
"sh": "application/x-sh",
"svg": "image/svg+xml",
"swf": "application/x-shockwave-flash",
"tar": "application/x-tar",
"tif": "image/tiff",
"tiff": "image/tiff",
"ts": "video/mp2t",
"ttf": "font/ttf",
"txt": "text/plain",
"vsd": "application/vnd.visio",
"wav": "audio/wav",
"weba": "audio/webm",
"webm": "video/webm",
"webp": "image/webp",
"woff": "font/woff",
"woff2": "font/woff2",
"xhtml": "application/xhtml+xml",
"xls": "application/vnd.ms-excel",
"xlsx": "application/vnd.openxmlformats-officedocument"
".spreadsheetml.sheet",
"xml": "application/xml",
"xul": "application/vnd.mozilla.xul+xml",
"zip": "application/zip",
"3gp": "video/3gpp",
"3g2": "video/3gpp2",
"7z": "application/x-7z-compressed",
}
34 changes: 31 additions & 3 deletions piccolo_api/media/s3.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from piccolo.columns.column_types import Array, Text, Varchar

from .base import ALLOWED_CHARACTERS, ALLOWED_EXTENSIONS, MediaStorage
from .content_type import CONTENT_TYPE

if t.TYPE_CHECKING: # pragma: no cover
from concurrent.futures._base import Executor
Expand All @@ -22,7 +23,10 @@ def __init__(
column: t.Union[Text, Varchar, Array],
bucket_name: str,
folder_name: str,
cache_max_age: t.Optional[int] = None,
default_acl: str = "private",
dantownsend marked this conversation as resolved.
Show resolved Hide resolved
connection_kwargs: t.Dict[str, t.Any] = None,
user_defined_meta: t.Dict[str, t.Any] = None,
signed_url_expiry: int = 3600,
executor: t.Optional[Executor] = None,
allowed_extensions: t.Optional[t.Sequence[str]] = ALLOWED_EXTENSIONS,
Expand All @@ -38,12 +42,22 @@ def __init__(
The Piccolo ``Column`` which the storage is for.
:param bucket_name:
Which S3 bucket the files are stored in.
:param folder:
:param folder_name:
The files will be stored in this folder within the bucket. S3
buckets don't really have folders, but if ``folder`` is
``'movie_screenshots'``, then we store the file at
``'movie_screenshots/my-file-abc-123.jpeg'``, to simulate it being
in a folder.
:param cache_max_age:
It takes the value in second
For example::
cache_max_age=86400
This will keep the cache for 24 hour.
:param default_acl:
Defines the visibility of the file uploaded.
:param user_defined_meta:
Assign Meta Data to the file other than system metadata
For details read AWS S3 documentation
:param connection_kwargs:
These kwargs are passed directly to ``boto3``. Learn more about
`available options <https://boto3.amazonaws.com/v1/documentation/api/latest/reference/core/session.html#boto3.session.Session.client>`_.
Expand Down Expand Up @@ -85,10 +99,13 @@ def __init__(
self.boto3 = boto3

self.bucket_name = bucket_name
self.default_acl = default_acl
self.cache_max_age = cache_max_age
self.folder_name = folder_name
self.connection_kwargs = connection_kwargs
self.signed_url_expiry = signed_url_expiry
self.executor = executor or ThreadPoolExecutor(max_workers=10)
self.user_defined_meta = user_defined_meta

super().__init__(
column=column,
Expand All @@ -98,7 +115,7 @@ def __init__(

def get_client(self): # pragma: no cover
"""
Returns an S3 clent.
Returns an S3 client.
"""
session = self.boto3.session.Session()
client = session.client(
Expand Down Expand Up @@ -127,13 +144,24 @@ def store_file_sync(
A sync wrapper around :meth:`store_file`.
"""
file_key = self.generate_file_key(file_name=file_name, user=user)

extension = file_key.rsplit(".", 1)[-1]
client = self.get_client()
metadata: t.Dict[str, t.Any] = {
"ACL": self.default_acl,
"ContentDisposition": "inline",
dantownsend marked this conversation as resolved.
Show resolved Hide resolved
}
if extension in CONTENT_TYPE:
metadata["ContentType"] = CONTENT_TYPE[extension]
if self.cache_max_age:
metadata["CacheControl"] = f"max-age={self.cache_max_age}"
if self.user_defined_meta:
metadata["Metadata"] = self.user_defined_meta

client.upload_fileobj(
file,
self.bucket_name,
str(pathlib.Path(self.folder_name, file_key)),
ExtraArgs=metadata,
)

return file_key
Expand Down
3 changes: 3 additions & 0 deletions tests/media/test_s3.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ def test_store_file(self, get_client: MagicMock, uuid_module: MagicMock):

storage = S3MediaStorage(
column=Movie.poster,
default_acl="public-read",
user_defined_meta={"visibility": "premium"},
cache_max_age=15000,
bucket_name=bucket_name,
folder_name=folder_name,
connection_kwargs=connection_kwargs,
Expand Down