Media Storage

Piccolo Admin has excellent support for managing media files (images, audio, video, and more). The files can be stored locally on the server, or in S3 compatible storage.

../_images/media_upload.png

User Interface

You’re able to view many file types within the user interface - images, videos, audio, documents, and more.

Viewing images:

../_images/media_viewer_image.png

Viewing video:

../_images/media_viewer_video.png

Viewing audio:

../_images/media_viewer_audio.png

Viewing PDFs:

../_images/media_viewer_pdf.png

Supported columns

We store the files in a folder on the server, or in a S3 bucket, and store unique references to those files in the database.

Note

We don’t store the files directly in the database, because database storage is typically much more expensive than block / object storage.

An example file reference is my-file-abc-123.jpeg. Since it’s a string, we can only store it in a database column which stores strings.

Varchar

Varchar is a good choice for storing file references. For example:

from piccolo.table import Table
from piccolo.column.column_types import Varchar


class Movie(Table):
    poster = Varchar()

Text

Text can also be used. For example:

from piccolo.table import Table
from piccolo.column.column_types import Text


class Movie(Table):
    poster = Text()

Array

We also support Array, but only when the base_column is either Varchar or Text. For example:

from piccolo.table import Table
from piccolo.column.column_types import Array, Varchar


class Movie(Table):
    screenshots = Array(base_column=Varchar())

This allows us to store multiple file references in a single column.

MediaStorage

For each column we want to use for media storage, we associate it with a MediaStorage instance.

Out of the box we have two subclasses - LocalMediaStorage and S3MediaStorage. You can also create your own subclass of MediaStorage to implement your own storage backend.

LocalMediaStorage

This stores media in a folder on the server.

Example

In order to associate a column with LocalMediaStorage, we do the following:

import os

from piccolo.columns import Array, Varchar
from piccolo_admin.endpoints import  (
    TableConfig,
    create_admin
)
from piccolo_api.media.local import LocalMediaStorage


class Movie(Table):
    title = Varchar()
    poster = Varchar()
    screenshots = Array(base_column=Varchar())


MEDIA_ROOT = '/srv/piccolo-admin/'


MOVIE_POSTER_MEDIA = LocalMediaStorage(
    column=Movie.poster,
    media_path=os.path.join(MEDIA_ROOT, 'movie_poster'),
    allowed_extensions=['jpg', 'jpeg', 'png']
)


MOVIE_SCREENSHOTS_MEDIA = LocalMediaStorage(
    column=Movie.screenshots,
    media_path=os.path.join(MEDIA_ROOT, 'movie_screenshots'),
    allowed_extensions=['jpg', 'jpeg', 'png']
)


movie_config = TableConfig(
    table_class=Movie,
    media_storage=[MOVIE_POSTER_MEDIA, MOVIE_SCREENSHOTS_MEDIA],
)


APP = create_admin([movie_config])

Some things to be aware of:

  • By specifiying allowed_extensions, we make sure that only images can be uploaded.

  • We store the files for posters and screenshots in separate folders - this is important when cleaning up files.

Source

class piccolo_api.media.local.LocalMediaStorage(column: t.Union[Text, Varchar, Array], media_path: str, executor: t.Optional[Executor] = None, allowed_extensions: t.Optional[t.Sequence[str]] = ALLOWED_EXTENSIONS, allowed_characters: t.Optional[t.Sequence[str]] = ALLOWED_CHARACTERS, file_permissions: t.Optional[int] = 0o600)[source]

Stores media files on a local path. This is good for simple applications, where you’re happy with the media files being stored on a single server.

Parameters:
  • column – The Piccolo Column which the storage is for.

  • media_path – This is the local folder where the media files will be stored. It should be an absolute path. For example, '/srv/piccolo-media/poster/'.

  • executor – An executor, which file save operations are run in, to avoid blocking the event loop. If not specified, we use a sensibly configured ThreadPoolExecutor.

  • allowed_extensions – Which file extensions are allowed. If None, then all extensions are allowed (not recommended unless the users are trusted).

  • allowed_characters – Which characters are allowed in the file name. By default, it’s very strict. If set to None then all characters are allowed.

  • file_permissions – If set to a value other than None, then all uploaded files are given these file permissions.

S3MediaStorage

This allows us to store files in a private S3 bucket. When we need to access a file, a signed URL is generated, so the file can be viewed securely.

Example

In order to associate a column with S3MediaStorage, we do the following:

import os

from piccolo.columns import Array, Varchar
from piccolo_admin.endpoints import  (
    TableConfig,
    create_admin
)
from piccolo_api.media.s3 import S3MediaStorage


class Movie(Table):
    title = Varchar()
    poster = Varchar()
    screenshots = Array(base_column=Varchar())


# Note - don't store credentials in source code if possible.
# It's safer to read them from environment variables.
# A tool like `python-dotenv` can help with this.
S3_CONNECTION_KWARGS = {
    "aws_access_key_id": os.environ.get("AWS_ACCESS_KEY_ID"),
    "aws_secret_access_key": os.environ.get("AWS_SECRET_ACCESS_KEY"),
}


