pyramid_storage

pyramid_storage is a simple file upload manager for the Pyramid framework. It currently supports uploads to the local file system and to the Amazon S3 cloud storage service.

Installation

Install with pip install pyramid_storage. To install from source, unzip/tar, cd and python setup.py install.

Bugs and issues

Please report bugs and issues (and better still, pull requests) on the Github repo.

Getting started

There are a number of ways to configure pyramid_storage with your Pyramid app.

The easiest way is to add pyramid_storage to the pyramid.includes directive in your configuration file(s):

pyramid.includes =
    pyramid_storage

Alternatively you can use pyramid.config.Configurator.include() in your app setup:

config.include('pyramid_storage')

Either setup will add an instance of pyramid_storage.storage.FileStorage to your app registry. The instance will also be available as a property of your request as request.storage.

To use S3 file storage instead of storing files locally on your server (the default assumption):

pyramid.includes =
    pyramid_storage.s3

alternatively:

config.include('pyramid_storage.s3')

Configuration

Local file storage (default)

The available settings are listed below:

Setting Default Description
base_path required Absolute location for storing uploads
base_url   Relative or absolute base URL for uploads; must end in slash (“/”)
extensions default List of extensions or extension groups (see below)
name storage Name of property added to request, e.g. request.storage

S3 file storage

Setting Default Description
aws.access_key required AWS access key
aws.secret_key required AWS secret key
aws.bucket_name required AWS bucket
aws.acl public-read AWS ACL permissions
base_url   Relative or absolute base URL for uploads; must end in slash (“/”)
extensions default List of extensions or extension groups (see below)
name storage Name of property added to request, e.g. request.storage

Configuring extensions: extensions are given as a list of space-separated extensions or groups of extensions. These groups provide a convenient shortcut for including a large number of extensions. Each group must be separated by a plus-sign “+”. Some examples:

  • storage.extensions = images - all image formats e.g. jpg, gif, png
  • storage.extensions = images+documents - image and document formats (rtf, doc etc)
  • storage.extensions = images+documents+rst xml json - all image and document formats, plus the extensions rst, xml and json.

The extension groups are listed below:

Group Extensions
any all extensions (including no extensions)
text txt
documents pdf rtf odf ods gnumeric abw doc docx xls xls
images jpg jpe jpeg png gif svg bmp tiff
audio wav mp3 aac ogg oga flac
video mpeg 3gp avi divx dvr flv mp4 wmv
data csv ini json plist xml yaml yml
scripts js php pl py rb sh
archives gz bz2 zip tar tgz txz 7z
executables so exe dll
default documents+images+text+data

Usage: local file storage

Warning

It is the responsibility of the deployment team to ensure that target directories used in file uploads have the appropriate read/write permissions.

When uploading a file in a view, call pyramid_storage.storage.FileStorage.save() to save the file to your file system:

from pyramid.view import view_config
from pyramid.httpexceptions import HTTPSeeOther

@view_config(route_name='upload',
             request_method='POST')
def upload(request):
    request.storage.save(request.POST['my_file'])
    return HTTPSeeOther(request.route_url('home'))

This operation will save the file to your file system under the top directory specified by the base_path setting.

If the file does not have the correct file extension, a pyramid_storage.exceptions.FileNotAllowed exception is raised. A more secure way of writing the above would be:

from pyramid_storage.exceptions import FileNotAllowed

@view_config(route_name='upload',
             request_method='POST')
def upload(request):
    try:
        request.storage.save(request.POST['my_file'])
    except FileNotAllowed:
        request.session.flash('Sorry, this file is not allowed')
    return HTTPSeeOther(request.route_url('home'))

You can override the default extensions in the method call:

from pyramid_storage.exceptions import FileNotAllowed

@view_config(route_name='upload',
             request_method='POST')
def upload(request):
    try:
        request.storage.save(request.POST['my_file'],
                             extensions=('jpg', 'png', 'txt'))
    except FileNotAllowed:
        request.session.flash('Sorry, this file is not allowed')
    return HTTPSeeOther(request.route_url('home'))

You may also wish to obfuscate or randomize the filename. The randomize argument will generate a random filename, preserving the extension:

filename = request.storage.save(request.POST['my_file'], randomize=True)

So for example if your filename is test.jpg the new filename will be something like 235a344c-8d70-498a-af0a-151afdfcd803.jpg.