MOVIE_POSTER_MEDIA = S3MediaStorage(
    column=Movie.poster,
    bucket_name="bucket123"
    folder_name="movie_poster"
    connection_kwargs=S3_CONNECTION_KWARGS,
    allowed_extensions=['jpg', 'jpeg', 'png']
)


MOVIE_SCREENSHOTS_MEDIA = S3MediaStorage(
    column=Movie.screenshots,
    bucket_name="bucket123"
    folder_name="movie_screenshots"
    connection_kwargs=S3_CONNECTION_KWARGS,
    allowed_extensions=['jpg', 'jpeg', 'png']
)


movie_config = TableConfig(
    table_class=Movie,
    media_storage=[MOVIE_POSTER_MEDIA, MOVIE_SCREENSHOTS_MEDIA],
)


APP = create_admin([movie_config])

Some things to be aware of:

  • By specifiying allowed_extensions, we make sure that only images can be uploaded.

  • We store the files for posters and screenshots in separate folders within the bucket - this is important when cleaning up files. You could even store them in separate buckets if you prefer.

Source

class piccolo_api.media.s3.S3MediaStorage(column: t.Union[Text, Varchar, Array], bucket_name: str, folder_name: t.Optional[str] = None, connection_kwargs: t.Optional[t.Dict[str, t.Any]] = None, sign_urls: bool = True, signed_url_expiry: int = 3600, upload_metadata: t.Optional[t.Dict[str, t.Any]] = None, executor: t.Optional[Executor] = None, allowed_extensions: t.Optional[t.Sequence[str]] = ALLOWED_EXTENSIONS, allowed_characters: t.Optional[t.Sequence[str]] = ALLOWED_CHARACTERS)[source]

Stores media files in S3 compatible storage. This is a good option when you have lots of files to store, and don’t want them stored locally on a server. Many cloud providers provide S3 compatible storage, besides from Amazon Web Services.

Parameters:
  • column – The Piccolo Column which the storage is for.

  • bucket_name – Which S3 bucket the files are stored in.

  • 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.

  • connection_kwargs

    These kwargs are passed directly to the boto3 client. For example:

    S3MediaStorage(
        ...,
        connection_kwargs={
            'aws_access_key_id': 'abc123',
            'aws_secret_access_key': 'xyz789',
            'endpoint_url': 's3.cloudprovider.com',
            'region_name': 'uk'
        }
    )
    

  • sign_urls – Whether to sign the URLs - by default this is True, as it’s highly recommended that your store your files in a private bucket.

  • signed_url_expiry – Files are accessed via signed URLs, which are only valid for this number of seconds.

  • upload_metadata

    You can provide additional metadata to the uploaded files. To see all available options see S3Transfer.ALLOWED_UPLOAD_ARGS. Below we show examples of common use cases.

    To set the ACL:

    S3MediaStorage(
        ...,
        upload_metadata={'ACL': 'my_acl'}
    )
    

    To set the content disposition (how the file behaves when opened - is it downloaded, or shown in the browser):

    S3MediaStorage(
        ...,
        # Shows the file within the browser:
        upload_metadata={'ContentDisposition': 'inline'}
    )
    

    To attach user defined metadata to the file:

    S3MediaStorage(
        ...,
        upload_metadata={'Metadata': {'myfield': 'abc123'}}
    )
    

    To specify how long browsers should cache the file for:

    S3MediaStorage(
        ...,
        # Cache the file for 24 hours:
        upload_metadata={'CacheControl': 'max-age=86400'}
    )
    

    Note: We automatically add the ContentType field based on the file type.

  • executor – An executor, which file save operations are run in, to avoid blocking the event loop. If not specified, we use a sensibly configured ThreadPoolExecutor.

  • allowed_extensions – Which file extensions are allowed. If None, then all extensions are allowed (not recommended unless the users are trusted).

  • allowed_characters – Which characters are allowed in the file name. By default, it’s very strict. If set to None then all characters are allowed.

Integrating it with your wider app

If you’re using Piccolo Admin as part of a larger application, you can easily gain access to the stored files, and use them within your own code.

For example:

# We can fetch a file from storage
file = await MOVIE_POSTER_MEDIA.get_file('some-file-key.jpeg')

# We can delete files from storage
await MOVIE_POSTER_MEDIA.delete_file('some-file-key.jpeg')

To see all of the methods available, look at MediaStorage.

Cleaning up files

If we delete a row from the database which references a stored file, the file isn’t automatically deleted. This is common practice, as it gives a bit more safety against accidentally deleting files.

We can periodically delete any files which are no longer referenced in the database.

await MOVIE_POSTER_MEDIA.delete_unused_files()

Warning

It’s very important that each column stores files in its own folder or S3 bucket. If multiple columns share the same folder, when we run delete_unused_files we may delete files needed by another column.