If there is a filename clash (i.e. another file with the same name is in the target directory) a numerical suffix is added to the new filename. For example, if you have an existing file test.jpg then the next file with that name will be renamed test-1.jpg and so on.

Warning

Remember to ensure your forms include the attribute enctype=”multipart/form-data” or your uploaded files will be empty.

If you pass in the folder argument this will be used to add subfolder(s):

request.storage.save(request.POST['my_file'], folder="photos")

The above call will store the contents of my_file under the directory photos under your base path.

If you want to check in advance that the extension is permitted (for example, in the form validation stage) you can use pyramid_storage.storage.FileStorage.file_allowed():

request.storage.file_allowed(request.POST['my_file'])

To access the URL of the file, for example in your Python code or templates, use the pyramid_storage.storage.FileStorage.url() method:

request.storage.url(filename)

You may not wish to provide public access to files - for example users may upload to private directories. In that case you can simply serve the file in your views:

from pyramid.response import FileResponse

@view_config(route_name='download')
def download(request):
    filename = request.params['filename']
    return FileResponse(request.storage.path(filename))

Usage: s3 file storage

Warning

S3 support requires you install the Boto library separately (e.g. pip install boto). As of writing this library is not Python 3 compatible.

Warning

It is the responsibility of the deployment team to ensure that the application has the correct AWS settings and permissions.

Basic usage is similar to LocalFileStorage:

from pyramid.view import view_config
from pyramid.httpexceptions import HTTPSeeOther

@view_config(route_name='upload',
             request_method='POST')
def upload(request):
    request.storage.save(request.POST['my_file'])
    return HTTPSeeOther(request.route_url('home'))

One difference is that filenames are not resolved with a numeric suffix as with local files, to prevent network round-trips. Instead you can pass the replace argument to replace the file on s3 (default is False):

from pyramid.view import view_config
from pyramid.httpexceptions import HTTPSeeOther

@view_config(route_name='upload',
             request_method='POST')
def upload(request):
    request.storage.save(request.POST['my_file'], replace=True)
    return HTTPSeeOther(request.route_url('home'))

Alternatively you can use the randomize argument to ensure a (near) unique filename.

The storage.base_url setting should be set to //s3amazonaws.com/<my-bucket-name>/ unless you want to serve the file behind a proxy or through your Pyramid application.

Testing

It’s easier to run unit tests on your upload views without actually handling real files. The class pyramid_storage.storage.DummyFileStorage provides a convenient way to mock these operations.

This class stores the names of the files internally for your assertions in the saved attribute:

import mock

from pyramid.testing import DummyRequest
from pyramid_storage.storage import DummyFileStorage

def test_my_upload():
    from .views import my_upload_view
    req = DummyRequest()
    req.storage = DummyFileStorage()

    my_file = mock.Mock()
    my_file.filename = 'test.jpg'

    req.POST['my_file'] = my_file

    res = my_upload_view(req)
    assert 'test.jpg' in req.storage.saved

Not that DummyFileStorage only provides one or two convenience methods. You may wish to extend this class for your own specific needs.

API

class FileNotAllowed[source]

Thrown if file does not have an allowed extension.

class LocalFileStorage(base_path, base_url='', extensions='default')[source]

Manages storage and retrieval of file uploads to local filesystem on server.

Parameters:
  • base_path – the absolute base path where uploads are stored
  • base_url – absolute or relative base URL for uploads
  • extensions – extensions string
delete(filename)[source]

Deletes the filename. Filename is resolved with the absolute path based on base_path. If file does not exist, returns False, otherwise True

Parameters:filename – base name of file
exists(filename)[source]

Checks if file exists. Resolves filename’s absolute path based on base_path.

Parameters:filename – base name of file
extension_allowed(ext, extensions=None)[source]

Checks if an extension is permitted. Both e.g. ”.jpg” and “jpg” can be passed in. Extension lookup is case-insensitive.

Parameters:extensions – iterable of extensions (or self.extensions)
file_allowed(fs, extensions=None)[source]

Checks if a file can be saved, based on extensions

Parameters:
  • fscgi.FieldStorage object or similar
  • extensions – iterable of extensions (or self.extensions)
filename_allowed(filename, extensions=None)[source]

Checks if a filename has an allowed extension

Parameters:
  • filename – base name of file
  • extensions – iterable of extensions (or self.extensions)
classmethod from_settings(settings, prefix)[source]

Returns a new instance from config settings.

Parameters:
  • settings – dict(-like) of settings
  • prefix – prefix separating these settings
path(filename)[source]

Returns absolute file path of the filename, joined to the base_path.

Parameters:filename – base name of file
resolve_name(name, folder)[source]

Resolves a unique name and the correct path. If a filename for that path already exists then a numeric prefix will be added, for example test.jpg -> test-1.jpg etc.

Parameters:
  • name – base name of file
  • folder – absolute folder path
save(fs, *args, **kwargs)[source]

Saves contents of a cgi.FieldStorage object to the file system. Returns modified filename(including folder). If there is a clash with an existing filename then filename will be resolved accordingly. If path directories do not exist they will be created.

Returns the resolved filename, i.e. the folder + the (randomized/incremented) base name.

Parameters:
  • fscgi.FieldStorage object (or similar)
  • folder – relative path of sub-folder
  • randomize – randomize the filename
  • extensions – iterable of allowed extensions, if not default
Returns:

modified filename

save_file(file, filename, folder=None, randomize=False, extensions=None)[source]

Saves a file object to the uploads location. Returns the resolved filename, i.e. the folder + the (randomized/incremented) base name.

Parameters:
  • fscgi.FieldStorage object (or similar)
  • filename – original filename
  • folder – relative path of sub-folder
  • randomize – randomize the filename
  • extensions – iterable of allowed extensions, if not default
Returns:

modified filename

save_filename(filename, *args, **kwargs)[source]

Saves a filename in local filesystem to the uploads location.

Returns the resolved filename, i.e. the folder + the (randomized/incremented) base name.

Parameters:
  • filename – local filename
  • folder – relative path of sub-folder
  • randomize – randomize the filename
  • extensions – iterable of allowed extensions, if not default
Returns:

modified filename

url(filename)[source]

Returns entire URL of the filename, joined to the base_url

Parameters:filename – base name of file
class S3FileStorage(access_key, secret_key, bucket_name, acl=None, base_url='', extensions='default')[source]
delete(filename)[source]

Deletes the filename. Filename is resolved with the absolute path based on base_path. If file does not exist, returns False, otherwise True

Parameters:filename – base name of file
extension_allowed(ext, extensions=None)[source]

Checks if an extension is permitted. Both e.g. ”.jpg” and “jpg” can be passed in. Extension lookup is case-insensitive.

Parameters:extensions – iterable of extensions (or self.extensions)
file_allowed(fs, extensions=None)[source]

Checks if a file can be saved, based on extensions

Parameters:
  • fscgi.FieldStorage object or similar
  • extensions – iterable of extensions (or self.extensions)
filename_allowed(filename, extensions=None)[source]

Checks if a filename has an allowed extension

Parameters:
  • filename – base name of file
  • extensions – iterable of extensions (or self.extensions)
save(fs, *args, **kwargs)[source]

Saves contents of a cgi.FieldStorage object to the file system. Returns modified filename(including folder).

Returns the resolved filename, i.e. the folder + (modified/randomized) filename.

Parameters:
  • fscgi.FieldStorage object (or similar)
  • folder – relative path of sub-folder
  • randomize – randomize the filename
  • extensions – iterable of allowed extensions, if not default
  • acl – ACL policy (if None then uses default)
  • replace – replace existing key
  • headers – dict of s3 request headers
Returns:

modified filename

save_file(file, filename, folder=None, randomize=False, extensions=None, acl=None, replace=False, headers=None)[source]
Parameters:
  • filename – local filename
  • folder – relative path of sub-folder
  • randomize – randomize the filename
  • extensions – iterable of allowed extensions, if not default
  • acl – ACL policy (if None then uses default)
  • replace – replace existing key
  • headers – dict of s3 request headers
Returns:

modified filename

save_filename(filename, *args, **kwargs)[source]

Saves a filename in local filesystem to the uploads location.

Returns the resolved filename, i.e. the folder + the (randomized/incremented) base name.

Parameters:
  • filename – local filename
  • folder – relative path of sub-folder
  • randomize – randomize the filename
  • extensions – iterable of allowed extensions, if not default
  • acl – ACL policy (if None then uses default)
  • replace – replace existing key
  • headers – dict of s3 request headers
Returns:

modified filename

url(filename)[source]

Returns entire URL of the filename, joined to the base_url

Parameters:filename – base name of file
class DummyFileStorage[source]

A fake file storage object for testing. Instead of saving to file the filename is added to a list

save(fs, folder=None, *args, **kwargs)[source]

Performs a fake saved operation

Table Of Contents

This Page